@volter/twin-stripe 2.0.38 → 3.0.1

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 (402) hide show
  1. package/README.md +112 -207
  2. package/dist/generated/pack-facts.json +94 -0
  3. package/dist/journeys/coverage.json +596 -0
  4. package/dist/journeys/customer-life.json +6108 -0
  5. package/dist/journeys/customer-life.outline.md +371 -0
  6. package/dist/journeys/decisions.json +677 -0
  7. package/dist/journeys/demand.json +706 -0
  8. package/dist/journeys/score.json +853 -0
  9. package/dist/journeys/unreachable.json +102 -0
  10. package/dist/journeys/vendor-examples.json +8010 -0
  11. package/dist/spec/SOURCE.md +18 -0
  12. package/dist/spec/doc-examples.json +445 -0
  13. package/dist/spec/fixtures3.json.gz +0 -0
  14. package/dist/spec/openapi.json.gz +0 -0
  15. package/dist/spec/patches.json +34 -0
  16. package/dist/spec/sources.json +306 -0
  17. package/dist/src/cli.d.ts +0 -1
  18. package/dist/src/cli.js +11 -31
  19. package/dist/src/engine/after-payment.d.ts +8 -0
  20. package/dist/src/engine/after-payment.js +27 -0
  21. package/dist/src/engine/apps-secrets.d.ts +1 -0
  22. package/dist/src/engine/apps-secrets.js +3 -0
  23. package/dist/src/engine/balance.d.ts +5 -0
  24. package/dist/src/engine/balance.js +15 -0
  25. package/dist/src/engine/checkout.d.ts +24 -0
  26. package/dist/src/engine/checkout.js +30 -0
  27. package/dist/src/engine/common.d.ts +19 -0
  28. package/dist/src/engine/common.js +34 -0
  29. package/dist/src/engine/connect.d.ts +45 -0
  30. package/dist/src/engine/connect.js +108 -0
  31. package/dist/src/engine/customers.d.ts +4 -0
  32. package/dist/src/engine/customers.js +8 -0
  33. package/dist/src/engine/display.d.ts +11 -0
  34. package/dist/src/engine/display.js +13 -0
  35. package/dist/src/engine/files.d.ts +11 -0
  36. package/dist/src/engine/files.js +23 -0
  37. package/dist/src/engine/invoices.d.ts +21 -0
  38. package/dist/src/engine/invoices.js +84 -0
  39. package/dist/src/engine/issuing.d.ts +33 -0
  40. package/dist/src/engine/issuing.js +54 -0
  41. package/dist/src/engine/ledger.d.ts +8 -0
  42. package/dist/src/engine/ledger.js +8 -0
  43. package/dist/src/engine/oauth.d.ts +4 -0
  44. package/dist/src/engine/oauth.js +24 -0
  45. package/dist/src/engine/onboarding.d.ts +13 -0
  46. package/dist/src/engine/onboarding.js +42 -0
  47. package/dist/src/{stripe-params.d.ts → engine/params.d.ts} +20 -3
  48. package/dist/src/{stripe-params.js → engine/params.js} +7 -60
  49. package/dist/src/engine/payment-intents.d.ts +10 -0
  50. package/dist/src/engine/payment-intents.js +27 -0
  51. package/dist/src/engine/payment-methods.d.ts +16 -0
  52. package/dist/src/engine/payment-methods.js +22 -0
  53. package/dist/src/engine/radar.d.ts +2 -0
  54. package/dist/src/engine/radar.js +5 -0
  55. package/dist/src/engine/refunds.d.ts +1 -0
  56. package/dist/src/engine/refunds.js +3 -0
  57. package/dist/src/engine/renewals.d.ts +36 -0
  58. package/dist/src/engine/renewals.js +65 -0
  59. package/dist/src/{stripe-twin.d.ts → engine/stripe.d.ts} +12 -45
  60. package/dist/src/{stripe-twin.js → engine/stripe.js} +53 -387
  61. package/dist/src/engine/subscriptions.d.ts +11 -0
  62. package/dist/src/engine/subscriptions.js +30 -0
  63. package/dist/src/engine/transfers.d.ts +2 -0
  64. package/dist/src/engine/transfers.js +1 -0
  65. package/dist/src/{stripe-version.d.ts → engine/version.d.ts} +0 -2
  66. package/dist/src/{stripe-version.js → engine/version.js} +21 -9
  67. package/dist/src/engine/webhook-endpoints.d.ts +1 -0
  68. package/dist/src/engine/webhook-endpoints.js +12 -0
  69. package/dist/src/engine/webhooks.d.ts +31 -0
  70. package/dist/src/engine/webhooks.js +68 -0
  71. package/dist/src/fetch.d.ts +2 -0
  72. package/dist/src/fetch.js +26 -0
  73. package/dist/src/generated/surface.gen.json +1 -1
  74. package/dist/src/generated/ui.gen.json +1 -1
  75. package/dist/src/index.d.ts +5 -14
  76. package/dist/src/index.js +10 -75
  77. package/dist/src/manifest.js +478 -876
  78. package/dist/src/screens/billing-portal.d.ts +3 -0
  79. package/dist/src/screens/{portal.js → billing-portal.js} +32 -58
  80. package/dist/src/screens/checkout-js.d.ts +2 -0
  81. package/dist/src/screens/checkout-js.js +84 -0
  82. package/dist/src/screens/checkout.d.ts +2 -31
  83. package/dist/src/screens/checkout.js +32 -121
  84. package/dist/src/screens/connect-onboarding.d.ts +3 -0
  85. package/dist/src/screens/{onboarding.js → connect-onboarding.js} +38 -104
  86. package/dist/src/screens/dashboard.d.ts +3 -0
  87. package/dist/src/screens/dashboard.js +116 -0
  88. package/dist/src/screens/shared.d.ts +27 -0
  89. package/dist/src/screens/shared.js +87 -0
  90. package/dist/src/screens/stripe-js.d.ts +3 -0
  91. package/dist/src/screens/stripe-js.js +95 -0
  92. package/dist/src/semantics/account.d.ts +2 -0
  93. package/dist/src/semantics/account.js +8 -0
  94. package/dist/src/semantics/account_links.d.ts +2 -0
  95. package/dist/src/semantics/account_links.js +22 -0
  96. package/dist/src/semantics/accounts.d.ts +4 -0
  97. package/dist/src/semantics/accounts.js +40 -0
  98. package/dist/src/semantics/apps.d.ts +4 -0
  99. package/dist/src/semantics/{apps-secrets.js → apps.js} +14 -26
  100. package/dist/src/semantics/around.d.ts +2 -0
  101. package/dist/src/semantics/around.js +13 -0
  102. package/dist/src/semantics/balance.d.ts +2 -11
  103. package/dist/src/semantics/balance.js +3 -194
  104. package/dist/src/semantics/balance_transactions.d.ts +2 -0
  105. package/dist/src/semantics/balance_transactions.js +15 -0
  106. package/dist/src/semantics/billing_portal.d.ts +4 -0
  107. package/dist/src/semantics/billing_portal.js +36 -0
  108. package/dist/src/semantics/charges.d.ts +3 -28
  109. package/dist/src/semantics/charges.js +5 -202
  110. package/dist/src/semantics/checkout.d.ts +3 -20
  111. package/dist/src/semantics/checkout.js +21 -271
  112. package/dist/src/semantics/clock.d.ts +2 -0
  113. package/dist/src/semantics/clock.js +15 -0
  114. package/dist/src/semantics/coupons.d.ts +2 -6
  115. package/dist/src/semantics/coupons.js +2 -60
  116. package/dist/src/semantics/customers.d.ts +5 -6
  117. package/dist/src/semantics/customers.js +47 -413
  118. package/dist/src/semantics/doors.d.ts +23 -0
  119. package/dist/src/semantics/doors.js +73 -0
  120. package/dist/src/semantics/entitlements.d.ts +0 -2
  121. package/dist/src/semantics/entitlements.js +4 -95
  122. package/dist/src/semantics/events.d.ts +11 -0
  123. package/dist/src/semantics/events.js +31 -0
  124. package/dist/src/semantics/index.d.ts +2 -0
  125. package/dist/src/semantics/index.js +85 -0
  126. package/dist/src/semantics/invoice_payments.d.ts +2 -0
  127. package/dist/src/semantics/invoice_payments.js +12 -0
  128. package/dist/src/semantics/invoices.d.ts +11 -18
  129. package/dist/src/semantics/invoices.js +121 -440
  130. package/dist/src/semantics/issuing.d.ts +6 -13
  131. package/dist/src/semantics/issuing.js +58 -556
  132. package/dist/src/semantics/payment_intents.d.ts +3 -0
  133. package/dist/src/semantics/payment_intents.js +71 -0
  134. package/dist/src/semantics/payment_methods.d.ts +3 -0
  135. package/dist/src/semantics/payment_methods.js +15 -0
  136. package/dist/src/semantics/payouts.d.ts +3 -0
  137. package/dist/src/semantics/payouts.js +28 -0
  138. package/dist/src/semantics/prices.d.ts +4 -0
  139. package/dist/src/semantics/prices.js +37 -0
  140. package/dist/src/semantics/products.d.ts +4 -2
  141. package/dist/src/semantics/products.js +12 -138
  142. package/dist/src/semantics/promotion_codes.d.ts +3 -0
  143. package/dist/src/semantics/promotion_codes.js +35 -0
  144. package/dist/src/semantics/radar.d.ts +2 -2
  145. package/dist/src/semantics/radar.js +5 -74
  146. package/dist/src/semantics/refunds.d.ts +2 -9
  147. package/dist/src/semantics/refunds.js +3 -193
  148. package/dist/src/semantics/shared.d.ts +558 -55
  149. package/dist/src/semantics/shared.js +2070 -114
  150. package/dist/src/semantics/states.d.ts +462 -0
  151. package/dist/src/semantics/states.js +634 -0
  152. package/dist/src/semantics/subscriptions.d.ts +6 -11
  153. package/dist/src/semantics/subscriptions.js +37 -530
  154. package/dist/src/semantics/test_helpers.d.ts +3 -0
  155. package/dist/src/semantics/test_helpers.js +125 -0
  156. package/dist/src/semantics/transfers.d.ts +2 -2
  157. package/dist/src/semantics/transfers.js +6 -101
  158. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  159. package/dist/src/semantics/webhook_endpoints.js +26 -0
  160. package/dist/src/server.d.ts +7 -0
  161. package/dist/src/server.js +7 -0
  162. package/generated/pack-facts.json +94 -0
  163. package/journeys/coverage.json +596 -0
  164. package/journeys/customer-life.json +6108 -0
  165. package/journeys/customer-life.outline.md +371 -0
  166. package/journeys/decisions.json +677 -0
  167. package/journeys/demand.json +706 -0
  168. package/journeys/score.json +853 -0
  169. package/journeys/unreachable.json +102 -0
  170. package/journeys/vendor-examples.json +8010 -0
  171. package/package.json +11 -20
  172. package/spec/SOURCE.md +18 -0
  173. package/spec/doc-examples.json +445 -0
  174. package/spec/fixtures3.json.gz +0 -0
  175. package/spec/openapi.json.gz +0 -0
  176. package/spec/patches.json +34 -0
  177. package/spec/sources.json +306 -0
  178. package/src/cli.ts +11 -28
  179. package/src/engine/after-payment.ts +29 -0
  180. package/src/engine/apps-secrets.ts +4 -0
  181. package/src/engine/balance.ts +32 -0
  182. package/src/engine/checkout.ts +39 -0
  183. package/src/engine/common.ts +49 -0
  184. package/src/engine/connect.ts +106 -0
  185. package/src/engine/customers.ts +10 -0
  186. package/src/engine/display.ts +18 -0
  187. package/src/engine/files.ts +28 -0
  188. package/src/engine/invoices.ts +102 -0
  189. package/src/engine/issuing.ts +66 -0
  190. package/src/engine/ledger.ts +48 -0
  191. package/src/engine/oauth.ts +18 -0
  192. package/src/engine/onboarding.ts +53 -0
  193. package/src/{stripe-params.ts → engine/params.ts} +7 -56
  194. package/src/engine/payment-intents.ts +32 -0
  195. package/src/engine/payment-methods.ts +24 -0
  196. package/src/engine/radar.ts +6 -0
  197. package/src/engine/refunds.ts +3 -0
  198. package/src/engine/renewals.ts +72 -0
  199. package/src/{stripe-twin.ts → engine/stripe.ts} +57 -400
  200. package/src/engine/subscriptions.ts +37 -0
  201. package/src/engine/transfers.ts +22 -0
  202. package/src/{stripe-version.ts → engine/version.ts} +19 -10
  203. package/src/engine/webhook-endpoints.ts +14 -0
  204. package/src/engine/webhooks.ts +70 -0
  205. package/src/fetch.ts +27 -0
  206. package/src/generated/surface.gen.json +1 -1
  207. package/src/generated/ui.gen.json +1 -1
  208. package/src/index.ts +10 -116
  209. package/src/manifest.ts +480 -904
  210. package/src/screens/{portal.tsx → billing-portal.tsx} +35 -42
  211. package/src/screens/checkout-js.ts +80 -0
  212. package/src/screens/checkout.tsx +32 -96
  213. package/src/screens/{onboarding.tsx → connect-onboarding.tsx} +39 -85
  214. package/src/screens/dashboard.tsx +178 -0
  215. package/src/screens/shared.tsx +84 -0
  216. package/src/screens/stripe-js.ts +102 -0
  217. package/src/semantics/account.ts +10 -0
  218. package/src/semantics/account_links.ts +20 -0
  219. package/src/semantics/accounts.ts +44 -0
  220. package/src/semantics/{apps-secrets.ts → apps.ts} +17 -33
  221. package/src/semantics/around.ts +17 -0
  222. package/src/semantics/balance.ts +6 -208
  223. package/src/semantics/balance_transactions.ts +18 -0
  224. package/src/semantics/billing_portal.ts +45 -0
  225. package/src/semantics/charges.ts +8 -212
  226. package/src/semantics/checkout.ts +25 -276
  227. package/src/semantics/clock.ts +19 -0
  228. package/src/semantics/coupons.ts +5 -68
  229. package/src/semantics/customers.ts +48 -414
  230. package/src/semantics/doors.ts +79 -0
  231. package/src/semantics/entitlements.ts +3 -94
  232. package/src/semantics/events.ts +37 -0
  233. package/src/semantics/index.ts +89 -0
  234. package/src/semantics/invoice_payments.ts +16 -0
  235. package/src/semantics/invoices.ts +112 -441
  236. package/src/semantics/issuing.ts +57 -573
  237. package/src/semantics/payment_intents.ts +76 -0
  238. package/src/semantics/payment_methods.ts +20 -0
  239. package/src/semantics/payouts.ts +29 -0
  240. package/src/semantics/prices.ts +41 -0
  241. package/src/semantics/products.ts +12 -146
  242. package/src/semantics/promotion_codes.ts +38 -0
  243. package/src/semantics/radar.ts +8 -79
  244. package/src/semantics/refunds.ts +4 -216
  245. package/src/semantics/shared.ts +2089 -135
  246. package/src/semantics/states.ts +662 -0
  247. package/src/semantics/subscriptions.ts +39 -542
  248. package/src/semantics/test_helpers.ts +119 -0
  249. package/src/semantics/transfers.ts +10 -132
  250. package/src/semantics/webhook_endpoints.ts +27 -0
  251. package/src/server.ts +7 -0
  252. package/client/dashboard-api.ts +0 -286
  253. package/client/stripe-mirror.css +0 -277
  254. package/client/stripe-mirror.tsx +0 -1559
  255. package/dist/client/dashboard-api.d.ts +0 -107
  256. package/dist/client/dashboard-api.js +0 -238
  257. package/dist/client/dashboard-api.ts +0 -286
  258. package/dist/client/stripe-mirror.bundle.js +0 -236
  259. package/dist/client/stripe-mirror.css +0 -277
  260. package/dist/client/stripe-mirror.d.ts +0 -134
  261. package/dist/client/stripe-mirror.js +0 -859
  262. package/dist/client/stripe-mirror.tsx +0 -1559
  263. package/dist/src/screens/connect-oauth.d.ts +0 -27
  264. package/dist/src/screens/connect-oauth.js +0 -414
  265. package/dist/src/screens/connect-settings.d.ts +0 -22
  266. package/dist/src/screens/connect-settings.js +0 -103
  267. package/dist/src/screens/consent-skin.d.ts +0 -4
  268. package/dist/src/screens/consent-skin.js +0 -18
  269. package/dist/src/screens/financial-connections.d.ts +0 -5
  270. package/dist/src/screens/financial-connections.js +0 -90
  271. package/dist/src/screens/identity.d.ts +0 -5
  272. package/dist/src/screens/identity.js +0 -86
  273. package/dist/src/screens/onboarding.d.ts +0 -13
  274. package/dist/src/screens/portal.d.ts +0 -5
  275. package/dist/src/screens/public-details.d.ts +0 -5
  276. package/dist/src/screens/public-details.js +0 -90
  277. package/dist/src/semantics/after-payment.d.ts +0 -22
  278. package/dist/src/semantics/after-payment.js +0 -99
  279. package/dist/src/semantics/apps-secrets.d.ts +0 -2
  280. package/dist/src/semantics/billing.d.ts +0 -2
  281. package/dist/src/semantics/billing.js +0 -220
  282. package/dist/src/semantics/connect.d.ts +0 -5
  283. package/dist/src/semantics/connect.js +0 -493
  284. package/dist/src/semantics/credit-notes.d.ts +0 -2
  285. package/dist/src/semantics/credit-notes.js +0 -172
  286. package/dist/src/semantics/disputes.d.ts +0 -2
  287. package/dist/src/semantics/disputes.js +0 -51
  288. package/dist/src/semantics/ephemeral-keys.d.ts +0 -2
  289. package/dist/src/semantics/ephemeral-keys.js +0 -34
  290. package/dist/src/semantics/files.d.ts +0 -2
  291. package/dist/src/semantics/files.js +0 -125
  292. package/dist/src/semantics/ledger.d.ts +0 -59
  293. package/dist/src/semantics/ledger.js +0 -200
  294. package/dist/src/semantics/payment-intents.d.ts +0 -18
  295. package/dist/src/semantics/payment-intents.js +0 -452
  296. package/dist/src/semantics/payment-links.d.ts +0 -2
  297. package/dist/src/semantics/payment-links.js +0 -133
  298. package/dist/src/semantics/payment-methods.d.ts +0 -20
  299. package/dist/src/semantics/payment-methods.js +0 -140
  300. package/dist/src/semantics/plans.d.ts +0 -5
  301. package/dist/src/semantics/plans.js +0 -121
  302. package/dist/src/semantics/platform.d.ts +0 -9
  303. package/dist/src/semantics/platform.js +0 -206
  304. package/dist/src/semantics/renewals.d.ts +0 -47
  305. package/dist/src/semantics/renewals.js +0 -281
  306. package/dist/src/semantics/setup-intents.d.ts +0 -2
  307. package/dist/src/semantics/setup-intents.js +0 -84
  308. package/dist/src/semantics/subscription-schedules.d.ts +0 -2
  309. package/dist/src/semantics/subscription-schedules.js +0 -119
  310. package/dist/src/semantics/tax.d.ts +0 -2
  311. package/dist/src/semantics/tax.js +0 -197
  312. package/dist/src/semantics/terminal.d.ts +0 -5
  313. package/dist/src/semantics/terminal.js +0 -182
  314. package/dist/src/semantics/test-clocks.d.ts +0 -6
  315. package/dist/src/semantics/test-clocks.js +0 -73
  316. package/dist/src/semantics/tokens.d.ts +0 -4
  317. package/dist/src/semantics/tokens.js +0 -44
  318. package/dist/src/semantics/treasury.d.ts +0 -2
  319. package/dist/src/semantics/treasury.js +0 -377
  320. package/dist/src/semantics/webhook-endpoints.d.ts +0 -3
  321. package/dist/src/semantics/webhook-endpoints.js +0 -85
  322. package/dist/src/stripe-budget.d.ts +0 -55
  323. package/dist/src/stripe-budget.js +0 -155
  324. package/dist/src/stripe-capabilities.d.ts +0 -3
  325. package/dist/src/stripe-capabilities.js +0 -5836
  326. package/dist/src/stripe-conformance.d.ts +0 -43
  327. package/dist/src/stripe-conformance.js +0 -105
  328. package/dist/src/stripe-connector.d.ts +0 -161
  329. package/dist/src/stripe-connector.js +0 -414
  330. package/dist/src/stripe-emit.d.ts +0 -2
  331. package/dist/src/stripe-emit.js +0 -145
  332. package/dist/src/stripe-events.d.ts +0 -93
  333. package/dist/src/stripe-events.js +0 -392
  334. package/dist/src/stripe-js.d.ts +0 -4
  335. package/dist/src/stripe-js.js +0 -70
  336. package/dist/src/stripe-mirror-ui.d.ts +0 -15
  337. package/dist/src/stripe-mirror-ui.js +0 -87
  338. package/dist/src/stripe-perform-harness.d.ts +0 -9
  339. package/dist/src/stripe-perform-harness.js +0 -26
  340. package/dist/src/stripe-server.d.ts +0 -33
  341. package/dist/src/stripe-server.js +0 -429
  342. package/dist/src/stripe-shared.d.ts +0 -109
  343. package/dist/src/stripe-shared.js +0 -276
  344. package/dist/src/stripe-ui-conformance.d.ts +0 -5
  345. package/dist/src/stripe-ui-conformance.js +0 -79
  346. package/dist/src/stripe-ui-structure.d.ts +0 -3
  347. package/dist/src/stripe-ui-structure.js +0 -168
  348. package/dist/test-fixtures/stripe-known-deviations.json +0 -110
  349. package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +0 -14
  350. package/dist/test-fixtures/stripe-openapi-operations.json +0 -4717
  351. package/dist/test-fixtures/stripe-schemas.SOURCE.md +0 -35
  352. package/dist/test-fixtures/stripe-schemas.json +0 -3813
  353. package/src/screens/connect-oauth.tsx +0 -400
  354. package/src/screens/connect-settings.tsx +0 -121
  355. package/src/screens/consent-skin.ts +0 -20
  356. package/src/screens/financial-connections.tsx +0 -101
  357. package/src/screens/identity.tsx +0 -96
  358. package/src/screens/public-details.tsx +0 -105
  359. package/src/semantics/after-payment.ts +0 -118
  360. package/src/semantics/billing.ts +0 -216
  361. package/src/semantics/connect.ts +0 -487
  362. package/src/semantics/credit-notes.ts +0 -168
  363. package/src/semantics/disputes.ts +0 -62
  364. package/src/semantics/ephemeral-keys.ts +0 -34
  365. package/src/semantics/files.ts +0 -143
  366. package/src/semantics/ledger.ts +0 -253
  367. package/src/semantics/payment-intents.ts +0 -463
  368. package/src/semantics/payment-links.ts +0 -148
  369. package/src/semantics/payment-methods.ts +0 -145
  370. package/src/semantics/plans.ts +0 -131
  371. package/src/semantics/platform.ts +0 -220
  372. package/src/semantics/renewals.ts +0 -279
  373. package/src/semantics/setup-intents.ts +0 -87
  374. package/src/semantics/subscription-schedules.ts +0 -129
  375. package/src/semantics/tax.ts +0 -220
  376. package/src/semantics/terminal.ts +0 -195
  377. package/src/semantics/test-clocks.ts +0 -77
  378. package/src/semantics/tokens.ts +0 -52
  379. package/src/semantics/treasury.ts +0 -383
  380. package/src/semantics/webhook-endpoints.ts +0 -87
  381. package/src/stripe-budget.ts +0 -181
  382. package/src/stripe-capabilities.ts +0 -5651
  383. package/src/stripe-conformance.ts +0 -124
  384. package/src/stripe-connector.ts +0 -462
  385. package/src/stripe-emit.ts +0 -157
  386. package/src/stripe-events.ts +0 -434
  387. package/src/stripe-js.ts +0 -70
  388. package/src/stripe-mirror-ui.ts +0 -90
  389. package/src/stripe-perform-harness.ts +0 -29
  390. package/src/stripe-server.ts +0 -393
  391. package/src/stripe-shared.ts +0 -297
  392. package/src/stripe-ui-conformance.ts +0 -88
  393. package/src/stripe-ui-structure.ts +0 -182
  394. package/test-fixtures/stripe-known-deviations.json +0 -110
  395. package/test-fixtures/stripe-openapi-operations.json +0 -4717
  396. package/test-fixtures/stripe-schemas.json +0 -3813
  397. /package/dist/src/{screens → engine}/industries.d.ts +0 -0
  398. /package/dist/src/{screens → engine}/industries.js +0 -0
  399. /package/dist/src/{semantics → engine}/test-cards.d.ts +0 -0
  400. /package/dist/src/{semantics → engine}/test-cards.js +0 -0
  401. /package/src/{screens → engine}/industries.ts +0 -0
  402. /package/src/{semantics → engine}/test-cards.ts +0 -0
@@ -1,353 +1,51 @@
1
- // Subscription and SubscriptionItem semantics. A subscription is created with its items, its
2
- // billing period and, under payment_behavior=default_incomplete, its first invoice and that
3
- // invoice's PaymentIntent; canceling it is the machine's move in ../manifest.ts. Retrieve of a
4
- // subscription and of an item are the derived core's.
5
- import type { Semantics, SemanticsContext } from '@volter/world-core';
6
- import {
7
- addBillingInterval,
8
- applyCouponDiscount,
9
- asBool,
10
- buildDiscount,
11
- buildSubscriptionItemsList,
12
- cardError,
13
- declineFor,
14
- mintClientSecret,
15
- priceAmount,
16
- resolveSubscriptionBillingInterval,
17
- resolveTrial,
18
- subscriptionCouponParam,
19
- subscriptionHasPrice,
20
- subscriptionItemEntries,
21
- subscriptionItemPrice,
22
- } from '../stripe-twin.ts';
23
- import { finalizedFields } from './invoices.ts';
24
- import { mintCharge } from './payment-intents.ts';
25
- import { advanceBilling, clockNow, collectSubscriptionInvoice, draftSubscriptionInvoice, itemQuantity, keptAfter, payerOf, spendOnceCoupon } from './renewals.ts';
26
- import { at, created, deletedDiscount, expanded, fail, finder, list, newest, redeemCoupon, search, send, where, type Row } from './shared.ts';
27
-
28
- const SUB = 'subscription';
29
- const subMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such subscription: '${id}'`, 404, 'resource_missing');
30
-
31
- const create: Semantics = async (ctx) => {
32
- const params = ctx.params;
33
- const customer = typeof params.customer === 'string' ? params.customer : '';
34
- if (!customer) return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
35
- if (!ctx.get('customer', customer)) return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
36
- const now = clockNow(ctx, customer);
37
- const badTrial = trialEndRefused(ctx, params.trial_end, now);
38
- if (badTrial) return badTrial;
39
- // a coupon (or discounts[0][coupon]) must exist; it becomes the subscription's discount
40
- const couponId = subscriptionCouponParam(params);
41
- let discounts: Row[] = [];
42
- if (couponId) {
43
- const coupon = ctx.get('coupon', couponId);
44
- if (!coupon) return fail(ctx, `No such coupon: '${couponId}'`, 400, 'resource_missing');
45
- discounts = [buildDiscount(coupon, customer, now)];
46
- }
47
- // a trial is the current period; otherwise one billing interval of the first item's price. The
48
- // period sits top-level: this twin's default API version predates basil moving it onto items.
49
- const trial = resolveTrial(params, now);
50
- const itemEntries = subscriptionItemEntries(params.items);
51
- const { interval, interval_count } = resolveSubscriptionBillingInterval(itemEntries, finder(ctx));
52
- const periodStart = trial ? trial.start : now;
53
- const periodEnd = trial ? trial.end : addBillingInterval(now, interval, interval_count);
54
- const subId = typeof params.id === 'string' && params.id ? params.id : ctx.mint(SUB);
55
- const itemsList = buildSubscriptionItemsList(itemEntries, subId, now, finder(ctx));
56
- const itemsData = itemsList.data as Row[];
57
- const currency = typeof params.currency === 'string' && params.currency ? params.currency : 'usd';
58
-
59
- // Stripe opens a subscription's first invoice at once (a trialing one has nothing due yet) and, by
60
- // payment_behavior (docs.stripe.com/api/subscriptions/create#create_subscription-payment_behavior):
61
- // allow_incomplete (the default) charges the default payment method now, and without one, or on a decline,
62
- // leaves the subscription incomplete with the invoice open; error_if_incomplete refuses the create instead;
63
- // default_incomplete never charges, leaving a PaymentIntent the frontend confirms.
64
- const behavior = typeof params.payment_behavior === 'string' ? params.payment_behavior : 'allow_incomplete';
65
- const needsFirstInvoice = !trial;
66
- const holder = ctx.get('customer', customer) ?? {};
67
- const payWith = behavior === 'default_incomplete' ? undefined
68
- : (typeof params.default_payment_method === 'string' ? params.default_payment_method : undefined) ?? ((holder.invoice_settings as Row | undefined)?.default_payment_method as string | undefined) ?? undefined;
69
- const declined = payWith ? declineFor(finder(ctx), { payment_method: payWith }) : undefined;
70
- let subtotal = 0;
71
- for (const it of itemsData) {
72
- const price = it.price && typeof it.price === 'object' ? (it.price as Row) : undefined;
73
- subtotal += priceAmount(price, itemQuantity(it));
74
- }
75
- const coupon = discounts[0]?.coupon as Row | undefined;
76
- const discountAmount = applyCouponDiscount(subtotal, coupon);
77
- const invoiceTotal = Math.max(0, subtotal - discountAmount);
78
- // a $0 first invoice is paid at once and needs no PaymentIntent
79
- const isFree = invoiceTotal <= 0;
80
- const pays = needsFirstInvoice && !isFree && !!payWith && !declined;
81
- if (needsFirstInvoice && !isFree && !pays && behavior === 'error_if_incomplete') return firstPaymentRefused(ctx, declined);
82
- // minted first so the subscription names its latest_invoice in its own create (a trial's $0 invoice too)
83
- const invoiceId = needsFirstInvoice ? ctx.mint('invoice') : null;
84
- const trialInvoiceId = needsFirstInvoice ? null : ctx.mint('invoice');
85
- const piId = needsFirstInvoice && !isFree ? ctx.mint('payment_intent') : null;
86
-
87
- const { coupon: _c, discounts: _ds, trial_period_days: _tpd, trial_end: _te, items: _items, id: _id, ...rest } = params;
88
- const sub = await created(ctx, SUB, { ...rest, id: subId }, {
89
- status: trial ? 'trialing' : needsFirstInvoice && !isFree && !pays ? 'incomplete' : 'active',
90
- livemode: false, currency, collection_method: 'charge_automatically',
91
- // a trial's end is the billing anchor: "The `billing_cycle_anchor` will be updated to the `trial_end` value"
92
- // (docs.stripe.com/api/subscriptions/create#create_subscription-trial_end)
93
- cancel_at_period_end: false, start_date: now, billing_cycle_anchor: trial ? trial.end : now, metadata: {},
94
- discounts: discounts.map((d) => ({ ...d, subscription: subId })), billing_schedules: [],
95
- ...(trial ? { trial_start: trial.start, trial_end: trial.end } : { trial_start: null, trial_end: null }),
96
- current_period_start: periodStart, current_period_end: periodEnd,
97
- items: itemsList,
98
- latest_invoice: invoiceId ?? trialInvoiceId,
99
- automatic_tax: { enabled: false, liability: null },
100
- billing_mode: { type: 'classic' },
101
- invoice_settings: { issuer: { type: 'self' } },
102
- });
103
- await storeItems(ctx, subId, itemsData);
104
- if (discounts.length) await redeemCoupon(ctx, couponId);
105
- // a customer billed for the first time takes the subscription's currency
106
- if (!holder.currency) await ctx.write('customer', customer, { currency }, 'customer.update');
107
- if (!invoiceId) {
108
- // a trial's subscription still opens an invoice at once: "An immediate invoice is still created, but the amount is 0"
109
- // (docs.stripe.com/billing/subscriptions/trials/free-trials); finalizing it spends a once coupon (renewals.ts)
110
- const trialInvoice = await draftSubscriptionInvoice(ctx, ctx.get(SUB, subId) ?? sub, { start: periodStart, end: periodEnd, reason: 'subscription_create', trial: true, ...(trialInvoiceId ? { id: trialInvoiceId } : {}) });
111
- await collectSubscriptionInvoice(ctx, trialInvoice, now, 'api');
112
- return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
113
- }
114
- // the default payment method pays the first invoice now: its charge (with its ledger entry) comes first
115
- const chargeId = pays && piId ? await mintCharge(ctx, piId, { currency, customer, payment_method: payWith, invoice: invoiceId }, invoiceTotal) : null;
116
-
117
- const invoiceLines = itemsData.map((it, i) => {
118
- const price = it.price && typeof it.price === 'object' ? (it.price as Row) : undefined;
119
- const quantity = itemQuantity(it);
120
- return {
121
- id: `il_twin_${invoiceId}_${i + 1}`, object: 'line_item', type: 'subscription',
122
- amount: priceAmount(price, quantity), currency, quantity, proration: false,
123
- price: it.price ?? null, subscription: subId, invoice_item: null, subscription_item: it.id ?? null,
124
- period: { start: periodStart, end: periodEnd }, description: null,
125
- };
126
- });
127
- await created(ctx, 'invoice', {
128
- id: invoiceId,
129
- ...finalizedFields(ctx, { id: invoiceId, customer }, now),
130
- status: isFree || pays ? 'paid' : 'open', livemode: false, currency, collection_method: 'charge_automatically',
131
- auto_advance: !(isFree || pays), attempt_count: payWith && !isFree ? 1 : 0, attempted: !isFree && behavior !== 'default_incomplete',
132
- customer, subscription: subId, billing_reason: 'subscription_create', charge: chargeId,
133
- amount_due: invoiceTotal, amount_paid: isFree || pays ? invoiceTotal : 0, amount_remaining: isFree || pays ? 0 : invoiceTotal,
134
- amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
135
- subtotal, total: invoiceTotal, starting_balance: 0,
136
- post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
137
- period_start: periodStart, period_end: periodEnd,
138
- default_tax_rates: [], discounts: discounts[0] ? [discounts[0]] : [],
139
- lines: { object: 'list', data: invoiceLines, has_more: false, total_count: invoiceLines.length, url: `/v1/invoices/${invoiceId}/lines` },
140
- automatic_tax: { enabled: false, liability: null, status: null },
141
- status_transitions: { finalized_at: now, marked_uncollectible_at: null, paid_at: isFree || pays ? now : null, voided_at: null },
142
- issuer: { type: 'self' },
143
- payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
144
- payment_intent: piId,
145
- }, {});
146
- if (piId) {
147
- // paid by the default payment method, or waiting on one (declined, absent, or collected client-side)
148
- await created(ctx, 'payment_intent', { id: piId, amount: invoiceTotal, currency, customer, invoice: invoiceId, ...(payWith ? { payment_method: pays ? payWith : null } : {}) }, {
149
- status: pays ? 'succeeded' : 'requires_payment_method', client_secret: mintClientSecret(piId), livemode: false,
150
- capture_method: 'automatic', amount_capturable: 0, amount_received: pays ? invoiceTotal : 0, next_action: null,
151
- latest_charge: chargeId,
152
- last_payment_error: declined ? { type: 'card_error', code: declined.code, decline_code: declined.decline_code ?? null, message: declined.message, payment_method: { id: payWith } } : null,
153
- automatic_payment_methods: null, payment_method_types: ['card', 'link'], payment_method_options: {},
154
- });
155
- }
156
- // the first invoice is finalized: the `once` coupon it took is spent (renewals.ts spendOnceCoupon)
157
- await spendOnceCoupon(ctx, ctx.get('invoice', invoiceId) ?? {});
158
- return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
159
- };
1
+ // Stripe's subscriptions operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
2
+ // handler contract's context, computing with the engine (../engine/).
3
+ import type { HandlerContext } from '@volter/world-core';
4
+ import { type Row } from '../engine/common.ts';
5
+ import { keptAfter } from '../engine/renewals.ts';
6
+ import { asBool, nowUnix, subscriptionCouponParam, subscriptionHasPrice, subscriptionItemEntries } from '../engine/stripe.ts';
7
+ import { pauseCollection, SUB } from '../engine/subscriptions.ts';
8
+ import { resourceView, advanceBilling, applyItemChanges, applyTrialEnd, at, clockNow, collectSubscriptionInvoice, expanded, itemChangesRefused, list, newest, redeemCoupon, replacedDiscounts, subMissing, trialEndBlocked, trialEndRefused, where } from './shared.ts';
9
+ import { deletedDiscount } from '../engine/common.ts';
10
+ import { fail } from './shared.ts';
11
+ // canceling keeps the subscription, now canceled, stamped with when it was canceled and ended
12
+ // (https://docs.stripe.com/api/subscriptions/object#subscription_object-canceled_at, #subscription_object-ended_at)
13
+ export async function DeleteSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
14
+ const id = at(ctx, 'subscription_exposed_id');
15
+ const sub = resourceView(ctx, SUB, id);
16
+ if (!sub) return subMissing(ctx, id);
17
+ const refused = ctx.legal(SUB, 'status', 'DeleteSubscriptionsSubscriptionExposedId', sub.status, undefined, id);
18
+ if (refused) return ctx.refuse(refused);
19
+ const now = nowUnix(ctx.occurredAt);
20
+ // why it was canceled: the request's `cancellation_details` ("Details about why this subscription was cancelled"), and
21
+ // the reason Stripe records for a cancel asked through the API, as the cancel page's example answers it:
22
+ // `"reason": "cancellation_requested"` (docs.stripe.com/api/subscriptions/cancel)
23
+ const given = ctx.params.cancellation_details && typeof ctx.params.cancellation_details === 'object' ? (ctx.params.cancellation_details as Row) : {};
24
+ const cancellation_details = { comment: given.comment ?? null, feedback: given.feedback ?? null, reason: 'cancellation_requested' };
25
+ return ctx.reply(ctx.expand(SUB, await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel')));
26
+ }
160
27
 
161
- const listSubscriptions: Semantics = async (ctx) => {
28
+ export async function GetSubscriptions(ctx: HandlerContext): Promise<Response> {
162
29
  await advanceBilling(ctx);
163
30
  return list(ctx, SUB, where(ctx, newest(ctx, SUB), {
164
31
  customer: (s, v) => s.customer === v,
165
32
  status: (s, v) => (v === 'all' ? true : s.status === v),
166
33
  price: (s, v) => subscriptionHasPrice(s, String(v)),
167
34
  }));
168
- };
169
-
170
- const searchSubscriptions: Semantics = async (ctx) => {
171
- await advanceBilling(ctx);
172
- return search(ctx, SUB);
173
- };
35
+ }
174
36
 
175
- const retrieveSubscription: Semantics = async (ctx) => {
37
+ export async function GetSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
176
38
  const id = at(ctx, 'subscription_exposed_id');
177
39
  await advanceBilling(ctx, (sub) => sub.id === id);
178
- const sub = ctx.get(SUB, id);
40
+ const sub = resourceView(ctx, SUB, id);
179
41
  return sub ? ctx.reply(expanded(ctx, SUB, sub)) : subMissing(ctx, id);
180
- };
181
-
182
- // pause_collection pauses billing (the subscription stays active; the paused event fires) and
183
- // clearing it resumes; cancel_at_period_end schedules or withdraws a cancel; a coupon replaces the
184
- // discount and an empty one clears it
185
- /** pause_collection pauses billing (the paused event fires) and clearing it resumes
186
- * (docs.stripe.com/billing/subscriptions/pause-payment); answers the event, or nothing for a value it does not take. */
187
- function pauseCollection(pc: unknown, fields: Row): string | undefined {
188
- const isClear = pc === '' || pc === null || (typeof pc === 'object' && pc !== null && Object.keys(pc as object).length === 0);
189
- if (isClear) {
190
- fields.pause_collection = null;
191
- return 'customer.subscription.resumed';
192
- }
193
- if (!pc || typeof pc !== 'object') return undefined;
194
- const p = pc as Row;
195
- fields.pause_collection = { behavior: typeof p.behavior === 'string' ? p.behavior : 'void', resumes_at: p.resumes_at !== undefined ? Number(p.resumes_at) : null };
196
- return 'customer.subscription.paused';
197
- }
198
-
199
- /** A coupon that replaces the subscription's discount, or an empty one that clears it (docs.stripe.com/api/subscriptions/update#update_subscription-discounts). */
200
- function replacedDiscounts(ctx: SemanticsContext, couponId: string, sub: Row): Row[] | Response {
201
- if (couponId === '') return [];
202
- const coupon = ctx.get('coupon', couponId);
203
- return coupon ? [buildDiscount(coupon, String(sub.customer ?? ''), Number(ctx.now()), { subscription: String(sub.id) })] : fail(ctx, `No such coupon: '${couponId}'`, 400, 'resource_missing');
204
- }
205
-
206
- /** The longest trial Stripe takes: `trial_end` "Can be at most two years from `billing_cycle_anchor`"
207
- * (docs.stripe.com/api/subscriptions/update#update_subscription-trial_end); "The trial period must be 730 days (2 years)
208
- * or less" (docs.stripe.com/billing/subscriptions/trials/free-trials). */
209
- const MAX_TRIAL = 730 * 86400;
210
-
211
- /** A `trial_end` Stripe refuses: the parameter is `"now" | timestamp` (docs.stripe.com/api/subscriptions/update), and a
212
- * timestamp that is not in the future answers "Invalid timestamp: must be an integer Unix timestamp in the future."
213
- * (Stripe's own message, as its SDKs' users report it: github.com/stripe/stripe-dotnet/issues/805,
214
- * github.com/stripe/stripe-ruby/issues/682). Where the documentation stops and the twin decides: a value that is no
215
- * number at all, and one more than two years on, answer that same message. */
216
- export function trialEndRefused(ctx: SemanticsContext, value: unknown, now: number): Response | undefined {
217
- const t = typeof value === 'number' ? value : typeof value === 'string' && /^\d+$/.test(value.trim()) ? Number(value) : NaN;
218
- return value === undefined || value === 'now' || (Number.isInteger(t) && t > now && t <= now + MAX_TRIAL) ? undefined : invalidTrialEnd(ctx);
219
- }
220
-
221
- function invalidTrialEnd(ctx: SemanticsContext): Response {
222
- return ctx.refuse({ status: 400, param: 'trial_end', message: 'Invalid timestamp: must be an integer Unix timestamp in the future.' });
223
- }
224
-
225
- /** An update's `trial_end` on the subscription, into `fields`.
226
- *
227
- * `now` on a trialing subscription ends its trial at once: "The special value `now` can be provided to end the customer's
228
- * trial immediately" (docs.stripe.com/api/subscriptions/update#update_subscription-trial_end; the trials guide's "To end a
229
- * trial early … setting the `trial_end` value to … **now** to end immediately", docs.stripe.com/billing/subscriptions/
230
- * trials/free-trials). The update resets the billing date and charges at once: "Switching prices does not normally change
231
- * the billing date or generate an immediate charge unless … A trial starts or ends. In these cases, we … immediately
232
- * charge the customer using the new price, and reset the billing date" (docs.stripe.com/api/subscriptions/update), and
233
- * "Stripe immediately attempts payment when a subscription's billing cycle anchor is reset … When billing is performed
234
- * immediately, but the required payment fails, the subscription change request succeeds and the subscription transitions
235
- * to `past_due`" (docs.stripe.com/billing/subscriptions/upgrade-downgrade#immediate-payment). So a new period starts now,
236
- * its invoice is made (`invoice.created`) and charged to the subscription's default payment method, else the
237
- * customer's (paid: `invoice.paid`, the subscription `active`; declined or none: the invoice open, `past_due`), and the
238
- * subscription's own write sends `customer.subscription.updated`. `payment_behavior` (the same page): `error_if_incomplete`
239
- * answers 402 and leaves the subscription as it was, `default_incomplete` leaves the invoice open without an attempt.
240
- *
241
- * A future timestamp moves the trial's end, and the period and billing anchor with it: "The `billing_cycle_anchor` will be
242
- * updated to the `trial_end` value" (the same parameter); on a subscription out of its trial it starts a new one ("You can
243
- * add a new trial on a non-trialing subscription by updating the subscription while specifying trial_end", and for
244
- * classic billing mode "the `trial_start` field remains set to the start of the first trial", docs.stripe.com/billing/
245
- * subscriptions/trials/free-trials).
246
- *
247
- * Where the documentation stops and the twin decides: the invoice's billing_reason is `subscription_update` ("A
248
- * subscription was updated", docs.stripe.com/api/invoices/object#invoice_object-billing_reason); `now` on a subscription
249
- * that is not trialing has no trial to end and changes nothing; the credit Stripe prorates for the unused time when a new
250
- * trial starts mid-period is not made (as `proration_behavior=none` would have it); `pending_if_incomplete` answers as
251
- * `allow_incomplete` (no pending update is modelled). Answers a refusal, or the invoice to collect once the subscription is written. */
252
- async function applyTrialEnd(ctx: SemanticsContext, sub: Row, fields: Row, now: number): Promise<{ invoice: string; payer?: string; attempt: boolean } | undefined> {
253
- const value = ctx.params.trial_end;
254
- delete fields.trial_end;
255
- const id = String(sub.id);
256
- if (value !== 'now' || sub.status !== 'trialing') return value === 'now' ? undefined : movedTrial(sub, fields, Number(value), now);
257
- // the period's invoice bills the items as this update leaves them, under its discount
258
- const next: Row = { ...sub, ...fields, items: ctx.get(SUB, id)?.items ?? sub.items };
259
- const { interval, interval_count } = resolveSubscriptionBillingInterval(((next.items as { data?: Row[] } | undefined)?.data) ?? [], finder(ctx));
260
- const period = { start: now, end: addBillingInterval(now, interval, interval_count) };
261
- const invoice = await draftSubscriptionInvoice(ctx, next, { ...period, reason: 'subscription_update' });
262
- Object.assign(fields, { trial_end: now, billing_cycle_anchor: now, current_period_start: period.start, current_period_end: period.end, latest_invoice: invoice });
263
- const payer = trialPayer(ctx, sub);
264
- return { invoice, ...(payer ? { payer } : {}), attempt: ctx.params.payment_behavior !== 'default_incomplete' };
265
- }
266
-
267
- /** A trial moved to end at `end`, or a new one to `end` on a subscription out of its trial (applyTrialEnd). */
268
- function movedTrial(sub: Row, fields: Row, end: number, now: number): undefined {
269
- Object.assign(fields, {
270
- status: 'trialing', trial_end: end, trial_start: sub.trial_start ?? now, billing_cycle_anchor: end,
271
- current_period_end: end, ...(sub.status !== 'trialing' ? { current_period_start: now } : {}),
272
- });
273
- return undefined;
274
- }
275
-
276
- /** What ending the trial charges: the update's own default payment method, else the subscription's, else its customer's. */
277
- function trialPayer(ctx: SemanticsContext, sub: Row): string | undefined {
278
- const given = ctx.params.default_payment_method;
279
- return typeof given === 'string' && given ? given : payerOf(ctx, sub);
280
- }
281
-
282
- /** What an update's `trial_end` refuses before anything is written: a new trial the machine does not allow from the
283
- * subscription's status, or, under `error_if_incomplete`, ending a trial that cannot be paid at once ("If payment fails,
284
- * return an HTTP `402` status code and don't update the subscription", docs.stripe.com/api/subscriptions/update). */
285
- function trialEndBlocked(ctx: SemanticsContext, sub: Row, discounts: Row[] | undefined): Response | undefined {
286
- if (ctx.params.trial_end !== 'now') return newTrialRefused(ctx, sub);
287
- if (sub.status !== 'trialing') return undefined;
288
- const charge = trialEndCharge(ctx, sub, discounts);
289
- if (ctx.params.payment_behavior === 'error_if_incomplete' && charge.due > 0 && !charge.pays) return firstPaymentRefused(ctx, charge.declined);
290
- // the move ending the trial makes (paid or nothing due: active; else past_due) is the machine's, asked before
291
- // anything is written
292
- const refused = ctx.legal(SUB, 'status', ctx.call.operation.id, 'trialing', charge.due <= 0 || charge.pays ? 'active' : 'past_due', String(sub.id));
293
- return refused ? ctx.refuse(refused) : undefined;
294
42
  }
295
43
 
296
- /** A price named by id, or given as an object. */
297
- function priceRow(ctx: SemanticsContext, price: unknown): Row | undefined {
298
- return typeof price === 'string' ? ctx.get('price', price) : price && typeof price === 'object' ? (price as Row) : undefined;
299
- }
300
-
301
- /** One period of the subscription's items as an update's `items` would leave them, before any discount: an entry naming
302
- * an item changes its price and quantity (a new price resets the quantity to 1 unless one is given) or deletes it, and
303
- * one naming none adds an item (applyItemChanges). */
304
- function dueAfter(ctx: SemanticsContext, sub: Row, entries: Row[]): number {
305
- const lines = new Map<string, { price: Row | undefined; quantity: number }>();
306
- for (const it of ((sub.items as { data?: Row[] } | undefined)?.data) ?? []) lines.set(String(it.id), { price: priceRow(ctx, it.price), quantity: itemQuantity(it) });
307
- entries.forEach((e, i) => entryAfter(ctx, lines, e, i));
308
- return [...lines.values()].reduce((sum, l) => sum + priceAmount(l.price, l.quantity), 0);
309
- }
310
-
311
- /** One entry of an update's `items` on the lines it would leave (dueAfter). */
312
- function entryAfter(ctx: SemanticsContext, lines: Map<string, { price: Row | undefined; quantity: number }>, e: Row, i: number): void {
313
- // an inline price bills as the Price inlinePrice makes of it
314
- const d = e.price_data && typeof e.price_data === 'object' ? (e.price_data as Row) : undefined;
315
- const inline: Row | undefined = d ? (d.unit_amount_decimal !== undefined ? { unit_amount_decimal: String(d.unit_amount_decimal) } : { unit_amount: Math.trunc(Number(d.unit_amount) || 0) }) : undefined;
316
- const price = typeof e.price === 'string' && e.price ? priceRow(ctx, e.price) : inline;
317
- const named = typeof e.id === 'string' && e.id;
318
- const key = named ? String(e.id) : `new_${i}`;
319
- // only a named item is deleted; an entry naming none adds one, as applyItemChanges does
320
- if (named && asBool(e.deleted)) { lines.delete(key); return; }
321
- const was = lines.get(key);
322
- const quantity = e.quantity !== undefined ? Math.max(0, Math.trunc(Number(e.quantity) || 0)) : price && was && price.id !== was.price?.id ? 1 : was?.quantity ?? 1;
323
- lines.set(key, { price: price ?? was?.price, quantity });
324
- }
325
-
326
- /** A new trial on a subscription whose status the machine does not let start one. */
327
- function newTrialRefused(ctx: SemanticsContext, sub: Row): Response | undefined {
328
- const refused = sub.status === 'trialing' ? undefined : ctx.legal(SUB, 'status', ctx.call.operation.id, sub.status, 'trialing', String(sub.id));
329
- return refused ? ctx.refuse(refused) : undefined;
330
- }
331
-
332
- /** What ending the trial charges, judged before anything is written: the period of the items as this update leaves them
333
- * (dueAfter) under the discount it leaves, and whether the payer pays it (an attempt made, a payer, no decline). */
334
- function trialEndCharge(ctx: SemanticsContext, sub: Row, discounts: Row[] | undefined): { due: number; pays: boolean; declined?: ReturnType<typeof declineFor> } {
335
- const subtotal = dueAfter(ctx, sub, subscriptionItemEntries(ctx.params.items));
336
- // the discount the subscription carries after this update takes this invoice (renewals.ts draftSubscriptionInvoice)
337
- const discount = (discounts ?? (sub.discounts as Row[] | undefined) ?? [])[0];
338
- const coupon = discount && typeof discount === 'object' ? (discount.coupon as Row | undefined) : undefined;
339
- const due = subtotal - (coupon ? applyCouponDiscount(subtotal, coupon) : 0);
340
- const payer = trialPayer(ctx, sub);
341
- const attempt = ctx.params.payment_behavior !== 'default_incomplete';
342
- const declined = payer && attempt ? declineFor(finder(ctx), { payment_method: payer }) : undefined;
343
- return { due, pays: attempt && !!payer && !declined, ...(declined ? { declined } : {}) };
344
- }
345
-
346
- const update: Semantics = async (ctx) => {
44
+ export async function PostSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
347
45
  const id = at(ctx, 'subscription_exposed_id');
348
46
  // a period that has already ended renews (or a trial that has, ends) before the update applies (renewals.ts)
349
47
  await advanceBilling(ctx, (s) => s.id === id);
350
- const sub = ctx.get(SUB, id);
48
+ const sub = resourceView(ctx, SUB, id);
351
49
  if (!sub) return subMissing(ctx, id);
352
50
  const params = ctx.params;
353
51
  const now = clockNow(ctx, sub.customer);
@@ -381,7 +79,7 @@ const update: Semantics = async (ctx) => {
381
79
  // scheduled cancel follows a period this update moves
382
80
  if ('cancel_at_period_end' in params) {
383
81
  fields.cancel_at_period_end = asBool(params.cancel_at_period_end);
384
- Object.assign(fields, fields.cancel_at_period_end ? { cancel_at: fields.current_period_end ?? sub.current_period_end ?? null, canceled_at: Number(ctx.now()) } : { cancel_at: null, canceled_at: null });
82
+ Object.assign(fields, fields.cancel_at_period_end ? { cancel_at: fields.current_period_end ?? sub.current_period_end ?? null, canceled_at: Number(nowUnix(ctx.occurredAt)) } : { cancel_at: null, canceled_at: null });
385
83
  } else if (sub.cancel_at_period_end === true && fields.current_period_end !== undefined) fields.cancel_at = fields.current_period_end;
386
84
  if (trial?.invoice) {
387
85
  // the trial's end is charged at once: paid (or nothing due), the subscription is active; not, past_due
@@ -391,220 +89,19 @@ const update: Semantics = async (ctx) => {
391
89
  fields.status = pays ? 'active' : 'past_due';
392
90
  if (fields.discounts !== undefined) fields.discounts = keptAfter(fields.discounts as Row[], ctx.get('invoice', trial.invoice) ?? {});
393
91
  }
394
- return ctx.reply(await ctx.write(SUB, id, fields, op));
395
- };
396
-
397
- /** A new item named with neither `price` nor `price_data`: "One of `price` or `price_data` is required"
398
- * (docs.stripe.com/api/subscriptions/update). */
399
- function noItemPrice(ctx: SemanticsContext): Response {
400
- return fail(ctx, 'Missing required param: items[price].', 400, 'parameter_missing');
401
- }
402
-
403
- /** A subscription asked to fail when its first invoice cannot be paid at once (payment_behavior=error_if_incomplete):
404
- * the card's decline, or no payment method to charge (docs.stripe.com/api/subscriptions/create#create_subscription-payment_behavior). */
405
- function firstPaymentRefused(ctx: SemanticsContext, declined: Parameters<typeof cardError>[0] | null | undefined): Response {
406
- return declined ? send(ctx, cardError(declined)) : fail(ctx, 'This customer has no attached payment source or default payment method.', 400, 'resource_missing');
407
- }
408
-
409
- /** An update's `items`: "A list of up to 20 subscription items, each with an attached price"
410
- * (docs.stripe.com/api/subscriptions/update): an entry naming an item's `id` changes that item (its price, quantity,
411
- * metadata) or, with `deleted` ("A flag that, if set to `true`, will delete the specified item"), removes it; "If you
412
- * omit `id`, the API adds a new subscription item rather than updating the existing one". Each takes `price` or
413
- * `price_data`, "Data used to generate a new Price object inline. One of `price` or `price_data` is required", and
414
- * "When changing a subscription item's price, `quantity` is set to 1 unless a `quantity` parameter is provided". An
415
- * inline price is made as the manage-prices guide says: "By default, prices created with `price_data` are effectively
416
- * archived (they're marked as `active=false`)" (docs.stripe.com/products-prices/manage-prices).
417
- * Where the documentation stops and the twin decides: the update is all or nothing, every entry checked before any is
418
- * written (Stripe answers one error for the request); the refusals are worded by the twin. */
419
- async function applyItemChanges(ctx: SemanticsContext, subId: string, entries: Row[]): Promise<Response | undefined> {
420
- // every entry is checked first: a refusal leaves the subscription as it was
421
- const refused = itemChangesRefused(ctx, subId, entries);
422
- if (refused) return refused;
423
- return writeItemChanges(ctx, subId, entries);
424
- }
425
-
426
- /** The refusal of an update's `items`, checked entry by entry before any is written (applyItemChanges). */
427
- function itemChangesRefused(ctx: SemanticsContext, subId: string, entries: Row[]): Response | undefined {
428
- for (const entry of entries) {
429
- const priceRef = typeof entry.price === 'string' && entry.price ? entry.price : undefined;
430
- if (priceRef && !ctx.get('price', priceRef)) return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
431
- const inline = !priceRef && entry.price_data && typeof entry.price_data === 'object' ? (entry.price_data as Row) : undefined;
432
- if (inline) {
433
- const refused = refuseInlinePrice(ctx, inline);
434
- if (refused) return refused;
435
- }
436
- if (typeof entry.id === 'string' && entry.id) {
437
- const si = ctx.get('subscription_item', entry.id);
438
- if (!si || si.subscription !== subId) return fail(ctx, `No such subscription_item: '${entry.id}'`, 400, 'resource_missing');
439
- } else if (!priceRef && !inline) return noItemPrice(ctx);
440
- }
441
- return undefined;
442
- }
443
-
444
- /** An update's checked `items`, written. */
445
- async function writeItemChanges(ctx: SemanticsContext, subId: string, entries: Row[]): Promise<undefined> {
446
- for (const entry of entries) {
447
- const inline = typeof entry.price === 'string' && entry.price ? undefined : entry.price_data && typeof entry.price_data === 'object' ? (entry.price_data as Row) : undefined;
448
- const priceRef = typeof entry.price === 'string' && entry.price ? entry.price : inline ? await inlinePrice(ctx, inline) : undefined;
449
- // only a price that changes resets the quantity; one naming the item's own price leaves it
450
- const was = typeof entry.id === 'string' && entry.id ? ctx.get('subscription_item', entry.id)?.price : undefined;
451
- const wasId = typeof was === 'string' ? was : was && typeof was === 'object' ? String((was as Row).id ?? '') : undefined;
452
- const quantity = entry.quantity !== undefined ? { quantity: Math.max(0, Math.trunc(Number(entry.quantity) || 0)) } : priceRef && priceRef !== wasId ? { quantity: 1 } : {};
453
- const metadata = entry.metadata && typeof entry.metadata === 'object' ? { metadata: entry.metadata } : {};
454
- if (typeof entry.id === 'string' && entry.id) {
455
- if (asBool(entry.deleted)) { await ctx.write('subscription_item', entry.id, { deleted: true }, 'subscription_item.delete'); continue; }
456
- await ctx.write('subscription_item', entry.id, { ...(priceRef ? subscriptionItemPrice(priceRef, finder(ctx)) : {}), ...quantity, ...metadata }, 'subscription_item.update');
457
- } else {
458
- await created(ctx, 'subscription_item', { subscription: subId, ...subscriptionItemPrice(priceRef, finder(ctx)), quantity: 1, ...quantity, ...metadata }, { livemode: false, metadata: {}, discounts: [], tax_rates: [], billing_thresholds: null });
459
- }
460
- }
461
- await syncItems(ctx, subId);
462
- return undefined;
92
+ return ctx.reply(ctx.expand(SUB, await ctx.write(SUB, id, fields, op)));
463
93
  }
464
94
 
465
- /** A subscription item's `price_data` Stripe would refuse: its required `currency`, `product` (an existing one) and
466
- * `recurring[interval]`, and "Only one of `unit_amount` and `unit_amount_decimal` can be set". */
467
- function refuseInlinePrice(ctx: SemanticsContext, d: Row): Response | undefined {
468
- for (const name of ['currency', 'product']) if (typeof d[name] !== 'string' || !d[name]) return fail(ctx, `Missing required param: items[price_data][${name}].`, 400, 'parameter_missing');
469
- if (!ctx.get('product', String(d.product))) return fail(ctx, `No such product: '${String(d.product)}'`, 400, 'resource_missing');
470
- const interval = (d.recurring as Row | undefined)?.interval;
471
- if (!['day', 'week', 'month', 'year'].includes(String(interval))) return fail(ctx, 'Missing required param: items[price_data][recurring][interval].', 400, 'parameter_missing');
472
- if (d.unit_amount !== undefined && d.unit_amount_decimal !== undefined) return fail(ctx, 'Only one of `unit_amount` and `unit_amount_decimal` can be set.', 400, 'parameter_invalid');
473
- return undefined;
474
- }
475
95
 
476
- /** The inline Price a `price_data` generates: recurring, archived (active=false). Answers its id. */
477
- async function inlinePrice(ctx: SemanticsContext, d: Row): Promise<string> {
478
- const r = d.recurring as Row;
479
- const decimal = d.unit_amount_decimal !== undefined ? String(d.unit_amount_decimal) : String(Math.trunc(Number(d.unit_amount) || 0));
480
- const price = await created(ctx, 'price', {
481
- product: d.product, currency: String(d.currency).toLowerCase(),
482
- // a decimal amount that is not a whole number of the smallest unit has no integer unit_amount (as semantics/plans.ts)
483
- unit_amount: d.unit_amount_decimal !== undefined ? (Number.isInteger(Number(d.unit_amount_decimal)) ? Number(d.unit_amount_decimal) : null) : Math.trunc(Number(d.unit_amount) || 0), unit_amount_decimal: decimal,
484
- recurring: { interval: r.interval, interval_count: Math.max(1, Math.trunc(Number(r.interval_count ?? 1)) || 1), usage_type: 'licensed', trial_period_days: null, meter: null },
485
- tax_behavior: typeof d.tax_behavior === 'string' ? d.tax_behavior : 'unspecified',
486
- }, { active: false, livemode: false, billing_scheme: 'per_unit', type: 'recurring', metadata: {}, lookup_key: null, nickname: null });
487
- return String(price.id);
488
- }
489
96
 
490
- const deleteDiscount: Semantics = async (ctx) => {
97
+ export async function DeleteSubscriptionsSubscriptionExposedIdDiscount(ctx: HandlerContext): Promise<Response> {
491
98
  const id = at(ctx, 'subscription_exposed_id');
492
- const sub = ctx.get(SUB, id);
99
+ const sub = resourceView(ctx, SUB, id);
493
100
  if (!sub) return subMissing(ctx, id);
101
+ // source: spec:DeleteSubscriptionsSubscriptionExposedIdDiscount "Removes the currently applied discount on a subscription."
494
102
  const current = ((sub.discounts as unknown[] | undefined) ?? [])[0] ?? sub.discount;
103
+ // Where the documentation stops and the twin decides: a subscription with no discount answers its absence as a missing resource.
495
104
  if (!current) return fail(ctx, `No active discount for subscription: '${id}'`, 404, 'resource_missing');
496
105
  await ctx.write(SUB, id, { discounts: [] }, 'subscription.update');
497
- return ctx.reply(deletedDiscount(typeof current === 'string' ? { id: current } : (current as Row), ctx.now()));
498
- };
499
-
500
- // canceling keeps the subscription, now canceled, stamped with when it was canceled and ended
501
- // (https://docs.stripe.com/api/subscriptions/object#subscription_object-canceled_at, #subscription_object-ended_at)
502
- const cancel: Semantics = async (ctx) => {
503
- const id = at(ctx, 'subscription_exposed_id');
504
- const sub = ctx.get(SUB, id);
505
- if (!sub) return subMissing(ctx, id);
506
- const refused = ctx.legal(SUB, 'status', 'DeleteSubscriptionsSubscriptionExposedId', sub.status, undefined, id);
507
- if (refused) return ctx.refuse(refused);
508
- const now = ctx.now();
509
- // why it was canceled: the request's `cancellation_details` ("Details about why this subscription was cancelled"), and
510
- // the reason Stripe records for a cancel asked through the API, as the cancel page's example answers it:
511
- // `"reason": "cancellation_requested"` (docs.stripe.com/api/subscriptions/cancel)
512
- const given = ctx.params.cancellation_details && typeof ctx.params.cancellation_details === 'object' ? (ctx.params.cancellation_details as Row) : {};
513
- const cancellation_details = { comment: given.comment ?? null, feedback: given.feedback ?? null, reason: 'cancellation_requested' };
514
- return ctx.reply(await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel'));
515
- };
516
-
517
- // ── items: each write keeps the parent subscription's items.data in step, rebuilt from the live items ──
518
-
519
- /** A new subscription's items, each its own subscription_item as Stripe keeps it (/v1/subscription_items/{id}). */
520
- export async function storeItems(ctx: SemanticsContext, subId: string, items: Row[]): Promise<void> {
521
- for (const item of items) {
522
- if (typeof item.id !== 'string' || ctx.get('subscription_item', item.id)) continue;
523
- await ctx.write('subscription_item', item.id, { ...item, subscription: subId }, 'subscription_item.create');
524
- }
106
+ return ctx.reply(deletedDiscount(typeof current === 'string' ? { id: current } : { ...(current as Row), subscription: id }, nowUnix(ctx.occurredAt)));
525
107
  }
526
-
527
- async function syncItems(ctx: SemanticsContext, subId: string): Promise<void> {
528
- if (!subId) return;
529
- const items = ctx.rows('subscription_item').filter((r) => r.subscription === subId);
530
- await ctx.write(SUB, subId, { items: { object: 'list', data: items, has_more: false, total_count: items.length, url: `/v1/subscription_items?subscription=${subId}` } }, 'subscription.items_synced');
531
- }
532
-
533
- /** A subscription item with its billing period: its subscription's (basil moved the period onto the items). */
534
- const withPeriod = (ctx: SemanticsContext, si: Row): Row => {
535
- const sub = ctx.get(SUB, String(si.subscription));
536
- return { ...si, current_period_start: si.current_period_start ?? sub?.current_period_start ?? null, current_period_end: si.current_period_end ?? sub?.current_period_end ?? null };
537
- };
538
-
539
- const createItem: Semantics = async (ctx) => {
540
- const params = ctx.params;
541
- const subId = typeof params.subscription === 'string' ? params.subscription : '';
542
- if (!subId) return fail(ctx, 'Missing required param: subscription.', 400, 'parameter_missing');
543
- if (!ctx.get(SUB, subId)) return subMissing(ctx, subId);
544
- const priceRef = typeof params.price === 'string' ? params.price : '';
545
- if (!priceRef && !(params.price_data && typeof params.price_data === 'object')) return fail(ctx, 'Missing required param: price.', 400, 'parameter_missing');
546
- if (priceRef && !ctx.get('price', priceRef)) return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
547
- const quantity = params.quantity !== undefined ? Math.max(0, Math.trunc(Number(params.quantity) || 0)) : 1;
548
- // "metadata: Set of key-value pairs that you can attach to an object" (docs.stripe.com/api/subscription_items/create)
549
- const metadata = params.metadata && typeof params.metadata === 'object' ? { metadata: params.metadata } : {};
550
- const body = await created(ctx, 'subscription_item', { subscription: subId, ...(priceRef ? subscriptionItemPrice(priceRef, finder(ctx)) : { price: null, plan: null }), quantity, ...metadata }, { livemode: false, metadata: {} });
551
- await syncItems(ctx, subId);
552
- return ctx.reply(withPeriod(ctx, body));
553
- };
554
-
555
- const listItems: Semantics = async (ctx) => {
556
- const subId = typeof ctx.params.subscription === 'string' ? ctx.params.subscription : '';
557
- if (!subId) return fail(ctx, 'Missing required param: subscription.', 400, 'parameter_missing');
558
- if (!ctx.get(SUB, subId)) return subMissing(ctx, subId);
559
- return list(ctx, 'subscription_item', newest(ctx, 'subscription_item').filter((si) => si.subscription === subId).map((si) => withPeriod(ctx, si)));
560
- };
561
-
562
- const ITEM_ATTRIBUTES = new Set(['billing_thresholds', 'discounts', 'metadata', 'price', 'quantity', 'tax_rates']);
563
-
564
- const itemMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such subscription_item: '${id}'`, 404, 'resource_missing');
565
-
566
- const updateItem: Semantics = async (ctx) => {
567
- const id = at(ctx, 'item');
568
- const si = ctx.get('subscription_item', id);
569
- if (!si) return itemMissing(ctx, id);
570
- // only the item's own attributes are kept; the update's instructions (off_session, payment_behavior, price_data,
571
- // proration_behavior, proration_date) are not attributes of the Subscription Item object
572
- // (docs.stripe.com/api/subscription_items/object lists none of them)
573
- const fields: Row = Object.fromEntries(Object.entries(ctx.params).filter(([k]) => ITEM_ATTRIBUTES.has(k)));
574
- if (ctx.params.quantity !== undefined) fields.quantity = Math.max(0, Math.trunc(Number(ctx.params.quantity) || 0));
575
- if (typeof ctx.params.price === 'string' && ctx.params.price && !ctx.get('price', ctx.params.price)) return fail(ctx, `No such price: '${ctx.params.price}'`, 400, 'resource_missing');
576
- // the item carries its Price and plan, as the subscription's own items do (subscriptionItemPrice)
577
- if (typeof ctx.params.price === 'string' && ctx.params.price) Object.assign(fields, subscriptionItemPrice(ctx.params.price, finder(ctx)));
578
- const out = await ctx.write('subscription_item', id, fields, 'subscription_item.update');
579
- await syncItems(ctx, String(si.subscription));
580
- return ctx.reply(withPeriod(ctx, out));
581
- };
582
-
583
- const retrieveItem: Semantics = async (ctx) => {
584
- const si = ctx.get('subscription_item', at(ctx, 'item'));
585
- return si ? ctx.reply(withPeriod(ctx, si)) : itemMissing(ctx, at(ctx, 'item'));
586
- };
587
-
588
- const deleteItem: Semantics = async (ctx) => {
589
- const id = at(ctx, 'item');
590
- const si = ctx.get('subscription_item', id);
591
- if (!si) return itemMissing(ctx, id);
592
- await ctx.write('subscription_item', id, { deleted: true }, 'subscription_item.delete');
593
- await syncItems(ctx, String(si.subscription));
594
- return ctx.reply({ id, object: 'subscription_item', deleted: true });
595
- };
596
-
597
- export const subscriptions: Record<string, Semantics> = {
598
- PostSubscriptions: create,
599
- GetSubscriptions: listSubscriptions,
600
- GetSubscriptionsSearch: searchSubscriptions,
601
- GetSubscriptionsSubscriptionExposedId: retrieveSubscription,
602
- PostSubscriptionsSubscriptionExposedId: update,
603
- DeleteSubscriptionsSubscriptionExposedIdDiscount: deleteDiscount,
604
- DeleteSubscriptionsSubscriptionExposedId: cancel,
605
- PostSubscriptionItems: createItem,
606
- GetSubscriptionItems: listItems,
607
- PostSubscriptionItemsItem: updateItem,
608
- GetSubscriptionItemsItem: retrieveItem,
609
- DeleteSubscriptionItemsItem: deleteItem,
610
- };