@cartbase/storefront 0.20.0 → 0.20.1
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/package.json +258 -258
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +778 -778
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -78
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-info.tsx +5 -5
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -110
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/lightbox-state.ts +46 -46
- package/src/reviews-ui/review-header.tsx +166 -166
- package/src/reviews-ui/review-lightbox.tsx +271 -271
- package/src/reviews-ui/review-list.tsx +193 -193
- package/src/reviews-ui/review-widget.tsx +219 -218
- package/src/reviews-ui/widget-options.ts +55 -55
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
package/src/api/orders.ts
CHANGED
|
@@ -1,290 +1,290 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront/api/orders — the authenticated customer's orders.
|
|
3
|
-
*
|
|
4
|
-
* EVERY function here requires a customer session
|
|
5
|
-
* (`authorization: Bearer <jwt>` via the client's `getAuthToken`) — there
|
|
6
|
-
* is no anonymous order read; a guest's only order handle is the
|
|
7
|
-
* `completeCart()` response. Missing/invalid JWT → 401 `unauthenticated`.
|
|
8
|
-
*
|
|
9
|
-
* Route ground truth: src/app/api/store/orders/**. Doc:
|
|
10
|
-
* docs/storefront/orders.md.
|
|
11
|
-
*
|
|
12
|
-
* `GET /api/store/orders/display/:displayId` was contract-listed but
|
|
13
|
-
* missing from the codebase when this module was first written; the route
|
|
14
|
-
* was built at the batch-9 merge and `retrieveOrderByDisplayId` wraps it.
|
|
15
|
-
*/
|
|
16
|
-
import type { StorefrontClient } from "./http"
|
|
17
|
-
import type { IsoDateString, ListEnvelope, MajorUnitAmount } from "./types"
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// DTOs — ground truth: orders/route.ts (list select `*`), orders/[id]/route.ts
|
|
21
|
-
// (STORE_ORDER_DETAIL_SELECT), orders/[id]/transfer/*.
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
/** Order row as listed (plain `orders` columns, no embeds). */
|
|
25
|
-
export interface StoreOrderSummaryRow {
|
|
26
|
-
id: string
|
|
27
|
-
/** Human-facing autoincrement. */
|
|
28
|
-
display_id: number
|
|
29
|
-
status: string
|
|
30
|
-
email: string | null
|
|
31
|
-
currency_code: string
|
|
32
|
-
customer_id: string | null
|
|
33
|
-
sales_channel_id: string | null
|
|
34
|
-
region_id: string | null
|
|
35
|
-
metadata: Record<string, unknown> | null
|
|
36
|
-
created_at: IsoDateString
|
|
37
|
-
updated_at: IsoDateString
|
|
38
|
-
[key: string]: unknown
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** `order_line_items` row (title/prices/product snapshot at purchase time). */
|
|
42
|
-
export interface StoreOrderLineItem {
|
|
43
|
-
id: string
|
|
44
|
-
title: string | null
|
|
45
|
-
subtitle: string | null
|
|
46
|
-
product_title: string | null
|
|
47
|
-
product_handle: string | null
|
|
48
|
-
thumbnail: string | null
|
|
49
|
-
variant_id: string | null
|
|
50
|
-
variant_title: string | null
|
|
51
|
-
variant_sku: string | null
|
|
52
|
-
unit_price: MajorUnitAmount
|
|
53
|
-
is_giftcard?: boolean
|
|
54
|
-
requires_shipping?: boolean
|
|
55
|
-
metadata: Record<string, unknown> | null
|
|
56
|
-
[key: string]: unknown
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Version pivot row: quantity + the embedded line item. */
|
|
60
|
-
export interface StoreOrderItem {
|
|
61
|
-
id: string
|
|
62
|
-
order_id: string
|
|
63
|
-
quantity: number
|
|
64
|
-
line_item: StoreOrderLineItem | null
|
|
65
|
-
[key: string]: unknown
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface StoreFulfillmentLabel {
|
|
69
|
-
tracking_number: string | null
|
|
70
|
-
tracking_url: string | null
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Store-safe fulfillment subset: lifecycle timestamps + tracking labels. */
|
|
74
|
-
export interface StoreOrderFulfillment {
|
|
75
|
-
fulfillment: {
|
|
76
|
-
id: string
|
|
77
|
-
packed_at: IsoDateString | null
|
|
78
|
-
shipped_at: IsoDateString | null
|
|
79
|
-
delivered_at: IsoDateString | null
|
|
80
|
-
canceled_at: IsoDateString | null
|
|
81
|
-
labels: StoreFulfillmentLabel[]
|
|
82
|
-
} | null
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** `order_addresses` row. */
|
|
86
|
-
export interface StoreOrderAddress {
|
|
87
|
-
id: string
|
|
88
|
-
first_name: string | null
|
|
89
|
-
last_name: string | null
|
|
90
|
-
company: string | null
|
|
91
|
-
address_1: string | null
|
|
92
|
-
address_2: string | null
|
|
93
|
-
city: string | null
|
|
94
|
-
country_code: string | null
|
|
95
|
-
province: string | null
|
|
96
|
-
postal_code: string | null
|
|
97
|
-
phone: string | null
|
|
98
|
-
[key: string]: unknown
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Order detail — a store-safe subset of the admin select: items (with
|
|
103
|
-
* embedded line_item), fulfillments with tracking, both addresses. No
|
|
104
|
-
* internal joins (customer row, payment internals) and NO internal staff
|
|
105
|
-
* notes (timeline comments never cross this surface).
|
|
106
|
-
*/
|
|
107
|
-
export interface StoreOrderDetail extends StoreOrderSummaryRow {
|
|
108
|
-
items: StoreOrderItem[]
|
|
109
|
-
fulfillments: StoreOrderFulfillment[]
|
|
110
|
-
shipping_address: StoreOrderAddress | null
|
|
111
|
-
billing_address: StoreOrderAddress | null
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface ListOrdersQuery {
|
|
115
|
-
/** 1–200, default 20. */
|
|
116
|
-
limit?: number
|
|
117
|
-
/** Default 0. */
|
|
118
|
-
offset?: number
|
|
119
|
-
/** Exact-match filter on `orders.status` (e.g. `pending`, `completed`, `canceled`). */
|
|
120
|
-
status?: string
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export interface OrderListResponse extends ListEnvelope {
|
|
124
|
-
orders: StoreOrderSummaryRow[]
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export interface OrderResponse {
|
|
128
|
-
order: StoreOrderDetail
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
// Reads
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* List MY orders (newest first). → 200 `{orders, count, offset, limit}` —
|
|
137
|
-
* plain order rows, no embeds (fetch the detail for items/tracking).
|
|
138
|
-
* Auth: REQUIRED Bearer JWT (+ `x-client-id`). Scoped to the session
|
|
139
|
-
* customer — another customer's orders are invisible, not 403.
|
|
140
|
-
* Errors: 401 `unauthenticated`, 400 `validation_failed` (bad
|
|
141
|
-
* limit/offset).
|
|
142
|
-
*/
|
|
143
|
-
export async function listOrders(
|
|
144
|
-
client: StorefrontClient,
|
|
145
|
-
query: ListOrdersQuery = {}
|
|
146
|
-
): Promise<OrderListResponse> {
|
|
147
|
-
return client.get("/api/store/orders", { query: { ...query } })
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Retrieve MY order with items, fulfillments (+ tracking labels) and
|
|
152
|
-
* addresses. → 200 `{order}`.
|
|
153
|
-
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
154
|
-
* Errors: 401 `unauthenticated`, 404 `not_found` (unknown id OR an order
|
|
155
|
-
* belonging to another customer — ownership is part of the lookup, so
|
|
156
|
-
* cross-customer reads are indistinguishable from missing).
|
|
157
|
-
*/
|
|
158
|
-
export async function retrieveOrder(
|
|
159
|
-
client: StorefrontClient,
|
|
160
|
-
orderId: string
|
|
161
|
-
): Promise<OrderResponse> {
|
|
162
|
-
return client.get(`/api/store/orders/${orderId}`)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Retrieve MY order by its human number: the autoincrement `display_id`
|
|
167
|
-
* (numeric segment) or a `custom_display_id` (any segment; a numeric
|
|
168
|
-
* custom id wins over a colliding display_id — it is the number the store
|
|
169
|
-
* showed the customer). Same detail embeds + ownership scope as
|
|
170
|
-
* `retrieveOrder`.
|
|
171
|
-
* → 200 `{order}`.
|
|
172
|
-
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
173
|
-
* Errors: 401 `unauthenticated`, 404 `not_found` (unknown number OR another
|
|
174
|
-
* customer's order — indistinguishable by design).
|
|
175
|
-
*/
|
|
176
|
-
export async function retrieveOrderByDisplayId(
|
|
177
|
-
client: StorefrontClient,
|
|
178
|
-
displayId: string | number
|
|
179
|
-
): Promise<OrderResponse> {
|
|
180
|
-
return client.get(`/api/store/orders/display/${displayId}`)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
// Transfers — move a (guest) order to the authenticated customer.
|
|
185
|
-
// Flow: the CLAIMING customer calls request → a token is issued for the
|
|
186
|
-
// order's email holder → the claimer (whose email must match the order's)
|
|
187
|
-
// calls accept with the token; decline/cancel end a pending transfer.
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
|
|
190
|
-
export interface RequestOrderTransferInput {
|
|
191
|
-
/** Free-text shown to the recipient; stored on the transfer action. */
|
|
192
|
-
description?: string
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* request → `{order:{id, transfer:{requested:true}}}` — ALWAYS this shape,
|
|
197
|
-
* for the first request and for duplicates alike. The transfer token is
|
|
198
|
-
* email-delivered only and never crosses the store surface.
|
|
199
|
-
*/
|
|
200
|
-
export interface RequestOrderTransferResponse {
|
|
201
|
-
order: {
|
|
202
|
-
id: string
|
|
203
|
-
transfer: { requested: true }
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Request the order be transferred to ME (the authenticated customer).
|
|
209
|
-
* Idempotent: an existing pending transfer is returned, not duplicated.
|
|
210
|
-
* → 200.
|
|
211
|
-
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
212
|
-
* Errors: 401 `unauthenticated`, 404 `not_found`, 400 `already_owned`
|
|
213
|
-
* (the order already belongs to this customer).
|
|
214
|
-
*/
|
|
215
|
-
export async function requestOrderTransfer(
|
|
216
|
-
client: StorefrontClient,
|
|
217
|
-
orderId: string,
|
|
218
|
-
input: RequestOrderTransferInput = {}
|
|
219
|
-
): Promise<RequestOrderTransferResponse> {
|
|
220
|
-
return client.post(`/api/store/orders/${orderId}/transfer/request`, input)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface CancelOrderTransferResponse {
|
|
224
|
-
order: { id: string; transfer: { canceled: true } }
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Cancel MY pending transfer request (only the requester may cancel).
|
|
229
|
-
* → 200.
|
|
230
|
-
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
231
|
-
* Errors: 401 `unauthenticated`, 404 `not_found` (order or no pending
|
|
232
|
-
* transfer), 403 `forbidden` (not the requester).
|
|
233
|
-
*/
|
|
234
|
-
export async function cancelOrderTransfer(
|
|
235
|
-
client: StorefrontClient,
|
|
236
|
-
orderId: string
|
|
237
|
-
): Promise<CancelOrderTransferResponse> {
|
|
238
|
-
return client.post(`/api/store/orders/${orderId}/transfer/cancel`)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export interface AcceptOrderTransferInput {
|
|
242
|
-
/** The transfer token received by email (≥16 chars). */
|
|
243
|
-
token: string
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** accept → the full updated order row (now owned by the caller). */
|
|
247
|
-
export interface AcceptOrderTransferResponse {
|
|
248
|
-
order: StoreOrderSummaryRow
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Accept a pending transfer with the emailed token — assigns the order to
|
|
253
|
-
* the authenticated customer. The caller's email must equal the order's
|
|
254
|
-
* original email (a leaked token alone is not enough). → 200 `{order}`.
|
|
255
|
-
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
256
|
-
* Errors: 401 `unauthenticated`, 404 `not_found` (order / no pending
|
|
257
|
-
* transfer), 403 `invalid_token` | `email_mismatch`, 400
|
|
258
|
-
* `validation_failed`.
|
|
259
|
-
*/
|
|
260
|
-
export async function acceptOrderTransfer(
|
|
261
|
-
client: StorefrontClient,
|
|
262
|
-
orderId: string,
|
|
263
|
-
input: AcceptOrderTransferInput
|
|
264
|
-
): Promise<AcceptOrderTransferResponse> {
|
|
265
|
-
return client.post(`/api/store/orders/${orderId}/transfer/accept`, input)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export interface DeclineOrderTransferInput {
|
|
269
|
-
/** The transfer token received by email (≥16 chars). */
|
|
270
|
-
token: string
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export interface DeclineOrderTransferResponse {
|
|
274
|
-
order: { id: string; transfer: { declined: true } }
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Decline a pending transfer (the order's original email holder rejects
|
|
279
|
-
* it) — validates the token, then removes the pending action. → 200.
|
|
280
|
-
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
281
|
-
* Errors: 401 `unauthenticated`, 404 `not_found`, 403 `invalid_token`,
|
|
282
|
-
* 400 `validation_failed`.
|
|
283
|
-
*/
|
|
284
|
-
export async function declineOrderTransfer(
|
|
285
|
-
client: StorefrontClient,
|
|
286
|
-
orderId: string,
|
|
287
|
-
input: DeclineOrderTransferInput
|
|
288
|
-
): Promise<DeclineOrderTransferResponse> {
|
|
289
|
-
return client.post(`/api/store/orders/${orderId}/transfer/decline`, input)
|
|
290
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/orders — the authenticated customer's orders.
|
|
3
|
+
*
|
|
4
|
+
* EVERY function here requires a customer session
|
|
5
|
+
* (`authorization: Bearer <jwt>` via the client's `getAuthToken`) — there
|
|
6
|
+
* is no anonymous order read; a guest's only order handle is the
|
|
7
|
+
* `completeCart()` response. Missing/invalid JWT → 401 `unauthenticated`.
|
|
8
|
+
*
|
|
9
|
+
* Route ground truth: src/app/api/store/orders/**. Doc:
|
|
10
|
+
* docs/storefront/orders.md.
|
|
11
|
+
*
|
|
12
|
+
* `GET /api/store/orders/display/:displayId` was contract-listed but
|
|
13
|
+
* missing from the codebase when this module was first written; the route
|
|
14
|
+
* was built at the batch-9 merge and `retrieveOrderByDisplayId` wraps it.
|
|
15
|
+
*/
|
|
16
|
+
import type { StorefrontClient } from "./http"
|
|
17
|
+
import type { IsoDateString, ListEnvelope, MajorUnitAmount } from "./types"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// DTOs — ground truth: orders/route.ts (list select `*`), orders/[id]/route.ts
|
|
21
|
+
// (STORE_ORDER_DETAIL_SELECT), orders/[id]/transfer/*.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Order row as listed (plain `orders` columns, no embeds). */
|
|
25
|
+
export interface StoreOrderSummaryRow {
|
|
26
|
+
id: string
|
|
27
|
+
/** Human-facing autoincrement. */
|
|
28
|
+
display_id: number
|
|
29
|
+
status: string
|
|
30
|
+
email: string | null
|
|
31
|
+
currency_code: string
|
|
32
|
+
customer_id: string | null
|
|
33
|
+
sales_channel_id: string | null
|
|
34
|
+
region_id: string | null
|
|
35
|
+
metadata: Record<string, unknown> | null
|
|
36
|
+
created_at: IsoDateString
|
|
37
|
+
updated_at: IsoDateString
|
|
38
|
+
[key: string]: unknown
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `order_line_items` row (title/prices/product snapshot at purchase time). */
|
|
42
|
+
export interface StoreOrderLineItem {
|
|
43
|
+
id: string
|
|
44
|
+
title: string | null
|
|
45
|
+
subtitle: string | null
|
|
46
|
+
product_title: string | null
|
|
47
|
+
product_handle: string | null
|
|
48
|
+
thumbnail: string | null
|
|
49
|
+
variant_id: string | null
|
|
50
|
+
variant_title: string | null
|
|
51
|
+
variant_sku: string | null
|
|
52
|
+
unit_price: MajorUnitAmount
|
|
53
|
+
is_giftcard?: boolean
|
|
54
|
+
requires_shipping?: boolean
|
|
55
|
+
metadata: Record<string, unknown> | null
|
|
56
|
+
[key: string]: unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Version pivot row: quantity + the embedded line item. */
|
|
60
|
+
export interface StoreOrderItem {
|
|
61
|
+
id: string
|
|
62
|
+
order_id: string
|
|
63
|
+
quantity: number
|
|
64
|
+
line_item: StoreOrderLineItem | null
|
|
65
|
+
[key: string]: unknown
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface StoreFulfillmentLabel {
|
|
69
|
+
tracking_number: string | null
|
|
70
|
+
tracking_url: string | null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Store-safe fulfillment subset: lifecycle timestamps + tracking labels. */
|
|
74
|
+
export interface StoreOrderFulfillment {
|
|
75
|
+
fulfillment: {
|
|
76
|
+
id: string
|
|
77
|
+
packed_at: IsoDateString | null
|
|
78
|
+
shipped_at: IsoDateString | null
|
|
79
|
+
delivered_at: IsoDateString | null
|
|
80
|
+
canceled_at: IsoDateString | null
|
|
81
|
+
labels: StoreFulfillmentLabel[]
|
|
82
|
+
} | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `order_addresses` row. */
|
|
86
|
+
export interface StoreOrderAddress {
|
|
87
|
+
id: string
|
|
88
|
+
first_name: string | null
|
|
89
|
+
last_name: string | null
|
|
90
|
+
company: string | null
|
|
91
|
+
address_1: string | null
|
|
92
|
+
address_2: string | null
|
|
93
|
+
city: string | null
|
|
94
|
+
country_code: string | null
|
|
95
|
+
province: string | null
|
|
96
|
+
postal_code: string | null
|
|
97
|
+
phone: string | null
|
|
98
|
+
[key: string]: unknown
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Order detail — a store-safe subset of the admin select: items (with
|
|
103
|
+
* embedded line_item), fulfillments with tracking, both addresses. No
|
|
104
|
+
* internal joins (customer row, payment internals) and NO internal staff
|
|
105
|
+
* notes (timeline comments never cross this surface).
|
|
106
|
+
*/
|
|
107
|
+
export interface StoreOrderDetail extends StoreOrderSummaryRow {
|
|
108
|
+
items: StoreOrderItem[]
|
|
109
|
+
fulfillments: StoreOrderFulfillment[]
|
|
110
|
+
shipping_address: StoreOrderAddress | null
|
|
111
|
+
billing_address: StoreOrderAddress | null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ListOrdersQuery {
|
|
115
|
+
/** 1–200, default 20. */
|
|
116
|
+
limit?: number
|
|
117
|
+
/** Default 0. */
|
|
118
|
+
offset?: number
|
|
119
|
+
/** Exact-match filter on `orders.status` (e.g. `pending`, `completed`, `canceled`). */
|
|
120
|
+
status?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface OrderListResponse extends ListEnvelope {
|
|
124
|
+
orders: StoreOrderSummaryRow[]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface OrderResponse {
|
|
128
|
+
order: StoreOrderDetail
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Reads
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* List MY orders (newest first). → 200 `{orders, count, offset, limit}` —
|
|
137
|
+
* plain order rows, no embeds (fetch the detail for items/tracking).
|
|
138
|
+
* Auth: REQUIRED Bearer JWT (+ `x-client-id`). Scoped to the session
|
|
139
|
+
* customer — another customer's orders are invisible, not 403.
|
|
140
|
+
* Errors: 401 `unauthenticated`, 400 `validation_failed` (bad
|
|
141
|
+
* limit/offset).
|
|
142
|
+
*/
|
|
143
|
+
export async function listOrders(
|
|
144
|
+
client: StorefrontClient,
|
|
145
|
+
query: ListOrdersQuery = {}
|
|
146
|
+
): Promise<OrderListResponse> {
|
|
147
|
+
return client.get("/api/store/orders", { query: { ...query } })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Retrieve MY order with items, fulfillments (+ tracking labels) and
|
|
152
|
+
* addresses. → 200 `{order}`.
|
|
153
|
+
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
154
|
+
* Errors: 401 `unauthenticated`, 404 `not_found` (unknown id OR an order
|
|
155
|
+
* belonging to another customer — ownership is part of the lookup, so
|
|
156
|
+
* cross-customer reads are indistinguishable from missing).
|
|
157
|
+
*/
|
|
158
|
+
export async function retrieveOrder(
|
|
159
|
+
client: StorefrontClient,
|
|
160
|
+
orderId: string
|
|
161
|
+
): Promise<OrderResponse> {
|
|
162
|
+
return client.get(`/api/store/orders/${orderId}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Retrieve MY order by its human number: the autoincrement `display_id`
|
|
167
|
+
* (numeric segment) or a `custom_display_id` (any segment; a numeric
|
|
168
|
+
* custom id wins over a colliding display_id — it is the number the store
|
|
169
|
+
* showed the customer). Same detail embeds + ownership scope as
|
|
170
|
+
* `retrieveOrder`.
|
|
171
|
+
* → 200 `{order}`.
|
|
172
|
+
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
173
|
+
* Errors: 401 `unauthenticated`, 404 `not_found` (unknown number OR another
|
|
174
|
+
* customer's order — indistinguishable by design).
|
|
175
|
+
*/
|
|
176
|
+
export async function retrieveOrderByDisplayId(
|
|
177
|
+
client: StorefrontClient,
|
|
178
|
+
displayId: string | number
|
|
179
|
+
): Promise<OrderResponse> {
|
|
180
|
+
return client.get(`/api/store/orders/display/${displayId}`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Transfers — move a (guest) order to the authenticated customer.
|
|
185
|
+
// Flow: the CLAIMING customer calls request → a token is issued for the
|
|
186
|
+
// order's email holder → the claimer (whose email must match the order's)
|
|
187
|
+
// calls accept with the token; decline/cancel end a pending transfer.
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
export interface RequestOrderTransferInput {
|
|
191
|
+
/** Free-text shown to the recipient; stored on the transfer action. */
|
|
192
|
+
description?: string
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* request → `{order:{id, transfer:{requested:true}}}` — ALWAYS this shape,
|
|
197
|
+
* for the first request and for duplicates alike. The transfer token is
|
|
198
|
+
* email-delivered only and never crosses the store surface.
|
|
199
|
+
*/
|
|
200
|
+
export interface RequestOrderTransferResponse {
|
|
201
|
+
order: {
|
|
202
|
+
id: string
|
|
203
|
+
transfer: { requested: true }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Request the order be transferred to ME (the authenticated customer).
|
|
209
|
+
* Idempotent: an existing pending transfer is returned, not duplicated.
|
|
210
|
+
* → 200.
|
|
211
|
+
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
212
|
+
* Errors: 401 `unauthenticated`, 404 `not_found`, 400 `already_owned`
|
|
213
|
+
* (the order already belongs to this customer).
|
|
214
|
+
*/
|
|
215
|
+
export async function requestOrderTransfer(
|
|
216
|
+
client: StorefrontClient,
|
|
217
|
+
orderId: string,
|
|
218
|
+
input: RequestOrderTransferInput = {}
|
|
219
|
+
): Promise<RequestOrderTransferResponse> {
|
|
220
|
+
return client.post(`/api/store/orders/${orderId}/transfer/request`, input)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface CancelOrderTransferResponse {
|
|
224
|
+
order: { id: string; transfer: { canceled: true } }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Cancel MY pending transfer request (only the requester may cancel).
|
|
229
|
+
* → 200.
|
|
230
|
+
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
231
|
+
* Errors: 401 `unauthenticated`, 404 `not_found` (order or no pending
|
|
232
|
+
* transfer), 403 `forbidden` (not the requester).
|
|
233
|
+
*/
|
|
234
|
+
export async function cancelOrderTransfer(
|
|
235
|
+
client: StorefrontClient,
|
|
236
|
+
orderId: string
|
|
237
|
+
): Promise<CancelOrderTransferResponse> {
|
|
238
|
+
return client.post(`/api/store/orders/${orderId}/transfer/cancel`)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface AcceptOrderTransferInput {
|
|
242
|
+
/** The transfer token received by email (≥16 chars). */
|
|
243
|
+
token: string
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** accept → the full updated order row (now owned by the caller). */
|
|
247
|
+
export interface AcceptOrderTransferResponse {
|
|
248
|
+
order: StoreOrderSummaryRow
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Accept a pending transfer with the emailed token — assigns the order to
|
|
253
|
+
* the authenticated customer. The caller's email must equal the order's
|
|
254
|
+
* original email (a leaked token alone is not enough). → 200 `{order}`.
|
|
255
|
+
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
256
|
+
* Errors: 401 `unauthenticated`, 404 `not_found` (order / no pending
|
|
257
|
+
* transfer), 403 `invalid_token` | `email_mismatch`, 400
|
|
258
|
+
* `validation_failed`.
|
|
259
|
+
*/
|
|
260
|
+
export async function acceptOrderTransfer(
|
|
261
|
+
client: StorefrontClient,
|
|
262
|
+
orderId: string,
|
|
263
|
+
input: AcceptOrderTransferInput
|
|
264
|
+
): Promise<AcceptOrderTransferResponse> {
|
|
265
|
+
return client.post(`/api/store/orders/${orderId}/transfer/accept`, input)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface DeclineOrderTransferInput {
|
|
269
|
+
/** The transfer token received by email (≥16 chars). */
|
|
270
|
+
token: string
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface DeclineOrderTransferResponse {
|
|
274
|
+
order: { id: string; transfer: { declined: true } }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Decline a pending transfer (the order's original email holder rejects
|
|
279
|
+
* it) — validates the token, then removes the pending action. → 200.
|
|
280
|
+
* Auth: REQUIRED Bearer JWT (+ `x-client-id`).
|
|
281
|
+
* Errors: 401 `unauthenticated`, 404 `not_found`, 403 `invalid_token`,
|
|
282
|
+
* 400 `validation_failed`.
|
|
283
|
+
*/
|
|
284
|
+
export async function declineOrderTransfer(
|
|
285
|
+
client: StorefrontClient,
|
|
286
|
+
orderId: string,
|
|
287
|
+
input: DeclineOrderTransferInput
|
|
288
|
+
): Promise<DeclineOrderTransferResponse> {
|
|
289
|
+
return client.post(`/api/store/orders/${orderId}/transfer/decline`, input)
|
|
290
|
+
}
|
package/src/api/redirects.ts
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront/api/redirects — 404-path URL redirect lookup
|
|
3
|
-
* (seo-listings card 21).
|
|
4
|
-
*
|
|
5
|
-
* Ground truth: src/app/api/store/url-redirects/route.ts +
|
|
6
|
-
* src/lib/content/url-redirects.ts.
|
|
7
|
-
*
|
|
8
|
-
* HOT-PATH RULE: call this ONLY from the storefront's not-found handler
|
|
9
|
-
* (the 404 path) — never on regular page loads. Exact-match lookup,
|
|
10
|
-
* index-covered on (client_id, from_path). When `to_path` is non-null the
|
|
11
|
-
* storefront issues a 301 to it.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { StorefrontClient } from "./http"
|
|
15
|
-
|
|
16
|
-
export interface RedirectLookupResponse {
|
|
17
|
-
/** The path you asked about (echoed). */
|
|
18
|
-
path: string
|
|
19
|
-
/** Redirect target, or null when no redirect exists — render the 404. */
|
|
20
|
-
to_path: string | null
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* GET /api/store/url-redirects?path=… — look up a redirect for a missed
|
|
25
|
-
* pathname (e.g. `/products/old-handle`).
|
|
26
|
-
*
|
|
27
|
-
* Auth: anon (x-client-id).
|
|
28
|
-
* Errors: 400 validation_failed (missing/empty path) · 400 missing_client_id.
|
|
29
|
-
* Settings: redirects are created in admin (and automatically on handle
|
|
30
|
-
* renames of products/collections/pages/posts).
|
|
31
|
-
*/
|
|
32
|
-
export async function lookupRedirect(
|
|
33
|
-
client: StorefrontClient,
|
|
34
|
-
path: string
|
|
35
|
-
): Promise<RedirectLookupResponse> {
|
|
36
|
-
return client.get("/api/store/url-redirects", { query: { path } })
|
|
37
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/redirects — 404-path URL redirect lookup
|
|
3
|
+
* (seo-listings card 21).
|
|
4
|
+
*
|
|
5
|
+
* Ground truth: src/app/api/store/url-redirects/route.ts +
|
|
6
|
+
* src/lib/content/url-redirects.ts.
|
|
7
|
+
*
|
|
8
|
+
* HOT-PATH RULE: call this ONLY from the storefront's not-found handler
|
|
9
|
+
* (the 404 path) — never on regular page loads. Exact-match lookup,
|
|
10
|
+
* index-covered on (client_id, from_path). When `to_path` is non-null the
|
|
11
|
+
* storefront issues a 301 to it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StorefrontClient } from "./http"
|
|
15
|
+
|
|
16
|
+
export interface RedirectLookupResponse {
|
|
17
|
+
/** The path you asked about (echoed). */
|
|
18
|
+
path: string
|
|
19
|
+
/** Redirect target, or null when no redirect exists — render the 404. */
|
|
20
|
+
to_path: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* GET /api/store/url-redirects?path=… — look up a redirect for a missed
|
|
25
|
+
* pathname (e.g. `/products/old-handle`).
|
|
26
|
+
*
|
|
27
|
+
* Auth: anon (x-client-id).
|
|
28
|
+
* Errors: 400 validation_failed (missing/empty path) · 400 missing_client_id.
|
|
29
|
+
* Settings: redirects are created in admin (and automatically on handle
|
|
30
|
+
* renames of products/collections/pages/posts).
|
|
31
|
+
*/
|
|
32
|
+
export async function lookupRedirect(
|
|
33
|
+
client: StorefrontClient,
|
|
34
|
+
path: string
|
|
35
|
+
): Promise<RedirectLookupResponse> {
|
|
36
|
+
return client.get("/api/store/url-redirects", { query: { path } })
|
|
37
|
+
}
|