create-cartbase 0.1.16 → 0.1.17
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/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +24 -24
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +376 -376
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +44 -11
- package/template/app/docs/consent.md +91 -91
- package/template/app/docs/deploy.md +197 -197
- package/template/app/docs/gift-cards.md +153 -153
- package/template/app/docs/metaobjects.md +126 -126
- package/template/app/docs/orders.md +221 -221
- package/template/app/docs/products.md +51 -2
- package/template/app/docs/regions.md +269 -269
- package/template/app/docs/reviews.md +223 -223
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +47 -47
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +315 -315
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
- package/template/app/src/app/checkout/page.tsx +48 -48
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/page.tsx +28 -28
- package/template/app/src/app/products/[handle]/page.tsx +87 -87
- package/template/app/src/app/providers.tsx +64 -64
- package/template/app/src/app/search/page.tsx +23 -23
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/config.ts +41 -41
- package/template/app/src/lib/server-client.ts +25 -25
- package/template/app/src/lib/cart-actions.ts +0 -47
|
@@ -1,221 +1,221 @@
|
|
|
1
|
-
# Orders
|
|
2
|
-
|
|
3
|
-
The authenticated customer's order surface: list, detail (items +
|
|
4
|
-
fulfillments with tracking + addresses) and order transfers. There is **no
|
|
5
|
-
anonymous order read** — every endpoint on this page requires a customer
|
|
6
|
-
session (`authorization: Bearer <jwt>`, obtained via the passwordless code
|
|
7
|
-
flow — see [auth.md](auth.md)); a guest's only order handle is the
|
|
8
|
-
`completeCart()` response ([checkout.md](checkout.md)). Missing/invalid
|
|
9
|
-
JWT → `401 {code: "unauthenticated"}` — demonstrated executably below;
|
|
10
|
-
happy-path shapes are shown as jsonc (a customer session is admin-owned
|
|
11
|
-
state on the shared tenant; the authenticated paths are proven by
|
|
12
|
-
`tests/store/customer-accounts-ownership.test.ts` and the transfer suite).
|
|
13
|
-
|
|
14
|
-
**Auth for every endpoint on this page:** `x-client-id` + Bearer JWT.
|
|
15
|
-
|
|
16
|
-
SDK module: `@cartbase/storefront/api/orders`.
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## GET /api/store/orders/display/:displayId — my order by its human number
|
|
21
|
-
|
|
22
|
-
- **Purpose** — resolve "order #42" (the number customers see in emails and
|
|
23
|
-
confirmations) to the full order detail; account order pages and support
|
|
24
|
-
links use this instead of the opaque id.
|
|
25
|
-
- **Auth** — Bearer JWT required, same ownership scope as the id read:
|
|
26
|
-
another customer's number is `404 not_found`. `display_id` is a guessable
|
|
27
|
-
autoincrement — that is exactly why there is no anonymous lookup.
|
|
28
|
-
- **Request** — `GET`, no query. A numeric segment matches the
|
|
29
|
-
autoincrement `display_id` OR a purely-numeric `custom_display_id`
|
|
30
|
-
(custom wins ties — it is the number the store actually showed); any
|
|
31
|
-
other segment matches `custom_display_id` only.
|
|
32
|
-
- **Response** — identical `{order}` detail shape to
|
|
33
|
-
`GET /api/store/orders/:id` above.
|
|
34
|
-
- **Errors** — 401 `unauthenticated`; 404 `not_found`.
|
|
35
|
-
- **SDK** — `orders.retrieveOrderByDisplayId(client, displayId)`.
|
|
36
|
-
- **Components** — order-confirmation deep links, account order detail.
|
|
37
|
-
- **Settings** — `custom_display_id` is set by merchants/imports (admin
|
|
38
|
-
surface); absent it, only the autoincrement resolves.
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
42
|
-
"$BASE/api/store/orders/display/42" -H "x-client-id: $CLIENT_ID")
|
|
43
|
-
test "$STATUS" = 401
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## GET /api/store/orders — list my orders
|
|
49
|
-
|
|
50
|
-
- **Purpose** — the account "My orders" list, newest first.
|
|
51
|
-
- **Auth** — Bearer JWT required. Scoped to the session customer AND the
|
|
52
|
-
tenant — another customer's orders are invisible (not 403).
|
|
53
|
-
- **Request** — `GET ?limit=&offset=&status=` — `limit` 1–200 (default
|
|
54
|
-
20), `offset` ≥ 0 (default 0), `status` exact-match filter (`pending`,
|
|
55
|
-
`completed`, `canceled`, …).
|
|
56
|
-
- **Response** — plain order rows (NO embeds — fetch the detail for
|
|
57
|
-
items/tracking):
|
|
58
|
-
|
|
59
|
-
```jsonc
|
|
60
|
-
{
|
|
61
|
-
"orders": [{
|
|
62
|
-
"id": "order_…",
|
|
63
|
-
"display_id": 42, // human-facing autoincrement
|
|
64
|
-
"status": "pending",
|
|
65
|
-
"email": "jane@example.com",
|
|
66
|
-
"currency_code": "eur",
|
|
67
|
-
"customer_id": "cus_…",
|
|
68
|
-
"sales_channel_id": null,
|
|
69
|
-
"region_id": "reg_…",
|
|
70
|
-
"metadata": {}, // internal staff notes NEVER appear here
|
|
71
|
-
"created_at": "2026-07-20T…", "updated_at": "2026-07-20T…"
|
|
72
|
-
}],
|
|
73
|
-
"count": 1, "offset": 0, "limit": 20
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
- **Errors** — 401 `unauthenticated`; 400 `validation_failed` (bad
|
|
78
|
-
limit/offset).
|
|
79
|
-
- **SDK** — `orders.listOrders(client, {limit, offset, status})`.
|
|
80
|
-
- **Components** — account order-history page.
|
|
81
|
-
- **Settings** — none (ownership is structural).
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
# Executable auth contract: no Bearer → 401 unauthenticated.
|
|
85
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
86
|
-
"$BASE/api/store/orders" -H "x-client-id: $CLIENT_ID")
|
|
87
|
-
test "$STATUS" = 401
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## GET /api/store/orders/:id — my order, in full
|
|
93
|
-
|
|
94
|
-
- **Purpose** — the order-detail page: items, fulfillment lifecycle +
|
|
95
|
-
tracking, both addresses.
|
|
96
|
-
- **Auth** — Bearer JWT required. Ownership is part of the lookup: an
|
|
97
|
-
unknown id and another customer's order are BOTH `404 not_found`
|
|
98
|
-
(indistinguishable — no existence oracle).
|
|
99
|
-
- **Request** — `GET`, no query.
|
|
100
|
-
- **Response** — a store-safe subset of the admin order select (no
|
|
101
|
-
customer row, no payment internals, no internal timeline comments):
|
|
102
|
-
|
|
103
|
-
```jsonc
|
|
104
|
-
{
|
|
105
|
-
"order": {
|
|
106
|
-
"id": "order_…", "display_id": 42, "status": "pending",
|
|
107
|
-
"email": "jane@example.com", "currency_code": "eur",
|
|
108
|
-
"items": [{ // version pivot + embedded line item
|
|
109
|
-
"id": "oi_…", "order_id": "order_…", "quantity": 1,
|
|
110
|
-
"line_item": {
|
|
111
|
-
"id": "oli_…", "title": "M", "product_title": "Linen Shirt",
|
|
112
|
-
"product_handle": "linen-shirt", "thumbnail": "https://…",
|
|
113
|
-
"variant_id": "variant_…", "variant_sku": "LIN-SHIRT-M",
|
|
114
|
-
"unit_price": 45, "metadata": null
|
|
115
|
-
}
|
|
116
|
-
}],
|
|
117
|
-
"fulfillments": [{ // couriers surface
|
|
118
|
-
"fulfillment": {
|
|
119
|
-
"id": "ful_…",
|
|
120
|
-
"packed_at": "2026-07-20T…", "shipped_at": null,
|
|
121
|
-
"delivered_at": null, "canceled_at": null,
|
|
122
|
-
"labels": [{ "tracking_number": "…", "tracking_url": "https://…" }]
|
|
123
|
-
}
|
|
124
|
-
}],
|
|
125
|
-
"shipping_address": { "first_name": "Jane", "city": "Sofia", "…": "order_addresses row" },
|
|
126
|
-
"billing_address": { "…": "order_addresses row" }
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
- **Errors** — 401 `unauthenticated`; 404 `not_found`.
|
|
132
|
-
- **SDK** — `orders.retrieveOrder(client, orderId)`.
|
|
133
|
-
- **Components** — order-detail page, tracking widget.
|
|
134
|
-
- **Settings** — fulfillment/tracking data appears as the merchant packs
|
|
135
|
-
and ships (admin fulfillment flow + carrier labels).
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
139
|
-
"$BASE/api/store/orders/order_any" -H "x-client-id: $CLIENT_ID")
|
|
140
|
-
test "$STATUS" = 401
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## Order transfers — claim a (guest) order into my account
|
|
146
|
-
|
|
147
|
-
Flow: the **claiming** customer calls `request` on an order that is not
|
|
148
|
-
theirs → a one-time token is stored for the order's email holder (email
|
|
149
|
-
dispatch pending server-side) → the claimer, whose account email must
|
|
150
|
-
EQUAL the order's email, calls `accept` with the token. `decline` (the
|
|
151
|
-
recipient rejects, token required) or `cancel` (the requester withdraws)
|
|
152
|
-
end a pending transfer. One pending transfer per order — a duplicate
|
|
153
|
-
`request` acknowledges with the same `{transfer: {requested: true}}` shape
|
|
154
|
-
without creating another. **The token never crosses this surface** — it is
|
|
155
|
-
email-delivered only (pinned by
|
|
156
|
-
`tests/store/order-transfer-token-leak.test.ts`).
|
|
157
|
-
|
|
158
|
-
### POST /api/store/orders/:id/transfer/request
|
|
159
|
-
|
|
160
|
-
- **Request** — `{description?}`.
|
|
161
|
-
- **Response** — `200 {"order":{"id":"order_…","transfer":{"requested":true}}}`
|
|
162
|
-
(or the existing pending action object on a duplicate request).
|
|
163
|
-
- **Errors** — 401 `unauthenticated`; 404 `not_found`; 400 `already_owned`
|
|
164
|
-
(the order already belongs to the caller).
|
|
165
|
-
- **SDK** — `orders.requestOrderTransfer(client, orderId, {description})`.
|
|
166
|
-
|
|
167
|
-
### POST /api/store/orders/:id/transfer/accept
|
|
168
|
-
|
|
169
|
-
- **Request** — `{token}` (≥16 chars, from the transfer email).
|
|
170
|
-
- **Response** — `200 {order}` — the full updated order row, now owned by
|
|
171
|
-
the caller.
|
|
172
|
-
- **Errors** — 401 `unauthenticated`; 404 `not_found` (order / no pending
|
|
173
|
-
transfer); 403 `invalid_token` | `email_mismatch` (the caller's email
|
|
174
|
-
must match the order's original email — a leaked token alone is not
|
|
175
|
-
enough); 400 `validation_failed`.
|
|
176
|
-
- **SDK** — `orders.acceptOrderTransfer(client, orderId, {token})`.
|
|
177
|
-
|
|
178
|
-
### POST /api/store/orders/:id/transfer/decline
|
|
179
|
-
|
|
180
|
-
- **Request** — `{token}`.
|
|
181
|
-
- **Response** — `200 {"order":{"id":"order_…","transfer":{"declined":true}}}`.
|
|
182
|
-
- **Errors** — 401 `unauthenticated`; 404 `not_found`; 403 `invalid_token`;
|
|
183
|
-
400 `validation_failed`.
|
|
184
|
-
- **SDK** — `orders.declineOrderTransfer(client, orderId, {token})`.
|
|
185
|
-
|
|
186
|
-
### POST /api/store/orders/:id/transfer/cancel
|
|
187
|
-
|
|
188
|
-
- **Request** — no body.
|
|
189
|
-
- **Response** — `200 {"order":{"id":"order_…","transfer":{"canceled":true}}}`.
|
|
190
|
-
- **Errors** — 401 `unauthenticated`; 404 `not_found`; 403 `forbidden`
|
|
191
|
-
(only the requester may cancel).
|
|
192
|
-
- **SDK** — `orders.cancelOrderTransfer(client, orderId)`.
|
|
193
|
-
- **Components** — account "claim this order" flow + the transfer email's
|
|
194
|
-
accept/decline landing page.
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
# Executable auth contract for the transfer family (all four Bearer-gated).
|
|
198
|
-
# Note the ordering nuance: accept/decline validate the BODY before auth,
|
|
199
|
-
# so a missing token 400s even for guests; request/cancel hit auth first.
|
|
200
|
-
for EP in transfer/request transfer/cancel; do
|
|
201
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
202
|
-
"$BASE/api/store/orders/order_any/$EP" \
|
|
203
|
-
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
|
|
204
|
-
test "$STATUS" = 401
|
|
205
|
-
done
|
|
206
|
-
for EP in transfer/accept transfer/decline; do
|
|
207
|
-
RES=$(curl -s -X POST "$BASE/api/store/orders/order_any/$EP" \
|
|
208
|
-
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
|
|
209
|
-
echo "$RES" | grep -q '"code":"validation_failed"'
|
|
210
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
211
|
-
"$BASE/api/store/orders/order_any/$EP" \
|
|
212
|
-
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
213
|
-
-d '{"token":"0123456789abcdef0123456789abcdef"}')
|
|
214
|
-
test "$STATUS" = 401
|
|
215
|
-
done
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Cleanup / accretion note
|
|
219
|
-
|
|
220
|
-
This page creates nothing — every executable block is a read/auth-contract
|
|
221
|
-
probe against nonexistent ids.
|
|
1
|
+
# Orders
|
|
2
|
+
|
|
3
|
+
The authenticated customer's order surface: list, detail (items +
|
|
4
|
+
fulfillments with tracking + addresses) and order transfers. There is **no
|
|
5
|
+
anonymous order read** — every endpoint on this page requires a customer
|
|
6
|
+
session (`authorization: Bearer <jwt>`, obtained via the passwordless code
|
|
7
|
+
flow — see [auth.md](auth.md)); a guest's only order handle is the
|
|
8
|
+
`completeCart()` response ([checkout.md](checkout.md)). Missing/invalid
|
|
9
|
+
JWT → `401 {code: "unauthenticated"}` — demonstrated executably below;
|
|
10
|
+
happy-path shapes are shown as jsonc (a customer session is admin-owned
|
|
11
|
+
state on the shared tenant; the authenticated paths are proven by
|
|
12
|
+
`tests/store/customer-accounts-ownership.test.ts` and the transfer suite).
|
|
13
|
+
|
|
14
|
+
**Auth for every endpoint on this page:** `x-client-id` + Bearer JWT.
|
|
15
|
+
|
|
16
|
+
SDK module: `@cartbase/storefront/api/orders`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## GET /api/store/orders/display/:displayId — my order by its human number
|
|
21
|
+
|
|
22
|
+
- **Purpose** — resolve "order #42" (the number customers see in emails and
|
|
23
|
+
confirmations) to the full order detail; account order pages and support
|
|
24
|
+
links use this instead of the opaque id.
|
|
25
|
+
- **Auth** — Bearer JWT required, same ownership scope as the id read:
|
|
26
|
+
another customer's number is `404 not_found`. `display_id` is a guessable
|
|
27
|
+
autoincrement — that is exactly why there is no anonymous lookup.
|
|
28
|
+
- **Request** — `GET`, no query. A numeric segment matches the
|
|
29
|
+
autoincrement `display_id` OR a purely-numeric `custom_display_id`
|
|
30
|
+
(custom wins ties — it is the number the store actually showed); any
|
|
31
|
+
other segment matches `custom_display_id` only.
|
|
32
|
+
- **Response** — identical `{order}` detail shape to
|
|
33
|
+
`GET /api/store/orders/:id` above.
|
|
34
|
+
- **Errors** — 401 `unauthenticated`; 404 `not_found`.
|
|
35
|
+
- **SDK** — `orders.retrieveOrderByDisplayId(client, displayId)`.
|
|
36
|
+
- **Components** — order-confirmation deep links, account order detail.
|
|
37
|
+
- **Settings** — `custom_display_id` is set by merchants/imports (admin
|
|
38
|
+
surface); absent it, only the autoincrement resolves.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
42
|
+
"$BASE/api/store/orders/display/42" -H "x-client-id: $CLIENT_ID")
|
|
43
|
+
test "$STATUS" = 401
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## GET /api/store/orders — list my orders
|
|
49
|
+
|
|
50
|
+
- **Purpose** — the account "My orders" list, newest first.
|
|
51
|
+
- **Auth** — Bearer JWT required. Scoped to the session customer AND the
|
|
52
|
+
tenant — another customer's orders are invisible (not 403).
|
|
53
|
+
- **Request** — `GET ?limit=&offset=&status=` — `limit` 1–200 (default
|
|
54
|
+
20), `offset` ≥ 0 (default 0), `status` exact-match filter (`pending`,
|
|
55
|
+
`completed`, `canceled`, …).
|
|
56
|
+
- **Response** — plain order rows (NO embeds — fetch the detail for
|
|
57
|
+
items/tracking):
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
{
|
|
61
|
+
"orders": [{
|
|
62
|
+
"id": "order_…",
|
|
63
|
+
"display_id": 42, // human-facing autoincrement
|
|
64
|
+
"status": "pending",
|
|
65
|
+
"email": "jane@example.com",
|
|
66
|
+
"currency_code": "eur",
|
|
67
|
+
"customer_id": "cus_…",
|
|
68
|
+
"sales_channel_id": null,
|
|
69
|
+
"region_id": "reg_…",
|
|
70
|
+
"metadata": {}, // internal staff notes NEVER appear here
|
|
71
|
+
"created_at": "2026-07-20T…", "updated_at": "2026-07-20T…"
|
|
72
|
+
}],
|
|
73
|
+
"count": 1, "offset": 0, "limit": 20
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **Errors** — 401 `unauthenticated`; 400 `validation_failed` (bad
|
|
78
|
+
limit/offset).
|
|
79
|
+
- **SDK** — `orders.listOrders(client, {limit, offset, status})`.
|
|
80
|
+
- **Components** — account order-history page.
|
|
81
|
+
- **Settings** — none (ownership is structural).
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Executable auth contract: no Bearer → 401 unauthenticated.
|
|
85
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
86
|
+
"$BASE/api/store/orders" -H "x-client-id: $CLIENT_ID")
|
|
87
|
+
test "$STATUS" = 401
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## GET /api/store/orders/:id — my order, in full
|
|
93
|
+
|
|
94
|
+
- **Purpose** — the order-detail page: items, fulfillment lifecycle +
|
|
95
|
+
tracking, both addresses.
|
|
96
|
+
- **Auth** — Bearer JWT required. Ownership is part of the lookup: an
|
|
97
|
+
unknown id and another customer's order are BOTH `404 not_found`
|
|
98
|
+
(indistinguishable — no existence oracle).
|
|
99
|
+
- **Request** — `GET`, no query.
|
|
100
|
+
- **Response** — a store-safe subset of the admin order select (no
|
|
101
|
+
customer row, no payment internals, no internal timeline comments):
|
|
102
|
+
|
|
103
|
+
```jsonc
|
|
104
|
+
{
|
|
105
|
+
"order": {
|
|
106
|
+
"id": "order_…", "display_id": 42, "status": "pending",
|
|
107
|
+
"email": "jane@example.com", "currency_code": "eur",
|
|
108
|
+
"items": [{ // version pivot + embedded line item
|
|
109
|
+
"id": "oi_…", "order_id": "order_…", "quantity": 1,
|
|
110
|
+
"line_item": {
|
|
111
|
+
"id": "oli_…", "title": "M", "product_title": "Linen Shirt",
|
|
112
|
+
"product_handle": "linen-shirt", "thumbnail": "https://…",
|
|
113
|
+
"variant_id": "variant_…", "variant_sku": "LIN-SHIRT-M",
|
|
114
|
+
"unit_price": 45, "metadata": null
|
|
115
|
+
}
|
|
116
|
+
}],
|
|
117
|
+
"fulfillments": [{ // couriers surface
|
|
118
|
+
"fulfillment": {
|
|
119
|
+
"id": "ful_…",
|
|
120
|
+
"packed_at": "2026-07-20T…", "shipped_at": null,
|
|
121
|
+
"delivered_at": null, "canceled_at": null,
|
|
122
|
+
"labels": [{ "tracking_number": "…", "tracking_url": "https://…" }]
|
|
123
|
+
}
|
|
124
|
+
}],
|
|
125
|
+
"shipping_address": { "first_name": "Jane", "city": "Sofia", "…": "order_addresses row" },
|
|
126
|
+
"billing_address": { "…": "order_addresses row" }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- **Errors** — 401 `unauthenticated`; 404 `not_found`.
|
|
132
|
+
- **SDK** — `orders.retrieveOrder(client, orderId)`.
|
|
133
|
+
- **Components** — order-detail page, tracking widget.
|
|
134
|
+
- **Settings** — fulfillment/tracking data appears as the merchant packs
|
|
135
|
+
and ships (admin fulfillment flow + carrier labels).
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
139
|
+
"$BASE/api/store/orders/order_any" -H "x-client-id: $CLIENT_ID")
|
|
140
|
+
test "$STATUS" = 401
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Order transfers — claim a (guest) order into my account
|
|
146
|
+
|
|
147
|
+
Flow: the **claiming** customer calls `request` on an order that is not
|
|
148
|
+
theirs → a one-time token is stored for the order's email holder (email
|
|
149
|
+
dispatch pending server-side) → the claimer, whose account email must
|
|
150
|
+
EQUAL the order's email, calls `accept` with the token. `decline` (the
|
|
151
|
+
recipient rejects, token required) or `cancel` (the requester withdraws)
|
|
152
|
+
end a pending transfer. One pending transfer per order — a duplicate
|
|
153
|
+
`request` acknowledges with the same `{transfer: {requested: true}}` shape
|
|
154
|
+
without creating another. **The token never crosses this surface** — it is
|
|
155
|
+
email-delivered only (pinned by
|
|
156
|
+
`tests/store/order-transfer-token-leak.test.ts`).
|
|
157
|
+
|
|
158
|
+
### POST /api/store/orders/:id/transfer/request
|
|
159
|
+
|
|
160
|
+
- **Request** — `{description?}`.
|
|
161
|
+
- **Response** — `200 {"order":{"id":"order_…","transfer":{"requested":true}}}`
|
|
162
|
+
(or the existing pending action object on a duplicate request).
|
|
163
|
+
- **Errors** — 401 `unauthenticated`; 404 `not_found`; 400 `already_owned`
|
|
164
|
+
(the order already belongs to the caller).
|
|
165
|
+
- **SDK** — `orders.requestOrderTransfer(client, orderId, {description})`.
|
|
166
|
+
|
|
167
|
+
### POST /api/store/orders/:id/transfer/accept
|
|
168
|
+
|
|
169
|
+
- **Request** — `{token}` (≥16 chars, from the transfer email).
|
|
170
|
+
- **Response** — `200 {order}` — the full updated order row, now owned by
|
|
171
|
+
the caller.
|
|
172
|
+
- **Errors** — 401 `unauthenticated`; 404 `not_found` (order / no pending
|
|
173
|
+
transfer); 403 `invalid_token` | `email_mismatch` (the caller's email
|
|
174
|
+
must match the order's original email — a leaked token alone is not
|
|
175
|
+
enough); 400 `validation_failed`.
|
|
176
|
+
- **SDK** — `orders.acceptOrderTransfer(client, orderId, {token})`.
|
|
177
|
+
|
|
178
|
+
### POST /api/store/orders/:id/transfer/decline
|
|
179
|
+
|
|
180
|
+
- **Request** — `{token}`.
|
|
181
|
+
- **Response** — `200 {"order":{"id":"order_…","transfer":{"declined":true}}}`.
|
|
182
|
+
- **Errors** — 401 `unauthenticated`; 404 `not_found`; 403 `invalid_token`;
|
|
183
|
+
400 `validation_failed`.
|
|
184
|
+
- **SDK** — `orders.declineOrderTransfer(client, orderId, {token})`.
|
|
185
|
+
|
|
186
|
+
### POST /api/store/orders/:id/transfer/cancel
|
|
187
|
+
|
|
188
|
+
- **Request** — no body.
|
|
189
|
+
- **Response** — `200 {"order":{"id":"order_…","transfer":{"canceled":true}}}`.
|
|
190
|
+
- **Errors** — 401 `unauthenticated`; 404 `not_found`; 403 `forbidden`
|
|
191
|
+
(only the requester may cancel).
|
|
192
|
+
- **SDK** — `orders.cancelOrderTransfer(client, orderId)`.
|
|
193
|
+
- **Components** — account "claim this order" flow + the transfer email's
|
|
194
|
+
accept/decline landing page.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Executable auth contract for the transfer family (all four Bearer-gated).
|
|
198
|
+
# Note the ordering nuance: accept/decline validate the BODY before auth,
|
|
199
|
+
# so a missing token 400s even for guests; request/cancel hit auth first.
|
|
200
|
+
for EP in transfer/request transfer/cancel; do
|
|
201
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
202
|
+
"$BASE/api/store/orders/order_any/$EP" \
|
|
203
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
|
|
204
|
+
test "$STATUS" = 401
|
|
205
|
+
done
|
|
206
|
+
for EP in transfer/accept transfer/decline; do
|
|
207
|
+
RES=$(curl -s -X POST "$BASE/api/store/orders/order_any/$EP" \
|
|
208
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
|
|
209
|
+
echo "$RES" | grep -q '"code":"validation_failed"'
|
|
210
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
211
|
+
"$BASE/api/store/orders/order_any/$EP" \
|
|
212
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
213
|
+
-d '{"token":"0123456789abcdef0123456789abcdef"}')
|
|
214
|
+
test "$STATUS" = 401
|
|
215
|
+
done
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Cleanup / accretion note
|
|
219
|
+
|
|
220
|
+
This page creates nothing — every executable block is a read/auth-contract
|
|
221
|
+
probe against nonexistent ids.
|
|
@@ -49,7 +49,8 @@ foreign key → 400 `invalid_publishable_key`.
|
|
|
49
49
|
"currency_code": "eur", // pricing context
|
|
50
50
|
"region_id": "reg_…", // or region → currency
|
|
51
51
|
"limit": 50, // 1–200, default 50
|
|
52
|
-
"offset": 0
|
|
52
|
+
"offset": 0,
|
|
53
|
+
"fields": "*variants.inventory_items,…" // Medusa's field selection: only the kit links change the answer (below)
|
|
53
54
|
}
|
|
54
55
|
```
|
|
55
56
|
|
|
@@ -150,6 +151,53 @@ curl -sf "$BASE/api/store/products?handle=linen-shirt" -H "x-client-id: $CLIENT_
|
|
|
150
151
|
| grep -q '"handle":"linen-shirt"'
|
|
151
152
|
```
|
|
152
153
|
|
|
154
|
+
### Sets: a variant's inventory kit (`fields`)
|
|
155
|
+
|
|
156
|
+
A set is an inventory kit, Medusa's model: the set's variant draws on the
|
|
157
|
+
stock items of the products inside it, each with a `required_quantity`.
|
|
158
|
+
Ask for the links the way Medusa's store API is asked, on the list or the
|
|
159
|
+
single product:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
fields=*variants.inventory_items,*variants.inventory_items.inventory,*variants.inventory_items.inventory.variants,*variants.inventory_items.inventory.variants.product
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Any `fields` entry naming `variants.inventory_items` adds `inventory_items`
|
|
166
|
+
to every variant (an empty list when it has none); nothing else in `fields`
|
|
167
|
+
changes the answer, which is always the full shape above. It costs no extra
|
|
168
|
+
round trip.
|
|
169
|
+
|
|
170
|
+
```jsonc
|
|
171
|
+
"inventory_items": [
|
|
172
|
+
{
|
|
173
|
+
"variant_id": "variant_…", // the variant being read
|
|
174
|
+
"inventory_item_id": "iitem_…",
|
|
175
|
+
"required_quantity": 2, // units of this item one variant takes
|
|
176
|
+
"inventory": {
|
|
177
|
+
"id": "iitem_…", "sku": "BELT-01", "title": "Leather Belt", "thumbnail": null,
|
|
178
|
+
// every PUBLISHED variant drawing on this item: the product that owns it
|
|
179
|
+
// and every set containing it (a draft set is never listed)
|
|
180
|
+
"variants": [
|
|
181
|
+
{ "id": "variant_…", "product_id": "prod_…", "title": "Default", "sku": "BELT-01",
|
|
182
|
+
"product": { "id": "prod_…", "title": "Leather Belt", "handle": "leather-belt", "thumbnail": null } }
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
A set is a variant with links to items other products own: its contents are
|
|
190
|
+
those owners. The sets a product belongs to are the other variants on its
|
|
191
|
+
own item.
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# The key rides only when fields asks for it.
|
|
195
|
+
curl -sf "$BASE/api/store/products/linen-shirt?fields=*variants.inventory_items.inventory.variants.product" \
|
|
196
|
+
-H "x-client-id: $CLIENT_ID" | grep -q '"inventory_items"'
|
|
197
|
+
if curl -sf "$BASE/api/store/products/linen-shirt" -H "x-client-id: $CLIENT_ID" \
|
|
198
|
+
| grep -q '"inventory_items"'; then exit 1; fi
|
|
199
|
+
```
|
|
200
|
+
|
|
153
201
|
- **Errors** — 400 `missing_client_id`, 400 `validation_failed`,
|
|
154
202
|
400 `invalid_publishable_key`, 400 `invalid_region`.
|
|
155
203
|
- **SDK** — `listProducts(client, query?)`.
|
|
@@ -164,7 +212,8 @@ curl -sf "$BASE/api/store/products?handle=linen-shirt" -H "x-client-id: $CLIENT_
|
|
|
164
212
|
- **Purpose** — the PDP read. Accepts a product id (`prod_…`) or a handle —
|
|
165
213
|
storefronts deep-link by handle.
|
|
166
214
|
- **Auth** — as the list.
|
|
167
|
-
- **Request** — query: pricing context
|
|
215
|
+
- **Request** — query: pricing context (`currency_code` / `region_id`) and
|
|
216
|
+
`fields` for a set's inventory kit (see Sets above).
|
|
168
217
|
- **Response** — `{ "product": { ...shape above... } }`
|
|
169
218
|
- **Working curl** — with pricing context; asserts the seeded base price
|
|
170
219
|
(Linen Shirt, EUR 45) and the leak rule structurally:
|