├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src ├── admin │ ├── controller │ │ └── extension │ │ │ └── module │ │ │ └── vsbridge.php │ ├── language │ │ └── en-gb │ │ │ └── extension │ │ │ └── module │ │ │ └── vsbridge.php │ └── view │ │ └── template │ │ └── extension │ │ └── module │ │ └── vsbridge.tpl ├── catalog │ ├── controller │ │ └── vsbridge │ │ │ ├── attributes.php │ │ │ ├── auth.php │ │ │ ├── cart.php │ │ │ ├── categories.php │ │ │ ├── order.php │ │ │ ├── product.php │ │ │ ├── products.php │ │ │ ├── stock.php │ │ │ ├── sync_session.php │ │ │ ├── taxrules.php │ │ │ └── user.php │ ├── language │ │ ├── en-gb │ │ │ └── vsbridge │ │ │ │ └── api.php │ │ └── hu-hu │ │ │ └── vsbridge │ │ │ └── api.php │ └── model │ │ └── vsbridge │ │ └── api.php └── system │ └── engine │ └── vsbridgecontroller.php └── tests └── test.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | vendor 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Butopêa.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Storefront Connector Extension for OpenCart 2 | **Compatible with: OpenCart 2.3.0.2** 3 | 4 | **API Base URL:** `https://site_url/vsbridge/` 5 | 6 | **API Credentials:** 7 | 8 | - Username: OC API Name 9 | - Password: OC API Key 10 | - Secret Key: Must be generated in VS Bridge module settings 11 | - Token Format: JWT 12 | 13 | **Installation:** 14 | 15 | * Add the following line in the extra section of your OpenCart's composer.json: 16 | 17 | **Make sure to change the destination folder to match your upload/public folder.** 18 | ```json 19 | "extra": { 20 | "filescopier": [ 21 | { 22 | "source": "vendor/butopea/vue-storefront-opencart-vsbridge/src", 23 | "destination": "upload", 24 | "debug": "true" 25 | } 26 | ] 27 | } 28 | ``` 29 | 30 | Notes about the `source` and `destination` paths: 31 | 32 | > * The destination element must be a folder. if the destination folder does not exists, it is recursively created using `mkdir($destination, 0755, true)` 33 | > * If the destination folder is not an absolute path, the relative path is calculated using the vendorDir path (`$project_path = \realpath($this->composer->getConfig()->get('vendor-dir').'/../').'/'`;) 34 | > * The source element is evaluated using the php function `\glob($source, GLOB_MARK)` and a recursive copy is made for every result of this function into the destination folder 35 | 36 | * Run the following command to add the required composer packages, including the VS Bridge itself: 37 | ```bash 38 | composer require butopea/vue-storefront-opencart-vsbridge 39 | ``` 40 | 41 | * Add the URL rewrite rule for VS Bridge (Nginx example): 42 | ```nginx 43 | location /vsbridge { 44 | rewrite ^/(.+)$ /index.php?route=$1 last; 45 | } 46 | ``` 47 | * Install the extension in OpenCart (Extensions -> Modules) and generate a secret key 48 | 49 | This extension will create its own tables in the database. An overview of the database changes [can be found here](https://github.com/butopea/vue-storefront-opencart-vsbridge/blob/master/src/admin/controller/extension/module/vsbridge.php#L103). 50 | 51 | * Get the [Vue Storefront OpenCart Indexer](https://github.com/butopea/vue-storefront-opencart-indexer) to import your data into ElasticSearch. 52 | 53 | *You need to whitelist your indexer's IP address in OpenCart at oc_url/admin/index.php?route=user/api* 54 | 55 | **Tests:** 56 | 57 | * Edit `tests/test.php` and add the credentials and settings 58 | * Run `php tests/test.php` 59 | 60 | **Development:** 61 | 62 | We're currently in the early stages of getting all the features working and would love other OpenCart developers to join in with us on this project! 63 | 64 | If you found a bug or want to contribute toward making this extension better, please fork this repository, make your changes, and make a pull request. 65 | 66 | **Credits:** 67 | 68 | Made with ❤ by [Butopêa](https://butopea.com) 69 | 70 | **Support:** 71 | 72 | 73 | Please ask your questions regarding this extension on Vue Storefront's Slack https://vuestorefront.slack.com/ You can join via [this invitation link](https://slack.vuestorefront.io/). 74 | 75 | **License:** 76 | 77 | This extension is completely free and released under the MIT License. 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "butopea/vue-storefront-opencart-vsbridge", 3 | "type": "opencart-extension", 4 | "description": "Vue Storefront Connector Extension for OpenCart", 5 | "keywords": ["vue-storefront", "opencart", "vsbridge"], 6 | "homepage": "https://github.com/butopea", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Mohammad Tomaraei", 11 | "email": "themreza@gmail.com", 12 | "homepage": "https://github.com/themreza", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "rbdwllr/reallysimplejwt": "2.0.2", 18 | "voku/urlify": "5.0.2", 19 | "butopea/composer-plugin-filecopier": "^1.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/admin/controller/extension/module/vsbridge.php: -------------------------------------------------------------------------------- 1 | load->language('extension/module/vsbridge'); 7 | 8 | $this->document->setTitle($this->language->get('heading_title')); 9 | 10 | $this->load->model('setting/setting'); 11 | 12 | if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) { 13 | $this->model_setting_setting->editSetting('vsbridge', $this->request->post); 14 | 15 | $this->session->data['success'] = $this->language->get('text_success'); 16 | 17 | $this->response->redirect($this->url->link('extension/extension', 'token=' . $this->session->data['token'] . '&type=module', true)); 18 | } 19 | 20 | $data['heading_title'] = $this->language->get('heading_title'); 21 | 22 | $data['text_edit'] = $this->language->get('text_edit'); 23 | $data['text_enabled'] = $this->language->get('text_enabled'); 24 | $data['text_disabled'] = $this->language->get('text_disabled'); 25 | 26 | $data['entry_status'] = $this->language->get('entry_status'); 27 | $data['entry_secret_key'] = $this->language->get('entry_secret_key'); 28 | $data['entry_endpoint_statuses'] = $this->language->get('entry_endpoint_statuses'); 29 | 30 | $data['info_secret_key'] = $this->language->get('info_secret_key'); 31 | $data['info_endpoint_statuses'] = $this->language->get('info_endpoint_statuses'); 32 | 33 | $data['button_save'] = $this->language->get('button_save'); 34 | $data['button_cancel'] = $this->language->get('button_cancel'); 35 | $data['button_generate_secret_key'] = $this->language->get('button_generate_secret_key'); 36 | 37 | if (isset($this->error['warning'])) { 38 | $data['error_warning'] = $this->error['warning']; 39 | } else { 40 | $data['error_warning'] = ''; 41 | } 42 | 43 | if (isset($this->error['secret_key'])) { 44 | $data['error_secret_key'] = $this->error['secret_key']; 45 | } else { 46 | $data['error_secret_key'] = ''; 47 | } 48 | 49 | $data['breadcrumbs'] = array(); 50 | 51 | $data['breadcrumbs'][] = array( 52 | 'text' => $this->language->get('text_home'), 53 | 'href' => $this->url->link('common/dashboard', 'token=' . $this->session->data['token'], true) 54 | ); 55 | 56 | $data['breadcrumbs'][] = array( 57 | 'text' => $this->language->get('text_module'), 58 | 'href' => $this->url->link('extension/extension', 'token=' . $this->session->data['token'] . '&type=module', true) 59 | ); 60 | 61 | $data['breadcrumbs'][] = array( 62 | 'text' => $this->language->get('heading_title'), 63 | 'href' => $this->url->link('extension/module/vsbridge', 'token=' . $this->session->data['token'], true) 64 | ); 65 | 66 | $data['action'] = $this->url->link('extension/module/vsbridge', 'token=' . $this->session->data['token'], true); 67 | 68 | $data['cancel'] = $this->url->link('extension/extension', 'token=' . $this->session->data['token'] . '&type=module', true); 69 | 70 | $default_endpoint_statuses = array( 71 | 'attributes' => true, 72 | 'auth' => true, 73 | 'cart' => true, 74 | 'categories' => true, 75 | 'order' => true, 76 | 'product' => true, 77 | 'products' => true, 78 | 'stock' => true, 79 | 'sync_session' => true, 80 | 'taxrules' => true, 81 | 'user' => true 82 | ); 83 | 84 | $data['vsbridge_status'] = $this->request->post['vsbridge_status'] ?? $this->config->get('vsbridge_status'); 85 | 86 | $data['vsbridge_secret_key'] = $this->request->post['vsbridge_secret_key'] ?? $this->config->get('vsbridge_secret_key'); 87 | 88 | if (isset($this->request->post['vsbridge_endpoint_statuses'])) { 89 | $data['vsbridge_endpoint_statuses'] = $this->request->post['vsbridge_endpoint_statuses']; 90 | } elseif(!empty($this->config->get('vsbridge_endpoint_statuses'))) { 91 | $data['vsbridge_endpoint_statuses'] = $this->config->get('vsbridge_endpoint_statuses'); 92 | } else { 93 | $data['vsbridge_endpoint_statuses'] = $default_endpoint_statuses; 94 | } 95 | 96 | $data['header'] = $this->load->controller('common/header'); 97 | $data['column_left'] = $this->load->controller('common/column_left'); 98 | $data['footer'] = $this->load->controller('common/footer'); 99 | 100 | $this->response->setOutput($this->load->view('extension/module/vsbridge', $data)); 101 | } 102 | 103 | protected function validate() { 104 | if (!$this->user->hasPermission('modify', 'extension/module/vsbridge')) { 105 | $this->error['warning'] = $this->language->get('error_permission'); 106 | } 107 | 108 | if (empty($this->request->post['vsbridge_secret_key'])) { 109 | $this->error['secret_key'] = $this->language->get('error_secret_key'); 110 | }else{ 111 | if(!preg_match('/^.*(?=.{12,}+)(?=.*[0-9]+)(?=.*[A-Z]+)(?=.*[a-z]+)(?=.*[\*&!@%\^#\$]+).*$/', $this->request->post['vsbridge_secret_key'])){ 112 | $this->error['secret_key'] = $this->language->get('error_secret_key'); 113 | } 114 | } 115 | 116 | return !$this->error; 117 | } 118 | 119 | public function install() 120 | { 121 | $this->db->query("CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "vsbridge_token` ( 122 | `vsbridge_token_id` int(11) NOT NULL, 123 | `customer_id` int(11) NOT NULL, 124 | `token` varchar(32) NOT NULL, 125 | `ip` varchar(40) NOT NULL, 126 | `timestamp` int(11) NOT NULL 127 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8;"); 128 | 129 | $this->db->query("ALTER TABLE 130 | `" . DB_PREFIX . "vsbridge_token` 131 | ADD 132 | PRIMARY KEY (`vsbridge_token_id`), 133 | ADD 134 | UNIQUE KEY `token` (`token`);"); 135 | 136 | $this->db->query("ALTER TABLE 137 | `" . DB_PREFIX . "vsbridge_token` MODIFY `vsbridge_token_id` int(11) NOT NULL AUTO_INCREMENT;"); 138 | 139 | $this->db->query("CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "vsbridge_refresh_token` ( 140 | `vsbridge_refresh_token_id` int(11) NOT NULL, 141 | `customer_id` int(11) NOT NULL, 142 | `ip` varchar(40) NOT NULL, 143 | `timestamp` int(11) NOT NULL 144 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8;"); 145 | 146 | $this->db->query("ALTER TABLE 147 | `" . DB_PREFIX . "vsbridge_refresh_token` 148 | ADD 149 | PRIMARY KEY (`vsbridge_refresh_token_id`);"); 150 | 151 | $this->db->query("ALTER TABLE 152 | `" . DB_PREFIX . "vsbridge_refresh_token` MODIFY `vsbridge_refresh_token_id` int(11) NOT NULL AUTO_INCREMENT;"); 153 | 154 | $this->db->query("CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "vsbridge_session` ( 155 | `customer_id` int(11) NOT NULL, 156 | `store_id` int(11) NOT NULL, 157 | `session_id` varchar(32) NOT NULL 158 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8;"); 159 | 160 | $this->db->query("ALTER TABLE 161 | `" . DB_PREFIX . "vsbridge_session` 162 | ADD 163 | UNIQUE `unique_index`(`customer_id`, `store_id`);"); 164 | } 165 | 166 | public function uninstall() { 167 | $this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "vsbridge_token`"); 168 | $this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "vsbridge_refresh_token`"); 169 | $this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "vsbridge_session`"); 170 | } 171 | } -------------------------------------------------------------------------------- /src/admin/language/en-gb/extension/module/vsbridge.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 |

25 |
26 |
27 |
28 |
29 | 30 |
31 | 40 |
41 |
42 |
43 | 44 |
45 |
46 | 47 | 48 | 49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 | 58 |
59 | $esvalue) { ?> 60 |
61 |
62 | 63 |
64 |
65 | 74 |
75 |
76 | 77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | 137 | 138 | -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/attributes.php: -------------------------------------------------------------------------------- 1 | validateToken($this->getParam('apikey')); 19 | 20 | $language_id = $this->language_id; 21 | 22 | $this->load->model('vsbridge/api'); 23 | 24 | $response = array(); 25 | 26 | // Add the actual attributes 27 | $attributes = $this->model_vsbridge_api->getAttributes($language_id); 28 | 29 | foreach($attributes as $attribute){ 30 | // To avoid the conflict of attribute IDs and filter IDs, an offset of 10000 is added to attribute IDs and attribute group IDs 31 | $attribute['attribute_id'] = ((int) $attribute['attribute_id']) + 10000; 32 | $attribute['attribute_group_id'] = ((int) $attribute['attribute_group_id']) + 10000; 33 | 34 | 35 | // We've also added an attribute_group_id field to accompany the /groups endpoint below 36 | array_push($response, array( 37 | 'attribute_code' => 'attribute_'.$attribute['attribute_id'], 38 | 'frontend_input' => 'text', 39 | 'frontend_label' => $attribute['name'], 40 | 'default_frontend_label' => $attribute['name'], 41 | 'is_user_defined' => true, 42 | 'is_unique' => false, 43 | 'attribute_id' => $attribute['attribute_id'], 44 | 'is_visible' => true, 45 | 'is_comparable' => true, 46 | 'is_visible_on_front' => true, 47 | 'position' => 0, 48 | 'id' => $attribute['attribute_id'], 49 | 'options' => array(), 50 | 'attribute_group_id' => (int) $attribute['attribute_group_id'] 51 | )); 52 | } 53 | 54 | // Add filters as hidden, searchable attributes 55 | $filter_groups = $this->model_vsbridge_api->getFilterGroups($language_id); 56 | 57 | foreach($filter_groups as $filter_group){ 58 | $options = array(); 59 | 60 | $filters = $this->model_vsbridge_api->getFilters($filter_group['filter_group_id'], $language_id); 61 | 62 | foreach($filters as $filter){ 63 | array_push($options, array( 64 | 'value' => (int) $filter['filter_id'], 65 | 'label' => trim($filter['name']) 66 | )); 67 | } 68 | 69 | array_push($response, array( 70 | 'attribute_code' => 'filter_group_'.$filter_group['filter_group_id'], 71 | 'frontend_input' => 'select', 72 | 'frontend_label' => $filter_group['name'], 73 | 'default_frontend_label' => $filter_group['name'], 74 | 'is_user_defined' => true, 75 | 'is_unique' => true, 76 | 'attribute_id' => (int) $filter_group['filter_group_id'], 77 | 'is_visible' => true, 78 | 'is_comparable' => true, 79 | 'is_visible_on_front' => true, 80 | 'position' => (int) $filter_group['sort_order'], 81 | 'id' => (int) $filter_group['filter_group_id'], 82 | 'options' => $options 83 | )); 84 | } 85 | 86 | $this->result = $response; 87 | 88 | $this->sendResponse(); 89 | } 90 | 91 | /* 92 | * [Note: This endpoint is custom-made and is not required by Vue Storefront.] 93 | * 94 | * GET /vsbridge/attributes/groups 95 | * This method is used to get all of the attribute groups in OpenCart. 96 | * 97 | * GET PARAMS: 98 | * apikey - authorization key provided by /vsbridge/auth/admin endpoint 99 | */ 100 | public function groups() { 101 | $this->validateToken($this->getParam('apikey')); 102 | 103 | $language_id = $this->language_id; 104 | 105 | $this->load->model('vsbridge/api'); 106 | 107 | $response = array(); 108 | 109 | // Retrieve the attribute groups 110 | $attribute_groups = $this->model_vsbridge_api->getAttributeGroups($language_id); 111 | 112 | foreach($attribute_groups as $attribute_group){ 113 | 114 | // To avoid the conflict of attribute group IDs and filter group IDs, an offset of 10000 is added 115 | $attribute_group['attribute_group_id'] = ((int) $attribute_group['attribute_group_id']) + 10000; 116 | 117 | array_push($response, array( 118 | 'frontend_label' => $attribute_group['name'], 119 | 'is_visible' => true, 120 | 'position' => (int) $attribute_group['sort_order'], 121 | 'id' => $attribute_group['attribute_group_id'], 122 | 'attribute_group_id' => $attribute_group['attribute_group_id'] 123 | )); 124 | } 125 | 126 | $this->result = $response; 127 | 128 | $this->sendResponse(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/auth.php: -------------------------------------------------------------------------------- 1 | getPost(); 19 | 20 | if(!empty($post['username']) && !empty($post['password'])){ 21 | 22 | /* Retrieve OpenCart API */ 23 | $this->load->model('account/api'); 24 | $api_info = $this->model_account_api->getApiByKey($post['password']); 25 | 26 | /* Authenticate username and password */ 27 | if($this->checkIndexValue($api_info, 'name', $post['username'])){ 28 | 29 | /* Check if the IP is whitelisted */ 30 | $whitelisted_ips = $this->model_account_api->getApiIps($api_info['api_id']); 31 | $client_ip = $this->getClientIp(); 32 | $whitelisted = false; 33 | 34 | foreach($whitelisted_ips as $whitelisted_ip){ 35 | if($this->checkIndexValue($whitelisted_ip, 'ip', $client_ip, 'trim')){ 36 | $whitelisted = true; 37 | } 38 | } 39 | 40 | if($whitelisted == true){ 41 | 42 | /* Create a new session ID and store it in OC API */ 43 | $session_id = $this->session->createId(); 44 | $this->session->start('api', $session_id); 45 | $this->session->data['api_id'] = $api_info['api_id']; 46 | 47 | $oc_token = $this->model_account_api->addApiSession($api_info['api_id'], $session_id, $client_ip); 48 | 49 | /* Generate a JWT token based on the OC token */ 50 | $this->result = $this->getToken($oc_token); 51 | 52 | }else{ 53 | $this->code = 401; 54 | $this->result = "Authentication failed. Your IP (".$client_ip.") is not whitelisted."; 55 | } 56 | 57 | }else{ 58 | $this->code = 401; 59 | $this->result = "Authentication failed. Invalid username and/or password."; 60 | } 61 | 62 | }else{ 63 | $this->code = 400; 64 | $this->result = "Invalid request. Missing username and/or password."; 65 | } 66 | 67 | $this->sendResponse(); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/cart.php: -------------------------------------------------------------------------------- 1 | apply_coupon(); 15 | break; 16 | case 'delete-coupon': 17 | $this->delete_coupon(); 18 | break; 19 | case 'payment-methods': 20 | $this->payment_methods(); 21 | break; 22 | case 'shipping-methods': 23 | $this->shipping_methods(); 24 | break; 25 | case 'shipping-information': 26 | $this->shipping_information(); 27 | break; 28 | case 'collect-totals': 29 | $this->collect_totals(); 30 | break; 31 | } 32 | } 33 | 34 | /* 35 | * POST /vsbridge/cart/create 36 | * This method is used to get all the products from the backend. 37 | * 38 | * WHEN: 39 | * This method is called when new Vue Storefront shopping cart is created. 40 | * First visit, page refresh, after user-authorization ... 41 | * If the token GET parameter is provided it's called as logged-in user; if not - it's called as guest-user. 42 | * To draw the difference - let's keep to Magento example. 43 | * For guest user vue-storefront-api is subsequently operating on /guest-carts API endpoints and for authorized users on /carts/ endpoints. 44 | * 45 | * GET PARAMS: 46 | * token - null OR user token obtained from /vsbridge/user/login 47 | * 48 | * Note: the cartId for authorized customers is NOT an integer due to OpenCart architecture. If it causes a problem in the future, create a table and map session_ids to integers row ids. 49 | */ 50 | 51 | public function create(){ 52 | $token = $this->getParam('token', true); 53 | 54 | /* 55 | * Generate a new session_id (just a value for the column, not a real session) to be inserted in the cart table. 56 | * The session_ids are marked with a vs_ prefix to be distinguished from real OpenCart sessions. 57 | * We can't use the API token because it expires and changes. 58 | * 59 | * For authenticated users, if there's a row in the cart table matching the customer_id, we will retrieve the saved session_id and not create a new one. 60 | */ 61 | 62 | if(!empty($token) && $customer_info = $this->validateCustomerToken($token)){ 63 | /* Authenticated customer */ 64 | $this->load->model('vsbridge/api'); 65 | $cart_id = $this->getSessionId($customer_info['customer_id']); 66 | }else{ 67 | /* Guest */ 68 | $cart_id = $this->getSessionId(); 69 | } 70 | 71 | $this->result = $cart_id; 72 | 73 | $this->sendResponse(); 74 | } 75 | 76 | /* 77 | * GET /vsbridge/cart/pull 78 | * Method used to fetch the current server side shopping cart content, used mostly for synchronization purposes when config.cart.synchronize=true 79 | * 80 | * WHEN: 81 | * This method is called just after any Vue Storefront cart modification to check if the server or client shopping cart items need to be updated. 82 | * It gets the current list of the shopping cart items. The synchronization algorithm in VueStorefront determines if server or client items need to be updated and executes api/cart/update or api/cart/delete accordngly. 83 | * 84 | * GET PARAMS: 85 | * token - null OR user token obtained from /vsbridge/user/login 86 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 87 | * 88 | * Note: Currently, all our products are non-configurable so the product_type is set manually to simple. 89 | * Remember to change it here too if we implement product options in the importer. 90 | */ 91 | 92 | public function pull(){ 93 | $token = $this->getParam('token', true); 94 | $cart_id = $this->getParam('cartId'); 95 | 96 | $this->load->model('vsbridge/api'); 97 | 98 | if($this->validateCartId($cart_id, $token)){ 99 | $cart = $this->cart->getProducts(); 100 | 101 | $response = array(); 102 | 103 | /* If the cart exists and has items, retrieve it */ 104 | if(!empty($cart)){ 105 | foreach($cart as $cart_product){ 106 | if(isset($cart_product['product_id']) && isset($cart_product['quantity'])){ 107 | $product_info = $this->model_vsbridge_api->getProductDetails($cart_product['product_id'], $this->language_id); 108 | if (!$cart_product['stock']) { 109 | // Sending an HTTP-500 error causes a bug where VSF continuously makes new tokens on each page refresh 110 | // A quick fix is to remove any out of stock products from the cart to avoid this issue 111 | $this->cart->remove($cart_product['cart_id']); 112 | } 113 | $response[] = array( 114 | 'item_id' => (int)$cart_product['cart_id'], 115 | 'sku' => $product_info['sku'], 116 | 'model' => $product_info['model'], 117 | 'qty' => (int)$cart_product['quantity'], 118 | 'name' => $product_info['name'], 119 | 'price' => (float)$product_info['price'], 120 | 'product_type' => 'simple', 121 | 'quote_id' => $cart_id 122 | ); 123 | } 124 | } 125 | } 126 | 127 | $this->result = $response; 128 | } 129 | 130 | $this->sendResponse(); 131 | } 132 | 133 | /* 134 | * POST /vsbridge/cart/update 135 | * Method used to add or update shopping cart item's server side. 136 | * As a request body there should be JSON given representing the cart item, sku, and qty are the two required options. 137 | * If you like to update/edit server cart item you need to pass item_id (cart_id in OpenCart) identifier as well (can be optainted from api/cart/pull). 138 | * 139 | * WHEN: 140 | * This method is called just after api/cart/pull as a consequence of the synchronization process. 141 | * 142 | * GET PARAMS: 143 | * token - null OR user token obtained from /vsbridge/user/login 144 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 145 | * 146 | * REQUEST BODY: 147 | * 148 | * "cartItem":{ 149 | * "sku":"WS12-XS-Orange", 150 | * "qty":1, 151 | * "product_option":{ 152 | * "extension_attributes":{ 153 | * "custom_options":[ 154 | * 155 | * ], 156 | * "configurable_item_options":[ 157 | * { 158 | * "option_id":"93", 159 | * "option_value":"56" 160 | * }, 161 | * { 162 | * "option_id":"142", 163 | * "option_value":"167" 164 | * } 165 | * ], 166 | * "bundle_options":[ 167 | * 168 | * ] 169 | * } 170 | * }, 171 | * "quoteId":"0a8109552020cc80c99c54ad13ef5d5a" 172 | * } 173 | * 174 | * Note: 175 | * quoteId is specific to magento and is the same as the cartId. We currently don't perform any checks on it. 176 | * We haven't implemented product_option due to products being of type 'single'. 177 | */ 178 | 179 | public function update(){ 180 | $token = $this->getParam('token', true); 181 | $cart_id = $this->getParam('cartId'); 182 | $input = $this->getPost(); 183 | 184 | $this->load->model('vsbridge/api'); 185 | 186 | if($this->validateCartId($cart_id, $token)){ 187 | if(empty($input['cartItem'])) { 188 | $this->load->language('vsbridge/api'); 189 | $this->code = 500; 190 | $this->result = $this->language->get('error_missing_cart_item'); 191 | $this->sendResponse(); 192 | } 193 | 194 | if(empty($input['cartItem']['sku'])){ 195 | $this->load->language('vsbridge/api'); 196 | $this->code = 500; 197 | $this->result = $this->language->get('error_missing_sku_qty'); 198 | $this->sendResponse(); 199 | } 200 | 201 | $input['cartItem']['qty'] = (int) $input['cartItem']['qty']; 202 | 203 | /* quantity must be greater than or equal to 1 */ 204 | /* the delete endpoint is used for removing the item from the cart */ 205 | if($input['cartItem']['qty'] < 1){ 206 | $input['cartItem']['qty'] = 1; 207 | } 208 | 209 | $product_info = $this->model_vsbridge_api->getProductBySku($input['cartItem']['sku'], $this->language_id); 210 | 211 | if(empty($product_info)){ 212 | $this->load->language('vsbridge/api'); 213 | $this->code = 500; 214 | $this->result = $this->language->get('error_invalid_sku'); 215 | $this->sendResponse(); 216 | } 217 | 218 | // Check if the requested quantity is available on the selected product 219 | if(intval($input['cartItem']['qty']) > intval($product_info['quantity'])){ 220 | $this->load->language('vsbridge/api'); 221 | $this->code = 500; 222 | $this->result = $this->language->get('error_out_of_stock').' ['.$product_info['model'].'] '.$product_info['name']; 223 | $this->sendResponse(); 224 | } 225 | 226 | // Check if the requested quantity meets the minimum (and multiple of minimum) product quantity 227 | if($product_info['minimum']){ 228 | if (intval($input['cartItem']['qty']) < intval($product_info['minimum'])) { 229 | $this->load->language('vsbridge/api'); 230 | $this->code = 500; 231 | $this->result = sprintf($this->language->get('error_minimum_product_quantity'), $product_info['model'], $product_info['minimum']); 232 | $this->sendResponse(); 233 | } 234 | 235 | $qty_multiple = intval($input['cartItem']['qty']) / intval($product_info['minimum']); 236 | if (!is_int($qty_multiple)) { 237 | $this->load->language('vsbridge/api'); 238 | $this->code = 500; 239 | $this->result = sprintf($this->language->get('error_minimum_multiple_product_quantity'), $product_info['model'], $product_info['minimum']); 240 | $this->sendResponse(); 241 | } 242 | } 243 | 244 | if(isset($input['cartItem']['item_id'])){ 245 | $this->cart->update($input['cartItem']['item_id'], $input['cartItem']['qty']); 246 | }else{ 247 | $this->cart->add($product_info['product_id'], $input['cartItem']['qty']); 248 | } 249 | 250 | $cart_products = $this->cart->getProducts(); 251 | 252 | $response = array(); 253 | 254 | foreach($cart_products as $cart_product){ 255 | if($cart_product['product_id'] == $product_info['product_id']){ 256 | $response = array( 257 | 'item_id' => (int) $cart_product['cart_id'], 258 | 'sku' => $cart_product['model'], 259 | 'qty' => (int) $cart_product['quantity'], 260 | 'name' => $cart_product['name'], 261 | 'price' => (float) $cart_product['price'], 262 | 'product_type' => 'simple', 263 | 'quote_id' => $cart_id 264 | ); 265 | } 266 | } 267 | 268 | $this->result = $response; 269 | } 270 | 271 | $this->sendResponse(); 272 | } 273 | 274 | 275 | /* 276 | * POST /vsbridge/cart/delete 277 | * This method is used to remove the shopping cart item on server side. 278 | * 279 | * WHEN: 280 | * This method is called just after api/cart/pull as a consequence of the synchronization process 281 | * 282 | * GET PARAMS: 283 | * token - null OR user token obtained from /vsbridge/user/login 284 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 285 | * 286 | * REQUEST BODY: 287 | * 288 | * { 289 | * "cartItem": 290 | * { 291 | * "sku":"MS10-XS-Black", 292 | * "item_id":5853, 293 | * "quoteId":"81668" 294 | * } 295 | * } 296 | * 297 | */ 298 | 299 | public function delete(){ 300 | $token = $this->getParam('token', true); 301 | $cart_id = $this->getParam('cartId'); 302 | $input = $this->getPost(); 303 | 304 | $this->load->model('vsbridge/api'); 305 | 306 | if($this->validateCartId($cart_id, $token)) { 307 | if (empty($input['cartItem'])) { 308 | $this->load->language('vsbridge/api'); 309 | $this->code = 500; 310 | $this->result = $this->language->get('error_missing_cart_item'); 311 | $this->sendResponse(); 312 | } 313 | 314 | if (empty($input['cartItem']['sku']) || !isset($input['cartItem']['item_id'])) { 315 | $this->load->language('vsbridge/api'); 316 | $this->code = 500; 317 | $this->result = $this->language->get('error_missing_sku_item_id'); 318 | $this->sendResponse(); 319 | } 320 | 321 | 322 | 323 | $product_info = $this->model_vsbridge_api->getProductBySku($input['cartItem']['sku'], $this->language_id); 324 | 325 | if(empty($product_info)){ 326 | $this->load->language('vsbridge/api'); 327 | $this->code = 500; 328 | $this->result = $this->language->get('error_invalid_sku'); 329 | $this->sendResponse(); 330 | } 331 | 332 | $this->cart->remove($input['cartItem']['item_id']); 333 | 334 | $this->result = true; 335 | } 336 | 337 | $this->sendResponse(); 338 | } 339 | 340 | 341 | /* 342 | * POST /vsbridge/cart/apply-coupon 343 | * This method is used to apply the discount code to the current server side quote. 344 | * 345 | * GET PARAMS: 346 | * token - null OR user token obtained from /vsbridge/user/login 347 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 348 | * coupon - coupon code to apply 349 | */ 350 | 351 | public function apply_coupon(){ 352 | $token = $this->getParam('token', true); 353 | $cart_id = $this->getParam('cartId'); 354 | $coupon = $this->getParam('coupon'); 355 | 356 | $this->load->model('extension/total/coupon'); 357 | 358 | if($this->validateCartId($cart_id, $token)) { 359 | $coupon_info = $this->model_extension_total_coupon->getCoupon($coupon); 360 | 361 | if(!$coupon_info){ 362 | $this->load->language('extension/total/coupon'); 363 | $this->code = 500; 364 | $this->result = $this->language->get('error_coupon'); 365 | $this->sendResponse(); 366 | } 367 | 368 | $this->session->data['coupon'] = $coupon; 369 | $this->result = true; 370 | } 371 | 372 | $this->sendResponse(); 373 | } 374 | 375 | /* 376 | * POST /vsbridge/cart/delete-coupon 377 | * This method is used to delete the discount code to the current server side quote. 378 | * 379 | * GET PARAMS: 380 | * token - null OR user token obtained from /vsbridge/user/login 381 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 382 | */ 383 | 384 | public function delete_coupon(){ 385 | $token = $this->getParam('token', true); 386 | $cart_id = $this->getParam('cartId'); 387 | 388 | if($this->validateCartId($cart_id, $token)) { 389 | unset($this->session->data['coupon']); 390 | $this->result = true; 391 | } 392 | 393 | $this->sendResponse(); 394 | } 395 | 396 | /* 397 | * GET /vsbridge/cart/coupon 398 | * This method is used to get the currently applied coupon code. 399 | * 400 | * GET PARAMS: 401 | * token - null OR user token obtained from /vsbridge/user/login 402 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 403 | */ 404 | 405 | public function coupon(){ 406 | $token = $this->getParam('token', true); 407 | $cart_id = $this->getParam('cartId'); 408 | 409 | if($this->validateCartId($cart_id, $token)) { 410 | if(!empty($this->session->data['coupon'])){ 411 | $this->result = $this->session->data['coupon']; 412 | }else{ 413 | $this->result = null; 414 | } 415 | } 416 | 417 | $this->sendResponse(); 418 | } 419 | 420 | 421 | /* 422 | * GET /vsbridge/cart/totals 423 | * Method called when the config.synchronize_totals=true just after any shopping cart modification. 424 | * It's used to synchronize the Magento / other CMS totals after all promotion rules processed with current Vue Storefront state. 425 | * 426 | * GET PARAMS: 427 | * token - null OR user token obtained from /vsbridge/user/login 428 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 429 | */ 430 | 431 | public function totals(){ 432 | $token = $this->getParam('token', true); 433 | $cart_id = $this->getParam('cartId'); 434 | 435 | if($this->validateCartId($cart_id, $token)) { 436 | 437 | $this->load->model('extension/extension'); 438 | 439 | $totals = array(); 440 | $taxes = $this->cart->getTaxes(); 441 | $total = 0; 442 | 443 | // Because __call can not keep var references so we put them into an array. 444 | $total_data = array( 445 | 'totals' => &$totals, 446 | 'taxes' => &$taxes, 447 | 'total' => &$total 448 | ); 449 | 450 | $sort_order = array(); 451 | 452 | $results = $this->model_extension_extension->getExtensions('total'); 453 | 454 | foreach ($results as $key => $value) { 455 | $sort_order[$key] = $this->config->get($value['code'] . '_sort_order'); 456 | } 457 | 458 | array_multisort($sort_order, SORT_ASC, $results); 459 | 460 | foreach ($results as $result) { 461 | if ($this->config->get($result['code'] . '_status')) { 462 | $this->load->model('extension/total/' . $result['code']); 463 | 464 | // We have to put the totals in an array so that they pass by reference. 465 | $this->{'model_extension_total_' . $result['code']}->getTotal($total_data); 466 | } 467 | } 468 | 469 | $cart_totals = array(); 470 | 471 | foreach ($totals as $key => $value) { 472 | switch($value['code']){ 473 | case 'sub_total': 474 | $cart_totals[] = array( 475 | 'code' => 'subtotal', 476 | 'title' => $value['title'], 477 | 'value' => (float)$value['value'] 478 | ); 479 | break; 480 | case 'shipping': 481 | $cart_totals[] = array( 482 | 'code' => 'shipping', 483 | 'title' => $value['title'], 484 | 'value' => (float)$value['value'] 485 | ); 486 | break; 487 | case 'tax': 488 | $cart_totals[] = array( 489 | 'code' => 'tax', 490 | 'title' => $value['title'], 491 | 'value' => (float)$value['value'] 492 | ); 493 | break; 494 | case 'total': 495 | $cart_totals[] = array( 496 | 'code' => 'grand_total', 497 | 'title' => $value['title'], 498 | 'value' => (float)$value['value'] 499 | ); 500 | break; 501 | case 'coupon': 502 | $cart_totals[] = array( 503 | 'code' => 'discount', 504 | 'title' => $value['title'], 505 | 'value' => (float)$value['value'] 506 | ); 507 | break; 508 | } 509 | } 510 | 511 | $cart_products = $this->cart->getProducts(); 512 | 513 | $cart_items = array(); 514 | 515 | /* Currently we don't calculate per-product tax/discount. Instead OpenCart calculates the cart total tax */ 516 | foreach($cart_products as $cart_product){ 517 | $cart_items[] = array( 518 | 'item_id' => (int)$cart_product['cart_id'], 519 | 'price' => (float)$cart_product['price'], 520 | 'base_price' => (float)$cart_product['price'], 521 | 'qty' => (int)$cart_product['quantity'], 522 | 'row_total' => (float)$cart_product['total'], 523 | 'base_row_total' => (float)$cart_product['total'], 524 | 'row_total_with_discount' => (float)$cart_product['total'], 525 | 'tax_amount' => 0, 526 | 'discount_amount' => 0, 527 | 'base_discount_amount' => 0, 528 | 'discount_percent' => 0, 529 | 'name' => $cart_product['name'], 530 | ); 531 | } 532 | 533 | $response = array( 534 | 'items' => $cart_items, 535 | 'total_segments' => $cart_totals 536 | ); 537 | 538 | $this->result = $response; 539 | 540 | } 541 | 542 | $this->sendResponse(); 543 | } 544 | 545 | /* 546 | * GET /vsbridge/cart/payment-methods 547 | * This method is used as a step in the cart synchronization process to get all the payment methods with actual costs as available inside the backend CMS 548 | * 549 | * GET PARAMS: 550 | * token - null OR user token obtained from /vsbridge/user/login 551 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 552 | * 553 | * Note: this method must be called after a shipment method is set. Otherwise it results in an error. 554 | */ 555 | 556 | public function payment_methods(){ 557 | $token = $this->getParam('token', true); 558 | $cart_id = $this->getParam('cartId'); 559 | 560 | if($this->validateCartId($cart_id, $token)) { 561 | /* We're estimating the payment methods since OpenCart requires address details that aren't provided by VSF at registration */ 562 | $this->load->language('checkout/checkout'); 563 | 564 | $country_id = $this->config->get('config_country_id'); 565 | $zone_id = $this->config->get('config_zone_id'); 566 | 567 | $this->load->model('extension/extension'); 568 | 569 | $this->load->model('localisation/country'); 570 | $country_data = $this->model_localisation_country->getCountry($country_id); 571 | 572 | $this->load->model('localisation/zone'); 573 | $zone_data = $this->model_localisation_zone->getZone($zone_id); 574 | 575 | $this->session->data['payment_address'] = array( 576 | 'firstname' => '', 577 | 'lastname' => '', 578 | 'company' => '', 579 | 'address_1' => '', 580 | 'address_2' => '', 581 | 'postcode' => '', 582 | 'city' => '', 583 | 'zone_id' => $zone_id, 584 | 'zone' => (isset($zone_data['name'])) ? $zone_data['name'] : '', 585 | 'zone_code' => (isset($zone_data['code'])) ? $zone_data['code'] : '', 586 | 'country_id' => $country_id, 587 | 'country' => (isset($country_data['name'])) ? $country_data['name'] : '', 588 | 'iso_code_2' => (isset($country_data['iso_code_2'])) ? $country_data['iso_code_2'] : '', 589 | 'iso_code_3' => (isset($country_data['iso_code_3'])) ? $country_data['iso_code_3'] : '', 590 | 'address_format' => (isset($country_data['address_format'])) ? $country_data['address_format'] : '', 591 | ); 592 | 593 | if (isset($this->session->data['payment_address'])) { 594 | // Totals 595 | $totals = array(); 596 | $taxes = $this->cart->getTaxes(); 597 | $total = 0; 598 | 599 | // Because __call can not keep var references so we put them into an array. 600 | $total_data = array( 601 | 'totals' => &$totals, 602 | 'taxes' => &$taxes, 603 | 'total' => &$total 604 | ); 605 | 606 | $sort_order = array(); 607 | 608 | $results = $this->model_extension_extension->getExtensions('total'); 609 | 610 | foreach ($results as $key => $value) { 611 | $sort_order[$key] = $this->config->get($value['code'] . '_sort_order'); 612 | } 613 | 614 | array_multisort($sort_order, SORT_ASC, $results); 615 | 616 | foreach ($results as $result) { 617 | if ($this->config->get($result['code'] . '_status')) { 618 | $this->load->model('extension/total/' . $result['code']); 619 | 620 | // We have to put the totals in an array so that they pass by reference. 621 | $this->{'model_extension_total_' . $result['code']}->getTotal($total_data); 622 | } 623 | } 624 | 625 | // Payment Methods 626 | $method_data = array(); 627 | 628 | $results = $this->model_extension_extension->getExtensions('payment'); 629 | 630 | $recurring = $this->cart->hasRecurringProducts(); 631 | 632 | foreach ($results as $result) { 633 | if ($this->config->get($result['code'] . '_status')) { 634 | $this->load->model('extension/payment/' . $result['code']); 635 | 636 | $method = $this->{'model_extension_payment_' . $result['code']}->getMethod($this->session->data['payment_address'], $total); 637 | 638 | if ($method) { 639 | if ($recurring) { 640 | if (property_exists($this->{'model_extension_payment_' . $result['code']}, 'recurringPayments') && $this->{'model_extension_payment_' . $result['code']}->recurringPayments()) { 641 | $method_data[$result['code']] = $method; 642 | } 643 | } else { 644 | $method_data[$result['code']] = $method; 645 | } 646 | } 647 | } 648 | } 649 | 650 | $sort_order = array(); 651 | 652 | foreach ($method_data as $key => $value) { 653 | $sort_order[$key] = $value['sort_order']; 654 | } 655 | 656 | array_multisort($sort_order, SORT_ASC, $method_data); 657 | 658 | $this->session->data['payment_methods'] = $method_data; 659 | } 660 | 661 | if (empty($this->session->data['payment_methods'])) { 662 | $this->load->language('vsbridge/api'); 663 | $this->code = 500; 664 | $this->result = $this->language->get('error_no_payment'); 665 | $this->sendResponse(); 666 | } 667 | 668 | $adjusted_payment_methods = array(); 669 | 670 | foreach($this->session->data['payment_methods'] as $payment_method){ 671 | $adjusted_payment_methods[] = array( 672 | 'code' => $payment_method['code'], 673 | 'title' => $payment_method['title'] 674 | ); 675 | } 676 | 677 | $this->result = $adjusted_payment_methods; 678 | } 679 | 680 | $this->sendResponse(); 681 | } 682 | 683 | 684 | /* 685 | * POST /vsbridge/cart/shipping-methods 686 | * This method is used as a step in the cart synchronization process to get all the shipping methods with actual costs as available inside the backend CMS. 687 | * 688 | * GET PARAMS: 689 | * token - null OR user token obtained from /vsbridge/user/login 690 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 691 | * 692 | * REQUEST BODY: 693 | * If the shipping methods are dependent on the full address - probably we need to pass the whole address record with the same format as it's passed to api/order/create or api/user/me. 694 | * The minimum required field is the country_id. 695 | * 696 | * { 697 | * "address": 698 | * { 699 | * "country_id":"PL" 700 | * } 701 | * } 702 | * 703 | * Note: since the minimum passed input is country_id, but OpenCart requires more fields, we will use default values unless provided. 704 | * Update: We decided to ignore country_id (even though it's sent by VSF) and estimate the shipping methods via default config values (since each store is tied to one country). 705 | */ 706 | 707 | public function shipping_methods(){ 708 | $token = $this->getParam('token', true); 709 | $cart_id = $this->getParam('cartId'); 710 | 711 | if($this->validateCartId($cart_id, $token)) { 712 | $this->load->language('api/shipping'); 713 | 714 | unset($this->session->data['shipping_methods']); 715 | unset($this->session->data['shipping_method']); 716 | 717 | /* We're estimating the shipping methods since OpenCart requires address details that aren't provided by VSF at registration */ 718 | 719 | $shipping_methods = array(); 720 | 721 | $country_id = $this->config->get('config_country_id'); 722 | $zone_id = $this->config->get('config_zone_id'); 723 | 724 | $this->load->model('extension/extension'); 725 | 726 | $results = $this->model_extension_extension->getExtensions('shipping'); 727 | 728 | $this->load->model('localisation/country'); 729 | $country_data = $this->model_localisation_country->getCountry($country_id); 730 | 731 | $this->load->model('localisation/zone'); 732 | $zone_data = $this->model_localisation_zone->getZone($zone_id); 733 | 734 | $this->session->data['shipping_address'] = array( 735 | 'firstname' => '', 736 | 'lastname' => '', 737 | 'company' => '', 738 | 'address_1' => '', 739 | 'address_2' => '', 740 | 'postcode' => '', 741 | 'city' => '', 742 | 'zone_id' => $zone_id, 743 | 'zone' => (isset($zone_data['name'])) ? $zone_data['name'] : '', 744 | 'zone_code' => (isset($zone_data['code'])) ? $zone_data['code'] : '', 745 | 'country_id' => $country_id, 746 | 'country' => (isset($country_data['name'])) ? $country_data['name'] : '', 747 | 'iso_code_2' => (isset($country_data['iso_code_2'])) ? $country_data['iso_code_2'] : '', 748 | 'iso_code_3' => (isset($country_data['iso_code_3'])) ? $country_data['iso_code_3'] : '', 749 | 'address_format' => (isset($country_data['address_format'])) ? $country_data['address_format'] : '', 750 | ); 751 | 752 | foreach ($results as $result) { 753 | if ($this->config->get($result['code'] . '_status')) { 754 | $this->load->model('extension/shipping/' . $result['code']); 755 | 756 | $quote = $this->{'model_extension_shipping_' . $result['code']}->getQuote($this->session->data['shipping_address']); 757 | 758 | if ($quote) { 759 | // Clear Thinking: Ultimate Restrictions 760 | if (($this->config->get('ultimate_restrictions_status') || $this->config->get('module_ultimate_restrictions_status')) && isset($this->session->data['ultimate_restrictions'])) { 761 | foreach ($quote['quote'] as $index => $restricting_quote) { 762 | foreach ($this->session->data['ultimate_restrictions'] as $extension => $rules) { 763 | if ($extension != $result['code']) continue; 764 | foreach ($rules as $comparison => $values) { 765 | $adjusted_title = explode('(', $restricting_quote['title']); 766 | $adjusted_title = strtolower(html_entity_decode(trim($adjusted_title[0]), ENT_QUOTES, 'UTF-8')); 767 | if (($comparison == 'is' && in_array($adjusted_title, $values)) || ($comparison == 'not' && !in_array($adjusted_title, $values))) { 768 | unset($quote['quote'][$index]); 769 | } 770 | } 771 | } 772 | } 773 | if (empty($quote['quote'])) { 774 | continue; 775 | } 776 | } 777 | // end 778 | $shipping_methods[$result['code']] = array( 779 | 'title' => $quote['title'], 780 | 'quote' => $quote['quote'], 781 | 'sort_order' => $quote['sort_order'], 782 | 'error' => $quote['error'] 783 | ); 784 | } 785 | } 786 | } 787 | 788 | $sort_order = array(); 789 | 790 | foreach ($shipping_methods as $key => $value) { 791 | $sort_order[$key] = $value['sort_order']; 792 | } 793 | 794 | array_multisort($sort_order, SORT_ASC, $shipping_methods); 795 | 796 | if (!$shipping_methods) { 797 | $this->code = 500; 798 | $this->result = $this->language->get('error_no_shipping'); 799 | $this->sendResponse(); 800 | } 801 | 802 | $this->session->data['shipping_methods'] = $shipping_methods; 803 | 804 | $adjusted_shipping_methods = array(); 805 | 806 | foreach($shipping_methods as $smkey => $smvalue){ 807 | $adjusted_shipping_methods[] = array( 808 | 'carrier_code' => $smvalue['quote'][$smkey]['code'], 809 | 'method_code' => $smvalue['quote'][$smkey]['code'], 810 | 'carrier_title' => $smvalue['quote'][$smkey]['title'], 811 | 'method_title' => $smvalue['quote'][$smkey]['title'], 812 | 'amount' => (float)$smvalue['quote'][$smkey]['cost'], // Using the shipping price excluding tax since the tax will be applied to the entire order 813 | 'base_amount' => (float)$smvalue['quote'][$smkey]['cost'], 814 | 'available' => $smvalue['error'] ? false : true, 815 | 'error_message' => '', 816 | 'price_excl_tax' => (float)$smvalue['quote'][$smkey]['cost'], 817 | 'price_incl_tax' => ($smvalue['quote'][$smkey]['cost']) ? (float)$this->currency->format($this->tax->calculate($smvalue['quote'][$smkey]['cost'], (int)$smvalue['quote'][$smkey]['tax_class_id'], $this->config->get('config_tax')), $this->session->data['currency'], '', false) : 0 818 | ); 819 | } 820 | 821 | $this->result = $adjusted_shipping_methods; 822 | } 823 | 824 | $this->sendResponse(); 825 | } 826 | 827 | /* 828 | * POST /vsbridge/cart/shipping-information 829 | * This method sets the shipping information on specified quote which is a required step before calling api/cart/collect-totals 830 | * 831 | * GET PARAMS: 832 | * token - null OR user token obtained from /vsbridge/user/login 833 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 834 | * 835 | * REQUEST BODY: 836 | * 837 | * { 838 | * "addressInformation": 839 | * { 840 | * "shipping_address": 841 | * { 842 | * "country_id":"PL" 843 | * }, 844 | * "shippingMethodCode":"flatrate", 845 | * "shippingCarrierCode":"flatrate" 846 | * } 847 | * } 848 | * 849 | * TODO: Check the resposne body if there are specific fields VSF needs. 850 | * Note: Changing the array information (i.e. shipping_method_code => shippingMethodCode) due to incorrect API specifications 851 | */ 852 | 853 | public function shipping_information(){ 854 | $token = $this->getParam('token', true); 855 | $cart_id = $this->getParam('cartId'); 856 | $input = $this->getPost(); 857 | 858 | if($this->validateCartId($cart_id, $token)) { 859 | // Delete old shipping method so not to cause any issues if there is an error 860 | unset($this->session->data['shipping_method']); 861 | 862 | $this->load->language('api/shipping'); 863 | 864 | if ($this->cart->hasShipping()) { 865 | // Shipping Address 866 | if (!isset($this->session->data['shipping_address'])) { 867 | $this->code = 500; 868 | $this->result = $this->language->get('error_address'); 869 | } 870 | 871 | // Shipping Method 872 | if (empty($this->session->data['shipping_methods'])) { 873 | $this->code = 500; 874 | $this->result = $this->language->get('error_no_shipping'); 875 | } elseif (!isset($input['addressInformation']['shippingMethodCode'])) { 876 | $this->code = 500; 877 | $this->result = $this->language->get('error_method'); 878 | } else { 879 | $shipping = explode('.', $input['addressInformation']['shippingMethodCode']); 880 | 881 | if (!isset($shipping[0]) || !isset($shipping[1]) || !isset($this->session->data['shipping_methods'][$shipping[0]]['quote'][$shipping[1]])) { 882 | $this->code = 500; 883 | $this->result = $this->language->get('error_method'); 884 | } 885 | } 886 | 887 | if (!$this->result) { 888 | $this->session->data['shipping_method'] = $this->session->data['shipping_methods'][$shipping[0]]['quote'][$shipping[1]]; 889 | 890 | $cart_products = $this->cart->getProducts(); 891 | 892 | $cart_items = array(); 893 | 894 | /* Currently we don't calculate per-product tax/discount. Instead OpenCart calculates the cart total tax */ 895 | foreach($cart_products as $cart_product){ 896 | $cart_items[] = array( 897 | 'item_id' => (int)$cart_product['cart_id'], 898 | 'price' => (float)$cart_product['price'], 899 | 'base_price' => (float)$cart_product['price'], 900 | 'qty' => (int)$cart_product['quantity'], 901 | 'row_total' => (float)$cart_product['total'], 902 | 'base_row_total' => (float)$cart_product['total'], 903 | 'row_total_with_discount' => (float)$cart_product['total'], 904 | 'tax_amount' => 0, 905 | 'discount_amount' => 0, 906 | 'base_discount_amount' => 0, 907 | 'discount_percent' => 0, 908 | 'name' => $cart_product['name'], 909 | ); 910 | } 911 | 912 | $this->result = array( 913 | 'items' => $cart_items, 914 | 'message' => $this->language->get('text_method') 915 | ); 916 | } 917 | } else { 918 | unset($this->session->data['shipping_address']); 919 | unset($this->session->data['shipping_method']); 920 | unset($this->session->data['shipping_methods']); 921 | } 922 | } 923 | 924 | $this->sendResponse(); 925 | } 926 | 927 | /* 928 | * POST /vsbridge/cart/collect-totals 929 | * This method is called to update the quote totals just after the address information has been changed. 930 | * 931 | * GET PARAMS: 932 | * token - null OR user token obtained from /vsbridge/user/login 933 | * cartId - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from api/cart/create 934 | * 935 | * REQUEST BODY: 936 | * { 937 | * "methods": { 938 | * "paymentMethod": { 939 | * "method": "cashondelivery" 940 | * }, 941 | * "shippingCarrierCode": "flatrate", 942 | * "shippingMethodCode": "flatrate" 943 | * } 944 | * } 945 | * 946 | * Notes: We haven't found any usage of this method in VSF yet. Will update if necessary since totals are automatically updated via the totals endpoint. 947 | * TODO: Check the resposne body if there are specific fields VSF needs. 948 | */ 949 | 950 | public function collect_totals(){ 951 | $this->result = array(); 952 | 953 | $this->sendResponse(); 954 | } 955 | } -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/categories.php: -------------------------------------------------------------------------------- 1 | validateToken($this->getParam('apikey')); 29 | 30 | $store_id = $this->store_id; 31 | $language_id = $this->language_id; 32 | 33 | $this->load->model('vsbridge/api'); 34 | 35 | $categories = $this->model_vsbridge_api->getCategories($language_id, $store_id); 36 | 37 | $categories_tree = $this->buildTree($categories); 38 | 39 | $this->mapFields($categories_tree); 40 | 41 | $this->flattenCopy($categories_tree); 42 | 43 | $categories_tree = array_merge($categories_tree, $this->flattenedElements); 44 | 45 | $this->result = $categories_tree; 46 | 47 | $this->sendResponse(); 48 | } 49 | 50 | public function buildTree(array &$elements, $parentId = 1) { 51 | $branch = array(); 52 | 53 | foreach ($elements as &$element) { 54 | if((int) $element['parent_id'] == 0){ 55 | $element['parent_id'] = 1; 56 | } 57 | 58 | if ((int)$element['parent_id'] == $parentId) { 59 | $children = $this->buildTree($elements, (int)$element['category_id']); 60 | if ($children) { 61 | $element['children_data'] = $children; 62 | } 63 | 64 | array_push($branch, $element); 65 | unset($element); 66 | } 67 | } 68 | 69 | return $branch; 70 | } 71 | 72 | public function mapField(array &$array, $oldkey, $newkey, $binary_to_truthy = null){ 73 | if(isset($array[$oldkey])){ 74 | if(!empty($binary_to_truthy)){ 75 | if($array[$oldkey] == 1){ 76 | $array[$newkey] = true; 77 | }else{ 78 | $array[$newkey] = false; 79 | } 80 | }else{ 81 | $array[$newkey] = $array[$oldkey]; 82 | } 83 | unset($array[$oldkey]); 84 | } 85 | } 86 | 87 | public function filterFields(array &$array, array $fields){ 88 | $new_array = array(); 89 | 90 | foreach($fields as $field){ 91 | if(isset($array[$field])){ 92 | $new_array[$field] = $array[$field]; 93 | } 94 | } 95 | 96 | return $new_array; 97 | } 98 | 99 | public function toInt(array &$array, $index){ 100 | if(isset($array[$index])){ 101 | $array[$index] = (int) $array[$index]; 102 | } 103 | } 104 | 105 | // Note: OpenCart does not have slugs for categories. That's why we generate one based on the category name and ID. 106 | // If your implementation uses a custom SEO extension, modify the mapFields function to reflect the correct slug and URL key/path. 107 | public function mapFields(array &$elements, $current_level = 0, $id_path = array(), $name_path = array()){ 108 | foreach ($elements as $element_key => &$element) { 109 | if((int) $element['parent_id'] == 1){ 110 | $current_level = 0; 111 | $id_path = array(); 112 | $name_path = array(); 113 | } 114 | 115 | $this->mapField($element, 'category_id', 'id'); 116 | $this->mapField($element, 'status', 'is_active', true); 117 | $this->mapField($element, 'date_added', 'created_at'); 118 | $this->mapField($element, 'date_modified', 'updated_at'); 119 | 120 | $this->toInt($element, 'id'); 121 | $this->toInt($element, 'parent_id'); 122 | 123 | $this->load->model('vsbridge/api'); 124 | $element['product_count'] = (int) $this->model_vsbridge_api->countCategoryProducts($element['id']); 125 | 126 | $name_slug = mb_strtolower(URLify::filter($element['name'], 60, $this->language_code)); 127 | 128 | $element['path'] = implode('/', array_merge($id_path, array($element['id']))); 129 | 130 | $element['slug'] = $name_slug.'-'.$element['id']; 131 | 132 | // Change if you use a custom SEO extension 133 | $element['url_key'] = trim($element['slug']); 134 | $element['url_path'] = trim(implode('/', array_merge($name_path, array($name_slug))).'-'.$element['id']); 135 | 136 | // Check for SEO URls via the OpenCart extension [SEO BackPack 2.9.1] 137 | $seo_url_alias = $this->model_vsbridge_api->getSeoUrlAlias('category', $element['id'], $this->language_id); 138 | 139 | if(!empty($seo_url_alias['keyword'])){ 140 | $element['url_path'] = trim($seo_url_alias['keyword']); 141 | } 142 | 143 | $element['level'] = $current_level + 1; 144 | $element['position'] = (int) $element_key; 145 | 146 | $element = $this->filterFields($element, array( 147 | 'id', 148 | 'parent_id', 149 | 'name', 150 | 'is_active', 151 | 'position', 152 | 'level', 153 | 'product_count', 154 | 'children_data', 155 | 'children_count', 156 | 'created_at', 157 | 'updated_at', 158 | 'path', 159 | 'slug', 160 | 'url_key', 161 | 'url_path' 162 | )); 163 | 164 | if(!empty($element['children_data'])){ 165 | $element['children_count'] = count($element['children_data']); 166 | $this->mapFields($element['children_data'], $current_level+1, array_merge($id_path, array($element['id'])), array_merge($name_path, array($name_slug))); 167 | }else{ 168 | $element['children_count'] = 0; 169 | $element['children_data'] = array(); 170 | } 171 | } 172 | 173 | unset($element); 174 | } 175 | 176 | public function flattenCopy(array &$elements){ 177 | foreach($elements as &$element){ 178 | if(!in_array($element['id'], $this->flattenedIds)){ 179 | 180 | array_push($this->flattenedElements, $element); 181 | array_push($this->flattenedIds, $element['id']); 182 | } 183 | 184 | if(!empty($element['children_data'])){ 185 | $this->flattenCopy($element['children_data']); 186 | } 187 | } 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/order.php: -------------------------------------------------------------------------------- 1 | getPost(); 80 | 81 | if(!empty($input['cart_id'])){ 82 | 83 | if($this->validateCartId($input['cart_id'], null)) { 84 | 85 | /* Following the logic from catalog/controller/checkout/confirm */ 86 | 87 | $this->load->language('vsbridge/api'); 88 | 89 | $this->load->model('vsbridge/api'); 90 | $this->load->model('localisation/country'); 91 | $this->load->model('localisation/zone'); 92 | 93 | if ($this->cart->hasShipping()) { 94 | // Validate if shipping address has been set, and copy it to the session info and save if the user is logged in 95 | if (!empty($input['addressInformation']['shippingAddress'])) { 96 | 97 | $shipping_address = array( 98 | 'firstname' => $this->checkInput($input['addressInformation']['shippingAddress'], 'firstname', true, false), 99 | 'lastname' => $this->checkInput($input['addressInformation']['shippingAddress'], 'lastname', true, false), 100 | 'company' => $this->checkInput($input['addressInformation']['shippingAddress'], 'company', false, true, ''), 101 | 'address_1' => implode(' ', $this->checkInput($input['addressInformation']['shippingAddress'], 'street', true, false)), 102 | 'address_2' => '', 103 | 'postcode' => $this->checkInput($input['addressInformation']['shippingAddress'], 'postcode', true, false), 104 | 'city' => $this->checkInput($input['addressInformation']['shippingAddress'], 'city', true, false) 105 | ); 106 | 107 | if($zone_id = $this->model_vsbridge_api->getZoneIdFromName($this->checkInput($input['addressInformation']['shippingAddress'], 'region', true, false))){ 108 | $shipping_address['zone_id'] = (int) $zone_id; 109 | }else{ 110 | $shipping_address['zone_id'] = $this->config->get('config_zone_id'); 111 | } 112 | 113 | if($country_id = $this->model_vsbridge_api->getCountryIdFromCode($this->checkInput($input['addressInformation']['shippingAddress'], 'country_id', true, false))){ 114 | $shipping_address['country_id'] = (int) $country_id; 115 | }else{ 116 | $shipping_address['country_id'] = $this->config->get('config_zone_id'); 117 | } 118 | 119 | $country_data = $this->model_localisation_country->getCountry($shipping_address['country_id']); 120 | $zone_data = $this->model_localisation_zone->getZone($shipping_address['zone_id']); 121 | 122 | $shipping_address['zone'] = (isset($zone_data['name'])) ? $zone_data['name'] : ''; 123 | $shipping_address['zone_code'] = (isset($zone_data['code'])) ? $zone_data['code'] : ''; 124 | $shipping_address['country'] = (isset($country_data['name'])) ? $country_data['name'] : ''; 125 | $shipping_address['iso_code_2'] = (isset($country_data['iso_code_2'])) ? $country_data['iso_code_2'] : ''; 126 | $shipping_address['iso_code_3'] = (isset($country_data['iso_code_3'])) ? $country_data['iso_code_3'] : ''; 127 | $shipping_address['address_format'] = (isset($country_data['address_format'])) ? $country_data['address_format'] : ''; 128 | 129 | $this->session->data['shipping_address'] = $shipping_address; 130 | 131 | }else{ 132 | $this->error[] = $this->language->get('error_no_shipping_address'); 133 | } 134 | 135 | // Validate if shipping method has been set. 136 | if (!isset($this->session->data['shipping_method'])) { 137 | $this->error[] = $this->language->get('error_no_shipping_method'); 138 | } 139 | } else { 140 | unset($this->session->data['shipping_address']); 141 | unset($this->session->data['shipping_method']); 142 | unset($this->session->data['shipping_methods']); 143 | } 144 | 145 | // Validate if payment address has been set. 146 | if (!empty($input['addressInformation']['billingAddress'])) { 147 | 148 | $payment_address = array( 149 | 'firstname' => $this->checkInput($input['addressInformation']['billingAddress'], 'firstname', true, false), 150 | 'lastname' => $this->checkInput($input['addressInformation']['billingAddress'], 'lastname', true, false), 151 | 'company' => $this->checkInput($input['addressInformation']['billingAddress'], 'company', false, true, ''), 152 | 'address_1' => implode(' ', $this->checkInput($input['addressInformation']['billingAddress'], 'street', true, false)), 153 | 'address_2' => '', 154 | 'postcode' => $this->checkInput($input['addressInformation']['billingAddress'], 'postcode', true, false), 155 | 'city' => $this->checkInput($input['addressInformation']['billingAddress'], 'city', true, false) 156 | ); 157 | 158 | if($zone_id = $this->model_vsbridge_api->getZoneIdFromName($this->checkInput($input['addressInformation']['billingAddress'], 'region', true, false))){ 159 | $payment_address['zone_id'] = (int) $zone_id; 160 | }else{ 161 | $payment_address['zone_id'] = $this->config->get('config_zone_id'); 162 | } 163 | 164 | if($country_id = $this->model_vsbridge_api->getCountryIdFromCode($this->checkInput($input['addressInformation']['billingAddress'], 'country_id', true, false))){ 165 | $payment_address['country_id'] = (int) $country_id; 166 | }else{ 167 | $payment_address['country_id'] = $this->config->get('config_zone_id'); 168 | } 169 | 170 | $country_data = $this->model_localisation_country->getCountry($payment_address['country_id']); 171 | $zone_data = $this->model_localisation_zone->getZone($payment_address['zone_id']); 172 | 173 | $payment_address['zone'] = (isset($zone_data['name'])) ? $zone_data['name'] : ''; 174 | $payment_address['zone_code'] = (isset($zone_data['code'])) ? $zone_data['code'] : ''; 175 | $payment_address['country'] = (isset($country_data['name'])) ? $country_data['name'] : ''; 176 | $payment_address['iso_code_2'] = (isset($country_data['iso_code_2'])) ? $country_data['iso_code_2'] : ''; 177 | $payment_address['iso_code_3'] = (isset($country_data['iso_code_3'])) ? $country_data['iso_code_3'] : ''; 178 | $payment_address['address_format'] = (isset($country_data['address_format'])) ? $country_data['address_format'] : ''; 179 | 180 | $this->session->data['payment_address'] = $payment_address; 181 | }else{ 182 | $this->error[] = $this->language->get('error_no_payment_address'); 183 | } 184 | 185 | // Validate if payment method has been set. 186 | // Since the payment method is sent via order/create in VSF, we won't check the session. 187 | if (isset($input['addressInformation']['payment_method_code'])) { 188 | $payment_methods = $this->model_extension_extension->getExtensions('payment'); 189 | 190 | foreach($payment_methods as $payment_method){ 191 | if($payment_method['code'] == $input['addressInformation']['payment_method_code']){ 192 | $this->session->data['payment_method']['title'] = ''; 193 | $this->session->data['payment_method']['code'] = $input['addressInformation']['payment_method_code']; 194 | $this->load->language('extension/payment/' . $payment_method['code']); 195 | $payment_title = $this->language->get('text_title'); 196 | 197 | if($payment_title != 'text_title'){ 198 | $this->session->data['payment_method']['title'] = $payment_title; 199 | } 200 | } 201 | } 202 | 203 | if(empty($this->session->data['payment_method']['code'])){ 204 | $this->error[] = $this->language->get('error_invalid_payment_code'); 205 | } 206 | }else{ 207 | $this->error[] = $this->language->get('error_no_payment_method'); 208 | } 209 | 210 | // Validate cart has products and has stock. 211 | if ((!$this->cart->hasProducts() && empty($this->session->data['vouchers'])) || (!$this->cart->hasStock() && !$this->config->get('config_stock_checkout'))) { 212 | $this->error[] = $this->language->get('error_cart_product_stock'); 213 | } 214 | 215 | // Validate minimum quantity requirements. 216 | $products = $this->cart->getProducts(); 217 | 218 | foreach ($products as $product) { 219 | $product_total = 0; 220 | 221 | foreach ($products as $product_2) { 222 | if ($product_2['product_id'] == $product['product_id']) { 223 | $product_total += $product_2['quantity']; 224 | } 225 | } 226 | 227 | if ($product['minimum'] > $product_total) { 228 | $this->error[] = sprintf($this->language->get('error_minimum_product_quantity'), $product['minimum'], $product['model'], $product_total); 229 | 230 | break; 231 | } 232 | } 233 | 234 | if(empty($this->error)){ 235 | 236 | $order_data = array(); 237 | 238 | $totals = array(); 239 | $taxes = $this->cart->getTaxes(); 240 | $total = 0; 241 | 242 | // Because __call can not keep var references so we put them into an array. 243 | $total_data = array( 244 | 'totals' => &$totals, 245 | 'taxes' => &$taxes, 246 | 'total' => &$total 247 | ); 248 | 249 | $this->load->model('extension/extension'); 250 | 251 | $sort_order = array(); 252 | 253 | $results = $this->model_extension_extension->getExtensions('total'); 254 | 255 | foreach ($results as $key => $value) { 256 | $sort_order[$key] = $this->config->get($value['code'] . '_sort_order'); 257 | } 258 | 259 | array_multisort($sort_order, SORT_ASC, $results); 260 | 261 | foreach ($results as $result) { 262 | if ($this->config->get($result['code'] . '_status')) { 263 | $this->load->model('extension/total/' . $result['code']); 264 | 265 | // We have to put the totals in an array so that they pass by reference. 266 | $this->{'model_extension_total_' . $result['code']}->getTotal($total_data); 267 | } 268 | } 269 | 270 | $sort_order = array(); 271 | 272 | foreach ($totals as $key => $value) { 273 | $sort_order[$key] = $value['sort_order']; 274 | } 275 | 276 | array_multisort($sort_order, SORT_ASC, $totals); 277 | 278 | $order_data['totals'] = $totals; 279 | 280 | $this->load->language('checkout/checkout'); 281 | 282 | $order_data['invoice_prefix'] = $this->config->get('config_invoice_prefix'); 283 | $order_data['store_id'] = $this->config->get('config_store_id'); 284 | $order_data['store_name'] = $this->config->get('config_name'); 285 | 286 | if ($order_data['store_id']) { 287 | $order_data['store_url'] = $this->config->get('config_url'); 288 | } else { 289 | if ($this->request->server['HTTPS']) { 290 | $order_data['store_url'] = HTTPS_SERVER; 291 | } else { 292 | $order_data['store_url'] = HTTP_SERVER; 293 | } 294 | } 295 | 296 | if ($this->customer->isLogged()) { 297 | $this->load->model('account/customer'); 298 | 299 | $customer_info = $this->model_account_customer->getCustomer($this->customer->getId()); 300 | 301 | $order_data['customer_id'] = $this->customer->getId(); 302 | $order_data['customer_group_id'] = $customer_info['customer_group_id']; 303 | $order_data['firstname'] = $customer_info['firstname']; 304 | $order_data['lastname'] = $customer_info['lastname']; 305 | $order_data['email'] = $customer_info['email']; 306 | $order_data['telephone'] = $customer_info['telephone']; 307 | $order_data['fax'] = $customer_info['fax']; 308 | $order_data['custom_field'] = json_decode($customer_info['custom_field'], true); 309 | } else { 310 | $order_data['customer_id'] = 0; 311 | $order_data['customer_group_id'] = $this->config->get('config_customer_group_id'); 312 | $order_data['firstname'] = $this->session->data['shipping_address']['firstname']; 313 | $order_data['lastname'] = $this->session->data['shipping_address']['lastname']; 314 | $order_data['email'] = $this->checkInput($input['addressInformation']['shippingAddress'], 'email', true, false); 315 | $order_data['telephone'] = $this->checkInput($input['addressInformation']['shippingAddress'], 'telephone', false, true); 316 | $order_data['fax'] = ''; 317 | $order_data['custom_field'] = array(); 318 | } 319 | 320 | $order_data['payment_firstname'] = $this->session->data['payment_address']['firstname']; 321 | $order_data['payment_lastname'] = $this->session->data['payment_address']['lastname']; 322 | $order_data['payment_company'] = $this->session->data['payment_address']['company']; 323 | $order_data['payment_address_1'] = $this->session->data['payment_address']['address_1']; 324 | $order_data['payment_address_2'] = $this->session->data['payment_address']['address_2']; 325 | $order_data['payment_city'] = $this->session->data['payment_address']['city']; 326 | $order_data['payment_postcode'] = $this->session->data['payment_address']['postcode']; 327 | $order_data['payment_zone'] = $this->session->data['payment_address']['zone']; 328 | $order_data['payment_zone_id'] = $this->session->data['payment_address']['zone_id']; 329 | $order_data['payment_country'] = $this->session->data['payment_address']['country']; 330 | $order_data['payment_country_id'] = $this->session->data['payment_address']['country_id']; 331 | $order_data['payment_address_format'] = $this->session->data['payment_address']['address_format']; 332 | $order_data['payment_custom_field'] = (isset($this->session->data['payment_address']['custom_field']) ? $this->session->data['payment_address']['custom_field'] : array()); 333 | 334 | if (isset($this->session->data['payment_method']['title'])) { 335 | $order_data['payment_method'] = $this->session->data['payment_method']['title']; 336 | } else { 337 | $order_data['payment_method'] = ''; 338 | } 339 | 340 | if (isset($this->session->data['payment_method']['code'])) { 341 | $order_data['payment_code'] = $this->session->data['payment_method']['code']; 342 | } else { 343 | $order_data['payment_code'] = ''; 344 | } 345 | 346 | if ($this->cart->hasShipping()) { 347 | $order_data['shipping_firstname'] = $this->session->data['shipping_address']['firstname']; 348 | $order_data['shipping_lastname'] = $this->session->data['shipping_address']['lastname']; 349 | $order_data['shipping_company'] = $this->session->data['shipping_address']['company']; 350 | $order_data['shipping_address_1'] = $this->session->data['shipping_address']['address_1']; 351 | $order_data['shipping_address_2'] = $this->session->data['shipping_address']['address_2']; 352 | $order_data['shipping_city'] = $this->session->data['shipping_address']['city']; 353 | $order_data['shipping_postcode'] = $this->session->data['shipping_address']['postcode']; 354 | $order_data['shipping_zone'] = $this->session->data['shipping_address']['zone']; 355 | $order_data['shipping_zone_id'] = $this->session->data['shipping_address']['zone_id']; 356 | $order_data['shipping_country'] = $this->session->data['shipping_address']['country']; 357 | $order_data['shipping_country_id'] = $this->session->data['shipping_address']['country_id']; 358 | $order_data['shipping_address_format'] = $this->session->data['shipping_address']['address_format']; 359 | $order_data['shipping_custom_field'] = (isset($this->session->data['shipping_address']['custom_field']) ? $this->session->data['shipping_address']['custom_field'] : array()); 360 | 361 | if (isset($this->session->data['shipping_method']['title'])) { 362 | $order_data['shipping_method'] = $this->session->data['shipping_method']['title']; 363 | } else { 364 | $order_data['shipping_method'] = ''; 365 | } 366 | 367 | if (isset($this->session->data['shipping_method']['code'])) { 368 | $order_data['shipping_code'] = $this->session->data['shipping_method']['code']; 369 | } else { 370 | $order_data['shipping_code'] = ''; 371 | } 372 | } else { 373 | $order_data['shipping_firstname'] = ''; 374 | $order_data['shipping_lastname'] = ''; 375 | $order_data['shipping_company'] = ''; 376 | $order_data['shipping_address_1'] = ''; 377 | $order_data['shipping_address_2'] = ''; 378 | $order_data['shipping_city'] = ''; 379 | $order_data['shipping_postcode'] = ''; 380 | $order_data['shipping_zone'] = ''; 381 | $order_data['shipping_zone_id'] = ''; 382 | $order_data['shipping_country'] = ''; 383 | $order_data['shipping_country_id'] = ''; 384 | $order_data['shipping_address_format'] = ''; 385 | $order_data['shipping_custom_field'] = array(); 386 | $order_data['shipping_method'] = ''; 387 | $order_data['shipping_code'] = ''; 388 | } 389 | 390 | $order_data['products'] = array(); 391 | 392 | foreach ($this->cart->getProducts() as $product) { 393 | $option_data = array(); 394 | 395 | foreach ($product['option'] as $option) { 396 | $option_data[] = array( 397 | 'product_option_id' => $option['product_option_id'], 398 | 'product_option_value_id' => $option['product_option_value_id'], 399 | 'option_id' => $option['option_id'], 400 | 'option_value_id' => $option['option_value_id'], 401 | 'name' => $option['name'], 402 | 'value' => $option['value'], 403 | 'type' => $option['type'] 404 | ); 405 | } 406 | 407 | $order_data['products'][] = array( 408 | 'product_id' => $product['product_id'], 409 | 'name' => $product['name'], 410 | 'base_price' => $product['base_price'], 411 | 'cost' => $product['cost'], 412 | 'supplier_id' => $product['supplier_id'], 413 | 'model' => $product['model'], 414 | 'option' => $option_data, 415 | 'download' => $product['download'], 416 | 'quantity' => $product['quantity'], 417 | 'subtract' => $product['subtract'], 418 | 'price' => $product['price'], 419 | 'total' => $product['total'], 420 | 'tax' => $this->tax->getTax($product['price'], $product['tax_class_id']), 421 | 'reward' => $product['reward'] 422 | ); 423 | } 424 | 425 | // Gift Voucher 426 | $order_data['vouchers'] = array(); 427 | 428 | if (!empty($this->session->data['vouchers'])) { 429 | foreach ($this->session->data['vouchers'] as $voucher) { 430 | $order_data['vouchers'][] = array( 431 | 'description' => $voucher['description'], 432 | 'code' => token(10), 433 | 'to_name' => $voucher['to_name'], 434 | 'to_email' => $voucher['to_email'], 435 | 'from_name' => $voucher['from_name'], 436 | 'from_email' => $voucher['from_email'], 437 | 'voucher_theme_id' => $voucher['voucher_theme_id'], 438 | 'message' => $voucher['message'], 439 | 'amount' => $voucher['amount'] 440 | ); 441 | } 442 | } 443 | 444 | $vat_id = $this->checkInput($input['addressInformation']['billingAddress'], 'vat_id', false, true, null); 445 | 446 | $order_data['comment'] = $vat_id ? 'VAT ID: '.$vat_id : ''; // not implemented yet - we store the VAT ID here if provided 447 | $order_data['total'] = $total_data['total']; 448 | 449 | if (isset($this->request->cookie['tracking'])) { 450 | $order_data['tracking'] = $this->request->cookie['tracking']; 451 | 452 | $subtotal = $this->cart->getSubTotal(); 453 | 454 | // Affiliate 455 | $this->load->model('affiliate/affiliate'); 456 | 457 | $affiliate_info = $this->model_affiliate_affiliate->getAffiliateByCode($this->request->cookie['tracking']); 458 | 459 | if ($affiliate_info) { 460 | $order_data['affiliate_id'] = $affiliate_info['affiliate_id']; 461 | $order_data['commission'] = ($subtotal / 100) * $affiliate_info['commission']; 462 | } else { 463 | $order_data['affiliate_id'] = 0; 464 | $order_data['commission'] = 0; 465 | } 466 | 467 | // Marketing 468 | $this->load->model('checkout/marketing'); 469 | 470 | $marketing_info = $this->model_checkout_marketing->getMarketingByCode($this->request->cookie['tracking']); 471 | 472 | if ($marketing_info) { 473 | $order_data['marketing_id'] = $marketing_info['marketing_id']; 474 | } else { 475 | $order_data['marketing_id'] = 0; 476 | } 477 | } else { 478 | $order_data['affiliate_id'] = 0; 479 | $order_data['commission'] = 0; 480 | $order_data['marketing_id'] = 0; 481 | $order_data['tracking'] = ''; 482 | } 483 | 484 | $order_data['language_id'] = $this->config->get('config_language_id'); 485 | $order_data['currency_id'] = $this->currency->getId($this->session->data['currency']); 486 | $order_data['currency_code'] = $this->session->data['currency']; 487 | $order_data['currency_value'] = $this->currency->getValue($this->session->data['currency']); 488 | $order_data['ip'] = $this->request->server['REMOTE_ADDR']; 489 | 490 | // Fixing some PHP notice errors due to OCmods 491 | $order_data['payment_cost'] = ''; 492 | $order_data['shipping_cost'] = ''; 493 | $order_data['extra_cost'] = ''; 494 | 495 | if (!empty($this->request->server['HTTP_X_FORWARDED_FOR'])) { 496 | $order_data['forwarded_ip'] = $this->request->server['HTTP_X_FORWARDED_FOR']; 497 | } elseif (!empty($this->request->server['HTTP_CLIENT_IP'])) { 498 | $order_data['forwarded_ip'] = $this->request->server['HTTP_CLIENT_IP']; 499 | } else { 500 | $order_data['forwarded_ip'] = ''; 501 | } 502 | 503 | if (isset($this->request->server['HTTP_USER_AGENT'])) { 504 | $order_data['user_agent'] = $this->request->server['HTTP_USER_AGENT']; 505 | } else { 506 | $order_data['user_agent'] = ''; 507 | } 508 | 509 | if (isset($this->request->server['HTTP_ACCEPT_LANGUAGE'])) { 510 | $order_data['accept_language'] = $this->request->server['HTTP_ACCEPT_LANGUAGE']; 511 | } else { 512 | $order_data['accept_language'] = ''; 513 | } 514 | 515 | $this->load->model('checkout/order'); 516 | 517 | $order_id = $this->model_checkout_order->addOrder($order_data); 518 | 519 | $this->session->data['order_id'] = $order_id; 520 | 521 | $order_status_id = $this->config->get('config_order_status_id'); 522 | 523 | $this->model_checkout_order->addOrderHistory($order_id, $order_status_id); 524 | 525 | // clear cart since the order has already been successfully stored. 526 | $this->cart->clear(); 527 | 528 | // clear session data 529 | unset($this->session->data['shipping_method']); 530 | unset($this->session->data['shipping_methods']); 531 | unset($this->session->data['payment_method']); 532 | unset($this->session->data['payment_methods']); 533 | unset($this->session->data['guest']); 534 | unset($this->session->data['comment']); 535 | unset($this->session->data['order_id']); 536 | unset($this->session->data['coupon']); 537 | unset($this->session->data['reward']); 538 | unset($this->session->data['voucher']); 539 | unset($this->session->data['vouchers']); 540 | unset($this->session->data['totals']); 541 | 542 | $this->result = array( 543 | 'order_id' => $order_id 544 | ); 545 | 546 | }else{ 547 | $this->code = 500; 548 | $this->result = implode("\n", $this->error); 549 | } 550 | 551 | } 552 | 553 | }else{ 554 | $this->load->language('vsbridge/api'); 555 | $this->code = 500; 556 | $this->result = $this->language->get('error_missing_input').'cart_id'; 557 | } 558 | 559 | $this->sendResponse(); 560 | } 561 | } 562 | 563 | -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/product.php: -------------------------------------------------------------------------------- 1 | list(); 28 | break; 29 | 30 | case 'render-list': 31 | $this->renderList(); 32 | break; 33 | } 34 | 35 | die(); 36 | } 37 | 38 | /* 39 | * RESPONSE: 40 | * { 41 | "code": 200, 42 | "result": { 43 | "items": [ 44 | { 45 | "id": 1866, 46 | "sku": "WP07", 47 | "name": "Aeon Capri", 48 | "price": 0, 49 | "status": 1, 50 | "visibility": 4, 51 | "type_id": "configurable", 52 | "created_at": "2017-11-06 12:17:26", 53 | "updated_at": "2017-11-06 12:17:26", 54 | "product_links": [], 55 | "tier_prices": [], 56 | "custom_attributes": [ 57 | { 58 | "attribute_code": "description", 59 | "value": "

Reach for the stars and beyond in these Aeon Capri pant. With a soft, comfortable feel and moisture wicking fabric, these duo-tone leggings are easy to wear -- and wear attractively.

\n

• Black capris with teal accents.
• Thick, 3\" flattering waistband.
• Media pocket on inner waistband.
• Dry wick finish for ultimate comfort and dryness.

" 60 | }, 61 | { 62 | "attribute_code": "image", 63 | "value": "/w/p/wp07-black_main.jpg" 64 | }, 65 | { 66 | "attribute_code": "category_ids", 67 | "value": [ 68 | "27", 69 | "32", 70 | "35", 71 | "2" 72 | ] 73 | }, 74 | { 75 | "attribute_code": "url_key", 76 | "value": "aeon-capri" 77 | }, 78 | { 79 | "attribute_code": "tax_class_id", 80 | "value": "2" 81 | }, 82 | { 83 | "attribute_code": "eco_collection", 84 | "value": "0" 85 | }, 86 | { 87 | "attribute_code": "performance_fabric", 88 | "value": "1" 89 | }, 90 | { 91 | "attribute_code": "erin_recommends", 92 | "value": "0" 93 | }, 94 | { 95 | "attribute_code": "new", 96 | "value": "0" 97 | }, 98 | { 99 | "attribute_code": "sale", 100 | "value": "0" 101 | }, 102 | { 103 | "attribute_code": "style_bottom", 104 | "value": "107" 105 | }, 106 | { 107 | "attribute_code": "pattern", 108 | "value": "195" 109 | }, 110 | { 111 | "attribute_code": "climate", 112 | "value": "205,212,206" 113 | } 114 | ] 115 | } 116 | ], 117 | "search_criteria": { 118 | "filter_groups": [ 119 | { 120 | "filters": [ 121 | { 122 | "field": "sku", 123 | "value": "WP07", 124 | "condition_type": "in" 125 | } 126 | ] 127 | } 128 | ] 129 | }, 130 | "total_count": 1 131 | } 132 | } 133 | */ 134 | public function list(){ 135 | $skus = $this->getParam('skus'); 136 | 137 | $sku_list = explode(',', $skus); 138 | 139 | $this->load->model('catalog/product'); 140 | $this->load->model('vsbridge/api'); 141 | 142 | $products = array(); 143 | 144 | foreach($sku_list as $sku){ 145 | $product_id = $this->model_vsbridge_api->getProductIdFromSku($sku); 146 | 147 | if(!empty($product_id)){ 148 | array_push($products, $this->model_catalog_product->getProduct($product_id['product_id'])); 149 | } 150 | } 151 | 152 | $populated_products = $this->load->controller('vsbridge/products/populateProducts', array( 153 | 'products' => $products, 154 | 'language_id' => $this->language_id 155 | )); 156 | 157 | $this->result = array('items' => $populated_products); 158 | 159 | $this->sendResponse(); 160 | } 161 | 162 | /* 163 | * RESPONSE: 164 | * 165 | * { 166 | "code": 200, 167 | "result": { 168 | "items": [ 169 | { 170 | "price_info": { 171 | "final_price": 59.04, 172 | "max_price": 59.04, 173 | "max_regular_price": 59.04, 174 | "minimal_regular_price": 59.04, 175 | "special_price": null, 176 | "minimal_price": 59.04, 177 | "regular_price": 48, 178 | "formatted_prices": { 179 | "final_price": "$59.04", 180 | "max_price": "$59.04", 181 | "minimal_price": "$59.04", 182 | "max_regular_price": "$59.04", 183 | "minimal_regular_price": null, 184 | "special_price": null, 185 | "regular_price": "$48.00" 186 | }, 187 | "extension_attributes": { 188 | "tax_adjustments": { 189 | "final_price": 47.999999, 190 | "max_price": 47.999999, 191 | "max_regular_price": 47.999999, 192 | "minimal_regular_price": 47.999999, 193 | "special_price": 47.999999, 194 | "minimal_price": 47.999999, 195 | "regular_price": 48, 196 | "formatted_prices": { 197 | "final_price": "$48.00", 198 | "max_price": "$48.00", 199 | "minimal_price": "$48.00", 200 | "max_regular_price": "$48.00", 201 | "minimal_regular_price": null, 202 | "special_price": "$48.00", 203 | "regular_price": "$48.00" 204 | } 205 | }, 206 | "weee_attributes": [], 207 | "weee_adjustment": "$59.04" 208 | } 209 | }, 210 | "url": "http://demo-magento2.vuestorefront.io/aeon-capri.html", 211 | "id": 1866, 212 | "name": "Aeon Capri", 213 | "type": "configurable", 214 | "store_id": 1, 215 | "currency_code": "USD", 216 | "sgn": "bCt7e44sl1iZV8hzYGioKvSq0EdsAcF21FhpTG5t8l8" 217 | } 218 | ] 219 | } 220 | } 221 | */ 222 | public function renderList(){ 223 | $skus = $this->getParam('skus'); 224 | $customer_group_id = $this->getParam('customerGroupId', true) ?? $this->config->get('config_customer_group_id'); 225 | 226 | $sku_list = explode(',', $skus); 227 | 228 | $this->load->model('catalog/product'); 229 | $this->load->model('vsbridge/api'); 230 | 231 | $products = array(); 232 | 233 | foreach($sku_list as $sku){ 234 | $product_id = $this->model_vsbridge_api->getProductIdFromSku($sku); 235 | 236 | if(!empty($product_id)){ 237 | array_push($products, $this->model_catalog_product->getProduct($product_id['product_id'])); 238 | } 239 | } 240 | 241 | $populated_products = $this->load->controller('vsbridge/products/populateProducts', array( 242 | 'products' => $products, 243 | 'language_id' => $this->language_id 244 | )); 245 | 246 | /* Adjust the output to reflect the format above */ 247 | $adjusted_products = array(); 248 | 249 | foreach($populated_products as $populated_product){ 250 | $customer_group_prices = $this->model_vsbridge_api->getProductDiscountsByCustomerGroup($populated_product['id'], $customer_group_id); 251 | 252 | if(!empty($customer_group_prices[0]['price'])){ 253 | $new_final_price = $this->currency->format($this->tax->calculate($customer_group_prices[0]['price'], $populated_product['tax_class_id'], $this->config->get('config_tax')), $this->config->get('config_currency'), NULL, FALSE); 254 | } 255 | 256 | $populated_product['price_info'] = array( 257 | 'final_price' => $new_final_price ?? $populated_product['final_price'], 258 | 'extension_attributes' => array( 259 | 'tax_adjustments' => array( 260 | 'final_price' => $new_final_price ?? $populated_product['final_price'] 261 | ) 262 | ) 263 | ); 264 | 265 | array_push($adjusted_products, $populated_product); 266 | } 267 | 268 | 269 | $this->result = array('items' => $adjusted_products); 270 | 271 | $this->sendResponse(); 272 | } 273 | } -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/products.php: -------------------------------------------------------------------------------- 1 | validateToken($this->getParam('apikey')); 85 | 86 | $store_id = $this->store_id; 87 | $language_id = $this->language_id; 88 | 89 | $pageSize = (int) $this->getParam('pageSize'); 90 | $page = (int) $this->getParam('page'); 91 | 92 | $this->load->model('vsbridge/api'); 93 | $this->load->model('catalog/product'); 94 | 95 | $filter_data = array( 96 | 'start' => ($page - 1) * $pageSize, 97 | 'limit' => $pageSize 98 | ); 99 | 100 | $products = $this->model_catalog_product->getProducts($filter_data); 101 | 102 | $response = $this->populateProducts(array( 103 | 'products' => $products, 104 | 'language_id' => $language_id 105 | )); 106 | 107 | $this->result = $response; 108 | 109 | $this->sendResponse(); 110 | } 111 | 112 | public function slugify($text) { 113 | // replace non letter or digits by - 114 | $text = preg_replace('#[^\\pL\d]+#u', '-', $text); 115 | 116 | // trim 117 | $text = trim($text, '-'); 118 | 119 | // transliterate 120 | if (function_exists('transliterator_transliterate')) 121 | { 122 | $text = transliterator_transliterate('Any-Latin; Latin-ASCII; Lower()', $text); 123 | } 124 | 125 | // lowercase 126 | $text = strtolower($text); 127 | 128 | // remove unwanted characters 129 | $text = preg_replace('#[^-\w]+#', '', $text); 130 | 131 | if (empty($text)) 132 | { 133 | return 'n-a'; 134 | } 135 | 136 | return $text; 137 | } 138 | 139 | public function createSlug($product_id, $product_name, $product_model, $language_code) { 140 | $slug = str_replace('/','', parse_url($this->url->link('product/product', 'product_id=' . $product_id))['path']); 141 | 142 | // If there are no SEO slugs available for the product, the resulting $slug will be index.php, which is invalid. 143 | // Detect if an invalid slug is present and generate a slug manually based on the product name. 144 | $invalid_slugs = array('index.php'); 145 | 146 | if(in_array($slug, $invalid_slugs) || empty($slug)) { 147 | $slug = $this->slugify($product_name . (!empty($product_model) ? '-' . $product_model : '') . (!empty($language_code) ? '-' . $language_code : '')); 148 | } 149 | 150 | return $slug; 151 | } 152 | 153 | public function populateProducts($input){ 154 | if(isset($input['products']) && isset($input['language_id'])){ 155 | $products = $input['products']; 156 | $language_id = $input['language_id']; 157 | 158 | $this->load->model('vsbridge/api'); 159 | 160 | $response = array(); 161 | 162 | /* There's a bug (no isset check) in seo_url.php line 358 due to MegaFilter's OCMod */ 163 | if(!isset($this->session->data['language'])){ 164 | $this->session->data['language'] = ''; 165 | } 166 | 167 | foreach($products as $product){ 168 | 169 | if(isset($product['product_id'])){ 170 | $product_categories = $this->model_vsbridge_api->getProductCategories($product['product_id']); 171 | 172 | $adjusted_categories = array(); 173 | 174 | $category_ids = array(); 175 | 176 | foreach($product_categories as $product_category){ 177 | if(isset($product_category['category_id'])){ 178 | if($category_details = $this->model_vsbridge_api->getCategoryDetails($product_category['category_id'], $language_id)){ 179 | array_push($category_ids, (int) $category_details[0]['category_id']); 180 | 181 | array_push($adjusted_categories, array( 182 | 'category_id' => (int) $category_details[0]['category_id'], 183 | 'name' => trim($category_details[0]['name']) 184 | )); 185 | } 186 | } 187 | } 188 | 189 | $product_attributes = $this->model_vsbridge_api->getProductAttributes($product['product_id'], $language_id); 190 | 191 | $custom_attributes = array(); 192 | 193 | $product_filters = $this->model_vsbridge_api->getProductFilters($product['product_id']); 194 | 195 | $stock = array(); 196 | 197 | if(isset($product['quantity']) && intval($product['quantity']) > 0) { 198 | $stock['is_in_stock'] = true; 199 | }else{ 200 | $stock['is_in_stock'] = false; 201 | } 202 | 203 | $product_images = $this->model_vsbridge_api->getProductImages($product['product_id']); 204 | 205 | $media_gallery = array(); 206 | 207 | foreach($product_images as $product_image){ 208 | if(isset($product_image['image'])){ 209 | array_push($media_gallery, array( 210 | 'image' => '/'.$product_image['image'], 211 | 'lab' => '', 212 | 'pos' => (int) $product_image['sort_order'], 213 | 'typ' => 'image' 214 | )); 215 | } 216 | } 217 | 218 | $tags = array(); 219 | if(!empty($product['tag'])) { 220 | foreach(explode(',', $product['tag']) as $tag) { 221 | array_push($tags, trim($tag)); 222 | } 223 | } 224 | 225 | $language_info = $this->model_vsbridge_api->getLanguageCode($language_id); 226 | $language_code = !empty($language_info) ? $language_info['code'] : ''; 227 | $language_code = substr($language_code, 0, strpos($language_code, "-")); 228 | $slug = $this->createSlug($product['product_id'], $product['name'], $product['model'], $language_code); 229 | 230 | $original_price_incl_tax = $this->currency->format($this->tax->calculate($product['price'], $product['tax_class_id'], $this->config->get('config_tax')), $this->config->get('config_currency'), NULL, FALSE); 231 | $original_price_excl_tax = $this->currency->format($product['price'], $this->config->get('config_currency'), NULL, FALSE); 232 | 233 | $special_price_incl_tax = null; 234 | $special_price_excl_tax = null; 235 | 236 | if(!empty($product['special'])){ 237 | $special_price_incl_tax = $this->currency->format($this->tax->calculate($product['special'], $product['tax_class_id'], $this->config->get('config_tax')), $this->config->get('config_currency'), NULL, FALSE); 238 | $special_price_excl_tax = $this->currency->format($product['special'], $this->config->get('config_currency'), NULL, FALSE); 239 | } 240 | 241 | $market_price_incl_tax = null; 242 | $market_price_excl_tax = null; 243 | 244 | if(!empty($product['recommended_price'])){ 245 | foreach($product['recommended_price'] as $recommended_price){ 246 | if($recommended_price['customer_group_id'] == 99){ 247 | $market_price_incl_tax = $this->currency->format($this->tax->calculate($recommended_price['price'], $product['tax_class_id'], $this->config->get('config_tax')), $this->config->get('config_currency'), NULL, FALSE); 248 | $market_price_excl_tax = $this->currency->format($recommended_price['price'], $this->config->get('config_currency'), NULL, FALSE); 249 | } 250 | } 251 | } 252 | 253 | $product_layout = $this->slugify($this->model_vsbridge_api->getProductLayoutName($product['product_id'], $this->store_id)); 254 | 255 | $product_array = array( 256 | 'id' => (int) $product['product_id'], 257 | 'type_id' => 'simple', 258 | 'sku' => $product['sku'], 259 | 'model' => $product['model'], 260 | 'category' => $adjusted_categories, 261 | 'category_ids' => $category_ids, 262 | 'description' => strip_tags(html_entity_decode($product['description'])), 263 | 'custom_attributes' => $custom_attributes, 264 | 'price' => $original_price_incl_tax, 265 | 'final_price' => isset($special_price_incl_tax) ? $special_price_incl_tax : $original_price_incl_tax, 266 | 'priceInclTax' => isset($special_price_incl_tax) ? $special_price_incl_tax : $original_price_incl_tax, 267 | 'priceTax' => 0, 268 | 'originalPrice' => $original_price_incl_tax, 269 | 'originalPriceInclTax' => $original_price_incl_tax, 270 | 'specialPriceInclTax' => $special_price_incl_tax ?? 0, 271 | 'specialPriceTax' => 0, 272 | 'special_price' => $special_price_incl_tax ?? 0, 273 | 'regular_price' => $original_price_incl_tax, 274 | 'marketPrice' => $market_price_incl_tax ?? 0, 275 | 'stock' => $stock, 276 | 'image' => '/'.$product['image'], 277 | 'thumbnail' => '/'.$product['image'], 278 | 'visibility' => 4, 279 | 'tax_class_id' => (int) $product['tax_class_id'], 280 | 'media_gallery' => $media_gallery, 281 | 'name' => $product['name'], 282 | 'manufacturer' => $product['manufacturer'], 283 | 'minimum' => !empty($product['minimum']) ? (int) $product['minimum'] : 1, 284 | 'status' => (int) $product['status'], 285 | 'slug' => $slug, 286 | 'sort_order' => (int) $product['sort_order'], 287 | 'tags' => $tags, 288 | 'url_path' => $slug, 289 | 'created_at' => $this->sanitizeDateTime($product['date_added']), 290 | 'updated_at' => $this->sanitizeDateTime($product['date_modified']), 291 | 'product_layout' => $product_layout, 292 | ); 293 | 294 | $product_array['qty'] = 0; 295 | 296 | if(isset($product['quantity']) && intval($product['quantity']) > 0) { 297 | // If the product has stock, set the add-to-cart qty as the minimum product quantity 298 | $product_array['qty'] = $product_array['minimum']; 299 | } 300 | 301 | foreach(array('length', 'width', 'height', 'weight') as $dimension) { 302 | $product_array[$dimension] = (string) number_format($product[$dimension], 2, '.', ''); 303 | } 304 | 305 | $weight_class = $this->model_vsbridge_api->getWeightClass($product['weight_class_id'], $this->language_id); 306 | $length_class = $this->model_vsbridge_api->getLengthClass($product[$dimension.'_class_id'], $this->language_id); 307 | 308 | $product_array['weight_class'] = isset($weight_class[0]['unit']) ? trim($weight_class[0]['unit']) : ''; 309 | $product_array['length_class'] = isset($length_class[0]['unit']) ? trim($length_class[0]['unit']) : ''; 310 | 311 | foreach($product_attributes as $product_attribute){ 312 | if(isset($product_attribute['attribute_id'])){ 313 | // To avoid the conflict of attribute IDs and filter IDs, an offset of 10000 is added 314 | $product_attribute['attribute_id'] = ((int) $product_attribute['attribute_id']) + 10000; 315 | $product_array['attribute_'.$product_attribute['attribute_id']] = trim($product_attribute['text']); 316 | } 317 | } 318 | 319 | foreach($product_filters as $product_filter){ 320 | if(isset($product_filter['filter_group_id']) && isset($product_filter['filter_id'])){ 321 | $product_array['filter_group_'.$product_filter['filter_group_id']][] = (int) $product_filter['filter_id']; 322 | } 323 | } 324 | 325 | $oc_url_alias = $this->model_vsbridge_api->getUrlAlias('product', $product['product_id']); 326 | 327 | if(!empty($oc_url_alias['keyword'])){ 328 | $product_array['slug'] = trim($oc_url_alias['keyword']); 329 | $product_array['url_path'] = trim($oc_url_alias['keyword']); 330 | } 331 | 332 | // Check for SEO URls via the OpenCart extension [SEO BackPack 2.9.1] 333 | $seo_url_alias = $this->model_vsbridge_api->getSeoUrlAlias('product', $product['product_id'], $this->language_id); 334 | 335 | if(!empty($seo_url_alias['keyword'])){ 336 | $product_array['slug'] = trim($seo_url_alias['keyword']); 337 | $product_array['url_path'] = trim($seo_url_alias['keyword']); 338 | } 339 | 340 | // Related products 341 | $related_products = $this->model_vsbridge_api->getRelatedProducts($product['product_id']); 342 | $related_product_ids = array(); 343 | 344 | if(!empty($related_products)){ 345 | foreach($related_products as $related_product){ 346 | if(!empty($related_product['related_id'])){ 347 | array_push($related_product_ids, $related_product['related_id']); 348 | } 349 | } 350 | } 351 | 352 | $product_array['related_products'] = $related_product_ids; 353 | 354 | // Product variants (only if Advanced Product Variants extension is installed) 355 | $product_variant_ids = $this->model_vsbridge_api->getProductVariants($product['product_id'], $this->language_id); 356 | 357 | if(!empty($product_variant_ids)){ 358 | $product_array['product_variants'] = $product_variant_ids; 359 | } 360 | 361 | // Load product discounts (tier prices) 362 | $product_discounts = $this->model_vsbridge_api->getProductDiscounts($product['product_id']); 363 | 364 | $tier_prices = array(); 365 | 366 | foreach($product_discounts as $product_discount){ 367 | array_push($tier_prices, array( 368 | 'customer_group_id' => (int) $product_discount['customer_group_id'], 369 | 'qty' => (int) $product_discount['quantity'], 370 | 'value' => $this->currency->format($this->tax->calculate($product_discount['price'], $product['tax_class_id'], $this->config->get('config_tax')), $this->config->get('config_currency'), NULL, FALSE) 371 | )); 372 | } 373 | 374 | $product_array['tier_prices'] = $tier_prices; 375 | 376 | // TODO: Change 1 and 0 to true and false when https://github.com/DivanteLtd/vue-storefront/issues/3800 is solved 377 | // Add the 'new' label if the product was added 30 days ago or less 378 | $time = time(); 379 | $max_days_for_new = 30; 380 | $date1 = strtotime($product['date_available']); 381 | $date2 = strtotime($product['date_added']); 382 | $is_new = $max_days_for_new > ($time - ($date1 > $date2 ? $date1 : $date2)) / 86400; 383 | $product_array['new'] = $is_new ? '1' : '0'; 384 | 385 | // Add the 'sale' label if the product is on sale 386 | $product_array['sale'] = isset($special_price_incl_tax) ? '1' : '0'; 387 | 388 | // Discount percentage 389 | if ($product_array['regular_price']) { 390 | $product_array['discount'] = round( 391 | 100 - (($product_array['final_price'] / $product_array['regular_price']) * 100) 392 | ); 393 | } else { 394 | $product_array['discount'] = 0; 395 | } 396 | 397 | array_push($response, $product_array); 398 | } 399 | } 400 | 401 | return $response; 402 | }else{ 403 | return false; 404 | } 405 | } 406 | 407 | } 408 | -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/stock.php: -------------------------------------------------------------------------------- 1 | sku_list(); 18 | break; 19 | } 20 | } 21 | 22 | /* 23 | * GET /vsbridge/stock/check/:sku 24 | * This method is used to check the stock item for specified product sku 25 | * 26 | * Note: Currently using the endpoint that appears in vue-storefront/core/modules/catalog/store/stock/actions.ts as mentioned above. 27 | */ 28 | 29 | public function check(){ 30 | $sku = urldecode($this->getParam('sku')); 31 | 32 | $this->load->model('vsbridge/api'); 33 | 34 | $product_info = $this->model_vsbridge_api->getProductBySku($sku, $this->language_id); 35 | 36 | if(!empty($product_info)){ 37 | 38 | $adjusted_product_info = array( 39 | 'item_id' => (int) $product_info['product_id'], 40 | 'product_id' => (int) $product_info['product_id'], 41 | 'qty' => (int) $product_info['quantity'], 42 | 'is_in_stock' => !empty($product_info['quantity']) 43 | ); 44 | 45 | $this->result = $adjusted_product_info; 46 | 47 | }else{ 48 | $this->load->language('vsbridge/api'); 49 | $this->code = 500; 50 | $this->result = $this->language->get('error_product_not_found'); 51 | } 52 | 53 | $this->sendResponse(); 54 | } 55 | 56 | /* 57 | * GET /vsbridge/stock/list 58 | * This method is used to check multiple stock items for specified product skus. 59 | * 60 | * GET PARAMS: 61 | * skus - param of comma-separated values to indicate which stock items to return. 62 | */ 63 | 64 | public function sku_list(){ 65 | $skus = explode(',', urldecode($this->getParam('skus'))); 66 | 67 | $this->load->model('vsbridge/api'); 68 | 69 | $adjusted_product_info_list = array(); 70 | 71 | foreach($skus as $sku){ 72 | $product_info = $this->model_vsbridge_api->getProductBySku($sku, $this->language_id); 73 | 74 | if(!empty($product_info)){ 75 | array_push($adjusted_product_info_list, array( 76 | 'item_id' => (int) $product_info['product_id'], 77 | 'product_id' => (int) $product_info['product_id'], 78 | 'qty' => (int) $product_info['quantity'], 79 | 'is_in_stock' => !empty($product_info['quantity']) 80 | )); 81 | } 82 | } 83 | 84 | if(!empty($adjusted_product_info_list)){ 85 | $this->result = $adjusted_product_info_list; 86 | }else{ 87 | $this->load->language('vsbridge/api'); 88 | $this->code = 500; 89 | $this->result = $this->language->get('error_product_not_found'); 90 | } 91 | 92 | $this->sendResponse(); 93 | } 94 | } -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/sync_session.php: -------------------------------------------------------------------------------- 1 | getParam('session_id'); 10 | 11 | session_abort(); 12 | session_id($vsbridge_session_id); 13 | session_start(); 14 | 15 | $this->session->start('default', $vsbridge_session_id); 16 | 17 | // to: GET parameter determining the redirection destination 18 | // any other GET parameter will also be piped 19 | $to = 'checkout/cart'; 20 | $get_params = []; 21 | 22 | foreach($this->request->get as $k => $v) { 23 | if ($k == 'to') { 24 | $to = $v; 25 | } elseif(!in_array($k, array('route', 'session_id'))) { 26 | $get_params[] = $k . '=' . $v; 27 | } 28 | } 29 | 30 | $params = implode('&', $get_params); 31 | 32 | $this->response->redirect($this->url->link($to, $params)); 33 | } 34 | } -------------------------------------------------------------------------------- /src/catalog/controller/vsbridge/taxrules.php: -------------------------------------------------------------------------------- 1 | validateToken($this->getParam('apikey')); 26 | 27 | $this->load->model('vsbridge/api'); 28 | 29 | $response = array(); 30 | 31 | $tax_rules = $this->model_vsbridge_api->getTaxRules(); 32 | 33 | foreach($tax_rules as $tax_rule){ 34 | if(!empty($tax_rule['tax_rate_id']) && !empty($tax_rule['tax_class_id'])){ 35 | $tax_rate = $this->model_vsbridge_api->getTaxRates($tax_rule['tax_rate_id'])[0]; 36 | $tax_class = $this->model_vsbridge_api->getTaxClasses($tax_rule['tax_class_id'])[0]; 37 | if(!empty($tax_rate) && !empty($tax_class)){ 38 | if($this->checkIndexValue($tax_rate, 'type','P', 'trim')){ 39 | array_push($response, array( 40 | 'id' => (int) $tax_rule['tax_rule_id'], 41 | 'code' => $tax_class['title'], 42 | 'priority' => (int) $tax_rule['priority'], 43 | 'product_tax_class_ids' => array((int) $tax_rule['tax_class_id']), 44 | 'rates' => array( 45 | array( 46 | 'id' => (int) $tax_rate['tax_rate_id'], 47 | 'tax_country_id' => (int) $tax_rate['geo_zone_id'], 48 | 'code' => $tax_rate['name'], 49 | 'rate' => (float) $tax_rate['rate'], 50 | ) 51 | ) 52 | )); 53 | } 54 | } 55 | } 56 | } 57 | 58 | $this->result = $response; 59 | 60 | $this->sendResponse(); 61 | } 62 | } -------------------------------------------------------------------------------- /src/catalog/language/en-gb/vsbridge/api.php: -------------------------------------------------------------------------------- 1 | db->query("SELECT * FROM `" . DB_PREFIX . "api_session` WHERE `token` = '" . $this->db->escape($token) . "'"); 5 | 6 | return $query->row; 7 | } 8 | 9 | public function getAttributes($language_id) { 10 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "attribute_description` ad INNER JOIN `" . DB_PREFIX . "attribute` a ON (a.attribute_id = ad.attribute_id) WHERE `language_id` = '".(int) $language_id."'"); 11 | 12 | return $query->rows; 13 | } 14 | 15 | public function getAttributeGroups($language_id) { 16 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "attribute_group_description` agd INNER JOIN `" . DB_PREFIX . "attribute_group` ag ON (ag.attribute_group_id = agd.attribute_group_id) WHERE `language_id` = '".(int) $language_id."'"); 17 | 18 | return $query->rows; 19 | } 20 | 21 | public function getFilters($filter_group_id, $language_id){ 22 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "filter_description` fd INNER JOIN `" . DB_PREFIX ."filter` f ON (f.filter_id = fd.filter_id) WHERE fd.`filter_group_id` = '".(int) $filter_group_id."' AND `language_id` = '".(int) $language_id."'"); 23 | 24 | return $query->rows; 25 | } 26 | 27 | public function getFilterGroups($language_id){ 28 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "filter_group_description` fgd INNER JOIN `" . DB_PREFIX ."filter_group` fg ON (fg.filter_group_id = fgd.filter_group_id) WHERE `language_id` = '".(int) $language_id."'"); 29 | 30 | return $query->rows; 31 | } 32 | 33 | public function getCategories($language_id, $store_id){ 34 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "category_description` AS tcatdesc INNER JOIN `" . DB_PREFIX . "category` AS tcat ON tcatdesc.category_id = tcat.category_id WHERE tcatdesc.category_id IN (SELECT category_id FROM `" . DB_PREFIX . "category_to_store` WHERE `store_id` = '".(int) $store_id."') AND `language_id` = '".(int) $language_id."'"); 35 | 36 | return $query->rows; 37 | } 38 | 39 | // OpenCart Extension [Seo BackPack 2.9.1] 40 | public function getSeoUrlAlias($type, $type_id, $language_id){ 41 | $check_table = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "seo_url_alias'"); 42 | 43 | if ($check_table->num_rows) { 44 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "seo_url_alias` WHERE `query` = '" . $this->db->escape($type) . "_id=" . (int)$type_id . "' AND `language_id` = '" . (int)$language_id . "' ORDER BY `id` DESC"); 45 | return $query->row; 46 | }else{ 47 | return false; 48 | } 49 | } 50 | 51 | // Native OpenCart URL aliases (doesn't support multiple languages) 52 | public function getUrlAlias($type, $type_id){ 53 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "url_alias` WHERE `query` = '".$this->db->escape($type)."_id=".(int) $type_id."' ORDER BY `url_alias_id` DESC"); 54 | 55 | return $query->row; 56 | } 57 | 58 | public function getStoreConfig($config_name, $store_id){ 59 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "setting` WHERE `store_id` = '".(int) $store_id."' AND `key` = '".$this->db->escape($config_name)."'"); 60 | 61 | return $query->row; 62 | } 63 | 64 | public function countCategoryProducts($category_id){ 65 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_to_category` WHERE `category_id` = '".(int) $category_id."'"); 66 | 67 | return $query->num_rows; 68 | } 69 | 70 | public function getTaxRules($tax_rule_id = null){ 71 | $tax_rule_id_check = ""; 72 | 73 | if(isset($tax_rule_id)){ 74 | $tax_rule_id_check = " WHERE `tax_rule_id` = '".(int) $tax_rule_id."'"; 75 | } 76 | 77 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "tax_rule`".$tax_rule_id_check); 78 | 79 | return $query->rows; 80 | } 81 | 82 | public function getTaxRates($tax_rate_id = null){ 83 | $tax_rate_id_check = ""; 84 | 85 | if(isset($tax_rate_id)){ 86 | $tax_rate_id_check = " WHERE `tax_rate_id` = '".(int) $tax_rate_id."'"; 87 | } 88 | 89 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "tax_rate`".$tax_rate_id_check); 90 | 91 | return $query->rows; 92 | } 93 | 94 | public function getTaxClasses($tax_class_id = null){ 95 | $tax_class_id_check = ""; 96 | 97 | if(isset($tax_class_id)){ 98 | $tax_class_id_check = " WHERE `tax_class_id` = '".(int) $tax_class_id."'"; 99 | } 100 | 101 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "tax_class`".$tax_class_id_check); 102 | 103 | return $query->rows; 104 | } 105 | 106 | public function getProducts($language_id, $store_id, $pageSize, $page){ 107 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_description` AS tproddesc INNER JOIN `" . DB_PREFIX . "product` AS tprod ON tproddesc.product_id = tprod.product_id WHERE tproddesc.product_id IN (SELECT product_id FROM `" . DB_PREFIX . "product_to_store` WHERE `store_id` = '".(int) $store_id."') AND tproddesc.language_id = '".(int) $language_id."' AND tprod.status = '1' ORDER BY tprod.product_id ASC LIMIT ".(int) ($page * $pageSize).",".(int) $pageSize); 108 | 109 | return $query->rows; 110 | } 111 | 112 | public function getProductSpecialPrice($product_id, $group_id){ 113 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_special` WHERE `product_id` = '" . (int) $product_id . "' AND customer_group_id = '" . (int)$group_id . "' AND ((date_start = '0000-00-00' OR date_start < NOW()) AND (date_end = '0000-00-00' OR date_end > NOW()))"); 114 | 115 | return $query->row; 116 | } 117 | 118 | public function getProductCategories($product_id){ 119 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_to_category` WHERE product_id = '".(int) $product_id."'"); 120 | 121 | return $query->rows; 122 | } 123 | 124 | public function getProductLayoutName($product_id, $store_id = 0){ 125 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_to_layout` LEFT JOIN `" . DB_PREFIX . "layout` ON `" . DB_PREFIX . "product_to_layout`.`layout_id` = `" . DB_PREFIX . "layout`.`layout_id` WHERE `" . DB_PREFIX . "product_to_layout`.`product_id` = '" . (int)$product_id . "' AND `" . DB_PREFIX . "product_to_layout`.`store_id` = '" . (int)$store_id. "'"); 126 | 127 | $product_layout_name = 'default'; 128 | 129 | if ($query->num_rows && $query->row['name']) { 130 | $product_layout_name = $query->row['name']; 131 | } 132 | 133 | return $product_layout_name; 134 | } 135 | 136 | // For use with the Advanced Product Variant extension 137 | public function getProductVariants($product_id, $language_id){ 138 | $check_table = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "variantproducts'"); 139 | 140 | if ($check_table->num_rows) { 141 | 142 | $query = $this->db->query(" 143 | SELECT vd.title, (SELECT GROUP_CONCAT(v2p.product_id) FROM " . DB_PREFIX . "variantproducts_to_product v2p WHERE v.variantproduct_id = v2p.variantproduct_id ) prodIds 144 | FROM " . DB_PREFIX . "variantproducts v 145 | LEFT JOIN " . DB_PREFIX . "variantproducts_description vd ON (v.variantproduct_id = vd.variantproduct_id) 146 | LEFT JOIN " . DB_PREFIX . "variantproducts_to_product v2p ON (v.variantproduct_id = v2p.variantproduct_id) 147 | WHERE 148 | v2p.product_id = '" . (int)$product_id . "' AND 149 | vd.language_id = '" . (int)$language_id . "' AND 150 | v.status = '1' 151 | ORDER BY v.sort_order, v.variantproduct_id ASC 152 | "); 153 | 154 | $product_variant_ids = array(); 155 | 156 | foreach($query->rows as $product_variants){ 157 | if(!empty($product_variants['prodIds'])){ 158 | $product_ids = explode(',', $product_variants['prodIds']); 159 | 160 | if(is_array($product_ids)){ 161 | foreach($product_ids as $pid){ 162 | if(!in_array($pid, $product_variant_ids) && $pid != $product_id){ 163 | array_push($product_variant_ids, $pid); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | return $product_variant_ids; 171 | }else{ 172 | return false; 173 | } 174 | } 175 | 176 | public function getCategoryDetails($category_id, $language_id){ 177 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "category_description` WHERE category_id = '".(int) $category_id."' AND language_id = '".(int) $language_id."'"); 178 | 179 | return $query->rows; 180 | } 181 | 182 | public function getProductAttributes($product_id, $language_id){ 183 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_attribute` WHERE product_id = '".(int) $product_id."' AND language_id = '".(int) $language_id."'"); 184 | 185 | return $query->rows; 186 | } 187 | 188 | public function getProductFilters($product_id){ 189 | $query = $this->db->query("SELECT pft.filter_id, ft.filter_group_id FROM `". DB_PREFIX ."product_filter` pft LEFT JOIN `". DB_PREFIX ."filter` ft ON ft.filter_id = pft.filter_id WHERE pft.product_id = '".(int) $product_id."'"); 190 | 191 | return $query->rows; 192 | } 193 | 194 | public function getRelatedProducts($product_id){ 195 | $query = $this->db->query("SELECT DISTINCT related_id FROM `". DB_PREFIX ."product_related` WHERE product_id = '".(int) $product_id."'"); 196 | 197 | return $query->rows; 198 | } 199 | 200 | public function getProductDiscountsByCustomerGroup($product_id, $customer_group_id){ 201 | $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "product_discount WHERE product_id = '" . (int)$product_id . "' AND customer_group_id = '" . (int)$customer_group_id . "' AND ((date_start = '0000-00-00' OR date_start < NOW()) AND (date_end = '0000-00-00' OR date_end > NOW())) ORDER BY quantity ASC, priority ASC, price ASC"); 202 | 203 | return $query->rows; 204 | } 205 | 206 | public function getProductDiscounts($product_id){ 207 | $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "product_discount WHERE product_id = '" . (int)$product_id . "' AND ((date_start = '0000-00-00' OR date_start < NOW()) AND (date_end = '0000-00-00' OR date_end > NOW())) ORDER BY quantity ASC, priority ASC, price ASC"); 208 | 209 | return $query->rows; 210 | } 211 | 212 | public function getProductImages($product_id){ 213 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_image` WHERE product_id = '".(int) $product_id."' ORDER BY sort_order ASC"); 214 | 215 | return $query->rows; 216 | } 217 | 218 | public function getProductBySku($sku, $language_id){ 219 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_description` AS tproddesc INNER JOIN `" . DB_PREFIX . "product` AS tprod ON tproddesc.product_id = tprod.product_id WHERE tprod.sku = '". $this->db->escape($sku)."' AND tproddesc.language_id = '".(int) $language_id."'"); 220 | 221 | return $query->row; 222 | } 223 | 224 | public function getProductIdFromSku($sku){ 225 | $query = $this->db->query("SELECT product_id FROM `" . DB_PREFIX . "product` WHERE sku = '". $this->db->escape($sku)."'"); 226 | 227 | return $query->row; 228 | } 229 | 230 | public function deleteDefaultCustomerAddress($customer_id){ 231 | $this->db->query("DELETE FROM `" . DB_PREFIX . "address` WHERE customer_id = '" . (int)$customer_id . "'"); 232 | $this->db->query("UPDATE `" . DB_PREFIX . "customer` SET address_id = '0' WHERE customer_id = '" . (int)$customer_id . "'"); 233 | } 234 | 235 | public function addCustomerToken($customer_id, $ip) { 236 | $token = token(32); 237 | 238 | $this->db->query("INSERT INTO `" . DB_PREFIX . "vsbridge_token` SET customer_id = '" . (int)$customer_id . "', token = '" . $this->db->escape($token) . "', ip = '" . $this->db->escape($ip) . "', timestamp = '".time()."'"); 239 | 240 | return $token; 241 | } 242 | 243 | public function addCustomerRefreshToken($customer_id, $ip) { 244 | $this->db->query("INSERT INTO `" . DB_PREFIX . "vsbridge_refresh_token` SET customer_id = '" . (int)$customer_id . "', ip = '" . $this->db->escape($ip) . "', timestamp = '".time()."'"); 245 | 246 | return $this->db->getLastId(); 247 | } 248 | 249 | public function getCustomerRefreshToken($refresh_token_id){ 250 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "vsbridge_refresh_token` WHERE `vsbridge_refresh_token_id` = '" . (int) $refresh_token_id . "'"); 251 | 252 | return $query->row; 253 | } 254 | 255 | public function getCustomerToken($token){ 256 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "vsbridge_token` WHERE `token` = '" . $this->db->escape($token) . "'"); 257 | 258 | return $query->row; 259 | } 260 | 261 | public function getCustomerOrders($customer_id){ 262 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "order` WHERE customer_id ='". (int)$customer_id ."' ORDER BY order_id DESC"); 263 | 264 | return $query->rows; 265 | } 266 | 267 | public function getOrderStatus($order_status_id, $language_id){ 268 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "order_status` WHERE order_status_id ='". (int)$order_status_id ."' AND language_id = '". (int)$language_id ."'"); 269 | 270 | return $query->row; 271 | } 272 | 273 | public function getOrderProducts($order_id){ 274 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "order_product` WHERE order_id ='". (int)$order_id ."'"); 275 | 276 | return $query->rows; 277 | } 278 | 279 | public function getProduct($product_id){ 280 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product` WHERE product_id ='". (int)$product_id ."'"); 281 | 282 | return $query->row; 283 | } 284 | 285 | public function getProductDetails($product_id, $language_id){ 286 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_description` AS tproddesc INNER JOIN `" . DB_PREFIX . "product` AS tprod ON tproddesc.product_id = tprod.product_id WHERE tprod.product_id = '". (int)$product_id ."' AND tproddesc.language_id = '".(int) $language_id."'"); 287 | 288 | return $query->row; 289 | } 290 | 291 | public function getOrderTotals($order_id){ 292 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "order_total` WHERE order_id ='". (int)$order_id ."'"); 293 | 294 | return $query->rows; 295 | } 296 | 297 | public function getCustomerAddresses($customer_id){ 298 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "address` WHERE customer_id ='". (int)$customer_id ."'"); 299 | 300 | return $query->rows; 301 | } 302 | 303 | public function getZone($zone_id){ 304 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "zone` WHERE zone_id ='". (int)$zone_id ."'"); 305 | 306 | return $query->row; 307 | } 308 | 309 | public function getCountry($country_id){ 310 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "country` WHERE country_id ='". (int)$country_id ."'"); 311 | 312 | return $query->row; 313 | } 314 | 315 | public function getCountryIdFromCode($country_code){ 316 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "country` WHERE iso_code_2 ='". $this->db->escape($country_code) ."'"); 317 | 318 | if(isset($query->row['country_id'])){ 319 | return $query->row['country_id']; 320 | } 321 | } 322 | 323 | public function getZoneIdFromName($zone_name){ 324 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "zone` WHERE name LIKE '". $this->db->escape($zone_name) ."'"); 325 | 326 | if(isset($query->row['zone_id'])){ 327 | return $query->row['zone_id']; 328 | } 329 | } 330 | 331 | public function editCustomer($customer_id, $data) { 332 | if(!empty($data)){ 333 | $update_array = array(); 334 | 335 | if(isset($data['field']) && isset($data['value']) && isset($data['type'])){ 336 | switch($data['type']){ 337 | default: 338 | case 'string': 339 | $data['value'] = $this->db->escape($data['value']); 340 | break; 341 | case 'integer': 342 | $data['value'] = (int) $data['value']; 343 | break; 344 | } 345 | 346 | array_push($update_array, $data['field']." = '".$data['value']."'"); 347 | } 348 | 349 | $this->db->query("UPDATE `" . DB_PREFIX . "customer` SET ".implode(',', $update_array)." WHERE customer_id = '" . (int)$customer_id . "'"); 350 | } 351 | } 352 | 353 | public function deleteCustomerAddresses($customer_id){ 354 | $this->db->query("DELETE FROM `" . DB_PREFIX . "address` WHERE customer_id = '" . (int)$customer_id. "'"); 355 | } 356 | 357 | public function addCustomerAddress($customer_id, $data, $default_address = false){ 358 | $insert_array = array(); 359 | 360 | foreach ($data as $datum) { 361 | 362 | if (isset($datum['field']) && isset($datum['value']) && isset($datum['type'])) { 363 | switch ($datum['type']) { 364 | default: 365 | case 'string': 366 | $datum['value'] = $this->db->escape($datum['value']); 367 | break; 368 | case 'integer': 369 | $datum['value'] = (int)$datum['value']; 370 | break; 371 | } 372 | 373 | array_push($insert_array, $datum['field'] . " = '" . $datum['value'] . "'"); 374 | } 375 | } 376 | 377 | $this->db->query("INSERT INTO `" . DB_PREFIX . "address` SET customer_id = '". (int) $customer_id ."', ".implode(',', $insert_array)); 378 | 379 | if(($inserted_id = $this->db->getLastId()) && ($default_address == true)){ 380 | $this->db->query("UPDATE `" . DB_PREFIX . "customer` SET address_id = '". (int)$inserted_id ."' WHERE customer_id = '" . (int)$customer_id . "'"); 381 | } 382 | } 383 | 384 | public function getCustomerSessionId($customer_id, $store_id){ 385 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "vsbridge_session` WHERE customer_id = '". (int)$customer_id ."' AND store_id = '". (int)$store_id ."'"); 386 | 387 | return $query->row; 388 | } 389 | 390 | public function SetCustomerSessionId($customer_id, $store_id, $session_id){ 391 | $this->db->query("INSERT INTO `" . DB_PREFIX . "vsbridge_session` (`customer_id`, `store_id`, `session_id`) VALUES ('". (int)$customer_id ."', '". (int) $store_id ."', '". $this->db->escape($session_id) ."') ON DUPLICATE KEY UPDATE session_id = '" . $this->db->escape($session_id) . "'"); 392 | } 393 | 394 | public function transferCartProducts($source_session_id, $destination_session_id, $customer_id) { 395 | $this->db->query("UPDATE `" . DB_PREFIX . "cart` SET `customer_id` = '". (int)$customer_id ."', `session_id` = '". $this->db->escape($destination_session_id) ."' WHERE `session_id` = '". $this->db->escape($source_session_id) ."' AND `customer_id` = '0'"); 396 | } 397 | 398 | public function getCustomerCartSessionId($customer_id){ 399 | $query = $this->db->query("SELECT DISTINCT session_id FROM `" . DB_PREFIX . "cart` WHERE customer_id = '". (int) $customer_id ."' ORDER BY cart_id DESC LIMIT 1"); 400 | 401 | return $query->row; 402 | } 403 | 404 | public function getCart($cart_id, $customer_id = null){ 405 | $customer_lookup = ""; 406 | 407 | if(isset($customer_id)){ 408 | $customer_lookup = "AND customer_id = '". (int)$customer_id ."'"; 409 | } 410 | 411 | $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "cart` WHERE session_id ='". $this->db->escape($cart_id) ."' ".$customer_lookup); 412 | 413 | return $query->rows; 414 | } 415 | 416 | public function getWeightClass($weight_class_id, $language_id) { 417 | $sql = "SELECT * FROM " . DB_PREFIX . "weight_class wc LEFT JOIN " . DB_PREFIX . "weight_class_description wcd ON (wc.weight_class_id = wcd.weight_class_id AND wcd.language_id = '". (int) $language_id ."') WHERE wc.weight_class_id = '". (int) $weight_class_id ."' ORDER BY title"; 418 | $query = $this->db->query($sql); 419 | 420 | return $query->rows; 421 | } 422 | 423 | public function getLengthClass($length_class_id, $language_id) { 424 | $sql = "SELECT * FROM " . DB_PREFIX . "length_class lc LEFT JOIN " . DB_PREFIX . "length_class_description lcd ON (lc.length_class_id = lcd.length_class_id AND lcd.language_id = '". (int) $language_id ."') WHERe lc.length_class_id = '". (int) $length_class_id ."' ORDER BY title"; 425 | 426 | $query = $this->db->query($sql); 427 | 428 | return $query->rows; 429 | } 430 | public function addWishlist($product_id, $customer_id) { 431 | $this->db->query("DELETE FROM " . DB_PREFIX . "customer_wishlist WHERE customer_id = '" . (int)$customer_id . "' AND product_id = '" . (int)$product_id . "'"); 432 | 433 | $this->db->query("INSERT INTO " . DB_PREFIX . "customer_wishlist SET customer_id = '" . (int)$customer_id . "', product_id = '" . (int)$product_id . "', date_added = NOW()"); 434 | } 435 | 436 | public function deleteWishlist($product_id, $customer_id) { 437 | $this->db->query("DELETE FROM " . DB_PREFIX . "customer_wishlist WHERE customer_id = '" . (int)$customer_id . "' AND product_id = '" . (int)$product_id . "'"); 438 | } 439 | 440 | public function getWishlist($customer_id) { 441 | $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "customer_wishlist WHERE customer_id = '" . (int)$customer_id . "'"); 442 | 443 | return $query->rows; 444 | } 445 | 446 | public function getTotalWishlist($customer_id) { 447 | $query = $this->db->query("SELECT COUNT(*) AS total FROM " . DB_PREFIX . "customer_wishlist WHERE customer_id = '" . (int)$customer_id . "'"); 448 | 449 | return $query->row['total']; 450 | } 451 | 452 | public function getLanguageCode($language_id) { 453 | $query = $this->db->query("SELECT `code` FROM " . DB_PREFIX . "language WHERE language_id = '" . (int)$language_id . "' LIMIT 1"); 454 | 455 | return $query->row; 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/system/engine/vsbridgecontroller.php: -------------------------------------------------------------------------------- 1 | checkExtensionStatus(); 27 | 28 | $this->checkEndpointStatus(); 29 | 30 | $this->load->model('vsbridge/api'); 31 | 32 | $this->language_id = $this->getLanguageId(); 33 | $this->store_id = (int) $this->config->get('config_store_id'); 34 | 35 | $this->language_code = 'en'; 36 | $config_language = $this->model_vsbridge_api->getStoreConfig('config_language', $this->store_id); 37 | 38 | if(!empty($config_language['value'])){ 39 | $this->language_code = explode('-', $config_language['value'])[0]; 40 | } 41 | 42 | /* HTTP_ACCEPT_LANGUAGE fix */ 43 | if(!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])){ 44 | $_SERVER['HTTP_ACCEPT_LANGUAGE'] = ''; 45 | } 46 | } 47 | 48 | /* Prevent access to controllers if the extension is disabled */ 49 | public function checkExtensionStatus() 50 | { 51 | if((int)$this->config->get('vsbridge_status') !== 1) { 52 | $this->load->language('vsbridge/api'); 53 | $this->code = 400; 54 | $this->result = $this->language->get('error_extension_disabled'); 55 | $this->sendResponse(); 56 | } 57 | } 58 | 59 | /* Prevent access to API endpoint if disabled in extension settings */ 60 | public function checkEndpointStatus() 61 | { 62 | $class_name = strtolower(preg_replace('/\B([A-Z])/', '_$1', get_class($this))); 63 | $endpoint_name = str_replace('controller_vsbridge_', '', $class_name); 64 | 65 | if(empty($this->config->get('vsbridge_endpoint_statuses')[$endpoint_name])) { 66 | $this->load->language('vsbridge/api'); 67 | $this->code = 400; 68 | $this->result = $this->language->get('error_api_endpoint_disabled'); 69 | $this->sendResponse(); 70 | } 71 | } 72 | 73 | /* Retrieve the language ID from the config */ 74 | public function getLanguageId(){ 75 | $this->load->model('localisation/language'); 76 | $languages = $this->model_localisation_language->getLanguages(); 77 | $language_code = $this->config->get('config_language'); 78 | $language_id = null; 79 | 80 | foreach($languages as $language){ 81 | if($language['code'] == $language_code){ 82 | $language_id = $language['language_id']; 83 | } 84 | } 85 | 86 | if($language_id != null){ 87 | return (int) $language_id; 88 | }else{ 89 | $this->code = 400; 90 | $this->result = "Failed to retrieve the langauge ID."; 91 | $this->sendResponse(); 92 | } 93 | } 94 | 95 | /* Render the API response */ 96 | public function sendResponse($no_code = null, $custom_fields = null){ 97 | http_response_code((int) $this->code); 98 | 99 | $this->response->addHeader('Content-Type: application/json; charset=utf-8'); 100 | 101 | $response_array = array( 102 | "code" => (int) $this->code, 103 | "result" => $this->result 104 | ); 105 | 106 | if($no_code){ 107 | $response_array = $this->result; 108 | } 109 | 110 | if($custom_fields){ 111 | foreach($custom_fields as $custom_field_key => $custom_field_value){ 112 | $response_array[$custom_field_key] = $custom_field_value; 113 | } 114 | } 115 | 116 | $this->response->setOutput(json_encode($response_array)); 117 | 118 | $this->response->output(); 119 | 120 | /* DO NOT REMOVE - This halts the rest of the code from being executed. Necessary when used to terminate the stack due to an error. */ 121 | die(); 122 | } 123 | 124 | /* Get the POST JSON payload */ 125 | public function getPost(){ 126 | $inputJSON = file_get_contents('php://input'); 127 | 128 | $post = json_decode($inputJSON, TRUE); 129 | 130 | if ((!is_array($post) || empty($post)) && $_SERVER['REQUEST_METHOD'] === 'POST') { 131 | $this->code = 400; 132 | $this->result = "Input JSON is invalid. Request body must be valid JSON if content type is application/json."; 133 | return $this->sendResponse(); 134 | } 135 | 136 | return $post; 137 | } 138 | 139 | /* Retrieve a GET parameter */ 140 | public function getParam($parameter, $optional = null){ 141 | if(isset($_GET[$parameter])) { 142 | return $_GET[$parameter]; 143 | }elseif($optional){ 144 | return false; 145 | }else{ 146 | $this->code = 400; 147 | $this->result = "Invalid request. Missing ".$parameter." GET parameter."; 148 | $this->sendResponse(); 149 | } 150 | } 151 | 152 | /* Check if the input index exists and matches with a given value */ 153 | /* We use this to avoid multiple if statements for undefined arrays and indexes */ 154 | public function checkIndexValue($input, $index, $value, $wrapper_function = null){ 155 | if(isset($input[$index])){ 156 | 157 | /* simple value match */ 158 | if($input[$index] == $value){ 159 | return true; 160 | } 161 | 162 | /* wrap a function around the index */ 163 | if(is_callable($wrapper_function) && ($wrapper_function($input[$index]) == $value)){ 164 | return true; 165 | } 166 | } 167 | 168 | return false; 169 | } 170 | 171 | /* Converts the default zero-filled date to null to avoid an ElasticSearch error */ 172 | public function sanitizeDateTime($date_time) { 173 | if ($date_time == '0000-00-00 00:00:00') { 174 | return null; 175 | } 176 | return $date_time; 177 | } 178 | 179 | /* Check if the input index exists and whether or not it's empty */ 180 | public function checkInput($input, $index, $required = false, $empty_allowed = true, $value_if_empty = null){ 181 | if(isset($input[$index])){ 182 | if(empty($input[$index])){ 183 | if($empty_allowed == false){ 184 | $this->load->language('vsbridge/api'); 185 | $this->code = 500; 186 | $this->result = $this->language->get('error_empty_input').$index; 187 | $this->sendResponse(); 188 | }else{ 189 | return $value_if_empty; 190 | } 191 | }else{ 192 | return $input[$index]; 193 | } 194 | }else{ 195 | if($required == true){ 196 | $this->load->language('vsbridge/api'); 197 | $this->code = 500; 198 | $this->result = $this->language->get('error_missing_input').$index; 199 | $this->sendResponse(); 200 | }else{ 201 | if($value_if_empty){ 202 | return $value_if_empty; 203 | }else{ 204 | return false; 205 | } 206 | } 207 | } 208 | } 209 | 210 | /* Get the client's IP address */ 211 | public function getClientIp() { 212 | if (isset($_SERVER['HTTP_CLIENT_IP'])) 213 | $ipaddress = $_SERVER['HTTP_CLIENT_IP']; 214 | else if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) 215 | $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR']; 216 | else if(isset($_SERVER['HTTP_X_FORWARDED'])) 217 | $ipaddress = $_SERVER['HTTP_X_FORWARDED']; 218 | else if(isset($_SERVER['HTTP_FORWARDED_FOR'])) 219 | $ipaddress = $_SERVER['HTTP_FORWARDED_FOR']; 220 | else if(isset($_SERVER['HTTP_FORWARDED'])) 221 | $ipaddress = $_SERVER['HTTP_FORWARDED']; 222 | else if(isset($_SERVER['REMOTE_ADDR'])) 223 | $ipaddress = $_SERVER['REMOTE_ADDR']; 224 | else 225 | $ipaddress = 'UNKNOWN'; 226 | return $ipaddress; 227 | } 228 | 229 | /* Generate a JWT token */ 230 | /* A secret key must be configured in the module settings for this to work! */ 231 | protected function getToken($oc_token, $refresh_token = null) { 232 | $this->load->model('setting/setting'); 233 | 234 | if(!empty($this->config->get('vsbridge_secret_key'))){ 235 | 236 | try { 237 | $secret_key = $this->config->get('vsbridge_secret_key'); 238 | // Normal JWT tokens expire every 60 minutes 239 | // Note: Currently vue-storefront only uses JWT tokens for the importer access tokens and the customer refresh tokens 240 | // Normal customer tokens are non-JWT and generated by OpenCart and linked to a session_id 241 | $expiration = time() + 3600; 242 | 243 | if($refresh_token){ 244 | // Refresh tokens are set to expire every 30 days, after which the user has to login again 245 | $expiration = time() + 2592000; 246 | } 247 | 248 | $issuer = HTTPS_SERVER; 249 | $token = Token::create($oc_token, $secret_key, $expiration, $issuer); 250 | 251 | return $token; 252 | 253 | } catch (Exception $e) { 254 | $this->code = 401; 255 | if($refresh_token){ 256 | $this->code = 500; 257 | } 258 | $this->result = $e->getMessage(); 259 | $this->sendResponse(); 260 | } 261 | 262 | }else{ 263 | $this->code = 500; 264 | $this->result = "Module not configured. Please provide a secret key."; 265 | $this->sendResponse(); 266 | } 267 | } 268 | 269 | /* Validate the JWT token and, if successful, load the session */ 270 | protected function validateToken($token, $customer_auth = NULL){ 271 | if(!empty($this->config->get('vsbridge_secret_key'))){ 272 | 273 | $secret_key = $this->config->get('vsbridge_secret_key'); 274 | 275 | if(Token::validate($token, $secret_key)) { 276 | $payload = Token::getPayload($token, $secret_key); 277 | $oc_token = $payload['user_id']; 278 | 279 | if($customer_auth){ 280 | return $oc_token; 281 | }else{ 282 | /* Validate the OC API session & verify client IP address */ 283 | $this->load->model('vsbridge/api'); 284 | $api_session = $this->model_vsbridge_api->getApiSession($oc_token); 285 | 286 | if($this->checkIndexValue($api_session, 'ip', $this->getClientIp(), 'trim')){ 287 | 288 | /* sendResponse will automatically switch over to the default session */ 289 | return $this->session->start('api', $api_session['session_id']); 290 | 291 | }else{ 292 | $this->code = 401; 293 | if($customer_auth){ 294 | $this->code = 500; 295 | } 296 | $this->result = "Authentication failed. Your IP address is not authorized to use this token."; 297 | $this->sendResponse(); 298 | } 299 | } 300 | } 301 | 302 | }else{ 303 | $this->code = 500; 304 | $this->result = "Module not configured. Please provide a secret key."; 305 | $this->sendResponse(); 306 | } 307 | 308 | $this->code = 401; 309 | if($customer_auth){ 310 | $this->code = 500; 311 | } 312 | $this->result = "Authentication failed. Invalid token."; 313 | $this->sendResponse(); 314 | } 315 | 316 | /* Retrieve or generate customer/guest session ID and save it in the database (because Vue Storefront does not use cookies/sessions) */ 317 | /* If the customer is not logged in, check for a session ID to use in order to retain cart content */ 318 | protected function getSessionId($customer_id = null){ 319 | $session_id = $this->getParam('session_id', true); 320 | if(empty($session_id)){ 321 | $session_id = md5(uniqid(rand(), true)); 322 | $session_id = substr_replace($session_id, $this->session_id_prefix,0, strlen($this->session_id_prefix)); 323 | } 324 | if(!empty($customer_id)){ 325 | $this->load->model('vsbridge/api'); 326 | $customer_session_id = $this->model_vsbridge_api->getCustomerSessionId($customer_id, $this->store_id); 327 | 328 | if(!empty($customer_session_id['session_id'])){ 329 | return $customer_session_id['session_id']; 330 | }else{ 331 | $this->model_vsbridge_api->SetCustomerSessionId($customer_id, $this->store_id, $session_id); 332 | return $session_id; 333 | } 334 | }else{ 335 | return $session_id; 336 | } 337 | } 338 | 339 | /* Validate customer token */ 340 | protected function validateCustomerToken($token){ 341 | $this->load->model('vsbridge/api'); 342 | $this->load->language('vsbridge/api'); 343 | $token_info = $this->model_vsbridge_api->getCustomerToken($token); 344 | 345 | if(!empty($token_info['timestamp']) && !empty($token_info['customer_id'])){ 346 | 347 | $token_age = time() - ((int) $token_info['timestamp']); 348 | 349 | /* Set this to change token lifetime in seconds */ 350 | $token_lifetime = 3600; // Default: 1 hour 351 | 352 | if($token_age <= $token_lifetime){ 353 | 354 | $this->load->model('account/customer'); 355 | $customer_info = $this->model_account_customer->getCustomer($token_info['customer_id']); 356 | 357 | return $customer_info; 358 | 359 | }else{ 360 | $this->code = 401; 361 | $this->result = array( 362 | "code" => $this->code, 363 | "error" => $this->language->get('error_token_expired') 364 | ); 365 | $this->sendResponse(); 366 | } 367 | 368 | }else{ 369 | $this->code = 401; 370 | $this->result = array( 371 | "code" => $this->code, 372 | "error" => $this->language->get('error_invalid_token') 373 | ); 374 | $this->sendResponse(); 375 | } 376 | } 377 | 378 | protected function validateCartId($cart_id, $token = null){ 379 | if(substr($cart_id, 0, 3 ) == $this->session_id_prefix){ 380 | if(!empty($token)) { 381 | if ($customer_info = $this->validateCustomerToken($token)) { 382 | $this->loadSession($this->getSessionId($customer_info['customer_id'])); 383 | return $customer_info; 384 | } 385 | }else{ 386 | $this->loadSession($cart_id); 387 | return true; 388 | } 389 | }else{ 390 | $this->load->language('vsbridge/api'); 391 | $this->code = 500; 392 | $this->result = $this->language->get('error_invalid_cart_id'); 393 | $this->sendResponse(); 394 | } 395 | } 396 | 397 | protected function loadSession($session_id){ 398 | session_abort(); 399 | session_id($session_id); 400 | session_start(); 401 | $this->session->start('default', $session_id); 402 | 403 | /* Reload the built-in classes to load user session */ 404 | $this->session->data['language'] = $this->config->get('config_language'); 405 | $this->session->data['currency'] = $this->config->get('config_currency'); 406 | 407 | $language = new Language($this->config->get('config_language')); 408 | $language->load($this->config->get('config_language')); 409 | 410 | if ($this->customer->isLogged()) { 411 | $this->config->set('config_customer_group_id', $this->customer->getGroupId()); 412 | }elseif (isset($this->session->data['guest']) && isset($this->session->data['guest']['customer_group_id'])) { 413 | $this->config->set('config_customer_group_id', $this->session->data['guest']['customer_group_id']); 414 | } 415 | 416 | $this->registry->set('language', $language); 417 | $this->registry->set('currency', new Cart\Currency($this->registry)); 418 | $this->registry->set('customer', new Cart\Customer($this->registry)); 419 | $this->registry->set('cart', new Cart\Cart($this->registry)); 420 | $this->registry->set('tax', new Cart\Tax($this->registry)); 421 | 422 | if (isset($this->session->data['shipping_address'])) { 423 | $this->tax->setShippingAddress($this->session->data['shipping_address']['country_id'], $this->session->data['shipping_address']['zone_id']); 424 | } elseif ($this->config->get('config_tax_default') == 'shipping') { 425 | $this->tax->setShippingAddress($this->config->get('config_country_id'), $this->config->get('config_zone_id')); 426 | } 427 | 428 | if (isset($this->session->data['payment_address'])) { 429 | $this->tax->setPaymentAddress($this->session->data['payment_address']['country_id'], $this->session->data['payment_address']['zone_id']); 430 | } elseif ($this->config->get('config_tax_default') == 'payment') { 431 | $this->tax->setPaymentAddress($this->config->get('config_country_id'), $this->config->get('config_zone_id')); 432 | } 433 | 434 | $this->tax->setStoreAddress($this->config->get('config_country_id'), $this->config->get('config_zone_id')); 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /tests/test.php: -------------------------------------------------------------------------------- 1 | '!', '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')'); 60 | return strtr(rawurlencode($str), $revert); 61 | } 62 | 63 | /* Helper function for calling the API */ 64 | function request($method, $route, $getparams = null, $postparams = null, $dontfail = null){ 65 | $client = new GuzzleHttp\Client([ 66 | 'verify' => false, /* don't verify SSL certificates for dev */ 67 | 'headers' => [ 68 | 'content-type' => 'application/json', 69 | 'accept' => '*/*' 70 | ] 71 | ]); 72 | 73 | if(!empty($getparams)){ 74 | $getparams = '?'.http_build_query($getparams); 75 | } 76 | 77 | try{ 78 | $res = $client->request($method, $this->vsbridge_api_base_url.$route.$getparams, ['body' => json_encode($postparams)]); 79 | if(!empty(trim($res->getBody()))){ 80 | if($this->isJson($res->getBody())){ 81 | return json_decode($res->getBody(), true); 82 | }else{ 83 | return $res->getBody(); 84 | } 85 | }else{ 86 | if(!$dontfail){ 87 | $this->fail('Empty response!'); 88 | } 89 | } 90 | }catch (Exception $e) { 91 | if(!$dontfail){ 92 | $this->fail($e->getMessage()); 93 | } 94 | } 95 | 96 | } 97 | 98 | /* Helper function for validating JWT tokens */ 99 | function validateToken($token){ 100 | if(Token::validate($token, $this->vsbridge_module_secret_key)){ 101 | return true; 102 | }else{ 103 | $this->fail('Failed to validate the JWT token.'); 104 | } 105 | } 106 | 107 | /* POST /vsbridge/auth/admin */ 108 | function auth_admin(){ 109 | /* Generate a JWT token */ 110 | $response = $this->request('POST', 'auth/admin', null, array( 111 | 'username' => $this->oc_api_name, 112 | 'password' => $this->oc_api_key 113 | )); 114 | 115 | /* Validate the JWT token against the secret key */ 116 | if($this->validateToken($response['result'])){ 117 | return $response['result']; 118 | } 119 | } 120 | 121 | /* GET /vsbridge/attributes/index */ 122 | function attributes_index($token){ 123 | $response = $this->request('GET', 'attributes/index', array( 124 | 'apikey' => $token, 125 | 'store_id' => $this->oc_store_id, 126 | 'language_id' => $this->oc_language_id 127 | )); 128 | 129 | return $response; 130 | } 131 | 132 | /* GET /vsbridge/categories/index */ 133 | function categories_index($token){ 134 | $response = $this->request('GET', 'categories/index', array( 135 | 'apikey' => $token, 136 | 'store_id' => $this->oc_store_id, 137 | 'language_id' => $this->oc_language_id 138 | )); 139 | 140 | return $response; 141 | } 142 | 143 | /* GET /vsbridge/taxrules/index */ 144 | function taxrules_index($token){ 145 | $response = $this->request('GET', 'taxrules/index', array( 146 | 'apikey' => $token 147 | )); 148 | 149 | return $response; 150 | } 151 | 152 | /* GET /vsbridge/products/index */ 153 | function products_index($token){ 154 | $response = $this->request('GET', 'products/index', array( 155 | 'apikey' => $token, 156 | 'store_id' => $this->oc_store_id, 157 | 'language_id' => $this->oc_language_id, 158 | 'pageSize' => 25, 159 | 'page' => false 160 | )); 161 | 162 | return $response; 163 | } 164 | 165 | /* POST /vsbridge/user/create */ 166 | public function user_create($input, $dontfail = null){ 167 | $response = $this->request('POST', 'user/create', null, $input, $dontfail); 168 | 169 | return $response; 170 | } 171 | 172 | /* POST /vsbridge/user/login */ 173 | public function user_login($input){ 174 | $response = $this->request('POST', 'user/login', null, $input); 175 | 176 | return $response; 177 | } 178 | 179 | /* POST /vsbridge/user/refresh */ 180 | public function user_refresh($input){ 181 | $response = $this->request('POST', 'user/refresh', null, $input); 182 | 183 | return $response; 184 | } 185 | 186 | /* POST /vsbridge/user/resetPassword */ 187 | public function user_resetPassword($input){ 188 | $response = $this->request('POST', 'user/resetPassword', null, $input); 189 | 190 | return $response; 191 | } 192 | 193 | /* POST /vsbridge/user/changePassword */ 194 | public function user_changePassword($token, $input){ 195 | $response = $this->request('POST', 'user/changePassword', array('token' => $token), $input); 196 | 197 | return $response; 198 | } 199 | 200 | /* GET /vsbridge/user/order-history */ 201 | public function user_orderHistory($token){ 202 | $response = $this->request('GET', 'user/order-history', array('token' => $token)); 203 | 204 | return $response; 205 | } 206 | 207 | /* GET /vsbridge/user/me */ 208 | public function user_me_get($token){ 209 | $response = $this->request('GET', 'user/me', array('token' => $token)); 210 | 211 | return $response; 212 | } 213 | 214 | /* POST /vsbridge/user/me */ 215 | public function user_me_post($token, $input){ 216 | $response = $this->request('POST', 'user/me', array('token' => $token), $input); 217 | 218 | return $response; 219 | } 220 | 221 | /* POST /vsbridge/cart/create */ 222 | /* For authenticated customers */ 223 | public function cart_create($token){ 224 | $response = $this->request('POST', 'cart/create', array('token' => $token), null); 225 | 226 | return $response; 227 | } 228 | 229 | /* POST /vsbridge/cart/create */ 230 | /* For guests */ 231 | public function cart_create_guest(){ 232 | $response = $this->request('POST', 'cart/create', null, null); 233 | 234 | return $response; 235 | } 236 | 237 | /* GET /vsbridge/cart/pull */ 238 | public function cart_pull($cart_id, $token = null){ 239 | $get_params = array('cartId' => $cart_id); 240 | 241 | if($token){ 242 | $get_params['token'] = $token; 243 | } 244 | 245 | $response = $this->request('POST', 'cart/pull', $get_params, null); 246 | 247 | return $response; 248 | } 249 | 250 | /* POST /vsbridge/cart/update */ 251 | public function cart_update($cart_id, $input, $token = null){ 252 | $get_params = array('cartId' => $cart_id); 253 | 254 | if($token){ 255 | $get_params['token'] = $token; 256 | } 257 | 258 | $response = $this->request('POST', 'cart/update', $get_params, $input); 259 | 260 | return $response; 261 | } 262 | 263 | /* POST /vsbridge/cart/delete */ 264 | public function cart_delete($cart_id, $input, $token = null){ 265 | $get_params = array('cartId' => $cart_id); 266 | 267 | if($token){ 268 | $get_params['token'] = $token; 269 | } 270 | 271 | $response = $this->request('POST', 'cart/delete', $get_params, $input); 272 | 273 | return $response; 274 | } 275 | 276 | /* POST /vsbridge/cart/apply-coupon */ 277 | public function cart_apply_coupon($cart_id, $coupon, $token = null){ 278 | $get_params = array('cartId' => $cart_id, 'coupon' => $coupon); 279 | 280 | if($token){ 281 | $get_params['token'] = $token; 282 | } 283 | 284 | $response = $this->request('POST', 'cart/apply-coupon', $get_params); 285 | 286 | return $response; 287 | } 288 | 289 | /* POST /vsbridge/cart/delete-coupon */ 290 | public function cart_delete_coupon($cart_id, $token = null){ 291 | $get_params = array('cartId' => $cart_id); 292 | 293 | if($token){ 294 | $get_params['token'] = $token; 295 | } 296 | 297 | $response = $this->request('POST', 'cart/delete-coupon', $get_params); 298 | 299 | return $response; 300 | } 301 | 302 | /* GET /vsbridge/cart/coupon */ 303 | public function cart_coupon($cart_id, $token = null){ 304 | $get_params = array('cartId' => $cart_id); 305 | 306 | if($token){ 307 | $get_params['token'] = $token; 308 | } 309 | 310 | $response = $this->request('GET', 'cart/coupon', $get_params); 311 | 312 | return $response; 313 | } 314 | 315 | /* GET /vsbridge/cart/totals */ 316 | public function cart_totals($cart_id, $token = null){ 317 | $get_params = array('cartId' => $cart_id); 318 | 319 | if($token){ 320 | $get_params['token'] = $token; 321 | } 322 | 323 | $response = $this->request('GET', 'cart/totals', $get_params); 324 | 325 | return $response; 326 | } 327 | 328 | /* GET /vsbridge/cart/payment-methods */ 329 | public function payment_methods($cart_id, $token = null){ 330 | $get_params = array('cartId' => $cart_id); 331 | 332 | if($token){ 333 | $get_params['token'] = $token; 334 | } 335 | 336 | $response = $this->request('GET', 'cart/payment-methods', $get_params); 337 | 338 | return $response; 339 | } 340 | 341 | /* POST /vsbridge/cart/shipping-methods */ 342 | public function shipping_methods($cart_id, $input, $token = null){ 343 | $get_params = array('cartId' => $cart_id); 344 | 345 | if($token){ 346 | $get_params['token'] = $token; 347 | } 348 | 349 | $response = $this->request('POST', 'cart/shipping-methods', $get_params, $input); 350 | 351 | return $response; 352 | } 353 | 354 | /* POST /vsbridge/cart/shipping-information */ 355 | public function shipping_information($cart_id, $input, $token = null){ 356 | $get_params = array('cartId' => $cart_id); 357 | 358 | if($token){ 359 | $get_params['token'] = $token; 360 | } 361 | 362 | $response = $this->request('POST', 'cart/shipping-information', $get_params, $input); 363 | 364 | return $response; 365 | } 366 | 367 | /* POST /vsbridge/cart/collect-totals */ 368 | public function collect_totals($cart_id, $input, $token = null){ 369 | $get_params = array('cartId' => $cart_id); 370 | 371 | if($token){ 372 | $get_params['token'] = $token; 373 | } 374 | 375 | $response = $this->request('POST', 'cart/collect-totals', $get_params, $input); 376 | 377 | return $response; 378 | } 379 | 380 | /* GET /vsbridge/stock/check */ 381 | public function stock_check($sku){ 382 | $response = $this->request('GET', 'stock/check', array('sku' => $sku)); 383 | 384 | return $response; 385 | } 386 | 387 | /* GET /vsbridge/stock/list */ 388 | public function stock_list($skus){ 389 | $response = $this->request('GET', 'stock/list', array('skus' => $skus)); 390 | 391 | return $response; 392 | } 393 | 394 | /* POST /vsbridge/order/create */ 395 | public function order_create($input){ 396 | $response = $this->request('POST', 'order/create', null, $input); 397 | 398 | return $response; 399 | } 400 | 401 | } 402 | 403 | /* ----------------- */ 404 | /* Run all the tests */ 405 | /* ----------------- */ 406 | 407 | $vsbridge = new vsbridge_tests(); 408 | 409 | $admin_token = $vsbridge->auth_admin(); 410 | 411 | $vsbridge->attributes_index($admin_token); 412 | 413 | $vsbridge->categories_index($admin_token); 414 | 415 | $vsbridge->taxrules_index($admin_token); 416 | 417 | $vsbridge->products_index($admin_token); 418 | 419 | // The rest of the tests are already written, but I need to rewrite them to apply for OC Vanilla database 420 | 421 | /* If the script reaches here, all tests have been accomplished! */ 422 | echo "Congratulations! All tests completed successfully." . PHP_EOL; --------------------------------------------------------------------------------