@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,297 +1,28 @@
1
- import { addBillingInterval, applyCouponDiscount, asBool, cardError, declineFor, mintClientSecret, paginate, priceAmount, subscriptionItemPairs } from "../stripe-twin.js";
2
- import { mintCharge } from "./payment-intents.js";
3
- import { spendOnceCoupon } from "./renewals.js";
4
- import { at, created, expanded, fail, finder, list, newest, path, search, send } from "./shared.js";
5
- const INV = 'invoice';
6
- const invoiceMissing = (ctx, id) => fail(ctx, `No such invoice: '${id}'`, 404, 'resource_missing');
7
- const linesOf = (inv) => inv.lines?.data ?? [];
8
- const sumLines = (lines) => lines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
9
- /** Write a draft's line list and the totals derived from it. */
10
- async function writeLines(ctx, invoiceId, next) {
11
- const subtotal = sumLines(next);
12
- const lines = { object: 'list', data: next, has_more: false, total_count: next.length, url: `/v1/invoices/${invoiceId}/lines` };
13
- return ctx.write(INV, invoiceId, { lines, subtotal, total: subtotal, amount_due: subtotal, amount_remaining: subtotal }, 'invoice.update');
14
- }
15
- /** Paying an invoice with nothing to pay it: "If not set, defaults to the subscription's default payment method, if any,
16
- * or to the default payment method in the customer's invoice settings" (docs.stripe.com/api/invoices/object,
17
- * default_payment_method), and none of them is set. */
18
- function nothingToPayWith(ctx) {
19
- return fail(ctx, 'This customer has no attached payment source or default payment method. Please consider adding a default payment method.', 400, 'missing');
20
- }
21
- /** A subscription waiting on the invoice paid through the invoice's own pay (incomplete on its first, past_due on a
22
- * failed renewal) is active again. */
23
- async function activatedByPayment(ctx, subId) {
24
- await ctx.write('subscription', subId, { status: 'active' }, 'subscription.update');
25
- }
26
- /** A draft's line no invoice item made (a subscription's) stays as it is when an item changes. */
27
- function unlinkedLine(line) {
28
- return line;
29
- }
30
- /** A preview's proration for a change at proration_date. Where the documentation stops and the twin decides: half the
31
- * period's charge credited as unused time and half charged as remaining time. */
32
- function prorationLines(out, subtotal, currency, now, period) {
33
- const half = Math.round(subtotal / 2);
34
- out.unshift({ id: 'il_twin_proration_unused', object: 'line_item', type: 'invoiceitem', amount: -half, currency, proration: true, period: { start: now, end: period }, description: 'Unused time (proration credit)' });
35
- out.push({ id: 'il_twin_proration_remaining', object: 'line_item', type: 'invoiceitem', amount: half, currency, proration: true, period: { start: now, end: period }, description: 'Remaining time (proration charge)' });
36
- }
37
- /** Re-derive a draft's lines from the items still on it: an item that changed or was deleted moves the invoice with it. */
38
- async function resyncDraftLines(ctx, invoiceId) {
39
- const inv = ctx.get(INV, invoiceId);
40
- if (!inv || inv.status !== 'draft')
41
- return;
42
- const next = [];
43
- for (const line of linesOf(inv)) {
44
- const itemId = typeof line.invoice_item === 'string' ? line.invoice_item : '';
45
- if (!itemId)
46
- next.push(unlinkedLine(line));
47
- const item = itemId ? ctx.get('invoiceitem', itemId) : undefined;
48
- if (item)
49
- next.push({ ...line, amount: Number(item.amount) || 0, description: item.description ?? null });
50
- }
51
- await writeLines(ctx, invoiceId, next);
52
- }
53
- // ── invoice items: a price-based item takes its amount from the price; one naming a draft invoice
54
- // lands on its lines at once ──
55
- /** An item priced by a Price: its unit amount times the quantity, in its currency (docs.stripe.com/api/invoiceitems/create#create_invoiceitem-pricing). */
56
- function pricedItem(ctx, priceRef, qty) {
57
- const price = ctx.get('price', priceRef);
58
- if (!price)
59
- return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
60
- return { amount: priceAmount(price, qty), currency: typeof price.currency === 'string' ? price.currency : undefined };
61
- }
62
- const createItem = async (ctx) => {
63
- const params = ctx.params;
64
- const now = Number(ctx.now());
65
- const qty = Number(params.quantity) || 1;
66
- // the served version names the price in `pricing[price]` ("The pricing information for the invoice item", the served
67
- // spec); an older version sends `price`. It is kept as the item's price, which the answer renders as `pricing`
68
- // (stripe-version.ts)
69
- const { pricing: pricingParam, ...stored } = params;
70
- const given = pricingParam?.price;
71
- const priceRef = typeof given === 'string' && given ? given : typeof params.price === 'string' ? params.price : '';
72
- const priced = priceRef ? pricedItem(ctx, priceRef, qty) : undefined;
73
- if (priced instanceof Response)
74
- return priced;
75
- const resolvedAmount = priced?.amount;
76
- let resolvedCurrency = priced?.currency;
77
- // only a draft invoice takes new items
78
- const invoiceRef = typeof params.invoice === 'string' ? params.invoice : '';
79
- let target;
80
- if (invoiceRef) {
81
- target = ctx.get(INV, invoiceRef);
82
- if (!target)
83
- return invoiceMissing(ctx, invoiceRef);
84
- if (target.status !== 'draft')
85
- return fail(ctx, `This invoice cannot have invoice items added because it has status ${String(target.status)}.`, 400, 'invoice_not_editable');
86
- if (resolvedCurrency === undefined && typeof target.currency === 'string')
87
- resolvedCurrency = target.currency;
88
- }
89
- // an invoice item is dated by `date`, not `created`; its currency is the price's, else the invoice's
90
- const item = await created(ctx, 'invoiceitem', { ...stored, ...(priceRef ? { price: priceRef } : {}) }, {
91
- livemode: false, discountable: true, proration: false, quantity: qty, quantity_decimal: String(qty),
92
- period: { start: now, end: now }, currency: resolvedCurrency ?? 'usd',
93
- ...(resolvedAmount !== undefined ? { amount: resolvedAmount } : {}),
94
- }, { timeField: 'date' });
95
- if (target) {
96
- await writeLines(ctx, invoiceRef, [...linesOf(target), {
97
- id: `il_${String(item.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(item.amount) || 0, currency: String(target.currency ?? item.currency ?? 'usd'),
98
- quantity: qty, proration: false, invoice_item: item.id, price: item.price ?? null,
99
- period: item.period ?? { start: now, end: now }, description: item.description ?? null,
100
- }]);
101
- }
102
- return ctx.reply(item);
103
- };
104
- const itemMissing = (ctx, id) => fail(ctx, `No such invoiceitem: '${id}'`, 404, 'resource_missing');
105
- const retrieveItem = async (ctx) => {
106
- const it = ctx.get('invoiceitem', at(ctx, 'invoiceitem'));
107
- return it ? ctx.reply(it) : itemMissing(ctx, at(ctx, 'invoiceitem'));
108
- };
109
- const updateItem = async (ctx) => {
110
- const id = at(ctx, 'invoiceitem');
111
- const it = ctx.get('invoiceitem', id);
112
- if (!it)
113
- return itemMissing(ctx, id);
114
- const updated = await ctx.write('invoiceitem', id, ctx.params, 'invoiceitem.update');
115
- if (typeof it.invoice === 'string')
116
- await resyncDraftLines(ctx, it.invoice);
117
- return ctx.reply(updated);
118
- };
119
- // an item is deleted only while its invoice is still editable
120
- const deleteItem = async (ctx) => {
121
- const id = at(ctx, 'invoiceitem');
122
- const it = ctx.get('invoiceitem', id);
123
- if (!it)
124
- return itemMissing(ctx, id);
125
- const onInvoice = typeof it.invoice === 'string' ? ctx.get(INV, it.invoice) : undefined;
126
- if (onInvoice && onInvoice.status !== 'draft')
127
- return fail(ctx, 'You cannot delete an invoice item that is attached to an invoice that is no longer editable.', 400, 'invoice_not_editable');
128
- await ctx.write('invoiceitem', id, { deleted: true }, 'invoiceitem.delete');
129
- if (typeof it.invoice === 'string')
130
- await resyncDraftLines(ctx, it.invoice);
131
- return ctx.reply({ id, object: 'invoiceitem', deleted: true });
132
- };
133
- // ── invoices ──
134
- /** A draft invoice's fields before anything is on it: what a new invoice and a preview start from. */
135
- const draft = (now) => ({
136
- status: 'draft', livemode: false, currency: 'usd', collection_method: 'charge_automatically',
137
- auto_advance: false, attempt_count: 0, attempted: false,
138
- amount_due: 0, amount_paid: 0, amount_remaining: 0, amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
139
- subtotal: 0, total: 0, starting_balance: 0, post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
140
- period_start: now, period_end: now, default_tax_rates: [], discounts: [],
141
- lines: { object: 'list', data: [], has_more: false, total_count: 0 },
142
- automatic_tax: { enabled: false, liability: null, status: null },
143
- status_transitions: { finalized_at: null, marked_uncollectible_at: null, paid_at: null, voided_at: null },
144
- issuer: { type: 'self' },
145
- payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
146
- // a draft has no PaymentIntent: finalize mints it
147
- payment_intent: null,
148
- });
149
- // a new invoice pulls in the customer's pending items only when asked: pending_invoice_items_behavior "Defaults to
150
- // `exclude` if the parameter is omitted" (docs.stripe.com/api/invoices/create)
151
- const create = async (ctx) => {
152
- const params = ctx.params;
153
- const now = Number(ctx.now());
154
- // the lines list names where it can be read, "The URL where this list can be accessed" (served spec, invoice.lines.url)
155
- const newId = ctx.mint(INV);
156
- // a send_invoice invoice is due `days_until_due` days "from when the invoice is created" (the same page); the customer's
157
- // email and name are its own "Until the invoice is finalized, this field will equal customer.email" (served spec,
158
- // invoice.customer_email; .customer_name likewise)
159
- const days = Number(params.days_until_due);
160
- const due = params.collection_method === 'send_invoice' && params.due_date === undefined && Number.isFinite(days) && days > 0 ? { due_date: now + days * 86_400 } : {};
161
- const holder = typeof params.customer === 'string' ? ctx.get('customer', params.customer) : undefined;
162
- const snapshot = holder ? { customer_email: holder.email ?? null, customer_name: holder.name ?? null } : {};
163
- const inv = await created(ctx, INV, { ...params, id: newId }, { ...draft(now), ...due, ...snapshot, lines: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/invoices/${newId}/lines` } });
164
- if (params.pending_invoice_items_behavior !== 'include')
165
- return ctx.reply(inv);
166
- const invoiceId = String(inv.id);
167
- const customerId = typeof inv.customer === 'string' ? inv.customer : '';
168
- const pending = customerId ? newest(ctx, 'invoiceitem', 'date').filter((it) => it.customer === customerId && !it.invoice).reverse() : [];
169
- for (const item of pending)
170
- await pullPendingItem(ctx, invoiceId, inv, item, now);
171
- return ctx.reply(pending.length > 0 ? (ctx.get(INV, invoiceId) ?? inv) : inv);
172
- };
173
- /** A draft invoice another object makes (an accepted quote): its customer's, with an invoice item per line the
174
- * object gives, and the object named as its parent. Answers the invoice's id. */
175
- export async function draftInvoiceFor(ctx, customer, currency, parent, lines) {
176
- const now = Number(ctx.now());
177
- const id = ctx.mint(INV);
178
- const holder = ctx.get('customer', customer);
179
- const inv = await created(ctx, INV, { id, customer, currency, parent }, { ...draft(now), currency, customer_email: holder?.email ?? null, customer_name: holder?.name ?? null, lines: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/invoices/${id}/lines` } });
180
- for (const l of lines) {
181
- const price = typeof l.price === 'string' ? l.price : l.price && typeof l.price === 'object' ? String(l.price.id) : null;
182
- const item = await created(ctx, 'invoiceitem', { customer, currency, amount: l.amount, quantity: l.quantity, ...(price ? { price } : {}), description: l.description ?? null }, { date: now, livemode: false, metadata: {}, discountable: true, proration: false, period: { start: now, end: now } });
183
- await pullPendingItem(ctx, id, inv, item, now);
184
- }
185
- return id;
186
- }
187
- /** A customer's pending invoice item, made before the invoice, pulled onto it (docs.stripe.com/api/invoices/create#create_invoice-pending_invoice_items_behavior). */
188
- async function pullPendingItem(ctx, invoiceId, inv, item, now) {
189
- const current = ctx.get(INV, invoiceId) ?? inv;
190
- await writeLines(ctx, invoiceId, [...linesOf(current), {
191
- id: `il_${String(item.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(item.amount) || 0, currency: String(current.currency ?? item.currency ?? 'usd'),
192
- quantity: Number(item.quantity) || 1, proration: false, invoice_item: item.id, price: item.price ?? null,
193
- period: item.period ?? { start: now, end: now }, description: item.description ?? null,
194
- discountable: item.discountable ?? true, discounts: [], livemode: false, metadata: item.metadata ?? {}, tax_amounts: [], tax_rates: [],
195
- }]);
196
- await ctx.write('invoiceitem', String(item.id), { invoice: invoiceId }, 'invoiceitem.update');
197
- }
198
- /** The invoice the path names, and the machine's refusal when this operation may not move it. */
199
- function load(ctx, operationId) {
1
+ import { INV, transitions } from "../engine/invoices.js";
2
+ import { asBool, cardError, declineFor, nowUnix } from "../engine/stripe.js";
3
+ import { resourceView, activatedByPayment, at, expanded, finalizeForPayment, finder, invoiceMissing, invoicesLoad, mintCharge, nothingToPayWith, send } from "./shared.js";
4
+ import { draft, prorationLines, sumLines } from "../engine/invoices.js";
5
+ import { addBillingInterval, applyCouponDiscount, priceAmount } from "../engine/stripe.js";
6
+ import { created, fail, newest, pendingPreviewLines, pullPendingItem } from "./shared.js";
7
+ export async function GetInvoicesInvoice(ctx) {
200
8
  const id = at(ctx, 'invoice');
201
9
  const inv = ctx.get(INV, id);
202
- if (!inv)
203
- return { answer: invoiceMissing(ctx, id) };
204
- const refused = ctx.legal(INV, 'status', operationId, inv.status, undefined, id);
205
- return refused ? { answer: ctx.refuse(refused) } : { inv };
206
- }
207
- /** The PaymentIntent a charge_automatically invoice with a balance gets when it is finalized; it
208
- * waits for confirmation when the invoice already names a default payment method. */
209
- async function openPaymentIntent(ctx, piId, inv, invoiceId) {
210
- const defaultPaymentMethod = typeof inv.default_payment_method === 'string' ? inv.default_payment_method : undefined;
211
- await created(ctx, 'payment_intent', {
212
- id: piId, amount: Number(inv.amount_due) || 0, currency: String(inv.currency ?? 'usd'), customer: inv.customer, invoice: invoiceId,
213
- ...(defaultPaymentMethod ? { payment_method: defaultPaymentMethod } : {}),
214
- }, {
215
- status: defaultPaymentMethod ? 'requires_confirmation' : 'requires_payment_method',
216
- client_secret: mintClientSecret(piId), livemode: false,
217
- capture_method: 'automatic', amount_capturable: 0, amount_received: 0, next_action: null,
218
- automatic_payment_methods: null, payment_method_types: ['card', 'link'], payment_method_options: {},
219
- });
220
- }
221
- /** Whether leaving draft mints a PaymentIntent: only a charge_automatically invoice with a balance. */
222
- // "Finalizing an invoice does the following: ... It creates an incomplete payment intent for the invoice"
223
- // (docs.stripe.com/invoicing/integration/workflow-transitions), whichever its collection method
224
- const collectsThroughIntent = (inv) => inv.status === 'draft' && (Number(inv.amount_due) || 0) > 0;
225
- const transitions = (inv) => inv.status_transitions ?? {};
226
- /** What finalizing assigns: the invoice's `number`, the customer's invoice_prefix and a sequence
227
- * (https://docs.stripe.com/api/invoices/object#invoice_object-number), and for send_invoice a
228
- * `due_date` days_until_due after finalization (#invoice_object-due_date). */
229
- /** A customer's invoice_prefix, "The prefix for the customer used to generate unique invoice numbers"
230
- * (docs.stripe.com/api/customers/object): the twin's, from the customer's id. */
231
- export const invoicePrefix = (customerId) => customerId.replace(/[^a-z0-9]/gi, '').slice(-8).toUpperCase() || 'TWIN';
232
- export function finalizedFields(ctx, inv, now) {
233
- const customerId = typeof inv.customer === 'string' ? inv.customer : '';
234
- const customer = customerId ? ctx.get('customer', customerId) : undefined;
235
- const prefix = typeof customer?.invoice_prefix === 'string' && customer.invoice_prefix
236
- ? customer.invoice_prefix
237
- : invoicePrefix(customerId);
238
- const numbered = ctx.rows(INV).filter((i) => i.customer === customerId && typeof i.number === 'string' && i.id !== inv.id).length;
239
- const fields = { number: typeof inv.number === 'string' && inv.number ? inv.number : `${prefix}-${String(numbered + 1).padStart(4, '0')}` };
240
- const days = Number(inv.days_until_due);
241
- if (inv.collection_method === 'send_invoice' && inv.due_date == null && Number.isFinite(days) && days > 0)
242
- fields.due_date = now + days * 86400;
243
- return fields;
10
+ return inv ? ctx.reply(expanded(ctx, INV, inv)) : invoiceMissing(ctx, id);
244
11
  }
245
- const finalize = async (ctx) => {
246
- const loaded = load(ctx, 'PostInvoicesInvoiceFinalize');
247
- if ('answer' in loaded)
248
- return loaded.answer;
249
- const inv = loaded.inv;
250
- const id = String(inv.id);
251
- const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
252
- const body = await ctx.write(INV, id, { ...finalizedFields(ctx, inv, Number(ctx.now())), status: 'open', status_transitions: { ...transitions(inv), finalized_at: ctx.now() }, payment_intent: piId }, 'invoice.finalize');
253
- // the customer's next_invoice_sequence, "The suffix of the customer's next invoice number" (docs.stripe.com/api/customers/object)
254
- const customerId = typeof inv.customer === 'string' ? inv.customer : '';
255
- if (customerId && ctx.get('customer', customerId))
256
- await ctx.write('customer', customerId, { next_invoice_sequence: ctx.rows(INV).filter((i) => i.customer === customerId && typeof i.number === 'string').length + 1 }, 'customer.invoice_sequence');
257
- if (piId)
258
- await openPaymentIntent(ctx, piId, inv, id);
259
- // a subscription's invoice finalized spends the once coupon it took (renewals.ts spendOnceCoupon)
260
- await spendOnceCoupon(ctx, inv);
261
- return ctx.reply(expanded(ctx, INV, body));
262
- };
263
- // sending a draft finalizes it; any other invoice keeps its status and records the send
264
- const sendInvoice = async (ctx) => {
265
- const loaded = load(ctx, 'PostInvoicesInvoiceSend');
266
- if ('answer' in loaded)
267
- return loaded.answer;
268
- const inv = loaded.inv;
269
- const id = String(inv.id);
270
- const becameOpen = inv.status === 'draft';
271
- const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
272
- const now = ctx.now();
273
- const body = await ctx.write(INV, id, {
274
- status: becameOpen ? 'open' : inv.status, _sent: true, _sent_at: now,
275
- status_transitions: { ...transitions(inv), ...(becameOpen ? { finalized_at: now } : {}) },
276
- ...(becameOpen ? { payment_intent: piId } : {}),
277
- }, 'invoice.sent');
278
- if (piId)
279
- await openPaymentIntent(ctx, piId, inv, id);
280
- if (becameOpen)
281
- await spendOnceCoupon(ctx, inv);
282
- return ctx.reply(expanded(ctx, INV, body));
283
- };
284
12
  // paying charges a payment method (the one passed, the invoice's default, or the customer's) unless
285
13
  // the payment is recorded out of band; a declining card answers 402 and leaves the invoice open. A
286
14
  // paid invoice settles the PaymentIntent finalize opened, and names the charge that took the money.
287
- const pay = async (ctx) => {
15
+ export async function PostInvoicesInvoicePay(ctx) {
288
16
  const id = at(ctx, 'invoice');
289
- const inv = ctx.get(INV, id);
290
- if (!inv)
17
+ const found = ctx.get(INV, id);
18
+ if (!found)
291
19
  return invoiceMissing(ctx, id);
292
- const refused = ctx.legal(INV, 'status', 'PostInvoicesInvoicePay', inv.status, undefined, id);
20
+ const refused = ctx.legal(INV, 'status', 'PostInvoicesInvoicePay', found.status, undefined, id);
293
21
  if (refused)
294
22
  return ctx.refuse(refused);
23
+ // paying a draft finalizes it first, as its finalize does (its number, and the PaymentIntent it collects through)
24
+ // source: https://docs.stripe.com/invoicing/integration "To finalize a draft invoice, use the Dashboard, send it to the customer, or pay it."
25
+ const inv = found.status === 'draft' ? await finalizeForPayment(ctx, found, id) : found;
295
26
  const params = ctx.params;
296
27
  const paidOutOfBand = params.paid_out_of_band !== undefined && asBool(params.paid_out_of_band);
297
28
  const payer = ctx.get('customer', String(inv.customer ?? ''));
@@ -300,7 +31,7 @@ const pay = async (ctx) => {
300
31
  return nothingToPayWith(ctx);
301
32
  // what it charges: the method passed, else the invoice's default, its subscription's, then its customer's
302
33
  // (docs.stripe.com/api/invoices/pay#pay_invoice-payment_method)
303
- const subscription = typeof inv.subscription === 'string' ? ctx.get('subscription', inv.subscription) : undefined;
34
+ const subscription = typeof inv.subscription === 'string' ? resourceView(ctx, 'subscription', inv.subscription) : undefined;
304
35
  const decline = declineFor(finder(ctx), params, { payment_method: inv.default_payment_method ?? subscription?.default_payment_method ?? customerDefault });
305
36
  const intent = typeof inv.payment_intent === 'string' ? ctx.get('payment_intent', inv.payment_intent) : undefined;
306
37
  if (decline)
@@ -309,10 +40,10 @@ const pay = async (ctx) => {
309
40
  let body = await ctx.write(INV, id, {
310
41
  // paid outside Stripe, the invoice counts it as such: `amount_paid_off_stripe`, "Amount, in cents (or local
311
42
  // equivalent), that was paid on the invoice outside of Stripe" (served spec, invoice), the only place the served
312
- // version shows it now that basil renders no `paid_out_of_band` (stripe-version.ts)
43
+ // version shows it now that basil renders no `paid_out_of_band` (../engine/version.ts)
313
44
  status: 'paid', paid: true, paid_out_of_band: paidOutOfBand, amount_paid: amountPaid, amount_remaining: 0,
314
45
  ...(paidOutOfBand ? { amount_paid_off_stripe: amountPaid } : {}),
315
- status_transitions: { ...transitions(inv), paid_at: ctx.now() },
46
+ status_transitions: { ...transitions(inv), paid_at: nowUnix(ctx.occurredAt) },
316
47
  }, 'invoice.pay');
317
48
  if (!paidOutOfBand && typeof inv.payment_intent === 'string' && inv.payment_intent) {
318
49
  const pi = ctx.get('payment_intent', inv.payment_intent);
@@ -324,109 +55,58 @@ const pay = async (ctx) => {
324
55
  }
325
56
  // a subscription waiting on this invoice (incomplete on its first, past_due on a failed renewal) is active once it is paid
326
57
  const subId = typeof inv.subscription === 'string' ? inv.subscription : undefined;
327
- const sub = subId ? ctx.get('subscription', subId) : undefined;
58
+ const sub = subId ? resourceView(ctx, 'subscription', subId) : undefined;
328
59
  if (subId && sub && (sub.status === 'incomplete' || sub.status === 'past_due') && !ctx.legal('subscription', 'status', 'PostInvoicesInvoicePay', sub.status, 'active', subId))
329
60
  await activatedByPayment(ctx, subId);
330
- return ctx.reply(expanded(ctx, INV, body));
331
- };
332
- // only a draft is deleted; a finalized invoice is voided instead
333
- const deleteDraft = async (ctx) => {
334
- const id = at(ctx, 'invoice');
335
- const inv = ctx.get(INV, id);
336
- if (!inv)
337
- return invoiceMissing(ctx, id);
338
- if (inv.status !== 'draft')
339
- return fail(ctx, 'You can only delete draft invoices.', 400, 'invoice_not_editable');
340
- await ctx.write(INV, id, { deleted: true }, 'invoice.deleted');
341
- return ctx.reply({ id, object: 'invoice', deleted: true });
342
- };
343
- const markUncollectible = async (ctx) => {
344
- const loaded = load(ctx, 'PostInvoicesInvoiceMarkUncollectible');
345
- if ('answer' in loaded)
346
- return loaded.answer;
347
- const inv = loaded.inv;
348
- const body = await ctx.write(INV, String(inv.id), { status: 'uncollectible', status_transitions: { ...transitions(inv), marked_uncollectible_at: ctx.now() } }, 'invoice.marked_uncollectible');
349
- return ctx.reply(expanded(ctx, INV, body));
350
- };
351
- const voidInvoice = async (ctx) => {
352
- const loaded = load(ctx, 'PostInvoicesInvoiceVoid');
61
+ return ctx.reply(expanded(ctx, INV, ctx.get(INV, id) ?? body));
62
+ }
63
+ // sending a draft finalizes it; any other invoice keeps its status and records the send
64
+ export async function PostInvoicesInvoiceVoid(ctx) {
65
+ const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceVoid');
353
66
  if ('answer' in loaded)
354
67
  return loaded.answer;
355
68
  const inv = loaded.inv;
356
- const body = await ctx.write(INV, String(inv.id), { status: 'void', status_transitions: { ...transitions(inv), voided_at: ctx.now() } }, 'invoice.void');
69
+ const body = await ctx.write(INV, String(inv.id), { status: 'void', status_transitions: { ...transitions(inv), voided_at: nowUnix(ctx.occurredAt) } }, 'invoice.void');
357
70
  return ctx.reply(expanded(ctx, INV, body));
358
- };
71
+ }
359
72
  // the invoice's lines, a page at a time
360
- const lines = async (ctx) => {
361
- const id = at(ctx, 'invoice');
362
- const inv = ctx.get(INV, id);
363
- if (!inv)
364
- return invoiceMissing(ctx, id);
365
- const { page, hasMore } = paginate(linesOf(inv), ctx.params);
366
- return ctx.reply({ object: 'list', url: path(ctx), has_more: hasMore, data: page.map((l) => ({ invoice: id, ...l })) });
367
- };
368
- // add_lines, update_lines and remove_lines edit a draft's lines in bulk and re-derive its totals
369
- function editLines(kind) {
370
- return async (ctx) => {
371
- const id = at(ctx, 'invoice');
372
- const inv = ctx.get(INV, id);
373
- if (!inv)
374
- return invoiceMissing(ctx, id);
375
- if (inv.status !== 'draft')
376
- return fail(ctx, `You can only add/update/remove lines on a draft invoice (this one is ${String(inv.status)}).`, 400, 'invoice_not_editable');
377
- const cur = linesOf(inv);
378
- let next = [...cur];
379
- const raw = ctx.params.lines;
380
- const entries = Array.isArray(raw) ? raw : raw && typeof raw === 'object' ? [raw] : [];
381
- if (entries.length === 0)
382
- return fail(ctx, 'Missing required param: lines.', 400, 'parameter_missing');
383
- const currency = String(inv.currency ?? 'usd');
384
- if (kind === 'add') {
385
- for (let i = 0; i < entries.length; i++) {
386
- const e = entries[i];
387
- const priceRef = typeof e.price === 'string' ? e.price : '';
388
- const price = priceRef ? ctx.get('price', priceRef) : undefined;
389
- if (priceRef && !price)
390
- return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
391
- const quantity = e.quantity !== undefined ? Math.max(1, Math.trunc(Number(e.quantity) || 1)) : 1;
392
- const unit = Math.trunc(Number(e.amount) || 0);
393
- next.push({ id: `il_twin_${id}_${cur.length + i + 1}`, object: 'line_item', amount: price ? priceAmount(price, quantity) : unit, currency, quantity, price: price ?? (priceRef || null), description: typeof e.description === 'string' ? e.description : null, proration: false });
394
- }
395
- }
396
- else if (kind === 'update') {
397
- for (const e of entries) {
398
- const lid = typeof e.id === 'string' ? e.id : '';
399
- next = next.map((l) => (l.id === lid ? { ...l, ...(e.amount !== undefined ? { amount: Math.trunc(Number(e.amount) || 0) } : {}), ...(e.quantity !== undefined ? { quantity: Math.max(1, Math.trunc(Number(e.quantity) || 1)) } : {}), ...(e.description !== undefined ? { description: e.description } : {}) } : l));
400
- }
401
- }
402
- else {
403
- const remove = new Set(entries.map((e) => (typeof e.id === 'string' ? e.id : '')).filter(Boolean));
404
- next = next.filter((l) => !remove.has(String(l.id)));
405
- }
406
- return ctx.reply(await writeLines(ctx, id, next));
407
- };
73
+ // a new invoice pulls in the customer's pending items only when asked: pending_invoice_items_behavior "Defaults to
74
+ // `exclude` if the parameter is omitted" (docs.stripe.com/api/invoices/create)
75
+ export async function PostInvoices(ctx) {
76
+ const params = ctx.params;
77
+ const now = Number(nowUnix(ctx.occurredAt));
78
+ // the lines list names where it can be read, "The URL where this list can be accessed" (served spec, invoice.lines.url)
79
+ const newId = ctx.mint(INV);
80
+ // a send_invoice invoice is due `days_until_due` days "from when the invoice is created" (the same page); the customer's
81
+ // email and name are its own "Until the invoice is finalized, this field will equal customer.email" (served spec,
82
+ // invoice.customer_email; .customer_name likewise)
83
+ const days = Number(params.days_until_due);
84
+ const due = params.collection_method === 'send_invoice' && params.due_date === undefined && Number.isFinite(days) && days > 0 ? { due_date: now + days * 86_400 } : {};
85
+ const holder = typeof params.customer === 'string' ? ctx.get('customer', params.customer) : undefined;
86
+ const snapshot = holder ? { customer_email: holder.email ?? null, customer_name: holder.name ?? null } : {};
87
+ const inv = await created(ctx, INV, { ...params, id: newId }, { ...draft(now), ...due, ...snapshot, lines: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/invoices/${newId}/lines` } });
88
+ if (params.pending_invoice_items_behavior !== 'include')
89
+ return ctx.reply(inv);
90
+ const invoiceId = String(inv.id);
91
+ const customerId = typeof inv.customer === 'string' ? inv.customer : '';
92
+ const pending = customerId ? newest(ctx, 'invoiceitem', 'date').filter((it) => it.customer === customerId && !it.invoice).reverse() : [];
93
+ for (const item of pending)
94
+ await pullPendingItem(ctx, invoiceId, inv, item, now);
95
+ return ctx.reply(pending.length > 0 ? (ctx.get(INV, invoiceId) ?? inv) : inv);
408
96
  }
409
- const searchInvoices = async (ctx) => search(ctx, INV);
410
- const retrieve = async (ctx) => {
411
- const id = at(ctx, 'invoice');
412
- const inv = ctx.get(INV, id);
413
- return inv ? ctx.reply(expanded(ctx, INV, inv)) : invoiceMissing(ctx, id);
414
- };
415
97
  /**
416
98
  * A preview of the next invoice for a customer or subscription (create_preview), computed and never
417
99
  * stored: the subscription's items at their prices, the customer's pending items, the subscription's
418
- * discount, and with `subscription_details.proration_date` a credit for the unused period against a
419
- * charge for the new one (split evenly: there is no clock here to measure the remaining fraction).
100
+ * discount, and an update's unused-time credit and remaining-time charge at the old and new prices.
420
101
  */
421
- const createPreview = async (ctx) => {
102
+ export async function PostInvoicesCreatePreview(ctx) {
422
103
  const params = ctx.params;
423
- const prorationDate = params.subscription_details?.proration_date;
424
- const now = Number(ctx.now());
104
+ const now = Number(nowUnix(ctx.occurredAt));
425
105
  let subId = typeof params.subscription === 'string' ? params.subscription : '';
426
106
  let customer = typeof params.customer === 'string' ? params.customer : '';
427
107
  let sub;
428
108
  if (subId) {
429
- sub = ctx.get('subscription', subId);
109
+ sub = resourceView(ctx, 'subscription', subId);
430
110
  if (!sub)
431
111
  return fail(ctx, `No such subscription: '${subId}'`, 404, 'resource_missing');
432
112
  customer = customer || String(sub.customer ?? '');
@@ -444,17 +124,58 @@ const createPreview = async (ctx) => {
444
124
  // the upcoming invoice bills the subscription's next period, its "subscription renewal charges"
445
125
  // (docs.stripe.com/api/invoices/create_preview): from the current period's end, one billing interval long
446
126
  const renewsAt = sub && Number(sub.current_period_end) > 0 ? Number(sub.current_period_end) : now;
447
- const itemRows = sub ? ctx.rows('subscription_item').filter((r) => r.subscription === subId) : [];
127
+ let itemRows = sub ? ctx.rows('subscription_item').filter((r) => r.subscription === subId) : [];
128
+ const details = params.subscription_details;
129
+ const previousItems = itemRows;
130
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "The default value is"
131
+ const behavior = details?.proration_behavior ?? 'create_prorations';
132
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "The prorated amount is calculated as soon as the API updates the subscription."
133
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "forces the proration to be calculated as though the update was done at the specified time."
134
+ const prorationTime = details?.proration_date === undefined ? now : Number(details.proration_date);
135
+ const periodStart = Number(sub?.current_period_start ?? now);
136
+ const periodEnd = Number(sub?.current_period_end ?? now);
137
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "The time given must be within the current subscription period"
138
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "cannot be set to"
139
+ if (details?.proration_date !== undefined && (behavior === 'none' || !sub || !Array.isArray(details.items) || prorationTime < periodStart || prorationTime > periodEnd))
140
+ return fail(ctx, 'Invalid subscription_details[proration_date] for this subscription preview.', 400, 'parameter_invalid_integer');
141
+ // source: spec:/paths/~1v1~1invoices~1create_preview/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/subscription_details/description "The subscription creation or modification params to apply as a preview."
142
+ // Preview changes are local to this calculation: the subscription and its stored items are never changed.
143
+ if (Array.isArray(details?.items)) {
144
+ itemRows = itemRows.map(item => ({ ...item }));
145
+ for (const change of details.items) {
146
+ const index = change.id ? itemRows.findIndex(item => item.id === change.id) : -1;
147
+ if (change.id && index < 0)
148
+ return fail(ctx, `No such subscription_item: '${change.id}'`, 400, 'resource_missing');
149
+ if (asBool(change.deleted)) {
150
+ if (index >= 0)
151
+ itemRows.splice(index, 1);
152
+ continue;
153
+ }
154
+ const previous = index >= 0 ? itemRows[index] : {};
155
+ const priceId = typeof change.price === 'string' ? change.price : typeof previous.price === 'string' ? previous.price : previous.price?.id;
156
+ const price = typeof priceId === 'string' ? ctx.get('price', priceId) : undefined;
157
+ if (!price)
158
+ return fail(ctx, `No such price: '${priceId ?? ''}'`, 400, 'resource_missing');
159
+ const quantity = change.quantity === undefined ? Number(previous.quantity ?? 1) : Number(change.quantity);
160
+ const previewItem = { ...previous, price, quantity };
161
+ if (index >= 0)
162
+ itemRows[index] = previewItem;
163
+ else
164
+ itemRows.push(previewItem);
165
+ }
166
+ }
448
167
  const out = [];
449
168
  let subtotal = 0;
450
169
  let period = now + 30 * 24 * 3600;
451
170
  if (sub) {
452
- for (const { price: priceId, quantity } of subscriptionItemPairs(sub)) {
171
+ for (const previewItem of itemRows) {
172
+ const priceId = typeof previewItem.price === 'string' ? previewItem.price : String(previewItem.price?.id ?? '');
173
+ const quantity = Number(previewItem.quantity) || 0;
453
174
  const price = ctx.get('price', priceId);
454
175
  const amount = priceAmount(price, quantity);
455
176
  const recurring = price?.recurring ?? {};
456
177
  period = recurring.interval ? addBillingInterval(renewsAt, recurring.interval, Number(recurring.interval_count) || 1) : period;
457
- const item = itemRows.find((r) => (typeof r.price === 'string' ? r.price : r.price?.id) === priceId);
178
+ const item = previewItem;
458
179
  subtotal += amount;
459
180
  out.push({
460
181
  id: `il_twin_${priceId}`, object: 'line_item', type: 'subscription', amount, currency,
@@ -462,19 +183,32 @@ const createPreview = async (ctx) => {
462
183
  period: { start: renewsAt, end: period }, description: `${quantity} × ${price?.nickname ?? priceId}`,
463
184
  });
464
185
  }
465
- if (prorationDate !== undefined && subtotal > 0)
466
- prorationLines(out, subtotal, currency, now, period);
467
- }
468
- for (const ii of newest(ctx, 'invoiceitem', 'date')) {
469
- if (ii.customer !== customer || ii.invoice)
470
- continue;
471
- out.push({ id: `il_${String(ii.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(ii.amount) || 0, currency, quantity: Number(ii.quantity) || 1, proration: false, invoice_item: ii.id, period: ii.period ?? { start: now, end: now }, description: ii.description ?? null });
472
186
  }
473
- subtotal = sumLines(out);
474
187
  const subDiscount = Array.isArray(sub?.discounts) && sub.discounts.length ? sub.discounts[0] : undefined;
475
188
  const coupon = subDiscount && typeof subDiscount.coupon === 'object' ? subDiscount.coupon : undefined;
476
- const discountAmount = applyCouponDiscount(Math.max(0, subtotal), coupon);
477
- const total = Math.max(0, subtotal - discountAmount);
189
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "Disable creating prorations in this request."
190
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "creates a non-proration debit, even when the debit covers a partial billing period."
191
+ if (sub && Array.isArray(details?.items) && behavior !== 'none' && sub.status !== 'trialing') {
192
+ const before = previousItems.map(item => {
193
+ const priceId = typeof item.price === 'string' ? item.price : String(item.price.id);
194
+ const price = ctx.get('price', priceId);
195
+ return { id: `il_twin_${priceId}`, object: 'line_item', type: 'subscription', amount: priceAmount(price, Number(item.quantity ?? 1)), currency,
196
+ price: price ?? priceId, quantity: Number(item.quantity ?? 1), subscription: subId, subscription_item: item.id,
197
+ period: { start: item.current_period_start ?? periodStart, end: item.current_period_end ?? periodEnd } };
198
+ });
199
+ const after = out.map(line => ({ ...line, period: { start: periodStart, end: periodEnd } }));
200
+ const prorations = prorationLines(before, after, prorationTime, periodStart, periodEnd, coupon);
201
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "Always invoice immediately for prorations."
202
+ if (behavior === 'always_invoice' && prorations.length)
203
+ out.splice(0, out.length, ...prorations);
204
+ else
205
+ out.unshift(...prorations);
206
+ }
207
+ out.push(...pendingPreviewLines(ctx, customer, currency, now));
208
+ subtotal = sumLines(out);
209
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "no additional discounts are applied to the proration line items themselves"
210
+ const discountAmount = applyCouponDiscount(Math.max(0, sumLines(out.filter(line => !line.proration))), coupon);
211
+ const total = subtotal - discountAmount;
478
212
  return ctx.reply({
479
213
  ...draft(now),
480
214
  // "For preview invoices created using the create preview endpoint, this id will be prefixed with `upcoming_in`"
@@ -483,63 +217,10 @@ const createPreview = async (ctx) => {
483
217
  object: 'invoice', id: `upcoming_${ctx.mint(INV)}`, customer, subscription: subId || null, currency,
484
218
  billing_reason: subId ? 'upcoming' : 'manual',
485
219
  created: now, period_start: now, period_end: period,
486
- subtotal, total, amount_due: total, amount_paid: 0, amount_remaining: total,
220
+ subtotal, total, amount_due: Math.max(0, total), amount_paid: 0, amount_remaining: Math.max(0, total),
487
221
  starting_balance: 0, ending_balance: null,
488
222
  discounts: discountAmount > 0 && subDiscount ? [subDiscount] : [],
489
223
  total_discount_amounts: discountAmount > 0 ? [{ amount: discountAmount, discount: subDiscount?.id ?? null }] : [],
490
224
  lines: { object: 'list', url: '/v1/invoices/create_preview/lines', has_more: false, total_count: out.length, data: out },
491
225
  });
492
- };
493
- /** An invoice's payment as Stripe answers it (docs.stripe.com/api/invoice-payment/object): the PaymentIntent that
494
- * collects the invoice, open until the invoice is paid. Where the documentation stops and the twin decides: an
495
- * invoice has one payment, its default, and the payment's id is the invoice's with `inpay_`. */
496
- function invoicePayment(inv) {
497
- if (typeof inv.payment_intent !== 'string')
498
- return undefined;
499
- const paid = inv.status === 'paid';
500
- const transitions = inv.status_transitions ?? {};
501
- return {
502
- id: `inpay_${String(inv.id).replace(/^in_/, '')}`, object: 'invoice_payment', invoice: inv.id, is_default: true, livemode: false,
503
- amount_requested: Number(inv.amount_due ?? 0), amount_paid: paid ? Number(inv.amount_paid ?? inv.amount_due ?? 0) : null, currency: inv.currency,
504
- created: inv.created, payment: { type: 'payment_intent', payment_intent: inv.payment_intent },
505
- status: paid ? 'paid' : inv.status === 'void' ? 'canceled' : 'open',
506
- status_transitions: { paid_at: paid ? (transitions.paid_at ?? null) : null, canceled_at: inv.status === 'void' ? (transitions.voided_at ?? null) : null },
507
- };
508
226
  }
509
- const invoicePayments = async (ctx) => {
510
- const p = ctx.params;
511
- const wanted = p.payment?.payment_intent;
512
- const items = newest(ctx, INV)
513
- .filter((inv) => typeof p.invoice !== 'string' || inv.id === p.invoice)
514
- .map(invoicePayment).filter((x) => !!x)
515
- .filter((x) => typeof wanted !== 'string' || x.payment.payment_intent === wanted)
516
- .filter((x) => typeof p.status !== 'string' || x.status === p.status);
517
- return list(ctx, 'invoice_payment', items);
518
- };
519
- const invoicePaymentOne = async (ctx) => {
520
- const id = at(ctx, 'invoice_payment');
521
- const found = newest(ctx, INV).map(invoicePayment).find((x) => x?.id === id);
522
- return found ? ctx.reply(found) : fail(ctx, `No such invoice payment: '${id}'`, 404, 'resource_missing');
523
- };
524
- export const invoices = {
525
- GetInvoicePayments: invoicePayments,
526
- GetInvoicePaymentsInvoicePayment: invoicePaymentOne,
527
- PostInvoiceitems: createItem,
528
- GetInvoiceitemsInvoiceitem: retrieveItem,
529
- PostInvoiceitemsInvoiceitem: updateItem,
530
- DeleteInvoiceitemsInvoiceitem: deleteItem,
531
- PostInvoices: create,
532
- PostInvoicesCreatePreview: createPreview,
533
- PostInvoicesInvoiceFinalize: finalize,
534
- PostInvoicesInvoiceSend: sendInvoice,
535
- PostInvoicesInvoicePay: pay,
536
- PostInvoicesInvoiceMarkUncollectible: markUncollectible,
537
- PostInvoicesInvoiceVoid: voidInvoice,
538
- DeleteInvoicesInvoice: deleteDraft,
539
- GetInvoicesInvoiceLines: lines,
540
- PostInvoicesInvoiceAddLines: editLines('add'),
541
- PostInvoicesInvoiceUpdateLines: editLines('update'),
542
- PostInvoicesInvoiceRemoveLines: editLines('remove'),
543
- GetInvoicesSearch: searchInvoices,
544
- GetInvoicesInvoice: retrieve,
545
- };