@growupsell/commerce 1.0.0-alpha.1 → 1.0.0-alpha.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.
@@ -1,3 +1,63 @@
1
+ // src/http/errors.ts
2
+ var GrowupsellApiError = class extends Error {
3
+ kind;
4
+ /** HTTP status; undefined when no response arrived (network, timeout). */
5
+ status;
6
+ /** Validation messages per field, as returned by the backend (`non_field_errors` included). */
7
+ fieldErrors;
8
+ /** The parsed response body, for anything the fields above don't cover. */
9
+ body;
10
+ /**
11
+ * For a 429 (throttled OTP checks and sends, backend S14): the seconds from `Retry-After`.
12
+ * Null otherwise, and when the header is missing or not a whole number of seconds.
13
+ */
14
+ retryAfterSeconds;
15
+ constructor(init) {
16
+ super(init.message);
17
+ this.name = "GrowupsellApiError";
18
+ this.kind = init.kind;
19
+ this.status = init.status;
20
+ this.fieldErrors = init.fieldErrors ?? {};
21
+ this.body = init.body;
22
+ this.retryAfterSeconds = init.retryAfterSeconds ?? null;
23
+ }
24
+ };
25
+ function parseRetryAfter(value) {
26
+ const text2 = value?.trim();
27
+ return text2 && /^\d+$/.test(text2) ? Number(text2) : null;
28
+ }
29
+ function messageFromBody(body) {
30
+ if (typeof body === "string") {
31
+ const text2 = body.trim();
32
+ return text2 && !text2.startsWith("<") ? text2 : void 0;
33
+ }
34
+ if (Array.isArray(body)) return body.length > 0 ? messageFromBody(body[0]) : void 0;
35
+ if (body && typeof body === "object") {
36
+ const record = body;
37
+ for (const key of ["detail", "message"]) {
38
+ if (typeof record[key] === "string" && record[key]) return record[key];
39
+ }
40
+ for (const value of Object.values(record)) {
41
+ if (typeof value === "boolean" || typeof value === "number" || value == null) continue;
42
+ const found = messageFromBody(value);
43
+ if (found) return found;
44
+ }
45
+ }
46
+ return void 0;
47
+ }
48
+ function fieldErrorsFromBody(body) {
49
+ if (!body || typeof body !== "object" || Array.isArray(body)) return {};
50
+ const errors = {};
51
+ for (const [field, value] of Object.entries(body)) {
52
+ if (field === "detail" || field === "message") continue;
53
+ if (typeof value === "string") errors[field] = [value];
54
+ else if (Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "string")) {
55
+ errors[field] = value;
56
+ }
57
+ }
58
+ return errors;
59
+ }
60
+
1
61
  // src/http/convert.ts
2
62
  var text = (value) => value == null || value.trim() === "" ? null : value;
3
63
  var amount = (value) => {
@@ -334,7 +394,9 @@ function toCartLine(wire) {
334
394
  isDigital: wire.is_digital_product,
335
395
  fulfillmentMode: text(wire.fulfillment_mode) ?? "none",
336
396
  customData: wire.custom_data ?? null,
337
- variantIds: sku?.selected_variants ?? []
397
+ // The cart serialises the SKU at depth 2, so `selected_variants` are variant rows, not ids
398
+ // (local backend, 2026-11-01): take each row's id.
399
+ variantIds: (sku?.selected_variants ?? []).map((variant) => variant.id)
338
400
  };
339
401
  }
340
402
  function toAppliedPromoCode(wire) {
@@ -569,6 +631,49 @@ function required(value, name) {
569
631
  return trimmed;
570
632
  }
571
633
 
634
+ // src/http/push.ts
635
+ var KEY = { anonymous: true };
636
+ var SUBSCRIBE = { business: "required" };
637
+ var UNSUBSCRIBE = { anonymous: true, business: "required" };
638
+ function createPushApi(request) {
639
+ return {
640
+ async vapidPublicKey() {
641
+ const wire = await request(
642
+ "GET",
643
+ "/subscribers/push/vapid-public-key",
644
+ KEY
645
+ );
646
+ return text(wire.public_key);
647
+ },
648
+ async subscribe(subscription) {
649
+ const json = { subscription: toWire(subscription, "subscribe") };
650
+ await request("POST", "/subscribers/create/push", {
651
+ ...SUBSCRIBE,
652
+ json
653
+ });
654
+ },
655
+ async unsubscribe(subscription) {
656
+ const json = { subscription: toWire(subscription, "unsubscribe") };
657
+ await request("POST", "/subscribers/push/unsubscribe", {
658
+ ...UNSUBSCRIBE,
659
+ json
660
+ });
661
+ }
662
+ };
663
+ }
664
+ function toWire(subscription, method) {
665
+ const { endpoint, keys } = subscription;
666
+ const missing = [
667
+ endpoint ? null : "endpoint",
668
+ keys?.p256dh ? null : "keys.p256dh",
669
+ keys?.auth ? null : "keys.auth"
670
+ ].filter((name) => name !== null);
671
+ if (missing.length > 0) {
672
+ throw new TypeError(`push.${method}: the subscription needs ${missing.join(", ")}.`);
673
+ }
674
+ return { endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } };
675
+ }
676
+
572
677
  // src/http/live-support.ts
573
678
  var PUBLIC7 = { anonymous: true, business: "required" };
574
679
  function createLiveSupportApi(request) {
@@ -932,6 +1037,8 @@ function toImage(wire) {
932
1037
  url: wire.photo,
933
1038
  originalUrl: text(wire.original_photo),
934
1039
  variantId: wire.variant ?? null,
1040
+ skuId: wire.sku ?? null,
1041
+ videoUrl: text(wire.link)?.trim() ?? null,
935
1042
  order: wire.display_order
936
1043
  };
937
1044
  }
@@ -1015,56 +1122,6 @@ function toProductOverview(wire) {
1015
1122
  };
1016
1123
  }
1017
1124
 
1018
- // src/http/errors.ts
1019
- var GrowupsellApiError = class extends Error {
1020
- kind;
1021
- /** HTTP status; undefined when no response arrived (network, timeout). */
1022
- status;
1023
- /** Validation messages per field, as returned by the backend (`non_field_errors` included). */
1024
- fieldErrors;
1025
- /** The parsed response body, for anything the fields above don't cover. */
1026
- body;
1027
- constructor(init) {
1028
- super(init.message);
1029
- this.name = "GrowupsellApiError";
1030
- this.kind = init.kind;
1031
- this.status = init.status;
1032
- this.fieldErrors = init.fieldErrors ?? {};
1033
- this.body = init.body;
1034
- }
1035
- };
1036
- function messageFromBody(body) {
1037
- if (typeof body === "string") {
1038
- const text2 = body.trim();
1039
- return text2 && !text2.startsWith("<") ? text2 : void 0;
1040
- }
1041
- if (Array.isArray(body)) return body.length > 0 ? messageFromBody(body[0]) : void 0;
1042
- if (body && typeof body === "object") {
1043
- const record = body;
1044
- for (const key of ["detail", "message"]) {
1045
- if (typeof record[key] === "string" && record[key]) return record[key];
1046
- }
1047
- for (const value of Object.values(record)) {
1048
- if (typeof value === "boolean" || typeof value === "number" || value == null) continue;
1049
- const found = messageFromBody(value);
1050
- if (found) return found;
1051
- }
1052
- }
1053
- return void 0;
1054
- }
1055
- function fieldErrorsFromBody(body) {
1056
- if (!body || typeof body !== "object" || Array.isArray(body)) return {};
1057
- const errors = {};
1058
- for (const [field, value] of Object.entries(body)) {
1059
- if (field === "detail" || field === "message") continue;
1060
- if (typeof value === "string") errors[field] = [value];
1061
- else if (Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "string")) {
1062
- errors[field] = value;
1063
- }
1064
- }
1065
- return errors;
1066
- }
1067
-
1068
1125
  // src/http/promotions.ts
1069
1126
  var DEFAULT_LIMIT2 = 20;
1070
1127
  var PUBLIC_BUSINESS = { anonymous: true, business: "required" };
@@ -1573,7 +1630,15 @@ function createSessionApi(request) {
1573
1630
  };
1574
1631
  }
1575
1632
  function toGuestSession(wire) {
1576
- return { id: wire.id, token: wire.session_token };
1633
+ const raw = wire && typeof wire === "object" ? wire : null;
1634
+ if (typeof raw?.id !== "number" || typeof raw.session_token !== "string" || !raw.session_token) {
1635
+ throw new GrowupsellApiError({
1636
+ kind: "parse",
1637
+ message: "The new guest session has no token.",
1638
+ body: wire
1639
+ });
1640
+ }
1641
+ return { id: raw.id, token: raw.session_token };
1577
1642
  }
1578
1643
 
1579
1644
  // src/http/lookup.ts
@@ -2510,6 +2575,14 @@ function createExchangesApi(request) {
2510
2575
  CUSTOMER8
2511
2576
  )
2512
2577
  );
2578
+ },
2579
+ async targetSkus(skuId) {
2580
+ const wire = await request(
2581
+ "GET",
2582
+ `/product/sku/avail/for/exchange/${skuId}`,
2583
+ CUSTOMER8
2584
+ );
2585
+ return wire.map(toExchangeTargetSku);
2513
2586
  }
2514
2587
  };
2515
2588
  }
@@ -2550,6 +2623,16 @@ function toExchangeResult(wire) {
2550
2623
  }
2551
2624
  return { status: "submitted", id: wire.id, message: text(wire.message) };
2552
2625
  }
2626
+ function toExchangeTargetSku(wire) {
2627
+ return {
2628
+ skuId: wire.id,
2629
+ skuCode: wire.sku,
2630
+ name: wire.product_name,
2631
+ photo: text(wire.product_photo),
2632
+ price: amount(wire.price) ?? 0,
2633
+ stockQty: wire.stock_qty
2634
+ };
2635
+ }
2553
2636
  function toExchangeListItem(wire) {
2554
2637
  return {
2555
2638
  id: wire.id,
@@ -2642,7 +2725,11 @@ function createClient(options) {
2642
2725
  init.body = JSON.stringify(opts.json);
2643
2726
  }
2644
2727
  const url = `${baseURL}/${path3.replace(/^\/+/, "")}${buildQuery(opts.query)}`;
2645
- const { status: status2, ok, text: text2, data } = await send(url, init, opts.timeoutMs ?? timeoutMs, binary);
2728
+ const res = await send(url, init, opts.timeoutMs ?? timeoutMs, binary);
2729
+ const { status: status2, ok, text: text2, data } = res;
2730
+ if (headers["x-session-token"] && sessionMarkedInvalid(res.headers)) {
2731
+ report("session", headers["x-session-token"]);
2732
+ }
2646
2733
  if (data) return data;
2647
2734
  const body = parseBody(text2, ok, status2);
2648
2735
  if (ok) return body;
@@ -2653,7 +2740,8 @@ function createClient(options) {
2653
2740
  status: status2,
2654
2741
  message: messageFromBody(body) ?? `Request failed with status ${status2}`,
2655
2742
  fieldErrors: fieldErrorsFromBody(body),
2656
- body
2743
+ body,
2744
+ retryAfterSeconds: status2 === 429 ? parseRetryAfter(res.headers?.get("retry-after")) : null
2657
2745
  });
2658
2746
  }
2659
2747
  async function send(url, init, ms, binary) {
@@ -2673,6 +2761,7 @@ function createClient(options) {
2673
2761
  doFetch(url, init).then(async (res) => ({
2674
2762
  status: res.status,
2675
2763
  ok: res.ok,
2764
+ headers: res.headers,
2676
2765
  // Error responses are read as text even for binary requests: they carry the message.
2677
2766
  text: binary && res.ok ? "" : await res.text(),
2678
2767
  data: binary && res.ok ? await readBinary(res) : void 0
@@ -2700,7 +2789,7 @@ function createClient(options) {
2700
2789
  function report(kind, token) {
2701
2790
  if (reported[kind] === token) return;
2702
2791
  reported[kind] = token;
2703
- options.onUnauthorized?.({ kind });
2792
+ options.onUnauthorized?.({ kind, token });
2704
2793
  }
2705
2794
  return {
2706
2795
  request,
@@ -2725,6 +2814,7 @@ function createClient(options) {
2725
2814
  forms: createFormsApi(request),
2726
2815
  helpdesk: createHelpdeskApi(request),
2727
2816
  newsletter: createNewsletterApi(request),
2817
+ push: createPushApi(request),
2728
2818
  liveSupport: createLiveSupportApi(request),
2729
2819
  customManufacturing: createCustomManufacturingApi(request),
2730
2820
  subscriptions: createSubscriptionsApi(request),
@@ -2754,6 +2844,9 @@ function buildHeaders(credentials, opts) {
2754
2844
  function isDeadSession(status2, body, headers) {
2755
2845
  return status2 === 404 && typeof body === "string" && !!headers["x-session-token"];
2756
2846
  }
2847
+ function sessionMarkedInvalid(headers) {
2848
+ return headers?.get("x-session-status")?.trim().toLowerCase() === "invalid";
2849
+ }
2757
2850
  function parseBody(text2, ok, status2) {
2758
2851
  if (text2 === "") return void 0;
2759
2852
  try {
@@ -2786,4 +2879,4 @@ export {
2786
2879
  GrowupsellApiError,
2787
2880
  createClient
2788
2881
  };
2789
- //# sourceMappingURL=chunk-YZJTPFCH.js.map
2882
+ //# sourceMappingURL=chunk-AI2CWQ22.js.map