@kasuvia/sdk 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +42 -15
  2. package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
  3. package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
  4. package/dist/components/index.d.mts +497 -0
  5. package/dist/components/index.d.ts +497 -0
  6. package/dist/components/index.js +4217 -0
  7. package/dist/components/index.js.map +1 -0
  8. package/dist/components/index.mjs +4194 -0
  9. package/dist/components/index.mjs.map +1 -0
  10. package/dist/config/server.d.mts +26 -0
  11. package/dist/config/server.d.ts +26 -0
  12. package/dist/config/server.js +38 -0
  13. package/dist/config/server.js.map +1 -0
  14. package/dist/config/server.mjs +13 -0
  15. package/dist/config/server.mjs.map +1 -0
  16. package/dist/hooks/index.d.mts +393 -160
  17. package/dist/hooks/index.d.ts +393 -160
  18. package/dist/hooks/index.js +2792 -2137
  19. package/dist/hooks/index.js.map +1 -1
  20. package/dist/hooks/index.mjs +2734 -2121
  21. package/dist/hooks/index.mjs.map +1 -1
  22. package/dist/index-CNW_LINy.d.mts +578 -0
  23. package/dist/index-CNW_LINy.d.ts +578 -0
  24. package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
  25. package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
  26. package/dist/index-NkI10J_E.d.mts +40 -0
  27. package/dist/index-NkI10J_E.d.ts +40 -0
  28. package/dist/index.d.mts +20437 -103
  29. package/dist/index.d.ts +20437 -103
  30. package/dist/index.js +2111 -1802
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +2006 -1786
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
  35. package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
  36. package/dist/services/index.d.mts +908 -2
  37. package/dist/services/index.d.ts +908 -2
  38. package/dist/services/index.js +1492 -1686
  39. package/dist/services/index.js.map +1 -1
  40. package/dist/services/index.mjs +1431 -1670
  41. package/dist/services/index.mjs.map +1 -1
  42. package/package.json +46 -6
  43. package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
  44. package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
  45. package/src/components/bespoke/index.ts +3 -0
  46. package/src/components/booking/SlotPicker.tsx +97 -0
  47. package/src/components/booking/index.ts +1 -0
  48. package/src/components/cart/CartItem.tsx +81 -0
  49. package/src/components/cart/CartSidebar.tsx +115 -0
  50. package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
  51. package/src/components/cart/index.ts +3 -0
  52. package/src/components/index.ts +8 -0
  53. package/src/components/orders/DeliverableReviewer.tsx +139 -0
  54. package/src/components/orders/index.ts +1 -0
  55. package/src/components/product/ProductCard.tsx +350 -0
  56. package/src/components/product/ProductGrid.tsx +84 -0
  57. package/src/components/product/index.ts +2 -0
  58. package/src/components/shared/AddressAutocomplete.tsx +212 -0
  59. package/src/components/shared/AnalyticsScripts.tsx +56 -0
  60. package/src/components/shared/CookieConsentBanner.tsx +98 -0
  61. package/src/components/shared/ImageEditorModal.tsx +1066 -0
  62. package/src/components/shared/ImageViewerModal.tsx +598 -0
  63. package/src/components/shared/LucideReactIcon.tsx +52 -0
  64. package/src/components/shared/MapPinDrop.tsx +101 -0
  65. package/src/components/shared/MultiSelectPicker.tsx +193 -0
  66. package/src/components/shared/OfflineBanner.tsx +46 -0
  67. package/src/components/shared/SearchableInput.tsx +153 -0
  68. package/src/components/shared/SearchablePopover.tsx +166 -0
  69. package/src/components/shared/SmartImage.tsx +311 -0
  70. package/src/components/shared/TruncatedValue.tsx +27 -0
  71. package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
  72. package/src/components/shared/index.ts +15 -0
  73. package/src/components/skeletons/CardSkeleton.tsx +26 -0
  74. package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
  75. package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
  76. package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
  77. package/src/components/skeletons/TableSkeleton.tsx +42 -0
  78. package/src/components/skeletons/index.ts +5 -0
  79. package/src/config/index.ts +22 -4
  80. package/src/config/server.ts +45 -0
  81. package/src/context/sdk-context.tsx +5 -2
  82. package/src/generated/auth-openapi.ts +2935 -0
  83. package/src/generated/cart-openapi.ts +652 -0
  84. package/src/generated/cms-openapi.ts +1668 -0
  85. package/src/generated/crm-openapi.ts +986 -0
  86. package/src/generated/graphql.ts +126 -0
  87. package/src/generated/index.ts +11 -0
  88. package/src/generated/media-openapi.ts +613 -0
  89. package/src/generated/notification-openapi.ts +862 -0
  90. package/src/generated/payment-openapi.ts +2713 -0
  91. package/src/generated/store-openapi.ts +9510 -0
  92. package/src/graphql/operations/collections.graphql +32 -0
  93. package/src/graphql/operations/products.graphql +197 -0
  94. package/src/graphql/operations/wishlist.graphql +25 -0
  95. package/src/hooks/index.ts +9 -2
  96. package/src/hooks/use-auth.ts +163 -69
  97. package/src/hooks/use-bespoke-availability.ts +39 -22
  98. package/src/hooks/use-business-config.ts +51 -0
  99. package/src/hooks/use-cart.ts +310 -238
  100. package/src/hooks/use-cms.ts +107 -127
  101. package/src/hooks/use-collections.ts +35 -55
  102. package/src/hooks/use-consultations.ts +9 -136
  103. package/src/hooks/use-countries.ts +19 -28
  104. package/src/hooks/use-currencies.ts +8 -20
  105. package/src/hooks/use-customer-address.ts +64 -0
  106. package/src/hooks/use-feedback.ts +34 -0
  107. package/src/hooks/use-languages.ts +26 -39
  108. package/src/hooks/use-media-upload.ts +23 -0
  109. package/src/hooks/use-notification-stream.ts +88 -0
  110. package/src/hooks/use-notifications.ts +163 -0
  111. package/src/hooks/use-order-stream.ts +29 -29
  112. package/src/hooks/use-orders.ts +91 -73
  113. package/src/hooks/use-payment.ts +116 -84
  114. package/src/hooks/use-products.ts +65 -269
  115. package/src/hooks/use-push-notifications.ts +122 -0
  116. package/src/hooks/use-reviews.ts +175 -0
  117. package/src/hooks/use-service-booking.ts +109 -132
  118. package/src/hooks/use-states.ts +16 -30
  119. package/src/hooks/use-wishlist.ts +44 -69
  120. package/src/index.ts +15 -11
  121. package/src/services/auth.service.ts +422 -132
  122. package/src/services/bespoke.service.ts +69 -10
  123. package/src/services/cart.service.ts +155 -186
  124. package/src/services/cms.service.ts +41 -50
  125. package/src/services/collection.service.ts +25 -63
  126. package/src/services/config.service.ts +10 -21
  127. package/src/services/consultation.service.ts +36 -93
  128. package/src/services/feedback.service.ts +76 -0
  129. package/src/services/index.ts +7 -1
  130. package/src/services/legal.service.ts +62 -0
  131. package/src/services/logistics.service.ts +235 -53
  132. package/src/services/media.service.ts +115 -0
  133. package/src/services/notifications.service.ts +204 -20
  134. package/src/services/order.service.ts +498 -127
  135. package/src/services/payment.service.ts +149 -100
  136. package/src/services/product.service.ts +157 -814
  137. package/src/services/review.service.ts +211 -0
  138. package/src/services/service-booking.service.ts +198 -99
  139. package/src/services/wishlist.service.ts +27 -74
  140. package/src/types/auth/index.ts +161 -18
  141. package/src/types/business/index.ts +9 -1
  142. package/src/types/cms/index.ts +91 -6
  143. package/src/types/cms/legal.ts +18 -0
  144. package/src/types/consultations/index.ts +60 -68
  145. package/src/types/index.ts +5 -3
  146. package/src/types/media/index.ts +35 -3
  147. package/src/types/payments/index.ts +80 -16
  148. package/src/types/reviews/index.ts +116 -0
  149. package/src/types/services/index.ts +113 -38
  150. package/src/types/store/checkout.ts +53 -0
  151. package/src/types/store/index.ts +273 -52
  152. package/src/types/store/invoice.ts +31 -0
  153. package/src/types/store/shipping.ts +84 -3
  154. package/src/types/ui/index.ts +1 -1
  155. package/src/utils/checkout.ts +19 -3
  156. package/src/utils/cookieConsent.ts +34 -0
  157. package/src/utils/format.ts +408 -0
  158. package/src/utils/geolocation.ts +134 -0
  159. package/src/utils/graphql-url.ts +47 -0
  160. package/src/utils/guest-session.ts +59 -0
  161. package/src/utils/http-client.ts +147 -0
  162. package/src/utils/index.ts +11 -4
  163. package/src/utils/status-colors.ts +79 -0
  164. package/src/utils/token-refresh.ts +115 -0
  165. package/src/utils/validators.ts +13 -0
  166. package/src/utils/youtube.ts +18 -0
  167. package/dist/index-C5YO9gTl.d.mts +0 -507
  168. package/dist/index-C5YO9gTl.d.ts +0 -507
  169. package/dist/index-CefG7_xq.d.mts +0 -793
  170. package/dist/index-DeHCf3zI.d.ts +0 -793
  171. package/src/graphql/collections.graphql.ts +0 -48
  172. package/src/graphql/index.ts +0 -7
  173. package/src/graphql/products.graphql.ts +0 -860
  174. package/src/graphql/wishlist.graphql.ts +0 -34
  175. package/src/hooks/use-consultation-stream.ts +0 -142
  176. package/src/types/bespoke/index.ts +0 -39
@@ -1,776 +1,154 @@
1
1
  /**
2
2
  * Product Service
3
- *
3
+ *
4
4
  * Handles product operations using GraphQL.
5
5
  * Stateless - all configuration passed as parameters.
6
6
  */
7
- import { GraphQLClient } from 'graphql-request'
8
7
  import type { Product, PaginatedResponse, ListParams } from '../types/store'
8
+ import { ProductTypeEnum } from '../types/store'
9
9
  import type { SdkConfig } from '../config'
10
+ import { createGraphQLClient } from '../utils/graphql-url'
11
+ import {
12
+ GetStorefrontProductsDocument,
13
+ GetStorefrontProductBySlugDocument,
14
+ GetStorefrontPopularDocument,
15
+ GetStorefrontFeaturedDocument,
16
+ GetStorefrontNewDocument,
17
+ GetStorefrontOnSaleDocument,
18
+ GetStorefrontRecommendedDocument,
19
+ type ProductFieldsFragment,
20
+ type ProductType as GeneratedProductType,
21
+ } from '../generated/graphql'
10
22
 
11
23
  /**
12
- * Create GraphQL client with provided config
24
+ * REAL BUG FIXED (2026-09-21), live-reproduced — the root cause of "nothing
25
+ * e-commerce-related works": kasuvia-store-management's ProductType is a
26
+ * Python Enum (`PHYSICAL = "physical"`, schema.py:16-20). Strawberry
27
+ * GraphQL serializes an enum field using the member NAME over the wire —
28
+ * standard, spec-correct GraphQL behavior (GraphQL enum values are
29
+ * conventionally ALL_CAPS) — so `storefront_products { items { product_type
30
+ * } } }` has always actually returned `"PHYSICAL"`/`"DIGITAL"`/`"SERVICE"`/
31
+ * `"BESPOKE"`, never the lowercase value every frontend `product_type ===
32
+ * "physical"` check (ProductCTA, ProductInfo, ProductTypeDetails,
33
+ * ProductCard, CheckoutClient, getGuestCartProductType — every one of them)
34
+ * was written assuming. Every type-aware branch silently fell through to
35
+ * whatever its default/fallback case was, for every single product,
36
+ * platform-wide — confirmed live: a real SERVICE product rendered a plain
37
+ * "Add to Cart" button instead of any service-specific flow.
38
+ *
39
+ * Originally fixed with a blind `.toLowerCase()` at the single point every
40
+ * product enters the SDK from GraphQL. Now that `product_type` is a real
41
+ * generated union (`GeneratedProductType`, from graphql-codegen against the
42
+ * live schema — see graphql/schema.graphql), this is an exhaustive switch
43
+ * instead: if kasuvia-store-management ever adds a 5th ProductType member,
44
+ * this function fails to COMPILE until it's handled here, rather than
45
+ * silently lowercasing an unrecognised string at runtime.
13
46
  */
14
- function createGraphQLClient(config: SdkConfig): GraphQLClient {
15
- const headers: Record<string, string> = {
16
- 'Content-Type': 'application/json',
17
- 'Accept': 'application/json',
18
- 'X-Storefront-Key': config.storefrontApiKey,
19
- 'X-Business-ID': config.businessId,
20
- }
21
- return new GraphQLClient(`${config.backendUrls.store}/graphql`, { headers })
22
- }
23
-
24
- const GET_STOREFRONT_PRODUCTS = `
25
- query GetStorefrontProducts($page: Int!, $pageSize: Int!, $collectionSlug: String, $search: String) {
26
- storefront_products(page: $page, pageSize: $pageSize, collectionSlug: $collectionSlug, search: $search) {
27
- items {
28
- id
29
- business_id
30
- name
31
- slug
32
- description
33
- brand
34
- product_type
35
- requires_shipping
36
- is_consumable
37
- is_bespoke
38
- base_price
39
- currency
40
- images
41
- tags
42
- custom_fields { label unit required }
43
- is_published
44
- is_featured
45
- is_popular
46
- is_new
47
- is_temporary
48
- digital_file_key
49
- download_limit
50
- download_expires_at
51
- weight_kg
52
- length_cm
53
- width_cm
54
- height_cm
55
- created_at
56
- updated_at
57
- variants {
58
- id
59
- product_id
60
- sku
61
- attributes
62
- price
63
- images
64
- weight_kg
65
- is_active
66
- inventory {
67
- id
68
- product_id
69
- variant_id
70
- quantity
71
- reserved
72
- reorder_point
73
- reorder_quantity
74
- track_quantity
75
- allow_backorder
76
- available
77
- is_in_stock
78
- is_low_stock
79
- updated_at
80
- }
81
- created_at
82
- updated_at
83
- }
84
- collections {
85
- id
86
- business_id
87
- name
88
- slug
89
- description
90
- image
91
- collection_type
92
- rules
93
- status
94
- sort_order
95
- created_at
96
- updated_at
97
- }
98
- }
99
- total
100
- page
101
- page_size
102
- total_pages
103
- }
104
- }
105
- `
106
-
107
- const GET_STOREFRONT_PRODUCT_BY_SLUG = `
108
- query GetStorefrontProductBySlug($slug: String!) {
109
- storefront_product_by_slug(slug: $slug) {
110
- id
111
- business_id
112
- name
113
- slug
114
- description
115
- brand
116
- product_type
117
- requires_shipping
118
- is_consumable
119
- is_bespoke
120
- base_price
121
- currency
122
- images
123
- tags
124
- custom_fields { label unit required }
125
- is_published
126
- is_featured
127
- is_popular
128
- is_new
129
- is_temporary
130
- digital_file_key
131
- download_limit
132
- download_expires_at
133
- weight_kg
134
- length_cm
135
- width_cm
136
- height_cm
137
- created_at
138
- updated_at
139
- variants {
140
- id
141
- product_id
142
- sku
143
- attributes
144
- price
145
- images
146
- weight_kg
147
- is_active
148
- inventory {
149
- id
150
- product_id
151
- variant_id
152
- quantity
153
- reserved
154
- reorder_point
155
- reorder_quantity
156
- track_quantity
157
- allow_backorder
158
- available
159
- is_in_stock
160
- is_low_stock
161
- updated_at
162
- }
163
- created_at
164
- updated_at
165
- }
166
- collections {
167
- id
168
- business_id
169
- name
170
- slug
171
- description
172
- image
173
- collection_type
174
- rules
175
- status
176
- sort_order
177
- created_at
178
- updated_at
179
- }
180
- }
181
- }
182
- `
183
-
184
- const GET_STOREFRONT_LATEST = `
185
- query GetStorefrontLatest($page: Int!, $pageSize: Int!) {
186
- storefront_latest(page: $page, pageSize: $pageSize) {
187
- items {
188
- id
189
- business_id
190
- name
191
- slug
192
- description
193
- brand
194
- product_type
195
- requires_shipping
196
- is_consumable
197
- is_bespoke
198
- base_price
199
- currency
200
- images
201
- tags
202
- custom_fields { label unit required }
203
- is_published
204
- is_featured
205
- is_popular
206
- is_new
207
- is_temporary
208
- digital_file_key
209
- download_limit
210
- download_expires_at
211
- weight_kg
212
- length_cm
213
- width_cm
214
- height_cm
215
- created_at
216
- updated_at
217
- variants {
218
- id
219
- product_id
220
- sku
221
- attributes
222
- price
223
- images
224
- weight_kg
225
- is_active
226
- inventory {
227
- id
228
- product_id
229
- variant_id
230
- quantity
231
- reserved
232
- reorder_point
233
- reorder_quantity
234
- track_quantity
235
- allow_backorder
236
- available
237
- is_in_stock
238
- is_low_stock
239
- updated_at
240
- }
241
- created_at
242
- updated_at
243
- }
244
- collections {
245
- id
246
- business_id
247
- name
248
- slug
249
- description
250
- image
251
- collection_type
252
- rules
253
- status
254
- sort_order
255
- created_at
256
- updated_at
257
- }
258
- }
259
- total
260
- page
261
- page_size
262
- total_pages
263
- }
264
- }
265
- `
266
-
267
- const GET_STOREFRONT_POPULAR = `
268
- query GetStorefrontPopular($page: Int!, $pageSize: Int!) {
269
- storefront_popular(page: $page, pageSize: $pageSize) {
270
- items {
271
- id
272
- business_id
273
- name
274
- slug
275
- description
276
- brand
277
- product_type
278
- requires_shipping
279
- is_consumable
280
- is_bespoke
281
- base_price
282
- currency
283
- images
284
- tags
285
- custom_fields { label unit required }
286
- is_published
287
- is_featured
288
- is_popular
289
- is_new
290
- is_temporary
291
- digital_file_key
292
- download_limit
293
- download_expires_at
294
- weight_kg
295
- length_cm
296
- width_cm
297
- height_cm
298
- created_at
299
- updated_at
300
- variants {
301
- id
302
- product_id
303
- sku
304
- attributes
305
- price
306
- images
307
- weight_kg
308
- is_active
309
- inventory {
310
- id
311
- product_id
312
- variant_id
313
- quantity
314
- reserved
315
- reorder_point
316
- reorder_quantity
317
- track_quantity
318
- allow_backorder
319
- available
320
- is_in_stock
321
- is_low_stock
322
- updated_at
323
- }
324
- created_at
325
- updated_at
326
- }
327
- collections {
328
- id
329
- business_id
330
- name
331
- slug
332
- description
333
- image
334
- collection_type
335
- rules
336
- status
337
- sort_order
338
- created_at
339
- updated_at
340
- }
341
- }
342
- total
343
- page
344
- page_size
345
- total_pages
346
- }
347
- }
348
- `
349
-
350
- const GET_STOREFRONT_FEATURED = `
351
- query GetStorefrontFeatured($page: Int!, $pageSize: Int!) {
352
- storefront_featured(page: $page, pageSize: $pageSize) {
353
- items {
354
- id
355
- business_id
356
- name
357
- slug
358
- description
359
- brand
360
- product_type
361
- requires_shipping
362
- is_consumable
363
- is_bespoke
364
- base_price
365
- currency
366
- images
367
- tags
368
- custom_fields { label unit required }
369
- is_published
370
- is_featured
371
- is_popular
372
- is_new
373
- is_temporary
374
- digital_file_key
375
- download_limit
376
- download_expires_at
377
- weight_kg
378
- length_cm
379
- width_cm
380
- height_cm
381
- created_at
382
- updated_at
383
- variants {
384
- id
385
- product_id
386
- sku
387
- attributes
388
- price
389
- images
390
- weight_kg
391
- is_active
392
- inventory {
393
- id
394
- product_id
395
- variant_id
396
- quantity
397
- reserved
398
- reorder_point
399
- reorder_quantity
400
- track_quantity
401
- allow_backorder
402
- available
403
- is_in_stock
404
- is_low_stock
405
- updated_at
406
- }
407
- created_at
408
- updated_at
409
- }
410
- collections {
411
- id
412
- business_id
413
- name
414
- slug
415
- description
416
- image
417
- collection_type
418
- rules
419
- status
420
- sort_order
421
- created_at
422
- updated_at
423
- }
424
- }
425
- total
426
- page
427
- page_size
428
- total_pages
47
+ function mapProductType(raw: GeneratedProductType): ProductTypeEnum {
48
+ switch (raw) {
49
+ case 'PHYSICAL':
50
+ return ProductTypeEnum.PHYSICAL
51
+ case 'DIGITAL':
52
+ return ProductTypeEnum.DIGITAL
53
+ case 'SERVICE':
54
+ return ProductTypeEnum.SERVICE
55
+ case 'BESPOKE':
56
+ return ProductTypeEnum.BESPOKE
57
+ default: {
58
+ const _exhaustive: never = raw
59
+ throw new Error(`Unhandled GraphQL ProductType member: ${_exhaustive}`)
429
60
  }
430
61
  }
431
- `
432
-
433
- const GET_STOREFRONT_NEW = `
434
- query GetStorefrontNew($page: Int!, $pageSize: Int!) {
435
- storefront_new(page: $page, pageSize: $pageSize) {
436
- items {
437
- id
438
- business_id
439
- name
440
- slug
441
- description
442
- brand
443
- product_type
444
- requires_shipping
445
- is_consumable
446
- is_bespoke
447
- base_price
448
- currency
449
- sale_price
450
- sale_start
451
- sale_end
452
- images
453
- tags
454
- custom_fields { label unit required }
455
- is_published
456
- is_featured
457
- is_popular
458
- is_new
459
- is_temporary
460
- digital_file_key
461
- download_limit
462
- download_expires_at
463
- weight_kg
464
- length_cm
465
- width_cm
466
- height_cm
467
- created_at
468
- updated_at
469
- variants {
470
- id
471
- product_id
472
- sku
473
- attributes
474
- price
475
- images
476
- weight_kg
477
- is_active
478
- inventory {
479
- id
480
- product_id
481
- variant_id
482
- quantity
483
- reserved
484
- reorder_point
485
- reorder_quantity
486
- track_quantity
487
- allow_backorder
488
- available
489
- is_in_stock
490
- is_low_stock
491
- updated_at
492
- }
493
- created_at
494
- updated_at
495
- }
496
- collections {
497
- id
498
- business_id
499
- name
500
- slug
501
- description
502
- image
503
- collection_type
504
- rules
505
- status
506
- sort_order
507
- created_at
508
- updated_at
509
- }
510
- }
511
- total
512
- page
513
- page_size
514
- total_pages
515
- }
516
- }
517
- `
62
+ }
518
63
 
519
- const GET_STOREFRONT_ON_SALE = `
520
- query GetStorefrontOnSale($page: Int!, $pageSize: Int!) {
521
- storefront_on_sale(page: $page, pageSize: $pageSize) {
522
- items {
523
- id
524
- business_id
525
- name
526
- slug
527
- description
528
- brand
529
- product_type
530
- requires_shipping
531
- is_consumable
532
- is_bespoke
533
- base_price
534
- currency
535
- sale_price
536
- sale_start
537
- sale_end
538
- images
539
- tags
540
- custom_fields { label unit required }
541
- is_published
542
- is_featured
543
- is_popular
544
- is_new
545
- is_temporary
546
- digital_file_key
547
- download_limit
548
- download_expires_at
549
- weight_kg
550
- length_cm
551
- width_cm
552
- height_cm
553
- created_at
554
- updated_at
555
- variants {
556
- id
557
- product_id
558
- sku
559
- attributes
560
- price
561
- images
562
- weight_kg
563
- is_active
564
- inventory {
565
- id
566
- product_id
567
- variant_id
568
- quantity
569
- reserved
570
- reorder_point
571
- reorder_quantity
572
- track_quantity
573
- allow_backorder
574
- available
575
- is_in_stock
576
- is_low_stock
577
- updated_at
578
- }
579
- created_at
580
- updated_at
581
- }
582
- collections {
583
- id
584
- business_id
585
- name
586
- slug
587
- description
588
- image
589
- collection_type
590
- rules
591
- status
592
- sort_order
593
- created_at
594
- updated_at
595
- }
596
- }
597
- total
598
- page
599
- page_size
600
- total_pages
601
- }
602
- }
603
- `
64
+ /**
65
+ * `field_type` is a plain GraphQL String (CustomField isn't a GraphQL enum),
66
+ * but the real backend (CustomFieldSchema's `Literal["text", "choice"]`)
67
+ * only ever writes one of these two values — validated at save time, not
68
+ * guessed here. An unrecognised value means the backend and this mapping
69
+ * have drifted, which should fail loud rather than silently coerce to a
70
+ * default that would misrender the field.
71
+ */
72
+ function mapCustomFieldType(raw: string): "text" | "choice" {
73
+ if (raw === "text" || raw === "choice") return raw
74
+ throw new Error(`Unrecognised custom field field_type from backend: "${raw}"`)
75
+ }
604
76
 
605
- const GET_STOREFRONT_LOW_STOCK = `
606
- query GetStorefrontLowStock($page: Int!, $pageSize: Int!) {
607
- storefront_low_stock(page: $page, pageSize: $pageSize) {
608
- items {
609
- id
610
- business_id
611
- name
612
- slug
613
- description
614
- brand
615
- product_type
616
- requires_shipping
617
- is_consumable
618
- is_bespoke
619
- base_price
620
- currency
621
- sale_price
622
- sale_start
623
- sale_end
624
- images
625
- tags
626
- custom_fields { label unit required }
627
- is_published
628
- is_featured
629
- is_popular
630
- is_new
631
- is_temporary
632
- digital_file_key
633
- download_limit
634
- download_expires_at
635
- weight_kg
636
- length_cm
637
- width_cm
638
- height_cm
639
- created_at
640
- updated_at
641
- variants {
642
- id
643
- product_id
644
- sku
645
- attributes
646
- price
647
- images
648
- weight_kg
649
- is_active
650
- inventory {
651
- id
652
- product_id
653
- variant_id
654
- quantity
655
- reserved
656
- reorder_point
657
- reorder_quantity
658
- track_quantity
659
- allow_backorder
660
- available
661
- is_in_stock
662
- is_low_stock
663
- updated_at
77
+ /**
78
+ * Maps the raw, generated GraphQL fragment shape to the SDK's public
79
+ * `Product` type. `satisfies Product` (not `as Product`) is deliberate: it
80
+ * keeps this a real compile-time check against the public type rather than
81
+ * an unchecked assertion — if `ProductFieldsFragment` and `Product` ever
82
+ * drift (a field renamed on one side, a nullability change), this function
83
+ * fails to compile instead of quietly shipping a mismatched shape.
84
+ */
85
+ function mapProduct(raw: ProductFieldsFragment): Product {
86
+ return {
87
+ ...raw,
88
+ product_type: mapProductType(raw.product_type),
89
+ custom_fields: raw.custom_fields.map((f) => ({
90
+ ...f,
91
+ unit: f.unit ?? undefined,
92
+ field_type: mapCustomFieldType(f.field_type),
93
+ })),
94
+ description: raw.description ?? undefined,
95
+ brand: raw.brand ?? undefined,
96
+ is_physical: raw.is_physical ?? undefined,
97
+ sale_price: raw.sale_price ?? undefined,
98
+ sale_start: (raw.sale_start as string | null) ?? undefined,
99
+ sale_end: (raw.sale_end as string | null) ?? undefined,
100
+ source_channel_ref_id: raw.source_channel_ref_id ?? undefined,
101
+ download_limit: raw.download_limit ?? undefined,
102
+ download_expires_at: (raw.download_expires_at as string | null) ?? undefined,
103
+ service_type: (raw.service_type as Product['service_type']) ?? undefined,
104
+ service_duration_minutes: raw.service_duration_minutes ?? undefined,
105
+ service_location_type: (raw.service_location_type as Product['service_location_type']) ?? undefined,
106
+ service_coverage_radius_km: raw.service_coverage_radius_km ?? undefined,
107
+ service_max_capacity_per_slot: raw.service_max_capacity_per_slot ?? undefined,
108
+ service_recurring_interval: (raw.service_recurring_interval as Product['service_recurring_interval']) ?? undefined,
109
+ length_cm: raw.length_cm ?? undefined,
110
+ width_cm: raw.width_cm ?? undefined,
111
+ height_cm: raw.height_cm ?? undefined,
112
+ average_rating: raw.average_rating ?? undefined,
113
+ created_at: raw.created_at as string,
114
+ updated_at: raw.updated_at as string,
115
+ variants: raw.variants.map((v) => ({
116
+ ...v,
117
+ sku: v.sku ?? undefined,
118
+ attributes: v.attributes ? JSON.parse(v.attributes) : {},
119
+ price: v.price ?? undefined,
120
+ weight_kg: v.weight_kg ?? undefined,
121
+ inventory: v.inventory
122
+ ? {
123
+ ...v.inventory,
124
+ variant_id: v.inventory.variant_id ?? undefined,
125
+ updated_at: v.inventory.updated_at as string,
664
126
  }
665
- created_at
666
- updated_at
667
- }
668
- collections {
669
- id
670
- business_id
671
- name
672
- slug
673
- description
674
- image
675
- collection_type
676
- rules
677
- status
678
- sort_order
679
- created_at
680
- updated_at
681
- }
682
- }
683
- total
684
- page
685
- page_size
686
- total_pages
687
- }
688
- }
689
- `
127
+ : undefined,
128
+ created_at: v.created_at as string,
129
+ updated_at: v.updated_at as string,
130
+ })),
131
+ collections: raw.collections.map((c) => ({
132
+ ...c,
133
+ business_id: c.business_id ?? undefined,
134
+ description: c.description ?? undefined,
135
+ image_url: c.image_url ?? undefined,
136
+ rules: c.rules ?? undefined,
137
+ created_at: c.created_at as string,
138
+ updated_at: c.updated_at as string,
139
+ })),
140
+ } satisfies Product
141
+ }
690
142
 
691
- const GET_STOREFRONT_RECOMMENDED = `
692
- query GetStorefrontRecommended($page: Int!, $pageSize: Int!, $excludeProductId: String) {
693
- storefront_recommended(page: $page, pageSize: $pageSize, excludeProductId: $excludeProductId) {
694
- items {
695
- id
696
- business_id
697
- name
698
- slug
699
- description
700
- brand
701
- product_type
702
- requires_shipping
703
- is_consumable
704
- is_bespoke
705
- base_price
706
- currency
707
- images
708
- tags
709
- custom_fields { label unit required }
710
- is_published
711
- is_featured
712
- is_popular
713
- is_new
714
- is_temporary
715
- digital_file_key
716
- download_limit
717
- download_expires_at
718
- weight_kg
719
- length_cm
720
- width_cm
721
- height_cm
722
- created_at
723
- updated_at
724
- variants {
725
- id
726
- product_id
727
- sku
728
- attributes
729
- price
730
- images
731
- weight_kg
732
- is_active
733
- inventory {
734
- id
735
- product_id
736
- variant_id
737
- quantity
738
- reserved
739
- reorder_point
740
- reorder_quantity
741
- track_quantity
742
- allow_backorder
743
- available
744
- is_in_stock
745
- is_low_stock
746
- updated_at
747
- }
748
- created_at
749
- updated_at
750
- }
751
- collections {
752
- id
753
- business_id
754
- name
755
- slug
756
- description
757
- image
758
- collection_type
759
- rules
760
- status
761
- sort_order
762
- created_at
763
- updated_at
764
- }
765
- }
766
- total
767
- page
768
- page_size
769
- total_pages
770
- }
771
- }
772
- `
143
+ function mapProductConnection<T extends { items: ProductFieldsFragment[]; total: number; page: number; page_size: number; total_pages: number }>(
144
+ connection: T
145
+ ): PaginatedResponse<Product> {
146
+ return { ...connection, items: connection.items.map(mapProduct) }
147
+ }
773
148
 
149
+ /**
150
+ * Create GraphQL client with provided config
151
+ */
774
152
  /**
775
153
  * Fetch all active products for the storefront.
776
154
  */
@@ -779,14 +157,14 @@ export async function getStorefrontProducts(
779
157
  params?: ListParams
780
158
  ): Promise<PaginatedResponse<Product>> {
781
159
  const client = createGraphQLClient(config)
782
- const variables = {
160
+ const data = await client.request(GetStorefrontProductsDocument, {
783
161
  page: params?.page || 1,
784
162
  pageSize: params?.pageSize || 20,
785
163
  collectionSlug: params?.collectionSlug,
786
164
  search: params?.search,
787
- }
788
- const data = await client.request(GET_STOREFRONT_PRODUCTS, variables)
789
- return data.storefront_products
165
+ productType: params?.productType,
166
+ })
167
+ return mapProductConnection(data.storefront_products)
790
168
  }
791
169
 
792
170
  /**
@@ -797,24 +175,11 @@ export async function getStorefrontProductBySlug(
797
175
  slug: string
798
176
  ): Promise<Product> {
799
177
  const client = createGraphQLClient(config)
800
- const data = await client.request(GET_STOREFRONT_PRODUCT_BY_SLUG, { slug })
801
- return data.storefront_product_by_slug
802
- }
803
-
804
- /**
805
- * Fetch latest products for the storefront.
806
- */
807
- export async function getLatestProducts(
808
- config: SdkConfig,
809
- params?: ListParams
810
- ): Promise<PaginatedResponse<Product>> {
811
- const client = createGraphQLClient(config)
812
- const variables = {
813
- page: params?.page || 1,
814
- pageSize: params?.pageSize || 4,
178
+ const data = await client.request(GetStorefrontProductBySlugDocument, { slug })
179
+ if (!data.storefront_product_by_slug) {
180
+ throw new Error(`Product not found: ${slug}`)
815
181
  }
816
- const data = await client.request(GET_STOREFRONT_LATEST, variables)
817
- return data.storefront_latest
182
+ return mapProduct(data.storefront_product_by_slug)
818
183
  }
819
184
 
820
185
  /**
@@ -825,12 +190,11 @@ export async function getPopularProducts(
825
190
  params?: ListParams
826
191
  ): Promise<PaginatedResponse<Product>> {
827
192
  const client = createGraphQLClient(config)
828
- const variables = {
193
+ const data = await client.request(GetStorefrontPopularDocument, {
829
194
  page: params?.page || 1,
830
195
  pageSize: params?.pageSize || 4,
831
- }
832
- const data = await client.request(GET_STOREFRONT_POPULAR, variables)
833
- return data.storefront_popular
196
+ })
197
+ return mapProductConnection(data.storefront_popular)
834
198
  }
835
199
 
836
200
  /**
@@ -841,12 +205,11 @@ export async function getFeaturedProducts(
841
205
  params?: ListParams
842
206
  ): Promise<PaginatedResponse<Product>> {
843
207
  const client = createGraphQLClient(config)
844
- const variables = {
208
+ const data = await client.request(GetStorefrontFeaturedDocument, {
845
209
  page: params?.page || 1,
846
210
  pageSize: params?.pageSize || 4,
847
- }
848
- const data = await client.request(GET_STOREFRONT_FEATURED, variables)
849
- return data.storefront_featured
211
+ })
212
+ return mapProductConnection(data.storefront_featured)
850
213
  }
851
214
 
852
215
  /**
@@ -857,12 +220,11 @@ export async function getNewProducts(
857
220
  params?: ListParams
858
221
  ): Promise<PaginatedResponse<Product>> {
859
222
  const client = createGraphQLClient(config)
860
- const variables = {
223
+ const data = await client.request(GetStorefrontNewDocument, {
861
224
  page: params?.page || 1,
862
225
  pageSize: params?.pageSize || 4,
863
- }
864
- const data = await client.request(GET_STOREFRONT_NEW, variables)
865
- return data.storefront_new
226
+ })
227
+ return mapProductConnection(data.storefront_new)
866
228
  }
867
229
 
868
230
  /**
@@ -873,28 +235,11 @@ export async function getOnSaleProducts(
873
235
  params?: ListParams
874
236
  ): Promise<PaginatedResponse<Product>> {
875
237
  const client = createGraphQLClient(config)
876
- const variables = {
238
+ const data = await client.request(GetStorefrontOnSaleDocument, {
877
239
  page: params?.page || 1,
878
240
  pageSize: params?.pageSize || 4,
879
- }
880
- const data = await client.request(GET_STOREFRONT_ON_SALE, variables)
881
- return data.storefront_on_sale
882
- }
883
-
884
- /**
885
- * Fetch products with low stock for the storefront.
886
- */
887
- export async function getLowStockProducts(
888
- config: SdkConfig,
889
- params?: ListParams
890
- ): Promise<PaginatedResponse<Product>> {
891
- const client = createGraphQLClient(config)
892
- const variables = {
893
- page: params?.page || 1,
894
- pageSize: params?.pageSize || 4,
895
- }
896
- const data = await client.request(GET_STOREFRONT_LOW_STOCK, variables)
897
- return data.storefront_low_stock
241
+ })
242
+ return mapProductConnection(data.storefront_on_sale)
898
243
  }
899
244
 
900
245
  /**
@@ -907,12 +252,10 @@ export async function getRecommendedProducts(
907
252
  params?: ListParams & { excludeProductId?: string }
908
253
  ): Promise<PaginatedResponse<Product>> {
909
254
  const client = createGraphQLClient(config)
910
- const variables = {
255
+ const data = await client.request(GetStorefrontRecommendedDocument, {
911
256
  page: params?.page || 1,
912
257
  pageSize: params?.pageSize || 4,
913
258
  excludeProductId: params?.excludeProductId,
914
- }
915
- const data = await client.request(GET_STOREFRONT_RECOMMENDED, variables)
916
- return data.storefront_recommended
259
+ })
260
+ return mapProductConnection(data.storefront_recommended)
917
261
  }
918
-