@kasuvia/sdk 0.0.8 → 0.0.9

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/README.md +42 -15
  2. package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
  3. package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
  4. package/dist/components/index.d.mts +497 -0
  5. package/dist/components/index.d.ts +497 -0
  6. package/dist/components/index.js +4217 -0
  7. package/dist/components/index.js.map +1 -0
  8. package/dist/components/index.mjs +4194 -0
  9. package/dist/components/index.mjs.map +1 -0
  10. package/dist/config/server.d.mts +26 -0
  11. package/dist/config/server.d.ts +26 -0
  12. package/dist/config/server.js +38 -0
  13. package/dist/config/server.js.map +1 -0
  14. package/dist/config/server.mjs +13 -0
  15. package/dist/config/server.mjs.map +1 -0
  16. package/dist/hooks/index.d.mts +393 -160
  17. package/dist/hooks/index.d.ts +393 -160
  18. package/dist/hooks/index.js +2792 -2137
  19. package/dist/hooks/index.js.map +1 -1
  20. package/dist/hooks/index.mjs +2734 -2121
  21. package/dist/hooks/index.mjs.map +1 -1
  22. package/dist/index-CNW_LINy.d.mts +578 -0
  23. package/dist/index-CNW_LINy.d.ts +578 -0
  24. package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
  25. package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
  26. package/dist/index-NkI10J_E.d.mts +40 -0
  27. package/dist/index-NkI10J_E.d.ts +40 -0
  28. package/dist/index.d.mts +20437 -103
  29. package/dist/index.d.ts +20437 -103
  30. package/dist/index.js +2111 -1802
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +2006 -1786
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
  35. package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
  36. package/dist/services/index.d.mts +908 -2
  37. package/dist/services/index.d.ts +908 -2
  38. package/dist/services/index.js +1492 -1686
  39. package/dist/services/index.js.map +1 -1
  40. package/dist/services/index.mjs +1431 -1670
  41. package/dist/services/index.mjs.map +1 -1
  42. package/package.json +46 -6
  43. package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
  44. package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
  45. package/src/components/bespoke/index.ts +3 -0
  46. package/src/components/booking/SlotPicker.tsx +97 -0
  47. package/src/components/booking/index.ts +1 -0
  48. package/src/components/cart/CartItem.tsx +81 -0
  49. package/src/components/cart/CartSidebar.tsx +115 -0
  50. package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
  51. package/src/components/cart/index.ts +3 -0
  52. package/src/components/index.ts +8 -0
  53. package/src/components/orders/DeliverableReviewer.tsx +139 -0
  54. package/src/components/orders/index.ts +1 -0
  55. package/src/components/product/ProductCard.tsx +350 -0
  56. package/src/components/product/ProductGrid.tsx +84 -0
  57. package/src/components/product/index.ts +2 -0
  58. package/src/components/shared/AddressAutocomplete.tsx +212 -0
  59. package/src/components/shared/AnalyticsScripts.tsx +56 -0
  60. package/src/components/shared/CookieConsentBanner.tsx +98 -0
  61. package/src/components/shared/ImageEditorModal.tsx +1066 -0
  62. package/src/components/shared/ImageViewerModal.tsx +598 -0
  63. package/src/components/shared/LucideReactIcon.tsx +52 -0
  64. package/src/components/shared/MapPinDrop.tsx +101 -0
  65. package/src/components/shared/MultiSelectPicker.tsx +193 -0
  66. package/src/components/shared/OfflineBanner.tsx +46 -0
  67. package/src/components/shared/SearchableInput.tsx +153 -0
  68. package/src/components/shared/SearchablePopover.tsx +166 -0
  69. package/src/components/shared/SmartImage.tsx +311 -0
  70. package/src/components/shared/TruncatedValue.tsx +27 -0
  71. package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
  72. package/src/components/shared/index.ts +15 -0
  73. package/src/components/skeletons/CardSkeleton.tsx +26 -0
  74. package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
  75. package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
  76. package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
  77. package/src/components/skeletons/TableSkeleton.tsx +42 -0
  78. package/src/components/skeletons/index.ts +5 -0
  79. package/src/config/index.ts +22 -4
  80. package/src/config/server.ts +45 -0
  81. package/src/context/sdk-context.tsx +5 -2
  82. package/src/generated/auth-openapi.ts +2935 -0
  83. package/src/generated/cart-openapi.ts +652 -0
  84. package/src/generated/cms-openapi.ts +1668 -0
  85. package/src/generated/crm-openapi.ts +986 -0
  86. package/src/generated/graphql.ts +126 -0
  87. package/src/generated/index.ts +11 -0
  88. package/src/generated/media-openapi.ts +613 -0
  89. package/src/generated/notification-openapi.ts +862 -0
  90. package/src/generated/payment-openapi.ts +2713 -0
  91. package/src/generated/store-openapi.ts +9510 -0
  92. package/src/graphql/operations/collections.graphql +32 -0
  93. package/src/graphql/operations/products.graphql +197 -0
  94. package/src/graphql/operations/wishlist.graphql +25 -0
  95. package/src/hooks/index.ts +9 -2
  96. package/src/hooks/use-auth.ts +163 -69
  97. package/src/hooks/use-bespoke-availability.ts +39 -22
  98. package/src/hooks/use-business-config.ts +51 -0
  99. package/src/hooks/use-cart.ts +310 -238
  100. package/src/hooks/use-cms.ts +107 -127
  101. package/src/hooks/use-collections.ts +35 -55
  102. package/src/hooks/use-consultations.ts +9 -136
  103. package/src/hooks/use-countries.ts +19 -28
  104. package/src/hooks/use-currencies.ts +8 -20
  105. package/src/hooks/use-customer-address.ts +64 -0
  106. package/src/hooks/use-feedback.ts +34 -0
  107. package/src/hooks/use-languages.ts +26 -39
  108. package/src/hooks/use-media-upload.ts +23 -0
  109. package/src/hooks/use-notification-stream.ts +88 -0
  110. package/src/hooks/use-notifications.ts +163 -0
  111. package/src/hooks/use-order-stream.ts +29 -29
  112. package/src/hooks/use-orders.ts +91 -73
  113. package/src/hooks/use-payment.ts +116 -84
  114. package/src/hooks/use-products.ts +65 -269
  115. package/src/hooks/use-push-notifications.ts +122 -0
  116. package/src/hooks/use-reviews.ts +175 -0
  117. package/src/hooks/use-service-booking.ts +109 -132
  118. package/src/hooks/use-states.ts +16 -30
  119. package/src/hooks/use-wishlist.ts +44 -69
  120. package/src/index.ts +15 -11
  121. package/src/services/auth.service.ts +422 -132
  122. package/src/services/bespoke.service.ts +69 -10
  123. package/src/services/cart.service.ts +155 -186
  124. package/src/services/cms.service.ts +41 -50
  125. package/src/services/collection.service.ts +25 -63
  126. package/src/services/config.service.ts +10 -21
  127. package/src/services/consultation.service.ts +36 -93
  128. package/src/services/feedback.service.ts +76 -0
  129. package/src/services/index.ts +7 -1
  130. package/src/services/legal.service.ts +62 -0
  131. package/src/services/logistics.service.ts +235 -53
  132. package/src/services/media.service.ts +115 -0
  133. package/src/services/notifications.service.ts +204 -20
  134. package/src/services/order.service.ts +498 -127
  135. package/src/services/payment.service.ts +149 -100
  136. package/src/services/product.service.ts +157 -814
  137. package/src/services/review.service.ts +211 -0
  138. package/src/services/service-booking.service.ts +198 -99
  139. package/src/services/wishlist.service.ts +27 -74
  140. package/src/types/auth/index.ts +161 -18
  141. package/src/types/business/index.ts +9 -1
  142. package/src/types/cms/index.ts +91 -6
  143. package/src/types/cms/legal.ts +18 -0
  144. package/src/types/consultations/index.ts +60 -68
  145. package/src/types/index.ts +5 -3
  146. package/src/types/media/index.ts +35 -3
  147. package/src/types/payments/index.ts +80 -16
  148. package/src/types/reviews/index.ts +116 -0
  149. package/src/types/services/index.ts +113 -38
  150. package/src/types/store/checkout.ts +53 -0
  151. package/src/types/store/index.ts +273 -52
  152. package/src/types/store/invoice.ts +31 -0
  153. package/src/types/store/shipping.ts +84 -3
  154. package/src/types/ui/index.ts +1 -1
  155. package/src/utils/checkout.ts +19 -3
  156. package/src/utils/cookieConsent.ts +34 -0
  157. package/src/utils/format.ts +408 -0
  158. package/src/utils/geolocation.ts +134 -0
  159. package/src/utils/graphql-url.ts +47 -0
  160. package/src/utils/guest-session.ts +59 -0
  161. package/src/utils/http-client.ts +147 -0
  162. package/src/utils/index.ts +11 -4
  163. package/src/utils/status-colors.ts +79 -0
  164. package/src/utils/token-refresh.ts +115 -0
  165. package/src/utils/validators.ts +13 -0
  166. package/src/utils/youtube.ts +18 -0
  167. package/dist/index-C5YO9gTl.d.mts +0 -507
  168. package/dist/index-C5YO9gTl.d.ts +0 -507
  169. package/dist/index-CefG7_xq.d.mts +0 -793
  170. package/dist/index-DeHCf3zI.d.ts +0 -793
  171. package/src/graphql/collections.graphql.ts +0 -48
  172. package/src/graphql/index.ts +0 -7
  173. package/src/graphql/products.graphql.ts +0 -860
  174. package/src/graphql/wishlist.graphql.ts +0 -34
  175. package/src/hooks/use-consultation-stream.ts +0 -142
  176. package/src/types/bespoke/index.ts +0 -39
@@ -1,25 +1,84 @@
1
1
  /**
2
2
  * Bespoke Service
3
- *
4
- * Handles bespoke product availability checks.
3
+ *
4
+ * Handles bespoke product availability checks and the buyer side of the
5
+ * bespoke order deliverable proofing flow.
5
6
  * Stateless - all configuration passed as parameters.
6
7
  */
7
8
  import type { SdkConfig } from '../config'
8
- import type { BespokeAvailability } from '../types/consultations'
9
+ import type { BespokeAvailability, BespokeDeliverableStatus } from '../types/consultations'
10
+ import type { components } from '../generated/store-openapi'
11
+ import { apiRequest, storefrontHeaders, authHeaders, JSON_HEADERS } from '../utils/http-client'
9
12
 
13
+ // ─── Codegen drift-proofs (openapi/README.md — regenerate via `pnpm run codegen:rest`) ─
14
+ // Same approach as service-booking.service.ts's own comment: these prove
15
+ // the hand-written types still match the real backend schema at compile
16
+ // time, without a risky full rewrite of working code. Found and fixed one
17
+ // real gap this way: BespokeDeliverableStatus was missing
18
+ // bespoke_revision_notes entirely, a field the real backend has always
19
+ // returned.
20
+ type _BespokeAvailabilityMatchesSchema = BespokeAvailability extends components['schemas']['BespokeAvailabilityResponse'] ? true : never
21
+ type _BespokeDeliverableStatusMatchesSchema = BespokeDeliverableStatus extends components['schemas']['BespokeStatusResponse'] ? true : never
22
+ const _bespokeAvailabilityCheck: _BespokeAvailabilityMatchesSchema = true
23
+ const _bespokeDeliverableStatusCheck: _BespokeDeliverableStatusMatchesSchema = true
24
+ void _bespokeAvailabilityCheck
25
+ void _bespokeDeliverableStatusCheck
26
+
27
+ /**
28
+ * REAL BUG FIXED (2026-09-20): was calling `/bespoke/availability/{id}` —
29
+ * missing the `/capacity` segment. Confirmed against the real router
30
+ * (kasuvia-store-management/app/api/v1/endpoints/bespoke/capacity.py:184,
31
+ * registered under the `/bespoke/capacity` prefix): the real route is
32
+ * `/bespoke/capacity/availability/{id}`. Every bespoke availability check
33
+ * was 404ing.
34
+ */
10
35
  export async function checkAvailability(config: SdkConfig, product_id: string): Promise<BespokeAvailability> {
11
- const response = await fetch(`${config.backendUrls.store}/bespoke/availability/${product_id}`, {
12
- headers: {
13
- "X-Storefront-Key": config.storefrontApiKey,
14
- "X-Business-ID": config.businessId,
15
- },
36
+ return apiRequest<BespokeAvailability>(`${config.backendUrls.store}/bespoke/capacity/availability/${product_id}`, {
37
+ headers: storefrontHeaders(config),
38
+ fallbackErrorMessage: 'Failed to check bespoke availability',
39
+ })
40
+ }
41
+
42
+ /**
43
+ * POST /orders/{order_id}/bespoke/approve — buyer approves the seller's
44
+ * submitted deliverable, starting the escrow release clock.
45
+ */
46
+ export async function approveBespokeDeliverable(
47
+ config: SdkConfig,
48
+ accessToken: string,
49
+ orderId: string
50
+ ): Promise<BespokeDeliverableStatus> {
51
+ return apiRequest<BespokeDeliverableStatus>(`${config.backendUrls.store}/orders/${orderId}/bespoke/approve`, {
52
+ method: 'POST',
53
+ headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
54
+ body: JSON.stringify({}),
55
+ fallbackErrorMessage: 'Failed to approve deliverable',
16
56
  })
57
+ }
17
58
 
18
- if (!response.ok) throw new Error('Failed to check bespoke availability')
19
- return response.json()
59
+ /**
60
+ * POST /orders/{order_id}/bespoke/request-revision — buyer asks the seller
61
+ * to revise the submitted deliverable. Capped at 2 revisions server-side;
62
+ * beyond that, the backend escalates to manual review rather than refusing
63
+ * silently (see the real endpoint's own doc comment).
64
+ */
65
+ export async function requestBespokeRevision(
66
+ config: SdkConfig,
67
+ accessToken: string,
68
+ orderId: string,
69
+ notes: string
70
+ ): Promise<BespokeDeliverableStatus> {
71
+ return apiRequest<BespokeDeliverableStatus>(`${config.backendUrls.store}/orders/${orderId}/bespoke/request-revision`, {
72
+ method: 'POST',
73
+ headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
74
+ body: JSON.stringify({ notes }),
75
+ fallbackErrorMessage: 'Failed to request revision',
76
+ })
20
77
  }
21
78
 
22
79
  // Service object export for backward compatibility
23
80
  export const bespokeService = {
24
81
  checkAvailability,
82
+ approveBespokeDeliverable,
83
+ requestBespokeRevision,
25
84
  }
@@ -1,11 +1,33 @@
1
1
  /**
2
2
  * Cart Service
3
- *
4
- * Handles cart operations for both guest and authenticated users.
5
- * Stateless - all configuration passed as parameters.
3
+ *
4
+ * Handles cart operations for both guest and authenticated users via
5
+ * kasuvia-cart-management (Go/Huma). All cart operations target the
6
+ * cart-management service, which is the single source of truth for cart state.
7
+ *
8
+ * KEY CONSTRAINT — backend-resolved fields:
9
+ * kasuvia-cart-management resolves product_name, product_slug, image_url,
10
+ * variant_attributes, unit_price, is_bespoke, and product_type entirely
11
+ * server-side from kasuvia-store-management at add/merge time. These MUST NOT
12
+ * be sent in request payloads — only product_id, variant_id, quantity, and
13
+ * custom_measurements are accepted by the backend.
14
+ *
15
+ * Stateless — all configuration passed as parameters.
6
16
  */
7
17
  import type { SdkConfig } from '../config'
8
- import type { CartItem, CartResponse } from '../types/store'
18
+ import { ProductTypeEnum, type CartItem, type CartResponse, type ProductType } from '../types/store'
19
+ import { apiRequest, storefrontHeaders, authHeaders, JSON_HEADERS } from '../utils/http-client'
20
+
21
+ // REAL BUG FIXED (2026-09-21): every fetch() in this file targeted
22
+ // `config.backendUrls.store` (kasuvia-store-management) even though this
23
+ // file's own header comment already documented that cart moved to
24
+ // kasuvia-cart-management (a separate Go/Huma service, strangler-fig
25
+ // migration). kasuvia-store-management's own router.py explicitly says
26
+ // "Cart is NOT registered here anymore" — every one of these calls 404s
27
+ // against `store`. Fixed by routing through `backendUrls.cart` instead,
28
+ // which callers must now provide (kasuvia-cart-management, same
29
+ // `/api/v1/cart/...` route shape it was ported from — only the base URL
30
+ // changes).
9
31
 
10
32
  function parseItems(items: CartItem[]): CartItem[] {
11
33
  return items.map((item) => ({
@@ -17,63 +39,54 @@ function parseItems(items: CartItem[]): CartItem[] {
17
39
  }));
18
40
  }
19
41
 
42
+ function guestHeaders(config: SdkConfig, guestSessionId: string, extra?: Record<string, string>): Record<string, string> {
43
+ return { ...extra, ...storefrontHeaders(config), 'X-Guest-Session': guestSessionId }
44
+ }
45
+
20
46
  /**
21
47
  * Fetch guest cart from backend (Redis-backed).
22
48
  */
23
- export async function getGuestCart(config: SdkConfig, guestSessionId: string): Promise<CartItem[]> {
24
- if (!guestSessionId) return []
49
+ export async function getGuestCart(config: SdkConfig, guestSessionId: string): Promise<CartResponse> {
50
+ if (!guestSessionId) throw new Error('Guest session ID is required to fetch guest cart');
25
51
 
26
- try {
27
- const response = await fetch(`${config.backendUrls.store}/cart/`, {
28
- headers: {
29
- "X-Storefront-Key": config.storefrontApiKey,
30
- "X-Business-ID": config.businessId,
31
- "X-Guest-Session": guestSessionId
32
- },
33
- })
34
- if (!response.ok) return []
35
- const data = await response.json()
36
- return parseItems(data.items || [])
37
- } catch {
38
- return []
39
- }
52
+ const data = await apiRequest<CartResponse>(`${config.backendUrls.cart}/cart/`, {
53
+ headers: guestHeaders(config, guestSessionId),
54
+ fallbackErrorMessage: 'Failed to fetch guest cart',
55
+ })
56
+ return { ...data, items: parseItems(data.items || []) }
40
57
  }
41
58
 
42
59
  /**
43
60
  * Add item to guest cart (Redis-backed).
61
+ * Only product_id, variant_id, quantity, and custom_measurements are sent —
62
+ * all other fields are resolved server-side by kasuvia-cart-management.
44
63
  */
45
- export async function addToGuestCart(config: SdkConfig, guestSessionId: string, item: CartItem): Promise<CartItem[]> {
46
- if (!guestSessionId) return []
64
+ export async function addToGuestCart(
65
+ config: SdkConfig,
66
+ guestSessionId: string,
67
+ item: {
68
+ product_id: string
69
+ variant_id?: string | null
70
+ quantity: number
71
+ custom_measurements?: string | null
72
+ }
73
+ ): Promise<CartResponse> {
74
+ if (!guestSessionId) throw new Error('Guest session ID is required to add to guest cart');
47
75
 
48
76
  const payload = {
49
77
  product_id: item.product_id,
50
- product_slug: item.product_slug,
51
- product_name: item.product_name,
52
- image: item.image,
53
- variant_id: item.variant_id,
54
- variant_attributes:
55
- typeof item.variant_attributes === "string"
56
- ? item.variant_attributes
57
- : JSON.stringify(item.variant_attributes),
58
- unit_price: item.unit_price,
78
+ variant_id: item.variant_id ?? undefined,
59
79
  quantity: item.quantity,
60
- custom_measurements: item.custom_measurements,
61
- is_bespoke: item.is_bespoke,
80
+ custom_measurements: item.custom_measurements ?? undefined,
62
81
  }
63
82
 
64
- const response = await fetch(`${config.backendUrls.store}/cart/`, {
83
+ const data = await apiRequest<CartResponse>(`${config.backendUrls.cart}/cart/`, {
65
84
  method: 'POST',
66
- headers: {
67
- 'Content-Type': 'application/json',
68
- "X-Storefront-Key": config.storefrontApiKey,
69
- "X-Business-ID": config.businessId,
70
- "X-Guest-Session": guestSessionId
71
- },
85
+ headers: guestHeaders(config, guestSessionId, JSON_HEADERS),
72
86
  body: JSON.stringify(payload),
87
+ fallbackErrorMessage: 'Failed to add to guest cart',
73
88
  })
74
- if (!response.ok) return []
75
- const data = await response.json()
76
- return parseItems(data.items || [])
89
+ return { ...data, items: parseItems(data.items || []) }
77
90
  }
78
91
 
79
92
  /**
@@ -84,59 +97,60 @@ export async function updateGuestCartItemQuantity(
84
97
  guestSessionId: string,
85
98
  itemId: string,
86
99
  quantity: number
87
- ): Promise<CartItem[]> {
88
- if (!guestSessionId) return []
100
+ ): Promise<CartResponse> {
101
+ if (!guestSessionId) throw new Error('Guest session ID is required to update guest cart item');
89
102
 
90
- const response = await fetch(`${config.backendUrls.store}/cart/${itemId}`, {
103
+ const data = await apiRequest<CartResponse>(`${config.backendUrls.cart}/cart/${itemId}`, {
91
104
  method: 'PATCH',
92
- headers: {
93
- 'Content-Type': 'application/json',
94
- "X-Storefront-Key": config.storefrontApiKey,
95
- "X-Business-ID": config.businessId,
96
- "X-Guest-Session": guestSessionId
97
- },
105
+ headers: guestHeaders(config, guestSessionId, JSON_HEADERS),
98
106
  body: JSON.stringify({ quantity }),
107
+ fallbackErrorMessage: 'Failed to update guest cart item',
99
108
  })
100
- if (!response.ok) return []
101
- const data = await response.json()
102
- return parseItems(data.items || [])
109
+ return { ...data, items: parseItems(data.items || []) }
103
110
  }
104
111
 
105
112
  /**
106
113
  * Remove an item from the guest cart.
114
+ *
115
+ * REAL BUG FIXED (2026-09-22), live-reproduced: this used to type its
116
+ * result as `CartResponse` and read `data.items` off it, but the real
117
+ * backend's `DELETE /cart/{item_id}` returns `204 No Content` with a
118
+ * genuinely empty body (confirmed live: `HTTP_STATUS:204 BODY_LENGTH:0`) -
119
+ * `apiRequest` resolves an empty body to `undefined`, so `data.items`
120
+ * threw `TypeError: Cannot read properties of undefined (reading 'items')`
121
+ * on every real delete. The item really was removed server-side (the
122
+ * DELETE request itself succeeded), but the thrown error sent
123
+ * `removeFromCartMutation` down `onError` instead of `onSuccess`, so
124
+ * `queryClient.invalidateQueries` never ran - a customer's cart kept
125
+ * showing the "removed" item until something else (a full page reload)
126
+ * happened to trigger a fresh, uncached fetch. Matches `clearGuestCart`'s
127
+ * existing `Promise<void>` shape, which was already correct for this same
128
+ * "backend returns no body" pattern.
107
129
  */
108
130
  export async function removeFromGuestCart(
109
131
  config: SdkConfig,
110
132
  guestSessionId: string,
111
133
  itemId: string
112
- ): Promise<CartItem[]> {
113
- if (!guestSessionId) return []
134
+ ): Promise<void> {
135
+ if (!guestSessionId) throw new Error('Guest session ID is required to remove from guest cart');
114
136
 
115
- await fetch(`${config.backendUrls.store}/cart/${itemId}`, {
137
+ await apiRequest(`${config.backendUrls.cart}/cart/${itemId}`, {
116
138
  method: 'DELETE',
117
- headers: {
118
- "X-Storefront-Key": config.storefrontApiKey,
119
- "X-Business-ID": config.businessId,
120
- "X-Guest-Session": guestSessionId
121
- },
139
+ headers: guestHeaders(config, guestSessionId),
140
+ fallbackErrorMessage: 'Failed to remove item from guest cart',
122
141
  })
123
-
124
- return getGuestCart(config, guestSessionId)
125
142
  }
126
143
 
127
144
  /**
128
145
  * Clear the entire guest cart.
129
146
  */
130
147
  export async function clearGuestCart(config: SdkConfig, guestSessionId: string): Promise<void> {
131
- if (!guestSessionId) return
148
+ if (!guestSessionId) throw new Error('Guest session ID is required to clear guest cart');
132
149
 
133
- await fetch(`${config.backendUrls.store}/cart/clear`, {
150
+ await apiRequest(`${config.backendUrls.cart}/cart/clear`, {
134
151
  method: 'DELETE',
135
- headers: {
136
- "X-Storefront-Key": config.storefrontApiKey,
137
- "X-Business-ID": config.businessId,
138
- "X-Guest-Session": guestSessionId
139
- },
152
+ headers: guestHeaders(config, guestSessionId),
153
+ fallbackErrorMessage: 'Failed to clear guest cart',
140
154
  })
141
155
  }
142
156
 
@@ -145,54 +159,54 @@ export async function clearGuestCart(config: SdkConfig, guestSessionId: string):
145
159
  */
146
160
  export async function getCart(config: SdkConfig, accessToken?: string | null): Promise<CartResponse> {
147
161
  if (!accessToken) {
148
- return { id: '', items: [], subtotal: 0, total: 0, product_type: 'physical' } as any;
162
+ throw new Error('Access token is required to fetch authenticated cart');
149
163
  }
150
- const response = await fetch(`${config.backendUrls.store}/cart/`, {
151
- headers: {
152
- "Authorization": `Bearer ${accessToken}`,
153
- "X-Storefront-Key": config.storefrontApiKey,
154
- "X-Business-ID": config.businessId,
155
- },
164
+ const data = await apiRequest<CartResponse>(`${config.backendUrls.cart}/cart/`, {
165
+ headers: authHeaders(config, accessToken),
166
+ fallbackErrorMessage: 'Failed to fetch cart',
156
167
  })
157
- if (!response.ok) throw new Error('Failed to fetch cart')
158
- const data = await response.json()
159
- const parsedItems = parseItems(data.items || [])
160
- return { ...data, items: parsedItems }
168
+ return { ...data, items: parseItems(data.items || []) }
161
169
  }
162
170
 
163
171
  /**
164
172
  * Add item to authenticated user's cart.
173
+ * Only product_id, variant_id, quantity, and custom_measurements are sent —
174
+ * all other fields are resolved server-side by kasuvia-cart-management.
165
175
  */
166
176
  export async function addToCart(
167
177
  config: SdkConfig,
168
178
  accessToken: string,
169
179
  item: {
170
180
  product_id: string
171
- product_slug: string
172
- product_name: string
173
- image?: string
174
- variant_id?: string
175
- variant_attributes?: string
176
- unit_price: number
181
+ variant_id?: string | null
177
182
  quantity: number
178
- custom_measurements?: string
179
- is_bespoke: boolean
183
+ custom_measurements?: string | null
184
+ // These fields are accepted from the caller for UI convenience but are
185
+ // NOT forwarded to the backend — kasuvia-cart-management resolves them
186
+ // server-side. Declared here so callers don't need to strip them.
187
+ product_slug?: string
188
+ product_name?: string
189
+ image_url?: string
190
+ variant_attributes?: string
191
+ unit_price?: number
192
+ is_bespoke?: boolean
180
193
  }
181
194
  ): Promise<CartResponse> {
182
- const response = await fetch(`${config.backendUrls.store}/cart/`, {
195
+ // Only send the fields the backend accepts — strip everything else.
196
+ const payload = {
197
+ product_id: item.product_id,
198
+ variant_id: item.variant_id ?? undefined,
199
+ quantity: item.quantity,
200
+ custom_measurements: item.custom_measurements ?? undefined,
201
+ }
202
+
203
+ const data = await apiRequest<CartResponse>(`${config.backendUrls.cart}/cart/`, {
183
204
  method: 'POST',
184
- headers: {
185
- 'Content-Type': 'application/json',
186
- "Authorization": `Bearer ${accessToken}`,
187
- "X-Storefront-Key": config.storefrontApiKey,
188
- "X-Business-ID": config.businessId,
189
- },
190
- body: JSON.stringify(item),
205
+ headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
206
+ body: JSON.stringify(payload),
207
+ fallbackErrorMessage: 'Failed to add to cart',
191
208
  })
192
- if (!response.ok) throw new Error('Failed to add to cart')
193
- const data = await response.json()
194
- const parsedItems = parseItems(data.items || [])
195
- return { ...data, items: parsedItems }
209
+ return { ...data, items: parseItems(data.items || []) }
196
210
  }
197
211
 
198
212
  /**
@@ -204,144 +218,99 @@ export async function updateCartItem(
204
218
  itemId: string,
205
219
  quantity: number
206
220
  ): Promise<CartResponse> {
207
- const response = await fetch(`${config.backendUrls.store}/cart/${itemId}`, {
221
+ const data = await apiRequest<CartResponse>(`${config.backendUrls.cart}/cart/${itemId}`, {
208
222
  method: 'PATCH',
209
- headers: {
210
- 'Content-Type': 'application/json',
211
- "Authorization": `Bearer ${accessToken}`,
212
- "X-Storefront-Key": config.storefrontApiKey,
213
- "X-Business-ID": config.businessId,
214
- },
223
+ headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
215
224
  body: JSON.stringify({ quantity }),
225
+ fallbackErrorMessage: 'Failed to update cart item',
216
226
  })
217
- if (!response.ok) throw new Error('Failed to update cart item')
218
- const data = await response.json()
219
- const parsedItems = parseItems(data.items || [])
220
- return { ...data, items: parsedItems }
227
+ return { ...data, items: parseItems(data.items || []) }
221
228
  }
222
229
 
223
230
  /**
224
231
  * Remove item from authenticated user's cart.
232
+ * Same real bug/fix as removeFromGuestCart above - see its doc comment.
225
233
  */
226
234
  export async function removeFromCart(
227
235
  config: SdkConfig,
228
236
  accessToken: string,
229
237
  itemId: string
230
- ): Promise<CartResponse> {
231
- await fetch(`${config.backendUrls.store}/cart/${itemId}`, {
238
+ ): Promise<void> {
239
+ await apiRequest(`${config.backendUrls.cart}/cart/${itemId}`, {
232
240
  method: 'DELETE',
233
- headers: {
234
- "Authorization": `Bearer ${accessToken}`,
235
- "X-Storefront-Key": config.storefrontApiKey,
236
- "X-Business-ID": config.businessId,
237
- },
241
+ headers: authHeaders(config, accessToken),
242
+ fallbackErrorMessage: 'Failed to remove cart item',
238
243
  })
239
- return getCart(config, accessToken)
240
244
  }
241
245
 
242
246
  /**
243
247
  * Clear authenticated user's cart.
244
248
  */
245
249
  export async function clearCart(config: SdkConfig, accessToken: string): Promise<void> {
246
- await fetch(`${config.backendUrls.store}/cart/clear`, {
250
+ await apiRequest(`${config.backendUrls.cart}/cart/clear`, {
247
251
  method: 'DELETE',
248
- headers: {
249
- "Authorization": `Bearer ${accessToken}`,
250
- "X-Storefront-Key": config.storefrontApiKey,
251
- "X-Business-ID": config.businessId,
252
- },
252
+ headers: authHeaders(config, accessToken),
253
+ fallbackErrorMessage: 'Failed to clear cart',
253
254
  })
254
255
  }
255
256
 
256
257
  /**
257
- * Merge guest cart (from Redis) into authenticated user's DB cart.
258
+ * Merge guest cart items into the authenticated customer's cart.
259
+ *
260
+ * The backend (POST /api/v1/cart/merge) accepts only:
261
+ * { items: [{ product_id, variant_id?, quantity, custom_measurements? }] }
262
+ * Product name, image, price, type are all re-resolved server-side.
263
+ *
264
+ * Sends X-Guest-Session so the backend can atomically delete the guest
265
+ * cart (best-effort) after merging.
258
266
  */
259
267
  export async function mergeGuestCart(
260
268
  config: SdkConfig,
261
269
  accessToken: string,
262
- guestSessionId: string
270
+ guestSessionId: string,
271
+ guestItems: Pick<CartItem, 'product_id' | 'variant_id' | 'quantity' | 'custom_measurements'>[]
263
272
  ): Promise<CartResponse> {
264
- const guestItems = guestSessionId ? await getGuestCart(config, guestSessionId) : []
265
-
266
- if (guestItems.length === 0) {
267
- return getCart(config, accessToken)
268
- }
269
-
270
273
  const payloadItems = guestItems.map((item) => ({
271
274
  product_id: item.product_id,
272
- product_slug: item.product_slug,
273
- product_name: item.product_name,
274
- image: item.image,
275
- variant_id: item.variant_id,
276
- variant_attributes:
277
- typeof item.variant_attributes === "string"
278
- ? item.variant_attributes
279
- : JSON.stringify(item.variant_attributes ?? {}),
280
- unit_price: item.unit_price,
275
+ variant_id: item.variant_id ?? undefined,
281
276
  quantity: item.quantity,
282
- custom_measurements:
283
- typeof item.custom_measurements === "string"
284
- ? item.custom_measurements
285
- : JSON.stringify(item.custom_measurements ?? {}),
286
- is_bespoke: item.is_bespoke,
277
+ custom_measurements: item.custom_measurements ?? undefined,
287
278
  }))
288
279
 
289
- const headers: Record<string, string> = {
290
- 'Content-Type': 'application/json',
291
- "Authorization": `Bearer ${accessToken}`,
292
- "X-Storefront-Key": config.storefrontApiKey,
293
- "X-Business-ID": config.businessId,
294
- }
280
+ const headers: Record<string, string> = { ...JSON_HEADERS, ...authHeaders(config, accessToken) }
295
281
  if (guestSessionId) {
296
282
  headers["X-Guest-Session"] = guestSessionId
297
283
  }
298
284
 
299
- const response = await fetch(`${config.backendUrls.store}/cart/merge`, {
285
+ const data = await apiRequest<CartResponse>(`${config.backendUrls.cart}/cart/merge`, {
300
286
  method: 'POST',
301
287
  headers,
302
288
  body: JSON.stringify({ items: payloadItems }),
289
+ fallbackErrorMessage: 'Failed to merge guest cart',
303
290
  })
304
- if (!response.ok) throw new Error('Failed to merge guest cart')
305
- const data = await response.json()
306
291
  return { ...data, items: parseItems(data.items || []) }
307
292
  }
308
293
 
309
294
  /**
310
- * Convenience wrapper: merge guest cart if it has items (called post-login).
295
+ * Convenience wrapper: fetch guest cart and merge if it has items (called post-login).
311
296
  */
312
297
  export async function mergeGuestCartIfPresent(
313
298
  config: SdkConfig,
314
299
  accessToken: string,
315
300
  guestSessionId: string
316
301
  ): Promise<void> {
317
- try {
318
- const guestItems = await getGuestCart(config, guestSessionId)
319
- if (guestItems.length === 0) return
320
- await mergeGuestCart(config, accessToken, guestSessionId)
321
- } catch (error) {
322
- console.warn("[Cart] Failed to merge guest cart:", error)
323
- }
324
- }
325
-
326
- /**
327
- * Returns 0 — count is now server-driven. Use the hook instead.
328
- * Kept for backward compatibility.
329
- */
330
- export function getGuestCartCount(): number {
331
- return 0
332
- }
333
-
334
- /**
335
- * Returns 0 — subtotal is now server-driven.
336
- * Kept for backward compatibility.
337
- */
338
- export function getGuestCartSubtotal(): number {
339
- return 0
302
+ const guestCart = await getGuestCart(config, guestSessionId)
303
+ if (!guestCart.items.length) return
304
+ await mergeGuestCart(config, accessToken, guestSessionId, guestCart.items)
340
305
  }
341
306
 
342
307
  /**
343
- * Derive product type from items (guest cart, no backend lookup needed).
308
+ * Helper to get a guest cart's product type (for type-mismatch checks before adding an item).
344
309
  */
345
- export function getGuestCartProductType(): "physical" | "digital" | "bespoke" {
346
- return "physical"
310
+ export function getGuestCartProductType(items?: CartItem[]): ProductType | null {
311
+ if (!items || !items.length) return null;
312
+ const types = new Set(items.map(i => i.product_type).filter(Boolean));
313
+ if (types.size === 0) return null;
314
+ if (types.size === 1) return ([...types][0] as ProductType) ?? null;
315
+ return ProductTypeEnum.PHYSICAL;
347
316
  }