@cartbase/storefront 0.1.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.
Files changed (176) hide show
  1. package/package.json +226 -0
  2. package/src/api/auth.ts +108 -0
  3. package/src/api/carts.ts +506 -0
  4. package/src/api/categories.ts +184 -0
  5. package/src/api/checkout.ts +440 -0
  6. package/src/api/collections.ts +130 -0
  7. package/src/api/consent.ts +75 -0
  8. package/src/api/content.ts +125 -0
  9. package/src/api/customers.ts +307 -0
  10. package/src/api/gift-cards.ts +112 -0
  11. package/src/api/http.ts +122 -0
  12. package/src/api/index.ts +29 -0
  13. package/src/api/integrations.ts +130 -0
  14. package/src/api/menus.ts +77 -0
  15. package/src/api/metaobjects.ts +136 -0
  16. package/src/api/orders.ts +290 -0
  17. package/src/api/products.ts +303 -0
  18. package/src/api/redirects.ts +37 -0
  19. package/src/api/regions.ts +141 -0
  20. package/src/api/reviews.ts +259 -0
  21. package/src/api/search.ts +133 -0
  22. package/src/api/types.ts +91 -0
  23. package/src/cart-drawer/cart-drawer.tsx +86 -0
  24. package/src/cart-drawer/context.tsx +569 -0
  25. package/src/cart-drawer/continue-shopping.tsx +28 -0
  26. package/src/cart-drawer/cross-sell-carousel.tsx +212 -0
  27. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -0
  28. package/src/cart-drawer/empty.tsx +52 -0
  29. package/src/cart-drawer/free-gift.tsx +71 -0
  30. package/src/cart-drawer/gift-wrap.tsx +83 -0
  31. package/src/cart-drawer/header.tsx +52 -0
  32. package/src/cart-drawer/index.ts +69 -0
  33. package/src/cart-drawer/item/index.tsx +164 -0
  34. package/src/cart-drawer/item/quantity.tsx +100 -0
  35. package/src/cart-drawer/item/upsell.tsx +110 -0
  36. package/src/cart-drawer/item/variant.tsx +46 -0
  37. package/src/cart-drawer/labels-bg.ts +72 -0
  38. package/src/cart-drawer/labels.ts +119 -0
  39. package/src/cart-drawer/notes.tsx +131 -0
  40. package/src/cart-drawer/payment-badges.tsx +96 -0
  41. package/src/cart-drawer/promo-banner.tsx +43 -0
  42. package/src/cart-drawer/rewards-points.tsx +78 -0
  43. package/src/cart-drawer/sticky-footer.tsx +73 -0
  44. package/src/cart-drawer/summary-breakdown.tsx +196 -0
  45. package/src/cart-drawer/template.tsx +225 -0
  46. package/src/cart-drawer/tiered-progress.tsx +168 -0
  47. package/src/checkout/address-error-copy.ts +119 -0
  48. package/src/checkout/address-form.tsx +224 -0
  49. package/src/checkout/address-select.tsx +79 -0
  50. package/src/checkout/boxnow-locker-selector.tsx +410 -0
  51. package/src/checkout/checkout-client.tsx +222 -0
  52. package/src/checkout/company-details.tsx +94 -0
  53. package/src/checkout/compare-addresses.ts +40 -0
  54. package/src/checkout/context.tsx +76 -0
  55. package/src/checkout/discount-section.tsx +218 -0
  56. package/src/checkout/econt-office-selector.tsx +332 -0
  57. package/src/checkout/error-message.tsx +25 -0
  58. package/src/checkout/geocode.ts +154 -0
  59. package/src/checkout/gift-card-section.tsx +224 -0
  60. package/src/checkout/index.ts +74 -0
  61. package/src/checkout/labels-bg.ts +128 -0
  62. package/src/checkout/labels.ts +263 -0
  63. package/src/checkout/line-item-card.tsx +152 -0
  64. package/src/checkout/order-summary.tsx +524 -0
  65. package/src/checkout/payment-button.tsx +373 -0
  66. package/src/checkout/payment-error-copy.ts +269 -0
  67. package/src/checkout/payment-method-list.tsx +365 -0
  68. package/src/checkout/payment-wrapper.tsx +102 -0
  69. package/src/checkout/promotion-error-copy.ts +124 -0
  70. package/src/checkout/shipping-method-list.tsx +335 -0
  71. package/src/checkout/stripe-wrapper.tsx +165 -0
  72. package/src/checkout/use-checkout-orchestration.ts +1504 -0
  73. package/src/common/cart-button-client.tsx +39 -0
  74. package/src/common/cart-button.tsx +28 -0
  75. package/src/common/country-select.tsx +65 -0
  76. package/src/common/delete-button.tsx +66 -0
  77. package/src/common/index.ts +17 -0
  78. package/src/common/language-select.tsx +78 -0
  79. package/src/common/localized-link.tsx +45 -0
  80. package/src/common/skeleton.tsx +29 -0
  81. package/src/index.ts +12 -0
  82. package/src/lib/cart-helpers.ts +113 -0
  83. package/src/lib/dual-price.tsx +73 -0
  84. package/src/lib/get-percentage-diff.ts +5 -0
  85. package/src/lib/get-product-price.ts +133 -0
  86. package/src/lib/hooks/use-intersection.ts +30 -0
  87. package/src/lib/hooks/use-toggle-state.ts +25 -0
  88. package/src/lib/money.ts +73 -0
  89. package/src/lib/payment-constants.ts +66 -0
  90. package/src/lib/product.ts +22 -0
  91. package/src/lib/sort-products.ts +63 -0
  92. package/src/lib/store-api-error.ts +36 -0
  93. package/src/lib/utils.ts +16 -0
  94. package/src/order/context.tsx +32 -0
  95. package/src/order/index.ts +63 -0
  96. package/src/order/labels-bg.ts +39 -0
  97. package/src/order/labels.ts +79 -0
  98. package/src/order/order-address-card.tsx +47 -0
  99. package/src/order/order-completed-template.tsx +165 -0
  100. package/src/order/order-confirmation-header.tsx +65 -0
  101. package/src/order/order-delivery-card.tsx +258 -0
  102. package/src/order/order-help-section.tsx +47 -0
  103. package/src/order/order-item.tsx +201 -0
  104. package/src/order/order-items-list.tsx +52 -0
  105. package/src/order/order-payment-card.tsx +95 -0
  106. package/src/order/order-timeline.tsx +141 -0
  107. package/src/order/order-totals.tsx +245 -0
  108. package/src/primitives/field.tsx +125 -0
  109. package/src/primitives/select-field.tsx +77 -0
  110. package/src/primitives/ui/accordion.tsx +61 -0
  111. package/src/primitives/ui/button.tsx +68 -0
  112. package/src/primitives/ui/collapsible.tsx +16 -0
  113. package/src/primitives/ui/dialog.tsx +112 -0
  114. package/src/primitives/ui/input.tsx +30 -0
  115. package/src/primitives/ui/label.tsx +31 -0
  116. package/src/primitives/ui/popover.tsx +38 -0
  117. package/src/primitives/ui/select.tsx +163 -0
  118. package/src/primitives/ui/sheet.tsx +131 -0
  119. package/src/primitives/ui/tabs.tsx +62 -0
  120. package/src/products/context.tsx +34 -0
  121. package/src/products/image-gallery.tsx +43 -0
  122. package/src/products/index.ts +44 -0
  123. package/src/products/labels-bg.ts +35 -0
  124. package/src/products/labels.ts +57 -0
  125. package/src/products/mobile-actions.tsx +180 -0
  126. package/src/products/option-select.tsx +67 -0
  127. package/src/products/preview-price.tsx +36 -0
  128. package/src/products/product-actions-wrapper.tsx +58 -0
  129. package/src/products/product-actions.tsx +217 -0
  130. package/src/products/product-info.tsx +43 -0
  131. package/src/products/product-preview.tsx +49 -0
  132. package/src/products/product-price.tsx +69 -0
  133. package/src/products/product-tabs.tsx +169 -0
  134. package/src/products/product-template.tsx +114 -0
  135. package/src/products/purchase-options.tsx +130 -0
  136. package/src/products/related-products.tsx +86 -0
  137. package/src/products/thumbnail.tsx +71 -0
  138. package/src/products/variant-matching.ts +71 -0
  139. package/src/reviews-ui/helpers.ts +174 -0
  140. package/src/reviews-ui/index.ts +74 -0
  141. package/src/reviews-ui/labels-bg.ts +91 -0
  142. package/src/reviews-ui/labels.ts +199 -0
  143. package/src/reviews-ui/photo-upload.tsx +345 -0
  144. package/src/reviews-ui/review-list.tsx +249 -0
  145. package/src/reviews-ui/review-widget.tsx +224 -0
  146. package/src/reviews-ui/review-wizard.tsx +560 -0
  147. package/src/reviews-ui/star-badge.tsx +104 -0
  148. package/src/reviews-ui/wizard-state.ts +81 -0
  149. package/src/store/category-template.tsx +129 -0
  150. package/src/store/collection-template.tsx +139 -0
  151. package/src/store/index.ts +41 -0
  152. package/src/store/labels-bg.ts +22 -0
  153. package/src/store/labels.ts +52 -0
  154. package/src/store/paginated-products.tsx +116 -0
  155. package/src/store/pagination.tsx +103 -0
  156. package/src/store/search-params.ts +256 -0
  157. package/src/store/search-template.tsx +249 -0
  158. package/src/store/skeleton-product-grid.tsx +26 -0
  159. package/src/store/sort-select.tsx +81 -0
  160. package/src/store/store-template.tsx +65 -0
  161. package/src/tracking/attribution.ts +418 -0
  162. package/src/tracking/consent-banner.tsx +355 -0
  163. package/src/tracking/consent-init.tsx +44 -0
  164. package/src/tracking/consent.ts +243 -0
  165. package/src/tracking/fbq.ts +168 -0
  166. package/src/tracking/ga4.tsx +49 -0
  167. package/src/tracking/get-tracking-attribution.ts +224 -0
  168. package/src/tracking/get-tracking-config.ts +50 -0
  169. package/src/tracking/gtag.ts +200 -0
  170. package/src/tracking/index.ts +133 -0
  171. package/src/tracking/meta-pixel.tsx +166 -0
  172. package/src/tracking/rybbit-events.ts +242 -0
  173. package/src/tracking/rybbit.tsx +40 -0
  174. package/src/tracking/types.ts +185 -0
  175. package/src/tracking/use-engagement-time.ts +58 -0
  176. package/tailwind-preset.cjs +72 -0
@@ -0,0 +1,506 @@
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 type { StorefrontClient } from "./http"
18
+ import type { IsoDateString, MajorUnitAmount } from "./types"
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // DTOs — ground truth: carts/route.ts, carts/_create.ts, carts/_helpers.ts,
22
+ // carts/_totals.ts, carts/_address.ts, [id]/line-items, [id]/customer,
23
+ // [id]/complete/_flow.ts.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Address input (create/update). All fields optional on the generic cart
28
+ * routes (`AddressSchema.passthrough()` server-side — extra keys are kept).
29
+ * NOTE: prepare-checkout has its OWN stricter address schema (see
30
+ * `checkout.ts` PrepareCheckoutAddress — most fields required there).
31
+ */
32
+ export interface CartAddressInput {
33
+ first_name?: string
34
+ last_name?: string
35
+ company?: string
36
+ address_1?: string
37
+ address_2?: string
38
+ city?: string
39
+ /** Lowercased server-side. */
40
+ country_code?: string
41
+ province?: string
42
+ postal_code?: string
43
+ phone?: string
44
+ metadata?: Record<string, unknown>
45
+ [key: string]: unknown
46
+ }
47
+
48
+ /** Stored cart address row (`cart_addresses`). Unset fields come back null. */
49
+ export interface CartAddress {
50
+ id: string
51
+ first_name: string | null
52
+ last_name: string | null
53
+ company: string | null
54
+ address_1: string | null
55
+ address_2: string | null
56
+ city: string | null
57
+ country_code: string | null
58
+ province: string | null
59
+ postal_code: string | null
60
+ phone: string | null
61
+ metadata: Record<string, unknown> | null
62
+ [key: string]: unknown
63
+ }
64
+
65
+ export interface CartLineItemAdjustment {
66
+ id: string
67
+ amount: MajorUnitAmount
68
+ promotion_id?: string | null
69
+ [key: string]: unknown
70
+ }
71
+
72
+ export interface CartTaxLine {
73
+ id: string
74
+ /** Percentage, e.g. 20 for BG VAT. */
75
+ rate: number
76
+ [key: string]: unknown
77
+ }
78
+
79
+ /**
80
+ * Per-line money decoration (carts/_totals.ts `decorateItem`) — present on
81
+ * every item and shipping method in every cart response.
82
+ */
83
+ export interface LineTotals {
84
+ subtotal: MajorUnitAmount
85
+ total: MajorUnitAmount
86
+ original_total: MajorUnitAmount
87
+ tax_total: MajorUnitAmount
88
+ original_tax_total: MajorUnitAmount
89
+ discount_total: MajorUnitAmount
90
+ discount_subtotal: MajorUnitAmount
91
+ discount_tax_total: MajorUnitAmount
92
+ }
93
+
94
+ /** Decorated cart line item (`cart_line_items` row + LineTotals). */
95
+ export interface CartLineItem extends LineTotals {
96
+ id: string
97
+ cart_id: string
98
+ variant_id: string | null
99
+ product_id: string | null
100
+ title: string | null
101
+ subtitle: string | null
102
+ product_title: string | null
103
+ product_handle: string | null
104
+ thumbnail: string | null
105
+ variant_title: string | null
106
+ variant_sku: string | null
107
+ quantity: number
108
+ unit_price: MajorUnitAmount
109
+ is_tax_inclusive: boolean
110
+ /** false for gift-card lines — they are never discountable. */
111
+ is_discountable: boolean
112
+ /** Gift-card product flag, copied onto the line at add time. */
113
+ is_giftcard: boolean
114
+ /** false for digital gift cards — lets digital-only carts complete without a shipping method. */
115
+ requires_shipping: boolean
116
+ metadata: Record<string, unknown> | null
117
+ adjustments: CartLineItemAdjustment[]
118
+ tax_lines: CartTaxLine[]
119
+ [key: string]: unknown
120
+ }
121
+
122
+ /** Decorated cart shipping method (`cart_shipping_methods` row + LineTotals). */
123
+ export interface CartShippingMethod extends LineTotals {
124
+ id: string
125
+ cart_id: string
126
+ shipping_option_id: string | null
127
+ name: string | null
128
+ amount: MajorUnitAmount
129
+ is_tax_inclusive: boolean
130
+ data: Record<string, unknown> | null
131
+ adjustments: CartLineItemAdjustment[]
132
+ tax_lines: CartTaxLine[]
133
+ [key: string]: unknown
134
+ }
135
+
136
+ /**
137
+ * Applied gift card as decorated onto every cart read (gift-card tender —
138
+ * see `gift-cards.ts` for apply/remove). `amount` is derived from the LIVE
139
+ * ledger at every read; a disabled/expired/depleted card stays listed at 0.
140
+ */
141
+ export interface AppliedGiftCard {
142
+ id: string
143
+ /** Masked — the code itself is never echoed. */
144
+ last4: string
145
+ amount: MajorUnitAmount
146
+ }
147
+
148
+ /**
149
+ * The decorated cart — every cart-returning endpoint sends this shape
150
+ * (`{cart: Cart}`). Totals fields are computed by decorateCartTotals
151
+ * (carts/_totals.ts); gift-card fields by resolveGiftCardTender; the COD fee
152
+ * by resolveCodFee. All numbers are EUR decimal major units.
153
+ */
154
+ export interface Cart {
155
+ id: string
156
+ region_id: string | null
157
+ currency_code: string
158
+ email: string | null
159
+ /** Set ONLY via the auth-derived paths (create with JWT / setCartCustomer). */
160
+ customer_id: string | null
161
+ sales_channel_id: string | null
162
+ locale: string | null
163
+ completed_at: IsoDateString | null
164
+ metadata: Record<string, unknown> | null
165
+ created_at: IsoDateString
166
+ updated_at: IsoDateString
167
+ region?: Record<string, unknown> | null
168
+ items: CartLineItem[]
169
+ shipping_methods: CartShippingMethod[]
170
+ shipping_address: CartAddress | null
171
+ billing_address: CartAddress | null
172
+ credit_lines: Array<{ amount: MajorUnitAmount; [key: string]: unknown }>
173
+ promotions: Array<Record<string, unknown>>
174
+ /** Embedded pivot → payment collection (+ its payment_sessions). */
175
+ payment_collection: unknown
176
+ // — cart-level totals (decorateCartTotals) —
177
+ total: MajorUnitAmount
178
+ subtotal: MajorUnitAmount
179
+ tax_total: MajorUnitAmount
180
+ discount_total: MajorUnitAmount
181
+ discount_subtotal: MajorUnitAmount
182
+ discount_tax_total: MajorUnitAmount
183
+ shipping_total: MajorUnitAmount
184
+ shipping_subtotal: MajorUnitAmount
185
+ shipping_tax_total: MajorUnitAmount
186
+ shipping_discount_total: MajorUnitAmount
187
+ original_shipping_total: MajorUnitAmount
188
+ original_shipping_subtotal: MajorUnitAmount
189
+ original_shipping_tax_total: MajorUnitAmount
190
+ item_total: MajorUnitAmount
191
+ item_subtotal: MajorUnitAmount
192
+ item_tax_total: MajorUnitAmount
193
+ item_discount_total: MajorUnitAmount
194
+ original_item_total: MajorUnitAmount
195
+ original_item_subtotal: MajorUnitAmount
196
+ original_item_tax_total: MajorUnitAmount
197
+ original_total: MajorUnitAmount
198
+ original_subtotal: MajorUnitAmount
199
+ original_tax_total: MajorUnitAmount
200
+ credit_line_total: MajorUnitAmount
201
+ credit_line_subtotal: MajorUnitAmount
202
+ credit_line_tax_total: MajorUnitAmount
203
+ /** Native COD fee — non-zero only when a live pp_cod session exists AND the cod integration is enabled. Folded into `total`. */
204
+ cod_fee_total: MajorUnitAmount
205
+ cod_fee_label: string | null
206
+ // — gift-card tender decoration (totals NEVER move; see gift-cards.ts) —
207
+ gift_cards: AppliedGiftCard[]
208
+ /** Σ of applied-card coverage (= the pp_giftcard session amount). */
209
+ gift_card_total: MajorUnitAmount
210
+ /** `max(total − gift_card_total, 0)` — what the remainder provider charges. */
211
+ gift_card_remainder: MajorUnitAmount
212
+ [key: string]: unknown
213
+ }
214
+
215
+ export interface CartResponse {
216
+ cart: Cart
217
+ }
218
+
219
+ /**
220
+ * Body of POST /api/store/carts (`.strict()` — unknown keys → 400
221
+ * `validation_failed`). `customer_id` is deliberately NOT a field: the
222
+ * customer is derived from the Bearer JWT (forgery-vector guard G2).
223
+ */
224
+ export interface CreateCartInput {
225
+ /** Falls back to `stores.default_region_id`; neither → 400 `region_required`. */
226
+ region_id?: string
227
+ email?: string
228
+ /** Must be enabled for the store; region currency is the default. */
229
+ currency_code?: string
230
+ items?: Array<{ variant_id: string; quantity: number; selling_plan_id?: string }>
231
+ /**
232
+ * B2B v1: with a publishable key, omitting this defaults to the key's
233
+ * channel; a channel OUTSIDE the key's scope → 400 `invalid_sales_channel`.
234
+ */
235
+ sales_channel_id?: string
236
+ promo_codes?: string[]
237
+ shipping_address?: CartAddressInput
238
+ billing_address?: CartAddressInput
239
+ metadata?: Record<string, unknown>
240
+ locale?: string
241
+ }
242
+
243
+ /**
244
+ * Body of POST /api/store/carts/:id (partial update, `.strict()`).
245
+ * Addresses: object = set/replace in place, null = clear, absent = untouched.
246
+ * A region change re-resolves currency, CLEARS the shipping address (unless
247
+ * a new one is provided in the same call), drops custom-priced items and
248
+ * re-prices the rest.
249
+ */
250
+ export interface UpdateCartInput {
251
+ region_id?: string
252
+ email?: string
253
+ currency_code?: string
254
+ sales_channel_id?: string
255
+ metadata?: Record<string, unknown>
256
+ locale?: string
257
+ shipping_address?: CartAddressInput | null
258
+ billing_address?: CartAddressInput | null
259
+ }
260
+
261
+ export interface AddLineItemInput {
262
+ variant_id: string
263
+ /**
264
+ * Positive integer. Adding a variant already in the cart UNDER THE SAME
265
+ * PLAN (or both one-time) bumps its quantity; a one-time line and a
266
+ * subscription line of the same variant stay separate.
267
+ */
268
+ quantity: number
269
+ /**
270
+ * Subscription plan for this line (from
271
+ * `products.listSellingPlans`). The server validates the plan is enabled
272
+ * and attached to the variant's product (else 400 `invalid_selling_plan`)
273
+ * and applies the plan price. Omit for a one-time purchase.
274
+ */
275
+ selling_plan_id?: string
276
+ metadata?: Record<string, unknown>
277
+ }
278
+
279
+ export interface UpdateLineItemInput {
280
+ /**
281
+ * Integer ≥ 0 — REQUIRED. 0 deletes the line. NOTE (code wins over
282
+ * store-api.md): `metadata` is NOT accepted on update, only on add.
283
+ */
284
+ quantity: number
285
+ }
286
+
287
+ /**
288
+ * Success shape of POST /api/store/carts/:id/complete.
289
+ *
290
+ * NOTE (code wins over store-api.md): the route NEVER returns the Medusa
291
+ * `{type:"cart", cart, error}` failure union — failures THROW the standard
292
+ * error envelope (StoreApiError) and the cart stays open/retryable.
293
+ */
294
+ export interface CompleteCartResult {
295
+ type: "order"
296
+ order: CompletedOrder
297
+ }
298
+
299
+ /** Order shape returned by complete (complete/_flow.ts loadOrder). */
300
+ export interface CompletedOrder {
301
+ id: string
302
+ display_id?: number
303
+ status: string
304
+ email: string | null
305
+ currency_code: string
306
+ customer_id: string | null
307
+ sales_channel_id?: string | null
308
+ created_at: IsoDateString
309
+ summary: Array<Record<string, unknown>> | Record<string, unknown> | null
310
+ /** Flattened line items (order_line_items + pivot quantity). */
311
+ items: Array<Record<string, unknown> & { quantity: number }>
312
+ [key: string]: unknown
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Functions
317
+ // ---------------------------------------------------------------------------
318
+
319
+ /**
320
+ * Create a cart. → 201 `{cart}`.
321
+ *
322
+ * Auth: anon `x-client-id`; optional Bearer JWT attaches the customer
323
+ * (customer_id + email) and prices initial items with the customer's B2B
324
+ * groups; optional publishable key applies channel scope.
325
+ * Errors: 400 `validation_failed` | `invalid_region` | `region_required` |
326
+ * `invalid_sales_channel` | `price_not_found` (an initial item has no price
327
+ * in the cart currency), 404 `variant_not_found`. An unsupported
328
+ * `currency_code` does NOT error — it silently falls back to the region
329
+ * currency (only `store_currencies` rows are honored).
330
+ * Settings: store default region; enabled store currencies; publishable-key
331
+ * channel scope; automatic promotions re-applied on create.
332
+ */
333
+ export async function createCart(
334
+ client: StorefrontClient,
335
+ input: CreateCartInput = {}
336
+ ): Promise<CartResponse> {
337
+ return client.post("/api/store/carts", input)
338
+ }
339
+
340
+ /**
341
+ * Retrieve the decorated cart. → 200 `{cart}`.
342
+ * Auth: anon `x-client-id`. Errors: 404 `cart_not_found`.
343
+ * Every read re-runs tax recalc + totals + gift-card tender — amounts are
344
+ * always current (a gift-card balance spent elsewhere shrinks this cart's
345
+ * tender at the next read).
346
+ */
347
+ export async function retrieveCart(
348
+ client: StorefrontClient,
349
+ cartId: string
350
+ ): Promise<CartResponse> {
351
+ return client.get(`/api/store/carts/${cartId}`)
352
+ }
353
+
354
+ /**
355
+ * Partial-update the cart. → 200 `{cart}`.
356
+ * Auth: anon `x-client-id`.
357
+ * Errors: 404 `cart_not_found`, 409 `cart_completed`, 400
358
+ * `validation_failed` | `invalid_region` (unsupported `currency_code` falls
359
+ * back to the region currency instead of erroring).
360
+ * Settings: region change re-prices lines using the cart customer's groups
361
+ * (B2B price lists) and re-applies promotions.
362
+ */
363
+ export async function updateCart(
364
+ client: StorefrontClient,
365
+ cartId: string,
366
+ input: UpdateCartInput
367
+ ): Promise<CartResponse> {
368
+ return client.post(`/api/store/carts/${cartId}`, input)
369
+ }
370
+
371
+ /**
372
+ * Attach the AUTHENTICATED customer to the cart. Body is empty by contract
373
+ * (`z.object({}).strict()`) — the customer comes from the Bearer JWT, never
374
+ * from the body (forgery-vector guard). → 200 `{cart}`.
375
+ * Auth: `x-client-id` + REQUIRED Bearer JWT.
376
+ * Errors: 401 `unauthenticated`, 404 `cart_not_found`, 409 `cart_completed`,
377
+ * 400 `validation_failed` (any body key).
378
+ */
379
+ export async function setCartCustomer(
380
+ client: StorefrontClient,
381
+ cartId: string
382
+ ): Promise<CartResponse> {
383
+ return client.post(`/api/store/carts/${cartId}/customer`, {})
384
+ }
385
+
386
+ /**
387
+ * Add a line item (same-variant add bumps quantity). → 200 `{cart}`.
388
+ * Auth: anon `x-client-id`.
389
+ * Errors: 404 `cart_not_found` | `variant_not_found`, 409 `cart_completed`,
390
+ * 400 `insufficient_inventory` (kit-aware: every linked inventory component
391
+ * is checked; details carry `{variant_id, inventory_item_id, available,
392
+ * requested}`) | `validation_failed` | `price_not_found` (variant has no
393
+ * price in the cart currency).
394
+ * Settings: B2B price lists (via the cart's attached customer), gift-card
395
+ * product flag (`is_giftcard`/`requires_shipping` ride the line), automatic
396
+ * promotions re-applied.
397
+ */
398
+ export async function addLineItem(
399
+ client: StorefrontClient,
400
+ cartId: string,
401
+ input: AddLineItemInput
402
+ ): Promise<CartResponse> {
403
+ return client.post(`/api/store/carts/${cartId}/line-items`, input)
404
+ }
405
+
406
+ /**
407
+ * Set a line's quantity (0 deletes it). → 200 `{cart}`.
408
+ * Auth: anon `x-client-id`.
409
+ * Errors: 404 `cart_not_found` | `line_item_not_found`, 409 `cart_completed`,
410
+ * 400 `insufficient_inventory` | `validation_failed`.
411
+ */
412
+ export async function updateLineItem(
413
+ client: StorefrontClient,
414
+ cartId: string,
415
+ lineItemId: string,
416
+ input: UpdateLineItemInput
417
+ ): Promise<CartResponse> {
418
+ return client.post(`/api/store/carts/${cartId}/line-items/${lineItemId}`, input)
419
+ }
420
+
421
+ /**
422
+ * Remove a line item (idempotent — removing an already-gone line still
423
+ * returns the cart). → 200 `{cart}`.
424
+ * Auth: anon `x-client-id`. Errors: 404 `cart_not_found` (bad cart id).
425
+ */
426
+ export async function deleteLineItem(
427
+ client: StorefrontClient,
428
+ cartId: string,
429
+ lineItemId: string
430
+ ): Promise<CartResponse> {
431
+ return client.delete(`/api/store/carts/${cartId}/line-items/${lineItemId}`)
432
+ }
433
+
434
+ /**
435
+ * Complete the cart → place the order. → 200 `{type:"order", order}`.
436
+ * Idempotent: recalling for a completed cart returns the SAME order
437
+ * (order_cart link); concurrent completes are serialized by a CAS lock
438
+ * (the loser either returns the winner's order or 409 `cart_locked`).
439
+ *
440
+ * Auth: anon `x-client-id` (guest checkout) — but when the store setting
441
+ * `accounts_mode='required'`, a guest cart (no attached customer) → 403
442
+ * `account_required`.
443
+ *
444
+ * Validation errors (400): `cart_email_required`, `cart_empty`,
445
+ * `shipping_address_required`, `shipping_method_required` (only when a line
446
+ * `requires_shipping` — digital-only carts skip it),
447
+ * `payment_collection_required`, `payment_session_required`,
448
+ * `checkout_method_hidden` (checkout-rules completion guard), 400
449
+ * `insufficient_inventory` (reserve step). Payment errors (402):
450
+ * `requires_action` (3DS — details carry `client_secret`),
451
+ * `payment_not_authorized`, `payment_not_initiated`, `payment_incomplete`,
452
+ * `gift_card_insufficient_balance`, `gift_card_not_redeemable`. 409
453
+ * `cart_locked`. Any failure compensates fully (order deleted, inventory
454
+ * released, gift tender reversed, cart unlocked) — the cart stays retryable.
455
+ *
456
+ * Settings: checkout rules (completion guard), accounts_mode, COD fee
457
+ * (carried onto the order via order_summaries.totals), gift-card tender
458
+ * (zero-remainder carts complete on the gift session alone).
459
+ * Emits `order.placed` on the durable bus after commit.
460
+ */
461
+ export async function completeCart(
462
+ client: StorefrontClient,
463
+ cartId: string
464
+ ): Promise<CompleteCartResult> {
465
+ return client.post(`/api/store/carts/${cartId}/complete`, {})
466
+ }
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // Promotions — promo codes on the cart (batch-10 merge: the checkout
470
+ // DiscountSection consumed the route raw before this wrapper existed).
471
+ // ---------------------------------------------------------------------------
472
+
473
+ /**
474
+ * Apply promo code(s) to the cart. → 200 `{cart}` (decorated — totals and
475
+ * `promotions` recomputed server-side; adding is idempotent per code).
476
+ * Auth: anon `x-client-id`.
477
+ * Errors: 404 `cart_not_found`, 400 zod (`promo_codes` must be a non-empty
478
+ * string array), 404 `promotion_not_found` (unknown code on ADD), 400
479
+ * `promotion_inactive` (draft/expired code on ADD).
480
+ */
481
+ export async function applyPromotions(
482
+ client: StorefrontClient,
483
+ cartId: string,
484
+ promoCodes: string[]
485
+ ): Promise<CartResponse> {
486
+ return client.post(`/api/store/carts/${cartId}/promotions`, {
487
+ promo_codes: promoCodes,
488
+ })
489
+ }
490
+
491
+ /**
492
+ * Remove promo code(s) from the cart. → 200 `{cart}` (decorated). Unknown
493
+ * codes silently no-op on remove (Medusa parity).
494
+ * Auth: anon `x-client-id`.
495
+ * Errors: 404 `cart_not_found`, 400 zod.
496
+ */
497
+ export async function removePromotions(
498
+ client: StorefrontClient,
499
+ cartId: string,
500
+ promoCodes: string[]
501
+ ): Promise<CartResponse> {
502
+ return client.request(`/api/store/carts/${cartId}/promotions`, {
503
+ method: "DELETE",
504
+ body: { promo_codes: promoCodes },
505
+ })
506
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @cartbase/storefront/api/categories — product categories, tags, types.
3
+ *
4
+ * Taxonomy reads for navigation trees and filter UIs. Categories are
5
+ * hierarchical (`parent_category_id`) with optional ancestor/descendant tree
6
+ * embedding; tags and types are flat value lists. Only ACTIVE, non-internal
7
+ * categories are storefront-visible (RLS-enforced — inactive/internal 404
8
+ * even by id). Docs: docs/storefront/categories.md.
9
+ */
10
+ import type { StorefrontClient } from "./http"
11
+ import type { IsoDateString, ListEnvelope, PaginationQuery } from "./types"
12
+
13
+ export interface StoreProductCategory {
14
+ id: string
15
+ name: string
16
+ handle: string
17
+ description: string | null
18
+ parent_category_id: string | null
19
+ rank: number
20
+ is_active: true
21
+ is_internal: false
22
+ /** Materialized path (internal ordering detail). */
23
+ mpath: string | null
24
+ /** SEO override; null = fall back to `name`. */
25
+ seo_title: string | null
26
+ /** SEO override; null = fall back to `description`. */
27
+ seo_description: string | null
28
+ metadata: Record<string, unknown> | null
29
+ created_at: IsoDateString
30
+ updated_at: IsoDateString
31
+ /** Present only when `include_descendants_tree` was requested. */
32
+ category_children?: StoreProductCategory[]
33
+ /** Present only when `include_ancestors_tree` was requested. */
34
+ parent_category?: StoreProductCategory | null
35
+ }
36
+
37
+ export interface StoreProductTag {
38
+ id: string
39
+ value: string
40
+ metadata: Record<string, unknown> | null
41
+ created_at: IsoDateString
42
+ updated_at: IsoDateString
43
+ }
44
+
45
+ export interface StoreProductType {
46
+ id: string
47
+ value: string
48
+ metadata: Record<string, unknown> | null
49
+ created_at: IsoDateString
50
+ updated_at: IsoDateString
51
+ }
52
+
53
+ export interface ListCategoriesQuery extends PaginationQuery {
54
+ /** Case-insensitive substring match on name. */
55
+ q?: string
56
+ handle?: string
57
+ /** List one level: children of this category. */
58
+ parent_category_id?: string
59
+ /** Embed `parent_category` chains on each row. */
60
+ include_ancestors_tree?: boolean
61
+ /** Embed `category_children` recursively on each row. */
62
+ include_descendants_tree?: boolean
63
+ }
64
+
65
+ export interface CategoryListResponse extends ListEnvelope {
66
+ product_categories: StoreProductCategory[]
67
+ }
68
+
69
+ export interface CategoryResponse {
70
+ product_category: StoreProductCategory
71
+ }
72
+
73
+ export interface RetrieveCategoryQuery {
74
+ include_ancestors_tree?: boolean
75
+ include_descendants_tree?: boolean
76
+ }
77
+
78
+ export interface ListTagsQuery extends PaginationQuery {
79
+ /** Case-insensitive substring match on value. */
80
+ q?: string
81
+ /** Exact value match. */
82
+ value?: string
83
+ }
84
+
85
+ export interface TagListResponse extends ListEnvelope {
86
+ product_tags: StoreProductTag[]
87
+ }
88
+
89
+ export interface TagResponse {
90
+ product_tag: StoreProductTag
91
+ }
92
+
93
+ export type ListTypesQuery = ListTagsQuery
94
+
95
+ export interface TypeListResponse extends ListEnvelope {
96
+ product_types: StoreProductType[]
97
+ }
98
+
99
+ export interface TypeResponse {
100
+ product_type: StoreProductType
101
+ }
102
+
103
+ /**
104
+ * List active, non-internal categories ordered by `rank`. Tree flags embed
105
+ * ancestors/descendants per row (visible categories only).
106
+ *
107
+ * Auth: anon (`x-client-id`). Errors: 400 `missing_client_id`,
108
+ * 400 `validation_failed`. Settings: category `is_active`/`is_internal`
109
+ * flags and rank ordering (Admin → Categories).
110
+ */
111
+ export async function listCategories(
112
+ client: StorefrontClient,
113
+ query?: ListCategoriesQuery
114
+ ): Promise<CategoryListResponse> {
115
+ return client.get("/api/store/product-categories", { query: { ...query } })
116
+ }
117
+
118
+ /**
119
+ * Retrieve one category by id, optionally with its ancestor/descendant tree.
120
+ *
121
+ * Auth: anon (`x-client-id`). Errors: 404 `not_found` (unknown, deleted, or
122
+ * inactive/internal — RLS hides them even by id).
123
+ */
124
+ export async function retrieveCategory(
125
+ client: StorefrontClient,
126
+ categoryId: string,
127
+ query?: RetrieveCategoryQuery
128
+ ): Promise<CategoryResponse> {
129
+ return client.get(`/api/store/product-categories/${encodeURIComponent(categoryId)}`, {
130
+ query: { ...query },
131
+ })
132
+ }
133
+
134
+ /**
135
+ * List product tags ordered by value (filter chips; tag ids feed
136
+ * `tag_id` filters on product listings and search).
137
+ *
138
+ * Auth: anon (`x-client-id`). Errors: 400 `missing_client_id`,
139
+ * 400 `validation_failed`.
140
+ */
141
+ export async function listProductTags(
142
+ client: StorefrontClient,
143
+ query?: ListTagsQuery
144
+ ): Promise<TagListResponse> {
145
+ return client.get("/api/store/product-tags", { query: { ...query } })
146
+ }
147
+
148
+ /**
149
+ * Retrieve one product tag by id.
150
+ *
151
+ * Auth: anon (`x-client-id`). Errors: 404 `not_found`.
152
+ */
153
+ export async function retrieveProductTag(
154
+ client: StorefrontClient,
155
+ tagId: string
156
+ ): Promise<TagResponse> {
157
+ return client.get(`/api/store/product-tags/${encodeURIComponent(tagId)}`)
158
+ }
159
+
160
+ /**
161
+ * List product types ordered by value (type ids feed `type_id` filters on
162
+ * product listings and search).
163
+ *
164
+ * Auth: anon (`x-client-id`). Errors: 400 `missing_client_id`,
165
+ * 400 `validation_failed`.
166
+ */
167
+ export async function listProductTypes(
168
+ client: StorefrontClient,
169
+ query?: ListTypesQuery
170
+ ): Promise<TypeListResponse> {
171
+ return client.get("/api/store/product-types", { query: { ...query } })
172
+ }
173
+
174
+ /**
175
+ * Retrieve one product type by id.
176
+ *
177
+ * Auth: anon (`x-client-id`). Errors: 404 `not_found`.
178
+ */
179
+ export async function retrieveProductType(
180
+ client: StorefrontClient,
181
+ typeId: string
182
+ ): Promise<TypeResponse> {
183
+ return client.get(`/api/store/product-types/${encodeURIComponent(typeId)}`)
184
+ }