@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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review and Comment Service
|
|
3
|
+
*
|
|
4
|
+
* Handles review and comment operations using REST API.
|
|
5
|
+
* Stateless - all configuration passed as parameters.
|
|
6
|
+
*
|
|
7
|
+
* REAL BUG FIXED (2026-09-21): every URL here hardcoded a literal `/api/v1`
|
|
8
|
+
* prefix AND used a completely different path shape than the real
|
|
9
|
+
* kasuvia-crm-go backend (`kasuvia-crm-go/internal/api/v1/endpoints/
|
|
10
|
+
* reviews.go` + `admin_reviews.go`) — e.g. `POST /api/v1/reviews/comments`
|
|
11
|
+
* vs the real `POST /api/v1/comments`, `GET /api/v1/reviews/products/{id}`
|
|
12
|
+
* vs the real `GET /api/v1/products/{id}/reviews`. Every function 404'd.
|
|
13
|
+
* `config.backendUrls.crm` already includes `/api/v1` (same convention as
|
|
14
|
+
* every other backendUrls.* entry — see payment.service.ts/order.service.ts)
|
|
15
|
+
* so the hardcoded prefix also risked doubling to `/api/v1/api/v1/...` once
|
|
16
|
+
* KASUVIA_CRM_URL was actually set.
|
|
17
|
+
*/
|
|
18
|
+
import type { SdkConfig } from '../config'
|
|
19
|
+
import type {
|
|
20
|
+
Review, Comment, ReviewCreate, ReviewUpdate, CommentCreate, CommentUpdate,
|
|
21
|
+
MerchantReplyCreate, ReviewModerationAction, CommentModerationAction,
|
|
22
|
+
ProductReviewsResponse, ProductCommentsResponse
|
|
23
|
+
} from '../types/reviews'
|
|
24
|
+
import { apiRequest, JSON_HEADERS, type ApiRequestOptions } from '../utils/http-client'
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
Review, Comment, ReviewCreate, ReviewUpdate, CommentCreate, CommentUpdate,
|
|
28
|
+
MerchantReplyCreate, ReviewModerationAction, CommentModerationAction,
|
|
29
|
+
ProductReviewsResponse, ProductCommentsResponse
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This service takes its identity from `config.authToken`/`config.
|
|
34
|
+
* storefrontApiKey`/`config.businessId` directly (unlike every other SDK
|
|
35
|
+
* service, which takes an explicit `accessToken` parameter) - preserved as-
|
|
36
|
+
* is here since callers (use-reviews.ts) already depend on this exact
|
|
37
|
+
* calling convention; only the underlying transport moved onto the shared
|
|
38
|
+
* client.
|
|
39
|
+
*/
|
|
40
|
+
function fetchWithConfig<T>(url: string, config: SdkConfig, options: ApiRequestOptions = {}): Promise<T> {
|
|
41
|
+
const headers: Record<string, string> = {
|
|
42
|
+
...JSON_HEADERS,
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
...(options.headers as Record<string, string> | undefined),
|
|
45
|
+
}
|
|
46
|
+
if (config.storefrontApiKey) headers['X-Storefront-Key'] = config.storefrontApiKey
|
|
47
|
+
if (config.businessId) headers['X-Business-ID'] = config.businessId
|
|
48
|
+
if (config.authToken) headers['Authorization'] = `Bearer ${config.authToken}`
|
|
49
|
+
|
|
50
|
+
return apiRequest<T>(url, { ...options, headers, fallbackErrorMessage: options.fallbackErrorMessage ?? 'Request failed' })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a new review
|
|
55
|
+
*/
|
|
56
|
+
export async function createReview(
|
|
57
|
+
reviewData: ReviewCreate,
|
|
58
|
+
config: SdkConfig
|
|
59
|
+
): Promise<Review> {
|
|
60
|
+
const url = `${config.backendUrls.crm}/reviews`
|
|
61
|
+
return fetchWithConfig<Review>(url, config, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
body: JSON.stringify(reviewData),
|
|
64
|
+
fallbackErrorMessage: 'Failed to create review',
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Update an existing review
|
|
70
|
+
*/
|
|
71
|
+
export async function updateReview(
|
|
72
|
+
reviewId: string,
|
|
73
|
+
reviewData: ReviewUpdate,
|
|
74
|
+
config: SdkConfig
|
|
75
|
+
): Promise<Review> {
|
|
76
|
+
const url = `${config.backendUrls.crm}/reviews/${reviewId}`
|
|
77
|
+
return fetchWithConfig<Review>(url, config, {
|
|
78
|
+
method: 'PUT',
|
|
79
|
+
body: JSON.stringify(reviewData),
|
|
80
|
+
fallbackErrorMessage: 'Failed to update review',
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get paginated reviews for a product
|
|
86
|
+
*/
|
|
87
|
+
export async function getProductReviews(
|
|
88
|
+
productId: string,
|
|
89
|
+
params: { page?: number; page_size?: number; moderation_status?: string } = {},
|
|
90
|
+
config: SdkConfig
|
|
91
|
+
): Promise<ProductReviewsResponse> {
|
|
92
|
+
const queryParams = new URLSearchParams()
|
|
93
|
+
if (params.page) queryParams.append('page', params.page.toString())
|
|
94
|
+
if (params.page_size) queryParams.append('page_size', params.page_size.toString())
|
|
95
|
+
if (params.moderation_status) queryParams.append('moderation_status', params.moderation_status)
|
|
96
|
+
|
|
97
|
+
const url = `${config.backendUrls.crm}/products/${productId}/reviews?${queryParams.toString()}`
|
|
98
|
+
return fetchWithConfig<ProductReviewsResponse>(url, config, { fallbackErrorMessage: 'Failed to fetch product reviews' })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a new comment
|
|
103
|
+
*/
|
|
104
|
+
export async function createComment(
|
|
105
|
+
commentData: CommentCreate,
|
|
106
|
+
config: SdkConfig
|
|
107
|
+
): Promise<Comment> {
|
|
108
|
+
const url = `${config.backendUrls.crm}/comments`
|
|
109
|
+
return fetchWithConfig<Comment>(url, config, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
body: JSON.stringify(commentData),
|
|
112
|
+
fallbackErrorMessage: 'Failed to create comment',
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Update an existing comment
|
|
118
|
+
*/
|
|
119
|
+
export async function updateComment(
|
|
120
|
+
commentId: string,
|
|
121
|
+
commentData: CommentUpdate,
|
|
122
|
+
config: SdkConfig
|
|
123
|
+
): Promise<Comment> {
|
|
124
|
+
const url = `${config.backendUrls.crm}/comments/${commentId}`
|
|
125
|
+
return fetchWithConfig<Comment>(url, config, {
|
|
126
|
+
method: 'PUT',
|
|
127
|
+
body: JSON.stringify(commentData),
|
|
128
|
+
fallbackErrorMessage: 'Failed to update comment',
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get paginated comments for a product
|
|
134
|
+
*/
|
|
135
|
+
export async function getProductComments(
|
|
136
|
+
productId: string,
|
|
137
|
+
params: { page?: number; page_size?: number; moderation_status?: string } = {},
|
|
138
|
+
config: SdkConfig
|
|
139
|
+
): Promise<ProductCommentsResponse> {
|
|
140
|
+
const queryParams = new URLSearchParams()
|
|
141
|
+
if (params.page) queryParams.append('page', params.page.toString())
|
|
142
|
+
if (params.page_size) queryParams.append('page_size', params.page_size.toString())
|
|
143
|
+
if (params.moderation_status) queryParams.append('moderation_status', params.moderation_status)
|
|
144
|
+
|
|
145
|
+
const url = `${config.backendUrls.crm}/products/${productId}/comments?${queryParams.toString()}`
|
|
146
|
+
return fetchWithConfig<ProductCommentsResponse>(url, config, { fallbackErrorMessage: 'Failed to fetch product comments' })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get pending reviews for moderation (merchant/staff only)
|
|
151
|
+
*/
|
|
152
|
+
export async function getPendingReviews(config: SdkConfig): Promise<Review[]> {
|
|
153
|
+
const url = `${config.backendUrls.crm}/admin/reviews/pending`
|
|
154
|
+
return fetchWithConfig<Review[]>(url, config, { fallbackErrorMessage: 'Failed to fetch pending reviews' })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get pending comments for moderation (merchant/staff only)
|
|
159
|
+
*/
|
|
160
|
+
export async function getPendingComments(config: SdkConfig): Promise<Comment[]> {
|
|
161
|
+
const url = `${config.backendUrls.crm}/admin/comments/pending`
|
|
162
|
+
return fetchWithConfig<Comment[]>(url, config, { fallbackErrorMessage: 'Failed to fetch pending comments' })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Moderate a review (approve/reject) - merchant/staff only
|
|
167
|
+
*/
|
|
168
|
+
export async function moderateReview(
|
|
169
|
+
reviewId: string,
|
|
170
|
+
action: ReviewModerationAction,
|
|
171
|
+
config: SdkConfig
|
|
172
|
+
): Promise<Review> {
|
|
173
|
+
const url = `${config.backendUrls.crm}/admin/reviews/${reviewId}/moderate`
|
|
174
|
+
return fetchWithConfig<Review>(url, config, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
body: JSON.stringify(action),
|
|
177
|
+
fallbackErrorMessage: 'Failed to moderate review',
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Moderate a comment (approve/reject) - merchant/staff only
|
|
183
|
+
*/
|
|
184
|
+
export async function moderateComment(
|
|
185
|
+
commentId: string,
|
|
186
|
+
action: CommentModerationAction,
|
|
187
|
+
config: SdkConfig
|
|
188
|
+
): Promise<Comment> {
|
|
189
|
+
const url = `${config.backendUrls.crm}/admin/comments/${commentId}/moderate`
|
|
190
|
+
return fetchWithConfig<Comment>(url, config, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
body: JSON.stringify(action),
|
|
193
|
+
fallbackErrorMessage: 'Failed to moderate comment',
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Post a merchant reply to a review - merchant/staff only
|
|
199
|
+
*/
|
|
200
|
+
export async function merchantReplyToReview(
|
|
201
|
+
reviewId: string,
|
|
202
|
+
replyData: MerchantReplyCreate,
|
|
203
|
+
config: SdkConfig
|
|
204
|
+
): Promise<Review> {
|
|
205
|
+
const url = `${config.backendUrls.crm}/admin/reviews/${reviewId}/reply`
|
|
206
|
+
return fetchWithConfig<Review>(url, config, {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
body: JSON.stringify(replyData),
|
|
209
|
+
fallbackErrorMessage: 'Failed to post merchant reply',
|
|
210
|
+
})
|
|
211
|
+
}
|
|
@@ -1,148 +1,245 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Service Booking Service
|
|
3
|
-
*
|
|
4
|
-
* Handles service appointments and
|
|
5
|
-
* Stateless - all configuration passed as parameters.
|
|
3
|
+
*
|
|
4
|
+
* Handles service appointments (bookings) and their milestone/escrow
|
|
5
|
+
* sub-resource. Stateless - all configuration passed as parameters.
|
|
6
|
+
*
|
|
7
|
+
* REAL BUGS FIXED (2026-09-20) — every function in this file was rewritten
|
|
8
|
+
* against the real backend (kasuvia-store-management/app/api/v1/endpoints/
|
|
9
|
+
* service/bookings.py, app/schemas/service.py). Before this fix:
|
|
10
|
+
* - getAvailableSlots called a nonexistent route (/service/available-slots/
|
|
11
|
+
* {id}) with the wrong params (date_from/date_to) — the real route is
|
|
12
|
+
* GET /catalog/products/{id}/available-slots?date=..., a single date.
|
|
13
|
+
* - bookAppointment POSTed to a nonexistent route (/service/appointments/)
|
|
14
|
+
* — the real route is POST /service/bookings/schedule.
|
|
15
|
+
* - getAppointments typed its result as a plain array, but the real
|
|
16
|
+
* response is the standard Kasuvia paginated envelope
|
|
17
|
+
* ({results,count,page,page_size,total_pages}) — this was a CONFIRMED
|
|
18
|
+
* LIVE CRASH: DashboardClient.tsx/DashboardAppointments.tsx called
|
|
19
|
+
* .length/.filter() directly on the envelope object.
|
|
20
|
+
* - cancelAppointment sent DELETE /service/bookings/{id} — that route is
|
|
21
|
+
* owner/staff-only (require_permission("services:manage")), so a
|
|
22
|
+
* customer got a 403; the real customer route is POST .../{id}/cancel.
|
|
23
|
+
* It also never checked response.ok at all, silently swallowing failures.
|
|
24
|
+
* - getDeliverables/approveDeliverable/requestRevision referenced a
|
|
25
|
+
* "/service/projects/{order_id}/deliverables/..." resource that does not
|
|
26
|
+
* exist anywhere in the codebase — replaced with the real milestone
|
|
27
|
+
* endpoints (a different resource: escrow-style payment milestones on
|
|
28
|
+
* a booking, not free-form deliverable files).
|
|
29
|
+
* - No coverage existed for reschedule, dispute, or buyer-confirm — all
|
|
30
|
+
* real, customer-facing routes.
|
|
6
31
|
*/
|
|
7
32
|
import type { SdkConfig } from '../config'
|
|
8
33
|
import type {
|
|
9
34
|
ServiceAppointment,
|
|
10
35
|
ServiceAppointmentCreatePayload,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
36
|
+
CancelAppointmentPayload,
|
|
37
|
+
RescheduleAppointmentPayload,
|
|
38
|
+
DisputeAppointmentPayload,
|
|
39
|
+
ConfirmAppointmentPayload,
|
|
40
|
+
AvailableSlotsResponse,
|
|
41
|
+
ServiceMilestone,
|
|
42
|
+
ApproveMilestonePayload,
|
|
14
43
|
} from '../types/services'
|
|
44
|
+
import type { components } from '../generated/store-openapi'
|
|
45
|
+
import { apiRequest, storefrontHeaders, authHeaders, JSON_HEADERS } from '../utils/http-client'
|
|
15
46
|
|
|
47
|
+
// ─── Codegen drift-proofs (openapi/README.md — regenerate via `pnpm run codegen:rest`) ─
|
|
48
|
+
// Unlike order.service.ts, this file's hand-written types (types/services/
|
|
49
|
+
// index.ts) were already verified field-for-field correct against the real
|
|
50
|
+
// backend Pydantic schemas (see that file's own header comment) - full
|
|
51
|
+
// replacement would only add risk for zero gain. These type-level
|
|
52
|
+
// assignments instead prove the match at compile time: if the real backend
|
|
53
|
+
// schema ever adds/renames/retypes a field these three interfaces declare,
|
|
54
|
+
// the corresponding line below fails to compile immediately (at `pnpm run
|
|
55
|
+
// codegen:rest` + `tsc` time), the same drift-detection value Workstream 1
|
|
56
|
+
// already proved for the GraphQL side, without touching working code.
|
|
57
|
+
// (One real drift WAS found and fixed this way: ServiceMilestone.amount
|
|
58
|
+
// was typed `number` but the real field is a Decimal-as-string.)
|
|
59
|
+
type _ServiceAppointmentMatchesSchema = ServiceAppointment extends components['schemas']['ServiceBookingResponse'] ? true : never
|
|
60
|
+
type _AvailableSlotsResponseMatchesSchema = AvailableSlotsResponse extends components['schemas']['AvailableSlotsResponse'] ? true : never
|
|
61
|
+
type _ServiceMilestoneMatchesSchema = ServiceMilestone extends components['schemas']['ServiceMilestoneResponse'] ? true : never
|
|
62
|
+
const _serviceAppointmentCheck: _ServiceAppointmentMatchesSchema = true
|
|
63
|
+
const _availableSlotsCheck: _AvailableSlotsResponseMatchesSchema = true
|
|
64
|
+
const _serviceMilestoneCheck: _ServiceMilestoneMatchesSchema = true
|
|
65
|
+
void _serviceAppointmentCheck
|
|
66
|
+
void _availableSlotsCheck
|
|
67
|
+
void _serviceMilestoneCheck
|
|
68
|
+
|
|
69
|
+
/** Standard Kasuvia pagination envelope — matches app/core/pagination.py's PaginatedResponse exactly. */
|
|
70
|
+
export interface ServiceBookingsPage {
|
|
71
|
+
results: ServiceAppointment[]
|
|
72
|
+
count: number
|
|
73
|
+
page: number
|
|
74
|
+
page_size: number
|
|
75
|
+
total_pages: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* GET /catalog/products/{product_id}/available-slots — real "what times are
|
|
80
|
+
* open" for a SERVICE product on one date. `serviceable: false` means the
|
|
81
|
+
* business has no/ambiguous location config for this product (check
|
|
82
|
+
* `reason`); `available_slots` is always empty in that case, not an error.
|
|
83
|
+
*/
|
|
16
84
|
export async function getAvailableSlots(
|
|
17
85
|
config: SdkConfig,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
): Promise<
|
|
22
|
-
const
|
|
23
|
-
|
|
86
|
+
productId: string,
|
|
87
|
+
date: string,
|
|
88
|
+
params?: { staffId?: string; locationId?: string; customerLatitude?: number; customerLongitude?: number }
|
|
89
|
+
): Promise<AvailableSlotsResponse> {
|
|
90
|
+
const query = new URLSearchParams({ date })
|
|
91
|
+
if (params?.staffId) query.set('staff_id', params.staffId)
|
|
92
|
+
if (params?.locationId) query.set('location_id', params.locationId)
|
|
93
|
+
if (params?.customerLatitude !== undefined) query.set('customer_latitude', String(params.customerLatitude))
|
|
94
|
+
if (params?.customerLongitude !== undefined) query.set('customer_longitude', String(params.customerLongitude))
|
|
95
|
+
|
|
96
|
+
return apiRequest<AvailableSlotsResponse>(
|
|
97
|
+
`${config.backendUrls.store}/catalog/products/${productId}/available-slots?${query.toString()}`,
|
|
24
98
|
{
|
|
25
|
-
headers:
|
|
26
|
-
|
|
27
|
-
"X-Business-ID": config.businessId,
|
|
28
|
-
},
|
|
99
|
+
headers: storefrontHeaders(config),
|
|
100
|
+
fallbackErrorMessage: 'Failed to get available slots',
|
|
29
101
|
}
|
|
30
102
|
)
|
|
31
|
-
if (!response.ok) throw new Error('Failed to get available slots')
|
|
32
|
-
return response.json()
|
|
33
103
|
}
|
|
34
104
|
|
|
105
|
+
/** POST /service/bookings/schedule */
|
|
35
106
|
export async function bookAppointment(
|
|
36
107
|
config: SdkConfig,
|
|
37
108
|
accessToken: string,
|
|
38
109
|
payload: ServiceAppointmentCreatePayload
|
|
39
110
|
): Promise<ServiceAppointment> {
|
|
40
|
-
|
|
111
|
+
return apiRequest<ServiceAppointment>(`${config.backendUrls.store}/service/bookings/schedule`, {
|
|
41
112
|
method: 'POST',
|
|
42
|
-
headers: {
|
|
43
|
-
'Content-Type': 'application/json',
|
|
44
|
-
"Authorization": `Bearer ${accessToken}`,
|
|
45
|
-
"X-Storefront-Key": config.storefrontApiKey,
|
|
46
|
-
"X-Business-ID": config.businessId,
|
|
47
|
-
},
|
|
113
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
48
114
|
body: JSON.stringify(payload),
|
|
115
|
+
fallbackErrorMessage: 'Failed to book appointment',
|
|
49
116
|
})
|
|
50
|
-
if (!response.ok) throw new Error('Failed to book appointment')
|
|
51
|
-
return response.json()
|
|
52
117
|
}
|
|
53
118
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
119
|
+
/** GET /service/bookings/my — paginated. */
|
|
120
|
+
export async function getAppointments(
|
|
121
|
+
config: SdkConfig,
|
|
122
|
+
accessToken: string,
|
|
123
|
+
params?: { status?: ServiceAppointment['status']; page?: number; page_size?: number }
|
|
124
|
+
): Promise<ServiceBookingsPage> {
|
|
125
|
+
const query = new URLSearchParams()
|
|
126
|
+
if (params?.status) query.set('status', params.status)
|
|
127
|
+
if (params?.page) query.set('page', String(params.page))
|
|
128
|
+
if (params?.page_size) query.set('page_size', String(params.page_size))
|
|
129
|
+
|
|
130
|
+
return apiRequest<ServiceBookingsPage>(`${config.backendUrls.store}/service/bookings/my?${query.toString()}`, {
|
|
131
|
+
headers: authHeaders(config, accessToken),
|
|
132
|
+
fallbackErrorMessage: 'Failed to get appointments',
|
|
61
133
|
})
|
|
62
|
-
if (!response.ok) throw new Error('Failed to get appointments')
|
|
63
|
-
return response.json()
|
|
64
134
|
}
|
|
65
135
|
|
|
136
|
+
/**
|
|
137
|
+
* GET /service/bookings/{id} — customer-reachable single-booking lookup,
|
|
138
|
+
* used by the paginated list's detail view (PATCH .../{id} is a separate,
|
|
139
|
+
* owner/staff-only route).
|
|
140
|
+
*
|
|
141
|
+
* REAL BUG FIXED (2026-09-22): this call shape was already correct, but the
|
|
142
|
+
* backend route it targeted didn't exist at all — `ServiceBookingRepository
|
|
143
|
+
* .get_by_id_and_business` was used internally by every merchant action
|
|
144
|
+
* (cancel/reschedule/dispute/assign/etc.) but never exposed as a customer
|
|
145
|
+
* GET route. Every real call to `AppointmentDetail.tsx` (via `useAppointment`)
|
|
146
|
+
* 404'd. Fixed by adding `GET /{booking_id}` to kasuvia-store-management's
|
|
147
|
+
* `service/bookings.py` (customer_id-scoped, same pattern as
|
|
148
|
+
* `customer_cancel_booking` — service bookings have no guest-checkout path,
|
|
149
|
+
* so no guest_email fallback is needed here unlike orders).
|
|
150
|
+
*/
|
|
66
151
|
export async function getAppointment(config: SdkConfig, accessToken: string, id: string): Promise<ServiceAppointment> {
|
|
67
|
-
|
|
68
|
-
headers:
|
|
69
|
-
|
|
70
|
-
"X-Storefront-Key": config.storefrontApiKey,
|
|
71
|
-
"X-Business-ID": config.businessId,
|
|
72
|
-
},
|
|
152
|
+
return apiRequest<ServiceAppointment>(`${config.backendUrls.store}/service/bookings/${id}`, {
|
|
153
|
+
headers: authHeaders(config, accessToken),
|
|
154
|
+
fallbackErrorMessage: 'Failed to get appointment',
|
|
73
155
|
})
|
|
74
|
-
if (!response.ok) throw new Error('Failed to get appointment')
|
|
75
|
-
return response.json()
|
|
76
156
|
}
|
|
77
157
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
158
|
+
/** POST /service/bookings/{id}/cancel — the real customer-cancel route (DELETE .../{id} is owner/staff-only). */
|
|
159
|
+
export async function cancelAppointment(
|
|
160
|
+
config: SdkConfig,
|
|
161
|
+
accessToken: string,
|
|
162
|
+
id: string,
|
|
163
|
+
payload?: CancelAppointmentPayload
|
|
164
|
+
): Promise<ServiceAppointment> {
|
|
165
|
+
return apiRequest<ServiceAppointment>(`${config.backendUrls.store}/service/bookings/${id}/cancel`, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
168
|
+
body: JSON.stringify(payload ?? {}),
|
|
169
|
+
fallbackErrorMessage: 'Failed to cancel appointment',
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** POST /service/bookings/{id}/reschedule — subject to the same cancellation_window_hours as a cancel. */
|
|
174
|
+
export async function rescheduleAppointment(
|
|
175
|
+
config: SdkConfig,
|
|
176
|
+
accessToken: string,
|
|
177
|
+
id: string,
|
|
178
|
+
payload: RescheduleAppointmentPayload
|
|
179
|
+
): Promise<ServiceAppointment> {
|
|
180
|
+
return apiRequest<ServiceAppointment>(`${config.backendUrls.store}/service/bookings/${id}/reschedule`, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
183
|
+
body: JSON.stringify(payload),
|
|
184
|
+
fallbackErrorMessage: 'Failed to reschedule appointment',
|
|
86
185
|
})
|
|
87
186
|
}
|
|
88
187
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
188
|
+
/** POST /service/bookings/{id}/dispute — freezes the escrow auto-release timer; requires a real reason. */
|
|
189
|
+
export async function disputeAppointment(
|
|
190
|
+
config: SdkConfig,
|
|
191
|
+
accessToken: string,
|
|
192
|
+
id: string,
|
|
193
|
+
payload: DisputeAppointmentPayload
|
|
194
|
+
): Promise<ServiceAppointment> {
|
|
195
|
+
return apiRequest<ServiceAppointment>(`${config.backendUrls.store}/service/bookings/${id}/dispute`, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
198
|
+
body: JSON.stringify(payload),
|
|
199
|
+
fallbackErrorMessage: 'Failed to open dispute',
|
|
96
200
|
})
|
|
97
|
-
if (!response.ok) throw new Error('Failed to get deliverables')
|
|
98
|
-
return response.json()
|
|
99
201
|
}
|
|
100
202
|
|
|
101
|
-
|
|
203
|
+
/** POST /service/bookings/{id}/confirm — buyer confirms the service was completed. */
|
|
204
|
+
export async function confirmAppointmentCompletion(
|
|
102
205
|
config: SdkConfig,
|
|
103
206
|
accessToken: string,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
): Promise<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return response.json()
|
|
207
|
+
id: string,
|
|
208
|
+
payload?: ConfirmAppointmentPayload
|
|
209
|
+
): Promise<ServiceAppointment> {
|
|
210
|
+
return apiRequest<ServiceAppointment>(`${config.backendUrls.store}/service/bookings/${id}/confirm`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
213
|
+
body: JSON.stringify(payload ?? {}),
|
|
214
|
+
fallbackErrorMessage: 'Failed to confirm completion',
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** GET /service/bookings/{id}/milestones — escrow-style payment milestones for a milestone-based booking. */
|
|
219
|
+
export async function getMilestones(config: SdkConfig, accessToken: string, bookingId: string): Promise<ServiceMilestone[]> {
|
|
220
|
+
return apiRequest<ServiceMilestone[]>(`${config.backendUrls.store}/service/bookings/${bookingId}/milestones`, {
|
|
221
|
+
headers: authHeaders(config, accessToken),
|
|
222
|
+
fallbackErrorMessage: 'Failed to get milestones',
|
|
223
|
+
})
|
|
122
224
|
}
|
|
123
225
|
|
|
124
|
-
|
|
226
|
+
/** POST /service/bookings/{id}/milestones/{milestone_id}/approve — buyer approves a submitted milestone, triggering escrow release. */
|
|
227
|
+
export async function approveMilestone(
|
|
125
228
|
config: SdkConfig,
|
|
126
229
|
accessToken: string,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
): Promise<
|
|
131
|
-
|
|
132
|
-
`${config.backendUrls.store}/service/
|
|
230
|
+
bookingId: string,
|
|
231
|
+
milestoneId: string,
|
|
232
|
+
payload?: ApproveMilestonePayload
|
|
233
|
+
): Promise<ServiceMilestone> {
|
|
234
|
+
return apiRequest<ServiceMilestone>(
|
|
235
|
+
`${config.backendUrls.store}/service/bookings/${bookingId}/milestones/${milestoneId}/approve`,
|
|
133
236
|
{
|
|
134
237
|
method: 'POST',
|
|
135
|
-
headers: {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"X-Storefront-Key": config.storefrontApiKey,
|
|
139
|
-
"X-Business-ID": config.businessId,
|
|
140
|
-
},
|
|
141
|
-
body: JSON.stringify({ approved: false, feedback }),
|
|
238
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
239
|
+
body: JSON.stringify(payload ?? {}),
|
|
240
|
+
fallbackErrorMessage: 'Failed to approve milestone',
|
|
142
241
|
}
|
|
143
242
|
)
|
|
144
|
-
if (!response.ok) throw new Error('Failed to request revision')
|
|
145
|
-
return response.json()
|
|
146
243
|
}
|
|
147
244
|
|
|
148
245
|
// Service object export for backward compatibility
|
|
@@ -152,7 +249,9 @@ export const serviceBookingService = {
|
|
|
152
249
|
getAppointments,
|
|
153
250
|
getAppointment,
|
|
154
251
|
cancelAppointment,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
252
|
+
rescheduleAppointment,
|
|
253
|
+
disputeAppointment,
|
|
254
|
+
confirmAppointmentCompletion,
|
|
255
|
+
getMilestones,
|
|
256
|
+
approveMilestone,
|
|
158
257
|
}
|