react-shopwave-connect 0.4.1 → 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.
@@ -281,7 +281,10 @@ function safeEqual(a, b) {
281
281
  * (https://developer.merchantstack.com/api-reference.html) and checked against
282
282
  * the live API where noted:
283
283
  * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
284
- * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
284
+ * - promotion: no DELETE; promotions are never deleted, only expired. Expiring
285
+ * reads the promotion and saves the whole record back with `endDate` = now (and
286
+ * `startDate` = now too if it hadn't started); the live API rejects a partial
287
+ * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.
285
288
  * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
286
289
  * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
287
290
  * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
@@ -322,7 +325,9 @@ const SHOPWAVE_ENTITIES = Object.freeze({
322
325
  idHeader: "promotionId",
323
326
  writable: true,
324
327
  deleteMode: "retire",
325
- retireField: "endDate"
328
+ retireField: "endDate",
329
+ retireWithRecord: true,
330
+ retireStartField: "startDate"
326
331
  },
327
332
  employee: {
328
333
  route: "employees",
@@ -521,6 +526,18 @@ function jsonError(status, error, message) {
521
526
  function shopwaveDateTime(date = /* @__PURE__ */ new Date()) {
522
527
  return date.toISOString().slice(0, 19).replace("T", " ");
523
528
  }
529
+ /**
530
+ * Reads a Shopwave datetime: ISO with a zone (what reads return, e.g.
531
+ * `2026-10-20T16:00:00.000Z`) or a zoneless `YYYY-MM-DD HH:mm:ss`, which is UTC.
532
+ * `null` when the value isn't a date.
533
+ */
534
+ function parseShopwaveDateTime(value) {
535
+ if (typeof value !== "string" || !value.trim()) return null;
536
+ const text = value.trim();
537
+ const zoneless = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(text);
538
+ const date = new Date(zoneless ? `${text.replace(" ", "T")}Z` : text);
539
+ return Number.isNaN(date.getTime()) ? null : date;
540
+ }
524
541
  const NULL_BODY_STATUS = new Set([
525
542
  101,
526
543
  103,
@@ -720,6 +737,45 @@ function createShopwaveApiHandlers(config) {
720
737
  function badId() {
721
738
  return jsonError(400, "invalid_id", "Missing or invalid id");
722
739
  }
740
+ /**
741
+ * Retires a record the API won't take a partial save for (promotions): reads it,
742
+ * sets `field` to now (and `def.retireStartField` to now too when the record
743
+ * hasn't started yet), and POSTs the whole record back. Both dates are sent as
744
+ * UTC `YYYY-MM-DD HH:mm:ss`; reads return them as ISO with `Z`.
745
+ */
746
+ async function retireWithRecord(request, def, id, field) {
747
+ const read = await forward(request, {
748
+ path: def.upstream,
749
+ headers: { [def.idsHeader]: id },
750
+ forwardExtras: false
751
+ });
752
+ if (!read.ok) return read;
753
+ let body;
754
+ try {
755
+ body = await read.json();
756
+ } catch {
757
+ return jsonError(502, "invalid_upstream", `The Shopwave API's ${def.upstream} read wasn't JSON`);
758
+ }
759
+ const map = isPlainObject(body) && isPlainObject(body[def.collection]) ? body[def.collection] : {};
760
+ const record = Object.values(map).find((entry) => isPlainObject(entry) && String(entry.id) === id);
761
+ if (!record) return jsonError(404, "not_found", `No ${def.upstream} with id ${id}`);
762
+ const now = /* @__PURE__ */ new Date();
763
+ const retired = {
764
+ ...record,
765
+ [field]: shopwaveDateTime(now)
766
+ };
767
+ const startField = def.retireStartField;
768
+ if (startField) {
769
+ const start = parseShopwaveDateTime(record[startField]);
770
+ if (start) retired[startField] = shopwaveDateTime(start > now ? now : start);
771
+ }
772
+ return forward(request, {
773
+ method: "POST",
774
+ path: def.upstream,
775
+ postBody: { [def.collection]: { 0: retired } },
776
+ forwardExtras: false
777
+ });
778
+ }
723
779
  return {
724
780
  forward,
725
781
  entity,
@@ -755,6 +811,7 @@ function createShopwaveApiHandlers(config) {
755
811
  if (def.deleteMode === "none") return notSupported(def, "deletes");
756
812
  if (def.deleteMode === "retire") {
757
813
  const field = def.retireField ?? "exitDate";
814
+ if (def.retireWithRecord) return retireWithRecord(request, def, id, field);
758
815
  return forward(request, {
759
816
  method: "POST",
760
817
  path: def.upstream,