react-shopwave-connect 0.4.0 → 0.4.1

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/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
@@ -565,7 +642,21 @@ async function fetchEntityById(kind, id, params = {}, options = {}) {
565
642
  const map = body?.[def.collection] ?? {};
566
643
  return map[String(id)] ?? Object.values(map).find((e) => String(e?.id) === String(id)) ?? null;
567
644
  }
568
- const saveProduct = (product, options) => saveEntity("product", product, options);
645
+ /**
646
+ * Creates (no `id`) or updates (with `id`) a product.
647
+ *
648
+ * Shopwave instances (prices) can't be updated, only replaced. Build
649
+ * `instances` with {@link toProductInstancesForSave} (passing the product as last
650
+ * read) so an edited price is sent as a new instance. As a safeguard, a new
651
+ * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
652
+ * another product would otherwise point the save at that product's instances.
653
+ */
654
+ const saveProduct = (product, options) => {
655
+ return saveEntity("product", (product.id == null || String(product.id).trim() === "") && product.instances != null ? {
656
+ ...product,
657
+ instances: toProductInstancesForSave(product.instances)
658
+ } : product, options);
659
+ };
569
660
  const deleteProduct = (id, options) => deleteEntityById("product", id, options);
570
661
  const fetchProduct = (id, params, options) => fetchEntityById("product", id, params, options);
571
662
  const saveCategory = (category, options) => saveEntity("category", category, options);
@@ -1809,6 +1900,7 @@ exports.fetchSession = fetchSession;
1809
1900
  exports.fetchStore = fetchStore;
1810
1901
  exports.fetchStores = fetchStores;
1811
1902
  exports.fetchUser = fetchUser;
1903
+ exports.findRepricedInstances = findRepricedInstances;
1812
1904
  exports.fitImageToSize = fitImageToSize;
1813
1905
  exports.formatDateForQuery = formatDateForQuery;
1814
1906
  exports.getApiErrorMap = getApiErrorMap;
@@ -1817,6 +1909,7 @@ exports.getImageUrl = getImageUrl;
1817
1909
  exports.getMerchantImage = getMerchantImage;
1818
1910
  exports.getMerchantImageUrl = getMerchantImageUrl;
1819
1911
  exports.imageFileName = imageFileName;
1912
+ exports.isSameInstancePrice = isSameInstancePrice;
1820
1913
  exports.isUploadKind = isUploadKind;
1821
1914
  exports.loginPath = loginPath;
1822
1915
  exports.logout = logout;
@@ -1836,6 +1929,7 @@ exports.savePromotion = savePromotion;
1836
1929
  exports.saveStore = saveStore;
1837
1930
  exports.setMerchantImage = setMerchantImage;
1838
1931
  exports.submitEntity = submitEntity;
1932
+ exports.toProductInstancesForSave = toProductInstancesForSave;
1839
1933
  exports.toShopwaveApiError = toShopwaveApiError;
1840
1934
  exports.toStoredImageIds = toStoredImageIds;
1841
1935
  exports.updateMerchant = updateMerchant;