@wocart/storefront 1.19.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 (52) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/USAGE.md +286 -4
  3. package/dist/abandoned-checkout.d.ts +45 -0
  4. package/dist/abandoned-checkout.js +81 -3
  5. package/dist/answer-readers.d.ts +11 -4
  6. package/dist/answer-readers.js +114 -0
  7. package/dist/astro.d.ts +2 -0
  8. package/dist/astro.js +1 -0
  9. package/dist/basket.d.ts +11 -1
  10. package/dist/basket.js +17 -0
  11. package/dist/catalog.d.ts +18 -0
  12. package/dist/catalog.js +27 -0
  13. package/dist/client/orders.d.ts +10 -1
  14. package/dist/client/orders.js +8 -1
  15. package/dist/client/products.d.ts +13 -3
  16. package/dist/client/products.js +35 -12
  17. package/dist/coupon.d.ts +16 -1
  18. package/dist/coupon.js +37 -0
  19. package/dist/endpoints/storefront-routes.d.ts +8 -2
  20. package/dist/endpoints/storefront-routes.js +12 -2
  21. package/dist/index.d.ts +15 -7
  22. package/dist/index.js +9 -4
  23. package/dist/operations.d.ts +4 -0
  24. package/dist/operations.js +4 -0
  25. package/dist/order-number.d.ts +9 -0
  26. package/dist/order-number.js +11 -0
  27. package/dist/product-view.d.ts +8 -0
  28. package/dist/product-view.js +26 -4
  29. package/dist/quote.d.ts +27 -0
  30. package/dist/quote.js +43 -0
  31. package/dist/review-capture.d.ts +77 -0
  32. package/dist/review-capture.js +172 -0
  33. package/dist/review-media.d.ts +8 -75
  34. package/dist/review-media.js +7 -170
  35. package/dist/review-session.d.ts +97 -0
  36. package/dist/review-session.js +157 -0
  37. package/dist/reviews.d.ts +33 -0
  38. package/dist/reviews.js +41 -0
  39. package/dist/schemas.d.ts +98 -0
  40. package/dist/schemas.js +31 -0
  41. package/dist/seo.d.ts +47 -0
  42. package/dist/seo.js +87 -0
  43. package/dist/server.d.ts +1 -0
  44. package/dist/server.js +1 -0
  45. package/dist/sitemap.d.ts +22 -0
  46. package/dist/sitemap.js +51 -0
  47. package/dist/store-request.d.ts +96 -0
  48. package/dist/store-request.js +102 -0
  49. package/dist/types.d.ts +50 -2
  50. package/dist/utils/image.d.ts +48 -0
  51. package/dist/utils/image.js +51 -0
  52. package/package.json +1 -1
@@ -1,5 +1,119 @@
1
1
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
2
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
3
+ const isInteger = (value) => typeof value === "number" && Number.isSafeInteger(value);
4
+ const isNullableString = (value) => value === null || typeof value === "string";
5
+ /** An optional field the schema reads as `.nullish()`: absent stays absent, null and the type are kept, anything else refuses. */
6
+ function nullish(source, key, accepts) {
7
+ if (!(key in source) || source[key] === undefined)
8
+ return { ok: true, entry: {} };
9
+ const value = source[key];
10
+ return value === null || accepts(value) ? { ok: true, entry: { [key]: value } } : { ok: false, entry: {} };
11
+ }
12
+ const isString = (value) => typeof value === "string";
13
+ const BLOCKERS = ["missing", "unlisted", "out_of_stock"];
14
+ function readQuoteLine(value) {
15
+ if (!isObject(value))
16
+ return null;
17
+ const { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, maxQuantity, blocker } = value;
18
+ if (typeof productId !== "string" || !isNullableString(variantId) || !isNullableString(variantLabel) || typeof productName !== "string")
19
+ return null;
20
+ if (!isInteger(quantity) || !isFiniteNumber(unitPrice) || !isFiniteNumber(lineTotal))
21
+ return null;
22
+ if (maxQuantity !== null && !isFiniteNumber(maxQuantity))
23
+ return null;
24
+ if (blocker !== null && !(typeof blocker === "string" && BLOCKERS.includes(blocker)))
25
+ return null;
26
+ const offerId = nullish(value, "offerId", isString);
27
+ const offerName = nullish(value, "offerName", isString);
28
+ const discount = nullish(value, "discount", isFiniteNumber);
29
+ if (!offerId.ok || !offerName.ok || !discount.ok)
30
+ return null;
31
+ return {
32
+ productId,
33
+ variantId,
34
+ variantLabel,
35
+ productName,
36
+ quantity,
37
+ unitPrice,
38
+ lineTotal,
39
+ ...offerId.entry,
40
+ ...offerName.entry,
41
+ ...discount.entry,
42
+ maxQuantity,
43
+ blocker: blocker,
44
+ };
45
+ }
46
+ function readReward(value) {
47
+ if (!isObject(value))
48
+ return null;
49
+ const { offerId, productId, productName, quantity } = value;
50
+ if (typeof offerId !== "string" || !isNullableString(productId) || !isNullableString(productName) || !isInteger(quantity))
51
+ return null;
52
+ const offerName = nullish(value, "offerName", isString);
53
+ const variantId = nullish(value, "variantId", isString);
54
+ const variantLabel = nullish(value, "variantLabel", isString);
55
+ if (!offerName.ok || !variantId.ok || !variantLabel.ok)
56
+ return null;
57
+ return { offerId, ...offerName.entry, productId, productName, ...variantId.entry, ...variantLabel.entry, quantity };
58
+ }
59
+ function readDelivery(value) {
60
+ if (!isObject(value))
61
+ return null;
62
+ if (value.available === true)
63
+ return isFiniteNumber(value.fee) && isFiniteNumber(value.total) ? { available: true, fee: value.fee, total: value.total } : null;
64
+ return value.available === false && value.fee === null && value.total === null ? { available: false, fee: null, total: null } : null;
65
+ }
66
+ function readList(value, read) {
67
+ if (!Array.isArray(value))
68
+ return null;
69
+ const items = [];
70
+ for (const item of value) {
71
+ const parsed = read(item);
72
+ if (parsed === null)
73
+ return null;
74
+ items.push(parsed);
75
+ }
76
+ return items;
77
+ }
78
+ /**
79
+ * A cart quote (`CartValidation`) — the `data` of the theme's cart-validate
80
+ * route — or null when the value is not one. What the quote says is the
81
+ * server's: lines re-priced from the catalog, rewards, the subtotal, the free
82
+ * delivery progress, and, when asked, the delivery fee and the coupon's outcome.
83
+ */
84
+ export function readCartQuote(value) {
85
+ if (!isObject(value) || !isFiniteNumber(value.subtotal))
86
+ return null;
87
+ const lines = readList(value.lines, readQuoteLine);
88
+ const rewards = readList(value.rewards, readReward);
89
+ if (lines === null || rewards === null)
90
+ return null;
91
+ const free = value.freeDelivery;
92
+ if (!isObject(free) || typeof free.fromOffer !== "boolean" || typeof free.qualified !== "boolean" || !isFiniteNumber(free.remaining))
93
+ return null;
94
+ if (free.threshold !== null && !isFiniteNumber(free.threshold))
95
+ return null;
96
+ let delivery = null;
97
+ if ("delivery" in value && value.delivery !== undefined) {
98
+ delivery = readDelivery(value.delivery);
99
+ if (delivery === null)
100
+ return null;
101
+ }
102
+ let coupon;
103
+ if ("coupon" in value && value.coupon !== undefined) {
104
+ coupon = value.coupon === null ? null : readCouponOutcome(value.coupon);
105
+ if (coupon === null && value.coupon !== null)
106
+ return null;
107
+ }
108
+ return {
109
+ lines,
110
+ subtotal: value.subtotal,
111
+ rewards,
112
+ freeDelivery: { fromOffer: free.fromOffer, threshold: free.threshold, qualified: free.qualified, remaining: free.remaining },
113
+ ...(delivery === null ? {} : { delivery }),
114
+ ...(coupon === undefined ? {} : { coupon }),
115
+ };
116
+ }
3
117
  /** The delivery rates of a value (`ShippingRates`), or null when it is not a rate table. */
4
118
  export function readShippingRates(value) {
5
119
  if (!isObject(value))
package/dist/astro.d.ts CHANGED
@@ -6,3 +6,5 @@ export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./
6
6
  export type { PlaceOrderInput, PlaceOrderResult } from "./actions.js";
7
7
  export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
8
8
  export type { PrgDecision, ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
9
+ export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
10
+ export type { NotFoundContext, ResolvedStore, StoreRequestOptions, StoreRequestResolution } from "./store-request.js";
package/dist/astro.js CHANGED
@@ -2,3 +2,4 @@ export { createProxyEndpoint } from "./endpoints/proxy.js";
2
2
  export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
3
3
  export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
4
4
  export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
5
+ export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
package/dist/basket.d.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * every line at checkout.
11
11
  */
12
12
  import type { BasketLine, BasketLineInput } from "./domain.js";
13
+ import type { CreateOrderVariantSelection } from "./types.js";
13
14
  /** Default key used in browser storage, versioned to prevent schema drift crashes. */
14
15
  export declare const DEFAULT_BASKET_STORAGE_KEY = "cod_cart_v1";
15
16
  /**
@@ -35,10 +36,12 @@ export interface BasketStoreOptions {
35
36
  maxLineQuantity?: number;
36
37
  }
37
38
  export type BasketListener = (lines: readonly BasketLine[]) => void;
39
+ /** A product line before its variant and quantity: what `addSelections` gives each variant chosen. */
40
+ export type BasketSelectionLine = Omit<BasketLine, "variantId" | "variantLabel" | "quantity">;
38
41
  /**
39
42
  * Basket Store Interface.
40
43
  *
41
- * Deep module interface: 9 simple, focused methods that hide all storage
44
+ * Deep module interface: simple, focused methods that hide all storage
42
45
  * failures, cross-tab events, line reconciliation, and normalization.
43
46
  */
44
47
  export interface BasketStore {
@@ -50,6 +53,13 @@ export interface BasketStore {
50
53
  getSubtotal(): number;
51
54
  /** Add a line. Merges with existing product+variant, caps quantities. */
52
55
  addLine(line: BasketLine): readonly BasketLine[];
56
+ /**
57
+ * Add a choice made piece by piece — an offer of three in two sizes, as an
58
+ * order's `variantSelections` carries it: one line per variant, its
59
+ * quantity the pieces of that variant, as one change. All or nothing: when
60
+ * the basket has no room for every new line, nothing is added.
61
+ */
62
+ addSelections(line: BasketSelectionLine, selections: readonly CreateOrderVariantSelection[]): readonly BasketLine[];
53
63
  /** Update quantity of an existing line. Removes if <= 0. */
54
64
  setQuantity(key: string, quantity: number): readonly BasketLine[];
55
65
  /** Remove a line by its composite key. */
package/dist/basket.js CHANGED
@@ -227,6 +227,23 @@ export function createBasketStore(options = {}) {
227
227
  }
228
228
  return commit([...state, line]);
229
229
  },
230
+ addSelections(line, selections) {
231
+ const pieces = new Map();
232
+ for (const selection of selections) {
233
+ const existing = pieces.get(selection.variantId);
234
+ if (existing)
235
+ existing.quantity += 1;
236
+ else
237
+ pieces.set(selection.variantId, { ...line, variantId: selection.variantId, variantLabel: selection.variantLabel ?? null, quantity: 1 });
238
+ }
239
+ if (pieces.size === 0)
240
+ return state;
241
+ const held = new Set(state.map((l) => basketLineKey(l.productId, l.variantId)));
242
+ const added = [...pieces.values()].filter((piece) => !held.has(basketLineKey(piece.productId, piece.variantId))).length;
243
+ if (state.length + added > maxLines)
244
+ return state;
245
+ return commit([...state, ...pieces.values()]);
246
+ },
230
247
  setQuantity(key, quantity) {
231
248
  if (!Number.isInteger(quantity) || quantity < 1) {
232
249
  return this.removeLine(key);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * How a storefront listing can be ordered, for a theme's sort control. Absent,
3
+ * a listing keeps its own order: a collection's, a search's best match first,
4
+ * else newest.
5
+ */
6
+ export declare const PRODUCT_SORTS: readonly ["newest", "price-ascending", "price-descending"];
7
+ /**
8
+ * Every step `orders.status()` can name, in a shopper's words, for a theme to
9
+ * word each one. A newer server may add one; a theme then shows its general
10
+ * words.
11
+ */
12
+ export declare const SHOPPER_ORDER_STAGES: readonly ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered", "returned", "cancelled"];
13
+ /**
14
+ * The collections every store starts with, the likeliest to hold products
15
+ * first: what to show when a page needs a collection and none was named (an
16
+ * editor's preview of the collection template).
17
+ */
18
+ export declare const DEFAULT_COLLECTION_HANDLES: readonly ["best-sellers", "new-arrivals", "featured"];
@@ -0,0 +1,27 @@
1
+ /**
2
+ * How a storefront listing can be ordered, for a theme's sort control. Absent,
3
+ * a listing keeps its own order: a collection's, a search's best match first,
4
+ * else newest.
5
+ */
6
+ export const PRODUCT_SORTS = ["newest", "price-ascending", "price-descending"];
7
+ /**
8
+ * Every step `orders.status()` can name, in a shopper's words, for a theme to
9
+ * word each one. A newer server may add one; a theme then shows its general
10
+ * words.
11
+ */
12
+ export const SHOPPER_ORDER_STAGES = [
13
+ "placed",
14
+ "confirmed",
15
+ "preparing",
16
+ "shipped",
17
+ "out_for_delivery",
18
+ "delivered",
19
+ "returned",
20
+ "cancelled",
21
+ ];
22
+ /**
23
+ * The collections every store starts with, the likeliest to hold products
24
+ * first: what to show when a page needs a collection and none was named (an
25
+ * editor's preview of the collection template).
26
+ */
27
+ export const DEFAULT_COLLECTION_HANDLES = ["best-sellers", "new-arrivals", "featured"];
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { type ClientCore, type ClientResult } from "./core.js";
15
15
  import type { AbandonedCheckoutInput } from "../domain.js";
16
- import type { CreateOrderInput, CreatedOrder, OrderTracking, OtpSendResult, OtpVerifyResult } from "../types.js";
16
+ import type { CreateOrderInput, CreatedOrder, OrderTracking, OtpSendResult, OtpVerifyResult, ShopperOrderProof, ShopperOrderStatus } from "../types.js";
17
17
  export interface AbandonedNamespace {
18
18
  /**
19
19
  * Upsert an abandoned-checkout record — fire-and-forget. `forwardedHeaders`
@@ -62,6 +62,15 @@ export interface OrdersNamespace {
62
62
  * the thank-you page fires no conversion at all in that case.
63
63
  */
64
64
  tracking(orderId: string): Promise<OrderTracking | null>;
65
+ /**
66
+ * Where a shopper's order is, proved by its number and the phone it was
67
+ * placed with: its step in the shopper's words, its lines and total, and
68
+ * the carrier's tracking number once shipped. A wrong number and a wrong
69
+ * phone both fail with `ORDER_NOT_FOUND`; too many tries with
70
+ * `ORDER_LOOKUP_RATE_LIMITED` and `windowSeconds`. `forwardedHeaders`
71
+ * carries the shopper's address: the limit counts per shopper.
72
+ */
73
+ status(proof: ShopperOrderProof, forwardedHeaders?: Record<string, string>): Promise<ClientResult<ShopperOrderStatus>>;
65
74
  abandoned: AbandonedNamespace;
66
75
  otp: OtpNamespace;
67
76
  }
@@ -11,7 +11,7 @@
11
11
  * - tracking resolves to null on any failure — firing no conversion event
12
12
  * is recoverable; reporting it to the wrong ad account is not.
13
13
  */
14
- import { CreatedOrderSchema, IdResponseSchema, OrderTrackingSchema, OtpSendResultSchema, OtpVerifyResultSchema, } from "../schemas.js";
14
+ import { CreatedOrderSchema, IdResponseSchema, OrderTrackingSchema, OtpSendResultSchema, ShopperOrderStatusSchema, OtpVerifyResultSchema, } from "../schemas.js";
15
15
  import { STOREFRONT_OPERATIONS } from "../operations.js";
16
16
  import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
17
17
  /**
@@ -39,6 +39,13 @@ export function createOrdersNamespace(core) {
39
39
  return null;
40
40
  }
41
41
  },
42
+ async status(proof, forwardedHeaders) {
43
+ return core.post("/store/orders/status", { orderNumber: proof.orderNumber, phone: proof.phone }, {
44
+ ...(forwardedHeaders ? { headers: forwardedHeaders } : {}),
45
+ schema: ShopperOrderStatusSchema,
46
+ operation: STOREFRONT_OPERATIONS.ordersStatus,
47
+ });
48
+ },
42
49
  abandoned: {
43
50
  async upsert(body, forwardedHeaders) {
44
51
  // The server answers `{ success, id }` — the payload sits at the
@@ -31,11 +31,21 @@ export interface ProductsNamespace {
31
31
  */
32
32
  list(filters?: ProductListFilters): Promise<ProductList[]>;
33
33
  /**
34
- * One page of products, and whether another page follows. The server is
35
- * asked for one row more than the page, so the answer is exact without a
36
- * count query. A failed read is an empty final page, as `list` degrades.
34
+ * One page of products, whether another page follows, and how many the
35
+ * whole listing holds. The server is asked for one row more than the page,
36
+ * so `hasNext` is exact even from a server that does not count. A failed
37
+ * read is an empty final page, as `list` degrades.
37
38
  */
38
39
  page(filters?: ProductListFilters): Promise<ProductPage>;
40
+ /**
41
+ * The products a shopper's words find — every word in the product's name,
42
+ * in the client's language or the catalog's, or in its tags — as a page,
43
+ * with the listing filters, sort and paging. Without a `sort`, products
44
+ * whose name holds every word come first. Blank words find nothing; a
45
+ * server that predates search answers an empty page (reported through
46
+ * `onError`), never the whole catalog.
47
+ */
48
+ search(text: string, filters?: ProductListFilters): Promise<ProductPage>;
39
49
  /**
40
50
  * Reviews sub-namespace
41
51
  */
@@ -12,7 +12,7 @@
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";
15
+ import { ProductListSchema, ProductPageEnvelopeSchema, ProductSchema, ReviewEligibilitySchema, ReviewListEnvelopeSchema, ReviewSubmitResultSchema, ReviewUploadSchema } from "../schemas.js";
16
16
  import { STOREFRONT_OPERATIONS } from "../operations.js";
17
17
  import { localizedPath } from "./paths.js";
18
18
  import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
@@ -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) {
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
+ }
@@ -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
@@ -82,6 +82,10 @@ const reviewSubmitInput = z.object({
82
82
  grant: z.string().min(1).max(2048).optional(),
83
83
  uploads: z.array(z.string().min(1).max(64)).max(8).optional(),
84
84
  });
85
+ const orderStatusInput = z.object({
86
+ orderNumber: z.string().min(1).max(64),
87
+ phone: z.string().min(1).max(32),
88
+ });
85
89
  const reviewEligibilityInput = z.object({
86
90
  orderNumber: z.string().min(1).max(64),
87
91
  phone: z.string().min(1).max(32),
@@ -191,5 +195,11 @@ export function createStorefrontRoutes(options) {
191
195
  successBody: (data) => ({ success: true, data }),
192
196
  call: (client, review) => client.products.reviews.submit(defined(review)),
193
197
  }),
198
+ orderStatus: route({
199
+ method: "POST",
200
+ inputSchema: orderStatusInput,
201
+ forwardAttribution: true,
202
+ call: (client, proof, headers) => client.orders.status(proof, headers),
203
+ }),
194
204
  };
195
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";
@@ -24,13 +24,21 @@ 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
26
  export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
27
- export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, } from "./basket.js";
27
+ export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, BasketSelectionLine, } from "./basket.js";
28
28
  export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
29
29
  export type { OtpMachine, OtpMachineOptions, OtpFailureContext, OtpResponseOptions, OtpState, OtpStatus, OtpTransport, OtpTransportResult, OtpStateListener, } from "./otp.js";
30
30
  export { WILAYAS } from "./data/wilayas.js";
31
31
  export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
32
32
  export { isWilayaId } from "./wilaya-id.js";
33
- export { readCouponOutcome, readShippingRates } from "./answer-readers.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";
34
42
  export type { CommuneTuple } from "./data/communes.js";
35
43
  export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
36
44
  export * from "./types.js";
@@ -39,12 +47,12 @@ export * from "./domain.js";
39
47
  export * from "./operations.js";
40
48
  export * from "./product-view.js";
41
49
  export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
42
- 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";
43
51
  export { initialVariant, optionStates, pickOption, variantFor } from "./variant-picker.js";
44
52
  export type { OptionSelection, OptionState, PickerVariant } from "./variant-picker.js";
45
53
  export type { CartQuoteOptions } from "./client/cart.js";
46
54
  export type { OfferTierInput, OfferTierView, QuantityTierView } from "./offer-view.js";
47
- export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
48
- 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";
49
57
  export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
50
58
  export type { MetaCartPayload, MetaCatalogPayload, MetaContent, MetaEventName, MetaPixelFunction, MetaPixelOptions, MetaPixelTracker, } from "./meta-tracking.js";
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
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";
@@ -22,7 +22,12 @@ export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailu
22
22
  export { WILAYAS } from "./data/wilayas.js";
23
23
  export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
24
24
  export { isWilayaId } from "./wilaya-id.js";
25
- export { readCouponOutcome, readShippingRates } from "./answer-readers.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";
26
31
  export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
27
32
  export * from "./types.js";
28
33
  export * from "./schemas.js";
@@ -30,7 +35,7 @@ export * from "./domain.js";
30
35
  export * from "./operations.js";
31
36
  export * from "./product-view.js";
32
37
  export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
33
- 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";
34
39
  export { initialVariant, optionStates, pickOption, variantFor } from "./variant-picker.js";
35
- export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
40
+ export { ABANDONED_CHECKOUT_SESSION_KEY, convertAbandonedCheckout, createAbandonedCheckoutTracker, createBrowserAbandonedCheckoutTracker, } from "./abandoned-checkout.js";
36
41
  export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
@@ -10,6 +10,7 @@ export declare const STOREFRONT_OPERATIONS: {
10
10
  readonly categoriesList: "categories.list";
11
11
  readonly productsGet: "products.get";
12
12
  readonly productsList: "products.list";
13
+ readonly productsSearch: "products.search";
13
14
  readonly productsReviewsList: "products.reviews.list";
14
15
  readonly productsReviewsSubmit: "products.reviews.submit";
15
16
  readonly productsReviewsEligibility: "products.reviews.eligibility";
@@ -25,6 +26,7 @@ export declare const STOREFRONT_OPERATIONS: {
25
26
  readonly pageDataGet: "pageData.get";
26
27
  readonly ordersCreate: "orders.create";
27
28
  readonly ordersTracking: "orders.tracking";
29
+ readonly ordersStatus: "orders.status";
28
30
  readonly ordersAbandonedUpsert: "orders.abandoned.upsert";
29
31
  readonly ordersAbandonedConvert: "orders.abandoned.convert";
30
32
  readonly ordersOtpSend: "orders.otp.send";
@@ -52,6 +54,7 @@ export declare const STOREFRONT_RETRY_POLICIES: {
52
54
  readonly "orders.otp.send": "never";
53
55
  readonly "orders.otp.verify": "never";
54
56
  readonly "orders.tracking": "safe-read";
57
+ readonly "orders.status": "never";
55
58
  readonly "pageData.get": "safe-read";
56
59
  readonly "pages.get": "safe-read";
57
60
  readonly "pages.landing": "safe-read";
@@ -59,6 +62,7 @@ export declare const STOREFRONT_RETRY_POLICIES: {
59
62
  readonly "collections.get": "safe-read";
60
63
  readonly "products.get": "safe-read";
61
64
  readonly "products.list": "safe-read";
65
+ readonly "products.search": "safe-read";
62
66
  readonly "products.reviews.list": "safe-read";
63
67
  readonly "products.reviews.submit": "never";
64
68
  readonly "products.reviews.eligibility": "never";