@growupsell/commerce 1.0.0-alpha.1 → 1.0.0-alpha.3

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) => {
@@ -137,8 +197,6 @@ function toBusiness(wire) {
137
197
  name: wire.name ?? null,
138
198
  description: wire.description ?? null,
139
199
  legalName: wire.legal_name ?? null,
140
- gstNumber: wire.gst_number ?? null,
141
- panNumber: wire.pan_number ?? null,
142
200
  isTemplate: wire.is_template ?? false,
143
201
  category: wire.category ? { id: wire.category.id, name: wire.category.name, type: wire.category.type } : null,
144
202
  legalForm: wire.legal_forms ? { id: wire.legal_forms.id, name: wire.legal_forms.name } : null,
@@ -334,7 +392,9 @@ function toCartLine(wire) {
334
392
  isDigital: wire.is_digital_product,
335
393
  fulfillmentMode: text(wire.fulfillment_mode) ?? "none",
336
394
  customData: wire.custom_data ?? null,
337
- variantIds: sku?.selected_variants ?? []
395
+ // The cart serialises the SKU at depth 2, so `selected_variants` are variant rows, not ids
396
+ // (local backend, 2026-11-01): take each row's id.
397
+ variantIds: (sku?.selected_variants ?? []).map((variant) => variant.id)
338
398
  };
339
399
  }
340
400
  function toAppliedPromoCode(wire) {
@@ -569,6 +629,49 @@ function required(value, name) {
569
629
  return trimmed;
570
630
  }
571
631
 
632
+ // src/http/push.ts
633
+ var KEY = { anonymous: true };
634
+ var SUBSCRIBE = { business: "required" };
635
+ var UNSUBSCRIBE = { anonymous: true, business: "required" };
636
+ function createPushApi(request) {
637
+ return {
638
+ async vapidPublicKey() {
639
+ const wire = await request(
640
+ "GET",
641
+ "/subscribers/push/vapid-public-key",
642
+ KEY
643
+ );
644
+ return text(wire.public_key);
645
+ },
646
+ async subscribe(subscription) {
647
+ const json = { subscription: toWire(subscription, "subscribe") };
648
+ await request("POST", "/subscribers/create/push", {
649
+ ...SUBSCRIBE,
650
+ json
651
+ });
652
+ },
653
+ async unsubscribe(subscription) {
654
+ const json = { subscription: toWire(subscription, "unsubscribe") };
655
+ await request("POST", "/subscribers/push/unsubscribe", {
656
+ ...UNSUBSCRIBE,
657
+ json
658
+ });
659
+ }
660
+ };
661
+ }
662
+ function toWire(subscription, method) {
663
+ const { endpoint, keys } = subscription;
664
+ const missing = [
665
+ endpoint ? null : "endpoint",
666
+ keys?.p256dh ? null : "keys.p256dh",
667
+ keys?.auth ? null : "keys.auth"
668
+ ].filter((name) => name !== null);
669
+ if (missing.length > 0) {
670
+ throw new TypeError(`push.${method}: the subscription needs ${missing.join(", ")}.`);
671
+ }
672
+ return { endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } };
673
+ }
674
+
572
675
  // src/http/live-support.ts
573
676
  var PUBLIC7 = { anonymous: true, business: "required" };
574
677
  function createLiveSupportApi(request) {
@@ -808,6 +911,12 @@ var SORT = {
808
911
  popularity: "popularity",
809
912
  category: "category"
810
913
  };
914
+ var LIST_ORDERING = {
915
+ newest: "-id",
916
+ oldest: "id",
917
+ "name-a-z": "name",
918
+ "name-z-a": "-name"
919
+ };
811
920
  var PUBLIC10 = { business: "required", anonymous: true };
812
921
  function createProductsApi(request) {
813
922
  return {
@@ -849,6 +958,7 @@ function createProductsApi(request) {
849
958
  status: "available",
850
959
  ids: options.ids,
851
960
  category_ids: options.categoryIds,
961
+ ordering: options.sort ? LIST_ORDERING[options.sort] : void 0,
852
962
  limit,
853
963
  offset
854
964
  }
@@ -932,6 +1042,8 @@ function toImage(wire) {
932
1042
  url: wire.photo,
933
1043
  originalUrl: text(wire.original_photo),
934
1044
  variantId: wire.variant ?? null,
1045
+ skuId: wire.sku ?? null,
1046
+ videoUrl: text(wire.link)?.trim() ?? null,
935
1047
  order: wire.display_order
936
1048
  };
937
1049
  }
@@ -953,7 +1065,8 @@ function toProductListItem(wire) {
953
1065
  unit: text(wire.unit),
954
1066
  variantGroups: (wire.variant ?? []).map(toVariantGroup),
955
1067
  selectedVariantIds: wire.selected_variants,
956
- fulfillmentMode: wire.fulfillment_mode,
1068
+ description: text(wire.description),
1069
+ fulfillmentMode: text(wire.fulfillment_mode) ?? "none",
957
1070
  hasDigitalDelivery: wire.has_digital_delivery,
958
1071
  advanceAmount: amount(wire.advance_amount)
959
1072
  };
@@ -1006,6 +1119,8 @@ function toProductOverview(wire) {
1006
1119
  id: wire.id,
1007
1120
  name: wire.name,
1008
1121
  slug: wire.url_slug,
1122
+ description: text(wire.description),
1123
+ fulfillmentMode: wire.fulfillment_mode ?? "none",
1009
1124
  categoryName: text(wire.category_name),
1010
1125
  images: liveImages(wire.images).map(toImage),
1011
1126
  variantGroups: (wire.variant ?? []).map(toVariantGroup),
@@ -1015,56 +1130,6 @@ function toProductOverview(wire) {
1015
1130
  };
1016
1131
  }
1017
1132
 
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
1133
  // src/http/promotions.ts
1069
1134
  var DEFAULT_LIMIT2 = 20;
1070
1135
  var PUBLIC_BUSINESS = { anonymous: true, business: "required" };
@@ -1573,7 +1638,15 @@ function createSessionApi(request) {
1573
1638
  };
1574
1639
  }
1575
1640
  function toGuestSession(wire) {
1576
- return { id: wire.id, token: wire.session_token };
1641
+ const raw = wire && typeof wire === "object" ? wire : null;
1642
+ if (typeof raw?.id !== "number" || typeof raw.session_token !== "string" || !raw.session_token) {
1643
+ throw new GrowupsellApiError({
1644
+ kind: "parse",
1645
+ message: "The new guest session has no token.",
1646
+ body: wire
1647
+ });
1648
+ }
1649
+ return { id: raw.id, token: raw.session_token };
1577
1650
  }
1578
1651
 
1579
1652
  // src/http/lookup.ts
@@ -2510,6 +2583,14 @@ function createExchangesApi(request) {
2510
2583
  CUSTOMER8
2511
2584
  )
2512
2585
  );
2586
+ },
2587
+ async targetSkus(skuId) {
2588
+ const wire = await request(
2589
+ "GET",
2590
+ `/product/sku/avail/for/exchange/${skuId}`,
2591
+ CUSTOMER8
2592
+ );
2593
+ return wire.map(toExchangeTargetSku);
2513
2594
  }
2514
2595
  };
2515
2596
  }
@@ -2550,6 +2631,16 @@ function toExchangeResult(wire) {
2550
2631
  }
2551
2632
  return { status: "submitted", id: wire.id, message: text(wire.message) };
2552
2633
  }
2634
+ function toExchangeTargetSku(wire) {
2635
+ return {
2636
+ skuId: wire.id,
2637
+ skuCode: wire.sku,
2638
+ name: wire.product_name,
2639
+ photo: text(wire.product_photo),
2640
+ price: amount(wire.price) ?? 0,
2641
+ stockQty: wire.stock_qty
2642
+ };
2643
+ }
2553
2644
  function toExchangeListItem(wire) {
2554
2645
  return {
2555
2646
  id: wire.id,
@@ -2642,7 +2733,11 @@ function createClient(options) {
2642
2733
  init.body = JSON.stringify(opts.json);
2643
2734
  }
2644
2735
  const url = `${baseURL}/${path3.replace(/^\/+/, "")}${buildQuery(opts.query)}`;
2645
- const { status: status2, ok, text: text2, data } = await send(url, init, opts.timeoutMs ?? timeoutMs, binary);
2736
+ const res = await send(url, init, opts.timeoutMs ?? timeoutMs, binary);
2737
+ const { status: status2, ok, text: text2, data } = res;
2738
+ if (headers["x-session-token"] && sessionMarkedInvalid(res.headers)) {
2739
+ report("session", headers["x-session-token"]);
2740
+ }
2646
2741
  if (data) return data;
2647
2742
  const body = parseBody(text2, ok, status2);
2648
2743
  if (ok) return body;
@@ -2653,7 +2748,8 @@ function createClient(options) {
2653
2748
  status: status2,
2654
2749
  message: messageFromBody(body) ?? `Request failed with status ${status2}`,
2655
2750
  fieldErrors: fieldErrorsFromBody(body),
2656
- body
2751
+ body,
2752
+ retryAfterSeconds: status2 === 429 ? parseRetryAfter(res.headers?.get("retry-after")) : null
2657
2753
  });
2658
2754
  }
2659
2755
  async function send(url, init, ms, binary) {
@@ -2673,6 +2769,7 @@ function createClient(options) {
2673
2769
  doFetch(url, init).then(async (res) => ({
2674
2770
  status: res.status,
2675
2771
  ok: res.ok,
2772
+ headers: res.headers,
2676
2773
  // Error responses are read as text even for binary requests: they carry the message.
2677
2774
  text: binary && res.ok ? "" : await res.text(),
2678
2775
  data: binary && res.ok ? await readBinary(res) : void 0
@@ -2700,7 +2797,7 @@ function createClient(options) {
2700
2797
  function report(kind, token) {
2701
2798
  if (reported[kind] === token) return;
2702
2799
  reported[kind] = token;
2703
- options.onUnauthorized?.({ kind });
2800
+ options.onUnauthorized?.({ kind, token });
2704
2801
  }
2705
2802
  return {
2706
2803
  request,
@@ -2725,6 +2822,7 @@ function createClient(options) {
2725
2822
  forms: createFormsApi(request),
2726
2823
  helpdesk: createHelpdeskApi(request),
2727
2824
  newsletter: createNewsletterApi(request),
2825
+ push: createPushApi(request),
2728
2826
  liveSupport: createLiveSupportApi(request),
2729
2827
  customManufacturing: createCustomManufacturingApi(request),
2730
2828
  subscriptions: createSubscriptionsApi(request),
@@ -2754,6 +2852,9 @@ function buildHeaders(credentials, opts) {
2754
2852
  function isDeadSession(status2, body, headers) {
2755
2853
  return status2 === 404 && typeof body === "string" && !!headers["x-session-token"];
2756
2854
  }
2855
+ function sessionMarkedInvalid(headers) {
2856
+ return headers?.get("x-session-status")?.trim().toLowerCase() === "invalid";
2857
+ }
2757
2858
  function parseBody(text2, ok, status2) {
2758
2859
  if (text2 === "") return void 0;
2759
2860
  try {
@@ -2786,4 +2887,4 @@ export {
2786
2887
  GrowupsellApiError,
2787
2888
  createClient
2788
2889
  };
2789
- //# sourceMappingURL=chunk-YZJTPFCH.js.map
2890
+ //# sourceMappingURL=chunk-BORVKACK.js.map