@mmailaender/convex-creem 0.1.0

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 (383) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1176 -0
  3. package/dist/client/helpers.d.ts +17 -0
  4. package/dist/client/helpers.d.ts.map +1 -0
  5. package/dist/client/helpers.js +43 -0
  6. package/dist/client/helpers.js.map +1 -0
  7. package/dist/client/index.d.ts +1041 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +1068 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/parsers.d.ts +45 -0
  12. package/dist/client/parsers.d.ts.map +1 -0
  13. package/dist/client/parsers.js +138 -0
  14. package/dist/client/parsers.js.map +1 -0
  15. package/dist/client/polyfill.d.ts +2 -0
  16. package/dist/client/polyfill.d.ts.map +1 -0
  17. package/dist/client/polyfill.js +3 -0
  18. package/dist/client/polyfill.js.map +1 -0
  19. package/dist/component/_generated/api.d.ts +36 -0
  20. package/dist/component/_generated/api.d.ts.map +1 -0
  21. package/dist/component/_generated/api.js +31 -0
  22. package/dist/component/_generated/api.js.map +1 -0
  23. package/dist/component/_generated/component.d.ts +542 -0
  24. package/dist/component/_generated/component.d.ts.map +1 -0
  25. package/dist/component/_generated/component.js +11 -0
  26. package/dist/component/_generated/component.js.map +1 -0
  27. package/dist/component/_generated/dataModel.d.ts +46 -0
  28. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  29. package/dist/component/_generated/dataModel.js +11 -0
  30. package/dist/component/_generated/dataModel.js.map +1 -0
  31. package/dist/component/_generated/server.d.ts +121 -0
  32. package/dist/component/_generated/server.d.ts.map +1 -0
  33. package/dist/component/_generated/server.js +78 -0
  34. package/dist/component/_generated/server.js.map +1 -0
  35. package/dist/component/convex.config.d.ts +3 -0
  36. package/dist/component/convex.config.d.ts.map +1 -0
  37. package/dist/component/convex.config.js +3 -0
  38. package/dist/component/convex.config.js.map +1 -0
  39. package/dist/component/lib.d.ts +1005 -0
  40. package/dist/component/lib.d.ts.map +1 -0
  41. package/dist/component/lib.js +647 -0
  42. package/dist/component/lib.js.map +1 -0
  43. package/dist/component/schema.d.ts +191 -0
  44. package/dist/component/schema.d.ts.map +1 -0
  45. package/dist/component/schema.js +104 -0
  46. package/dist/component/schema.js.map +1 -0
  47. package/dist/component/util.d.ts +61 -0
  48. package/dist/component/util.d.ts.map +1 -0
  49. package/dist/component/util.js +142 -0
  50. package/dist/component/util.js.map +1 -0
  51. package/dist/core/catalog.d.ts +18 -0
  52. package/dist/core/catalog.d.ts.map +1 -0
  53. package/dist/core/catalog.js +82 -0
  54. package/dist/core/catalog.js.map +1 -0
  55. package/dist/core/index.d.ts +9 -0
  56. package/dist/core/index.d.ts.map +1 -0
  57. package/dist/core/index.js +9 -0
  58. package/dist/core/index.js.map +1 -0
  59. package/dist/core/markdown.d.ts +12 -0
  60. package/dist/core/markdown.d.ts.map +1 -0
  61. package/dist/core/markdown.js +26 -0
  62. package/dist/core/markdown.js.map +1 -0
  63. package/dist/core/payments.d.ts +11 -0
  64. package/dist/core/payments.d.ts.map +1 -0
  65. package/dist/core/payments.js +27 -0
  66. package/dist/core/payments.js.map +1 -0
  67. package/dist/core/pendingCheckout.d.ts +15 -0
  68. package/dist/core/pendingCheckout.d.ts.map +1 -0
  69. package/dist/core/pendingCheckout.js +40 -0
  70. package/dist/core/pendingCheckout.js.map +1 -0
  71. package/dist/core/resolver.d.ts +11 -0
  72. package/dist/core/resolver.d.ts.map +1 -0
  73. package/dist/core/resolver.js +106 -0
  74. package/dist/core/resolver.js.map +1 -0
  75. package/dist/core/selectors.d.ts +12 -0
  76. package/dist/core/selectors.d.ts.map +1 -0
  77. package/dist/core/selectors.js +18 -0
  78. package/dist/core/selectors.js.map +1 -0
  79. package/dist/core/subscriptionUpdate.d.ts +20 -0
  80. package/dist/core/subscriptionUpdate.d.ts.map +1 -0
  81. package/dist/core/subscriptionUpdate.js +64 -0
  82. package/dist/core/subscriptionUpdate.js.map +1 -0
  83. package/dist/core/types.d.ts +170 -0
  84. package/dist/core/types.d.ts.map +1 -0
  85. package/dist/core/types.js +15 -0
  86. package/dist/core/types.js.map +1 -0
  87. package/dist/design-system/colors/color-utils.d.ts +10 -0
  88. package/dist/design-system/colors/color-utils.d.ts.map +1 -0
  89. package/dist/design-system/colors/color-utils.js +91 -0
  90. package/dist/design-system/colors/color-utils.js.map +1 -0
  91. package/dist/design-system/colors/config.d.ts +33 -0
  92. package/dist/design-system/colors/config.d.ts.map +1 -0
  93. package/dist/design-system/colors/config.js +224 -0
  94. package/dist/design-system/colors/config.js.map +1 -0
  95. package/dist/design-system/colors/index.d.ts +3 -0
  96. package/dist/design-system/colors/index.d.ts.map +1 -0
  97. package/dist/design-system/colors/index.js +3 -0
  98. package/dist/design-system/colors/index.js.map +1 -0
  99. package/dist/design-system/rounded/config.d.ts +31 -0
  100. package/dist/design-system/rounded/config.d.ts.map +1 -0
  101. package/dist/design-system/rounded/config.js +76 -0
  102. package/dist/design-system/rounded/config.js.map +1 -0
  103. package/dist/design-system/rounded/index.d.ts +2 -0
  104. package/dist/design-system/rounded/index.d.ts.map +1 -0
  105. package/dist/design-system/rounded/index.js +2 -0
  106. package/dist/design-system/rounded/index.js.map +1 -0
  107. package/dist/design-system/typography/config.d.ts +55 -0
  108. package/dist/design-system/typography/config.d.ts.map +1 -0
  109. package/dist/design-system/typography/config.js +308 -0
  110. package/dist/design-system/typography/config.js.map +1 -0
  111. package/dist/design-system/typography/index.d.ts +3 -0
  112. package/dist/design-system/typography/index.d.ts.map +1 -0
  113. package/dist/design-system/typography/index.js +3 -0
  114. package/dist/design-system/typography/index.js.map +1 -0
  115. package/dist/design-system/typography/tokens.d.ts +23 -0
  116. package/dist/design-system/typography/tokens.d.ts.map +1 -0
  117. package/dist/design-system/typography/tokens.js +99 -0
  118. package/dist/design-system/typography/tokens.js.map +1 -0
  119. package/dist/react/hooks/useCheckoutSuccessParams.d.ts +2 -0
  120. package/dist/react/hooks/useCheckoutSuccessParams.d.ts.map +1 -0
  121. package/dist/react/hooks/useCheckoutSuccessParams.js +5 -0
  122. package/dist/react/hooks/useCheckoutSuccessParams.js.map +1 -0
  123. package/dist/react/index.d.ts +25 -0
  124. package/dist/react/index.d.ts.map +1 -0
  125. package/dist/react/index.js +22 -0
  126. package/dist/react/index.js.map +1 -0
  127. package/dist/react/primitives/BillingGate.d.ts +8 -0
  128. package/dist/react/primitives/BillingGate.d.ts.map +1 -0
  129. package/dist/react/primitives/BillingGate.js +13 -0
  130. package/dist/react/primitives/BillingGate.js.map +1 -0
  131. package/dist/react/primitives/BillingToggle.d.ts +8 -0
  132. package/dist/react/primitives/BillingToggle.d.ts.map +1 -0
  133. package/dist/react/primitives/BillingToggle.js +12 -0
  134. package/dist/react/primitives/BillingToggle.js.map +1 -0
  135. package/dist/react/primitives/CheckoutButton.d.ts +11 -0
  136. package/dist/react/primitives/CheckoutButton.d.ts.map +1 -0
  137. package/dist/react/primitives/CheckoutButton.js +21 -0
  138. package/dist/react/primitives/CheckoutButton.js.map +1 -0
  139. package/dist/react/primitives/CheckoutSuccessSummary.d.ts +7 -0
  140. package/dist/react/primitives/CheckoutSuccessSummary.d.ts.map +1 -0
  141. package/dist/react/primitives/CheckoutSuccessSummary.js +11 -0
  142. package/dist/react/primitives/CheckoutSuccessSummary.js.map +1 -0
  143. package/dist/react/primitives/CustomerPortalButton.d.ts +8 -0
  144. package/dist/react/primitives/CustomerPortalButton.d.ts.map +1 -0
  145. package/dist/react/primitives/CustomerPortalButton.js +21 -0
  146. package/dist/react/primitives/CustomerPortalButton.js.map +1 -0
  147. package/dist/react/primitives/NumberInput.d.ts +11 -0
  148. package/dist/react/primitives/NumberInput.d.ts.map +1 -0
  149. package/dist/react/primitives/NumberInput.js +18 -0
  150. package/dist/react/primitives/NumberInput.js.map +1 -0
  151. package/dist/react/primitives/OneTimeCheckoutButton.d.ts +11 -0
  152. package/dist/react/primitives/OneTimeCheckoutButton.d.ts.map +1 -0
  153. package/dist/react/primitives/OneTimeCheckoutButton.js +4 -0
  154. package/dist/react/primitives/OneTimeCheckoutButton.js.map +1 -0
  155. package/dist/react/primitives/OneTimePaymentStatusBadge.d.ts +6 -0
  156. package/dist/react/primitives/OneTimePaymentStatusBadge.d.ts.map +1 -0
  157. package/dist/react/primitives/OneTimePaymentStatusBadge.js +11 -0
  158. package/dist/react/primitives/OneTimePaymentStatusBadge.js.map +1 -0
  159. package/dist/react/primitives/PaymentWarningBanner.d.ts +7 -0
  160. package/dist/react/primitives/PaymentWarningBanner.d.ts.map +1 -0
  161. package/dist/react/primitives/PaymentWarningBanner.js +18 -0
  162. package/dist/react/primitives/PaymentWarningBanner.js.map +1 -0
  163. package/dist/react/primitives/PricingCard.d.ts +37 -0
  164. package/dist/react/primitives/PricingCard.d.ts.map +1 -0
  165. package/dist/react/primitives/PricingCard.js +125 -0
  166. package/dist/react/primitives/PricingCard.js.map +1 -0
  167. package/dist/react/primitives/PricingSection.d.ts +39 -0
  168. package/dist/react/primitives/PricingSection.d.ts.map +1 -0
  169. package/dist/react/primitives/PricingSection.js +24 -0
  170. package/dist/react/primitives/PricingSection.js.map +1 -0
  171. package/dist/react/primitives/ScheduledChangeBanner.d.ts +8 -0
  172. package/dist/react/primitives/ScheduledChangeBanner.d.ts.map +1 -0
  173. package/dist/react/primitives/ScheduledChangeBanner.js +13 -0
  174. package/dist/react/primitives/ScheduledChangeBanner.js.map +1 -0
  175. package/dist/react/primitives/SegmentControl.d.ts +11 -0
  176. package/dist/react/primitives/SegmentControl.d.ts.map +1 -0
  177. package/dist/react/primitives/SegmentControl.js +8 -0
  178. package/dist/react/primitives/SegmentControl.js.map +1 -0
  179. package/dist/react/primitives/SegmentGroup.d.ts +14 -0
  180. package/dist/react/primitives/SegmentGroup.d.ts.map +1 -0
  181. package/dist/react/primitives/SegmentGroup.js +11 -0
  182. package/dist/react/primitives/SegmentGroup.js.map +1 -0
  183. package/dist/react/primitives/TrialLimitBanner.d.ts +7 -0
  184. package/dist/react/primitives/TrialLimitBanner.d.ts.map +1 -0
  185. package/dist/react/primitives/TrialLimitBanner.js +14 -0
  186. package/dist/react/primitives/TrialLimitBanner.js.map +1 -0
  187. package/dist/react/shared.d.ts +28 -0
  188. package/dist/react/shared.d.ts.map +1 -0
  189. package/dist/react/shared.js +109 -0
  190. package/dist/react/shared.js.map +1 -0
  191. package/dist/react/widgets/BillingPortal.d.ts +9 -0
  192. package/dist/react/widgets/BillingPortal.d.ts.map +1 -0
  193. package/dist/react/widgets/BillingPortal.js +30 -0
  194. package/dist/react/widgets/BillingPortal.js.map +1 -0
  195. package/dist/react/widgets/ProductItem.d.ts +8 -0
  196. package/dist/react/widgets/ProductItem.d.ts.map +1 -0
  197. package/dist/react/widgets/ProductItem.js +14 -0
  198. package/dist/react/widgets/ProductItem.js.map +1 -0
  199. package/dist/react/widgets/ProductRoot.d.ts +16 -0
  200. package/dist/react/widgets/ProductRoot.d.ts.map +1 -0
  201. package/dist/react/widgets/ProductRoot.js +171 -0
  202. package/dist/react/widgets/ProductRoot.js.map +1 -0
  203. package/dist/react/widgets/SubscriptionItem.d.ts +27 -0
  204. package/dist/react/widgets/SubscriptionItem.d.ts.map +1 -0
  205. package/dist/react/widgets/SubscriptionItem.js +32 -0
  206. package/dist/react/widgets/SubscriptionItem.js.map +1 -0
  207. package/dist/react/widgets/SubscriptionRoot.d.ts +16 -0
  208. package/dist/react/widgets/SubscriptionRoot.d.ts.map +1 -0
  209. package/dist/react/widgets/SubscriptionRoot.js +405 -0
  210. package/dist/react/widgets/SubscriptionRoot.js.map +1 -0
  211. package/dist/react/widgets/index.d.ts +19 -0
  212. package/dist/react/widgets/index.d.ts.map +1 -0
  213. package/dist/react/widgets/index.js +16 -0
  214. package/dist/react/widgets/index.js.map +1 -0
  215. package/dist/react/widgets/productGroupContext.d.ts +6 -0
  216. package/dist/react/widgets/productGroupContext.d.ts.map +1 -0
  217. package/dist/react/widgets/productGroupContext.js +3 -0
  218. package/dist/react/widgets/productGroupContext.js.map +1 -0
  219. package/dist/react/widgets/subscriptionContext.d.ts +6 -0
  220. package/dist/react/widgets/subscriptionContext.d.ts.map +1 -0
  221. package/dist/react/widgets/subscriptionContext.js +3 -0
  222. package/dist/react/widgets/subscriptionContext.js.map +1 -0
  223. package/dist/react/widgets/types.d.ts +171 -0
  224. package/dist/react/widgets/types.d.ts.map +1 -0
  225. package/dist/react/widgets/types.js +2 -0
  226. package/dist/react/widgets/types.js.map +1 -0
  227. package/dist/svelte/index.d.ts +22 -0
  228. package/dist/svelte/index.d.ts.map +1 -0
  229. package/dist/svelte/index.js +20 -0
  230. package/dist/svelte/index.js.map +1 -0
  231. package/dist/svelte/primitives/BillingGate.svelte +28 -0
  232. package/dist/svelte/primitives/BillingToggle.svelte +27 -0
  233. package/dist/svelte/primitives/CheckoutButton.svelte +60 -0
  234. package/dist/svelte/primitives/CheckoutSuccessSummary.svelte +34 -0
  235. package/dist/svelte/primitives/CustomerPortalButton.svelte +60 -0
  236. package/dist/svelte/primitives/NumberInput.svelte +71 -0
  237. package/dist/svelte/primitives/OneTimeCheckoutButton.svelte +37 -0
  238. package/dist/svelte/primitives/OneTimePaymentStatusBadge.svelte +20 -0
  239. package/dist/svelte/primitives/PaymentWarningBanner.svelte +30 -0
  240. package/dist/svelte/primitives/PricingCard.svelte +356 -0
  241. package/dist/svelte/primitives/PricingSection.svelte +121 -0
  242. package/dist/svelte/primitives/ScheduledChangeBanner.svelte +46 -0
  243. package/dist/svelte/primitives/SegmentControl.svelte +38 -0
  244. package/dist/svelte/primitives/SegmentGroup.svelte +52 -0
  245. package/dist/svelte/primitives/TrialLimitBanner.svelte +32 -0
  246. package/dist/svelte/primitives/shared.d.ts +13 -0
  247. package/dist/svelte/primitives/shared.d.ts.map +1 -0
  248. package/dist/svelte/primitives/shared.js +87 -0
  249. package/dist/svelte/primitives/shared.js.map +1 -0
  250. package/dist/svelte/widgets/BillingPortal.svelte +55 -0
  251. package/dist/svelte/widgets/Product.svelte +35 -0
  252. package/dist/svelte/widgets/ProductRoot.svelte +428 -0
  253. package/dist/svelte/widgets/Subscription.svelte +52 -0
  254. package/dist/svelte/widgets/SubscriptionRoot.svelte +690 -0
  255. package/dist/svelte/widgets/index.d.ts +19 -0
  256. package/dist/svelte/widgets/index.d.ts.map +1 -0
  257. package/dist/svelte/widgets/index.js +16 -0
  258. package/dist/svelte/widgets/index.js.map +1 -0
  259. package/dist/svelte/widgets/productGroupContext.d.ts +6 -0
  260. package/dist/svelte/widgets/productGroupContext.d.ts.map +1 -0
  261. package/dist/svelte/widgets/productGroupContext.js +2 -0
  262. package/dist/svelte/widgets/productGroupContext.js.map +1 -0
  263. package/dist/svelte/widgets/subscriptionContext.d.ts +6 -0
  264. package/dist/svelte/widgets/subscriptionContext.d.ts.map +1 -0
  265. package/dist/svelte/widgets/subscriptionContext.js +2 -0
  266. package/dist/svelte/widgets/subscriptionContext.js.map +1 -0
  267. package/dist/svelte/widgets/types.d.ts +171 -0
  268. package/dist/svelte/widgets/types.d.ts.map +1 -0
  269. package/dist/svelte/widgets/types.js +2 -0
  270. package/dist/svelte/widgets/types.js.map +1 -0
  271. package/package.json +182 -0
  272. package/src/client/helpers.test.ts +139 -0
  273. package/src/client/helpers.ts +51 -0
  274. package/src/client/index.test.ts +1554 -0
  275. package/src/client/index.ts +1504 -0
  276. package/src/client/parsers.test.ts +1017 -0
  277. package/src/client/parsers.ts +182 -0
  278. package/src/client/polyfill.ts +2 -0
  279. package/src/component/_generated/api.ts +52 -0
  280. package/src/component/_generated/component.ts +619 -0
  281. package/src/component/_generated/dataModel.ts +60 -0
  282. package/src/component/_generated/server.ts +156 -0
  283. package/src/component/convex.config.ts +3 -0
  284. package/src/component/lib.test.ts +1359 -0
  285. package/src/component/lib.ts +726 -0
  286. package/src/component/schema.ts +112 -0
  287. package/src/component/util.test.ts +281 -0
  288. package/src/component/util.ts +228 -0
  289. package/src/core/catalog.test.ts +212 -0
  290. package/src/core/catalog.ts +119 -0
  291. package/src/core/index.ts +8 -0
  292. package/src/core/markdown.test.ts +43 -0
  293. package/src/core/markdown.ts +26 -0
  294. package/src/core/payments.test.ts +69 -0
  295. package/src/core/payments.ts +33 -0
  296. package/src/core/pendingCheckout.test.ts +44 -0
  297. package/src/core/pendingCheckout.ts +40 -0
  298. package/src/core/resolver.test.ts +283 -0
  299. package/src/core/resolver.ts +160 -0
  300. package/src/core/selectors.test.ts +119 -0
  301. package/src/core/selectors.ts +35 -0
  302. package/src/core/subscriptionUpdate.test.ts +164 -0
  303. package/src/core/subscriptionUpdate.ts +102 -0
  304. package/src/core/types.ts +220 -0
  305. package/src/design-system/README.md +40 -0
  306. package/src/design-system/base.css +27 -0
  307. package/src/design-system/colors/color-utils.ts +110 -0
  308. package/src/design-system/colors/config.ts +282 -0
  309. package/src/design-system/colors/index.ts +2 -0
  310. package/src/design-system/colors/utilities.css +2328 -0
  311. package/src/design-system/components/badges.css +65 -0
  312. package/src/design-system/components/buttons.css +256 -0
  313. package/src/design-system/components/dialog.css +218 -0
  314. package/src/design-system/components/icon-buttons.css +115 -0
  315. package/src/design-system/components/inputs.css +94 -0
  316. package/src/design-system/components/links.css +53 -0
  317. package/src/design-system/components/prose.css +67 -0
  318. package/src/design-system/components/segment-control.css +303 -0
  319. package/src/design-system/index.css +21 -0
  320. package/src/design-system/rounded/config.ts +91 -0
  321. package/src/design-system/rounded/index.ts +1 -0
  322. package/src/design-system/rounded/utilities.css +37 -0
  323. package/src/design-system/typography/config.ts +340 -0
  324. package/src/design-system/typography/index.ts +2 -0
  325. package/src/design-system/typography/tokens.ts +148 -0
  326. package/src/design-system/typography/utilities.css +728 -0
  327. package/src/library.css +20 -0
  328. package/src/react/hooks/useCheckoutSuccessParams.ts +7 -0
  329. package/src/react/index.tsx +47 -0
  330. package/src/react/primitives/BillingGate.tsx +26 -0
  331. package/src/react/primitives/BillingToggle.tsx +29 -0
  332. package/src/react/primitives/CheckoutButton.tsx +47 -0
  333. package/src/react/primitives/CheckoutSuccessSummary.tsx +36 -0
  334. package/src/react/primitives/CustomerPortalButton.tsx +50 -0
  335. package/src/react/primitives/NumberInput.tsx +83 -0
  336. package/src/react/primitives/OneTimeCheckoutButton.tsx +27 -0
  337. package/src/react/primitives/OneTimePaymentStatusBadge.tsx +18 -0
  338. package/src/react/primitives/PaymentWarningBanner.tsx +33 -0
  339. package/src/react/primitives/PricingCard.tsx +421 -0
  340. package/src/react/primitives/PricingSection.tsx +129 -0
  341. package/src/react/primitives/ScheduledChangeBanner.tsx +52 -0
  342. package/src/react/primitives/SegmentControl.tsx +32 -0
  343. package/src/react/primitives/SegmentGroup.tsx +53 -0
  344. package/src/react/primitives/TrialLimitBanner.tsx +32 -0
  345. package/src/react/shared.ts +138 -0
  346. package/src/react/widgets/BillingPortal.tsx +56 -0
  347. package/src/react/widgets/ProductItem.tsx +26 -0
  348. package/src/react/widgets/ProductRoot.tsx +441 -0
  349. package/src/react/widgets/SubscriptionItem.tsx +71 -0
  350. package/src/react/widgets/SubscriptionRoot.tsx +759 -0
  351. package/src/react/widgets/index.ts +36 -0
  352. package/src/react/widgets/productGroupContext.ts +10 -0
  353. package/src/react/widgets/subscriptionContext.ts +10 -0
  354. package/src/react/widgets/types.ts +179 -0
  355. package/src/svelte/index.ts +43 -0
  356. package/src/svelte/primitives/BillingGate.svelte +28 -0
  357. package/src/svelte/primitives/BillingToggle.svelte +27 -0
  358. package/src/svelte/primitives/CheckoutButton.svelte +60 -0
  359. package/src/svelte/primitives/CheckoutSuccessSummary.svelte +34 -0
  360. package/src/svelte/primitives/CustomerPortalButton.svelte +60 -0
  361. package/src/svelte/primitives/NumberInput.svelte +71 -0
  362. package/src/svelte/primitives/OneTimeCheckoutButton.svelte +37 -0
  363. package/src/svelte/primitives/OneTimePaymentStatusBadge.svelte +20 -0
  364. package/src/svelte/primitives/PaymentWarningBanner.svelte +30 -0
  365. package/src/svelte/primitives/PricingCard.svelte +356 -0
  366. package/src/svelte/primitives/PricingSection.svelte +121 -0
  367. package/src/svelte/primitives/ScheduledChangeBanner.svelte +46 -0
  368. package/src/svelte/primitives/SegmentControl.svelte +38 -0
  369. package/src/svelte/primitives/SegmentGroup.svelte +52 -0
  370. package/src/svelte/primitives/TrialLimitBanner.svelte +32 -0
  371. package/src/svelte/primitives/shared.ts +113 -0
  372. package/src/svelte/svelte.d.ts +6 -0
  373. package/src/svelte/widgets/BillingPortal.svelte +55 -0
  374. package/src/svelte/widgets/Product.svelte +35 -0
  375. package/src/svelte/widgets/ProductRoot.svelte +428 -0
  376. package/src/svelte/widgets/Subscription.svelte +52 -0
  377. package/src/svelte/widgets/SubscriptionRoot.svelte +690 -0
  378. package/src/svelte/widgets/index.ts +36 -0
  379. package/src/svelte/widgets/productGroupContext.ts +7 -0
  380. package/src/svelte/widgets/subscriptionContext.ts +7 -0
  381. package/src/svelte/widgets/types.ts +179 -0
  382. package/src/tailwind.css +6 -0
  383. package/src/test.ts +18 -0
@@ -0,0 +1,1359 @@
1
+ /// <reference types="vite/client" />
2
+ import { describe, it, expect, beforeEach } from "vitest";
3
+ import { convexTest } from "convex-test";
4
+ import type { TestConvex } from "convex-test";
5
+ import type { Infer } from "convex/values";
6
+ import schema from "./schema.js";
7
+ import { api } from "./_generated/api.js";
8
+ import { convertToDatabaseProduct } from "./util.js";
9
+ import type { ProductEntity } from "creem/models/components";
10
+
11
+ const modules = import.meta.glob("./**/*.ts");
12
+
13
+ // Types derived from schema validators
14
+ type DbSubscription = Infer<typeof schema.tables.subscriptions.validator>;
15
+ type DbProduct = Infer<typeof schema.tables.products.validator>;
16
+ type DbCustomer = Infer<typeof schema.tables.customers.validator>;
17
+
18
+ // Helper to create a minimal valid subscription for testing
19
+ function createTestSubscription(
20
+ overrides: Partial<DbSubscription> = {},
21
+ ): DbSubscription {
22
+ return {
23
+ id: "sub_123",
24
+ customerId: "cust_456",
25
+ productId: "prod_789",
26
+ checkoutId: "checkout_abc",
27
+ createdAt: "2025-01-15T10:00:00.000Z",
28
+ modifiedAt: "2025-01-16T12:00:00.000Z",
29
+ amount: 1000,
30
+ currency: "usd",
31
+ recurringInterval: "month",
32
+ status: "active",
33
+ currentPeriodStart: "2025-01-15T10:00:00.000Z",
34
+ currentPeriodEnd: "2025-02-15T10:00:00.000Z",
35
+ cancelAtPeriodEnd: false,
36
+ startedAt: "2025-01-15T10:00:00.000Z",
37
+ endedAt: null,
38
+ metadata: {},
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ // Helper to create a minimal valid product for testing
44
+ function createTestProduct(overrides: Partial<DbProduct> = {}): DbProduct {
45
+ return {
46
+ id: "prod_123",
47
+ name: "Test Product",
48
+ description: "A test product",
49
+ price: 1000,
50
+ currency: "USD",
51
+ billingType: "recurring",
52
+ billingPeriod: "every-month",
53
+ status: "active",
54
+ createdAt: "2025-01-10T08:00:00.000Z",
55
+ modifiedAt: "2025-01-12T09:00:00.000Z",
56
+ metadata: {},
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ // Helper to create a minimal valid customer for testing
62
+ function createTestCustomer(overrides: Partial<DbCustomer> = {}): DbCustomer {
63
+ return {
64
+ id: "cust_123",
65
+ entityId: "user_456",
66
+ ...overrides,
67
+ };
68
+ }
69
+
70
+ // Helper to create Creem SDK-shaped Product objects.
71
+ function createSdkProduct(
72
+ overrides: Partial<ProductEntity> = {},
73
+ ): ProductEntity {
74
+ return {
75
+ id: "prod_123",
76
+ mode: "test_mode",
77
+ object: "product",
78
+ name: "Test Product",
79
+ description: "A test product",
80
+ price: 1000,
81
+ currency: "USD",
82
+ billingType: "recurring",
83
+ billingPeriod: "every-month",
84
+ status: "active",
85
+ taxMode: "inclusive",
86
+ taxCategory: "saas",
87
+ createdAt: new Date("2025-01-10T08:00:00.000Z"),
88
+ updatedAt: new Date("2025-01-12T09:00:00.000Z"),
89
+ features: [],
90
+ ...overrides,
91
+ } as ProductEntity;
92
+ }
93
+
94
+ describe("createSubscription mutation", () => {
95
+ let t: TestConvex<typeof schema>;
96
+
97
+ beforeEach(() => {
98
+ t = convexTest(schema, modules);
99
+ });
100
+
101
+ it("inserts when no existing record", async () => {
102
+ const subscription = createTestSubscription();
103
+
104
+ await t.mutation(api.lib.createSubscription, { subscription });
105
+
106
+ const result = await t.query(api.lib.getSubscription, { id: "sub_123" });
107
+ expect(result).not.toBeNull();
108
+ expect(result?.id).toBe("sub_123");
109
+ expect(result?.status).toBe("active");
110
+ });
111
+
112
+ it("patches when existing record has older modifiedAt", async () => {
113
+ const oldSubscription = createTestSubscription({
114
+ modifiedAt: "2025-01-15T10:00:00.000Z",
115
+ status: "active",
116
+ });
117
+ await t.mutation(api.lib.createSubscription, {
118
+ subscription: oldSubscription,
119
+ });
120
+
121
+ const newSubscription = createTestSubscription({
122
+ modifiedAt: "2025-01-16T12:00:00.000Z",
123
+ status: "canceled",
124
+ });
125
+ await t.mutation(api.lib.createSubscription, {
126
+ subscription: newSubscription,
127
+ });
128
+
129
+ const result = await t.query(api.lib.getSubscription, { id: "sub_123" });
130
+ expect(result?.status).toBe("canceled");
131
+ expect(result?.modifiedAt).toBe("2025-01-16T12:00:00.000Z");
132
+ });
133
+
134
+ it("skips when existing record has newer modifiedAt (stale webhook)", async () => {
135
+ const newSubscription = createTestSubscription({
136
+ modifiedAt: "2025-01-20T10:00:00.000Z",
137
+ status: "active",
138
+ });
139
+ await t.mutation(api.lib.createSubscription, {
140
+ subscription: newSubscription,
141
+ });
142
+
143
+ const staleSubscription = createTestSubscription({
144
+ modifiedAt: "2025-01-15T10:00:00.000Z",
145
+ status: "canceled",
146
+ });
147
+ await t.mutation(api.lib.createSubscription, {
148
+ subscription: staleSubscription,
149
+ });
150
+
151
+ const result = await t.query(api.lib.getSubscription, { id: "sub_123" });
152
+ expect(result?.status).toBe("active");
153
+ expect(result?.modifiedAt).toBe("2025-01-20T10:00:00.000Z");
154
+ });
155
+
156
+ it("patches when modifiedAt values are equal", async () => {
157
+ const subscription1 = createTestSubscription({
158
+ modifiedAt: "2025-01-15T10:00:00.000Z",
159
+ status: "active",
160
+ });
161
+ await t.mutation(api.lib.createSubscription, {
162
+ subscription: subscription1,
163
+ });
164
+
165
+ const subscription2 = createTestSubscription({
166
+ modifiedAt: "2025-01-15T10:00:00.000Z",
167
+ status: "canceled",
168
+ });
169
+ await t.mutation(api.lib.createSubscription, {
170
+ subscription: subscription2,
171
+ });
172
+
173
+ const result = await t.query(api.lib.getSubscription, { id: "sub_123" });
174
+ expect(result?.status).toBe("canceled");
175
+ });
176
+
177
+ it("treats null modifiedAt as oldest", async () => {
178
+ const subscription1 = createTestSubscription({
179
+ modifiedAt: null,
180
+ status: "active",
181
+ });
182
+ await t.mutation(api.lib.createSubscription, {
183
+ subscription: subscription1,
184
+ });
185
+
186
+ const subscription2 = createTestSubscription({
187
+ modifiedAt: "2025-01-15T10:00:00.000Z",
188
+ status: "canceled",
189
+ });
190
+ await t.mutation(api.lib.createSubscription, {
191
+ subscription: subscription2,
192
+ });
193
+
194
+ const result = await t.query(api.lib.getSubscription, { id: "sub_123" });
195
+ expect(result?.status).toBe("canceled");
196
+ });
197
+ });
198
+
199
+ describe("updateSubscription mutation", () => {
200
+ let t: TestConvex<typeof schema>;
201
+
202
+ beforeEach(() => {
203
+ t = convexTest(schema, modules);
204
+ });
205
+
206
+ it("inserts when no existing record (upsert behavior)", async () => {
207
+ const subscription = createTestSubscription();
208
+
209
+ await t.mutation(api.lib.updateSubscription, { subscription });
210
+
211
+ const result = await t.query(api.lib.getSubscription, { id: "sub_123" });
212
+ expect(result).not.toBeNull();
213
+ expect(result?.id).toBe("sub_123");
214
+ });
215
+
216
+ it("patches when existing record has older modifiedAt", async () => {
217
+ const oldSubscription = createTestSubscription({
218
+ modifiedAt: "2025-01-15T10:00:00.000Z",
219
+ status: "active",
220
+ });
221
+ await t.mutation(api.lib.createSubscription, {
222
+ subscription: oldSubscription,
223
+ });
224
+
225
+ const newSubscription = createTestSubscription({
226
+ modifiedAt: "2025-01-16T12:00:00.000Z",
227
+ status: "canceled",
228
+ });
229
+ await t.mutation(api.lib.updateSubscription, {
230
+ subscription: newSubscription,
231
+ });
232
+
233
+ const result = await t.query(api.lib.getSubscription, { id: "sub_123" });
234
+ expect(result?.status).toBe("canceled");
235
+ });
236
+
237
+ it("skips when existing record has newer modifiedAt (stale webhook)", async () => {
238
+ const newSubscription = createTestSubscription({
239
+ modifiedAt: "2025-01-20T10:00:00.000Z",
240
+ status: "active",
241
+ });
242
+ await t.mutation(api.lib.createSubscription, {
243
+ subscription: newSubscription,
244
+ });
245
+
246
+ const staleSubscription = createTestSubscription({
247
+ modifiedAt: "2025-01-15T10:00:00.000Z",
248
+ status: "canceled",
249
+ });
250
+ await t.mutation(api.lib.updateSubscription, {
251
+ subscription: staleSubscription,
252
+ });
253
+
254
+ const result = await t.query(api.lib.getSubscription, { id: "sub_123" });
255
+ expect(result?.status).toBe("active");
256
+ });
257
+ });
258
+
259
+ describe("createProduct mutation", () => {
260
+ let t: TestConvex<typeof schema>;
261
+
262
+ beforeEach(() => {
263
+ t = convexTest(schema, modules);
264
+ });
265
+
266
+ it("inserts when no existing record", async () => {
267
+ const product = createTestProduct();
268
+
269
+ await t.mutation(api.lib.createProduct, { product });
270
+
271
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
272
+ expect(result).not.toBeNull();
273
+ expect(result?.id).toBe("prod_123");
274
+ expect(result?.name).toBe("Test Product");
275
+ });
276
+
277
+ it("patches when existing record has older modifiedAt", async () => {
278
+ const oldProduct = createTestProduct({
279
+ modifiedAt: "2025-01-10T10:00:00.000Z",
280
+ name: "Old Name",
281
+ });
282
+ await t.mutation(api.lib.createProduct, { product: oldProduct });
283
+
284
+ const newProduct = createTestProduct({
285
+ modifiedAt: "2025-01-15T10:00:00.000Z",
286
+ name: "New Name",
287
+ });
288
+ await t.mutation(api.lib.createProduct, { product: newProduct });
289
+
290
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
291
+ expect(result?.name).toBe("New Name");
292
+ });
293
+
294
+ it("skips when existing record has newer modifiedAt (stale webhook)", async () => {
295
+ const newProduct = createTestProduct({
296
+ modifiedAt: "2025-01-20T10:00:00.000Z",
297
+ name: "Current Name",
298
+ });
299
+ await t.mutation(api.lib.createProduct, { product: newProduct });
300
+
301
+ const staleProduct = createTestProduct({
302
+ modifiedAt: "2025-01-10T10:00:00.000Z",
303
+ name: "Stale Name",
304
+ });
305
+ await t.mutation(api.lib.createProduct, { product: staleProduct });
306
+
307
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
308
+ expect(result?.name).toBe("Current Name");
309
+ });
310
+
311
+ it("treats null modifiedAt as oldest", async () => {
312
+ const product1 = createTestProduct({
313
+ modifiedAt: null,
314
+ name: "Original Name",
315
+ });
316
+ await t.mutation(api.lib.createProduct, { product: product1 });
317
+
318
+ const product2 = createTestProduct({
319
+ modifiedAt: "2025-01-15T10:00:00.000Z",
320
+ name: "Updated Name",
321
+ });
322
+ await t.mutation(api.lib.createProduct, { product: product2 });
323
+
324
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
325
+ expect(result?.name).toBe("Updated Name");
326
+ });
327
+ });
328
+
329
+ describe("updateProduct mutation", () => {
330
+ let t: TestConvex<typeof schema>;
331
+
332
+ beforeEach(() => {
333
+ t = convexTest(schema, modules);
334
+ });
335
+
336
+ it("inserts when no existing record (upsert behavior)", async () => {
337
+ const product = createTestProduct();
338
+
339
+ await t.mutation(api.lib.updateProduct, { product });
340
+
341
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
342
+ expect(result).not.toBeNull();
343
+ expect(result?.id).toBe("prod_123");
344
+ });
345
+
346
+ it("patches when existing record has older modifiedAt", async () => {
347
+ const oldProduct = createTestProduct({
348
+ modifiedAt: "2025-01-10T10:00:00.000Z",
349
+ name: "Old Name",
350
+ });
351
+ await t.mutation(api.lib.createProduct, { product: oldProduct });
352
+
353
+ const newProduct = createTestProduct({
354
+ modifiedAt: "2025-01-15T10:00:00.000Z",
355
+ name: "New Name",
356
+ });
357
+ await t.mutation(api.lib.updateProduct, { product: newProduct });
358
+
359
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
360
+ expect(result?.name).toBe("New Name");
361
+ });
362
+
363
+ it("skips when existing record has newer modifiedAt (stale webhook)", async () => {
364
+ const newProduct = createTestProduct({
365
+ modifiedAt: "2025-01-20T10:00:00.000Z",
366
+ name: "Current Name",
367
+ });
368
+ await t.mutation(api.lib.createProduct, { product: newProduct });
369
+
370
+ const staleProduct = createTestProduct({
371
+ modifiedAt: "2025-01-10T10:00:00.000Z",
372
+ name: "Stale Name",
373
+ });
374
+ await t.mutation(api.lib.updateProduct, { product: staleProduct });
375
+
376
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
377
+ expect(result?.name).toBe("Current Name");
378
+ });
379
+ });
380
+
381
+ describe("product conversion (Creem SDK → DB)", () => {
382
+ let t: TestConvex<typeof schema>;
383
+
384
+ beforeEach(() => {
385
+ t = convexTest(schema, modules);
386
+ });
387
+
388
+ it("converts recurring products with Creem-native fields", async () => {
389
+ const sdkProduct = createSdkProduct({
390
+ price: 1500,
391
+ currency: "USD",
392
+ billingType: "recurring",
393
+ billingPeriod: "every-month",
394
+ });
395
+
396
+ const dbProduct = convertToDatabaseProduct(sdkProduct);
397
+ await t.mutation(api.lib.createProduct, { product: dbProduct });
398
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
399
+
400
+ expect(result?.price).toBe(1500);
401
+ expect(result?.currency).toBe("USD");
402
+ expect(result?.billingType).toBe("recurring");
403
+ expect(result?.billingPeriod).toBe("every-month");
404
+ expect(result?.status).toBe("active");
405
+ });
406
+
407
+ it("converts one-time products with Creem-native fields", async () => {
408
+ const sdkProduct = createSdkProduct({
409
+ billingType: "onetime",
410
+ billingPeriod: "once",
411
+ price: 4900,
412
+ });
413
+
414
+ const dbProduct = convertToDatabaseProduct(sdkProduct);
415
+ await t.mutation(api.lib.createProduct, { product: dbProduct });
416
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
417
+
418
+ expect(result?.price).toBe(4900);
419
+ expect(result?.billingType).toBe("onetime");
420
+ });
421
+
422
+ it("converts Creem features", async () => {
423
+ const sdkProduct = createSdkProduct({
424
+ features: [
425
+ {
426
+ id: "feature_123",
427
+ description: "Priority support",
428
+ type: "custom",
429
+ },
430
+ ],
431
+ });
432
+
433
+ const dbProduct = convertToDatabaseProduct(sdkProduct);
434
+ await t.mutation(api.lib.createProduct, { product: dbProduct });
435
+ const result = await t.query(api.lib.getProduct, { id: "prod_123" });
436
+
437
+ expect(result?.features).toHaveLength(1);
438
+ expect(result?.features?.[0].id).toBe("feature_123");
439
+ expect(result?.features?.[0].description).toBe("Priority support");
440
+ });
441
+ });
442
+
443
+ describe("insertCustomer mutation", () => {
444
+ let t: TestConvex<typeof schema>;
445
+
446
+ beforeEach(() => {
447
+ t = convexTest(schema, modules);
448
+ });
449
+
450
+ it("inserts new customer when none exists", async () => {
451
+ const customer = createTestCustomer();
452
+
453
+ const id = await t.mutation(api.lib.insertCustomer, customer);
454
+
455
+ expect(id).toBeDefined();
456
+ const result = await t.query(api.lib.getCustomerByEntityId, {
457
+ entityId: "user_456",
458
+ });
459
+ expect(result).not.toBeNull();
460
+ expect(result?.id).toBe("cust_123");
461
+ });
462
+
463
+ it("returns existing customer id when customer already exists for entityId", async () => {
464
+ const customer = createTestCustomer();
465
+
466
+ const id1 = await t.mutation(api.lib.insertCustomer, customer);
467
+
468
+ const customer2 = createTestCustomer({
469
+ id: "cust_different",
470
+ });
471
+ const id2 = await t.mutation(api.lib.insertCustomer, customer2);
472
+
473
+ expect(id1).toBe(id2);
474
+
475
+ const result = await t.query(api.lib.getCustomerByEntityId, {
476
+ entityId: "user_456",
477
+ });
478
+ expect(result?.id).toBe("cust_123");
479
+ });
480
+
481
+ it("allows different customers for different entityIds", async () => {
482
+ const customer1 = createTestCustomer({
483
+ id: "cust_123",
484
+ entityId: "user_123",
485
+ });
486
+ const customer2 = createTestCustomer({
487
+ id: "cust_456",
488
+ entityId: "user_456",
489
+ });
490
+
491
+ await t.mutation(api.lib.insertCustomer, customer1);
492
+ await t.mutation(api.lib.insertCustomer, customer2);
493
+
494
+ const result1 = await t.query(api.lib.getCustomerByEntityId, {
495
+ entityId: "user_123",
496
+ });
497
+ const result2 = await t.query(api.lib.getCustomerByEntityId, {
498
+ entityId: "user_456",
499
+ });
500
+
501
+ expect(result1?.id).toBe("cust_123");
502
+ expect(result2?.id).toBe("cust_456");
503
+ });
504
+ });
505
+
506
+ describe("getCurrentSubscription query", () => {
507
+ let t: TestConvex<typeof schema>;
508
+
509
+ beforeEach(() => {
510
+ t = convexTest(schema, modules);
511
+ });
512
+
513
+ it("returns null when no customer exists", async () => {
514
+ const result = await t.query(api.lib.getCurrentSubscription, {
515
+ entityId: "user_nonexistent",
516
+ });
517
+
518
+ expect(result).toBeNull();
519
+ });
520
+
521
+ it("returns null when customer has no subscriptions", async () => {
522
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
523
+
524
+ const result = await t.query(api.lib.getCurrentSubscription, {
525
+ entityId: "user_456",
526
+ });
527
+
528
+ expect(result).toBeNull();
529
+ });
530
+
531
+ it("returns null when customer only has ended subscriptions", async () => {
532
+ const customer = createTestCustomer();
533
+ await t.mutation(api.lib.insertCustomer, customer);
534
+ await t.mutation(api.lib.createProduct, {
535
+ product: createTestProduct({ id: "prod_789" }),
536
+ });
537
+ await t.mutation(api.lib.createSubscription, {
538
+ subscription: createTestSubscription({
539
+ customerId: "cust_123",
540
+ endedAt: "2025-01-10T10:00:00.000Z",
541
+ }),
542
+ });
543
+
544
+ const result = await t.query(api.lib.getCurrentSubscription, {
545
+ entityId: "user_456",
546
+ });
547
+
548
+ expect(result).toBeNull();
549
+ });
550
+
551
+ it("returns active subscription with product", async () => {
552
+ const customer = createTestCustomer();
553
+ await t.mutation(api.lib.insertCustomer, customer);
554
+ await t.mutation(api.lib.createProduct, {
555
+ product: createTestProduct({ id: "prod_789" }),
556
+ });
557
+ await t.mutation(api.lib.createSubscription, {
558
+ subscription: createTestSubscription({
559
+ customerId: "cust_123",
560
+ endedAt: null,
561
+ }),
562
+ });
563
+
564
+ const result = await t.query(api.lib.getCurrentSubscription, {
565
+ entityId: "user_456",
566
+ });
567
+
568
+ expect(result).not.toBeNull();
569
+ expect(result?.id).toBe("sub_123");
570
+ expect(result?.product.id).toBe("prod_789");
571
+ expect(result?.product.name).toBe("Test Product");
572
+ });
573
+
574
+ it("returns null when trial has expired", async () => {
575
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
576
+ await t.mutation(api.lib.createProduct, {
577
+ product: createTestProduct({ id: "prod_789" }),
578
+ });
579
+ await t.mutation(api.lib.createSubscription, {
580
+ subscription: createTestSubscription({
581
+ customerId: "cust_123",
582
+ endedAt: null,
583
+ status: "trialing",
584
+ trialStart: "2025-01-01T00:00:00.000Z",
585
+ trialEnd: "2025-01-08T00:00:00.000Z",
586
+ }),
587
+ });
588
+
589
+ const result = await t.query(api.lib.getCurrentSubscription, {
590
+ entityId: "user_456",
591
+ });
592
+
593
+ expect(result).toBeNull();
594
+ });
595
+
596
+ it("returns subscription when trial is still active", async () => {
597
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
598
+ await t.mutation(api.lib.createProduct, {
599
+ product: createTestProduct({ id: "prod_789" }),
600
+ });
601
+ await t.mutation(api.lib.createSubscription, {
602
+ subscription: createTestSubscription({
603
+ customerId: "cust_123",
604
+ endedAt: null,
605
+ status: "trialing",
606
+ trialStart: "2025-01-01T00:00:00.000Z",
607
+ trialEnd: "2099-01-01T00:00:00.000Z",
608
+ }),
609
+ });
610
+
611
+ const result = await t.query(api.lib.getCurrentSubscription, {
612
+ entityId: "user_456",
613
+ });
614
+
615
+ expect(result).not.toBeNull();
616
+ expect(result?.status).toBe("trialing");
617
+ });
618
+ });
619
+
620
+ describe("listUserSubscriptions query", () => {
621
+ let t: TestConvex<typeof schema>;
622
+
623
+ beforeEach(() => {
624
+ t = convexTest(schema, modules);
625
+ });
626
+
627
+ it("returns empty array when no customer exists", async () => {
628
+ const result = await t.query(api.lib.listUserSubscriptions, {
629
+ entityId: "user_nonexistent",
630
+ });
631
+
632
+ expect(result).toEqual([]);
633
+ });
634
+
635
+ it("returns empty array when customer has no subscriptions", async () => {
636
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
637
+
638
+ const result = await t.query(api.lib.listUserSubscriptions, {
639
+ entityId: "user_456",
640
+ });
641
+
642
+ expect(result).toEqual([]);
643
+ });
644
+
645
+ it("excludes ended subscriptions", async () => {
646
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
647
+ await t.mutation(api.lib.createProduct, {
648
+ product: createTestProduct({ id: "prod_789" }),
649
+ });
650
+ await t.mutation(api.lib.createSubscription, {
651
+ subscription: createTestSubscription({
652
+ id: "sub_ended",
653
+ customerId: "cust_123",
654
+ endedAt: "2020-01-01T00:00:00.000Z",
655
+ }),
656
+ });
657
+
658
+ const result = await t.query(api.lib.listUserSubscriptions, {
659
+ entityId: "user_456",
660
+ });
661
+
662
+ expect(result).toHaveLength(0);
663
+ });
664
+
665
+ it("returns active subscriptions with products", async () => {
666
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
667
+ await t.mutation(api.lib.createProduct, {
668
+ product: createTestProduct({ id: "prod_789" }),
669
+ });
670
+ await t.mutation(api.lib.createSubscription, {
671
+ subscription: createTestSubscription({
672
+ customerId: "cust_123",
673
+ endedAt: null,
674
+ }),
675
+ });
676
+
677
+ const result = await t.query(api.lib.listUserSubscriptions, {
678
+ entityId: "user_456",
679
+ });
680
+
681
+ expect(result).toHaveLength(1);
682
+ expect(result[0].id).toBe("sub_123");
683
+ expect(result[0].product?.id).toBe("prod_789");
684
+ });
685
+
686
+ it("excludes expired trials", async () => {
687
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
688
+ await t.mutation(api.lib.createProduct, {
689
+ product: createTestProduct({ id: "prod_789" }),
690
+ });
691
+ await t.mutation(api.lib.createSubscription, {
692
+ subscription: createTestSubscription({
693
+ customerId: "cust_123",
694
+ endedAt: null,
695
+ status: "trialing",
696
+ trialStart: "2025-01-01T00:00:00.000Z",
697
+ trialEnd: "2025-01-08T00:00:00.000Z",
698
+ }),
699
+ });
700
+
701
+ const result = await t.query(api.lib.listUserSubscriptions, {
702
+ entityId: "user_456",
703
+ });
704
+
705
+ expect(result).toHaveLength(0);
706
+ });
707
+
708
+ it("includes active trials", async () => {
709
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
710
+ await t.mutation(api.lib.createProduct, {
711
+ product: createTestProduct({ id: "prod_789" }),
712
+ });
713
+ await t.mutation(api.lib.createSubscription, {
714
+ subscription: createTestSubscription({
715
+ customerId: "cust_123",
716
+ endedAt: null,
717
+ status: "trialing",
718
+ trialStart: "2025-01-01T00:00:00.000Z",
719
+ trialEnd: "2099-01-01T00:00:00.000Z",
720
+ }),
721
+ });
722
+
723
+ const result = await t.query(api.lib.listUserSubscriptions, {
724
+ entityId: "user_456",
725
+ });
726
+
727
+ expect(result).toHaveLength(1);
728
+ expect(result[0].status).toBe("trialing");
729
+ });
730
+
731
+ it("returns multiple subscriptions", async () => {
732
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
733
+ await t.mutation(api.lib.createProduct, {
734
+ product: createTestProduct({ id: "prod_1" }),
735
+ });
736
+ await t.mutation(api.lib.createProduct, {
737
+ product: createTestProduct({ id: "prod_2" }),
738
+ });
739
+ await t.mutation(api.lib.createSubscription, {
740
+ subscription: createTestSubscription({
741
+ id: "sub_1",
742
+ customerId: "cust_123",
743
+ productId: "prod_1",
744
+ endedAt: null,
745
+ }),
746
+ });
747
+ await t.mutation(api.lib.createSubscription, {
748
+ subscription: createTestSubscription({
749
+ id: "sub_2",
750
+ customerId: "cust_123",
751
+ productId: "prod_2",
752
+ endedAt: null,
753
+ }),
754
+ });
755
+
756
+ const result = await t.query(api.lib.listUserSubscriptions, {
757
+ entityId: "user_456",
758
+ });
759
+
760
+ expect(result).toHaveLength(2);
761
+ });
762
+ });
763
+
764
+ describe("listProducts query", () => {
765
+ let t: TestConvex<typeof schema>;
766
+
767
+ beforeEach(() => {
768
+ t = convexTest(schema, modules);
769
+ });
770
+
771
+ it("returns empty array when no products exist", async () => {
772
+ const result = await t.query(api.lib.listProducts, {});
773
+
774
+ expect(result).toEqual([]);
775
+ });
776
+
777
+ it("returns only active products by default", async () => {
778
+ await t.mutation(api.lib.createProduct, {
779
+ product: createTestProduct({ id: "prod_1", status: "active" }),
780
+ });
781
+ await t.mutation(api.lib.createProduct, {
782
+ product: createTestProduct({ id: "prod_2", status: "active" }),
783
+ });
784
+ await t.mutation(api.lib.createProduct, {
785
+ product: createTestProduct({ id: "prod_inactive", status: "inactive" }),
786
+ });
787
+
788
+ const result = await t.query(api.lib.listProducts, {});
789
+
790
+ expect(result).toHaveLength(2);
791
+ expect(result.map((p) => p.id).sort()).toEqual(["prod_1", "prod_2"]);
792
+ });
793
+
794
+ it("includes inactive products when includeArchived is true", async () => {
795
+ await t.mutation(api.lib.createProduct, {
796
+ product: createTestProduct({ id: "prod_1", status: "active" }),
797
+ });
798
+ await t.mutation(api.lib.createProduct, {
799
+ product: createTestProduct({ id: "prod_inactive", status: "inactive" }),
800
+ });
801
+
802
+ const result = await t.query(api.lib.listProducts, {
803
+ includeArchived: true,
804
+ });
805
+
806
+ expect(result).toHaveLength(2);
807
+ expect(result.map((p) => p.id).sort()).toEqual(["prod_1", "prod_inactive"]);
808
+ });
809
+ });
810
+
811
+ describe("listCustomerSubscriptions query", () => {
812
+ let t: TestConvex<typeof schema>;
813
+
814
+ beforeEach(() => {
815
+ t = convexTest(schema, modules);
816
+ });
817
+
818
+ it("returns empty array when no subscriptions exist", async () => {
819
+ const result = await t.query(api.lib.listCustomerSubscriptions, {
820
+ customerId: "cust_nonexistent",
821
+ });
822
+
823
+ expect(result).toEqual([]);
824
+ });
825
+
826
+ it("returns all subscriptions for customer", async () => {
827
+ await t.mutation(api.lib.createSubscription, {
828
+ subscription: createTestSubscription({
829
+ id: "sub_1",
830
+ customerId: "cust_123",
831
+ }),
832
+ });
833
+ await t.mutation(api.lib.createSubscription, {
834
+ subscription: createTestSubscription({
835
+ id: "sub_2",
836
+ customerId: "cust_123",
837
+ }),
838
+ });
839
+ await t.mutation(api.lib.createSubscription, {
840
+ subscription: createTestSubscription({
841
+ id: "sub_other",
842
+ customerId: "cust_other",
843
+ }),
844
+ });
845
+
846
+ const result = await t.query(api.lib.listCustomerSubscriptions, {
847
+ customerId: "cust_123",
848
+ });
849
+
850
+ expect(result).toHaveLength(2);
851
+ expect(result.map((s) => s.id).sort()).toEqual(["sub_1", "sub_2"]);
852
+ });
853
+ });
854
+
855
+ describe("listAllUserSubscriptions query", () => {
856
+ let t: TestConvex<typeof schema>;
857
+
858
+ beforeEach(() => {
859
+ t = convexTest(schema, modules);
860
+ });
861
+
862
+ it("returns empty array when no customer exists", async () => {
863
+ const result = await t.query(api.lib.listAllUserSubscriptions, {
864
+ entityId: "user_nonexistent",
865
+ });
866
+
867
+ expect(result).toEqual([]);
868
+ });
869
+
870
+ it("includes ended subscriptions", async () => {
871
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
872
+ await t.mutation(api.lib.createProduct, {
873
+ product: createTestProduct({ id: "prod_789" }),
874
+ });
875
+ await t.mutation(api.lib.createSubscription, {
876
+ subscription: createTestSubscription({
877
+ customerId: "cust_123",
878
+ endedAt: "2020-01-01T00:00:00.000Z",
879
+ }),
880
+ });
881
+
882
+ const result = await t.query(api.lib.listAllUserSubscriptions, {
883
+ entityId: "user_456",
884
+ });
885
+
886
+ expect(result).toHaveLength(1);
887
+ expect(result[0].endedAt).toBe("2020-01-01T00:00:00.000Z");
888
+ });
889
+
890
+ it("includes expired trials", async () => {
891
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
892
+ await t.mutation(api.lib.createProduct, {
893
+ product: createTestProduct({ id: "prod_789" }),
894
+ });
895
+ await t.mutation(api.lib.createSubscription, {
896
+ subscription: createTestSubscription({
897
+ customerId: "cust_123",
898
+ endedAt: null,
899
+ status: "trialing",
900
+ trialStart: "2025-01-01T00:00:00.000Z",
901
+ trialEnd: "2025-01-08T00:00:00.000Z",
902
+ }),
903
+ });
904
+
905
+ const result = await t.query(api.lib.listAllUserSubscriptions, {
906
+ entityId: "user_456",
907
+ });
908
+
909
+ expect(result).toHaveLength(1);
910
+ expect(result[0].status).toBe("trialing");
911
+ });
912
+
913
+ it("returns all subscriptions regardless of status", async () => {
914
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
915
+ await t.mutation(api.lib.createProduct, {
916
+ product: createTestProduct({ id: "prod_789" }),
917
+ });
918
+ // Active subscription
919
+ await t.mutation(api.lib.createSubscription, {
920
+ subscription: createTestSubscription({
921
+ id: "sub_active",
922
+ customerId: "cust_123",
923
+ endedAt: null,
924
+ status: "active",
925
+ }),
926
+ });
927
+ // Ended subscription
928
+ await t.mutation(api.lib.createSubscription, {
929
+ subscription: createTestSubscription({
930
+ id: "sub_ended",
931
+ customerId: "cust_123",
932
+ endedAt: "2020-01-01T00:00:00.000Z",
933
+ status: "canceled",
934
+ }),
935
+ });
936
+ // Expired trial
937
+ await t.mutation(api.lib.createSubscription, {
938
+ subscription: createTestSubscription({
939
+ id: "sub_expired_trial",
940
+ customerId: "cust_123",
941
+ endedAt: null,
942
+ status: "trialing",
943
+ trialStart: "2025-01-01T00:00:00.000Z",
944
+ trialEnd: "2025-01-08T00:00:00.000Z",
945
+ }),
946
+ });
947
+
948
+ const result = await t.query(api.lib.listAllUserSubscriptions, {
949
+ entityId: "user_456",
950
+ });
951
+
952
+ expect(result).toHaveLength(3);
953
+ expect(result.map((s) => s.id).sort()).toEqual([
954
+ "sub_active",
955
+ "sub_ended",
956
+ "sub_expired_trial",
957
+ ]);
958
+ });
959
+ });
960
+
961
+ // Helper to create a minimal valid order for testing
962
+ type DbOrder = Infer<typeof schema.tables.orders.validator>;
963
+ function createTestOrder(overrides: Partial<DbOrder> = {}): DbOrder {
964
+ return {
965
+ id: "ord_123",
966
+ customerId: "cust_123",
967
+ productId: "prod_789",
968
+ amount: 2999,
969
+ currency: "USD",
970
+ status: "paid",
971
+ type: "onetime",
972
+ createdAt: "2025-01-15T10:00:00.000Z",
973
+ updatedAt: "2025-01-15T10:00:00.000Z",
974
+ ...overrides,
975
+ };
976
+ }
977
+
978
+ describe("createOrder mutation", () => {
979
+ let t: TestConvex<typeof schema>;
980
+
981
+ beforeEach(() => {
982
+ t = convexTest(schema, modules);
983
+ });
984
+
985
+ it("inserts a new order", async () => {
986
+ await t.mutation(api.lib.createOrder, {
987
+ order: createTestOrder(),
988
+ });
989
+ await t.mutation(api.lib.createOrder, {
990
+ order: createTestOrder({ id: "ord_456", amount: 5000 }),
991
+ });
992
+ // Verify by inserting a customer + querying orders
993
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
994
+ const orders = await t.query(api.lib.listUserOrders, {
995
+ entityId: "user_456",
996
+ });
997
+ expect(orders).toHaveLength(2);
998
+ });
999
+
1000
+ it("updates existing order when incoming is newer", async () => {
1001
+ await t.mutation(api.lib.createOrder, {
1002
+ order: createTestOrder({ updatedAt: "2025-01-15T10:00:00.000Z" }),
1003
+ });
1004
+ await t.mutation(api.lib.createOrder, {
1005
+ order: createTestOrder({
1006
+ updatedAt: "2025-01-16T10:00:00.000Z",
1007
+ amount: 5000,
1008
+ }),
1009
+ });
1010
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
1011
+ const orders = await t.query(api.lib.listUserOrders, {
1012
+ entityId: "user_456",
1013
+ });
1014
+ expect(orders).toHaveLength(1);
1015
+ expect(orders[0].amount).toBe(5000);
1016
+ });
1017
+
1018
+ it("does not update when existing is newer", async () => {
1019
+ await t.mutation(api.lib.createOrder, {
1020
+ order: createTestOrder({ updatedAt: "2025-01-16T10:00:00.000Z" }),
1021
+ });
1022
+ await t.mutation(api.lib.createOrder, {
1023
+ order: createTestOrder({
1024
+ updatedAt: "2025-01-15T10:00:00.000Z",
1025
+ amount: 5000,
1026
+ }),
1027
+ });
1028
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
1029
+ const orders = await t.query(api.lib.listUserOrders, {
1030
+ entityId: "user_456",
1031
+ });
1032
+ expect(orders).toHaveLength(1);
1033
+ expect(orders[0].amount).toBe(2999);
1034
+ });
1035
+ });
1036
+
1037
+ describe("listUserOrders query", () => {
1038
+ let t: TestConvex<typeof schema>;
1039
+
1040
+ beforeEach(() => {
1041
+ t = convexTest(schema, modules);
1042
+ });
1043
+
1044
+ it("returns empty array when no customer exists", async () => {
1045
+ const orders = await t.query(api.lib.listUserOrders, {
1046
+ entityId: "user_nonexistent",
1047
+ });
1048
+ expect(orders).toEqual([]);
1049
+ });
1050
+
1051
+ it("filters to only paid onetime orders", async () => {
1052
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
1053
+ // Paid onetime — should be included
1054
+ await t.mutation(api.lib.createOrder, {
1055
+ order: createTestOrder({
1056
+ id: "ord_paid",
1057
+ status: "paid",
1058
+ type: "onetime",
1059
+ }),
1060
+ });
1061
+ // Pending onetime — should be excluded
1062
+ await t.mutation(api.lib.createOrder, {
1063
+ order: createTestOrder({
1064
+ id: "ord_pending",
1065
+ status: "pending",
1066
+ type: "onetime",
1067
+ }),
1068
+ });
1069
+ // Paid recurring — should be excluded
1070
+ await t.mutation(api.lib.createOrder, {
1071
+ order: createTestOrder({
1072
+ id: "ord_recurring",
1073
+ status: "paid",
1074
+ type: "recurring",
1075
+ }),
1076
+ });
1077
+ const orders = await t.query(api.lib.listUserOrders, {
1078
+ entityId: "user_456",
1079
+ });
1080
+ expect(orders).toHaveLength(1);
1081
+ expect(orders[0].id).toBe("ord_paid");
1082
+ });
1083
+ });
1084
+
1085
+ describe("updateProducts mutation", () => {
1086
+ let t: TestConvex<typeof schema>;
1087
+
1088
+ beforeEach(() => {
1089
+ t = convexTest(schema, modules);
1090
+ });
1091
+
1092
+ it("inserts new products", async () => {
1093
+ await t.mutation(api.lib.updateProducts, {
1094
+ products: [
1095
+ createTestProduct({ id: "prod_a", name: "Product A" }),
1096
+ createTestProduct({ id: "prod_b", name: "Product B" }),
1097
+ ],
1098
+ });
1099
+ const products = await t.query(api.lib.listProducts, {});
1100
+ expect(products).toHaveLength(2);
1101
+ });
1102
+
1103
+ it("patches existing products", async () => {
1104
+ await t.mutation(api.lib.createProduct, {
1105
+ product: createTestProduct({ id: "prod_a", name: "Old Name" }),
1106
+ });
1107
+ await t.mutation(api.lib.updateProducts, {
1108
+ products: [createTestProduct({ id: "prod_a", name: "New Name" })],
1109
+ });
1110
+ const products = await t.query(api.lib.listProducts, {});
1111
+ expect(products).toHaveLength(1);
1112
+ expect(products[0].name).toBe("New Name");
1113
+ });
1114
+ });
1115
+
1116
+ describe("patchSubscription mutation", () => {
1117
+ let t: TestConvex<typeof schema>;
1118
+
1119
+ beforeEach(() => {
1120
+ t = convexTest(schema, modules);
1121
+ });
1122
+
1123
+ it("throws when subscription not found", async () => {
1124
+ await expect(
1125
+ t.mutation(api.lib.patchSubscription, {
1126
+ subscriptionId: "sub_nonexistent",
1127
+ }),
1128
+ ).rejects.toThrow("Subscription not found");
1129
+ });
1130
+
1131
+ it("patches seats and sets optimistic metadata", async () => {
1132
+ await t.mutation(api.lib.createSubscription, {
1133
+ subscription: createTestSubscription({ id: "sub_1", seats: 3 }),
1134
+ });
1135
+ await t.mutation(api.lib.patchSubscription, {
1136
+ subscriptionId: "sub_1",
1137
+ seats: 5,
1138
+ });
1139
+ const sub = await t.query(api.lib.getSubscription, { id: "sub_1" });
1140
+ expect(sub).not.toBeNull();
1141
+ expect(sub!.seats).toBe(5);
1142
+ const meta = sub!.metadata as Record<string, unknown>;
1143
+ expect(meta._optimisticFields).toContain("seats");
1144
+ expect(meta._optimisticPendingAt).toBeDefined();
1145
+ });
1146
+
1147
+ it("patches productId and sets optimistic metadata", async () => {
1148
+ await t.mutation(api.lib.createSubscription, {
1149
+ subscription: createTestSubscription({ id: "sub_1" }),
1150
+ });
1151
+ await t.mutation(api.lib.patchSubscription, {
1152
+ subscriptionId: "sub_1",
1153
+ productId: "prod_new",
1154
+ });
1155
+ const sub = await t.query(api.lib.getSubscription, { id: "sub_1" });
1156
+ expect(sub!.productId).toBe("prod_new");
1157
+ const meta = sub!.metadata as Record<string, unknown>;
1158
+ expect(meta._optimisticFields).toContain("productId");
1159
+ });
1160
+
1161
+ it("clears optimistic metadata with clearOptimistic flag", async () => {
1162
+ await t.mutation(api.lib.createSubscription, {
1163
+ subscription: createTestSubscription({ id: "sub_1" }),
1164
+ });
1165
+ await t.mutation(api.lib.patchSubscription, {
1166
+ subscriptionId: "sub_1",
1167
+ seats: 10,
1168
+ });
1169
+ await t.mutation(api.lib.patchSubscription, {
1170
+ subscriptionId: "sub_1",
1171
+ clearOptimistic: true,
1172
+ });
1173
+ const sub = await t.query(api.lib.getSubscription, { id: "sub_1" });
1174
+ const meta = sub!.metadata as Record<string, unknown>;
1175
+ expect(meta._optimisticPendingAt).toBeUndefined();
1176
+ expect(meta._optimisticFields).toBeUndefined();
1177
+ });
1178
+
1179
+ it("patches status and cancelAtPeriodEnd", async () => {
1180
+ await t.mutation(api.lib.createSubscription, {
1181
+ subscription: createTestSubscription({ id: "sub_1" }),
1182
+ });
1183
+ await t.mutation(api.lib.patchSubscription, {
1184
+ subscriptionId: "sub_1",
1185
+ status: "scheduled_cancel",
1186
+ cancelAtPeriodEnd: true,
1187
+ });
1188
+ const sub = await t.query(api.lib.getSubscription, { id: "sub_1" });
1189
+ expect(sub!.status).toBe("scheduled_cancel");
1190
+ expect(sub!.cancelAtPeriodEnd).toBe(true);
1191
+ });
1192
+
1193
+ it("merges optimistic fields from consecutive patches", async () => {
1194
+ await t.mutation(api.lib.createSubscription, {
1195
+ subscription: createTestSubscription({ id: "sub_1", seats: 3 }),
1196
+ });
1197
+ await t.mutation(api.lib.patchSubscription, {
1198
+ subscriptionId: "sub_1",
1199
+ seats: 5,
1200
+ });
1201
+ await t.mutation(api.lib.patchSubscription, {
1202
+ subscriptionId: "sub_1",
1203
+ productId: "prod_new",
1204
+ });
1205
+ const sub = await t.query(api.lib.getSubscription, { id: "sub_1" });
1206
+ const meta = sub!.metadata as Record<string, unknown>;
1207
+ const fields = meta._optimisticFields as string[];
1208
+ expect(fields).toContain("seats");
1209
+ expect(fields).toContain("productId");
1210
+ });
1211
+ });
1212
+
1213
+ describe("updateSubscription optimistic guard", () => {
1214
+ let t: TestConvex<typeof schema>;
1215
+
1216
+ beforeEach(() => {
1217
+ t = convexTest(schema, modules);
1218
+ });
1219
+
1220
+ it("preserves optimistic seats when webhook sends stale value", async () => {
1221
+ // Insert subscription with seats=3
1222
+ await t.mutation(api.lib.createSubscription, {
1223
+ subscription: createTestSubscription({
1224
+ id: "sub_1",
1225
+ seats: 3,
1226
+ modifiedAt: "2025-01-16T12:00:00.000Z",
1227
+ }),
1228
+ });
1229
+ // Optimistic patch: seats → 5
1230
+ await t.mutation(api.lib.patchSubscription, {
1231
+ subscriptionId: "sub_1",
1232
+ seats: 5,
1233
+ });
1234
+ // Webhook arrives with seats=3 (stale intermediate) but newer modifiedAt
1235
+ await t.mutation(api.lib.updateSubscription, {
1236
+ subscription: createTestSubscription({
1237
+ id: "sub_1",
1238
+ seats: 3,
1239
+ modifiedAt: "2025-01-17T12:00:00.000Z",
1240
+ }),
1241
+ });
1242
+ const sub = await t.query(api.lib.getSubscription, { id: "sub_1" });
1243
+ // Guard should preserve optimistic seats=5
1244
+ expect(sub!.seats).toBe(5);
1245
+ });
1246
+
1247
+ it("clears guard when webhook confirms optimistic value", async () => {
1248
+ await t.mutation(api.lib.createSubscription, {
1249
+ subscription: createTestSubscription({
1250
+ id: "sub_1",
1251
+ seats: 3,
1252
+ modifiedAt: "2025-01-16T12:00:00.000Z",
1253
+ }),
1254
+ });
1255
+ await t.mutation(api.lib.patchSubscription, {
1256
+ subscriptionId: "sub_1",
1257
+ seats: 5,
1258
+ });
1259
+ // Webhook arrives confirming seats=5
1260
+ await t.mutation(api.lib.updateSubscription, {
1261
+ subscription: createTestSubscription({
1262
+ id: "sub_1",
1263
+ seats: 5,
1264
+ modifiedAt: "2025-01-17T12:00:00.000Z",
1265
+ }),
1266
+ });
1267
+ const sub = await t.query(api.lib.getSubscription, { id: "sub_1" });
1268
+ expect(sub!.seats).toBe(5);
1269
+ const meta = sub!.metadata as Record<string, unknown>;
1270
+ expect(meta._optimisticPendingAt).toBeUndefined();
1271
+ expect(meta._optimisticFields).toBeUndefined();
1272
+ });
1273
+
1274
+ it("preserves optimistic productId when webhook sends stale value", async () => {
1275
+ await t.mutation(api.lib.createSubscription, {
1276
+ subscription: createTestSubscription({
1277
+ id: "sub_1",
1278
+ productId: "prod_old",
1279
+ modifiedAt: "2025-01-16T12:00:00.000Z",
1280
+ }),
1281
+ });
1282
+ await t.mutation(api.lib.patchSubscription, {
1283
+ subscriptionId: "sub_1",
1284
+ productId: "prod_new",
1285
+ });
1286
+ // Webhook with stale productId
1287
+ await t.mutation(api.lib.updateSubscription, {
1288
+ subscription: createTestSubscription({
1289
+ id: "sub_1",
1290
+ productId: "prod_old",
1291
+ modifiedAt: "2025-01-17T12:00:00.000Z",
1292
+ }),
1293
+ });
1294
+ const sub = await t.query(api.lib.getSubscription, { id: "sub_1" });
1295
+ expect(sub!.productId).toBe("prod_new");
1296
+ });
1297
+ });
1298
+
1299
+ describe("insertCustomer mutation enrichment", () => {
1300
+ let t: TestConvex<typeof schema>;
1301
+
1302
+ beforeEach(() => {
1303
+ t = convexTest(schema, modules);
1304
+ });
1305
+
1306
+ it("enriches existing customer with new email and name", async () => {
1307
+ await t.mutation(
1308
+ api.lib.insertCustomer,
1309
+ createTestCustomer({
1310
+ id: "cust_1",
1311
+ entityId: "user_1",
1312
+ }),
1313
+ );
1314
+ // Insert again with additional fields
1315
+ await t.mutation(
1316
+ api.lib.insertCustomer,
1317
+ createTestCustomer({
1318
+ id: "cust_1",
1319
+ entityId: "user_1",
1320
+ email: "test@example.com",
1321
+ name: "Test User",
1322
+ country: "US",
1323
+ mode: "live",
1324
+ updatedAt: "2025-02-01T00:00:00.000Z",
1325
+ }),
1326
+ );
1327
+ const customer = await t.query(api.lib.getCustomerByEntityId, {
1328
+ entityId: "user_1",
1329
+ });
1330
+ expect(customer).not.toBeNull();
1331
+ expect(customer!.email).toBe("test@example.com");
1332
+ expect(customer!.name).toBe("Test User");
1333
+ expect(customer!.country).toBe("US");
1334
+ });
1335
+ });
1336
+
1337
+ describe("getCurrentSubscription query edge cases", () => {
1338
+ let t: TestConvex<typeof schema>;
1339
+
1340
+ beforeEach(() => {
1341
+ t = convexTest(schema, modules);
1342
+ });
1343
+
1344
+ it("throws when product is missing for subscription", async () => {
1345
+ await t.mutation(api.lib.insertCustomer, createTestCustomer());
1346
+ await t.mutation(api.lib.createSubscription, {
1347
+ subscription: createTestSubscription({
1348
+ id: "sub_no_product",
1349
+ customerId: "cust_123",
1350
+ productId: "prod_nonexistent",
1351
+ endedAt: null,
1352
+ status: "active",
1353
+ }),
1354
+ });
1355
+ await expect(
1356
+ t.query(api.lib.getCurrentSubscription, { entityId: "user_456" }),
1357
+ ).rejects.toThrow("Product not found");
1358
+ });
1359
+ });