@wocart/storefront 1.0.0 → 1.2.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 +80 -0
- package/README.md +2 -1
- package/USAGE.md +117 -56
- package/dist/actions.d.ts +5 -4
- package/dist/actions.js +14 -9
- package/dist/astro.d.ts +3 -1
- package/dist/astro.js +2 -1
- package/dist/client/core.d.ts +12 -2
- package/dist/client/core.js +73 -79
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.js +4 -3
- package/dist/client/orders.d.ts +5 -2
- package/dist/client/orders.js +11 -5
- package/dist/client/page-data.d.ts +1 -2
- package/dist/client/page-data.js +4 -4
- package/dist/client/pages.d.ts +1 -1
- package/dist/client/pages.js +6 -4
- package/dist/client/products.js +3 -2
- package/dist/client/shipping.d.ts +1 -2
- package/dist/client/shipping.js +1 -2
- package/dist/domain.d.ts +9 -0
- package/dist/endpoints/proxy.d.ts +5 -3
- package/dist/endpoints/proxy.js +21 -4
- package/dist/endpoints/storefront-routes.d.ts +50 -0
- package/dist/endpoints/storefront-routes.js +133 -0
- package/dist/middleware.d.ts +29 -35
- package/dist/middleware.js +32 -42
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,86 @@ follows [semantic versioning](https://semver.org/): breaking changes ship only
|
|
|
5
5
|
in a new major version, and an export is marked `@deprecated` for at least one
|
|
6
6
|
minor release before a major removes it.
|
|
7
7
|
|
|
8
|
+
## 1.2.0 — 2026-10-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`createStorefrontRoutes`** (`@wocart/storefront/astro`): the eight
|
|
13
|
+
same-origin API routes every storefront needs — OTP send and verify, cart
|
|
14
|
+
validation, abandoned-checkout tracking and conversion, communes, and
|
|
15
|
+
reviews (list and submit) — ready to export, so a theme's route file is one
|
|
16
|
+
line. Every POST refuses cross-origin requests, the OTP and abandoned
|
|
17
|
+
routes forward the shopper's address, and input is bounded before it is
|
|
18
|
+
sent. The theme passes only the request's own client.
|
|
19
|
+
- **`runFormAction`** (`@wocart/storefront/astro`) runs an action posted from
|
|
20
|
+
a form and stores its result for the page that POST renders.
|
|
21
|
+
- `orders.otp.send` and `orders.otp.verify` take the shopper's forwarded
|
|
22
|
+
headers as a last argument. The server limits sends and attempts per
|
|
23
|
+
shopper; without them every shopper of a store shared the theme worker's
|
|
24
|
+
one limit.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **A proxy route's failure status follows the server's category** when
|
|
29
|
+
`errorStatus` is unset: 400 for validation, 422 for a business refusal, 429
|
|
30
|
+
when the server named a wait, 504 for a timeout, and 502 for anything the
|
|
31
|
+
shopper cannot fix (and for a failure that carries no `domainError`, as
|
|
32
|
+
before). An explicit `errorStatus` still wins.
|
|
33
|
+
- A client function that throws answers 502 with
|
|
34
|
+
`code: "PROXY_UPSTREAM_FAILED"` and a generic message; the exception's own
|
|
35
|
+
text no longer reaches the browser.
|
|
36
|
+
- `placeOrderInput` refusals name their field without a sentence. The Arabic
|
|
37
|
+
messages it carried reached French and English stores as-is; the theme
|
|
38
|
+
answers each field with its own copy.
|
|
39
|
+
- `placeOrderInput` and `executePlaceOrder` accept a `pricePerUnit` of 0. The
|
|
40
|
+
price is display-only, and a product the merchant prices at 0 was not
|
|
41
|
+
orderable from its product page.
|
|
42
|
+
|
|
43
|
+
### Deprecated
|
|
44
|
+
|
|
45
|
+
- `resolveActionRedirectDecision`: use `runFormAction` and call `next()`. It
|
|
46
|
+
now always answers `{ kind: "next" }`. Its 303 back to the form on an
|
|
47
|
+
error lost the result in Astro 7 — the shopper returned to the form with no
|
|
48
|
+
message — so an errored post now renders in place. Its `referer` and
|
|
49
|
+
`pathname` inputs are unused.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- A server error's `context` can no longer overwrite the result's own
|
|
54
|
+
`success`, `error`, `code` or `domainError` fields.
|
|
55
|
+
- A `*_NOT_FOUND` answer from `products.get`, `pages.get`, `pages.landing`,
|
|
56
|
+
`pageData.get` or `orders.tracking` is absence, not a failure: it no longer
|
|
57
|
+
reaches `onError` as a degraded read.
|
|
58
|
+
|
|
59
|
+
## 1.1.0 — 2026-10-14
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- **A read never waits on another read's pending request.** The client used to
|
|
64
|
+
merge an identical read with one already in flight, and its opt-in caches
|
|
65
|
+
held the pending request itself. A client kept across requests (one per
|
|
66
|
+
isolate) could therefore make a request wait on a read another request had
|
|
67
|
+
started; on Cloudflare Workers that read never settles once the request
|
|
68
|
+
that started it is cancelled (a shopper leaving mid-load, a navigation
|
|
69
|
+
interrupted by a click), and every later request for the same URL hung.
|
|
70
|
+
Each call now makes its own request, and the caches keep finished values
|
|
71
|
+
only.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **`readTimeoutMs`** on `createStoreClient` bounds every read, retries
|
|
76
|
+
included: 10 000 ms by default, `0` for no limit. A read that runs out of
|
|
77
|
+
time degrades like any failed read and reports `REQUEST_TIMEOUT` to
|
|
78
|
+
`onError`, so a server that stops answering costs a page seconds instead of
|
|
79
|
+
holding it open. Writes keep no default limit: a write cut off by a timer
|
|
80
|
+
may already have happened.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- Parallel identical reads are no longer coalesced into one request. A page
|
|
85
|
+
that asks for the same data from several places should ask once and share
|
|
86
|
+
the result within its render.
|
|
87
|
+
|
|
8
88
|
## 1.0.0 — 2026-10-13
|
|
9
89
|
|
|
10
90
|
The first public release.
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ if (result.success) {
|
|
|
69
69
|
| --- | --- | --- |
|
|
70
70
|
| `@wocart/storefront` | browser and server | read-model types and schemas, reference data (58 wilayas, 1,551 communes), money and image helpers, the basket and OTP state machines, language routing, Meta pixel payloads |
|
|
71
71
|
| `@wocart/storefront/server` | server only | `createStoreClient`, `StoreClient`, `ClientResult`, `StorefrontError` — the API key lives here |
|
|
72
|
-
| `@wocart/storefront/astro` | server only | `createProxyEndpoint`, `placeOrderInput`, `executePlaceOrder`, `collectForwardedHeaders`, `
|
|
72
|
+
| `@wocart/storefront/astro` | server only | `createStorefrontRoutes`, `createProxyEndpoint`, `placeOrderInput`, `executePlaceOrder`, `collectForwardedHeaders`, `runFormAction` |
|
|
73
73
|
|
|
74
74
|
Browser code never receives the key. It calls your own same-origin routes
|
|
75
75
|
(for example `src/pages/api/cart.ts`), which call the SDK on the server:
|
|
@@ -115,6 +115,7 @@ import { createBasketStore, formatMoney } from "@wocart/storefront";
|
|
|
115
115
|
| `locale` | no | read merchant text in a published store language (`"fr"`); one client per language |
|
|
116
116
|
| `theme` | no | your theme's key (`defineTheme({ key })`), to receive the merchant's edits to your theme's text |
|
|
117
117
|
| `onError` | no | observability hook for degraded reads |
|
|
118
|
+
| `readTimeoutMs` | no | how long one read may take, retries included, before it degrades (default 10 000; `0` for no limit) |
|
|
118
119
|
|
|
119
120
|
A storefront deployed by Wocart receives its origin and key as the
|
|
120
121
|
`COD_SERVER_URL` and `STORE_API_KEY` environment variables. The SDK reads no
|
package/USAGE.md
CHANGED
|
@@ -162,9 +162,11 @@ const [config, featuredProducts] = await Promise.all([
|
|
|
162
162
|
]);
|
|
163
163
|
~~~
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
Every call makes its own request; a call never waits on another call's
|
|
166
|
+
pending request, even an identical one. Within one render, ask once and share
|
|
167
|
+
the result (`Promise.all`, or a per-request memo) when several parts of a page
|
|
168
|
+
need the same data. A client never holds pending work across calls, so one
|
|
169
|
+
kept for several requests cannot leave a request waiting on another's read.
|
|
168
170
|
|
|
169
171
|
### Available imports
|
|
170
172
|
|
|
@@ -438,10 +440,13 @@ conversion event.
|
|
|
438
440
|
|
|
439
441
|
### Caching and retries
|
|
440
442
|
|
|
441
|
-
- GET requests are fresh by default.
|
|
442
|
-
-
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
- GET requests are fresh by default, and each call makes its own request.
|
|
444
|
+
- config.get(), categories.list(), and geo.communes() keep their finished
|
|
445
|
+
result for the client's lifetime; a call made while the first is still
|
|
446
|
+
pending makes its own request rather than waiting on it.
|
|
447
|
+
- Every read is bounded by `readTimeoutMs` (10 seconds by default), retries
|
|
448
|
+
included; a read that runs out of time degrades and reports
|
|
449
|
+
`REQUEST_TIMEOUT` to `onError`. Writes have no default limit.
|
|
445
450
|
- null results and failures are not cached, so a temporary failure can recover
|
|
446
451
|
on the next call.
|
|
447
452
|
- GET network failures use two short exponential backoff retries.
|
|
@@ -910,6 +915,12 @@ verify returns a verified status with otpToken on success. Failure context can
|
|
|
910
915
|
include attemptsRemaining and a terminal state. Use it for the countdown,
|
|
911
916
|
retry, and lockout UI.
|
|
912
917
|
|
|
918
|
+
The server limits sends and attempts per shopper. Both calls take the
|
|
919
|
+
shopper's forwarded headers as their last argument
|
|
920
|
+
(`send(phone, collectForwardedHeaders(request))`); without them every shopper
|
|
921
|
+
of the store shares the one limit of the theme's worker. The `otpSend` and
|
|
922
|
+
`otpVerify` routes of `createStorefrontRoutes` pass them for you.
|
|
923
|
+
|
|
913
924
|
### Using the SDK's OtpMachine
|
|
914
925
|
|
|
915
926
|
The SDK exports `createOtpMachine` to manage the complete interactive WhatsApp
|
|
@@ -1012,6 +1023,8 @@ wrapper while the SDK owns reusable validation and execution logic.
|
|
|
1012
1023
|
placeOrderInput is the shared form schema. It:
|
|
1013
1024
|
|
|
1014
1025
|
- whitelists supported fields and strips unknown fields;
|
|
1026
|
+
- names the failing field on a refusal, with no shopper-facing sentence — the
|
|
1027
|
+
theme answers each field with its own copy in the store's language;
|
|
1015
1028
|
- coerces form number strings;
|
|
1016
1029
|
- turns empty optional values into undefined;
|
|
1017
1030
|
- canonicalizes phone input;
|
|
@@ -1041,7 +1054,7 @@ export const server = {
|
|
|
1041
1054
|
|
|
1042
1055
|
if (!result.success) {
|
|
1043
1056
|
throw new ActionError({
|
|
1044
|
-
code: "BAD_REQUEST",
|
|
1057
|
+
code: result.errorKind === "shape" ? "BAD_REQUEST" : "UNPROCESSABLE_CONTENT",
|
|
1045
1058
|
message: result.error,
|
|
1046
1059
|
});
|
|
1047
1060
|
}
|
|
@@ -1071,71 +1084,113 @@ order request:
|
|
|
1071
1084
|
Use it when the server route is the trusted boundary that forwards the order.
|
|
1072
1085
|
Do not let arbitrary browser code choose identity headers.
|
|
1073
1086
|
|
|
1074
|
-
###
|
|
1087
|
+
### Running the action from middleware
|
|
1075
1088
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1089
|
+
runFormAction runs an action posted from an HTML form and stores its result
|
|
1090
|
+
for the page that POST renders:
|
|
1078
1091
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1092
|
+
~~~ts
|
|
1093
|
+
// src/middleware.ts
|
|
1094
|
+
import { defineMiddleware } from "astro:middleware";
|
|
1095
|
+
import { getActionContext } from "astro:actions";
|
|
1096
|
+
import { runFormAction } from "@wocart/storefront/astro";
|
|
1097
|
+
|
|
1098
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
1099
|
+
const { action, setActionResult, serializeActionResult } = getActionContext(context);
|
|
1100
|
+
await runFormAction({
|
|
1101
|
+
action: action && {
|
|
1102
|
+
calledFrom: action.calledFrom,
|
|
1103
|
+
name: action.name,
|
|
1104
|
+
handler: action.handler,
|
|
1105
|
+
},
|
|
1106
|
+
setActionResult,
|
|
1107
|
+
serializeActionResult,
|
|
1108
|
+
});
|
|
1109
|
+
return next();
|
|
1110
|
+
});
|
|
1111
|
+
~~~
|
|
1084
1112
|
|
|
1085
|
-
The
|
|
1086
|
-
|
|
1087
|
-
the
|
|
1113
|
+
The page reads the result with `Astro.getActionResult()`: on success it
|
|
1114
|
+
redirects to the thank-you page, and on a refusal it renders the form again
|
|
1115
|
+
with the error in place.
|
|
1116
|
+
|
|
1117
|
+
Never redirect from the middleware, on success or on error. Astro 7 keeps an
|
|
1118
|
+
action result for the response being rendered only, so a redirect arrives at
|
|
1119
|
+
the next page with no result: the thank-you redirect never fires, and a
|
|
1120
|
+
refused order shows no message. resolveActionRedirectDecision, the earlier
|
|
1121
|
+
name, is deprecated and now always answers next.
|
|
1088
1122
|
|
|
1089
1123
|
## Browser-facing proxy routes
|
|
1090
1124
|
|
|
1091
|
-
Browser code
|
|
1092
|
-
|
|
1125
|
+
Browser code calls same-origin theme routes, which call the platform with the
|
|
1126
|
+
server-only API key. The key never reaches the browser.
|
|
1093
1127
|
|
|
1094
|
-
|
|
1095
|
-
validation, parameter parsing, attribution forwarding, and error shape.
|
|
1128
|
+
### The storefront routes, ready to export
|
|
1096
1129
|
|
|
1097
|
-
|
|
1130
|
+
createStorefrontRoutes returns the eight routes every storefront needs. Bind
|
|
1131
|
+
them once to the request's own client:
|
|
1098
1132
|
|
|
1099
1133
|
~~~ts
|
|
1100
|
-
|
|
1101
|
-
import {
|
|
1102
|
-
|
|
1103
|
-
import { BasketLineInputSchema, MAX_BASKET_LINES } from "@wocart/storefront";
|
|
1134
|
+
// src/theme/api-routes.ts
|
|
1135
|
+
import type { APIContext } from "astro";
|
|
1136
|
+
import { createStorefrontRoutes } from "@wocart/storefront/astro";
|
|
1104
1137
|
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
export const POST = createProxyEndpoint({
|
|
1110
|
-
method: "POST",
|
|
1111
|
-
client: (input) => store.cart.validate(input.items),
|
|
1112
|
-
inputSchema: cartInputSchema,
|
|
1113
|
-
successBody: (data) => ({ success: true, data }),
|
|
1138
|
+
export const routes = createStorefrontRoutes<APIContext>({
|
|
1139
|
+
client: (context) => context.locals.store.client,
|
|
1114
1140
|
});
|
|
1115
1141
|
~~~
|
|
1116
1142
|
|
|
1117
|
-
|
|
1118
|
-
directly and never needs the API key.
|
|
1119
|
-
|
|
1120
|
-
### Order route with attribution
|
|
1143
|
+
Each route file then exports one of them:
|
|
1121
1144
|
|
|
1122
1145
|
~~~ts
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
forwardAttribution: true,
|
|
1127
|
-
successStatus: 201,
|
|
1128
|
-
errorStatus: 422,
|
|
1129
|
-
});
|
|
1146
|
+
// src/pages/api/otp/send.ts
|
|
1147
|
+
import { routes } from "@/theme/api-routes";
|
|
1148
|
+
export const POST = routes.otpSend;
|
|
1130
1149
|
~~~
|
|
1131
1150
|
|
|
1132
|
-
|
|
1151
|
+
| Route | Method | Browser sends | Answers |
|
|
1152
|
+
| --- | --- | --- | --- |
|
|
1153
|
+
| `otpSend` | POST | `{ phone }` | `{ data: OtpSendResult }` |
|
|
1154
|
+
| `otpVerify` | POST | `{ phone, requestId, code }` | `{ data: OtpVerifyResult }` |
|
|
1155
|
+
| `cartValidate` | POST | `{ items }` | `{ success: true, data: CartValidation }` |
|
|
1156
|
+
| `abandonedUpsert` | POST | `AbandonedCheckoutInput` | 204 |
|
|
1157
|
+
| `abandonedConvert` | POST | `{ sessionId, orderId, orderNumber }` | 204 |
|
|
1158
|
+
| `communes` | GET | path param `wilayaId` | `{ data: Commune[] }` |
|
|
1159
|
+
| `reviewsList` | GET | path param `productId`, query `limit` (1–50, default 20) and `offset` | `{ data: Review[], total }` |
|
|
1160
|
+
| `reviewSubmit` | POST | `ReviewInput` | 201 `{ success: true, data: { id } }` |
|
|
1161
|
+
|
|
1162
|
+
What every route gets right:
|
|
1163
|
+
|
|
1164
|
+
- every POST refuses a cross-origin request with 403;
|
|
1165
|
+
- the OTP routes and abandoned tracking forward the shopper's address and
|
|
1166
|
+
user agent, so the server limits and attributes per shopper;
|
|
1167
|
+
- input is bounded before it is sent, and a malformed body is a 400 that never
|
|
1168
|
+
reaches the server;
|
|
1169
|
+
- a refusal answers with the status the server's category implies — 400 for
|
|
1170
|
+
validation, 422 for a business refusal, 429 when the server named a wait —
|
|
1171
|
+
and carries the server's `code` and context (`windowSeconds`,
|
|
1172
|
+
`attemptsRemaining`, `terminal`), which is what the browser branches on;
|
|
1173
|
+
- a failure the shopper cannot fix (a wrong store key, a server fault, a
|
|
1174
|
+
dropped connection) is a 502, and a timeout a 504.
|
|
1175
|
+
|
|
1176
|
+
The client resolver runs once per request. Return the request's own client,
|
|
1177
|
+
never one kept across requests.
|
|
1178
|
+
|
|
1179
|
+
### A custom route
|
|
1180
|
+
|
|
1181
|
+
createProxyEndpoint is the factory behind those routes, for a route of the
|
|
1182
|
+
theme's own. Its client function returns a result (`{ success, data }` or a
|
|
1183
|
+
failure), so wrap a read that returns a plain value:
|
|
1133
1184
|
|
|
1134
1185
|
~~~ts
|
|
1186
|
+
import { z } from "zod";
|
|
1187
|
+
import { createProxyEndpoint } from "@wocart/storefront/astro";
|
|
1188
|
+
|
|
1135
1189
|
export const GET = createProxyEndpoint({
|
|
1136
1190
|
method: "GET",
|
|
1137
|
-
|
|
1138
|
-
|
|
1191
|
+
pathParams: { slug: "string" },
|
|
1192
|
+
inputSchema: z.object({ slug: z.string().min(1).max(200) }),
|
|
1193
|
+
client: async ({ slug }) => ({ success: true, data: await store.products.get(slug) }),
|
|
1139
1194
|
});
|
|
1140
1195
|
~~~
|
|
1141
1196
|
|
|
@@ -1148,12 +1203,18 @@ Proxy configuration supports:
|
|
|
1148
1203
|
- numeric or string path parameter parsing;
|
|
1149
1204
|
- CSRF protection, enabled by default for POST and disabled by default for GET;
|
|
1150
1205
|
- attribution forwarding;
|
|
1151
|
-
- custom success
|
|
1152
|
-
-
|
|
1153
|
-
|
|
1206
|
+
- a custom success status, successBody and noContent;
|
|
1207
|
+
- errorStatus, which overrides the status derived from the failure. Leave it
|
|
1208
|
+
unset unless the browser contract needs one fixed status.
|
|
1209
|
+
|
|
1210
|
+
A thrown client function answers 502 with `code: "PROXY_UPSTREAM_FAILED"`
|
|
1211
|
+
and a generic message: an exception's own text never reaches the browser.
|
|
1212
|
+
|
|
1213
|
+
Orders do not go through a proxy route: they go through the placeOrder action
|
|
1214
|
+
(see above), which validates the form and forwards attribution itself.
|
|
1154
1215
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1216
|
+
Do not solve a proxy problem by moving the API key into public environment
|
|
1217
|
+
variables.
|
|
1157
1218
|
|
|
1158
1219
|
## Money, images, and presentation utilities
|
|
1159
1220
|
|
package/dist/actions.d.ts
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
* handler: async (input, context) => {
|
|
18
18
|
* const result = await executePlaceOrder(input, context.request, client);
|
|
19
19
|
* if (!result.success) {
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* // A refusal renders on the form, in place (see middleware.ts).
|
|
21
|
+
* throw new ActionError({
|
|
22
|
+
* code: result.errorKind === "shape" ? "BAD_REQUEST" : "UNPROCESSABLE_CONTENT",
|
|
23
|
+
* message: result.error,
|
|
24
|
+
* });
|
|
24
25
|
* }
|
|
25
26
|
* return result.data;
|
|
26
27
|
* },
|
package/dist/actions.js
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
* handler: async (input, context) => {
|
|
18
18
|
* const result = await executePlaceOrder(input, context.request, client);
|
|
19
19
|
* if (!result.success) {
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* // A refusal renders on the form, in place (see middleware.ts).
|
|
21
|
+
* throw new ActionError({
|
|
22
|
+
* code: result.errorKind === "shape" ? "BAD_REQUEST" : "UNPROCESSABLE_CONTENT",
|
|
23
|
+
* message: result.error,
|
|
24
|
+
* });
|
|
24
25
|
* }
|
|
25
26
|
* return result.data;
|
|
26
27
|
* },
|
|
@@ -92,7 +93,9 @@ export const placeOrderInput = z.object({
|
|
|
92
93
|
// Forms always submit all hidden inputs — empty string must become undefined
|
|
93
94
|
variantId: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.string().min(1).optional()),
|
|
94
95
|
variantLabel: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.string().max(100).optional()),
|
|
95
|
-
|
|
96
|
+
// Display-only — the server prices every order from the catalog — so a
|
|
97
|
+
// product the merchant prices at 0 is as orderable as any other.
|
|
98
|
+
pricePerUnit: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.coerce.number().nonnegative().optional()),
|
|
96
99
|
quantity: z.coerce.number().int().min(1).max(100).default(1),
|
|
97
100
|
// Explicit offer tier selected by the user
|
|
98
101
|
offerId: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.string().optional()),
|
|
@@ -101,10 +104,12 @@ export const placeOrderInput = z.object({
|
|
|
101
104
|
variantId: z.string().min(1),
|
|
102
105
|
variantLabel: z.string().optional(),
|
|
103
106
|
})).optional()),
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
// No custom messages: a failure names its field, and the theme answers it
|
|
108
|
+
// with its own copy in the store's language.
|
|
109
|
+
customerName: z.string().min(2).max(100),
|
|
110
|
+
phone: z.preprocess((v) => (typeof v === "string" ? toLocalAlgerianMobile(v) ?? v : v), z.string().regex(/^0[567]\d{8}$/)),
|
|
106
111
|
wilayaId: z.coerce.number().int().min(1).max(58),
|
|
107
|
-
communeId: z.string().min(1
|
|
112
|
+
communeId: z.string().min(1),
|
|
108
113
|
address: z.string().max(300).optional(),
|
|
109
114
|
deliveryType: z.enum(["home", "stop_desk"]).default("home"),
|
|
110
115
|
notes: z.string().max(500).optional(),
|
|
@@ -176,7 +181,7 @@ export function collectForwardedHeaders(request) {
|
|
|
176
181
|
export async function executePlaceOrder(input, request, client) {
|
|
177
182
|
// An order is either a basket or a single product, never neither.
|
|
178
183
|
if (!input.items?.length &&
|
|
179
|
-
(!input.productId || !input.productName ||
|
|
184
|
+
(!input.productId || !input.productName || input.pricePerUnit === undefined)) {
|
|
180
185
|
return {
|
|
181
186
|
success: false,
|
|
182
187
|
error: "An order needs either a basket or a product.",
|
package/dist/astro.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { createProxyEndpoint } from "./endpoints/proxy.js";
|
|
2
2
|
export type { ProxyClientFn, ProxyEndpointConfig, AstroAPIContext, AstroAPIRoute, } from "./endpoints/proxy.js";
|
|
3
|
+
export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
|
|
4
|
+
export type { StorefrontApiRoute, StorefrontRoutes, StorefrontRoutesOptions, } from "./endpoints/storefront-routes.js";
|
|
3
5
|
export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
|
|
4
6
|
export type { PlaceOrderInput, PlaceOrderResult } from "./actions.js";
|
|
5
|
-
export { resolveActionRedirectDecision } from "./middleware.js";
|
|
7
|
+
export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
|
|
6
8
|
export type { PrgDecision, ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
|
package/dist/astro.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { createProxyEndpoint } from "./endpoints/proxy.js";
|
|
2
|
+
export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
|
|
2
3
|
export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
|
|
3
|
-
export { resolveActionRedirectDecision } from "./middleware.js";
|
|
4
|
+
export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
|
package/dist/client/core.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ export type ClientConfig = StoreScope;
|
|
|
17
17
|
/**
|
|
18
18
|
* Options every request accepts.
|
|
19
19
|
*
|
|
20
|
-
* `signal` cancels the request — an aborted call never retries, never
|
|
21
|
-
*
|
|
20
|
+
* `signal` cancels the request — an aborted call never retries, is never
|
|
21
|
+
* cached, and returns a REQUEST_ABORTED DomainError
|
|
22
22
|
* (a read throws instead). `timeoutMs` bounds the whole operation, retries
|
|
23
23
|
* included, and reads as REQUEST_TIMEOUT. `operation` names the call in the
|
|
24
24
|
* error it produces; namespaces pass a stable label (`orders.otp.send`) so logs
|
|
@@ -61,6 +61,8 @@ export interface ClientCore {
|
|
|
61
61
|
post: <TInput, TOutput>(path: string, body: TInput, opts?: PostOptions<TOutput>) => Promise<ClientResult<TOutput>>;
|
|
62
62
|
patch: <TInput, TOutput>(path: string, body: TInput, optsOrSchema?: z.Schema<TOutput> | PatchOptions<TOutput>) => Promise<ClientResult<TOutput>>;
|
|
63
63
|
}
|
|
64
|
+
/** How long a read may take, retries included, unless the client sets another limit. */
|
|
65
|
+
export declare const DEFAULT_READ_TIMEOUT_MS = 10000;
|
|
64
66
|
/**
|
|
65
67
|
* Create client core infrastructure
|
|
66
68
|
*
|
|
@@ -68,6 +70,14 @@ export interface ClientCore {
|
|
|
68
70
|
* @returns Core infrastructure for namespace implementations
|
|
69
71
|
*/
|
|
70
72
|
export declare function createClientCore(config: ClientConfig): ClientCore;
|
|
73
|
+
/**
|
|
74
|
+
* The server's own answer that a looked-up record does not exist
|
|
75
|
+
* (`PRODUCT_NOT_FOUND`, `STORE_PAGE_NOT_FOUND`, …). A lookup returns `null` for
|
|
76
|
+
* it without reporting a failure: a stale link is not an outage. A 404 that
|
|
77
|
+
* does not carry the server's code — a wrong `serverUrl`, a route the platform
|
|
78
|
+
* does not have — is not absence, and is still reported.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isNotFoundAnswer(error: unknown): boolean;
|
|
71
81
|
/**
|
|
72
82
|
* Report a read that a namespace swallowed.
|
|
73
83
|
*
|