@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
@@ -1,5 +1,10 @@
1
1
  import { toLocalAlgerianMobile } from "./phone.js";
2
- const SESSION_KEY = "cod_session_id";
2
+ /**
3
+ * Where the browser keeps the checkout's id for the tab (`sessionStorage`):
4
+ * every capture of one checkout updates one record, and the thank-you page
5
+ * marks that record converted.
6
+ */
7
+ export const ABANDONED_CHECKOUT_SESSION_KEY = "cod_session_id";
3
8
  const DEBOUNCE_MS = 3000;
4
9
  export function createAbandonedCheckoutTracker(ports) {
5
10
  let sessionId = readSessionId(ports);
@@ -76,14 +81,87 @@ function readCookie(ports, name) {
76
81
  }
77
82
  function readSessionId(ports) {
78
83
  try {
79
- const existing = ports.storage.getItem(SESSION_KEY);
84
+ const existing = ports.storage.getItem(ABANDONED_CHECKOUT_SESSION_KEY);
80
85
  if (existing)
81
86
  return existing;
82
87
  const created = ports.createId();
83
- ports.storage.setItem(SESSION_KEY, created);
88
+ ports.storage.setItem(ABANDONED_CHECKOUT_SESSION_KEY, created);
84
89
  return created;
85
90
  }
86
91
  catch {
87
92
  return ports.createId();
88
93
  }
89
94
  }
95
+ /**
96
+ * `createAbandonedCheckoutTracker` wired to the page: the checkout's id in
97
+ * `sessionStorage`, timers, the Meta cookies, a `keepalive` POST to the
98
+ * theme's route, and `sendBeacon` when the shopper leaves. A page that blocks
99
+ * storage still captures; nothing it does can fail the order.
100
+ */
101
+ export function createBrowserAbandonedCheckoutTracker(options, host = globalThis) {
102
+ const post = (payload) => (host.fetch ?? globalThis.fetch)(options.endpoint, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify(payload),
106
+ keepalive: true,
107
+ });
108
+ return createAbandonedCheckoutTracker({
109
+ storage: {
110
+ getItem: (key) => host.sessionStorage?.getItem(key) ?? null,
111
+ setItem: (key, value) => host.sessionStorage?.setItem(key, value),
112
+ },
113
+ createId: () => uuid(host),
114
+ schedule: (callback, delayMs) => host.setTimeout(callback, delayMs),
115
+ cancel: (handle) => host.clearTimeout(handle),
116
+ readCookie: (name) => cookieValue(host.document?.cookie ?? "", name),
117
+ send: async (payload) => {
118
+ await post(payload);
119
+ },
120
+ beacon: (payload) => {
121
+ const sent = host.navigator?.sendBeacon?.(options.endpoint, new Blob([JSON.stringify(payload)], { type: "application/json" })) ?? false;
122
+ if (!sent)
123
+ void post(payload).catch(() => undefined);
124
+ },
125
+ });
126
+ }
127
+ /**
128
+ * Marks the tab's checkout converted, from the thank-you page: the merchant
129
+ * no longer sees the order it became as abandoned. The id is used once and
130
+ * forgotten. Best-effort — it never throws and never holds the page.
131
+ */
132
+ export function convertAbandonedCheckout(conversion, host = globalThis) {
133
+ try {
134
+ const sessionId = host.sessionStorage?.getItem(ABANDONED_CHECKOUT_SESSION_KEY);
135
+ if (!sessionId || !conversion.orderId)
136
+ return;
137
+ host.sessionStorage?.removeItem(ABANDONED_CHECKOUT_SESSION_KEY);
138
+ void (host.fetch ?? globalThis.fetch)(conversion.endpoint, {
139
+ method: "POST",
140
+ headers: { "Content-Type": "application/json" },
141
+ body: JSON.stringify({ sessionId, orderId: conversion.orderId, orderNumber: conversion.orderNumber }),
142
+ keepalive: true,
143
+ }).catch(() => undefined);
144
+ }
145
+ catch {
146
+ return;
147
+ }
148
+ }
149
+ function cookieValue(cookies, name) {
150
+ for (const part of cookies.split(";")) {
151
+ const separator = part.indexOf("=");
152
+ if (separator > 0 && part.slice(0, separator).trim() === name)
153
+ return part.slice(separator + 1).trim() || undefined;
154
+ }
155
+ return undefined;
156
+ }
157
+ /** A version 4 UUID: the platform's `randomUUID`, else one drawn from `getRandomValues`. */
158
+ function uuid(host) {
159
+ const crypto = host.crypto ?? globalThis.crypto;
160
+ if (typeof crypto.randomUUID === "function")
161
+ return crypto.randomUUID();
162
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
163
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
164
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
165
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
166
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
167
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Readers for what this package's server parts hand the browser: the delivery
3
+ * rates a page carries, a cart quote, and the coupon outcome in it.
4
+ *
5
+ * The server built them with `ShippingRatesSchema`, `CartValidationSchema` and
6
+ * `CouponOutcomeSchema`, so the browser need not run those schemas. A page can still be cached apart
7
+ * from its scripts, so a value of another version's shape is possible; these
8
+ * accept exactly what the schemas accept and return what they would — extra
9
+ * keys left out — without bringing Zod into a shopper's page. A property test
10
+ * holds each reader to its schema.
11
+ */
12
+ import type { CartValidation, CouponOutcome, ShippingRates } from "./types.js";
13
+ /**
14
+ * A cart quote (`CartValidation`) — the `data` of the theme's cart-validate
15
+ * route — or null when the value is not one. What the quote says is the
16
+ * server's: lines re-priced from the catalog, rewards, the subtotal, the free
17
+ * delivery progress, and, when asked, the delivery fee and the coupon's outcome.
18
+ */
19
+ export declare function readCartQuote(value: unknown): CartValidation | null;
20
+ /** The delivery rates of a value (`ShippingRates`), or null when it is not a rate table. */
21
+ export declare function readShippingRates(value: unknown): ShippingRates | null;
22
+ /** What a coupon did in a quote (`CouponOutcome`), or null when the value is not one. */
23
+ export declare function readCouponOutcome(value: unknown): CouponOutcome | null;
@@ -0,0 +1,155 @@
1
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
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
+ }
117
+ /** The delivery rates of a value (`ShippingRates`), or null when it is not a rate table. */
118
+ export function readShippingRates(value) {
119
+ if (!isObject(value))
120
+ return null;
121
+ const rates = {};
122
+ for (const [wilaya, entry] of Object.entries(value)) {
123
+ if (wilaya === "__proto__")
124
+ continue;
125
+ if (!isObject(entry) || !isFiniteNumber(entry.home) || !isFiniteNumber(entry.stopDesk))
126
+ return null;
127
+ rates[wilaya] = { home: entry.home, stopDesk: entry.stopDesk };
128
+ }
129
+ return rates;
130
+ }
131
+ /** What a coupon did in a quote (`CouponOutcome`), or null when the value is not one. */
132
+ export function readCouponOutcome(value) {
133
+ if (!isObject(value) || typeof value.code !== "string")
134
+ return null;
135
+ const { code } = value;
136
+ if (value.applied === true) {
137
+ if (!isFiniteNumber(value.discount) || typeof value.freeShipping !== "boolean")
138
+ return null;
139
+ return { code, applied: true, discount: value.discount, freeShipping: value.freeShipping };
140
+ }
141
+ if (value.applied !== false || typeof value.reason !== "string")
142
+ return null;
143
+ const { reason, shortBy, startsAt } = value;
144
+ if (shortBy !== undefined && !isFiniteNumber(shortBy))
145
+ return null;
146
+ if (startsAt !== undefined && typeof startsAt !== "string")
147
+ return null;
148
+ return {
149
+ code,
150
+ applied: false,
151
+ reason,
152
+ ...(shortBy === undefined ? {} : { shortBy }),
153
+ ...(startsAt === undefined ? {} : { startsAt }),
154
+ };
155
+ }
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";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A basket line's identity and the server's limits for one basket: what the
3
+ * browser's basket needs to keep itself well-formed. Plain values and no
4
+ * schema — this module runs in every shopper's browser that holds a basket,
5
+ * and the schemas that check a basket on the server (`domain.ts`) read the
6
+ * same limits from here.
7
+ */
8
+ /** The server's hard limits for one canonical browser basket. */
9
+ export declare const MAX_BASKET_LINES: 20;
10
+ export declare const MAX_BASKET_LINE_QUANTITY: 100;
11
+ /**
12
+ * Product + variant is the identity of a basket line. A null variant is a
13
+ * real identity, not the same thing as any selected variant.
14
+ */
15
+ export declare function basketLineKey(productId: string, variantId: string | null): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A basket line's identity and the server's limits for one basket: what the
3
+ * browser's basket needs to keep itself well-formed. Plain values and no
4
+ * schema — this module runs in every shopper's browser that holds a basket,
5
+ * and the schemas that check a basket on the server (`domain.ts`) read the
6
+ * same limits from here.
7
+ */
8
+ /** The server's hard limits for one canonical browser basket. */
9
+ export const MAX_BASKET_LINES = 20;
10
+ export const MAX_BASKET_LINE_QUANTITY = 100;
11
+ /**
12
+ * Product + variant is the identity of a basket line. A null variant is a
13
+ * real identity, not the same thing as any selected variant.
14
+ */
15
+ export function basketLineKey(productId, variantId) {
16
+ return `${productId}\u0000${variantId ?? ""}`;
17
+ }
package/dist/basket.d.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  * (or memory fake in server/tests). The server always validates and re-prices
10
10
  * every line at checkout.
11
11
  */
12
- import { type BasketLine, type BasketLineInput } from "./domain.js";
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
@@ -9,7 +9,7 @@
9
9
  * (or memory fake in server/tests). The server always validates and re-prices
10
10
  * every line at checkout.
11
11
  */
12
- import { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey, } from "./domain.js";
12
+ import { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
13
13
  /** Default key used in browser storage, versioned to prevent schema drift crashes. */
14
14
  export const DEFAULT_BASKET_STORAGE_KEY = "cod_cart_v1";
15
15
  /**
@@ -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"];
@@ -12,7 +12,7 @@
12
12
  * keeps its optimistic view and checkout re-validates everything anyway.
13
13
  */
14
14
  import { CartValidationSchema } from "../schemas.js";
15
- import { STOREFRONT_OPERATIONS } from "../domain.js";
15
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
16
16
  import { localizedPath } from "./paths.js";
17
17
  /**
18
18
  * Create cart namespace
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { z } from "zod";
14
14
  import { CategorySchema } from "../schemas.js";
15
- import { STOREFRONT_OPERATIONS } from "../domain.js";
15
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
16
16
  import { localizedPath } from "./paths.js";
17
17
  import { reportDegradedRead } from "./core.js";
18
18
  /**
@@ -10,7 +10,7 @@
10
10
  * failed read — the page answers a real 404.
11
11
  */
12
12
  import { StoreCollectionSchema } from "../schemas.js";
13
- import { STOREFRONT_OPERATIONS } from "../domain.js";
13
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
14
14
  import { localizedPath } from "./paths.js";
15
15
  import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
16
16
  export function createCollectionsNamespace(core) {
@@ -15,7 +15,7 @@
15
15
  * outage recovers on the next request.
16
16
  */
17
17
  import { StoreConfigSchema } from "../schemas.js";
18
- import { STOREFRONT_OPERATIONS } from "../domain.js";
18
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
19
19
  import { localizedPath } from "./paths.js";
20
20
  import { reportDegradedRead } from "./core.js";
21
21
  /**
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { z } from "zod";
16
16
  import { CommuneSchema } from "../schemas.js";
17
- import { STOREFRONT_OPERATIONS } from "../domain.js";
17
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
18
18
  import { reportDegradedRead } from "./core.js";
19
19
  import { WILAYAS } from "../data/wilayas.js";
20
20
  /**
@@ -11,7 +11,7 @@
11
11
  * read — the theme renders its own fallback navigation.
12
12
  */
13
13
  import { StoreMenuSchema } from "../schemas.js";
14
- import { STOREFRONT_OPERATIONS } from "../domain.js";
14
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
15
15
  import { localizedPath } from "./paths.js";
16
16
  import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
17
17
  export function createMenusNamespace(core) {
@@ -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,8 +11,8 @@
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";
15
- import { STOREFRONT_OPERATIONS } from "../domain.js";
14
+ import { CreatedOrderSchema, IdResponseSchema, OrderTrackingSchema, OtpSendResultSchema, ShopperOrderStatusSchema, OtpVerifyResultSchema, } from "../schemas.js";
15
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
16
16
  import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
17
17
  /**
18
18
  * Create orders namespace
@@ -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
@@ -17,7 +17,7 @@
17
17
  * the next render, subject only to the theme's route cache).
18
18
  */
19
19
  import { PageDocumentPayloadSchema } from "../schemas.js";
20
- import { STOREFRONT_OPERATIONS } from "../domain.js";
20
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
21
21
  import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
22
22
  /**
23
23
  * Create the page-data namespace.
@@ -12,7 +12,7 @@
12
12
  * redirects to the store home instead of a dead end.
13
13
  */
14
14
  import { LandingPageSchema, StorePagePublicSchema } from "../schemas.js";
15
- import { STOREFRONT_OPERATIONS } from "../domain.js";
15
+ import { STOREFRONT_OPERATIONS } from "../operations.js";
16
16
  import { localizedPath } from "./paths.js";
17
17
  import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
18
18
  /**
@@ -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
  */