@wocart/storefront 1.18.0 → 1.20.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/USAGE.md +313 -11
  3. package/dist/abandoned-checkout.d.ts +45 -0
  4. package/dist/abandoned-checkout.js +81 -3
  5. package/dist/answer-readers.d.ts +23 -0
  6. package/dist/answer-readers.js +155 -0
  7. package/dist/astro.d.ts +2 -0
  8. package/dist/astro.js +1 -0
  9. package/dist/basket-line.d.ts +15 -0
  10. package/dist/basket-line.js +17 -0
  11. package/dist/basket.d.ts +12 -2
  12. package/dist/basket.js +18 -1
  13. package/dist/catalog.d.ts +18 -0
  14. package/dist/catalog.js +27 -0
  15. package/dist/client/cart.js +1 -1
  16. package/dist/client/categories.js +1 -1
  17. package/dist/client/collections.js +1 -1
  18. package/dist/client/config.js +1 -1
  19. package/dist/client/geo.js +1 -1
  20. package/dist/client/menus.js +1 -1
  21. package/dist/client/orders.d.ts +10 -1
  22. package/dist/client/orders.js +9 -2
  23. package/dist/client/page-data.js +1 -1
  24. package/dist/client/pages.js +1 -1
  25. package/dist/client/products.d.ts +13 -3
  26. package/dist/client/products.js +36 -13
  27. package/dist/client/shipping.js +1 -1
  28. package/dist/coupon.d.ts +16 -1
  29. package/dist/coupon.js +37 -0
  30. package/dist/domain.d.ts +0 -70
  31. package/dist/domain.js +1 -70
  32. package/dist/endpoints/storefront-routes.d.ts +8 -2
  33. package/dist/endpoints/storefront-routes.js +14 -3
  34. package/dist/index.d.ts +18 -6
  35. package/dist/index.js +12 -3
  36. package/dist/operations.d.ts +72 -0
  37. package/dist/operations.js +70 -0
  38. package/dist/order-number.d.ts +9 -0
  39. package/dist/order-number.js +11 -0
  40. package/dist/otp.d.ts +8 -1
  41. package/dist/otp.js +3 -0
  42. package/dist/product-view.d.ts +8 -0
  43. package/dist/product-view.js +26 -4
  44. package/dist/quote.d.ts +27 -0
  45. package/dist/quote.js +43 -0
  46. package/dist/review-capture.d.ts +77 -0
  47. package/dist/review-capture.js +172 -0
  48. package/dist/review-media.d.ts +8 -75
  49. package/dist/review-media.js +7 -170
  50. package/dist/review-session.d.ts +97 -0
  51. package/dist/review-session.js +157 -0
  52. package/dist/reviews.d.ts +33 -0
  53. package/dist/reviews.js +41 -0
  54. package/dist/schemas.d.ts +98 -0
  55. package/dist/schemas.js +31 -0
  56. package/dist/seo.d.ts +47 -0
  57. package/dist/seo.js +87 -0
  58. package/dist/server.d.ts +1 -0
  59. package/dist/server.js +1 -0
  60. package/dist/sitemap.d.ts +22 -0
  61. package/dist/sitemap.js +51 -0
  62. package/dist/store-request.d.ts +96 -0
  63. package/dist/store-request.js +102 -0
  64. package/dist/types.d.ts +50 -2
  65. package/dist/utils/image.d.ts +48 -0
  66. package/dist/utils/image.js +51 -0
  67. package/dist/wilaya-id.d.ts +1 -0
  68. package/dist/wilaya-id.js +10 -0
  69. package/package.json +1 -1
@@ -12,8 +12,8 @@
12
12
  * half-shaped product into a theme.
13
13
  */
14
14
  import { z } from "zod";
15
- import { ProductListSchema, ProductSchema, ReviewEligibilitySchema, ReviewListEnvelopeSchema, ReviewSubmitResultSchema, ReviewUploadSchema } from "../schemas.js";
16
- import { STOREFRONT_OPERATIONS } from "../domain.js";
15
+ import { ProductListSchema, ProductPageEnvelopeSchema, ProductSchema, ReviewEligibilitySchema, ReviewListEnvelopeSchema, ReviewSubmitResultSchema, ReviewUploadSchema } from "../schemas.js";
16
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
17
17
  import { localizedPath } from "./paths.js";
18
18
  import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
19
19
  /** Page size when a caller names none — the storefront's default grid. */
@@ -24,16 +24,19 @@ export const PRODUCT_PAGE_MAX = 99;
24
24
  * Create products namespace
25
25
  */
26
26
  export function createProductsNamespace(core) {
27
+ const query = (filters, extra = {}) => ({
28
+ ...extra,
29
+ categoryId: filters?.categoryId || undefined,
30
+ collection: filters?.collection || undefined,
31
+ onSale: filters?.onSale ? "true" : undefined,
32
+ featured: filters?.featured ? "true" : undefined,
33
+ sort: filters?.sort || undefined,
34
+ limit: filters?.limit === undefined ? undefined : String(filters.limit),
35
+ offset: filters?.offset ? String(filters.offset) : undefined,
36
+ });
27
37
  const list = async (filters) => {
28
38
  try {
29
- const products = await core.get(localizedPath(core.config, "/store/products", {
30
- categoryId: filters?.categoryId || undefined,
31
- collection: filters?.collection || undefined,
32
- onSale: filters?.onSale ? "true" : undefined,
33
- featured: filters?.featured ? "true" : undefined,
34
- limit: filters?.limit === undefined ? undefined : String(filters.limit),
35
- offset: filters?.offset ? String(filters.offset) : undefined,
36
- }), z.array(ProductListSchema), { operation: STOREFRONT_OPERATIONS.productsList });
39
+ const products = await core.get(localizedPath(core.config, "/store/products", query(filters)), z.array(ProductListSchema), { operation: STOREFRONT_OPERATIONS.productsList });
37
40
  return products ?? [];
38
41
  }
39
42
  catch (error) {
@@ -41,6 +44,22 @@ export function createProductsNamespace(core) {
41
44
  return [];
42
45
  }
43
46
  };
47
+ /** One page of `path`, asked for one row more than the page so `hasNext` is exact. */
48
+ const page = async (path, filters, extra, operation) => {
49
+ const size = Math.min(Math.max(1, filters?.limit ?? PRODUCT_PAGE_DEFAULT), PRODUCT_PAGE_MAX);
50
+ try {
51
+ const envelope = await core.get(localizedPath(core.config, path, query({ ...filters, limit: size + 1 }, extra)), ProductPageEnvelopeSchema, {
52
+ envelope: true,
53
+ operation,
54
+ });
55
+ const rows = envelope?.data ?? [];
56
+ return { items: rows.slice(0, size), hasNext: rows.length > size, total: envelope?.total ?? null };
57
+ }
58
+ catch (error) {
59
+ reportDegradedRead(core.config, error, operation);
60
+ return { items: [], hasNext: false, total: null };
61
+ }
62
+ };
44
63
  return {
45
64
  async get(handle) {
46
65
  try {
@@ -55,9 +74,13 @@ export function createProductsNamespace(core) {
55
74
  },
56
75
  list,
57
76
  async page(filters) {
58
- const size = Math.min(Math.max(1, filters?.limit ?? PRODUCT_PAGE_DEFAULT), PRODUCT_PAGE_MAX);
59
- const rows = await list({ ...filters, limit: size + 1 });
60
- return { items: rows.slice(0, size), hasNext: rows.length > size };
77
+ return page("/store/products", filters, {}, STOREFRONT_OPERATIONS.productsList);
78
+ },
79
+ async search(text, filters) {
80
+ const q = text.trim().slice(0, 100);
81
+ if (q === "")
82
+ return { items: [], hasNext: false, total: 0 };
83
+ return page("/store/search", filters, { q }, STOREFRONT_OPERATIONS.productsSearch);
61
84
  },
62
85
  reviews: {
63
86
  async list(productId, pagination) {
@@ -13,7 +13,7 @@
13
13
  * treated as unsupported.
14
14
  */
15
15
  import { ShippingRatesSchema } from "../schemas.js";
16
- import { STOREFRONT_OPERATIONS } from "../domain.js";
16
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
17
17
  import { reportDegradedRead } from "./core.js";
18
18
  /**
19
19
  * Create shipping namespace
package/dist/coupon.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * code does not apply. A theme shows those answers; it never decides one.
8
8
  */
9
9
  import type { ClientResult } from "./domain.js";
10
- import type { CouponRefusal } from "./types.js";
10
+ import type { CouponRefusal, CouponRefusalReason } from "./types.js";
11
11
  /** The error code of an order refused for its code. */
12
12
  export declare const COUPON_NOT_APPLICABLE = "COUPON_NOT_APPLICABLE";
13
13
  /** The query parameter a shareable coupon link carries: `/products/robe?coupon=RAMADAN10`. */
@@ -16,3 +16,18 @@ export declare const COUPON_URL_PARAM = "coupon";
16
16
  export declare function couponFromUrl(url: URL): string | null;
17
17
  /** Why an order was refused for its code, or null when it was refused — or placed — for anything else. */
18
18
  export declare function couponRefusalOf(result: ClientResult<unknown>): CouponRefusal | null;
19
+ /** Every reason a code is refused for, as the platform names them. A newer server may add one; it then reads as `refused`. */
20
+ export declare const COUPON_REFUSAL_REASONS: readonly ["not_found", "not_started", "expired", "used_up", "identify_first", "not_eligible", "already_used", "no_eligible_products", "offer_applied", "below_minimum_subtotal", "below_minimum_quantity", "delivery_already_free"];
21
+ /**
22
+ * The reasons about the basket, not the code: the code is good, but this
23
+ * basket does not meet it yet — a phone still to type, a product under an
24
+ * offer, a minimum not reached, delivery already free.
25
+ */
26
+ export declare const COUPON_HINT_REASONS: readonly CouponRefusalReason[];
27
+ /**
28
+ * How a refusal reads: `hint` when the shopper can still meet the code with
29
+ * this basket (see `COUPON_HINT_REASONS`), `refused` when the code itself
30
+ * cannot be used — unknown, outside its dates, used up, not for this shopper —
31
+ * and for a reason this release does not know.
32
+ */
33
+ export declare function couponRefusalTone(reason: CouponRefusalReason): "refused" | "hint";
package/dist/coupon.js CHANGED
@@ -17,3 +17,40 @@ export function couponRefusalOf(result) {
17
17
  ...(typeof result.startsAt === "string" ? { startsAt: result.startsAt } : {}),
18
18
  };
19
19
  }
20
+ /** Every reason a code is refused for, as the platform names them. A newer server may add one; it then reads as `refused`. */
21
+ export const COUPON_REFUSAL_REASONS = [
22
+ "not_found",
23
+ "not_started",
24
+ "expired",
25
+ "used_up",
26
+ "identify_first",
27
+ "not_eligible",
28
+ "already_used",
29
+ "no_eligible_products",
30
+ "offer_applied",
31
+ "below_minimum_subtotal",
32
+ "below_minimum_quantity",
33
+ "delivery_already_free",
34
+ ];
35
+ /**
36
+ * The reasons about the basket, not the code: the code is good, but this
37
+ * basket does not meet it yet — a phone still to type, a product under an
38
+ * offer, a minimum not reached, delivery already free.
39
+ */
40
+ export const COUPON_HINT_REASONS = [
41
+ "identify_first",
42
+ "no_eligible_products",
43
+ "offer_applied",
44
+ "below_minimum_subtotal",
45
+ "below_minimum_quantity",
46
+ "delivery_already_free",
47
+ ];
48
+ /**
49
+ * How a refusal reads: `hint` when the shopper can still meet the code with
50
+ * this basket (see `COUPON_HINT_REASONS`), `refused` when the code itself
51
+ * cannot be used — unknown, outside its dates, used up, not for this shopper —
52
+ * and for a reason this release does not know.
53
+ */
54
+ export function couponRefusalTone(reason) {
55
+ return COUPON_HINT_REASONS.includes(reason) ? "hint" : "refused";
56
+ }
package/dist/domain.d.ts CHANGED
@@ -46,9 +46,6 @@ export interface StoreScope {
46
46
  */
47
47
  readonly readTimeoutMs?: number;
48
48
  }
49
- /** The server's hard limits for one canonical browser basket. */
50
- export declare const MAX_BASKET_LINES: 20;
51
- export declare const MAX_BASKET_LINE_QUANTITY: 100;
52
49
  /**
53
50
  * A wilaya id, bounded by the reference dataset itself rather than a literal.
54
51
  *
@@ -57,11 +54,6 @@ export declare const MAX_BASKET_LINE_QUANTITY: 100;
57
54
  * range in step.
58
55
  */
59
56
  export declare const WilayaIdSchema: z.ZodNumber;
60
- /**
61
- * Product + variant is the identity of a basket line. A null variant is a
62
- * real identity, not the same thing as any selected variant.
63
- */
64
- export declare function basketLineKey(productId: string, variantId: string | null): string;
65
57
  /**
66
58
  * A browser-owned basket line. `unitPrice` is an optimistic display value;
67
59
  * the server re-prices every line during validation and order creation.
@@ -347,65 +339,3 @@ export type ClientResult<T> = {
347
339
  readonly domainError: DomainError;
348
340
  readonly [key: string]: unknown;
349
341
  };
350
- export type RetryPolicy = "safe-read" | "never";
351
- export declare const STOREFRONT_OPERATIONS: {
352
- readonly configGet: "config.get";
353
- readonly categoriesList: "categories.list";
354
- readonly productsGet: "products.get";
355
- readonly productsList: "products.list";
356
- readonly productsReviewsList: "products.reviews.list";
357
- readonly productsReviewsSubmit: "products.reviews.submit";
358
- readonly productsReviewsEligibility: "products.reviews.eligibility";
359
- readonly productsReviewsUpload: "products.reviews.upload";
360
- readonly pagesGet: "pages.get";
361
- readonly pagesLanding: "pages.landing";
362
- readonly menusGet: "menus.get";
363
- readonly collectionsGet: "collections.get";
364
- readonly shippingRates: "shipping.rates";
365
- readonly shippingRate: "shipping.rate";
366
- readonly geoCommunes: "geo.communes";
367
- readonly cartValidate: "cart.validate";
368
- readonly pageDataGet: "pageData.get";
369
- readonly ordersCreate: "orders.create";
370
- readonly ordersTracking: "orders.tracking";
371
- readonly ordersAbandonedUpsert: "orders.abandoned.upsert";
372
- readonly ordersAbandonedConvert: "orders.abandoned.convert";
373
- readonly ordersOtpSend: "orders.otp.send";
374
- readonly ordersOtpVerify: "orders.otp.verify";
375
- };
376
- export type StorefrontOperation = (typeof STOREFRONT_OPERATIONS)[keyof typeof STOREFRONT_OPERATIONS];
377
- /**
378
- * The retry policy per operation, as a contract a caller can read.
379
- *
380
- * The transport enforces it by kind: every `safe-read` goes through the
381
- * caching/deduplicating GET path, which repeats a transport or 5xx failure,
382
- * while every `never` goes through the POST/PATCH path, which returns a result
383
- * and never retries (a replayed write is how a shopper ends up with two
384
- * orders). A test asserts this table covers every public operation and agrees
385
- * with that classification, so a new operation cannot ship unclassified.
386
- */
387
- export declare const STOREFRONT_RETRY_POLICIES: {
388
- readonly "cart.validate": "safe-read";
389
- readonly "categories.list": "safe-read";
390
- readonly "config.get": "safe-read";
391
- readonly "geo.communes": "safe-read";
392
- readonly "orders.abandoned.convert": "never";
393
- readonly "orders.abandoned.upsert": "never";
394
- readonly "orders.create": "never";
395
- readonly "orders.otp.send": "never";
396
- readonly "orders.otp.verify": "never";
397
- readonly "orders.tracking": "safe-read";
398
- readonly "pageData.get": "safe-read";
399
- readonly "pages.get": "safe-read";
400
- readonly "pages.landing": "safe-read";
401
- readonly "menus.get": "safe-read";
402
- readonly "collections.get": "safe-read";
403
- readonly "products.get": "safe-read";
404
- readonly "products.list": "safe-read";
405
- readonly "products.reviews.list": "safe-read";
406
- readonly "products.reviews.submit": "never";
407
- readonly "products.reviews.eligibility": "never";
408
- readonly "products.reviews.upload": "never";
409
- readonly "shipping.rate": "safe-read";
410
- readonly "shipping.rates": "safe-read";
411
- };
package/dist/domain.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { WILAYAS } from "./data/wilayas.js";
3
- /** The server's hard limits for one canonical browser basket. */
4
- export const MAX_BASKET_LINES = 20;
5
- export const MAX_BASKET_LINE_QUANTITY = 100;
3
+ import { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
6
4
  /**
7
5
  * A wilaya id, bounded by the reference dataset itself rather than a literal.
8
6
  *
@@ -11,13 +9,6 @@ export const MAX_BASKET_LINE_QUANTITY = 100;
11
9
  * range in step.
12
10
  */
13
11
  export const WilayaIdSchema = z.number().int().min(1).max(WILAYAS.length);
14
- /**
15
- * Product + variant is the identity of a basket line. A null variant is a
16
- * real identity, not the same thing as any selected variant.
17
- */
18
- export function basketLineKey(productId, variantId) {
19
- return `${productId}\u0000${variantId ?? ""}`;
20
- }
21
12
  /**
22
13
  * A browser-owned basket line. `unitPrice` is an optimistic display value;
23
14
  * the server re-prices every line during validation and order creation.
@@ -201,63 +192,3 @@ export const TrackingDecisionSchema = z.object({
201
192
  pixelId: z.string().nullable(),
202
193
  event: z.enum(["Purchase", "Lead"]).nullable(),
203
194
  });
204
- export const STOREFRONT_OPERATIONS = {
205
- configGet: "config.get",
206
- categoriesList: "categories.list",
207
- productsGet: "products.get",
208
- productsList: "products.list",
209
- productsReviewsList: "products.reviews.list",
210
- productsReviewsSubmit: "products.reviews.submit",
211
- productsReviewsEligibility: "products.reviews.eligibility",
212
- productsReviewsUpload: "products.reviews.upload",
213
- pagesGet: "pages.get",
214
- pagesLanding: "pages.landing",
215
- menusGet: "menus.get",
216
- collectionsGet: "collections.get",
217
- shippingRates: "shipping.rates",
218
- shippingRate: "shipping.rate",
219
- geoCommunes: "geo.communes",
220
- cartValidate: "cart.validate",
221
- pageDataGet: "pageData.get",
222
- ordersCreate: "orders.create",
223
- ordersTracking: "orders.tracking",
224
- ordersAbandonedUpsert: "orders.abandoned.upsert",
225
- ordersAbandonedConvert: "orders.abandoned.convert",
226
- ordersOtpSend: "orders.otp.send",
227
- ordersOtpVerify: "orders.otp.verify",
228
- };
229
- /**
230
- * The retry policy per operation, as a contract a caller can read.
231
- *
232
- * The transport enforces it by kind: every `safe-read` goes through the
233
- * caching/deduplicating GET path, which repeats a transport or 5xx failure,
234
- * while every `never` goes through the POST/PATCH path, which returns a result
235
- * and never retries (a replayed write is how a shopper ends up with two
236
- * orders). A test asserts this table covers every public operation and agrees
237
- * with that classification, so a new operation cannot ship unclassified.
238
- */
239
- export const STOREFRONT_RETRY_POLICIES = {
240
- [STOREFRONT_OPERATIONS.cartValidate]: "safe-read",
241
- [STOREFRONT_OPERATIONS.categoriesList]: "safe-read",
242
- [STOREFRONT_OPERATIONS.configGet]: "safe-read",
243
- [STOREFRONT_OPERATIONS.geoCommunes]: "safe-read",
244
- [STOREFRONT_OPERATIONS.ordersAbandonedConvert]: "never",
245
- [STOREFRONT_OPERATIONS.ordersAbandonedUpsert]: "never",
246
- [STOREFRONT_OPERATIONS.ordersCreate]: "never",
247
- [STOREFRONT_OPERATIONS.ordersOtpSend]: "never",
248
- [STOREFRONT_OPERATIONS.ordersOtpVerify]: "never",
249
- [STOREFRONT_OPERATIONS.ordersTracking]: "safe-read",
250
- [STOREFRONT_OPERATIONS.pageDataGet]: "safe-read",
251
- [STOREFRONT_OPERATIONS.pagesGet]: "safe-read",
252
- [STOREFRONT_OPERATIONS.pagesLanding]: "safe-read",
253
- [STOREFRONT_OPERATIONS.menusGet]: "safe-read",
254
- [STOREFRONT_OPERATIONS.collectionsGet]: "safe-read",
255
- [STOREFRONT_OPERATIONS.productsGet]: "safe-read",
256
- [STOREFRONT_OPERATIONS.productsList]: "safe-read",
257
- [STOREFRONT_OPERATIONS.productsReviewsList]: "safe-read",
258
- [STOREFRONT_OPERATIONS.productsReviewsSubmit]: "never",
259
- [STOREFRONT_OPERATIONS.productsReviewsEligibility]: "never",
260
- [STOREFRONT_OPERATIONS.productsReviewsUpload]: "never",
261
- [STOREFRONT_OPERATIONS.shippingRate]: "safe-read",
262
- [STOREFRONT_OPERATIONS.shippingRates]: "safe-read",
263
- };
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Every theme needs the same routes between the shopper's browser and the
5
5
  * platform: OTP send and verify, cart validation, abandoned-checkout tracking
6
- * and its conversion, communes, and reviews (list, find the order, send a
7
- * photo or voice note, submit). What
6
+ * and its conversion, communes, reviews (list, find the order, send a photo
7
+ * or voice note, submit), and where a shopper's order is. What
8
8
  * each one must get right is decided here once — the cross-origin check on
9
9
  * every POST, the shopper's address forwarded wherever the server counts or
10
10
  * attributes per shopper, the input bounds, and the response shape the
@@ -59,6 +59,12 @@ export interface StorefrontRoutes<C extends AstroAPIContext> {
59
59
  readonly reviewUpload: StorefrontApiRoute<C>;
60
60
  /** POST a `ReviewInput` → 201 `{ success: true, data: { id, visibility } }`. */
61
61
  readonly reviewSubmit: StorefrontApiRoute<C>;
62
+ /**
63
+ * POST `{ orderNumber, phone }` → `{ data: ShopperOrderStatus }`. A wrong
64
+ * number or phone is refused with `ORDER_NOT_FOUND`; too many tries with
65
+ * `ORDER_LOOKUP_RATE_LIMITED` (429, `windowSeconds`).
66
+ */
67
+ readonly orderStatus: StorefrontApiRoute<C>;
62
68
  }
63
69
  export declare const REVIEWS_PAGE_DEFAULT = 20;
64
70
  export declare const REVIEWS_PAGE_MAX = 50;
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Every theme needs the same routes between the shopper's browser and the
5
5
  * platform: OTP send and verify, cart validation, abandoned-checkout tracking
6
- * and its conversion, communes, and reviews (list, find the order, send a
7
- * photo or voice note, submit). What
6
+ * and its conversion, communes, reviews (list, find the order, send a photo
7
+ * or voice note, submit), and where a shopper's order is. What
8
8
  * each one must get right is decided here once — the cross-origin check on
9
9
  * every POST, the shopper's address forwarded wherever the server counts or
10
10
  * attributes per shopper, the input bounds, and the response shape the
@@ -22,7 +22,8 @@
22
22
  * (see `createProxyEndpoint`), and the server's diagnostic fields ride along.
23
23
  */
24
24
  import { z } from "zod";
25
- import { AbandonedCheckoutInputSchema, BasketLineInputSchema, DeliveryDestinationSchema, ProductQuoteInputSchema, MAX_BASKET_LINES, WilayaIdSchema, } from "../domain.js";
25
+ import { AbandonedCheckoutInputSchema, BasketLineInputSchema, DeliveryDestinationSchema, ProductQuoteInputSchema, WilayaIdSchema, } from "../domain.js";
26
+ import { MAX_BASKET_LINES } from "../basket-line.js";
26
27
  import { createProxyEndpoint, } from "./proxy.js";
27
28
  export const REVIEWS_PAGE_DEFAULT = 20;
28
29
  export const REVIEWS_PAGE_MAX = 50;
@@ -81,6 +82,10 @@ const reviewSubmitInput = z.object({
81
82
  grant: z.string().min(1).max(2048).optional(),
82
83
  uploads: z.array(z.string().min(1).max(64)).max(8).optional(),
83
84
  });
85
+ const orderStatusInput = z.object({
86
+ orderNumber: z.string().min(1).max(64),
87
+ phone: z.string().min(1).max(32),
88
+ });
84
89
  const reviewEligibilityInput = z.object({
85
90
  orderNumber: z.string().min(1).max(64),
86
91
  phone: z.string().min(1).max(32),
@@ -190,5 +195,11 @@ export function createStorefrontRoutes(options) {
190
195
  successBody: (data) => ({ success: true, data }),
191
196
  call: (client, review) => client.products.reviews.submit(defined(review)),
192
197
  }),
198
+ orderStatus: route({
199
+ method: "POST",
200
+ inputSchema: orderStatusInput,
201
+ forwardAttribution: true,
202
+ call: (client, proof, headers) => client.orders.status(proof, headers),
203
+ }),
193
204
  };
194
205
  }
package/dist/index.d.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  */
12
12
  export { formatMoney } from "./utils/money.js";
13
13
  export type { MoneyFormatOptions } from "./utils/money.js";
14
- export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, mediaImageUrl, mediaUrl, } from "./utils/image.js";
15
- export type { ImageHelpers, ImageResizeOptions, MediaImageParams } from "./utils/image.js";
14
+ export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, mediaImageUrl, mediaImageSrcset, mediaUrl, pictureOf, } from "./utils/image.js";
15
+ export type { ImageHelpers, ImageResizeOptions, MediaImageParams, MediaImageSrcsetOptions, PickedImage, Picture } from "./utils/image.js";
16
16
  export { productImageSource } from "./utils/product-image.js";
17
17
  export type { ProductImageSourceInput } from "./utils/product-image.js";
18
18
  export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
@@ -23,24 +23,36 @@ export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "
23
23
  export type { LocalePath, LocaleRouting } from "./locale-routing.js";
24
24
  export type { StorefrontPageMatch, StorefrontPageType, StorefrontPathParams, StorefrontRoute } from "./routes.js";
25
25
  export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
26
- export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, } from "./basket.js";
26
+ export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
27
+ export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, BasketSelectionLine, } from "./basket.js";
27
28
  export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
28
29
  export type { OtpMachine, OtpMachineOptions, OtpFailureContext, OtpResponseOptions, OtpState, OtpStatus, OtpTransport, OtpTransportResult, OtpStateListener, } from "./otp.js";
29
30
  export { WILAYAS } from "./data/wilayas.js";
30
31
  export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
32
+ export { isWilayaId } from "./wilaya-id.js";
33
+ export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-readers.js";
34
+ export { createQuoter } from "./quote.js";
35
+ export type { Quoter, QuoterOptions, QuoteResult } from "./quote.js";
36
+ export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
37
+ export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_STAGES } from "./catalog.js";
38
+ export { SEO_TEXT_LIMITS, jsonLd, productMeta, productStructuredData } from "./seo.js";
39
+ export type { PageMeta, ProductStructuredDataOptions } from "./seo.js";
40
+ export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
41
+ export type { ReviewRefusalCode } from "./reviews.js";
31
42
  export type { CommuneTuple } from "./data/communes.js";
32
43
  export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
33
44
  export * from "./types.js";
34
45
  export * from "./schemas.js";
35
46
  export * from "./domain.js";
47
+ export * from "./operations.js";
36
48
  export * from "./product-view.js";
37
49
  export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
38
- export { COUPON_NOT_APPLICABLE, COUPON_URL_PARAM, couponFromUrl, couponRefusalOf } from "./coupon.js";
50
+ export { COUPON_HINT_REASONS, COUPON_NOT_APPLICABLE, COUPON_REFUSAL_REASONS, COUPON_URL_PARAM, couponFromUrl, couponRefusalOf, couponRefusalTone, } from "./coupon.js";
39
51
  export { initialVariant, optionStates, pickOption, variantFor } from "./variant-picker.js";
40
52
  export type { OptionSelection, OptionState, PickerVariant } from "./variant-picker.js";
41
53
  export type { CartQuoteOptions } from "./client/cart.js";
42
54
  export type { OfferTierInput, OfferTierView, QuantityTierView } from "./offer-view.js";
43
- export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
44
- export type { AbandonedCheckoutPorts, AbandonedCheckoutSnapshot, AbandonedCheckoutStorage, AbandonedCheckoutTracker, } from "./abandoned-checkout.js";
55
+ export { ABANDONED_CHECKOUT_SESSION_KEY, convertAbandonedCheckout, createAbandonedCheckoutTracker, createBrowserAbandonedCheckoutTracker, } from "./abandoned-checkout.js";
56
+ export type { AbandonedCheckoutConversion, AbandonedCheckoutHost, AbandonedCheckoutPorts, AbandonedCheckoutSnapshot, AbandonedCheckoutStorage, AbandonedCheckoutTracker, BrowserAbandonedCheckoutOptions, } from "./abandoned-checkout.js";
45
57
  export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
46
58
  export type { MetaCartPayload, MetaCatalogPayload, MetaContent, MetaEventName, MetaPixelFunction, MetaPixelOptions, MetaPixelTracker, } from "./meta-tracking.js";
package/dist/index.js CHANGED
@@ -10,23 +10,32 @@
10
10
  * Internal modules stay reachable only through their intentional entry points.
11
11
  */
12
12
  export { formatMoney } from "./utils/money.js";
13
- export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, mediaImageUrl, mediaUrl, } from "./utils/image.js";
13
+ export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, mediaImageUrl, mediaImageSrcset, mediaUrl, pictureOf, } from "./utils/image.js";
14
14
  export { productImageSource } from "./utils/product-image.js";
15
15
  export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
16
16
  export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
17
17
  export { servedLanguage } from "./locales.js";
18
18
  export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
19
19
  export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
20
+ export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
20
21
  export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
21
22
  export { WILAYAS } from "./data/wilayas.js";
22
23
  export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
24
+ export { isWilayaId } from "./wilaya-id.js";
25
+ export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-readers.js";
26
+ export { createQuoter } from "./quote.js";
27
+ export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
28
+ export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_STAGES } from "./catalog.js";
29
+ export { SEO_TEXT_LIMITS, jsonLd, productMeta, productStructuredData } from "./seo.js";
30
+ export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
23
31
  export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
24
32
  export * from "./types.js";
25
33
  export * from "./schemas.js";
26
34
  export * from "./domain.js";
35
+ export * from "./operations.js";
27
36
  export * from "./product-view.js";
28
37
  export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
29
- export { COUPON_NOT_APPLICABLE, COUPON_URL_PARAM, couponFromUrl, couponRefusalOf } from "./coupon.js";
38
+ export { COUPON_HINT_REASONS, COUPON_NOT_APPLICABLE, COUPON_REFUSAL_REASONS, COUPON_URL_PARAM, couponFromUrl, couponRefusalOf, couponRefusalTone, } from "./coupon.js";
30
39
  export { initialVariant, optionStates, pickOption, variantFor } from "./variant-picker.js";
31
- export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
40
+ export { ABANDONED_CHECKOUT_SESSION_KEY, convertAbandonedCheckout, createAbandonedCheckoutTracker, createBrowserAbandonedCheckoutTracker, } from "./abandoned-checkout.js";
32
41
  export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The platform operations a storefront calls, and the retry policy each one
3
+ * keeps. Plain values and no schema: browser code reads the operation names
4
+ * too — an OTP route's failure names the operation it was — so this module
5
+ * stays free of anything that would bring a validator into a shopper's page.
6
+ */
7
+ export type RetryPolicy = "safe-read" | "never";
8
+ export declare const STOREFRONT_OPERATIONS: {
9
+ readonly configGet: "config.get";
10
+ readonly categoriesList: "categories.list";
11
+ readonly productsGet: "products.get";
12
+ readonly productsList: "products.list";
13
+ readonly productsSearch: "products.search";
14
+ readonly productsReviewsList: "products.reviews.list";
15
+ readonly productsReviewsSubmit: "products.reviews.submit";
16
+ readonly productsReviewsEligibility: "products.reviews.eligibility";
17
+ readonly productsReviewsUpload: "products.reviews.upload";
18
+ readonly pagesGet: "pages.get";
19
+ readonly pagesLanding: "pages.landing";
20
+ readonly menusGet: "menus.get";
21
+ readonly collectionsGet: "collections.get";
22
+ readonly shippingRates: "shipping.rates";
23
+ readonly shippingRate: "shipping.rate";
24
+ readonly geoCommunes: "geo.communes";
25
+ readonly cartValidate: "cart.validate";
26
+ readonly pageDataGet: "pageData.get";
27
+ readonly ordersCreate: "orders.create";
28
+ readonly ordersTracking: "orders.tracking";
29
+ readonly ordersStatus: "orders.status";
30
+ readonly ordersAbandonedUpsert: "orders.abandoned.upsert";
31
+ readonly ordersAbandonedConvert: "orders.abandoned.convert";
32
+ readonly ordersOtpSend: "orders.otp.send";
33
+ readonly ordersOtpVerify: "orders.otp.verify";
34
+ };
35
+ export type StorefrontOperation = (typeof STOREFRONT_OPERATIONS)[keyof typeof STOREFRONT_OPERATIONS];
36
+ /**
37
+ * The retry policy per operation, as a contract a caller can read.
38
+ *
39
+ * The transport enforces it by kind: every `safe-read` goes through the
40
+ * caching/deduplicating GET path, which repeats a transport or 5xx failure,
41
+ * while every `never` goes through the POST/PATCH path, which returns a result
42
+ * and never retries (a replayed write is how a shopper ends up with two
43
+ * orders). A test asserts this table covers every public operation and agrees
44
+ * with that classification, so a new operation cannot ship unclassified.
45
+ */
46
+ export declare const STOREFRONT_RETRY_POLICIES: {
47
+ readonly "cart.validate": "safe-read";
48
+ readonly "categories.list": "safe-read";
49
+ readonly "config.get": "safe-read";
50
+ readonly "geo.communes": "safe-read";
51
+ readonly "orders.abandoned.convert": "never";
52
+ readonly "orders.abandoned.upsert": "never";
53
+ readonly "orders.create": "never";
54
+ readonly "orders.otp.send": "never";
55
+ readonly "orders.otp.verify": "never";
56
+ readonly "orders.tracking": "safe-read";
57
+ readonly "orders.status": "never";
58
+ readonly "pageData.get": "safe-read";
59
+ readonly "pages.get": "safe-read";
60
+ readonly "pages.landing": "safe-read";
61
+ readonly "menus.get": "safe-read";
62
+ readonly "collections.get": "safe-read";
63
+ readonly "products.get": "safe-read";
64
+ readonly "products.list": "safe-read";
65
+ readonly "products.search": "safe-read";
66
+ readonly "products.reviews.list": "safe-read";
67
+ readonly "products.reviews.submit": "never";
68
+ readonly "products.reviews.eligibility": "never";
69
+ readonly "products.reviews.upload": "never";
70
+ readonly "shipping.rate": "safe-read";
71
+ readonly "shipping.rates": "safe-read";
72
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The platform operations a storefront calls, and the retry policy each one
3
+ * keeps. Plain values and no schema: browser code reads the operation names
4
+ * too — an OTP route's failure names the operation it was — so this module
5
+ * stays free of anything that would bring a validator into a shopper's page.
6
+ */
7
+ export const STOREFRONT_OPERATIONS = {
8
+ configGet: "config.get",
9
+ categoriesList: "categories.list",
10
+ productsGet: "products.get",
11
+ productsList: "products.list",
12
+ productsSearch: "products.search",
13
+ productsReviewsList: "products.reviews.list",
14
+ productsReviewsSubmit: "products.reviews.submit",
15
+ productsReviewsEligibility: "products.reviews.eligibility",
16
+ productsReviewsUpload: "products.reviews.upload",
17
+ pagesGet: "pages.get",
18
+ pagesLanding: "pages.landing",
19
+ menusGet: "menus.get",
20
+ collectionsGet: "collections.get",
21
+ shippingRates: "shipping.rates",
22
+ shippingRate: "shipping.rate",
23
+ geoCommunes: "geo.communes",
24
+ cartValidate: "cart.validate",
25
+ pageDataGet: "pageData.get",
26
+ ordersCreate: "orders.create",
27
+ ordersTracking: "orders.tracking",
28
+ ordersStatus: "orders.status",
29
+ ordersAbandonedUpsert: "orders.abandoned.upsert",
30
+ ordersAbandonedConvert: "orders.abandoned.convert",
31
+ ordersOtpSend: "orders.otp.send",
32
+ ordersOtpVerify: "orders.otp.verify",
33
+ };
34
+ /**
35
+ * The retry policy per operation, as a contract a caller can read.
36
+ *
37
+ * The transport enforces it by kind: every `safe-read` goes through the
38
+ * caching/deduplicating GET path, which repeats a transport or 5xx failure,
39
+ * while every `never` goes through the POST/PATCH path, which returns a result
40
+ * and never retries (a replayed write is how a shopper ends up with two
41
+ * orders). A test asserts this table covers every public operation and agrees
42
+ * with that classification, so a new operation cannot ship unclassified.
43
+ */
44
+ export const STOREFRONT_RETRY_POLICIES = {
45
+ [STOREFRONT_OPERATIONS.cartValidate]: "safe-read",
46
+ [STOREFRONT_OPERATIONS.categoriesList]: "safe-read",
47
+ [STOREFRONT_OPERATIONS.configGet]: "safe-read",
48
+ [STOREFRONT_OPERATIONS.geoCommunes]: "safe-read",
49
+ [STOREFRONT_OPERATIONS.ordersAbandonedConvert]: "never",
50
+ [STOREFRONT_OPERATIONS.ordersAbandonedUpsert]: "never",
51
+ [STOREFRONT_OPERATIONS.ordersCreate]: "never",
52
+ [STOREFRONT_OPERATIONS.ordersOtpSend]: "never",
53
+ [STOREFRONT_OPERATIONS.ordersOtpVerify]: "never",
54
+ [STOREFRONT_OPERATIONS.ordersTracking]: "safe-read",
55
+ [STOREFRONT_OPERATIONS.ordersStatus]: "never",
56
+ [STOREFRONT_OPERATIONS.pageDataGet]: "safe-read",
57
+ [STOREFRONT_OPERATIONS.pagesGet]: "safe-read",
58
+ [STOREFRONT_OPERATIONS.pagesLanding]: "safe-read",
59
+ [STOREFRONT_OPERATIONS.menusGet]: "safe-read",
60
+ [STOREFRONT_OPERATIONS.collectionsGet]: "safe-read",
61
+ [STOREFRONT_OPERATIONS.productsGet]: "safe-read",
62
+ [STOREFRONT_OPERATIONS.productsList]: "safe-read",
63
+ [STOREFRONT_OPERATIONS.productsSearch]: "safe-read",
64
+ [STOREFRONT_OPERATIONS.productsReviewsList]: "safe-read",
65
+ [STOREFRONT_OPERATIONS.productsReviewsSubmit]: "never",
66
+ [STOREFRONT_OPERATIONS.productsReviewsEligibility]: "never",
67
+ [STOREFRONT_OPERATIONS.productsReviewsUpload]: "never",
68
+ [STOREFRONT_OPERATIONS.shippingRate]: "safe-read",
69
+ [STOREFRONT_OPERATIONS.shippingRates]: "safe-read",
70
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The order number a shopper is given (`ORD-20261023-0042`) and types back to
3
+ * review what they bought or follow their order. The server refuses any other
4
+ * shape with `VALIDATION_FAILED`; a form can say so before asking.
5
+ */
6
+ /** `ORD-`, the day the order was placed (`YYYYMMDD`), a dash and its digits; any case. */
7
+ export declare const ORDER_NUMBER_PATTERN: RegExp;
8
+ /** Whether `value`, without the spaces around it, has the shape of an order number. */
9
+ export declare function isOrderNumber(value: string): boolean;