@wocart/storefront 1.8.0 → 1.10.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/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ follows [semantic versioning](https://semver.org/): breaking changes ship only
5
5
  in a new major version, and an export is marked `@deprecated` for at least one
6
6
  minor release before a major removes it.
7
7
 
8
+ ## 1.10.0 — 2026-10-17
9
+
10
+ ### Added
11
+
12
+ - **Quantity pricing.** `ProductDetail.quantityPricing` (`QuantityPricing`,
13
+ `QuantityPricingSchema`): a product's price tiers by quantity, each with an
14
+ optional free delivery — null or absent when it has none.
15
+ `getQuantityTierViews(pricing, unitPrice)` projects them for tiles (price,
16
+ per-unit price, saving); the total is the server's cart quote.
17
+ - `CartValidationLine.offerId`: the quantity-pricing offer that priced the
18
+ line, whose `lineTotal` is then the tier's price. Optional.
19
+
20
+ ## 1.9.0 — 2026-10-17
21
+
22
+ ### Added
23
+
24
+ - Page type `collection` at `/collections/:handle`: a collection's page,
25
+ which a merchant's menu can now link to. `storefrontPath("collection", {
26
+ handle })` builds it, `matchStorefrontPath` recognises it, and the WhatsApp
27
+ widget counts it as the catalog surface.
28
+ - `store.collections.get(handle)` → `StoreCollection` (handle, title,
29
+ description, in the client's language), `null` for an unknown handle, which
30
+ is not reported as a failure. `StoreCollectionSchema`.
31
+
8
32
  ## 1.8.0 — 2026-10-17
9
33
 
10
34
  ### Added
package/USAGE.md CHANGED
@@ -105,6 +105,7 @@ break those links.
105
105
  | `home` | `/` | the home page | required |
106
106
  | `catalog` | `/products` | every product | required |
107
107
  | `category` | `/category/:slug` | one category's products | required when the store has categories |
108
+ | `collection` | `/collections/:handle` | one collection's products, in the merchant's order | required — a menu can link to any collection |
108
109
  | `product` | `/products/:handle` | one product (its public handle, never an id) | required |
109
110
  | `page` | `/pages/:slug` | a legal or merchant page | required — checkout consent links to the store's legal pages |
110
111
  | `landing` | `/lp/:slug` | a landing page | required when the store runs landing pages |
@@ -222,6 +223,7 @@ server directly.
222
223
  | `productImageSource` | Which photo represents a product (cover → gallery fallback) |
223
224
  | `getProductView`, `getProductListView` | Pure price, discount, default-variant, and stock values for detail and list projections |
224
225
  | `getOfferTierView` | Pure offer-tier price, compare-price, savings, and free-shipping projection; display-only, never order authority |
226
+ | `getQuantityTierViews` | A product's quantity-pricing tiers as tiles show them — price, per-unit price, saving against the unit price, free delivery; display-only, the total is the server's cart quote |
225
227
  | `META_CURRENCY`, `buildMetaProductPayload`, `buildMetaViewContentPayload`, `buildMetaAddToCartPayload`, `buildMetaInitiateCheckoutPayload` | Browser-safe Meta event payloads with the platform currency and catalog shape |
226
228
  | `createMetaPixelTracker` | Injected browser-pixel dispatch; the theme supplies the vendor global and keeps bootstrap/timing |
227
229
  | `createBasketStore`, `createLocalStorageAdapter`, `createMemoryStorageAdapter`, `DEFAULT_BASKET_STORAGE_KEY` | The browser basket, its storage, and the key it persists under (`cod_cart_v1`) |
@@ -624,8 +626,26 @@ if (!product) {
624
626
  ~~~
625
627
 
626
628
  Product detail includes the product fields, parsed variant options and tags,
627
- the category, variants, images, review statistics, and offers. The server
628
- decides whether a product is publicly available.
629
+ the category, variants, images, review statistics, quantity pricing, and
630
+ offers. The server decides whether a product is publicly available.
631
+
632
+ **Quantity pricing** (`product.quantityPricing`, null when the product has
633
+ none) prices a product by how many are bought — "1 for 2,000 DA, 2 for 3,500
634
+ DA, 3 for 4,500 DA". The highest tier the quantity reaches prices every unit
635
+ at its per-unit price, and a tier may include free delivery for a basket of
636
+ that product alone. A product has at most one offer per order, so it has
637
+ either quantity pricing or offers in `offers`, never both at once.
638
+
639
+ ~~~ts
640
+ const tiles = product.quantityPricing ? getQuantityTierViews(product.quantityPricing, product.price) : [];
641
+ // tiles: [{ quantity, price, perUnit, savingsAmount, savePct, freeShipping }]
642
+ ~~~
643
+
644
+ Show the tiers as choices; never compute the total yourself. The cart quote
645
+ (`POST /api/cart/validate`) answers what the shopper pays — each line's
646
+ `lineTotal` already priced by its tier (`offerId` names the offer), and the
647
+ delivery — and the order charges exactly that. A tier locked to one variant
648
+ (`triggerVariantId`) counts and prices only that variant.
629
649
 
630
650
  ### Product lists
631
651
 
@@ -651,6 +671,11 @@ const onSale = await store.products.list({ onSale: true, limit: 8 });
651
671
  - An empty or unknown collection lists nothing — show nothing, never the
652
672
  whole catalog instead: "Best sellers" means the merchant's picks.
653
673
  - `onSale` is computed: a product priced below its compare-at price.
674
+ - A collection has a page of its own (page type `collection`,
675
+ `/collections/:handle`) that a merchant's menu can link to. Name it with
676
+ `store.collections.get(handle)` — title and description in the client's
677
+ language, `null` for an unknown handle (answer a real 404) — and list it
678
+ with `products.list({ collection: handle })`.
654
679
  - `featured: true` and `ProductList.storeFeatured` are deprecated: they mean
655
680
  the `featured` collection.
656
681
 
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Collections Namespace - the merchant's curated product lists
3
+ *
4
+ * A collection has a page of its own (`/collections/:handle`, page type
5
+ * `collection`): `get(handle)` names it — title and description in the
6
+ * client's language — and `products.list({ collection })` lists its products
7
+ * in the merchant's order.
8
+ *
9
+ * Failure contract: `get(handle)` → null means "no such collection" or a
10
+ * failed read — the page answers a real 404.
11
+ */
12
+ import { type ClientCore } from "./core.js";
13
+ import type { StoreCollection } from "../types.js";
14
+ export interface CollectionsNamespace {
15
+ /** A collection by handle (`best-sellers`). Resolves to `null` for an unknown handle or a failed read. */
16
+ get(handle: string): Promise<StoreCollection | null>;
17
+ }
18
+ export declare function createCollectionsNamespace(core: ClientCore): CollectionsNamespace;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Collections Namespace - the merchant's curated product lists
3
+ *
4
+ * A collection has a page of its own (`/collections/:handle`, page type
5
+ * `collection`): `get(handle)` names it — title and description in the
6
+ * client's language — and `products.list({ collection })` lists its products
7
+ * in the merchant's order.
8
+ *
9
+ * Failure contract: `get(handle)` → null means "no such collection" or a
10
+ * failed read — the page answers a real 404.
11
+ */
12
+ import { StoreCollectionSchema } from "../schemas.js";
13
+ import { STOREFRONT_OPERATIONS } from "../domain.js";
14
+ import { localizedPath } from "./paths.js";
15
+ import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
16
+ export function createCollectionsNamespace(core) {
17
+ return {
18
+ async get(handle) {
19
+ try {
20
+ return await core.get(localizedPath(core.config, `/store/collections/${encodeURIComponent(handle)}`), StoreCollectionSchema, { operation: STOREFRONT_OPERATIONS.collectionsGet });
21
+ }
22
+ catch (error) {
23
+ if (!isNotFoundAnswer(error))
24
+ reportDegradedRead(core.config, error, "collections.get");
25
+ return null;
26
+ }
27
+ },
28
+ };
29
+ }
@@ -2,7 +2,8 @@
2
2
  * Store Client Factory — the SDK's front door.
3
3
  *
4
4
  * One server-side call builds the whole read/write surface a theme needs, over one
5
- * shared core: config, products, categories, pages, menus, shipping, geo, cart,
5
+ * shared core: config, products, categories, collections, pages, menus,
6
+ * shipping, geo, cart,
6
7
  * orders. The core is internal — consumers see only namespaces.
7
8
  *
8
9
  * One core means one connection story for every namespace: every read is
@@ -32,6 +33,7 @@ export declare function createStoreClient(config: StoreScope): {
32
33
  categories: import("./categories.js").CategoriesNamespace;
33
34
  pages: import("./pages.js").PagesNamespace;
34
35
  menus: import("./menus.js").MenusNamespace;
36
+ collections: import("./collections.js").CollectionsNamespace;
35
37
  pageData: import("./page-data.js").PageDataNamespace;
36
38
  shipping: import("./shipping.js").ShippingNamespace;
37
39
  geo: import("./geo.js").GeoNamespace;
@@ -2,7 +2,8 @@
2
2
  * Store Client Factory — the SDK's front door.
3
3
  *
4
4
  * One server-side call builds the whole read/write surface a theme needs, over one
5
- * shared core: config, products, categories, pages, menus, shipping, geo, cart,
5
+ * shared core: config, products, categories, collections, pages, menus,
6
+ * shipping, geo, cart,
6
7
  * orders. The core is internal — consumers see only namespaces.
7
8
  *
8
9
  * One core means one connection story for every namespace: every read is
@@ -32,6 +33,7 @@ import { createProductsNamespace } from "./products.js";
32
33
  import { createCategoriesNamespace } from "./categories.js";
33
34
  import { createPagesNamespace } from "./pages.js";
34
35
  import { createMenusNamespace } from "./menus.js";
36
+ import { createCollectionsNamespace } from "./collections.js";
35
37
  import { createPageDataNamespace } from "./page-data.js";
36
38
  import { createShippingNamespace } from "./shipping.js";
37
39
  import { createGeoNamespace } from "./geo.js";
@@ -46,6 +48,7 @@ export function createStoreClient(config) {
46
48
  categories: createCategoriesNamespace(core),
47
49
  pages: createPagesNamespace(core),
48
50
  menus: createMenusNamespace(core),
51
+ collections: createCollectionsNamespace(core),
49
52
  pageData: createPageDataNamespace(core),
50
53
  shipping: createShippingNamespace(core),
51
54
  geo: createGeoNamespace(core),
package/dist/domain.d.ts CHANGED
@@ -354,6 +354,7 @@ export declare const STOREFRONT_OPERATIONS: {
354
354
  readonly pagesGet: "pages.get";
355
355
  readonly pagesLanding: "pages.landing";
356
356
  readonly menusGet: "menus.get";
357
+ readonly collectionsGet: "collections.get";
357
358
  readonly shippingRates: "shipping.rates";
358
359
  readonly shippingRate: "shipping.rate";
359
360
  readonly geoCommunes: "geo.communes";
@@ -392,6 +393,7 @@ export declare const STOREFRONT_RETRY_POLICIES: {
392
393
  readonly "pages.get": "safe-read";
393
394
  readonly "pages.landing": "safe-read";
394
395
  readonly "menus.get": "safe-read";
396
+ readonly "collections.get": "safe-read";
395
397
  readonly "products.get": "safe-read";
396
398
  readonly "products.list": "safe-read";
397
399
  readonly "products.reviews.list": "safe-read";
package/dist/domain.js CHANGED
@@ -207,6 +207,7 @@ export const STOREFRONT_OPERATIONS = {
207
207
  pagesGet: "pages.get",
208
208
  pagesLanding: "pages.landing",
209
209
  menusGet: "menus.get",
210
+ collectionsGet: "collections.get",
210
211
  shippingRates: "shipping.rates",
211
212
  shippingRate: "shipping.rate",
212
213
  geoCommunes: "geo.communes",
@@ -244,6 +245,7 @@ export const STOREFRONT_RETRY_POLICIES = {
244
245
  [STOREFRONT_OPERATIONS.pagesGet]: "safe-read",
245
246
  [STOREFRONT_OPERATIONS.pagesLanding]: "safe-read",
246
247
  [STOREFRONT_OPERATIONS.menusGet]: "safe-read",
248
+ [STOREFRONT_OPERATIONS.collectionsGet]: "safe-read",
247
249
  [STOREFRONT_OPERATIONS.productsGet]: "safe-read",
248
250
  [STOREFRONT_OPERATIONS.productsList]: "safe-read",
249
251
  [STOREFRONT_OPERATIONS.productsReviewsList]: "safe-read",
package/dist/index.d.ts CHANGED
@@ -34,8 +34,8 @@ export * from "./types.js";
34
34
  export * from "./schemas.js";
35
35
  export * from "./domain.js";
36
36
  export * from "./product-view.js";
37
- export { getOfferTierView } from "./offer-view.js";
38
- export type { OfferTierInput, OfferTierView } from "./offer-view.js";
37
+ export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
38
+ export type { OfferTierInput, OfferTierView, QuantityTierView } from "./offer-view.js";
39
39
  export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
40
40
  export type { AbandonedCheckoutPorts, AbandonedCheckoutSnapshot, AbandonedCheckoutStorage, AbandonedCheckoutTracker, } from "./abandoned-checkout.js";
41
41
  export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
package/dist/index.js CHANGED
@@ -25,6 +25,6 @@ export * from "./types.js";
25
25
  export * from "./schemas.js";
26
26
  export * from "./domain.js";
27
27
  export * from "./product-view.js";
28
- export { getOfferTierView } from "./offer-view.js";
28
+ export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
29
29
  export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
30
30
  export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
@@ -1,4 +1,4 @@
1
- import type { Offer } from "./types.js";
1
+ import type { Offer, QuantityPricing } from "./types.js";
2
2
  export interface OfferTierView {
3
3
  isFreeShipping: boolean;
4
4
  savePct: number;
@@ -8,3 +8,20 @@ export interface OfferTierView {
8
8
  }
9
9
  export type OfferTierInput = Pick<Offer, "discountType" | "triggerQuantity" | "rewardQuantity" | "rewardUnitPrice">;
10
10
  export declare function getOfferTierView(offer: OfferTierInput, basePrice: number, comparePrice: number | null): OfferTierView;
11
+ export interface QuantityTierView {
12
+ quantity: number;
13
+ /** What that many units cost. */
14
+ price: number;
15
+ /** What one unit costs within the tier, rounded to the dinar. */
16
+ perUnit: number;
17
+ /** What the tier saves against buying that many at `unitPrice`; 0 when it saves nothing. */
18
+ savingsAmount: number;
19
+ savePct: number;
20
+ freeShipping: boolean;
21
+ }
22
+ /**
23
+ * The tiers of a product's quantity pricing as tiles show them: price, per-unit
24
+ * price and saving against the product's unit price. Presentation only — the
25
+ * total for a quantity is the server's cart quote.
26
+ */
27
+ export declare function getQuantityTierViews(pricing: Pick<QuantityPricing, "tiers">, unitPrice: number): QuantityTierView[];
@@ -15,3 +15,24 @@ export function getOfferTierView(offer, basePrice, comparePrice) {
15
15
  comparePrice: comparePrice === null ? null : comparePrice * offer.triggerQuantity,
16
16
  };
17
17
  }
18
+ /**
19
+ * The tiers of a product's quantity pricing as tiles show them: price, per-unit
20
+ * price and saving against the product's unit price. Presentation only — the
21
+ * total for a quantity is the server's cart quote.
22
+ */
23
+ export function getQuantityTierViews(pricing, unitPrice) {
24
+ return [...pricing.tiers]
25
+ .sort((a, b) => a.quantity - b.quantity)
26
+ .map((tier) => {
27
+ const listValue = unitPrice * tier.quantity;
28
+ const savingsAmount = Math.max(0, listValue - tier.price);
29
+ return {
30
+ quantity: tier.quantity,
31
+ price: tier.price,
32
+ perUnit: Math.round(tier.price / tier.quantity),
33
+ savingsAmount,
34
+ savePct: listValue > 0 ? Math.round((savingsAmount / listValue) * 100) : 0,
35
+ freeShipping: tier.freeShipping,
36
+ };
37
+ });
38
+ }
package/dist/routes.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare const STOREFRONT_PAGE_PATHS: {
20
20
  readonly home: "/";
21
21
  readonly catalog: "/products";
22
22
  readonly category: "/category/:slug";
23
+ readonly collection: "/collections/:handle";
23
24
  readonly product: "/products/:handle";
24
25
  readonly page: "/pages/:slug";
25
26
  readonly landing: "/lp/:slug";
package/dist/routes.js CHANGED
@@ -3,6 +3,7 @@ export const STOREFRONT_PAGE_PATHS = {
3
3
  home: "/",
4
4
  catalog: "/products",
5
5
  category: "/category/:slug",
6
+ collection: "/collections/:handle",
6
7
  product: "/products/:handle",
7
8
  page: "/pages/:slug",
8
9
  landing: "/lp/:slug",
@@ -58,6 +59,7 @@ const WIDGET_SURFACE = {
58
59
  home: "home",
59
60
  catalog: "catalog",
60
61
  category: "catalog",
62
+ collection: "catalog",
61
63
  product: "product",
62
64
  page: "pages",
63
65
  landing: "landing",
package/dist/schemas.d.ts CHANGED
@@ -43,6 +43,21 @@ export declare const OfferSchema: z.ZodObject<{
43
43
  rewardVariantLabel: z.ZodNullable<z.ZodString>;
44
44
  rewardUnitPrice: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
45
45
  }, z.core.$strip>;
46
+ /**
47
+ * Quantity pricing — price tiers by quantity ("2 for 3,500 DA"). The highest
48
+ * tier the quantity reaches prices every unit at its per-unit price; the
49
+ * server's cart quote is the total.
50
+ */
51
+ export declare const QuantityPricingSchema: z.ZodObject<{
52
+ offerId: z.ZodString;
53
+ name: z.ZodString;
54
+ triggerVariantId: z.ZodNullable<z.ZodString>;
55
+ tiers: z.ZodArray<z.ZodObject<{
56
+ quantity: z.ZodNumber;
57
+ price: z.ZodNumber;
58
+ freeShipping: z.ZodBoolean;
59
+ }, z.core.$strip>>;
60
+ }, z.core.$strip>;
46
61
  export declare const CategorySchema: z.ZodObject<{
47
62
  id: z.ZodString;
48
63
  name: z.ZodString;
@@ -201,6 +216,16 @@ export declare const ProductSchema: z.ZodObject<{
201
216
  avgRating: z.ZodNumber;
202
217
  reviewCount: z.ZodNumber;
203
218
  }, z.core.$strip>>;
219
+ quantityPricing: z.ZodOptional<z.ZodNullable<z.ZodObject<{
220
+ offerId: z.ZodString;
221
+ name: z.ZodString;
222
+ triggerVariantId: z.ZodNullable<z.ZodString>;
223
+ tiers: z.ZodArray<z.ZodObject<{
224
+ quantity: z.ZodNumber;
225
+ price: z.ZodNumber;
226
+ freeShipping: z.ZodBoolean;
227
+ }, z.core.$strip>>;
228
+ }, z.core.$strip>>>;
204
229
  offers: z.ZodArray<z.ZodObject<{
205
230
  id: z.ZodString;
206
231
  name: z.ZodString;
@@ -246,6 +271,12 @@ interface StoreMenuItemShape {
246
271
  type: string;
247
272
  children: StoreMenuItemShape[];
248
273
  }
274
+ /** A collection's page — GET /store/collections/{handle}. Its products come from `products.list({ collection })`. */
275
+ export declare const StoreCollectionSchema: z.ZodObject<{
276
+ handle: z.ZodString;
277
+ title: z.ZodString;
278
+ description: z.ZodNullable<z.ZodString>;
279
+ }, z.core.$strip>;
249
280
  export declare const StoreMenuSchema: z.ZodObject<{
250
281
  handle: z.ZodString;
251
282
  title: z.ZodString;
@@ -623,6 +654,16 @@ export declare const LandingPageSchema: z.ZodObject<{
623
654
  avgRating: z.ZodNumber;
624
655
  reviewCount: z.ZodNumber;
625
656
  }, z.core.$strip>>;
657
+ quantityPricing: z.ZodOptional<z.ZodNullable<z.ZodObject<{
658
+ offerId: z.ZodString;
659
+ name: z.ZodString;
660
+ triggerVariantId: z.ZodNullable<z.ZodString>;
661
+ tiers: z.ZodArray<z.ZodObject<{
662
+ quantity: z.ZodNumber;
663
+ price: z.ZodNumber;
664
+ freeShipping: z.ZodBoolean;
665
+ }, z.core.$strip>>;
666
+ }, z.core.$strip>>>;
626
667
  offers: z.ZodArray<z.ZodObject<{
627
668
  id: z.ZodString;
628
669
  name: z.ZodString;
@@ -671,6 +712,7 @@ export declare const CartValidationLineSchema: z.ZodObject<{
671
712
  quantity: z.ZodNumber;
672
713
  unitPrice: z.ZodNumber;
673
714
  lineTotal: z.ZodNumber;
715
+ offerId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
674
716
  maxQuantity: z.ZodNullable<z.ZodNumber>;
675
717
  blocker: z.ZodNullable<z.ZodEnum<{
676
718
  missing: "missing";
@@ -693,6 +735,7 @@ export declare const CartValidationSchema: z.ZodObject<{
693
735
  quantity: z.ZodNumber;
694
736
  unitPrice: z.ZodNumber;
695
737
  lineTotal: z.ZodNumber;
738
+ offerId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
696
739
  maxQuantity: z.ZodNullable<z.ZodNumber>;
697
740
  blocker: z.ZodNullable<z.ZodEnum<{
698
741
  missing: "missing";
package/dist/schemas.js CHANGED
@@ -42,6 +42,21 @@ export const OfferSchema = z.object({
42
42
  // it omits the field, and the product's own price stands in for the gift.
43
43
  rewardUnitPrice: z.number().nullish(),
44
44
  });
45
+ /**
46
+ * Quantity pricing — price tiers by quantity ("2 for 3,500 DA"). The highest
47
+ * tier the quantity reaches prices every unit at its per-unit price; the
48
+ * server's cart quote is the total.
49
+ */
50
+ export const QuantityPricingSchema = z.object({
51
+ offerId: z.string(),
52
+ name: z.string(),
53
+ triggerVariantId: z.string().nullable(),
54
+ tiers: z.array(z.object({
55
+ quantity: z.number().int(),
56
+ price: z.number().int(),
57
+ freeShipping: z.boolean(),
58
+ })),
59
+ });
45
60
  const productCategorySchema = z.object({
46
61
  id: z.string(),
47
62
  name: z.string(),
@@ -122,6 +137,11 @@ export const ProductSchema = z.object({
122
137
  variants: z.array(ProductVariantSchema),
123
138
  images: z.array(ProductImageSchema),
124
139
  reviewStats: reviewStatsSchema.nullable(),
140
+ /**
141
+ * The product's live quantity pricing; null or absent when it has none
142
+ * (absent from a server that predates it).
143
+ */
144
+ quantityPricing: QuantityPricingSchema.nullish(),
125
145
  offers: z.array(OfferSchema),
126
146
  });
127
147
  export const StorePagePublicSchema = z.object({
@@ -145,6 +165,12 @@ export const StoreMenuItemSchema = z.lazy(() => z.object({
145
165
  type: z.string(),
146
166
  children: z.array(StoreMenuItemSchema),
147
167
  }));
168
+ /** A collection's page — GET /store/collections/{handle}. Its products come from `products.list({ collection })`. */
169
+ export const StoreCollectionSchema = z.object({
170
+ handle: z.string(),
171
+ title: z.string(),
172
+ description: z.string().nullable(),
173
+ });
148
174
  export const StoreMenuSchema = z.object({
149
175
  handle: z.string(),
150
176
  title: z.string(),
@@ -325,6 +351,8 @@ export const CartValidationLineSchema = z.object({
325
351
  quantity: z.number().int(),
326
352
  unitPrice: z.number(),
327
353
  lineTotal: z.number(),
354
+ /** The quantity-pricing offer that set `lineTotal`; absent from a server that predates offers on lines. */
355
+ offerId: z.string().nullish(),
328
356
  maxQuantity: z.number().nullable(),
329
357
  blocker: z.enum(["missing", "unlisted", "out_of_stock"]).nullable(),
330
358
  });
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { ProductSchema, ProductListSchema, CategorySchema, ProductImageSchema, ProductVariantSchema, OfferSchema, PageDocumentPayloadSchema } from "./schemas.js";
2
+ import { ProductSchema, ProductListSchema, CategorySchema, ProductImageSchema, ProductVariantSchema, OfferSchema, QuantityPricingSchema, PageDocumentPayloadSchema } from "./schemas.js";
3
3
  export type Product = z.infer<typeof ProductSchema>;
4
4
  export type ProductList = z.infer<typeof ProductListSchema>;
5
5
  /** Compact catalog projection used in grids, cards and search results. */
@@ -10,6 +10,9 @@ export type Category = z.infer<typeof CategorySchema>;
10
10
  export type ProductImage = z.infer<typeof ProductImageSchema>;
11
11
  export type ProductVariant = z.infer<typeof ProductVariantSchema>;
12
12
  export type Offer = z.infer<typeof OfferSchema>;
13
+ /** A product's quantity pricing — `ProductDetail.quantityPricing`. */
14
+ export type QuantityPricing = z.infer<typeof QuantityPricingSchema>;
15
+ export type QuantityPricingTier = QuantityPricing["tiers"][number];
13
16
  /**
14
17
  * One store's saved composition document for one surface. Transport shape
15
18
  * only — `sections` items are `unknown`; the theme
@@ -212,6 +215,13 @@ export interface StorePagePublic {
212
215
  metaTitle: string | null;
213
216
  metaDescription: string | null;
214
217
  }
218
+ /** A collection as its page names it — GET /store/collections/{handle}. */
219
+ export interface StoreCollection {
220
+ handle: string;
221
+ /** In the client's language when the store translated it, else the primary. */
222
+ title: string;
223
+ description: string | null;
224
+ }
215
225
  /** A navigation menu — GET /store/menus/{handle}. */
216
226
  export interface StoreMenu {
217
227
  handle: string;
@@ -300,7 +310,10 @@ export interface CartValidationLine {
300
310
  quantity: number;
301
311
  /** CURRENT catalog price — the cart's optimistic price is display-only. */
302
312
  unitPrice: number;
313
+ /** What the line costs: unit price × quantity, or its quantity-pricing tier's price. */
303
314
  lineTotal: number;
315
+ /** The quantity-pricing offer that set `lineTotal`; null or absent at the catalog price. */
316
+ offerId?: string | null | undefined;
304
317
  /** Units still available; null when the product is not stock-tracked. */
305
318
  maxQuantity: number | null;
306
319
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Typed, validated SDK for Wocart cash-on-delivery storefronts: catalog reads, basket, checkout, OTP and order tracking, with Astro adapters.",
5
5
  "keywords": [
6
6
  "wocart",