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/README.md CHANGED
@@ -161,6 +161,7 @@ How they behave (matches what the Shopwave API does):
161
161
  - **Promotions:** Shopwave has no promotion DELETE; `deletePromotion` (and `DELETE /api/promotions/:id`) ends the promotion by setting `endDate` to now.
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
+ - **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.
164
165
  - Errors are read from `api.message.errors` and also `api.message.error` (the name used in the [API reference](https://developer.merchantstack.com/api-reference.html)).
165
166
 
166
167
  The generic forms are `saveEntity(kind, item)`, `deleteEntityById(kind, id)` and `fetchEntityById(kind, id)`; `SHOPWAVE_ENTITIES` lists each entity's route, collection key, Shopwave path and id headers.
@@ -225,7 +226,7 @@ The shapes below are the ones adminV1 saves and adminV2 reads, so all three admi
225
226
  | category | `fetchCategories`, `fetchCategory`, `saveCategory`, `deleteCategory` |
226
227
  | consumer | `fetchConsumers`, `fetchConsumer` (read-only) |
227
228
  | employee | `fetchEmployees`, `fetchEmployee`, `saveEmployee`, `deleteEmployee` |
228
- | product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct` |
229
+ | product | `fetchProducts`, `fetchProductsMap` (batched, keyed), `fetchProduct`, `saveProduct`, `deleteProduct`; prices `toProductInstancesForSave`, `findRepricedInstances`, `isSameInstancePrice` |
229
230
  | promotion | `fetchPromotions`, `fetchPromotion`, `savePromotion`, `deletePromotion` |
230
231
  | store | `fetchStores` (now with `storeIds`), `fetchStore`, `saveStore`, `deleteStore` |
231
232
  | report | `fetchReport` |
@@ -41,8 +41,8 @@ function getApiErrorMap(body) {
41
41
  }
42
42
  function describeErrors(errors) {
43
43
  return Object.entries(errors).map(([code, e]) => {
44
- const text = [e?.title, e?.details].filter(Boolean).join(": ");
45
- return text ? `${code} ${text}` : code;
44
+ const text$1 = [e?.title, e?.details].filter(Boolean).join(": ");
45
+ return text$1 ? `${code} ${text$1}` : code;
46
46
  }).join("; ");
47
47
  }
48
48
  /**
@@ -104,12 +104,12 @@ function buildExtras(extras) {
104
104
  }
105
105
  /** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */
106
106
  async function readBody(response) {
107
- const text = await response.text();
108
- if (!text) return null;
107
+ const text$1 = await response.text();
108
+ if (!text$1) return null;
109
109
  try {
110
- return JSON.parse(text);
110
+ return JSON.parse(text$1);
111
111
  } catch {
112
- return { raw: text };
112
+ return { raw: text$1 };
113
113
  }
114
114
  }
115
115
  /**
@@ -257,6 +257,83 @@ async function fetchProducts(params = {}, options = {}) {
257
257
  assertNoApiErrors(json, 200);
258
258
  return Object.values(json?.products ?? {});
259
259
  }
260
+ /** Fields the server sets on an instance; never sent back. */
261
+ const SERVER_INSTANCE_FIELDS = ["timestamp"];
262
+ function instanceList(instances) {
263
+ if (!instances) return [];
264
+ return (Array.isArray(instances) ? [...instances] : Object.values(instances)).filter((inst) => inst != null && typeof inst === "object");
265
+ }
266
+ function hasId(id) {
267
+ return id != null && String(id).trim() !== "";
268
+ }
269
+ /** Same number, so `"422.0"`, `"422"` and `422` are equal; blanks equal each other. */
270
+ function sameAmount(a, b) {
271
+ const blank = (v) => v == null || String(v).trim() === "";
272
+ if (blank(a) || blank(b)) return blank(a) && blank(b);
273
+ const x = Number(a);
274
+ const y = Number(b);
275
+ return Number.isNaN(x) || Number.isNaN(y) ? String(a).trim() === String(b).trim() : x === y;
276
+ }
277
+ /** True when two instances have the same price and tax (the fields Shopwave won't update). */
278
+ function isSameInstancePrice(a, b) {
279
+ return sameAmount(a.price, b.price) && sameAmount(a.taxPercentage, b.taxPercentage);
280
+ }
281
+ const text = (v) => v == null ? "" : String(v);
282
+ /**
283
+ * Lists the instances whose price or tax differs from the `original` instance
284
+ * with the same id: saving them creates new instances with new ids, which can
285
+ * affect anything that stores instance ids (integrations, reports, modifiers).
286
+ * Use it to warn before saving. New rows (no id) aren't listed.
287
+ */
288
+ function findRepricedInstances(instances, original) {
289
+ const byId = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
290
+ const result = [];
291
+ instanceList(instances).forEach((inst, index) => {
292
+ if (!hasId(inst.id)) return;
293
+ const before = byId.get(String(inst.id));
294
+ if (!before || isSameInstancePrice(inst, before)) return;
295
+ result.push({
296
+ index,
297
+ id: inst.id,
298
+ name: text(inst.name),
299
+ before: {
300
+ price: text(before.price),
301
+ taxPercentage: text(before.taxPercentage)
302
+ },
303
+ after: {
304
+ price: text(inst.price),
305
+ taxPercentage: text(inst.taxPercentage)
306
+ }
307
+ });
308
+ });
309
+ return result;
310
+ }
311
+ /**
312
+ * Builds the `instances` map for a product save:
313
+ * - keyed by position (`"0"`, `"1"`, …), like every other ref in a save;
314
+ * - an instance keeps its `id` only when `original` has an instance with that id
315
+ * and the same price and tax; otherwise the id is removed, so Shopwave creates
316
+ * a new instance instead of re-pointing or rewriting someone else's;
317
+ * - server fields (`timestamp`) are removed.
318
+ *
319
+ * Pass the product as last read from the API as `original`. Leave it out for a
320
+ * new product (including a duplicate): every id is then removed.
321
+ * Instances left out of the list are left out of the save (removed).
322
+ */
323
+ function toProductInstancesForSave(instances, original) {
324
+ const keep = new Map(instanceList(original).filter((o) => hasId(o.id)).map((o) => [String(o.id), o]));
325
+ const out = {};
326
+ instanceList(instances).forEach((inst, index) => {
327
+ const { id,...rest } = inst;
328
+ for (const field of SERVER_INSTANCE_FIELDS) delete rest[field];
329
+ const before = hasId(id) ? keep.get(String(id)) : void 0;
330
+ out[String(index)] = before && isSameInstancePrice(inst, before) ? {
331
+ id,
332
+ ...rest
333
+ } : rest;
334
+ });
335
+ return out;
336
+ }
260
337
  /**
261
338
  * Fetches products by id and returns them keyed by id, transparently batching
262
339
  * the request (default 200 ids per call) to stay within API limits. Extracted
@@ -540,7 +617,21 @@ async function fetchEntityById(kind, id, params = {}, options = {}) {
540
617
  const map = body?.[def.collection] ?? {};
541
618
  return map[String(id)] ?? Object.values(map).find((e) => String(e?.id) === String(id)) ?? null;
542
619
  }
543
- const saveProduct = (product, options) => saveEntity("product", product, options);
620
+ /**
621
+ * Creates (no `id`) or updates (with `id`) a product.
622
+ *
623
+ * Shopwave instances (prices) can't be updated, only replaced. Build
624
+ * `instances` with {@link toProductInstancesForSave} (passing the product as last
625
+ * read) so an edited price is sent as a new instance. As a safeguard, a new
626
+ * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from
627
+ * another product would otherwise point the save at that product's instances.
628
+ */
629
+ const saveProduct = (product, options) => {
630
+ return saveEntity("product", (product.id == null || String(product.id).trim() === "") && product.instances != null ? {
631
+ ...product,
632
+ instances: toProductInstancesForSave(product.instances)
633
+ } : product, options);
634
+ };
544
635
  const deleteProduct = (id, options) => deleteEntityById("product", id, options);
545
636
  const fetchProduct = (id, params, options) => fetchEntityById("product", id, params, options);
546
637
  const saveCategory = (category, options) => saveEntity("category", category, options);
@@ -1245,6 +1336,7 @@ exports.fetchSession = fetchSession;
1245
1336
  exports.fetchStore = fetchStore;
1246
1337
  exports.fetchStores = fetchStores;
1247
1338
  exports.fetchUser = fetchUser;
1339
+ exports.findRepricedInstances = findRepricedInstances;
1248
1340
  exports.fitImageToSize = fitImageToSize;
1249
1341
  exports.formatDateForQuery = formatDateForQuery;
1250
1342
  exports.getApiErrorMap = getApiErrorMap;
@@ -1253,6 +1345,7 @@ exports.getImageUrl = getImageUrl;
1253
1345
  exports.getMerchantImage = getMerchantImage;
1254
1346
  exports.getMerchantImageUrl = getMerchantImageUrl;
1255
1347
  exports.imageFileName = imageFileName;
1348
+ exports.isSameInstancePrice = isSameInstancePrice;
1256
1349
  exports.isUploadKind = isUploadKind;
1257
1350
  exports.loginPath = loginPath;
1258
1351
  exports.logout = logout;
@@ -1272,6 +1365,7 @@ exports.savePromotion = savePromotion;
1272
1365
  exports.saveStore = saveStore;
1273
1366
  exports.setMerchantImage = setMerchantImage;
1274
1367
  exports.submitEntity = submitEntity;
1368
+ exports.toProductInstancesForSave = toProductInstancesForSave;
1275
1369
  exports.toShopwaveApiError = toShopwaveApiError;
1276
1370
  exports.toStoredImageIds = toStoredImageIds;
1277
1371
  exports.updateMerchant = updateMerchant;