@wocart/storefront 1.2.0 → 1.4.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,35 @@ 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.4.0 — 2026-10-15
9
+
10
+ ### Added
11
+
12
+ - **`cart.quote(lines, { destination })`** quotes what an order would cost for
13
+ a basket, or for one product exactly as its order form posts it: a quantity,
14
+ a variant per unit (`variantSelections`), an offer tier (`offerId`). The
15
+ server builds the lines the way the order does, so a product page's delivery
16
+ and total are the order's own. `validate(items, options)` is now its basket
17
+ form.
18
+ - `ProductQuoteInputSchema`, `CartQuoteLinesSchema` and the `ProductQuoteInput`
19
+ and `CartQuoteLines` types.
20
+ - The `cartValidate` route accepts a product form as well as a basket, each
21
+ with an optional `destination`.
22
+
23
+ ## 1.3.0 — 2026-10-15
24
+
25
+ ### Added
26
+
27
+ - **Delivery quotes.** `cart.validate(items, { destination })` answers
28
+ `delivery`: the fee and total the order will charge at that wilaya, commune
29
+ and delivery type — product shipping profiles, commune overrides, the
30
+ free-delivery threshold and free-shipping offers included — or
31
+ `available: false` where the store does not deliver. The server computes the
32
+ quote with the same function it charges orders with. A platform server that
33
+ predates the quote answers without `delivery`.
34
+ - `DeliveryDestinationSchema`, `DeliveryDestination` and `CartDeliveryQuote`.
35
+ - The `cartValidate` route passes a `destination` through.
36
+
8
37
  ## 1.2.0 — 2026-10-15
9
38
 
10
39
  ### Added
package/USAGE.md CHANGED
@@ -745,6 +745,28 @@ type ShippingRate = {
745
745
  If the map is empty or one rate is null, show a useful unavailable state. Do
746
746
  not silently display zero shipping.
747
747
 
748
+ These are the store default profile's wilaya rates — a guide for "delivery
749
+ from X DZD", never the fee an order is charged. The fee also depends on each
750
+ product's own shipping profile, the commune, the free-delivery threshold and
751
+ free-shipping offers. To show what the shopper will pay, quote it:
752
+
753
+ ~~~ts
754
+ const result = await store.cart.validate(items, {
755
+ destination: { wilayaId: 16, communeId: "c-16-001", deliveryType: "home" },
756
+ });
757
+
758
+ if (result.success && result.data.delivery) {
759
+ const quote = result.data.delivery;
760
+ // quote.available → quote.fee and quote.total are what the order charges.
761
+ // !quote.available → the store does not deliver there; an order is refused.
762
+ }
763
+ ~~~
764
+
765
+ The quote comes from the same function the order uses, so the fee and total
766
+ it names are the ones charged. A single-product order form quotes the same
767
+ way, with its one product as the only line. From the browser, post
768
+ `{ items, destination }` to the `cartValidate` route.
769
+
748
770
  ### Wilayas and communes
749
771
 
750
772
  ~~~ts
@@ -1430,8 +1452,9 @@ render null as not found.
1430
1452
  ### The cart total is different after checkout
1431
1453
 
1432
1454
  This is expected when a browser price, offer, stock state, or delivery value
1433
- was stale. Render cart.validate results and treat order creation as the final
1434
- server decision.
1455
+ was stale, or when the theme computed a fee or total itself. Show the totals
1456
+ cart.validate answers — with a destination for the delivery fee — and treat
1457
+ order creation as the final server decision.
1435
1458
 
1436
1459
  ### OTP says unavailable
1437
1460
 
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { ClientCore, ClientResult } from "./core.js";
15
15
  import type { CartValidation } from "../types.js";
16
- import type { BasketLineInput } from "../domain.js";
16
+ import type { BasketLineInput, CartQuoteLines, DeliveryDestination } from "../domain.js";
17
17
  export interface CartNamespace {
18
18
  /**
19
19
  * Re-price and re-check a basket against the live catalog.
@@ -21,8 +21,22 @@ export interface CartNamespace {
21
21
  * optional variant fields); pricing from their side is never trusted, and
22
22
  * each line's product name and variant label come back from the catalog in
23
23
  * the client's language.
24
+ *
25
+ * With a `destination`, the answer also carries `delivery`: the fee and
26
+ * total the order would charge there. A theme shows that, never a fee or
27
+ * total of its own making.
24
28
  */
25
- validate(items: readonly BasketLineInput[]): Promise<ClientResult<CartValidation>>;
29
+ validate(items: readonly BasketLineInput[], options?: {
30
+ destination?: DeliveryDestination;
31
+ }): Promise<ClientResult<CartValidation>>;
32
+ /**
33
+ * Quote what an order would cost, for a basket or for one product exactly as
34
+ * its order form posts it (quantity, variants per unit, offer tier). The lines
35
+ * are built the way the order builds them, so the quote and the order agree.
36
+ */
37
+ quote(lines: CartQuoteLines, options?: {
38
+ destination?: DeliveryDestination;
39
+ }): Promise<ClientResult<CartValidation>>;
26
40
  }
27
41
  /**
28
42
  * Create cart namespace
@@ -18,9 +18,13 @@ import { localizedPath } from "./paths.js";
18
18
  * Create cart namespace
19
19
  */
20
20
  export function createCartNamespace(core) {
21
+ // A closure rather than `this`: a theme may destructure `store.cart`.
22
+ const quote = async (lines, options) => {
23
+ const destination = options?.destination;
24
+ return core.post(localizedPath(core.config, "/store/cart/validate"), destination ? { ...lines, destination } : lines, { schema: CartValidationSchema, operation: STOREFRONT_OPERATIONS.cartValidate });
25
+ };
21
26
  return {
22
- async validate(items) {
23
- return core.post(localizedPath(core.config, "/store/cart/validate"), { items }, { schema: CartValidationSchema, operation: STOREFRONT_OPERATIONS.cartValidate });
24
- },
27
+ validate: (items, options) => quote({ items: [...items] }, options),
28
+ quote,
25
29
  };
26
30
  }
package/dist/domain.d.ts CHANGED
@@ -175,6 +175,62 @@ export declare const AbandonedCheckoutInputSchema: z.ZodObject<{
175
175
  * `items[]` line: absent — not null — is the common wire shape.
176
176
  */
177
177
  export type AbandonedCheckoutInput = z.infer<typeof AbandonedCheckoutInputSchema>;
178
+ /**
179
+ * One product as a product page's order form posts it: a quantity, or a
180
+ * variant chosen per unit, and an offer tier. The quote reads these the way the
181
+ * order does, so the page asks with exactly what its form will submit.
182
+ */
183
+ export declare const ProductQuoteInputSchema: z.ZodObject<{
184
+ productId: z.ZodString;
185
+ productName: z.ZodString;
186
+ quantity: z.ZodOptional<z.ZodNumber>;
187
+ variantId: z.ZodOptional<z.ZodString>;
188
+ variantLabel: z.ZodOptional<z.ZodString>;
189
+ offerId: z.ZodOptional<z.ZodString>;
190
+ variantSelections: z.ZodOptional<z.ZodArray<z.ZodObject<{
191
+ variantId: z.ZodString;
192
+ variantLabel: z.ZodOptional<z.ZodString>;
193
+ }, z.core.$strip>>>;
194
+ }, z.core.$strip>;
195
+ export type ProductQuoteInput = z.infer<typeof ProductQuoteInputSchema>;
196
+ /** What a quote is asked about: a basket, or one product as its form posts it. */
197
+ export declare const CartQuoteLinesSchema: z.ZodUnion<readonly [z.ZodObject<{
198
+ items: z.ZodArray<z.ZodObject<{
199
+ productId: z.ZodString;
200
+ productName: z.ZodString;
201
+ variantId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
202
+ variantLabel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
203
+ quantity: z.ZodNumber;
204
+ pricePerUnit: z.ZodOptional<z.ZodNumber>;
205
+ offerId: z.ZodOptional<z.ZodString>;
206
+ }, z.core.$strip>>;
207
+ }, z.core.$strip>, z.ZodObject<{
208
+ productId: z.ZodString;
209
+ productName: z.ZodString;
210
+ quantity: z.ZodOptional<z.ZodNumber>;
211
+ variantId: z.ZodOptional<z.ZodString>;
212
+ variantLabel: z.ZodOptional<z.ZodString>;
213
+ offerId: z.ZodOptional<z.ZodString>;
214
+ variantSelections: z.ZodOptional<z.ZodArray<z.ZodObject<{
215
+ variantId: z.ZodString;
216
+ variantLabel: z.ZodOptional<z.ZodString>;
217
+ }, z.core.$strip>>>;
218
+ }, z.core.$strip>]>;
219
+ export type CartQuoteLines = z.infer<typeof CartQuoteLinesSchema>;
220
+ /**
221
+ * Where a basket would be delivered, for a delivery quote. The commune is
222
+ * optional — without it the wilaya's rate applies; with it, the commune's own
223
+ * override when the merchant set one.
224
+ */
225
+ export declare const DeliveryDestinationSchema: z.ZodObject<{
226
+ wilayaId: z.ZodNumber;
227
+ communeId: z.ZodOptional<z.ZodString>;
228
+ deliveryType: z.ZodEnum<{
229
+ home: "home";
230
+ stop_desk: "stop_desk";
231
+ }>;
232
+ }, z.core.$strip>;
233
+ export type DeliveryDestination = z.infer<typeof DeliveryDestinationSchema>;
178
234
  /** Shopper's selected delivery mode and destination. Policy decides whether address is required. */
179
235
  export declare const DeliverySelectionSchema: z.ZodObject<{
180
236
  wilayaId: z.ZodNumber;
package/dist/domain.js CHANGED
@@ -126,6 +126,41 @@ export const AbandonedCheckoutInputSchema = z.object({
126
126
  fbc: z.string().max(500).optional(),
127
127
  fbp: z.string().max(500).optional(),
128
128
  });
129
+ /**
130
+ * One product as a product page's order form posts it: a quantity, or a
131
+ * variant chosen per unit, and an offer tier. The quote reads these the way the
132
+ * order does, so the page asks with exactly what its form will submit.
133
+ */
134
+ export const ProductQuoteInputSchema = z.object({
135
+ productId: z.string().min(1).max(200),
136
+ productName: z.string().min(1).max(200),
137
+ quantity: z.number().int().min(1).max(MAX_BASKET_LINE_QUANTITY).optional(),
138
+ variantId: z.string().min(1).max(200).optional(),
139
+ variantLabel: z.string().max(200).optional(),
140
+ offerId: z.string().min(1).max(200).optional(),
141
+ variantSelections: z
142
+ .array(z.object({
143
+ variantId: z.string().min(1).max(200),
144
+ variantLabel: z.string().max(200).optional(),
145
+ }))
146
+ .max(MAX_BASKET_LINE_QUANTITY)
147
+ .optional(),
148
+ });
149
+ /** What a quote is asked about: a basket, or one product as its form posts it. */
150
+ export const CartQuoteLinesSchema = z.union([
151
+ z.object({ items: z.array(BasketLineInputSchema).min(1).max(MAX_BASKET_LINES) }),
152
+ ProductQuoteInputSchema,
153
+ ]);
154
+ /**
155
+ * Where a basket would be delivered, for a delivery quote. The commune is
156
+ * optional — without it the wilaya's rate applies; with it, the commune's own
157
+ * override when the merchant set one.
158
+ */
159
+ export const DeliveryDestinationSchema = z.object({
160
+ wilayaId: WilayaIdSchema,
161
+ communeId: z.string().min(1).max(200).optional(),
162
+ deliveryType: z.enum(["home", "stop_desk"]),
163
+ });
129
164
  /** Shopper's selected delivery mode and destination. Policy decides whether address is required. */
130
165
  export const DeliverySelectionSchema = z.object({
131
166
  wilayaId: z.number().int().min(1).max(58),
@@ -32,7 +32,12 @@ export interface StorefrontRoutes<C extends AstroAPIContext> {
32
32
  readonly otpSend: StorefrontApiRoute<C>;
33
33
  /** POST `{ phone, requestId, code }` → `{ data: OtpVerifyResult }`. */
34
34
  readonly otpVerify: StorefrontApiRoute<C>;
35
- /** POST `{ items }` → `{ success: true, data: CartValidation }`. */
35
+ /**
36
+ * POST a basket `{ items }` or one product `{ productId, productName, quantity,
37
+ * variantSelections, offerId, … }`, each with an optional `destination` →
38
+ * `{ success: true, data: CartValidation }`.
39
+ * With a destination, `data.delivery` is the fee and total the order will charge.
40
+ */
36
41
  readonly cartValidate: StorefrontApiRoute<C>;
37
42
  /** POST an `AbandonedCheckoutInput` → 204. */
38
43
  readonly abandonedUpsert: StorefrontApiRoute<C>;
@@ -21,7 +21,7 @@
21
21
  * (see `createProxyEndpoint`), and the server's diagnostic fields ride along.
22
22
  */
23
23
  import { z } from "zod";
24
- import { AbandonedCheckoutInputSchema, BasketLineInputSchema, MAX_BASKET_LINES, WilayaIdSchema, } from "../domain.js";
24
+ import { AbandonedCheckoutInputSchema, BasketLineInputSchema, DeliveryDestinationSchema, ProductQuoteInputSchema, MAX_BASKET_LINES, WilayaIdSchema, } from "../domain.js";
25
25
  import { createProxyEndpoint, } from "./proxy.js";
26
26
  export const REVIEWS_PAGE_DEFAULT = 20;
27
27
  export const REVIEWS_PAGE_MAX = 50;
@@ -31,9 +31,17 @@ const otpVerifyInput = z.object({
31
31
  requestId: z.string().min(1).max(200),
32
32
  code: z.string().min(1).max(16),
33
33
  });
34
- const cartValidateInput = z.object({
35
- items: z.array(BasketLineInputSchema).min(1).max(MAX_BASKET_LINES),
36
- });
34
+ /**
35
+ * A basket, or one product as its order form posts it — the same two shapes the
36
+ * server's quote takes — each with an optional destination to quote to.
37
+ */
38
+ const cartValidateInput = z.union([
39
+ z.object({
40
+ items: z.array(BasketLineInputSchema).min(1).max(MAX_BASKET_LINES),
41
+ destination: DeliveryDestinationSchema.optional(),
42
+ }),
43
+ ProductQuoteInputSchema.extend({ destination: DeliveryDestinationSchema.optional() }),
44
+ ]);
37
45
  const abandonedConvertInput = z.object({
38
46
  sessionId: z.string().uuid(),
39
47
  orderId: z.string().min(1).max(200),
@@ -87,7 +95,7 @@ export function createStorefrontRoutes(options) {
87
95
  method: "POST",
88
96
  inputSchema: cartValidateInput,
89
97
  successBody: (data) => ({ success: true, data }),
90
- call: (client, { items }) => client.cart.validate(items),
98
+ call: (client, { destination, ...lines }) => client.cart.quote(lines, destination ? { destination } : undefined),
91
99
  }),
92
100
  abandonedUpsert: route({
93
101
  method: "POST",
package/dist/schemas.d.ts CHANGED
@@ -692,6 +692,15 @@ export declare const CartValidationSchema: z.ZodObject<{
692
692
  qualified: z.ZodBoolean;
693
693
  remaining: z.ZodNumber;
694
694
  }, z.core.$strip>;
695
+ delivery: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
696
+ available: z.ZodLiteral<true>;
697
+ fee: z.ZodNumber;
698
+ total: z.ZodNumber;
699
+ }, z.core.$strip>, z.ZodObject<{
700
+ available: z.ZodLiteral<false>;
701
+ fee: z.ZodNull;
702
+ total: z.ZodNull;
703
+ }, z.core.$strip>], "available">>;
695
704
  }, z.core.$strip>;
696
705
  export declare const CreatedOrderSchema: z.ZodObject<{
697
706
  orderId: z.ZodString;
package/dist/schemas.js CHANGED
@@ -314,6 +314,12 @@ export const CartValidationSchema = z.object({
314
314
  qualified: z.boolean(),
315
315
  remaining: z.number(),
316
316
  }),
317
+ delivery: z
318
+ .discriminatedUnion("available", [
319
+ z.object({ available: z.literal(true), fee: z.number(), total: z.number() }),
320
+ z.object({ available: z.literal(false), fee: z.null(), total: z.null() }),
321
+ ])
322
+ .optional(),
317
323
  });
318
324
  export const CreatedOrderSchema = z.object({
319
325
  orderId: z.string(),
package/dist/types.d.ts CHANGED
@@ -275,7 +275,24 @@ export interface CartValidation {
275
275
  qualified: boolean;
276
276
  remaining: number;
277
277
  };
278
+ /**
279
+ * Present when `validate` was given a destination: what the order would
280
+ * charge there, from the same quote the order itself uses (shipping
281
+ * profiles, commune overrides, the free-delivery threshold and offers).
282
+ * `available: false` — fee and total null — means the store does not
283
+ * deliver there, and an order to it is refused.
284
+ */
285
+ delivery?: CartDeliveryQuote | undefined;
278
286
  }
287
+ export type CartDeliveryQuote = {
288
+ available: true;
289
+ fee: number;
290
+ total: number;
291
+ } | {
292
+ available: false;
293
+ fee: null;
294
+ total: null;
295
+ };
279
296
  /** What POST /store/orders returns on 201. */
280
297
  export interface CreatedOrder {
281
298
  orderId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "1.2.0",
3
+ "version": "1.4.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",