@cartbase/storefront 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/package.json +4 -1
- 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 +181 -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 +56 -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/payment-badges.tsx +36 -96
- 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/icons/cartbase-mark.ts +9 -0
- package/src/common/icons/payment-marks.ts +73 -0
- package/src/common/icons/social-marks.ts +61 -0
- package/src/common/index.ts +35 -20
- package/src/common/market-select.tsx +57 -57
- package/src/common/payment-icons.tsx +54 -0
- package/src/common/powered-by-cartbase.tsx +47 -0
- package/src/common/social-links.tsx +65 -0
- 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/bg.ts +3 -0
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/es.ts +3 -0
- 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/store/labels.ts +10 -0
- 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/carts.ts
CHANGED
|
@@ -1,523 +1,523 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront/api/carts — cart lifecycle.
|
|
3
|
-
*
|
|
4
|
-
* Wraps the store cart routes (src/app/api/store/carts/**). Every mutation
|
|
5
|
-
* returns the FULL decorated cart (loadAndDecorate → decorateCartTotals +
|
|
6
|
-
* gift-card tender fields) so the storefront never recomputes money —
|
|
7
|
-
* totals are SERVER truth (DOCTRINE: the SDK displays, the API owns math).
|
|
8
|
-
*
|
|
9
|
-
* Auth: all cart routes are anon-writable — `x-client-id` (sent by the
|
|
10
|
-
* client) is the only requirement. A customer JWT (`authorization: Bearer`)
|
|
11
|
-
* is OPTIONAL and changes behavior where noted (customer attach, B2B group
|
|
12
|
-
* pricing). Amounts are EUR decimal major units.
|
|
13
|
-
*
|
|
14
|
-
* Doc: docs/storefront/carts.md · Errors envelope: `{error, code, details?}`
|
|
15
|
-
* → thrown as StoreApiError.
|
|
16
|
-
*/
|
|
17
|
-
import { readVisitorId } from "../lib/visitor"
|
|
18
|
-
import type { StorefrontClient } from "./http"
|
|
19
|
-
import type { IsoDateString, MajorUnitAmount } from "./types"
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// DTOs — ground truth: carts/route.ts, carts/_create.ts, carts/_helpers.ts,
|
|
23
|
-
// carts/_totals.ts, carts/_address.ts, [id]/line-items, [id]/customer,
|
|
24
|
-
// [id]/complete/_flow.ts.
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Address input (create/update). All fields optional on the generic cart
|
|
29
|
-
* routes (`AddressSchema.passthrough()` server-side — extra keys are kept).
|
|
30
|
-
* NOTE: prepare-checkout has its OWN stricter address schema (see
|
|
31
|
-
* `checkout.ts` PrepareCheckoutAddress — most fields required there).
|
|
32
|
-
*/
|
|
33
|
-
export interface CartAddressInput {
|
|
34
|
-
first_name?: string
|
|
35
|
-
last_name?: string
|
|
36
|
-
company?: string
|
|
37
|
-
address_1?: string
|
|
38
|
-
address_2?: string
|
|
39
|
-
city?: string
|
|
40
|
-
/** Lowercased server-side. */
|
|
41
|
-
country_code?: string
|
|
42
|
-
province?: string
|
|
43
|
-
postal_code?: string
|
|
44
|
-
phone?: string
|
|
45
|
-
metadata?: Record<string, unknown>
|
|
46
|
-
[key: string]: unknown
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Stored cart address row (`cart_addresses`). Unset fields come back null. */
|
|
50
|
-
export interface CartAddress {
|
|
51
|
-
id: string
|
|
52
|
-
first_name: string | null
|
|
53
|
-
last_name: string | null
|
|
54
|
-
company: string | null
|
|
55
|
-
address_1: string | null
|
|
56
|
-
address_2: string | null
|
|
57
|
-
city: string | null
|
|
58
|
-
country_code: string | null
|
|
59
|
-
province: string | null
|
|
60
|
-
postal_code: string | null
|
|
61
|
-
phone: string | null
|
|
62
|
-
metadata: Record<string, unknown> | null
|
|
63
|
-
[key: string]: unknown
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface CartLineItemAdjustment {
|
|
67
|
-
id: string
|
|
68
|
-
amount: MajorUnitAmount
|
|
69
|
-
promotion_id?: string | null
|
|
70
|
-
[key: string]: unknown
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface CartTaxLine {
|
|
74
|
-
id: string
|
|
75
|
-
/** Percentage, e.g. 20 for BG VAT. */
|
|
76
|
-
rate: number
|
|
77
|
-
[key: string]: unknown
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Per-line money decoration (carts/_totals.ts `decorateItem`) — present on
|
|
82
|
-
* every item and shipping method in every cart response.
|
|
83
|
-
*/
|
|
84
|
-
export interface LineTotals {
|
|
85
|
-
subtotal: MajorUnitAmount
|
|
86
|
-
total: MajorUnitAmount
|
|
87
|
-
original_total: MajorUnitAmount
|
|
88
|
-
tax_total: MajorUnitAmount
|
|
89
|
-
original_tax_total: MajorUnitAmount
|
|
90
|
-
discount_total: MajorUnitAmount
|
|
91
|
-
discount_subtotal: MajorUnitAmount
|
|
92
|
-
discount_tax_total: MajorUnitAmount
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Decorated cart line item (`cart_line_items` row + LineTotals). */
|
|
96
|
-
export interface CartLineItem extends LineTotals {
|
|
97
|
-
id: string
|
|
98
|
-
cart_id: string
|
|
99
|
-
variant_id: string | null
|
|
100
|
-
product_id: string | null
|
|
101
|
-
title: string | null
|
|
102
|
-
subtitle: string | null
|
|
103
|
-
product_title: string | null
|
|
104
|
-
product_handle: string | null
|
|
105
|
-
thumbnail: string | null
|
|
106
|
-
variant_title: string | null
|
|
107
|
-
variant_sku: string | null
|
|
108
|
-
quantity: number
|
|
109
|
-
unit_price: MajorUnitAmount
|
|
110
|
-
is_tax_inclusive: boolean
|
|
111
|
-
/** false for gift-card lines — they are never discountable. */
|
|
112
|
-
is_discountable: boolean
|
|
113
|
-
/** Gift-card product flag, copied onto the line at add time. */
|
|
114
|
-
is_giftcard: boolean
|
|
115
|
-
/** false for digital gift cards — lets digital-only carts complete without a shipping method. */
|
|
116
|
-
requires_shipping: boolean
|
|
117
|
-
metadata: Record<string, unknown> | null
|
|
118
|
-
adjustments: CartLineItemAdjustment[]
|
|
119
|
-
tax_lines: CartTaxLine[]
|
|
120
|
-
[key: string]: unknown
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Decorated cart shipping method (`cart_shipping_methods` row + LineTotals). */
|
|
124
|
-
export interface CartShippingMethod extends LineTotals {
|
|
125
|
-
id: string
|
|
126
|
-
cart_id: string
|
|
127
|
-
shipping_option_id: string | null
|
|
128
|
-
name: string | null
|
|
129
|
-
amount: MajorUnitAmount
|
|
130
|
-
is_tax_inclusive: boolean
|
|
131
|
-
data: Record<string, unknown> | null
|
|
132
|
-
adjustments: CartLineItemAdjustment[]
|
|
133
|
-
tax_lines: CartTaxLine[]
|
|
134
|
-
[key: string]: unknown
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Applied gift card as decorated onto every cart read (gift-card tender —
|
|
139
|
-
* see `gift-cards.ts` for apply/remove). `amount` is derived from the LIVE
|
|
140
|
-
* ledger at every read; a disabled/expired/depleted card stays listed at 0.
|
|
141
|
-
*/
|
|
142
|
-
export interface AppliedGiftCard {
|
|
143
|
-
id: string
|
|
144
|
-
/** Masked — the code itself is never echoed. */
|
|
145
|
-
last4: string
|
|
146
|
-
amount: MajorUnitAmount
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* The decorated cart — every cart-returning endpoint sends this shape
|
|
151
|
-
* (`{cart: Cart}`). Totals fields are computed by decorateCartTotals
|
|
152
|
-
* (carts/_totals.ts); gift-card fields by resolveGiftCardTender; the COD fee
|
|
153
|
-
* by resolveCodFee. All numbers are EUR decimal major units.
|
|
154
|
-
*/
|
|
155
|
-
export interface Cart {
|
|
156
|
-
id: string
|
|
157
|
-
region_id: string | null
|
|
158
|
-
currency_code: string
|
|
159
|
-
email: string | null
|
|
160
|
-
/** Set ONLY via the auth-derived paths (create with JWT / setCartCustomer). */
|
|
161
|
-
customer_id: string | null
|
|
162
|
-
sales_channel_id: string | null
|
|
163
|
-
locale: string | null
|
|
164
|
-
completed_at: IsoDateString | null
|
|
165
|
-
metadata: Record<string, unknown> | null
|
|
166
|
-
created_at: IsoDateString
|
|
167
|
-
updated_at: IsoDateString
|
|
168
|
-
region?: Record<string, unknown> | null
|
|
169
|
-
items: CartLineItem[]
|
|
170
|
-
shipping_methods: CartShippingMethod[]
|
|
171
|
-
shipping_address: CartAddress | null
|
|
172
|
-
billing_address: CartAddress | null
|
|
173
|
-
credit_lines: Array<{ amount: MajorUnitAmount; [key: string]: unknown }>
|
|
174
|
-
promotions: Array<Record<string, unknown>>
|
|
175
|
-
/** Embedded pivot → payment collection (+ its payment_sessions). */
|
|
176
|
-
payment_collection: unknown
|
|
177
|
-
// — cart-level totals (decorateCartTotals) —
|
|
178
|
-
total: MajorUnitAmount
|
|
179
|
-
subtotal: MajorUnitAmount
|
|
180
|
-
tax_total: MajorUnitAmount
|
|
181
|
-
discount_total: MajorUnitAmount
|
|
182
|
-
discount_subtotal: MajorUnitAmount
|
|
183
|
-
discount_tax_total: MajorUnitAmount
|
|
184
|
-
shipping_total: MajorUnitAmount
|
|
185
|
-
shipping_subtotal: MajorUnitAmount
|
|
186
|
-
shipping_tax_total: MajorUnitAmount
|
|
187
|
-
shipping_discount_total: MajorUnitAmount
|
|
188
|
-
original_shipping_total: MajorUnitAmount
|
|
189
|
-
original_shipping_subtotal: MajorUnitAmount
|
|
190
|
-
original_shipping_tax_total: MajorUnitAmount
|
|
191
|
-
item_total: MajorUnitAmount
|
|
192
|
-
item_subtotal: MajorUnitAmount
|
|
193
|
-
item_tax_total: MajorUnitAmount
|
|
194
|
-
item_discount_total: MajorUnitAmount
|
|
195
|
-
original_item_total: MajorUnitAmount
|
|
196
|
-
original_item_subtotal: MajorUnitAmount
|
|
197
|
-
original_item_tax_total: MajorUnitAmount
|
|
198
|
-
original_total: MajorUnitAmount
|
|
199
|
-
original_subtotal: MajorUnitAmount
|
|
200
|
-
original_tax_total: MajorUnitAmount
|
|
201
|
-
credit_line_total: MajorUnitAmount
|
|
202
|
-
credit_line_subtotal: MajorUnitAmount
|
|
203
|
-
credit_line_tax_total: MajorUnitAmount
|
|
204
|
-
/** The payment method fee — the chosen method's fee_amount (any method
|
|
205
|
-
* may carry one); non-zero only while a live method session is selected.
|
|
206
|
-
* Folded into `total`; its own totals row, never a line item. */
|
|
207
|
-
payment_method_fee_total: MajorUnitAmount
|
|
208
|
-
payment_method_fee_label: string | null
|
|
209
|
-
// — gift-card tender decoration (totals NEVER move; see gift-cards.ts) —
|
|
210
|
-
gift_cards: AppliedGiftCard[]
|
|
211
|
-
/** Σ of applied-card coverage (= the pp_giftcard session amount). */
|
|
212
|
-
gift_card_total: MajorUnitAmount
|
|
213
|
-
/** `max(total − gift_card_total, 0)` — what the remainder provider charges. */
|
|
214
|
-
gift_card_remainder: MajorUnitAmount
|
|
215
|
-
[key: string]: unknown
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export interface CartResponse {
|
|
219
|
-
cart: Cart
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Body of POST /api/store/carts (`.strict()` — unknown keys → 400
|
|
224
|
-
* `validation_failed`). `customer_id` is deliberately NOT a field: the
|
|
225
|
-
* customer is derived from the Bearer JWT (forgery-vector guard G2).
|
|
226
|
-
*/
|
|
227
|
-
export interface CreateCartInput {
|
|
228
|
-
/** Falls back to `stores.default_region_id`; neither → 400 `region_required`. */
|
|
229
|
-
region_id?: string
|
|
230
|
-
email?: string
|
|
231
|
-
/** Must be enabled for the store; region currency is the default. */
|
|
232
|
-
currency_code?: string
|
|
233
|
-
items?: Array<{ variant_id: string; quantity: number; selling_plan_id?: string }>
|
|
234
|
-
/**
|
|
235
|
-
* B2B v1: with a publishable key, omitting this defaults to the key's
|
|
236
|
-
* channel; a channel OUTSIDE the key's scope → 400 `invalid_sales_channel`.
|
|
237
|
-
*/
|
|
238
|
-
sales_channel_id?: string
|
|
239
|
-
promo_codes?: string[]
|
|
240
|
-
shipping_address?: CartAddressInput
|
|
241
|
-
billing_address?: CartAddressInput
|
|
242
|
-
metadata?: Record<string, unknown>
|
|
243
|
-
locale?: string
|
|
244
|
-
/**
|
|
245
|
-
* THE JOIN KEY: this browser's visitor id, which is what connects the
|
|
246
|
-
* browsing that led here to the money. Attached automatically by
|
|
247
|
-
* `createCart` when the page can read the cookie, so a storefront never
|
|
248
|
-
* has to remember it. Stamped at birth or never.
|
|
249
|
-
*/
|
|
250
|
-
device_id?: string
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Body of POST /api/store/carts/:id (partial update, `.strict()`).
|
|
255
|
-
* Addresses: object = set/replace in place, null = clear, absent = untouched.
|
|
256
|
-
* A region change re-resolves currency, CLEARS the shipping address (unless
|
|
257
|
-
* a new one is provided in the same call), drops custom-priced items and
|
|
258
|
-
* re-prices the rest.
|
|
259
|
-
*/
|
|
260
|
-
export interface UpdateCartInput {
|
|
261
|
-
region_id?: string
|
|
262
|
-
email?: string
|
|
263
|
-
currency_code?: string
|
|
264
|
-
sales_channel_id?: string
|
|
265
|
-
metadata?: Record<string, unknown>
|
|
266
|
-
locale?: string
|
|
267
|
-
/** Write-once: a cart that already carries a visitor keeps it. */
|
|
268
|
-
device_id?: string
|
|
269
|
-
shipping_address?: CartAddressInput | null
|
|
270
|
-
billing_address?: CartAddressInput | null
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export interface AddLineItemInput {
|
|
274
|
-
variant_id: string
|
|
275
|
-
/**
|
|
276
|
-
* Positive integer. Adding a variant already in the cart UNDER THE SAME
|
|
277
|
-
* PLAN (or both one-time) bumps its quantity; a one-time line and a
|
|
278
|
-
* subscription line of the same variant stay separate.
|
|
279
|
-
*/
|
|
280
|
-
quantity: number
|
|
281
|
-
/**
|
|
282
|
-
* Subscription plan for this line (from
|
|
283
|
-
* `products.listSellingPlans`). The server validates the plan is enabled
|
|
284
|
-
* and attached to the variant's product (else 400 `invalid_selling_plan`)
|
|
285
|
-
* and applies the plan price. Omit for a one-time purchase.
|
|
286
|
-
*/
|
|
287
|
-
selling_plan_id?: string
|
|
288
|
-
metadata?: Record<string, unknown>
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export interface UpdateLineItemInput {
|
|
292
|
-
/**
|
|
293
|
-
* Integer ≥ 0 — REQUIRED. 0 deletes the line. NOTE (code wins over
|
|
294
|
-
* store-api.md): `metadata` is NOT accepted on update, only on add.
|
|
295
|
-
*/
|
|
296
|
-
quantity: number
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Success shape of POST /api/store/carts/:id/complete.
|
|
301
|
-
*
|
|
302
|
-
* NOTE (code wins over store-api.md): the route NEVER returns the Medusa
|
|
303
|
-
* `{type:"cart", cart, error}` failure union — failures THROW the standard
|
|
304
|
-
* error envelope (StoreApiError) and the cart stays open/retryable.
|
|
305
|
-
*/
|
|
306
|
-
export interface CompleteCartResult {
|
|
307
|
-
type: "order"
|
|
308
|
-
order: CompletedOrder
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/** Order shape returned by complete (complete/_flow.ts loadOrder). */
|
|
312
|
-
export interface CompletedOrder {
|
|
313
|
-
id: string
|
|
314
|
-
display_id?: number
|
|
315
|
-
status: string
|
|
316
|
-
email: string | null
|
|
317
|
-
currency_code: string
|
|
318
|
-
customer_id: string | null
|
|
319
|
-
sales_channel_id?: string | null
|
|
320
|
-
created_at: IsoDateString
|
|
321
|
-
summary: Array<Record<string, unknown>> | Record<string, unknown> | null
|
|
322
|
-
/** Flattened line items (order_line_items + pivot quantity). */
|
|
323
|
-
items: Array<Record<string, unknown> & { quantity: number }>
|
|
324
|
-
[key: string]: unknown
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ---------------------------------------------------------------------------
|
|
328
|
-
// Functions
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Create a cart. → 201 `{cart}`.
|
|
333
|
-
*
|
|
334
|
-
* Auth: anon `x-client-id`; optional Bearer JWT attaches the customer
|
|
335
|
-
* (customer_id + email) and prices initial items with the customer's B2B
|
|
336
|
-
* groups; optional publishable key applies channel scope.
|
|
337
|
-
* Errors: 400 `validation_failed` | `invalid_region` | `region_required` |
|
|
338
|
-
* `invalid_sales_channel` | `price_not_found` (an initial item has no price
|
|
339
|
-
* in the cart currency), 404 `variant_not_found`. An unsupported
|
|
340
|
-
* `currency_code` does NOT error — it silently falls back to the region
|
|
341
|
-
* currency (only `store_currencies` rows are honored).
|
|
342
|
-
* Settings: store default region; enabled store currencies; publishable-key
|
|
343
|
-
* channel scope; automatic promotions re-applied on create.
|
|
344
|
-
*/
|
|
345
|
-
export async function createCart(
|
|
346
|
-
client: StorefrontClient,
|
|
347
|
-
input: CreateCartInput = {}
|
|
348
|
-
): Promise<CartResponse> {
|
|
349
|
-
// The visitor id rides along automatically. It exists only in this
|
|
350
|
-
// browser at this moment, so a cart created without it can never be
|
|
351
|
-
// attributed afterwards — which is why this is here and not left to the
|
|
352
|
-
// caller to remember.
|
|
353
|
-
const device_id = input.device_id ?? readVisitorId()
|
|
354
|
-
return client.post("/api/store/carts", device_id ? { ...input, device_id } : input)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Retrieve the decorated cart. → 200 `{cart}`.
|
|
359
|
-
* Auth: anon `x-client-id`. Errors: 404 `cart_not_found`.
|
|
360
|
-
* Every read re-runs tax recalc + totals + gift-card tender — amounts are
|
|
361
|
-
* always current (a gift-card balance spent elsewhere shrinks this cart's
|
|
362
|
-
* tender at the next read).
|
|
363
|
-
*/
|
|
364
|
-
export async function retrieveCart(
|
|
365
|
-
client: StorefrontClient,
|
|
366
|
-
cartId: string
|
|
367
|
-
): Promise<CartResponse> {
|
|
368
|
-
return client.get(`/api/store/carts/${cartId}`)
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Partial-update the cart. → 200 `{cart}`.
|
|
373
|
-
* Auth: anon `x-client-id`.
|
|
374
|
-
* Errors: 404 `cart_not_found`, 409 `cart_completed`, 400
|
|
375
|
-
* `validation_failed` | `invalid_region` (unsupported `currency_code` falls
|
|
376
|
-
* back to the region currency instead of erroring).
|
|
377
|
-
* Settings: region change re-prices lines using the cart customer's groups
|
|
378
|
-
* (B2B price lists) and re-applies promotions.
|
|
379
|
-
*/
|
|
380
|
-
export async function updateCart(
|
|
381
|
-
client: StorefrontClient,
|
|
382
|
-
cartId: string,
|
|
383
|
-
input: UpdateCartInput
|
|
384
|
-
): Promise<CartResponse> {
|
|
385
|
-
return client.post(`/api/store/carts/${cartId}`, input)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Attach the AUTHENTICATED customer to the cart. Body is empty by contract
|
|
390
|
-
* (`z.object({}).strict()`) — the customer comes from the Bearer JWT, never
|
|
391
|
-
* from the body (forgery-vector guard). → 200 `{cart}`.
|
|
392
|
-
* Auth: `x-client-id` + REQUIRED Bearer JWT.
|
|
393
|
-
* Errors: 401 `unauthenticated`, 404 `cart_not_found`, 409 `cart_completed`,
|
|
394
|
-
* 400 `validation_failed` (any body key).
|
|
395
|
-
*/
|
|
396
|
-
export async function setCartCustomer(
|
|
397
|
-
client: StorefrontClient,
|
|
398
|
-
cartId: string
|
|
399
|
-
): Promise<CartResponse> {
|
|
400
|
-
return client.post(`/api/store/carts/${cartId}/customer`, {})
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Add a line item (same-variant add bumps quantity). → 200 `{cart}`.
|
|
405
|
-
* Auth: anon `x-client-id`.
|
|
406
|
-
* Errors: 404 `cart_not_found` | `variant_not_found`, 409 `cart_completed`,
|
|
407
|
-
* 400 `insufficient_inventory` (kit-aware: every linked inventory component
|
|
408
|
-
* is checked; details carry `{variant_id, inventory_item_id, available,
|
|
409
|
-
* requested}`) | `validation_failed` | `price_not_found` (variant has no
|
|
410
|
-
* price in the cart currency).
|
|
411
|
-
* Settings: B2B price lists (via the cart's attached customer), gift-card
|
|
412
|
-
* product flag (`is_giftcard`/`requires_shipping` ride the line), automatic
|
|
413
|
-
* promotions re-applied.
|
|
414
|
-
*/
|
|
415
|
-
export async function addLineItem(
|
|
416
|
-
client: StorefrontClient,
|
|
417
|
-
cartId: string,
|
|
418
|
-
input: AddLineItemInput
|
|
419
|
-
): Promise<CartResponse> {
|
|
420
|
-
return client.post(`/api/store/carts/${cartId}/line-items`, input)
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Set a line's quantity (0 deletes it). → 200 `{cart}`.
|
|
425
|
-
* Auth: anon `x-client-id`.
|
|
426
|
-
* Errors: 404 `cart_not_found` | `line_item_not_found`, 409 `cart_completed`,
|
|
427
|
-
* 400 `insufficient_inventory` | `validation_failed`.
|
|
428
|
-
*/
|
|
429
|
-
export async function updateLineItem(
|
|
430
|
-
client: StorefrontClient,
|
|
431
|
-
cartId: string,
|
|
432
|
-
lineItemId: string,
|
|
433
|
-
input: UpdateLineItemInput
|
|
434
|
-
): Promise<CartResponse> {
|
|
435
|
-
return client.post(`/api/store/carts/${cartId}/line-items/${lineItemId}`, input)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Remove a line item (idempotent — removing an already-gone line still
|
|
440
|
-
* returns the cart). → 200 `{cart}`.
|
|
441
|
-
* Auth: anon `x-client-id`. Errors: 404 `cart_not_found` (bad cart id).
|
|
442
|
-
*/
|
|
443
|
-
export async function deleteLineItem(
|
|
444
|
-
client: StorefrontClient,
|
|
445
|
-
cartId: string,
|
|
446
|
-
lineItemId: string
|
|
447
|
-
): Promise<CartResponse> {
|
|
448
|
-
return client.delete(`/api/store/carts/${cartId}/line-items/${lineItemId}`)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Complete the cart → place the order. → 200 `{type:"order", order}`.
|
|
453
|
-
* Idempotent: recalling for a completed cart returns the SAME order
|
|
454
|
-
* (order_cart link); concurrent completes are serialized by a CAS lock
|
|
455
|
-
* (the loser either returns the winner's order or 409 `cart_locked`).
|
|
456
|
-
*
|
|
457
|
-
* Auth: anon `x-client-id` (guest checkout) — but when the store setting
|
|
458
|
-
* `accounts_mode='required'`, a guest cart (no attached customer) → 403
|
|
459
|
-
* `account_required`.
|
|
460
|
-
*
|
|
461
|
-
* Validation errors (400): `cart_email_required`, `cart_empty`,
|
|
462
|
-
* `shipping_address_required`, `shipping_method_required` (only when a line
|
|
463
|
-
* `requires_shipping` — digital-only carts skip it),
|
|
464
|
-
* `payment_collection_required`, `payment_session_required`,
|
|
465
|
-
* `checkout_method_hidden` (checkout-rules completion guard), 400
|
|
466
|
-
* `insufficient_inventory` (reserve step). Payment errors (402):
|
|
467
|
-
* `requires_action` (3DS — details carry `client_secret`),
|
|
468
|
-
* `payment_not_authorized`, `payment_not_initiated`, `payment_incomplete`,
|
|
469
|
-
* `gift_card_insufficient_balance`, `gift_card_not_redeemable`. 409
|
|
470
|
-
* `cart_locked`. Any failure compensates fully (order deleted, inventory
|
|
471
|
-
* released, gift tender reversed, cart unlocked) — the cart stays retryable.
|
|
472
|
-
*
|
|
473
|
-
* Settings: checkout rules (completion guard), accounts_mode, COD fee
|
|
474
|
-
* (carried onto the order via order_summaries.totals), gift-card tender
|
|
475
|
-
* (zero-remainder carts complete on the gift session alone).
|
|
476
|
-
* Emits `order.placed` on the durable bus after commit.
|
|
477
|
-
*/
|
|
478
|
-
export async function completeCart(
|
|
479
|
-
client: StorefrontClient,
|
|
480
|
-
cartId: string
|
|
481
|
-
): Promise<CompleteCartResult> {
|
|
482
|
-
return client.post(`/api/store/carts/${cartId}/complete`, {})
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// ---------------------------------------------------------------------------
|
|
486
|
-
// Promotions — promo codes on the cart (batch-10 merge: the checkout
|
|
487
|
-
// DiscountSection consumed the route raw before this wrapper existed).
|
|
488
|
-
// ---------------------------------------------------------------------------
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Apply promo code(s) to the cart. → 200 `{cart}` (decorated — totals and
|
|
492
|
-
* `promotions` recomputed server-side; adding is idempotent per code).
|
|
493
|
-
* Auth: anon `x-client-id`.
|
|
494
|
-
* Errors: 404 `cart_not_found`, 400 zod (`promo_codes` must be a non-empty
|
|
495
|
-
* string array), 404 `promotion_not_found` (unknown code on ADD), 400
|
|
496
|
-
* `promotion_inactive` (draft/expired code on ADD).
|
|
497
|
-
*/
|
|
498
|
-
export async function applyPromotions(
|
|
499
|
-
client: StorefrontClient,
|
|
500
|
-
cartId: string,
|
|
501
|
-
promoCodes: string[]
|
|
502
|
-
): Promise<CartResponse> {
|
|
503
|
-
return client.post(`/api/store/carts/${cartId}/promotions`, {
|
|
504
|
-
promo_codes: promoCodes,
|
|
505
|
-
})
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Remove promo code(s) from the cart. → 200 `{cart}` (decorated). Unknown
|
|
510
|
-
* codes silently no-op on remove (Medusa parity).
|
|
511
|
-
* Auth: anon `x-client-id`.
|
|
512
|
-
* Errors: 404 `cart_not_found`, 400 zod.
|
|
513
|
-
*/
|
|
514
|
-
export async function removePromotions(
|
|
515
|
-
client: StorefrontClient,
|
|
516
|
-
cartId: string,
|
|
517
|
-
promoCodes: string[]
|
|
518
|
-
): Promise<CartResponse> {
|
|
519
|
-
return client.request(`/api/store/carts/${cartId}/promotions`, {
|
|
520
|
-
method: "DELETE",
|
|
521
|
-
body: { promo_codes: promoCodes },
|
|
522
|
-
})
|
|
523
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/carts — cart lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the store cart routes (src/app/api/store/carts/**). Every mutation
|
|
5
|
+
* returns the FULL decorated cart (loadAndDecorate → decorateCartTotals +
|
|
6
|
+
* gift-card tender fields) so the storefront never recomputes money —
|
|
7
|
+
* totals are SERVER truth (DOCTRINE: the SDK displays, the API owns math).
|
|
8
|
+
*
|
|
9
|
+
* Auth: all cart routes are anon-writable — `x-client-id` (sent by the
|
|
10
|
+
* client) is the only requirement. A customer JWT (`authorization: Bearer`)
|
|
11
|
+
* is OPTIONAL and changes behavior where noted (customer attach, B2B group
|
|
12
|
+
* pricing). Amounts are EUR decimal major units.
|
|
13
|
+
*
|
|
14
|
+
* Doc: docs/storefront/carts.md · Errors envelope: `{error, code, details?}`
|
|
15
|
+
* → thrown as StoreApiError.
|
|
16
|
+
*/
|
|
17
|
+
import { readVisitorId } from "../lib/visitor"
|
|
18
|
+
import type { StorefrontClient } from "./http"
|
|
19
|
+
import type { IsoDateString, MajorUnitAmount } from "./types"
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// DTOs — ground truth: carts/route.ts, carts/_create.ts, carts/_helpers.ts,
|
|
23
|
+
// carts/_totals.ts, carts/_address.ts, [id]/line-items, [id]/customer,
|
|
24
|
+
// [id]/complete/_flow.ts.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Address input (create/update). All fields optional on the generic cart
|
|
29
|
+
* routes (`AddressSchema.passthrough()` server-side — extra keys are kept).
|
|
30
|
+
* NOTE: prepare-checkout has its OWN stricter address schema (see
|
|
31
|
+
* `checkout.ts` PrepareCheckoutAddress — most fields required there).
|
|
32
|
+
*/
|
|
33
|
+
export interface CartAddressInput {
|
|
34
|
+
first_name?: string
|
|
35
|
+
last_name?: string
|
|
36
|
+
company?: string
|
|
37
|
+
address_1?: string
|
|
38
|
+
address_2?: string
|
|
39
|
+
city?: string
|
|
40
|
+
/** Lowercased server-side. */
|
|
41
|
+
country_code?: string
|
|
42
|
+
province?: string
|
|
43
|
+
postal_code?: string
|
|
44
|
+
phone?: string
|
|
45
|
+
metadata?: Record<string, unknown>
|
|
46
|
+
[key: string]: unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Stored cart address row (`cart_addresses`). Unset fields come back null. */
|
|
50
|
+
export interface CartAddress {
|
|
51
|
+
id: string
|
|
52
|
+
first_name: string | null
|
|
53
|
+
last_name: string | null
|
|
54
|
+
company: string | null
|
|
55
|
+
address_1: string | null
|
|
56
|
+
address_2: string | null
|
|
57
|
+
city: string | null
|
|
58
|
+
country_code: string | null
|
|
59
|
+
province: string | null
|
|
60
|
+
postal_code: string | null
|
|
61
|
+
phone: string | null
|
|
62
|
+
metadata: Record<string, unknown> | null
|
|
63
|
+
[key: string]: unknown
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CartLineItemAdjustment {
|
|
67
|
+
id: string
|
|
68
|
+
amount: MajorUnitAmount
|
|
69
|
+
promotion_id?: string | null
|
|
70
|
+
[key: string]: unknown
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CartTaxLine {
|
|
74
|
+
id: string
|
|
75
|
+
/** Percentage, e.g. 20 for BG VAT. */
|
|
76
|
+
rate: number
|
|
77
|
+
[key: string]: unknown
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Per-line money decoration (carts/_totals.ts `decorateItem`) — present on
|
|
82
|
+
* every item and shipping method in every cart response.
|
|
83
|
+
*/
|
|
84
|
+
export interface LineTotals {
|
|
85
|
+
subtotal: MajorUnitAmount
|
|
86
|
+
total: MajorUnitAmount
|
|
87
|
+
original_total: MajorUnitAmount
|
|
88
|
+
tax_total: MajorUnitAmount
|
|
89
|
+
original_tax_total: MajorUnitAmount
|
|
90
|
+
discount_total: MajorUnitAmount
|
|
91
|
+
discount_subtotal: MajorUnitAmount
|
|
92
|
+
discount_tax_total: MajorUnitAmount
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Decorated cart line item (`cart_line_items` row + LineTotals). */
|
|
96
|
+
export interface CartLineItem extends LineTotals {
|
|
97
|
+
id: string
|
|
98
|
+
cart_id: string
|
|
99
|
+
variant_id: string | null
|
|
100
|
+
product_id: string | null
|
|
101
|
+
title: string | null
|
|
102
|
+
subtitle: string | null
|
|
103
|
+
product_title: string | null
|
|
104
|
+
product_handle: string | null
|
|
105
|
+
thumbnail: string | null
|
|
106
|
+
variant_title: string | null
|
|
107
|
+
variant_sku: string | null
|
|
108
|
+
quantity: number
|
|
109
|
+
unit_price: MajorUnitAmount
|
|
110
|
+
is_tax_inclusive: boolean
|
|
111
|
+
/** false for gift-card lines — they are never discountable. */
|
|
112
|
+
is_discountable: boolean
|
|
113
|
+
/** Gift-card product flag, copied onto the line at add time. */
|
|
114
|
+
is_giftcard: boolean
|
|
115
|
+
/** false for digital gift cards — lets digital-only carts complete without a shipping method. */
|
|
116
|
+
requires_shipping: boolean
|
|
117
|
+
metadata: Record<string, unknown> | null
|
|
118
|
+
adjustments: CartLineItemAdjustment[]
|
|
119
|
+
tax_lines: CartTaxLine[]
|
|
120
|
+
[key: string]: unknown
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Decorated cart shipping method (`cart_shipping_methods` row + LineTotals). */
|
|
124
|
+
export interface CartShippingMethod extends LineTotals {
|
|
125
|
+
id: string
|
|
126
|
+
cart_id: string
|
|
127
|
+
shipping_option_id: string | null
|
|
128
|
+
name: string | null
|
|
129
|
+
amount: MajorUnitAmount
|
|
130
|
+
is_tax_inclusive: boolean
|
|
131
|
+
data: Record<string, unknown> | null
|
|
132
|
+
adjustments: CartLineItemAdjustment[]
|
|
133
|
+
tax_lines: CartTaxLine[]
|
|
134
|
+
[key: string]: unknown
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Applied gift card as decorated onto every cart read (gift-card tender —
|
|
139
|
+
* see `gift-cards.ts` for apply/remove). `amount` is derived from the LIVE
|
|
140
|
+
* ledger at every read; a disabled/expired/depleted card stays listed at 0.
|
|
141
|
+
*/
|
|
142
|
+
export interface AppliedGiftCard {
|
|
143
|
+
id: string
|
|
144
|
+
/** Masked — the code itself is never echoed. */
|
|
145
|
+
last4: string
|
|
146
|
+
amount: MajorUnitAmount
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The decorated cart — every cart-returning endpoint sends this shape
|
|
151
|
+
* (`{cart: Cart}`). Totals fields are computed by decorateCartTotals
|
|
152
|
+
* (carts/_totals.ts); gift-card fields by resolveGiftCardTender; the COD fee
|
|
153
|
+
* by resolveCodFee. All numbers are EUR decimal major units.
|
|
154
|
+
*/
|
|
155
|
+
export interface Cart {
|
|
156
|
+
id: string
|
|
157
|
+
region_id: string | null
|
|
158
|
+
currency_code: string
|
|
159
|
+
email: string | null
|
|
160
|
+
/** Set ONLY via the auth-derived paths (create with JWT / setCartCustomer). */
|
|
161
|
+
customer_id: string | null
|
|
162
|
+
sales_channel_id: string | null
|
|
163
|
+
locale: string | null
|
|
164
|
+
completed_at: IsoDateString | null
|
|
165
|
+
metadata: Record<string, unknown> | null
|
|
166
|
+
created_at: IsoDateString
|
|
167
|
+
updated_at: IsoDateString
|
|
168
|
+
region?: Record<string, unknown> | null
|
|
169
|
+
items: CartLineItem[]
|
|
170
|
+
shipping_methods: CartShippingMethod[]
|
|
171
|
+
shipping_address: CartAddress | null
|
|
172
|
+
billing_address: CartAddress | null
|
|
173
|
+
credit_lines: Array<{ amount: MajorUnitAmount; [key: string]: unknown }>
|
|
174
|
+
promotions: Array<Record<string, unknown>>
|
|
175
|
+
/** Embedded pivot → payment collection (+ its payment_sessions). */
|
|
176
|
+
payment_collection: unknown
|
|
177
|
+
// — cart-level totals (decorateCartTotals) —
|
|
178
|
+
total: MajorUnitAmount
|
|
179
|
+
subtotal: MajorUnitAmount
|
|
180
|
+
tax_total: MajorUnitAmount
|
|
181
|
+
discount_total: MajorUnitAmount
|
|
182
|
+
discount_subtotal: MajorUnitAmount
|
|
183
|
+
discount_tax_total: MajorUnitAmount
|
|
184
|
+
shipping_total: MajorUnitAmount
|
|
185
|
+
shipping_subtotal: MajorUnitAmount
|
|
186
|
+
shipping_tax_total: MajorUnitAmount
|
|
187
|
+
shipping_discount_total: MajorUnitAmount
|
|
188
|
+
original_shipping_total: MajorUnitAmount
|
|
189
|
+
original_shipping_subtotal: MajorUnitAmount
|
|
190
|
+
original_shipping_tax_total: MajorUnitAmount
|
|
191
|
+
item_total: MajorUnitAmount
|
|
192
|
+
item_subtotal: MajorUnitAmount
|
|
193
|
+
item_tax_total: MajorUnitAmount
|
|
194
|
+
item_discount_total: MajorUnitAmount
|
|
195
|
+
original_item_total: MajorUnitAmount
|
|
196
|
+
original_item_subtotal: MajorUnitAmount
|
|
197
|
+
original_item_tax_total: MajorUnitAmount
|
|
198
|
+
original_total: MajorUnitAmount
|
|
199
|
+
original_subtotal: MajorUnitAmount
|
|
200
|
+
original_tax_total: MajorUnitAmount
|
|
201
|
+
credit_line_total: MajorUnitAmount
|
|
202
|
+
credit_line_subtotal: MajorUnitAmount
|
|
203
|
+
credit_line_tax_total: MajorUnitAmount
|
|
204
|
+
/** The payment method fee — the chosen method's fee_amount (any method
|
|
205
|
+
* may carry one); non-zero only while a live method session is selected.
|
|
206
|
+
* Folded into `total`; its own totals row, never a line item. */
|
|
207
|
+
payment_method_fee_total: MajorUnitAmount
|
|
208
|
+
payment_method_fee_label: string | null
|
|
209
|
+
// — gift-card tender decoration (totals NEVER move; see gift-cards.ts) —
|
|
210
|
+
gift_cards: AppliedGiftCard[]
|
|
211
|
+
/** Σ of applied-card coverage (= the pp_giftcard session amount). */
|
|
212
|
+
gift_card_total: MajorUnitAmount
|
|
213
|
+
/** `max(total − gift_card_total, 0)` — what the remainder provider charges. */
|
|
214
|
+
gift_card_remainder: MajorUnitAmount
|
|
215
|
+
[key: string]: unknown
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface CartResponse {
|
|
219
|
+
cart: Cart
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Body of POST /api/store/carts (`.strict()` — unknown keys → 400
|
|
224
|
+
* `validation_failed`). `customer_id` is deliberately NOT a field: the
|
|
225
|
+
* customer is derived from the Bearer JWT (forgery-vector guard G2).
|
|
226
|
+
*/
|
|
227
|
+
export interface CreateCartInput {
|
|
228
|
+
/** Falls back to `stores.default_region_id`; neither → 400 `region_required`. */
|
|
229
|
+
region_id?: string
|
|
230
|
+
email?: string
|
|
231
|
+
/** Must be enabled for the store; region currency is the default. */
|
|
232
|
+
currency_code?: string
|
|
233
|
+
items?: Array<{ variant_id: string; quantity: number; selling_plan_id?: string }>
|
|
234
|
+
/**
|
|
235
|
+
* B2B v1: with a publishable key, omitting this defaults to the key's
|
|
236
|
+
* channel; a channel OUTSIDE the key's scope → 400 `invalid_sales_channel`.
|
|
237
|
+
*/
|
|
238
|
+
sales_channel_id?: string
|
|
239
|
+
promo_codes?: string[]
|
|
240
|
+
shipping_address?: CartAddressInput
|
|
241
|
+
billing_address?: CartAddressInput
|
|
242
|
+
metadata?: Record<string, unknown>
|
|
243
|
+
locale?: string
|
|
244
|
+
/**
|
|
245
|
+
* THE JOIN KEY: this browser's visitor id, which is what connects the
|
|
246
|
+
* browsing that led here to the money. Attached automatically by
|
|
247
|
+
* `createCart` when the page can read the cookie, so a storefront never
|
|
248
|
+
* has to remember it. Stamped at birth or never.
|
|
249
|
+
*/
|
|
250
|
+
device_id?: string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Body of POST /api/store/carts/:id (partial update, `.strict()`).
|
|
255
|
+
* Addresses: object = set/replace in place, null = clear, absent = untouched.
|
|
256
|
+
* A region change re-resolves currency, CLEARS the shipping address (unless
|
|
257
|
+
* a new one is provided in the same call), drops custom-priced items and
|
|
258
|
+
* re-prices the rest.
|
|
259
|
+
*/
|
|
260
|
+
export interface UpdateCartInput {
|
|
261
|
+
region_id?: string
|
|
262
|
+
email?: string
|
|
263
|
+
currency_code?: string
|
|
264
|
+
sales_channel_id?: string
|
|
265
|
+
metadata?: Record<string, unknown>
|
|
266
|
+
locale?: string
|
|
267
|
+
/** Write-once: a cart that already carries a visitor keeps it. */
|
|
268
|
+
device_id?: string
|
|
269
|
+
shipping_address?: CartAddressInput | null
|
|
270
|
+
billing_address?: CartAddressInput | null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface AddLineItemInput {
|
|
274
|
+
variant_id: string
|
|
275
|
+
/**
|
|
276
|
+
* Positive integer. Adding a variant already in the cart UNDER THE SAME
|
|
277
|
+
* PLAN (or both one-time) bumps its quantity; a one-time line and a
|
|
278
|
+
* subscription line of the same variant stay separate.
|
|
279
|
+
*/
|
|
280
|
+
quantity: number
|
|
281
|
+
/**
|
|
282
|
+
* Subscription plan for this line (from
|
|
283
|
+
* `products.listSellingPlans`). The server validates the plan is enabled
|
|
284
|
+
* and attached to the variant's product (else 400 `invalid_selling_plan`)
|
|
285
|
+
* and applies the plan price. Omit for a one-time purchase.
|
|
286
|
+
*/
|
|
287
|
+
selling_plan_id?: string
|
|
288
|
+
metadata?: Record<string, unknown>
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface UpdateLineItemInput {
|
|
292
|
+
/**
|
|
293
|
+
* Integer ≥ 0 — REQUIRED. 0 deletes the line. NOTE (code wins over
|
|
294
|
+
* store-api.md): `metadata` is NOT accepted on update, only on add.
|
|
295
|
+
*/
|
|
296
|
+
quantity: number
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Success shape of POST /api/store/carts/:id/complete.
|
|
301
|
+
*
|
|
302
|
+
* NOTE (code wins over store-api.md): the route NEVER returns the Medusa
|
|
303
|
+
* `{type:"cart", cart, error}` failure union — failures THROW the standard
|
|
304
|
+
* error envelope (StoreApiError) and the cart stays open/retryable.
|
|
305
|
+
*/
|
|
306
|
+
export interface CompleteCartResult {
|
|
307
|
+
type: "order"
|
|
308
|
+
order: CompletedOrder
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Order shape returned by complete (complete/_flow.ts loadOrder). */
|
|
312
|
+
export interface CompletedOrder {
|
|
313
|
+
id: string
|
|
314
|
+
display_id?: number
|
|
315
|
+
status: string
|
|
316
|
+
email: string | null
|
|
317
|
+
currency_code: string
|
|
318
|
+
customer_id: string | null
|
|
319
|
+
sales_channel_id?: string | null
|
|
320
|
+
created_at: IsoDateString
|
|
321
|
+
summary: Array<Record<string, unknown>> | Record<string, unknown> | null
|
|
322
|
+
/** Flattened line items (order_line_items + pivot quantity). */
|
|
323
|
+
items: Array<Record<string, unknown> & { quantity: number }>
|
|
324
|
+
[key: string]: unknown
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Functions
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Create a cart. → 201 `{cart}`.
|
|
333
|
+
*
|
|
334
|
+
* Auth: anon `x-client-id`; optional Bearer JWT attaches the customer
|
|
335
|
+
* (customer_id + email) and prices initial items with the customer's B2B
|
|
336
|
+
* groups; optional publishable key applies channel scope.
|
|
337
|
+
* Errors: 400 `validation_failed` | `invalid_region` | `region_required` |
|
|
338
|
+
* `invalid_sales_channel` | `price_not_found` (an initial item has no price
|
|
339
|
+
* in the cart currency), 404 `variant_not_found`. An unsupported
|
|
340
|
+
* `currency_code` does NOT error — it silently falls back to the region
|
|
341
|
+
* currency (only `store_currencies` rows are honored).
|
|
342
|
+
* Settings: store default region; enabled store currencies; publishable-key
|
|
343
|
+
* channel scope; automatic promotions re-applied on create.
|
|
344
|
+
*/
|
|
345
|
+
export async function createCart(
|
|
346
|
+
client: StorefrontClient,
|
|
347
|
+
input: CreateCartInput = {}
|
|
348
|
+
): Promise<CartResponse> {
|
|
349
|
+
// The visitor id rides along automatically. It exists only in this
|
|
350
|
+
// browser at this moment, so a cart created without it can never be
|
|
351
|
+
// attributed afterwards — which is why this is here and not left to the
|
|
352
|
+
// caller to remember.
|
|
353
|
+
const device_id = input.device_id ?? readVisitorId()
|
|
354
|
+
return client.post("/api/store/carts", device_id ? { ...input, device_id } : input)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Retrieve the decorated cart. → 200 `{cart}`.
|
|
359
|
+
* Auth: anon `x-client-id`. Errors: 404 `cart_not_found`.
|
|
360
|
+
* Every read re-runs tax recalc + totals + gift-card tender — amounts are
|
|
361
|
+
* always current (a gift-card balance spent elsewhere shrinks this cart's
|
|
362
|
+
* tender at the next read).
|
|
363
|
+
*/
|
|
364
|
+
export async function retrieveCart(
|
|
365
|
+
client: StorefrontClient,
|
|
366
|
+
cartId: string
|
|
367
|
+
): Promise<CartResponse> {
|
|
368
|
+
return client.get(`/api/store/carts/${cartId}`)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Partial-update the cart. → 200 `{cart}`.
|
|
373
|
+
* Auth: anon `x-client-id`.
|
|
374
|
+
* Errors: 404 `cart_not_found`, 409 `cart_completed`, 400
|
|
375
|
+
* `validation_failed` | `invalid_region` (unsupported `currency_code` falls
|
|
376
|
+
* back to the region currency instead of erroring).
|
|
377
|
+
* Settings: region change re-prices lines using the cart customer's groups
|
|
378
|
+
* (B2B price lists) and re-applies promotions.
|
|
379
|
+
*/
|
|
380
|
+
export async function updateCart(
|
|
381
|
+
client: StorefrontClient,
|
|
382
|
+
cartId: string,
|
|
383
|
+
input: UpdateCartInput
|
|
384
|
+
): Promise<CartResponse> {
|
|
385
|
+
return client.post(`/api/store/carts/${cartId}`, input)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Attach the AUTHENTICATED customer to the cart. Body is empty by contract
|
|
390
|
+
* (`z.object({}).strict()`) — the customer comes from the Bearer JWT, never
|
|
391
|
+
* from the body (forgery-vector guard). → 200 `{cart}`.
|
|
392
|
+
* Auth: `x-client-id` + REQUIRED Bearer JWT.
|
|
393
|
+
* Errors: 401 `unauthenticated`, 404 `cart_not_found`, 409 `cart_completed`,
|
|
394
|
+
* 400 `validation_failed` (any body key).
|
|
395
|
+
*/
|
|
396
|
+
export async function setCartCustomer(
|
|
397
|
+
client: StorefrontClient,
|
|
398
|
+
cartId: string
|
|
399
|
+
): Promise<CartResponse> {
|
|
400
|
+
return client.post(`/api/store/carts/${cartId}/customer`, {})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Add a line item (same-variant add bumps quantity). → 200 `{cart}`.
|
|
405
|
+
* Auth: anon `x-client-id`.
|
|
406
|
+
* Errors: 404 `cart_not_found` | `variant_not_found`, 409 `cart_completed`,
|
|
407
|
+
* 400 `insufficient_inventory` (kit-aware: every linked inventory component
|
|
408
|
+
* is checked; details carry `{variant_id, inventory_item_id, available,
|
|
409
|
+
* requested}`) | `validation_failed` | `price_not_found` (variant has no
|
|
410
|
+
* price in the cart currency).
|
|
411
|
+
* Settings: B2B price lists (via the cart's attached customer), gift-card
|
|
412
|
+
* product flag (`is_giftcard`/`requires_shipping` ride the line), automatic
|
|
413
|
+
* promotions re-applied.
|
|
414
|
+
*/
|
|
415
|
+
export async function addLineItem(
|
|
416
|
+
client: StorefrontClient,
|
|
417
|
+
cartId: string,
|
|
418
|
+
input: AddLineItemInput
|
|
419
|
+
): Promise<CartResponse> {
|
|
420
|
+
return client.post(`/api/store/carts/${cartId}/line-items`, input)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Set a line's quantity (0 deletes it). → 200 `{cart}`.
|
|
425
|
+
* Auth: anon `x-client-id`.
|
|
426
|
+
* Errors: 404 `cart_not_found` | `line_item_not_found`, 409 `cart_completed`,
|
|
427
|
+
* 400 `insufficient_inventory` | `validation_failed`.
|
|
428
|
+
*/
|
|
429
|
+
export async function updateLineItem(
|
|
430
|
+
client: StorefrontClient,
|
|
431
|
+
cartId: string,
|
|
432
|
+
lineItemId: string,
|
|
433
|
+
input: UpdateLineItemInput
|
|
434
|
+
): Promise<CartResponse> {
|
|
435
|
+
return client.post(`/api/store/carts/${cartId}/line-items/${lineItemId}`, input)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Remove a line item (idempotent — removing an already-gone line still
|
|
440
|
+
* returns the cart). → 200 `{cart}`.
|
|
441
|
+
* Auth: anon `x-client-id`. Errors: 404 `cart_not_found` (bad cart id).
|
|
442
|
+
*/
|
|
443
|
+
export async function deleteLineItem(
|
|
444
|
+
client: StorefrontClient,
|
|
445
|
+
cartId: string,
|
|
446
|
+
lineItemId: string
|
|
447
|
+
): Promise<CartResponse> {
|
|
448
|
+
return client.delete(`/api/store/carts/${cartId}/line-items/${lineItemId}`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Complete the cart → place the order. → 200 `{type:"order", order}`.
|
|
453
|
+
* Idempotent: recalling for a completed cart returns the SAME order
|
|
454
|
+
* (order_cart link); concurrent completes are serialized by a CAS lock
|
|
455
|
+
* (the loser either returns the winner's order or 409 `cart_locked`).
|
|
456
|
+
*
|
|
457
|
+
* Auth: anon `x-client-id` (guest checkout) — but when the store setting
|
|
458
|
+
* `accounts_mode='required'`, a guest cart (no attached customer) → 403
|
|
459
|
+
* `account_required`.
|
|
460
|
+
*
|
|
461
|
+
* Validation errors (400): `cart_email_required`, `cart_empty`,
|
|
462
|
+
* `shipping_address_required`, `shipping_method_required` (only when a line
|
|
463
|
+
* `requires_shipping` — digital-only carts skip it),
|
|
464
|
+
* `payment_collection_required`, `payment_session_required`,
|
|
465
|
+
* `checkout_method_hidden` (checkout-rules completion guard), 400
|
|
466
|
+
* `insufficient_inventory` (reserve step). Payment errors (402):
|
|
467
|
+
* `requires_action` (3DS — details carry `client_secret`),
|
|
468
|
+
* `payment_not_authorized`, `payment_not_initiated`, `payment_incomplete`,
|
|
469
|
+
* `gift_card_insufficient_balance`, `gift_card_not_redeemable`. 409
|
|
470
|
+
* `cart_locked`. Any failure compensates fully (order deleted, inventory
|
|
471
|
+
* released, gift tender reversed, cart unlocked) — the cart stays retryable.
|
|
472
|
+
*
|
|
473
|
+
* Settings: checkout rules (completion guard), accounts_mode, COD fee
|
|
474
|
+
* (carried onto the order via order_summaries.totals), gift-card tender
|
|
475
|
+
* (zero-remainder carts complete on the gift session alone).
|
|
476
|
+
* Emits `order.placed` on the durable bus after commit.
|
|
477
|
+
*/
|
|
478
|
+
export async function completeCart(
|
|
479
|
+
client: StorefrontClient,
|
|
480
|
+
cartId: string
|
|
481
|
+
): Promise<CompleteCartResult> {
|
|
482
|
+
return client.post(`/api/store/carts/${cartId}/complete`, {})
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Promotions — promo codes on the cart (batch-10 merge: the checkout
|
|
487
|
+
// DiscountSection consumed the route raw before this wrapper existed).
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Apply promo code(s) to the cart. → 200 `{cart}` (decorated — totals and
|
|
492
|
+
* `promotions` recomputed server-side; adding is idempotent per code).
|
|
493
|
+
* Auth: anon `x-client-id`.
|
|
494
|
+
* Errors: 404 `cart_not_found`, 400 zod (`promo_codes` must be a non-empty
|
|
495
|
+
* string array), 404 `promotion_not_found` (unknown code on ADD), 400
|
|
496
|
+
* `promotion_inactive` (draft/expired code on ADD).
|
|
497
|
+
*/
|
|
498
|
+
export async function applyPromotions(
|
|
499
|
+
client: StorefrontClient,
|
|
500
|
+
cartId: string,
|
|
501
|
+
promoCodes: string[]
|
|
502
|
+
): Promise<CartResponse> {
|
|
503
|
+
return client.post(`/api/store/carts/${cartId}/promotions`, {
|
|
504
|
+
promo_codes: promoCodes,
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Remove promo code(s) from the cart. → 200 `{cart}` (decorated). Unknown
|
|
510
|
+
* codes silently no-op on remove (Medusa parity).
|
|
511
|
+
* Auth: anon `x-client-id`.
|
|
512
|
+
* Errors: 404 `cart_not_found`, 400 zod.
|
|
513
|
+
*/
|
|
514
|
+
export async function removePromotions(
|
|
515
|
+
client: StorefrontClient,
|
|
516
|
+
cartId: string,
|
|
517
|
+
promoCodes: string[]
|
|
518
|
+
): Promise<CartResponse> {
|
|
519
|
+
return client.request(`/api/store/carts/${cartId}/promotions`, {
|
|
520
|
+
method: "DELETE",
|
|
521
|
+
body: { promo_codes: promoCodes },
|
|
522
|
+
})
|
|
523
|
+
}
|