@businessdash/sdk 0.9.60 → 0.9.80

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 (239) hide show
  1. package/README.md +991 -295
  2. package/assets/biab-sw.js +92 -0
  3. package/dist/account-core/index.d.ts +126 -0
  4. package/dist/account-core/index.d.ts.map +1 -0
  5. package/dist/account-core/index.js +191 -0
  6. package/dist/account-core/index.js.map +1 -0
  7. package/dist/account.d.ts +11 -0
  8. package/dist/account.d.ts.map +1 -0
  9. package/dist/account.js +11 -0
  10. package/dist/account.js.map +1 -0
  11. package/dist/auth-handler.d.ts +1 -1
  12. package/dist/auth-links.d.ts +33 -0
  13. package/dist/auth-links.d.ts.map +1 -0
  14. package/dist/auth-links.js +64 -0
  15. package/dist/auth-links.js.map +1 -0
  16. package/dist/blog-core/index.d.ts +112 -0
  17. package/dist/blog-core/index.d.ts.map +1 -0
  18. package/dist/blog-core/index.js +330 -0
  19. package/dist/blog-core/index.js.map +1 -0
  20. package/dist/blog.d.ts +11 -0
  21. package/dist/blog.d.ts.map +1 -0
  22. package/dist/blog.js +11 -0
  23. package/dist/blog.js.map +1 -0
  24. package/dist/chat-core/index.d.ts +97 -0
  25. package/dist/chat-core/index.d.ts.map +1 -0
  26. package/dist/chat-core/index.js +237 -0
  27. package/dist/chat-core/index.js.map +1 -0
  28. package/dist/chat.d.ts +11 -0
  29. package/dist/chat.d.ts.map +1 -0
  30. package/dist/chat.js +11 -0
  31. package/dist/chat.js.map +1 -0
  32. package/dist/checkout-core/index.d.ts +65 -0
  33. package/dist/checkout-core/index.d.ts.map +1 -0
  34. package/dist/checkout-core/index.js +159 -0
  35. package/dist/checkout-core/index.js.map +1 -0
  36. package/dist/checkout.d.ts +10 -0
  37. package/dist/checkout.d.ts.map +1 -0
  38. package/dist/checkout.js +10 -0
  39. package/dist/checkout.js.map +1 -0
  40. package/dist/cli.js +133 -25
  41. package/dist/cli.js.map +1 -1
  42. package/dist/client.d.ts +622 -1
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +888 -1
  45. package/dist/client.js.map +1 -1
  46. package/dist/consent-core/index.cjs +107 -0
  47. package/dist/consent-core/index.d.ts +103 -0
  48. package/dist/consent-core/index.d.ts.map +1 -0
  49. package/dist/consent-core/index.js +157 -0
  50. package/dist/consent-core/index.js.map +1 -0
  51. package/dist/contracts.d.ts +3526 -767
  52. package/dist/contracts.d.ts.map +1 -1
  53. package/dist/contracts.js +571 -4
  54. package/dist/contracts.js.map +1 -1
  55. package/dist/data-model-schema.d.ts +1 -1
  56. package/dist/data-model-schema.d.ts.map +1 -1
  57. package/dist/element.d.ts +30 -0
  58. package/dist/element.d.ts.map +1 -1
  59. package/dist/element.js +56 -0
  60. package/dist/element.js.map +1 -1
  61. package/dist/embed-protocol.d.ts +86 -0
  62. package/dist/embed-protocol.d.ts.map +1 -0
  63. package/dist/embed-protocol.js +60 -0
  64. package/dist/embed-protocol.js.map +1 -0
  65. package/dist/framework-support.d.ts +122 -0
  66. package/dist/framework-support.d.ts.map +1 -0
  67. package/dist/framework-support.js +323 -0
  68. package/dist/framework-support.js.map +1 -0
  69. package/dist/index.cjs +1355 -24
  70. package/dist/legal-core/index.cjs +115 -0
  71. package/dist/legal-core/index.d.ts +160 -0
  72. package/dist/legal-core/index.d.ts.map +1 -0
  73. package/dist/legal-core/index.js +220 -0
  74. package/dist/legal-core/index.js.map +1 -0
  75. package/dist/marketing-core/index.d.ts +66 -0
  76. package/dist/marketing-core/index.d.ts.map +1 -0
  77. package/dist/marketing-core/index.js +154 -0
  78. package/dist/marketing-core/index.js.map +1 -0
  79. package/dist/marketing.d.ts +7 -0
  80. package/dist/marketing.d.ts.map +1 -0
  81. package/dist/marketing.js +7 -0
  82. package/dist/marketing.js.map +1 -0
  83. package/dist/mcp-adapters/index.d.ts +135 -0
  84. package/dist/mcp-adapters/index.d.ts.map +1 -0
  85. package/dist/mcp-adapters/index.js +187 -0
  86. package/dist/mcp-adapters/index.js.map +1 -0
  87. package/dist/mcp-adapters.d.ts +7 -0
  88. package/dist/mcp-adapters.d.ts.map +1 -0
  89. package/dist/mcp-adapters.js +7 -0
  90. package/dist/mcp-adapters.js.map +1 -0
  91. package/dist/model3d-core/index.cjs +95 -0
  92. package/dist/model3d-core/index.d.ts +167 -0
  93. package/dist/model3d-core/index.d.ts.map +1 -0
  94. package/dist/model3d-core/index.js +158 -0
  95. package/dist/model3d-core/index.js.map +1 -0
  96. package/dist/notifications-core/index.cjs +315 -0
  97. package/dist/notifications-core/index.d.ts +229 -0
  98. package/dist/notifications-core/index.d.ts.map +1 -0
  99. package/dist/notifications-core/index.js +445 -0
  100. package/dist/notifications-core/index.js.map +1 -0
  101. package/dist/portal-core/index.d.ts +451 -0
  102. package/dist/portal-core/index.d.ts.map +1 -0
  103. package/dist/portal-core/index.js +819 -0
  104. package/dist/portal-core/index.js.map +1 -0
  105. package/dist/portal.d.ts +7 -0
  106. package/dist/portal.d.ts.map +1 -0
  107. package/dist/portal.js +7 -0
  108. package/dist/portal.js.map +1 -0
  109. package/dist/push-core/index.cjs +171 -0
  110. package/dist/push-core/index.d.ts +149 -0
  111. package/dist/push-core/index.d.ts.map +1 -0
  112. package/dist/push-core/index.js +231 -0
  113. package/dist/push-core/index.js.map +1 -0
  114. package/dist/react-marketing.d.ts +1 -17
  115. package/dist/react-marketing.d.ts.map +1 -1
  116. package/dist/react-marketing.js +4 -0
  117. package/dist/react-marketing.js.map +1 -1
  118. package/dist/react.d.ts +294 -101
  119. package/dist/react.d.ts.map +1 -1
  120. package/dist/react.js +302 -246
  121. package/dist/react.js.map +1 -1
  122. package/dist/seed-core/index.d.ts +354 -0
  123. package/dist/seed-core/index.d.ts.map +1 -0
  124. package/dist/seed-core/index.js +420 -0
  125. package/dist/seed-core/index.js.map +1 -0
  126. package/dist/seed.d.ts +7 -0
  127. package/dist/seed.d.ts.map +1 -0
  128. package/dist/seed.js +7 -0
  129. package/dist/seed.js.map +1 -0
  130. package/dist/seo-core/blog.d.ts +94 -0
  131. package/dist/seo-core/blog.d.ts.map +1 -0
  132. package/dist/seo-core/blog.js +149 -0
  133. package/dist/seo-core/blog.js.map +1 -0
  134. package/dist/seo-core/commerce.d.ts +64 -0
  135. package/dist/seo-core/commerce.d.ts.map +1 -0
  136. package/dist/seo-core/commerce.js +87 -0
  137. package/dist/seo-core/commerce.js.map +1 -0
  138. package/dist/seo-core/index.cjs +751 -0
  139. package/dist/seo-core/index.d.ts +226 -0
  140. package/dist/seo-core/index.d.ts.map +1 -0
  141. package/dist/seo-core/index.js +455 -0
  142. package/dist/seo-core/index.js.map +1 -0
  143. package/dist/seo-core/local.d.ts +181 -0
  144. package/dist/seo-core/local.d.ts.map +1 -0
  145. package/dist/seo-core/local.js +193 -0
  146. package/dist/seo-core/local.js.map +1 -0
  147. package/dist/sitemap-core/index.cjs +167 -0
  148. package/dist/sitemap-core/index.d.ts +155 -0
  149. package/dist/sitemap-core/index.d.ts.map +1 -0
  150. package/dist/sitemap-core/index.js +195 -0
  151. package/dist/sitemap-core/index.js.map +1 -0
  152. package/dist/social-core/index.d.ts +74 -0
  153. package/dist/social-core/index.d.ts.map +1 -0
  154. package/dist/social-core/index.js +72 -0
  155. package/dist/social-core/index.js.map +1 -0
  156. package/dist/social.d.ts +7 -0
  157. package/dist/social.d.ts.map +1 -0
  158. package/dist/social.js +7 -0
  159. package/dist/social.js.map +1 -0
  160. package/dist/solid-compat.d.ts +204 -0
  161. package/dist/solid-compat.d.ts.map +1 -0
  162. package/dist/solid-compat.js +221 -0
  163. package/dist/solid-compat.js.map +1 -0
  164. package/dist/store-core/index.d.ts +141 -0
  165. package/dist/store-core/index.d.ts.map +1 -0
  166. package/dist/store-core/index.js +277 -0
  167. package/dist/store-core/index.js.map +1 -0
  168. package/dist/store.d.ts +12 -0
  169. package/dist/store.d.ts.map +1 -0
  170. package/dist/store.js +12 -0
  171. package/dist/store.js.map +1 -0
  172. package/dist/vanilla.d.ts +111 -0
  173. package/dist/vanilla.d.ts.map +1 -1
  174. package/dist/vanilla.js +206 -0
  175. package/dist/vanilla.js.map +1 -1
  176. package/dist/verify-core/index.d.ts +98 -0
  177. package/dist/verify-core/index.d.ts.map +1 -0
  178. package/dist/verify-core/index.js +186 -0
  179. package/dist/verify-core/index.js.map +1 -0
  180. package/dist/verify.d.ts +8 -0
  181. package/dist/verify.d.ts.map +1 -0
  182. package/dist/verify.js +8 -0
  183. package/dist/verify.js.map +1 -0
  184. package/openapi.json +4516 -1311
  185. package/package.json +128 -11
  186. package/src/account-core/index.ts +309 -0
  187. package/src/account.ts +11 -0
  188. package/src/angular/headless.ts +519 -0
  189. package/src/angular/index.ts +3 -0
  190. package/src/auth-links.ts +76 -0
  191. package/src/blog-core/index.ts +423 -0
  192. package/src/blog.ts +11 -0
  193. package/src/chat-core/index.ts +335 -0
  194. package/src/chat.ts +11 -0
  195. package/src/checkout-core/index.ts +225 -0
  196. package/src/checkout.ts +10 -0
  197. package/src/cli.ts +184 -28
  198. package/src/client.ts +1097 -0
  199. package/src/consent-core/index.ts +213 -0
  200. package/src/contracts.ts +768 -11
  201. package/src/data-model-schema.ts +1 -1
  202. package/src/element.ts +102 -0
  203. package/src/embed-protocol.ts +100 -0
  204. package/src/framework-support.ts +440 -0
  205. package/src/legal-core/index.ts +276 -0
  206. package/src/marketing-core/index.ts +209 -0
  207. package/src/marketing.ts +7 -0
  208. package/src/mcp-adapters/index.ts +264 -0
  209. package/src/mcp-adapters.ts +7 -0
  210. package/src/model3d-core/index.ts +248 -0
  211. package/src/notifications-core/index.ts +650 -0
  212. package/src/portal-core/index.ts +1564 -0
  213. package/src/portal.ts +7 -0
  214. package/src/push-core/index.ts +335 -0
  215. package/src/qwik.tsx +602 -2
  216. package/src/react-marketing.tsx +5 -0
  217. package/src/react.tsx +434 -319
  218. package/src/seed-core/index.ts +735 -0
  219. package/src/seed.ts +7 -0
  220. package/src/seo-core/blog.ts +216 -0
  221. package/src/seo-core/commerce.ts +138 -0
  222. package/src/seo-core/index.ts +605 -0
  223. package/src/seo-core/local.ts +333 -0
  224. package/src/sitemap-core/index.ts +296 -0
  225. package/src/social-core/index.ts +103 -0
  226. package/src/social.ts +7 -0
  227. package/src/solid-compat.tsx +347 -0
  228. package/src/solid-surfaces.tsx +1311 -0
  229. package/src/solid.tsx +82 -60
  230. package/src/store-core/index.ts +426 -0
  231. package/src/store.ts +12 -0
  232. package/src/svelte/SocialLinks.svelte +84 -0
  233. package/src/svelte/headless.ts +435 -0
  234. package/src/svelte/index.ts +9 -0
  235. package/src/vanilla.ts +367 -0
  236. package/src/verify-core/index.ts +288 -0
  237. package/src/verify.ts +8 -0
  238. package/src/vue/headless.ts +527 -0
  239. package/src/vue/index.ts +3 -0
package/src/contracts.ts CHANGED
@@ -541,21 +541,44 @@ export const chatbotFormFieldSchema = z
541
541
  .passthrough();
542
542
  export type ChatbotFormField = z.infer<typeof chatbotFormFieldSchema>;
543
543
 
544
- export const chatbotFormDefinitionSchema = z.object({
545
- id: z.string(),
546
- slug: z.string(),
547
- name: z.string(),
548
- description: z.string(),
549
- fields: z.array(chatbotFormFieldSchema),
550
- addsInquiry: z.boolean(),
551
- addsLead: z.boolean(),
552
- });
544
+ export const chatbotFormDefinitionSchema = z
545
+ .object({
546
+ id: z.string(),
547
+ slug: z.string(),
548
+ name: z.string(),
549
+ description: z.string(),
550
+ fields: z.array(chatbotFormFieldSchema),
551
+ addsInquiry: z.boolean(),
552
+ addsLead: z.boolean(),
553
+ /**
554
+ * The RENDER contract — animation, multi-step + progress style, exit
555
+ * confirmation, URL safety. The server has always sent this; the schema
556
+ * dropped it, so anyone building a form UI on the typed client got a
557
+ * bare field list and none of the behaviour the dashboard configured.
558
+ */
559
+ settings: z.record(z.unknown()).nullish(),
560
+ /** Org avatar for the form header, when the form opts in. */
561
+ orgIcon: z.string().nullish(),
562
+ /** Public action previews ("submitting adds you to the list"). */
563
+ actions: z.array(z.record(z.unknown())).nullish(),
564
+ /** Non-sensitive business functions so `calculation` fields compute. */
565
+ businessFunctions: z.array(z.record(z.unknown())).nullish(),
566
+ })
567
+ // Additive server fields reach consumers instead of being silently dropped
568
+ // — the failure this schema just demonstrated.
569
+ .passthrough();
553
570
  export type ChatbotFormDefinition = z.infer<typeof chatbotFormDefinitionSchema>;
554
571
 
555
572
  export const chatbotFormSubmitInputSchema = z.object({
556
573
  data: z.record(z.unknown()),
557
574
  submitterEmail: z.string().email().optional(),
558
575
  submitterName: z.string().optional(),
576
+ /** Validate and run the full pipeline WITHOUT persisting. */
577
+ dryRun: z.boolean().optional(),
578
+ /** Provenance the route accepts and this schema previously omitted. */
579
+ source: z.string().max(200).optional(),
580
+ referrer: z.string().max(500).optional(),
581
+ metadata: z.record(z.unknown()).optional(),
559
582
  priorChat: z
560
583
  .array(
561
584
  z.object({
@@ -573,8 +596,14 @@ export type ChatbotFormSubmitInput = z.infer<
573
596
 
574
597
  export const chatbotFormSubmitResponseSchema = z.object({
575
598
  success: z.literal(true),
576
- submissionId: z.string(),
599
+ /**
600
+ * NULL on a dry run — the submission validated but was deliberately not
601
+ * persisted. Requiring a string here made every test submit throw.
602
+ */
603
+ submissionId: z.string().nullable(),
577
604
  slug: z.string(),
605
+ /** True when nothing was written. Pairs with a null `submissionId`. */
606
+ dryRun: z.boolean().optional(),
578
607
  });
579
608
  export type ChatbotFormSubmitResponse = z.infer<
580
609
  typeof chatbotFormSubmitResponseSchema
@@ -966,6 +995,44 @@ export const customerPortalProfileSchema = z
966
995
  })
967
996
  .nullable();
968
997
 
998
+ /**
999
+ * One quiz outcome for the signed-in customer. `scoreWithheld` marks an
1000
+ * interview-mode quiz: the attempt exists, the verdict is staff-only —
1001
+ * render "completed" and nothing else.
1002
+ */
1003
+ export const customerPortalQuizResultSchema = z.object({
1004
+ invitationId: z.string(),
1005
+ quizName: z.string(),
1006
+ competency: z.string().nullable(),
1007
+ status: z.string(),
1008
+ completedAt: z.string().nullable(),
1009
+ scoreWithheld: z.boolean(),
1010
+ result: z
1011
+ .object({
1012
+ status: z.string(),
1013
+ scorePercent: z.number().nullable(),
1014
+ provisionalScorePercent: z.number().nullable(),
1015
+ passed: z.boolean().nullable(),
1016
+ pointsEarned: z.number().nullable(),
1017
+ pointsPossible: z.number().nullable(),
1018
+ passingScore: z.number(),
1019
+ results: z.array(z.record(z.unknown())),
1020
+ submittedAt: z.string().nullable(),
1021
+ durationSeconds: z.number().nullable(),
1022
+ })
1023
+ .nullable(),
1024
+ });
1025
+ export type CustomerPortalQuizResult = z.infer<
1026
+ typeof customerPortalQuizResultSchema
1027
+ >;
1028
+
1029
+ export const customerPortalQuizResultsResponseSchema = z.object({
1030
+ items: z.array(customerPortalQuizResultSchema),
1031
+ });
1032
+ export type CustomerPortalQuizResultsResponse = z.infer<
1033
+ typeof customerPortalQuizResultsResponseSchema
1034
+ >;
1035
+
969
1036
  export const customerPortalUpdateProfileInputSchema = z.object({
970
1037
  displayName: z.string().min(1).max(120).optional(),
971
1038
  profile: z.record(z.unknown()).optional(),
@@ -1496,6 +1563,16 @@ export const customerReturnRequestSchema = z.object({
1496
1563
  * are separate enums and each is spelled as the database spells it.
1497
1564
  */
1498
1565
  status: openEnum("requested", "approved", "denied", "refunded", "cancelled"),
1566
+ /**
1567
+ * What was asked for: money back, or a replacement. Mirrors
1568
+ * `order_return_request_kind`. Rows created before this existed read
1569
+ * `refund`, which is what they were.
1570
+ */
1571
+ kind: openEnum("refund", "exchange"),
1572
+ /** On an exchange: the variant wanted instead, when one was chosen. */
1573
+ exchangeForVariantId: z.string().nullable(),
1574
+ /** Set once a replacement ships — track it like any other shipment. */
1575
+ exchangeShipmentId: z.string().nullable(),
1499
1576
  reason: z.string().nullable(),
1500
1577
  items: z
1501
1578
  .array(
@@ -1542,6 +1619,16 @@ export const customerOrderDetailSchema = z.object({
1542
1619
 
1543
1620
  /** Input for `submitReturnRequest`. Omit `items` for a whole-order request. */
1544
1621
  export const customerReturnRequestInputSchema = z.object({
1622
+ /**
1623
+ * `refund` returns the money; `exchange` asks for a replacement. Defaults
1624
+ * to `refund`, so an omitted field means what it always meant.
1625
+ */
1626
+ kind: z.enum(["refund", "exchange"]).optional(),
1627
+ /**
1628
+ * On an exchange, the variant wanted instead. Optional even then — "a
1629
+ * different size" is a real request that needs a conversation first.
1630
+ */
1631
+ exchangeForVariantId: z.string().uuid().optional(),
1545
1632
  reason: z.string().max(120).optional(),
1546
1633
  customerNote: z.string().max(2000).optional(),
1547
1634
  items: z
@@ -1559,6 +1646,372 @@ export const customerReturnRequestCreatedSchema = z.object({
1559
1646
  id: z.string().nullable(),
1560
1647
  /** Mirrors the `order_return_request_status` pg enum. */
1561
1648
  status: openEnum("requested", "approved", "denied", "refunded", "cancelled"),
1649
+ /** Echoes back what was opened, so a UI need not assume. */
1650
+ kind: openEnum("refund", "exchange"),
1651
+ createdAt: z.string().nullable(),
1652
+ });
1653
+
1654
+ /**
1655
+ * Result of reporting that a parcel never arrived.
1656
+ *
1657
+ * `carrierStatus` is the carrier's own verdict, UNTOUCHED by the report — the
1658
+ * two facts travel together on purpose. When `contested` is true the carrier
1659
+ * says delivered and the customer says otherwise, which is the case an org
1660
+ * needs to see first.
1661
+ */
1662
+ export const customerNotReceivedResultSchema = z.object({
1663
+ id: z.string(),
1664
+ /** True when a claim was already open; the original report is unchanged. */
1665
+ alreadyOpen: z.boolean(),
1666
+ reportedAt: z.string(),
1667
+ carrierStatus: openEnum(
1668
+ "pending",
1669
+ "label_created",
1670
+ "in_transit",
1671
+ "delivered",
1672
+ "returned",
1673
+ ),
1674
+ carrierDeliveredAt: z.string().nullable(),
1675
+ contested: z.boolean().optional(),
1676
+ });
1677
+
1678
+ /** How an org's legal pages are dressed. Shared by all of them. */
1679
+ export const legalPresentationSchema = z.object({
1680
+ /** Already resolved against the company logo — render as given. */
1681
+ logoUrl: z.string().nullable(),
1682
+ showLogoInHeader: z.boolean(),
1683
+ showLogoInFooter: z.boolean(),
1684
+ headerHtml: z.string().nullable(),
1685
+ footerHtml: z.string().nullable(),
1686
+ });
1687
+
1688
+ /** A legal page in a list — enough for a footer, without the body. */
1689
+ export const legalDocumentSummarySchema = z.object({
1690
+ /** The URL path, without a leading slash: `privacy`, `terms`, … */
1691
+ slug: z.string(),
1692
+ title: z.string(),
1693
+ version: z.number(),
1694
+ /** ISO 8601. Render as "Last updated". */
1695
+ effectiveDate: z.string().nullable(),
1696
+ showInFooter: z.boolean(),
1697
+ updatedAt: z.string(),
1698
+ });
1699
+
1700
+ export const legalListResponseSchema = z.object({
1701
+ items: z.array(legalDocumentSummarySchema),
1702
+ presentation: legalPresentationSchema,
1703
+ });
1704
+
1705
+ /** One legal page, body included. */
1706
+ export const legalDocumentSchema = legalDocumentSummarySchema.extend({
1707
+ /** Sanitised server-side on write, so this is safe to render as HTML. */
1708
+ contentHtml: z.string(),
1709
+ presentation: legalPresentationSchema,
1710
+ });
1711
+
1712
+ export type LegalPresentation = z.infer<typeof legalPresentationSchema>;
1713
+ export type LegalDocumentSummary = z.infer<typeof legalDocumentSummarySchema>;
1714
+ export type LegalListResponse = z.infer<typeof legalListResponseSchema>;
1715
+ export type LegalDocument = z.infer<typeof legalDocumentSchema>;
1716
+
1717
+ /** One browser a customer has registered for push. */
1718
+ export const customerPushDeviceSchema = z.object({
1719
+ id: z.string(),
1720
+ endpoint: z.string(),
1721
+ userAgent: z.string().nullable(),
1722
+ label: z.string().nullable(),
1723
+ /** Null = every category. `[]` = none, but the device stays registered. */
1724
+ categories: z.array(z.string()).nullable(),
1725
+ lastSeenAt: z.string().nullable(),
1726
+ createdAt: z.string(),
1727
+ });
1728
+
1729
+ /**
1730
+ * Push config plus this customer's devices.
1731
+ *
1732
+ * `vapidPublicKey` rides along because a page cannot call `subscribe()` without
1733
+ * it, and a second round trip for one string is one nobody remembers to make.
1734
+ * `enabled: false` means the platform has no key configured — render nothing
1735
+ * rather than a button that cannot work.
1736
+ */
1737
+ export const customerPushConfigSchema = z.object({
1738
+ vapidPublicKey: z.string().nullable(),
1739
+ enabled: z.boolean(),
1740
+ devices: z.array(customerPushDeviceSchema),
1741
+ });
1742
+
1743
+ export type CustomerPushDevice = z.infer<typeof customerPushDeviceSchema>;
1744
+ export type CustomerPushConfig = z.infer<typeof customerPushConfigSchema>;
1745
+
1746
+ /** Result of handing a customer-portal invite link out again. */
1747
+ export const customerInviteResendResponseSchema = z.object({
1748
+ id: z.string(),
1749
+ /** The NEW link. The previous one no longer works. */
1750
+ url: z.string(),
1751
+ expiresAt: z.string().nullable(),
1752
+ /** Includes this send, so a list can show "sent 3 times". */
1753
+ sendCount: z.number(),
1754
+ });
1755
+
1756
+ export type CustomerInviteResendResponse = z.infer<
1757
+ typeof customerInviteResendResponseSchema
1758
+ >;
1759
+
1760
+ // ── Scheduling / conference calls ─────────────────────────────────────
1761
+ //
1762
+ // The booking surface: what an org offers, when it is free, and the three
1763
+ // token-gated things an invitee can do afterwards.
1764
+ //
1765
+ // `conferenceLink` is the conference-call half — a Meet/Zoom/Teams URL minted
1766
+ // when the event type has a provider. It is null until the booking is
1767
+ // confirmed, and re-minted on reschedule, so a UI must read it back after
1768
+ // either rather than caching the one it first saw.
1769
+
1770
+ export const schedulingEventTypeSchema = z.object({
1771
+ id: z.string(),
1772
+ name: z.string(),
1773
+ slug: z.string(),
1774
+ description: z.string().nullable(),
1775
+ durationMinutes: z.number(),
1776
+ locationType: z.string(),
1777
+ /** google_meet | zoom | teams | null when the meeting is in person. */
1778
+ meetingProvider: z.string().nullable(),
1779
+ color: z.string().nullable(),
1780
+ /** When true a booking lands `pending`, not `confirmed`. */
1781
+ requiresApproval: z.boolean(),
1782
+ });
1783
+
1784
+ export const schedulingQuestionSchema = z
1785
+ .object({
1786
+ id: z.string(),
1787
+ label: z.string(),
1788
+ })
1789
+ .passthrough();
1790
+
1791
+ export const schedulingEventTypeListSchema = z.object({
1792
+ eventTypes: z.array(schedulingEventTypeSchema),
1793
+ });
1794
+
1795
+ export const schedulingEventTypeDetailSchema = z.object({
1796
+ eventType: schedulingEventTypeSchema,
1797
+ questions: z.array(schedulingQuestionSchema),
1798
+ });
1799
+
1800
+ export const schedulingSlotSchema = z.object({
1801
+ startAt: z.string(),
1802
+ endAt: z.string(),
1803
+ });
1804
+
1805
+ export const schedulingSlotsResponseSchema = z.object({
1806
+ slots: z.array(schedulingSlotSchema),
1807
+ });
1808
+
1809
+ export const schedulingBookingStatusSchema = openEnum(
1810
+ "pending",
1811
+ "confirmed",
1812
+ "canceled",
1813
+ "rescheduled",
1814
+ );
1815
+
1816
+ export const schedulingBookingViewSchema = z.object({
1817
+ booking: z.object({
1818
+ id: z.string(),
1819
+ startAt: z.string(),
1820
+ endAt: z.string(),
1821
+ status: schedulingBookingStatusSchema,
1822
+ title: z.string(),
1823
+ notes: z.string().nullable(),
1824
+ /** The conference URL, once there is one. */
1825
+ conferenceLink: z.string().nullable(),
1826
+ }),
1827
+ invitee: z
1828
+ .object({ name: z.string(), email: z.string(), timezone: z.string() })
1829
+ .nullable(),
1830
+ });
1831
+
1832
+ export const schedulingBookingResultSchema = z
1833
+ .object({
1834
+ bookingId: z.string(),
1835
+ status: schedulingBookingStatusSchema,
1836
+ /**
1837
+ * Three separate signed tokens, deliberately.
1838
+ *
1839
+ * A cancel link in an email must not also be able to reschedule, and
1840
+ * neither should read the whole booking. Handing out one token for all
1841
+ * three would make every emailed link as powerful as the strongest
1842
+ * action it enables.
1843
+ */
1844
+ manageToken: z.string(),
1845
+ rescheduleToken: z.string(),
1846
+ cancelToken: z.string(),
1847
+ })
1848
+ .passthrough();
1849
+
1850
+ export const schedulingRescheduleResultSchema = z
1851
+ .object({
1852
+ bookingId: z.string(),
1853
+ startAt: z.string(),
1854
+ endAt: z.string(),
1855
+ /** Re-minted on reschedule — read it back rather than reusing the old one. */
1856
+ conferenceLink: z.string().nullable().optional(),
1857
+ })
1858
+ .passthrough();
1859
+
1860
+ export const schedulingCancelResultSchema = z
1861
+ .object({ ok: z.boolean() })
1862
+ .passthrough();
1863
+
1864
+ export type SchedulingEventTypeContract = z.infer<
1865
+ typeof schedulingEventTypeSchema
1866
+ >;
1867
+ export type SchedulingEventTypeList = z.infer<
1868
+ typeof schedulingEventTypeListSchema
1869
+ >;
1870
+ export type SchedulingEventTypeDetail = z.infer<
1871
+ typeof schedulingEventTypeDetailSchema
1872
+ >;
1873
+ export type SchedulingSlotsResponse = z.infer<
1874
+ typeof schedulingSlotsResponseSchema
1875
+ >;
1876
+ export type SchedulingBookingView = z.infer<typeof schedulingBookingViewSchema>;
1877
+ export type SchedulingBookingResult = z.infer<
1878
+ typeof schedulingBookingResultSchema
1879
+ >;
1880
+ export type SchedulingRescheduleResult = z.infer<
1881
+ typeof schedulingRescheduleResultSchema
1882
+ >;
1883
+ export type SchedulingCancelResult = z.infer<
1884
+ typeof schedulingCancelResultSchema
1885
+ >;
1886
+
1887
+ /** One of the org's live subscription offerings. */
1888
+ export const customerSubscriptionOfferingSchema = z.object({
1889
+ id: z.string(),
1890
+ name: z.string(),
1891
+ description: z.string().nullable(),
1892
+ imageUrl: z.string().nullable(),
1893
+ amountCents: z.number(),
1894
+ interval: z.string(),
1895
+ });
1896
+
1897
+ /** The customer's subscription state with one org. */
1898
+ export const customerSubscriptionSchema = z.object({
1899
+ /** active | expired | cancelled | pending | failed */
1900
+ status: openEnum("active", "expired", "cancelled", "pending", "failed"),
1901
+ /** trial | monthly_subscribed | annual_subscribed | lifetime */
1902
+ type: openEnum(
1903
+ "trial",
1904
+ "monthly_subscribed",
1905
+ "annual_subscribed",
1906
+ "lifetime",
1907
+ ).nullable(),
1908
+ /**
1909
+ * The entitlement decision, computed server-side by the same function the
1910
+ * content gates use.
1911
+ *
1912
+ * **Render from this, not from `status`.** Two cases break a naive
1913
+ * `status === "active"` check: a lifetime purchase has no period to expire,
1914
+ * and a cancelled subscription keeps access until the period already paid
1915
+ * for runs out.
1916
+ */
1917
+ hasAccess: z.boolean(),
1918
+ /**
1919
+ * Cancelled, but the paid period has not ended. The honest label is
1920
+ * "active until {expiresAt}" — not "cancelled".
1921
+ */
1922
+ cancelledButActive: z.boolean(),
1923
+ startedAt: z.string().nullable(),
1924
+ /** End of the paid period. Null for lifetime. */
1925
+ expiresAt: z.string().nullable(),
1926
+ cancelledAt: z.string().nullable(),
1927
+ renewedAt: z.string().nullable(),
1928
+ trialEndsAt: z.string().nullable(),
1929
+ trialUsed: z.boolean(),
1930
+ provider: z.string().nullable(),
1931
+ /** The offering they are on, when it still exists. */
1932
+ plan: customerSubscriptionOfferingSchema.nullable(),
1933
+ /** Null means the plan includes no service calls at all. */
1934
+ includedServiceCallsPerPeriod: z.number().nullable(),
1935
+ serviceCallsUsedInPeriod: z.number(),
1936
+ periodStartedAt: z.string().nullable(),
1937
+ });
1938
+
1939
+ export const customerSubscriptionResponseSchema = z.object({
1940
+ /** Null when this customer has never subscribed to this org. */
1941
+ subscription: customerSubscriptionSchema.nullable(),
1942
+ /** The org's live offerings, cheapest first. */
1943
+ offerings: z.array(customerSubscriptionOfferingSchema),
1944
+ });
1945
+
1946
+ /** Result of cancelling — or un-cancelling — a subscription. */
1947
+ export const customerSubscriptionCancelResponseSchema = z.object({
1948
+ /** True when a cancellation is now pending at the end of the period. */
1949
+ cancelAtPeriodEnd: z.boolean(),
1950
+ resumed: z.boolean(),
1951
+ /**
1952
+ * Access continues until this moment. Say "active until {this}", because
1953
+ * that is what is true — cancelling ends the renewal, not the access.
1954
+ */
1955
+ accessUntil: z.string().nullable(),
1956
+ /**
1957
+ * The stored status still reads its old value until the provider's webhook
1958
+ * lands. Render from `cancelAtPeriodEnd`, not from a re-read.
1959
+ */
1960
+ statusPending: z.boolean(),
1961
+ });
1962
+
1963
+ /** One piece of subscriber-gated content. */
1964
+ export const customerSubscriberContentItemSchema = z.object({
1965
+ id: z.string(),
1966
+ slug: z.string(),
1967
+ title: z.string(),
1968
+ /** The pitch. Travels even when locked — the BODY never does. */
1969
+ excerpt: z.string().nullable(),
1970
+ featuredImageUrl: z.string().nullable(),
1971
+ publishedAt: z.string().nullable(),
1972
+ tags: z.array(z.string()),
1973
+ accessLevel: z.string(),
1974
+ downloadAccessLevel: z.string(),
1975
+ /** False when the post is readable but its downloads are not. */
1976
+ downloadsAvailable: z.boolean(),
1977
+ });
1978
+
1979
+ export const customerSubscriberContentResponseSchema = z.object({
1980
+ items: z.array(customerSubscriberContentItemSchema),
1981
+ /**
1982
+ * False means these are LOCKED previews, not an empty entitlement. Show
1983
+ * them with the offer — an empty list would hide the pitch at exactly the
1984
+ * moment it is most relevant.
1985
+ */
1986
+ entitled: z.boolean(),
1987
+ });
1988
+
1989
+ /** One product review the customer has written. */
1990
+ export const customerProductReviewSchema = z.object({
1991
+ id: z.string(),
1992
+ productId: z.string(),
1993
+ productName: z.string(),
1994
+ rating: z.number(),
1995
+ content: z.string(),
1996
+ /**
1997
+ * False means QUEUED for the org to approve, not rejected. Surface it as
1998
+ * "awaiting review" — a customer who thinks their review vanished writes it
1999
+ * again, or writes to support.
2000
+ */
2001
+ isApproved: z.boolean(),
2002
+ createdAt: z.string(),
2003
+ });
2004
+
2005
+ export const customerProductReviewListSchema = z.object({
2006
+ items: z.array(customerProductReviewSchema),
2007
+ });
2008
+
2009
+ /** Result of submitting a product review from the portal. */
2010
+ export const customerProductReviewCreatedSchema = z.object({
2011
+ id: z.string().nullable(),
2012
+ isApproved: z.boolean(),
2013
+ /** The inverse of `isApproved`, named for what the UI should say. */
2014
+ awaitingModeration: z.boolean(),
1562
2015
  createdAt: z.string().nullable(),
1563
2016
  });
1564
2017
 
@@ -1615,6 +2068,32 @@ export type CustomerReturnRequestInput = z.infer<
1615
2068
  export type CustomerReturnRequestCreated = z.infer<
1616
2069
  typeof customerReturnRequestCreatedSchema
1617
2070
  >;
2071
+ export type CustomerNotReceivedResult = z.infer<
2072
+ typeof customerNotReceivedResultSchema
2073
+ >;
2074
+ export type CustomerProductReview = z.infer<typeof customerProductReviewSchema>;
2075
+ export type CustomerProductReviewList = z.infer<
2076
+ typeof customerProductReviewListSchema
2077
+ >;
2078
+ export type CustomerProductReviewCreated = z.infer<
2079
+ typeof customerProductReviewCreatedSchema
2080
+ >;
2081
+ export type CustomerSubscriptionOffering = z.infer<
2082
+ typeof customerSubscriptionOfferingSchema
2083
+ >;
2084
+ export type CustomerSubscription = z.infer<typeof customerSubscriptionSchema>;
2085
+ export type CustomerSubscriptionResponse = z.infer<
2086
+ typeof customerSubscriptionResponseSchema
2087
+ >;
2088
+ export type CustomerSubscriptionCancelResponse = z.infer<
2089
+ typeof customerSubscriptionCancelResponseSchema
2090
+ >;
2091
+ export type CustomerSubscriberContentItem = z.infer<
2092
+ typeof customerSubscriberContentItemSchema
2093
+ >;
2094
+ export type CustomerSubscriberContentResponse = z.infer<
2095
+ typeof customerSubscriberContentResponseSchema
2096
+ >;
1618
2097
  export type CustomerDownload = z.infer<typeof customerDownloadSchema>;
1619
2098
  export type CustomerDownloadsResponse = z.infer<
1620
2099
  typeof customerDownloadsResponseSchema
@@ -1646,6 +2125,148 @@ export type CustomerInvoicePayment = z.infer<
1646
2125
  export type CustomerPaymentHistoryEntry = z.infer<
1647
2126
  typeof customerPaymentHistoryEntrySchema
1648
2127
  >;
2128
+ // ── Customer portal: list endpoints (0.9.61) ──────────────────────────
2129
+ //
2130
+ // The portal had detail-by-id routes for invoices, quotes, contracts and
2131
+ // shipments and no way to LIST any of them, so "show me everything I owe" had
2132
+ // no call behind it.
2133
+ //
2134
+ // The rows reuse the existing `customer*SummarySchema` shapes rather than
2135
+ // defining parallel ones — those already carry the right open enums, and two
2136
+ // definitions of "an invoice summary" would drift.
2137
+
2138
+ export const customerInvoiceListResponseSchema = z.object({
2139
+ items: z.array(customerInvoiceSummarySchema),
2140
+ });
2141
+
2142
+ export const customerQuoteListResponseSchema = z.object({
2143
+ items: z.array(customerQuoteSummarySchema),
2144
+ });
2145
+
2146
+ export const customerContractListResponseSchema = z.object({
2147
+ items: z.array(customerContractSummarySchema),
2148
+ });
2149
+
2150
+ /** No summary shape existed for shipments, so this is the first one. */
2151
+ export const customerShipmentSummarySchema = z.object({
2152
+ id: z.string(),
2153
+ orderId: z.string(),
2154
+ orderNumber: z.string().nullable(),
2155
+ carrier: z.string().nullable(),
2156
+ trackingNumber: z.string().nullable(),
2157
+ trackingUrl: z.string().nullable(),
2158
+ serviceName: z.string().nullable(),
2159
+ status: z.string(),
2160
+ /** A date STRING, not a timestamp — the column is text. */
2161
+ estimatedDelivery: z.string().nullable(),
2162
+ shippedAt: z.string().nullable(),
2163
+ deliveredAt: z.string().nullable(),
2164
+ });
2165
+
2166
+ export const customerShipmentListResponseSchema = z.object({
2167
+ items: z.array(customerShipmentSummarySchema),
2168
+ });
2169
+
2170
+ export type CustomerInvoiceListResponse = z.infer<
2171
+ typeof customerInvoiceListResponseSchema
2172
+ >;
2173
+ export type CustomerQuoteListResponse = z.infer<
2174
+ typeof customerQuoteListResponseSchema
2175
+ >;
2176
+ export type CustomerContractListResponse = z.infer<
2177
+ typeof customerContractListResponseSchema
2178
+ >;
2179
+ export type CustomerShipmentSummary = z.infer<
2180
+ typeof customerShipmentSummarySchema
2181
+ >;
2182
+ export type CustomerShipmentListResponse = z.infer<
2183
+ typeof customerShipmentListResponseSchema
2184
+ >;
2185
+
2186
+ // ── Customer portal: staff chat (0.9.61) ──────────────────────────────
2187
+ //
2188
+ // `org_customer_messages` has always carried a `portal` channel and staff have
2189
+ // always read and written it from the CRM timeline. What was missing was any
2190
+ // package-api route, so a customer could be messaged and had no way to read it
2191
+ // back or reply. Both directions land in the SAME table the staff timeline
2192
+ // reads — a reply appears in the CRM beside everything else about that
2193
+ // customer, not in a parallel inbox nobody watches.
2194
+
2195
+ export const customerPortalMessageSchema = z.object({
2196
+ id: z.string(),
2197
+ /** `inbound` = the customer wrote it; `outbound` = staff did. */
2198
+ direction: openEnum("inbound", "outbound"),
2199
+ channel: openEnum("portal", "email", "sms"),
2200
+ authorName: z.string().nullable(),
2201
+ body: z.string(),
2202
+ attachment: z
2203
+ .object({
2204
+ url: z.string(),
2205
+ name: z.string(),
2206
+ mimeType: z.string(),
2207
+ sizeBytes: z.number(),
2208
+ })
2209
+ .nullable(),
2210
+ jobId: z.string().nullable(),
2211
+ readByCustomerAt: z.string().nullable(),
2212
+ createdAt: z.string(),
2213
+ });
2214
+
2215
+ export const customerPortalMessageListResponseSchema = z.object({
2216
+ items: z.array(customerPortalMessageSchema),
2217
+ });
2218
+
2219
+ export const customerPortalMessagePostInputSchema = z.object({
2220
+ body: z.string().min(1).max(4000),
2221
+ /** Thread the message under a specific job. */
2222
+ jobId: z.string().uuid().optional(),
2223
+ });
2224
+
2225
+ export const customerPortalMessagesMarkReadResponseSchema = z.object({
2226
+ marked: z.number(),
2227
+ });
2228
+
2229
+ export type CustomerPortalMessage = z.infer<typeof customerPortalMessageSchema>;
2230
+ export type CustomerPortalMessageListResponse = z.infer<
2231
+ typeof customerPortalMessageListResponseSchema
2232
+ >;
2233
+ export type CustomerPortalMessagePostInput = z.infer<
2234
+ typeof customerPortalMessagePostInputSchema
2235
+ >;
2236
+ export type CustomerPortalMessagesMarkReadResponse = z.infer<
2237
+ typeof customerPortalMessagesMarkReadResponseSchema
2238
+ >;
2239
+
2240
+ // ── Customer portal: return status readback (0.9.61) ──────────────────
2241
+ //
2242
+ // Closes the audit's gap #5. The submit route existed and nothing read the
2243
+ // status back, so a customer could start a return and then had no way to see
2244
+ // what happened to it — which is the exact moment they pick up the phone.
2245
+
2246
+ export const customerReturnRequestStatusSchema = z.object({
2247
+ id: z.string(),
2248
+ /** Mirrors the `order_return_request_status` pg enum. */
2249
+ status: openEnum("requested", "approved", "denied", "refunded", "cancelled"),
2250
+ reason: z.string().nullable(),
2251
+ customerNote: z.string().nullable(),
2252
+ /** Staff's answer. The whole point when a return is denied. */
2253
+ resolutionNote: z.string().nullable(),
2254
+ items: z.array(z.record(z.string(), z.unknown())).nullable(),
2255
+ resolvedAt: z.string().nullable(),
2256
+ createdAt: z.string(),
2257
+ });
2258
+
2259
+ export const customerReturnRequestListResponseSchema = z.object({
2260
+ items: z.array(customerReturnRequestStatusSchema),
2261
+ });
2262
+
2263
+ export type CustomerReturnRequestStatus = z.infer<
2264
+ typeof customerReturnRequestStatusSchema
2265
+ >;
2266
+ export type CustomerReturnRequestListResponse = z.infer<
2267
+ typeof customerReturnRequestListResponseSchema
2268
+ >;
2269
+
1649
2270
  export type CustomerPaymentHistoryResponse = z.infer<
1650
2271
  typeof customerPaymentHistoryResponseSchema
1651
2272
  >;
@@ -1752,7 +2373,19 @@ export type CustomerContactCompanyResponse = z.infer<
1752
2373
  export const customerJobEtaAssignmentSchema = z.object({
1753
2374
  displayName: z.string(),
1754
2375
  role: z.string().nullable(),
1755
- dispatchStatus: z.string(),
2376
+ /**
2377
+ * This technician's own stage. `openEnum` because the column is TEXT and
2378
+ * an org's board can grow a value before the SDK knows about it — an
2379
+ * unknown status should render as itself, not break the response.
2380
+ */
2381
+ dispatchStatus: openEnum(
2382
+ "pending",
2383
+ "en_route",
2384
+ "on_site",
2385
+ "in_progress",
2386
+ "completed",
2387
+ "declined",
2388
+ ),
1756
2389
  lastKnownLocation: z
1757
2390
  .object({
1758
2391
  lat: z.number(),
@@ -1763,6 +2396,35 @@ export const customerJobEtaAssignmentSchema = z.object({
1763
2396
  });
1764
2397
 
1765
2398
  export const customerJobEtaResponseSchema = z.object({
2399
+ /**
2400
+ * Where the JOB is — promoted from the assignees by the dispatch board,
2401
+ * NOT the same as any one technician's status.
2402
+ *
2403
+ * The promotion rules are why this is returned rather than left to the
2404
+ * consumer: `en_route`/`arrived`/`started` land on the FIRST assignee to
2405
+ * reach them, while `completed` waits for the LAST. Aggregating that
2406
+ * client-side gets it wrong in the direction that tells a customer the work
2407
+ * is finished when one of three techs has packed up.
2408
+ */
2409
+ dispatchStatus: openEnum(
2410
+ "unscheduled",
2411
+ "scheduled",
2412
+ "en_route",
2413
+ "arrived",
2414
+ "started",
2415
+ "completed",
2416
+ ),
2417
+ /**
2418
+ * When each stage happened. A customer reading "en route" wants to know
2419
+ * since when — a status with no timestamp is why people phone to ask.
2420
+ */
2421
+ timeline: z.object({
2422
+ scheduledStartAt: z.string().nullable(),
2423
+ enRouteAt: z.string().nullable(),
2424
+ arrivedAt: z.string().nullable(),
2425
+ startedAt: z.string().nullable(),
2426
+ completedAt: z.string().nullable(),
2427
+ }),
1766
2428
  assignments: z.array(customerJobEtaAssignmentSchema),
1767
2429
  eta: z
1768
2430
  .object({
@@ -1973,6 +2635,30 @@ export const storefrontProductRowSchema = z
1973
2635
  /** Resolved category (name/description), when the product is categorized. */
1974
2636
  category: storefrontProductCategorySchema.nullable().optional(),
1975
2637
  images: z.array(z.string()).nullable().optional(),
2638
+ /**
2639
+ * Web-renderable 3D model — `.glb`, `.gltf`, `.obj` or `.stl`.
2640
+ *
2641
+ * All four fields below are ABSENT (not null) unless the org has the
2642
+ * `ecommerce.product_3d` add-on. Absence is the normal case: most
2643
+ * businesses do not sell anything with a model. Use
2644
+ * `@businessdash/sdk/model3d` to turn these into something renderable.
2645
+ */
2646
+ model3dUrl: z.string().nullable().optional(),
2647
+ /** Apple AR Quick Look companion, `.usdz`. iOS/iPadOS/visionOS Safari. */
2648
+ model3dUsdzUrl: z.string().nullable().optional(),
2649
+ /**
2650
+ * True when the org attached a CAD file (`.step`/`.iges`/native).
2651
+ *
2652
+ * The URL itself is deliberately NOT here. CAD files are the largest
2653
+ * thing the platform serves and are the org's intellectual property, so
2654
+ * they are issued by `GET storefront/products/{id}/cad`, which requires
2655
+ * cookies, rate-limits the caller and re-checks the entitlement.
2656
+ * Publishing the raw URL in a payload any visitor can fetch would route
2657
+ * around all three.
2658
+ */
2659
+ model3dCadAvailable: z.boolean().optional(),
2660
+ /** Filename for the download link — "bracket-v3.step". */
2661
+ model3dCadName: z.string().nullable().optional(),
1976
2662
  createdAt: z.string(),
1977
2663
  })
1978
2664
  .passthrough();
@@ -1982,6 +2668,75 @@ export const storefrontListProductsResponseSchema = z.object({
1982
2668
  nextCursor: z.number().nullable(),
1983
2669
  });
1984
2670
 
2671
+ /**
2672
+ * Result of pushing the catalog to the org's own Stripe account.
2673
+ * Per-product outcomes rather than one all-or-nothing verdict: a single bad
2674
+ * product shouldn't abandon a batch, and the caller needs to know which.
2675
+ */
2676
+ export const couponSchema = z.object({
2677
+ id: z.string(),
2678
+ code: z.string(),
2679
+ discountType: z.string(),
2680
+ value: z.number(),
2681
+ isActive: z.boolean(),
2682
+ maxUses: z.number().nullable(),
2683
+ usedCount: z.number(),
2684
+ validUntil: z.string().nullable(),
2685
+ stripeCouponId: z.string().nullable(),
2686
+ stripePromotionCodeId: z.string().nullable(),
2687
+ });
2688
+ export type Coupon = z.infer<typeof couponSchema>;
2689
+
2690
+ export const couponCreateInputSchema = z.object({
2691
+ code: z.string().min(3).max(64),
2692
+ discountType: z.enum(["percent", "amount"]),
2693
+ value: z.number().positive(),
2694
+ validFrom: z.coerce.date().optional(),
2695
+ validUntil: z.coerce.date().nullish(),
2696
+ maxUses: z.number().int().positive().nullish(),
2697
+ isActive: z.boolean().optional(),
2698
+ duration: z.enum(["once", "forever", "repeating"]).optional(),
2699
+ durationInMonths: z.number().int().positive().nullish(),
2700
+ appliesTo: z.enum(["order", "shipping", "both"]).optional(),
2701
+ });
2702
+ export type CouponCreateInput = z.input<typeof couponCreateInputSchema>;
2703
+
2704
+ /**
2705
+ * `stripeSynced: false` means the coupon EXISTS locally but is not yet live in
2706
+ * Stripe — reported rather than thrown, so the caller can retry the sync
2707
+ * without re-creating the row.
2708
+ */
2709
+ export const couponCreateResponseSchema = z.object({
2710
+ coupon: couponSchema,
2711
+ stripeSynced: z.boolean(),
2712
+ stripeError: z.string().nullable(),
2713
+ });
2714
+ export type CouponCreateResponse = z.infer<typeof couponCreateResponseSchema>;
2715
+
2716
+ export const couponListResponseSchema = z.object({
2717
+ coupons: z.array(couponSchema),
2718
+ });
2719
+ export type CouponListResponse = z.infer<typeof couponListResponseSchema>;
2720
+
2721
+ export const storefrontSyncToStripeResponseSchema = z.object({
2722
+ synced: z.array(
2723
+ z.object({
2724
+ productId: z.string(),
2725
+ stripeProductId: z.string(),
2726
+ /** Variant combinations that got a Stripe Price. */
2727
+ combinationsSynced: z.number(),
2728
+ }),
2729
+ ),
2730
+ failed: z.array(z.object({ productId: z.string(), error: z.string() })),
2731
+ /** Products still unsynced after this call — keep calling until 0. */
2732
+ remaining: z.number(),
2733
+ /** Connected account written to; null means the platform account. */
2734
+ connectedAccountId: z.string().nullable(),
2735
+ });
2736
+ export type StorefrontSyncToStripeResponse = z.infer<
2737
+ typeof storefrontSyncToStripeResponseSchema
2738
+ >;
2739
+
1985
2740
  export const storefrontProductDetailSchema = storefrontProductRowSchema.and(
1986
2741
  z.object({
1987
2742
  variants: z.array(z.record(z.string(), z.unknown())),
@@ -2799,6 +3554,8 @@ const bundleCompanySchema = z.object({
2799
3554
 
2800
3555
  const bundleGalleryItemSchema = z.object({
2801
3556
  id: z.string(),
3557
+ /** Sent by the server on every item and dropped here until now. */
3558
+ description: z.string().nullish(),
2802
3559
  type: z.enum(["image", "video"]),
2803
3560
  src: z.string(),
2804
3561
  width: z.number().nullable(),