@asstio-dev/storefront 0.11.0 → 0.13.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/gtin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** Schema.org's GTIN property for a `gtin` value from the API (GTIN spec §13.3): gtin8, gtin12, gtin13 or gtin14 by
2
+ * length. The API sends the canonical form, so the length is the format. Spread it into a Product or Offer object. */
3
+ export declare function gtinJsonLd(gtin: string | null | undefined): Record<string, string>;
package/dist/gtin.js ADDED
@@ -0,0 +1,13 @@
1
+ /** Schema.org's GTIN property for a `gtin` value from the API (GTIN spec §13.3): gtin8, gtin12, gtin13 or gtin14 by
2
+ * length. The API sends the canonical form, so the length is the format. Spread it into a Product or Offer object. */
3
+ export function gtinJsonLd(gtin) {
4
+ if (!gtin || !/^\d+$/.test(gtin))
5
+ return {};
6
+ switch (gtin.length) {
7
+ case 8: return { gtin8: gtin };
8
+ case 12: return { gtin12: gtin };
9
+ case 13: return { gtin13: gtin };
10
+ case 14: return { gtin14: gtin };
11
+ default: return {};
12
+ }
13
+ }
package/dist/index.d.ts CHANGED
@@ -53,6 +53,7 @@ export { mountCheckout, type CheckoutHandle, type MountOptions, type MinimalDocu
53
53
  export { controllers, type CheckoutController, type ProviderWindow } from "./checkout/controllers.js";
54
54
  export { breadcrumbJsonLd } from "./seo/breadcrumb.js";
55
55
  export { brandHead, collectionHead, productHead, toNextMetadata } from "./seo/head.js";
56
+ export { gtinJsonLd } from "./gtin.js";
56
57
  export { productJsonLd } from "./seo/product.js";
57
58
  export { jsonLdScript } from "./seo/serialize.js";
58
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";
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ export { mountCheckout } from "./checkout/browser.js";
42
42
  export { controllers } from "./checkout/controllers.js";
43
43
  export { breadcrumbJsonLd } from "./seo/breadcrumb.js";
44
44
  export { brandHead, collectionHead, productHead, toNextMetadata } from "./seo/head.js";
45
+ export { gtinJsonLd } from "./gtin.js";
45
46
  export { productJsonLd } from "./seo/product.js";
46
47
  export { jsonLdScript } from "./seo/serialize.js";
47
48
  export { SeoError, } from "./seo/types.js";
package/dist/schema.d.ts CHANGED
@@ -852,6 +852,8 @@ export interface components {
852
852
  collections: components["schemas"]["CollectionRefDto"][];
853
853
  description: null | components["schemas"]["DescriptionDto"];
854
854
  files: components["schemas"]["MediaDescriptorDto"][];
855
+ gtin: null | string;
856
+ gtins: string[];
855
857
  id: string;
856
858
  image: null | components["schemas"]["MediaDescriptorDto"];
857
859
  images: components["schemas"]["MediaDescriptorDto"][];
@@ -1003,6 +1005,8 @@ export interface components {
1003
1005
  };
1004
1006
  VariantDto: {
1005
1007
  brand: null | components["schemas"]["BrandDto"];
1008
+ gtin: null | string;
1009
+ gtins: string[];
1006
1010
  hasOwnImages: boolean;
1007
1011
  id: string;
1008
1012
  image: null | components["schemas"]["MediaDescriptorDto"];
@@ -1,3 +1,4 @@
1
+ import { gtinJsonLd } from "../gtin.js";
1
2
  import { jsonLdDescription, pickImages, productDescription, rejectRedirect, requireMarket } from "./shared.js";
2
3
  import { SeoError } from "./types.js";
3
4
  const PROPERTIES = ["size", "color", "material", "pattern"];
@@ -81,8 +82,13 @@ export function productJsonLd(p, ctx) {
81
82
  ...offerExtras(ctx),
82
83
  });
83
84
  const gtin = (v) => {
84
- const g = ctx.gtin?.(v);
85
- return g ? { gtin: g } : {};
85
+ // GTIN spec §13.3 (plan P14): the hook wins when given, and may omit; otherwise the variant's own gtin. A value of
86
+ // 8, 12, 13 or 14 digits becomes gtin8/12/13/14; any other hook value keeps the generic property.
87
+ const g = ctx.gtin ? ctx.gtin(v) : (v.gtin ?? undefined);
88
+ if (!g)
89
+ return {};
90
+ const keyed = gtinJsonLd(g);
91
+ return Object.keys(keyed).length > 0 ? keyed : { gtin: g };
86
92
  };
87
93
  if (list === null || sellable.length === 0)
88
94
  return { ...common("Product"), sku: p.sku, ...(p.brand ? { brand: brandNode(p.brand.label) } : {}) };
@@ -41,6 +41,7 @@ export type ProductJsonLdOptions = SeoContext & {
41
41
  variesBy?: Record<string, SchemaProperty>;
42
42
  /** Used only when `p.variants` is `null` (more than 100 variants), e.g. the first page of `/variants`. */
43
43
  variants?: VariantDto[];
44
+ /** Overrides the GTIN per variant; return `undefined` to omit. Defaults to the variant's `gtin`. */
44
45
  gtin?: (v: VariantDto) => string | undefined;
45
46
  seller?: {
46
47
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asstio-dev/storefront",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Typed client for the Asstio Storefront API (generated from OpenAPI).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,7 +21,7 @@
21
21
  "test": "vitest run",
22
22
  "test:browser": "playwright test -c browser/playwright.config.ts",
23
23
  "check": "npm run generate && npm run typecheck-examples && npm test && git diff --exit-code -- src/schema.d.ts && test -z \"$(git status --porcelain -- src/schema.d.ts)\"",
24
- "smoke": "node --input-type=module -e \"import(\\\"./dist/index.js\\\").then(m => { for (const f of [\\\"createStorefrontClient\\\", \\\"filterQuery\\\", \\\"encodeFilterValues\\\", \\\"decodeFilterValues\\\", \\\"pictureSources\\\", \\\"mediaUrl\\\", \\\"formatImage\\\", \\\"styleToCss\\\", \\\"startCheckout\\\", \\\"syncCheckout\\\", \\\"completeCheckout\\\", \\\"getCheckoutStatus\\\", \\\"getOrder\\\", \\\"awaitCompletion\\\", \\\"mountCheckout\\\", \\\"productJsonLd\\\", \\\"breadcrumbJsonLd\\\", \\\"productHead\\\", \\\"collectionHead\\\", \\\"brandHead\\\", \\\"toNextMetadata\\\", \\\"jsonLdScript\\\", \\\"SeoError\\\"]) if (typeof m[f] !== \\\"function\\\") throw new Error(\\\"missing export \\\" + f) })\""
24
+ "smoke": "node --input-type=module -e \"import(\\\"./dist/index.js\\\").then(m => { for (const f of [\\\"createStorefrontClient\\\", \\\"filterQuery\\\", \\\"encodeFilterValues\\\", \\\"decodeFilterValues\\\", \\\"pictureSources\\\", \\\"mediaUrl\\\", \\\"formatImage\\\", \\\"styleToCss\\\", \\\"startCheckout\\\", \\\"syncCheckout\\\", \\\"completeCheckout\\\", \\\"getCheckoutStatus\\\", \\\"getOrder\\\", \\\"awaitCompletion\\\", \\\"mountCheckout\\\", \\\"productJsonLd\\\", \\\"gtinJsonLd\\\", \\\"breadcrumbJsonLd\\\", \\\"productHead\\\", \\\"collectionHead\\\", \\\"brandHead\\\", \\\"toNextMetadata\\\", \\\"jsonLdScript\\\", \\\"SeoError\\\"]) if (typeof m[f] !== \\\"function\\\") throw new Error(\\\"missing export \\\" + f) })\""
25
25
  },
26
26
  "dependencies": {
27
27
  "openapi-fetch": "0.17.0"
package/src/schema.d.ts CHANGED
@@ -852,6 +852,8 @@ export interface components {
852
852
  collections: components["schemas"]["CollectionRefDto"][];
853
853
  description: null | components["schemas"]["DescriptionDto"];
854
854
  files: components["schemas"]["MediaDescriptorDto"][];
855
+ gtin: null | string;
856
+ gtins: string[];
855
857
  id: string;
856
858
  image: null | components["schemas"]["MediaDescriptorDto"];
857
859
  images: components["schemas"]["MediaDescriptorDto"][];
@@ -1003,6 +1005,8 @@ export interface components {
1003
1005
  };
1004
1006
  VariantDto: {
1005
1007
  brand: null | components["schemas"]["BrandDto"];
1008
+ gtin: null | string;
1009
+ gtins: string[];
1006
1010
  hasOwnImages: boolean;
1007
1011
  id: string;
1008
1012
  image: null | components["schemas"]["MediaDescriptorDto"];