@wocart/storefront 0.0.0-stage → 1.0.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 +66 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +143 -2
- package/USAGE.md +1465 -0
- package/dist/abandoned-checkout.d.ts +22 -0
- package/dist/abandoned-checkout.js +89 -0
- package/dist/actions.d.ts +117 -0
- package/dist/actions.js +196 -0
- package/dist/astro.d.ts +6 -0
- package/dist/astro.js +3 -0
- package/dist/basket.d.ts +87 -0
- package/dist/basket.js +258 -0
- package/dist/client/cart.d.ts +30 -0
- package/dist/client/cart.js +26 -0
- package/dist/client/categories.d.ts +25 -0
- package/dist/client/categories.js +37 -0
- package/dist/client/config.d.ts +29 -0
- package/dist/client/config.js +41 -0
- package/dist/client/core.d.ts +86 -0
- package/dist/client/core.js +582 -0
- package/dist/client/errors.d.ts +19 -0
- package/dist/client/errors.js +31 -0
- package/dist/client/geo.d.ts +31 -0
- package/dist/client/geo.js +39 -0
- package/dist/client/index.d.ts +39 -0
- package/dist/client/index.js +80 -0
- package/dist/client/orders.d.ts +68 -0
- package/dist/client/orders.js +76 -0
- package/dist/client/page-data.d.ts +31 -0
- package/dist/client/page-data.js +37 -0
- package/dist/client/pages.d.ts +33 -0
- package/dist/client/pages.js +44 -0
- package/dist/client/paths.d.ts +8 -0
- package/dist/client/paths.js +10 -0
- package/dist/client/products.d.ts +56 -0
- package/dist/client/products.js +78 -0
- package/dist/client/shipping.d.ts +37 -0
- package/dist/client/shipping.js +48 -0
- package/dist/data/communes.d.ts +25 -0
- package/dist/data/communes.js +1690 -0
- package/dist/data/wilayas.d.ts +6 -0
- package/dist/data/wilayas.js +64 -0
- package/dist/domain.d.ts +332 -0
- package/dist/domain.js +214 -0
- package/dist/endpoints/proxy.d.ts +111 -0
- package/dist/endpoints/proxy.js +161 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +30 -0
- package/dist/locale-routing.d.ts +59 -0
- package/dist/locale-routing.js +62 -0
- package/dist/locales.d.ts +15 -0
- package/dist/locales.js +8 -0
- package/dist/meta-tracking.d.ts +34 -0
- package/dist/meta-tracking.js +72 -0
- package/dist/middleware.d.ts +100 -0
- package/dist/middleware.js +72 -0
- package/dist/offer-view.d.ts +10 -0
- package/dist/offer-view.js +13 -0
- package/dist/otp.d.ts +80 -0
- package/dist/otp.js +295 -0
- package/dist/phone.d.ts +2 -0
- package/dist/phone.js +34 -0
- package/dist/product-view.d.ts +12 -0
- package/dist/product-view.js +42 -0
- package/dist/routes.d.ts +45 -0
- package/dist/routes.js +59 -0
- package/dist/schemas.d.ts +753 -0
- package/dist/schemas.js +377 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +2 -0
- package/dist/types.d.ts +418 -0
- package/dist/types.js +1 -0
- package/dist/utils/image.d.ts +140 -0
- package/dist/utils/image.js +131 -0
- package/dist/utils/money.d.ts +39 -0
- package/dist/utils/money.js +43 -0
- package/dist/utils/product-image.d.ts +37 -0
- package/dist/utils/product-image.js +38 -0
- package/dist/whatsapp.d.ts +55 -0
- package/dist/whatsapp.js +89 -0
- package/package.json +76 -4
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Algeria's 58 wilayas — static reference data, also served through
|
|
3
|
+
* `geo.wilayas()`.
|
|
4
|
+
*/
|
|
5
|
+
export const WILAYAS = [
|
|
6
|
+
{ id: 1, name: "Adrar", nameAr: "أدرار" },
|
|
7
|
+
{ id: 2, name: "Chlef", nameAr: "الشلف" },
|
|
8
|
+
{ id: 3, name: "Laghouat", nameAr: "الأغواط" },
|
|
9
|
+
{ id: 4, name: "Oum El Bouaghi", nameAr: "أم البواقي" },
|
|
10
|
+
{ id: 5, name: "Batna", nameAr: "باتنة" },
|
|
11
|
+
{ id: 6, name: "Béjaïa", nameAr: "بجاية" },
|
|
12
|
+
{ id: 7, name: "Biskra", nameAr: "بسكرة" },
|
|
13
|
+
{ id: 8, name: "Béchar", nameAr: "بشار" },
|
|
14
|
+
{ id: 9, name: "Blida", nameAr: "البليدة" },
|
|
15
|
+
{ id: 10, name: "Bouira", nameAr: "البويرة" },
|
|
16
|
+
{ id: 11, name: "Tamanrasset", nameAr: "تمنراست" },
|
|
17
|
+
{ id: 12, name: "Tébessa", nameAr: "تبسة" },
|
|
18
|
+
{ id: 13, name: "Tlemcen", nameAr: "تلمسان" },
|
|
19
|
+
{ id: 14, name: "Tiaret", nameAr: "تيارت" },
|
|
20
|
+
{ id: 15, name: "Tizi Ouzou", nameAr: "تيزي وزو" },
|
|
21
|
+
{ id: 16, name: "Alger", nameAr: "الجزائر" },
|
|
22
|
+
{ id: 17, name: "Djelfa", nameAr: "الجلفة" },
|
|
23
|
+
{ id: 18, name: "Jijel", nameAr: "جيجل" },
|
|
24
|
+
{ id: 19, name: "Sétif", nameAr: "سطيف" },
|
|
25
|
+
{ id: 20, name: "Saïda", nameAr: "سعيدة" },
|
|
26
|
+
{ id: 21, name: "Skikda", nameAr: "سكيكدة" },
|
|
27
|
+
{ id: 22, name: "Sidi Bel Abbès", nameAr: "سيدي بلعباس" },
|
|
28
|
+
{ id: 23, name: "Annaba", nameAr: "عنابة" },
|
|
29
|
+
{ id: 24, name: "Guelma", nameAr: "قالمة" },
|
|
30
|
+
{ id: 25, name: "Constantine", nameAr: "قسنطينة" },
|
|
31
|
+
{ id: 26, name: "Médéa", nameAr: "المدية" },
|
|
32
|
+
{ id: 27, name: "Mostaganem", nameAr: "مستغانم" },
|
|
33
|
+
{ id: 28, name: "M'Sila", nameAr: "المسيلة" },
|
|
34
|
+
{ id: 29, name: "Mascara", nameAr: "معسكر" },
|
|
35
|
+
{ id: 30, name: "Ouargla", nameAr: "ورقلة" },
|
|
36
|
+
{ id: 31, name: "Oran", nameAr: "وهران" },
|
|
37
|
+
{ id: 32, name: "El Bayadh", nameAr: "البيض" },
|
|
38
|
+
{ id: 33, name: "Illizi", nameAr: "إليزي" },
|
|
39
|
+
{ id: 34, name: "Bordj Bou Arréridj", nameAr: "برج بوعريريج" },
|
|
40
|
+
{ id: 35, name: "Boumerdès", nameAr: "بومرداس" },
|
|
41
|
+
{ id: 36, name: "El Tarf", nameAr: "الطارف" },
|
|
42
|
+
{ id: 37, name: "Tindouf", nameAr: "تندوف" },
|
|
43
|
+
{ id: 38, name: "Tissemsilt", nameAr: "تيسمسيلت" },
|
|
44
|
+
{ id: 39, name: "El Oued", nameAr: "الوادي" },
|
|
45
|
+
{ id: 40, name: "Khenchela", nameAr: "خنشلة" },
|
|
46
|
+
{ id: 41, name: "Souk Ahras", nameAr: "سوق أهراس" },
|
|
47
|
+
{ id: 42, name: "Tipaza", nameAr: "تيبازة" },
|
|
48
|
+
{ id: 43, name: "Mila", nameAr: "ميلة" },
|
|
49
|
+
{ id: 44, name: "Aïn Defla", nameAr: "عين الدفلى" },
|
|
50
|
+
{ id: 45, name: "Naâma", nameAr: "النعامة" },
|
|
51
|
+
{ id: 46, name: "Aïn Témouchent", nameAr: "عين تموشنت" },
|
|
52
|
+
{ id: 47, name: "Ghardaïa", nameAr: "غرداية" },
|
|
53
|
+
{ id: 48, name: "Relizane", nameAr: "غليزان" },
|
|
54
|
+
{ id: 49, name: "Timimoun", nameAr: "تيميمون" },
|
|
55
|
+
{ id: 50, name: "Bordj Badji Mokhtar", nameAr: "برج باجي مختار" },
|
|
56
|
+
{ id: 51, name: "Ouled Djellal", nameAr: "أولاد جلال" },
|
|
57
|
+
{ id: 52, name: "Béni Abbès", nameAr: "بني عباس" },
|
|
58
|
+
{ id: 53, name: "In Salah", nameAr: "عين صالح" },
|
|
59
|
+
{ id: 54, name: "In Guezzam", nameAr: "عين قزام" },
|
|
60
|
+
{ id: 55, name: "Touggourt", nameAr: "تقرت" },
|
|
61
|
+
{ id: 56, name: "Djanet", nameAr: "جانت" },
|
|
62
|
+
{ id: 57, name: "El M'Ghair", nameAr: "المغير" },
|
|
63
|
+
{ id: 58, name: "El Meniaa", nameAr: "المنيعة" },
|
|
64
|
+
];
|
package/dist/domain.d.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The scope of one Store Client. A client is never a cross-store client: its
|
|
4
|
+
* API key selects exactly one store and its caches live inside that client.
|
|
5
|
+
* This type is exported from the browser-safe barrel for type reuse only; the
|
|
6
|
+
* API key must still be supplied and retained by server-side code.
|
|
7
|
+
*/
|
|
8
|
+
export interface StoreScope {
|
|
9
|
+
readonly serverUrl: string;
|
|
10
|
+
readonly apiKey: string;
|
|
11
|
+
/**
|
|
12
|
+
* The key of the theme this client renders (`defineTheme({ key })` in
|
|
13
|
+
* `@wocart/theme`). The store's edits to that theme's text arrive as
|
|
14
|
+
* `StoreConfig.contentOverrides`; without it there are none, because edits
|
|
15
|
+
* belong to the theme that declares their keys. The server owns the key
|
|
16
|
+
* format and refuses a malformed one.
|
|
17
|
+
*/
|
|
18
|
+
readonly theme?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The language this client reads merchant text in (BCP 47, e.g. `fr`) —
|
|
21
|
+
* Shopify Hydrogen's per-request i18n. The server serves it when the store
|
|
22
|
+
* publishes it and the primary otherwise; `StoreConfig.locale` says which.
|
|
23
|
+
* Untranslated fields fall back to the primary one by one. Create one client
|
|
24
|
+
* per language: each keeps its own caches. Omit it for the primary.
|
|
25
|
+
*/
|
|
26
|
+
readonly locale?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Opt-in degradation hook. Reads resolve to `null`/`[]`/`{}` when a call
|
|
29
|
+
* cannot be completed — that is the theme-facing contract and it does not
|
|
30
|
+
* change. But "the network is down" and "the server's response no longer
|
|
31
|
+
* matches the SDK's schemas" must not look identical to the operator, and
|
|
32
|
+
* a degraded read is the one failure a theme never sees. Every swallowed
|
|
33
|
+
* failure is reported here, carrying the same `DomainError` a mutation
|
|
34
|
+
* returns (`code`, `operation`, `requestId`, `retryable`). Never called on
|
|
35
|
+
* success.
|
|
36
|
+
*/
|
|
37
|
+
onError?: (error: DomainError) => void;
|
|
38
|
+
}
|
|
39
|
+
/** The server's hard limits for one canonical browser basket. */
|
|
40
|
+
export declare const MAX_BASKET_LINES: 20;
|
|
41
|
+
export declare const MAX_BASKET_LINE_QUANTITY: 100;
|
|
42
|
+
/**
|
|
43
|
+
* A wilaya id, bounded by the reference dataset itself rather than a literal.
|
|
44
|
+
*
|
|
45
|
+
* The server validates the same 1..58 range; deriving the ceiling from
|
|
46
|
+
* `WILAYAS` keeps the route's early rejection and the platform's accepted
|
|
47
|
+
* range in step.
|
|
48
|
+
*/
|
|
49
|
+
export declare const WilayaIdSchema: z.ZodNumber;
|
|
50
|
+
/**
|
|
51
|
+
* Product + variant is the identity of a basket line. A null variant is a
|
|
52
|
+
* real identity, not the same thing as any selected variant.
|
|
53
|
+
*/
|
|
54
|
+
export declare function basketLineKey(productId: string, variantId: string | null): string;
|
|
55
|
+
/**
|
|
56
|
+
* A browser-owned basket line. `unitPrice` is an optimistic display value;
|
|
57
|
+
* the server re-prices every line during validation and order creation.
|
|
58
|
+
*/
|
|
59
|
+
export declare const BasketLineSchema: z.ZodObject<{
|
|
60
|
+
productId: z.ZodString;
|
|
61
|
+
productName: z.ZodString;
|
|
62
|
+
variantId: z.ZodNullable<z.ZodString>;
|
|
63
|
+
variantLabel: z.ZodNullable<z.ZodString>;
|
|
64
|
+
quantity: z.ZodNumber;
|
|
65
|
+
unitPrice: z.ZodNumber;
|
|
66
|
+
href: z.ZodOptional<z.ZodString>;
|
|
67
|
+
image: z.ZodOptional<z.ZodString>;
|
|
68
|
+
}, z.core.$strip>;
|
|
69
|
+
export type BasketLine = z.infer<typeof BasketLineSchema>;
|
|
70
|
+
/**
|
|
71
|
+
* The server-facing projection of a basket line. UI-only fields are omitted,
|
|
72
|
+
* and `pricePerUnit` remains display-only even when present.
|
|
73
|
+
*/
|
|
74
|
+
export declare const BasketLineInputSchema: z.ZodObject<{
|
|
75
|
+
productId: z.ZodString;
|
|
76
|
+
productName: z.ZodString;
|
|
77
|
+
variantId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
78
|
+
variantLabel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
79
|
+
quantity: z.ZodNumber;
|
|
80
|
+
pricePerUnit: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
offerId: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
export type BasketLineInput = z.infer<typeof BasketLineInputSchema>;
|
|
84
|
+
/**
|
|
85
|
+
* A canonical browser basket. Empty is valid for a persisted cart, but every
|
|
86
|
+
* line must be unique by product + variant. Order submission requires at
|
|
87
|
+
* least one line and is governed by the Checkout Draft invariant below.
|
|
88
|
+
*/
|
|
89
|
+
export declare const BasketSchema: z.ZodObject<{
|
|
90
|
+
lines: z.ZodArray<z.ZodObject<{
|
|
91
|
+
productId: z.ZodString;
|
|
92
|
+
productName: z.ZodString;
|
|
93
|
+
variantId: z.ZodNullable<z.ZodString>;
|
|
94
|
+
variantLabel: z.ZodNullable<z.ZodString>;
|
|
95
|
+
quantity: z.ZodNumber;
|
|
96
|
+
unitPrice: z.ZodNumber;
|
|
97
|
+
href: z.ZodOptional<z.ZodString>;
|
|
98
|
+
image: z.ZodOptional<z.ZodString>;
|
|
99
|
+
}, z.core.$strip>>;
|
|
100
|
+
}, z.core.$strip>;
|
|
101
|
+
export type Basket = z.infer<typeof BasketSchema>;
|
|
102
|
+
/**
|
|
103
|
+
* The basket-line projection used by abandoned-checkout tracking.
|
|
104
|
+
*
|
|
105
|
+
* Variant fields are optional and nullable: a product-page payload has no
|
|
106
|
+
* variant at all and omits the key (the tracker strips it with `?? undefined`),
|
|
107
|
+
* while a checkout-page payload can carry a selected variant — the proxy
|
|
108
|
+
* accepts both, so the schema must too.
|
|
109
|
+
*/
|
|
110
|
+
export declare const AbandonedBasketLineSchema: z.ZodObject<{
|
|
111
|
+
productId: z.ZodString;
|
|
112
|
+
productName: z.ZodString;
|
|
113
|
+
variantId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
114
|
+
variantLabel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
115
|
+
quantity: z.ZodNumber;
|
|
116
|
+
unitPrice: z.ZodNumber;
|
|
117
|
+
}, z.core.$strip>;
|
|
118
|
+
export type AbandonedBasketLine = z.infer<typeof AbandonedBasketLineSchema>;
|
|
119
|
+
/**
|
|
120
|
+
* Runtime schema for the optional abandoned-checkout signal — the body
|
|
121
|
+
* `orders.abandoned.upsert()` accepts.
|
|
122
|
+
*
|
|
123
|
+
* This is the contract, not a convenience: a theme's abandoned-checkout route
|
|
124
|
+
* validates its body with THIS schema rather than a hand-copied twin. Bounds
|
|
125
|
+
* match the server's, with two deliberate client-side differences:
|
|
126
|
+
*
|
|
127
|
+
* - `communeId` is capped at 200 (the server has no bound; a commune id is
|
|
128
|
+
* `c-01-001`, so nothing legitimate is refused and the proxy body stays
|
|
129
|
+
* bounded).
|
|
130
|
+
* - `variantId`/`variantLabel` accept an explicit `null` as well as omission;
|
|
131
|
+
* the server's top-level fields take omission only, and the tracker strips
|
|
132
|
+
* nulls, so nothing a browser sends reaches that difference.
|
|
133
|
+
*/
|
|
134
|
+
export declare const AbandonedCheckoutInputSchema: z.ZodObject<{
|
|
135
|
+
sessionId: z.ZodString;
|
|
136
|
+
customerName: z.ZodString;
|
|
137
|
+
phone: z.ZodString;
|
|
138
|
+
wilayaId: z.ZodOptional<z.ZodNumber>;
|
|
139
|
+
communeId: z.ZodOptional<z.ZodString>;
|
|
140
|
+
wilayaName: z.ZodOptional<z.ZodString>;
|
|
141
|
+
communeName: z.ZodOptional<z.ZodString>;
|
|
142
|
+
productId: z.ZodOptional<z.ZodString>;
|
|
143
|
+
productName: z.ZodOptional<z.ZodString>;
|
|
144
|
+
variantId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
145
|
+
variantLabel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
146
|
+
price: z.ZodOptional<z.ZodNumber>;
|
|
147
|
+
items: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
148
|
+
productId: z.ZodString;
|
|
149
|
+
productName: z.ZodString;
|
|
150
|
+
variantId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
151
|
+
variantLabel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
152
|
+
quantity: z.ZodNumber;
|
|
153
|
+
unitPrice: z.ZodNumber;
|
|
154
|
+
}, z.core.$strip>>>;
|
|
155
|
+
deliveryType: z.ZodOptional<z.ZodEnum<{
|
|
156
|
+
home: "home";
|
|
157
|
+
stop_desk: "stop_desk";
|
|
158
|
+
}>>;
|
|
159
|
+
fbc: z.ZodOptional<z.ZodString>;
|
|
160
|
+
fbp: z.ZodOptional<z.ZodString>;
|
|
161
|
+
}, z.core.$strip>;
|
|
162
|
+
/**
|
|
163
|
+
* Exact public input for the optional abandoned-checkout signal.
|
|
164
|
+
*
|
|
165
|
+
* `variantId`/`variantLabel` follow the same omit-or-null rule as each
|
|
166
|
+
* `items[]` line: absent — not null — is the common wire shape.
|
|
167
|
+
*/
|
|
168
|
+
export type AbandonedCheckoutInput = z.infer<typeof AbandonedCheckoutInputSchema>;
|
|
169
|
+
/** Shopper's selected delivery mode and destination. Policy decides whether address is required. */
|
|
170
|
+
export declare const DeliverySelectionSchema: z.ZodObject<{
|
|
171
|
+
wilayaId: z.ZodNumber;
|
|
172
|
+
communeId: z.ZodString;
|
|
173
|
+
deliveryType: z.ZodEnum<{
|
|
174
|
+
home: "home";
|
|
175
|
+
stop_desk: "stop_desk";
|
|
176
|
+
}>;
|
|
177
|
+
address: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
178
|
+
}, z.core.$strip>;
|
|
179
|
+
export type DeliverySelection = z.infer<typeof DeliverySelectionSchema>;
|
|
180
|
+
/** A shopper answer keyed by the server-minted checkout-field id. */
|
|
181
|
+
export interface CheckoutFieldAnswer {
|
|
182
|
+
readonly id: string;
|
|
183
|
+
readonly value: string;
|
|
184
|
+
}
|
|
185
|
+
export interface CheckoutAttribution {
|
|
186
|
+
readonly fbc: string | null;
|
|
187
|
+
readonly fbp: string | null;
|
|
188
|
+
readonly landingPageSlug: string | null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Mutable checkout intent before the server accepts an order.
|
|
192
|
+
*
|
|
193
|
+
* Invariant: `mode: "single"` has exactly one line; `mode: "basket"` has
|
|
194
|
+
* one or more lines. This is intentionally a domain contract, not a second
|
|
195
|
+
* copy of merchant checkout-policy validation: the server remains the owner
|
|
196
|
+
* of required fields, legal pages, prices, stock and delivery rules.
|
|
197
|
+
*/
|
|
198
|
+
export interface CheckoutDraft {
|
|
199
|
+
readonly mode: "single" | "basket";
|
|
200
|
+
readonly lines: readonly BasketLineInput[];
|
|
201
|
+
readonly customerName: string;
|
|
202
|
+
readonly phone: string;
|
|
203
|
+
readonly delivery: DeliverySelection;
|
|
204
|
+
readonly address?: string | null;
|
|
205
|
+
readonly email?: string | null;
|
|
206
|
+
readonly notes?: string | null;
|
|
207
|
+
readonly customFieldResponses: readonly CheckoutFieldAnswer[];
|
|
208
|
+
readonly otpProof: OtpProof | null;
|
|
209
|
+
readonly attribution: CheckoutAttribution;
|
|
210
|
+
}
|
|
211
|
+
/** Proof that the checkout phone was verified or that the provider failed open. */
|
|
212
|
+
export declare const OtpProofSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
213
|
+
kind: z.ZodLiteral<"verified">;
|
|
214
|
+
phone: z.ZodString;
|
|
215
|
+
token: z.ZodString;
|
|
216
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
217
|
+
kind: z.ZodLiteral<"bypass">;
|
|
218
|
+
phone: z.ZodString;
|
|
219
|
+
token: z.ZodString;
|
|
220
|
+
reason: z.ZodString;
|
|
221
|
+
}, z.core.$strip>], "kind">;
|
|
222
|
+
export type OtpProof = z.infer<typeof OtpProofSchema>;
|
|
223
|
+
/** A server-accepted purchase. Totals are authoritative only after acceptance. */
|
|
224
|
+
export declare const OrderSchema: z.ZodObject<{
|
|
225
|
+
orderId: z.ZodString;
|
|
226
|
+
orderNumber: z.ZodString;
|
|
227
|
+
price: z.ZodNumber;
|
|
228
|
+
deliveryFee: z.ZodNumber;
|
|
229
|
+
total: z.ZodNumber;
|
|
230
|
+
}, z.core.$strip>;
|
|
231
|
+
export type Order = z.infer<typeof OrderSchema>;
|
|
232
|
+
/** Server-owned decision for the thank-you page's conversion event. */
|
|
233
|
+
export declare const TrackingDecisionSchema: z.ZodObject<{
|
|
234
|
+
pixelId: z.ZodNullable<z.ZodString>;
|
|
235
|
+
event: z.ZodNullable<z.ZodEnum<{
|
|
236
|
+
Lead: "Lead";
|
|
237
|
+
Purchase: "Purchase";
|
|
238
|
+
}>>;
|
|
239
|
+
}, z.core.$strip>;
|
|
240
|
+
export type TrackingDecision = z.infer<typeof TrackingDecisionSchema>;
|
|
241
|
+
export type DomainErrorCategory = "validation" | "authentication" | "business" | "system" | "transport" | "unknown";
|
|
242
|
+
export type SideEffectOutcome = "not_started" | "rejected" | "committed" | "unknown";
|
|
243
|
+
export interface DomainFieldIssue {
|
|
244
|
+
readonly field: string;
|
|
245
|
+
readonly code: string;
|
|
246
|
+
readonly message: string;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The error shape every SDK failure normalizes to. Keeping
|
|
250
|
+
* category, retry safety, and side-effect outcome together prevents a generic
|
|
251
|
+
* retry from duplicating orders, OTP requests, reviews, or abandonment writes.
|
|
252
|
+
*/
|
|
253
|
+
export interface DomainError {
|
|
254
|
+
readonly category: DomainErrorCategory;
|
|
255
|
+
readonly code: string;
|
|
256
|
+
readonly message: string;
|
|
257
|
+
readonly retryable: boolean;
|
|
258
|
+
readonly operation: string;
|
|
259
|
+
readonly sideEffect: SideEffectOutcome;
|
|
260
|
+
readonly fieldIssues?: readonly DomainFieldIssue[];
|
|
261
|
+
readonly retryAfterSeconds?: number;
|
|
262
|
+
/**
|
|
263
|
+
* Correlation id the server logged this failure under (`context.requestId`).
|
|
264
|
+
* The only diagnostic a theme may safely surface — it never carries customer
|
|
265
|
+
* data, and it is how a support request is traced back to one server log line.
|
|
266
|
+
*/
|
|
267
|
+
readonly requestId?: string;
|
|
268
|
+
}
|
|
269
|
+
export type ClientResult<T> = {
|
|
270
|
+
readonly success: true;
|
|
271
|
+
readonly data: T;
|
|
272
|
+
} | {
|
|
273
|
+
readonly success: false;
|
|
274
|
+
readonly error: string;
|
|
275
|
+
readonly code?: string;
|
|
276
|
+
readonly domainError: DomainError;
|
|
277
|
+
readonly [key: string]: unknown;
|
|
278
|
+
};
|
|
279
|
+
export type RetryPolicy = "safe-read" | "never";
|
|
280
|
+
export declare const STOREFRONT_OPERATIONS: {
|
|
281
|
+
readonly configGet: "config.get";
|
|
282
|
+
readonly categoriesList: "categories.list";
|
|
283
|
+
readonly productsGet: "products.get";
|
|
284
|
+
readonly productsList: "products.list";
|
|
285
|
+
readonly productsReviewsList: "products.reviews.list";
|
|
286
|
+
readonly productsReviewsSubmit: "products.reviews.submit";
|
|
287
|
+
readonly pagesGet: "pages.get";
|
|
288
|
+
readonly pagesLanding: "pages.landing";
|
|
289
|
+
readonly shippingRates: "shipping.rates";
|
|
290
|
+
readonly shippingRate: "shipping.rate";
|
|
291
|
+
readonly geoCommunes: "geo.communes";
|
|
292
|
+
readonly cartValidate: "cart.validate";
|
|
293
|
+
readonly pageDataGet: "pageData.get";
|
|
294
|
+
readonly ordersCreate: "orders.create";
|
|
295
|
+
readonly ordersTracking: "orders.tracking";
|
|
296
|
+
readonly ordersAbandonedUpsert: "orders.abandoned.upsert";
|
|
297
|
+
readonly ordersAbandonedConvert: "orders.abandoned.convert";
|
|
298
|
+
readonly ordersOtpSend: "orders.otp.send";
|
|
299
|
+
readonly ordersOtpVerify: "orders.otp.verify";
|
|
300
|
+
};
|
|
301
|
+
export type StorefrontOperation = (typeof STOREFRONT_OPERATIONS)[keyof typeof STOREFRONT_OPERATIONS];
|
|
302
|
+
/**
|
|
303
|
+
* The retry policy per operation, as a contract a caller can read.
|
|
304
|
+
*
|
|
305
|
+
* The transport enforces it by kind: every `safe-read` goes through the
|
|
306
|
+
* caching/deduplicating GET path, which repeats a transport or 5xx failure,
|
|
307
|
+
* while every `never` goes through the POST/PATCH path, which returns a result
|
|
308
|
+
* and never retries (a replayed write is how a shopper ends up with two
|
|
309
|
+
* orders). A test asserts this table covers every public operation and agrees
|
|
310
|
+
* with that classification, so a new operation cannot ship unclassified.
|
|
311
|
+
*/
|
|
312
|
+
export declare const STOREFRONT_RETRY_POLICIES: {
|
|
313
|
+
readonly "cart.validate": "safe-read";
|
|
314
|
+
readonly "categories.list": "safe-read";
|
|
315
|
+
readonly "config.get": "safe-read";
|
|
316
|
+
readonly "geo.communes": "safe-read";
|
|
317
|
+
readonly "orders.abandoned.convert": "never";
|
|
318
|
+
readonly "orders.abandoned.upsert": "never";
|
|
319
|
+
readonly "orders.create": "never";
|
|
320
|
+
readonly "orders.otp.send": "never";
|
|
321
|
+
readonly "orders.otp.verify": "never";
|
|
322
|
+
readonly "orders.tracking": "safe-read";
|
|
323
|
+
readonly "pageData.get": "safe-read";
|
|
324
|
+
readonly "pages.get": "safe-read";
|
|
325
|
+
readonly "pages.landing": "safe-read";
|
|
326
|
+
readonly "products.get": "safe-read";
|
|
327
|
+
readonly "products.list": "safe-read";
|
|
328
|
+
readonly "products.reviews.list": "safe-read";
|
|
329
|
+
readonly "products.reviews.submit": "never";
|
|
330
|
+
readonly "shipping.rate": "safe-read";
|
|
331
|
+
readonly "shipping.rates": "safe-read";
|
|
332
|
+
};
|
package/dist/domain.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { WILAYAS } from "./data/wilayas.js";
|
|
3
|
+
/** The server's hard limits for one canonical browser basket. */
|
|
4
|
+
export const MAX_BASKET_LINES = 20;
|
|
5
|
+
export const MAX_BASKET_LINE_QUANTITY = 100;
|
|
6
|
+
/**
|
|
7
|
+
* A wilaya id, bounded by the reference dataset itself rather than a literal.
|
|
8
|
+
*
|
|
9
|
+
* The server validates the same 1..58 range; deriving the ceiling from
|
|
10
|
+
* `WILAYAS` keeps the route's early rejection and the platform's accepted
|
|
11
|
+
* range in step.
|
|
12
|
+
*/
|
|
13
|
+
export const WilayaIdSchema = z.number().int().min(1).max(WILAYAS.length);
|
|
14
|
+
/**
|
|
15
|
+
* Product + variant is the identity of a basket line. A null variant is a
|
|
16
|
+
* real identity, not the same thing as any selected variant.
|
|
17
|
+
*/
|
|
18
|
+
export function basketLineKey(productId, variantId) {
|
|
19
|
+
return `${productId}\u0000${variantId ?? ""}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A browser-owned basket line. `unitPrice` is an optimistic display value;
|
|
23
|
+
* the server re-prices every line during validation and order creation.
|
|
24
|
+
*/
|
|
25
|
+
export const BasketLineSchema = z.object({
|
|
26
|
+
productId: z.string().min(1).max(200),
|
|
27
|
+
productName: z.string().min(1).max(200),
|
|
28
|
+
variantId: z.string().min(1).max(200).nullable(),
|
|
29
|
+
variantLabel: z.string().max(200).nullable(),
|
|
30
|
+
quantity: z.number().int().min(1).max(MAX_BASKET_LINE_QUANTITY),
|
|
31
|
+
unitPrice: z.number().finite().nonnegative(),
|
|
32
|
+
href: z.string().min(1).optional(),
|
|
33
|
+
image: z.string().min(1).optional(),
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* The server-facing projection of a basket line. UI-only fields are omitted,
|
|
37
|
+
* and `pricePerUnit` remains display-only even when present.
|
|
38
|
+
*/
|
|
39
|
+
export const BasketLineInputSchema = z.object({
|
|
40
|
+
productId: z.string().min(1).max(200),
|
|
41
|
+
productName: z.string().min(1).max(200),
|
|
42
|
+
variantId: z.string().min(1).max(200).nullable().optional(),
|
|
43
|
+
variantLabel: z.string().max(200).nullable().optional(),
|
|
44
|
+
quantity: z.number().int().min(1).max(MAX_BASKET_LINE_QUANTITY),
|
|
45
|
+
pricePerUnit: z.number().finite().nonnegative().optional(),
|
|
46
|
+
offerId: z.string().min(1).max(200).optional(),
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* A canonical browser basket. Empty is valid for a persisted cart, but every
|
|
50
|
+
* line must be unique by product + variant. Order submission requires at
|
|
51
|
+
* least one line and is governed by the Checkout Draft invariant below.
|
|
52
|
+
*/
|
|
53
|
+
export const BasketSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
lines: z.array(BasketLineSchema).max(MAX_BASKET_LINES),
|
|
56
|
+
})
|
|
57
|
+
.superRefine((basket, context) => {
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
basket.lines.forEach((line, index) => {
|
|
60
|
+
const key = basketLineKey(line.productId, line.variantId);
|
|
61
|
+
if (seen.has(key)) {
|
|
62
|
+
context.addIssue({
|
|
63
|
+
code: "custom",
|
|
64
|
+
path: ["lines", index],
|
|
65
|
+
message: "Basket lines must be unique by product and variant.",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
seen.add(key);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* The basket-line projection used by abandoned-checkout tracking.
|
|
73
|
+
*
|
|
74
|
+
* Variant fields are optional and nullable: a product-page payload has no
|
|
75
|
+
* variant at all and omits the key (the tracker strips it with `?? undefined`),
|
|
76
|
+
* while a checkout-page payload can carry a selected variant — the proxy
|
|
77
|
+
* accepts both, so the schema must too.
|
|
78
|
+
*/
|
|
79
|
+
export const AbandonedBasketLineSchema = z.object({
|
|
80
|
+
productId: z.string().min(1).max(200),
|
|
81
|
+
productName: z.string().min(1).max(200),
|
|
82
|
+
variantId: z.string().max(200).nullish(),
|
|
83
|
+
variantLabel: z.string().max(200).nullish(),
|
|
84
|
+
quantity: z.number().int().min(1).max(MAX_BASKET_LINE_QUANTITY),
|
|
85
|
+
unitPrice: z.number().nonnegative(),
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Runtime schema for the optional abandoned-checkout signal — the body
|
|
89
|
+
* `orders.abandoned.upsert()` accepts.
|
|
90
|
+
*
|
|
91
|
+
* This is the contract, not a convenience: a theme's abandoned-checkout route
|
|
92
|
+
* validates its body with THIS schema rather than a hand-copied twin. Bounds
|
|
93
|
+
* match the server's, with two deliberate client-side differences:
|
|
94
|
+
*
|
|
95
|
+
* - `communeId` is capped at 200 (the server has no bound; a commune id is
|
|
96
|
+
* `c-01-001`, so nothing legitimate is refused and the proxy body stays
|
|
97
|
+
* bounded).
|
|
98
|
+
* - `variantId`/`variantLabel` accept an explicit `null` as well as omission;
|
|
99
|
+
* the server's top-level fields take omission only, and the tracker strips
|
|
100
|
+
* nulls, so nothing a browser sends reaches that difference.
|
|
101
|
+
*/
|
|
102
|
+
export const AbandonedCheckoutInputSchema = z.object({
|
|
103
|
+
sessionId: z.string().uuid(),
|
|
104
|
+
customerName: z.string().min(2).max(100),
|
|
105
|
+
phone: z
|
|
106
|
+
.string()
|
|
107
|
+
.min(9)
|
|
108
|
+
.max(20)
|
|
109
|
+
.regex(/^[0-9+\s-]+$/),
|
|
110
|
+
wilayaId: WilayaIdSchema.optional(),
|
|
111
|
+
communeId: z.string().max(200).optional(),
|
|
112
|
+
wilayaName: z.string().max(100).optional(),
|
|
113
|
+
communeName: z.string().max(100).optional(),
|
|
114
|
+
productId: z.string().max(200).optional(),
|
|
115
|
+
productName: z.string().max(200).optional(),
|
|
116
|
+
variantId: z.string().max(200).nullish(),
|
|
117
|
+
variantLabel: z.string().max(200).nullish(),
|
|
118
|
+
price: z.number().positive().optional(),
|
|
119
|
+
/** The whole basket, when the shopper walked away from a cart checkout. */
|
|
120
|
+
items: z
|
|
121
|
+
.array(AbandonedBasketLineSchema)
|
|
122
|
+
.min(1)
|
|
123
|
+
.max(MAX_BASKET_LINES)
|
|
124
|
+
.optional(),
|
|
125
|
+
deliveryType: z.enum(["home", "stop_desk"]).optional(),
|
|
126
|
+
fbc: z.string().max(500).optional(),
|
|
127
|
+
fbp: z.string().max(500).optional(),
|
|
128
|
+
});
|
|
129
|
+
/** Shopper's selected delivery mode and destination. Policy decides whether address is required. */
|
|
130
|
+
export const DeliverySelectionSchema = z.object({
|
|
131
|
+
wilayaId: z.number().int().min(1).max(58),
|
|
132
|
+
communeId: z.string().min(1),
|
|
133
|
+
deliveryType: z.enum(["home", "stop_desk"]),
|
|
134
|
+
address: z.string().max(300).nullable().optional(),
|
|
135
|
+
});
|
|
136
|
+
/** Proof that the checkout phone was verified or that the provider failed open. */
|
|
137
|
+
export const OtpProofSchema = z.discriminatedUnion("kind", [
|
|
138
|
+
z.object({
|
|
139
|
+
kind: z.literal("verified"),
|
|
140
|
+
phone: z.string().min(1),
|
|
141
|
+
token: z.string().min(1),
|
|
142
|
+
}),
|
|
143
|
+
z.object({
|
|
144
|
+
kind: z.literal("bypass"),
|
|
145
|
+
phone: z.string().min(1),
|
|
146
|
+
token: z.string().min(1),
|
|
147
|
+
reason: z.string().min(1),
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
/** A server-accepted purchase. Totals are authoritative only after acceptance. */
|
|
151
|
+
export const OrderSchema = z.object({
|
|
152
|
+
orderId: z.string().min(1),
|
|
153
|
+
orderNumber: z.string().min(1),
|
|
154
|
+
price: z.number().finite().nonnegative(),
|
|
155
|
+
deliveryFee: z.number().finite().nonnegative(),
|
|
156
|
+
total: z.number().finite().nonnegative(),
|
|
157
|
+
});
|
|
158
|
+
/** Server-owned decision for the thank-you page's conversion event. */
|
|
159
|
+
export const TrackingDecisionSchema = z.object({
|
|
160
|
+
pixelId: z.string().nullable(),
|
|
161
|
+
event: z.enum(["Purchase", "Lead"]).nullable(),
|
|
162
|
+
});
|
|
163
|
+
export const STOREFRONT_OPERATIONS = {
|
|
164
|
+
configGet: "config.get",
|
|
165
|
+
categoriesList: "categories.list",
|
|
166
|
+
productsGet: "products.get",
|
|
167
|
+
productsList: "products.list",
|
|
168
|
+
productsReviewsList: "products.reviews.list",
|
|
169
|
+
productsReviewsSubmit: "products.reviews.submit",
|
|
170
|
+
pagesGet: "pages.get",
|
|
171
|
+
pagesLanding: "pages.landing",
|
|
172
|
+
shippingRates: "shipping.rates",
|
|
173
|
+
shippingRate: "shipping.rate",
|
|
174
|
+
geoCommunes: "geo.communes",
|
|
175
|
+
cartValidate: "cart.validate",
|
|
176
|
+
pageDataGet: "pageData.get",
|
|
177
|
+
ordersCreate: "orders.create",
|
|
178
|
+
ordersTracking: "orders.tracking",
|
|
179
|
+
ordersAbandonedUpsert: "orders.abandoned.upsert",
|
|
180
|
+
ordersAbandonedConvert: "orders.abandoned.convert",
|
|
181
|
+
ordersOtpSend: "orders.otp.send",
|
|
182
|
+
ordersOtpVerify: "orders.otp.verify",
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* The retry policy per operation, as a contract a caller can read.
|
|
186
|
+
*
|
|
187
|
+
* The transport enforces it by kind: every `safe-read` goes through the
|
|
188
|
+
* caching/deduplicating GET path, which repeats a transport or 5xx failure,
|
|
189
|
+
* while every `never` goes through the POST/PATCH path, which returns a result
|
|
190
|
+
* and never retries (a replayed write is how a shopper ends up with two
|
|
191
|
+
* orders). A test asserts this table covers every public operation and agrees
|
|
192
|
+
* with that classification, so a new operation cannot ship unclassified.
|
|
193
|
+
*/
|
|
194
|
+
export const STOREFRONT_RETRY_POLICIES = {
|
|
195
|
+
[STOREFRONT_OPERATIONS.cartValidate]: "safe-read",
|
|
196
|
+
[STOREFRONT_OPERATIONS.categoriesList]: "safe-read",
|
|
197
|
+
[STOREFRONT_OPERATIONS.configGet]: "safe-read",
|
|
198
|
+
[STOREFRONT_OPERATIONS.geoCommunes]: "safe-read",
|
|
199
|
+
[STOREFRONT_OPERATIONS.ordersAbandonedConvert]: "never",
|
|
200
|
+
[STOREFRONT_OPERATIONS.ordersAbandonedUpsert]: "never",
|
|
201
|
+
[STOREFRONT_OPERATIONS.ordersCreate]: "never",
|
|
202
|
+
[STOREFRONT_OPERATIONS.ordersOtpSend]: "never",
|
|
203
|
+
[STOREFRONT_OPERATIONS.ordersOtpVerify]: "never",
|
|
204
|
+
[STOREFRONT_OPERATIONS.ordersTracking]: "safe-read",
|
|
205
|
+
[STOREFRONT_OPERATIONS.pageDataGet]: "safe-read",
|
|
206
|
+
[STOREFRONT_OPERATIONS.pagesGet]: "safe-read",
|
|
207
|
+
[STOREFRONT_OPERATIONS.pagesLanding]: "safe-read",
|
|
208
|
+
[STOREFRONT_OPERATIONS.productsGet]: "safe-read",
|
|
209
|
+
[STOREFRONT_OPERATIONS.productsList]: "safe-read",
|
|
210
|
+
[STOREFRONT_OPERATIONS.productsReviewsList]: "safe-read",
|
|
211
|
+
[STOREFRONT_OPERATIONS.productsReviewsSubmit]: "never",
|
|
212
|
+
[STOREFRONT_OPERATIONS.shippingRate]: "safe-read",
|
|
213
|
+
[STOREFRONT_OPERATIONS.shippingRates]: "safe-read",
|
|
214
|
+
};
|