@kasuvia/sdk 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +42 -15
  2. package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
  3. package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
  4. package/dist/components/index.d.mts +497 -0
  5. package/dist/components/index.d.ts +497 -0
  6. package/dist/components/index.js +4217 -0
  7. package/dist/components/index.js.map +1 -0
  8. package/dist/components/index.mjs +4194 -0
  9. package/dist/components/index.mjs.map +1 -0
  10. package/dist/config/server.d.mts +26 -0
  11. package/dist/config/server.d.ts +26 -0
  12. package/dist/config/server.js +38 -0
  13. package/dist/config/server.js.map +1 -0
  14. package/dist/config/server.mjs +13 -0
  15. package/dist/config/server.mjs.map +1 -0
  16. package/dist/hooks/index.d.mts +393 -160
  17. package/dist/hooks/index.d.ts +393 -160
  18. package/dist/hooks/index.js +2792 -2137
  19. package/dist/hooks/index.js.map +1 -1
  20. package/dist/hooks/index.mjs +2734 -2121
  21. package/dist/hooks/index.mjs.map +1 -1
  22. package/dist/index-CNW_LINy.d.mts +578 -0
  23. package/dist/index-CNW_LINy.d.ts +578 -0
  24. package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
  25. package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
  26. package/dist/index-NkI10J_E.d.mts +40 -0
  27. package/dist/index-NkI10J_E.d.ts +40 -0
  28. package/dist/index.d.mts +20437 -103
  29. package/dist/index.d.ts +20437 -103
  30. package/dist/index.js +2111 -1802
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +2006 -1786
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
  35. package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
  36. package/dist/services/index.d.mts +908 -2
  37. package/dist/services/index.d.ts +908 -2
  38. package/dist/services/index.js +1492 -1686
  39. package/dist/services/index.js.map +1 -1
  40. package/dist/services/index.mjs +1431 -1670
  41. package/dist/services/index.mjs.map +1 -1
  42. package/package.json +46 -6
  43. package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
  44. package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
  45. package/src/components/bespoke/index.ts +3 -0
  46. package/src/components/booking/SlotPicker.tsx +97 -0
  47. package/src/components/booking/index.ts +1 -0
  48. package/src/components/cart/CartItem.tsx +81 -0
  49. package/src/components/cart/CartSidebar.tsx +115 -0
  50. package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
  51. package/src/components/cart/index.ts +3 -0
  52. package/src/components/index.ts +8 -0
  53. package/src/components/orders/DeliverableReviewer.tsx +139 -0
  54. package/src/components/orders/index.ts +1 -0
  55. package/src/components/product/ProductCard.tsx +350 -0
  56. package/src/components/product/ProductGrid.tsx +84 -0
  57. package/src/components/product/index.ts +2 -0
  58. package/src/components/shared/AddressAutocomplete.tsx +212 -0
  59. package/src/components/shared/AnalyticsScripts.tsx +56 -0
  60. package/src/components/shared/CookieConsentBanner.tsx +98 -0
  61. package/src/components/shared/ImageEditorModal.tsx +1066 -0
  62. package/src/components/shared/ImageViewerModal.tsx +598 -0
  63. package/src/components/shared/LucideReactIcon.tsx +52 -0
  64. package/src/components/shared/MapPinDrop.tsx +101 -0
  65. package/src/components/shared/MultiSelectPicker.tsx +193 -0
  66. package/src/components/shared/OfflineBanner.tsx +46 -0
  67. package/src/components/shared/SearchableInput.tsx +153 -0
  68. package/src/components/shared/SearchablePopover.tsx +166 -0
  69. package/src/components/shared/SmartImage.tsx +311 -0
  70. package/src/components/shared/TruncatedValue.tsx +27 -0
  71. package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
  72. package/src/components/shared/index.ts +15 -0
  73. package/src/components/skeletons/CardSkeleton.tsx +26 -0
  74. package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
  75. package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
  76. package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
  77. package/src/components/skeletons/TableSkeleton.tsx +42 -0
  78. package/src/components/skeletons/index.ts +5 -0
  79. package/src/config/index.ts +22 -4
  80. package/src/config/server.ts +45 -0
  81. package/src/context/sdk-context.tsx +5 -2
  82. package/src/generated/auth-openapi.ts +2935 -0
  83. package/src/generated/cart-openapi.ts +652 -0
  84. package/src/generated/cms-openapi.ts +1668 -0
  85. package/src/generated/crm-openapi.ts +986 -0
  86. package/src/generated/graphql.ts +126 -0
  87. package/src/generated/index.ts +11 -0
  88. package/src/generated/media-openapi.ts +613 -0
  89. package/src/generated/notification-openapi.ts +862 -0
  90. package/src/generated/payment-openapi.ts +2713 -0
  91. package/src/generated/store-openapi.ts +9510 -0
  92. package/src/graphql/operations/collections.graphql +32 -0
  93. package/src/graphql/operations/products.graphql +197 -0
  94. package/src/graphql/operations/wishlist.graphql +25 -0
  95. package/src/hooks/index.ts +9 -2
  96. package/src/hooks/use-auth.ts +163 -69
  97. package/src/hooks/use-bespoke-availability.ts +39 -22
  98. package/src/hooks/use-business-config.ts +51 -0
  99. package/src/hooks/use-cart.ts +310 -238
  100. package/src/hooks/use-cms.ts +107 -127
  101. package/src/hooks/use-collections.ts +35 -55
  102. package/src/hooks/use-consultations.ts +9 -136
  103. package/src/hooks/use-countries.ts +19 -28
  104. package/src/hooks/use-currencies.ts +8 -20
  105. package/src/hooks/use-customer-address.ts +64 -0
  106. package/src/hooks/use-feedback.ts +34 -0
  107. package/src/hooks/use-languages.ts +26 -39
  108. package/src/hooks/use-media-upload.ts +23 -0
  109. package/src/hooks/use-notification-stream.ts +88 -0
  110. package/src/hooks/use-notifications.ts +163 -0
  111. package/src/hooks/use-order-stream.ts +29 -29
  112. package/src/hooks/use-orders.ts +91 -73
  113. package/src/hooks/use-payment.ts +116 -84
  114. package/src/hooks/use-products.ts +65 -269
  115. package/src/hooks/use-push-notifications.ts +122 -0
  116. package/src/hooks/use-reviews.ts +175 -0
  117. package/src/hooks/use-service-booking.ts +109 -132
  118. package/src/hooks/use-states.ts +16 -30
  119. package/src/hooks/use-wishlist.ts +44 -69
  120. package/src/index.ts +15 -11
  121. package/src/services/auth.service.ts +422 -132
  122. package/src/services/bespoke.service.ts +69 -10
  123. package/src/services/cart.service.ts +155 -186
  124. package/src/services/cms.service.ts +41 -50
  125. package/src/services/collection.service.ts +25 -63
  126. package/src/services/config.service.ts +10 -21
  127. package/src/services/consultation.service.ts +36 -93
  128. package/src/services/feedback.service.ts +76 -0
  129. package/src/services/index.ts +7 -1
  130. package/src/services/legal.service.ts +62 -0
  131. package/src/services/logistics.service.ts +235 -53
  132. package/src/services/media.service.ts +115 -0
  133. package/src/services/notifications.service.ts +204 -20
  134. package/src/services/order.service.ts +498 -127
  135. package/src/services/payment.service.ts +149 -100
  136. package/src/services/product.service.ts +157 -814
  137. package/src/services/review.service.ts +211 -0
  138. package/src/services/service-booking.service.ts +198 -99
  139. package/src/services/wishlist.service.ts +27 -74
  140. package/src/types/auth/index.ts +161 -18
  141. package/src/types/business/index.ts +9 -1
  142. package/src/types/cms/index.ts +91 -6
  143. package/src/types/cms/legal.ts +18 -0
  144. package/src/types/consultations/index.ts +60 -68
  145. package/src/types/index.ts +5 -3
  146. package/src/types/media/index.ts +35 -3
  147. package/src/types/payments/index.ts +80 -16
  148. package/src/types/reviews/index.ts +116 -0
  149. package/src/types/services/index.ts +113 -38
  150. package/src/types/store/checkout.ts +53 -0
  151. package/src/types/store/index.ts +273 -52
  152. package/src/types/store/invoice.ts +31 -0
  153. package/src/types/store/shipping.ts +84 -3
  154. package/src/types/ui/index.ts +1 -1
  155. package/src/utils/checkout.ts +19 -3
  156. package/src/utils/cookieConsent.ts +34 -0
  157. package/src/utils/format.ts +408 -0
  158. package/src/utils/geolocation.ts +134 -0
  159. package/src/utils/graphql-url.ts +47 -0
  160. package/src/utils/guest-session.ts +59 -0
  161. package/src/utils/http-client.ts +147 -0
  162. package/src/utils/index.ts +11 -4
  163. package/src/utils/status-colors.ts +79 -0
  164. package/src/utils/token-refresh.ts +115 -0
  165. package/src/utils/validators.ts +13 -0
  166. package/src/utils/youtube.ts +18 -0
  167. package/dist/index-C5YO9gTl.d.mts +0 -507
  168. package/dist/index-C5YO9gTl.d.ts +0 -507
  169. package/dist/index-CefG7_xq.d.mts +0 -793
  170. package/dist/index-DeHCf3zI.d.ts +0 -793
  171. package/src/graphql/collections.graphql.ts +0 -48
  172. package/src/graphql/index.ts +0 -7
  173. package/src/graphql/products.graphql.ts +0 -860
  174. package/src/graphql/wishlist.graphql.ts +0 -34
  175. package/src/hooks/use-consultation-stream.ts +0 -142
  176. package/src/types/bespoke/index.ts +0 -39
@@ -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 deliverables.
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
- AvailableSlot,
12
- ServiceDeliverable,
13
- DeliverableApprovalPayload,
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
- product_id: string,
19
- date_from: string,
20
- date_to: string
21
- ): Promise<AvailableSlot[]> {
22
- const response = await fetch(
23
- `${config.backendUrls.store}/service/available-slots/${product_id}?date_from=${date_from}&date_to=${date_to}`,
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
- "X-Storefront-Key": config.storefrontApiKey,
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
- const response = await fetch(`${config.backendUrls.store}/service/appointments/`, {
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
- export async function getAppointments(config: SdkConfig, accessToken: string): Promise<ServiceAppointment[]> {
55
- const response = await fetch(`${config.backendUrls.store}/service/bookings/my`, {
56
- headers: {
57
- "Authorization": `Bearer ${accessToken}`,
58
- "X-Storefront-Key": config.storefrontApiKey,
59
- "X-Business-ID": config.businessId,
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
- const response = await fetch(`${config.backendUrls.store}/service/bookings/${id}`, {
68
- headers: {
69
- "Authorization": `Bearer ${accessToken}`,
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
- export async function cancelAppointment(config: SdkConfig, accessToken: string, id: string): Promise<void> {
79
- await fetch(`${config.backendUrls.store}/service/bookings/${id}`, {
80
- method: 'DELETE',
81
- headers: {
82
- "Authorization": `Bearer ${accessToken}`,
83
- "X-Storefront-Key": config.storefrontApiKey,
84
- "X-Business-ID": config.businessId,
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
- export async function getDeliverables(config: SdkConfig, accessToken: string, order_id: string): Promise<ServiceDeliverable[]> {
90
- const response = await fetch(`${config.backendUrls.store}/service/projects/${order_id}/deliverables/`, {
91
- headers: {
92
- "Authorization": `Bearer ${accessToken}`,
93
- "X-Storefront-Key": config.storefrontApiKey,
94
- "X-Business-ID": config.businessId,
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
- export async function approveDeliverable(
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
- order_id: string,
105
- deliverable_id: string
106
- ): Promise<ServiceDeliverable> {
107
- const response = await fetch(
108
- `${config.backendUrls.store}/service/projects/${order_id}/deliverables/${deliverable_id}/approve`,
109
- {
110
- method: 'POST',
111
- headers: {
112
- 'Content-Type': 'application/json',
113
- "Authorization": `Bearer ${accessToken}`,
114
- "X-Storefront-Key": config.storefrontApiKey,
115
- "X-Business-ID": config.businessId,
116
- },
117
- body: JSON.stringify({ approved: true }),
118
- }
119
- )
120
- if (!response.ok) throw new Error('Failed to approve deliverable')
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
- export async function requestRevision(
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
- order_id: string,
128
- deliverable_id: string,
129
- feedback: string
130
- ): Promise<ServiceDeliverable> {
131
- const response = await fetch(
132
- `${config.backendUrls.store}/service/projects/${order_id}/deliverables/${deliverable_id}/revision`,
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
- 'Content-Type': 'application/json',
137
- "Authorization": `Bearer ${accessToken}`,
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
- getDeliverables,
156
- approveDeliverable,
157
- requestRevision,
252
+ rescheduleAppointment,
253
+ disputeAppointment,
254
+ confirmAppointmentCompletion,
255
+ getMilestones,
256
+ approveMilestone,
158
257
  }