@asstio-dev/storefront 0.12.0 → 0.14.0

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
@@ -380,6 +380,27 @@ Problem codes you will meet here: `CART_EMPTY`, `CHECKOUT_CART_CHANGED` (with `c
380
380
  - When our validation declines a purchase (the cart moved on, the attempt expired, a line is out of stock), Qliro shows
381
381
  its out-of-stock message and fires `q1.onPaymentDeclined`; the SDK then syncs by itself and remounts or resumes.
382
382
 
383
+ #### Kustom (`klarna-kco`)
384
+
385
+ - The widget is Kustom's HTML snippet (Kustom Checkout v3); `mountCheckout` re-creates its scripts so they run.
386
+ - After every cart change, `handle.sync(update)` suspends the checkout with `suspend({ autoResume: { enabled: false } })`,
387
+ so Kustom never reopens payment by itself during a slow sync or while the SDK retries a `locked` answer. It resumes only
388
+ on the confirmed `open` answer for the same attempt; Kustom re-reads its order on `resume()`. A change made before the
389
+ snippet's script has defined `window._klarnaCheckout` is handed over as soon as it has.
390
+ - When our validation declines a purchase, Kustom sends the **whole page** to your checkout page with `declined=validation`
391
+ added to its query (the server answers Kustom with a 303 to the attempt's checkout page). That page is a fresh load:
392
+ show a short message for `declined=validation`, sync the real cart, and handle `completed`/`pending` before mounting a
393
+ widget, as on any page load. There is no `onDeclined` for Kustom.
394
+ - `can_not_complete_order` (no offered payment method can complete the order) reaches `onProblem` with the code
395
+ `SDK_PROVIDER_CANNOT_COMPLETE`; the copy is yours.
396
+ - The re-entry refresh (a stored snippet older than ten minutes is re-fetched) and the acknowledgement to Kustom are the
397
+ server's; the page does nothing for them.
398
+ - The checkout page needs a `checkoutPath` and a `termsPath` in the market's checkout block (Purchase refuses a Kustom
399
+ option without them).
400
+ - **CSP hosts: pending K-C10** (`docs/ops/kustom-playground-checks.md`). Until the playground run records the hosts the
401
+ snippet really loads (card with 3-D Secure and the default method), allow `*.kustom.co` and `*.klarna.com` for
402
+ `script-src`, `frame-src` and `connect-src` as the general paragraph above says, and watch the console for blocked requests.
403
+
383
404
  ## Headers
384
405
 
385
406
  Every call the client makes carries these; you never set them by hand.
@@ -35,6 +35,7 @@ export function mountCheckout(container, checkout, options) {
35
35
  let active = 0;
36
36
  let queue = Promise.resolve(); // *(plan review 1 #43)*
37
37
  let unsubscribeDeclined;
38
+ let unsubscribeCannotComplete;
38
39
  let expiryTimer;
39
40
  let firedPreExpiryFor; // *(plan review 1 #42, plan review 2 #12)*
40
41
  // Every pending sleep's resolver, so a remount or destroy can settle it instead of clearing its timer and leaving
@@ -107,10 +108,16 @@ export function mountCheckout(container, checkout, options) {
107
108
  }
108
109
  function suspend() {
109
110
  suspended = true;
110
- if (suspendedOn !== controller) {
111
- suspendedOn = controller;
112
- controller.suspend();
113
- }
111
+ if (suspendedOn === controller)
112
+ return;
113
+ // Recorded before the call, as on main: a controller whose suspend throws (a broken provider `lock()`) stays recorded, so a
114
+ // later autonomous sync reaches the server instead of throwing again *(final review B3 T2; review F2-#21)*. Only the
115
+ // explicit "not handed over yet" answer (`false`, spec F §7.1) un-records it, so the next suspend() offers it again —
116
+ // recording it while the provider global was absent made every later suspend() skip the controller (round B).
117
+ const target = controller;
118
+ suspendedOn = target;
119
+ if (target.suspend() === false && suspendedOn === target)
120
+ suspendedOn = undefined;
114
121
  }
115
122
  function resume(target) { if (suspended) {
116
123
  suspended = false;
@@ -182,28 +189,36 @@ export function mountCheckout(container, checkout, options) {
182
189
  // and the new widget's decline signal was never bound *(plan review 2 #13)*.
183
190
  unsubscribeDeclined?.();
184
191
  unsubscribeDeclined = undefined;
192
+ unsubscribeCannotComplete?.();
193
+ unsubscribeCannotComplete = undefined;
185
194
  controller = bound;
186
- // A provider without the signal: nothing to bind and nothing to retry *(plan review 2 #13)*.
187
- if (!bound.onDeclined)
188
- return;
189
195
  const gen = generation;
190
196
  // `render` appends the provider's script; it has not executed when this runs, so the global the controller reads
191
- // does not exist yet. Retry every `bindRetryMs` until the subscription is registered (`onDeclined` returns its
192
- // unsubscriber) or the generation is gone — one interval for as long as the widget is mounted, so a script that
193
- // loads late is still bound. The timers are the handle's, so a remount or destroy cancels them
197
+ // does not exist yet. Each signal the controller offers is retried every `bindRetryMs` until it is registered
198
+ // (the subscribe function returns its unsubscriber) or the generation is gone — one interval for as long as the
199
+ // widget is mounted, so a script that loads late is still bound. A controller without the signal binds nothing and
200
+ // retries nothing. The timers are the handle's, so a remount or destroy cancels them
194
201
  // *(plan review 1 #43, plan review 2 #13, plan review 3 #4, final review S8)*.
195
- const attach = () => {
196
- if (!alive(gen))
197
- return;
198
- const unsubscribe = bound.onDeclined(() => autonomousSync("declined", gen));
199
- if (unsubscribe) {
200
- unsubscribeDeclined = unsubscribe;
202
+ const bind = (subscribe, handler, keep) => {
203
+ if (!subscribe)
201
204
  return;
202
- }
203
- const h = setT(() => { timers.delete(h); attach(); }, bindRetryMs);
204
- timers.add(h);
205
+ const attach = () => {
206
+ if (!alive(gen))
207
+ return;
208
+ const off = subscribe(handler);
209
+ if (off) {
210
+ keep(off);
211
+ return;
212
+ }
213
+ const h = setT(() => { timers.delete(h); attach(); }, bindRetryMs);
214
+ timers.add(h);
215
+ };
216
+ attach();
205
217
  };
206
- attach();
218
+ bind(bound.onDeclined, () => autonomousSync("declined", gen), (off) => { unsubscribeDeclined = off; });
219
+ // Spec F §7.1: Kustom's can_not_complete_order is the app's problem to show, not a validation decline (no sync).
220
+ bind(bound.onCannotComplete, () => { if (alive(gen))
221
+ options.onProblem(localProblem("SDK_PROVIDER_CANNOT_COMPLETE", "can_not_complete_order"), { status: 0 }); }, (off) => { unsubscribeCannotComplete = off; });
207
222
  }
208
223
  function remount(dto) {
209
224
  if (dto.status !== "open" || !dto.widget)
@@ -211,6 +226,7 @@ export function mountCheckout(container, checkout, options) {
211
226
  // Resolved before any state changes: an unknown controller throws while the generation is still alive, so the
212
227
  // dispatch's catch reports it instead of dropping it for a generation that moved on *(final review B3 T2)*.
213
228
  const next = controllerFor(dto.widget.controller, win, controllerTimer);
229
+ controller.dispose?.(); // spec F §7.1: the old mount's deferred callbacks and timers do nothing now
214
230
  generation++;
215
231
  cancelTimers(); // *(plan review 1 #37)*
216
232
  expiryTimer = undefined;
@@ -419,6 +435,9 @@ export function mountCheckout(container, checkout, options) {
419
435
  cancelTimers(); // *(plan review 1 #37)*
420
436
  unsubscribeDeclined?.();
421
437
  unsubscribeDeclined = undefined;
438
+ unsubscribeCannotComplete?.();
439
+ unsubscribeCannotComplete = undefined;
440
+ controller.dispose?.();
422
441
  doc.removeEventListener("visibilitychange", onVisible);
423
442
  },
424
443
  sync(update) {
@@ -1,3 +1,14 @@
1
+ /** Kustom's client API (docs.kustom.co → Client-side events). A no-argument `suspend()` auto-resumes after 10 s; the SDK
2
+ * always passes `{ autoResume: { enabled: false } }` (spec F §7.1, review #5). */
3
+ export interface KlarnaCheckoutApi {
4
+ suspend(options?: {
5
+ autoResume?: {
6
+ enabled: boolean;
7
+ };
8
+ }): void;
9
+ resume(): void;
10
+ on?(events: Record<string, (data?: unknown) => void>): void;
11
+ }
1
12
  /** What the provider's JavaScript offers (spec §4.4 `ClientSuspend`); each is optional on the page until the widget's script has loaded. */
2
13
  export interface ProviderWindow {
3
14
  q1?: {
@@ -10,10 +21,7 @@ export interface ProviderWindow {
10
21
  * (developers.qliro.com → Listeners). */
11
22
  onPaymentDeclined?(cb: (declineReason?: string, declineReasonMessage?: string) => void): void;
12
23
  };
13
- _klarnaCheckout?: (cb: (api: {
14
- suspend(): void;
15
- resume(): void;
16
- }) => void) => void;
24
+ _klarnaCheckout?: (cb: (api: KlarnaCheckoutApi) => void) => void;
17
25
  walley?: {
18
26
  checkout?: {
19
27
  api?: {
@@ -33,7 +41,9 @@ export interface ResumeTarget {
33
41
  onUnsynced(): void;
34
42
  }
35
43
  export interface CheckoutController {
36
- suspend(): void;
44
+ /** Returns `false` when the suspension could not be handed to the provider yet (its script has not defined its API): the
45
+ * handle does not record it, and the controller hands it over once the script is ready (spec F §7.1). */
46
+ suspend(): void | false;
37
47
  /** `target` lets a controller that can see the provider's order wait for it (qliro-q1); the others ignore it. */
38
48
  resume(target?: ResumeTarget): void;
39
49
  /** Subscribe to the provider's payment-declined signal where its API offers one. Returns the unsubscribe once the
@@ -41,7 +51,14 @@ export interface CheckoutController {
41
51
  * handle retries only then. A controller whose provider has no such signal leaves this member out, and the handle
42
52
  * never retries for it *(plan review 2 #13)*. */
43
53
  onDeclined?(handler: () => void): (() => void) | undefined;
54
+ /** The provider says no offered payment method can complete the order (Kustom's `can_not_complete_order`). Same binding
55
+ * contract as `onDeclined`: `undefined` until the provider's script exposes it. */
56
+ onCannotComplete?(handler: () => void): (() => void) | undefined;
57
+ /** This mount is over (remount, destroy): every deferred provider callback and timer of this controller does nothing after it. */
58
+ dispose?(): void;
44
59
  }
60
+ /** How often `klarna-kco` checks whether the snippet has defined `_klarnaCheckout`, while a suspension waits for it. */
61
+ export declare const KLARNA_READY_POLL_MS = 100;
45
62
  /** A controller's own timer, scheduled on the handle so that a remount or `destroy()` cancels it. */
46
63
  export type ControllerTimer = (fn: () => void, ms: number) => void;
47
64
  export type ControllerFactory = (win: ProviderWindow, timer: ControllerTimer) => CheckoutController;
@@ -1,3 +1,5 @@
1
+ /** How often `klarna-kco` checks whether the snippet has defined `_klarnaCheckout`, while a suspension waits for it. */
2
+ export const KLARNA_READY_POLL_MS = 100;
1
3
  /** How long `qliro-q1`'s resume waits for `q1.onOrderUpdated` to show the confirmed cart. Past it the widget is
2
4
  * re-rendered, never unlocked blindly *(plan review 1 #3)*. */
3
5
  export const QLIRO_SYNC_DEADLINE_MS = 10_000;
@@ -58,10 +60,103 @@ export const controllers = {
58
60
  },
59
61
  };
60
62
  },
61
- "klarna-kco": (win) => ({
62
- suspend: () => win._klarnaCheckout?.((api) => api.suspend()),
63
- resume: () => win._klarnaCheckout?.((api) => api.resume()),
64
- }),
63
+ "klarna-kco": (win, timer) => {
64
+ // Spec F §7.1: every deferred _klarnaCheckout callback carries this mount (`disposed`) and the desired state when it was
65
+ // queued (`seq`, `desired`); it acts only while it is still the latest wish of a live mount. KCO re-reads its order on
66
+ // resume(), so no total comparison is needed — the lifecycle guards are.
67
+ let disposed = false;
68
+ let seq = 0;
69
+ let desired;
70
+ let waiting = false;
71
+ // The desired-state generation already handed to `_klarnaCheckout`: the readiness timer hands over only a wish that was
72
+ // not, so an immediate handover by a later suspend() is never repeated by the timer *(review F2-#22)*.
73
+ let handed = 0;
74
+ const hand = (want, mine) => {
75
+ const kco = win._klarnaCheckout;
76
+ if (!kco)
77
+ return false;
78
+ const before = handed;
79
+ handed = mine;
80
+ try {
81
+ kco((api) => {
82
+ if (disposed || mine !== seq || desired !== want)
83
+ return;
84
+ if (want === "suspended")
85
+ api.suspend({ autoResume: { enabled: false } });
86
+ else
87
+ api.resume();
88
+ });
89
+ }
90
+ catch (e) {
91
+ handed = before; // the provider never took it: a later handover is not suppressed
92
+ throw e;
93
+ }
94
+ return true;
95
+ };
96
+ const awaitScript = () => {
97
+ if (waiting)
98
+ return;
99
+ waiting = true;
100
+ const tick = () => {
101
+ if (disposed)
102
+ return;
103
+ if (!win._klarnaCheckout) {
104
+ timer(tick, KLARNA_READY_POLL_MS);
105
+ return;
106
+ }
107
+ if (desired === "suspended" && handed !== seq) {
108
+ // A global that throws is not ready yet: keep waiting, never throw out of the handle's timer (review I1).
109
+ try {
110
+ hand("suspended", seq);
111
+ }
112
+ catch {
113
+ timer(tick, KLARNA_READY_POLL_MS);
114
+ return;
115
+ }
116
+ }
117
+ waiting = false;
118
+ };
119
+ timer(tick, KLARNA_READY_POLL_MS);
120
+ };
121
+ return {
122
+ suspend: () => {
123
+ const mine = ++seq;
124
+ desired = "suspended";
125
+ if (hand("suspended", mine))
126
+ return;
127
+ awaitScript();
128
+ return false;
129
+ },
130
+ resume: () => {
131
+ const mine = ++seq;
132
+ desired = "resumed";
133
+ // Before the script ran there is nothing to resume; a pending readiness check sees `resumed` and stays quiet. A global
134
+ // that throws is the same "not ready": `hand` restores `handed`, and nothing escapes into the handle (final review, Task 19).
135
+ try {
136
+ hand("resumed", mine);
137
+ }
138
+ catch { /* nothing to resume yet */ }
139
+ },
140
+ onCannotComplete: (handler) => {
141
+ const kco = win._klarnaCheckout;
142
+ if (!kco)
143
+ return undefined;
144
+ let live = true;
145
+ // A global that throws at bind time is "not bound yet": the handle retries, and mount/remount never throw for it
146
+ // (main's klarna-kco made no controller call at bind time) (review I1).
147
+ try {
148
+ kco((api) => api.on?.({ can_not_complete_order: () => { if (live && !disposed)
149
+ handler(); } }));
150
+ }
151
+ catch {
152
+ live = false;
153
+ return undefined;
154
+ }
155
+ return () => { live = false; };
156
+ },
157
+ dispose: () => { disposed = true; },
158
+ };
159
+ },
65
160
  walley: (win) => ({
66
161
  suspend: () => win.walley?.checkout?.api?.suspend(),
67
162
  resume: () => win.walley?.checkout?.api?.resume(),
package/dist/index.d.ts CHANGED
@@ -56,4 +56,4 @@ export { brandHead, collectionHead, productHead, toNextMetadata } from "./seo/he
56
56
  export { gtinJsonLd } from "./gtin.js";
57
57
  export { productJsonLd } from "./seo/product.js";
58
58
  export { jsonLdScript } from "./seo/serialize.js";
59
- export { SeoError, type BreadcrumbLink, type CollectionHeadOptions, type HeadOptions, type JsonLd, type NextMetadataLike, type ProductJsonLdOptions, type SchemaProperty, type SeoContext, type SeoHead, type SeoMeta, } from "./seo/types.js";
59
+ export { SeoError, type BreadcrumbLink, type CollectionHeadOptions, type HeadOptions, type JsonLd, type NextMetadataLike, type ProductJsonLdOptions, type SchemaProperty, type SeoContext, type SeoHead, type SeoMeta, type VariantAvailability, } from "./seo/types.js";
package/dist/schema.d.ts CHANGED
@@ -456,6 +456,7 @@ export interface components {
456
456
  };
457
457
  CartDto: {
458
458
  currency: string;
459
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
459
460
  grandTotalInc: string;
460
461
  lines: components["schemas"]["CartLineDto"][];
461
462
  market: string;
@@ -467,6 +468,8 @@ export interface components {
467
468
  warnings: components["schemas"]["WarningDto"][];
468
469
  };
469
470
  CartLineDto: {
471
+ availability: components["schemas"]["LineAvailability"];
472
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
470
473
  id: string;
471
474
  image: null | components["schemas"]["MediaDescriptorDto"];
472
475
  price: components["schemas"]["PriceDto"];
@@ -501,6 +504,7 @@ export interface components {
501
504
  };
502
505
  CheckoutCartDto: {
503
506
  currency: string;
507
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
504
508
  grandTotalInc: string;
505
509
  lines: components["schemas"]["CheckoutCartLineDto"][];
506
510
  subtotalInc: string;
@@ -508,6 +512,8 @@ export interface components {
508
512
  version: number | string;
509
513
  };
510
514
  CheckoutCartLineDto: {
515
+ availability: null | components["schemas"]["LineAvailability"];
516
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
511
517
  image: null | components["schemas"]["MediaDescriptorDto"];
512
518
  lineTotalInc: string;
513
519
  name: string;
@@ -568,6 +574,7 @@ export interface components {
568
574
  attemptId: string;
569
575
  order: null | components["schemas"]["CheckoutOrderRefDto"];
570
576
  paymentStatus: null | string;
577
+ promisedDelivery: null | components["schemas"]["DeliveryRangeDto"];
571
578
  status: string;
572
579
  widget: null | components["schemas"]["CheckoutWidgetDto"];
573
580
  };
@@ -611,6 +618,12 @@ export interface components {
611
618
  slug: string;
612
619
  url: string;
613
620
  };
621
+ DeliveryRangeDto: {
622
+ /** Format: int32 */
623
+ max: number;
624
+ /** Format: int32 */
625
+ min: number;
626
+ };
614
627
  DescriptionDto: {
615
628
  html: string;
616
629
  text: string;
@@ -649,6 +662,8 @@ export interface components {
649
662
  facet: string;
650
663
  values: components["schemas"]["FacetValueDto"][];
651
664
  };
665
+ /** @enum {unknown} */
666
+ LineAvailability: "backorder" | "inStock";
652
667
  ListingResponse: {
653
668
  appliedFilters: components["schemas"]["AppliedFilterDto"][];
654
669
  facets: components["schemas"]["FacetDto"][];
@@ -774,6 +789,7 @@ export interface components {
774
789
  orderNumber: null | string;
775
790
  /** Format: date-time */
776
791
  placedAt: string;
792
+ promisedDelivery: null | components["schemas"]["DeliveryRangeDto"];
777
793
  reference: string;
778
794
  status: components["schemas"]["OrderStatusDto"];
779
795
  test: boolean;
@@ -827,6 +843,7 @@ export interface components {
827
843
  };
828
844
  ProductCardDto: {
829
845
  brand: null | components["schemas"]["BrandDto"];
846
+ buyable: boolean;
830
847
  collections: string[];
831
848
  id: string;
832
849
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -897,6 +914,9 @@ export interface components {
897
914
  importer: null | components["schemas"]["PartyDto"];
898
915
  manufacturer: null | components["schemas"]["PartyDto"];
899
916
  };
917
+ SellabilityDto: {
918
+ mode: components["schemas"]["SiteSellabilityMode"];
919
+ };
900
920
  SeoDto: {
901
921
  description: null | string;
902
922
  title: string;
@@ -912,8 +932,11 @@ export interface components {
912
932
  media: components["schemas"]["MediaDto"];
913
933
  name: string;
914
934
  preview: boolean;
935
+ sellability: components["schemas"]["SellabilityDto"];
915
936
  urlPatterns: components["schemas"]["UrlPatternsDto"];
916
937
  };
938
+ /** @enum {unknown} */
939
+ SiteSellabilityMode: "backorder" | "onHand";
917
940
  SitemapEntryDto: {
918
941
  alternateUrls: {
919
942
  [key: string]: string;
@@ -1003,8 +1026,15 @@ export interface components {
1003
1026
  collection: string;
1004
1027
  product: string;
1005
1028
  };
1029
+ /** @enum {unknown} */
1030
+ VariantAvailability: "backorder" | "inStock" | "unavailable";
1006
1031
  VariantDto: {
1032
+ availability: components["schemas"]["VariantAvailability"];
1033
+ /** Format: int32 */
1034
+ availableUnits: number;
1035
+ backorderDelivery: null | components["schemas"]["DeliveryRangeDto"];
1007
1036
  brand: null | components["schemas"]["BrandDto"];
1037
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
1008
1038
  gtin: null | string;
1009
1039
  gtins: string[];
1010
1040
  hasOwnImages: boolean;
@@ -3,7 +3,15 @@ import { jsonLdDescription, pickImages, productDescription, rejectRedirect, requ
3
3
  import { SeoError } from "./types.js";
4
4
  const PROPERTIES = ["size", "color", "material", "pattern"];
5
5
  const IN_STOCK = "https://schema.org/InStock";
6
+ const BACK_ORDER = "https://schema.org/BackOrder";
6
7
  const OUT_OF_STOCK = "https://schema.org/OutOfStock";
8
+ const SCHEMA_AVAILABILITY = { inStock: IN_STOCK, backorder: BACK_ORDER, unavailable: OUT_OF_STOCK };
9
+ /** Sellability C1 §6: the caller's answer, else the variant's own; a response from an API older than 0.14 has no
10
+ * `availability` and falls back to `inStock`. */
11
+ function availabilityOf(v, ctx) {
12
+ const state = ctx.availability?.(v) ?? v.availability ?? (v.inStock ? "inStock" : "unavailable");
13
+ return SCHEMA_AVAILABILITY[state] ?? OUT_OF_STOCK;
14
+ }
7
15
  const brandNode = (label) => ({ "@type": "Brand", name: label });
8
16
  /** Spec §4.12: absolute http(s), distinct per variant; checked for every emitted variant, single path included. */
9
17
  function checkVariantUrls(variants, variantUrl) {
@@ -77,7 +85,7 @@ export function productJsonLd(p, ctx) {
77
85
  "@type": "Offer",
78
86
  price: v.price.amount,
79
87
  priceCurrency: v.price.currency,
80
- availability: v.inStock ? IN_STOCK : OUT_OF_STOCK,
88
+ availability: availabilityOf(v, ctx),
81
89
  url,
82
90
  ...offerExtras(ctx),
83
91
  });
@@ -4,6 +4,7 @@ export type SiteDto = Schemas["SiteDto"];
4
4
  export type MarketDto = Schemas["MarketDto"];
5
5
  export type ProductDto = Schemas["ProductDto"];
6
6
  export type VariantDto = Schemas["VariantDto"];
7
+ export type VariantAvailability = Schemas["VariantAvailability"];
7
8
  export type CollectionListingResponse = Schemas["CollectionListingResponse"];
8
9
  export type CollectionCrumbDto = Schemas["CollectionCrumbDto"];
9
10
  export type BrandDetailDto = Schemas["BrandDetailDto"];
@@ -43,6 +44,9 @@ export type ProductJsonLdOptions = SeoContext & {
43
44
  variants?: VariantDto[];
44
45
  /** Overrides the GTIN per variant; return `undefined` to omit. Defaults to the variant's `gtin`. */
45
46
  gtin?: (v: VariantDto) => string | undefined;
47
+ /** Sellability C1 §6: the availability for the quantity this page sells (e.g. a pair of the same lens needs two units).
48
+ * Return `undefined` to use the variant's own `availability` (its state for quantity 1). */
49
+ availability?: (v: VariantDto) => VariantAvailability | undefined;
46
50
  seller?: {
47
51
  name: string;
48
52
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asstio-dev/storefront",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Typed client for the Asstio Storefront API (generated from OpenAPI).",
5
5
  "repository": {
6
6
  "type": "git",
package/src/schema.d.ts CHANGED
@@ -456,6 +456,7 @@ export interface components {
456
456
  };
457
457
  CartDto: {
458
458
  currency: string;
459
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
459
460
  grandTotalInc: string;
460
461
  lines: components["schemas"]["CartLineDto"][];
461
462
  market: string;
@@ -467,6 +468,8 @@ export interface components {
467
468
  warnings: components["schemas"]["WarningDto"][];
468
469
  };
469
470
  CartLineDto: {
471
+ availability: components["schemas"]["LineAvailability"];
472
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
470
473
  id: string;
471
474
  image: null | components["schemas"]["MediaDescriptorDto"];
472
475
  price: components["schemas"]["PriceDto"];
@@ -501,6 +504,7 @@ export interface components {
501
504
  };
502
505
  CheckoutCartDto: {
503
506
  currency: string;
507
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
504
508
  grandTotalInc: string;
505
509
  lines: components["schemas"]["CheckoutCartLineDto"][];
506
510
  subtotalInc: string;
@@ -508,6 +512,8 @@ export interface components {
508
512
  version: number | string;
509
513
  };
510
514
  CheckoutCartLineDto: {
515
+ availability: null | components["schemas"]["LineAvailability"];
516
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
511
517
  image: null | components["schemas"]["MediaDescriptorDto"];
512
518
  lineTotalInc: string;
513
519
  name: string;
@@ -568,6 +574,7 @@ export interface components {
568
574
  attemptId: string;
569
575
  order: null | components["schemas"]["CheckoutOrderRefDto"];
570
576
  paymentStatus: null | string;
577
+ promisedDelivery: null | components["schemas"]["DeliveryRangeDto"];
571
578
  status: string;
572
579
  widget: null | components["schemas"]["CheckoutWidgetDto"];
573
580
  };
@@ -611,6 +618,12 @@ export interface components {
611
618
  slug: string;
612
619
  url: string;
613
620
  };
621
+ DeliveryRangeDto: {
622
+ /** Format: int32 */
623
+ max: number;
624
+ /** Format: int32 */
625
+ min: number;
626
+ };
614
627
  DescriptionDto: {
615
628
  html: string;
616
629
  text: string;
@@ -649,6 +662,8 @@ export interface components {
649
662
  facet: string;
650
663
  values: components["schemas"]["FacetValueDto"][];
651
664
  };
665
+ /** @enum {unknown} */
666
+ LineAvailability: "backorder" | "inStock";
652
667
  ListingResponse: {
653
668
  appliedFilters: components["schemas"]["AppliedFilterDto"][];
654
669
  facets: components["schemas"]["FacetDto"][];
@@ -774,6 +789,7 @@ export interface components {
774
789
  orderNumber: null | string;
775
790
  /** Format: date-time */
776
791
  placedAt: string;
792
+ promisedDelivery: null | components["schemas"]["DeliveryRangeDto"];
777
793
  reference: string;
778
794
  status: components["schemas"]["OrderStatusDto"];
779
795
  test: boolean;
@@ -827,6 +843,7 @@ export interface components {
827
843
  };
828
844
  ProductCardDto: {
829
845
  brand: null | components["schemas"]["BrandDto"];
846
+ buyable: boolean;
830
847
  collections: string[];
831
848
  id: string;
832
849
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -897,6 +914,9 @@ export interface components {
897
914
  importer: null | components["schemas"]["PartyDto"];
898
915
  manufacturer: null | components["schemas"]["PartyDto"];
899
916
  };
917
+ SellabilityDto: {
918
+ mode: components["schemas"]["SiteSellabilityMode"];
919
+ };
900
920
  SeoDto: {
901
921
  description: null | string;
902
922
  title: string;
@@ -912,8 +932,11 @@ export interface components {
912
932
  media: components["schemas"]["MediaDto"];
913
933
  name: string;
914
934
  preview: boolean;
935
+ sellability: components["schemas"]["SellabilityDto"];
915
936
  urlPatterns: components["schemas"]["UrlPatternsDto"];
916
937
  };
938
+ /** @enum {unknown} */
939
+ SiteSellabilityMode: "backorder" | "onHand";
917
940
  SitemapEntryDto: {
918
941
  alternateUrls: {
919
942
  [key: string]: string;
@@ -1003,8 +1026,15 @@ export interface components {
1003
1026
  collection: string;
1004
1027
  product: string;
1005
1028
  };
1029
+ /** @enum {unknown} */
1030
+ VariantAvailability: "backorder" | "inStock" | "unavailable";
1006
1031
  VariantDto: {
1032
+ availability: components["schemas"]["VariantAvailability"];
1033
+ /** Format: int32 */
1034
+ availableUnits: number;
1035
+ backorderDelivery: null | components["schemas"]["DeliveryRangeDto"];
1007
1036
  brand: null | components["schemas"]["BrandDto"];
1037
+ delivery: null | components["schemas"]["DeliveryRangeDto"];
1008
1038
  gtin: null | string;
1009
1039
  gtins: string[];
1010
1040
  hasOwnImages: boolean;