@wocart/storefront 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/USAGE.md +317 -4
- package/dist/abandoned-checkout.d.ts +45 -0
- package/dist/abandoned-checkout.js +81 -3
- package/dist/answer-readers.d.ts +11 -4
- package/dist/answer-readers.js +114 -0
- package/dist/astro.d.ts +4 -0
- package/dist/astro.js +2 -0
- package/dist/basket.d.ts +11 -1
- package/dist/basket.js +17 -0
- package/dist/catalog.d.ts +24 -0
- package/dist/catalog.js +33 -0
- package/dist/client/orders.d.ts +10 -1
- package/dist/client/orders.js +8 -1
- package/dist/client/products.d.ts +13 -3
- package/dist/client/products.js +35 -12
- package/dist/coupon.d.ts +16 -1
- package/dist/coupon.js +37 -0
- package/dist/endpoints/storefront-routes.d.ts +8 -2
- package/dist/endpoints/storefront-routes.js +9 -2
- package/dist/index.d.ts +15 -7
- package/dist/index.js +9 -4
- package/dist/operations.d.ts +4 -0
- package/dist/operations.js +4 -0
- package/dist/order-lookup.d.ts +51 -0
- package/dist/order-lookup.js +44 -0
- package/dist/order-number.d.ts +9 -0
- package/dist/order-number.js +11 -0
- package/dist/product-view.d.ts +8 -0
- package/dist/product-view.js +26 -4
- package/dist/quote.d.ts +27 -0
- package/dist/quote.js +43 -0
- package/dist/review-capture.d.ts +77 -0
- package/dist/review-capture.js +172 -0
- package/dist/review-media.d.ts +8 -75
- package/dist/review-media.js +7 -170
- package/dist/review-session.d.ts +97 -0
- package/dist/review-session.js +157 -0
- package/dist/reviews.d.ts +33 -0
- package/dist/reviews.js +41 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +3 -0
- package/dist/schemas.d.ts +98 -0
- package/dist/schemas.js +31 -0
- package/dist/seo.d.ts +47 -0
- package/dist/seo.js +87 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/sitemap.d.ts +22 -0
- package/dist/sitemap.js +51 -0
- package/dist/store-request.d.ts +96 -0
- package/dist/store-request.js +102 -0
- package/dist/types.d.ts +50 -2
- package/dist/utils/image.d.ts +48 -0
- package/dist/utils/image.js +51 -0
- package/package.json +1 -1
package/dist/answer-readers.d.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Readers for what this package's server parts hand the browser: the delivery
|
|
3
|
-
* rates a page carries and the coupon outcome
|
|
3
|
+
* rates a page carries, a cart quote, and the coupon outcome in it.
|
|
4
4
|
*
|
|
5
|
-
* The server built
|
|
6
|
-
* the browser need not run those schemas. A page can still be cached apart
|
|
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
7
|
* from its scripts, so a value of another version's shape is possible; these
|
|
8
8
|
* accept exactly what the schemas accept and return what they would — extra
|
|
9
9
|
* keys left out — without bringing Zod into a shopper's page. A property test
|
|
10
10
|
* holds each reader to its schema.
|
|
11
11
|
*/
|
|
12
|
-
import type { CouponOutcome, ShippingRates } from "./types.js";
|
|
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;
|
|
13
20
|
/** The delivery rates of a value (`ShippingRates`), or null when it is not a rate table. */
|
|
14
21
|
export declare function readShippingRates(value: unknown): ShippingRates | null;
|
|
15
22
|
/** What a coupon did in a quote (`CouponOutcome`), or null when the value is not one. */
|
package/dist/answer-readers.js
CHANGED
|
@@ -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,7 @@ 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 { lookupOrder, orderLookupInput } from "./order-lookup.js";
|
|
10
|
+
export type { OrderLookup, OrderLookupInput } from "./order-lookup.js";
|
|
11
|
+
export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
|
12
|
+
export type { NotFoundContext, ResolvedStore, StoreRequestOptions, StoreRequestResolution } from "./store-request.js";
|
package/dist/astro.js
CHANGED
|
@@ -2,3 +2,5 @@ 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 { lookupOrder, orderLookupInput } from "./order-lookup.js";
|
|
6
|
+
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:
|
|
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,24 @@
|
|
|
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 steps an order passes on its way to the shopper, in order — what a
|
|
15
|
+
* tracking page's progress shows. `returned` and `cancelled` end an order off
|
|
16
|
+
* this path, and a stage a newer server adds is on none of it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SHOPPER_ORDER_JOURNEY: readonly ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered"];
|
|
19
|
+
/**
|
|
20
|
+
* The collections every store starts with, the likeliest to hold products
|
|
21
|
+
* first: what to show when a page needs a collection and none was named (an
|
|
22
|
+
* editor's preview of the collection template).
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_COLLECTION_HANDLES: readonly ["best-sellers", "new-arrivals", "featured"];
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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 steps an order passes on its way to the shopper, in order — what a
|
|
24
|
+
* tracking page's progress shows. `returned` and `cancelled` end an order off
|
|
25
|
+
* this path, and a stage a newer server adds is on none of it.
|
|
26
|
+
*/
|
|
27
|
+
export const SHOPPER_ORDER_JOURNEY = ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered"];
|
|
28
|
+
/**
|
|
29
|
+
* The collections every store starts with, the likeliest to hold products
|
|
30
|
+
* first: what to show when a page needs a collection and none was named (an
|
|
31
|
+
* editor's preview of the collection template).
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_COLLECTION_HANDLES = ["best-sellers", "new-arrivals", "featured"];
|
package/dist/client/orders.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/client/orders.js
CHANGED
|
@@ -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,
|
|
35
|
-
* asked for one row more than the page,
|
|
36
|
-
*
|
|
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
|
*/
|
package/dist/client/products.js
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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,
|
|
7
|
-
*
|
|
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,
|
|
7
|
-
*
|
|
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
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { z } from "zod";
|
|
25
25
|
import { AbandonedCheckoutInputSchema, BasketLineInputSchema, DeliveryDestinationSchema, ProductQuoteInputSchema, WilayaIdSchema, } from "../domain.js";
|
|
26
26
|
import { MAX_BASKET_LINES } from "../basket-line.js";
|
|
27
|
+
import { orderLookupInput } from "../order-lookup.js";
|
|
27
28
|
import { createProxyEndpoint, } from "./proxy.js";
|
|
28
29
|
export const REVIEWS_PAGE_DEFAULT = 20;
|
|
29
30
|
export const REVIEWS_PAGE_MAX = 50;
|
|
@@ -191,5 +192,11 @@ export function createStorefrontRoutes(options) {
|
|
|
191
192
|
successBody: (data) => ({ success: true, data }),
|
|
192
193
|
call: (client, review) => client.products.reviews.submit(defined(review)),
|
|
193
194
|
}),
|
|
195
|
+
orderStatus: route({
|
|
196
|
+
method: "POST",
|
|
197
|
+
inputSchema: orderLookupInput,
|
|
198
|
+
forwardAttribution: true,
|
|
199
|
+
call: (client, proof, headers) => client.orders.status(proof, headers),
|
|
200
|
+
}),
|
|
194
201
|
};
|
|
195
202
|
}
|
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_JOURNEY, 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";
|