react-shopwave-connect 0.4.0 → 0.4.2
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 +5 -4
- package/dist/core/index.cjs +116 -11
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +95 -7
- package/dist/core/index.d.ts +95 -7
- package/dist/core/index.js +114 -12
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +116 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -7
- package/dist/index.d.ts +95 -7
- package/dist/index.js +114 -12
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +59 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +18 -3
- package/dist/next/index.d.ts +18 -3
- package/dist/next/index.js +59 -2
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +59 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +18 -3
- package/dist/server/index.d.ts +18 -3
- package/dist/server/index.js +59 -2
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -66,8 +66,8 @@ function getApiErrorMap(body) {
|
|
|
66
66
|
}
|
|
67
67
|
function describeErrors(errors) {
|
|
68
68
|
return Object.entries(errors).map(([code, e]) => {
|
|
69
|
-
const text = [e?.title, e?.details].filter(Boolean).join(": ");
|
|
70
|
-
return text ? `${code} ${text}` : code;
|
|
69
|
+
const text$1 = [e?.title, e?.details].filter(Boolean).join(": ");
|
|
70
|
+
return text$1 ? `${code} ${text$1}` : code;
|
|
71
71
|
}).join("; ");
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
@@ -129,12 +129,12 @@ function buildExtras(extras) {
|
|
|
129
129
|
}
|
|
130
130
|
/** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */
|
|
131
131
|
async function readBody(response) {
|
|
132
|
-
const text = await response.text();
|
|
133
|
-
if (!text) return null;
|
|
132
|
+
const text$1 = await response.text();
|
|
133
|
+
if (!text$1) return null;
|
|
134
134
|
try {
|
|
135
|
-
return JSON.parse(text);
|
|
135
|
+
return JSON.parse(text$1);
|
|
136
136
|
} catch {
|
|
137
|
-
return { raw: text };
|
|
137
|
+
return { raw: text$1 };
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
@@ -282,6 +282,83 @@ async function fetchProducts(params = {}, options = {}) {
|
|
|
282
282
|
assertNoApiErrors(json, 200);
|
|
283
283
|
return Object.values(json?.products ?? {});
|
|
284
284
|
}
|
|
285
|
+
/** Fields the server sets on an instance; never sent back. */
|
|
286
|
+
const SERVER_INSTANCE_FIELDS = ["timestamp"];
|
|
287
|
+
function instanceList(instances) {
|
|
288
|
+
if (!instances) return [];
|
|
289
|
+
return (Array.isArray(instances) ? [...instances] : Object.values(instances)).filter((inst) => inst != null && typeof inst === "object");
|
|
290
|
+
}
|
|
291
|
+
function hasId(id) {
|
|
292
|
+
return id != null && String(id).trim() !== "";
|
|
293
|
+
}
|
|
294
|
+
/** Same number, so `"422.0"`, `"422"` and `422` are equal; blanks equal each other. */
|
|
295
|
+
function sameAmount(a, b) {
|
|
296
|
+
const blank = (v) => v == null || String(v).trim() === "";
|
|
297
|
+
if (blank(a) || blank(b)) return blank(a) && blank(b);
|
|
298
|
+
const x = Number(a);
|
|
299
|
+
const y = Number(b);
|
|
300
|
+
return Number.isNaN(x) || Number.isNaN(y) ? String(a).trim() === String(b).trim() : x === y;
|
|
301
|
+
}
|
|
302
|
+
/** True when two instances have the same price and tax (the fields Shopwave won't update). */
|
|
303
|
+
function isSameInstancePrice(a, b) {
|
|
304
|
+
return sameAmount(a.price, b.price) && sameAmount(a.taxPercentage, b.taxPercentage);
|
|
305
|
+
}
|
|
306
|
+
const text = (v) => v == null ? "" : String(v);
|
|
307
|
+
/**
|
|
308
|
+
* Lists the instances whose price or tax differs from the `original` instance
|
|
309
|
+
* with the same id: saving them creates new instances with new ids, which can
|
|
310
|
+
* affect anything that stores instance ids (integrations, reports, modifiers).
|
|
311
|
+
* Use it to warn before saving. New rows (no id) aren't listed.
|
|
312
|
+
*/
|
|
313
|
+
function findRepricedInstances(instances, original) {
|
|
314
|
+
const byId = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
|
|
315
|
+
const result = [];
|
|
316
|
+
instanceList(instances).forEach((inst, index) => {
|
|
317
|
+
if (!hasId(inst.id)) return;
|
|
318
|
+
const before = byId.get(String(inst.id));
|
|
319
|
+
if (!before || isSameInstancePrice(inst, before)) return;
|
|
320
|
+
result.push({
|
|
321
|
+
index,
|
|
322
|
+
id: inst.id,
|
|
323
|
+
name: text(inst.name),
|
|
324
|
+
before: {
|
|
325
|
+
price: text(before.price),
|
|
326
|
+
taxPercentage: text(before.taxPercentage)
|
|
327
|
+
},
|
|
328
|
+
after: {
|
|
329
|
+
price: text(inst.price),
|
|
330
|
+
taxPercentage: text(inst.taxPercentage)
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Builds the `instances` map for a product save:
|
|
338
|
+
* - keyed by position (`"0"`, `"1"`, …), like every other ref in a save;
|
|
339
|
+
* - an instance keeps its `id` only when `original` has an instance with that id
|
|
340
|
+
* and the same price and tax; otherwise the id is removed, so Shopwave creates
|
|
341
|
+
* a new instance instead of re-pointing or rewriting someone else's;
|
|
342
|
+
* - server fields (`timestamp`) are removed.
|
|
343
|
+
*
|
|
344
|
+
* Pass the product as last read from the API as `original`. Leave it out for a
|
|
345
|
+
* new product (including a duplicate): every id is then removed.
|
|
346
|
+
* Instances left out of the list are left out of the save (removed).
|
|
347
|
+
*/
|
|
348
|
+
function toProductInstancesForSave(instances, original) {
|
|
349
|
+
const keep = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
|
|
350
|
+
const out = {};
|
|
351
|
+
instanceList(instances).forEach((inst, index) => {
|
|
352
|
+
const { id,...rest } = inst;
|
|
353
|
+
for (const field of SERVER_INSTANCE_FIELDS) delete rest[field];
|
|
354
|
+
const before = hasId(id) ? keep.get(String(id)) : void 0;
|
|
355
|
+
out[String(index)] = before && isSameInstancePrice(inst, before) ? {
|
|
356
|
+
id,
|
|
357
|
+
...rest
|
|
358
|
+
} : rest;
|
|
359
|
+
});
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
285
362
|
/**
|
|
286
363
|
* Fetches products by id and returns them keyed by id, transparently batching
|
|
287
364
|
* the request (default 200 ids per call) to stay within API limits. Extracted
|
|
@@ -432,7 +509,10 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
|
|
|
432
509
|
* (https://developer.merchantstack.com/api-reference.html) and checked against
|
|
433
510
|
* the live API where noted:
|
|
434
511
|
* - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
|
|
435
|
-
* - promotion: no DELETE;
|
|
512
|
+
* - promotion: no DELETE; promotions are never deleted, only expired. Expiring
|
|
513
|
+
* reads the promotion and saves the whole record back with `endDate` = now (and
|
|
514
|
+
* `startDate` = now too if it hadn't started); the live API rejects a partial
|
|
515
|
+
* `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
|
|
436
516
|
* - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
|
|
437
517
|
* changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
|
|
438
518
|
* - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
|
|
@@ -473,7 +553,9 @@ const SHOPWAVE_ENTITIES = Object.freeze({
|
|
|
473
553
|
idHeader: "promotionId",
|
|
474
554
|
writable: true,
|
|
475
555
|
deleteMode: "retire",
|
|
476
|
-
retireField: "endDate"
|
|
556
|
+
retireField: "endDate",
|
|
557
|
+
retireWithRecord: true,
|
|
558
|
+
retireStartField: "startDate"
|
|
477
559
|
},
|
|
478
560
|
employee: {
|
|
479
561
|
route: "employees",
|
|
@@ -548,7 +630,8 @@ async function saveEntity(kind, item, options = {}) {
|
|
|
548
630
|
/**
|
|
549
631
|
* Deletes one entity (a soft delete: it stays readable with `deleted: true`).
|
|
550
632
|
* Employees and promotions can't be deleted in Shopwave; for them the route
|
|
551
|
-
* sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`)
|
|
633
|
+
* sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`), and a
|
|
634
|
+
* promotion is saved back whole (see {@link deletePromotion}).
|
|
552
635
|
*
|
|
553
636
|
* Shopwave answers 205 with an empty body — also for ids that don't exist — so
|
|
554
637
|
* a resolved promise means "the API accepted the request", not "a record was
|
|
@@ -565,7 +648,21 @@ async function fetchEntityById(kind, id, params = {}, options = {}) {
|
|
|
565
648
|
const map = body?.[def.collection] ?? {};
|
|
566
649
|
return map[String(id)] ?? Object.values(map).find((e) => String(e?.id) === String(id)) ?? null;
|
|
567
650
|
}
|
|
568
|
-
|
|
651
|
+
/**
|
|
652
|
+
* Creates (no `id`) or updates (with `id`) a product.
|
|
653
|
+
*
|
|
654
|
+
* Shopwave instances (prices) can't be updated, only replaced. Build
|
|
655
|
+
* `instances` with {@link toProductInstancesForSave} (passing the product as last
|
|
656
|
+
* read) so an edited price is sent as a new instance. As a safeguard, a new
|
|
657
|
+
* product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
|
|
658
|
+
* another product would otherwise point the save at that product's instances.
|
|
659
|
+
*/
|
|
660
|
+
const saveProduct = (product, options) => {
|
|
661
|
+
return saveEntity("product", (product.id == null || String(product.id).trim() === "") && product.instances != null ? {
|
|
662
|
+
...product,
|
|
663
|
+
instances: toProductInstancesForSave(product.instances)
|
|
664
|
+
} : product, options);
|
|
665
|
+
};
|
|
569
666
|
const deleteProduct = (id, options) => deleteEntityById("product", id, options);
|
|
570
667
|
const fetchProduct = (id, params, options) => fetchEntityById("product", id, params, options);
|
|
571
668
|
const saveCategory = (category, options) => saveEntity("category", category, options);
|
|
@@ -575,7 +672,12 @@ const saveStore = (store, options) => saveEntity("store", store, options);
|
|
|
575
672
|
const deleteStore = (id, options) => deleteEntityById("store", id, options);
|
|
576
673
|
const fetchStore = (id, params, options) => fetchEntityById("store", id, params, options);
|
|
577
674
|
const savePromotion = (promotion, options) => saveEntity("promotion", promotion, options);
|
|
578
|
-
/**
|
|
675
|
+
/**
|
|
676
|
+
* Expires a promotion (Shopwave never deletes promotions). The route reads the
|
|
677
|
+
* promotion and saves it back with `endDate` set to now; a promotion that hasn't
|
|
678
|
+
* started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't
|
|
679
|
+
* exist or the save fails.
|
|
680
|
+
*/
|
|
579
681
|
const deletePromotion = (id, options) => deleteEntityById("promotion", id, options);
|
|
580
682
|
const fetchPromotion = (id, params, options) => fetchEntityById("promotion", id, params, options);
|
|
581
683
|
/**
|
|
@@ -1809,6 +1911,7 @@ exports.fetchSession = fetchSession;
|
|
|
1809
1911
|
exports.fetchStore = fetchStore;
|
|
1810
1912
|
exports.fetchStores = fetchStores;
|
|
1811
1913
|
exports.fetchUser = fetchUser;
|
|
1914
|
+
exports.findRepricedInstances = findRepricedInstances;
|
|
1812
1915
|
exports.fitImageToSize = fitImageToSize;
|
|
1813
1916
|
exports.formatDateForQuery = formatDateForQuery;
|
|
1814
1917
|
exports.getApiErrorMap = getApiErrorMap;
|
|
@@ -1817,6 +1920,7 @@ exports.getImageUrl = getImageUrl;
|
|
|
1817
1920
|
exports.getMerchantImage = getMerchantImage;
|
|
1818
1921
|
exports.getMerchantImageUrl = getMerchantImageUrl;
|
|
1819
1922
|
exports.imageFileName = imageFileName;
|
|
1923
|
+
exports.isSameInstancePrice = isSameInstancePrice;
|
|
1820
1924
|
exports.isUploadKind = isUploadKind;
|
|
1821
1925
|
exports.loginPath = loginPath;
|
|
1822
1926
|
exports.logout = logout;
|
|
@@ -1836,6 +1940,7 @@ exports.savePromotion = savePromotion;
|
|
|
1836
1940
|
exports.saveStore = saveStore;
|
|
1837
1941
|
exports.setMerchantImage = setMerchantImage;
|
|
1838
1942
|
exports.submitEntity = submitEntity;
|
|
1943
|
+
exports.toProductInstancesForSave = toProductInstancesForSave;
|
|
1839
1944
|
exports.toShopwaveApiError = toShopwaveApiError;
|
|
1840
1945
|
exports.toStoredImageIds = toStoredImageIds;
|
|
1841
1946
|
exports.updateMerchant = updateMerchant;
|