@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,22 +1,13 @@
1
1
  /**
2
2
  * Logistics Service
3
- *
3
+ *
4
4
  * Handles shipping rates and address validation.
5
5
  * Stateless - all configuration passed as parameters.
6
6
  */
7
7
  import type { SdkConfig } from '../config'
8
8
  import type { ShippingRatesResponse } from '../types/store/shipping'
9
- import type { Product, ProductVariant } from '../types/store'
10
-
11
- export interface CartItemPayload {
12
- product: {
13
- name: string
14
- price: number
15
- weight: number
16
- }
17
- variant: ProductVariant | null
18
- quantity: number
19
- }
9
+ import type { components } from '../generated/store-openapi'
10
+ import { apiRequest, storefrontHeaders, JSON_HEADERS } from '../utils/http-client'
20
11
 
21
12
  export interface RateAddress {
22
13
  city: string
@@ -24,11 +15,28 @@ export interface RateAddress {
24
15
  country: string
25
16
  }
26
17
 
18
+ /**
19
+ * REAL BUG FIXED (2026-09-20): this request shape used to be `{cart_items,
20
+ * origin_address, destination_address, currency?, shipping_method?}` —
21
+ * completely stale against the real backend. Confirmed against the real
22
+ * Pydantic schema (kasuvia-store-management/app/schemas/logistics.py:121-144,
23
+ * `ShippingRatesRequest`), which explicitly documents why: "No origin_address
24
+ * field — the server resolves the fulfilling StoreLocation itself... A
25
+ * client-supplied origin would either have to be silently ignored or
26
+ * silently trusted over the resolved location; neither is acceptable, so it
27
+ * isn't accepted at all." The server also fetches cart items itself from
28
+ * `cart_id` (server-side cart as source of truth, preventing client-side
29
+ * cart manipulation) rather than trusting a client-submitted item list.
30
+ * Sending the old shape meant every physical checkout's shipping-rates call
31
+ * was missing the required `cart_id` field entirely and would always 422.
32
+ */
27
33
  export interface ShippingRatesRequest {
28
- cart_items: CartItemPayload[]
29
- origin_address: RateAddress
34
+ cart_id: string
30
35
  destination_address: RateAddress
31
- currency?: string
36
+ currency: string
37
+ shipping_method?: 'carrier' | 'self_pickup'
38
+ /** Required when shipping_method is 'self_pickup' — see GET /logistics/pickup-locations. */
39
+ pickup_location_id?: string
32
40
  }
33
41
 
34
42
  /**
@@ -38,43 +46,34 @@ export async function getShippingRates(
38
46
  config: SdkConfig,
39
47
  payload: ShippingRatesRequest
40
48
  ): Promise<ShippingRatesResponse> {
41
- const response = await fetch(`${config.backendUrls.store}/logistics/shipping/rates`, {
49
+ return apiRequest<ShippingRatesResponse>(`${config.backendUrls.store}/logistics/shipping/rates`, {
42
50
  method: 'POST',
43
- headers: {
44
- 'Content-Type': 'application/json',
45
- "X-Storefront-Key": config.storefrontApiKey,
46
- "X-Business-ID": config.businessId,
47
- },
51
+ headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
48
52
  body: JSON.stringify(payload),
53
+ fallbackErrorMessage: 'Failed to get shipping rates',
49
54
  })
50
- if (!response.ok) throw new Error('Failed to get shipping rates')
51
- return response.json()
52
55
  }
53
56
 
54
57
  /**
55
- * Fetch the seller's registered shipping address.
58
+ * Result of independently forward-geocoding the typed street address via
59
+ * LocationIQ and cross-checking it against Terminal Africa's own (city/
60
+ * state/country-only, street-line-blind) validation. See
61
+ * app/services/address_trust_service.py on the backend. Only present when
62
+ * Terminal Africa itself reported the address valid.
56
63
  */
57
- export async function getSellerShippingAddress(config: SdkConfig): Promise<{
58
- terminal_address_id: string | null
59
- address_details: {
60
- city: string
61
- state: string
62
- country: string
63
- line1: string
64
- line2: string
65
- zip: string
64
+ export interface AddressTrustResult {
65
+ trusted: boolean
66
+ confidence: 'high' | 'medium' | 'low' | 'unavailable'
67
+ reason: string | null
68
+ resolved: {
69
+ latitude: number
70
+ longitude: number
71
+ display_name: string
72
+ city: string | null
73
+ state: string | null
74
+ country: string | null
66
75
  } | null
67
- business_id: string
68
- message: string
69
- }> {
70
- const response = await fetch(`${config.backendUrls.store}/logistics/shipping-address`, {
71
- headers: {
72
- "X-Storefront-Key": config.storefrontApiKey,
73
- "X-Business-ID": config.businessId,
74
- },
75
- })
76
- if (!response.ok) throw new Error('Failed to get seller shipping address')
77
- return response.json()
76
+ plus_code: string | null
78
77
  }
79
78
 
80
79
  /**
@@ -100,23 +99,206 @@ export async function validateShippingAddress(
100
99
  data?: unknown
101
100
  cities?: Array<{ name: string; city_id: string; stateCode: string; countryCode: string }>
102
101
  states?: Array<{ name: string; code: string; countryCode: string }>
102
+ /** Street-level trust check — only present when valid is true. See AddressTrustResult. */
103
+ address_trust?: AddressTrustResult
103
104
  }> {
104
- const response = await fetch(`${config.backendUrls.store}/logistics/shipping-address/validate`, {
105
+ return apiRequest(`${config.backendUrls.store}/logistics/shipping-address/validate`, {
105
106
  method: 'POST',
106
- headers: {
107
- 'Content-Type': 'application/json',
108
- "X-Storefront-Key": config.storefrontApiKey,
109
- "X-Business-ID": config.businessId,
110
- },
107
+ headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
111
108
  body: JSON.stringify(payload),
109
+ fallbackErrorMessage: 'Failed to validate shipping address',
110
+ })
111
+ }
112
+
113
+ export interface PickupLocation {
114
+ id: string
115
+ name: string
116
+ description: string | null
117
+ address: {
118
+ line1: string
119
+ line2: string | null
120
+ city: string
121
+ state: string
122
+ country: string
123
+ postal_code: string
124
+ }
125
+ coordinates: { latitude: number; longitude: number }
126
+ /** km from the coordinates passed to listPickupLocations, or null if none were passed. */
127
+ distance_km: number | null
128
+ operating_hours: Record<string, string> | null
129
+ }
130
+
131
+ /**
132
+ * List the business's active pickup locations, so the customer can choose
133
+ * one at checkout for shipping_method="self_pickup" instead of the merchant
134
+ * assigning one blindly after the fact. Pass the customer's shipping
135
+ * coordinates to sort nearest-first.
136
+ */
137
+ export interface PickupLocationsResult {
138
+ locations: PickupLocation[]
139
+ }
140
+
141
+ async function fetchLocationList(
142
+ config: SdkConfig,
143
+ path: 'pickup-locations' | 'locations',
144
+ coords: { latitude: number; longitude: number } | undefined,
145
+ fallbackErrorMessage: string
146
+ ): Promise<PickupLocationsResult> {
147
+ const params = new URLSearchParams()
148
+ if (coords) {
149
+ params.set('latitude', String(coords.latitude))
150
+ params.set('longitude', String(coords.longitude))
151
+ }
152
+ const qs = params.toString()
153
+ return apiRequest(`${config.backendUrls.store}/logistics/${path}${qs ? `?${qs}` : ''}`, {
154
+ headers: storefrontHeaders(config),
155
+ fallbackErrorMessage,
112
156
  })
113
- if (!response.ok) throw new Error('Failed to validate shipping address')
114
- return response.json()
115
157
  }
116
158
 
159
+ export async function listPickupLocations(
160
+ config: SdkConfig,
161
+ coords?: { latitude: number; longitude: number }
162
+ ): Promise<PickupLocationsResult> {
163
+ return fetchLocationList(config, 'pickup-locations', coords, 'Failed to list pickup locations')
164
+ }
165
+
166
+ /**
167
+ * List ALL of the business's active locations (not just pickup-eligible
168
+ * ones) — the set a customer needs to choose from for an 'at_business'
169
+ * service appointment when the business runs more than one branch. Sibling
170
+ * of listPickupLocations, same response shape (PickupLocationsResult),
171
+ * different backend filter (GET /logistics/locations, no
172
+ * is_pickup_location gate).
173
+ */
174
+ export async function listBusinessLocations(
175
+ config: SdkConfig,
176
+ coords?: { latitude: number; longitude: number }
177
+ ): Promise<PickupLocationsResult> {
178
+ return fetchLocationList(config, 'locations', coords, 'Failed to list business locations')
179
+ }
180
+
181
+ /**
182
+ * Reverse geocode coordinates to address components using backend LocationIQ service.
183
+ */
184
+ export interface ReverseGeocodeResult {
185
+ city: string | null
186
+ state: string | null
187
+ country: string | null
188
+ country_code: string | null
189
+ display_name: string | null
190
+ }
191
+
192
+ export async function reverseGeocode(
193
+ config: SdkConfig,
194
+ payload: { latitude: number; longitude: number }
195
+ ): Promise<ReverseGeocodeResult> {
196
+ return apiRequest(`${config.backendUrls.store}/logistics/reverse-geocode`, {
197
+ method: 'POST',
198
+ headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
199
+ body: JSON.stringify(payload),
200
+ fallbackErrorMessage: 'Failed to reverse geocode coordinates',
201
+ })
202
+ }
203
+
204
+ /**
205
+ * Decode a Plus Code to coordinates.
206
+ */
207
+ export interface PlusCodeDecodeResult {
208
+ latitude: number
209
+ longitude: number
210
+ latitude_lo: number
211
+ longitude_hi: number
212
+ plus_code: string
213
+ }
214
+
215
+ export async function decodePlusCode(
216
+ config: SdkConfig,
217
+ payload: { plus_code: string }
218
+ ): Promise<PlusCodeDecodeResult> {
219
+ return apiRequest(`${config.backendUrls.store}/logistics/plus-code/decode`, {
220
+ method: 'POST',
221
+ headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
222
+ body: JSON.stringify(payload),
223
+ fallbackErrorMessage: 'Failed to decode Plus Code',
224
+ })
225
+ }
226
+
227
+ /**
228
+ * As-you-type address autocomplete suggestions.
229
+ */
230
+ export interface AddressSuggestionResult {
231
+ place_id: string
232
+ display_name: string
233
+ line1: string
234
+ city: string | null
235
+ state: string | null
236
+ postal_code: string | null
237
+ country: string | null
238
+ country_code: string | null
239
+ latitude: number | null
240
+ longitude: number | null
241
+ }
242
+
243
+ export interface AddressAutocompleteResult {
244
+ results: AddressSuggestionResult[]
245
+ }
246
+
247
+ export async function autocompleteAddress(
248
+ config: SdkConfig,
249
+ q: string,
250
+ country?: string
251
+ ): Promise<AddressAutocompleteResult> {
252
+ const params = new URLSearchParams()
253
+ params.set('q', q)
254
+ if (country) params.set('country', country)
255
+
256
+ return apiRequest(`${config.backendUrls.store}/logistics/address/autocomplete?${params.toString()}`, {
257
+ headers: storefrontHeaders(config),
258
+ fallbackErrorMessage: 'Failed to autocomplete address',
259
+ })
260
+ }
261
+
262
+ // ─── Codegen drift-proofs (openapi/README.md — regenerate via `pnpm run codegen:rest`) ─
263
+ // Same approach as the other 3 target files' own comments. `getShippingRates`
264
+ // and `validateShippingAddress` are deliberately NOT checked here:
265
+ // GET /logistics/shipping/rates passes through Terminal Africa's own
266
+ // response envelope as a raw dict (no Kasuvia-owned schema exists to check
267
+ // against - a third party's contract, not this platform's), and
268
+ // POST /logistics/shipping-address/validate similarly has no
269
+ // response_model= (confirmed absent, same underlying gap class as
270
+ // pickup-locations was until this pass, but out of scope for this initial
271
+ // sweep - flagged here rather than silently left unchecked with no
272
+ // explanation). PickupLocationsResult/ReverseGeocodeResult/
273
+ // PlusCodeDecodeResult/AddressAutocompleteResult all have real backend
274
+ // schemas and are checked below - PickupLocationsResponse's
275
+ // response_model was added to the backend as part of this exact change
276
+ // (previously also a raw-dict gap).
277
+ // `operating_hours` is excluded from this check: it's a schemaless
278
+ // `Dict[str, Any]` on the backend, which openapi-typescript can only infer
279
+ // as `Record<string, never>` (an empty object) - a known tooling
280
+ // imprecision (same class as shipping_address/variant_details elsewhere),
281
+ // not a real type mismatch, so it would fail this check even when correct.
282
+ type _PickupLocationsMatchesSchema = Omit<PickupLocationsResult['locations'][number], 'operating_hours'> extends Omit<components['schemas']['PickupLocationItem'], 'operating_hours'> ? true : never
283
+ type _ReverseGeocodeMatchesSchema = ReverseGeocodeResult extends components['schemas']['ReverseGeocodeResponse'] ? true : never
284
+ type _PlusCodeDecodeMatchesSchema = PlusCodeDecodeResult extends components['schemas']['PlusCodeDecodeResponse'] ? true : never
285
+ type _AddressAutocompleteMatchesSchema = AddressAutocompleteResult extends components['schemas']['AddressAutocompleteResponse'] ? true : never
286
+ const _pickupLocationsCheck: _PickupLocationsMatchesSchema = true
287
+ const _reverseGeocodeCheck: _ReverseGeocodeMatchesSchema = true
288
+ const _plusCodeDecodeCheck: _PlusCodeDecodeMatchesSchema = true
289
+ const _addressAutocompleteCheck: _AddressAutocompleteMatchesSchema = true
290
+ void _pickupLocationsCheck
291
+ void _reverseGeocodeCheck
292
+ void _plusCodeDecodeCheck
293
+ void _addressAutocompleteCheck
294
+
117
295
  // Service object for backward compatibility
118
296
  export const logisticsService = {
119
297
  getShippingRates,
120
- getSellerShippingAddress,
121
298
  validateShippingAddress,
299
+ listPickupLocations,
300
+ listBusinessLocations,
301
+ reverseGeocode,
302
+ decodePlusCode,
303
+ autocompleteAddress,
122
304
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Media Upload Service
3
+ *
4
+ * The real 3-step direct-to-R2 upload orchestration (presigned URL → PUT →
5
+ * confirm) that a browser client actually needs to run. Distinct from
6
+ * `auth.service.ts`'s `getPresignedUploadUrl`/`confirmMediaUpload`, which
7
+ * take an explicit bearer `accessToken` and are meant for server-side use
8
+ * (Next.js Route Handlers reading a customer's httpOnly cookie) — a "use
9
+ * client" component has no access token value in JS at all in that model,
10
+ * only the cookie, so it calls two thin same-origin proxy routes instead
11
+ * (`credentials: 'include'`) that wrap those same two SDK functions
12
+ * server-side.
13
+ *
14
+ * REAL DUPLICATION FIXED (2026-09-22): this exact 3-step sequence had been
15
+ * hand-written independently in two different kasuvia-storefront components
16
+ * (BespokeStep.tsx, ProductReviews.tsx) — identical logic, copy-pasted, with
17
+ * no shared SDK function to reuse. A second storefront project would have
18
+ * had to write it a third time. The two endpoint paths are parameterized
19
+ * (defaulting to the same `/api/auth/media/*` convention this storefront
20
+ * already uses) precisely so a *different* storefront that names its proxy
21
+ * routes differently can still use this function without a fork.
22
+ */
23
+ import { apiRequest, JSON_HEADERS } from '../utils/http-client'
24
+ import type { MediaAsset } from '../types/media'
25
+ import type { PresignedUploadUrlResponse } from './auth.service'
26
+
27
+ export interface UploadMediaFileParams {
28
+ file: File
29
+ /** e.g. "review", "digital_product" — matches kasuvia-media-system's context_validation.py. */
30
+ context: string
31
+ entityId: string
32
+ affiliation?: string
33
+ ownerId?: string
34
+ presignedUrlEndpoint?: string
35
+ confirmUploadEndpoint?: string
36
+ }
37
+
38
+ /**
39
+ * Upload one file end-to-end: request a presigned URL, PUT the file
40
+ * directly to R2, then confirm the upload to get back a real MediaAsset.
41
+ */
42
+ export async function uploadMediaFile(params: UploadMediaFileParams): Promise<MediaAsset> {
43
+ const presignedUrlEndpoint = params.presignedUrlEndpoint ?? '/api/auth/media/presigned-url'
44
+ const confirmUploadEndpoint = params.confirmUploadEndpoint ?? '/api/auth/media/confirm-upload'
45
+
46
+ const { presigned_url, storage_path } = await apiRequest<PresignedUploadUrlResponse>(presignedUrlEndpoint, {
47
+ method: 'POST',
48
+ headers: JSON_HEADERS,
49
+ credentials: 'include',
50
+ body: JSON.stringify({
51
+ filename: params.file.name,
52
+ content_type: params.file.type,
53
+ context: params.context,
54
+ entity_id: params.entityId,
55
+ affiliation: params.affiliation,
56
+ owner_id: params.ownerId,
57
+ }),
58
+ fallbackErrorMessage: 'Could not start upload',
59
+ })
60
+
61
+ const putResponse = await fetch(presigned_url, {
62
+ method: 'PUT',
63
+ headers: { 'Content-Type': params.file.type },
64
+ body: params.file,
65
+ })
66
+ if (!putResponse.ok) {
67
+ throw new Error(`Failed to upload ${params.file.name}`)
68
+ }
69
+
70
+ return apiRequest<MediaAsset>(confirmUploadEndpoint, {
71
+ method: 'POST',
72
+ headers: JSON_HEADERS,
73
+ credentials: 'include',
74
+ body: JSON.stringify({
75
+ storage_path,
76
+ entity_id: params.entityId,
77
+ original_filename: params.file.name,
78
+ content_type: params.file.type,
79
+ size: params.file.size,
80
+ context: params.context,
81
+ affiliation: params.affiliation,
82
+ owner_id: params.ownerId,
83
+ }),
84
+ fallbackErrorMessage: `Could not confirm upload of ${params.file.name}`,
85
+ })
86
+ }
87
+
88
+ export interface UploadMediaFilesResult {
89
+ assets: MediaAsset[]
90
+ errors: { file: string; message: string }[]
91
+ }
92
+
93
+ /**
94
+ * Upload several files, one at a time (sequential — real progress feedback
95
+ * per file rather than an opaque Promise.all batch). A single file's
96
+ * failure does not abort the rest: matches the original BespokeStep/
97
+ * ProductReviews behaviour of keeping whatever succeeded before a later
98
+ * file failed, rather than discarding it. The caller decides how to present
99
+ * `errors` (e.g. one toast per failed filename).
100
+ */
101
+ export async function uploadMediaFiles(
102
+ files: File[],
103
+ params: Omit<UploadMediaFileParams, 'file'>
104
+ ): Promise<UploadMediaFilesResult> {
105
+ const assets: MediaAsset[] = []
106
+ const errors: { file: string; message: string }[] = []
107
+ for (const file of files) {
108
+ try {
109
+ assets.push(await uploadMediaFile({ ...params, file }))
110
+ } catch (err) {
111
+ errors.push({ file: file.name, message: err instanceof Error ? err.message : 'Upload failed' })
112
+ }
113
+ }
114
+ return { assets, errors }
115
+ }