@asstio-dev/storefront 0.16.3 → 0.18.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
@@ -243,6 +243,61 @@ config. Holes in the data never throw: a missing image, description or price onl
243
243
 
244
244
  hreflang sets are only as reciprocal as the API's `alternateUrls`; the SDK renders what the API gives.
245
245
 
246
+ ### Product facts and GA4
247
+
248
+ Cards and cart lines carry the product facts the product page has: `brand`, `categories`, `shortDescription` and
249
+ `attributes` (minus `type: "html"` attributes, which stay on the product page). A card has them flat; a cart line under
250
+ `line.product`, with the line's own brand on `line.variant.brand`. An order line has the same `product`/`variant`
251
+ shape without `shortDescription` (`productFacts(orderLine).shortDescription` is `null`): its facts are the ones the
252
+ charged checkout snapshot captured, never re-resolved — `url`, `facet` and attribute `facetKey`/`valueKeys` are `null`
253
+ there, the blocks are empty on orders placed before they existed, and `null` on fee, shipping and adjustment lines.
254
+ A checkout cart line (`checkout.cart.lines[]`) has the order-line shape too, but is resolved **live while the attempt is
255
+ `open`**, like a cart line, with `url`, `facet` and attribute keys filled in. In every other status, and for a line the
256
+ catalogue can no longer describe, it carries the snapshot's frozen facts (links `null`). What it shows may therefore
257
+ differ from what the order later freezes. `productFacts()` reads them from any source:
258
+
259
+ ```ts
260
+ const facts = productFacts(line); // card, product, { product, variant }, cart line, checkout cart line or product order line
261
+ render(facts.brand?.label); // the item's brand: the variant's own, never a fallback to the family's
262
+ render(facts.productBrand?.label); // the family brand
263
+ ```
264
+
265
+ `brand` is the selected or purchased variant's brand, and `null` can mean the variant deliberately has none — do not
266
+ fall back to `productBrand`. **Categories carry no primary meaning**: `categories[0]` may be a gender, a brand or a
267
+ product-type node. Pick the one you want by its root.
268
+
269
+ `toGa4Item()` and `toGa4Ecommerce()` map the same sources to GA4 ecommerce data; `toGa4Ecommerce(checkout.cart)` is the
270
+ `begin_checkout` payload. They are pure: they send nothing and
271
+ know nothing about consent or a data layer.
272
+
273
+ | Source | `item_id` | `item_brand` | `price` | `quantity` | `item_variant` |
274
+ |---|---|---|---|---|---|
275
+ | card (impression) | the family `sku` | `brand.label` | from-price | 1 | — |
276
+ | `{ product, variant }` | the variant `sku` | the variant's brand | the variant's price | 1 | selected options |
277
+ | cart line | the line `sku` | the variant's brand | the line's unit price | the line's | selected options |
278
+ | checkout cart line | the line `sku` | the variant's brand | the unit price | the line's | options |
279
+ | order line (product) | the line `sku` | the variant's brand | the unit price | the line's | options |
280
+
281
+ - A card is a family impression; later events carry the purchased sku. Pass `itemId` to report at family level.
282
+ - `price` and `value` are **net of VAT** by default, as GA4 defines them: 2 × 125.00 at 25 % gives `price: 100`,
283
+ `value: 200`. `value` is the sum of the items' rounded prices, so it need not match accounting to the öre (3 × 9.99 at
284
+ 25 % gives `price: 7.99`, `value: 23.97`). Pass `tax: "include"` to report gross on purpose.
285
+ - For `add_to_cart` and `remove_from_cart`, pass `quantity`: the positive number of units added or removed, never the
286
+ line's new total.
287
+ - `item_category`…`item_category5` come from one category's `pathNames`: by default the longest, or the one your
288
+ `pickCategory` returns. A `pickCategory` that returns `undefined` means no category fields.
289
+ - No `discount`: `compareAt` is a recommended price, not a discount applied.
290
+ - A source without a price throws `AnalyticsError`, as does a non-product order line; never `price: 0`.
291
+ - For an order, `transaction_id` is `order.id` (stable from completion; `orderNumber` is null until export), `tax` the
292
+ recorded VAT and `shipping` the shipping lines' net.
293
+
294
+ ```ts
295
+ const byProductType = (cs: readonly CategoryRefDto[]) => cs.find((c) => c.pathNames[0] === "Produkttyp");
296
+ gtag("event", "view_item_list", { items: listing.items.map((card, index) => toGa4Item(card, { index, pickCategory: byProductType })) });
297
+ gtag("event", "add_to_cart", { currency: cart.currency, items: [toGa4Item(line, { quantity: added })] });
298
+ gtag("event", "purchase", toGa4Ecommerce(order, { pickCategory: byProductType }));
299
+ ```
300
+
246
301
  ### Search-as-you-type and search-in-facet
247
302
 
248
303
  `/v1/search/suggest` answers three short lists for what the shopper has typed so far: up to 5 product cards
@@ -0,0 +1,13 @@
1
+ /** An exact rational `n / d` (d > 0). Every API amount is a decimal string; nothing here ever goes through a float. */
2
+ export type Rational = {
3
+ n: bigint;
4
+ d: bigint;
5
+ };
6
+ /** `"125.00"`, `"25"`, `"-3.5"` → an exact rational. Anything else (exponents, commas, blanks) throws. */
7
+ export declare function parseDecimal(s: string): Rational;
8
+ /** The net of a VAT-inclusive amount: `gross / (1 + rate / 100)`, exactly. */
9
+ export declare function netOf(gross: Rational, vatRatePercent: Rational): Rational;
10
+ export declare function add(a: Rational, b: Rational): Rational;
11
+ /** Rounded to whole cents (öre), half away from zero. */
12
+ export declare function toCents(r: Rational): bigint;
13
+ export declare const centsToNumber: (cents: bigint) => number;
@@ -0,0 +1,29 @@
1
+ import { AnalyticsError } from "./types.js";
2
+ const DECIMAL = /^-?\d+(\.\d+)?$/;
3
+ /** `"125.00"`, `"25"`, `"-3.5"` → an exact rational. Anything else (exponents, commas, blanks) throws. */
4
+ export function parseDecimal(s) {
5
+ if (typeof s !== "string" || !DECIMAL.test(s))
6
+ throw new AnalyticsError(`not a decimal amount: "${s}"`);
7
+ const negative = s.startsWith("-");
8
+ const [whole, fraction = ""] = (negative ? s.slice(1) : s).split(".");
9
+ const n = BigInt(whole + fraction);
10
+ return { n: negative ? -n : n, d: 10n ** BigInt(fraction.length) };
11
+ }
12
+ /** The net of a VAT-inclusive amount: `gross / (1 + rate / 100)`, exactly. */
13
+ export function netOf(gross, vatRatePercent) {
14
+ return { n: gross.n * 100n * vatRatePercent.d, d: gross.d * (100n * vatRatePercent.d + vatRatePercent.n) };
15
+ }
16
+ export function add(a, b) {
17
+ return { n: a.n * b.d + b.n * a.d, d: a.d * b.d };
18
+ }
19
+ /** Rounded to whole cents (öre), half away from zero. */
20
+ export function toCents(r) {
21
+ const num = r.n * 100n;
22
+ let q = num / r.d; // truncates toward zero
23
+ const rem = num % r.d;
24
+ const twice = 2n * (rem < 0n ? -rem : rem);
25
+ if (twice >= r.d)
26
+ q += num < 0n ? -1n : 1n;
27
+ return q;
28
+ }
29
+ export const centsToNumber = (cents) => Number(cents) / 100;
@@ -0,0 +1,13 @@
1
+ import { type CartLineDto, type CheckoutCartLineDto, type OrderLineDto, type ProductCardDto, type ProductDto, type ProductFacts, type SelectedVariant } from "./types.js";
2
+ /**
3
+ * The product facts of a card, a product page (with or without a selected variant), a cart line, a checkout cart line or an order line, so a
4
+ * frontend never needs to know where a surface keeps them. Returns the DTO's own arrays and objects — never copies,
5
+ * filters or mutates them. `brand` is the item's brand: a selected or purchased variant's own, with no fallback to the
6
+ * family brand (null can mean an explicit "none"). An order line that is not a product line throws.
7
+ */
8
+ export declare function productFacts(source: ProductCardDto): ProductFacts;
9
+ export declare function productFacts(source: ProductDto): ProductFacts;
10
+ export declare function productFacts(source: SelectedVariant): ProductFacts;
11
+ export declare function productFacts(source: CartLineDto): ProductFacts;
12
+ export declare function productFacts(source: CheckoutCartLineDto): ProductFacts;
13
+ export declare function productFacts(source: OrderLineDto): ProductFacts;
@@ -0,0 +1,32 @@
1
+ import { AnalyticsError, } from "./types.js";
2
+ export function productFacts(source) {
3
+ const s = source;
4
+ if (typeof s.type === "string") {
5
+ // An order line (checked first): the generated `type` is a plain string, so the product-line rule is checked here.
6
+ const line = source;
7
+ if (line.type !== "product" || !line.product || !line.variant)
8
+ throw new AnalyticsError(`an order line of type "${line.type}" has no product facts`);
9
+ return {
10
+ brand: line.variant.brand, productBrand: line.product.brand, categories: line.product.categories,
11
+ attributes: line.product.attributes, shortDescription: null,
12
+ };
13
+ }
14
+ if (isObject(s.product) && isObject(s.variant)) {
15
+ // A cart line, a checkout cart line or a selected-variant wrapper: the same fact paths. A checkout line's product has no
16
+ // `shortDescription` (it is the order-line shape), so it reads null there.
17
+ const { product, variant } = source;
18
+ const short = product.shortDescription;
19
+ return {
20
+ brand: variant.brand, productBrand: product.brand, categories: product.categories,
21
+ attributes: product.attributes, shortDescription: short ?? null,
22
+ };
23
+ }
24
+ const flat = source;
25
+ if (!Array.isArray(flat.categories) || !Array.isArray(flat.attributes))
26
+ throw new AnalyticsError("not a product facts source");
27
+ return {
28
+ brand: flat.brand, productBrand: flat.brand, categories: flat.categories, attributes: flat.attributes,
29
+ shortDescription: flat.shortDescription ?? null,
30
+ };
31
+ }
32
+ const isObject = (v) => typeof v === "object" && v !== null;
@@ -0,0 +1,22 @@
1
+ import { type CartDto, type CheckoutCartDto, type Ga4Ecommerce, type Ga4Item, type Ga4ItemOptions, type Ga4ItemSource, type Ga4Purchase, type Ga4SharedOptions, type OrderDto } from "./types.js";
2
+ /** An item and its unit price in cents, so a payload's `value` sums exact cents rather than re-reading floats. */
3
+ export type MappedItem = {
4
+ item: Ga4Item;
5
+ priceCents: bigint;
6
+ };
7
+ /**
8
+ * One GA4 item from a card (an impression: the family sku and from-price), a product with its selected variant, a cart
9
+ * line, a checkout cart line or a product order line. `item_brand` and the categories come from `productFacts`, so they never disagree with it.
10
+ * A source without a price throws — never `price: 0`.
11
+ */
12
+ export declare function toGa4Item(source: Ga4ItemSource, opts?: Ga4ItemOptions): Ga4Item;
13
+ export declare function mapItem(source: Ga4ItemSource, opts?: Ga4ItemOptions): MappedItem;
14
+ /**
15
+ * A whole GA4 payload: `{ currency, value, items }` for a cart; plus `transaction_id`, `tax` and `shipping` for an order
16
+ * (`purchase`). Each line keeps its own quantity and id; `index` is its position. Only product lines are items.
17
+ * `value` is the sum of the emitted items' rounded prices × quantities, not gross minus tax. An order's `tax` is its
18
+ * recorded VAT and its `shipping` comes from its shipping lines, each netted at its own rate and rounded once.
19
+ */
20
+ export declare function toGa4Ecommerce(source: CartDto, opts?: Ga4SharedOptions): Ga4Ecommerce;
21
+ export declare function toGa4Ecommerce(source: CheckoutCartDto, opts?: Ga4SharedOptions): Ga4Ecommerce;
22
+ export declare function toGa4Ecommerce(source: OrderDto, opts?: Ga4SharedOptions): Ga4Purchase;
@@ -0,0 +1,116 @@
1
+ import { add, centsToNumber, netOf, parseDecimal, toCents } from "./decimal.js";
2
+ import { productFacts } from "./facts.js";
3
+ import { AnalyticsError, } from "./types.js";
4
+ /**
5
+ * One GA4 item from a card (an impression: the family sku and from-price), a product with its selected variant, a cart
6
+ * line, a checkout cart line or a product order line. `item_brand` and the categories come from `productFacts`, so they never disagree with it.
7
+ * A source without a price throws — never `price: 0`.
8
+ */
9
+ export function toGa4Item(source, opts = {}) {
10
+ return mapItem(source, opts).item;
11
+ }
12
+ export function mapItem(source, opts = {}) {
13
+ const line = lineOf(source);
14
+ const facts = productFacts(source);
15
+ const quantity = opts.quantity === undefined ? line.quantity : checkedQuantity(opts.quantity);
16
+ const priceCents = toCents(opts.tax === "include" ? line.gross : netOf(line.gross, line.vatRate));
17
+ const item = { item_id: opts.itemId ?? line.id, item_name: line.name, price: centsToNumber(priceCents), quantity };
18
+ if (facts.brand)
19
+ item.item_brand = facts.brand.label;
20
+ const variant = itemVariant(line.options);
21
+ if (variant !== undefined)
22
+ item.item_variant = variant;
23
+ const picked = opts.pickCategory ? opts.pickCategory(facts.categories) : longest(facts.categories);
24
+ if (picked) {
25
+ const keys = ["item_category", "item_category2", "item_category3", "item_category4", "item_category5"];
26
+ picked.pathNames.slice(0, keys.length).forEach((name, i) => { item[keys[i]] = name; });
27
+ }
28
+ if (opts.index !== undefined)
29
+ item.index = opts.index;
30
+ if (opts.affiliation !== undefined)
31
+ item.affiliation = opts.affiliation;
32
+ if (opts.itemListId !== undefined)
33
+ item.item_list_id = opts.itemListId;
34
+ if (opts.itemListName !== undefined)
35
+ item.item_list_name = opts.itemListName;
36
+ return { item, priceCents };
37
+ }
38
+ export function toGa4Ecommerce(source, opts = {}) {
39
+ const isOrder = "totals" in source && "reference" in source;
40
+ const lines = isOrder ? source.lines.filter(l => l.type === "product") : source.lines;
41
+ // Only the shared options: an options object that also carries item overrides (quantity, itemId, index) must never
42
+ // rewrite every line's identity or quantity.
43
+ const shared = {
44
+ tax: opts.tax, affiliation: opts.affiliation, itemListId: opts.itemListId, itemListName: opts.itemListName, pickCategory: opts.pickCategory,
45
+ };
46
+ const mapped = lines.map((l, index) => mapItem(l, { ...shared, index }));
47
+ const valueCents = mapped.reduce((sum, m) => sum + m.priceCents * BigInt(m.item.quantity), 0n);
48
+ const payload = { currency: source.currency, value: centsToNumber(valueCents), items: mapped.map(m => m.item) };
49
+ if (!isOrder)
50
+ return payload;
51
+ const order = source;
52
+ const include = opts.tax === "include";
53
+ const tax = include ? 0n : order.totals.vat.reduce((sum, v) => sum + toCents(parseDecimal(v.amount)), 0n);
54
+ const shippingLines = order.lines.filter(l => l.type === "shipping");
55
+ const shipping = include
56
+ ? shippingLines.reduce((sum, l) => sum + toCents(parseDecimal(l.lineTotalInc)), 0n)
57
+ : toCents(shippingLines.reduce((sum, l) => add(sum, netOf(parseDecimal(l.lineTotalInc), parseDecimal(l.vatRate))), { n: 0n, d: 1n }));
58
+ return { ...payload, transaction_id: order.id, tax: centsToNumber(tax), shipping: centsToNumber(shipping) };
59
+ }
60
+ function lineOf(source) {
61
+ const s = source;
62
+ if (typeof s.type === "string") {
63
+ const l = source;
64
+ if (l.type !== "product")
65
+ throw new AnalyticsError(`an order line of type "${l.type}" is not an item`);
66
+ return { id: l.sku, name: l.name, gross: parseDecimal(l.unitPriceInc), vatRate: parseDecimal(l.vatRate), quantity: sourceQuantity(l.quantity), options: l.options };
67
+ }
68
+ if (typeof s.unitPriceInc === "string" && typeof s.productId === "string" && typeof s.type !== "string") {
69
+ // A checkout cart line: the snapshot's own amounts (`unitPriceInc`, `vatRate`), not a PriceDto, and the options on the line.
70
+ const l = source;
71
+ return { id: l.sku, name: l.name, gross: parseDecimal(l.unitPriceInc), vatRate: parseDecimal(l.vatRate), quantity: sourceQuantity(l.quantity), options: l.options };
72
+ }
73
+ if (typeof s.variant === "object" && s.variant !== null && "selectedOptions" in s.variant) {
74
+ if ("quantity" in s) {
75
+ const l = source;
76
+ return { id: l.sku, name: l.product.name, ...money(l.price), quantity: sourceQuantity(l.quantity), options: l.variant.selectedOptions };
77
+ }
78
+ const { product, variant } = source;
79
+ if (!variant.price)
80
+ throw new AnalyticsError(`variant ${variant.sku} has no price on this market`);
81
+ return { id: variant.sku, name: product.name, ...money(variant.price), quantity: 1, options: variant.selectedOptions };
82
+ }
83
+ const c = source;
84
+ if (!c.price)
85
+ throw new AnalyticsError(`product ${c.sku} has no price on this market`);
86
+ return { id: c.sku, name: c.name, ...money(c.price), quantity: 1, options: null };
87
+ }
88
+ const money = (p) => ({ gross: parseDecimal(p.amount), vatRate: parseDecimal(p.vatRate) });
89
+ /** The generated quantity is `number | string`. */
90
+ function sourceQuantity(q) {
91
+ const n = typeof q === "number" ? q : Number(q);
92
+ if (!Number.isFinite(n))
93
+ throw new AnalyticsError(`not a quantity: ${q}`);
94
+ return n;
95
+ }
96
+ /** add_to_cart/remove_from_cart: the positive whole number of units added or removed. */
97
+ function checkedQuantity(q) {
98
+ if (!Number.isInteger(q) || q <= 0)
99
+ throw new AnalyticsError(`quantity must be a positive whole number, got ${q}`);
100
+ return q;
101
+ }
102
+ /** Options sorted ordinally by axis (never DTO order: jsonb does not keep key order), `"axis: value"` joined with ", ". */
103
+ function itemVariant(options) {
104
+ if (!options)
105
+ return undefined;
106
+ const axes = Object.keys(options).sort();
107
+ return axes.length === 0 ? undefined : axes.map(a => `${a}: ${options[a]}`).join(", ");
108
+ }
109
+ /** The default pick: the longest path, the first of equals. Never mutates the array. */
110
+ function longest(categories) {
111
+ let best;
112
+ for (const c of categories)
113
+ if (!best || c.pathNames.length > best.pathNames.length)
114
+ best = c;
115
+ return best;
116
+ }
@@ -0,0 +1,88 @@
1
+ import type { components } from "../schema";
2
+ type Schemas = components["schemas"];
3
+ export type ProductCardDto = Schemas["ProductCardDto"];
4
+ export type ProductDto = Schemas["ProductDto"];
5
+ export type VariantDto = Schemas["VariantDto"];
6
+ export type CartDto = Schemas["CartDto"];
7
+ export type CartLineDto = Schemas["CartLineDto"];
8
+ export type OrderDto = Schemas["OrderDto"];
9
+ export type OrderLineDto = Schemas["OrderLineDto"];
10
+ export type CheckoutCartDto = Schemas["CheckoutCartDto"];
11
+ export type CheckoutCartLineDto = Schemas["CheckoutCartLineDto"];
12
+ export type BrandDto = Schemas["BrandDto"];
13
+ export type CategoryRefDto = Schemas["CategoryRefDto"];
14
+ export type AttributeDto = Schemas["AttributeDto"];
15
+ export type PriceDto = Schemas["PriceDto"];
16
+ /** A product page with the variant the shopper selected. */
17
+ export type SelectedVariant = {
18
+ product: ProductDto;
19
+ variant: VariantDto;
20
+ };
21
+ /** The product facts of one source, whatever surface it came from (product facts spec §5.0). */
22
+ export type ProductFacts = {
23
+ /** The item's brand: a card's (family) brand, else the selected or purchased variant's — never a fallback to the family's. */
24
+ brand: BrandDto | null;
25
+ /** The family brand on every source (on a card, the same as `brand`). */
26
+ productBrand: BrandDto | null;
27
+ /** Classification memberships with no primary meaning; `categories[0]` may be any root. */
28
+ categories: CategoryRefDto[];
29
+ /** As the source carries them: a `ProductDto` may include `html` attributes; cards, cart and order lines do not. */
30
+ attributes: AttributeDto[];
31
+ /** Null on order lines and checkout cart lines. */
32
+ shortDescription: string | null;
33
+ };
34
+ /** A source the analytics helpers cannot map: a non-product order line, an unpriced variant, an invalid quantity. */
35
+ export declare class AnalyticsError extends Error {
36
+ name: string;
37
+ }
38
+ /** One GA4 ecommerce item. Members without a value are omitted, never `undefined`. */
39
+ export type Ga4Item = {
40
+ item_id: string;
41
+ item_name: string;
42
+ item_brand?: string;
43
+ item_variant?: string;
44
+ item_category?: string;
45
+ item_category2?: string;
46
+ item_category3?: string;
47
+ item_category4?: string;
48
+ item_category5?: string;
49
+ /** Net of VAT by default (GA4 defines price without tax); gross with `tax: "include"`. */
50
+ price: number;
51
+ quantity: number;
52
+ index?: number;
53
+ affiliation?: string;
54
+ item_list_id?: string;
55
+ item_list_name?: string;
56
+ };
57
+ /** Options shared by every item of a payload. */
58
+ export type Ga4SharedOptions = {
59
+ /** `"exclude"` (default): prices and value net of VAT, as GA4 defines them. `"include"`: gross, for shops that report gross revenue. */
60
+ tax?: "exclude" | "include";
61
+ affiliation?: string;
62
+ itemListId?: string;
63
+ itemListName?: string;
64
+ /** Which category feeds `item_category`…`item_category5`. Default: the longest path, ties in the order given. Returning
65
+ * `undefined` means no category fields — never a fallback to the default. Categories carry no primary meaning. */
66
+ pickCategory?: (categories: readonly CategoryRefDto[]) => CategoryRefDto | undefined;
67
+ };
68
+ /** One item's options: `quantity` overrides the source's (for add/remove events: the positive number of units added or
69
+ * removed), `itemId` its id (e.g. for family-level reporting), `index` its position in a list. */
70
+ export type Ga4ItemOptions = Ga4SharedOptions & {
71
+ quantity?: number;
72
+ itemId?: string;
73
+ index?: number;
74
+ };
75
+ export type Ga4ItemSource = ProductCardDto | SelectedVariant | CartLineDto | CheckoutCartLineDto | OrderLineDto;
76
+ /** `view_cart`, `begin_checkout` and the like. */
77
+ export type Ga4Ecommerce = {
78
+ currency: string;
79
+ value: number;
80
+ items: Ga4Item[];
81
+ };
82
+ /** `purchase`: the order's payload. */
83
+ export type Ga4Purchase = Ga4Ecommerce & {
84
+ transaction_id: string;
85
+ tax: number;
86
+ shipping: number;
87
+ };
88
+ export {};
@@ -0,0 +1,4 @@
1
+ /** A source the analytics helpers cannot map: a non-product order line, an unpriced variant, an invalid quantity. */
2
+ export class AnalyticsError extends Error {
3
+ name = "AnalyticsError";
4
+ }
package/dist/index.d.ts CHANGED
@@ -56,4 +56,7 @@ 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 { productFacts } from "./analytics/facts.js";
60
+ export { toGa4Ecommerce, toGa4Item } from "./analytics/ga4.js";
61
+ export { AnalyticsError, type Ga4Ecommerce, type Ga4Item, type Ga4ItemOptions, type Ga4ItemSource, type Ga4Purchase, type Ga4SharedOptions, type ProductFacts, type SelectedVariant, } from "./analytics/types.js";
59
62
  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/index.js CHANGED
@@ -45,4 +45,7 @@ export { brandHead, collectionHead, productHead, toNextMetadata } from "./seo/he
45
45
  export { gtinJsonLd } from "./gtin.js";
46
46
  export { productJsonLd } from "./seo/product.js";
47
47
  export { jsonLdScript } from "./seo/serialize.js";
48
+ export { productFacts } from "./analytics/facts.js";
49
+ export { toGa4Ecommerce, toGa4Item } from "./analytics/ga4.js";
50
+ export { AnalyticsError, } from "./analytics/types.js";
48
51
  export { SeoError, } from "./seo/types.js";
package/dist/schema.d.ts CHANGED
@@ -496,11 +496,16 @@ export interface components {
496
496
  variant: components["schemas"]["CartLineVariantDto"];
497
497
  };
498
498
  CartLineProductDto: {
499
+ attributes: components["schemas"]["AttributeDto"][];
500
+ brand: null | components["schemas"]["BrandDto"];
501
+ categories: components["schemas"]["CategoryRefDto"][];
499
502
  id: string;
500
503
  name: string;
504
+ shortDescription: null | string;
501
505
  slug: string;
502
506
  };
503
507
  CartLineVariantDto: {
508
+ brand: null | components["schemas"]["BrandDto"];
504
509
  id: string;
505
510
  selectedOptions: {
506
511
  [key: string]: string;
@@ -536,11 +541,13 @@ export interface components {
536
541
  options: {
537
542
  [key: string]: string;
538
543
  };
544
+ product: components["schemas"]["OrderLineProductDto"];
539
545
  productId: string;
540
546
  /** Format: int32 */
541
547
  quantity: number | string;
542
548
  sku: string;
543
549
  unitPriceInc: string;
550
+ variant: components["schemas"]["OrderLineVariantDto"];
544
551
  variantId: string;
545
552
  vatRate: string;
546
553
  };
@@ -833,15 +840,25 @@ export interface components {
833
840
  options: {
834
841
  [key: string]: string;
835
842
  };
843
+ product: null | components["schemas"]["OrderLineProductDto"];
836
844
  productId: null | string;
837
845
  /** Format: int32 */
838
846
  quantity: number | string;
839
847
  sku: string;
840
848
  type: string;
841
849
  unitPriceInc: string;
850
+ variant: null | components["schemas"]["OrderLineVariantDto"];
842
851
  variantId: null | string;
843
852
  vatRate: string;
844
853
  };
854
+ OrderLineProductDto: {
855
+ attributes: components["schemas"]["AttributeDto"][];
856
+ brand: null | components["schemas"]["BrandDto"];
857
+ categories: components["schemas"]["CategoryRefDto"][];
858
+ };
859
+ OrderLineVariantDto: {
860
+ brand: null | components["schemas"]["BrandDto"];
861
+ };
845
862
  OrderShippingDto: {
846
863
  carrier: string;
847
864
  deliveryTime: null | components["schemas"]["DeliveryTimeDto"];
@@ -887,8 +904,10 @@ export interface components {
887
904
  vatRate: string;
888
905
  };
889
906
  ProductCardDto: {
907
+ attributes: components["schemas"]["AttributeDto"][];
890
908
  brand: null | components["schemas"]["BrandDto"];
891
909
  buyable: boolean;
910
+ categories: components["schemas"]["CategoryRefDto"][];
892
911
  collections: string[];
893
912
  id: string;
894
913
  image: null | components["schemas"]["MediaDescriptorDto"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asstio-dev/storefront",
3
- "version": "0.16.3",
3
+ "version": "0.18.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\\\", \\\"gtinJsonLd\\\", \\\"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\\\", \\\"productFacts\\\", \\\"toGa4Item\\\", \\\"toGa4Ecommerce\\\", \\\"AnalyticsError\\\"]) 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
@@ -496,11 +496,16 @@ export interface components {
496
496
  variant: components["schemas"]["CartLineVariantDto"];
497
497
  };
498
498
  CartLineProductDto: {
499
+ attributes: components["schemas"]["AttributeDto"][];
500
+ brand: null | components["schemas"]["BrandDto"];
501
+ categories: components["schemas"]["CategoryRefDto"][];
499
502
  id: string;
500
503
  name: string;
504
+ shortDescription: null | string;
501
505
  slug: string;
502
506
  };
503
507
  CartLineVariantDto: {
508
+ brand: null | components["schemas"]["BrandDto"];
504
509
  id: string;
505
510
  selectedOptions: {
506
511
  [key: string]: string;
@@ -536,11 +541,13 @@ export interface components {
536
541
  options: {
537
542
  [key: string]: string;
538
543
  };
544
+ product: components["schemas"]["OrderLineProductDto"];
539
545
  productId: string;
540
546
  /** Format: int32 */
541
547
  quantity: number | string;
542
548
  sku: string;
543
549
  unitPriceInc: string;
550
+ variant: components["schemas"]["OrderLineVariantDto"];
544
551
  variantId: string;
545
552
  vatRate: string;
546
553
  };
@@ -833,15 +840,25 @@ export interface components {
833
840
  options: {
834
841
  [key: string]: string;
835
842
  };
843
+ product: null | components["schemas"]["OrderLineProductDto"];
836
844
  productId: null | string;
837
845
  /** Format: int32 */
838
846
  quantity: number | string;
839
847
  sku: string;
840
848
  type: string;
841
849
  unitPriceInc: string;
850
+ variant: null | components["schemas"]["OrderLineVariantDto"];
842
851
  variantId: null | string;
843
852
  vatRate: string;
844
853
  };
854
+ OrderLineProductDto: {
855
+ attributes: components["schemas"]["AttributeDto"][];
856
+ brand: null | components["schemas"]["BrandDto"];
857
+ categories: components["schemas"]["CategoryRefDto"][];
858
+ };
859
+ OrderLineVariantDto: {
860
+ brand: null | components["schemas"]["BrandDto"];
861
+ };
845
862
  OrderShippingDto: {
846
863
  carrier: string;
847
864
  deliveryTime: null | components["schemas"]["DeliveryTimeDto"];
@@ -887,8 +904,10 @@ export interface components {
887
904
  vatRate: string;
888
905
  };
889
906
  ProductCardDto: {
907
+ attributes: components["schemas"]["AttributeDto"][];
890
908
  brand: null | components["schemas"]["BrandDto"];
891
909
  buyable: boolean;
910
+ categories: components["schemas"]["CategoryRefDto"][];
892
911
  collections: string[];
893
912
  id: string;
894
913
  image: null | components["schemas"]["MediaDescriptorDto"];