├── .gitignore ├── Dynamic API specification.md ├── Format-attribute.md ├── Format-category.md ├── Format-product.md ├── How to configure Vue Storefront.md ├── LICENSE ├── Prices how-to.md ├── README.md ├── Vue Storefront Integration Architecture.pdf ├── Vue Storefront Integration Architecture.pptx ├── sample-api-js ├── LICENSE ├── Procfile ├── README.md ├── babel.config.js ├── config │ ├── .gitignore │ └── default.json ├── nodemon.json ├── package.json ├── src │ ├── api │ │ ├── cart.js │ │ ├── catalog.js │ │ ├── img.js │ │ ├── index.js │ │ ├── order.js │ │ ├── stock.js │ │ └── user.js │ ├── db.js │ ├── index.ts │ ├── lib │ │ ├── image.js │ │ └── util.js │ └── middleware │ │ └── index.ts ├── tsconfig.json └── yarn.lock ├── sample-data ├── attributes.json ├── categories.json ├── fetch_demo_attributes.sh ├── fetch_demo_categories.sh ├── fetch_demo_products.sh ├── import.js ├── package.json └── products.json └── screens └── screen_0_products.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | .idea/ 63 | -------------------------------------------------------------------------------- /Dynamic API specification.md: -------------------------------------------------------------------------------- 1 | # API docs for Vue Storefront 2 | 3 | To integrate the [vue-storefront](https://github.com/DivanteLtd/vue-storefront) with third party platform You should start with building the API around the platform. It should be compatible with the following specification to let Vue Storefront app seamlesly use it and process the data. 4 | 5 | **Note:** Please check the [sample API implementation](sample-api-js). It's compliant with this API specification. 6 | 7 | Here you can find some example implementations of this API for different platforms: 8 | - [`vue-storefront-api`](https://github.com/DivanteLtd/vue-storefront-api/) - NodeJS, this is our default API integrated with magento2 9 | - [`magento1-vsbridge`](https://github.com/DivanteLtd/magento1-vsbridge/tree/master/magento1-module/app/code/local/Divante/VueStorefrontBridge/controllers) - API implementation done as a native Magento1 module 10 | - [`coreshop-bridge`](https://github.com/DivanteLtd/coreshop-vsbridge/tree/master/src/CoreShop2VueStorefrontBundle/Controller) - Symfony based implementation for Coreshop, 11 | - [`spree2vuestorefront`](https://github.com/spark-solutions/spree2vuestorefront) - Ruby implementation for the Spree Commerce platform. 12 | 13 | ## Dynamic requests API specification 14 | 15 | All methods accept and respond with `application/json` content type. 16 | 17 | ## Cart module 18 | 19 | Cart module is in charge of creating the eCommerce backend shopping carts and synchronizing the items users have in Vue Storefront and eCommerce backend. For example it can synchronize Vue Storefront shopping cart with the Magento quotes. 20 | 21 | ### POST /api/cart/create 22 | 23 | #### WHEN: 24 | 25 | This method is called when new Vue Storefront shopping cart is created. First visit, page refresh, after user-authorization ... If the `token` GET parameter is provided it's called as logged-in user; if not - it's called as guest-user. To draw the difference - let's keep to Magento example. For guest user vue-storefront-api is subsequently operating on `/guest-carts` API endpoints and for authorized users on `/carts/` endpoints) 26 | 27 | #### GET PARAMS: 28 | `token` - null OR user token obtained from [`/api/user/login`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/user.js#L48) 29 | 30 | #### EXAMPLE CALL: 31 | 32 | ```bash 33 | curl 'https://your-domain.example.com/api/cart/create' -X POST 34 | ``` 35 | 36 | For authorized user: 37 | 38 | ```bash 39 | curl 'https://your-domain.example.com/api/cart/create?token=xu8h02nd66yq0gaayj4x3kpqwity02or' -X POST 40 | ``` 41 | 42 | 43 | #### RESPONSE BODY: 44 | 45 | For guest user 46 | 47 | ``` 48 | { 49 | "code": 200, 50 | "result": "a17b9b5fb9f56652b8280bb94c52cd93" 51 | } 52 | ``` 53 | 54 | The `result` is a guest-cart id that should be used for all subsequent cart related operations as `?cartId=a17b9b5fb9f56652b8280bb94c52cd93` 55 | 56 | For authorized user 57 | ``` 58 | { 59 | "code":200, 60 | "result":"81668" 61 | } 62 | ``` 63 | The `result` is a cart-id that should be used for all subsequent cart related operations as `?cartId=81668` 64 | 65 | #### RESPONSE CODES: 66 | 67 | - `200` when success 68 | - `500` in case of error 69 | 70 | 71 | ### GET /api/cart/pull 72 | 73 | Method used to fetch the current server side shopping cart content, used mostly for synchronization purposes when `config.cart.synchronize=true` 74 | 75 | #### WHEN: 76 | 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. 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. 77 | 78 | #### GET PARAMS: 79 | `token` - null OR user token obtained from [`/api/user/login`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/user.js#L48) 80 | `cartId` - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from [`api/cart/create`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/cart.js#L26) 81 | 82 | 83 | #### RESPONSE BODY: 84 | ```json 85 | { 86 | "code": 200, 87 | "result": [ 88 | { 89 | "item_id": 66257, 90 | "sku": "WS08-M-Black", 91 | "qty": 1, 92 | "name": "Minerva LumaTech™ V-Tee", 93 | "price": 32, 94 | "product_type": "configurable", 95 | "quote_id": "dceac8e2172a1ff0cfba24d757653257", 96 | "product_option": { 97 | "extension_attributes": { 98 | "configurable_item_options": [ 99 | { 100 | "option_id": "93", 101 | "option_value": 49 102 | }, 103 | { 104 | "option_id": "142", 105 | "option_value": 169 106 | } 107 | ] 108 | } 109 | } 110 | }, 111 | { 112 | "item_id": 66266, 113 | "sku": "WS08-XS-Red", 114 | "qty": 1, 115 | "name": "Minerva LumaTech™ V-Tee", 116 | "price": 32, 117 | "product_type": "configurable", 118 | "quote_id": "dceac8e2172a1ff0cfba24d757653257", 119 | "product_option": { 120 | "extension_attributes": { 121 | "configurable_item_options": [ 122 | { 123 | "option_id": "93", 124 | "option_value": 58 125 | }, 126 | { 127 | "option_id": "142", 128 | "option_value": 167 129 | } 130 | ] 131 | } 132 | } 133 | } 134 | ] 135 | } 136 | 137 | ``` 138 | 139 | 140 | ### POST /api/cart/update 141 | 142 | Method used to add or update shopping cart item's server side. As a request body there should be JSON given representing the cart item. `sku` and `qty` are the two required options. If you like to update/edit server cart item You need to pass `item_id` identifier as well (can be obtainted from `api/cart/pull`) 143 | 144 | #### WHEN: 145 | This method is called just after `api/cart/pull` as a consequence of the synchronization process 146 | 147 | #### GET PARAMS: 148 | `token` - null OR user token obtained from [`/api/user/login`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/user.js#L48) 149 | `cartId` - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from [`api/cart/create`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/cart.js#L26) 150 | 151 | #### REQUEST BODY: 152 | 153 | ```json 154 | { 155 | "cartItem":{ 156 | "sku":"WS12-XS-Orange", 157 | "qty":1, 158 | "product_option":{ 159 | "extension_attributes":{ 160 | "custom_options":[ 161 | 162 | ], 163 | "configurable_item_options":[ 164 | { 165 | "option_id":"93", 166 | "option_value":"56" 167 | }, 168 | { 169 | "option_id":"142", 170 | "option_value":"167" 171 | } 172 | ], 173 | "bundle_options":[ 174 | 175 | ] 176 | } 177 | }, 178 | "quoteId":"0a8109552020cc80c99c54ad13ef5d5a" 179 | } 180 | } 181 | ``` 182 | 183 | #### EXAMPLE CALL: 184 | 185 | ```bash 186 | curl 'https://your-domain.example.com/api/cart/update?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' -H 'accept: */*' --data-binary '{"cartItem":{"sku":"MS10-XS-Black","item_id":5853,"quoteId":"81668"}}' --compressed 187 | ``` 188 | 189 | #### RESPONSE BODY: 190 | 191 | ```json 192 | { 193 | "code":200, 194 | "result": 195 | { 196 | "item_id":5853, 197 | "sku":"MS10-XS-Black", 198 | "qty":2, 199 | "name":"Logan HeatTec® Tee-XS-Black", 200 | "price":24, 201 | "product_type":"simple", 202 | "quote_id":"81668" 203 | } 204 | } 205 | ``` 206 | 207 | ### POST /api/cart/delete 208 | 209 | This method is used to remove the shopping cart item on server side. 210 | 211 | #### WHEN: 212 | This method is called just after `api/cart/pull` as a consequence of the synchronization process 213 | 214 | #### GET PARAMS: 215 | `token` - null OR user token obtained from [`/api/user/login`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/user.js#L48) 216 | `cartId` - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from [`api/cart/create`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/cart.js#L26) 217 | 218 | #### EXAMPLE CALL: 219 | 220 | ```bash 221 | curl 'https://your-domain.example.com/api/cart/delete?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' -H 'accept: */*' --data-binary '{"cartItem":{"sku":"MS10-XS-Black","item_id":5853,"quoteId":"81668"}}' --compressed 222 | ``` 223 | 224 | #### REQUEST BODY: 225 | 226 | ```json 227 | { 228 | "cartItem": 229 | { 230 | "sku":"MS10-XS-Black", 231 | "item_id":5853, 232 | "quoteId":"81668" 233 | } 234 | } 235 | ``` 236 | 237 | #### RESPONSE BODY: 238 | 239 | ```json 240 | { 241 | "code":200, 242 | "result":true 243 | } 244 | ``` 245 | 246 | ### POST /api/cart/apply-coupon 247 | 248 | This method is used to apply the discount code to the current server side quote. 249 | 250 | #### EXAMPLE CALL: 251 | 252 | ```bash 253 | curl 'https://your-domain.example.com/api/cart/apply-coupon?token=2q1w9oixh3bukxyj947tiordnehai4td&cartId=5effb906a97ebecd6ae96e3958d04edc&coupon=ARMANI' -X POST -H 'content-type: application/json' -H 'accept: */*' 254 | ``` 255 | 256 | #### RESPONSE BODY: 257 | 258 | ```json 259 | { 260 | "code":200, 261 | "result":true 262 | } 263 | ``` 264 | 265 | 266 | ### POST /api/cart/delete-coupon 267 | 268 | This method is used to delete the discount code to the current server side quote. 269 | 270 | #### EXAMPLE CALL: 271 | 272 | ```bash 273 | curl 'https://your-domain.example.com/api/cart/delete-coupon?token=2q1w9oixh3bukxyj947tiordnehai4td&cartId=5effb906a97ebecd6ae96e3958d04edc' -X POST -H 'content-type: application/json' -H 'accept: */*' 274 | ``` 275 | 276 | #### RESPONSE BODY: 277 | 278 | ```json 279 | { 280 | "code":200, 281 | "result":true 282 | } 283 | ``` 284 | 285 | ### GET /api/cart/coupon 286 | 287 | This method is used to get the currently applied coupon code 288 | 289 | #### EXAMPLE CALL: 290 | 291 | ```bash 292 | curl 'https://your-domain.example.com/api/cart/coupon?token=2q1w9oixh3bukxyj947tiordnehai4td&cartId=5effb906a97ebecd6ae96e3958d04edc' -H 'content-type: application/json' -H 'accept: */*' 293 | ``` 294 | 295 | #### RESPONSE BODY: 296 | 297 | ```json 298 | { 299 | "code":200, 300 | "result":"ARMANI" 301 | } 302 | ``` 303 | 304 | ### GET /api/cart/totals 305 | 306 | Method called when the `config.synchronize_totals=true` just after any shopping cart modification. It's used to synchronize the Magento / other CMS totals after all promotion rules processed with current Vue Storefront state. 307 | 308 | **Note**: Make sure to check notes on [Cart prices](https://github.com/DivanteLtd/storefront-integration-sdk/blob/master/Prices%20how-to.md#cart-prices) 309 | 310 | #### EXAMPLE CALL: 311 | 312 | ```bash 313 | curl 'https://your-domain.example.com/api/cart/totals?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' -H 'accept: */*' 314 | ``` 315 | 316 | #### GET PARAMS: 317 | `token` - null OR user token obtained from [`/api/user/login`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/user.js#L48) 318 | `cartId` - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from [`api/cart/create`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/cart.js#L26) 319 | 320 | #### RESPONSE BODY: 321 | 322 | You have totals data for the current, synchronized quote returned: 323 | 324 | ```json 325 | { 326 | "code":200, 327 | "result": 328 | { 329 | "grand_total":0, 330 | "base_currency_code":"USD", 331 | "quote_currency_code":"USD", 332 | "items_qty":1, 333 | "items": 334 | [ 335 | { 336 | "item_id":5853, 337 | "price":0, 338 | "qty":1, 339 | "row_total":0, 340 | "row_total_with_discount":0, 341 | "tax_amount":0, 342 | "tax_percent":0, 343 | "discount_amount":0, 344 | "base_discount_amount":0, 345 | "discount_percent":0, 346 | "name":"Logan HeatTec® Tee-XS-Black", 347 | "options": "[{ \"label\": \"Color\", \"value\": \"red\" }, { \"label\": \"Size\", \"value\": \"XL\" }]", 348 | "product_option":{ 349 | "extension_attributes":{ 350 | "custom_options":[ 351 | 352 | ], 353 | "configurable_item_options":[ 354 | { 355 | "option_id":"93", 356 | "option_value":"56" 357 | }, 358 | { 359 | "option_id":"142", 360 | "option_value":"167" 361 | } 362 | ], 363 | "bundle_options":[ 364 | 365 | ] 366 | } 367 | } 368 | } 369 | ], 370 | "total_segments": 371 | [ 372 | { 373 | "code":"subtotal", 374 | "title":"Subtotal", 375 | "value":0 376 | }, 377 | { 378 | "code":"shipping", 379 | "title":"Shipping & Handling", 380 | "value":null 381 | }, 382 | { 383 | "code":"tax", 384 | "title":"Tax", 385 | "value":0, 386 | "extension_attributes": 387 | { 388 | "tax_grandtotal_details":[] 389 | } 390 | }, 391 | { 392 | "code":"grand_total", 393 | "title":"Grand Total", 394 | "value":null, 395 | "area":"footer" 396 | } 397 | ] 398 | } 399 | } 400 | ``` 401 | 402 | ### GET /api/cart/payment-methods 403 | 404 | 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 405 | 406 | #### EXAMPLE CALL: 407 | 408 | ```bash 409 | curl 'https://your-domain.example.com/api/cart/payment-methods?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' -H 'accept: */*' 410 | ``` 411 | 412 | #### GET PARAMS: 413 | `token` - null OR user token obtained from [`/api/user/login`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/user.js#L48) 414 | `cartId` - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from [`api/cart/create`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/cart.js#L26) 415 | 416 | 417 | #### RESPONSE BODY: 418 | 419 | ```json 420 | { 421 | "code":200, 422 | "result": 423 | [ 424 | { 425 | "code":"cashondelivery", 426 | "title":"Cash On Delivery" 427 | }, 428 | { 429 | "code":"checkmo","title": 430 | "Check / Money order" 431 | }, 432 | { 433 | "code":"free", 434 | "title":"No Payment Information Required" 435 | } 436 | ] 437 | } 438 | ``` 439 | 440 | ### POST /api/cart/shipping-methods 441 | 442 | 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 443 | 444 | #### EXAMPLE CALL: 445 | 446 | ```bash 447 | curl 'https://your-domain.example.com/api/cart/shipping-methods?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' -H 'accept: */*' --data-binary '{"address":{"country_id":"PL"}}' 448 | ``` 449 | 450 | #### GET PARAMS: 451 | `token` - null OR user token obtained from [`/api/user/login`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/user.js#L48) 452 | `cartId` - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from [`api/cart/create`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/cart.js#L26) 453 | 454 | 455 | #### REQUEST BODY: 456 | 457 | If the shipping methods are dependend 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`. The minimum required field is the `country_id` 458 | 459 | ```json 460 | { 461 | "address": 462 | { 463 | "country_id":"PL" 464 | } 465 | } 466 | ``` 467 | 468 | #### RESPONSE BODY: 469 | 470 | ```json 471 | { 472 | "code":200, 473 | "result": 474 | [ 475 | { 476 | "carrier_code":"flatrate", 477 | "method_code":"flatrate", 478 | "carrier_title":"Flat Rate", 479 | "method_title":"Fixed", 480 | "amount":5, 481 | "base_amount":5 482 | ,"available":true, 483 | "error_message":"", 484 | "price_excl_tax":5, 485 | "price_incl_tax":5 486 | } 487 | ] 488 | } 489 | ``` 490 | 491 | ### POST /api/cart/shipping-information 492 | 493 | This method sets the shipping information on specified quote which is a required step before calling `api/cart/totals` 494 | 495 | #### EXAMPLE CALL: 496 | 497 | ```bash 498 | curl 'https://your-domain.example.com/api/cart/shipping-information?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' -H 'accept: */*' --data-binary '{"addressInformation":{"shipping_address":{"country_id":"PL"},"shipping_method_code":"flatrate","shipping_carrier_code":"flatrate"}}' 499 | ``` 500 | 501 | #### GET PARAMS: 502 | `token` - null OR user token obtained from [`/api/user/login`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/user.js#L48) 503 | `cartId` - numeric (integer) value for authorized user cart id or GUID (mixed string) for guest cart ID obtained from [`api/cart/create`](https://github.com/DivanteLtd/vue-storefront-api/blob/7d98771994b1009ad17d69c458f9e93686cfb145/src/api/cart.js#L26) 504 | 505 | 506 | #### REQUEST BODY: 507 | 508 | ```json 509 | { 510 | "addressInformation": 511 | { 512 | "shipping_address": 513 | { 514 | "country_id":"PL" 515 | }, 516 | "shipping_method_code":"flatrate", 517 | "shipping_carrier_code":"flatrate" 518 | } 519 | } 520 | ``` 521 | 522 | #### RESPONSE BODY: 523 | 524 | ```json 525 | { 526 | "code": 200, 527 | "result": { 528 | "payment_methods": [ 529 | { 530 | "code": "cashondelivery", 531 | "title": "Cash On Delivery" 532 | }, 533 | { 534 | "code": "checkmo", 535 | "title": "Check / Money order" 536 | } 537 | ], 538 | "totals": { 539 | "grand_total": 45.8, 540 | "subtotal": 48, 541 | "discount_amount": -8.86, 542 | "subtotal_with_discount": 39.14, 543 | "shipping_amount": 5, 544 | "shipping_discount_amount": 0, 545 | "tax_amount": 9.38, 546 | "shipping_tax_amount": 0, 547 | "base_shipping_tax_amount": 0, 548 | "subtotal_incl_tax": 59.04, 549 | "shipping_incl_tax": 5, 550 | "base_currency_code": "USD", 551 | "quote_currency_code": "USD", 552 | "items_qty": 2, 553 | "items": [ 554 | { 555 | "item_id": 5853, 556 | "price": 24, 557 | "qty": 2, 558 | "row_total": 48, 559 | "row_total_with_discount": 0, 560 | "tax_amount": 9.38, 561 | "tax_percent": 23, 562 | "discount_amount": 8.86, 563 | "discount_percent": 15, 564 | "price_incl_tax": 29.52, 565 | "row_total_incl_tax": 59.04, 566 | "base_row_total_incl_tax": 59.04, 567 | "options": "[]", 568 | "name": "Logan HeatTec® Tee-XS-Black" 569 | } 570 | ], 571 | "total_segments": [ 572 | { 573 | "code": "subtotal", 574 | "title": "Subtotal", 575 | "value": 59.04 576 | }, 577 | { 578 | "code": "shipping", 579 | "title": "Shipping & Handling (Flat Rate - Fixed)", 580 | "value": 5 581 | }, 582 | { 583 | "code": "discount", 584 | "title": "Discount", 585 | "value": -8.86 586 | }, 587 | { 588 | "code": "tax", 589 | "title": "Tax", 590 | "value": 9.38, 591 | "area": "taxes", 592 | "extension_attributes": { 593 | "tax_grandtotal_details": [ 594 | { 595 | "amount": 9.38, 596 | "rates": [ 597 | { 598 | "percent": "23", 599 | "title": "VAT23" 600 | } 601 | ], 602 | "group_id": 1 603 | } 604 | ] 605 | } 606 | }, 607 | { 608 | "code": "grand_total", 609 | "title": "Grand Total", 610 | "value": 55.18, 611 | "area": "footer" 612 | } 613 | ] 614 | } 615 | } 616 | } 617 | ``` 618 | 619 | 620 | 621 | 622 | ## User module 623 | 624 | ### POST /api/user/create 625 | 626 | Registers new user to eCommerce backend users database. 627 | 628 | #### EXAMPLE CALL: 629 | 630 | ```bash 631 | curl 'https://your-domain.example.com/api/user/create' -H 'content-type: application/json' -H 'accept: application/json, text/plain, */*'--data-binary '{"customer":{"email":"pkarwatka9998@divante.pl","firstname":"Joe","lastname":"Black"},"password":"SecretPassword!@#123"}' 632 | ``` 633 | 634 | #### REQUEST BODY: 635 | 636 | ```json 637 | { 638 | "customer": { 639 | "email": "pkarwatka9998@divante.pl", 640 | "firstname": "Joe", 641 | "lastname": "Black" 642 | }, 643 | "password": "SecretPassword" 644 | } 645 | ``` 646 | 647 | #### RESPONSE BODY: 648 | 649 | In case of success 650 | 651 | ```json 652 | { 653 | "code": 200, 654 | "result": { 655 | "id": 286, 656 | "group_id": 1, 657 | "created_at": "2018-04-03 13:35:13", 658 | "updated_at": "2018-04-03 13:35:13", 659 | "created_in": "Default Store View", 660 | "email": "pkarwatka9998@divante.pl", 661 | "firstname": "Joe", 662 | "lastname": "Black", 663 | "store_id": 1, 664 | "website_id": 1, 665 | "addresses": [], 666 | "disable_auto_group_change": 0 667 | } 668 | } 669 | ``` 670 | 671 | In case of error: 672 | 673 | ```json 674 | { 675 | "code": 500, 676 | "result": "Minimum of different classes of characters in password is 3. Classes of characters: Lower Case, Upper Case, Digits, Special Characters." 677 | } 678 | ``` 679 | 680 | 681 | ### POST /api/user/login 682 | 683 | Authorizes the user. It's called after user submits "Login" form inside the Vue Storefront app. It returns the user token which should be used for all subsequent API calls that requires authorization 684 | 685 | #### GET PARAMS: 686 | 687 | ``` 688 | null 689 | ``` 690 | 691 | #### REQUEST BODY: 692 | 693 | ```json 694 | { 695 | "username":"pkarwatka102@divante.pl", 696 | "password":"TopSecretPassword"} 697 | ``` 698 | 699 | #### RESPONSE BODY: 700 | 701 | `curl 'https://your-domain.example.com/api/user/login' -H 'content-type: application/json' -H 'accept: application/json' --data-binary '"username":"pkarwatka102@divante.pl","password":"TopSecretPassword}'` 702 | 703 | ```json 704 | { 705 | "code":200, 706 | "result":"xu8h02nd66yq0gaayj4x3kpqwity02or", 707 | "meta": { "refreshToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzOSJ9.a4HQc2HODmOj5SRMiv-EzWuMZbyIz0CLuVRhPw_MrOM" } 708 | } 709 | ``` 710 | 711 | or in case of error: 712 | 713 | ```json 714 | { 715 | "code":500, 716 | "result":"You did not sign in correctly or your account is temporarily disabled." 717 | } 718 | ``` 719 | 720 | The result is a authorization token, that should be passed via `?token=xu8h02nd66yq0gaayj4x3kpqwity02or` GET param to all subsequent API calls that requires authorization 721 | 722 | #### RESPONSE CODES: 723 | 724 | - `200` when success 725 | - `500` in case of error 726 | 727 | 728 | ### POST /api/user/refresh 729 | 730 | Refresh the user token 731 | 732 | #### GET PARAMS: 733 | 734 | ``` 735 | null 736 | ``` 737 | 738 | #### REQUEST BODY: 739 | 740 | ```json 741 | { 742 | "refreshToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzOSJ9.a4HQc2HODmOj5SRMiv-EzWuMZbyIz0CLuVRhPw_MrOM" 743 | } 744 | ``` 745 | 746 | #### RESPONSE BODY: 747 | 748 | `curl 'https://your-domain.example.com/api/user/login' -H 'content-type: application/json' -H 'accept: application/json' --data-binary '"refreshToken":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzOSJ9.a4HQc2HODmOj5SRMiv-EzWuMZbyIz0CLuVRhPw_MrOM"}'` 749 | 750 | ```json 751 | { 752 | "code":200, 753 | "result":"xu8h02nd66yq0gaayj4x3kpqwity02or" 754 | } 755 | ``` 756 | 757 | or in case of error: 758 | 759 | ```json 760 | { 761 | "code":500, 762 | "result":"You did not sign in correctly or your account is temporarily disabled." 763 | } 764 | ``` 765 | 766 | The result is a authorization token, that should be passed via `?token=xu8h02nd66yq0gaayj4x3kpqwity02or` GET param to all subsequent API calls that requires authorization 767 | 768 | #### RESPONSE CODES: 769 | 770 | - `200` when success 771 | - `500` in case of error 772 | 773 | ### POST /api/user/reset-password 774 | 775 | Sends the password reset link for the specified user. 776 | 777 | #### EXAMPLE CALL: 778 | 779 | ```bash 780 | curl 'https://your-domain.example.com/api/user/reset-password' -H 'content-type: application/json' -H 'accept: application/json, text/plain, */*' --data-binary '{"email":"pkarwatka992@divante.pl"}' 781 | ``` 782 | 783 | #### REQUEST BODY: 784 | 785 | ```json 786 | { 787 | "email": "pkarwatka992@divante.pl" 788 | } 789 | ``` 790 | 791 | #### RESPONSE BODY: 792 | 793 | ```json 794 | { 795 | "code": 500, 796 | "result": "No such entity with email = pkarwatka992@divante.pl, websiteId = 1" 797 | } 798 | ``` 799 | 800 | 801 | ### POST /api/user/change-password 802 | 803 | This method is used to change password for current user identified by `token` obtained from `api/user/login` 804 | 805 | #### GET PARAMS: 806 | 807 | `token` - user token returned from `POST /api/user/login` 808 | 809 | #### REQUEST BODY: 810 | 811 | ```json 812 | { 813 | "currentPassword":"OldPassword", 814 | "newPassword":"NewPassword" 815 | } 816 | ``` 817 | 818 | 819 | #### RESPONSE BODY: 820 | 821 | ```json 822 | { 823 | "code":500, 824 | "result":"The password doesn't match this account." 825 | } 826 | ``` 827 | 828 | ### GET /api/user/order-history 829 | 830 | Get the user order history from server side 831 | 832 | #### GET PARAMS: 833 | 834 | `token` - user token returned from `POST /api/user/login` 835 | 836 | #### RESPONSE BODY: 837 | 838 | ```json 839 | { 840 | "code": 200, 841 | "result": { 842 | "items": [ 843 | { 844 | "applied_rule_ids": "1,5", 845 | "base_currency_code": "USD", 846 | "base_discount_amount": -3.3, 847 | "base_grand_total": 28, 848 | "base_discount_tax_compensation_amount": 0, 849 | "base_shipping_amount": 5, 850 | "base_shipping_discount_amount": 0, 851 | "base_shipping_incl_tax": 5, 852 | "base_shipping_tax_amount": 0, 853 | "base_subtotal": 22, 854 | "base_subtotal_incl_tax": 27.06, 855 | "base_tax_amount": 4.3, 856 | "base_total_due": 28, 857 | "base_to_global_rate": 1, 858 | "base_to_order_rate": 1, 859 | "billing_address_id": 204, 860 | "created_at": "2018-01-23 15:30:04", 861 | "customer_email": "pkarwatka28@example.com", 862 | "customer_group_id": 0, 863 | "customer_is_guest": 1, 864 | "customer_note_notify": 1, 865 | "discount_amount": -3.3, 866 | "email_sent": 1, 867 | "entity_id": 102, 868 | "global_currency_code": "USD", 869 | "grand_total": 28, 870 | "discount_tax_compensation_amount": 0, 871 | "increment_id": "000000102", 872 | "is_virtual": 0, 873 | "order_currency_code": "USD", 874 | "protect_code": "3984835d33abd2423b8a47efd0f74579", 875 | "quote_id": 1112, 876 | "shipping_amount": 5, 877 | "shipping_description": "Flat Rate - Fixed", 878 | "shipping_discount_amount": 0, 879 | "shipping_discount_tax_compensation_amount": 0, 880 | "shipping_incl_tax": 5, 881 | "shipping_tax_amount": 0, 882 | "state": "new", 883 | "status": "pending", 884 | "store_currency_code": "USD", 885 | "store_id": 1, 886 | "store_name": "Main Website\nMain Website Store\n", 887 | "store_to_base_rate": 0, 888 | "store_to_order_rate": 0, 889 | "subtotal": 22, 890 | "subtotal_incl_tax": 27.06, 891 | "tax_amount": 4.3, 892 | "total_due": 28, 893 | "total_item_count": 1, 894 | "total_qty_ordered": 1, 895 | "updated_at": "2018-01-23 15:30:05", 896 | "weight": 1, 897 | "items": [ 898 | { 899 | "amount_refunded": 0, 900 | "applied_rule_ids": "1,5", 901 | "base_amount_refunded": 0, 902 | "base_discount_amount": 3.3, 903 | "base_discount_invoiced": 0, 904 | "base_discount_tax_compensation_amount": 0, 905 | "base_original_price": 22, 906 | "base_price": 22, 907 | "base_price_incl_tax": 27.06, 908 | "base_row_invoiced": 0, 909 | "base_row_total": 22, 910 | "base_row_total_incl_tax": 27.06, 911 | "base_tax_amount": 4.3, 912 | "base_tax_invoiced": 0, 913 | "created_at": "2018-01-23 15:30:04", 914 | "discount_amount": 3.3, 915 | "discount_invoiced": 0, 916 | "discount_percent": 15, 917 | "free_shipping": 0, 918 | "discount_tax_compensation_amount": 0, 919 | "is_qty_decimal": 0, 920 | "is_virtual": 0, 921 | "item_id": 224, 922 | "name": "Radiant Tee-XS-Blue", 923 | "no_discount": 0, 924 | "order_id": 102, 925 | "original_price": 22, 926 | "price": 22, 927 | "price_incl_tax": 27.06, 928 | "product_id": 1546, 929 | "product_type": "simple", 930 | "qty_canceled": 0, 931 | "qty_invoiced": 0, 932 | "qty_ordered": 1, 933 | "qty_refunded": 0, 934 | "qty_shipped": 0, 935 | "quote_item_id": 675, 936 | "row_invoiced": 0, 937 | "row_total": 22, 938 | "row_total_incl_tax": 27.06, 939 | "row_weight": 1, 940 | "sku": "WS12-XS-Blue", 941 | "store_id": 1, 942 | "tax_amount": 4.3, 943 | "tax_invoiced": 0, 944 | "tax_percent": 23, 945 | "updated_at": "2018-01-23 15:30:04", 946 | "weight": 1 947 | } 948 | ], 949 | "billing_address": { 950 | "address_type": "billing", 951 | "city": "Some city2", 952 | "company": "Divante", 953 | "country_id": "PL", 954 | "email": "pkarwatka28@example.com", 955 | "entity_id": 204, 956 | "firstname": "Piotr", 957 | "lastname": "Karwatka", 958 | "parent_id": 102, 959 | "postcode": "50-203", 960 | "street": [ 961 | "XYZ", 962 | "17" 963 | ], 964 | "telephone": null, 965 | "vat_id": "PL8951930748" 966 | }, 967 | "payment": { 968 | "account_status": null, 969 | "additional_information": [ 970 | "Cash On Delivery", 971 | "" 972 | ], 973 | "amount_ordered": 28, 974 | "base_amount_ordered": 28, 975 | "base_shipping_amount": 5, 976 | "cc_last4": null, 977 | "entity_id": 102, 978 | "method": "cashondelivery", 979 | "parent_id": 102, 980 | "shipping_amount": 5 981 | }, 982 | "status_histories": [], 983 | "extension_attributes": { 984 | "shipping_assignments": [ 985 | { 986 | "shipping": { 987 | "address": { 988 | "address_type": "shipping", 989 | "city": "Some city", 990 | "company": "NA", 991 | "country_id": "PL", 992 | "email": "pkarwatka28@example.com", 993 | "entity_id": 203, 994 | "firstname": "Piotr", 995 | "lastname": "Karwatka", 996 | "parent_id": 102, 997 | "postcode": "51-169", 998 | "street": [ 999 | "XYZ", 1000 | "13" 1001 | ], 1002 | "telephone": null 1003 | }, 1004 | "method": "flatrate_flatrate", 1005 | "total": { 1006 | "base_shipping_amount": 5, 1007 | "base_shipping_discount_amount": 0, 1008 | "base_shipping_incl_tax": 5, 1009 | "base_shipping_tax_amount": 0, 1010 | "shipping_amount": 5, 1011 | "shipping_discount_amount": 0, 1012 | "shipping_discount_tax_compensation_amount": 0, 1013 | "shipping_incl_tax": 5, 1014 | "shipping_tax_amount": 0 1015 | } 1016 | }, 1017 | "items": [ 1018 | { 1019 | "amount_refunded": 0, 1020 | "applied_rule_ids": "1,5", 1021 | "base_amount_refunded": 0, 1022 | "base_discount_amount": 3.3, 1023 | "base_discount_invoiced": 0, 1024 | "base_discount_tax_compensation_amount": 0, 1025 | "base_original_price": 22, 1026 | "base_price": 22, 1027 | "base_price_incl_tax": 27.06, 1028 | "base_row_invoiced": 0, 1029 | "base_row_total": 22, 1030 | "base_row_total_incl_tax": 27.06, 1031 | "base_tax_amount": 4.3, 1032 | "base_tax_invoiced": 0, 1033 | "created_at": "2018-01-23 15:30:04", 1034 | "discount_amount": 3.3, 1035 | "discount_invoiced": 0, 1036 | "discount_percent": 15, 1037 | "free_shipping": 0, 1038 | "discount_tax_compensation_amount": 0, 1039 | "is_qty_decimal": 0, 1040 | "is_virtual": 0, 1041 | "item_id": 224, 1042 | "name": "Radiant Tee-XS-Blue", 1043 | "no_discount": 0, 1044 | "order_id": 102, 1045 | "original_price": 22, 1046 | "price": 22, 1047 | "price_incl_tax": 27.06, 1048 | "product_id": 1546, 1049 | "product_type": "simple", 1050 | "qty_canceled": 0, 1051 | "qty_invoiced": 0, 1052 | "qty_ordered": 1, 1053 | "qty_refunded": 0, 1054 | "qty_shipped": 0, 1055 | "quote_item_id": 675, 1056 | "row_invoiced": 0, 1057 | "row_total": 22, 1058 | "row_total_incl_tax": 27.06, 1059 | "row_weight": 1, 1060 | "sku": "WS12-XS-Blue", 1061 | "store_id": 1, 1062 | "tax_amount": 4.3, 1063 | "tax_invoiced": 0, 1064 | "tax_percent": 23, 1065 | "updated_at": "2018-01-23 15:30:04", 1066 | "weight": 1 1067 | } 1068 | ] 1069 | } 1070 | ] 1071 | } 1072 | } 1073 | ], 1074 | "search_criteria": { 1075 | "filter_groups": [ 1076 | { 1077 | "filters": [ 1078 | { 1079 | "field": "customer_email", 1080 | "value": "pkarwatka28@example.com", 1081 | "condition_type": "eq" 1082 | } 1083 | ] 1084 | } 1085 | ] 1086 | }, 1087 | "total_count": 61 1088 | } 1089 | } 1090 | ``` 1091 | 1092 | ### GET /api/user/me 1093 | 1094 | Gets the User profile for currently authorized user. It's called after `POST /api/user/login` successful call. 1095 | 1096 | #### GET PARAMS: 1097 | 1098 | `token` - user token returned from `POST /api/user/login` 1099 | 1100 | #### RESPONSE BODY: 1101 | 1102 | ```json 1103 | { 1104 | "code":200, 1105 | "result": 1106 | { 1107 | "id":158, 1108 | "group_id":1, 1109 | "default_shipping":"67", 1110 | "created_at":"2018-02-28 12:05:39", 1111 | "updated_at":"2018-03-29 10:46:03", 1112 | "created_in":"Default Store View", 1113 | "email":"pkarwatka102@divante.pl", 1114 | "firstname":"Piotr", 1115 | "lastname":"Karwatka", 1116 | "store_id":1, 1117 | "website_id":1, 1118 | "addresses":[ 1119 | { 1120 | "id":67, 1121 | "customer_id":158, 1122 | "region": 1123 | { 1124 | "region_code":null, 1125 | "region":null, 1126 | "region_id":0 1127 | }, 1128 | "region_id":0, 1129 | "country_id":"PL", 1130 | "street": ["Street name","13"], 1131 | "telephone":"", 1132 | "postcode":"41-157", 1133 | "city":"Wrocław", 1134 | "firstname":"John","lastname":"Murphy", 1135 | "default_shipping":true 1136 | }], 1137 | "disable_auto_group_change":0 1138 | } 1139 | } 1140 | ``` 1141 | #### RESPONSE CODES: 1142 | 1143 | - `200` when success 1144 | - `500` in case of error 1145 | 1146 | 1147 | 1148 | ### POST /api/user/me 1149 | 1150 | Updates the user address and other data information. 1151 | 1152 | #### GET PARAMS: 1153 | 1154 | `token` - user token returned from `POST /api/user/login` 1155 | 1156 | #### REQUEST BODY: 1157 | 1158 | As the request You should post the address information You like to apply to the current user (identified by the token). 1159 | 1160 | ```json 1161 | { 1162 | "customer": { 1163 | "id": 222, 1164 | "group_id": 1, 1165 | "default_billing": "105", 1166 | "default_shipping": "105", 1167 | "created_at": "2018-03-16 19:01:18", 1168 | "updated_at": "2018-04-03 12:59:13", 1169 | "created_in": "Default Store View", 1170 | "email": "pkarwatka30@divante.pl", 1171 | "firstname": "Piotr", 1172 | "lastname": "Karwatka", 1173 | "store_id": 1, 1174 | "website_id": 1, 1175 | "addresses": [ 1176 | { 1177 | "id": 109, 1178 | "customer_id": 222, 1179 | "region": { 1180 | "region_code": null, 1181 | "region": null, 1182 | "region_id": 0 1183 | }, 1184 | "region_id": 0, 1185 | "country_id": "PL", 1186 | "street": [ 1187 | "Dmowskiego", 1188 | "17" 1189 | ], 1190 | "company": "Divante2", 1191 | "telephone": "", 1192 | "postcode": "50-203", 1193 | "city": "Wrocław", 1194 | "firstname": "Piotr", 1195 | "lastname": "Karwatka2", 1196 | "vat_id": "PL8951930748" 1197 | } 1198 | ], 1199 | "disable_auto_group_change": 0 1200 | } 1201 | } 1202 | ``` 1203 | 1204 | #### RESPONSE BODY: 1205 | 1206 | In the response You'll get the current, updated information about the user. 1207 | 1208 | ```json 1209 | { 1210 | "code": 200, 1211 | "result": { 1212 | "id": 222, 1213 | "group_id": 1, 1214 | "created_at": "2018-03-16 19:01:18", 1215 | "updated_at": "2018-04-04 02:59:52", 1216 | "created_in": "Default Store View", 1217 | "email": "pkarwatka30@divante.pl", 1218 | "firstname": "Piotr", 1219 | "lastname": "Karwatka", 1220 | "store_id": 1, 1221 | "website_id": 1, 1222 | "addresses": [ 1223 | { 1224 | "id": 109, 1225 | "customer_id": 222, 1226 | "region": { 1227 | "region_code": null, 1228 | "region": null, 1229 | "region_id": 0 1230 | }, 1231 | "region_id": 0, 1232 | "country_id": "PL", 1233 | "street": [ 1234 | "Dmowskiego", 1235 | "17" 1236 | ], 1237 | "company": "Divante2", 1238 | "telephone": "", 1239 | "postcode": "50-203", 1240 | "city": "Wrocław", 1241 | "firstname": "Piotr", 1242 | "lastname": "Karwatka2", 1243 | "vat_id": "PL8951930748" 1244 | } 1245 | ], 1246 | "disable_auto_group_change": 0 1247 | } 1248 | } 1249 | ``` 1250 | 1251 | #### RESPONSE CODES: 1252 | 1253 | - `200` when success 1254 | - `500` in case of error 1255 | 1256 | 1257 | ## Stock module 1258 | 1259 | ### GET `/api/stock/check/:sku` 1260 | 1261 | This method is used to check the stock item for specified product sku 1262 | 1263 | #### RESPONSE BODY: 1264 | 1265 | ```json 1266 | { 1267 | "code": 200, 1268 | "result": { 1269 | "item_id": 580, 1270 | "product_id": 580, 1271 | "stock_id": 1, 1272 | "qty": 53, 1273 | "is_in_stock": true, 1274 | "is_qty_decimal": false, 1275 | "show_default_notification_message": false, 1276 | "use_config_min_qty": true, 1277 | "min_qty": 0, 1278 | "use_config_min_sale_qty": 1, 1279 | "min_sale_qty": 1, 1280 | "use_config_max_sale_qty": true, 1281 | "max_sale_qty": 10000, 1282 | "use_config_backorders": true, 1283 | "backorders": 0, 1284 | "use_config_notify_stock_qty": true, 1285 | "notify_stock_qty": 1, 1286 | "use_config_qty_increments": true, 1287 | "qty_increments": 0, 1288 | "use_config_enable_qty_inc": true, 1289 | "enable_qty_increments": false, 1290 | "use_config_manage_stock": true, 1291 | "manage_stock": true, 1292 | "low_stock_date": null, 1293 | "is_decimal_divided": false, 1294 | "stock_status_changed_auto": 0 1295 | } 1296 | } 1297 | ``` 1298 | 1299 | ### GET `/api/stock/list` 1300 | 1301 | This method is used to check multiple stock items for specified product skus. Requires `skus` param of comma-separated values to indicate which stock items to return. 1302 | 1303 | #### RESPONSE BODY: 1304 | 1305 | ```json 1306 | { 1307 | "code": 200, 1308 | "result": [ 1309 | { 1310 | "item_id": 580, 1311 | "product_id": 580, 1312 | "stock_id": 1, 1313 | "qty": 53, 1314 | "is_in_stock": true, 1315 | "is_qty_decimal": false, 1316 | "show_default_notification_message": false, 1317 | "use_config_min_qty": true, 1318 | "min_qty": 0, 1319 | "use_config_min_sale_qty": 1, 1320 | "min_sale_qty": 1, 1321 | "use_config_max_sale_qty": true, 1322 | "max_sale_qty": 10000, 1323 | "use_config_backorders": true, 1324 | "backorders": 0, 1325 | "use_config_notify_stock_qty": true, 1326 | "notify_stock_qty": 1, 1327 | "use_config_qty_increments": true, 1328 | "qty_increments": 0, 1329 | "use_config_enable_qty_inc": true, 1330 | "enable_qty_increments": false, 1331 | "use_config_manage_stock": true, 1332 | "manage_stock": true, 1333 | "low_stock_date": null, 1334 | "is_decimal_divided": false, 1335 | "stock_status_changed_auto": 0 1336 | } 1337 | ] 1338 | 1339 | } 1340 | ``` 1341 | 1342 | ## Order module 1343 | 1344 | ### POST '/api/order/create` 1345 | 1346 | Queue the order into the order queue which will be asynchronously submitted to the eCommerce backend. 1347 | 1348 | #### REQUEST BODY: 1349 | 1350 | The `user_id` field is a numeric user id as returned in `api/user/me`. 1351 | The `cart_id` is a guest or authorized users quote id (You can mix guest cart with authroized user as well) 1352 | 1353 | ```json 1354 | { 1355 | "user_id": "", 1356 | "cart_id": "d90e9869fbfe3357281a67e3717e3524", 1357 | "products": [ 1358 | { 1359 | "sku": "WT08-XS-Yellow", 1360 | "qty": 1 1361 | } 1362 | ], 1363 | "addressInformation": { 1364 | "shippingAddress": { 1365 | "region": "", 1366 | "region_id": 0, 1367 | "country_id": "PL", 1368 | "street": [ 1369 | "Example", 1370 | "12" 1371 | ], 1372 | "company": "NA", 1373 | "telephone": "", 1374 | "postcode": "50-201", 1375 | "city": "Wroclaw", 1376 | "firstname": "Piotr", 1377 | "lastname": "Karwatka", 1378 | "email": "pkarwatka30@divante.pl", 1379 | "region_code": "" 1380 | }, 1381 | "billingAddress": { 1382 | "region": "", 1383 | "region_id": 0, 1384 | "country_id": "PL", 1385 | "street": [ 1386 | "Example", 1387 | "12" 1388 | ], 1389 | "company": "Company name", 1390 | "telephone": "", 1391 | "postcode": "50-201", 1392 | "city": "Wroclaw", 1393 | "firstname": "Piotr", 1394 | "lastname": "Karwatka", 1395 | "email": "pkarwatka30@divante.pl", 1396 | "region_code": "", 1397 | "vat_id": "PL88182881112" 1398 | }, 1399 | "shipping_method_code": "flatrate", 1400 | "shipping_carrier_code": "flatrate", 1401 | "payment_method_code": "cashondelivery", 1402 | "payment_method_additional": {} 1403 | }, 1404 | "order_id": "1522811662622-d3736c94-49a5-cd34-724c-87a3a57c2750", 1405 | "transmited": false, 1406 | "created_at": "2018-04-04T03:14:22.622Z", 1407 | "updated_at": "2018-04-04T03:14:22.622Z" 1408 | } 1409 | ``` 1410 | 1411 | #### RESPONSE BODY: 1412 | 1413 | ```json 1414 | { 1415 | "code":200, 1416 | "result":"OK" 1417 | } 1418 | ``` 1419 | 1420 | In case of the JSON validation error, the validation errors will be returned inside the `result` object. 1421 | 1422 | ## Catalog module 1423 | 1424 | ### POST|GET /api/catalog 1425 | 1426 | Catalog endpoints are a proxy to Elastic Search 5.x and can be used to search the store catalog (synchronized with Magento2 or other platform). 1427 | 1428 | **Note:** This endpoint is not required as it's possible to configure the `vue-storefront` to connect directly to Elastic. Please just set the proper Elastic URL in the `config/local.json:elasticsearch` 1429 | 1430 | #### GET PARAMETERS 1431 | 1432 | `/api/catalog/:index-name/:entity-name/_search?size=:pageSize&from=:offset&sort=` 1433 | 1434 | `index-name` is an Elastic Search index name - by default it's `vue_storefront_catalog` for most instalations 1435 | `entity-name` is an Elastic Search entity name - `product`, `attribute`, `taxrule`, `category` ... 1436 | `pageSize` numeric value of the number of records to be returned 1437 | `offset` numeric value of the first record to be returned 1438 | 1439 | #### EXAMPLE CALL 1440 | 1441 | ```bash 1442 | curl 'https://your-domain.example.com/api/catalog/vue_storefront_catalog/attribute/_search?size=50&from=0&sort=' -H 'content-type: application/json' -H 'accept: */*' --data-binary '{"query":{"bool":{"filter":{"bool":{"should":[{"term":{"attribute_code":"color"}},{"term":{"attribute_code":"size"}},{"term":{"attribute_code":"price"}}]}}}}}' 1443 | ``` 1444 | 1445 | #### REQUEST BODY 1446 | 1447 | Request body is a Elastic Search query. [Please read more on Elastic querying DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/_introducing_the_query_language.html) 1448 | 1449 | ```json 1450 | { 1451 | "query": { 1452 | "bool": { 1453 | "filter": { 1454 | "bool": { 1455 | "should": [ 1456 | { 1457 | "term": { 1458 | "attribute_code": "color" 1459 | } 1460 | }, 1461 | { 1462 | "term": { 1463 | "attribute_code": "size" 1464 | } 1465 | }, 1466 | { 1467 | "term": { 1468 | "attribute_code": "price" 1469 | } 1470 | } 1471 | ] 1472 | } 1473 | } 1474 | } 1475 | } 1476 | } 1477 | ``` 1478 | 1479 | #### RESPONSE BODY: 1480 | 1481 | Elastic Search data format. Please read more on [data formats used in Vue Storefront](README.md) 1482 | 1483 | ```json 1484 | { 1485 | "took": 0, 1486 | "timed_out": false, 1487 | "_shards": { 1488 | "total": 5, 1489 | "successful": 5, 1490 | "failed": 0 1491 | }, 1492 | "hits": { 1493 | "total": 4, 1494 | "max_score": 0, 1495 | "hits": [ 1496 | { 1497 | "_index": "vue_storefront_catalog", 1498 | "_type": "attribute", 1499 | "_id": "157", 1500 | "_score": 0, 1501 | "_source": { 1502 | "is_used_in_grid": false, 1503 | "is_visible_in_grid": false, 1504 | "is_filterable_in_grid": false, 1505 | "position": 0, 1506 | "is_comparable": "0", 1507 | "is_visible_on_front": "0", 1508 | "is_visible": true, 1509 | "scope": "global", 1510 | "attribute_id": 157, 1511 | "attribute_code": "size", 1512 | "frontend_input": "select", 1513 | "options": [ 1514 | { 1515 | "label": " ", 1516 | "value": "" 1517 | }, 1518 | { 1519 | "label": "55 cm", 1520 | "value": "91" 1521 | }, 1522 | { 1523 | "label": "XS", 1524 | "value": "167" 1525 | }, 1526 | { 1527 | "label": "65 cm", 1528 | "value": "92" 1529 | }, 1530 | { 1531 | "label": "S", 1532 | "value": "168" 1533 | }, 1534 | { 1535 | "label": "75 cm", 1536 | "value": "93" 1537 | }, 1538 | { 1539 | "label": "M", 1540 | "value": "169" 1541 | }, 1542 | { 1543 | "label": "6 foot", 1544 | "value": "94" 1545 | }, 1546 | { 1547 | "label": "L", 1548 | "value": "170" 1549 | }, 1550 | { 1551 | "label": "8 foot", 1552 | "value": "95" 1553 | }, 1554 | { 1555 | "label": "XL", 1556 | "value": "171" 1557 | }, 1558 | { 1559 | "label": "10 foot", 1560 | "value": "96" 1561 | }, 1562 | { 1563 | "label": "28", 1564 | "value": "172" 1565 | }, 1566 | { 1567 | "label": "29", 1568 | "value": "173" 1569 | }, 1570 | { 1571 | "label": "30", 1572 | "value": "174" 1573 | }, 1574 | { 1575 | "label": "31", 1576 | "value": "175" 1577 | }, 1578 | { 1579 | "label": "32", 1580 | "value": "176" 1581 | }, 1582 | { 1583 | "label": "33", 1584 | "value": "177" 1585 | }, 1586 | { 1587 | "label": "34", 1588 | "value": "178" 1589 | }, 1590 | { 1591 | "label": "36", 1592 | "value": "179" 1593 | }, 1594 | { 1595 | "label": "38", 1596 | "value": "180" 1597 | } 1598 | ], 1599 | "is_user_defined": true, 1600 | "default_frontend_label": "Size", 1601 | "frontend_labels": null, 1602 | "is_unique": "0", 1603 | "validation_rules": [], 1604 | "id": 157, 1605 | "tsk": 1507209128867, 1606 | "sgn": "lHoCOBS4B8qUtgG_ne8N1XnfdTwcWgRyvwAeVPRdVUE" 1607 | } 1608 | }, 1609 | { 1610 | "_index": "vue_storefront_catalog", 1611 | "_type": "attribute", 1612 | "_id": "142", 1613 | "_score": 0, 1614 | "_source": { 1615 | "is_filterable": true, 1616 | "is_used_in_grid": false, 1617 | "is_visible_in_grid": false, 1618 | "is_filterable_in_grid": false, 1619 | "position": 0, 1620 | "is_comparable": "0", 1621 | "is_visible_on_front": "0", 1622 | "is_visible": true, 1623 | "scope": "global", 1624 | "attribute_id": 142, 1625 | "attribute_code": "size", 1626 | "frontend_input": "select", 1627 | "options": [ 1628 | { 1629 | "label": " ", 1630 | "value": "" 1631 | }, 1632 | { 1633 | "label": "55 cm", 1634 | "value": "91" 1635 | }, 1636 | { 1637 | "label": "XS", 1638 | "value": "167" 1639 | }, 1640 | { 1641 | "label": "65 cm", 1642 | "value": "92" 1643 | }, 1644 | { 1645 | "label": "S", 1646 | "value": "168" 1647 | }, 1648 | { 1649 | "label": "75 cm", 1650 | "value": "93" 1651 | }, 1652 | { 1653 | "label": "M", 1654 | "value": "169" 1655 | }, 1656 | { 1657 | "label": "6 foot", 1658 | "value": "94" 1659 | }, 1660 | { 1661 | "label": "L", 1662 | "value": "170" 1663 | }, 1664 | { 1665 | "label": "8 foot", 1666 | "value": "95" 1667 | }, 1668 | { 1669 | "label": "XL", 1670 | "value": "171" 1671 | }, 1672 | { 1673 | "label": "10 foot", 1674 | "value": "96" 1675 | }, 1676 | { 1677 | "label": "28", 1678 | "value": "172" 1679 | }, 1680 | { 1681 | "label": "29", 1682 | "value": "173" 1683 | }, 1684 | { 1685 | "label": "30", 1686 | "value": "174" 1687 | }, 1688 | { 1689 | "label": "31", 1690 | "value": "175" 1691 | }, 1692 | { 1693 | "label": "32", 1694 | "value": "176" 1695 | }, 1696 | { 1697 | "label": "33", 1698 | "value": "177" 1699 | }, 1700 | { 1701 | "label": "34", 1702 | "value": "178" 1703 | }, 1704 | { 1705 | "label": "36", 1706 | "value": "179" 1707 | }, 1708 | { 1709 | "label": "38", 1710 | "value": "180" 1711 | } 1712 | ], 1713 | "is_user_defined": true, 1714 | "default_frontend_label": "Size", 1715 | "frontend_labels": null, 1716 | "is_unique": "0", 1717 | "validation_rules": [], 1718 | "id": 142, 1719 | "tsk": 1512134647691, 1720 | "sgn": "vHkjS2mGumtgjjzlDrGJnF6i8EeUU2twc2zkZe69ABc" 1721 | } 1722 | }, 1723 | { 1724 | "_index": "vue_storefront_catalog", 1725 | "_type": "attribute", 1726 | "_id": "93", 1727 | "_score": 0, 1728 | "_source": { 1729 | "is_filterable": true, 1730 | "is_used_in_grid": true, 1731 | "is_visible_in_grid": false, 1732 | "is_filterable_in_grid": true, 1733 | "position": 0, 1734 | "is_comparable": "0", 1735 | "is_visible_on_front": "0", 1736 | "is_visible": true, 1737 | "scope": "global", 1738 | "attribute_id": 93, 1739 | "attribute_code": "color", 1740 | "frontend_input": "select", 1741 | "options": [ 1742 | { 1743 | "label": " ", 1744 | "value": "" 1745 | }, 1746 | { 1747 | "label": "Black", 1748 | "value": "49" 1749 | }, 1750 | { 1751 | "label": "Blue", 1752 | "value": "50" 1753 | }, 1754 | { 1755 | "label": "Brown", 1756 | "value": "51" 1757 | }, 1758 | { 1759 | "label": "Gray", 1760 | "value": "52" 1761 | }, 1762 | { 1763 | "label": "Green", 1764 | "value": "53" 1765 | }, 1766 | { 1767 | "label": "Lavender", 1768 | "value": "54" 1769 | }, 1770 | { 1771 | "label": "Multi", 1772 | "value": "55" 1773 | }, 1774 | { 1775 | "label": "Orange", 1776 | "value": "56" 1777 | }, 1778 | { 1779 | "label": "Purple", 1780 | "value": "57" 1781 | }, 1782 | { 1783 | "label": "Red", 1784 | "value": "58" 1785 | }, 1786 | { 1787 | "label": "White", 1788 | "value": "59" 1789 | }, 1790 | { 1791 | "label": "Yellow", 1792 | "value": "60" 1793 | } 1794 | ], 1795 | "is_user_defined": true, 1796 | "default_frontend_label": "Color", 1797 | "frontend_labels": null, 1798 | "is_unique": "0", 1799 | "validation_rules": [], 1800 | "id": 93, 1801 | "tsk": 1512134647691, 1802 | "sgn": "-FiYBhiIoVUHYxoL5kIEy3WP00emAeT-RtwqsmB69Lo" 1803 | } 1804 | }, 1805 | { 1806 | "_index": "vue_storefront_catalog", 1807 | "_type": "attribute", 1808 | "_id": "77", 1809 | "_score": 0, 1810 | "_source": { 1811 | "is_filterable": true, 1812 | "is_used_in_grid": false, 1813 | "is_visible_in_grid": false, 1814 | "is_filterable_in_grid": false, 1815 | "position": 0, 1816 | "is_comparable": "0", 1817 | "is_visible_on_front": "0", 1818 | "is_visible": true, 1819 | "scope": "global", 1820 | "attribute_id": 77, 1821 | "attribute_code": "price", 1822 | "frontend_input": "price", 1823 | "options": [], 1824 | "is_user_defined": false, 1825 | "default_frontend_label": "Price", 1826 | "frontend_labels": null, 1827 | "is_unique": "0", 1828 | "validation_rules": [], 1829 | "id": 77, 1830 | "tsk": 1512134647691, 1831 | "sgn": "qU1O7BGcjcqZA_5KgJIaw4-HSUHcMyqgTy9jXy0THoE" 1832 | } 1833 | } 1834 | ] 1835 | } 1836 | } 1837 | ``` 1838 | 1839 | ### /api/product/list 1840 | 1841 | Magento specific methods to return the product details for specifed SKUs. 1842 | Methods are mostly used for data synchronization with Magento two and for some specific cases when overriding the platform prices inside Vue Storefront. 1843 | 1844 | **Note:** these two methods are optional and are being used ONLY when the [`config.products.alwaysSyncPlatformPricesOver` option is set on](https://docs.vuestorefront.io/guide/basics/configuration.html#products). 1845 | 1846 | #### GET PARAMS: 1847 | `skus` - comma separated list of skus to get 1848 | 1849 | #### EXAMPLE CALL: 1850 | 1851 | ```bash 1852 | curl https://your-domain.example.com/api/product/list?skus=WP07 1853 | curl https://your-domain.example.com/api/product/render-list?skus=WP07 1854 | ``` 1855 | 1856 | #### RESPONSE BODY: 1857 | 1858 | For list: 1859 | 1860 | ```json 1861 | { 1862 | "code": 200, 1863 | "result": { 1864 | "items": [ 1865 | { 1866 | "id": 1866, 1867 | "sku": "WP07", 1868 | "name": "Aeon Capri", 1869 | "price": 0, 1870 | "status": 1, 1871 | "visibility": 4, 1872 | "type_id": "configurable", 1873 | "created_at": "2017-11-06 12:17:26", 1874 | "updated_at": "2017-11-06 12:17:26", 1875 | "product_links": [], 1876 | "tier_prices": [], 1877 | "custom_attributes": [ 1878 | { 1879 | "attribute_code": "description", 1880 | "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.

" 1881 | }, 1882 | { 1883 | "attribute_code": "image", 1884 | "value": "/w/p/wp07-black_main.jpg" 1885 | }, 1886 | { 1887 | "attribute_code": "category_ids", 1888 | "value": [ 1889 | "27", 1890 | "32", 1891 | "35", 1892 | "2" 1893 | ] 1894 | }, 1895 | { 1896 | "attribute_code": "url_key", 1897 | "value": "aeon-capri" 1898 | }, 1899 | { 1900 | "attribute_code": "tax_class_id", 1901 | "value": "2" 1902 | }, 1903 | { 1904 | "attribute_code": "eco_collection", 1905 | "value": "0" 1906 | }, 1907 | { 1908 | "attribute_code": "performance_fabric", 1909 | "value": "1" 1910 | }, 1911 | { 1912 | "attribute_code": "erin_recommends", 1913 | "value": "0" 1914 | }, 1915 | { 1916 | "attribute_code": "new", 1917 | "value": "0" 1918 | }, 1919 | { 1920 | "attribute_code": "sale", 1921 | "value": "0" 1922 | }, 1923 | { 1924 | "attribute_code": "style_bottom", 1925 | "value": "107" 1926 | }, 1927 | { 1928 | "attribute_code": "pattern", 1929 | "value": "195" 1930 | }, 1931 | { 1932 | "attribute_code": "climate", 1933 | "value": "205,212,206" 1934 | } 1935 | ] 1936 | } 1937 | ], 1938 | "search_criteria": { 1939 | "filter_groups": [ 1940 | { 1941 | "filters": [ 1942 | { 1943 | "field": "sku", 1944 | "value": "WP07", 1945 | "condition_type": "in" 1946 | } 1947 | ] 1948 | } 1949 | ] 1950 | }, 1951 | "total_count": 1 1952 | } 1953 | } 1954 | ``` 1955 | 1956 | For render-list: 1957 | 1958 | ```json 1959 | { 1960 | "code": 200, 1961 | "result": { 1962 | "items": [ 1963 | { 1964 | "price_info": { 1965 | "final_price": 59.04, 1966 | "max_price": 59.04, 1967 | "max_regular_price": 59.04, 1968 | "minimal_regular_price": 59.04, 1969 | "special_price": null, 1970 | "minimal_price": 59.04, 1971 | "regular_price": 48, 1972 | "formatted_prices": { 1973 | "final_price": "$59.04", 1974 | "max_price": "$59.04", 1975 | "minimal_price": "$59.04", 1976 | "max_regular_price": "$59.04", 1977 | "minimal_regular_price": null, 1978 | "special_price": null, 1979 | "regular_price": "$48.00" 1980 | }, 1981 | "extension_attributes": { 1982 | "tax_adjustments": { 1983 | "final_price": 47.999999, 1984 | "max_price": 47.999999, 1985 | "max_regular_price": 47.999999, 1986 | "minimal_regular_price": 47.999999, 1987 | "special_price": 47.999999, 1988 | "minimal_price": 47.999999, 1989 | "regular_price": 48, 1990 | "formatted_prices": { 1991 | "final_price": "$48.00", 1992 | "max_price": "$48.00", 1993 | "minimal_price": "$48.00", 1994 | "max_regular_price": "$48.00", 1995 | "minimal_regular_price": null, 1996 | "special_price": "$48.00", 1997 | "regular_price": "$48.00" 1998 | } 1999 | }, 2000 | "weee_attributes": [], 2001 | "weee_adjustment": "$59.04" 2002 | } 2003 | }, 2004 | "url": "http://demo-magento2.vuestorefront.io/aeon-capri.html", 2005 | "id": 1866, 2006 | "name": "Aeon Capri", 2007 | "type": "configurable", 2008 | "store_id": 1, 2009 | "currency_code": "USD", 2010 | "sgn": "bCt7e44sl1iZV8hzYGioKvSq0EdsAcF21FhpTG5t8l8" 2011 | } 2012 | ] 2013 | } 2014 | } 2015 | ``` 2016 | 2017 | 2018 | ## Image module 2019 | 2020 | ### /img 2021 | 2022 | This simple API module is used to just resize the images using [Sharp](https://github.com/lovell/sharp) node library. 2023 | 2024 | #### GET PARAMS 2025 | 2026 | `/img/{width}/{height}/{operation}/{relativeUrl}` 2027 | 2028 | or 2029 | 2030 | `/img/{width}/{height}/{operation}?absoluteUrl={absoluteUrl}` 2031 | 2032 | for example: 2033 | 2034 | `https://your-domain.example.com/img/310/300/resize/w/p/wp07-black_main.jpg` 2035 | 2036 | `width` - numeric value of the picure width - to be "resized", "cropped" ... regarding the `operation` parameter 2037 | `height` - numeric value of the picure height - to be "resized", "cropped" ... regarding the `operation` parameter 2038 | `operation` - one of the operations supported by [Imageable](https://github.com/sdepold/node-imageable): crop, fit, resize, identify (to get the picture EXIF data) 2039 | `relatveUrl` is the relative to 2040 | 2041 | Other examples: 2042 | 2043 | - https://your-domain.example.com/img/310/300/identify/w/p/wp07-black_main.jpg - to get the JSON encoded EXIF information 2044 | - https://your-domain.example.com/img/310/300/crop/w/p/wp07-black_main.jpg?crop=500x500%2B200%2B400 - to crop image (the crop parameter format = '{width}x{height}+{left}+{top}') 2045 | -------------------------------------------------------------------------------- /Format-attribute.md: -------------------------------------------------------------------------------- 1 | # Attribute entity 2 | 3 | For supported attribute types (which are used in `frontend_input`), 4 | please refer to [Magento Attribute Types Dev Docs](https://devdocs.magento.com/guides/m1x/api/soap/catalog/catalogProductAttribute/product_attribute.types.html) 5 | However, only `multiselect` and `select` are currently used, every other type will be treated as if they were `text` and this is handled by `core/modules/catalog/components/ProductAttribute.ts` in vue-storefront. 6 | 7 | ## Adding attributes 8 | 9 | To add new attribute to your custom API you need to know how you're going to use it. 10 | In all cases you'll have to push new attribute specification to ElasticSearch. 11 | Format may vary depending on how you want to use them, however here are the most common cases. 12 | 13 | ### 1. Simply show attribute on product page 14 | In that case the atributes/index endpoint should have the attribute returned as:all you need to do is return the 15 | ```json 16 | { 17 | "attribute_code": "supplier_note", 18 | "frontend_input": "text", 19 | "frontend_label": "Supplier note", 20 | "is_user_defined": true, 21 | "is_unique": false, 22 | "attribute_id": 123, 23 | "is_visible": true, 24 | "is_comparable": false, 25 | "is_visible_on_front": true, 26 | "position": 0, 27 | "id": 123, 28 | "options": [] 29 | } 30 | ``` 31 | and then in the `products/index` product record: 32 | ```json 33 | { 34 | (...) 35 | "supplier_note": "Keep away from the sun. Store in below 0 temperatures", 36 | (...) 37 | } 38 | ``` 39 | Unless you didn't define the `supplier_note` format in vue-storefront `core/modules/catalog/types/Product.ts` The value 40 | can also be an empty string `""` or `null`. 41 | 42 | ### 2. Use attribute to filter products 43 | Checklist: 44 | 45 | - For this to work, the filterable attribute `frontend_input` must be `select` in `attributes/index` endpoint 46 | - the attribute in attributes/index endpoint must have `options` node which needs to look like this: 47 | ```json 48 | "options": [ 49 | { 50 | "value": 7, 51 | "label": "Pink" 52 | }, 53 | { 54 | "value": 8, 55 | "label": "Gold" 56 | } 57 | ] 58 | ``` 59 | The value MUST BE `integer` type. 60 | 61 | - the product record on `products/index` must have the attribute value as `integer` type that matches the attribute 62 | option `value` field. So... Pink product would have 63 | ```json 64 | { 65 | (...) 66 | "color": 7, 67 | (...) 68 | } 69 | ``` 70 | and `7` will be later converted to "Pink" by vue-storefront logic. 71 | 72 | - IF the product is `configurable` type: 73 | ```json 74 | { 75 | (...) 76 | "type_id": "configurable", 77 | (...) 78 | } 79 | ``` 80 | and the color defines the product children, the parent product should have color set to `null`, and the products 81 | within its `configurable_children` should have the `color` value set. 82 | 83 | - If the product has children which are defined by color. Parent product should additionally have `color_options` 84 | node that defines color Ids of its children, e.g. if the product is available in pink and gold, it should have: 85 | ```json 86 | "color_options": [ 87 | 7, 88 | 8 89 | ] 90 | ``` 91 | returned in it, where one row (Pink colored child) in `configurable_children` has 92 | ```json 93 | "color": 7, 94 | ``` 95 | and the other (Gold colored child) has 96 | ```json 97 | "color": 8, 98 | ``` 99 | - if the product doesn't have the `color` set or available, the attribute should still be returned and the value 100 | should be `null` -------------------------------------------------------------------------------- /Format-category.md: -------------------------------------------------------------------------------- 1 | ## Category entity 2 | 3 | Please check the [sample-data/categories.json](sample-data/categories.json) to make sure which fields are trully crucial for Vue Storefront to work. 4 | 5 | Remember - you can add any properties you like to the JSON objects to consume them on Vue Storefront level. Please just make sure you added the new property names to [the proper `includeFields` list for queries](https://github.com/DivanteLtd/vue-storefront/blob/bb6f8e70b5587ed73c457d382c7ac93bd14db413/config/default.json#L151). 6 | 7 | Here we present the core purpose of the product properties: 8 | 9 | ```json 10 | "id": 24, 11 | ``` 12 | The type is undefined (it can be anything) - but must be unique. Category identifier used mostly for caching purposes (as a key) 13 | 14 | ```json 15 | "parent_id": 21, 16 | ``` 17 | 18 | If this is a child category please set the parent category id in here. This field is being used for building up the Breadcrumbs. 19 | 20 | ```json 21 | "path": "1/2/29", 22 | ``` 23 | 24 | This is string, list of IDs of the parent categories. Used to build the breadcrumbs more easily. 25 | 26 | ```json 27 | "name": "Hoodies & Sweatshirts", 28 | ``` 29 | 30 | This is just a category name. 31 | 32 | ```json 33 | "url_key": "hoodies-and-sweatshirts-24", 34 | "url_path": "women/tops-women/hoodies-and-sweatshirts-women/hoodies-and-sweatshirts-24", 35 | "slug": "hoodies-and-sweatshirts-24" 36 | ``` 37 | 38 | ```json 39 | "is_active": true, 40 | ``` 41 | 42 | If it's false - the category won't be displayed. 43 | 44 | ```json 45 | "position": 2, 46 | ``` 47 | 48 | Sorting position of the category on it's level 49 | 50 | ```json 51 | "level": 4, 52 | ``` 53 | 54 | The category level in the tree. By default Vue Storefront is displaying categories witht `level: 2` in the main menu. 55 | 56 | ```json 57 | "product_count": 182, 58 | ``` 59 | 60 | If it's false - the category won't be displayed. 61 | 62 | ```json 63 | "children_data": [ 64 | { 65 | "id": 27, 66 | "children_data": [] 67 | }, 68 | { 69 | "id": 28, 70 | "children_data": [] 71 | } 72 | ] 73 | ``` 74 | 75 | This is the children structure. It's being used for cosntructing the queries to get the child products. 76 | -------------------------------------------------------------------------------- /Format-product.md: -------------------------------------------------------------------------------- 1 | ## Product entity 2 | 3 | In the `Vue Storefront` there is a [defined Product type](https://github.com/DivanteLtd/vue-storefront/blob/master/core/modules/catalog/types/Product.ts) you're to use in your TypeScript code. It contains quite many optional fields. Please check the [sample-data/products.json](sample-data/products.json) to make sure which fields are trully crucial for Vue Storefront to work. 4 | 5 | Here we present the core purpose of the product properties: 6 | 7 | ```json 8 | "id": 1769, 9 | ``` 10 | 11 | This is unique product identifier, it's numeric - and it's defined as integer in the [elastic.schema.product.json](https://github.com/DivanteLtd/vue-storefront-api/blob/d7b6fe516eeb214615f54726fb382e72ff2cc34b/config/elastic.schema.product.json#L15) however nowhere in the code is it used as `intval`. That means when you need to have product IDs presented as GUID's or strings - please just feel free to [modify the schema](https://github.com/DivanteLtd/vue-storefront-api/blob/d7b6fe516eeb214615f54726fb382e72ff2cc34b/config/elastic.schema.product.json#L15) and run `yarn db rebuild`. Should be fine! 12 | 13 | 14 | ```json 15 | "name": "Chloe Compete Tank", 16 | ``` 17 | 18 | This is just a product name :-) 19 | 20 | ```json 21 | "image": "/w/t/wt06-blue_main.jpg", 22 | ``` 23 | 24 | Proudct image - by deafult it's relative because [`vue-storefront-api/img` endpoint](https://github.com/DivanteLtd/vue-storefront-api/blob/d7b6fe516eeb214615f54726fb382e72ff2cc34b/src/api/img.js#L38) uses this relative URL against the base platform images URL/CDN in order to generate the thumbnail. 25 | 26 | **Note:** If you like to use the **absolute urls** that's not a problem. Please put the absolute URL in this field and then make sure the `vue-storefront` knows about it by setting the `config.images.useExactUrlsNoProxy`. It will use the exact image URLs without the resizer. You can also do the trick to use the resizer still with the absolute URLS, by setting the `config.images.baseUrl` to the URL address containing `{{url}}` placeholder. Something like: [`https://demo.vuestorefront.io/img/?url={{url}}`](https://github.com/DivanteLtd/vue-storefront-api/blob/d7b6fe516eeb214615f54726fb382e72ff2cc34b/src/api/img.js#L28). The [magic happens here](https://github.com/DivanteLtd/vue-storefront/blob/5602c144a36e7829698240f5123224e2aad6fe4e/core/helpers/index.ts#L61). 27 | 28 | ```json 29 | "sku": "WT06", 30 | ``` 31 | 32 | Stock Keeping Unit is a unique string. Format is not restricted to any form. It's used as a cache key for products. It's also being used for figuring out the selected configurable variant of `configurable` product. 33 | 34 | ```json 35 | "url_key": "chloe-compete-tank", 36 | "url_path": "women/tops-women/tanks-women/bras-and-tanks-26/chloe-compete-tank-1769.html", 37 | ``` 38 | 39 | As of Vue Storefront 1.9 the `url_key` is no longer used for URL routing. It's just a string and well.. it's optional. The `urlo_path` however is a must. It must be also unique across all routable URL addresses, because it's being used by the [Url Dispatcher](https://github.com/DivanteLtd/vue-storefront/blob/bb6f8e70b5587ed73c457d382c7ac93bd14db413/core/modules/url/store/actions.ts#L59) to map the URL to specific product for PDP. 40 | 41 | ```json 42 | "type_id": "configurable", 43 | ``` 44 | 45 | Vue Storefront is supporting the following product types: 46 | - `simple` - [simple product](https://demo.storefrontcloud.io/gear/gear-3/wayfarer-messenger-bag-4.html) with no configurable options, 47 | - `configurable` - [product with variants](https://demo.storefrontcloud.io/men/bottoms-men/shorts-men/shorts-19/sol-active-short-1007.html?childSku=MSH10) - they're assigned in the `configurable_children` and the options used to select the proper variant (like `color` and `size`) are defined in the `configurable_options`, 48 | - `bundle` - [product that consits other products](https://demo.storefrontcloud.io/gear/gear-3/sprite-yoga-companion-kit-45.html) under single virtual SKU. The sub-products can be configured / checked / unchecked. 49 | - `grouped` - [product grouping different products](https://demo.storefrontcloud.io/gear/gear-3/set-of-sprite-yoga-straps-2046.html) that are added to the cart as separate items, 50 | - `virtual` - virtual products are partially supported (Vue Storefront is not asking the user for shipping info if there are just virtuals in the cart, that's it). 51 | 52 | The `routes` in the Vue Storefront [are customizable](https://github.com/DivanteLtd/vue-storefront/blob/bb6f8e70b5587ed73c457d382c7ac93bd14db413/src/themes/default/router/index.js#L43) to specific product types so you can create different PDP's for specific types of products. 53 | 54 | ```json 55 | "price": 39, 56 | ``` 57 | 58 | This is the price that Vue Storefront [treats as Nett price](https://github.com/DivanteLtd/vue-storefront/blob/develop/core/modules/catalog/helpers/taxCalc.ts) (not including tax). The thing is that by default Vue Storefront is taking the prices from the Elastic/Backend but you can switch the `config.tax.calculateServerSide=false` in order to start calculating the taxes in the frontend app (for example based on the current address). 59 | 60 | ```json 61 | "special_price": 0, 62 | ``` 63 | 64 | This is a special price (if set, the `price` will be crossed over in the UI) - also Nett. 65 | 66 | ```json 67 | "price_incl_tax": null, 68 | "special_price_incl_tax": null, 69 | ``` 70 | 71 | If these fields are set, Vue Storefront is showing these prices as default, end user prices in the store. They should include all the taxes. 72 | 73 | ```json 74 | "special_to_date": null, 75 | "special_from_date": null, 76 | ``` 77 | 78 | Special price field is limited in the time by these dates (should be ISO date format). [See how](https://github.com/DivanteLtd/vue-storefront/blob/5602c144a36e7829698240f5123224e2aad6fe4e/core/modules/catalog/helpers/taxCalc.ts#L3). 79 | 80 | ```json 81 | "status": 1, 82 | ``` 83 | 84 | Product status: 85 | - <=1 - product is enabledd, 86 | - 2 - product is disabled, 87 | - 3 - product is out of stock (however VSF is rather checking the `stock.is_in_stock` property). 88 | 89 | 90 | ```json 91 | "visibility": 4, 92 | ``` 93 | 94 | Visibility status: 95 | - 1 - not visible (won't be displayed in the listings), 96 | - 2 - visible in catalog, 97 | - 3 - visible in search, 98 | - 4 - visible in both 99 | 100 | ```json 101 | "size": null, 102 | "color": null, 103 | ``` 104 | 105 | Color, size - typically a numeric indexes. Vue Storefront for all [non system properties](https://github.com/DivanteLtd/vue-storefront/blob/bb6f8e70b5587ed73c457d382c7ac93bd14db413/config/default.json#L181) is loading the `attribute` definitions. 106 | 107 | If the definition exists then if the type is `select` or `multiselect` the value of the property is used as a index in the attribute values dictionary. [Read more on attributes](Format-attribute.md). Otherwise it's being used as a text. 108 | 109 | So you can put any color name you like in this field and it still could be used for product browsing. This is for example how the [`bigcommerce2vuestorefront`](https://github.com/DivanteLtd/bigcommerce2vuestorefront/blob/42efbb05aef1f37bfb944910b662d39c5de5e37a/src/templates/product.js#L77) integration works. It's not using the attribute metadata at all because for some platforms using kind of Wordpress like semantics it's very hard to create an attribute dictionary. 110 | 111 | ```json 112 | "size_options": [ 113 | 167, 114 | 168, 115 | 169, 116 | 170, 117 | 171 118 | ], 119 | "color_options": [ 120 | 50, 121 | 58, 122 | 60 123 | ], 124 | ``` 125 | 126 | For any property (color and sizes are just an examples) you might want to create an `propertyName + "_options"` helper which [is being used for product filtering](https://github.com/DivanteLtd/vue-storefront/blob/5602c144a36e7829698240f5123224e2aad6fe4e/core/lib/search/adapter/api/elasticsearchQuery.js#L72). In this case it consist of all `configurable_children` colors and sizes. 127 | 128 | ```json 129 | "category_ids": [ 130 | "26" 131 | ], 132 | ``` 133 | 134 | Category IDs (don't have to be numeric but usually they are :-)). This field is used for product filtering on the `Category.vue` page in Vue Storefront. 135 | 136 | ```json 137 | "category": [ 138 | { 139 | "category_id": 26, 140 | "name": "Bras & Tanks", 141 | "slug": "bras-and-tanks-26", 142 | "path": "women/tops-women/tanks-women/bras-and-tanks-26" 143 | } 144 | ], 145 | ``` 146 | Additionaly to `category_ids` we have a `category` collection which is denormalized set of categories assigned to this product. It's being used in `SearchPanel` [for generating the output categories](https://github.com/DivanteLtd/vue-storefront/blob/5602c144a36e7829698240f5123224e2aad6fe4e/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.vue#L119) in the search results and .. probably that's all. So if you disable this feature, the `category` property is no longer needed. 147 | 148 | ```json 149 | "media_gallery": [ 150 | { 151 | "image": "/w/t/wt06-blue_main.jpg", 152 | "pos": 1, 153 | "typ": "image", 154 | "lab": null, 155 | "vid": null 156 | }, 157 | { 158 | "image": "/w/t/wt06-blue_back.jpg", 159 | "pos": 2, 160 | "typ": "image", 161 | "lab": null, 162 | "vid": null 163 | } 164 | ], 165 | ``` 166 | 167 | This is just a list of images used by the `ProductGallery` component. Paths can be relative or absolute - exactly the same as with `product.image`. 168 | 169 | ```json 170 | "configurable_options": [ 171 | { 172 | "id": 300, 173 | "attribute_id": "93", 174 | "label": "Color", 175 | "position": 1, 176 | "values": [ 177 | { 178 | "value_index": 50, 179 | "label": "Blue" 180 | }, 181 | { 182 | "value_index": 58, 183 | "label": "Red" 184 | }, 185 | { 186 | "value_index": 60, 187 | "label": "Yellow" 188 | } 189 | ], 190 | "product_id": 1769, 191 | "attribute_code": "color" 192 | }, 193 | { 194 | "id": 301, 195 | "attribute_id": "142", 196 | "label": "Size", 197 | "position": 0, 198 | "values": [ 199 | { 200 | "value_index": 167, 201 | "label": "XS" 202 | }, 203 | { 204 | "value_index": 168, 205 | "label": "S" 206 | }, 207 | { 208 | "value_index": 169, 209 | "label": "M" 210 | }, 211 | { 212 | "value_index": 170, 213 | "label": "L" 214 | }, 215 | { 216 | "value_index": 171, 217 | "label": "XL" 218 | } 219 | ], 220 | "product_id": 1769, 221 | "attribute_code": "size" 222 | } 223 | ], 224 | ``` 225 | 226 | This collection contains all configurable options that can be used to identify the `simple` product, assigned in the `configurable_children` collection. Usually it's a set of available `colors` and `sizes`. It's being used to construct the Color/Size switcher on `Product.vue` page. If you set the proper `label`'s then the `attribute_id` is not required. It means you don't have to have the [attribute defined in the dictionary](Format-attribute.md). It's pretty usefull option for the platforms that doesn't support attribute dictionaries like [BigCommerce](https://github.com/DivanteLtd/bigcommerce2vuestorefront/blob/42efbb05aef1f37bfb944910b662d39c5de5e37a/src/templates/product.js#L90). 227 | 228 | ```json 229 | "stock": [ 230 | { 231 | "is_in_stock": true, 232 | "qty": 0 233 | } 234 | ], 235 | ``` 236 | 237 | Stock is being used to check if the product is available or not. There is also a `api/stock` endpoint (to be implemented dynamically) to make sure the Vue Storefront is up to date with the data. This Elastic based stock is used mostly for filtering out unavailable products (and not as a source of truth for adding to the cart). 238 | 239 | 240 | ```json 241 | "configurable_children": [ 242 | { 243 | "type_id": null, 244 | "sku": "WT06-XS-Blue", 245 | "special_price": 0, 246 | "special_to_date": null, 247 | "special_from_date": null, 248 | "name": "Chloe Compete Tank-XS-Blue - tier price", 249 | "price": 39, 250 | "price_incl_tax": null, 251 | "special_price_incl_tax": null, 252 | "id": 1754, 253 | "image": "/w/t/wt06-blue_main.jpg", 254 | "url_key": "chloe-compete-tank-xs-blue", 255 | "url_path": null, 256 | "status": 1, 257 | "size": "167", 258 | "color": "50" 259 | }, 260 | { 261 | "type_id": null, 262 | "sku": "WT06-XS-Red", 263 | "special_price": 0, 264 | "special_to_date": null, 265 | "special_from_date": null, 266 | "name": "Chloe Compete Tank-XS-Red", 267 | "price": 39, 268 | "price_incl_tax": null, 269 | "special_price_incl_tax": null, 270 | "id": 1755, 271 | "image": "/w/t/wt06-red_main.jpg", 272 | "url_key": "chloe-compete-tank-xs-red", 273 | "url_path": null, 274 | "status": 1, 275 | "size": "167", 276 | "color": "58" 277 | } 278 | ] 279 | }, 280 | ``` 281 | 282 | All `configurable` products consists of `simple` products assigned in the `configurable_childdren` collection. Those are the ones finally ordered. The important feature of `configurable_children` collection is that it should consist only the properties that differentiate these products from the main `configurable` one. Probably you could skip the `name`. It's because each product is being **merged** with it's `configurable_children` - well: selected configurable children when user is switching the color and sizes. There is a Vuex action `product/confgure` doing exactly this merge operation. 283 | 284 | 285 | ### What was skipped? 286 | 287 | We havent' described the Bundle and Grouped products. It's on our TODO :) -------------------------------------------------------------------------------- /How to configure Vue Storefront.md: -------------------------------------------------------------------------------- 1 | ## How to configure Vue Storefront 2 | 3 | **Layer A** integration is very simple. 4 | 5 | By default Vue Storefront uses ES index named `vue_storefront_catalog`. Please apply the changes accordingly to: 6 | - `vue-storefront` [config file](https://github.com/DivanteLtd/vue-storefront/tree/master/config) `local.json` to point to right index name, 7 | - `vue-storefront-api` [config file](https://github.com/DivanteLtd/vue-storefront-api/tree/master/config) `local.json` to point to right index name. 8 | 9 | Restart `vue-storefront` and `vue-storefront-api`. 10 | 11 | **Please note**: By default `vue-storefront` is using `vue-storefront-api` as a proxy to ElasticSearch. You might want to use the Elastic connection directly. In that case feel free to put the `http://localhost:9200` or whatever Elastic URL you have as a `elasticsearch.host` to `vue-storefront/config/local.json`. 12 | 13 | **Note**: When connecting to Elastic directly, please make sure to change the `config.elasticsearch.queryMethod` to `POST` in [the config file](https://github.com/DivanteLtd/vue-storefront/blob/8f3ce717a823ef3a5c7469082b8a8bcb36abb5c1/config/default.json#L56). 14 | 15 | The integration requires You to change the `config/local.json` to set the proper endpoints for [Dynamic API calls](Dynamic%20API%20specification.md). 16 | 17 | The `cart` section: 18 | - `create_endpoint` - Should point to: [cart/create](Dynamic%20API%20specification.md#post-vsbridgecartcreate) 19 | - `updateitem_endpoint` - Should point to: [cart/update](Dynamic%20API%20specification.md#post-vsbridgecartupdate) 20 | - `deleteitem_endpoint` - Should point to: [cart/delete](Dynamic%20API%20specification.md#post-vsbridgecartdelete) 21 | - `pull_endpoint` - Should point to: [cart/pull](Dynamic%20API%20specification.md#get-vsbridgecartpull) 22 | - `totals_endpoint` - Should point to: [cart/totals](Dynamic%20API%20specification.md#get-vsbridgecarttotals) 23 | - `paymentmethods_endpoint` - Should point to: [cart/payment-methods](Dynamic%20API%20specification.md#get-vsbridgecartpayment-methods) 24 | - `shippingmethods_endpoint` - Should point to: [cart/shipping-methods](Dynamic%20API%20specification.md#post-vsbridgecartshipping-methods) 25 | - `shippinginfo_endpoint` - Should point to: [cart/shipping-information](Dynamic%20API%20specification.md#post-vsbridgecartshipping-information) 26 | - `collecttotals_endpoint` - Should point to: [cart/collect-totals](Dynamic%20API%20specification.md#post-vsbridgecartcollect-totals) 27 | - `deletecoupon_endpoint` - Should point to: [cart/delete-coupon](Dynamic%20API%20specification.md#post-vsbridgecartdelete-coupon) 28 | - `applycoupon_endpoint` - Should point to: [cart/apply-coupon](Dynamic%20API%20specification.md#post-vsbridgecartapply-coupon) 29 | 30 | The `users` section: 31 | - `history_endpoint` - Should point to: [user/order-history](Dynamic%20API%20specification.md#get-vsbridgeuserorder-history) 32 | - `resetPassword_endpoint` - Should point to: [user/resetPassword](Dynamic%20API%20specification.md#post-vsbridgeuserresetpassword) 33 | - `changePassword_endpoint` - Should point to: [user/changePassword](Dynamic%20API%20specification.md#post-vsbridgeuserchangepassword) 34 | - `login_endpoint` - Should point to: [user/login](Dynamic%20API%20specification.md#post-vsbridgeuserlogin) 35 | - `create_endpoint` - Should point to: [user/create](Dynamic%20API%20specification.md#post-vsbridgeusercreate) 36 | - `me_endpoint` - Should point to: [user/me](Dynamic%20API%20specification.md#get-vsbridgeuserme) 37 | - `refresh_endpoint` - Should point to: [user/refresh](Dynamic%20API%20specification.md#post-vsbridgeuserrefresh) 38 | 39 | The `stock` section: 40 | - `endpoint` - Should point to: [stock/check](Dynamic%20API%20specification.md#get-vsbridgestockchecksku) 41 | 42 | The `orders` section: 43 | - `endpoint` - Should point to: [order/create](Dynamic%20API%20specification.md#post-vsbridgeordercreate) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Divante Ltd. 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 | -------------------------------------------------------------------------------- /Prices how-to.md: -------------------------------------------------------------------------------- 1 | # Prices how-to 2 | 3 | Vue Storefront has two modes of calculating the product prices: 4 | - Client side (when `config.tax.calculateServerSide` is set to `false`) - that can be usefull in case the tax should be recalculated based on the address change, 5 | - Server side (when `config.tax.calculateServerSide` is set to `true`) - which is our default mode. 6 | 7 | Depending on the mode, taxes are calulated by [`taxCalc.ts` client side](https://github.com/DivanteLtd/vue-storefront/blob/5f2b5cd6a8496a60884c091e8509d3b58b7a0358/core/modules/catalog/helpers/taxCalc.ts#L74) or [`taxcalc.js` server side](https://github.com/DivanteLtd/vue-storefront-api/blob/d3d0e7892cd063bbd69e545f3f2b6fdd9843d524/src/lib/taxcalc.js#L251-L253). 8 | 9 | You may see that both these files are applying **exactly** the same logic. 10 | 11 | In order to calculate the prices and taxes we need first toget the proper tax rate. It's based on [`taxrate`](https://github.com/DivanteLtd/vue-storefront-integration-sdk#taxrate-entity) entity, stored in the Elastic. Each product can have the property [`product.tax_class_id`](https://github.com/DivanteLtd/vue-storefront/blob/5f2b5cd6a8496a60884c091e8509d3b58b7a0358/core/modules/catalog/helpers/taxCalc.ts#L213) set. Depending on it's value Vue Storefront is applying the `taxrate`, it's also applying the [country and region to the filter](https://github.com/DivanteLtd/vue-storefront/blob/5f2b5cd6a8496a60884c091e8509d3b58b7a0358/core/modules/catalog/helpers/taxCalc.ts#L226). 12 | 13 | **Note:** We're currently not supporting searching the tax rules by `customer_tax_class_id` neither by the `tax_postcode` fields of `taxrate` entity. Pull requests more than welcome ;) 14 | 15 | After getting the right tax rate we can calculate the prices. 16 | 17 | We've got the following price fields priority in the VS: 18 | - `final_price` - if set, depending on the `config.tax.finalPriceIncludesTax` - it's taken as final price or Net final price, 19 | - `special_price` - if it's set and it's lower than `price` it will replace the `price` and the `price` value will be set into `original_price` property, 20 | - `price` - if set, dedending on the `config.tax.sourcePriceIncludesTax` - it's taken as final price or Net final price. 21 | 22 | Depending on the `config.tax.finalPriceIncludesTax` and `config.tax.sourcePriceIncludesTax` settings Vue Storefront calculates the prices and stores them into following fields. 23 | 24 | Product Special price: 25 | - `special_price` - optional, if set - it's always Net price, 26 | - `special_price_incl_tax` - optional, if set - it's always price after taxes, 27 | - `special_price_tax` - optional, if set it's the tax amount. 28 | 29 | Product Regular price: 30 | - `price` - required, if set - it's always Net price, 31 | - `price_incl_tax` - required, if set - it's always price after taxes, 32 | - `price_tax` - required, if set it's the tax amount, 33 | 34 | Product Final price: 35 | - `final_price` - optional, if set - it's always Net price, 36 | - `final_price_incl_tax` - optional, if set - it's always price after taxes, 37 | - `final_price_tax` - optional, if set it's the tax amount, 38 | 39 | Product Original price (set only if `final_price` or `special_price` are lower than `price`): 40 | - `original_price` - optional, if set - it's always Net price, 41 | - `original_price_incl_tax` - optional, if set - it's always price after taxes, 42 | - `original_price_tax` - optional, if set it's the tax amount. 43 | 44 | **Note:** The prices are being set for all `configurable_children` with the exact same format 45 | **Note:** If any of the `configurable_children` has the price lower than the main product, the main product price will be updated accordingly. 46 | 47 | #### Cart prices 48 | Additionally to product prices, the cart item prices in cart/totals endpoint contains following price keys which are always Net prices: 49 | - `price` 50 | - `base_price` 51 | - `row_total` 52 | - `base_row_total` 53 | each of them also have their gross price equivalent suffixed by `_incl_tax` 54 | 55 | The cart/totals has a key "total_segments" in which there are two segments: `subtotal` and `grand_total`. The amounts returned there behave differently for each store depending on the backend setting. 56 | For Magento backend, this is described here: https://docs.magento.com/m2/ce/user_guide/configuration/sales/tax.html#shopping-cart-display-settings 57 | So make sure to adjust it in your custom integration according to your business logic. 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Storefront Custom Integration Tutorial 2 | 3 | Vue Storefront is platform agnostic which means it can be connected to virtually any eCommerce backend and CMS. This repository is created to make the integration with any 3rd party backend platform as easy as possible. 4 | 5 | **Note:** This tutorial shows how to build a **Generic** integration for any custom backend API. This is the recommended approach giving you most of the Vue Storefront features of the box. [Check the other options...](Vue%20Storefront%20Integration%20Architecture.pdf) 6 | 7 | ## Three steps for the integration 8 | 9 | - **Step One** Vue Storefront uses Elastic Search as backend for all catalog operations. We do have **three** default types of entities that must be supported: `product`, `category`, `attribute` and **three optional entities** `taxrule`, `cms_block` and `cms_page` in the ES. You may find some sample-data json [files in `sample-data` subdirectory](sample-data). 10 | 11 | - **Step Two** The second step is to support the **dynamic calls** that are used to synchronize shopping carts, promotion rules, user accounts, and so on. To have this step accomplished you'll need to implement the actuall endpoints business logic. Check the [boilerplate API implementation in Express.js](sample-api-js) 12 | 13 | - **Step Three** Is to configure `vue-storefront` to use the right set of endpoints from Step Two. 14 | 15 | 16 | ## Tutorial 17 | 18 | Now, we're to go through all three steps to integrate Vue Storefront with custom or 3rd party eCommerce platform. 19 | 20 | First, make sure you've got the [vue-storefront and vue-storefront-api installed](https://docs.vuestorefront.io/guide/installation/linux-mac.html#installing-the-vue-storefront-api-locally) on your local machine, up and running. Opening the [http://localhost:3000](http://localhost:3000) should display default Vue Storefront theme with demo products. 21 | 22 | **Note:** As we'll be using extensively Elastic Search for the next steps in this tutorial, make sure you've got the right tooling to browse the ES indexes. I'm using [es-head](https://chrome.google.com/webstore/detail/elasticsearch-head/ffmkiejjmecolpfloofpjologoblkegm). Pretty easy to use and simple tool, provided as a Google Chrome plugin. 23 | 24 | 25 | ### **Empty the `vue_storefront_catalog` index**. 26 | This is the default Vue Storefront index which is configured in both `vue-storefront` and `vue-storefront-api` projects - in the `config/local.json`, `elasticsearch.indices` section. We'll be using "default". 27 | 28 | First, please go to `vue-storefront-api` directory with the following command: 29 | 30 | ```bash 31 | $ cd ./vue-storefront-api 32 | ``` 33 | 34 | Then you can empty the default index: 35 | 36 | ```bash 37 | $ yarn db new 38 | yarn run v1.17.3 39 | $ node scripts/db.js new 40 | Elasticsearch INFO: 2019-09-06T19:32:23Z 41 | Adding iconnection to http://localhost:9200/ 42 | 43 | ** Hello! I am going to create s cNEW ES index 44 | Elasticsearch DEBUG: 2019-09-06T19:32:23Z 45 | starting request { 46 | "method": "DELETE", 47 | "path": "/*/_alias/vue_storefront_catalog", 48 | "query": {} 49 | } 50 | 51 | ... 52 | ``` 53 | 54 | **Note:** Please make sure your local Elastic instance is up and running. After you've got the `vue-storefront` plus `vue-storefront-api` installed, you can ensure it by just running `docker-compose up -d` in the `vue-storefront-api` directory. 55 | 56 | ### **Import data**. 57 | In your custom integration, you'll probably be pumping the data directly to ElasticSearch as it changed in the platform admin panel. 58 | 59 | This is exactly how other Vue Storefront integrations work. 60 | You might want to get inspired by: 61 | - [`magento2-vsbridge-indexer`](https://github.com/DivanteLtd/magento2-vsbridge-indexer) - the PHP based integration for Magento2, 62 | - [`shopware2vuestorefront`](https://github.com/DivanteLtd/shopware2vuestorefront/tree/master/vsf-shopware-indexer) - which is using a NodeJS app to pull the data from Shopware API and push it to Elastic, 63 | - [`spree2vuestorefront`](https://github.com/spark-solutions/spree2vuestorefront/) - which is putting thte data to Elastic directly from Ruby code, from Spree Commerce database, 64 | - [See other integrations ...](https://github.com/frqnck/awesome-vue-storefront#github-repos) 65 | 66 | In our example, we'll push the static JSON files from `sample-data` directly to the ElasticSearch index. Then I'll explain these data formats in details to let you prepare such an automatic exporter on your own. 67 | 68 | To push the data into ElasticSearch we'll be using a simple NodeJS tool [located in the sample-data folder](https://github.com/DivanteLtd/vue-storefront-integration-boilerplate/blob/tutorial/sample-data/import.js). 69 | 70 | Now we can import the data: 71 | 72 | ```bash 73 | $ cd ./vue-storefront-integration-boilerplate/sample-data/ 74 | $ yarn install Or npm install 75 | $ node import.js products.json product vue_storefront_catalog 76 | Importing product { id: 1769, 77 | name: 'Chloe Compete Tank', 78 | image: '/w/t/wt06-blue_main.jpg', 79 | sku: 'WT06', 80 | url_key: 'chloe-compete-tank', 81 | url_path: 82 | 'women/tops-women/tanks-women/bras-and-tanks-26/chloe-compete-tank-1769.html', 83 | type_id: 'configurable', 84 | price: 39, 85 | special_price: 0, 86 | price_incl_tax: null, 87 | special_price_incl_tax: null, 88 | special_to_date: null, 89 | special_from_date: null, 90 | status: 1, 91 | size: null, 92 | color: null, 93 | size_options: [ 167, 168, 169, 170, 171 ], 94 | color_options: [ 50, 58, 60 ], 95 | category_ids: [ '26' ], 96 | media_gallery: 97 | ... 98 | { _index: 'vue_storefront_catalog', 99 | _type: 'product', 100 | _id: '1433', 101 | _version: 2, 102 | result: 'updated', 103 | _shards: { total: 2, successful: 1, failed: 0 }, 104 | created: false } 105 | { _index: 'vue_storefront_catalog', 106 | _type: 'product', 107 | _id: '1529', 108 | _version: 2, 109 | result: 'updated', 110 | _shards: { total: 2, successful: 1, failed: 0 }, 111 | created: false } 112 | ``` 113 | 114 | Then please do execute the same import scripts for `atttribute` and `category` entities: 115 | 116 | ```bash 117 | $ node import.js attributes.json attribute vue_storefront_catalog 118 | $ node import.js categories.json category vue_storefront_catalog 119 | ``` 120 | 121 | After importing the data, we need to make sure the Vue Storefront Elastic index schema has been properly applied. To ensure this, we'll use the [Database tool](https://docs.vuestorefront.io/guide/data/database-tool.html) used previously to clear out the index - once again: 122 | 123 | ```bash 124 | $ yarn db rebuild 125 | yarn run v1.17.3 126 | $ node scripts/db.js rebuild 127 | Elasticsearch INFO: 2019-09-06T20:13:28Z 128 | Adding connection to http://localhost:9200/ 129 | 130 | ** Hello! I am going to rebuild EXISTING ES index to fix the schema 131 | ** Creating temporary index vue_storefront_catalog_1567800809 132 | Elasticsearch DEBUG: 2019-09-06T20:13:28Z 133 | starting request { 134 | "method": "DELETE", 135 | "path": "/*/_alias/vue_storefront_catalog_1567800809", 136 | "query": {} 137 | } 138 | ``` 139 | 140 | After data has been imported you can check if it works by opening `http://localhost:3000` and using the Search feature: 141 | 142 | ![Search Feature](https://github.com/DivanteLtd/vue-storefront-integration-boilerplate/blob/tutorial/screens/screen_0_products.png "Search Feature") 143 | 144 | 145 | **Congratulations!** Now it's a good moment to take a deep breath and study the data formats we'd just imported to create your own mapper from the custom platform of your choice to Vue Storefront format. 146 | 147 | **Note:** please make sure that you use non-zero IDs in the following entities to avoid unexpected behavior. 148 | 149 | ### Product entity 150 | 151 | You might have seen that our data formats are pretty much similar to Magento formats. We've simplified them and aggregated. **Some parts are denormalized** on purpose. We're trying to avoid the relations known from the standard databases and rather use the [DTO](https://en.wikipedia.org/wiki/Data_transfer_object) concept. For example, Product is a DTO containing all information necessary to display the PDP (Product Details Page): including `media_gallery`, `configurable_children` and other features. It's then fairly easy to cache the data for the Offline mode and performance. 152 | 153 | [Read the full Product entity specification](Format-product.md) 154 | 155 | ### Attribute entity 156 | 157 | Vue Storefront uses the attributes meta data dictionaries saved in the `attribute` entities. They're related to the `product`. The `attribute.attribute_code` represents the `product[attribute_code]` proeprty - when defined. When not, the `product[attribute_code]` is being used as a plain tetxt. 158 | 159 | [Read more on why Attributes are important](Format-attribute.md) 160 | 161 | ### Category entity 162 | 163 | Categories are being used mostly for building tree navigation. Vue Storefront uses the [dynamic-catetgories-prefetching](https://docs.vuestorefront.io/guide/basics/configuration.html#dynamic-categories-prefetching). Please make sure that **all the categories** are indexed on the main level - even if they exist as a `category.children_data` assigned to any other category. 164 | 165 | [Read the Category format specification](Format-category.md) 166 | 167 | 168 | ### TaxRate entity 169 | 170 | **Note:** TaxRates are skipped from `sample-data` as they're not crucial to display the products and categories in Vue Storefront (as long as the taxes are calculated before product pricing is imported to Elastic) 171 | 172 | Here is the data format: 173 | 174 | ```json 175 | { 176 | "id": 2, 177 | "code": "Poland", 178 | "priority": 0, 179 | "position": 0, 180 | "customer_tax_class_ids": [3], 181 | "product_tax_class_ids": [2], 182 | "tax_rate_ids": [4], 183 | "calculate_subtotal": false, 184 | "rates": [ 185 | { 186 | "id": 4, 187 | "tax_country_id": "PL", 188 | "tax_region_id": 0, 189 | "tax_postcode": "*", 190 | "rate": 23, 191 | "code": "VAT23%", 192 | "titles": [] 193 | } 194 | ] 195 | } 196 | ``` 197 | 198 | To read more on how tax rates are processed when `config.tax.calculateServerSide=false`, please read the [Prices how to](Prices%20how-to.md) and then [study the taxCalc.ts](https://github.com/DivanteLtd/vue-storefront/blob/develop/core/modules/catalog/helpers/taxCalc.ts). 199 | 200 | 201 | ### Write your API adapter for dynamic requests 202 | 203 | Vue Storefront doesn't store any user data, order or payment information. Even shopping carts are only stored locally in the browser and then synced with the server (`cart/merge` Vuex action). 204 | 205 | Whenever a product is added to the cart, or user authorization is performed, there is an API request executed. 206 | 207 | [Read more on the required API endpoints you must provide to have Vue Storefront synchronized](Dynamic%20API%20specification.md) 208 | 209 | **Note:** If you're to use Vue Storefront just for catalog browsing purposes you can probably skip this step. In that case please make sure your `vue-storefront` instance is properly configured with the `config.cart.synchronize=false` and `config.cart.synchronize_totals=false`. 210 | 211 | 212 | ### Configure vue-storefront 213 | 214 | All You need to do is to set the proper dynamic API endpoints in the `config/local.json`. [Here you have the details](How%20to%20configure%20Vue%20Storefront.md). 215 | 216 | 217 | # Support 218 | 219 | This is a project under MIT license so it's just AS IS :) However, if you're planning to add the new platform to the Vue Storefront ecosystem and publish it freely as an open-source - we'll do our best to support you! 220 | 221 | Please feel free to contact the core team at [Vue Storefront Forum](https://forum.vuestorefront.io/c/development/integrations), on [Slack channel](http://slack.vuestorefront.io) or via contributors@vuestorefront.io 222 | -------------------------------------------------------------------------------- /Vue Storefront Integration Architecture.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivanteLtd/storefront-integration-sdk/080a26c9f2fe674ed03bfa67579863ac3e36620e/Vue Storefront Integration Architecture.pdf -------------------------------------------------------------------------------- /Vue Storefront Integration Architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivanteLtd/storefront-integration-sdk/080a26c9f2fe674ed03bfa67579863ac3e36620e/Vue Storefront Integration Architecture.pptx -------------------------------------------------------------------------------- /sample-api-js/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Divante Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /sample-api-js/Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /sample-api-js/README.md: -------------------------------------------------------------------------------- 1 | EXAMPLE REST API backend for vue-storefront 2 | =========================================== 3 | 4 | This is a mocking backend service for [vue-storefront](https://github.com/DivanteLtd/vue-storefront). 5 | Read more on how to [integrate custom backend](https://github.com/DivanteLtd/vue-storefront-integration-sdk) with Vue Storefront. 6 | 7 | 8 | ## How to start? 9 | 10 | Please just run the app in the development mode: 11 | 12 | `yarn; yarn dev` 13 | 14 | Then, please make surre you've [configured the Vue Storefront](https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/How%20to%20configure%20Vue%20Storefront.md) to use the right API endpoints. 15 | 16 | ## Vue Storefront 17 | 18 | Vue Storefront is a standalone [PWA](https://developers.google.com/web/progressive-web-apps/) (Progressive Web Application ) storefront for your eCommerce, possible to connect with any eCommerce backend (eg. Magento, Prestashop or Shopware) through the API. 19 | 20 | Vue Storefront is and always will be in the open source. Anyone can use and support the project, we want it to be a tool for the improvement of the shopping experience. The project is still in the prove of concept phase. We are looking for Contributors and Designer willing to help us the the solution development. 21 | 22 | License 23 | ------- 24 | 25 | [MIT](./LICENSE) 26 | -------------------------------------------------------------------------------- /sample-api-js/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', { 5 | targets: { 6 | node: "8" 7 | } 8 | } 9 | ] 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /sample-api-js/config/.gitignore: -------------------------------------------------------------------------------- 1 | *.extension.json -------------------------------------------------------------------------------- /sample-api-js/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "host": "localhost", 4 | "port": 8080 5 | }, 6 | "images": { 7 | "baseUrl": "http://demo-magento2.vuestorefront.io/media/catalog/product" 8 | }, 9 | "bodyLimit": "100kb", 10 | "corsHeaders": [ 11 | "Link" 12 | ], 13 | "elasticsearch": { 14 | "host": "localhost", 15 | "port": 9200, 16 | "protocol": "http", 17 | "user": "elastic", 18 | "password": "changeme", 19 | "min_score": 0.01, 20 | "indices": [ 21 | "vue_storefront_catalog", 22 | "vue_storefront_catalog_de", 23 | "vue_storefront_catalog_it" 24 | ], 25 | "indexTypes": [ 26 | "product", 27 | "category", 28 | "cms", 29 | "attribute", 30 | "taxrule", 31 | "review" 32 | ], 33 | "apiVersion": "5.6" 34 | }, 35 | "imageable": { 36 | "maxListeners": 512, 37 | "imageSizeLimit": 1024, 38 | "whitelist": { 39 | "allowedHosts": [ 40 | ".*divante.pl", 41 | ".*vuestorefront.io" 42 | ] 43 | }, 44 | "cache": { 45 | "memory": 50, 46 | "files": 20, 47 | "items": 100 48 | }, 49 | "concurrency": 0, 50 | "counters": { 51 | "queue": 2, 52 | "process": 4 53 | }, 54 | "simd": true, 55 | "caching": { 56 | "active": false, 57 | "type": "file", 58 | "file": { 59 | "path": "/tmp/vue-storefront-api" 60 | }, 61 | "google-cloud-storage": { 62 | "libraryOptions": {}, 63 | "bucket": "", 64 | "prefix": "vue-storefront-api/image-cache" 65 | } 66 | }, 67 | "action": { 68 | "type": "local" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sample-api-js/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "debug": false, 4 | "exec": "ts-node src", 5 | "watch": ["./src"], 6 | "ext": "ts, js", 7 | "inspect": true 8 | } 9 | -------------------------------------------------------------------------------- /sample-api-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-storefront-api", 3 | "version": "1.10.0", 4 | "private": true, 5 | "description": "vue-storefront API and data services", 6 | "main": "dist", 7 | "scripts": { 8 | "dev": "nodemon", 9 | "dev:inspect": "nodemon --exec \"node --inspect -r ts-node/register src\"", 10 | "build": "npm run -s build:code", 11 | "build:code": "tsc --build", 12 | "start": "pm2 start ecosystem.json $PM2_ARGS", 13 | "prestart": "npm run -s build", 14 | "test": "eslint src", 15 | "lint": "eslint --ext .js src migrations scripts" 16 | }, 17 | "eslintConfig": { 18 | "extends": "eslint:recommended", 19 | "parserOptions": { 20 | "ecmaVersion": 2018, 21 | "sourceType": "module" 22 | }, 23 | "env": { 24 | "node": true, 25 | "es6": true 26 | }, 27 | "rules": { 28 | "no-console": 0, 29 | "no-unused-vars": 1 30 | } 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/DivanteLtd/vue-storefront-integration-sdk.git" 35 | }, 36 | "author": "Piotr Karwatka ", 37 | "license": "MIT", 38 | "dependencies": { 39 | "ajv": "^6.4.0", 40 | "ajv-keywords": "^3.4.0", 41 | "body-parser": "^1.18.2", 42 | "bodybuilder": "2.2.13", 43 | "compression": "^1.7.2", 44 | "config": "^1.30.0", 45 | "cors": "^2.8.4", 46 | "express": "^4.16.3", 47 | "humps": "^1.1.0", 48 | "jsonfile": "^4.0.0", 49 | "jwa": "^1.1.5", 50 | "jwt-simple": "^0.5.1", 51 | "lodash": "^4.17.10", 52 | "md5": "^2.2.1", 53 | "mime-types": "^2.1.18", 54 | "morgan": "^1.9.0", 55 | "pm2": "^2.10.4", 56 | "request": "^2.85.0", 57 | "request-promise-native": "^1.0.5", 58 | "resource-router-middleware": "^0.6.0", 59 | "sharp": "^0.21.0", 60 | "soap": "^0.25.0", 61 | "winston": "^2.4.2" 62 | }, 63 | "devDependencies": { 64 | "@types/body-parser": "^1.17.0", 65 | "@types/config": "^0.0.34", 66 | "@types/express": "^4.16.1", 67 | "@types/node": "^11.13.4", 68 | "cpx": "^1.5.0", 69 | "eslint": "^4.16.0", 70 | "nodemon": "^1.18.7", 71 | "ts-node": "^8.1.0", 72 | "tslib": "^1.9.3", 73 | "typescript": "3.3.*" 74 | }, 75 | "bugs": { 76 | "url": "https://github.com/DivanteLtd/vue-storefront-integration-sdk/issues" 77 | }, 78 | "homepage": "https://github.com/DivanteLtd/vue-storefront-integration-sdk/", 79 | "keywords": [ 80 | "storefront", 81 | "rest", 82 | "api", 83 | "nodejs" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /sample-api-js/src/api/cart.js: -------------------------------------------------------------------------------- 1 | import { apiStatus, apiError } from '../lib/util'; 2 | import { Router } from 'express'; 3 | 4 | export default ({ config, db }) => { 5 | 6 | let cartApi = Router(); 7 | 8 | /** 9 | * POST create a cart 10 | * req.query.token - user token 11 | * 12 | * For authorized user: 13 | * 14 | * ```bash 15 | * curl 'http://localhost:8080/api/cart/create?token=xu8h02nd66yq0gaayj4x3kpqwity02or' -X POST 16 | * ``` 17 | * 18 | * For anonymous user: 19 | * 20 | * ```bash 21 | * curl 'https://localhost:8080/api/cart/create' -X POST 22 | * ``` 23 | * 24 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgecartcreate 25 | * 26 | */ 27 | cartApi.post('/create', (req, res) => { 28 | res.json({ 29 | "code": 200, 30 | "result": "a17b9b5fb9f56652b8280bb94c52cd93" 31 | }) 32 | }) 33 | 34 | /** 35 | * POST update or add the cart item 36 | * 37 | * Request body: 38 | * 39 | * { 40 | * "cartItem":{ 41 | * "sku":"WS12-XS-Orange", 42 | * "qty":1, 43 | * "product_option":{ 44 | * "extension_attributes":{ 45 | * "custom_options":[ 46 | * 47 | * ], 48 | * "configurable_item_options":[ 49 | * { 50 | * "option_id":"93", 51 | * "option_value":"56" 52 | * }, 53 | * { 54 | * "option_id":"142", 55 | * "option_value":"167" 56 | * } 57 | * ], 58 | * "bundle_options":[ 59 | * 60 | * ] 61 | * } 62 | * }, 63 | * "quoteId":"0a8109552020cc80c99c54ad13ef5d5a" 64 | * } 65 | *} 66 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgecartupdate 67 | */ 68 | cartApi.post('/update', (req, res) => { 69 | res.json({ 70 | "code":200, 71 | "result": 72 | { 73 | "item_id":5853, 74 | "sku":"MS10-XS-Black", 75 | "qty":2, 76 | "name":"Logan HeatTec® Tee-XS-Black", 77 | "price":24, 78 | "product_type":"simple", 79 | "quote_id":"81668" 80 | } 81 | }) 82 | }) 83 | 84 | /** 85 | * POST apply the coupon code 86 | * req.query.token - user token 87 | * req.query.cartId - cart Ids 88 | * req.query.coupon - coupon 89 | * 90 | * ```bash 91 | * curl 'http://localhost:8080/api/cart/apply-coupon?token=2q1w9oixh3bukxyj947tiordnehai4td&cartId=5effb906a97ebecd6ae96e3958d04edc&coupon=ARMANi' -X POST -H 'content-type: application/json' 92 | * ``` 93 | * 94 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgecartapply-coupon 95 | */ 96 | cartApi.post('/apply-coupon', (req, res) => { 97 | res.json({ 98 | "code":200, 99 | "result":true 100 | }) 101 | }) 102 | 103 | /** 104 | * POST remove the coupon code 105 | * req.query.token - user token 106 | * req.query.cartId - cart Ids 107 | * 108 | * ```bash 109 | * curl 'https://your-domain.example.com/vsbridge/cart/delete-coupon?token=2q1w9oixh3bukxyj947tiordnehai4td&cartId=5effb906a97ebecd6ae96e3958d04edc' -X POST -H 'content-type: application/json' 110 | * ``` 111 | * 112 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgecartdelete-coupon 113 | */ 114 | cartApi.post('/delete-coupon', (req, res) => { 115 | res.json({ 116 | "code":200, 117 | "result":true 118 | }) 119 | }) 120 | 121 | /** 122 | * GET get the applied coupon code 123 | * req.query.token - user token 124 | * req.query.cartId - cart Ids 125 | * 126 | * ```bash 127 | * curl 'http://loccalhost:8080/api/cart/coupon?token=2q1w9oixh3bukxyj947tiordnehai4td&cartId=5effb906a97ebecd6ae96e3958d04edc' -H 'content-type: application/json' 128 | * ``` 129 | * 130 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#get-vsbridgecartcoupon 131 | */ 132 | cartApi.get('/coupon', (req, res) => { 133 | res.json({ 134 | "code":200, 135 | "result":"ARMANI" 136 | }) 137 | }) 138 | 139 | /** 140 | * POST delete the cart item 141 | * req.query.token - user token 142 | * 143 | * Request body; 144 | * { 145 | * "cartItem": 146 | * { 147 | * "sku":"MS10-XS-Black", 148 | * "item_id":5853, 149 | * "quoteId":"81668" 150 | * } 151 | * } 152 | * 153 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgecartdelete 154 | */ 155 | cartApi.post('/delete', (req, res) => { 156 | res.json({ 157 | "code":200, 158 | "result":true 159 | }) 160 | }) 161 | 162 | /** 163 | * GET pull the whole cart as it's currently se server side 164 | * req.query.token - user token 165 | * req.query.cartId - cartId 166 | * 167 | * For authorized users; 168 | * 169 | * ```bash 170 | * curl http://localhost:8080/api/cart/pull?token=xu8h02nd66yq0gaqwity02or 171 | * ``` 172 | * 173 | * Details: 174 | * https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#get-vsbridgecartpull 175 | */ 176 | cartApi.get('/pull', (req, res) => { 177 | res.json({ 178 | "code": 200, 179 | "result": [ 180 | { 181 | "item_id": 66257, 182 | "sku": "WS08-M-Black", 183 | "qty": 1, 184 | "name": "Minerva LumaTech™ V-Tee", 185 | "price": 32, 186 | "product_type": "configurable", 187 | "quote_id": "dceac8e2172a1ff0cfba24d757653257", 188 | "product_option": { 189 | "extension_attributes": { 190 | "configurable_item_options": [ 191 | { 192 | "option_id": "93", 193 | "option_value": 49 194 | }, 195 | { 196 | "option_id": "142", 197 | "option_value": 169 198 | } 199 | ] 200 | } 201 | } 202 | }, 203 | { 204 | "item_id": 66266, 205 | "sku": "WS08-XS-Red", 206 | "qty": 1, 207 | "name": "Minerva LumaTech™ V-Tee", 208 | "price": 32, 209 | "product_type": "configurable", 210 | "quote_id": "dceac8e2172a1ff0cfba24d757653257", 211 | "product_option": { 212 | "extension_attributes": { 213 | "configurable_item_options": [ 214 | { 215 | "option_id": "93", 216 | "option_value": 58 217 | }, 218 | { 219 | "option_id": "142", 220 | "option_value": 167 221 | } 222 | ] 223 | } 224 | } 225 | } 226 | ] 227 | }) 228 | }) 229 | 230 | /** 231 | * GET totals the cart totals 232 | * req.query.token - user token 233 | * req.query.cartId - cartId 234 | * 235 | * ```bash 236 | * curl 'http://localhost:8080/api/cart/totals?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' 237 | * ``` 238 | */ 239 | cartApi.get('/totals', (req, res) => { 240 | res.json({ 241 | "code":200, 242 | "result": 243 | { 244 | "grand_total":0, 245 | "base_currency_code":"USD", 246 | "quote_currency_code":"USD", 247 | "items_qty":1, 248 | "items": 249 | [ 250 | { 251 | "item_id":5853, 252 | "price":0, 253 | "qty":1, 254 | "row_total":0, 255 | "row_total_with_discount":0, 256 | "tax_amount":0, 257 | "tax_percent":0, 258 | "discount_amount":0, 259 | "base_discount_amount":0, 260 | "discount_percent":0, 261 | "name":"Logan HeatTec® Tee-XS-Black", 262 | "options": "[{ \"label\": \"Color\", \"value\": \"red\" }, { \"label\": \"Size\", \"value\": \"XL\" }]", 263 | "product_option":{ 264 | "extension_attributes":{ 265 | "custom_options":[ 266 | 267 | ], 268 | "configurable_item_options":[ 269 | { 270 | "option_id":"93", 271 | "option_value":"56" 272 | }, 273 | { 274 | "option_id":"142", 275 | "option_value":"167" 276 | } 277 | ], 278 | "bundle_options":[ 279 | 280 | ] 281 | } 282 | } 283 | } 284 | ], 285 | "total_segments": 286 | [ 287 | { 288 | "code":"subtotal", 289 | "title":"Subtotal", 290 | "value":0 291 | }, 292 | { 293 | "code":"shipping", 294 | "title":"Shipping & Handling", 295 | "value":null 296 | }, 297 | { 298 | "code":"tax", 299 | "title":"Tax", 300 | "value":0, 301 | "extension_attributes": 302 | { 303 | "tax_grandtotal_details":[] 304 | } 305 | }, 306 | { 307 | "code":"grand_total", 308 | "title":"Grand Total", 309 | "value":null, 310 | "area":"footer" 311 | } 312 | ] 313 | } 314 | } 315 | ) 316 | }) 317 | 318 | /** 319 | * POST /shipping-methods - available shipping methods for a given address 320 | * req.query.token - user token 321 | * req.query.cartId - cart ID if user is logged in, cart token if not 322 | * req.body.address - shipping address object 323 | * 324 | * Request body: 325 | * { 326 | * "address": 327 | * { 328 | * "country_id":"PL" 329 | * } 330 | * } 331 | * 332 | * ```bash 333 | * curl 'https://your-domain.example.com/vsbridge/cart/shipping-methods?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' --data-binary '{"address":{"country_id":"PL"}}' 334 | * 335 | */ 336 | cartApi.post('/shipping-methods', (req, res) => { 337 | res.json({ 338 | "code":200, 339 | "result": 340 | [ 341 | { 342 | "carrier_code":"flatrate", 343 | "method_code":"flatrate", 344 | "carrier_title":"Flat Rate", 345 | "method_title":"Fixed", 346 | "amount":5, 347 | "base_amount":5 348 | ,"available":true, 349 | "error_message":"", 350 | "price_excl_tax":5, 351 | "price_incl_tax":5 352 | } 353 | ] 354 | }) 355 | }) 356 | 357 | /** 358 | * GET /payment-methods - available payment methods 359 | * req.query.token - user token 360 | * req.query.cartId - cart ID if user is logged in, cart token if not 361 | * 362 | * ```bash 363 | * curl 'https://your-domain.example.com/vsbridge/cart/payment-methods?token=xu8h02nd66yq0gaayj4x3kpqwity02or&cartId=81668' -H 'content-type: application/json' 364 | * 365 | */ 366 | cartApi.get('/payment-methods', (req, res) => { 367 | res.json({ 368 | "code":200, 369 | "result": 370 | [ 371 | { 372 | "code":"cashondelivery", 373 | "title":"Cash On Delivery" 374 | }, 375 | { 376 | "code":"checkmo","title": 377 | "Check / Money order" 378 | }, 379 | { 380 | "code":"free", 381 | "title":"No Payment Information Required" 382 | } 383 | ] 384 | }) 385 | }) 386 | 387 | /** 388 | * POST /shipping-information - set shipping information for collecting cart totals after address changed 389 | * req.query.token - user token 390 | * req.query.cartId - cart ID if user is logged in, cart token if not 391 | * req.body.addressInformation - shipping address object 392 | * 393 | * Request body: 394 | * { 395 | * "addressInformation": 396 | * { 397 | * "shipping_address": 398 | * { 399 | * "country_id":"PL" 400 | * }, 401 | * "shipping_method_code":"flatrate", 402 | * "shipping_carrier_code":"flatrate" 403 | * } 404 | * } 405 | */ 406 | cartApi.post('/shipping-information', (req, res) => { 407 | res.json({ 408 | "code": 200, 409 | "result": { 410 | "payment_methods": [ 411 | { 412 | "code": "cashondelivery", 413 | "title": "Cash On Delivery" 414 | }, 415 | { 416 | "code": "checkmo", 417 | "title": "Check / Money order" 418 | } 419 | ], 420 | "totals": { 421 | "grand_total": 45.8, 422 | "subtotal": 48, 423 | "discount_amount": -8.86, 424 | "subtotal_with_discount": 39.14, 425 | "shipping_amount": 5, 426 | "shipping_discount_amount": 0, 427 | "tax_amount": 9.38, 428 | "shipping_tax_amount": 0, 429 | "base_shipping_tax_amount": 0, 430 | "subtotal_incl_tax": 59.04, 431 | "shipping_incl_tax": 5, 432 | "base_currency_code": "USD", 433 | "quote_currency_code": "USD", 434 | "items_qty": 2, 435 | "items": [ 436 | { 437 | "item_id": 5853, 438 | "price": 24, 439 | "qty": 2, 440 | "row_total": 48, 441 | "row_total_with_discount": 0, 442 | "tax_amount": 9.38, 443 | "tax_percent": 23, 444 | "discount_amount": 8.86, 445 | "discount_percent": 15, 446 | "price_incl_tax": 29.52, 447 | "row_total_incl_tax": 59.04, 448 | "base_row_total_incl_tax": 59.04, 449 | "options": "[]", 450 | "name": "Logan HeatTec® Tee-XS-Black" 451 | } 452 | ], 453 | "total_segments": [ 454 | { 455 | "code": "subtotal", 456 | "title": "Subtotal", 457 | "value": 59.04 458 | }, 459 | { 460 | "code": "shipping", 461 | "title": "Shipping & Handling (Flat Rate - Fixed)", 462 | "value": 5 463 | }, 464 | { 465 | "code": "discount", 466 | "title": "Discount", 467 | "value": -8.86 468 | }, 469 | { 470 | "code": "tax", 471 | "title": "Tax", 472 | "value": 9.38, 473 | "area": "taxes", 474 | "extension_attributes": { 475 | "tax_grandtotal_details": [ 476 | { 477 | "amount": 9.38, 478 | "rates": [ 479 | { 480 | "percent": "23", 481 | "title": "VAT23" 482 | } 483 | ], 484 | "group_id": 1 485 | } 486 | ] 487 | } 488 | }, 489 | { 490 | "code": "grand_total", 491 | "title": "Grand Total", 492 | "value": 55.18, 493 | "area": "footer" 494 | } 495 | ] 496 | } 497 | } 498 | }) 499 | }) 500 | 501 | return cartApi 502 | } 503 | -------------------------------------------------------------------------------- /sample-api-js/src/api/catalog.js: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | 3 | function _updateQueryStringParameter (uri, key, value) { 4 | var re = new RegExp('([?&])' + key + '=.*?(&|#|$)', 'i'); 5 | if (uri.match(re)) { 6 | if (value) { 7 | return uri.replace(re, '$1' + key + '=' + value + '$2'); 8 | } else { 9 | return uri.replace(re, '$1' + '$2'); 10 | } 11 | } else { 12 | var hash = ''; 13 | if (uri.indexOf('#') !== -1) { 14 | hash = uri.replace(/.*#/, '#'); 15 | uri = uri.replace(/#.*/, ''); 16 | } 17 | var separator = uri.indexOf('?') !== -1 ? '&' : '?'; 18 | return uri + separator + key + '=' + value + hash; 19 | } 20 | } 21 | 22 | /** 23 | * Elastic proxy implementation to support GET queries format (for better caching) 24 | * You might use this proxy to adjust the elastic results programmaticaly (eg. implemeting tax calc logic) 25 | * 26 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#vsbridgecatalog 27 | */ 28 | export default ({config, db}) => function (req, res, body) { 29 | let groupId = null 30 | 31 | // Request method handling: exit if not GET or POST 32 | // Other metods - like PUT, DELETE etc. should be available only for authorized users or not available at all) 33 | if (!(req.method === 'GET' || req.method === 'POST' || req.method === 'OPTIONS')) { 34 | throw new Error('ERROR: ' + req.method + ' request method is not supported.') 35 | } 36 | 37 | let requestBody = {} 38 | if (req.method === 'GET') { 39 | if (req.query.request) { // this is in fact optional 40 | requestBody = JSON.parse(decodeURIComponent(req.query.request)) 41 | console.log(requestBody) 42 | } 43 | } else { 44 | requestBody = req.body 45 | } 46 | 47 | const urlSegments = req.url.split('/'); 48 | 49 | let indexName = '' 50 | let entityType = '' 51 | if (urlSegments.length < 2) { throw new Error('No index name given in the URL. Please do use following URL format: /api/catalog//_search') } else { 52 | indexName = urlSegments[1]; 53 | 54 | if (urlSegments.length > 2) { entityType = urlSegments[2] } 55 | 56 | if (config.elasticsearch.indices.indexOf(indexName) < 0) { 57 | throw new Error('Invalid / inaccessible index name given in the URL. Please do use following URL format: /api/catalog//_search') 58 | } 59 | 60 | if (urlSegments[urlSegments.length - 1].indexOf('_search') !== 0) { 61 | throw new Error('Please do use following URL format: /api/catalog///_search') 62 | } 63 | } 64 | 65 | // pass the request to elasticsearch 66 | let url = config.elasticsearch.host + ':' + config.elasticsearch.port + (req.query.request ? _updateQueryStringParameter(req.url, 'request', null) : req.url) 67 | 68 | if (!url.startsWith('http')) { 69 | url = config.elasticsearch.protocol + '://' + url 70 | } 71 | 72 | 73 | let auth = null; 74 | 75 | // Only pass auth if configured 76 | if (config.elasticsearch.user || config.elasticsearch.password) { 77 | auth = { 78 | user: config.elasticsearch.user, 79 | pass: config.elasticsearch.password 80 | }; 81 | } 82 | const s = Date.now() 83 | request({ // do the elasticsearch request 84 | uri: url, 85 | method: req.method, 86 | body: requestBody, 87 | json: true, 88 | auth: auth 89 | }, (_err, _res, _resBody) => { // TODO: add caching layer to speed up SSR? How to invalidate products (checksum on the response BEFORE processing it) 90 | res.json(_resBody); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /sample-api-js/src/api/img.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { downloadImage, fit, identify, resize } from '../lib/image'; 3 | import mime from 'mime-types'; 4 | import URL from 'url'; 5 | 6 | const SUPPORTED_ACTIONS = ['fit', 'resize', 'identify']; 7 | const SUPPORTED_MIMETYPES = ['image/gif', 'image/png', 'image/jpeg', 'image/webp', 'image/svg+xml']; 8 | const ONE_YEAR = 31557600000; 9 | 10 | const asyncMiddleware = fn => (req, res, next) => { 11 | Promise.resolve(fn(req, res, next)).catch(next); 12 | }; 13 | 14 | /** 15 | * Image resizer 16 | * 17 | * ```bash 18 | * curl https://your-domain.example.com/img/310/300/resize/w/p/wp07-black_main.jpg 19 | * ``` 20 | * 21 | * or 22 | * 23 | * ```bash 24 | * curl https://your-domain.example.com/img/310/300/resize?url=https%3A%2F%2Fimages.yourdomain.com%2Fw%2Fp%2Fwp07-black_main.jpg 25 | * ``` 26 | * 27 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#img 28 | */ 29 | export default ({ config, db }) => 30 | asyncMiddleware(async (req, res, body) => { 31 | if (!(req.method == 'GET')) { 32 | res.set('Allow', 'GET'); 33 | return res.status(405).send('Method Not Allowed'); 34 | } 35 | 36 | req.socket.setMaxListeners(config.imageable.maxListeners || 50); 37 | 38 | let width 39 | let height 40 | let action 41 | let imgUrl 42 | 43 | if (req.query.url) { // url provided as the query param 44 | imgUrl = decodeURIComponent(req.query.url) 45 | width = parseInt(req.query.width) 46 | height = parseInt(req.query.height) 47 | action = req.query.action 48 | } else { 49 | let urlParts = req.url.split('/'); 50 | width = parseInt(urlParts[1]); 51 | height = parseInt(urlParts[2]); 52 | action = urlParts[3]; 53 | imgUrl = `${config.images.baseUrl}/${urlParts.slice(4).join('/')}`; // full original image url 54 | 55 | if (urlParts.length < 4) { 56 | return res.status(400).send({ 57 | code: 400, 58 | result: 'Please provide following parameters: /img////' 59 | }); 60 | } 61 | } 62 | 63 | 64 | if (isNaN(width) || isNaN(height) || !SUPPORTED_ACTIONS.includes(action)) { 65 | return res.status(400).send({ 66 | code: 400, 67 | result: 'Please provide following parameters: /img//// OR ?url=&width=&height=&action=' 68 | }); 69 | } 70 | 71 | if (width > config.imageable.imageSizeLimit || width < 0 || height > config.imageable.imageSizeLimit || height < 0) { 72 | return res.status(400).send({ 73 | code: 400, 74 | result: `Width and height must have a value between 0 and ${config.imageable.imageSizeLimit}` 75 | }); 76 | } 77 | 78 | if (!isImageSourceHostAllowed(imgUrl, config.imageable.whitelist)) { 79 | return res.status(400).send({ 80 | code: 400, 81 | result: `Host is not allowed` 82 | }); 83 | } 84 | 85 | const mimeType = mime.lookup(imgUrl); 86 | 87 | if (mimeType === false || !SUPPORTED_MIMETYPES.includes(mimeType)) { 88 | return res.status(400).send({ 89 | code: 400, 90 | result: 'Unsupported file type' 91 | }); 92 | } 93 | 94 | console.log(`[URL]: ${imgUrl} - [ACTION]: ${action} - [WIDTH]: ${width} - [HEIGHT]: ${height}`); 95 | 96 | let buffer; 97 | try { 98 | buffer = await downloadImage(imgUrl); 99 | } catch (err) { 100 | return res.status(400).send({ 101 | code: 400, 102 | result: `Unable to download the requested image ${imgUrl}` 103 | }); 104 | } 105 | 106 | switch (action) { 107 | case 'resize': 108 | return res 109 | .type(mimeType) 110 | .set({ 'Cache-Control': `max-age=${ONE_YEAR}` }) 111 | .send(await resize(buffer, width, height)); 112 | case 'fit': 113 | return res 114 | .type(mimeType) 115 | .set({ 'Cache-Control': `max-age=${ONE_YEAR}` }) 116 | .send(await fit(buffer, width, height)); 117 | case 'identify': 118 | return res.set({ 'Cache-Control': `max-age=${ONE_YEAR}` }).send(await identify(buffer)); 119 | default: 120 | throw new Error('Unknown action'); 121 | } 122 | }); 123 | 124 | function _isUrlWhitelisted(url, whitelistType, defaultValue, whitelist) { 125 | if (arguments.length != 4) throw new Error('params are not optional!'); 126 | 127 | if (whitelist && whitelist.hasOwnProperty(whitelistType)) { 128 | const requestedHost = URL.parse(url).host; 129 | const matches = whitelist[whitelistType].map(allowedHost => { 130 | allowedHost = allowedHost instanceof RegExp ? allowedHost : new RegExp(allowedHost); 131 | return !!requestedHost.match(allowedHost); 132 | }); 133 | 134 | return matches.indexOf(true) > -1; 135 | } else { 136 | return defaultValue; 137 | } 138 | } 139 | 140 | function isImageSourceHostAllowed(url, whitelist) { 141 | return _isUrlWhitelisted(url, 'allowedHosts', true, whitelist); 142 | } 143 | -------------------------------------------------------------------------------- /sample-api-js/src/api/index.js: -------------------------------------------------------------------------------- 1 | import { version } from '../../package.json'; 2 | import { Router } from 'express'; 3 | import order from './order'; 4 | import user from './user'; 5 | import stock from './stock'; 6 | import cart from './cart'; 7 | import catalog from './catalog'; 8 | 9 | export default ({ config, db }) => { 10 | let api = Router(); 11 | 12 | // mount the catalog resource 13 | api.use('/catalog', catalog({ config, db })) 14 | 15 | // mount the order resource 16 | api.use('/order', order({ config, db })); 17 | 18 | // mount the user resource 19 | api.use('/user', user({ config, db })); 20 | 21 | // mount the stock resource 22 | api.use('/stock', stock({ config, db })); 23 | 24 | // mount the cart resource 25 | api.use('/cart', cart({ config, db })); 26 | 27 | // perhaps expose some API metadata at the root 28 | api.get('/', (req, res) => { 29 | res.json({ version }); 30 | }); 31 | 32 | return api; 33 | } 34 | -------------------------------------------------------------------------------- /sample-api-js/src/api/order.js: -------------------------------------------------------------------------------- 1 | import resource from 'resource-router-middleware'; 2 | 3 | export default ({ config, db }) => resource({ 4 | 5 | /** Property name to store preloaded entity on `request`. */ 6 | id : 'order', 7 | 8 | /** 9 | * POST create an order 10 | * 11 | * Request body: 12 | * 13 | * { 14 | * "user_id": "", 15 | * "cart_id": "d90e9869fbfe3357281a67e3717e3524", 16 | * "products": [ 17 | * { 18 | * "sku": "WT08-XS-Yellow", 19 | * "qty": 1 20 | * } 21 | * ], 22 | * "addressInformation": { 23 | * "shippingAddress": { 24 | * "region": "", 25 | * "region_id": 0, 26 | * "country_id": "PL", 27 | * "street": [ 28 | * "Example", 29 | * "12" 30 | * ], 31 | * "company": "NA", 32 | * "telephone": "", 33 | * "postcode": "50-201", 34 | * "city": "Wroclaw", 35 | * "firstname": "Piotr", 36 | * "lastname": "Karwatka", 37 | * "email": "pkarwatka30@divante.pl", 38 | * "region_code": "" 39 | * }, 40 | * "billingAddress": { 41 | * "region": "", 42 | * "region_id": 0, 43 | * "country_id": "PL", 44 | * "street": [ 45 | * "Example", 46 | * "12" 47 | * ], 48 | * "company": "Company name", 49 | * "telephone": "", 50 | * "postcode": "50-201", 51 | * "city": "Wroclaw", 52 | * "firstname": "Piotr", 53 | * "lastname": "Karwatka", 54 | * "email": "pkarwatka30@divante.pl", 55 | * "region_code": "", 56 | * "vat_id": "PL88182881112" 57 | * }, 58 | * "shipping_method_code": "flatrate", 59 | * "shipping_carrier_code": "flatrate", 60 | * "payment_method_code": "cashondelivery", 61 | * "payment_method_additional": {} 62 | * }, 63 | * "order_id": "1522811662622-d3736c94-49a5-cd34-724c-87a3a57c2750", 64 | * "transmited": false, 65 | * "created_at": "2018-04-04T03:14:22.622Z", 66 | * "updated_at": "2018-04-04T03:14:22.622Z" 67 | * } 68 | */ 69 | create(req, res) { 70 | res.json({ 71 | "code":200, 72 | "result":"OK" 73 | }) 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /sample-api-js/src/api/stock.js: -------------------------------------------------------------------------------- 1 | import { apiStatus, apiError } from '../lib/util';import { Router } from 'express'; 2 | import PlatformFactory from '../platform/factory' 3 | 4 | export default ({ config, db }) => { 5 | 6 | let api = Router(); 7 | 8 | /** 9 | * GET get stock item 10 | * 11 | * req.params.sku - sku of the prodduct to check 12 | * 13 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#get-vsbridgestockchecksku 14 | * 15 | */ 16 | api.get('/check/:sku', (req, res) => { 17 | res.json({ 18 | "code": 200, 19 | "result": { 20 | "item_id": 580, 21 | "product_id": 580, // required field 22 | "stock_id": 1, 23 | "qty": 53, // required field 24 | "is_in_stock": true, // required field 25 | "is_qty_decimal": false, 26 | "show_default_notification_message": false, 27 | "use_config_min_qty": true, 28 | "min_qty": 0, 29 | "use_config_min_sale_qty": 1, 30 | "min_sale_qty": 1, 31 | "use_config_max_sale_qty": true, 32 | "max_sale_qty": 10000, 33 | "use_config_backorders": true, 34 | "backorders": 0, 35 | "use_config_notify_stock_qty": true, 36 | "notify_stock_qty": 1, 37 | "use_config_qty_increments": true, 38 | "qty_increments": 0, 39 | "use_config_enable_qty_inc": true, 40 | "enable_qty_increments": false, 41 | "use_config_manage_stock": true, 42 | "manage_stock": true, 43 | "low_stock_date": null, 44 | "is_decimal_divided": false, 45 | "stock_status_changed_auto": 0 46 | } 47 | }) 48 | }) 49 | 50 | /** 51 | * GET get stock item - 2nd version with the query url parameter 52 | * 53 | * req.query.url - sku of the product to check 54 | */ 55 | api.get('/check', (req, res) => { 56 | res.json({ 57 | "code": 200, 58 | "result": { 59 | "item_id": 580, 60 | "product_id": 580, // required field 61 | "stock_id": 1, 62 | "qty": 53, // required field 63 | "is_in_stock": true, // required field 64 | "is_qty_decimal": false, 65 | "show_default_notification_message": false, 66 | "use_config_min_qty": true, 67 | "min_qty": 0, 68 | "use_config_min_sale_qty": 1, 69 | "min_sale_qty": 1, 70 | "use_config_max_sale_qty": true, 71 | "max_sale_qty": 10000, 72 | "use_config_backorders": true, 73 | "backorders": 0, 74 | "use_config_notify_stock_qty": true, 75 | "notify_stock_qty": 1, 76 | "use_config_qty_increments": true, 77 | "qty_increments": 0, 78 | "use_config_enable_qty_inc": true, 79 | "enable_qty_increments": false, 80 | "use_config_manage_stock": true, 81 | "manage_stock": true, 82 | "low_stock_date": null, 83 | "is_decimal_divided": false, 84 | "stock_status_changed_auto": 0 85 | } 86 | }) 87 | }) 88 | 89 | /** 90 | * GET get stock item list by skus (comma separated) 91 | * 92 | * req.query.skus = url encoded list of the SKUs 93 | */ 94 | api.get('/list', (req, res) => { 95 | res.json({ 96 | "code": 200, 97 | "result": [ 98 | { 99 | "item_id": 580, 100 | "product_id": 580, // requirerd field 101 | "stock_id": 1, 102 | "qty": 53, // required field 103 | "is_in_stock": true, // required field 104 | "is_qty_decimal": false, 105 | "show_default_notification_message": false, 106 | "use_config_min_qty": true, 107 | "min_qty": 0, 108 | "use_config_min_sale_qty": 1, 109 | "min_sale_qty": 1, 110 | "use_config_max_sale_qty": true, 111 | "max_sale_qty": 10000, 112 | "use_config_backorders": true, 113 | "backorders": 0, 114 | "use_config_notify_stock_qty": true, 115 | "notify_stock_qty": 1, 116 | "use_config_qty_increments": true, 117 | "qty_increments": 0, 118 | "use_config_enable_qty_inc": true, 119 | "enable_qty_increments": false, 120 | "use_config_manage_stock": true, 121 | "manage_stock": true, 122 | "low_stock_date": null, 123 | "is_decimal_divided": false, 124 | "stock_status_changed_auto": 0 125 | } 126 | ] 127 | 128 | }) 129 | }) 130 | 131 | return api 132 | } 133 | -------------------------------------------------------------------------------- /sample-api-js/src/api/user.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | export default ({config, db}) => { 4 | 5 | let userApi = Router(); 6 | /** 7 | * POST create an user 8 | * 9 | * ```bash 10 | * curl 'https://your-domain.example.com/vsbridge/user/create' -H 'content-type: application/json' -H 'accept: application/json, text/plain'--data-binary '{"customer":{"email":"pkarwatka9998@divante.pl","firstname":"Joe","lastname":"Black"},"password":"SecretPassword!@#123"}' 11 | * ``` 12 | * Request body: 13 | * 14 | * { 15 | * "customer": { 16 | * "email": "pkarwatka9998@divante.pl", 17 | * "firstname": "Joe", 18 | * "lastname": "Black" 19 | * }, 20 | * "password": "SecretPassword" 21 | * } 22 | * 23 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgeusercreate 24 | */ 25 | userApi.post('/create', (req, res) => { 26 | res.json({ 27 | "code": 200, 28 | "result": { 29 | "id": 286, 30 | "group_id": 1, 31 | "created_at": "2018-04-03 13:35:13", 32 | "updated_at": "2018-04-03 13:35:13", 33 | "created_in": "Default Store View", 34 | "email": "pkarwatka9998@divante.pl", 35 | "firstname": "Joe", 36 | "lastname": "Black", 37 | "store_id": 1, 38 | "website_id": 1, 39 | "addresses": [], 40 | "disable_auto_group_change": 0 41 | } 42 | }) 43 | }) 44 | 45 | /** 46 | * POST login an user 47 | * 48 | * Request body: 49 | * 50 | * { 51 | * "username":"pkarwatka102@divante.pl", 52 | * "password":"TopSecretPassword" 53 | * } 54 | * 55 | * ```bash 56 | * curl 'https://your-domain.example.com/vsbridge/user/login' -H 'content-type: application/json' -H 'accept: application/json' --data-binary '"username":"pkarwatka102@divante.pl","password":"TopSecretPassword}' 57 | * ``` 58 | * 59 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgeuserlogin 60 | */ 61 | userApi.post('/login', (req, res) => { 62 | res.json({ 63 | "code":200, 64 | "result":"xu8h02nd66yq0gaayj4x3kpqwity02or", 65 | "meta": { "refreshToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzOSJ9.a4HQc2HODmOj5SRMiv-EzWuMZbyIz0CLuVRhPw_MrOM" } 66 | }) 67 | }); 68 | 69 | /** 70 | * POST refresh user token 71 | * 72 | * Request body: 73 | * { 74 | * "refreshToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzOSJ9.a4HQc2HODmOj5SRMiv-EzWuMZbyIz0CLuVRhPw_MrOM" 75 | * } 76 | * 77 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgeuserrefresh 78 | */ 79 | userApi.post('/refresh', (req, res) => { 80 | }); 81 | 82 | /** 83 | * POST reset-password 84 | * 85 | * ```bash 86 | * curl 'https://your-domain.example.com/vsbridge/user/resetPassword' -H 'content-type: application/json' -H 'accept: application/json, text/plain' --data-binary '{"email":"pkarwatka992@divante.pl"}' 87 | * ``` 88 | * 89 | * Request body: 90 | * { 91 | * "email": "pkarwatka992@divante.pl" 92 | * } 93 | * 94 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgeuserresetpassword 95 | */ 96 | userApi.post('/reset-password', (req, res) => { 97 | res.json({ 98 | "email": "pkarwatka992@divante.pl" 99 | }) 100 | }); 101 | 102 | /** 103 | * GET an user 104 | * 105 | * req.query.token - user token obtained from the `/api/user/login` 106 | * 107 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#get-vsbridgeuserme 108 | */ 109 | userApi.get('/me', (req, res) => { 110 | res.json({ 111 | "code":200, 112 | "result": 113 | { 114 | "id":158, 115 | "group_id":1, 116 | "default_shipping":"67", 117 | "created_at":"2018-02-28 12:05:39", 118 | "updated_at":"2018-03-29 10:46:03", 119 | "created_in":"Default Store View", 120 | "email":"pkarwatka102@divante.pl", 121 | "firstname":"Piotr", 122 | "lastname":"Karwatka", 123 | "store_id":1, 124 | "website_id":1, 125 | "addresses":[ 126 | { 127 | "id":67, 128 | "customer_id":158, 129 | "region": 130 | { 131 | "region_code":null, 132 | "region":null, 133 | "region_id":0 134 | }, 135 | "region_id":0, 136 | "country_id":"PL", 137 | "street": ["Street name","13"], 138 | "telephone":"", 139 | "postcode":"41-157", 140 | "city":"Wrocław", 141 | "firstname":"John","lastname":"Murphy", 142 | "default_shipping":true 143 | }], 144 | "disable_auto_group_change":0 145 | } 146 | }) 147 | }); 148 | 149 | /** 150 | * GET an user order history 151 | * 152 | * req.query.token - user token 153 | * 154 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#get-vsbridgeuserorder-history 155 | */ 156 | userApi.get('/order-history', (req, res) => { 157 | res.json({ 158 | "code": 200, 159 | "result": { 160 | "items": [ 161 | { 162 | "applied_rule_ids": "1,5", 163 | "base_currency_code": "USD", 164 | "base_discount_amount": -3.3, 165 | "base_grand_total": 28, 166 | "base_discount_tax_compensation_amount": 0, 167 | "base_shipping_amount": 5, 168 | "base_shipping_discount_amount": 0, 169 | "base_shipping_incl_tax": 5, 170 | "base_shipping_tax_amount": 0, 171 | "base_subtotal": 22, 172 | "base_subtotal_incl_tax": 27.06, 173 | "base_tax_amount": 4.3, 174 | "base_total_due": 28, 175 | "base_to_global_rate": 1, 176 | "base_to_order_rate": 1, 177 | "billing_address_id": 204, 178 | "created_at": "2018-01-23 15:30:04", 179 | "customer_email": "pkarwatka28@example.com", 180 | "customer_group_id": 0, 181 | "customer_is_guest": 1, 182 | "customer_note_notify": 1, 183 | "discount_amount": -3.3, 184 | "email_sent": 1, 185 | "entity_id": 102, 186 | "global_currency_code": "USD", 187 | "grand_total": 28, 188 | "discount_tax_compensation_amount": 0, 189 | "increment_id": "000000102", 190 | "is_virtual": 0, 191 | "order_currency_code": "USD", 192 | "protect_code": "3984835d33abd2423b8a47efd0f74579", 193 | "quote_id": 1112, 194 | "shipping_amount": 5, 195 | "shipping_description": "Flat Rate - Fixed", 196 | "shipping_discount_amount": 0, 197 | "shipping_discount_tax_compensation_amount": 0, 198 | "shipping_incl_tax": 5, 199 | "shipping_tax_amount": 0, 200 | "state": "new", 201 | "status": "pending", 202 | "store_currency_code": "USD", 203 | "store_id": 1, 204 | "store_name": "Main Website\nMain Website Store\n", 205 | "store_to_base_rate": 0, 206 | "store_to_order_rate": 0, 207 | "subtotal": 22, 208 | "subtotal_incl_tax": 27.06, 209 | "tax_amount": 4.3, 210 | "total_due": 28, 211 | "total_item_count": 1, 212 | "total_qty_ordered": 1, 213 | "updated_at": "2018-01-23 15:30:05", 214 | "weight": 1, 215 | "items": [ 216 | { 217 | "amount_refunded": 0, 218 | "applied_rule_ids": "1,5", 219 | "base_amount_refunded": 0, 220 | "base_discount_amount": 3.3, 221 | "base_discount_invoiced": 0, 222 | "base_discount_tax_compensation_amount": 0, 223 | "base_original_price": 22, 224 | "base_price": 22, 225 | "base_price_incl_tax": 27.06, 226 | "base_row_invoiced": 0, 227 | "base_row_total": 22, 228 | "base_row_total_incl_tax": 27.06, 229 | "base_tax_amount": 4.3, 230 | "base_tax_invoiced": 0, 231 | "created_at": "2018-01-23 15:30:04", 232 | "discount_amount": 3.3, 233 | "discount_invoiced": 0, 234 | "discount_percent": 15, 235 | "free_shipping": 0, 236 | "discount_tax_compensation_amount": 0, 237 | "is_qty_decimal": 0, 238 | "is_virtual": 0, 239 | "item_id": 224, 240 | "name": "Radiant Tee-XS-Blue", 241 | "no_discount": 0, 242 | "order_id": 102, 243 | "original_price": 22, 244 | "price": 22, 245 | "price_incl_tax": 27.06, 246 | "product_id": 1546, 247 | "product_type": "simple", 248 | "qty_canceled": 0, 249 | "qty_invoiced": 0, 250 | "qty_ordered": 1, 251 | "qty_refunded": 0, 252 | "qty_shipped": 0, 253 | "quote_item_id": 675, 254 | "row_invoiced": 0, 255 | "row_total": 22, 256 | "row_total_incl_tax": 27.06, 257 | "row_weight": 1, 258 | "sku": "WS12-XS-Blue", 259 | "store_id": 1, 260 | "tax_amount": 4.3, 261 | "tax_invoiced": 0, 262 | "tax_percent": 23, 263 | "updated_at": "2018-01-23 15:30:04", 264 | "weight": 1 265 | } 266 | ], 267 | "billing_address": { 268 | "address_type": "billing", 269 | "city": "Some city2", 270 | "company": "Divante", 271 | "country_id": "PL", 272 | "email": "pkarwatka28@example.com", 273 | "entity_id": 204, 274 | "firstname": "Piotr", 275 | "lastname": "Karwatka", 276 | "parent_id": 102, 277 | "postcode": "50-203", 278 | "street": [ 279 | "XYZ", 280 | "17" 281 | ], 282 | "telephone": null, 283 | "vat_id": "PL8951930748" 284 | }, 285 | "payment": { 286 | "account_status": null, 287 | "additional_information": [ 288 | "Cash On Delivery", 289 | "" 290 | ], 291 | "amount_ordered": 28, 292 | "base_amount_ordered": 28, 293 | "base_shipping_amount": 5, 294 | "cc_last4": null, 295 | "entity_id": 102, 296 | "method": "cashondelivery", 297 | "parent_id": 102, 298 | "shipping_amount": 5 299 | }, 300 | "status_histories": [], 301 | "extension_attributes": { 302 | "shipping_assignments": [ 303 | { 304 | "shipping": { 305 | "address": { 306 | "address_type": "shipping", 307 | "city": "Some city", 308 | "company": "NA", 309 | "country_id": "PL", 310 | "email": "pkarwatka28@example.com", 311 | "entity_id": 203, 312 | "firstname": "Piotr", 313 | "lastname": "Karwatka", 314 | "parent_id": 102, 315 | "postcode": "51-169", 316 | "street": [ 317 | "XYZ", 318 | "13" 319 | ], 320 | "telephone": null 321 | }, 322 | "method": "flatrate_flatrate", 323 | "total": { 324 | "base_shipping_amount": 5, 325 | "base_shipping_discount_amount": 0, 326 | "base_shipping_incl_tax": 5, 327 | "base_shipping_tax_amount": 0, 328 | "shipping_amount": 5, 329 | "shipping_discount_amount": 0, 330 | "shipping_discount_tax_compensation_amount": 0, 331 | "shipping_incl_tax": 5, 332 | "shipping_tax_amount": 0 333 | } 334 | }, 335 | "items": [ 336 | { 337 | "amount_refunded": 0, 338 | "applied_rule_ids": "1,5", 339 | "base_amount_refunded": 0, 340 | "base_discount_amount": 3.3, 341 | "base_discount_invoiced": 0, 342 | "base_discount_tax_compensation_amount": 0, 343 | "base_original_price": 22, 344 | "base_price": 22, 345 | "base_price_incl_tax": 27.06, 346 | "base_row_invoiced": 0, 347 | "base_row_total": 22, 348 | "base_row_total_incl_tax": 27.06, 349 | "base_tax_amount": 4.3, 350 | "base_tax_invoiced": 0, 351 | "created_at": "2018-01-23 15:30:04", 352 | "discount_amount": 3.3, 353 | "discount_invoiced": 0, 354 | "discount_percent": 15, 355 | "free_shipping": 0, 356 | "discount_tax_compensation_amount": 0, 357 | "is_qty_decimal": 0, 358 | "is_virtual": 0, 359 | "item_id": 224, 360 | "name": "Radiant Tee-XS-Blue", 361 | "no_discount": 0, 362 | "order_id": 102, 363 | "original_price": 22, 364 | "price": 22, 365 | "price_incl_tax": 27.06, 366 | "product_id": 1546, 367 | "product_type": "simple", 368 | "qty_canceled": 0, 369 | "qty_invoiced": 0, 370 | "qty_ordered": 1, 371 | "qty_refunded": 0, 372 | "qty_shipped": 0, 373 | "quote_item_id": 675, 374 | "row_invoiced": 0, 375 | "row_total": 22, 376 | "row_total_incl_tax": 27.06, 377 | "row_weight": 1, 378 | "sku": "WS12-XS-Blue", 379 | "store_id": 1, 380 | "tax_amount": 4.3, 381 | "tax_invoiced": 0, 382 | "tax_percent": 23, 383 | "updated_at": "2018-01-23 15:30:04", 384 | "weight": 1 385 | } 386 | ] 387 | } 388 | ] 389 | } 390 | } 391 | ], 392 | "search_criteria": { 393 | "filter_groups": [ 394 | { 395 | "filters": [ 396 | { 397 | "field": "customer_email", 398 | "value": "pkarwatka28@example.com", 399 | "condition_type": "eq" 400 | } 401 | ] 402 | } 403 | ] 404 | }, 405 | "total_count": 61 406 | } 407 | }) 408 | }); 409 | 410 | /** 411 | * POST for updating user 412 | * 413 | * Request body: 414 | * 415 | * { 416 | * "customer": { 417 | * "id": 222, 418 | * "group_id": 1, 419 | * "default_billing": "105", 420 | * "default_shipping": "105", 421 | * "created_at": "2018-03-16 19:01:18", 422 | * "updated_at": "2018-04-03 12:59:13", 423 | * "created_in": "Default Store View", 424 | * "email": "pkarwatka30@divante.pl", 425 | * "firstname": "Piotr", 426 | * "lastname": "Karwatka", 427 | * "store_id": 1, 428 | * "website_id": 1, 429 | * "addresses": [ 430 | * { 431 | * "id": 109, 432 | * "customer_id": 222, 433 | * "region": { 434 | * "region_code": null, 435 | * "region": null, 436 | * "region_id": 0 437 | * }, 438 | * "region_id": 0, 439 | * "country_id": "PL", 440 | * "street": [ 441 | * "Dmowskiego", 442 | * "17" 443 | * ], 444 | * "company": "Divante2", 445 | * "telephone": "", 446 | * "postcode": "50-203", 447 | * "city": "Wrocław", 448 | * "firstname": "Piotr", 449 | * "lastname": "Karwatka2", 450 | * "vat_id": "PL8951930748" 451 | * } 452 | * ], 453 | * "disable_auto_group_change": 0 454 | * } 455 | *} 456 | * 457 | * Details: https://github.com/DivanteLtd/vue-storefront-integration-sdk/blob/tutorial/Dynamic%20API%20specification.md#post-vsbridgeuserme 458 | */ 459 | userApi.post('/me', (req, res) => { 460 | res.json({ 461 | "code": 200, 462 | "result": { 463 | "id": 222, 464 | "group_id": 1, 465 | "created_at": "2018-03-16 19:01:18", 466 | "updated_at": "2018-04-04 02:59:52", 467 | "created_in": "Default Store View", 468 | "email": "pkarwatka30@divante.pl", 469 | "firstname": "Piotr", 470 | "lastname": "Karwatka", 471 | "store_id": 1, 472 | "website_id": 1, 473 | "addresses": [ 474 | { 475 | "id": 109, 476 | "customer_id": 222, 477 | "region": { 478 | "region_code": null, 479 | "region": null, 480 | "region_id": 0 481 | }, 482 | "region_id": 0, 483 | "country_id": "PL", 484 | "street": [ 485 | "Dmowskiego", 486 | "17" 487 | ], 488 | "company": "Divante2", 489 | "telephone": "", 490 | "postcode": "50-203", 491 | "city": "Wrocław", 492 | "firstname": "Piotr", 493 | "lastname": "Karwatka2", 494 | "vat_id": "PL8951930748" 495 | } 496 | ], 497 | "disable_auto_group_change": 0 498 | } 499 | }) 500 | }) 501 | 502 | /** 503 | * POST for changing user's password 504 | * 505 | * Request body: 506 | * 507 | * { 508 | * "currentPassword":"OldPassword", 509 | * "newPassword":"NewPassword" 510 | * } 511 | */ 512 | userApi.post('/change-password', (req, res) => { 513 | res.json({ 514 | "code":500, 515 | "result":"The password doesn't match this account." 516 | }) 517 | }); 518 | 519 | return userApi 520 | } 521 | -------------------------------------------------------------------------------- /sample-api-js/src/db.js: -------------------------------------------------------------------------------- 1 | export default callback => { 2 | // connect to a database if needed, then pass it to `callback`: 3 | callback(); 4 | } 5 | -------------------------------------------------------------------------------- /sample-api-js/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import morgan from 'morgan'; 4 | import bodyParser from 'body-parser'; 5 | import initializeDb from './db'; 6 | import middleware from './middleware'; 7 | import api from './api'; 8 | import config from 'config'; 9 | import img from './api/img'; 10 | 11 | 12 | const app = express(); 13 | 14 | // logger 15 | app.use(morgan('dev')); 16 | 17 | // 3rd party middleware 18 | app.use(cors({ 19 | exposedHeaders: config.get('corsHeaders'), 20 | })); 21 | 22 | app.use(bodyParser.json({ 23 | limit : config.get('bodyLimit') 24 | })); 25 | 26 | // connect to db 27 | initializeDb( db => { 28 | // internal middleware 29 | app.use(middleware({ config, db })); 30 | 31 | // api router 32 | app.use('/api', api({ config, db })); 33 | app.use('/img', img({ config, db })); 34 | 35 | const port = process.env.PORT || config.get('server.port') 36 | const host = process.env.HOST || config.get('server.host') 37 | app.listen(parseInt(port), host, () => { 38 | console.log(`Vue Storefront Sample API started at http://${host}:${port}`); 39 | }); 40 | }); 41 | 42 | 43 | app.use(bodyParser.urlencoded({ extended: true })); 44 | app.use(bodyParser.json()); 45 | 46 | export default app; 47 | -------------------------------------------------------------------------------- /sample-api-js/src/lib/image.js: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | import rp from 'request-promise-native'; 3 | import config from 'config'; 4 | 5 | sharp.cache(config.imageable.cache); 6 | sharp.concurrency(config.imageable.concurrency); 7 | sharp.counters(config.imageable.counters); 8 | sharp.simd(config.imageable.simd); 9 | 10 | export async function downloadImage (url) { 11 | return await rp.get(url, { encoding: null }); 12 | } 13 | 14 | export async function identify (buffer) { 15 | try { 16 | const transformer = sharp(buffer); 17 | 18 | return transformer.metadata(); 19 | } catch (err) { 20 | console.log(err); 21 | } 22 | } 23 | 24 | export async function resize (buffer, width, height) { 25 | try { 26 | const transformer = sharp(buffer); 27 | 28 | if (width || height) { 29 | const options = { 30 | withoutEnlargement: true, 31 | fit: sharp.fit.inside 32 | } 33 | transformer.resize(width, height, options) 34 | } 35 | 36 | return transformer.toBuffer(); 37 | } catch (err) { 38 | console.log(err); 39 | } 40 | } 41 | 42 | export async function fit (buffer, width, height) { 43 | try { 44 | const transformer = sharp(buffer); 45 | 46 | if (width || height) { 47 | transformer.resize(width, height).crop(); 48 | } 49 | 50 | return transformer.toBuffer(); 51 | } catch (err) { 52 | console.log(err); 53 | } 54 | } 55 | 56 | export async function crop (buffer, width, height, x, y) { 57 | try { 58 | const transformer = sharp(buffer); 59 | 60 | if (width || height || x || y) { 61 | transformer.extract({ left: x, top: y, width, height }); 62 | } 63 | 64 | return transformer.toBuffer(); 65 | } catch (err) { 66 | console.log(err); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sample-api-js/src/lib/util.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import crypto from 'crypto'; 3 | const algorithm = 'aes-256-ctr'; 4 | 5 | /** Creates a callback that proxies node callback style arguments to an Express Response object. 6 | * @param {express.Response} res Express HTTP Response 7 | * @param {number} [status=200] Status code to send on success 8 | * 9 | * @example 10 | * list(req, res) { 11 | * collection.find({}, toRes(res)); 12 | * } 13 | */ 14 | export function toRes(res, status=200) { 15 | return (err, thing) => { 16 | if (err) return res.status(500).send(err); 17 | 18 | if (thing && typeof thing.toObject==='function') { 19 | thing = thing.toObject(); 20 | } 21 | res.status(status).json(thing); 22 | }; 23 | } 24 | 25 | export function sgnSrc (sgnObj, item) { 26 | if (config.tax.alwaysSyncPlatformPricesOver) { 27 | sgnObj.id = item.id 28 | } else { 29 | sgnObj.sku = item.sku 30 | } 31 | // console.log(sgnObj) 32 | return sgnObj 33 | } 34 | 35 | /** Creates a api status call and sends it thru to Express Response object. 36 | * @param {express.Response} res Express HTTP Response 37 | * @param {number} [code=200] Status code to send on success 38 | * @param {json} [result='OK'] Text message or result information object 39 | */ 40 | export function apiStatus(res, result = 'OK', code = 200, meta = null) { 41 | let apiResult = { code: code, result: result }; 42 | if (meta !== null) { 43 | apiResult.meta = meta; 44 | } 45 | res.status(code).json(apiResult); 46 | return result; 47 | } 48 | 49 | 50 | /** Creates a api error status Express Response object. 51 | * @param {express.Response} res Express HTTP Response 52 | * @param {number} [code=200] Status code to send on success 53 | * @param {json} [result='OK'] Text message or result information object 54 | */ 55 | export function apiError(res, errorObj, code = 500) { 56 | return apiStatus(res, errorObj.errorMessage ? errorObj.errorMessage : errorObj, errorObj.code ? errorObj.code : 500) 57 | } 58 | 59 | export function encryptToken(textToken, secret) { 60 | const cipher = crypto.createCipher(algorithm, secret) 61 | let crypted = cipher.update(textToken, 'utf8', 'hex') 62 | crypted += cipher.final('hex'); 63 | return crypted; 64 | } 65 | 66 | export function decryptToken(textToken, secret) { 67 | const decipher = crypto.createDecipher(algorithm, secret) 68 | let dec = decipher.update(textToken, 'hex', 'utf8') 69 | dec += decipher.final('utf8'); 70 | return dec; 71 | } -------------------------------------------------------------------------------- /sample-api-js/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { json } from "body-parser"; 3 | import { NextHandleFunction } from "connect"; 4 | import { IConfig } from "config"; 5 | 6 | export default ({ config, db }: { config: IConfig, db: CallableFunction }): [ NextHandleFunction, Router ] => { 7 | let routes:Router = Router(); 8 | let bp:NextHandleFunction = json(); 9 | return [ bp, routes ]; 10 | } 11 | -------------------------------------------------------------------------------- /sample-api-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "strict": false, 5 | "allowJs": true, 6 | "importHelpers": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": false, 13 | "outDir": "dist", 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "lib": ["es7"] 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /sample-data/attributes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 149, 4 | "is_user_defined": true, 5 | "is_visible": true, 6 | "frontend_input": "multiselect", 7 | "attribute_code": "style_bottom", 8 | "default_value": "", 9 | "options": [ 10 | { 11 | "label": " ", 12 | "value": "" 13 | }, 14 | { 15 | "label": "Base Layer", 16 | "value": "105" 17 | }, 18 | { 19 | "label": "Basic", 20 | "value": "106" 21 | }, 22 | { 23 | "label": "Capri", 24 | "value": "107" 25 | }, 26 | { 27 | "label": "Compression", 28 | "value": "108" 29 | }, 30 | { 31 | "label": "Leggings", 32 | "value": "109" 33 | }, 34 | { 35 | "label": "Parachute", 36 | "value": "110" 37 | }, 38 | { 39 | "label": "Skort", 40 | "value": "111" 41 | }, 42 | { 43 | "label": "Snug", 44 | "value": "112" 45 | }, 46 | { 47 | "label": "Sweatpants", 48 | "value": "113" 49 | }, 50 | { 51 | "label": "Tights", 52 | "value": "114" 53 | }, 54 | { 55 | "label": "Track Pants", 56 | "value": "115" 57 | }, 58 | { 59 | "label": "Workout Pants", 60 | "value": "116" 61 | } 62 | ], 63 | "default_frontend_label": "Style Bottom" 64 | }, 65 | { 66 | "id": 144, 67 | "is_user_defined": true, 68 | "is_visible": true, 69 | "frontend_input": "boolean", 70 | "attribute_code": "performance_fabric", 71 | "default_value": "", 72 | "options": [ 73 | { 74 | "label": "Yes", 75 | "value": "1" 76 | }, 77 | { 78 | "label": "No", 79 | "value": "0" 80 | } 81 | ], 82 | "default_frontend_label": "Performance Fabric" 83 | }, 84 | { 85 | "id": 143, 86 | "is_user_defined": true, 87 | "is_visible": true, 88 | "frontend_input": "boolean", 89 | "attribute_code": "eco_collection", 90 | "default_value": "", 91 | "options": [ 92 | { 93 | "label": "Yes", 94 | "value": "1" 95 | }, 96 | { 97 | "label": "No", 98 | "value": "0" 99 | } 100 | ], 101 | "default_frontend_label": "Eco Collection" 102 | }, 103 | { 104 | "id": 146, 105 | "is_user_defined": true, 106 | "is_visible": true, 107 | "frontend_input": "boolean", 108 | "attribute_code": "new", 109 | "default_value": "", 110 | "options": [ 111 | { 112 | "label": "Yes", 113 | "value": "1" 114 | }, 115 | { 116 | "label": "No", 117 | "value": "0" 118 | } 119 | ], 120 | "default_frontend_label": "New" 121 | }, 122 | { 123 | "id": 142, 124 | "is_user_defined": true, 125 | "is_visible": true, 126 | "frontend_input": "select", 127 | "attribute_code": "size", 128 | "default_value": "91", 129 | "options": [ 130 | { 131 | "label": " ", 132 | "value": "" 133 | }, 134 | { 135 | "label": "55 cm", 136 | "value": "91" 137 | }, 138 | { 139 | "label": "XS", 140 | "value": "167" 141 | }, 142 | { 143 | "label": "65 cm", 144 | "value": "92" 145 | }, 146 | { 147 | "label": "S", 148 | "value": "168" 149 | }, 150 | { 151 | "label": "75 cm", 152 | "value": "93" 153 | }, 154 | { 155 | "label": "M", 156 | "value": "169" 157 | }, 158 | { 159 | "label": "6 foot", 160 | "value": "94" 161 | }, 162 | { 163 | "label": "L", 164 | "value": "170" 165 | }, 166 | { 167 | "label": "8 foot", 168 | "value": "95" 169 | }, 170 | { 171 | "label": "XL", 172 | "value": "171" 173 | }, 174 | { 175 | "label": "10 foot", 176 | "value": "96" 177 | }, 178 | { 179 | "label": "28", 180 | "value": "172" 181 | }, 182 | { 183 | "label": "29", 184 | "value": "173" 185 | }, 186 | { 187 | "label": "30", 188 | "value": "174" 189 | }, 190 | { 191 | "label": "31", 192 | "value": "175" 193 | }, 194 | { 195 | "label": "32", 196 | "value": "176" 197 | }, 198 | { 199 | "label": "33", 200 | "value": "177" 201 | }, 202 | { 203 | "label": "34", 204 | "value": "178" 205 | }, 206 | { 207 | "label": "36", 208 | "value": "179" 209 | }, 210 | { 211 | "label": "38", 212 | "value": "180" 213 | } 214 | ], 215 | "default_frontend_label": "Size" 216 | }, 217 | { 218 | "id": 137, 219 | "is_user_defined": true, 220 | "is_visible": true, 221 | "frontend_input": "multiselect", 222 | "attribute_code": "material", 223 | "default_value": "", 224 | "options": [ 225 | { 226 | "label": " ", 227 | "value": "" 228 | }, 229 | { 230 | "label": "Burlap", 231 | "value": "31" 232 | }, 233 | { 234 | "label": "Cocona® performance fabric", 235 | "value": "143" 236 | }, 237 | { 238 | "label": "Canvas", 239 | "value": "32" 240 | }, 241 | { 242 | "label": "Wool", 243 | "value": "144" 244 | }, 245 | { 246 | "label": "Cotton", 247 | "value": "33" 248 | }, 249 | { 250 | "label": "Fleece", 251 | "value": "145" 252 | }, 253 | { 254 | "label": "Faux Leather", 255 | "value": "34" 256 | }, 257 | { 258 | "label": "Hemp", 259 | "value": "146" 260 | }, 261 | { 262 | "label": "Jersey", 263 | "value": "147" 264 | }, 265 | { 266 | "label": "Leather", 267 | "value": "35" 268 | }, 269 | { 270 | "label": "LumaTech™", 271 | "value": "148" 272 | }, 273 | { 274 | "label": "Mesh", 275 | "value": "36" 276 | }, 277 | { 278 | "label": "Lycra®", 279 | "value": "149" 280 | }, 281 | { 282 | "label": "Nylon", 283 | "value": "37" 284 | }, 285 | { 286 | "label": "Microfiber", 287 | "value": "150" 288 | }, 289 | { 290 | "label": "Polyester", 291 | "value": "38" 292 | }, 293 | { 294 | "label": "Rayon", 295 | "value": "39" 296 | }, 297 | { 298 | "label": "Spandex", 299 | "value": "151" 300 | }, 301 | { 302 | "label": "HeatTec®", 303 | "value": "152" 304 | }, 305 | { 306 | "label": "Ripstop", 307 | "value": "40" 308 | }, 309 | { 310 | "label": "EverCool™", 311 | "value": "153" 312 | }, 313 | { 314 | "label": "Suede", 315 | "value": "41" 316 | }, 317 | { 318 | "label": "Foam", 319 | "value": "42" 320 | }, 321 | { 322 | "label": "Organic Cotton", 323 | "value": "154" 324 | }, 325 | { 326 | "label": "Metal", 327 | "value": "43" 328 | }, 329 | { 330 | "label": "TENCEL", 331 | "value": "155" 332 | }, 333 | { 334 | "label": "CoolTech™", 335 | "value": "156" 336 | }, 337 | { 338 | "label": "Plastic", 339 | "value": "44" 340 | }, 341 | { 342 | "label": "Khaki", 343 | "value": "157" 344 | }, 345 | { 346 | "label": "Rubber", 347 | "value": "45" 348 | }, 349 | { 350 | "label": "Linen", 351 | "value": "158" 352 | }, 353 | { 354 | "label": "Synthetic", 355 | "value": "46" 356 | }, 357 | { 358 | "label": "Stainless Steel", 359 | "value": "47" 360 | }, 361 | { 362 | "label": "Wool", 363 | "value": "159" 364 | }, 365 | { 366 | "label": "Silicone", 367 | "value": "48" 368 | }, 369 | { 370 | "label": "Terry", 371 | "value": "160" 372 | } 373 | ], 374 | "default_frontend_label": "Material" 375 | }, 376 | { 377 | "id": 93, 378 | "is_user_defined": true, 379 | "is_visible": true, 380 | "frontend_input": "select", 381 | "attribute_code": "color", 382 | "default_value": "49", 383 | "options": [ 384 | { 385 | "label": " ", 386 | "value": "" 387 | }, 388 | { 389 | "label": "Black", 390 | "value": "49" 391 | }, 392 | { 393 | "label": "Blue", 394 | "value": "50" 395 | }, 396 | { 397 | "label": "Brown", 398 | "value": "51" 399 | }, 400 | { 401 | "label": "Gray", 402 | "value": "52" 403 | }, 404 | { 405 | "label": "Green", 406 | "value": "53" 407 | }, 408 | { 409 | "label": "Lavender", 410 | "value": "54" 411 | }, 412 | { 413 | "label": "Multi", 414 | "value": "55" 415 | }, 416 | { 417 | "label": "Orange", 418 | "value": "56" 419 | }, 420 | { 421 | "label": "Purple", 422 | "value": "57" 423 | }, 424 | { 425 | "label": "Red", 426 | "value": "58" 427 | }, 428 | { 429 | "label": "White", 430 | "value": "59" 431 | }, 432 | { 433 | "label": "Yellow", 434 | "value": "60" 435 | } 436 | ], 437 | "default_frontend_label": "Color" 438 | }, 439 | { 440 | "id": 147, 441 | "is_user_defined": true, 442 | "is_visible": true, 443 | "frontend_input": "boolean", 444 | "attribute_code": "sale", 445 | "default_value": "", 446 | "options": [ 447 | { 448 | "label": "Yes", 449 | "value": "1" 450 | }, 451 | { 452 | "label": "No", 453 | "value": "0" 454 | } 455 | ], 456 | "default_frontend_label": "Sale" 457 | }, 458 | { 459 | "id": 154, 460 | "is_user_defined": true, 461 | "is_visible": true, 462 | "frontend_input": "multiselect", 463 | "attribute_code": "climate", 464 | "default_value": "", 465 | "options": [ 466 | { 467 | "label": " ", 468 | "value": "" 469 | }, 470 | { 471 | "label": "All-Weather", 472 | "value": "202" 473 | }, 474 | { 475 | "label": "Cold", 476 | "value": "203" 477 | }, 478 | { 479 | "label": "Cool", 480 | "value": "204" 481 | }, 482 | { 483 | "label": "Indoor", 484 | "value": "205" 485 | }, 486 | { 487 | "label": "Mild", 488 | "value": "206" 489 | }, 490 | { 491 | "label": "Rainy", 492 | "value": "207" 493 | }, 494 | { 495 | "label": "Spring", 496 | "value": "208" 497 | }, 498 | { 499 | "label": "Warm", 500 | "value": "209" 501 | }, 502 | { 503 | "label": "Windy", 504 | "value": "210" 505 | }, 506 | { 507 | "label": "Wintry", 508 | "value": "211" 509 | }, 510 | { 511 | "label": "Hot", 512 | "value": "212" 513 | } 514 | ], 515 | "default_frontend_label": "Climate" 516 | }, 517 | { 518 | "id": 153, 519 | "is_user_defined": true, 520 | "is_visible": true, 521 | "frontend_input": "multiselect", 522 | "attribute_code": "pattern", 523 | "default_value": "", 524 | "options": [ 525 | { 526 | "label": " ", 527 | "value": "" 528 | }, 529 | { 530 | "label": "Color-Blocked", 531 | "value": "193" 532 | }, 533 | { 534 | "label": "Checked", 535 | "value": "194" 536 | }, 537 | { 538 | "label": "Color-Blocked", 539 | "value": "195" 540 | }, 541 | { 542 | "label": "Graphic Print", 543 | "value": "196" 544 | }, 545 | { 546 | "label": "Solid", 547 | "value": "197" 548 | }, 549 | { 550 | "label": "Solid-Highlight", 551 | "value": "198" 552 | }, 553 | { 554 | "label": "Striped", 555 | "value": "199" 556 | }, 557 | { 558 | "label": "Camo", 559 | "value": "200" 560 | }, 561 | { 562 | "label": "Geometric", 563 | "value": "201" 564 | } 565 | ], 566 | "default_frontend_label": "Pattern" 567 | } 568 | ] 569 | -------------------------------------------------------------------------------- /sample-data/categories.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 14, 4 | "parent_id": 12, 5 | "name": "Jackets", 6 | "url_key": "jackets-14", 7 | "path": "1/2/11/12/14", 8 | "url_path": "men/tops-men/jackets-men/jackets-14", 9 | "is_active": true, 10 | "position": 1, 11 | "level": 4, 12 | "product_count": 176, 13 | "children_data": [] 14 | }, 15 | { 16 | "id": 19, 17 | "parent_id": 13, 18 | "name": "Shorts", 19 | "url_key": "shorts-19", 20 | "path": "1/2/11/13/19", 21 | "url_path": "men/bottoms-men/shorts-men/shorts-19", 22 | "is_active": true, 23 | "position": 2, 24 | "level": 4, 25 | "product_count": 148, 26 | "children_data": [] 27 | }, 28 | { 29 | "id": 26, 30 | "parent_id": 21, 31 | "name": "Bras & Tanks", 32 | "url_key": "bras-and-tanks-26", 33 | "path": "1/2/20/21/26", 34 | "url_path": "women/tops-women/tanks-women/bras-and-tanks-26", 35 | "is_active": true, 36 | "position": 4, 37 | "level": 4, 38 | "product_count": 224, 39 | "children_data": [] 40 | }, 41 | { 42 | "id": 25, 43 | "parent_id": 21, 44 | "name": "Tees", 45 | "url_key": "tees-25", 46 | "path": "1/2/20/21/25", 47 | "url_path": "women/tops-women/tees-women/tees-25", 48 | "is_active": true, 49 | "position": 3, 50 | "level": 4, 51 | "product_count": 192, 52 | "children_data": [] 53 | }, 54 | { 55 | "id": 24, 56 | "parent_id": 21, 57 | "name": "Hoodies & Sweatshirts", 58 | "url_key": "hoodies-and-sweatshirts-24", 59 | "path": "1/2/20/21/24", 60 | "url_path": "women/tops-women/hoodies-and-sweatshirts-women/hoodies-and-sweatshirts-24", 61 | "is_active": true, 62 | "position": 2, 63 | "level": 4, 64 | "product_count": 182, 65 | "children_data": [] 66 | }, 67 | { 68 | "id": 22, 69 | "parent_id": 20, 70 | "name": "Bottoms", 71 | "url_key": "bottoms-22", 72 | "path": "1/2/20/22", 73 | "url_path": "women/bottoms-women/bottoms-22", 74 | "is_active": true, 75 | "position": 2, 76 | "level": 3, 77 | "product_count": 0, 78 | "children_data": [ 79 | { 80 | "id": 27, 81 | "children_data": [] 82 | }, 83 | { 84 | "id": 28, 85 | "children_data": [] 86 | } 87 | ] 88 | }, 89 | { 90 | "id": 40, 91 | "parent_id": 7, 92 | "name": "Eco Collection New", 93 | "url_key": "eco-collection-new-40", 94 | "path": "1/2/7/40", 95 | "url_path": "collections/eco-new/eco-collection-new-40", 96 | "is_active": true, 97 | "position": 6, 98 | "level": 3, 99 | "product_count": 0, 100 | "children_data": [] 101 | }, 102 | { 103 | "id": 29, 104 | "parent_id": 2, 105 | "name": "Promotions", 106 | "url_key": "promotions-29", 107 | "path": "1/2/29", 108 | "url_path": "promotions/promotions-29", 109 | "is_active": false, 110 | "position": 6, 111 | "level": 2, 112 | "product_count": 0, 113 | "children_data": [ 114 | { 115 | "id": 30, 116 | "children_data": [] 117 | }, 118 | { 119 | "id": 31, 120 | "children_data": [] 121 | }, 122 | { 123 | "id": 32, 124 | "children_data": [] 125 | }, 126 | { 127 | "id": 33, 128 | "children_data": [] 129 | } 130 | ] 131 | }, 132 | { 133 | "id": 12, 134 | "parent_id": 11, 135 | "name": "Tops", 136 | "url_key": "tops-12", 137 | "path": "1/2/11/12", 138 | "url_path": "men/tops-men/tops-12", 139 | "is_active": true, 140 | "position": 1, 141 | "level": 3, 142 | "product_count": 0, 143 | "children_data": [ 144 | { 145 | "id": 14, 146 | "children_data": [] 147 | }, 148 | { 149 | "id": 15, 150 | "children_data": [] 151 | }, 152 | { 153 | "id": 16, 154 | "children_data": [] 155 | }, 156 | { 157 | "id": 17, 158 | "children_data": [] 159 | } 160 | ] 161 | }, 162 | { 163 | "id": 21, 164 | "parent_id": 20, 165 | "name": "Tops", 166 | "url_key": "tops-21", 167 | "path": "1/2/20/21", 168 | "url_path": "women/tops-women/tops-21", 169 | "is_active": true, 170 | "position": 1, 171 | "level": 3, 172 | "product_count": 0, 173 | "children_data": [ 174 | { 175 | "id": 23, 176 | "children_data": [] 177 | }, 178 | { 179 | "id": 24, 180 | "children_data": [] 181 | }, 182 | { 183 | "id": 25, 184 | "children_data": [] 185 | }, 186 | { 187 | "id": 26, 188 | "children_data": [] 189 | } 190 | ] 191 | }, 192 | { 193 | "id": 8, 194 | "parent_id": 7, 195 | "name": "New Luma Yoga Collection", 196 | "url_key": "new-luma-yoga-collection-8", 197 | "path": "1/2/7/8", 198 | "url_path": "collections/yoga-new/new-luma-yoga-collection-8", 199 | "is_active": true, 200 | "position": 1, 201 | "level": 3, 202 | "product_count": 347, 203 | "children_data": [] 204 | }, 205 | { 206 | "id": 34, 207 | "parent_id": 7, 208 | "name": "Erin Recommends", 209 | "url_key": "erin-recommends-34", 210 | "path": "1/2/7/34", 211 | "url_path": "collections/erin-recommends/erin-recommends-34", 212 | "is_active": true, 213 | "position": 2, 214 | "level": 3, 215 | "product_count": 279, 216 | "children_data": [] 217 | }, 218 | { 219 | "id": 32, 220 | "parent_id": 29, 221 | "name": "Pants", 222 | "url_key": "pants-32", 223 | "path": "1/2/29/32", 224 | "url_path": "promotions/pants-all/pants-32", 225 | "is_active": true, 226 | "position": 3, 227 | "level": 3, 228 | "product_count": 247, 229 | "children_data": [] 230 | }, 231 | { 232 | "id": 30, 233 | "parent_id": 29, 234 | "name": "Women Sale", 235 | "url_key": "women-sale-30", 236 | "path": "1/2/29/30", 237 | "url_path": "promotions/women-sale/women-sale-30", 238 | "is_active": true, 239 | "position": 1, 240 | "level": 3, 241 | "product_count": 224, 242 | "children_data": [] 243 | }, 244 | { 245 | "id": 5, 246 | "parent_id": 3, 247 | "name": "Fitness Equipment", 248 | "url_key": "fitness-equipment-5", 249 | "path": "1/2/3/5", 250 | "url_path": "gear/fitness-equipment/fitness-equipment-5", 251 | "is_active": true, 252 | "position": 2, 253 | "level": 3, 254 | "product_count": 23, 255 | "children_data": [] 256 | }, 257 | { 258 | "id": 33, 259 | "parent_id": 29, 260 | "name": "Tees", 261 | "url_key": "tees-33", 262 | "path": "1/2/29/33", 263 | "url_path": "promotions/tees-all/tees-33", 264 | "is_active": true, 265 | "position": 4, 266 | "level": 3, 267 | "product_count": 192, 268 | "children_data": [] 269 | }, 270 | { 271 | "id": 10, 272 | "parent_id": 9, 273 | "name": "Video Download", 274 | "url_key": "video-download-10", 275 | "path": "1/2/9/10", 276 | "url_path": "training/training-video/video-download-10", 277 | "is_active": true, 278 | "position": 1, 279 | "level": 3, 280 | "product_count": 6, 281 | "children_data": [] 282 | }, 283 | { 284 | "id": 9, 285 | "parent_id": 2, 286 | "name": "Training", 287 | "url_key": "training-9", 288 | "path": "1/2/9", 289 | "url_path": "training/training-9", 290 | "is_active": true, 291 | "position": 5, 292 | "level": 2, 293 | "product_count": 6, 294 | "children_data": [ 295 | { 296 | "id": 10, 297 | "children_data": [] 298 | } 299 | ] 300 | }, 301 | { 302 | "id": 15, 303 | "parent_id": 12, 304 | "name": "Hoodies & Sweatshirts", 305 | "url_key": "hoodies-and-sweatshirts-15", 306 | "path": "1/2/11/12/15", 307 | "url_path": "men/tops-men/hoodies-and-sweatshirts-men/hoodies-and-sweatshirts-15", 308 | "is_active": true, 309 | "position": 2, 310 | "level": 4, 311 | "product_count": 208, 312 | "children_data": [] 313 | }, 314 | { 315 | "id": 27, 316 | "parent_id": 22, 317 | "name": "Pants", 318 | "url_key": "pants-27", 319 | "path": "1/2/20/22/27", 320 | "url_path": "women/bottoms-women/pants-women/pants-27", 321 | "is_active": true, 322 | "position": 1, 323 | "level": 4, 324 | "product_count": 91, 325 | "children_data": [] 326 | }, 327 | { 328 | "id": 35, 329 | "parent_id": 7, 330 | "name": "Performance Fabrics", 331 | "url_key": "performance-fabrics-35", 332 | "path": "1/2/7/35", 333 | "url_path": "collections/performance-fabrics/performance-fabrics-35", 334 | "is_active": true, 335 | "position": 3, 336 | "level": 3, 337 | "product_count": 310, 338 | "children_data": [] 339 | }, 340 | { 341 | "id": 6, 342 | "parent_id": 3, 343 | "name": "Watches", 344 | "url_key": "watches-6", 345 | "path": "1/2/3/6", 346 | "url_path": "gear/watches/watches-6", 347 | "is_active": true, 348 | "position": 3, 349 | "level": 3, 350 | "product_count": 9, 351 | "children_data": [] 352 | }, 353 | { 354 | "id": 4, 355 | "parent_id": 3, 356 | "name": "Bags", 357 | "url_key": "bags-4", 358 | "path": "1/2/3/4", 359 | "url_path": "gear/bags/bags-4", 360 | "is_active": true, 361 | "position": 1, 362 | "level": 3, 363 | "product_count": 14, 364 | "children_data": [] 365 | }, 366 | { 367 | "id": 36, 368 | "parent_id": 7, 369 | "name": "Eco Friendly", 370 | "url_key": "eco-friendly-36", 371 | "path": "1/2/7/36", 372 | "url_path": "collections/eco-friendly/eco-friendly-36", 373 | "is_active": true, 374 | "position": 4, 375 | "level": 3, 376 | "product_count": 247, 377 | "children_data": [] 378 | }, 379 | { 380 | "id": 20, 381 | "parent_id": 2, 382 | "name": "Women", 383 | "url_key": "women-20", 384 | "path": "1/2/20", 385 | "url_path": "women/women-20", 386 | "is_active": true, 387 | "position": 2, 388 | "level": 2, 389 | "product_count": 0, 390 | "children_data": [ 391 | { 392 | "id": 21, 393 | "children_data": [ 394 | { 395 | "id": 23, 396 | "children_data": [] 397 | }, 398 | { 399 | "id": 24, 400 | "children_data": [] 401 | }, 402 | { 403 | "id": 25, 404 | "children_data": [] 405 | }, 406 | { 407 | "id": 26, 408 | "children_data": [] 409 | } 410 | ] 411 | }, 412 | { 413 | "id": 22, 414 | "children_data": [ 415 | { 416 | "id": 27, 417 | "children_data": [] 418 | }, 419 | { 420 | "id": 28, 421 | "children_data": [] 422 | } 423 | ] 424 | } 425 | ] 426 | }, 427 | { 428 | "id": 38, 429 | "parent_id": 2, 430 | "name": "What's New", 431 | "url_key": "whats-new-38", 432 | "path": "1/2/38", 433 | "url_path": "what-is-new/whats-new-38", 434 | "is_active": true, 435 | "position": 1, 436 | "level": 2, 437 | "product_count": 0, 438 | "children_data": [] 439 | }, 440 | { 441 | "id": 2, 442 | "parent_id": 1, 443 | "name": "Default Category", 444 | "url_key": "default-category-2", 445 | "path": "1/2", 446 | "url_path": "default-category-2", 447 | "is_active": true, 448 | "position": 1, 449 | "level": 1, 450 | "product_count": 1181, 451 | "children_data": [ 452 | { 453 | "id": 38, 454 | "children_data": [] 455 | }, 456 | { 457 | "id": 20, 458 | "children_data": [ 459 | { 460 | "id": 21, 461 | "children_data": [ 462 | { 463 | "id": 23, 464 | "children_data": [] 465 | }, 466 | { 467 | "id": 24, 468 | "children_data": [] 469 | }, 470 | { 471 | "id": 25, 472 | "children_data": [] 473 | }, 474 | { 475 | "id": 26, 476 | "children_data": [] 477 | } 478 | ] 479 | }, 480 | { 481 | "id": 22, 482 | "children_data": [ 483 | { 484 | "id": 27, 485 | "children_data": [] 486 | }, 487 | { 488 | "id": 28, 489 | "children_data": [] 490 | } 491 | ] 492 | } 493 | ] 494 | }, 495 | { 496 | "id": 11, 497 | "children_data": [ 498 | { 499 | "id": 12, 500 | "children_data": [ 501 | { 502 | "id": 14, 503 | "children_data": [] 504 | }, 505 | { 506 | "id": 15, 507 | "children_data": [] 508 | }, 509 | { 510 | "id": 16, 511 | "children_data": [] 512 | }, 513 | { 514 | "id": 17, 515 | "children_data": [] 516 | } 517 | ] 518 | }, 519 | { 520 | "id": 13, 521 | "children_data": [ 522 | { 523 | "id": 18, 524 | "children_data": [] 525 | }, 526 | { 527 | "id": 19, 528 | "children_data": [] 529 | } 530 | ] 531 | } 532 | ] 533 | }, 534 | { 535 | "id": 3, 536 | "children_data": [ 537 | { 538 | "id": 4, 539 | "children_data": [] 540 | }, 541 | { 542 | "id": 5, 543 | "children_data": [] 544 | }, 545 | { 546 | "id": 6, 547 | "children_data": [] 548 | } 549 | ] 550 | }, 551 | { 552 | "id": 7, 553 | "children_data": [ 554 | { 555 | "id": 8, 556 | "children_data": [] 557 | }, 558 | { 559 | "id": 34, 560 | "children_data": [] 561 | }, 562 | { 563 | "id": 35, 564 | "children_data": [] 565 | }, 566 | { 567 | "id": 36, 568 | "children_data": [] 569 | }, 570 | { 571 | "id": 39, 572 | "children_data": [] 573 | }, 574 | { 575 | "id": 40, 576 | "children_data": [] 577 | } 578 | ] 579 | }, 580 | { 581 | "id": 9, 582 | "children_data": [ 583 | { 584 | "id": 10, 585 | "children_data": [] 586 | } 587 | ] 588 | }, 589 | { 590 | "id": 29, 591 | "children_data": [ 592 | { 593 | "id": 30, 594 | "children_data": [] 595 | }, 596 | { 597 | "id": 31, 598 | "children_data": [] 599 | }, 600 | { 601 | "id": 32, 602 | "children_data": [] 603 | }, 604 | { 605 | "id": 33, 606 | "children_data": [] 607 | } 608 | ] 609 | }, 610 | { 611 | "id": 37, 612 | "children_data": [] 613 | } 614 | ] 615 | }, 616 | { 617 | "id": 18, 618 | "parent_id": 13, 619 | "name": "Pants", 620 | "url_key": "pants-18", 621 | "path": "1/2/11/13/18", 622 | "url_path": "men/bottoms-men/pants-men/pants-18", 623 | "is_active": true, 624 | "position": 1, 625 | "level": 4, 626 | "product_count": 156, 627 | "children_data": [] 628 | }, 629 | { 630 | "id": 16, 631 | "parent_id": 12, 632 | "name": "Tees", 633 | "url_key": "tees-16", 634 | "path": "1/2/11/12/16", 635 | "url_path": "men/tops-men/tees-men/tees-16", 636 | "is_active": true, 637 | "position": 3, 638 | "level": 4, 639 | "product_count": 192, 640 | "children_data": [] 641 | }, 642 | { 643 | "id": 28, 644 | "parent_id": 22, 645 | "name": "Shorts", 646 | "url_key": "shorts-28", 647 | "path": "1/2/20/22/28", 648 | "url_path": "women/bottoms-women/shorts-women/shorts-28", 649 | "is_active": true, 650 | "position": 2, 651 | "level": 4, 652 | "product_count": 137, 653 | "children_data": [] 654 | }, 655 | { 656 | "id": 13, 657 | "parent_id": 11, 658 | "name": "Bottoms", 659 | "url_key": "bottoms-13", 660 | "path": "1/2/11/13", 661 | "url_path": "men/bottoms-men/bottoms-13", 662 | "is_active": true, 663 | "position": 2, 664 | "level": 3, 665 | "product_count": 0, 666 | "children_data": [ 667 | { 668 | "id": 18, 669 | "children_data": [] 670 | }, 671 | { 672 | "id": 19, 673 | "children_data": [] 674 | } 675 | ] 676 | }, 677 | { 678 | "id": 39, 679 | "parent_id": 7, 680 | "name": "Performance Sportswear New", 681 | "url_key": "performance-sportswear-new-39", 682 | "path": "1/2/7/39", 683 | "url_path": "collections/performance-new/performance-sportswear-new-39", 684 | "is_active": true, 685 | "position": 5, 686 | "level": 3, 687 | "product_count": 0, 688 | "children_data": [] 689 | }, 690 | { 691 | "id": 7, 692 | "parent_id": 2, 693 | "name": "Collections", 694 | "url_key": "collections-7", 695 | "path": "1/2/7", 696 | "url_path": "collections/collections-7", 697 | "is_active": false, 698 | "position": 5, 699 | "level": 2, 700 | "product_count": 13, 701 | "children_data": [ 702 | { 703 | "id": 8, 704 | "children_data": [] 705 | }, 706 | { 707 | "id": 34, 708 | "children_data": [] 709 | }, 710 | { 711 | "id": 35, 712 | "children_data": [] 713 | }, 714 | { 715 | "id": 36, 716 | "children_data": [] 717 | }, 718 | { 719 | "id": 39, 720 | "children_data": [] 721 | }, 722 | { 723 | "id": 40, 724 | "children_data": [] 725 | } 726 | ] 727 | }, 728 | { 729 | "id": 17, 730 | "parent_id": 12, 731 | "name": "Tanks", 732 | "url_key": "tanks-17", 733 | "path": "1/2/11/12/17", 734 | "url_path": "men/tops-men/tanks-men/tanks-17", 735 | "is_active": true, 736 | "position": 4, 737 | "level": 4, 738 | "product_count": 102, 739 | "children_data": [] 740 | }, 741 | { 742 | "id": 23, 743 | "parent_id": 21, 744 | "name": "Jackets", 745 | "url_key": "jackets-23", 746 | "path": "1/2/20/21/23", 747 | "url_path": "women/tops-women/jackets-women/jackets-23", 748 | "is_active": true, 749 | "position": 1, 750 | "level": 4, 751 | "product_count": 186, 752 | "children_data": [] 753 | }, 754 | { 755 | "id": 31, 756 | "parent_id": 29, 757 | "name": "Men Sale", 758 | "url_key": "men-sale-31", 759 | "path": "1/2/29/31", 760 | "url_path": "promotions/men-sale/men-sale-31", 761 | "is_active": true, 762 | "position": 2, 763 | "level": 3, 764 | "product_count": 39, 765 | "children_data": [] 766 | }, 767 | { 768 | "id": 11, 769 | "parent_id": 2, 770 | "name": "Men", 771 | "url_key": "men-11", 772 | "path": "1/2/11", 773 | "url_path": "men/men-11", 774 | "is_active": true, 775 | "position": 3, 776 | "level": 2, 777 | "product_count": 0, 778 | "children_data": [ 779 | { 780 | "id": 12, 781 | "children_data": [ 782 | { 783 | "id": 14, 784 | "children_data": [] 785 | }, 786 | { 787 | "id": 15, 788 | "children_data": [] 789 | }, 790 | { 791 | "id": 16, 792 | "children_data": [] 793 | }, 794 | { 795 | "id": 17, 796 | "children_data": [] 797 | } 798 | ] 799 | }, 800 | { 801 | "id": 13, 802 | "children_data": [ 803 | { 804 | "id": 18, 805 | "children_data": [] 806 | }, 807 | { 808 | "id": 19, 809 | "children_data": [] 810 | } 811 | ] 812 | } 813 | ] 814 | }, 815 | { 816 | "id": 3, 817 | "parent_id": 2, 818 | "name": "Gear", 819 | "url_key": "gear-3", 820 | "path": "1/2/3", 821 | "url_path": "gear/gear-3", 822 | "is_active": true, 823 | "position": 4, 824 | "level": 2, 825 | "product_count": 46, 826 | "children_data": [ 827 | { 828 | "id": 4, 829 | "children_data": [] 830 | }, 831 | { 832 | "id": 5, 833 | "children_data": [] 834 | }, 835 | { 836 | "id": 6, 837 | "children_data": [] 838 | } 839 | ] 840 | }, 841 | { 842 | "id": 37, 843 | "parent_id": 2, 844 | "name": "Sale", 845 | "url_key": "sale-37", 846 | "path": "1/2/37", 847 | "url_path": "sale/sale-37", 848 | "is_active": true, 849 | "position": 6, 850 | "level": 2, 851 | "product_count": 0, 852 | "children_data": [] 853 | } 854 | ] 855 | -------------------------------------------------------------------------------- /sample-data/fetch_demo_attributes.sh: -------------------------------------------------------------------------------- 1 | # This command requires "jq" -> https://stedolan.github.io/jq/ 2 | if ! [ -x "$(command -v jq)" ]; then 3 | echo 'Error: jq is not installed. Please download it from https://stedolan.github.io/jq/' >&2 4 | exit 1 5 | fi 6 | 7 | curl -sS "https://demo.storefrontcloud.io/api/catalog/vue_storefront_catalog/attribute/_search?size=50&from=0&sort=&_source_include=attribute_code%2Cid%2Centity_type_id%2Coptions%2Cdefault_value%2Cis_user_defined%2Cfrontend_label%2Cattribute_id%2Cdefault_frontend_label%2Cis_visible_on_front%2Cis_visible%2Cis_comparable%2Ctier_prices%2Cfrontend_input&request=%7B%22query%22%3A%7B%22bool%22%3A%7B%22filter%22%3A%7B%22bool%22%3A%7B%22must%22%3A%5B%7B%22terms%22%3A%7B%22attribute_code%22%3A%5B%22pattern%22%2C%22eco_collection%22%2C%22new%22%2C%22climate%22%2C%22style_bottom%22%2C%22size%22%2C%22color%22%2C%22performance_fabric%22%2C%22sale%22%2C%22material%22%5D%7D%7D%2C%7B%22terms%22%3A%7B%22is_user_defined%22%3A%5Btrue%5D%7D%7D%2C%7B%22terms%22%3A%7B%22is_visible%22%3A%5Btrue%5D%7D%7D%5D%7D%7D%7D%7D%7D" | jq ".hits.hits[]._source | { id, is_user_defined, is_visible, frontend_input, attribute_code, default_value, options, default_frontend_label }" | jq -s -M \ > attributes.json 8 | 9 | echo "Attributes dumped into 'attributes.json'" -------------------------------------------------------------------------------- /sample-data/fetch_demo_categories.sh: -------------------------------------------------------------------------------- 1 | # This command requires "jq" -> https://stedolan.github.io/jq/ 2 | if ! [ -x "$(command -v jq)" ]; then 3 | echo 'Error: jq is not installed. Please download it from https://stedolan.github.io/jq/' >&2 4 | exit 1 5 | fi 6 | 7 | curl -sS "https://demo.storefrontcloud.io/api/catalog/vue_storefront_catalog/category/_search?size=2500&from=0" | jq ".hits.hits[]._source | { id, parent_id, name, url_key, path, url_path, is_active, position, level, product_count, children_data: [ .children_data[] | { id, children_data: [ .children_data[] | { id, children_data: [ .children_data[] | { id, children_data: [ .children_data[] | { id } ] } ] } ] } ] }" | jq -s -M \ > categories.json 8 | 9 | echo "Categories dumped into 'categories.json'" -------------------------------------------------------------------------------- /sample-data/fetch_demo_products.sh: -------------------------------------------------------------------------------- 1 | # This command requires "jq" -> https://stedolan.github.io/jq/ 2 | if ! [ -x "$(command -v jq)" ]; then 3 | echo 'Error: jq is not installed. Please download it from https://stedolan.github.io/jq/' >&2 4 | exit 1 5 | fi 6 | 7 | curl -sS "https://demo.storefrontcloud.io/api/catalog/vue_storefront_catalog/product/_search?size=50&from=0&sort=updated_at%3Adesc&request=%7B%22query%22%3A%7B%22bool%22%3A%7B%22filter%22%3A%7B%22bool%22%3A%7B%22must%22%3A%5B%7B%22terms%22%3A%7B%22visibility%22%3A%5B2%2C3%2C4%5D%7D%7D%2C%7B%22terms%22%3A%7B%22status%22%3A%5B0%2C1%5D%7D%7D%2C%7B%22terms%22%3A%7B%22stock.is_in_stock%22%3A%5Btrue%5D%7D%7D%2C%7B%22terms%22%3A%7B%22category_ids%22%3A%5B20%2C21%2C23%2C24%2C25%2C26%2C22%2C27%2C28%5D%7D%7D%5D%7D%7D%7D%7D%2C%22aggs%22%3A%7B%22agg_terms_color%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22color%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_color_options%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22color_options%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_size%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22size%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_size_options%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22size_options%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_price%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22price%22%7D%7D%2C%22agg_range_price%22%3A%7B%22range%22%3A%7B%22field%22%3A%22price%22%2C%22ranges%22%3A%5B%7B%22from%22%3A0%2C%22to%22%3A50%7D%2C%7B%22from%22%3A50%2C%22to%22%3A100%7D%2C%7B%22from%22%3A100%2C%22to%22%3A150%7D%2C%7B%22from%22%3A150%7D%5D%7D%7D%2C%22agg_terms_erin_recommends%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22erin_recommends%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_erin_recommends_options%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22erin_recommends_options%22%2C%22size%22%3A10%7D%7D%7D%7D" | jq ".hits.hits[]._source | { id, name, image, sku, url_key, url_path, type_id, price, special_price, price_incl_tax, special_price_incl_tax, special_to_date, special_from_date, name, status, visibility, size, color, size_options, color_options, category_ids, category, media_gallery, configurable_options, stock: [ .stock | { is_in_stock, qty } ], configurable_children: [ .configurable_children[] | { type_id, sku, special_price, special_to_date, special_from_date, name, price, price_incl_tax, special_price_incl_tax, id, image, url_key, url_path, status, size, color } ] }" | jq -s -M \ > products.json 8 | 9 | echo "Products dumped into 'products.json'" -------------------------------------------------------------------------------- /sample-data/import.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // This is just an example app to import the files to Vue Storefront default index 3 | const elasticsearch = require('elasticsearch'); 4 | const fs = require('fs'); 5 | const client = new elasticsearch.Client({ 6 | hosts: [ 'http://localhost:9200'], 7 | apiVersion: '5.6' 8 | }); 9 | 10 | const fileName = process.argv[2] 11 | const entityType = process.argv[3] 12 | const indexName = process.argv[4] 13 | 14 | if (!fileName || !entityType) { 15 | console.error('Please run `node import.js [fileName] [product|attribute|category] [indexName]') 16 | } 17 | 18 | const records = JSON.parse(fs.readFileSync(fileName)) 19 | for (const record of records) { 20 | console.log(`Importing ${entityType}`, record) 21 | client.index({ 22 | index: indexName, 23 | id: record.id, 24 | type: entityType, 25 | body: record 26 | }, function(err, resp, status) { 27 | console.log(resp); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /sample-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-data", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "import.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "elasticsearch": "^16.3.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /screens/screen_0_products.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivanteLtd/storefront-integration-sdk/080a26c9f2fe674ed03bfa67579863ac3e36620e/screens/screen_0_products.png --------------------------------------------------------------------------------