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.
@@ -255,7 +255,10 @@ function safeEqual(a, b) {
255
255
  * (https://developer.merchantstack.com/api-reference.html) and checked against
256
256
  * the live API where noted:
257
257
  * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
258
- * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
258
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
259
+ * reads the promotion and saves the whole record back with `endDate` = now (and
260
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
261
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
259
262
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
260
263
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
261
264
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
@@ -296,7 +299,9 @@ const SHOPWAVE_ENTITIES = Object.freeze({
296
299
  idHeader: "promotionId",
297
300
  writable: true,
298
301
  deleteMode: "retire",
299
- retireField: "endDate"
302
+ retireField: "endDate",
303
+ retireWithRecord: true,
304
+ retireStartField: "startDate"
300
305
  },
301
306
  employee: {
302
307
  route: "employees",
@@ -495,6 +500,18 @@ function jsonError(status, error, message) {
495
500
  function shopwaveDateTime(date = /* @__PURE__ */ new Date()) {
496
501
  return date.toISOString().slice(0, 19).replace("T", " ");
497
502
  }
503
+ /**
504
+ * Reads a Shopwave datetime: ISO with a zone (what reads return, e.g.
505
+ * `2026-10-20T16:00:00.000Z`) or a zoneless `YYYY-MM-DD HH:mm:ss`, which is UTC.
506
+ * `null` when the value isn't a date.
507
+ */
508
+ function parseShopwaveDateTime(value) {
509
+ if (typeof value !== "string" || !value.trim()) return null;
510
+ const text = value.trim();
511
+ const zoneless = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(text);
512
+ const date = new Date(zoneless ? `${text.replace(" ", "T")}Z` : text);
513
+ return Number.isNaN(date.getTime()) ? null : date;
514
+ }
498
515
  const NULL_BODY_STATUS = new Set([
499
516
  101,
500
517
  103,
@@ -694,6 +711,45 @@ function createShopwaveApiHandlers(config) {
694
711
  function badId() {
695
712
  return jsonError(400, "invalid_id", "Missing or invalid id");
696
713
  }
714
+ /**
715
+ * Retires a record the API won't take a partial save for (promotions): reads it,
716
+ * sets `field` to now (and `def.retireStartField` to now too when the record
717
+ * hasn't started yet), and POSTs the whole record back. Both dates are sent as
718
+ * UTC `YYYY-MM-DD HH:mm:ss`; reads return them as ISO with `Z`.
719
+ */
720
+ async function retireWithRecord(request, def, id, field) {
721
+ const read = await forward(request, {
722
+ path: def.upstream,
723
+ headers: { [def.idsHeader]: id },
724
+ forwardExtras: false
725
+ });
726
+ if (!read.ok) return read;
727
+ let body;
728
+ try {
729
+ body = await read.json();
730
+ } catch {
731
+ return jsonError(502, "invalid_upstream", `The Shopwave API's ${def.upstream} read wasn't JSON`);
732
+ }
733
+ const map = isPlainObject(body) && isPlainObject(body[def.collection]) ? body[def.collection] : {};
734
+ const record = Object.values(map).find((entry) => isPlainObject(entry) && String(entry.id) === id);
735
+ if (!record) return jsonError(404, "not_found", `No ${def.upstream} with id ${id}`);
736
+ const now = /* @__PURE__ */ new Date();
737
+ const retired = {
738
+ ...record,
739
+ [field]: shopwaveDateTime(now)
740
+ };
741
+ const startField = def.retireStartField;
742
+ if (startField) {
743
+ const start = parseShopwaveDateTime(record[startField]);
744
+ if (start) retired[startField] = shopwaveDateTime(start > now ? now : start);
745
+ }
746
+ return forward(request, {
747
+ method: "POST",
748
+ path: def.upstream,
749
+ postBody: { [def.collection]: { 0: retired } },
750
+ forwardExtras: false
751
+ });
752
+ }
697
753
  return {
698
754
  forward,
699
755
  entity,
@@ -729,6 +785,7 @@ function createShopwaveApiHandlers(config) {
729
785
  if (def.deleteMode === "none") return notSupported(def, "deletes");
730
786
  if (def.deleteMode === "retire") {
731
787
  const field = def.retireField ?? "exitDate";
788
+ if (def.retireWithRecord) return retireWithRecord(request, def, id, field);
732
789
  return forward(request, {
733
790
  method: "POST",
734
791
  path: def.upstream,