create-cartbase 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +25 -25
  3. package/dist/index.js +20 -20
  4. package/package.json +24 -24
  5. package/template/app/docs/auth.md +105 -105
  6. package/template/app/docs/carts.md +23 -4
  7. package/template/app/docs/categories.md +194 -194
  8. package/template/app/docs/checkout.md +714 -714
  9. package/template/app/docs/components.md +288 -31
  10. package/template/app/docs/consent.md +91 -91
  11. package/template/app/docs/deploy.md +197 -197
  12. package/template/app/docs/gift-cards.md +153 -153
  13. package/template/app/docs/metaobjects.md +126 -126
  14. package/template/app/docs/orders.md +221 -221
  15. package/template/app/docs/regions.md +269 -269
  16. package/template/app/docs/reviews.md +258 -223
  17. package/template/app/docs/search.md +227 -227
  18. package/template/app/docs/store.md +23 -1
  19. package/template/app/docs/subscriptions.md +148 -148
  20. package/template/app/docs/variables.md +331 -315
  21. package/template/app/package.json +1 -1
  22. package/template/app/postcss.config.cjs +11 -11
  23. package/template/app/src/app/checkout/checkout-empty.tsx +36 -0
  24. package/template/app/src/app/checkout/checkout-page-client.tsx +80 -73
  25. package/template/app/src/app/checkout/error.tsx +23 -0
  26. package/template/app/src/app/checkout/page.tsx +16 -7
  27. package/template/app/src/app/globals.css +26 -26
  28. package/template/app/src/app/layout.tsx +126 -126
  29. package/template/app/src/app/page.tsx +37 -37
  30. package/template/app/src/app/products/[handle]/page.tsx +89 -89
  31. package/template/app/src/app/search/page.tsx +33 -33
  32. package/template/app/src/lib/browser-client.ts +35 -35
  33. package/template/app/src/lib/catalog.ts +120 -120
  34. package/template/app/src/lib/server-client.ts +25 -25
  35. package/template/app/src/lib/store-client.ts +16 -16
@@ -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.