@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,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
|
-
|
|
12
|
-
headers:
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
*
|
|
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
|
|
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<
|
|
24
|
-
if (!guestSessionId)
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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<
|
|
88
|
-
if (!guestSessionId)
|
|
100
|
+
): Promise<CartResponse> {
|
|
101
|
+
if (!guestSessionId) throw new Error('Guest session ID is required to update guest cart item');
|
|
89
102
|
|
|
90
|
-
const
|
|
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
|
-
|
|
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<
|
|
113
|
-
if (!guestSessionId)
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
if (!guestSessionId) throw new Error('Guest session ID is required to remove from guest cart');
|
|
114
136
|
|
|
115
|
-
await
|
|
137
|
+
await apiRequest(`${config.backendUrls.cart}/cart/${itemId}`, {
|
|
116
138
|
method: 'DELETE',
|
|
117
|
-
headers:
|
|
118
|
-
|
|
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)
|
|
148
|
+
if (!guestSessionId) throw new Error('Guest session ID is required to clear guest cart');
|
|
132
149
|
|
|
133
|
-
await
|
|
150
|
+
await apiRequest(`${config.backendUrls.cart}/cart/clear`, {
|
|
134
151
|
method: 'DELETE',
|
|
135
|
-
headers:
|
|
136
|
-
|
|
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
|
-
|
|
162
|
+
throw new Error('Access token is required to fetch authenticated cart');
|
|
149
163
|
}
|
|
150
|
-
const
|
|
151
|
-
headers:
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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<
|
|
231
|
-
await
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
await apiRequest(`${config.backendUrls.cart}/cart/${itemId}`, {
|
|
232
240
|
method: 'DELETE',
|
|
233
|
-
headers:
|
|
234
|
-
|
|
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
|
|
250
|
+
await apiRequest(`${config.backendUrls.cart}/cart/clear`, {
|
|
247
251
|
method: 'DELETE',
|
|
248
|
-
headers:
|
|
249
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
*
|
|
308
|
+
* Helper to get a guest cart's product type (for type-mismatch checks before adding an item).
|
|
344
309
|
*/
|
|
345
|
-
export function getGuestCartProductType():
|
|
346
|
-
return
|
|
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
|
}
|