@cartbase/storefront 0.19.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.
Files changed (105) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -258
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/redirects.ts +37 -37
  18. package/src/api/regions.ts +200 -200
  19. package/src/api/reviews.ts +22 -1
  20. package/src/api/search.ts +163 -163
  21. package/src/api/store.ts +35 -35
  22. package/src/api/types.ts +91 -91
  23. package/src/cart-drawer/context.tsx +778 -778
  24. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  25. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  26. package/src/cart-drawer/gift-wrap.tsx +82 -82
  27. package/src/cart-drawer/item/index.tsx +162 -162
  28. package/src/cart-drawer/item/upsell.tsx +110 -110
  29. package/src/cart-drawer/labels.ts +123 -123
  30. package/src/cart-drawer/mutation-queue.ts +78 -78
  31. package/src/cart-drawer/notes.tsx +131 -131
  32. package/src/cart-drawer/sticky-footer.tsx +73 -73
  33. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  34. package/src/checkout/address-error-copy.ts +117 -117
  35. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  36. package/src/checkout/compare-addresses.ts +40 -40
  37. package/src/checkout/discount-section.tsx +218 -218
  38. package/src/checkout/error-copy-codes.ts +63 -63
  39. package/src/checkout/geocode.ts +154 -154
  40. package/src/checkout/gift-card-section.tsx +224 -224
  41. package/src/checkout/index.ts +72 -72
  42. package/src/checkout/labels.ts +495 -495
  43. package/src/checkout/payment-button.tsx +372 -372
  44. package/src/checkout/payment-error-copy.ts +154 -154
  45. package/src/checkout/promotion-error-copy.ts +91 -91
  46. package/src/common/country-flag.tsx +52 -52
  47. package/src/common/country-select.tsx +11 -11
  48. package/src/common/index.ts +20 -20
  49. package/src/common/market-select.tsx +57 -57
  50. package/src/index.ts +12 -12
  51. package/src/lib/country-name.ts +59 -59
  52. package/src/lib/get-product-price.ts +133 -133
  53. package/src/lib/media-image.tsx +39 -39
  54. package/src/lib/payment-constants.ts +53 -53
  55. package/src/lib/platform.ts +13 -13
  56. package/src/lib/price.tsx +39 -39
  57. package/src/lib/store-api-error.ts +36 -36
  58. package/src/lib/variant-caption.ts +32 -32
  59. package/src/locales/bg.ts +3 -2
  60. package/src/locales/context.ts +37 -37
  61. package/src/locales/en.ts +26 -26
  62. package/src/locales/es.ts +3 -2
  63. package/src/locales/index.ts +19 -19
  64. package/src/locales/provider.tsx +59 -59
  65. package/src/locales/types.ts +77 -77
  66. package/src/order/index.ts +62 -62
  67. package/src/order/labels.ts +79 -79
  68. package/src/order/order-totals.tsx +250 -250
  69. package/src/primitives/select-field.tsx +93 -93
  70. package/src/products/image-gallery.tsx +43 -43
  71. package/src/products/option-select.tsx +67 -67
  72. package/src/products/product-info.tsx +5 -5
  73. package/src/products/product-price.tsx +69 -69
  74. package/src/products/product-promises.tsx +61 -61
  75. package/src/products/product-specs.tsx +65 -65
  76. package/src/products/product-tabs.tsx +123 -123
  77. package/src/products/purchase-options.tsx +130 -130
  78. package/src/products/sets.ts +110 -110
  79. package/src/products/variant-matching.ts +71 -71
  80. package/src/products/variant-url.ts +74 -74
  81. package/src/reviews-ui/index.ts +12 -0
  82. package/src/reviews-ui/labels.ts +5 -2
  83. package/src/reviews-ui/lightbox-state.ts +46 -46
  84. package/src/reviews-ui/review-header.tsx +166 -0
  85. package/src/reviews-ui/review-lightbox.tsx +271 -271
  86. package/src/reviews-ui/review-list.tsx +193 -191
  87. package/src/reviews-ui/review-widget.tsx +63 -71
  88. package/src/reviews-ui/star-badge.tsx +6 -4
  89. package/src/reviews-ui/widget-options.ts +55 -0
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. 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
+ }
@@ -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
+ }