@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.
- package/README.md +42 -15
- package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
- package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
- package/dist/components/index.d.mts +497 -0
- package/dist/components/index.d.ts +497 -0
- package/dist/components/index.js +4217 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +4194 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/server.d.mts +26 -0
- package/dist/config/server.d.ts +26 -0
- package/dist/config/server.js +38 -0
- package/dist/config/server.js.map +1 -0
- package/dist/config/server.mjs +13 -0
- package/dist/config/server.mjs.map +1 -0
- package/dist/hooks/index.d.mts +393 -160
- package/dist/hooks/index.d.ts +393 -160
- package/dist/hooks/index.js +2792 -2137
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +2734 -2121
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index-CNW_LINy.d.mts +578 -0
- package/dist/index-CNW_LINy.d.ts +578 -0
- package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
- package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
- package/dist/index-NkI10J_E.d.mts +40 -0
- package/dist/index-NkI10J_E.d.ts +40 -0
- package/dist/index.d.mts +20437 -103
- package/dist/index.d.ts +20437 -103
- package/dist/index.js +2111 -1802
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2006 -1786
- package/dist/index.mjs.map +1 -1
- package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
- package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
- package/dist/services/index.d.mts +908 -2
- package/dist/services/index.d.ts +908 -2
- package/dist/services/index.js +1492 -1686
- package/dist/services/index.js.map +1 -1
- package/dist/services/index.mjs +1431 -1670
- package/dist/services/index.mjs.map +1 -1
- package/package.json +46 -6
- package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
- package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
- package/src/components/bespoke/index.ts +3 -0
- package/src/components/booking/SlotPicker.tsx +97 -0
- package/src/components/booking/index.ts +1 -0
- package/src/components/cart/CartItem.tsx +81 -0
- package/src/components/cart/CartSidebar.tsx +115 -0
- package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
- package/src/components/cart/index.ts +3 -0
- package/src/components/index.ts +8 -0
- package/src/components/orders/DeliverableReviewer.tsx +139 -0
- package/src/components/orders/index.ts +1 -0
- package/src/components/product/ProductCard.tsx +350 -0
- package/src/components/product/ProductGrid.tsx +84 -0
- package/src/components/product/index.ts +2 -0
- package/src/components/shared/AddressAutocomplete.tsx +212 -0
- package/src/components/shared/AnalyticsScripts.tsx +56 -0
- package/src/components/shared/CookieConsentBanner.tsx +98 -0
- package/src/components/shared/ImageEditorModal.tsx +1066 -0
- package/src/components/shared/ImageViewerModal.tsx +598 -0
- package/src/components/shared/LucideReactIcon.tsx +52 -0
- package/src/components/shared/MapPinDrop.tsx +101 -0
- package/src/components/shared/MultiSelectPicker.tsx +193 -0
- package/src/components/shared/OfflineBanner.tsx +46 -0
- package/src/components/shared/SearchableInput.tsx +153 -0
- package/src/components/shared/SearchablePopover.tsx +166 -0
- package/src/components/shared/SmartImage.tsx +311 -0
- package/src/components/shared/TruncatedValue.tsx +27 -0
- package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
- package/src/components/shared/index.ts +15 -0
- package/src/components/skeletons/CardSkeleton.tsx +26 -0
- package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
- package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
- package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
- package/src/components/skeletons/TableSkeleton.tsx +42 -0
- package/src/components/skeletons/index.ts +5 -0
- package/src/config/index.ts +22 -4
- package/src/config/server.ts +45 -0
- package/src/context/sdk-context.tsx +5 -2
- package/src/generated/auth-openapi.ts +2935 -0
- package/src/generated/cart-openapi.ts +652 -0
- package/src/generated/cms-openapi.ts +1668 -0
- package/src/generated/crm-openapi.ts +986 -0
- package/src/generated/graphql.ts +126 -0
- package/src/generated/index.ts +11 -0
- package/src/generated/media-openapi.ts +613 -0
- package/src/generated/notification-openapi.ts +862 -0
- package/src/generated/payment-openapi.ts +2713 -0
- package/src/generated/store-openapi.ts +9510 -0
- package/src/graphql/operations/collections.graphql +32 -0
- package/src/graphql/operations/products.graphql +197 -0
- package/src/graphql/operations/wishlist.graphql +25 -0
- package/src/hooks/index.ts +9 -2
- package/src/hooks/use-auth.ts +163 -69
- package/src/hooks/use-bespoke-availability.ts +39 -22
- package/src/hooks/use-business-config.ts +51 -0
- package/src/hooks/use-cart.ts +310 -238
- package/src/hooks/use-cms.ts +107 -127
- package/src/hooks/use-collections.ts +35 -55
- package/src/hooks/use-consultations.ts +9 -136
- package/src/hooks/use-countries.ts +19 -28
- package/src/hooks/use-currencies.ts +8 -20
- package/src/hooks/use-customer-address.ts +64 -0
- package/src/hooks/use-feedback.ts +34 -0
- package/src/hooks/use-languages.ts +26 -39
- package/src/hooks/use-media-upload.ts +23 -0
- package/src/hooks/use-notification-stream.ts +88 -0
- package/src/hooks/use-notifications.ts +163 -0
- package/src/hooks/use-order-stream.ts +29 -29
- package/src/hooks/use-orders.ts +91 -73
- package/src/hooks/use-payment.ts +116 -84
- package/src/hooks/use-products.ts +65 -269
- package/src/hooks/use-push-notifications.ts +122 -0
- package/src/hooks/use-reviews.ts +175 -0
- package/src/hooks/use-service-booking.ts +109 -132
- package/src/hooks/use-states.ts +16 -30
- package/src/hooks/use-wishlist.ts +44 -69
- package/src/index.ts +15 -11
- package/src/services/auth.service.ts +422 -132
- package/src/services/bespoke.service.ts +69 -10
- package/src/services/cart.service.ts +155 -186
- package/src/services/cms.service.ts +41 -50
- package/src/services/collection.service.ts +25 -63
- package/src/services/config.service.ts +10 -21
- package/src/services/consultation.service.ts +36 -93
- package/src/services/feedback.service.ts +76 -0
- package/src/services/index.ts +7 -1
- package/src/services/legal.service.ts +62 -0
- package/src/services/logistics.service.ts +235 -53
- package/src/services/media.service.ts +115 -0
- package/src/services/notifications.service.ts +204 -20
- package/src/services/order.service.ts +498 -127
- package/src/services/payment.service.ts +149 -100
- package/src/services/product.service.ts +157 -814
- package/src/services/review.service.ts +211 -0
- package/src/services/service-booking.service.ts +198 -99
- package/src/services/wishlist.service.ts +27 -74
- package/src/types/auth/index.ts +161 -18
- package/src/types/business/index.ts +9 -1
- package/src/types/cms/index.ts +91 -6
- package/src/types/cms/legal.ts +18 -0
- package/src/types/consultations/index.ts +60 -68
- package/src/types/index.ts +5 -3
- package/src/types/media/index.ts +35 -3
- package/src/types/payments/index.ts +80 -16
- package/src/types/reviews/index.ts +116 -0
- package/src/types/services/index.ts +113 -38
- package/src/types/store/checkout.ts +53 -0
- package/src/types/store/index.ts +273 -52
- package/src/types/store/invoice.ts +31 -0
- package/src/types/store/shipping.ts +84 -3
- package/src/types/ui/index.ts +1 -1
- package/src/utils/checkout.ts +19 -3
- package/src/utils/cookieConsent.ts +34 -0
- package/src/utils/format.ts +408 -0
- package/src/utils/geolocation.ts +134 -0
- package/src/utils/graphql-url.ts +47 -0
- package/src/utils/guest-session.ts +59 -0
- package/src/utils/http-client.ts +147 -0
- package/src/utils/index.ts +11 -4
- package/src/utils/status-colors.ts +79 -0
- package/src/utils/token-refresh.ts +115 -0
- package/src/utils/validators.ts +13 -0
- package/src/utils/youtube.ts +18 -0
- package/dist/index-C5YO9gTl.d.mts +0 -507
- package/dist/index-C5YO9gTl.d.ts +0 -507
- package/dist/index-CefG7_xq.d.mts +0 -793
- package/dist/index-DeHCf3zI.d.ts +0 -793
- package/src/graphql/collections.graphql.ts +0 -48
- package/src/graphql/index.ts +0 -7
- package/src/graphql/products.graphql.ts +0 -860
- package/src/graphql/wishlist.graphql.ts +0 -34
- package/src/hooks/use-consultation-stream.ts +0 -142
- 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 {
|
|
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
|
-
|
|
29
|
-
origin_address: RateAddress
|
|
34
|
+
cart_id: string
|
|
30
35
|
destination_address: RateAddress
|
|
31
|
-
currency
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|