@wocart/storefront 1.9.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,18 @@ 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
+
8
20
  ## 1.9.0 — 2026-10-17
9
21
 
10
22
  ### Added
package/USAGE.md CHANGED
@@ -223,6 +223,7 @@ server directly.
223
223
  | `productImageSource` | Which photo represents a product (cover → gallery fallback) |
224
224
  | `getProductView`, `getProductListView` | Pure price, discount, default-variant, and stock values for detail and list projections |
225
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 |
226
227
  | `META_CURRENCY`, `buildMetaProductPayload`, `buildMetaViewContentPayload`, `buildMetaAddToCartPayload`, `buildMetaInitiateCheckoutPayload` | Browser-safe Meta event payloads with the platform currency and catalog shape |
227
228
  | `createMetaPixelTracker` | Injected browser-pixel dispatch; the theme supplies the vendor global and keeps bootstrap/timing |
228
229
  | `createBasketStore`, `createLocalStorageAdapter`, `createMemoryStorageAdapter`, `DEFAULT_BASKET_STORAGE_KEY` | The browser basket, its storage, and the key it persists under (`cod_cart_v1`) |
@@ -625,8 +626,26 @@ if (!product) {
625
626
  ~~~
626
627
 
627
628
  Product detail includes the product fields, parsed variant options and tags,
628
- the category, variants, images, review statistics, and offers. The server
629
- 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.
630
649
 
631
650
  ### Product lists
632
651
 
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/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;
@@ -629,6 +654,16 @@ export declare const LandingPageSchema: z.ZodObject<{
629
654
  avgRating: z.ZodNumber;
630
655
  reviewCount: z.ZodNumber;
631
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>>>;
632
667
  offers: z.ZodArray<z.ZodObject<{
633
668
  id: z.ZodString;
634
669
  name: z.ZodString;
@@ -677,6 +712,7 @@ export declare const CartValidationLineSchema: z.ZodObject<{
677
712
  quantity: z.ZodNumber;
678
713
  unitPrice: z.ZodNumber;
679
714
  lineTotal: z.ZodNumber;
715
+ offerId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
680
716
  maxQuantity: z.ZodNullable<z.ZodNumber>;
681
717
  blocker: z.ZodNullable<z.ZodEnum<{
682
718
  missing: "missing";
@@ -699,6 +735,7 @@ export declare const CartValidationSchema: z.ZodObject<{
699
735
  quantity: z.ZodNumber;
700
736
  unitPrice: z.ZodNumber;
701
737
  lineTotal: z.ZodNumber;
738
+ offerId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
702
739
  maxQuantity: z.ZodNullable<z.ZodNumber>;
703
740
  blocker: z.ZodNullable<z.ZodEnum<{
704
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({
@@ -331,6 +351,8 @@ export const CartValidationLineSchema = z.object({
331
351
  quantity: z.number().int(),
332
352
  unitPrice: z.number(),
333
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(),
334
356
  maxQuantity: z.number().nullable(),
335
357
  blocker: z.enum(["missing", "unlisted", "out_of_stock"]).nullable(),
336
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
@@ -307,7 +310,10 @@ export interface CartValidationLine {
307
310
  quantity: number;
308
311
  /** CURRENT catalog price — the cart's optimistic price is display-only. */
309
312
  unitPrice: number;
313
+ /** What the line costs: unit price × quantity, or its quantity-pricing tier's price. */
310
314
  lineTotal: number;
315
+ /** The quantity-pricing offer that set `lineTotal`; null or absent at the catalog price. */
316
+ offerId?: string | null | undefined;
311
317
  /** Units still available; null when the product is not stock-tracked. */
312
318
  maxQuantity: number | null;
313
319
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "1.9.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",