react-shopwave-connect 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -5
- package/dist/core/index.cjs +119 -48
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +80 -9
- package/dist/core/index.d.ts +80 -9
- package/dist/core/index.js +116 -49
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +129 -44
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +21 -1
- package/dist/hooks/index.d.ts +21 -1
- package/dist/hooks/index.js +128 -45
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +148 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -9
- package/dist/index.d.ts +100 -9
- package/dist/index.js +143 -49
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +83 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +63 -7
- package/dist/next/index.d.ts +63 -7
- package/dist/next/index.js +83 -3
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +80 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +60 -7
- package/dist/server/index.d.ts +60 -7
- package/dist/server/index.js +80 -3
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ await deleteCategory(created.id);
|
|
|
149
149
|
| product | `saveProduct`, `deleteProduct`, `fetchProduct` | `/api/products[/:id]` |
|
|
150
150
|
| category | `saveCategory`, `deleteCategory`, `fetchCategory` | `/api/categories[/:id]` |
|
|
151
151
|
| store | `saveStore`, `deleteStore`, `fetchStore` | `/api/stores[/:id]` |
|
|
152
|
-
| promotion | `savePromotion`, `deletePromotion` (
|
|
152
|
+
| promotion | `savePromotion`, `deletePromotion` (expires it), `fetchPromotion` | `/api/promotions[/:id]` |
|
|
153
153
|
| employee | `saveEmployee`, `deleteEmployee` (retires), `fetchEmployee` | `/api/employees[/:id]` |
|
|
154
154
|
| consumer | `fetchConsumer` only — read-only in the API | `/api/consumer[/:id]` (GET) |
|
|
155
155
|
|
|
@@ -158,7 +158,7 @@ How they behave (matches what the Shopwave API does):
|
|
|
158
158
|
- **Save** sends `{ <collection>: { "0": entity } }`. Shopwave upserts (an `id` means update), answers **201** and echoes the entity under the same ref with its `id` and server fields. The function checks `api.message.errors` and that the ref came back, and returns the saved entity (fields the echo leaves out are kept from your input). `saveEntities(kind, [a, b])` saves several at once and matches results back by ref.
|
|
159
159
|
- **Delete** is a soft delete (read it back with `deleted: true`). Shopwave answers **205 with an empty body — also for ids that don't exist**, so a resolved promise means "accepted", not "a record was deleted".
|
|
160
160
|
- **Read one** calls `GET /api/<route>/:id` and returns the record or `null`. List reads return `[]` when nothing matches (Shopwave answers with an empty body).
|
|
161
|
-
- **Promotions:**
|
|
161
|
+
- **Promotions:** promotions are never deleted, only expired. `deletePromotion` (and `DELETE /api/promotions/:id`) reads the promotion and saves the whole record back with `endDate` set to now; one that hasn't started yet gets `startDate` = `endDate` = now. (The API rejects a partial `{ id, endDate }` save with error 899.) Dates go as UTC `YYYY-MM-DD HH:mm:ss`.
|
|
162
162
|
- **Employees:** Shopwave has no employee DELETE, so `deleteEmployee` (and `DELETE /api/employees/:id`) retires the employee by setting `exitDate`. Updating an existing employee only changes `roleId`, `joinedDate` and `exitDate` — names and email are fixed — and the echo carries only those fields, so re-read with `fetchEmployee` if you need the stored record.
|
|
163
163
|
- **Consumers** are read-only (`GET /consumer` by `ids`); their write/delete routes answer 405.
|
|
164
164
|
- **Product prices (instances) can't be updated** — the API keeps them for history; a price change means a new instance with a new id. Build `instances` with `toProductInstancesForSave(formInstances, productAsLastRead)`: it keys them `"0"…"n"`, keeps an instance's `id` only when the original has that id with the same price and tax, and drops it otherwise (and drops server `timestamp`). `findRepricedInstances(formInstances, productAsLastRead)` lists the instances that will get new ids, so you can warn first. `saveProduct` without an `id` (a new product or a duplicate) always strips instance ids, so a copy can't point at the original's prices.
|
|
@@ -226,7 +226,7 @@ The shapes below are the ones adminV1 saves and adminV2 reads, so all three admi
|
|
|
226
226
|
| category | `fetchCategories`, `fetchCategory`, `saveCategory`, `deleteCategory` |
|
|
227
227
|
| consumer | `fetchConsumers`, `fetchConsumer` (read-only) |
|
|
228
228
|
| employee | `fetchEmployees`, `fetchEmployee`, `saveEmployee`, `deleteEmployee` |
|
|
229
|
-
| product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct`; prices `toProductInstancesForSave`, `findRepricedInstances`, `isSameInstancePrice` |
|
|
229
|
+
| product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct`; prices `toProductInstancesForSave`, `findRepricedInstances`, `isSameInstancePrice`; tag lists `fetchProductTags`, `fetchProductInstanceTags` (`string[]`), `normalizeTagList` |
|
|
230
230
|
| promotion | `fetchPromotions`, `fetchPromotion`, `savePromotion`, `deletePromotion` |
|
|
231
231
|
| store | `fetchStores` (now with `storeIds`), `fetchStore`, `saveStore`, `deleteStore` |
|
|
232
232
|
| report | `fetchReport` |
|
|
@@ -281,7 +281,7 @@ const { data } = useProduct({ storeId }, { baseUrl: "https://api.merchantstack.c
|
|
|
281
281
|
|
|
282
282
|
### Auto-fetch hooks (`useEffect`-based)
|
|
283
283
|
|
|
284
|
-
`useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `usePromotion`, `useStore`, `useReport`, `useSession`, `useMerchant`, `useUser` (`useUser({ enabled: false })` stays idle).
|
|
284
|
+
`useCategory`, `useConsumer`, `useEmployee`, `useProduct`, `useProductTags`, `useProductInstanceTags`, `usePromotion`, `useStore`, `useReport`, `useSession`, `useMerchant`, `useUser` (`useUser({ enabled: false })` stays idle; so do the tag hooks, so a tag field can load its list the first time it opens).
|
|
285
285
|
They fetch on mount and re-run when their arguments change. `useConsumer` and `useReport` stay idle until you pass ids / a query (they return `loading: false`, `data: null` until then). `refetch()` replaces the old `reloadFlag` argument.
|
|
286
286
|
|
|
287
287
|
### Manually-triggered hooks (`useCallback`-based)
|
|
@@ -467,12 +467,19 @@ export const { GET } = shopwave.resource("user");
|
|
|
467
467
|
|
|
468
468
|
// app/api/upload/route.ts — image upload (uploadImage)
|
|
469
469
|
export const { POST } = shopwave.upload(); // or upload({ maxBytes, accept })
|
|
470
|
+
|
|
471
|
+
// app/api/products/tags/route.ts — the merchant's product tags (fetchProductTags)
|
|
472
|
+
export const { GET } = shopwave.lookup("productTags");
|
|
473
|
+
// app/api/products/instance-tags/route.ts — price-instance (channel) tags (fetchProductInstanceTags)
|
|
474
|
+
export const { GET } = shopwave.lookup("productInstanceTags");
|
|
470
475
|
```
|
|
471
476
|
|
|
477
|
+
- `lookup(kind)`: the read-only lists in `SHOPWAVE_LOOKUPS`. `productTags` → `GET /product/tags`, `productInstanceTags` → `GET /product/instance/tags`; both answer `{ tags: string[] }`. They take no parameters, so `extras` aren't forwarded. The app routes are `products/tags` and `products/instance-tags`: static segments, so they sit beside `products/[id]`.
|
|
478
|
+
|
|
472
479
|
- `resource("merchant")`: `PUT`/`POST` take `{ merchant: {…} }` or the bare object and send every field on as `postBody`. The `id` is required (400 without it), because a POST without one would create a second merchant. `resource("user")` answers 405 to writes.
|
|
473
480
|
- `upload()`: takes multipart `file` + `kind`, checks the size (default 10 MB → 413) and type (default `image/*` → 415), and `PUT`s only the file to `/uploader` with the `contentType` header. `extras` are ignored on uploads. Shopwave's reply (201 `{ fileName, path }`) is passed through.
|
|
474
481
|
|
|
475
|
-
Entities: `product`, `category`, `store`, `promotion`, `employee` (DELETE retires via `exitDate`), `promotion` (DELETE
|
|
482
|
+
Entities: `product`, `category`, `store`, `promotion`, `employee` (DELETE retires via `exitDate`), `promotion` (DELETE expires it: the whole record saved back with `endDate`, and a not-yet-started `startDate`, set to now), `consumer` (mounted at `/api/consumer`; GET only, writes answer 405). For anything else, `shopwave.forward(request, { method, path, headers, postBody })` does the same token/refresh/encoding work for a custom handler.
|
|
476
483
|
|
|
477
484
|
Every handler:
|
|
478
485
|
|
|
@@ -503,6 +510,11 @@ Options: `apiVersion` (default `"2.0"`), `fetch`, `entities` (override a route/h
|
|
|
503
510
|
|
|
504
511
|
---
|
|
505
512
|
|
|
513
|
+
## Contributing
|
|
514
|
+
|
|
515
|
+
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): how the package is built and the rules for changing it: layers, request flow, contract tables, endpoint kinds (and how to choose one), a checklist for adding an endpoint, code rules, known gaps, and versioning.
|
|
516
|
+
- [CLAUDE.md](CLAUDE.md): the short rules for coding agents working in this repo.
|
|
517
|
+
|
|
506
518
|
## Build
|
|
507
519
|
|
|
508
520
|
Built with [`tsdown`](https://tsdown.dev) into dual ESM + CJS with `.d.ts` types, via separate entries: `core` (no externals), `hooks` (`react`/`react-dom` external), `server` (no externals) and `next` (`next`/`iron-session` external — optional peer dependencies). `server` and `next` are never re-exported from the root entry, so they can't end up in client bundles.
|
package/dist/core/index.cjs
CHANGED
|
@@ -350,6 +350,106 @@ async function fetchProductsMap(productIds, options = {}, batchSize = 200) {
|
|
|
350
350
|
return result;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/core/resources.ts
|
|
355
|
+
/**
|
|
356
|
+
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
357
|
+
* updates it (without an `id` the API would create a second merchant, so the
|
|
358
|
+
* route requires one).
|
|
359
|
+
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
360
|
+
* needs the admin scope).
|
|
361
|
+
*/
|
|
362
|
+
const SHOPWAVE_RESOURCES = Object.freeze({
|
|
363
|
+
merchant: {
|
|
364
|
+
route: "merchant",
|
|
365
|
+
key: "merchant",
|
|
366
|
+
upstream: "merchant",
|
|
367
|
+
writable: true
|
|
368
|
+
},
|
|
369
|
+
user: {
|
|
370
|
+
route: "user",
|
|
371
|
+
key: "user",
|
|
372
|
+
upstream: "user",
|
|
373
|
+
writable: false
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
/** `UploadKind` → the API's `contentType` value. */
|
|
377
|
+
const UPLOAD_KINDS = Object.freeze({
|
|
378
|
+
merchant: "merchant",
|
|
379
|
+
merchantFeature: "merchantFeature",
|
|
380
|
+
product: "product",
|
|
381
|
+
user: "user",
|
|
382
|
+
applicationLogo: "applicaionLogo",
|
|
383
|
+
applicationImages: "applicaionImages"
|
|
384
|
+
});
|
|
385
|
+
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
386
|
+
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
387
|
+
route: "upload",
|
|
388
|
+
upstream: "uploader",
|
|
389
|
+
kindHeader: "contentType",
|
|
390
|
+
fileField: "file",
|
|
391
|
+
kindField: "kind"
|
|
392
|
+
});
|
|
393
|
+
function isUploadKind(value) {
|
|
394
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
395
|
+
}
|
|
396
|
+
const SHOPWAVE_LOOKUPS = Object.freeze({
|
|
397
|
+
productTags: {
|
|
398
|
+
route: "products/tags",
|
|
399
|
+
key: "tags",
|
|
400
|
+
upstream: "product/tags"
|
|
401
|
+
},
|
|
402
|
+
productInstanceTags: {
|
|
403
|
+
route: "products/instance-tags",
|
|
404
|
+
key: "tags",
|
|
405
|
+
upstream: "product/instance/tags"
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/core/productTags.ts
|
|
411
|
+
/**
|
|
412
|
+
* Turns a tag list from the API into clean strings: trims each tag, drops
|
|
413
|
+
* blanks and anything that isn't text, and removes exact duplicates (first one
|
|
414
|
+
* wins). The API's own order is kept. A map is accepted too (its values are
|
|
415
|
+
* used), in case the API ever answers keyed by id.
|
|
416
|
+
*/
|
|
417
|
+
function normalizeTagList(value) {
|
|
418
|
+
const list = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
|
|
419
|
+
const seen = /* @__PURE__ */ new Set();
|
|
420
|
+
const out = [];
|
|
421
|
+
for (const item of list) {
|
|
422
|
+
if (typeof item !== "string" && typeof item !== "number") continue;
|
|
423
|
+
const tag = String(item).trim();
|
|
424
|
+
if (!tag || seen.has(tag)) continue;
|
|
425
|
+
seen.add(tag);
|
|
426
|
+
out.push(tag);
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
async function fetchLookup(kind, options) {
|
|
431
|
+
const def = SHOPWAVE_LOOKUPS[kind];
|
|
432
|
+
if (!def) throw new Error(`Unknown Shopwave lookup "${kind}"`);
|
|
433
|
+
const json = await apiGet(`/api/${def.route}`, {}, options);
|
|
434
|
+
assertNoApiErrors(json, 200);
|
|
435
|
+
return normalizeTagList(json?.[def.key]);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Every distinct tag used on the merchant's products (`GET /product/tags`),
|
|
439
|
+
* for a tag field's suggestions. Returns `[]` when there are none.
|
|
440
|
+
*/
|
|
441
|
+
function fetchProductTags(options = {}) {
|
|
442
|
+
return fetchLookup("productTags", options);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Every distinct tag used on the merchant's price instances
|
|
446
|
+
* (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).
|
|
447
|
+
* Returns `[]` when there are none.
|
|
448
|
+
*/
|
|
449
|
+
function fetchProductInstanceTags(options = {}) {
|
|
450
|
+
return fetchLookup("productInstanceTags", options);
|
|
451
|
+
}
|
|
452
|
+
|
|
353
453
|
//#endregion
|
|
354
454
|
//#region src/core/promotion.ts
|
|
355
455
|
/** Promotion rule identifiers as stored by the Shopwave API. */
|
|
@@ -484,7 +584,10 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
|
|
|
484
584
|
* (https://developer.merchantstack.com/api-reference.html) and checked against
|
|
485
585
|
* the live API where noted:
|
|
486
586
|
* - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
|
|
487
|
-
* - promotion: no DELETE;
|
|
587
|
+
* - promotion: no DELETE; promotions are never deleted, only expired. Expiring
|
|
588
|
+
* reads the promotion and saves the whole record back with `endDate` = now (and
|
|
589
|
+
* `startDate` = now too if it hadn't started); the live API rejects a partial
|
|
590
|
+
* `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
|
|
488
591
|
* - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
|
|
489
592
|
* changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
|
|
490
593
|
* - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
|
|
@@ -525,7 +628,9 @@ const SHOPWAVE_ENTITIES = Object.freeze({
|
|
|
525
628
|
idHeader: "promotionId",
|
|
526
629
|
writable: true,
|
|
527
630
|
deleteMode: "retire",
|
|
528
|
-
retireField: "endDate"
|
|
631
|
+
retireField: "endDate",
|
|
632
|
+
retireWithRecord: true,
|
|
633
|
+
retireStartField: "startDate"
|
|
529
634
|
},
|
|
530
635
|
employee: {
|
|
531
636
|
route: "employees",
|
|
@@ -600,7 +705,8 @@ async function saveEntity(kind, item, options = {}) {
|
|
|
600
705
|
/**
|
|
601
706
|
* Deletes one entity (a soft delete: it stays readable with `deleted: true`).
|
|
602
707
|
* Employees and promotions can't be deleted in Shopwave; for them the route
|
|
603
|
-
* sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`)
|
|
708
|
+
* sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
|
|
709
|
+
* promotion is saved back whole (see {@link deletePromotion}).
|
|
604
710
|
*
|
|
605
711
|
* Shopwave answers 205 with an empty body — also for ids that don't exist — so
|
|
606
712
|
* a resolved promise means "the API accepted the request", not "a record was
|
|
@@ -641,7 +747,12 @@ const saveStore = (store, options) => saveEntity("store", store, options);
|
|
|
641
747
|
const deleteStore = (id, options) => deleteEntityById("store", id, options);
|
|
642
748
|
const fetchStore = (id, params, options) => fetchEntityById("store", id, params, options);
|
|
643
749
|
const savePromotion = (promotion, options) => saveEntity("promotion", promotion, options);
|
|
644
|
-
/**
|
|
750
|
+
/**
|
|
751
|
+
* Expires a promotion (Shopwave never deletes promotions). The route reads the
|
|
752
|
+
* promotion and saves it back with `endDate` set to now; a promotion that hasn't
|
|
753
|
+
* started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
|
|
754
|
+
* exist or the save fails.
|
|
755
|
+
*/
|
|
645
756
|
const deletePromotion = (id, options) => deleteEntityById("promotion", id, options);
|
|
646
757
|
const fetchPromotion = (id, params, options) => fetchEntityById("promotion", id, params, options);
|
|
647
758
|
/**
|
|
@@ -800,50 +911,6 @@ function buildConsumerMap(consumers) {
|
|
|
800
911
|
return map;
|
|
801
912
|
}
|
|
802
913
|
|
|
803
|
-
//#endregion
|
|
804
|
-
//#region src/core/resources.ts
|
|
805
|
-
/**
|
|
806
|
-
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
807
|
-
* updates it (without an `id` the API would create a second merchant, so the
|
|
808
|
-
* route requires one).
|
|
809
|
-
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
810
|
-
* needs the admin scope).
|
|
811
|
-
*/
|
|
812
|
-
const SHOPWAVE_RESOURCES = Object.freeze({
|
|
813
|
-
merchant: {
|
|
814
|
-
route: "merchant",
|
|
815
|
-
key: "merchant",
|
|
816
|
-
upstream: "merchant",
|
|
817
|
-
writable: true
|
|
818
|
-
},
|
|
819
|
-
user: {
|
|
820
|
-
route: "user",
|
|
821
|
-
key: "user",
|
|
822
|
-
upstream: "user",
|
|
823
|
-
writable: false
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
/** `UploadKind` → the API's `contentType` value. */
|
|
827
|
-
const UPLOAD_KINDS = Object.freeze({
|
|
828
|
-
merchant: "merchant",
|
|
829
|
-
merchantFeature: "merchantFeature",
|
|
830
|
-
product: "product",
|
|
831
|
-
user: "user",
|
|
832
|
-
applicationLogo: "applicaionLogo",
|
|
833
|
-
applicationImages: "applicaionImages"
|
|
834
|
-
});
|
|
835
|
-
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
836
|
-
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
837
|
-
route: "upload",
|
|
838
|
-
upstream: "uploader",
|
|
839
|
-
kindHeader: "contentType",
|
|
840
|
-
fileField: "file",
|
|
841
|
-
kindField: "kind"
|
|
842
|
-
});
|
|
843
|
-
function isUploadKind(value) {
|
|
844
|
-
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
914
|
//#endregion
|
|
848
915
|
//#region src/core/imageFit.ts
|
|
849
916
|
/**
|
|
@@ -1294,6 +1361,7 @@ exports.MERCHANT_READ_ONLY_FIELDS = MERCHANT_READ_ONLY_FIELDS;
|
|
|
1294
1361
|
exports.PromotionRuleSet = PromotionRuleSet;
|
|
1295
1362
|
exports.SHOPWAVE_ENTITIES = SHOPWAVE_ENTITIES;
|
|
1296
1363
|
exports.SHOPWAVE_IMAGE_BASE_URL = SHOPWAVE_IMAGE_BASE_URL;
|
|
1364
|
+
exports.SHOPWAVE_LOOKUPS = SHOPWAVE_LOOKUPS;
|
|
1297
1365
|
exports.SHOPWAVE_RESOURCES = SHOPWAVE_RESOURCES;
|
|
1298
1366
|
exports.SHOPWAVE_UPLOAD = SHOPWAVE_UPLOAD;
|
|
1299
1367
|
exports.ShopwaveApiError = ShopwaveApiError;
|
|
@@ -1327,6 +1395,8 @@ exports.fetchEmployees = fetchEmployees;
|
|
|
1327
1395
|
exports.fetchEntityById = fetchEntityById;
|
|
1328
1396
|
exports.fetchMerchant = fetchMerchant;
|
|
1329
1397
|
exports.fetchProduct = fetchProduct;
|
|
1398
|
+
exports.fetchProductInstanceTags = fetchProductInstanceTags;
|
|
1399
|
+
exports.fetchProductTags = fetchProductTags;
|
|
1330
1400
|
exports.fetchProducts = fetchProducts;
|
|
1331
1401
|
exports.fetchProductsMap = fetchProductsMap;
|
|
1332
1402
|
exports.fetchPromotion = fetchPromotion;
|
|
@@ -1353,6 +1423,7 @@ exports.logoutPath = logoutPath;
|
|
|
1353
1423
|
exports.merchantLinksFromForm = merchantLinksFromForm;
|
|
1354
1424
|
exports.merchantLinksToForm = merchantLinksToForm;
|
|
1355
1425
|
exports.normalizeMerchantLinks = normalizeMerchantLinks;
|
|
1426
|
+
exports.normalizeTagList = normalizeTagList;
|
|
1356
1427
|
exports.parseBasketReportData = parseBasketReportData;
|
|
1357
1428
|
exports.readImageSize = readImageSize;
|
|
1358
1429
|
exports.rolesById = rolesById;
|