├── .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 | 
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
--------------------------------------------------------------------------------