@volter/twin-stripe 2.0.37 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/README.md +82 -207
  2. package/dist/generated/pack-facts.json +94 -0
  3. package/dist/spec/SOURCE.md +14 -0
  4. package/dist/spec/fixtures3.json.gz +0 -0
  5. package/dist/spec/openapi.json.gz +0 -0
  6. package/dist/spec/patches.json +34 -0
  7. package/dist/spec/sources.json +378 -0
  8. package/dist/src/cli.js +12 -30
  9. package/dist/src/engine/after-payment.d.ts +9 -0
  10. package/dist/src/engine/after-payment.js +28 -0
  11. package/dist/src/engine/apps-secrets.d.ts +1 -0
  12. package/dist/src/engine/apps-secrets.js +3 -0
  13. package/dist/src/engine/balance.d.ts +5 -0
  14. package/dist/src/engine/balance.js +15 -0
  15. package/dist/src/engine/billing.d.ts +5 -0
  16. package/dist/src/engine/billing.js +6 -0
  17. package/dist/src/engine/charges.d.ts +6 -0
  18. package/dist/src/engine/charges.js +13 -0
  19. package/dist/src/engine/checkout.d.ts +24 -0
  20. package/dist/src/engine/checkout.js +30 -0
  21. package/dist/src/engine/common.d.ts +19 -0
  22. package/dist/src/engine/common.js +32 -0
  23. package/dist/src/{screens → engine}/connect-settings.d.ts +1 -7
  24. package/dist/src/engine/connect-settings.js +26 -0
  25. package/dist/src/engine/connect.d.ts +66 -0
  26. package/dist/src/engine/connect.js +189 -0
  27. package/dist/src/engine/coupons.d.ts +0 -0
  28. package/dist/src/engine/coupons.js +5 -0
  29. package/dist/src/engine/credit-notes.d.ts +1 -0
  30. package/dist/src/engine/credit-notes.js +5 -0
  31. package/dist/src/engine/customers.d.ts +12 -0
  32. package/dist/src/engine/customers.js +30 -0
  33. package/dist/src/engine/display.d.ts +14 -0
  34. package/dist/src/engine/display.js +16 -0
  35. package/dist/src/engine/disputes.d.ts +0 -0
  36. package/dist/src/engine/disputes.js +9 -0
  37. package/dist/src/engine/entitlements.d.ts +1 -0
  38. package/dist/src/engine/entitlements.js +4 -0
  39. package/dist/src/engine/ephemeral-keys.d.ts +0 -0
  40. package/dist/src/engine/ephemeral-keys.js +4 -0
  41. package/dist/src/engine/files.d.ts +18 -0
  42. package/dist/src/engine/files.js +33 -0
  43. package/dist/src/engine/front.d.ts +5 -0
  44. package/dist/src/engine/front.js +25 -0
  45. package/dist/src/engine/invoices.d.ts +24 -0
  46. package/dist/src/engine/invoices.js +57 -0
  47. package/dist/src/engine/issuing.d.ts +35 -0
  48. package/dist/src/engine/issuing.js +45 -0
  49. package/dist/src/engine/ledger.d.ts +9 -0
  50. package/dist/src/engine/ledger.js +9 -0
  51. package/dist/src/engine/oauth.d.ts +4 -0
  52. package/dist/src/engine/oauth.js +25 -0
  53. package/dist/src/engine/onboarding.d.ts +13 -0
  54. package/dist/src/engine/onboarding.js +42 -0
  55. package/dist/src/{stripe-params.d.ts → engine/params.d.ts} +20 -3
  56. package/dist/src/{stripe-params.js → engine/params.js} +7 -60
  57. package/dist/src/engine/payment-intents.d.ts +10 -0
  58. package/dist/src/engine/payment-intents.js +27 -0
  59. package/dist/src/engine/payment-links.d.ts +1 -0
  60. package/dist/src/engine/payment-links.js +5 -0
  61. package/dist/src/engine/payment-methods.d.ts +16 -0
  62. package/dist/src/engine/payment-methods.js +22 -0
  63. package/dist/src/engine/plans.d.ts +6 -0
  64. package/dist/src/engine/plans.js +26 -0
  65. package/dist/src/engine/platform.d.ts +39 -0
  66. package/dist/src/engine/platform.js +34 -0
  67. package/dist/src/engine/products.d.ts +0 -0
  68. package/dist/src/engine/products.js +4 -0
  69. package/dist/src/engine/radar.d.ts +2 -0
  70. package/dist/src/engine/radar.js +5 -0
  71. package/dist/src/engine/refunds.d.ts +1 -0
  72. package/dist/src/engine/refunds.js +28 -0
  73. package/dist/src/engine/renewals.d.ts +36 -0
  74. package/dist/src/engine/renewals.js +65 -0
  75. package/dist/src/engine/setup-intents.d.ts +1 -0
  76. package/dist/src/engine/setup-intents.js +4 -0
  77. package/dist/src/{stripe-twin.d.ts → engine/stripe.d.ts} +11 -39
  78. package/dist/src/{stripe-twin.js → engine/stripe.js} +25 -310
  79. package/dist/src/engine/subscription-schedules.d.ts +3 -0
  80. package/dist/src/engine/subscription-schedules.js +1 -0
  81. package/dist/src/engine/subscriptions.d.ts +12 -0
  82. package/dist/src/engine/subscriptions.js +31 -0
  83. package/dist/src/engine/tax.d.ts +7 -0
  84. package/dist/src/engine/tax.js +28 -0
  85. package/dist/src/engine/terminal.d.ts +4 -0
  86. package/dist/src/engine/terminal.js +20 -0
  87. package/dist/src/engine/test-clocks.d.ts +5 -0
  88. package/dist/src/engine/test-clocks.js +9 -0
  89. package/dist/src/engine/tokens.d.ts +7 -0
  90. package/dist/src/engine/tokens.js +20 -0
  91. package/dist/src/engine/transfers.d.ts +2 -0
  92. package/dist/src/engine/transfers.js +1 -0
  93. package/dist/src/engine/treasury.d.ts +23 -0
  94. package/dist/src/engine/treasury.js +45 -0
  95. package/dist/src/{stripe-version.js → engine/version.js} +4 -3
  96. package/dist/src/engine/webhook-endpoints.d.ts +1 -0
  97. package/dist/src/engine/webhook-endpoints.js +12 -0
  98. package/dist/src/engine/webhooks.d.ts +31 -0
  99. package/dist/src/engine/webhooks.js +68 -0
  100. package/dist/src/fetch.d.ts +2 -0
  101. package/dist/src/fetch.js +26 -0
  102. package/dist/src/generated/surface.gen.json +1 -1
  103. package/dist/src/generated/ui.gen.json +1 -1
  104. package/dist/src/index.d.ts +5 -14
  105. package/dist/src/index.js +8 -75
  106. package/dist/src/manifest.js +232 -758
  107. package/dist/src/screens/billing-portal.d.ts +3 -0
  108. package/dist/src/screens/{portal.js → billing-portal.js} +28 -54
  109. package/dist/src/screens/checkout.d.ts +2 -31
  110. package/dist/src/screens/checkout.js +32 -121
  111. package/dist/src/screens/connect-oauth.d.ts +3 -27
  112. package/dist/src/screens/connect-oauth.js +51 -132
  113. package/dist/src/screens/connect-onboarding.d.ts +3 -0
  114. package/dist/src/screens/{onboarding.js → connect-onboarding.js} +36 -102
  115. package/dist/src/screens/dashboard.d.ts +3 -0
  116. package/dist/src/screens/dashboard.js +117 -0
  117. package/dist/src/screens/shared.d.ts +27 -0
  118. package/dist/src/screens/shared.js +87 -0
  119. package/dist/src/screens/stripe-js.d.ts +3 -0
  120. package/dist/src/screens/stripe-js.js +135 -0
  121. package/dist/src/semantics/account_links.d.ts +2 -0
  122. package/dist/src/semantics/account_links.js +22 -0
  123. package/dist/src/semantics/accounts.d.ts +4 -0
  124. package/dist/src/semantics/accounts.js +38 -0
  125. package/dist/src/semantics/apps.d.ts +4 -0
  126. package/dist/src/semantics/{apps-secrets.js → apps.js} +14 -26
  127. package/dist/src/semantics/around.d.ts +2 -0
  128. package/dist/src/semantics/around.js +16 -0
  129. package/dist/src/semantics/balance.d.ts +2 -11
  130. package/dist/src/semantics/balance.js +3 -194
  131. package/dist/src/semantics/balance_transactions.d.ts +2 -0
  132. package/dist/src/semantics/balance_transactions.js +15 -0
  133. package/dist/src/semantics/billing_portal.d.ts +5 -0
  134. package/dist/src/semantics/billing_portal.js +42 -0
  135. package/dist/src/semantics/charges.d.ts +3 -28
  136. package/dist/src/semantics/charges.js +5 -202
  137. package/dist/src/semantics/checkout.d.ts +3 -20
  138. package/dist/src/semantics/checkout.js +8 -261
  139. package/dist/src/semantics/clock.d.ts +2 -0
  140. package/dist/src/semantics/clock.js +21 -0
  141. package/dist/src/semantics/coupons.d.ts +2 -6
  142. package/dist/src/semantics/coupons.js +2 -60
  143. package/dist/src/semantics/customers.d.ts +6 -6
  144. package/dist/src/semantics/customers.js +27 -387
  145. package/dist/src/semantics/disputes.d.ts +2 -2
  146. package/dist/src/semantics/disputes.js +3 -49
  147. package/dist/src/semantics/doors.d.ts +23 -0
  148. package/dist/src/semantics/doors.js +73 -0
  149. package/dist/src/semantics/entitlements.d.ts +2 -2
  150. package/dist/src/semantics/entitlements.js +5 -50
  151. package/dist/src/semantics/events.d.ts +12 -0
  152. package/dist/src/semantics/events.js +36 -0
  153. package/dist/src/semantics/index.d.ts +2 -0
  154. package/dist/src/semantics/index.js +107 -0
  155. package/dist/src/semantics/invoice_payments.d.ts +2 -0
  156. package/dist/src/semantics/invoice_payments.js +12 -0
  157. package/dist/src/semantics/invoiceitems.d.ts +3 -0
  158. package/dist/src/semantics/invoiceitems.js +59 -0
  159. package/dist/src/semantics/invoices.d.ts +16 -18
  160. package/dist/src/semantics/invoices.js +118 -444
  161. package/dist/src/semantics/payment_intents.d.ts +4 -0
  162. package/dist/src/semantics/payment_intents.js +79 -0
  163. package/dist/src/semantics/payment_methods.d.ts +5 -0
  164. package/dist/src/semantics/payment_methods.js +39 -0
  165. package/dist/src/semantics/payouts.d.ts +2 -0
  166. package/dist/src/semantics/payouts.js +21 -0
  167. package/dist/src/semantics/prices.d.ts +4 -0
  168. package/dist/src/semantics/prices.js +37 -0
  169. package/dist/src/semantics/products.d.ts +6 -2
  170. package/dist/src/semantics/products.js +15 -130
  171. package/dist/src/semantics/promotion_codes.d.ts +3 -0
  172. package/dist/src/semantics/promotion_codes.js +35 -0
  173. package/dist/src/semantics/radar.d.ts +2 -2
  174. package/dist/src/semantics/radar.js +5 -74
  175. package/dist/src/semantics/refunds.d.ts +3 -9
  176. package/dist/src/semantics/refunds.js +6 -192
  177. package/dist/src/semantics/setup_intents.d.ts +3 -0
  178. package/dist/src/semantics/{setup-intents.js → setup_intents.js} +10 -53
  179. package/dist/src/semantics/shared.d.ts +822 -47
  180. package/dist/src/semantics/shared.js +3044 -78
  181. package/dist/src/semantics/states.d.ts +531 -0
  182. package/dist/src/semantics/states.js +852 -0
  183. package/dist/src/semantics/subscription_items.d.ts +5 -0
  184. package/dist/src/semantics/subscription_items.js +55 -0
  185. package/dist/src/semantics/subscription_schedules.d.ts +4 -0
  186. package/dist/src/semantics/subscription_schedules.js +60 -0
  187. package/dist/src/semantics/subscriptions.d.ts +7 -11
  188. package/dist/src/semantics/subscriptions.js +44 -415
  189. package/dist/src/semantics/test_helpers.d.ts +6 -0
  190. package/dist/src/semantics/{test-clocks.js → test_helpers.js} +14 -40
  191. package/dist/src/semantics/transfers.d.ts +2 -2
  192. package/dist/src/semantics/transfers.js +6 -101
  193. package/dist/src/server.d.ts +7 -0
  194. package/dist/src/server.js +7 -0
  195. package/generated/pack-facts.json +94 -0
  196. package/package.json +10 -20
  197. package/spec/SOURCE.md +14 -0
  198. package/spec/fixtures3.json.gz +0 -0
  199. package/spec/openapi.json.gz +0 -0
  200. package/spec/patches.json +34 -0
  201. package/spec/sources.json +378 -0
  202. package/src/cli.ts +12 -26
  203. package/src/engine/after-payment.ts +31 -0
  204. package/src/engine/apps-secrets.ts +4 -0
  205. package/src/engine/balance.ts +33 -0
  206. package/src/engine/billing.ts +16 -0
  207. package/src/engine/charges.ts +19 -0
  208. package/src/engine/checkout.ts +40 -0
  209. package/src/engine/common.ts +47 -0
  210. package/src/engine/connect-settings.ts +23 -0
  211. package/src/engine/connect.ts +184 -0
  212. package/src/engine/coupons.ts +5 -0
  213. package/src/engine/credit-notes.ts +6 -0
  214. package/src/engine/customers.ts +35 -0
  215. package/src/engine/display.ts +19 -0
  216. package/src/engine/disputes.ts +9 -0
  217. package/src/engine/entitlements.ts +5 -0
  218. package/src/engine/ephemeral-keys.ts +3 -0
  219. package/src/engine/files.ts +41 -0
  220. package/src/engine/front.ts +31 -0
  221. package/src/engine/invoices.ts +71 -0
  222. package/src/engine/issuing.ts +61 -0
  223. package/src/engine/ledger.ts +50 -0
  224. package/src/engine/oauth.ts +19 -0
  225. package/src/engine/onboarding.ts +54 -0
  226. package/src/{stripe-params.ts → engine/params.ts} +8 -56
  227. package/src/engine/payment-intents.ts +33 -0
  228. package/src/engine/payment-links.ts +6 -0
  229. package/src/engine/payment-methods.ts +25 -0
  230. package/src/engine/plans.ts +42 -0
  231. package/src/engine/platform.ts +47 -0
  232. package/src/engine/products.ts +3 -0
  233. package/src/engine/radar.ts +6 -0
  234. package/src/engine/refunds.ts +29 -0
  235. package/src/engine/renewals.ts +73 -0
  236. package/src/engine/setup-intents.ts +5 -0
  237. package/src/{stripe-twin.ts → engine/stripe.ts} +28 -323
  238. package/src/engine/subscription-schedules.ts +10 -0
  239. package/src/engine/subscriptions.ts +40 -0
  240. package/src/engine/tax.ts +39 -0
  241. package/src/engine/terminal.ts +23 -0
  242. package/src/engine/test-clocks.ts +11 -0
  243. package/src/engine/tokens.ts +22 -0
  244. package/src/engine/transfers.ts +23 -0
  245. package/src/engine/treasury.ts +47 -0
  246. package/src/{stripe-version.ts → engine/version.ts} +4 -3
  247. package/src/engine/webhook-endpoints.ts +14 -0
  248. package/src/engine/webhooks.ts +72 -0
  249. package/src/fetch.ts +27 -0
  250. package/src/generated/surface.gen.json +1 -1
  251. package/src/generated/ui.gen.json +1 -1
  252. package/src/index.ts +8 -116
  253. package/src/manifest.ts +234 -786
  254. package/src/screens/{portal.tsx → billing-portal.tsx} +32 -37
  255. package/src/screens/checkout.tsx +33 -95
  256. package/src/screens/connect-oauth.tsx +64 -88
  257. package/src/screens/{onboarding.tsx → connect-onboarding.tsx} +38 -82
  258. package/src/screens/dashboard.tsx +198 -0
  259. package/src/screens/shared.tsx +84 -0
  260. package/src/screens/stripe-js.ts +170 -0
  261. package/src/semantics/account_links.ts +21 -0
  262. package/src/semantics/accounts.ts +43 -0
  263. package/src/semantics/{apps-secrets.ts → apps.ts} +17 -32
  264. package/src/semantics/around.ts +20 -0
  265. package/src/semantics/balance.ts +6 -207
  266. package/src/semantics/balance_transactions.ts +20 -0
  267. package/src/semantics/billing_portal.ts +51 -0
  268. package/src/semantics/charges.ts +12 -211
  269. package/src/semantics/checkout.ts +13 -268
  270. package/src/semantics/clock.ts +26 -0
  271. package/src/semantics/coupons.ts +5 -67
  272. package/src/semantics/customers.ts +29 -392
  273. package/src/semantics/disputes.ts +9 -59
  274. package/src/semantics/doors.ts +80 -0
  275. package/src/semantics/entitlements.ts +9 -56
  276. package/src/semantics/events.ts +44 -0
  277. package/src/semantics/index.ts +111 -0
  278. package/src/semantics/invoice_payments.ts +18 -0
  279. package/src/semantics/invoiceitems.ts +59 -0
  280. package/src/semantics/invoices.ts +109 -443
  281. package/src/semantics/payment_intents.ts +83 -0
  282. package/src/semantics/payment_methods.ts +43 -0
  283. package/src/semantics/payouts.ts +23 -0
  284. package/src/semantics/prices.ts +42 -0
  285. package/src/semantics/products.ts +18 -142
  286. package/src/semantics/promotion_codes.ts +39 -0
  287. package/src/semantics/radar.ts +8 -77
  288. package/src/semantics/refunds.ts +9 -212
  289. package/src/semantics/setup_intents.ts +45 -0
  290. package/src/semantics/shared.ts +3092 -106
  291. package/src/semantics/states.ts +883 -0
  292. package/src/semantics/subscription_items.ts +54 -0
  293. package/src/semantics/subscription_schedules.ts +66 -0
  294. package/src/semantics/subscriptions.ts +49 -432
  295. package/src/semantics/{test-clocks.ts → test_helpers.ts} +23 -47
  296. package/src/semantics/transfers.ts +11 -131
  297. package/src/server.ts +8 -0
  298. package/client/dashboard-api.ts +0 -286
  299. package/client/stripe-mirror.css +0 -277
  300. package/client/stripe-mirror.tsx +0 -1559
  301. package/dist/client/dashboard-api.d.ts +0 -107
  302. package/dist/client/dashboard-api.js +0 -238
  303. package/dist/client/dashboard-api.ts +0 -286
  304. package/dist/client/stripe-mirror.bundle.js +0 -236
  305. package/dist/client/stripe-mirror.css +0 -277
  306. package/dist/client/stripe-mirror.d.ts +0 -134
  307. package/dist/client/stripe-mirror.js +0 -859
  308. package/dist/client/stripe-mirror.tsx +0 -1559
  309. package/dist/src/screens/connect-settings.js +0 -103
  310. package/dist/src/screens/consent-skin.d.ts +0 -4
  311. package/dist/src/screens/consent-skin.js +0 -18
  312. package/dist/src/screens/financial-connections.d.ts +0 -5
  313. package/dist/src/screens/financial-connections.js +0 -90
  314. package/dist/src/screens/identity.d.ts +0 -5
  315. package/dist/src/screens/identity.js +0 -86
  316. package/dist/src/screens/onboarding.d.ts +0 -13
  317. package/dist/src/screens/portal.d.ts +0 -5
  318. package/dist/src/screens/public-details.d.ts +0 -5
  319. package/dist/src/screens/public-details.js +0 -90
  320. package/dist/src/semantics/after-payment.d.ts +0 -22
  321. package/dist/src/semantics/after-payment.js +0 -99
  322. package/dist/src/semantics/apps-secrets.d.ts +0 -2
  323. package/dist/src/semantics/billing.d.ts +0 -2
  324. package/dist/src/semantics/billing.js +0 -220
  325. package/dist/src/semantics/connect.d.ts +0 -5
  326. package/dist/src/semantics/connect.js +0 -493
  327. package/dist/src/semantics/credit-notes.d.ts +0 -2
  328. package/dist/src/semantics/credit-notes.js +0 -172
  329. package/dist/src/semantics/ephemeral-keys.d.ts +0 -2
  330. package/dist/src/semantics/ephemeral-keys.js +0 -34
  331. package/dist/src/semantics/files.d.ts +0 -2
  332. package/dist/src/semantics/files.js +0 -125
  333. package/dist/src/semantics/issuing.d.ts +0 -13
  334. package/dist/src/semantics/issuing.js +0 -575
  335. package/dist/src/semantics/ledger.d.ts +0 -59
  336. package/dist/src/semantics/ledger.js +0 -200
  337. package/dist/src/semantics/payment-intents.d.ts +0 -18
  338. package/dist/src/semantics/payment-intents.js +0 -452
  339. package/dist/src/semantics/payment-links.d.ts +0 -2
  340. package/dist/src/semantics/payment-links.js +0 -133
  341. package/dist/src/semantics/payment-methods.d.ts +0 -20
  342. package/dist/src/semantics/payment-methods.js +0 -140
  343. package/dist/src/semantics/plans.d.ts +0 -5
  344. package/dist/src/semantics/plans.js +0 -121
  345. package/dist/src/semantics/platform.d.ts +0 -9
  346. package/dist/src/semantics/platform.js +0 -206
  347. package/dist/src/semantics/renewals.d.ts +0 -47
  348. package/dist/src/semantics/renewals.js +0 -281
  349. package/dist/src/semantics/setup-intents.d.ts +0 -2
  350. package/dist/src/semantics/subscription-schedules.d.ts +0 -2
  351. package/dist/src/semantics/subscription-schedules.js +0 -119
  352. package/dist/src/semantics/tax.d.ts +0 -2
  353. package/dist/src/semantics/tax.js +0 -197
  354. package/dist/src/semantics/terminal.d.ts +0 -5
  355. package/dist/src/semantics/terminal.js +0 -182
  356. package/dist/src/semantics/test-clocks.d.ts +0 -6
  357. package/dist/src/semantics/tokens.d.ts +0 -4
  358. package/dist/src/semantics/tokens.js +0 -44
  359. package/dist/src/semantics/treasury.d.ts +0 -2
  360. package/dist/src/semantics/treasury.js +0 -377
  361. package/dist/src/semantics/webhook-endpoints.d.ts +0 -3
  362. package/dist/src/semantics/webhook-endpoints.js +0 -85
  363. package/dist/src/stripe-budget.d.ts +0 -55
  364. package/dist/src/stripe-budget.js +0 -155
  365. package/dist/src/stripe-capabilities.d.ts +0 -3
  366. package/dist/src/stripe-capabilities.js +0 -5836
  367. package/dist/src/stripe-conformance.d.ts +0 -43
  368. package/dist/src/stripe-conformance.js +0 -105
  369. package/dist/src/stripe-connector.d.ts +0 -161
  370. package/dist/src/stripe-connector.js +0 -414
  371. package/dist/src/stripe-emit.d.ts +0 -2
  372. package/dist/src/stripe-emit.js +0 -145
  373. package/dist/src/stripe-events.d.ts +0 -93
  374. package/dist/src/stripe-events.js +0 -392
  375. package/dist/src/stripe-js.d.ts +0 -4
  376. package/dist/src/stripe-js.js +0 -70
  377. package/dist/src/stripe-mirror-ui.d.ts +0 -15
  378. package/dist/src/stripe-mirror-ui.js +0 -87
  379. package/dist/src/stripe-perform-harness.d.ts +0 -9
  380. package/dist/src/stripe-perform-harness.js +0 -26
  381. package/dist/src/stripe-server.d.ts +0 -33
  382. package/dist/src/stripe-server.js +0 -429
  383. package/dist/src/stripe-shared.d.ts +0 -109
  384. package/dist/src/stripe-shared.js +0 -276
  385. package/dist/src/stripe-ui-conformance.d.ts +0 -5
  386. package/dist/src/stripe-ui-conformance.js +0 -79
  387. package/dist/src/stripe-ui-structure.d.ts +0 -3
  388. package/dist/src/stripe-ui-structure.js +0 -168
  389. package/dist/test-fixtures/stripe-known-deviations.json +0 -110
  390. package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +0 -14
  391. package/dist/test-fixtures/stripe-openapi-operations.json +0 -4717
  392. package/dist/test-fixtures/stripe-schemas.SOURCE.md +0 -35
  393. package/dist/test-fixtures/stripe-schemas.json +0 -3813
  394. package/src/screens/connect-settings.tsx +0 -121
  395. package/src/screens/consent-skin.ts +0 -20
  396. package/src/screens/financial-connections.tsx +0 -101
  397. package/src/screens/identity.tsx +0 -96
  398. package/src/screens/public-details.tsx +0 -105
  399. package/src/semantics/after-payment.ts +0 -118
  400. package/src/semantics/billing.ts +0 -216
  401. package/src/semantics/connect.ts +0 -487
  402. package/src/semantics/credit-notes.ts +0 -168
  403. package/src/semantics/ephemeral-keys.ts +0 -34
  404. package/src/semantics/files.ts +0 -143
  405. package/src/semantics/issuing.ts +0 -590
  406. package/src/semantics/ledger.ts +0 -253
  407. package/src/semantics/payment-intents.ts +0 -463
  408. package/src/semantics/payment-links.ts +0 -148
  409. package/src/semantics/payment-methods.ts +0 -145
  410. package/src/semantics/plans.ts +0 -131
  411. package/src/semantics/platform.ts +0 -220
  412. package/src/semantics/renewals.ts +0 -279
  413. package/src/semantics/setup-intents.ts +0 -87
  414. package/src/semantics/subscription-schedules.ts +0 -129
  415. package/src/semantics/tax.ts +0 -220
  416. package/src/semantics/terminal.ts +0 -195
  417. package/src/semantics/tokens.ts +0 -52
  418. package/src/semantics/treasury.ts +0 -383
  419. package/src/semantics/webhook-endpoints.ts +0 -87
  420. package/src/stripe-budget.ts +0 -181
  421. package/src/stripe-capabilities.ts +0 -5651
  422. package/src/stripe-conformance.ts +0 -124
  423. package/src/stripe-connector.ts +0 -462
  424. package/src/stripe-emit.ts +0 -157
  425. package/src/stripe-events.ts +0 -434
  426. package/src/stripe-js.ts +0 -70
  427. package/src/stripe-mirror-ui.ts +0 -90
  428. package/src/stripe-perform-harness.ts +0 -29
  429. package/src/stripe-server.ts +0 -393
  430. package/src/stripe-shared.ts +0 -297
  431. package/src/stripe-ui-conformance.ts +0 -88
  432. package/src/stripe-ui-structure.ts +0 -182
  433. package/test-fixtures/stripe-known-deviations.json +0 -110
  434. package/test-fixtures/stripe-openapi-operations.json +0 -4717
  435. package/test-fixtures/stripe-schemas.json +0 -3813
  436. /package/dist/src/{screens → engine}/industries.d.ts +0 -0
  437. /package/dist/src/{screens → engine}/industries.js +0 -0
  438. /package/dist/src/{semantics → engine}/test-cards.d.ts +0 -0
  439. /package/dist/src/{semantics → engine}/test-cards.js +0 -0
  440. /package/dist/src/{stripe-version.d.ts → engine/version.d.ts} +0 -0
  441. /package/src/{screens → engine}/industries.ts +0 -0
  442. /package/src/{semantics → engine}/test-cards.ts +0 -0
@@ -1,6 +1,1402 @@
1
- import surface from '../generated/surface.gen.json' with { type: 'json' };
2
- import { manifest } from "../manifest.js";
3
- import { OBJECT_NAME, paginate, searchOver, stashVendorType, view } from "../stripe-twin.js";
1
+ import { NAMED_EVENTS, readDecision, STRIPE_EVENT_TYPES, STRIPE_REALTIME_AUTH_TIMEOUT_MS, stripeSignature } from "../engine/webhooks.js";
2
+ import { redactKey, requestKey } from "../engine/oauth.js";
3
+ import { HOUR, INV, itemQuantity, itemsOf, keptAfter, oneTimeLines, RENEWING, SUB } from "../engine/renewals.js";
4
+ import { DAY, payoutDay } from "../engine/balance.js";
5
+ import { AFTER_SUCCESS_CARDS, BY_NUMBER } from "../engine/after-payment.js";
6
+ import { accountSettings, addBillingInterval, applyCouponDiscount, asBool, buildDiscount, buildSubscriptionItemsList, cardError, chargeDefaults, declineFor, disputeEvidence, lineItemEntries, microdepositsNextAction, mintClientSecret, nowUnix, OBJECT_NAME, paginate, paymentMethodSubObject, PLATFORM_ACCOUNT_ID, priceAmount, requiresAuthentication, requiresMicrodeposits, resolveLineItem, resolveSubscriptionBillingInterval, resolveTrial, searchOver, subscriptionItemEntries, subscriptionItemPrice, TEST_TOKEN_DECLINES, threeDsNextAction, validateMoney, view } from "../engine/stripe.js";
7
+ import { AUTO_VALIDATED, euVatCountry } from "../engine/customers.js";
8
+ import { SI } from "../engine/setup-intents.js";
9
+ import { BY_CLIENT_SECRET, PUBLISHABLE_CREATES } from "../engine/front.js";
10
+ import { WE } from "../engine/webhook-endpoints.js";
11
+ import { ENTRY, ENTRY_TYPES, FA, TX } from "../engine/treasury.js";
12
+ import { SECRET } from "../engine/apps-secrets.js";
13
+ import { draft, invoicePrefix, linesOf, sumLines, unlinkedLine } from "../engine/invoices.js";
14
+ import { CN } from "../engine/credit-notes.js";
15
+ import { CS, isOneTime, nestedMetadata } from "../engine/checkout.js";
16
+ import { externalList, refundDestination, SPEC_FIELDS } from "../engine/common.js";
17
+ import { noBilling, PM, TEST_PM_CARDS, testCardOf } from "../engine/payment-methods.js";
18
+ import { SS } from "../engine/subscription-schedules.js";
19
+ import { OAUTH_CONNECTIONS } from "../engine/display.js";
20
+ import { intentCarries, PI, TYPES_GIVEN } from "../engine/payment-intents.js";
21
+ import { BT, BYPASS_NUMBERS, feeOf, onAccount, UNSENT } from "../engine/ledger.js";
22
+ import { withoutCard } from "../engine/charges.js";
23
+ import { MAX_TRIAL, movedTrial } from "../engine/subscriptions.js";
24
+ import { VL, VLI } from "../engine/radar.js";
25
+ import { AUTH, CARD, historyEntry, merchantData, PD, REALTIME_WINDOW_SECONDS, TXN } from "../engine/issuing.js";
26
+ import { REFUND_REASONS } from "../engine/refunds.js";
27
+ import { ADVANCE_SECONDS, CLOCK } from "../engine/test-clocks.js";
28
+ import { BYPASS_PENDING_CARDS } from "../engine/test-cards.js";
29
+ import { PLATFORM_CLIENT_ID, SETTINGS } from "../engine/connect-settings.js";
30
+ import { controllerOf, customRequirements } from "../engine/connect.js";
31
+ import { READER } from "../engine/terminal.js";
32
+ import { expandOf, render, SERVED_VERSION, servesCurrent, withoutEndpointSecret } from "../engine/version.js";
33
+ import { ALERT, GRANT, METER } from "../engine/billing.js";
34
+ import { CHECKOUT_METHOD_TYPES, DOCUMENTED, methodTypesRemoved, oneOf, OPS, PAYMENT_METHOD_TYPES, REMOVED_TYPES, TYPED, TYPES_DOC } from "../engine/params.js";
35
+ /** Stripe's events: `Stripe-Signature: t=…,v1=…` over `${t}.${body}` keyed by the endpoint's secret
36
+ * (docs.stripe.com/webhooks#verify-manually); the Event object (docs.stripe.com/api/events/object) kept as the account's
37
+ * own (`GET /v1/events`), whether or not an endpoint takes it; an endpoint live while `enabled`, taking the types its
38
+ * `enabled_events` names (`*` all, `<family>.*` a family), and a Connect endpoint (`connect: true`) its connected
39
+ * accounts' events ("Connected accounts" scope, docs.stripe.com/connect/webhooks), any other the account's own. */
40
+ export const STRIPE_EVENTS = {
41
+ scheme: { kind: 'timestamp-v1', header: 'Stripe-Signature' },
42
+ types: NAMED_EVENTS,
43
+ known: STRIPE_EVENT_TYPES,
44
+ store: 'event',
45
+ envelope: {
46
+ id: '$id', object: 'event', account: '$account', api_version: '$version', created: '$time.s', data: '$data', livemode: false,
47
+ pending_webhooks: 0, request: { id: null, idempotency_key: null }, type: '$type',
48
+ },
49
+ endpoints: { storedAs: 'webhook_endpoint', url: 'url', secret: 'secret', filter: 'enabled_events', status: { field: 'status', live: 'enabled' }, scope: { field: 'connect', value: '$account' } },
50
+ };
51
+ /** Whether a connected account's OAuth connection to the platform was revoked (deauthorized, or its code reused). */
52
+ export function connectionRevoked(ctx, account) {
53
+ return ctx.rowsRaw(OAUTH_CONNECTIONS).some((c) => c.id === account && c.revoked === true);
54
+ }
55
+ /** The connected account an OAuth key acts as — the connection's current access_token (sk_test_oauth_…) or its
56
+ * stripe_publishable_key (pk_test_oauth_…) — with the scope it was issued; undefined for any other key, and null for an
57
+ * OAuth-shaped key no live connection holds (revoked, replaced by a refresh, or never issued). */
58
+ export function oauthKeyAccount(ctx, key) {
59
+ if (!/^(sk|pk)_test_oauth_/.test(key))
60
+ return undefined;
61
+ const held = ctx.rowsRaw(OAUTH_CONNECTIONS).find((c) => c.revoked !== true && (c.access_token === key || c.publishable_key === key));
62
+ return held ? { account: String(held.id), scope: String(key.startsWith('pk_') ? held.scope : held.access_scope ?? held.scope) } : null;
63
+ }
64
+ /** A customer's now: its test clock's frozen time when it is on one (docs.stripe.com/billing/testing/test-clocks), the
65
+ * World's otherwise. */
66
+ export function clockNow(ctx, customer) {
67
+ const clock = typeof customer === 'string' ? ctx.get('customer', customer)?.test_clock : undefined;
68
+ const frozen = typeof clock === 'string' ? ctx.get('test_helpers.test_clock', clock)?.frozen_time : undefined;
69
+ return frozen !== undefined ? Number(frozen) : Number(nowUnix(ctx.occurredAt));
70
+ }
71
+ /** What a subscription's invoices charge: its own default payment method, else its customer's. */
72
+ export function payerOf(ctx, sub) {
73
+ return typeof sub.default_payment_method === 'string' && sub.default_payment_method ? sub.default_payment_method : customerDefault(ctx, sub);
74
+ }
75
+ /** The customer's default for invoices, charged when the subscription names none of its own. */
76
+ export function customerDefault(ctx, sub) {
77
+ const customer = typeof sub.customer === 'string' ? ctx.get('customer', sub.customer) : undefined;
78
+ const settings = customer?.invoice_settings;
79
+ return typeof settings?.default_payment_method === 'string' && settings.default_payment_method ? settings.default_payment_method : undefined;
80
+ }
81
+ /** A subscription whose invoice has just been finalized: the `once` coupon that invoice took is spent, and a once coupon
82
+ * it did not take (added after it was drafted) stays for the next (unspentDiscounts). */
83
+ export async function spendOnceCoupon(ctx, invoice) {
84
+ const subId = typeof invoice.subscription === 'string' ? invoice.subscription : undefined;
85
+ const sub = subId ? ctx.get(SUB, subId) : undefined;
86
+ if (!sub || !subId)
87
+ return;
88
+ const all = sub.discounts ?? [];
89
+ const kept = keptAfter(all, invoice);
90
+ if (kept.length !== all.length)
91
+ await ctx.write(SUB, subId, { discounts: kept }, 'subscription.discount_spent');
92
+ }
93
+ /** A draft invoice for one period of a subscription, its lines the subscription's items and any one-time lines `once`
94
+ * gives it (a Checkout session's one-time prices, "on the initial invoice only", docs.stripe.com/api/checkout/sessions/
95
+ * create#create_checkout_session-line_items), under the subscription's discount. Answers the invoice id. */
96
+ export async function draftSubscriptionInvoice(ctx, sub, o) {
97
+ const id = o.id ?? ctx.mint(INV);
98
+ const currency = String(sub.currency ?? 'usd');
99
+ const lines = itemsOf(sub).map((it, i) => {
100
+ const price = it.price && typeof it.price === 'object' ? it.price : ctx.get('price', String(it.price ?? ''));
101
+ const quantity = itemQuantity(it);
102
+ return {
103
+ id: `il_twin_${id}_${i + 1}`, object: 'line_item', type: 'subscription',
104
+ // a trial's first invoice bills nothing for the trial period (docs.stripe.com/billing/subscriptions/trials)
105
+ amount: o.trial ? 0 : priceAmount(price, quantity), currency, quantity, proration: false,
106
+ price: price ?? null, subscription: sub.id, subscription_item: it.id ?? null, invoice_item: null,
107
+ period: { start: o.start, end: o.end }, description: o.trial ? `Trial period for ${String(ctx.get('product', String(price?.product ?? ''))?.name ?? 'the plan')}` : null,
108
+ };
109
+ });
110
+ if (o.once?.length)
111
+ lines.push(...oneTimeLines(id, sub, o.once, lines.length, o.start, currency));
112
+ const subtotal = lines.reduce((s, l) => s + l.amount, 0);
113
+ // a discount the subscription still carries applies; a `once` coupon is removed once an invoice finalizes
114
+ // (spendOnceCoupon), so it discounts one invoice (docs.stripe.com/billing/subscriptions/coupons#coupon-duration)
115
+ const discount = (sub.discounts ?? [])[0];
116
+ const coupon = discount && typeof discount === 'object' ? discount.coupon : undefined;
117
+ const off = coupon ? applyCouponDiscount(subtotal, coupon) : 0;
118
+ const total = Math.max(0, subtotal - off);
119
+ await created(ctx, INV, { id, customer: sub.customer, subscription: sub.id }, {
120
+ created: o.start, status: 'draft', livemode: false, currency, collection_method: 'charge_automatically',
121
+ billing_reason: o.reason, auto_advance: true, attempt_count: 0, attempted: false, next_payment_attempt: null,
122
+ amount_due: total, amount_paid: 0, amount_remaining: total, amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
123
+ subtotal, total, starting_balance: 0, post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
124
+ // "The invoice that consumed the coupon still shows the applied discount" (docs.stripe.com/billing/subscriptions/coupons)
125
+ period_start: o.start, period_end: o.end, default_tax_rates: [], discounts: coupon && discount ? [discount] : [],
126
+ lines: { object: 'list', data: lines, has_more: false, total_count: lines.length, url: `/v1/invoices/${id}/lines` },
127
+ automatic_tax: { enabled: false, liability: null, status: null },
128
+ status_transitions: { finalized_at: null, marked_uncollectible_at: null, paid_at: null, voided_at: null },
129
+ issuer: { type: 'self' }, payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
130
+ payment_intent: null, charge: null,
131
+ });
132
+ return id;
133
+ }
134
+ /** Finalize a subscription's draft invoice at `at` and charge what its subscription pays with. The move is time's (a
135
+ * renewal), the customer's (the Checkout page they paid on) or the API's (an update that ends a trial, which names the
136
+ * payer, may ask for no attempt, and moves the subscription itself: semantics/subscriptions.ts applyTrialEnd). Answers
137
+ * whether it was paid. */
138
+ export async function collectSubscriptionInvoice(ctx, invoiceId, at, actor, opts = {}) {
139
+ const inv = ctx.get(INV, invoiceId);
140
+ const sub = inv && typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
141
+ if (!inv || !sub || inv.status !== 'draft')
142
+ return false;
143
+ const total = Number(inv.total) || 0;
144
+ const numbered = { ...finalizedFields(ctx, inv, at), status_transitions: { ...inv.status_transitions, finalized_at: at } };
145
+ const op = ctx.call.operation.id;
146
+ // nothing due (a trial's first invoice) is paid as it is finalized; finalizing spends a `once` coupon
147
+ if (total <= 0) {
148
+ if (ctx.legal(INV, 'status', op, 'draft', 'paid', invoiceId, actor))
149
+ return false;
150
+ await spendOnceCoupon(ctx, inv);
151
+ await ctx.write(INV, invoiceId, { ...numbered, status: 'paid', paid: true, auto_advance: false, status_transitions: { ...numbered.status_transitions, paid_at: at } }, 'invoice.paid');
152
+ return true;
153
+ }
154
+ const payer = actor === 'api' ? opts.payer : payerOf(ctx, sub);
155
+ // default_incomplete finalizes the invoice without attempting payment (docs.stripe.com/api/subscriptions/update#update_subscription-payment_behavior)
156
+ const attempt = opts.attempt !== false;
157
+ const decline = payer && attempt ? declineFor(finder(ctx), { payment_method: payer }) : undefined;
158
+ const pays = attempt && !!payer && !decline;
159
+ if (ctx.legal(INV, 'status', op, 'draft', pays ? 'paid' : 'open', invoiceId, actor))
160
+ return false;
161
+ await spendOnceCoupon(ctx, inv);
162
+ const piId = ctx.mint('payment_intent');
163
+ const currency = String(inv.currency ?? 'usd');
164
+ const chargeId = pays ? await mintCharge(ctx, piId, { currency, customer: inv.customer, payment_method: payer, invoice: invoiceId }, total) : null;
165
+ await created(ctx, 'payment_intent', { id: piId, amount: total, currency, customer: inv.customer, invoice: invoiceId, payment_method: pays ? payer : null }, {
166
+ status: pays ? 'succeeded' : 'requires_payment_method', client_secret: mintClientSecret(piId, await ctx.secret(`client-secret:${piId}`)), livemode: false,
167
+ capture_method: 'automatic', amount_capturable: 0, amount_received: pays ? total : 0, next_action: null, latest_charge: chargeId,
168
+ last_payment_error: decline ? { type: 'card_error', code: decline.code, decline_code: decline.decline_code ?? null, message: decline.message, payment_method: { id: payer } } : null,
169
+ automatic_payment_methods: null, payment_method_types: ['card'], payment_method_options: {},
170
+ });
171
+ await ctx.write(INV, invoiceId, {
172
+ ...numbered, status: pays ? 'paid' : 'open', paid: pays, attempt_count: attempt ? 1 : 0, attempted: attempt, payment_intent: piId, charge: chargeId,
173
+ amount_paid: pays ? total : 0, amount_remaining: pays ? 0 : total, auto_advance: attempt && !pays, next_payment_attempt: null,
174
+ status_transitions: { ...numbered.status_transitions, paid_at: pays ? at : null },
175
+ }, pays ? 'invoice.paid' : attempt ? 'invoice.payment_failed' : 'invoice.finalized');
176
+ if (actor !== 'api' && !pays && sub.status !== 'past_due' && !ctx.legal(SUB, 'status', op, sub.status, 'past_due', String(sub.id), 'time')) {
177
+ await ctx.write(SUB, String(sub.id), { status: 'past_due' }, 'customer.subscription.updated');
178
+ }
179
+ return pays;
180
+ }
181
+ /** Pay a subscription's open invoice with `paymentMethod` (the customer on the portal, `external`): the charge on the
182
+ * invoice's PaymentIntent, the invoice paid, a past_due subscription active. Answers the decline, if the card is refused. */
183
+ export async function payOpenInvoice(ctx, invoiceId, paymentMethod) {
184
+ const inv = ctx.get(INV, invoiceId);
185
+ if (!inv || inv.status !== 'open')
186
+ return { code: 'invoice_not_open', message: 'This invoice is no longer open.' };
187
+ const decline = declineFor(finder(ctx), { payment_method: paymentMethod });
188
+ if (decline)
189
+ return decline;
190
+ const op = ctx.call.operation.id;
191
+ const refused = ctx.legal(INV, 'status', op, 'open', 'paid', invoiceId, 'external');
192
+ if (refused)
193
+ return { code: String(refused.code ?? 'invoice_not_open'), message: refused.message };
194
+ const total = Number(inv.amount_remaining ?? inv.total) || 0;
195
+ const now = Number(nowUnix(ctx.occurredAt));
196
+ const piId = typeof inv.payment_intent === 'string' ? inv.payment_intent : undefined;
197
+ let chargeId = null;
198
+ const pi = piId ? ctx.get('payment_intent', piId) : undefined;
199
+ if (piId && pi && !ctx.legal('payment_intent', 'status', op, pi.status, 'succeeded', piId, 'external')) {
200
+ chargeId = await mintCharge(ctx, piId, { ...pi, payment_method: paymentMethod, invoice: invoiceId }, total);
201
+ await ctx.write('payment_intent', piId, { status: 'succeeded', payment_method: paymentMethod, amount_received: total, last_payment_error: null, latest_charge: chargeId }, 'payment_intent.succeeded');
202
+ }
203
+ await ctx.write(INV, invoiceId, {
204
+ status: 'paid', paid: true, amount_paid: total, amount_remaining: 0, charge: chargeId, auto_advance: false,
205
+ attempt_count: (Number(inv.attempt_count) || 0) + 1, attempted: true,
206
+ status_transitions: { ...inv.status_transitions, paid_at: now },
207
+ }, 'invoice.paid');
208
+ const sub = typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
209
+ if (sub && sub.status === 'past_due' && !ctx.legal(SUB, 'status', op, 'past_due', 'active', String(sub.id), 'external')) {
210
+ await ctx.write(SUB, String(sub.id), { status: 'active' }, 'customer.subscription.updated');
211
+ }
212
+ return undefined;
213
+ }
214
+ /** Time's moves on subscriptions, caught up to each customer's clock in the order they fell due, each made at the
215
+ * moment it was due: a period that ended renews (a trial's end among them) or, set to cancel, cancels; a renewal
216
+ * drafted an hour or more ago is finalized and charged. */
217
+ export async function advanceBilling(ctx, only) {
218
+ // Stripe's own move wherever a handler reaches it: a read that catches a renewal up records it with no caller
219
+ await ctx.asVendor(async () => {
220
+ const at = at_(ctx);
221
+ for (let guard = 0; guard < 10_000 && (await billingStep(ctx, at, only)); guard++)
222
+ ;
223
+ });
224
+ }
225
+ /** The earliest move that has come due, made at its moment; false when none has. */
226
+ export async function billingStep(ctx, at, only) {
227
+ // the earliest move that has come due, of any subscription
228
+ let next;
229
+ for (const sub of ctx.rows(SUB)) {
230
+ if (only && !only(sub))
231
+ continue;
232
+ if (!RENEWING.includes(String(sub.status)) || typeof sub.current_period_end !== 'number' || sub.pause_collection)
233
+ continue;
234
+ if (sub.current_period_end <= clockNow(ctx, sub.customer) && (!next || sub.current_period_end < next.t))
235
+ next = { t: sub.current_period_end, kind: 'period', id: String(sub.id) };
236
+ }
237
+ for (const inv of ctx.rows(INV)) {
238
+ if (inv.status !== 'draft' || inv.billing_reason !== 'subscription_cycle' || inv.auto_advance !== true)
239
+ continue;
240
+ const sub = typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
241
+ if (!sub || (only && !only(sub)))
242
+ continue;
243
+ const t = Number(inv.created) + HOUR;
244
+ if (t <= clockNow(ctx, sub.customer) && (!next || t < next.t))
245
+ next = { t, kind: 'collect', id: String(inv.id) };
246
+ }
247
+ if (!next)
248
+ return false;
249
+ const c = await at(next.t);
250
+ if (next.kind === 'collect') {
251
+ await collectSubscriptionInvoice(c, next.id, next.t, 'time');
252
+ return true;
253
+ }
254
+ const id = next.id;
255
+ const sub = c.get(SUB, id);
256
+ const op = c.call.operation.id;
257
+ // one set to cancel at its period's end is canceled then, with no renewal
258
+ // (docs.stripe.com/billing/subscriptions/cancel#cancel-at-end-of-cycle)
259
+ if (sub.cancel_at_period_end === true) {
260
+ if (c.legal(SUB, 'status', op, sub.status, 'canceled', id, 'time'))
261
+ return false;
262
+ await c.write(SUB, id, { status: 'canceled', ended_at: next.t, canceled_at: sub.canceled_at ?? next.t }, 'customer.subscription.deleted');
263
+ return true;
264
+ }
265
+ const start = next.t;
266
+ const { interval, interval_count } = resolveSubscriptionBillingInterval(itemsOf(sub), finder(c));
267
+ const end = addBillingInterval(start, interval, interval_count);
268
+ const trialEnds = sub.status === 'trialing';
269
+ if (trialEnds && c.legal(SUB, 'status', op, 'trialing', 'active', id, 'time'))
270
+ return false;
271
+ const invoiceId = await draftSubscriptionInvoice(c, sub, { start, end, reason: 'subscription_cycle' });
272
+ await c.write(SUB, id, { current_period_start: start, current_period_end: end, latest_invoice: invoiceId, ...(trialEnds ? { status: 'active' } : {}) }, 'customer.subscription.updated');
273
+ return true;
274
+ }
275
+ export const account = (ctx) => actingAccount(ctx);
276
+ export const payoutMissing = (ctx, id) => fail(ctx, `No such payout: '${id}'`, 404, 'resource_missing');
277
+ /** The acting account's Balance object (docs.stripe.com/api/balance/balance_object). */
278
+ export function balanceBody(ctx, acct = account(ctx)) {
279
+ const { available, pending, issuing } = balanceOf(ctx, acct);
280
+ const toArr = (m) => {
281
+ const out = [...m.entries()].map(([currency, amount]) => ({ amount, currency, source_types: { card: amount } }));
282
+ return out.length ? out : [{ amount: 0, currency: 'usd', source_types: { card: 0 } }];
283
+ };
284
+ // a platform's balance holds its connected accounts' reserve, "Funds held due to negative balances on connected accounts
285
+ // where account.controller.requirement_collection is `application`" (docs.stripe.com/api/balance/balance_object, whose
286
+ // example answers `[{"amount": 0, "currency": "usd"}]`); the twin models no such reserve, so it is zero in each currency
287
+ const reserved = acct ? {} : { connect_reserved: toArr(available).map((b) => ({ amount: 0, currency: b.currency })) };
288
+ return {
289
+ object: 'balance', available: toArr(available), pending: toArr(pending), ...reserved, livemode: false,
290
+ ...(issuing.size ? { issuing: { available: [...issuing.entries()].map(([currency, amount]) => ({ amount, currency, source_types: { card: amount } })) } } : {}),
291
+ };
292
+ }
293
+ /** Time's arrivals, written: each of the acting account's payouts whose arrival date has come moves pending → paid
294
+ * (the clock's move, as asOf reads it), written as `payout.paid`, the event Stripe sends for it. Answers their ids. */
295
+ export async function payDuePayouts(ctx, acct = account(ctx)) {
296
+ const now = Number(nowUnix(ctx.occurredAt));
297
+ const paid = [];
298
+ for (const p of newest(ctx, 'payout')) {
299
+ if ((acct ? kept(ctx, 'payout', p, '_account') !== acct : !!kept(ctx, 'payout', p, '_account')) || p.status !== 'pending' || Number(p.arrival_date) > now)
300
+ continue;
301
+ if (ctx.legal('payout', 'status', ctx.call.operation.id, 'pending', 'paid', String(p.id), 'time'))
302
+ continue;
303
+ await ctx.write('payout', String(p.id), { status: 'paid' }, 'payout.paid');
304
+ paid.push(String(p.id));
305
+ }
306
+ return paid;
307
+ }
308
+ /** Where a connected account's payout goes: "ID of the bank account or card the payout is sent to" (served spec,
309
+ * payout.destination), its default external account for the currency, "When multiple accounts are available for a given
310
+ * currency, Stripe uses the one set as `default_for_currency`" (docs.stripe.com/connect/payouts-bank-accounts), the
311
+ * newest such. Where the documentation stops and the twin decides: the platform's own bank account is not modelled, so
312
+ * its payouts name none. */
313
+ export function payoutBank(ctx, account, currency) {
314
+ const banks = newest(ctx, 'external_account').filter((e) => e.account === account && String(e.currency ?? currency) === currency);
315
+ return String((banks.find((e) => e.default_for_currency === true) ?? banks[0])?.id ?? '') || null;
316
+ }
317
+ export const payoutDefaults = (ctx) => ({
318
+ method: 'standard', type: 'bank_account', source_type: 'card', automatic: false,
319
+ reconciliation_status: 'not_applicable', arrival_date: Number(nowUnix(ctx.occurredAt)) + 2 * DAY, livemode: false, metadata: {},
320
+ });
321
+ /** A payout as the clock reads it: paid once its arrival date has come, the clock's move, asked of the
322
+ * machine as a write asks it. */
323
+ export function asOf(ctx, p) {
324
+ if (p.status !== 'pending' || Number(p.arrival_date) > Number(nowUnix(ctx.occurredAt)))
325
+ return p;
326
+ ctx.legal('payout', 'status', ctx.call.operation.id, 'pending', 'paid', String(p.id), 'time');
327
+ return { ...p, status: 'paid' };
328
+ }
329
+ /** A connected account's payout in a currency none of its bank accounts takes. */
330
+ export function noExternalAccount(ctx, currency) {
331
+ return fail(ctx, `Sorry, you don't have any external accounts in that currency (${currency}).`, 400);
332
+ }
333
+ /** Time's payouts, caught up to the World's clock: each account on an automatic schedule is paid out, on each of its
334
+ * scheduled days that has come, what became available by then. */
335
+ export async function advancePayouts(ctx) {
336
+ const now = Number(nowUnix(ctx.occurredAt));
337
+ const platform = ctx.get('account', PLATFORM_ACCOUNT_ID);
338
+ const accounts = [{ id: undefined, settings: platform?.settings }];
339
+ for (const a of ctx.rows('account')) {
340
+ if (a.id === PLATFORM_ACCOUNT_ID || a.payouts_enabled !== true)
341
+ continue;
342
+ if (!ctx.rows('external_account').some((e) => e.account === a.id))
343
+ continue;
344
+ accounts.push({ id: String(a.id), settings: a.settings });
345
+ }
346
+ for (const acct of accounts) {
347
+ const schedule = (accountSettings(acct.settings, acct.settings ?? undefined).payouts.schedule ?? {});
348
+ if (schedule.interval === 'manual')
349
+ continue;
350
+ const unpaid = unpaidEntries(ctx, acct.id);
351
+ const days = [...new Set(unpaid.map((t) => payoutDay(Number(t.available_on) || 0, schedule)))].filter((d) => d <= now).sort((x, y) => x - y);
352
+ const paid = new Set();
353
+ for (const day of days) {
354
+ const due = unpaid.filter((t) => !paid.has(t.id) && (Number(t.available_on) || 0) <= day);
355
+ const byCurrency = new Map();
356
+ for (const t of due)
357
+ byCurrency.set(String(t.currency ?? 'usd'), [...(byCurrency.get(String(t.currency ?? 'usd')) ?? []), t]);
358
+ for (const [currency, entries] of byCurrency) {
359
+ const amount = entries.reduce((n, t) => n + (Number(t.net) || 0), 0);
360
+ // nothing to pay yet: what came due is carried to the next payout
361
+ if (amount <= 0)
362
+ continue;
363
+ // made on its day, as Stripe makes it, whenever the request that catches it up comes
364
+ const c = await at_(ctx)(day);
365
+ const id = c.mint('payout');
366
+ const bt = await settleAutomaticPayout(c, id, amount, currency, acct.id, day, entries);
367
+ await created(c, 'payout', { id, amount, currency }, {
368
+ status: 'pending', ...payoutDefaults(c), arrival_date: day + 2 * DAY, automatic: true, balance_transaction: bt, destination: acct.id ? payoutBank(c, String(acct.id), currency) : null,
369
+ description: 'STRIPE PAYOUT', ...(acct.id ? { _account: acct.id } : {}),
370
+ });
371
+ for (const t of entries)
372
+ paid.add(t.id);
373
+ }
374
+ }
375
+ }
376
+ }
377
+ /** What Stripe does after a card succeeds: from a raw number, a test name (pm_card_* or tok_*), or a stored
378
+ * PaymentMethod that recorded it when it was made. */
379
+ export function afterSuccessOf(ctx, ref) {
380
+ // a card given as a token (card[token], payment_method_data[card][token]) is the card the token names
381
+ if (ref && typeof ref === 'object' && typeof ref.token === 'string')
382
+ return afterSuccessOf(ctx, ref.token);
383
+ if (ref && typeof ref === 'object')
384
+ return BY_NUMBER[String(ref.number ?? '').replace(/\D/g, '')];
385
+ if (typeof ref !== 'string' || !ref)
386
+ return undefined;
387
+ const stored = ctx.row('payment_method', ref);
388
+ if (stored)
389
+ return stored._afterSuccess ?? undefined;
390
+ return AFTER_SUCCESS_CARDS[ref.replace(/^tok_/, 'pm_card_')]?.outcome ?? BY_NUMBER[ref.replace(/\D/g, '')];
391
+ }
392
+ /** Stripe's acts on a charge that just succeeded: a dispute or a review its card brings, and the platform's fee. */
393
+ export async function afterCharge(ctx, chargeId, charge, ref) {
394
+ const outcome = afterSuccessOf(ctx, ref);
395
+ const pi = charge.payment_intent ? { payment_intent: charge.payment_intent } : { payment_intent: null };
396
+ const now = Number(nowUnix(ctx.occurredAt));
397
+ if (outcome === 'dispute' || outcome === 'dispute_not_received' || outcome === 'inquiry') {
398
+ const dispute = await created(ctx, 'dispute', { charge: chargeId, ...pi, amount: charge.amount, currency: charge.currency }, {
399
+ reason: outcome === 'dispute_not_received' ? 'product_not_received' : 'fraudulent', status: outcome === 'inquiry' ? 'warning_needs_response' : 'needs_response', is_charge_refundable: false,
400
+ evidence: disputeEvidence(), evidence_details: { due_by: now + 7 * DAY, has_evidence: false, past_due: false, submission_count: 0, enhanced_eligibility: {} },
401
+ balance_transactions: [], livemode: false, metadata: {}, enhanced_eligibility_types: [],
402
+ });
403
+ // an inquiry moves no money; a dispute takes the amount and its fee until it is decided
404
+ if (outcome !== 'inquiry') {
405
+ const bt = await settleDispute(ctx, String(dispute.id), charge.amount, charge.currency);
406
+ await ctx.write('dispute', String(dispute.id), { balance_transactions: [ctx.get('balance_transaction', bt) ?? bt] }, 'dispute.funds_withdrawn');
407
+ }
408
+ await ctx.write('charge', chargeId, { disputed: true }, 'charge.dispute.created');
409
+ }
410
+ if (outcome === 'review')
411
+ await openReview(ctx, chargeId, pi);
412
+ const feeAmount = Math.trunc(Number(charge.application_fee_amount) || 0);
413
+ const destination = typeof charge.destination === 'string' && charge.destination ? charge.destination : undefined;
414
+ const direct = actingAccount(ctx);
415
+ const account = destination ?? direct;
416
+ // a destination charge transfers what it collected, less the platform's fee, available when the charge's funds are
417
+ if (destination) {
418
+ const amount = charge.amount - feeAmount;
419
+ const chargeBt = ctx.get('charge', chargeId)?.balance_transaction;
420
+ const availableOn = Number(ctx.get('balance_transaction', String(chargeBt))?.available_on ?? nowUnix(ctx.occurredAt));
421
+ const tr = await created(ctx, 'transfer', { amount, currency: charge.currency, destination }, {
422
+ amount_reversed: 0, balance_transaction: null, livemode: false, metadata: {}, reversed: false, source_type: 'card', source_transaction: chargeId,
423
+ reversals: { object: 'list', data: [], has_more: false, total_count: 0, url: '' },
424
+ });
425
+ const bt = await settleTransfer(ctx, String(tr.id), amount, charge.currency, destination, availableOn);
426
+ await ctx.write('transfer', String(tr.id), { balance_transaction: bt, destination_payment: `py_${String(tr.id).replace(/^tr_/, '')}`, reversals: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/transfers/${String(tr.id)}/reversals` } }, 'transfer.created');
427
+ await ctx.write('charge', chargeId, { transfer: tr.id }, 'charge.updated');
428
+ }
429
+ if (feeAmount > 0 && account) {
430
+ const fee = await created(ctx, 'application_fee', { account, amount: feeAmount, charge: chargeId, currency: charge.currency }, {
431
+ amount_refunded: 0, application: 'ca_twin', balance_transaction: null, originating_transaction: null, refunded: false, livemode: false,
432
+ });
433
+ // a direct charge's fee moves from the connected account to the platform (a destination charge kept it back)
434
+ const bt = !destination && direct ? await settleApplicationFee(ctx, String(fee.id), feeAmount, charge.currency, direct) : null;
435
+ await ctx.write('application_fee', String(fee.id), { balance_transaction: bt, refunds: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/application_fees/${String(fee.id)}/refunds` } }, 'application_fee.created');
436
+ await ctx.write('charge', chargeId, { application_fee: fee.id, application_fee_amount: feeAmount }, 'charge.updated');
437
+ }
438
+ }
439
+ /** Radar places an elevated-risk card's charge in review (docs.stripe.com/radar/reviews). */
440
+ export async function openReview(ctx, chargeId, pi) {
441
+ const review = await created(ctx, 'review', { charge: chargeId, ...pi }, {
442
+ open: true, opened_reason: 'rule', reason: 'rule', closed_reason: null, billing_zip: null, ip_address: null, ip_address_location: null, session: null, livemode: false,
443
+ });
444
+ const existing = ctx.get('charge', chargeId)?.outcome ?? {};
445
+ await ctx.write('charge', chargeId, {
446
+ review: review.id,
447
+ outcome: { ...existing, type: 'manual_review', risk_level: 'elevated', risk_score: 67, seller_message: 'Stripe evaluated this payment as having elevated risk, and placed it in review.' },
448
+ }, 'review.opened');
449
+ }
450
+ /** The mandate a verified bank account carries for a payment (single use) or a setup (multi use). */
451
+ export async function mintMandate(ctx, paymentMethod, use, amount, currency) {
452
+ const now = Number(nowUnix(ctx.occurredAt));
453
+ const mandate = await created(ctx, 'mandate', { payment_method: typeof paymentMethod === 'string' ? paymentMethod : null }, {
454
+ status: 'active', type: use, livemode: false,
455
+ customer_acceptance: { type: 'online', accepted_at: now, online: { ip_address: '127.0.0.1', user_agent: 'twin' } },
456
+ payment_method_details: { type: 'us_bank_account', us_bank_account: { collection_method: 'paper' } },
457
+ ...(use === 'single_use' ? { single_use: { amount: amount ?? 0, currency: currency ?? 'usd' } } : { multi_use: {} }),
458
+ }, { timeField: '_created' });
459
+ return String(mandate.id);
460
+ }
461
+ export const customerMissing = (ctx, id) => fail(ctx, `No such customer: '${id}'`, 404, 'resource_missing');
462
+ /** The discount a customer's `coupon` makes, a parameter of API versions before the served one. */
463
+ export function customerDiscount(ctx, couponId) {
464
+ const coupon = ctx.get('coupon', couponId);
465
+ return coupon ? buildDiscount(coupon, '', Number(nowUnix(ctx.occurredAt))) : fail(ctx, `No such coupon: '${couponId}'`, 400, 'resource_missing');
466
+ }
467
+ /** A tax ID for its owner: the account itself, or one of its customers. */
468
+ export async function makeTaxId(ctx, customer) {
469
+ const given = ctx.params;
470
+ const { owner: _owner, ...params } = given;
471
+ if (params.type === undefined || params.type === '')
472
+ return fail(ctx, 'Missing required param: type.', 400, 'parameter_missing');
473
+ if (params.value === undefined || params.value === '')
474
+ return fail(ctx, 'Missing required param: value.', 400, 'parameter_missing');
475
+ const vatCountry = params.type === 'eu_vat' ? euVatCountry(String(params.value)) : undefined;
476
+ if (params.type === 'eu_vat' && !vatCountry)
477
+ return ctx.refuse({ status: 400, code: 'tax_id_invalid', param: 'value', message: `Invalid value for eu_vat.` });
478
+ return ctx.reply(await created(ctx, 'tax_id', { ...params, ...(customer ? { customer } : {}) }, {
479
+ livemode: false,
480
+ country: vatCountry ?? (typeof params.country === 'string' ? params.country : null),
481
+ owner: customer ? { type: 'customer', customer } : { type: 'self' },
482
+ verification: { status: AUTO_VALIDATED.has(String(params.type)) ? 'pending' : 'unavailable', verified_address: null, verified_name: null },
483
+ }));
484
+ }
485
+ /** The owner a top-level tax-ID call names: the account itself unless `owner[type]=customer` names a customer
486
+ * (docs.stripe.com/api/tax_ids/list: owner defaults to self). */
487
+ export function ownerOf(ctx) {
488
+ const owner = (ctx.params.owner ?? {});
489
+ const type = String(owner.type ?? 'self');
490
+ if (type === 'self')
491
+ return {};
492
+ if (type !== 'customer')
493
+ return fail(ctx, `Invalid owner[type]: must be one of self or customer.`, 400, 'parameter_invalid_string_enum');
494
+ const customer = typeof owner.customer === 'string' ? owner.customer : '';
495
+ if (!customer)
496
+ return fail(ctx, 'Missing required param: owner[customer].', 400, 'parameter_missing');
497
+ if (!ctx.get('customer', customer))
498
+ return customerMissing(ctx, customer);
499
+ return { customer };
500
+ }
501
+ // ── the cash balance: per-currency sums of the funding ledger, and the reconciliation setting ──
502
+ export function cashBalance(ctx, customer) {
503
+ const byCurrency = {};
504
+ for (const t of ctx.rows('customer_cash_balance_transaction')) {
505
+ if (t.customer !== customer)
506
+ continue;
507
+ const cur = String(t.currency ?? 'usd');
508
+ byCurrency[cur] = (byCurrency[cur] ?? 0) + (Number(t.net_amount) || 0);
509
+ }
510
+ const cust = ctx.row('customer', customer);
511
+ const mode = typeof cust?._cash_balance_mode === 'string' ? cust._cash_balance_mode : 'automatic';
512
+ return {
513
+ object: 'cash_balance', customer,
514
+ available: Object.keys(byCurrency).length ? byCurrency : null,
515
+ settings: { reconciliation_mode: mode, using_merchant_default: mode === 'merchant_default' },
516
+ livemode: false,
517
+ };
518
+ }
519
+ /** Automatic reconciliation (docs.stripe.com/payments/customer-balance/reconciliation): funds that arrive pay the
520
+ * customer's open invoices that take bank transfers (payment_method_types customer_balance), the one whose number
521
+ * the transfer's reference names first, then the oldest. Where the documentation stops and the twin decides: an
522
+ * invoice is paid only when the balance covers all of what remains; a smaller balance waits for more funds. */
523
+ export async function reconcile(ctx, customer, currency, reference) {
524
+ const takesTransfers = (inv) => inv.payment_settings?.payment_method_types?.includes('customer_balance') === true;
525
+ const open = ctx.rows('invoice').filter((inv) => inv.customer === customer && inv.status === 'open' && String(inv.currency ?? 'usd') === currency && takesTransfers(inv))
526
+ .sort((a, b) => (reference && a.number === reference ? -1 : reference && b.number === reference ? 1 : Number(a.created) - Number(b.created)));
527
+ for (const inv of open) {
528
+ const available = Number((cashBalance(ctx, customer).available ?? {})[currency] ?? 0);
529
+ const amount = Number(inv.amount_remaining ?? inv.amount_due) || 0;
530
+ if (amount <= 0 || available < amount)
531
+ continue;
532
+ const id = String(inv.id);
533
+ if (ctx.legal('invoice', 'status', 'PostTestHelpersCustomersCustomerFundCashBalance', inv.status, 'paid', id))
534
+ continue;
535
+ // the payment the balance makes: the invoice's own PaymentIntent (every invoice finalized with an amount due has one,
536
+ // semantics/invoices.ts), paid from the cash balance, its charge, and the ledger's entry
537
+ const piId = String(inv.payment_intent);
538
+ await created(ctx, 'customer_cash_balance_transaction', { customer }, { currency, type: 'applied_to_payment', net_amount: -amount, ending_balance: available - amount, applied_to_payment: { payment_intent: piId }, livemode: false });
539
+ const charge = await mintCharge(ctx, piId, { currency, customer, invoice: id }, amount);
540
+ await ctx.write('payment_intent', piId, { status: 'succeeded', amount_received: amount, latest_charge: charge }, 'payment_intent.succeeded');
541
+ await ctx.write('invoice', id, {
542
+ status: 'paid', paid: true, amount_paid: amount, amount_remaining: 0, payment_intent: piId, charge,
543
+ status_transitions: { ...(inv.status_transitions ?? {}), paid_at: nowUnix(ctx.occurredAt) },
544
+ }, 'invoice.paid');
545
+ }
546
+ }
547
+ /** The customer's source the path names, or undefined. */
548
+ export const customerSource = (ctx) => {
549
+ const s = ctx.get('source', at(ctx, 'id'));
550
+ return s && s.customer === at(ctx, 'customer') ? s : undefined;
551
+ };
552
+ export const sourceMissing = (ctx) => fail(ctx, `No such source: '${at(ctx, 'id')}'`, 404, 'resource_missing');
553
+ /** The intent the path names, and the machine's refusal when this operation may not move it. */
554
+ export function load(ctx, operationId) {
555
+ const id = at(ctx, 'intent');
556
+ const si = ctx.get(SI, id);
557
+ if (!si)
558
+ return { answer: fail(ctx, `No such setup_intent: '${id}'`, 404, 'resource_missing') };
559
+ const refused = ctx.legal(SI, 'status', operationId, si.status, undefined, id);
560
+ return refused ? { answer: ctx.refuse(refused) } : { si };
561
+ }
562
+ /** The request event asked of the endpoint, signed with its secret and rendered in the event's API version, and its
563
+ * answer read as the decision. */
564
+ export async function askForAuthorization(ctx, url, event, secret) {
565
+ const payload = JSON.stringify(render(event, typeof event.api_version === 'string' ? event.api_version : undefined));
566
+ const signature = stripeSignature(payload, secret, Math.floor(Date.parse(ctx.occurredAt) / 1000));
567
+ const answer = await ctx.ask(url, { method: 'POST', headers: { 'content-type': 'application/json', 'stripe-signature': signature }, body: payload }, STRIPE_REALTIME_AUTH_TIMEOUT_MS);
568
+ if (answer.missed === 'timeout')
569
+ return { kind: 'timeout', message: 'Webhook endpoint did not respond within the real-time authorization window.' };
570
+ if (answer.missed === 'unreachable')
571
+ return { kind: 'timeout', message: 'Webhook endpoint was unreachable within the real-time authorization window.' };
572
+ return readDecision(answer);
573
+ }
574
+ /** Stripe's refusal of a call a publishable key may not make, or undefined when the key may make it. */
575
+ export function publishableKeyRefused(ctx) {
576
+ // the key however it is sent, Bearer or Basic's user (requestKey): a publishable key is public, and sent as Basic it
577
+ // is still one
578
+ if (!requestKey(ctx.call.request).startsWith('pk_'))
579
+ return undefined;
580
+ const op = ctx.call.operation.id;
581
+ if (PUBLISHABLE_CREATES.has(op))
582
+ return undefined;
583
+ const scoped = BY_CLIENT_SECRET[op];
584
+ if (!scoped)
585
+ return ctx.refuse({ status: 403, code: 'secret_key_required', message: 'The API key provided is a publishable key, but a secret key is required.' });
586
+ const secret = typeof ctx.params.client_secret === 'string' ? ctx.params.client_secret : '';
587
+ if (!secret)
588
+ return ctx.refuse({ status: 400, code: 'parameter_missing', param: 'client_secret', message: 'Missing required param: client_secret.' });
589
+ const id = ctx.call.params[scoped.param] ?? '';
590
+ return ctx.get(scoped.type, id)?.client_secret === secret ? undefined : ctx.refuse({ status: 404, code: 'resource_missing', param: scoped.param, message: `No such ${scoped.type}: '${id}'` });
591
+ }
592
+ // ── an account whose OAuth connection was revoked ──
593
+ // "After revocation, the account can't be accessed by your platform in the Dashboard or through the API"
594
+ // (docs.stripe.com/connect/oauth-reference, deauthorize; so too a connection revoked by a reused code). A call made as it
595
+ // (the Stripe-Account header) or naming it (/v1/accounts/{account}…) is refused with the error Stripe documents for a
596
+ // Stripe-Account the key cannot use: "account_invalid | The account ID provided as a value for the Stripe-Account header
597
+ // is invalid" (docs.stripe.com/error-codes), as 403. Where the documentation stops and the twin decides: the message's
598
+ // wording; an account the platform created itself, or never connected by OAuth, is not affected.
599
+ export function revokedAccountRefused(ctx) {
600
+ const request = ctx.call.request;
601
+ const named = [request.headers.get('stripe-account') ?? undefined, ctx.call.operation.path.startsWith('/v1/accounts/{account}') ? ctx.call.params.account : undefined].filter((a) => !!a);
602
+ const revoked = named.find((a) => connectionRevoked(ctx, a));
603
+ if (!revoked)
604
+ return undefined;
605
+ return ctx.refuse({ status: 403, code: 'account_invalid', message: `The provided key '${redactKey(requestKey(request))}' does not have access to account '${revoked}' (or that account does not exist). Application access may have been revoked.` });
606
+ }
607
+ // A connected account's OAuth keys (../screens/connect-oauth.tsx): the token endpoint's access_token is "Use the
608
+ // Stripe-Account header with your platform's secret key (that can make requests on behalf of this Stripe account)" and
609
+ // its stripe_publishable_key the same with the publishable key (docs.stripe.com/connect/oauth-reference): a request made
610
+ // with one acts as that account, as a request with the Stripe-Account header does. A key no live connection holds (its
611
+ // connection revoked, or replaced by a refresh: "Any existing access token with the same scope and mode ... is
612
+ // revoked") is Stripe's unknown key, 401 "Invalid API Key provided" (docs.stripe.com/api/authentication); one sent with
613
+ // a Stripe-Account header naming another account cannot act as it (account_invalid). Where the documentation stops and
614
+ // the twin decides: the messages' wording; a read_only key is not held to reads (manifest todo
615
+ // stripe.connect.oauth_scope_enforcement).
616
+ /** The platform's own keys, as the dashboard's API keys page makes them (the `appCredentials` door): a secret key and a
617
+ * publishable key, held by their SHA-256. A request with no key is refused, as is a platform key Stripe never issued
618
+ * ("Invalid API Key provided"); a connected account's OAuth key is checked where it is read (actingAsOAuthKey). */
619
+ // source: https://docs.stripe.com/api/authentication "API requests without authentication also fail."
620
+ export function platformKeyRefused(ctx, request) {
621
+ const key = requestKey(request);
622
+ if (!key)
623
+ return ctx.refuse({ status: 401, message: "You did not provide an API key. You need to provide your API key in the Authorization header, using Bearer auth (e.g. 'Authorization: Bearer YOUR_SECRET_KEY'). See https://stripe.com/docs/api#authentication for details, or we can help at https://support.stripe.com/." });
624
+ if (/^(sk|pk)_test_oauth_/.test(key))
625
+ return undefined;
626
+ const held = ctx.rowsRaw('_platform_key').some((k) => k._sha256 === ctx.crypto.sha256(key) && k.deleted !== true);
627
+ return held ? undefined : ctx.refuse({ status: 401, message: `Invalid API Key provided: ${redactKey(key)}` });
628
+ }
629
+ export async function actingAsOAuthKey(ctx, request) {
630
+ const key = requestKey(request);
631
+ if (!/^(sk|pk)_test_oauth_/.test(key))
632
+ return request;
633
+ const held = oauthKeyAccount(ctx, key);
634
+ if (!held)
635
+ return ctx.refuse({ status: 401, message: `Invalid API Key provided: ${redactKey(key)}` });
636
+ const named = request.headers.get('stripe-account');
637
+ if (named && named !== held.account)
638
+ return ctx.refuse({ status: 403, code: 'account_invalid', message: `The provided key '${redactKey(key)}' does not have access to account '${named}' (or that account does not exist). Application access may have been revoked.` });
639
+ const headers = new Headers(request.headers);
640
+ headers.set('stripe-account', held.account);
641
+ const bodied = request.method !== 'GET' && request.method !== 'HEAD';
642
+ return new Request(request.url, { method: request.method, headers, ...(bodied ? { body: await request.arrayBuffer() } : {}) });
643
+ }
644
+ // ── metadata ──
645
+ // "This parameter uses a merge mechanism, which allows you to add new key-value pairs to an object in an update call
646
+ // without affecting any existing metadata"; "Pass in the key with an empty string as the value to remove the key from the
647
+ // metadata"; "Pass an empty string as the value for the metadata attribute to delete all of the keys simultaneously"
648
+ // (docs.stripe.com/metadata), and "Individual keys can be unset by posting an empty value to them" holds on a create
649
+ // too: an object never holds a key set to "" (stripe-node encodes a `null` value as `metadata[key]=`). The derived core
650
+ // merges a nested hash on an update (dropping keys set to ""), so for it only a clear-all is rewritten; where a handler
651
+ // stores the request's metadata whole, the request carries the merged hash, its removed keys as "". The rewritten
652
+ // request is JSON.
653
+ export function withMetadata(ctx, request) {
654
+ if (request.method !== 'POST' || (request.headers.get('content-type') ?? '').includes('multipart'))
655
+ return request;
656
+ if (!('metadata' in ctx.params))
657
+ return request;
658
+ const op = ctx.call.operation;
659
+ const given = ctx.params.metadata;
660
+ let metadata;
661
+ if (op.class === 'create') {
662
+ if (given === '')
663
+ metadata = {};
664
+ else if (given && typeof given === 'object' && !Array.isArray(given) && Object.values(given).includes(''))
665
+ metadata = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== ''));
666
+ else
667
+ return request;
668
+ }
669
+ else if (op.class === 'update' && op.resource) {
670
+ // a plan is kept as the recurring price it is (../semantics/plans.ts)
671
+ const resource = op.resource === 'plan' ? 'price' : op.resource;
672
+ const id = Object.values(ctx.call.params).at(-1) ?? '';
673
+ const held = ctx.get(resource, id)?.metadata;
674
+ const prior = held && typeof held === 'object' ? held : {};
675
+ // the merged hash, a key removed carried as "" so a merging update (the derived core's) drops it too; an object
676
+ // never answers a key set to "" (../engine/version.ts render)
677
+ if (given === '')
678
+ metadata = Object.fromEntries(Object.keys(prior).map((k) => [k, '']));
679
+ else if (given && typeof given === 'object' && !Array.isArray(given))
680
+ metadata = { ...prior, ...given };
681
+ else
682
+ return request;
683
+ }
684
+ else
685
+ return request;
686
+ const headers = new Headers(request.headers);
687
+ headers.set('content-type', 'application/json');
688
+ return new Request(request.url, { method: request.method, headers, body: JSON.stringify({ ...ctx.params, metadata }) });
689
+ }
690
+ /** `disabled` switches delivery off or back on (docs.stripe.com/api/webhook_endpoints/update#update_webhook_endpoint-disabled). */
691
+ export async function switchDelivery(ctx, id, we, fields, off) {
692
+ const status = off ? 'disabled' : 'enabled';
693
+ const refused = ctx.legal(WE, 'status', 'PostWebhookEndpointsWebhookEndpoint', we.status, status, id);
694
+ if (refused)
695
+ return ctx.refuse(refused);
696
+ return ctx.reply(await ctx.write(WE, id, { ...fields, status }, 'webhook_endpoint.update'));
697
+ }
698
+ /** Whether a stored event is the acting account's (no `account`: the platform's). */
699
+ export const ownEvent = (ctx, e) => (typeof e.account === 'string' ? e.account : undefined) === actingAccount(ctx);
700
+ export const faMissing = (ctx, id, status = 404) => fail(ctx, `No such financial account: '${id}'`, status, 'resource_missing');
701
+ export const metadataOf = (ctx) => (ctx.params.metadata && typeof ctx.params.metadata === 'object' ? ctx.params.metadata : {});
702
+ /** The account's bank address once financial_addresses.aba is active: "The FinancialAccount gains a FinancialAddress
703
+ * when the `financial_addresses.aba` feature is active", and "A FinancialAddress is not added until the
704
+ * financial_addresses.aba feature has been activated" (docs.stripe.com/treasury/account-management/financial-accounts),
705
+ * whose test example answers "bank_name": "Stripe Test Bank", "routing_number": "000000001" and the networks "ach",
706
+ * "us_domestic_wire", "rtp"; "The full account_number is only returned if the request expands it" (the same page).
707
+ * Where the documentation stops and the twin decides: the account number is ten digits drawn from the account's id,
708
+ * answered by its last four (the full number is null: expanding it is not modelled), and the holder's name is the
709
+ * platform account's business name, its dashboard name or, with neither, its id. */
710
+ export function abaAddress(ctx, faId) {
711
+ let n = 0;
712
+ for (const ch of faId)
713
+ n = (n * 31 + ch.charCodeAt(0)) % 10_000_000_000;
714
+ const number = String(n).padStart(10, '0');
715
+ const platform = ctx.get('account', PLATFORM_ACCOUNT_ID);
716
+ const name = (platform?.business_profile?.name ?? platform?.settings?.dashboard?.display_name ?? PLATFORM_ACCOUNT_ID);
717
+ return { type: 'aba', supported_networks: ['ach', 'us_domestic_wire', 'rtp'], aba: { account_holder_name: name, account_number: null, account_number_last4: number.slice(-4), bank_name: 'Stripe Test Bank', routing_number: '000000001' } };
718
+ }
719
+ export const addressesOf = (ctx, faId, active) => (active.includes('financial_addresses.aba') ? [abaAddress(ctx, faId)] : []);
720
+ /** Post a flow's double entry: a ledger transaction with its net balance impact, and its one entry. */
721
+ export async function post(ctx, o) {
722
+ const now = nowUnix(ctx.occurredAt);
723
+ const txnId = ctx.mint(TX);
724
+ await ctx.write(TX, txnId, {
725
+ object: 'treasury.transaction', created: now, livemode: false,
726
+ amount: o.amount, currency: o.currency, financial_account: o.financial_account,
727
+ flow: o.flow, flow_type: o.flow_type, flow_details: null,
728
+ status: o.status, balance_impact: o.balance_impact, description: o.description,
729
+ entries: { object: 'list', has_more: false, url: `/v1/treasury/transaction_entries?transaction=${txnId}`, data: [] },
730
+ status_transitions: { posted_at: o.status === 'posted' ? now : null, void_at: null },
731
+ }, 'treasury_transaction.created');
732
+ await ctx.write(ENTRY, ctx.mint(ENTRY), {
733
+ object: 'treasury.transaction_entry', created: now, livemode: false,
734
+ amount: o.amount, currency: o.currency, financial_account: o.financial_account,
735
+ flow: o.flow, flow_type: o.flow_type,
736
+ flow_details: o.flowDetailKey ? { type: o.flowDetailKey, [o.flowDetailKey]: o.flow } : null,
737
+ transaction: txnId, effective_at: now,
738
+ balance_impact: o.balance_impact,
739
+ // what the entry records: its flow's kind where Stripe names one (docs.stripe.com/api/treasury/transaction_entries/object);
740
+ // a vendor `type`, kept apart from the kernel's row type
741
+ type: ENTRY_TYPES.has(String(o.flow_type)) ? o.flow_type : 'other',
742
+ }, 'treasury_transaction_entry.created');
743
+ // the account's balance moves by the entry's impact: cash "Funds the user can spend right now", inbound_pending
744
+ // "Funds not spendable yet", outbound_pending "held for pending outbound flows" (the served spec's balance)
745
+ const account = ctx.get(FA, o.financial_account);
746
+ if (account) {
747
+ const bal = account.balance ?? { cash: {}, inbound_pending: {}, outbound_pending: {} };
748
+ const moved = Object.fromEntries(['cash', 'inbound_pending', 'outbound_pending'].map((k) => [k, { ...(bal[k] ?? {}), [o.currency]: ((bal[k] ?? {})[o.currency] ?? 0) + (Number(o.balance_impact[k]) || 0) }]));
749
+ await ctx.write(FA, o.financial_account, { balance: moved }, 'financial_account.balance_updated');
750
+ }
751
+ return txnId;
752
+ }
753
+ /** The request's funding account, or the refusal. */
754
+ export function fundingAccount(ctx) {
755
+ const bad = validateMoney(ctx.params);
756
+ if (bad)
757
+ return send(ctx, bad);
758
+ const fa = typeof ctx.params.financial_account === 'string' ? ctx.params.financial_account : '';
759
+ if (!fa)
760
+ return fail(ctx, 'Missing required param: financial_account.', 400, 'parameter_missing');
761
+ return ctx.get(FA, fa) ? fa : faMissing(ctx, fa, 400);
762
+ }
763
+ /** Where an outbound flow sends its money, as the served spec's payment method details give it: the bank account's
764
+ * holder type, last four digits and routing number from what the request sent (a saved method's, or the test bank's
765
+ * when none is given), and its billing details (required): the name and email sent, the address lines null where not
766
+ * given. Where the documentation stops and the twin decides: a saved method's details are the test bank's. */
767
+ export function destinationDetails(ctx, dest) {
768
+ const data = ctx.params.destination_payment_method_data && typeof ctx.params.destination_payment_method_data === 'object' ? ctx.params.destination_payment_method_data : {};
769
+ const bank = data.us_bank_account && typeof data.us_bank_account === 'object' ? data.us_bank_account : {};
770
+ const billing = data.billing_details && typeof data.billing_details === 'object' ? data.billing_details : {};
771
+ const address = billing.address && typeof billing.address === 'object' ? billing.address : {};
772
+ const number = typeof bank.account_number === 'string' ? bank.account_number.replace(/\D/g, '') : '';
773
+ const billing_details = { name: billing.name ?? null, email: billing.email ?? null, address: Object.fromEntries(['city', 'country', 'line1', 'line2', 'postal_code', 'state'].map((k) => [k, address[k] ?? null])) };
774
+ if (dest && data.type === undefined && dest.startsWith('fa_'))
775
+ return { type: 'financial_account', financial_account: { id: dest, network: 'stripe' }, billing_details };
776
+ return {
777
+ type: 'us_bank_account', billing_details,
778
+ us_bank_account: { last4: number ? number.slice(-4) : '6789', network: 'ach', routing_number: typeof bank.routing_number === 'string' ? bank.routing_number : '110000000', account_holder_type: typeof bank.account_holder_type === 'string' ? bank.account_holder_type : 'individual', account_type: typeof bank.account_type === 'string' ? bank.account_type : 'checking', bank_name: 'STRIPE TEST BANK', mandate: null },
779
+ };
780
+ }
781
+ /** An outbound payment that names no destination: "You must provide either destination_payment_method or
782
+ * destination_payment_method_data" (the served spec's outbound payment create). */
783
+ export function noDestination(ctx) {
784
+ return fail(ctx, 'You must provide either `destination_payment_method` or `destination_payment_method_data`.', 400, 'parameter_missing');
785
+ }
786
+ /** The bank account an inbound transfer may pull from: "You must first set up the account-attached payment method for
787
+ * inbound flows and verify the bank account using a SetupIntent", and invalid ones ("of unsupported types, containing
788
+ * an unverified bank account, or not set up for inbound flows") "throw the same errors as in live mode"
789
+ * (docs.stripe.com/treasury/moving-money/financial-accounts/into/inbound-transfers). A payment method is set up for
790
+ * inbound flows by a SetupIntent whose flow_directions holds `inbound`, and verified when that SetupIntent succeeded
791
+ * ("The bank account has been instantly verified or verification isn't necessary", its `succeeded` status,
792
+ * docs.stripe.com/treasury/connect/legacy/v1/moving-money/working-with-bankaccount-objects).
793
+ * Where the documentation stops and the twin decides: the page quotes no error, so the twin answers the documented code
794
+ * for the class, payment_method_unexpected_state ("The provided payment method's state was incompatible with the
795
+ * operation you were trying to perform", docs.stripe.com/error-codes), in its own words; an existing verified
796
+ * BankAccount (`ba_`), which the page also allows, is not checked. Answers the payment method, or the refusal. */
797
+ export function inboundOrigin(ctx, origin) {
798
+ if (origin.startsWith('ba_'))
799
+ return undefined;
800
+ const pm = ctx.get('payment_method', origin);
801
+ if (!pm)
802
+ return fail(ctx, `No such PaymentMethod: '${origin}'`, 400, 'resource_missing');
803
+ if (pm.type !== 'us_bank_account')
804
+ return fail(ctx, `The PaymentMethod '${origin}' is of type ${String(pm.type)}; an InboundTransfer pulls from a us_bank_account.`, 400, 'payment_method_unexpected_state');
805
+ const setUp = ctx.rows('setup_intent').some((si) => si.payment_method === origin && si.status === 'succeeded' && Array.isArray(si.flow_directions) && si.flow_directions.map(String).includes('inbound'));
806
+ if (!setUp)
807
+ return fail(ctx, `The PaymentMethod '${origin}' has not been set up for inbound flows and verified with a SetupIntent.`, 400, 'payment_method_unexpected_state');
808
+ return pm;
809
+ }
810
+ // test mode's stand-ins for money another party sends to the account or pulls from it
811
+ // (docs.stripe.com/treasury/moving-money/receiving-funds, docs.stripe.com/api/treasury/received_credits/test_mode_create)
812
+ export function received(kind) {
813
+ return async (ctx) => {
814
+ const fa = fundingAccount(ctx);
815
+ if (fa instanceof Response)
816
+ return fa;
817
+ const network = typeof ctx.params.network === 'string' ? ctx.params.network : '';
818
+ if (!['ach', 'us_domestic_wire', 'stripe'].includes(network))
819
+ return fail(ctx, 'Missing required param: network.', 400, 'parameter_missing');
820
+ const amount = ctx.params.amount;
821
+ const currency = String(ctx.params.currency);
822
+ const id = ctx.mint(`treasury.${kind}`);
823
+ const signed = kind === 'received_credit' ? amount : -amount;
824
+ const txn = await post(ctx, { financial_account: fa, amount: signed, currency, flow: id, flow_type: kind, status: 'posted', balance_impact: { cash: signed, inbound_pending: 0, outbound_pending: 0 }, description: kind === 'received_credit' ? 'ReceivedCredit' : 'ReceivedDebit', flowDetailKey: kind });
825
+ return ctx.reply(await created(ctx, `treasury.${kind}`, { id }, {
826
+ amount, currency, financial_account: fa, network, livemode: false, status: 'succeeded', failure_code: null,
827
+ description: typeof ctx.params.description === 'string' ? ctx.params.description : kind === 'received_credit' ? 'Received credit' : 'Received debit',
828
+ hosted_regulatory_receipt_url: null, transaction: txn, reversal_details: null,
829
+ initiating_payment_method_details: { type: 'us_bank_account', billing_details: { address: {}, email: null, name: null }, us_bank_account: { bank_name: 'STRIPE TEST BANK', last4: '6789', routing_number: '110000000' } },
830
+ linked_flows: kind === 'received_credit' ? { credit_reversal: null, issuing_authorization: null, issuing_transaction: null, source_flow: null, source_flow_type: null } : { debit_reversal: null, inbound_transfer: null, issuing_authorization: null, issuing_transaction: null, payout: null },
831
+ }));
832
+ };
833
+ }
834
+ /** Cancel a flow still processing; an outbound one must also still be cancelable. */
835
+ export function cancel(resource, param, name, operationId, op, checkCancelable) {
836
+ return async (ctx) => {
837
+ const id = at(ctx, param);
838
+ const flow = ctx.get(resource, id);
839
+ if (!flow)
840
+ return fail(ctx, `No such ${name}: '${id}'`, 404, 'resource_missing');
841
+ // a processing flow that is no longer cancelable is refused as one no longer processing
842
+ const refused = ctx.legal(resource, 'status', operationId, checkCancelable && flow.cancelable !== true ? 'not_cancelable' : flow.status, undefined, id);
843
+ if (refused)
844
+ return ctx.refuse(refused);
845
+ // a canceled flow's open transaction is voided and what it held returns to the account (the reverse of its
846
+ // balance impact). Where the documentation stops and the twin decides: a flow already posted is only marked canceled
847
+ const txn = typeof flow.transaction === 'string' ? ctx.get(TX, flow.transaction) : undefined;
848
+ if (txn && txn.status === 'open') {
849
+ await ctx.write(TX, String(txn.id), { status: 'void', status_transitions: { ...txn.status_transitions, void_at: nowUnix(ctx.occurredAt) } }, 'treasury_transaction.voided');
850
+ const account = ctx.get(FA, String(txn.financial_account));
851
+ const impact = txn.balance_impact ?? {};
852
+ const cur = String(txn.currency ?? 'usd');
853
+ if (account) {
854
+ const bal = account.balance ?? { cash: {}, inbound_pending: {}, outbound_pending: {} };
855
+ const back = Object.fromEntries(['cash', 'inbound_pending', 'outbound_pending'].map((k) => [k, { ...(bal[k] ?? {}), [cur]: ((bal[k] ?? {})[cur] ?? 0) - (Number(impact[k]) || 0) }]));
856
+ await ctx.write(FA, String(account.id), { balance: back }, 'financial_account.balance_updated');
857
+ }
858
+ }
859
+ return ctx.reply(await ctx.write(resource, id, { status: 'canceled', cancelable: false, status_transitions: { ...flow.status_transitions, canceled_at: nowUnix(ctx.occurredAt) } }, op));
860
+ };
861
+ }
862
+ // the ledger is always read for one account
863
+ export function ledger(resource, spec) {
864
+ return async (ctx) => {
865
+ if (typeof ctx.params.financial_account !== 'string' || !ctx.params.financial_account)
866
+ return fail(ctx, 'Missing required param: financial_account.', 400, 'parameter_missing');
867
+ return list(ctx, resource, where(ctx, newest(ctx, resource), spec));
868
+ };
869
+ }
870
+ export function scopeKeyOf(ctx) {
871
+ const scope = ctx.params.scope && typeof ctx.params.scope === 'object' ? ctx.params.scope : undefined;
872
+ const scopeType = scope && typeof scope.type === 'string' ? scope.type : 'account';
873
+ return scopeType === 'user' ? `user:${String(scope.user)}` : 'account';
874
+ }
875
+ export const named = (ctx, name) => ctx.rows(SECRET).find((s) => s.name === name && kept(ctx, SECRET, s, '_scope_key') === scopeKeyOf(ctx));
876
+ export const invoiceMissing = (ctx, id) => fail(ctx, `No such invoice: '${id}'`, 404, 'resource_missing');
877
+ /** Write a draft's line list and the totals derived from it. */
878
+ export async function writeLines(ctx, invoiceId, next) {
879
+ const subtotal = sumLines(next);
880
+ const lines = { object: 'list', data: next, has_more: false, total_count: next.length, url: `/v1/invoices/${invoiceId}/lines` };
881
+ return ctx.write(INV, invoiceId, { lines, subtotal, total: subtotal, amount_due: subtotal, amount_remaining: subtotal }, 'invoice.update');
882
+ }
883
+ /** Paying an invoice with nothing to pay it: "If not set, defaults to the subscription's default payment method, if any,
884
+ * or to the default payment method in the customer's invoice settings" (docs.stripe.com/api/invoices/object,
885
+ * default_payment_method), and none of them is set. */
886
+ export function nothingToPayWith(ctx) {
887
+ return fail(ctx, 'This customer has no attached payment source or default payment method. Please consider adding a default payment method.', 400, 'missing');
888
+ }
889
+ /** A subscription waiting on the invoice paid through the invoice's own pay (incomplete on its first, past_due on a
890
+ * failed renewal) is active again. */
891
+ export async function activatedByPayment(ctx, subId) {
892
+ await ctx.write('subscription', subId, { status: 'active' }, 'subscription.update');
893
+ }
894
+ /** Re-derive a draft's lines from the items still on it: an item that changed or was deleted moves the invoice with it. */
895
+ export async function resyncDraftLines(ctx, invoiceId) {
896
+ const inv = ctx.get(INV, invoiceId);
897
+ if (!inv || inv.status !== 'draft')
898
+ return;
899
+ const next = [];
900
+ for (const line of linesOf(inv)) {
901
+ const itemId = typeof line.invoice_item === 'string' ? line.invoice_item : '';
902
+ if (!itemId)
903
+ next.push(unlinkedLine(line));
904
+ const item = itemId ? ctx.get('invoiceitem', itemId) : undefined;
905
+ if (item)
906
+ next.push({ ...line, amount: Number(item.amount) || 0, description: item.description ?? null });
907
+ }
908
+ await writeLines(ctx, invoiceId, next);
909
+ }
910
+ // ── invoice items: a price-based item takes its amount from the price; one naming a draft invoice
911
+ // lands on its lines at once ──
912
+ /** An item priced by a Price: its unit amount times the quantity, in its currency (docs.stripe.com/api/invoiceitems/create#create_invoiceitem-pricing). */
913
+ export function pricedItem(ctx, priceRef, qty) {
914
+ const price = ctx.get('price', priceRef);
915
+ if (!price)
916
+ return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
917
+ return { amount: priceAmount(price, qty), currency: typeof price.currency === 'string' ? price.currency : undefined };
918
+ }
919
+ export const itemMissing = (ctx, id) => fail(ctx, `No such invoiceitem: '${id}'`, 404, 'resource_missing');
920
+ /** A draft invoice another object makes (an accepted quote): its customer's, with an invoice item per line the
921
+ * object gives, and the object named as its parent. Answers the invoice's id. */
922
+ export async function draftInvoiceFor(ctx, customer, currency, parent, lines) {
923
+ const now = Number(nowUnix(ctx.occurredAt));
924
+ const id = ctx.mint(INV);
925
+ const holder = ctx.get('customer', customer);
926
+ 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` } });
927
+ for (const l of lines) {
928
+ const price = typeof l.price === 'string' ? l.price : l.price && typeof l.price === 'object' ? String(l.price.id) : null;
929
+ 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 } });
930
+ await pullPendingItem(ctx, id, inv, item, now);
931
+ }
932
+ return id;
933
+ }
934
+ /** 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). */
935
+ export async function pullPendingItem(ctx, invoiceId, inv, item, now) {
936
+ const current = ctx.get(INV, invoiceId) ?? inv;
937
+ await writeLines(ctx, invoiceId, [...linesOf(current), {
938
+ id: `il_${String(item.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(item.amount) || 0, currency: String(current.currency ?? item.currency ?? 'usd'),
939
+ quantity: Number(item.quantity) || 1, proration: false, invoice_item: item.id, price: item.price ?? null,
940
+ period: item.period ?? { start: now, end: now }, description: item.description ?? null,
941
+ discountable: item.discountable ?? true, discounts: [], livemode: false, metadata: item.metadata ?? {}, tax_amounts: [], tax_rates: [],
942
+ }]);
943
+ await ctx.write('invoiceitem', String(item.id), { invoice: invoiceId }, 'invoiceitem.update');
944
+ }
945
+ /** The invoice the path names, and the machine's refusal when this operation may not move it. */
946
+ export function invoicesLoad(ctx, operationId) {
947
+ const id = at(ctx, 'invoice');
948
+ const inv = ctx.get(INV, id);
949
+ if (!inv)
950
+ return { answer: invoiceMissing(ctx, id) };
951
+ const refused = ctx.legal(INV, 'status', operationId, inv.status, undefined, id);
952
+ return refused ? { answer: ctx.refuse(refused) } : { inv };
953
+ }
954
+ /** The PaymentIntent a charge_automatically invoice with a balance gets when it is finalized; it
955
+ * waits for confirmation when the invoice already names a default payment method. */
956
+ export async function openPaymentIntent(ctx, piId, inv, invoiceId) {
957
+ const defaultPaymentMethod = typeof inv.default_payment_method === 'string' ? inv.default_payment_method : undefined;
958
+ await created(ctx, 'payment_intent', {
959
+ id: piId, amount: Number(inv.amount_due) || 0, currency: String(inv.currency ?? 'usd'), customer: inv.customer, invoice: invoiceId,
960
+ ...(defaultPaymentMethod ? { payment_method: defaultPaymentMethod } : {}),
961
+ }, {
962
+ status: defaultPaymentMethod ? 'requires_confirmation' : 'requires_payment_method',
963
+ client_secret: mintClientSecret(piId, await ctx.secret(`client-secret:${piId}`)), livemode: false,
964
+ capture_method: 'automatic', amount_capturable: 0, amount_received: 0, next_action: null,
965
+ automatic_payment_methods: null, payment_method_types: ['card', 'link'], payment_method_options: {},
966
+ });
967
+ }
968
+ export function finalizedFields(ctx, inv, now) {
969
+ const customerId = typeof inv.customer === 'string' ? inv.customer : '';
970
+ const customer = customerId ? ctx.get('customer', customerId) : undefined;
971
+ const prefix = typeof customer?.invoice_prefix === 'string' && customer.invoice_prefix
972
+ ? customer.invoice_prefix
973
+ : invoicePrefix(customerId);
974
+ const numbered = ctx.rows(INV).filter((i) => i.customer === customerId && typeof i.number === 'string' && i.id !== inv.id).length;
975
+ const fields = { number: typeof inv.number === 'string' && inv.number ? inv.number : `${prefix}-${String(numbered + 1).padStart(4, '0')}` };
976
+ const days = Number(inv.days_until_due);
977
+ if (inv.collection_method === 'send_invoice' && inv.due_date == null && Number.isFinite(days) && days > 0)
978
+ fields.due_date = now + days * 86400;
979
+ return fields;
980
+ }
981
+ // add_lines, update_lines and remove_lines edit a draft's lines in bulk and re-derive its totals
982
+ export function editLines(kind) {
983
+ return async (ctx) => {
984
+ const id = at(ctx, 'invoice');
985
+ const inv = ctx.get(INV, id);
986
+ if (!inv)
987
+ return invoiceMissing(ctx, id);
988
+ if (inv.status !== 'draft')
989
+ return fail(ctx, `You can only add/update/remove lines on a draft invoice (this one is ${String(inv.status)}).`, 400, 'invoice_not_editable');
990
+ const cur = linesOf(inv);
991
+ let next = [...cur];
992
+ const raw = ctx.params.lines;
993
+ const entries = Array.isArray(raw) ? raw : raw && typeof raw === 'object' ? [raw] : [];
994
+ if (entries.length === 0)
995
+ return fail(ctx, 'Missing required param: lines.', 400, 'parameter_missing');
996
+ const currency = String(inv.currency ?? 'usd');
997
+ if (kind === 'add') {
998
+ for (let i = 0; i < entries.length; i++) {
999
+ const e = entries[i];
1000
+ const priceRef = typeof e.price === 'string' ? e.price : '';
1001
+ const price = priceRef ? ctx.get('price', priceRef) : undefined;
1002
+ if (priceRef && !price)
1003
+ return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
1004
+ const quantity = e.quantity !== undefined ? Math.max(1, Math.trunc(Number(e.quantity) || 1)) : 1;
1005
+ const unit = Math.trunc(Number(e.amount) || 0);
1006
+ 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 });
1007
+ }
1008
+ }
1009
+ else if (kind === 'update') {
1010
+ for (const e of entries) {
1011
+ const lid = typeof e.id === 'string' ? e.id : '';
1012
+ 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));
1013
+ }
1014
+ }
1015
+ else {
1016
+ const remove = new Set(entries.map((e) => (typeof e.id === 'string' ? e.id : '')).filter(Boolean));
1017
+ next = next.filter((l) => !remove.has(String(l.id)));
1018
+ }
1019
+ return ctx.reply(await writeLines(ctx, id, next));
1020
+ };
1021
+ }
1022
+ export const noteMissing = (ctx, id) => fail(ctx, `No such credit note: '${id}'`, 404, 'resource_missing');
1023
+ /** The credit note the request describes (the `amount` form: one custom line), or Stripe's refusal. */
1024
+ export function build(ctx, id) {
1025
+ const params = ctx.params;
1026
+ const invoiceId = typeof params.invoice === 'string' ? params.invoice : '';
1027
+ if (!invoiceId)
1028
+ return fail(ctx, 'Missing required param: invoice.', 400, 'parameter_missing');
1029
+ const invoice = ctx.get('invoice', invoiceId);
1030
+ if (!invoice)
1031
+ return fail(ctx, `No such invoice: '${invoiceId}'`, 404, 'resource_missing');
1032
+ if (params.amount === undefined)
1033
+ return fail(ctx, 'Missing required param: amount.', 400, 'parameter_missing');
1034
+ const amount = Number(params.amount);
1035
+ if (!Number.isInteger(amount) || amount <= 0)
1036
+ return fail(ctx, 'Invalid integer: amount must be a positive integer.', 400, 'parameter_invalid_integer');
1037
+ const vendorType = invoice.status === 'paid' ? 'post_payment' : 'pre_payment';
1038
+ const outOfBand = Number(params.out_of_band_amount) || 0;
1039
+ // a paid invoice's credit is paid back: "The sum of refunds, customer balance credits, and outside of Stripe credits
1040
+ // must equal the post_payment_amount" (docs.stripe.com/api/credit_notes/create); the message's wording is the twin's
1041
+ if (vendorType === 'post_payment') {
1042
+ const split = (Number(params.refund_amount) || 0) + (Number(params.credit_amount) || 0) + outOfBand;
1043
+ if (split !== amount)
1044
+ return fail(ctx, `The sum of refund_amount, credit_amount and out_of_band_amount (${split}) must equal the credit note's post_payment_amount (${amount}).`, 400, 'parameter_invalid_integer');
1045
+ }
1046
+ const line = {
1047
+ object: 'credit_note_line_item', id: `cnli_${id}_1`, type: 'custom_line_item',
1048
+ amount, description: typeof params.memo === 'string' ? params.memo : null,
1049
+ // a custom line credits no invoice line: invoice_line_item ("ID of the invoice line item being credited") is left
1050
+ // out, the served spec not letting it be null
1051
+ discount_amount: 0, discount_amounts: [], livemode: false,
1052
+ pretax_credit_amounts: [], quantity: null, tax_rates: [], taxes: [],
1053
+ unit_amount: null, unit_amount_decimal: null,
1054
+ };
1055
+ return {
1056
+ amount, amount_shipping: 0, created: nowUnix(ctx.occurredAt), currency: typeof invoice.currency === 'string' ? invoice.currency : 'usd', customer: typeof invoice.customer === 'string' ? invoice.customer : null,
1057
+ customer_balance_transaction: null,
1058
+ // "The date when this credit note is in effect. Same as `created` unless overwritten" (the served spec)
1059
+ discount_amount: 0, discount_amounts: [], effective_at: params.effective_at !== undefined ? Number(params.effective_at) : nowUnix(ctx.occurredAt), invoice: invoiceId,
1060
+ lines: { object: 'list', data: [line], has_more: false, total_count: 1, url: `/v1/credit_notes/${id}/lines` },
1061
+ livemode: false, memo: params.memo ?? null, metadata: params.metadata && typeof params.metadata === 'object' ? params.metadata : {},
1062
+ number: `${invoiceId}-CN-01`, out_of_band_amount: outOfBand || null,
1063
+ pdf: `https://files.twin.local/credit_notes/${id}.pdf`,
1064
+ post_payment_amount: vendorType === 'post_payment' ? amount : 0,
1065
+ pre_payment_amount: vendorType === 'pre_payment' ? amount : 0,
1066
+ pretax_credit_amounts: [],
1067
+ reason: typeof params.reason === 'string' ? params.reason : null,
1068
+ refunds: [], shipping_cost: null, status: 'issued',
1069
+ subtotal: amount, subtotal_excluding_tax: amount,
1070
+ type: vendorType, total: amount, total_excluding_tax: amount, total_taxes: [],
1071
+ voided_at: null,
1072
+ };
1073
+ }
1074
+ /** The invoice's credit-note sums, from its issued credit notes; a pre-payment credit lowers what is owed. */
1075
+ export async function syncInvoice(ctx, invoiceId) {
1076
+ const inv = ctx.get('invoice', invoiceId);
1077
+ if (!inv)
1078
+ return;
1079
+ let pre = 0;
1080
+ let post = 0;
1081
+ for (const cn of ctx.rows(CN)) {
1082
+ if (cn.invoice !== invoiceId || cn.status === 'void')
1083
+ continue;
1084
+ if (cn.type === 'post_payment')
1085
+ post += Number(cn.amount) || 0;
1086
+ else
1087
+ pre += Number(cn.amount) || 0;
1088
+ }
1089
+ const total = Number(inv.total ?? inv.amount_due) || 0;
1090
+ const paid = Number(inv.amount_paid) || 0;
1091
+ await ctx.write('invoice', invoiceId, { pre_payment_credit_notes_amount: pre, post_payment_credit_notes_amount: post, amount_remaining: Math.max(0, total - paid - pre) }, 'invoice.update');
1092
+ }
1093
+ /** What a post-payment credit pays back: "Refunds: create a new refund (using refund_amount)"; "Customer balance credit:
1094
+ * credit the customer's balance (using credit_amount) which will be automatically applied to their next invoice when
1095
+ * it's finalized" (docs.stripe.com/api/credit_notes/create). The refund is made on the invoice's charge as a refund
1096
+ * call makes it (refunds.ts); the credit is a customer balance transaction of type credit_note. Where the documentation
1097
+ * stops and the twin decides: the refund carries no reason of its own. */
1098
+ export async function payBack(ctx, id, cn) {
1099
+ const params = ctx.params;
1100
+ const out = {};
1101
+ const refundAmount = Number(params.refund_amount) || 0;
1102
+ if (refundAmount > 0) {
1103
+ const invoice = ctx.get('invoice', String(cn.invoice));
1104
+ const pi = typeof invoice?.payment_intent === 'string' ? ctx.get('payment_intent', invoice.payment_intent) : undefined;
1105
+ const chargeId = typeof invoice?.charge === 'string' ? invoice.charge : typeof pi?.latest_charge === 'string' ? pi.latest_charge : undefined;
1106
+ const charge = chargeId ? ctx.get('charge', chargeId) : undefined;
1107
+ if (!charge)
1108
+ return fail(ctx, 'This invoice was not paid with a charge that can be refunded.', 400, 'invoice_no_charge_to_refund');
1109
+ const answer = await refundCharge(ctx, charge, { amount: refundAmount });
1110
+ if (answer.status >= 400)
1111
+ return answer;
1112
+ const refund = (await answer.json());
1113
+ out.refunds = [{ amount_refunded: refundAmount, refund: refund.id, type: 'refund', payment_record_refund: null }];
1114
+ }
1115
+ const creditAmount = Number(params.credit_amount) || 0;
1116
+ const customer = typeof cn.customer === 'string' ? cn.customer : undefined;
1117
+ if (creditAmount > 0 && customer) {
1118
+ const ending = (Number(ctx.get('customer', customer)?.balance) || 0) - creditAmount;
1119
+ const txn = await created(ctx, 'customer_balance_transaction', { customer, amount: -creditAmount, currency: cn.currency, credit_note: id, invoice: cn.invoice }, {
1120
+ ending_balance: ending, type: 'credit_note', livemode: false, metadata: {}, description: null, checkout_session: null,
1121
+ });
1122
+ await ctx.write('customer', customer, { balance: ending }, `customer.balance_adjusted:${String(txn.id)}`);
1123
+ out.customer_balance_transaction = txn.id;
1124
+ }
1125
+ return out;
1126
+ }
1127
+ /** A coupon for a fixed amount off, in its currency (docs.stripe.com/api/coupons/create#create_coupon-amount_off). */
1128
+ export function amountOffOf(ctx, params) {
1129
+ const amountOff = Number(params.amount_off);
1130
+ if (!Number.isInteger(amountOff) || amountOff <= 0)
1131
+ return fail(ctx, 'Invalid integer: amount_off must be a positive integer.', 400, 'parameter_invalid_integer');
1132
+ if (params.currency === undefined || params.currency === '')
1133
+ return fail(ctx, 'Missing required param: currency.', 400, 'parameter_missing');
1134
+ return amountOff;
1135
+ }
1136
+ /** Time's lapsing of coupons, caught up to the World's clock: a valid coupon whose redeem_by has passed is no longer
1137
+ * valid from that moment (manifest.ts). Where the documentation stops and the twin decides: the lapse sends no event
1138
+ * (the events page names none for it). */
1139
+ export async function lapseCoupons(ctx) {
1140
+ const now = Number(nowUnix(ctx.occurredAt));
1141
+ for (const c of ctx.rowsRaw('coupon')) {
1142
+ if (c.valid !== true || typeof c.redeem_by !== 'number' || c.redeem_by > now)
1143
+ continue;
1144
+ const t = await at_(ctx)(c.redeem_by);
1145
+ t.legal('coupon', 'valid', ctx.call.operation.id, 'true', 'false', String(c.id), 'time');
1146
+ await t.write('coupon', String(c.id), { valid: false }, 'coupon.lapsed');
1147
+ }
1148
+ }
1149
+ export const planMissing = (ctx, id) => fail(ctx, `No such plan: '${id}'`, 404, 'resource_missing');
1150
+ /** A recurring price the Plans API still answers: one whose plan was deleted is not a plan any more. */
1151
+ export const recurring = (ctx, p) => !!p && !!p.recurring && kept(ctx, 'price', p, '_plan_deleted') !== true;
1152
+ /** The request's line items resolved against their prices, or the refusal for a missing price. */
1153
+ export function resolved(ctx) {
1154
+ const entries = lineItemEntries(ctx.params);
1155
+ const items = [];
1156
+ for (let i = 0; i < entries.length; i++) {
1157
+ const { item, priceMissing } = resolveLineItem(entries[i], i, 'usd', finder(ctx));
1158
+ if (priceMissing)
1159
+ return fail(ctx, `No such price: '${priceMissing}'`, 400, 'resource_missing');
1160
+ items.push(item);
1161
+ }
1162
+ return items;
1163
+ }
1164
+ /** A page of the line items a stored object carries. */
1165
+ export function linePage(ctx, holder) {
1166
+ const li = holder.line_items;
1167
+ const { page, hasMore } = paginate(Array.isArray(li?.data) ? li.data : [], ctx.params);
1168
+ return ctx.reply({ object: 'list', url: path(ctx), has_more: hasMore, data: page });
1169
+ }
1170
+ export const linkMissing = (ctx, id) => fail(ctx, `No such payment link: '${id}'`, 404, 'resource_missing');
1171
+ // ── quotes ──
1172
+ export const quoteMissing = (ctx, id) => fail(ctx, `No such quote: '${id}'`, 404, 'resource_missing');
1173
+ export function move(action, operationId) {
1174
+ return async (ctx) => {
1175
+ const id = at(ctx, 'quote');
1176
+ const q = ctx.get('quote', id);
1177
+ if (!q)
1178
+ return quoteMissing(ctx, id);
1179
+ const refused = ctx.legal('quote', 'status', operationId, q.status, undefined, id);
1180
+ if (refused)
1181
+ return ctx.refuse(refused);
1182
+ const now = nowUnix(ctx.occurredAt);
1183
+ const stamp = action === 'finalize' ? { finalized_at: now } : action === 'accept' ? { accepted_at: now } : { canceled_at: now };
1184
+ // "Accepted quotes automatically generate an invoice, subscription, or subscription schedule"; "Quotes without
1185
+ // recurring prices: A draft invoice is created with auto_advance set to false" (docs.stripe.com/quotes/overview),
1186
+ // the quote its parent ("Details about the quote that generated this invoice", the served spec). Where the
1187
+ // documentation stops and the twin decides: a quote with recurring prices makes no subscription yet.
1188
+ let made = {};
1189
+ if (action === 'accept') {
1190
+ const lines = q.line_items?.data ?? [];
1191
+ const recurring = lines.some((l) => l.price && typeof l.price === 'object' && l.price.recurring);
1192
+ if (!recurring && typeof q.customer === 'string') {
1193
+ const invoice = await draftInvoiceFor(ctx, q.customer, String(q.currency ?? 'usd'), { type: 'quote_details', quote_details: { quote: id }, subscription_details: null }, lines.map((l) => ({ price: l.price, quantity: Number(l.quantity) || 1, amount: Number(l.amount_subtotal ?? l.amount_total) || 0, description: l.description })));
1194
+ made = { invoice };
1195
+ }
1196
+ }
1197
+ return ctx.reply(await ctx.write('quote', id, {
1198
+ status: action === 'finalize' ? 'open' : action === 'accept' ? 'accepted' : 'canceled',
1199
+ status_transitions: { ...(q.status_transitions ?? {}), ...stamp },
1200
+ ...(action === 'finalize' ? { number: `QT-${id.toUpperCase()}` } : {}),
1201
+ ...made,
1202
+ }, `quote.${action}`));
1203
+ };
1204
+ }
1205
+ export const sessionMissing = (ctx, id) => fail(ctx, `No such checkout.session: '${id}'`, 404, 'resource_missing');
1206
+ // Stripe normalizes the create input into the Session: line_items become the list envelope, and
1207
+ // subscription_data (create-only) is kept aside for completion to copy onto the subscription
1208
+ /** The discount a session is created with (discounts[0][promotion_code] or [coupon]), or Stripe's refusal: an unknown
1209
+ * code or coupon, and one that has expired or been deactivated, are refused at create
1210
+ * (docs.stripe.com/api/checkout/sessions/create#create_checkout_session-discounts, docs.stripe.com/error-codes#coupon-expired).
1211
+ * Where the documentation stops and the twin decides: the refusals' wording. */
1212
+ export function discountOf(ctx, params, now) {
1213
+ const first = Array.isArray(params.discounts) ? params.discounts[0] : undefined;
1214
+ if (!first || typeof first !== 'object')
1215
+ return undefined;
1216
+ const expired = (what) => ctx.refuse({ status: 400, code: 'coupon_expired', param: 'discounts', message: `This ${what} has expired.` });
1217
+ let promotion;
1218
+ let couponId = typeof first.coupon === 'string' ? first.coupon : undefined;
1219
+ if (typeof first.promotion_code === 'string') {
1220
+ const pc = ctx.get('promotion_code', first.promotion_code);
1221
+ if (!pc)
1222
+ return ctx.refuse({ status: 400, code: 'resource_missing', param: 'discounts[0][promotion_code]', message: `No such promotion code: '${first.promotion_code}'` });
1223
+ if (typeof pc.expires_at === 'number' && pc.expires_at <= now)
1224
+ return expired('promotion code');
1225
+ if (pc.active === false)
1226
+ return ctx.refuse({ status: 400, param: 'discounts[0][promotion_code]', message: 'This promotion code is inactive.' });
1227
+ promotion = String(pc.id);
1228
+ couponId = typeof pc.coupon === 'string' ? pc.coupon : String(pc.coupon?.id ?? '');
1229
+ }
1230
+ const coupon = couponId ? ctx.get('coupon', couponId) : undefined;
1231
+ if (!coupon)
1232
+ return ctx.refuse({ status: 400, code: 'resource_missing', param: 'discounts[0][coupon]', message: `No such coupon: '${couponId ?? ''}'` });
1233
+ if (coupon.valid === false || (typeof coupon.redeem_by === 'number' && coupon.redeem_by <= now))
1234
+ return expired('coupon');
1235
+ return { coupon, ...(promotion ? { promotion } : {}) };
1236
+ }
1237
+ /** A payment or subscription session with nothing to sell (docs.stripe.com/api/checkout/sessions/create#create_checkout_session-line_items). */
1238
+ export function noLineItems(ctx) {
1239
+ return fail(ctx, 'You must provide at least one recurring price in `subscription` mode when not using `line_items[].price_data` or a Price with `recurring`, and at least one `line_items` for `payment` mode.', 400, 'parameter_missing');
1240
+ }
1241
+ /** A destination charge to an account the platform does not have (docs.stripe.com/connect/destination-charges). */
1242
+ export function checkoutNoDestination(ctx, destination) {
1243
+ return ctx.refuse({ status: 400, code: 'resource_missing', param: 'payment_intent_data[transfer_data][destination]', message: `No such destination: '${String(destination)}'` });
1244
+ }
1245
+ /** A session's trial Stripe refuses: `subscription_data.trial_end` "Has to be at least 48 hours in the future", and
1246
+ * `subscription_data.trial_period_days` "Has to be at least 1" (docs.stripe.com/api/checkout/sessions/create). Where the
1247
+ * documentation stops and the twin decides: the refusals' wording. */
1248
+ export function sessionTrialRefused(ctx, data, now) {
1249
+ if (!data || typeof data !== 'object')
1250
+ return undefined;
1251
+ const { trial_end: end, trial_period_days: days } = data;
1252
+ const endRefused = end !== undefined && !(Number.isInteger(Number(end)) && Number(end) >= now + 48 * 3600);
1253
+ const daysRefused = days !== undefined && !(Number.isInteger(Number(days)) && Number(days) >= 1);
1254
+ return endRefused || daysRefused ? sessionTrialRefusal(ctx, endRefused ? 'trial_end' : 'trial_period_days') : undefined;
1255
+ }
1256
+ export function sessionTrialRefusal(ctx, param) {
1257
+ const message = param === 'trial_end' ? 'The `subscription_data[trial_end]` timestamp has to be at least 48 hours in the future.' : 'The `subscription_data[trial_period_days]` has to be at least 1.';
1258
+ return ctx.refuse({ status: 400, param: `subscription_data[${param}]`, message });
1259
+ }
1260
+ /** What completing a session makes, by its mode, as the fields that link it: a payment is a PaymentIntent paid with
1261
+ * the entered card, its charge in the ledger as any payment's is. */
1262
+ export async function complete(ctx, id, existing, card) {
1263
+ const link = { status: 'complete' };
1264
+ const customer = typeof existing.customer === 'string' ? existing.customer : undefined;
1265
+ const currency = typeof existing.currency === 'string' ? existing.currency : 'usd';
1266
+ // the entered card becomes a PaymentMethod carrying what a later charge to it answers and what Stripe does after it
1267
+ // succeeds (docs.stripe.com/testing#declined-payments, #disputes)
1268
+ const made = card ? await created(ctx, 'payment_method', {}, { type: 'card', customer: null, livemode: false, billing_details: { address: null, email: null, name: null, phone: null }, ...paymentMethodSubObject('card', { card }), _declineOutcome: declineFor(() => undefined, { card }) ?? null, ...(afterSuccessOf(ctx, card) ? { _afterSuccess: afterSuccessOf(ctx, card) } : {}) }) : undefined;
1269
+ // and Checkout attaches it to the customer when it saves it: a subscription's card ("If your Checkout Session uses
1270
+ // subscription mode, Stripe saves the payment method by default", docs.stripe.com/payments/checkout/how-checkout-works),
1271
+ // a setup session's (it exists to save one), and a payment's only under payment_intent_data.setup_future_usage ("to have
1272
+ // Checkout automatically attach the payment method to the Customer you pass in", docs.stripe.com/api/checkout/sessions/
1273
+ // create#create_checkout_session-customer). Attaching is its own write, so payment_method.attached is delivered ("Occurs
1274
+ // whenever a new payment method is attached to a customer", docs.stripe.com/api/events/types) before what the card then
1275
+ // pays for. Where the documentation stops and the twin decides: the order of the events, which Stripe does not
1276
+ // guarantee (docs.stripe.com/webhooks#event-ordering). A gap: the customer's own opt-in to save a payment's card
1277
+ // (saved_payment_method_options.payment_method_save) is not modelled; the page offers no such box.
1278
+ const saves = existing.mode !== 'payment' || !!(ctx.row(CS, id)?._payment_intent_data ?? {}).setup_future_usage;
1279
+ const pm = made && customer && saves ? await ctx.write('payment_method', String(made.id), { customer }, 'payment_method.attach') : made;
1280
+ if (existing.mode === 'payment') {
1281
+ const amount = Number(existing.amount_total) || 0;
1282
+ const piId = ctx.mint('payment_intent');
1283
+ const pid = (ctx.row(CS, id)?._payment_intent_data ?? {});
1284
+ const connect = {
1285
+ ...(pid.application_fee_amount !== undefined ? { application_fee_amount: Math.trunc(Number(pid.application_fee_amount) || 0) } : {}),
1286
+ ...(pid.transfer_data && typeof pid.transfer_data === 'object' ? { transfer_data: { destination: pid.transfer_data.destination } } : {}),
1287
+ ...(typeof pid.description === 'string' ? { description: pid.description } : {}),
1288
+ // the session's setup_future_usage is its PaymentIntent's (payment_intent_data: "A subset of parameters to be passed to PaymentIntent creation")
1289
+ ...(typeof pid.setup_future_usage === 'string' ? { setup_future_usage: pid.setup_future_usage } : {}),
1290
+ ...(nestedMetadata(pid.metadata) ? { metadata: nestedMetadata(pid.metadata) } : {}),
1291
+ };
1292
+ const fields = { amount, currency, id: piId, ...(customer ? { customer } : {}), ...(pm ? { payment_method: pm.id } : {}), ...connect };
1293
+ await created(ctx, 'payment_intent', fields, { status: 'succeeded', amount_received: amount, client_secret: mintClientSecret(piId, await ctx.secret(`client-secret:${piId}`)), livemode: false, payment_method_types: Array.isArray(existing.payment_method_types) ? existing.payment_method_types : ['card'] });
1294
+ const charge = await mintCharge(ctx, piId, fields, amount);
1295
+ await ctx.write('payment_intent', piId, { latest_charge: charge }, 'payment_intent.succeeded');
1296
+ link.payment_intent = piId;
1297
+ link.payment_status = 'paid';
1298
+ }
1299
+ else if (existing.mode === 'subscription') {
1300
+ if (customer) {
1301
+ // the subscription bills the session's own (already resolved) line items, and takes the
1302
+ // session's subscription_data: metadata, description, and a trial as its first period
1303
+ const now = Number(nowUnix(ctx.occurredAt));
1304
+ // "Line items with one-time Prices will be on the initial invoice only" (docs.stripe.com/api/checkout/sessions/
1305
+ // create#create_checkout_session-line_items): the subscription's items are the recurring ones
1306
+ const inline = ctx.row(CS, id)?._price_data ?? [];
1307
+ const lines = existing.line_items?.data ?? [];
1308
+ const allLineItems = inline.some(Boolean) ? await sessionPrices(ctx, lines, inline) : lines;
1309
+ const sessionLineItems = allLineItems.filter((it) => !isOneTime(it));
1310
+ const oneTime = allLineItems.filter((it) => isOneTime(it));
1311
+ const { interval, interval_count } = resolveSubscriptionBillingInterval(sessionLineItems, finder(ctx));
1312
+ const subId = ctx.mint('subscription');
1313
+ // the session's view never carries its create-only subscription_data: read the stored row
1314
+ const raw = ctx.row(CS, id)?._subscription_data;
1315
+ const subData = (raw && typeof raw === 'object' ? raw : {});
1316
+ const subMetadata = nestedMetadata(subData.metadata) ?? {};
1317
+ // a trial of `trial_period_days` from now, or to `trial_end` (docs.stripe.com/payments/checkout/free-trials)
1318
+ const trialEnd = resolveTrial(subData, now)?.end ?? null;
1319
+ const sub = await created(ctx, 'subscription', { customer, id: subId }, {
1320
+ status: trialEnd === null ? 'active' : 'trialing', livemode: false, currency, collection_method: 'charge_automatically',
1321
+ // a trial's end is the billing anchor (docs.stripe.com/api/subscriptions/create#create_subscription-trial_end)
1322
+ cancel_at_period_end: false, start_date: now, billing_cycle_anchor: trialEnd ?? now, metadata: subMetadata,
1323
+ description: typeof subData.description === 'string' ? subData.description : null,
1324
+ // the session's discount is the subscription's (docs.stripe.com/payments/checkout/discounts)
1325
+ discounts: sessionDiscount(ctx, id, customer, now), billing_schedules: [],
1326
+ trial_start: trialEnd === null ? null : now, trial_end: trialEnd,
1327
+ current_period_start: now, current_period_end: trialEnd ?? addBillingInterval(now, interval, interval_count),
1328
+ // Checkout saves the card as the subscription's default payment method (docs.stripe.com/payments/checkout/how-checkout-works)
1329
+ default_payment_method: pm?.id ?? null,
1330
+ items: buildSubscriptionItemsList(sessionLineItems, subId, now, finder(ctx)),
1331
+ automatic_tax: { enabled: false, liability: null }, billing_mode: { type: 'classic' },
1332
+ invoice_settings: { issuer: { type: 'self' } },
1333
+ });
1334
+ await storeItems(ctx, subId, sub.items?.data ?? []);
1335
+ // its first invoice, paid on the page by the card just entered (nothing is due on a trial's)
1336
+ // (docs.stripe.com/billing/subscriptions/overview#how-payments-work-subscriptions)
1337
+ const invoice = await draftSubscriptionInvoice(ctx, sub, { start: now, end: Number(sub.current_period_end), reason: 'subscription_create', trial: trialEnd !== null, once: oneTime });
1338
+ await collectSubscriptionInvoice(ctx, invoice, now, 'external');
1339
+ await ctx.write('subscription', subId, { latest_invoice: invoice }, 'subscription.update');
1340
+ link.subscription = sub.id;
1341
+ link.invoice = invoice;
1342
+ }
1343
+ link.payment_status = 'paid';
1344
+ }
1345
+ else
1346
+ link.setup_intent = await completeSetup(ctx, customer, pm);
1347
+ return link;
1348
+ }
1349
+ /** A subscription session's lines at completion, each inline `price_data` made the Price it generates ("Data used to
1350
+ * generate a new Price object inline", the same page), under the product it names or one made from its product_data,
1351
+ * so the subscription's items bill it. Where the documentation stops and the twin decides: such a Price is archived
1352
+ * (active=false), as a subscription item's price_data makes one (docs.stripe.com/products-prices/manage-prices). */
1353
+ export async function sessionPrices(ctx, lines, inline) {
1354
+ const out = [];
1355
+ for (const [i, line] of lines.entries()) {
1356
+ const d = inline[i];
1357
+ if (!d) {
1358
+ out.push(line);
1359
+ continue;
1360
+ }
1361
+ const pd = d.product_data && typeof d.product_data === 'object' ? d.product_data : undefined;
1362
+ const product = typeof d.product === 'string' ? d.product : String((await created(ctx, 'product', { name: pd?.name ?? 'Item' }, { active: true, livemode: false, images: [], marketing_features: [], metadata: {}, updated: nowUnix(ctx.occurredAt) })).id);
1363
+ const r = d.recurring && typeof d.recurring === 'object' ? d.recurring : undefined;
1364
+ const price = await created(ctx, 'price', {
1365
+ product, currency: String(d.currency ?? line.currency ?? 'usd').toLowerCase(),
1366
+ unit_amount: d.unit_amount_decimal !== undefined ? null : Math.trunc(Number(d.unit_amount) || 0),
1367
+ unit_amount_decimal: d.unit_amount_decimal !== undefined ? String(d.unit_amount_decimal) : String(Math.trunc(Number(d.unit_amount) || 0)),
1368
+ ...(r ? { 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 } } : { recurring: null }),
1369
+ }, { active: false, livemode: false, billing_scheme: 'per_unit', type: r ? 'recurring' : 'one_time', metadata: {}, lookup_key: null, nickname: null, tax_behavior: 'unspecified' });
1370
+ out.push({ ...line, price });
1371
+ }
1372
+ return out;
1373
+ }
1374
+ /** A setup session saves the card through a succeeded SetupIntent (docs.stripe.com/payments/save-and-reuse?platform=checkout). */
1375
+ export async function completeSetup(ctx, customer, pm) {
1376
+ const siId = ctx.mint('setup_intent');
1377
+ const si = await created(ctx, 'setup_intent', { id: siId, ...(customer ? { customer } : {}), ...(pm ? { payment_method: pm.id } : {}) }, { status: 'succeeded', usage: 'off_session', client_secret: mintClientSecret(siId, await ctx.secret(`client-secret:${siId}`)), payment_method_types: ['card'], livemode: false });
1378
+ return si.id;
1379
+ }
1380
+ /** The discount a completed subscription session gives its subscription. */
1381
+ export function sessionDiscount(ctx, id, customer, now) {
1382
+ const d = ctx.row(CS, id)?._discount;
1383
+ const coupon = d && typeof d.coupon === 'string' ? ctx.get('coupon', d.coupon) : undefined;
1384
+ return coupon ? [{ ...buildDiscount(coupon, customer, now), promotion_code: d?.promotion_code ?? null, checkout_session: id }] : [];
1385
+ }
1386
+ /** The customer paying on the hosted page (src/screens/checkout.tsx): the external actor's move on the
1387
+ * session's machine, then what the payment made, linked, and the session stored complete. */
1388
+ export async function completeSession(ctx, id, existing, details = {}, card) {
1389
+ const refused = ctx.legal(CS, 'status', 'PostCheckoutSessionsSession', existing.status, 'complete', id, 'external');
1390
+ if (refused)
1391
+ return ctx.refuse(refused);
1392
+ const link = await complete(ctx, id, existing, card);
1393
+ // the discount the buyer took is redeemed (shared.ts redeemCoupon)
1394
+ const d = ctx.row(CS, id)?._discount;
1395
+ if (d)
1396
+ await redeemCoupon(ctx, d.coupon, d.promotion_code);
1397
+ // the session's url "is only present when the session is active" (docs.stripe.com/api/checkout/sessions/object)
1398
+ return ctx.write(CS, id, { ...details, ...link, url: null }, 'checkout.session.completed');
1399
+ }
4
1400
  export const send = (ctx, r) => ctx.reply(r.body, r.status);
5
1401
  /** Stripe's error body: an invalid_request_error with the message, and the code when there is one. */
6
1402
  export const fail = (ctx, message, status = 404, code) => ctx.refuse({ status, message, ...(code ? { code } : {}) });
@@ -39,20 +1435,9 @@ export function mergeUpdate(ctx, resource, id, fields) {
39
1435
  }
40
1436
  return out;
41
1437
  }
42
- /** The tree's type for a resource: what its rows are stored under. */
43
- export const stored = (resource) => manifest.resources[resource]?.storedAs ?? resource;
44
1438
  /** A resource's rows in Stripe's list order: newest first by `timeField`, ties by mint order (as the derived core breaks them). */
45
1439
  /** A subject's bookkeeping field (`_…`): kept on the stored row, never in the wire view ctx.get and ctx.rows answer. */
46
1440
  export const kept = (ctx, resource, row, key) => ctx.row(resource, String(row.id), { withDeleted: true })?.[key];
47
- /** A removed discount as Stripe answers it: the discount, marked deleted (docs.stripe.com/api/discounts/delete). */
48
- export function deletedDiscount(d, now) {
49
- const coupon = d.coupon && typeof d.coupon === 'object' ? String(d.coupon.id) : typeof d.coupon === 'string' ? d.coupon : null;
50
- return {
51
- id: d.id ?? `di_${coupon ?? 'twin'}`, object: 'discount', deleted: true, checkout_session: d.checkout_session ?? null, customer: d.customer ?? null,
52
- invoice: d.invoice ?? null, invoice_item: d.invoice_item ?? null, promotion_code: d.promotion_code ?? null, subscription: d.subscription ?? null,
53
- subscription_item: d.subscription_item ?? null, source: d.source ?? { type: 'coupon', coupon }, start: d.start ?? now,
54
- };
55
- }
56
1441
  /** A create's parameter that describes a confirmation, sent without confirm=true: the served spec gives each "This
57
1442
  * parameter can only be used with `confirm=true`" (off_session: "can only be used when confirm=true"); the message's
58
1443
  * wording is the twin's. Answers the refusal, or undefined. */
@@ -76,20 +1461,16 @@ export function newest(ctx, resource, timeField = 'created') {
76
1461
  // an object Stripe gives no creation time is ordered by the one the twin keeps for it
77
1462
  const raw = new Map(ctx.rowsRaw(resource).map((r) => [String(r.id), r]));
78
1463
  const when = (r) => Number(r[timeField] ?? raw.get(String(r.id))?._created ?? 0);
79
- return ctx.rows(resource).sort((a, b) => {
1464
+ // ties by the order the World made them (an event's id is a digest, not a count)
1465
+ const rows = ctx.rows(resource);
1466
+ const made = new Map(rows.map((r, i) => [r, i]));
1467
+ return rows.sort((a, b) => {
80
1468
  const ta = when(a), tb = when(b);
81
1469
  if (tb !== ta)
82
1470
  return tb - ta;
83
- return String(b.id).localeCompare(String(a.id), undefined, { numeric: true });
1471
+ return made.get(b) - made.get(a);
84
1472
  });
85
1473
  }
86
- /** Keep the rows whose field equals the request's parameter of the same name, for each name given. */
87
- /** A list's range filter on a unix time (`created[gte]=…&created[lt]=…`, or one exact time). */
88
- export function inRange(t, v) {
89
- const at = Number(t);
90
- const r = (v && typeof v === 'object' ? v : { gte: v, lte: v });
91
- return (r.gt === undefined || at > Number(r.gt)) && (r.gte === undefined || at >= Number(r.gte)) && (r.lt === undefined || at < Number(r.lt)) && (r.lte === undefined || at <= Number(r.lte));
92
- }
93
1474
  /** A context at the moment Stripe would have made a move time makes (a renewal, a payout on its schedule), so what
94
1475
  * the move writes carries that moment (its events' occurredAt, `created`, a charge's funds' available_on), not the
95
1476
  * moment a later request caught it up. */
@@ -105,48 +1486,35 @@ export function where(ctx, items, spec) {
105
1486
  }
106
1487
  return out;
107
1488
  }
108
- function expandPaths(ctx) {
1489
+ export function expandPaths(ctx) {
109
1490
  const expand = ctx.params.expand;
110
1491
  return Array.isArray(expand) ? expand.map((p) => String(p).split('.')) : [];
111
1492
  }
112
1493
  /** A balance transaction as the clock reads it: available once its available_on has passed, the clock's move,
113
- * asked of the machine as a write asks it (semantics/ledger.ts). */
114
- export function asOf(ctx, t) {
1494
+ * asked of the machine as a write asks it (./ledger.ts). */
1495
+ export function commonAsOf(ctx, t) {
115
1496
  const due = Number(t.available_on);
116
- if (t.status !== 'pending' || !Number.isFinite(due) || due > Number(ctx.now()))
1497
+ if (t.status !== 'pending' || !Number.isFinite(due) || due > Number(nowUnix(ctx.occurredAt)))
117
1498
  return t;
118
1499
  ctx.legal('balance_transaction', 'status', ctx.call.operation.id, 'pending', 'available', String(t.id), 'time');
119
1500
  return { ...t, status: 'available' };
120
1501
  }
121
- /** Stripe's expand walk: each dotted path replaces an id with the resource the manifest says it
122
- * holds, one segment at a time; an id the tree lacks stays an id. A balance transaction reads as the clock has it
123
- * (asOf), as its own retrieve does. */
1502
+ /** Stripe's expand walk (the kernel's, over the manifest's `embeds`): each dotted path replaces an id with the resource
1503
+ * it holds, one segment at a time; an id the tree lacks stays an id. An embedded balance transaction reads as the clock
1504
+ * has it (asOf), as its own retrieve does. */
124
1505
  export function expandRow(ctx, resource, body, paths) {
125
- const embeds = manifest.resources[resource]?.embeds;
126
- if (!embeds || paths.length === 0)
1506
+ if (paths.length === 0)
127
1507
  return body;
128
- let out = body;
129
- const byHead = new Map();
130
- for (const p of paths) {
131
- if (p.length === 0)
132
- continue;
133
- const list = byHead.get(p[0]) ?? [];
134
- list.push(p.slice(1));
135
- byHead.set(p[0], list);
136
- }
137
- for (const [head, tails] of byHead) {
138
- const target = embeds[head];
139
- if (!target)
140
- continue;
141
- const cur = out[head];
142
- const found = typeof cur === 'string' && cur ? ctx.get(target, cur) : cur && typeof cur === 'object' && !Array.isArray(cur) ? cur : undefined;
143
- if (!found)
144
- continue;
145
- const sub = target === 'balance_transaction' ? asOf(ctx, found) : found;
146
- const deeper = tails.filter((t) => t.length > 0);
147
- out = { ...out, [head]: deeper.length ? expandRow(ctx, target, sub, deeper) : sub };
148
- }
149
- return out;
1508
+ const asOf = (v) => {
1509
+ if (Array.isArray(v))
1510
+ return v.map(asOf);
1511
+ if (!v || typeof v !== 'object')
1512
+ return v;
1513
+ const o = Object.fromEntries(Object.entries(v).map(([k, x]) => [k, asOf(x)]));
1514
+ return o.object === 'balance_transaction' ? commonAsOf(ctx, o) : o;
1515
+ };
1516
+ const out = ctx.expand(resource, body, paths.map((p) => p.join('.')));
1517
+ return Object.fromEntries(Object.entries(out).map(([k, v]) => [k, asOf(v)]));
150
1518
  }
151
1519
  /** One resource with the request's `expand[]` applied. */
152
1520
  export const expanded = (ctx, resource, body) => expandRow(ctx, resource, body, expandPaths(ctx));
@@ -158,46 +1526,1644 @@ export function list(ctx, resource, items) {
158
1526
  }
159
1527
  /** Stripe's search over a resource's rows (`query` in its search language). */
160
1528
  export const search = (ctx, resource, timeField = 'created') => send(ctx, searchOver(newest(ctx, resource, timeField), ctx.params, path(ctx)));
161
- const SPEC_FIELDS = new Map(surface.resources.map((r) => [r.name, new Set(r.fields.map((f) => f.name))]));
162
1529
  /** A create as Stripe stamps it: the caller's id when it gave one (seeding uses real ids), else a
163
1530
  * minted one; `object`, the creation time, the defaults, then the request's own fields. An object
164
1531
  * Stripe gives no creation time keeps the twin's as `_created`, for ordering. (A request parameter the
165
1532
  * object has no field for stays on the row, where the rules read it; the wire answer leaves it out,
166
- * stripe-version.ts.) */
1533
+ * ./version.ts.) */
167
1534
  export async function created(ctx, resource, params, defaults, opts = {}) {
168
1535
  const { id: provided, ...rest } = params;
169
- const type = stored(resource);
1536
+ const type = ctx.storedType(resource);
170
1537
  const id = typeof provided === 'string' && provided ? provided : ctx.mint(resource);
171
1538
  const known = SPEC_FIELDS.get(resource);
172
1539
  const timeField = opts.timeField ?? 'created';
173
1540
  const stamped = !known || known.has(timeField) || timeField in defaults || timeField in rest ? timeField : '_created';
174
- const fields = stashVendorType(type, { object: OBJECT_NAME[type] ?? type, [stamped]: ctx.now(), ...defaults, ...rest });
1541
+ const fields = { object: OBJECT_NAME[type] ?? type, [stamped]: nowUnix(ctx.occurredAt), ...defaults, ...rest };
175
1542
  return ctx.write(resource, id, fields, opts.operation ?? `${type}.create`);
176
1543
  }
177
- export const finder = (ctx) => {
178
- const tree = ctx.tree();
179
- return (storedType, id) => {
180
- const r = tree.find((x) => x.type === storedType && x.id === id);
181
- return r ? view(storedType, r) : undefined;
182
- };
1544
+ export const finder = (ctx) => (storedType, id) => {
1545
+ const r = ctx.rowsRaw(storedType, { withDeleted: true }).find((x) => x.id === id);
1546
+ return r ? view(storedType, r) : undefined;
183
1547
  };
184
- /** A refund's destination_details for a card charge: "If this is a `card` refund, this hash contains the transaction
185
- * specific details" and its `type` "can be `refund`, `reversal`, or `pending`" — a reversal, where "The original charge
186
- * will drop off the bank statement altogether", for an authorization released uncaptured, else a refund, "a credit entry
187
- * on the bank statement" (docs.stripe.com/api/refunds/object). The network's reference number the twin never has. A
188
- * charge not paid by card answers none. */
189
- export function refundDestination(charge, type) {
190
- const pmd = charge.payment_method_details;
191
- if (pmd && pmd.type !== 'card')
192
- return {};
193
- return { destination_details: { type: 'card', card: { reference: null, reference_status: null, reference_type: null, type } } };
194
- }
195
- /** An account's external_accounts, "External accounts (bank accounts and debit cards) currently attached to this
196
- * account" (docs.stripe.com/api/accounts/object), as the list its example answers. */
197
- export const externalList = (account, data) => ({ object: 'list', data, has_more: false, total_count: data.length, url: `/v1/accounts/${account}/external_accounts` });
198
1548
  /** The account's external_accounts list, rewritten from its external accounts after one is added or removed. */
199
1549
  export async function syncExternals(ctx, account) {
200
1550
  if (!ctx.get('account', account))
201
1551
  return;
202
1552
  await ctx.write('account', account, { external_accounts: externalList(account, newest(ctx, 'external_account').filter((e) => e.account === account)) }, 'account.external_accounts');
203
1553
  }
1554
+ /** A decision over the rows and the write it makes, taken together: the read and the write follow one another with
1555
+ * nothing between them (a code spent once, a connection revoked once). */
1556
+ export async function decide(ctx, decision) {
1557
+ const d = decision((resource) => ctx.rowsRaw(resource));
1558
+ if (d.write)
1559
+ await ctx.write(d.write.resource, d.write.id, d.write.fields, d.write.operation);
1560
+ return d.value;
1561
+ }
1562
+ export const catalog = (ctx, data) => ctx.reply({ object: 'list', url: path(ctx), has_more: false, data });
1563
+ export const orderMissing = (ctx, id) => fail(ctx, `No such order: '${id}'`, 404, 'resource_missing');
1564
+ /** Each pending report run completes, caught up before anything is answered: "When the run completes, Stripe updates
1565
+ * the object, and it has a status of succeeded. It also has a nested result object, containing a URL that you can use
1566
+ * to access the file", the guide's example a File of purpose report_run and type csv (docs.stripe.com/reports/api).
1567
+ * "Most runs complete within a few minutes" (the same page). Where the documentation stops and the twin decides: a run
1568
+ * completes one minute after it was created (the guide's example: created 1580832900, succeeded_at 1580832960), and
1569
+ * its CSV holds the report type's header row alone (its size that row's bytes). */
1570
+ export async function finishReportRuns(ctx) {
1571
+ for (const run of ctx.rowsRaw('reporting.report_run')) {
1572
+ if (run.status !== 'pending')
1573
+ continue;
1574
+ const done = Number(run.created) + 60;
1575
+ if (Number(nowUnix(ctx.occurredAt)) < done)
1576
+ continue;
1577
+ const id = String(run.id);
1578
+ const header = 'category,currency,description,net_amount\n';
1579
+ const file = await created(ctx, 'file', { purpose: 'report_run' }, {
1580
+ filename: `${String(run.report_type)}_${id}.csv`, size: header.length, type: 'csv', title: null, expires_at: null, url: null,
1581
+ links: { object: 'list', data: [], has_more: false, url: '' },
1582
+ });
1583
+ await ctx.write('file', String(file.id), { url: `https://files.twin.local/v1/files/${String(file.id)}/contents` }, 'file.updated');
1584
+ ctx.legal('reporting.report_run', 'status', ctx.call.operation.id, 'pending', 'succeeded', id, 'vendor');
1585
+ await ctx.write('reporting.report_run', id, { status: 'succeeded', succeeded_at: done, result: ctx.get('file', String(file.id)) }, 'reporting.report_run.succeeded');
1586
+ }
1587
+ }
1588
+ /** A feature whose lookup_key an unarchived feature already has: "A unique key you provide as your own system identifier"
1589
+ * (docs.stripe.com/api/entitlements/feature/create). */
1590
+ export function lookupKeyTaken(ctx, lookupKey) {
1591
+ return fail(ctx, `A feature with lookup_key '${lookupKey}' already exists.`, 400, 'resource_already_exists');
1592
+ }
1593
+ export const pmMissing = (ctx, id) => fail(ctx, `No such payment_method: '${id}'`, 404, 'resource_missing');
1594
+ /** The card a charge was paid with, as its payment_method_details reports it
1595
+ * (docs.stripe.com/api/charges/object#charge_object-payment_method_details): a stored PaymentMethod's card, a
1596
+ * documented test name (pm_card_* or tok_*, the same cards), or a raw card; undefined for anything else. */
1597
+ export function paymentMethodDetails(ctx, ref) {
1598
+ let card;
1599
+ // a payment method of another kind (a US bank account) is described by its own block, as the charge reports it
1600
+ const stored = typeof ref === 'string' && ref ? ctx.get(PM, ref) : undefined;
1601
+ // Stripe's US bank account test methods (pm_us_bank_account, pm_usBankAccount_*) are its test bank's account
1602
+ const pm = stored ?? (typeof ref === 'string' && /^pm_us_?bank_?account/i.test(ref) ? { type: 'us_bank_account', ...paymentMethodSubObject('us_bank_account', {}) } : undefined);
1603
+ if (pm && typeof pm.type === 'string' && pm.type !== 'card') {
1604
+ const own = pm[pm.type] ?? {};
1605
+ const pick = ['account_holder_type', 'account_type', 'bank_name', 'fingerprint', 'last4', 'routing_number'];
1606
+ return { type: pm.type, [pm.type]: Object.fromEntries(pick.filter((k) => own[k] !== undefined).map((k) => [k, own[k]])) };
1607
+ }
1608
+ if (typeof ref === 'string' && ref) {
1609
+ const stored = ctx.get(PM, ref)?.card;
1610
+ if (stored)
1611
+ card = stored;
1612
+ else
1613
+ card = testCardOf(ref);
1614
+ }
1615
+ else if (ref && typeof ref === 'object')
1616
+ card = paymentMethodSubObject('card', { card: ref }).card;
1617
+ if (!card)
1618
+ return undefined;
1619
+ return {
1620
+ type: 'card',
1621
+ card: {
1622
+ brand: card.brand, last4: card.last4, exp_month: card.exp_month, exp_year: card.exp_year, funding: card.funding ?? 'credit',
1623
+ country: card.country ?? 'US', network: card.brand, fingerprint: card.fingerprint ?? null,
1624
+ checks: card.checks ?? { address_line1_check: null, address_postal_code_check: null, cvc_check: 'pass' },
1625
+ three_d_secure: null, wallet: null, installments: null, mandate: null,
1626
+ },
1627
+ };
1628
+ }
1629
+ // attaching needs an existing customer; a well-known test token (pm_card_visa, …) is a template:
1630
+ // attaching it mints a new PaymentMethod on the customer carrying that card's outcome
1631
+ /** Whether a payment method can be attached: a stored one, or a documented test name that attaching mints. */
1632
+ export const attachable = (ctx, id) => !!ctx.get(PM, id) || (id.startsWith('pm_card_') && (id in TEST_TOKEN_DECLINES || id in AFTER_SUCCESS_CARDS));
1633
+ export async function attachPaymentMethod(ctx, id, customer) {
1634
+ if (!ctx.get(PM, id))
1635
+ return materializeTestMethod(ctx, id, customer);
1636
+ return ctx.write(PM, id, { customer }, 'payment_method.attach');
1637
+ }
1638
+ /** A documented test name (pm_card_visa, pm_card_visa_chargeDeclined, …) made a real PaymentMethod, as Stripe
1639
+ * does when one is used: the brand and number it stands for (docs.stripe.com/testing#cards) and its outcome. */
1640
+ export async function materializeTestMethod(ctx, id, customer) {
1641
+ const card = TEST_PM_CARDS[id] ?? { brand: 'visa', number: '4242424242424242' };
1642
+ const sub = paymentMethodSubObject('card', { card: { number: card.number } });
1643
+ sub.card.brand = card.brand;
1644
+ const afterSuccess = AFTER_SUCCESS_CARDS[id]?.outcome;
1645
+ return created(ctx, PM, { type: 'card', customer, livemode: false, billing_details: noBilling, ...sub, _declineOutcome: TEST_TOKEN_DECLINES[id] ?? null, ...(afterSuccess ? { _afterSuccess: afterSuccess } : {}) }, {});
1646
+ }
1647
+ /** A PaymentMethod made from a confirm's `payment_method_data` ("If provided, this hash will be used to create a
1648
+ * PaymentMethod", docs.stripe.com/api/setup_intents/confirm, docs.stripe.com/api/payment_intents/confirm), or none. */
1649
+ export async function methodFromData(ctx, data) {
1650
+ if (!data || typeof data !== 'object')
1651
+ return undefined;
1652
+ const d = data;
1653
+ const type = typeof d.type === 'string' ? d.type : 'card';
1654
+ const made = await created(ctx, PM, {}, {
1655
+ type, customer: null, livemode: false, metadata: {},
1656
+ billing_details: { ...noBilling, ...(d.billing_details ?? {}) },
1657
+ ...paymentMethodSubObject(type, d),
1658
+ // what follows its card's success (a dispute, funds straight to available), as a PaymentMethod created directly carries
1659
+ ...(type === 'card' && afterSuccessOf(ctx, d.card) ? { _afterSuccess: afterSuccessOf(ctx, d.card) } : {}),
1660
+ });
1661
+ return String(made.id);
1662
+ }
1663
+ /** Whether an intent sets up or pays with a bank account by micro-deposits because it asks for them:
1664
+ * payment_method_options[us_bank_account][verification_method]=microdeposits with a us_bank_account method
1665
+ * ("Microdeposit only verification", docs.stripe.com/payments/ach-direct-debit/set-up-payment). */
1666
+ export function microdepositsAsked(ctx, params, intent, pm) {
1667
+ const options = (params.payment_method_options ?? intent.payment_method_options)?.us_bank_account;
1668
+ return options?.verification_method === 'microdeposits' && !!pm && ctx.get(PM, pm)?.type === 'us_bank_account';
1669
+ }
1670
+ export const scheduleMissing = (ctx, id) => fail(ctx, `No such subscription_schedule: '${id}'`, 404, 'resource_missing');
1671
+ /** A schedule's phases as Stripe answers them (the served spec's subscription_schedule_phase_configuration): each
1672
+ * starts where the last ended (the first at the schedule's start_date, "now" or a timestamp), and ends at its
1673
+ * end_date or after its duration ("duration: interval, interval_count", the served spec's phases[] parameter) on the
1674
+ * calendar; its currency is its first price's, each item carries its price, quantity and (empty) discounts. Where the
1675
+ * documentation stops and the twin decides: a phase with neither lasts one interval of its first price. */
1676
+ export function phasesOf(ctx, given, start) {
1677
+ let cursor = start;
1678
+ return given.map((p) => {
1679
+ const items = (Array.isArray(p.items) ? p.items : []).map((it) => ({ price: it.price ?? null, quantity: Math.max(1, Math.trunc(Number(it.quantity) || 1)), discounts: [], metadata: {}, tax_rates: [], billing_thresholds: null }));
1680
+ const price = typeof items[0]?.price === 'string' ? ctx.get('price', items[0].price) : undefined;
1681
+ const recurring = price?.recurring ?? {};
1682
+ const d = p.duration && typeof p.duration === 'object' ? p.duration : undefined;
1683
+ const startDate = cursor;
1684
+ const end = p.end_date !== undefined && p.end_date !== 'now' ? Number(p.end_date)
1685
+ : d ? addBillingInterval(startDate, String(d.interval), Math.max(1, Number(d.interval_count) || 1))
1686
+ : addBillingInterval(startDate, String(recurring.interval ?? 'month'), Math.max(1, Number(recurring.interval_count) || 1));
1687
+ cursor = end;
1688
+ return {
1689
+ add_invoice_items: [], application_fee_percent: null, automatic_tax: { enabled: false, liability: null, disabled_reason: null }, billing_cycle_anchor: null,
1690
+ billing_thresholds: null, collection_method: null, currency: String(price?.currency ?? 'usd'), default_payment_method: null, default_tax_rates: [],
1691
+ description: null, discounts: [], end_date: end, invoice_settings: null, items, metadata: p.metadata && typeof p.metadata === 'object' ? p.metadata : {},
1692
+ on_behalf_of: null, proration_behavior: typeof p.proration_behavior === 'string' ? p.proration_behavior : 'create_prorations', start_date: startDate,
1693
+ transfer_data: null, trial_end: null,
1694
+ };
1695
+ });
1696
+ }
1697
+ export function end(kind) {
1698
+ return async (ctx) => {
1699
+ const id = at(ctx, 'schedule');
1700
+ const sched = ctx.get(SS, id);
1701
+ if (!sched)
1702
+ return scheduleMissing(ctx, id);
1703
+ const refused = ctx.legal(SS, 'status', kind === 'cancel' ? 'PostSubscriptionSchedulesScheduleCancel' : 'PostSubscriptionSchedulesScheduleRelease', sched.status, undefined, id);
1704
+ if (refused)
1705
+ return ctx.refuse(refused);
1706
+ const now = nowUnix(ctx.occurredAt);
1707
+ const subId = typeof sched.subscription === 'string' ? sched.subscription : undefined;
1708
+ const sub = subId ? ctx.get('subscription', subId) : undefined;
1709
+ // "Cancels a subscription schedule and its associated subscription immediately (if the subscription schedule has an
1710
+ // active subscription)" (docs.stripe.com/api/subscription_schedules/cancel); releasing "will remove its subscription
1711
+ // property and set the subscription's ID to the released_subscription property", leaving the subscription in place
1712
+ // (docs.stripe.com/api/subscription_schedules/release)
1713
+ if (kind === 'cancel' && subId && sub && sub.status !== 'canceled') {
1714
+ ctx.legal('subscription', 'status', 'PostSubscriptionSchedulesScheduleCancel', String(sub.status), 'canceled', subId);
1715
+ await ctx.write('subscription', subId, { status: 'canceled', canceled_at: now, ended_at: now }, 'subscription.cancel');
1716
+ }
1717
+ if (kind === 'release' && subId && sub)
1718
+ await ctx.write('subscription', subId, { schedule: null }, 'subscription.update');
1719
+ const fields = kind === 'cancel'
1720
+ ? { status: 'canceled', canceled_at: now, current_phase: null }
1721
+ : { status: 'released', released_at: now, released_subscription: subId ?? null, subscription: null, current_phase: null };
1722
+ return ctx.reply(await ctx.write(SS, id, fields, `subscription_schedule.${kind}`));
1723
+ };
1724
+ }
1725
+ /** A token asked for none of what a token holds. */
1726
+ export function noTokenSource(ctx) {
1727
+ return fail(ctx, 'You must provide one of the following parameters: card, bank_account, pii.', 400, 'parameter_missing');
1728
+ }
1729
+ /** A bank account token's account, as the legacy BankAccount object holds it. */
1730
+ export function bankAccountOf(ctx, ba) {
1731
+ const acct = typeof ba.account_number === 'string' ? ba.account_number.replace(/\D/g, '') : '';
1732
+ return {
1733
+ id: ctx.mint('external_account'), object: 'bank_account', account_holder_name: ba.account_holder_name ?? null,
1734
+ account_holder_type: ba.account_holder_type ?? null, bank_name: 'STRIPE TEST BANK',
1735
+ country: ba.country ?? 'US', currency: ba.currency ?? 'usd', fingerprint: 'twin_ba_fp',
1736
+ last4: acct ? acct.slice(-4) : '6789', routing_number: ba.routing_number ?? '110000000', status: 'new',
1737
+ };
1738
+ }
1739
+ export const productMissing = (ctx, id) => fail(ctx, `No such product: '${id}'`, 404, 'resource_missing');
1740
+ // a product with prices cannot be deleted; Stripe says to deactivate it instead
1741
+ /** A product deleted while it has prices: "Deleting a product is only possible if it has no prices associated with it"
1742
+ * (docs.stripe.com/api/products/delete). */
1743
+ export function pricedProductRefused(ctx) {
1744
+ return fail(ctx, 'You cannot delete the product because it has one or more prices. Deactivate the product instead.', 400, 'resource_already_exists');
1745
+ }
1746
+ // ── features: an entitlements feature granted to a product, at most once ──
1747
+ /** A feature attached to a product that already has it. */
1748
+ export function alreadyAttached(ctx) {
1749
+ return fail(ctx, 'This feature is already attached to the product.', 400, 'resource_already_exists');
1750
+ }
1751
+ export const productFeature = (ctx) => {
1752
+ const pf = ctx.get('product_feature', at(ctx, 'id'));
1753
+ // the product a feature is attached to is its address, not a field Stripe answers
1754
+ return pf && kept(ctx, 'product_feature', pf, '_product') === at(ctx, 'product') ? pf : undefined;
1755
+ };
1756
+ export const featureMissing = (ctx) => fail(ctx, `No such product feature: '${at(ctx, 'id')}'`, 404, 'resource_missing');
1757
+ // ── prices ──
1758
+ /** A tiered price: tiers_mode and tiers, each tier with up_to and a unit or flat amount; no single unit_amount, and
1759
+ * the last tier's up_to null (∞) (docs.stripe.com/products-prices/pricing-models#tiered-pricing). */
1760
+ export async function tieredPrice(ctx, params) {
1761
+ const mode = typeof params.tiers_mode === 'string' ? params.tiers_mode : '';
1762
+ if (mode !== 'graduated' && mode !== 'volume')
1763
+ return fail(ctx, 'Missing required param: tiers_mode (graduated or volume).', 400, 'parameter_missing');
1764
+ const rawTiers = Array.isArray(params.tiers) ? params.tiers : [];
1765
+ if (rawTiers.length === 0)
1766
+ return fail(ctx, 'Missing required param: tiers.', 400, 'parameter_missing');
1767
+ const tiers = rawTiers.map((t) => {
1768
+ const upToRaw = t.up_to;
1769
+ return {
1770
+ up_to: upToRaw === 'inf' || upToRaw === undefined || upToRaw === null ? null : Math.trunc(Number(upToRaw)),
1771
+ unit_amount: t.unit_amount !== undefined ? Math.trunc(Number(t.unit_amount)) : null,
1772
+ unit_amount_decimal: t.unit_amount !== undefined ? String(Math.trunc(Number(t.unit_amount))) : null,
1773
+ flat_amount: t.flat_amount !== undefined ? Math.trunc(Number(t.flat_amount)) : null,
1774
+ flat_amount_decimal: t.flat_amount !== undefined ? String(Math.trunc(Number(t.flat_amount))) : null,
1775
+ };
1776
+ });
1777
+ if (tiers.some((t) => t.unit_amount === null && t.flat_amount === null))
1778
+ return fail(ctx, 'Each tier must specify a unit_amount or flat_amount.', 400, 'parameter_invalid_empty');
1779
+ const { tiers: _t, ...rest } = params;
1780
+ return ctx.reply(await created(ctx, 'price', { ...rest, billing_scheme: 'tiered', tiers_mode: mode, tiers, unit_amount: null }, { active: true, livemode: false, type: rest.recurring ? 'recurring' : 'one_time', metadata: {} }));
1781
+ }
1782
+ /** Whether the intent's caller named its methods: on create (TYPES_GIVEN), or by an update that set its
1783
+ * payment_method_types ("Alternatively, update the allowed payment_method_types for this PaymentIntent",
1784
+ * methodNotAllowed below), which the generic update writes as given, so the intent's history holds it. */
1785
+ export const typesGiven = (ctx, id) => ctx.rowsRaw(TYPES_GIVEN).some((r) => r.id === id)
1786
+ || ctx.history(PI, id).some((e) => e.operation !== 'payment_intent.create' && Array.isArray(e.fields?.payment_method_types));
1787
+ export const paymentIntentsSend = (ctx, r) => ctx.reply(r.body, r.status);
1788
+ export async function mintCharge(ctx, piId, pi, amount) {
1789
+ const already = ctx.get(PI, piId)?.latest_charge;
1790
+ if (typeof already === 'string' && ctx.get('charge', already)?.status === 'succeeded')
1791
+ return already;
1792
+ const chargeId = ctx.mint('charge');
1793
+ const bt = await settleCharge(ctx, chargeId, amount, String(pi.currency ?? 'usd'), typeof pi.payment_method === 'string' ? pi.payment_method : undefined);
1794
+ await created(ctx, 'charge', {
1795
+ id: chargeId, amount, currency: pi.currency ?? 'usd', payment_intent: piId,
1796
+ ...(typeof pi.customer === 'string' ? { customer: pi.customer } : {}),
1797
+ ...(typeof pi.payment_method === 'string' ? { payment_method: pi.payment_method } : {}),
1798
+ ...(typeof pi.invoice === 'string' ? { invoice: pi.invoice } : {}),
1799
+ ...intentCarries(pi),
1800
+ }, { ...chargeDefaults(chargeId, amount, true, ctx.occurredAt), balance_transaction: bt, payment_method_details: paymentMethodDetails(ctx, pi.payment_method) ?? null }, { operation: 'charge.succeeded' });
1801
+ await afterCharge(ctx, chargeId, { amount, currency: String(pi.currency ?? 'usd'), payment_intent: piId, application_fee_amount: pi.application_fee_amount, destination: pi.transfer_data?.destination }, pi.payment_method);
1802
+ return chargeId;
1803
+ }
1804
+ /** A manual-capture intent's authorization: "Separate payment authorization and capture to create a charge now, but
1805
+ * capture funds later", and on capture "A partial capture automatically releases the remaining amount"
1806
+ * (docs.stripe.com/payments/place-a-hold-on-a-payment-method). The charge succeeded and is not captured, holding the
1807
+ * amount until capture_before, with nothing on the balance yet. Written as
1808
+ * charge.succeeded, as a charge made with capture=false is (charges.ts); its capture writes charge.captured
1809
+ * (captureAuthorization). Where the documentation stops and the twin decides: the events page names no event for an
1810
+ * authorization of its own, so the authorized charge sends charge.succeeded, its status being succeeded. */
1811
+ export async function authorizeCharge(ctx, piId, pi, amount) {
1812
+ const chargeId = ctx.mint('charge');
1813
+ await created(ctx, 'charge', {
1814
+ id: chargeId, amount, currency: pi.currency ?? 'usd', payment_intent: piId,
1815
+ ...(typeof pi.customer === 'string' ? { customer: pi.customer } : {}),
1816
+ ...(typeof pi.payment_method === 'string' ? { payment_method: pi.payment_method } : {}),
1817
+ ...(typeof pi.invoice === 'string' ? { invoice: pi.invoice } : {}),
1818
+ ...intentCarries(pi),
1819
+ }, { ...chargeDefaults(chargeId, amount, false, ctx.occurredAt), balance_transaction: null, payment_method_details: paymentMethodDetails(ctx, pi.payment_method) ?? null }, { operation: 'charge.succeeded' });
1820
+ return chargeId;
1821
+ }
1822
+ /**
1823
+ * An intent an invoice collects through succeeded (the default_incomplete subscription flow: create
1824
+ * now, confirm client-side): its invoice is paid and, when it is a subscription's first, the
1825
+ * subscription leaves incomplete for active. Both are moves the invoice and subscription machines
1826
+ * declare under the confirming operation. No-op when the intent names no invoice, or the invoice is
1827
+ * already paid (a duplicate confirm) or void (voiding ends what it collects).
1828
+ */
1829
+ export async function payInvoiceOf(ctx, pi, operationId, actor) {
1830
+ const invoiceId = typeof pi.invoice === 'string' ? pi.invoice : undefined;
1831
+ const invoice = invoiceId ? ctx.get('invoice', invoiceId) : undefined;
1832
+ if (!invoiceId || !invoice || invoice.status === 'paid' || invoice.status === 'void')
1833
+ return;
1834
+ if (ctx.legal('invoice', 'status', operationId, invoice.status, 'paid', invoiceId, actor))
1835
+ return;
1836
+ const amount = Number(invoice.total ?? invoice.amount_due) || 0;
1837
+ // `invoice.pay` is the event the standalone pay action sends: invoice.paid
1838
+ await ctx.write('invoice', invoiceId, {
1839
+ status: 'paid', paid: true, amount_paid: amount, amount_remaining: 0,
1840
+ status_transitions: { ...(invoice.status_transitions ?? {}), paid_at: nowUnix(ctx.occurredAt) },
1841
+ }, 'invoice.pay');
1842
+ const subId = typeof invoice.subscription === 'string' ? invoice.subscription : undefined;
1843
+ const sub = subId ? ctx.get('subscription', subId) : undefined;
1844
+ if (subId && sub && sub.status === 'incomplete' && !ctx.legal('subscription', 'status', operationId, sub.status, 'active', subId, actor)) {
1845
+ await ctx.write('subscription', subId, { status: 'active' }, 'subscription.update');
1846
+ }
1847
+ }
1848
+ /** The intent the path names, and the machine's refusal when this operation may not move it. */
1849
+ export function paymentIntentsLoad(ctx, operationId) {
1850
+ const pi = ctx.id ? ctx.get(PI, ctx.id) : undefined;
1851
+ if (!pi)
1852
+ return { answer: ctx.notFound(PI, String(ctx.id)) };
1853
+ const refusal = ctx.legal(PI, 'status', operationId, pi.status);
1854
+ return refusal ? { answer: ctx.refuse(refusal) } : { pi };
1855
+ }
1856
+ /** The type of the payment method an intent is paid with: its PaymentMethod's, or a documented test name's (pm_card_*,
1857
+ * tok_* a card; pm_usBankAccount_* a US bank account), else unknown. */
1858
+ export function methodType(ctx, method) {
1859
+ if (typeof method !== 'string' || !method)
1860
+ return undefined;
1861
+ const row = ctx.get('payment_method', method);
1862
+ if (row)
1863
+ return typeof row.type === 'string' ? row.type : undefined;
1864
+ if (/^pm_us_?bank_?account/i.test(method))
1865
+ return 'us_bank_account';
1866
+ if (/^(pm_card_|tok_)/.test(method))
1867
+ return 'card';
1868
+ return undefined;
1869
+ }
1870
+ /** A confirm with a payment method of a type the intent's caller did not allow, refused as Stripe refuses it: "The
1871
+ * PaymentMethod provided (card_present) is not allowed for this PaymentIntent. Please attach a PaymentMethod of one of
1872
+ * the following types: card. Alternatively, update the allowed payment_method_types for this PaymentIntent to include
1873
+ * "card_present"" (a real answer quoted in github.com/stripe/stripe-terminal-react-native/issues/745, an intent made
1874
+ * with payment_method_types ['card']). Only a list the caller gave restricts: without one Stripe offers the methods
1875
+ * the account enables, which the twin does not model. Where the evidence stops: that answer's code is not quoted; the
1876
+ * twin answers payment_intent_invalid_parameter ("One or more provided parameters wasn't allowed for the given
1877
+ * operation on the PaymentIntent", docs.stripe.com/error-codes), param payment_method. */
1878
+ export function methodNotAllowed(ctx, existing, params) {
1879
+ const types = Array.isArray(params.payment_method_types) ? params.payment_method_types.map(String)
1880
+ : Array.isArray(params.allowed_payment_method_types) ? params.allowed_payment_method_types.map(String)
1881
+ : typesGiven(ctx, String(existing.id)) && Array.isArray(existing.payment_method_types) ? existing.payment_method_types.map(String) : undefined;
1882
+ if (!types)
1883
+ return undefined;
1884
+ const type = methodType(ctx, params.payment_method ?? existing.payment_method);
1885
+ if (!type || types.includes(type))
1886
+ return undefined;
1887
+ return ctx.refuse({ status: 400, code: 'payment_intent_invalid_parameter', param: 'payment_method', message: `The PaymentMethod provided (${type}) is not allowed for this PaymentIntent. Please attach a PaymentMethod of one of the following types: ${types.join(', ')}. Alternatively, update the allowed payment_method_types for this PaymentIntent to include "${type}"` });
1888
+ }
1889
+ /** Confirm an intent the confirm machine allows to move: the confirm action, and a create with `confirm=true`. */
1890
+ export async function confirmIntent(ctx, existing, params) {
1891
+ const id = String(existing.id);
1892
+ // a payment names a mandate only an active one authorizes: an inactive mandate "was rejected, revoked, or previously
1893
+ // used, and may not be used to initiate future payments" (docs.stripe.com/api/mandates/object), refused as
1894
+ // payment_intent_mandate_invalid, "The provided mandate is invalid and can't be used for the payment intent"
1895
+ // (docs.stripe.com/error-codes)
1896
+ const named = typeof params.mandate === 'string' ? params.mandate : typeof existing.mandate === 'string' ? existing.mandate : undefined;
1897
+ if (named && ctx.get('mandate', named)?.status !== 'active')
1898
+ return fail(ctx, 'The provided mandate is invalid and can\'t be used for the payment intent.', 400, 'payment_intent_mandate_invalid');
1899
+ const notAllowed = methodNotAllowed(ctx, existing, params);
1900
+ if (notAllowed)
1901
+ return notAllowed;
1902
+ // a declining test card leaves the intent needing a payment method, with the error on it and a 402
1903
+ const decline = declineFor(finder(ctx), params, existing);
1904
+ if (decline) {
1905
+ // Stripe records the attempt: a failed charge the intent's latest_charge names, the declined card on
1906
+ // last_payment_error (a test name made a real PaymentMethod), and no payment_method left on the intent
1907
+ // (docs.stripe.com/payments/paymentintents/lifecycle, docs.stripe.com/declines)
1908
+ const ref = params.payment_method ?? existing.payment_method;
1909
+ let pmId = typeof ref === 'string' ? ref : undefined;
1910
+ if (pmId && !ctx.get('payment_method', pmId) && pmId.startsWith('pm_card_'))
1911
+ pmId = String((await materializeTestMethod(ctx, pmId, typeof existing.customer === 'string' ? existing.customer : null)).id);
1912
+ const pm = pmId ? ctx.get('payment_method', pmId) : undefined;
1913
+ const amount = Number(params.amount ?? existing.amount) || 0;
1914
+ const chargeId = ctx.mint('charge');
1915
+ await created(ctx, 'charge', {
1916
+ id: chargeId, amount, currency: existing.currency ?? 'usd', payment_intent: id,
1917
+ ...(typeof existing.customer === 'string' ? { customer: existing.customer } : {}),
1918
+ ...(pmId ? { payment_method: pmId } : {}),
1919
+ ...intentCarries({ ...existing, ...params }),
1920
+ }, {
1921
+ ...chargeDefaults(chargeId, amount, false, ctx.occurredAt), status: 'failed', paid: false, captured: false, capture_before: null,
1922
+ failure_code: decline.code, failure_message: decline.message, balance_transaction: null,
1923
+ outcome: { type: 'issuer_declined', network_status: 'declined_by_network', reason: decline.decline_code ?? decline.code, risk_level: 'normal', seller_message: 'The bank did not return any further details with this decline.' },
1924
+ payment_method_details: pmId ? paymentMethodDetails(ctx, pmId) ?? null : null,
1925
+ }, { operation: 'charge.failed' }); // "Occurs whenever a failed charge attempt occurs." (docs.stripe.com/api/events/types)
1926
+ const { payment_method: _pm, ...rest } = params;
1927
+ const last_payment_error = { type: 'card_error', code: decline.code, ...(decline.decline_code ? { decline_code: decline.decline_code } : {}), message: decline.message, param: 'card', charge: chargeId, payment_method: pm ?? null };
1928
+ const failed = await ctx.write(PI, id, { ...rest, payment_method: null, latest_charge: chargeId, status: 'requires_payment_method', last_payment_error }, 'payment_intent.payment_failed');
1929
+ return paymentIntentsSend(ctx, cardError(decline, { payment_intent: failed, charge: chargeId }));
1930
+ }
1931
+ // a bank debit that needs micro-deposit verification waits in requires_action
1932
+ const method = typeof params.payment_method === 'string' ? params.payment_method : typeof existing.payment_method === 'string' ? existing.payment_method : undefined;
1933
+ // a bank transfer is paid from the customer's cash balance, or waits for the transfer (fundFromCashBalance)
1934
+ if (method && ctx.get('payment_method', method)?.type === 'customer_balance')
1935
+ return ctx.reply(ctx.expand(PI, await fundFromCashBalance(ctx, { ...existing, ...params }, params)));
1936
+ if (requiresMicrodeposits(params, existing) || microdepositsAsked(ctx, params, existing, method)) {
1937
+ return ctx.reply(ctx.expand(PI, await ctx.write(PI, id, { ...params, status: 'requires_action', next_action: microdepositsNextAction(), last_payment_error: null }, 'payment_intent.requires_action')));
1938
+ }
1939
+ // 3DS: the first confirm asks for authentication; a second one completes it
1940
+ if (existing.status !== 'requires_action' && requiresAuthentication(params, existing)) {
1941
+ return ctx.reply(ctx.expand(PI, await ctx.write(PI, id, { ...params, status: 'requires_action', next_action: threeDsNextAction(), last_payment_error: null }, 'payment_intent.requires_action')));
1942
+ }
1943
+ // manual capture authorizes and waits for /capture: the authorization is a charge, succeeded and not captured, the
1944
+ // intent's latest_charge
1945
+ if ((params.capture_method ?? existing.capture_method) === 'manual') {
1946
+ const amount = Number(existing.amount ?? params.amount ?? 0);
1947
+ const charge = await authorizeCharge(ctx, id, { ...existing, ...params }, amount);
1948
+ return ctx.reply(ctx.expand(PI, await ctx.write(PI, id, { ...params, status: 'requires_capture', amount_capturable: amount, amount_received: 0, next_action: null, last_payment_error: null, latest_charge: charge }, 'payment_intent.amount_capturable_updated')));
1949
+ }
1950
+ // a bank debit is submitted and settles later (beginBankDebit)
1951
+ if (isBankDebit(ctx, method))
1952
+ return ctx.reply(ctx.expand(PI, await beginBankDebit(ctx, id, params)));
1953
+ const amount = Number(existing.amount ?? params.amount ?? 0);
1954
+ // a succeeded intent has a Charge, and latest_charge names it: what a refund of it lands on
1955
+ const charge = await mintCharge(ctx, id, { ...existing, ...params }, amount);
1956
+ const body = await ctx.write(PI, id, { ...params, status: 'succeeded', amount_received: amount, next_action: null, last_payment_error: null, latest_charge: charge }, 'payment_intent.confirm');
1957
+ await payInvoiceOf(ctx, body, 'PostPaymentIntentsIntentConfirm');
1958
+ return ctx.reply(ctx.expand(PI, body));
1959
+ }
1960
+ /** An increment that does not raise the authorization. */
1961
+ export function incrementTooSmall(ctx) {
1962
+ return ctx.refuse({ status: 400, code: 'parameter_invalid_integer', message: 'The new amount must be greater than the current amount of the PaymentIntent.' });
1963
+ }
1964
+ // ── a bank debit is submitted, and settles ──
1965
+ //
1966
+ // ACH Direct Debit "is a delayed notification payment method ... The PaymentIntent you create initially has a status
1967
+ // of `processing`. After the payment has succeeded, the PaymentIntent status is updated from `processing` to
1968
+ // `succeeded`"; verification by micro-deposits returns "a status of `processing`, and sends a payment_intent.processing
1969
+ // webhook event"; and, the twin being a test-mode account: "Test transactions settle instantly and are added to your
1970
+ // available test balance. This behavior differs from live mode, where transactions can take multiple days to settle"
1971
+ // (docs.stripe.com/payments/ach-direct-debit/accept-a-payment?payment-ui=direct-api). A confirm answers processing; the
1972
+ // debit succeeds at that same instant of the World's clock, caught up before the next request is answered (as
1973
+ // renewals and payouts are). The page's test accounts give each debit its outcome: pm_usBankAccount_success
1974
+ // (000123456789) "The payment succeeds"; pm_usBankAccount_processing (000000000009) "The payment stays in processing
1975
+ // indefinitely". Where the documentation stops and the twin decides: its Charge is made when it succeeds (the page
1976
+ // names none while it processes); the failing test accounts (closed, no account, insufficient funds, debit not
1977
+ // authorized, invalid currency, dispute, weekly limit, Radar block) are not modelled and succeed.
1978
+ /** Whether the method an intent is confirmed with is a bank debit: a us_bank_account PaymentMethod, or one of Stripe's
1979
+ * test bank accounts named as one (pm_usBankAccount_*, pm_us_bank_account). */
1980
+ export function isBankDebit(ctx, method) {
1981
+ if (typeof method !== 'string')
1982
+ return false;
1983
+ return ctx.get('payment_method', method)?.type === 'us_bank_account' || /^pm_us_?bank_?account/i.test(method);
1984
+ }
1985
+ /** Whether a bank debit stays processing: the page's pm_usBankAccount_processing, or its account 000000000009. */
1986
+ export function staysProcessing(ctx, method) {
1987
+ if (method === 'pm_usBankAccount_processing')
1988
+ return true;
1989
+ const bank = typeof method === 'string' ? ctx.get('payment_method', method)?.us_bank_account : undefined;
1990
+ return bank?.last4 === '0009';
1991
+ }
1992
+ /** A bank debit submitted: the intent processes, nothing received yet, sent as payment_intent.processing; it is due to
1993
+ * settle at this instant, unless its test account stays processing. */
1994
+ export async function beginBankDebit(ctx, id, fields) {
1995
+ const method = fields.payment_method ?? ctx.get(PI, id)?.payment_method;
1996
+ return ctx.write(PI, id, { ...fields, status: 'processing', amount_received: 0, next_action: null, last_payment_error: null, _settles_at: staysProcessing(ctx, method) ? null : Number(nowUnix(ctx.occurredAt)) }, 'payment_intent.processing');
1997
+ }
1998
+ /** Time's settling of bank debits, caught up to the World's clock: each processing debit whose moment has come succeeds
1999
+ * then, its Charge made (naming the mandate it ran under) and the invoice it collects for paid. */
2000
+ export async function settleBankDebits(ctx) {
2001
+ const now = Number(nowUnix(ctx.occurredAt));
2002
+ const due = ctx.rowsRaw(PI).filter((p) => p.status === 'processing' && typeof p._settles_at === 'number' && p._settles_at <= now).sort((a, b) => Number(a._settles_at) - Number(b._settles_at));
2003
+ for (const pi of due) {
2004
+ const id = String(pi.id);
2005
+ const c = await at_(ctx)(Number(pi._settles_at));
2006
+ c.legal(PI, 'status', ctx.call.operation.id, 'processing', 'succeeded', id, 'vendor');
2007
+ const amount = Number(pi.amount) || 0;
2008
+ const charge = await mintCharge(c, id, pi, amount);
2009
+ // the charge names the mandate its bank debit ran under (payment_method_details.us_bank_account.mandate)
2010
+ const details = (c.get('charge', charge)?.payment_method_details ?? null);
2011
+ const kind = typeof details?.type === 'string' ? details.type : undefined;
2012
+ if (details && kind && typeof pi.mandate === 'string')
2013
+ await c.write('charge', charge, { payment_method_details: { ...details, [kind]: { ...(details[kind] ?? {}), mandate: pi.mandate } } }, 'charge.mandate');
2014
+ const body = await c.write(PI, id, { status: 'succeeded', amount_received: amount, latest_charge: charge, _settles_at: null }, 'payment_intent.succeeded');
2015
+ // a single-use mandate is spent by its payment: "previously used, and may not be used to initiate future payments"
2016
+ const used = typeof pi.mandate === 'string' ? c.get('mandate', pi.mandate) : undefined;
2017
+ if (used && used.type === 'single_use' && used.status === 'active') {
2018
+ c.legal('mandate', 'status', ctx.call.operation.id, 'active', 'inactive', String(used.id), 'vendor');
2019
+ await c.write('mandate', String(used.id), { status: 'inactive' }, 'mandate.updated');
2020
+ }
2021
+ await payInvoiceOf(c, body, ctx.call.operation.id, 'vendor');
2022
+ }
2023
+ }
2024
+ /** A customer_balance intent funded from the customer's cash balance: paid when the balance covers it ("If the customer
2025
+ * already has a balance high enough to cover the payment amount, the PaymentIntent immediately succeeds"), else
2026
+ * waiting for a transfer: "If the customer balance isn’t high enough to cover the request amount, the PaymentIntent
2027
+ * shows a requires_action status ... next_action ... display_bank_transfer_instructions" with the amount_remaining
2028
+ * (docs.stripe.com/payments/bank-transfers/accept-a-payment). Answers the intent as written. */
2029
+ export async function fundFromCashBalance(ctx, pi, fields = {}) {
2030
+ const customer = typeof pi.customer === 'string' ? pi.customer : '';
2031
+ const currency = String(pi.currency ?? 'usd');
2032
+ // what the customer's funded cash balance holds in this currency, from its ledger
2033
+ const available = customer ? ctx.rows('customer_cash_balance_transaction').filter((t) => t.customer === customer && String(t.currency) === currency).reduce((n, t) => n + (Number(t.net_amount) || 0), 0) : 0;
2034
+ const amount = Number(pi.amount) || 0;
2035
+ if (available >= amount) {
2036
+ // the balance pays the intent: its ledger records what was applied
2037
+ await created(ctx, 'customer_cash_balance_transaction', { customer }, { currency, type: 'applied_to_payment', net_amount: -amount, ending_balance: available - amount, applied_to_payment: { payment_intent: String(pi.id) }, livemode: false });
2038
+ const charge = await mintCharge(ctx, String(pi.id), { ...pi, ...fields }, amount);
2039
+ return ctx.write(PI, String(pi.id), { ...fields, status: 'succeeded', next_action: null, amount_received: amount, last_payment_error: null, latest_charge: charge }, 'payment_intent.succeeded');
2040
+ }
2041
+ // not enough yet: the intent waits for the rest, with the instructions to send it
2042
+ const next_action = { type: 'display_bank_transfer_instructions', display_bank_transfer_instructions: { amount_remaining: amount - available, currency, type: 'us_bank_transfer' } };
2043
+ return ctx.write(PI, String(pi.id), { ...fields, status: 'requires_action', next_action }, 'payment_intent.requires_action');
2044
+ }
2045
+ /** Whether a charge's funds go straight to the available balance: a bypass card as a raw number, a test name (pm_card_*
2046
+ * or tok_*), or a stored PaymentMethod made from one (which records it as what follows its success, after-payment.ts);
2047
+ * or a US bank account, stored or one of Stripe's test bank accounts named as one (pm_usBankAccount_*). */
2048
+ export function bypassesPending(ctx, card) {
2049
+ if (!card)
2050
+ return false;
2051
+ const stored = ctx.row('payment_method', card);
2052
+ // the row carries what the method recorded; its type is the vendor object's (ctx.get), not the row's envelope
2053
+ if (stored)
2054
+ return stored._afterSuccess === 'available' || ctx.get('payment_method', card)?.type === 'us_bank_account';
2055
+ if (/^pm_us_?bank_?account/i.test(card))
2056
+ return true;
2057
+ return BYPASS_NUMBERS.has(card.replace(/\D/g, '')) || card.replace(/^tok_/, 'pm_card_') in BYPASS_PENDING_CARDS;
2058
+ }
2059
+ /** The connected account a request acts for, or undefined for the platform. */
2060
+ export const actingAccount = (ctx) => ctx.call.request.headers.get('stripe-account') ?? undefined;
2061
+ /** A ledger entry on an account's balance: the acting account's unless one is named (null names the platform). */
2062
+ export async function write(ctx, fields, account = actingAccount(ctx)) {
2063
+ // a credit available at once waits for the drain's balance.available (settleDueEntries)
2064
+ const unsent = (fields.status ?? 'available') === 'available' && Number(fields.net) > 0 ? { [UNSENT]: true } : {};
2065
+ const bt = await created(ctx, BT, {}, { status: 'available', fee_details: [], description: null, exchange_rate: null, balance_type: 'payments', ...fields, ...unsent, ...(account ? { _account: account } : {}) });
2066
+ return String(bt.id);
2067
+ }
2068
+ /** A captured charge's credit: its amount less the fee, pending two days unless the card bypasses the pending
2069
+ * balance (bypassesPending), when it is due at once. The
2070
+ * caller mints the charge's id first and stores the returned id as the charge's balance_transaction. */
2071
+ export async function settleCharge(ctx, chargeId, amount, currency, card) {
2072
+ const now = Number(nowUnix(ctx.occurredAt));
2073
+ const fee = feeOf(amount);
2074
+ const atOnce = bypassesPending(ctx, card);
2075
+ const id = await write(ctx, {
2076
+ amount, currency, fee, net: amount - fee, type: 'charge', reporting_category: 'charge', source: chargeId,
2077
+ ...(atOnce ? { status: 'available', available_on: now } : { status: 'pending', available_on: now + 2 * DAY }),
2078
+ fee_details: fee ? [{ amount: fee, application: null, currency, description: 'Stripe processing fees', type: 'stripe_fee' }] : [],
2079
+ });
2080
+ return id;
2081
+ }
2082
+ /** A refund's debit, at once, on the acting account's balance unless one is named (null names the platform). */
2083
+ export async function settleRefund(ctx, refundId, amount, currency, account = actingAccount(ctx)) {
2084
+ return write(ctx, { amount: -amount, currency, fee: 0, net: -amount, type: 'refund', reporting_category: 'refund', source: refundId, available_on: Number(nowUnix(ctx.occurredAt)) }, account);
2085
+ }
2086
+ /** What an account had available in a currency at a moment: every entry of its payments balance whose funds had come
2087
+ * due by then. */
2088
+ export function availableAt(ctx, account, currency, at) {
2089
+ let sum = 0;
2090
+ for (const t of ctx.rowsRaw(BT)) {
2091
+ if (!onAccount(t, account) || t.balance_type === 'issuing' || String(t.currency ?? 'usd') !== currency)
2092
+ continue;
2093
+ const due = Number(t.available_on ?? t.created);
2094
+ if (Number.isFinite(due) && due <= at)
2095
+ sum += Number(t.net ?? 0);
2096
+ }
2097
+ return sum;
2098
+ }
2099
+ /** The moments after `from` and up to `until` when funds of an account came due, in order: when a held refund can
2100
+ * next be covered. */
2101
+ export function fundsDueBetween(ctx, account, currency, from, until) {
2102
+ const times = ctx.rowsRaw(BT).filter((t) => onAccount(t, account) && String(t.currency ?? 'usd') === currency).map((t) => Number(t.available_on ?? t.created)).filter((d) => Number.isFinite(d) && d > from && d <= until);
2103
+ return [...new Set(times)].sort((a, b) => a - b);
2104
+ }
2105
+ /** A dispute's debit, at once: the disputed amount and the dispute fee (the twin's 1500 cents, Stripe's US $15). */
2106
+ export async function settleDispute(ctx, disputeId, amount, currency) {
2107
+ const fee = 1500;
2108
+ return write(ctx, {
2109
+ amount: -amount, currency, fee, net: -amount - fee, type: 'adjustment', reporting_category: 'dispute', source: disputeId, available_on: Number(nowUnix(ctx.occurredAt)),
2110
+ fee_details: [{ amount: fee, application: null, currency, description: 'Dispute fee', type: 'stripe_fee' }],
2111
+ });
2112
+ }
2113
+ /** A won dispute's credit, at once: the disputed amount returned (docs.stripe.com/disputes/how-disputes-work). Where the
2114
+ * documentation stops and the twin decides: the dispute fee is not returned. */
2115
+ export async function settleDisputeWon(ctx, disputeId, amount, currency) {
2116
+ return write(ctx, { amount, currency, fee: 0, net: amount, type: 'adjustment', reporting_category: 'dispute_reversal', source: disputeId, available_on: Number(nowUnix(ctx.occurredAt)) });
2117
+ }
2118
+ /** An account's ledger entries an automatic payout has not yet paid out: every entry on its payments balance that no
2119
+ * automatic payout took (the automatic payouts' own debits excluded), as stored. */
2120
+ export function unpaidEntries(ctx, account) {
2121
+ const automatic = new Set(ctx.rowsRaw('payout').filter((p) => p.automatic === true).map((p) => p.id));
2122
+ return ctx.rowsRaw(BT).filter((t) => onAccount(t, account) && t.balance_type !== 'issuing' && !t._payout && !automatic.has(t.source));
2123
+ }
2124
+ /** An automatic payout's debit at `at`, and the entries it pays out marked with it, so the ledger lists them under
2125
+ * the payout (docs.stripe.com/api/balance_transactions/list#balance_transaction_list-payout). */
2126
+ export async function settleAutomaticPayout(ctx, payoutId, amount, currency, account, at, entries) {
2127
+ for (const t of entries)
2128
+ await ctx.write(BT, String(t.id), { _payout: payoutId }, 'balance_transaction.paid_out');
2129
+ return write(ctx, { amount: -amount, currency, fee: 0, net: -amount, type: 'payout', reporting_category: 'payout', source: payoutId, available_on: at, _payout: payoutId }, account ?? null);
2130
+ }
2131
+ /** A payout's debit (negative) or a cancellation's or reversal's credit (positive), at once, on the payout's account. */
2132
+ export async function settlePayout(ctx, payoutId, amount, currency, account = actingAccount(ctx)) {
2133
+ const type = amount < 0 ? 'payout' : 'payout_cancel';
2134
+ return write(ctx, { amount, currency, fee: 0, net: amount, type, reporting_category: type === 'payout' ? 'payout' : 'payout_reversal', source: payoutId, available_on: Number(nowUnix(ctx.occurredAt)) }, account);
2135
+ }
2136
+ /** A transfer's two entries: out of the platform's balance at once, into the destination's when `fromCharge` (its
2137
+ * charge's funds' availability) comes (a plain transfer, with no `fromCharge`, moves funds available already).
2138
+ * Answers the platform's entry. */
2139
+ export async function settleTransfer(ctx, transferId, amount, currency, destination, fromCharge, fromPending = false) {
2140
+ const availableOn = fromCharge ?? Number(nowUnix(ctx.occurredAt));
2141
+ const settled = availableOn <= Number(nowUnix(ctx.occurredAt));
2142
+ // a transfer from a charge's pending funds (source_transaction) leaves the platform when they arrive, not before
2143
+ const platform = await write(ctx, { amount: -amount, currency, fee: 0, net: -amount, type: 'transfer', reporting_category: 'transfer', source: transferId, ...(fromPending ? { status: settled ? 'available' : 'pending', available_on: availableOn } : { available_on: Number(nowUnix(ctx.occurredAt)) }) }, null);
2144
+ await write(ctx, { amount, currency, fee: 0, net: amount, type: 'payment', reporting_category: 'charge', source: transferId, status: settled ? 'available' : 'pending', available_on: availableOn }, destination);
2145
+ return platform;
2146
+ }
2147
+ /** A transfer reversal's two entries: back into the platform's balance, out of the destination's. */
2148
+ export async function settleTransferReversal(ctx, reversalId, amount, currency, destination) {
2149
+ const platform = await write(ctx, { amount, currency, fee: 0, net: amount, type: 'transfer_refund', reporting_category: 'transfer_reversal', source: reversalId, available_on: Number(nowUnix(ctx.occurredAt)) }, null);
2150
+ await write(ctx, { amount: -amount, currency, fee: 0, net: -amount, type: 'payment_refund', reporting_category: 'refund', source: reversalId, available_on: Number(nowUnix(ctx.occurredAt)) }, destination);
2151
+ return platform;
2152
+ }
2153
+ /** A direct charge's application fee: out of the connected account's balance, into the platform's. */
2154
+ export async function settleApplicationFee(ctx, feeId, amount, currency, account) {
2155
+ await write(ctx, { amount: -amount, currency, fee: 0, net: -amount, type: 'application_fee', reporting_category: 'platform_earning', source: feeId, available_on: Number(nowUnix(ctx.occurredAt)) }, account);
2156
+ return write(ctx, { amount, currency, fee: 0, net: amount, type: 'application_fee', reporting_category: 'platform_earning', source: feeId, available_on: Number(nowUnix(ctx.occurredAt)) }, null);
2157
+ }
2158
+ /** A top-up's credit, to the payments balance or, with `destination_balance=issuing`, to Issuing's
2159
+ * (docs.stripe.com/issuing/funding/balance). Where the documentation stops and the twin decides: a test-mode top-up
2160
+ * is available at once. */
2161
+ export async function settleTopup(ctx, topupId, amount, currency, destination) {
2162
+ const issuing = destination === 'issuing';
2163
+ return write(ctx, { amount, currency, fee: 0, net: amount, type: 'topup', reporting_category: 'topup', source: topupId, available_on: Number(nowUnix(ctx.occurredAt)), balance_type: issuing ? 'issuing' : 'payments' });
2164
+ }
2165
+ /** Time's settlements, written: each entry on the acting account's balance whose funds came due by the World's clock
2166
+ * moves pending → available (the clock's move the machine allows), so a settlement is recorded once. A read already
2167
+ * sees it (asOf); the record is what lets Stripe's balance.available be sent once (./front.ts, the drain).
2168
+ * Funds that were available at once and not yet sent are taken too, and marked sent. Answers the entries taken. */
2169
+ export async function settleDueEntries(ctx, account = actingAccount(ctx)) {
2170
+ const now = Number(nowUnix(ctx.occurredAt));
2171
+ const moved = [];
2172
+ for (const t of ctx.rowsRaw(BT)) {
2173
+ if (!onAccount(t, account))
2174
+ continue;
2175
+ if (t.status === 'available' && t[UNSENT] === true) {
2176
+ await ctx.write(BT, String(t.id), { [UNSENT]: false }, 'balance_transaction.available_sent');
2177
+ moved.push(t);
2178
+ continue;
2179
+ }
2180
+ if (t.status !== 'pending' || !(Number(t.available_on) <= now))
2181
+ continue;
2182
+ if (ctx.legal(BT, 'status', ctx.call.operation.id, 'pending', 'available', String(t.id), 'time'))
2183
+ continue;
2184
+ await ctx.write(BT, String(t.id), { status: 'available' }, 'balance_transaction.available');
2185
+ moved.push(t);
2186
+ }
2187
+ return moved;
2188
+ }
2189
+ /** An account's balance, by currency: what the clock has made available, what is still pending, and Issuing's own. */
2190
+ export function balanceOf(ctx, account = actingAccount(ctx)) {
2191
+ const available = new Map();
2192
+ const pending = new Map();
2193
+ const issuing = new Map();
2194
+ for (const raw of ctx.rowsRaw(BT)) {
2195
+ if (!onAccount(raw, account))
2196
+ continue;
2197
+ const t = commonAsOf(ctx, raw);
2198
+ const cur = String(t.currency ?? 'usd');
2199
+ const net = Number(t.net ?? 0);
2200
+ const bucket = t.balance_type === 'issuing' ? issuing : t.status === 'available' ? available : pending;
2201
+ bucket.set(cur, (bucket.get(cur) ?? 0) + net);
2202
+ }
2203
+ return { available, pending, issuing };
2204
+ }
2205
+ /** balance_insufficient when a payout or transfer would take more than the account has available in its currency. */
2206
+ export function refusePayout(ctx, amount, currency, account = actingAccount(ctx)) {
2207
+ const have = balanceOf(ctx, account).available.get(currency) ?? 0;
2208
+ if (amount <= have)
2209
+ return undefined;
2210
+ return fail(ctx, "The transfer or payout couldn't be completed because the associated account doesn't have a sufficient balance available.", 400, 'balance_insufficient');
2211
+ }
2212
+ /** The ids of the ledger entries on the acting account's balance. */
2213
+ export const mine = (ctx) => new Set(ctx.rowsRaw(BT).filter((t) => onAccount(t, actingAccount(ctx))).map((t) => t.id));
2214
+ export const chargeMissing = (ctx, id) => fail(ctx, `No such charge: '${id}'`, 404, 'resource_missing');
2215
+ /** Every egress of a charge carries its refunds, derived from the refund rows in list order. */
2216
+ export function chargeBody(ctx, c) {
2217
+ const data = newest(ctx, 'refund').filter((r) => r.charge === c.id);
2218
+ return { ...c, refunds: { object: 'list', data, has_more: false, total_count: data.length, url: `/v1/charges/${String(c.id)}/refunds` } };
2219
+ }
2220
+ /** A Charges-API charge a declining test card refuses: a 402 card_error naming the failed charge Stripe records for the
2221
+ * attempt (docs.stripe.com/declines). */
2222
+ export async function declinedCharge(ctx, decline) {
2223
+ const params = ctx.params;
2224
+ const failedId = ctx.mint('charge');
2225
+ const amount = Number(params.amount) || 0;
2226
+ await created(ctx, 'charge', { ...withoutCard(params), id: failedId }, {
2227
+ ...chargeDefaults(failedId, amount, false, ctx.occurredAt), status: 'failed', paid: false, captured: false, capture_before: null,
2228
+ failure_code: decline.code, failure_message: decline.message, balance_transaction: null,
2229
+ outcome: { type: 'issuer_declined', network_status: 'declined_by_network', reason: decline.decline_code ?? decline.code, risk_level: 'normal', seller_message: 'The bank did not return any further details with this decline.' },
2230
+ payment_method_details: paymentMethodDetails(ctx, params.payment_method ?? params.source ?? params.card) ?? null,
2231
+ }, { operation: 'charge.failed' });
2232
+ return send(ctx, cardError(decline, { charge: failedId }));
2233
+ }
2234
+ /** A charge's fraud report: "user_report … Assessments reported by you. If set, possible values of are `safe` and
2235
+ * `fraudulent`" (docs.stripe.com/api/charges/object, fraud_details). */
2236
+ export function fraudReport(ctx, existing, fd) {
2237
+ const report = String(fd.user_report);
2238
+ if (report !== 'safe' && report !== 'fraudulent')
2239
+ return fail(ctx, "Invalid fraud_details[user_report]: must be 'safe' or 'fraudulent'.", 400, 'parameter_invalid_string_enum');
2240
+ const prior = existing.fraud_details && typeof existing.fraud_details === 'object' ? existing.fraud_details : {};
2241
+ return { user_report: report, stripe_report: prior.stripe_report ?? null };
2242
+ }
2243
+ /** A capture=false charge refunded while uncaptured: its authorization is released by the refund, as it would be
2244
+ * "automatically refunded if uncaptured" (spec/openapi.json.gz, `capture_before`). The charge stays uncaptured and
2245
+ * becomes refunded in full, a Refund records the release, and no money moves: none was ever received. Written as
2246
+ * charge.refunded. Where the documentation stops and the twin decides: Stripe's basil change ("Partially capturing or
2247
+ * canceling payments no longer creates a Refund", docs.stripe.com/changelog/basil/2025-03-31/remove-refund-from-partial-
2248
+ * capture-and-payment-cancellation-flow) names partial capture and cancellation, not a refund asked for, so a refund
2249
+ * still makes one; its reason is null and it has no balance transaction. Answers the Refund. */
2250
+ export async function releaseAuthorization(ctx, ch, operationId, given = {}) {
2251
+ const id = String(ch.id);
2252
+ const full = Number(ch.amount) || 0;
2253
+ if (ch.refunded !== true)
2254
+ ctx.legal('charge', 'refunded', operationId, 'false', 'true', id);
2255
+ const refund = await created(ctx, 'refund', { ...given, charge: id, amount: full - (Number(ch.amount_refunded) || 0), currency: String(ch.currency ?? 'usd') }, {
2256
+ status: 'succeeded', metadata: {}, reason: null, receipt_number: null,
2257
+ balance_transaction: null, source_transfer_reversal: null, transfer_reversal: null, ...refundDestination(ch, 'reversal'),
2258
+ ...(typeof ch.payment_intent === 'string' ? { payment_intent: ch.payment_intent } : {}),
2259
+ });
2260
+ await ctx.write('charge', id, { amount_refunded: full, refunded: true }, 'charge.refunded');
2261
+ return refund;
2262
+ }
2263
+ /** A PaymentIntent's authorization released by its cancel: "For PaymentIntents with a `status` of `requires_capture`, the
2264
+ * remaining `amount_capturable` is automatically refunded" (docs.stripe.com/api/payment_intents/cancel), and since basil
2265
+ * a cancellation makes no Refund: "`amount_captured` will be 0 instead of `nil` in payment cancellation flows",
2266
+ * "`amount_refunded` will no longer be updated by these actions", "`refunded` will no longer be `true` for payment
2267
+ * cancellation flows", and no charge.refunded is sent (docs.stripe.com/changelog/basil/2025-03-31/remove-refund-from-
2268
+ * partial-capture-and-payment-cancellation-flow). The charge stays uncaptured; nothing is written as an event. */
2269
+ export async function cancelAuthorization(ctx, ch) {
2270
+ return ctx.write('charge', String(ch.id), { amount_captured: 0 }, 'charge.authorization_released');
2271
+ }
2272
+ /** An authorized charge captured, by the charge's capture or its PaymentIntent's: what is captured credits the balance.
2273
+ * Written as charge.captured, "Occurs whenever a previously uncaptured charge is captured" (docs.stripe.com/api/events/types).
2274
+ * A partial capture releases the rest with no Refund and leaves amount_refunded and refunded as they were: "The following
2275
+ * flows no longer result in a `Refund` object created and linked to the payment: Partial capture", "`amount_refunded`
2276
+ * will no longer be updated by these actions", and "There will only be a single balance transaction for partial captures"
2277
+ * (docs.stripe.com/changelog/basil/2025-03-31/remove-refund-from-partial-capture-and-payment-cancellation-flow).
2278
+ * The caller has asked the machine whether it may capture. */
2279
+ export async function captureAuthorization(ctx, ch, toCapture, _operationId) {
2280
+ const id = String(ch.id);
2281
+ const bt = toCapture > 0 ? await settleCharge(ctx, id, toCapture, String(ch.currency ?? 'usd'), String(ch.source ?? ch.payment_method ?? '')) : null;
2282
+ return ctx.write('charge', id, { captured: true, amount_captured: toCapture, balance_transaction: bt }, 'charge.captured');
2283
+ }
2284
+ export const chargeRefund = (ctx) => {
2285
+ if (!ctx.get('charge', at(ctx, 'charge')))
2286
+ return chargeMissing(ctx, at(ctx, 'charge'));
2287
+ const r = ctx.get('refund', at(ctx, 'refund'));
2288
+ return r && r.charge === at(ctx, 'charge') ? r : fail(ctx, `No such refund: '${at(ctx, 'refund')}'`, 404, 'resource_missing');
2289
+ };
2290
+ export const subMissing = (ctx, id) => fail(ctx, `No such subscription: '${id}'`, 404, 'resource_missing');
2291
+ /** A coupon that replaces the subscription's discount, or an empty one that clears it (docs.stripe.com/api/subscriptions/update#update_subscription-discounts). */
2292
+ export function replacedDiscounts(ctx, couponId, sub) {
2293
+ if (couponId === '')
2294
+ return [];
2295
+ const coupon = ctx.get('coupon', couponId);
2296
+ return coupon ? [buildDiscount(coupon, String(sub.customer ?? ''), Number(nowUnix(ctx.occurredAt)), { subscription: String(sub.id) })] : fail(ctx, `No such coupon: '${couponId}'`, 400, 'resource_missing');
2297
+ }
2298
+ /** A `trial_end` Stripe refuses: the parameter is `"now" | timestamp` (docs.stripe.com/api/subscriptions/update), and a
2299
+ * timestamp that is not in the future answers "Invalid timestamp: must be an integer Unix timestamp in the future."
2300
+ * (Stripe's own message, as its SDKs' users report it: github.com/stripe/stripe-dotnet/issues/805,
2301
+ * github.com/stripe/stripe-ruby/issues/682). Where the documentation stops and the twin decides: a value that is no
2302
+ * number at all, and one more than two years on, answer that same message. */
2303
+ export function trialEndRefused(ctx, value, now) {
2304
+ const t = typeof value === 'number' ? value : typeof value === 'string' && /^\d+$/.test(value.trim()) ? Number(value) : NaN;
2305
+ return value === undefined || value === 'now' || (Number.isInteger(t) && t > now && t <= now + MAX_TRIAL) ? undefined : invalidTrialEnd(ctx);
2306
+ }
2307
+ export function invalidTrialEnd(ctx) {
2308
+ return ctx.refuse({ status: 400, param: 'trial_end', message: 'Invalid timestamp: must be an integer Unix timestamp in the future.' });
2309
+ }
2310
+ /** An update's `trial_end` on the subscription, into `fields`.
2311
+ *
2312
+ * `now` on a trialing subscription ends its trial at once: "The special value `now` can be provided to end the customer's
2313
+ * trial immediately" (docs.stripe.com/api/subscriptions/update#update_subscription-trial_end; the trials guide's "To end a
2314
+ * trial early … setting the `trial_end` value to … **now** to end immediately", docs.stripe.com/billing/subscriptions/
2315
+ * trials/free-trials). The update resets the billing date and charges at once: "Switching prices does not normally change
2316
+ * the billing date or generate an immediate charge unless … A trial starts or ends. In these cases, we … immediately
2317
+ * charge the customer using the new price, and reset the billing date" (docs.stripe.com/api/subscriptions/update), and
2318
+ * "Stripe immediately attempts payment when a subscription's billing cycle anchor is reset … When billing is performed
2319
+ * immediately, but the required payment fails, the subscription change request succeeds and the subscription transitions
2320
+ * to `past_due`" (docs.stripe.com/billing/subscriptions/upgrade-downgrade#immediate-payment). So a new period starts now,
2321
+ * its invoice is made (`invoice.created`) and charged to the subscription's default payment method, else the
2322
+ * customer's (paid: `invoice.paid`, the subscription `active`; declined or none: the invoice open, `past_due`), and the
2323
+ * subscription's own write sends `customer.subscription.updated`. `payment_behavior` (the same page): `error_if_incomplete`
2324
+ * answers 402 and leaves the subscription as it was, `default_incomplete` leaves the invoice open without an attempt.
2325
+ *
2326
+ * A future timestamp moves the trial's end, and the period and billing anchor with it: "The `billing_cycle_anchor` will be
2327
+ * updated to the `trial_end` value" (the same parameter); on a subscription out of its trial it starts a new one ("You can
2328
+ * add a new trial on a non-trialing subscription by updating the subscription while specifying trial_end", and for
2329
+ * classic billing mode "the `trial_start` field remains set to the start of the first trial", docs.stripe.com/billing/
2330
+ * subscriptions/trials/free-trials).
2331
+ *
2332
+ * Where the documentation stops and the twin decides: the invoice's billing_reason is `subscription_update` ("A
2333
+ * subscription was updated", docs.stripe.com/api/invoices/object#invoice_object-billing_reason); `now` on a subscription
2334
+ * that is not trialing has no trial to end and changes nothing; the credit Stripe prorates for the unused time when a new
2335
+ * trial starts mid-period is not made (as `proration_behavior=none` would have it); `pending_if_incomplete` answers as
2336
+ * `allow_incomplete` (no pending update is modelled). Answers a refusal, or the invoice to collect once the subscription is written. */
2337
+ export async function applyTrialEnd(ctx, sub, fields, now) {
2338
+ const value = ctx.params.trial_end;
2339
+ delete fields.trial_end;
2340
+ const id = String(sub.id);
2341
+ if (value !== 'now' || sub.status !== 'trialing')
2342
+ return value === 'now' ? undefined : movedTrial(sub, fields, Number(value), now);
2343
+ // the period's invoice bills the items as this update leaves them, under its discount
2344
+ const next = { ...sub, ...fields, items: ctx.get(SUB, id)?.items ?? sub.items };
2345
+ const { interval, interval_count } = resolveSubscriptionBillingInterval((next.items?.data) ?? [], finder(ctx));
2346
+ const period = { start: now, end: addBillingInterval(now, interval, interval_count) };
2347
+ const invoice = await draftSubscriptionInvoice(ctx, next, { ...period, reason: 'subscription_update' });
2348
+ Object.assign(fields, { trial_end: now, billing_cycle_anchor: now, current_period_start: period.start, current_period_end: period.end, latest_invoice: invoice });
2349
+ const payer = trialPayer(ctx, sub);
2350
+ return { invoice, ...(payer ? { payer } : {}), attempt: ctx.params.payment_behavior !== 'default_incomplete' };
2351
+ }
2352
+ /** What ending the trial charges: the update's own default payment method, else the subscription's, else its customer's. */
2353
+ export function trialPayer(ctx, sub) {
2354
+ const given = ctx.params.default_payment_method;
2355
+ return typeof given === 'string' && given ? given : payerOf(ctx, sub);
2356
+ }
2357
+ /** What an update's `trial_end` refuses before anything is written: a new trial the machine does not allow from the
2358
+ * subscription's status, or, under `error_if_incomplete`, ending a trial that cannot be paid at once ("If payment fails,
2359
+ * return an HTTP `402` status code and don't update the subscription", docs.stripe.com/api/subscriptions/update). */
2360
+ export function trialEndBlocked(ctx, sub, discounts) {
2361
+ if (ctx.params.trial_end !== 'now')
2362
+ return newTrialRefused(ctx, sub);
2363
+ if (sub.status !== 'trialing')
2364
+ return undefined;
2365
+ const charge = trialEndCharge(ctx, sub, discounts);
2366
+ if (ctx.params.payment_behavior === 'error_if_incomplete' && charge.due > 0 && !charge.pays)
2367
+ return firstPaymentRefused(ctx, charge.declined);
2368
+ // the move ending the trial makes (paid or nothing due: active; else past_due) is the machine's, asked before
2369
+ // anything is written
2370
+ const refused = ctx.legal(SUB, 'status', ctx.call.operation.id, 'trialing', charge.due <= 0 || charge.pays ? 'active' : 'past_due', String(sub.id));
2371
+ return refused ? ctx.refuse(refused) : undefined;
2372
+ }
2373
+ /** A price named by id, or given as an object. */
2374
+ export function priceRow(ctx, price) {
2375
+ return typeof price === 'string' ? ctx.get('price', price) : price && typeof price === 'object' ? price : undefined;
2376
+ }
2377
+ /** One period of the subscription's items as an update's `items` would leave them, before any discount: an entry naming
2378
+ * an item changes its price and quantity (a new price resets the quantity to 1 unless one is given) or deletes it, and
2379
+ * one naming none adds an item (applyItemChanges). */
2380
+ export function dueAfter(ctx, sub, entries) {
2381
+ const lines = new Map();
2382
+ for (const it of (sub.items?.data) ?? [])
2383
+ lines.set(String(it.id), { price: priceRow(ctx, it.price), quantity: itemQuantity(it) });
2384
+ entries.forEach((e, i) => entryAfter(ctx, lines, e, i));
2385
+ return [...lines.values()].reduce((sum, l) => sum + priceAmount(l.price, l.quantity), 0);
2386
+ }
2387
+ /** One entry of an update's `items` on the lines it would leave (dueAfter). */
2388
+ export function entryAfter(ctx, lines, e, i) {
2389
+ // an inline price bills as the Price inlinePrice makes of it
2390
+ const d = e.price_data && typeof e.price_data === 'object' ? e.price_data : undefined;
2391
+ const inline = d ? (d.unit_amount_decimal !== undefined ? { unit_amount_decimal: String(d.unit_amount_decimal) } : { unit_amount: Math.trunc(Number(d.unit_amount) || 0) }) : undefined;
2392
+ const price = typeof e.price === 'string' && e.price ? priceRow(ctx, e.price) : inline;
2393
+ const named = typeof e.id === 'string' && e.id;
2394
+ const key = named ? String(e.id) : `new_${i}`;
2395
+ // only a named item is deleted; an entry naming none adds one, as applyItemChanges does
2396
+ if (named && asBool(e.deleted)) {
2397
+ lines.delete(key);
2398
+ return;
2399
+ }
2400
+ const was = lines.get(key);
2401
+ 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;
2402
+ lines.set(key, { price: price ?? was?.price, quantity });
2403
+ }
2404
+ /** A new trial on a subscription whose status the machine does not let start one. */
2405
+ export function newTrialRefused(ctx, sub) {
2406
+ const refused = sub.status === 'trialing' ? undefined : ctx.legal(SUB, 'status', ctx.call.operation.id, sub.status, 'trialing', String(sub.id));
2407
+ return refused ? ctx.refuse(refused) : undefined;
2408
+ }
2409
+ /** What ending the trial charges, judged before anything is written: the period of the items as this update leaves them
2410
+ * (dueAfter) under the discount it leaves, and whether the payer pays it (an attempt made, a payer, no decline). */
2411
+ export function trialEndCharge(ctx, sub, discounts) {
2412
+ const subtotal = dueAfter(ctx, sub, subscriptionItemEntries(ctx.params.items));
2413
+ // the discount the subscription carries after this update takes this invoice (renewals.ts draftSubscriptionInvoice)
2414
+ const discount = (discounts ?? sub.discounts ?? [])[0];
2415
+ const coupon = discount && typeof discount === 'object' ? discount.coupon : undefined;
2416
+ const due = subtotal - (coupon ? applyCouponDiscount(subtotal, coupon) : 0);
2417
+ const payer = trialPayer(ctx, sub);
2418
+ const attempt = ctx.params.payment_behavior !== 'default_incomplete';
2419
+ const declined = payer && attempt ? declineFor(finder(ctx), { payment_method: payer }) : undefined;
2420
+ return { due, pays: attempt && !!payer && !declined, ...(declined ? { declined } : {}) };
2421
+ }
2422
+ /** A new item named with neither `price` nor `price_data`: "One of `price` or `price_data` is required"
2423
+ * (docs.stripe.com/api/subscriptions/update). */
2424
+ export function noItemPrice(ctx) {
2425
+ return fail(ctx, 'Missing required param: items[price].', 400, 'parameter_missing');
2426
+ }
2427
+ /** A subscription asked to fail when its first invoice cannot be paid at once (payment_behavior=error_if_incomplete):
2428
+ * the card's decline, or no payment method to charge (docs.stripe.com/api/subscriptions/create#create_subscription-payment_behavior). */
2429
+ export function firstPaymentRefused(ctx, declined) {
2430
+ return declined ? send(ctx, cardError(declined)) : fail(ctx, 'This customer has no attached payment source or default payment method.', 400, 'resource_missing');
2431
+ }
2432
+ /** An update's `items`: "A list of up to 20 subscription items, each with an attached price"
2433
+ * (docs.stripe.com/api/subscriptions/update): an entry naming an item's `id` changes that item (its price, quantity,
2434
+ * metadata) or, with `deleted` ("A flag that, if set to `true`, will delete the specified item"), removes it; "If you
2435
+ * omit `id`, the API adds a new subscription item rather than updating the existing one". Each takes `price` or
2436
+ * `price_data`, "Data used to generate a new Price object inline. One of `price` or `price_data` is required", and
2437
+ * "When changing a subscription item's price, `quantity` is set to 1 unless a `quantity` parameter is provided". An
2438
+ * inline price is made as the manage-prices guide says: "By default, prices created with `price_data` are effectively
2439
+ * archived (they're marked as `active=false`)" (docs.stripe.com/products-prices/manage-prices).
2440
+ * Where the documentation stops and the twin decides: the update is all or nothing, every entry checked before any is
2441
+ * written (Stripe answers one error for the request); the refusals are worded by the twin. */
2442
+ export async function applyItemChanges(ctx, subId, entries) {
2443
+ // every entry is checked first: a refusal leaves the subscription as it was
2444
+ const refused = itemChangesRefused(ctx, subId, entries);
2445
+ if (refused)
2446
+ return refused;
2447
+ return writeItemChanges(ctx, subId, entries);
2448
+ }
2449
+ /** The refusal of an update's `items`, checked entry by entry before any is written (applyItemChanges). */
2450
+ export function itemChangesRefused(ctx, subId, entries) {
2451
+ for (const entry of entries) {
2452
+ const priceRef = typeof entry.price === 'string' && entry.price ? entry.price : undefined;
2453
+ if (priceRef && !ctx.get('price', priceRef))
2454
+ return fail(ctx, `No such price: '${priceRef}'`, 400, 'resource_missing');
2455
+ const inline = !priceRef && entry.price_data && typeof entry.price_data === 'object' ? entry.price_data : undefined;
2456
+ if (inline) {
2457
+ const refused = refuseInlinePrice(ctx, inline);
2458
+ if (refused)
2459
+ return refused;
2460
+ }
2461
+ if (typeof entry.id === 'string' && entry.id) {
2462
+ const si = ctx.get('subscription_item', entry.id);
2463
+ if (!si || si.subscription !== subId)
2464
+ return fail(ctx, `No such subscription_item: '${entry.id}'`, 400, 'resource_missing');
2465
+ }
2466
+ else if (!priceRef && !inline)
2467
+ return noItemPrice(ctx);
2468
+ }
2469
+ return undefined;
2470
+ }
2471
+ /** An update's checked `items`, written. */
2472
+ export async function writeItemChanges(ctx, subId, entries) {
2473
+ for (const entry of entries) {
2474
+ const inline = typeof entry.price === 'string' && entry.price ? undefined : entry.price_data && typeof entry.price_data === 'object' ? entry.price_data : undefined;
2475
+ const priceRef = typeof entry.price === 'string' && entry.price ? entry.price : inline ? await inlinePrice(ctx, inline) : undefined;
2476
+ // only a price that changes resets the quantity; one naming the item's own price leaves it
2477
+ const was = typeof entry.id === 'string' && entry.id ? ctx.get('subscription_item', entry.id)?.price : undefined;
2478
+ const wasId = typeof was === 'string' ? was : was && typeof was === 'object' ? String(was.id ?? '') : undefined;
2479
+ const quantity = entry.quantity !== undefined ? { quantity: Math.max(0, Math.trunc(Number(entry.quantity) || 0)) } : priceRef && priceRef !== wasId ? { quantity: 1 } : {};
2480
+ const metadata = entry.metadata && typeof entry.metadata === 'object' ? { metadata: entry.metadata } : {};
2481
+ if (typeof entry.id === 'string' && entry.id) {
2482
+ if (asBool(entry.deleted)) {
2483
+ await ctx.write('subscription_item', entry.id, { deleted: true }, 'subscription_item.delete');
2484
+ continue;
2485
+ }
2486
+ await ctx.write('subscription_item', entry.id, { ...(priceRef ? subscriptionItemPrice(priceRef, finder(ctx)) : {}), ...quantity, ...metadata }, 'subscription_item.update');
2487
+ }
2488
+ else {
2489
+ await created(ctx, 'subscription_item', { subscription: subId, ...subscriptionItemPrice(priceRef, finder(ctx)), quantity: 1, ...quantity, ...metadata }, { livemode: false, metadata: {}, discounts: [], tax_rates: [], billing_thresholds: null });
2490
+ }
2491
+ }
2492
+ await syncItems(ctx, subId);
2493
+ return undefined;
2494
+ }
2495
+ /** A subscription item's `price_data` Stripe would refuse: its required `currency`, `product` (an existing one) and
2496
+ * `recurring[interval]`, and "Only one of `unit_amount` and `unit_amount_decimal` can be set". */
2497
+ export function refuseInlinePrice(ctx, d) {
2498
+ for (const name of ['currency', 'product'])
2499
+ if (typeof d[name] !== 'string' || !d[name])
2500
+ return fail(ctx, `Missing required param: items[price_data][${name}].`, 400, 'parameter_missing');
2501
+ if (!ctx.get('product', String(d.product)))
2502
+ return fail(ctx, `No such product: '${String(d.product)}'`, 400, 'resource_missing');
2503
+ const interval = d.recurring?.interval;
2504
+ if (!['day', 'week', 'month', 'year'].includes(String(interval)))
2505
+ return fail(ctx, 'Missing required param: items[price_data][recurring][interval].', 400, 'parameter_missing');
2506
+ if (d.unit_amount !== undefined && d.unit_amount_decimal !== undefined)
2507
+ return fail(ctx, 'Only one of `unit_amount` and `unit_amount_decimal` can be set.', 400, 'parameter_invalid');
2508
+ return undefined;
2509
+ }
2510
+ /** The inline Price a `price_data` generates: recurring, archived (active=false). Answers its id. */
2511
+ export async function inlinePrice(ctx, d) {
2512
+ const r = d.recurring;
2513
+ const decimal = d.unit_amount_decimal !== undefined ? String(d.unit_amount_decimal) : String(Math.trunc(Number(d.unit_amount) || 0));
2514
+ const price = await created(ctx, 'price', {
2515
+ product: d.product, currency: String(d.currency).toLowerCase(),
2516
+ // a decimal amount that is not a whole number of the smallest unit has no integer unit_amount (as semantics/plans.ts)
2517
+ 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,
2518
+ 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 },
2519
+ tax_behavior: typeof d.tax_behavior === 'string' ? d.tax_behavior : 'unspecified',
2520
+ }, { active: false, livemode: false, billing_scheme: 'per_unit', type: 'recurring', metadata: {}, lookup_key: null, nickname: null });
2521
+ return String(price.id);
2522
+ }
2523
+ // ── items: each write keeps the parent subscription's items.data in step, rebuilt from the live items ──
2524
+ /** A new subscription's items, each its own subscription_item as Stripe keeps it (/v1/subscription_items/{id}). */
2525
+ export async function storeItems(ctx, subId, items) {
2526
+ for (const item of items) {
2527
+ if (typeof item.id !== 'string' || ctx.get('subscription_item', item.id))
2528
+ continue;
2529
+ await ctx.write('subscription_item', item.id, { ...item, subscription: subId }, 'subscription_item.create');
2530
+ }
2531
+ }
2532
+ export async function syncItems(ctx, subId) {
2533
+ if (!subId)
2534
+ return;
2535
+ const items = ctx.rows('subscription_item').filter((r) => r.subscription === subId);
2536
+ 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');
2537
+ }
2538
+ /** A subscription item with its billing period: its subscription's (basil moved the period onto the items). */
2539
+ export const withPeriod = (ctx, si) => {
2540
+ const sub = ctx.get(SUB, String(si.subscription));
2541
+ 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 };
2542
+ };
2543
+ export const subscriptionsItemMissing = (ctx, id) => fail(ctx, `No such subscription_item: '${id}'`, 404, 'resource_missing');
2544
+ export const listMissing = (ctx, id, status = 404) => fail(ctx, `No such radar value list: '${id}'`, status, 'resource_missing');
2545
+ /** A value list's list_items, "List of items contained within this value list" (the served spec), kept in step with
2546
+ * its items, at the address the fixture names (/v1/radar/value_list_items?value_list=…). */
2547
+ export async function radarSyncItems(ctx, listId) {
2548
+ const data = newest(ctx, VLI).filter((i) => i.value_list === listId);
2549
+ await ctx.write(VL, listId, { list_items: { object: 'list', data, has_more: false, url: `/v1/radar/value_list_items?value_list=${listId}` } }, 'radar.value_list.items_synced');
2550
+ }
2551
+ /** A requested move of a state field the machine declares, legal or refused; a value the machine
2552
+ * does not name is written as given, as the hand-written route did. */
2553
+ export function requestedMove(ctx, resource, operationId, current, values) {
2554
+ const to = ctx.params.status;
2555
+ if (typeof to !== 'string' || !values.includes(to))
2556
+ return undefined;
2557
+ const refused = ctx.legal(resource, 'status', operationId, current, to);
2558
+ return refused ? ctx.refuse(refused) : undefined;
2559
+ }
2560
+ // ── authorizations ──
2561
+ /** An authorization as every egress carries it: its card embedded in full. */
2562
+ export function withCard(ctx, a) {
2563
+ const out = { ...a };
2564
+ if (typeof out.card === 'string') {
2565
+ const card = ctx.get(CARD, out.card);
2566
+ if (card)
2567
+ out.card = card;
2568
+ }
2569
+ return out;
2570
+ }
2571
+ export const authMissing = (ctx, id) => fail(ctx, `No such authorization: '${id}'`, 404, 'resource_missing');
2572
+ /** A capture: an Issuing transaction (negative: it debits) and the issuing balance's debit. */
2573
+ export async function capture(ctx, authId, auth, amountCents) {
2574
+ const currency = typeof auth.currency === 'string' ? auth.currency : 'usd';
2575
+ const bt = await created(ctx, 'balance_transaction', {}, {
2576
+ amount: -amountCents, currency, fee: 0, net: -amountCents, type: 'issuing_transaction',
2577
+ status: 'available', balance_type: 'issuing', reporting_category: 'issuing_transaction',
2578
+ available_on: nowUnix(ctx.occurredAt), fee_details: [],
2579
+ });
2580
+ const txn = await created(ctx, TXN, { authorization: authId, card: auth.card, cardholder: auth.cardholder }, {
2581
+ type: 'capture', amount: -amountCents, currency,
2582
+ merchant_amount: -amountCents, merchant_currency: typeof auth.merchant_currency === 'string' ? auth.merchant_currency : currency,
2583
+ merchant_data: auth.merchant_data ?? merchantData(undefined),
2584
+ livemode: false, metadata: {}, dispute: null, wallet: null, network_data: null,
2585
+ amount_details: null, purchase_details: null, balance_transaction: bt.id,
2586
+ });
2587
+ return { txnId: String(txn.id), btId: String(bt.id) };
2588
+ }
2589
+ /** Issuing refusals of a request no story sends: an unknown card status or authorization method, and a capture of an
2590
+ * authorization that is not approved and pending. */
2591
+ export function issuingRefusal(ctx, what, auth) {
2592
+ switch (what) {
2593
+ case 'card_status': return fail(ctx, "Invalid status: must be one of 'active', 'inactive', or 'canceled'.", 400, 'parameter_invalid_string_enum');
2594
+ case 'authorization_method': return fail(ctx, "Invalid authorization_method: must be one of 'chip', 'contactless', 'keyed_in', 'online', or 'swipe'.", 400, 'parameter_invalid_string_enum');
2595
+ case 'not_capturable': return fail(ctx, `This authorization cannot be captured because it is not an approved pending authorization (status: ${String(auth?.status)}, approved: ${auth?.approved === true}).`, 400);
2596
+ }
2597
+ }
2598
+ /** A partial approval's refusal: "You can specify the amount you want to approve by setting the amount in the webhook
2599
+ * response body or the approve call" only "When an authorization is partially authorized, the is_amount_controllable
2600
+ * field on the authorization request is set to true" (docs.stripe.com/issuing/purchases/authorizations), and never a
2601
+ * credit. Answers the refusal, or undefined when the amount may be approved. */
2602
+ export function partialApprovalRefused(ctx, auth) {
2603
+ if (Math.trunc(Number(ctx.params.amount) || 0) <= 0)
2604
+ return fail(ctx, 'Invalid integer: amount must be a positive integer.', 400, 'parameter_invalid_integer');
2605
+ if (auth.pending_request?.is_amount_controllable !== true)
2606
+ return fail(ctx, 'amount may only be provided when the authorization was presented with is_amount_controllable.', 400);
2607
+ return undefined;
2608
+ }
2609
+ // the (deprecated but real) API decision on a pending authorization: it lands in request_history
2610
+ // as the webhook's answer would, consumes the pending request, and an approval captures
2611
+ export function issuingDecide(approved) {
2612
+ return async (ctx) => {
2613
+ const id = at(ctx, 'authorization');
2614
+ const auth = ctx.get(AUTH, id);
2615
+ if (!auth)
2616
+ return authMissing(ctx, id);
2617
+ const finalized = { status: 400, code: 'authorization_already_finalized', message: `This authorization has already been finalized (status ${String(auth.status)}).` };
2618
+ // a webhook-approved authorization is decided, though still pending
2619
+ if (auth.approved === true)
2620
+ return ctx.refuse(finalized);
2621
+ const refused = ctx.legal(AUTH, 'status', approved ? 'PostIssuingAuthorizationsAuthorizationApprove' : 'PostIssuingAuthorizationsAuthorizationDecline', auth.status, undefined, id);
2622
+ if (refused)
2623
+ return ctx.refuse(refused);
2624
+ // a partial approval only for an amount-controllable presentment, and never a credit
2625
+ const badAmount = approved && ctx.params.amount !== undefined ? partialApprovalRefused(ctx, auth) : undefined;
2626
+ if (badAmount)
2627
+ return badAmount;
2628
+ const decidedAmount = approved && ctx.params.amount !== undefined ? Math.trunc(Number(ctx.params.amount) || 0) : Number(auth.amount) || 0;
2629
+ const currency = typeof auth.currency === 'string' ? auth.currency : 'usd';
2630
+ const history = Array.isArray(auth.request_history) ? auth.request_history : [];
2631
+ // an approval holds the funds and waits for the merchant's capture; a decline closes it
2632
+ // (docs.stripe.com/issuing/purchases/authorizations, docs.stripe.com/issuing/funding/balance)
2633
+ const hold = approved ? await holdFor(ctx, id, decidedAmount, currency) : null;
2634
+ return ctx.reply(withCard(ctx, await ctx.write(AUTH, id, {
2635
+ ...(approved ? {} : { status: 'closed' }), approved, pending_request: null,
2636
+ ...(approved && ctx.params.amount !== undefined ? { amount: decidedAmount } : {}),
2637
+ request_history: [...history, historyEntry({
2638
+ amount: decidedAmount, currency, approved, reason: approved ? 'webhook_approved' : 'webhook_declined', reasonMessage: null,
2639
+ createdSec: Number(nowUnix(ctx.occurredAt)), merchantAmount: Number(auth.merchant_amount) || decidedAmount,
2640
+ merchantCurrency: typeof auth.merchant_currency === 'string' ? auth.merchant_currency : currency,
2641
+ })],
2642
+ ...(hold ? { balance_transactions: [...(Array.isArray(auth.balance_transactions) ? auth.balance_transactions : []), hold.id] } : {}),
2643
+ }, 'issuing_authorization.updated')));
2644
+ };
2645
+ }
2646
+ /** Time's decision on a real-time request no one answered, caught up to the World's clock: once its window has ended,
2647
+ * a pending authorization whose issuing_authorization.request went unanswered, and that no approve or decline decided,
2648
+ * is decided automatically, its request_history reason webhook_timeout ("webhook_timeout for all other failure modes",
2649
+ * the same page, beside webhook_error for a response Stripe cannot process).
2650
+ * Where the documentation stops and the twin decides: the timeout setting is the Dashboard's, which the API does not
2651
+ * answer, and the twin's is to decline; the window is measured in World time from the authorization's created. */
2652
+ export async function lapseRealtimeRequests(ctx) {
2653
+ const now = Number(nowUnix(ctx.occurredAt));
2654
+ const requested = new Set(ctx.rows('event').filter((e) => e.type === 'issuing_authorization.request').map((e) => String(e.data?.object?.id ?? '')));
2655
+ for (const a of ctx.rowsRaw(AUTH)) {
2656
+ if (a.status !== 'pending' || a.approved === true || !a.pending_request || !requested.has(String(a.id)))
2657
+ continue;
2658
+ const at = Number(a.created) + REALTIME_WINDOW_SECONDS;
2659
+ if (now < at)
2660
+ continue;
2661
+ const id = String(a.id);
2662
+ const currency = typeof a.currency === 'string' ? a.currency : 'usd';
2663
+ const amount = Number(a.amount) || 0;
2664
+ ctx.legal(AUTH, 'status', ctx.call.operation.id, 'pending', 'closed', id, 'time');
2665
+ await ctx.write(AUTH, id, {
2666
+ status: 'closed', approved: false, pending_request: null,
2667
+ request_history: [...(Array.isArray(a.request_history) ? a.request_history : []), historyEntry({
2668
+ amount, currency, approved: false, reason: 'webhook_timeout', reasonMessage: null, createdSec: at,
2669
+ merchantAmount: Number(a.merchant_amount) || amount, merchantCurrency: typeof a.merchant_currency === 'string' ? a.merchant_currency : currency,
2670
+ })],
2671
+ }, 'issuing_authorization.updated');
2672
+ }
2673
+ }
2674
+ /** An approval's hold: "If you approve the authorization, we deduct the `amount` from your Issuing balance and hold it
2675
+ * in reserve until the authorization is either captured, voided, or expired without capture"
2676
+ * (docs.stripe.com/issuing/purchases/authorizations). */
2677
+ export async function holdFor(ctx, authId, amount, currency) {
2678
+ return created(ctx, 'balance_transaction', {}, {
2679
+ amount: -amount, currency, fee: 0, net: -amount, type: 'issuing_authorization_hold',
2680
+ status: 'available', balance_type: 'issuing', reporting_category: 'issuing_authorization_hold',
2681
+ available_on: nowUnix(ctx.occurredAt), fee_details: [], source: authId,
2682
+ });
2683
+ }
2684
+ /** An authorization approved as it is presented, its amount held: approved and pending until the merchant captures. */
2685
+ export async function approvedAtOnce(ctx, subject, fields, held, entry) {
2686
+ const hold = await holdFor(ctx, String(subject.id), held, String(fields.currency ?? 'usd'));
2687
+ return ctx.reply(withCard(ctx, await created(ctx, AUTH, subject, { ...fields, amount: held, status: 'pending', approved: true, pending_request: null, request_history: [entry], balance_transactions: [hold.id] })));
2688
+ }
2689
+ /** An authorization as the platform's real-time answer makes it: approved (in part, only when the request was
2690
+ * amount-controllable), declined, or declined for an answer Stripe cannot read (docs.stripe.com/issuing/controls/
2691
+ * real-time-authorizations: the response's `approved` and `amount`; webhook_error "if we can't process your response"). */
2692
+ export async function answeredByEndpoint(ctx, outcome, o) {
2693
+ if (outcome.kind === 'approved') {
2694
+ const held = o.controllable && outcome.amount !== undefined ? outcome.amount : o.amount;
2695
+ return approvedAtOnce(ctx, o.subject, o.fields, held, o.entry({ amount: held, approved: true, reason: 'webhook_approved', reasonMessage: null }));
2696
+ }
2697
+ return outcome.kind === 'declined' ? o.decline('webhook_declined', null) : o.decline('webhook_error', outcome.message);
2698
+ }
2699
+ // ── disputes: opened against a transaction, unsubmitted until submitted ──
2700
+ export const disputeMissing = (ctx, id) => fail(ctx, `No such dispute: '${id}'`, 404, 'resource_missing');
2701
+ // ── personalization designs: in review until Stripe's review (the test helpers in test mode) decides ──
2702
+ export const designMissing = (ctx, id) => fail(ctx, `No such personalization design: '${id}'`, 404, 'resource_missing');
2703
+ export function review(action, operationId) {
2704
+ return async (ctx) => {
2705
+ const id = at(ctx, 'personalization_design');
2706
+ const d = ctx.get(PD, id);
2707
+ if (!d)
2708
+ return designMissing(ctx, id);
2709
+ const refused = ctx.legal(PD, 'status', operationId, d.status, undefined, id);
2710
+ if (refused)
2711
+ return ctx.refuse(refused);
2712
+ const status = action === 'activate' ? 'active' : action === 'deactivate' ? 'inactive' : 'rejected';
2713
+ // a rejection records its reasons: the reject helper's required rejection_reasons, "The reason(s) the
2714
+ // personalization design was rejected" (the served spec), per card_logo and carrier_text
2715
+ const given = ctx.params.rejection_reasons && typeof ctx.params.rejection_reasons === 'object' ? ctx.params.rejection_reasons : undefined;
2716
+ if (action === 'reject' && !given)
2717
+ return fail(ctx, 'Missing required param: rejection_reasons.', 400, 'parameter_missing');
2718
+ const reasons = action === 'reject' ? { rejection_reasons: { card_logo: Array.isArray(given.card_logo) ? given.card_logo : [], carrier_text: Array.isArray(given.carrier_text) ? given.carrier_text : [] } } : {};
2719
+ return ctx.reply(await ctx.write(PD, id, { status, ...reasons }, `personalization_design.${action}`));
2720
+ };
2721
+ }
2722
+ /**
2723
+ * Create a refund and land it on its charge. An omitted amount is the whole remaining balance. The
2724
+ * charge's write records `charge.refunded`, the event Stripe sends carrying the charge's new totals;
2725
+ * `refunded` is true only at the full amount.
2726
+ */
2727
+ /** A reason Stripe does not take (docs.stripe.com/api/refunds/create#create_refund-reason). */
2728
+ export function badReason(ctx) {
2729
+ return fail(ctx, `Invalid refund reason: must be one of ${[...REFUND_REASONS].join(', ')}.`, 400, 'parameter_invalid_string_enum');
2730
+ }
2731
+ /** A bank-transfer payment refunded with origin=customer_balance goes back to the cash balance
2732
+ * (docs.stripe.com/payments/customer-balance/refunding). */
2733
+ export async function backToCashBalance(ctx, customer, currency, amount, refundId) {
2734
+ const before = ctx.rows('customer_cash_balance_transaction').filter((t) => t.customer === customer && String(t.currency) === currency).reduce((n, t) => n + (Number(t.net_amount) || 0), 0);
2735
+ await created(ctx, 'customer_cash_balance_transaction', { customer }, { currency, type: 'refunded_from_payment', net_amount: amount, ending_balance: before + amount, refunded_from_payment: { refund: refundId }, livemode: false });
2736
+ }
2737
+ /** A capture=false charge's refund, which releases its whole authorization (see the header). */
2738
+ export async function refundUncaptured(ctx, charge, amount, remaining, params) {
2739
+ if (amount < remaining)
2740
+ return fail(ctx, `Charge ${String(charge.id)} is uncaptured: refunding it releases the whole authorization (${remaining}).`, 400, 'amount_too_small');
2741
+ const { reason, metadata } = params;
2742
+ return ctx.reply(await releaseAuthorization(ctx, charge, ctx.call.operation.id, { ...(reason !== undefined ? { reason } : {}), ...(metadata !== undefined ? { metadata } : {}) }));
2743
+ }
2744
+ export async function refundCharge(ctx, charge, params) {
2745
+ const chargeId = String(charge.id);
2746
+ const total = Number(charge.amount) || 0;
2747
+ const already = Number(charge.amount_refunded) || 0;
2748
+ const remaining = Math.max(0, total - already);
2749
+ const amount = params.amount === undefined ? remaining : Math.trunc(Number(params.amount));
2750
+ if (!Number.isFinite(amount) || amount <= 0)
2751
+ return fail(ctx, 'Invalid integer: amount must be a positive integer.', 400, 'parameter_invalid_integer');
2752
+ if (amount > remaining)
2753
+ return fail(ctx, `Refund amount (${amount}) is greater than unrefunded amount on charge (${remaining}).`, 400, 'amount_too_large');
2754
+ if (params.reason !== undefined && !REFUND_REASONS.has(String(params.reason)))
2755
+ return badReason(ctx);
2756
+ // an authorization is released, never refunded out of the balance: a PaymentIntent's by cancelling the intent, a
2757
+ // capture=false charge's by its refund (refundUncaptured)
2758
+ if (charge.captured === false && typeof charge.payment_intent === 'string')
2759
+ return fail(ctx, `Charge ${chargeId} is uncaptured and can't be refunded directly; cancel its PaymentIntent (${charge.payment_intent}) to release the authorization.`, 400, 'payment_intent_unexpected_state');
2760
+ if (charge.captured === false)
2761
+ return refundUncaptured(ctx, charge, amount, remaining, params);
2762
+ const refunded = already + amount;
2763
+ // the refund that takes the rest refunds the charge: the charge machine's move under this operation
2764
+ if (refunded >= total && charge.refunded !== true) {
2765
+ const refused = ctx.legal('charge', 'refunded', ctx.call.operation.id, 'false', 'true', chargeId);
2766
+ if (refused)
2767
+ return ctx.refuse(refused);
2768
+ }
2769
+ // a refund has no `livemode` field; it debits the balance at once
2770
+ const refundId = ctx.mint('refund');
2771
+ const currency = String(charge.currency ?? 'usd');
2772
+ const pi = typeof charge.payment_intent === 'string' ? ctx.get('payment_intent', charge.payment_intent) : undefined;
2773
+ const byTransfer = (pi?.payment_method_types ?? []).includes('customer_balance');
2774
+ const toCashBalance = byTransfer && params.origin === 'customer_balance';
2775
+ const awaitsBank = byTransfer && !toCashBalance;
2776
+ const now = Number(nowUnix(ctx.occurredAt));
2777
+ const customer = typeof charge.customer === 'string' ? charge.customer : typeof pi?.customer === 'string' ? pi.customer : undefined;
2778
+ const emailTo = typeof params.instructions_email === 'string' ? params.instructions_email : (customer ? ctx.get('customer', customer)?.email : undefined) ?? null;
2779
+ // "Refunds use your available Stripe balance (not including pending amounts). If your available balance doesn't
2780
+ // cover the amount of the refund, Stripe holds the refund as pending for card transactions ... until your Stripe
2781
+ // balance becomes sufficient" (docs.stripe.com/refunds): the refund waits, moving no money, until the balance covers
2782
+ // it (settleHeldRefunds). Where the documentation stops and the twin decides: a refund of another payment method
2783
+ // short of balance is not modelled (Stripe fails it); the twin makes it as a card's.
2784
+ const account = actingAccount(ctx);
2785
+ const isCard = charge.payment_method_details?.type === 'card';
2786
+ const held = !awaitsBank && !toCashBalance && isCard && availableAt(ctx, account, currency, now) < amount;
2787
+ if (held)
2788
+ ctx.legal('refund', 'status', ctx.call.operation.id, 'succeeded', 'pending', refundId);
2789
+ const bt = awaitsBank || held ? null : await settleRefund(ctx, refundId, amount, currency);
2790
+ if (toCashBalance && customer)
2791
+ await backToCashBalance(ctx, customer, currency, amount, refundId);
2792
+ const { reverse_transfer: reverseTransfer, refund_application_fee: refundFee, instructions_email: _email, origin: _origin, ...stored } = params;
2793
+ const share = (whole) => Math.round((whole * amount) / (total || 1));
2794
+ let transferReversal = null;
2795
+ const transfer = typeof charge.transfer === 'string' ? ctx.get('transfer', charge.transfer) : undefined;
2796
+ if (reverseTransfer !== undefined && asBool(reverseTransfer) && transfer) {
2797
+ const back = Math.min(share(Number(transfer.amount) || 0), (Number(transfer.amount) || 0) - (Number(transfer.amount_reversed) || 0));
2798
+ const trr = await created(ctx, 'transfer_reversal', { amount: back, currency, transfer: transfer.id }, { balance_transaction: null, destination_payment_refund: null, source_refund: refundId, metadata: {} });
2799
+ const trrBt = await settleTransferReversal(ctx, String(trr.id), back, currency, String(transfer.destination));
2800
+ const reversal = await ctx.write('transfer_reversal', String(trr.id), { balance_transaction: trrBt }, 'transfer_reversal.updated');
2801
+ const reversed = (Number(transfer.amount_reversed) || 0) + back;
2802
+ const list = transfer.reversals ?? { object: 'list', data: [], has_more: false, total_count: 0 };
2803
+ const data = [...((list.data ?? [])), reversal];
2804
+ await ctx.write('transfer', String(transfer.id), { amount_reversed: reversed, reversed: reversed >= (Number(transfer.amount) || 0), reversals: { ...list, data, total_count: data.length, url: `/v1/transfers/${String(transfer.id)}/reversals` } }, 'transfer.reversed');
2805
+ transferReversal = String(trr.id);
2806
+ }
2807
+ const fee = typeof charge.application_fee === 'string' ? ctx.get('application_fee', charge.application_fee) : undefined;
2808
+ if (refundFee !== undefined && asBool(refundFee) && fee) {
2809
+ const feeTotal = Number(fee.amount) || 0;
2810
+ const back = Math.min(share(feeTotal), feeTotal - (Number(fee.amount_refunded) || 0));
2811
+ const done = (Number(fee.amount_refunded) || 0) + back;
2812
+ if (!(done >= feeTotal && fee.refunded !== true && ctx.legal('application_fee', 'refunded', ctx.call.operation.id, 'false', 'true', String(fee.id)))) {
2813
+ const fr = await created(ctx, 'fee_refund', { amount: back, currency, fee: fee.id }, { balance_transaction: null, metadata: {} });
2814
+ const list = fee.refunds ?? { object: 'list', data: [], has_more: false, total_count: 0 };
2815
+ const data = [...((list.data ?? [])), fr];
2816
+ await ctx.write('application_fee', String(fee.id), { amount_refunded: done, refunded: done >= feeTotal, refunds: { ...list, data, total_count: data.length, url: `/v1/application_fees/${String(fee.id)}/refunds` } }, 'application_fee.refunded');
2817
+ }
2818
+ }
2819
+ const body = await created(ctx, 'refund', {
2820
+ ...stored, id: refundId, charge: chargeId, amount, currency, transfer_reversal: transferReversal,
2821
+ ...(typeof charge.payment_intent === 'string' ? { payment_intent: charge.payment_intent } : {}),
2822
+ }, {
2823
+ status: awaitsBank ? 'requires_action' : held ? 'pending' : 'succeeded', metadata: {}, reason: null, receipt_number: null,
2824
+ ...(held ? { pending_reason: 'insufficient_funds', _account: account ?? null } : {}),
2825
+ balance_transaction: bt, source_transfer_reversal: null,
2826
+ // a bank transfer's refund goes to the customer's bank (a us_bank_transfer) or back to its cash balance
2827
+ // (customer_cash_balance): "If this is a `us_bank_transfer` refund, this hash contains the transaction specific details",
2828
+ // "If this is a `customer_cash_balance` refund ..." (docs.stripe.com/api/refunds/object, destination_details)
2829
+ ...(byTransfer
2830
+ ? { destination_details: toCashBalance ? { type: 'customer_cash_balance', customer_cash_balance: {} } : { type: 'us_bank_transfer', us_bank_transfer: { reference: null, reference_status: null } } }
2831
+ : refundDestination(charge, 'refund')),
2832
+ ...(awaitsBank ? { next_action: { type: 'display_details', display_details: { email_sent: { email_sent_at: now, email_sent_to: emailTo }, expires_at: now + 30 * 86_400 } } } : {}),
2833
+ });
2834
+ await ctx.write('charge', chargeId, { amount_refunded: refunded, refunded: refunded >= total }, 'charge.refunded');
2835
+ return ctx.reply(body);
2836
+ }
2837
+ /** Time's settling of held refunds, caught up to the World's clock: each refund held for want of balance, oldest first,
2838
+ * is made at the first moment the account's available balance covers it (the funds that come due then), its debit
2839
+ * written then, as `refund.updated`. Where the documentation stops and the twin decides: a held refund is made the
2840
+ * moment the balance covers it. */
2841
+ export async function settleHeldRefunds(ctx) {
2842
+ const now = Number(nowUnix(ctx.occurredAt));
2843
+ const heldRefunds = ctx.rowsRaw('refund').filter((r) => r.status === 'pending' && r.pending_reason === 'insufficient_funds').sort((a, b) => Number(a.created) - Number(b.created));
2844
+ for (const r of heldRefunds) {
2845
+ const account = typeof r._account === 'string' ? r._account : undefined;
2846
+ const currency = String(r.currency ?? 'usd');
2847
+ const amount = Number(r.amount) || 0;
2848
+ const when = [Number(r.created), ...fundsDueBetween(ctx, account, currency, Number(r.created), now)].find((t) => availableAt(ctx, account, currency, t) >= amount);
2849
+ if (when === undefined)
2850
+ continue;
2851
+ const c = await at_(ctx)(when);
2852
+ c.legal('refund', 'status', ctx.call.operation.id, 'pending', 'succeeded', String(r.id), 'vendor');
2853
+ const bt = await settleRefund(c, String(r.id), amount, currency, account ?? null);
2854
+ await c.write('refund', String(r.id), { status: 'succeeded', pending_reason: null, balance_transaction: bt }, 'refund.updated');
2855
+ }
2856
+ }
2857
+ /** The charge a refund aims at: named directly, or reached through its PaymentIntent. */
2858
+ export function target(ctx) {
2859
+ const chargeId = typeof ctx.params.charge === 'string' ? ctx.params.charge : '';
2860
+ const piId = typeof ctx.params.payment_intent === 'string' ? ctx.params.payment_intent : '';
2861
+ if (!chargeId && !piId)
2862
+ return fail(ctx, 'One of `charge` or `payment_intent` is required.', 400, 'parameter_missing');
2863
+ if (chargeId)
2864
+ return ctx.get('charge', chargeId) ?? fail(ctx, `No such charge: '${chargeId}'`, 404, 'resource_missing');
2865
+ const pi = ctx.get('payment_intent', piId);
2866
+ if (!pi)
2867
+ return fail(ctx, `No such payment_intent: '${piId}'`, 404, 'resource_missing');
2868
+ const latest = typeof pi.latest_charge === 'string' ? pi.latest_charge : undefined;
2869
+ const ch = (latest ? ctx.get('charge', latest) : undefined) ?? newest(ctx, 'charge').find((c) => c.payment_intent === piId);
2870
+ return ch ?? fail(ctx, `This PaymentIntent does not have a successful charge to refund. Its status is ${String(pi.status)}.`, 400, 'payment_intent_unexpected_state');
2871
+ }
2872
+ export const refundMissing = (ctx, id) => fail(ctx, `No such refund: '${id}'`, 404, 'resource_missing');
2873
+ /** Each advancing clock whose advance has taken its time reaches its target, caught up before anything is answered:
2874
+ * its frozen_time moves, it is ready, and its customers' trials and periods that ended by then end
2875
+ * (./renewals.ts advanceBilling). The advance's own write is when it began (the kernel's updatedAt). */
2876
+ export async function finishClockAdvances(ctx) {
2877
+ const now = Number(nowUnix(ctx.occurredAt));
2878
+ for (const clock of ctx.rowsRaw(CLOCK)) {
2879
+ if (clock.status !== 'advancing')
2880
+ continue;
2881
+ if (Date.parse(String(clock.updatedAt)) / 1000 + ADVANCE_SECONDS > now)
2882
+ continue;
2883
+ const id = String(clock.id);
2884
+ const target = Number(clock.status_details?.advancing?.target_frozen_time);
2885
+ ctx.legal(CLOCK, 'status', ctx.call.operation.id, 'advancing', 'ready', id, 'vendor');
2886
+ await ctx.write(CLOCK, id, { frozen_time: target, status: 'ready', status_details: {} }, 'test_clock.ready');
2887
+ await advanceBilling(ctx, (sub) => typeof sub.customer === 'string' && ctx.get('customer', sub.customer)?.test_clock === id);
2888
+ }
2889
+ }
2890
+ /** What the platform saved, or the settings a new platform starts with (OAuth off, no redirect URI). */
2891
+ export function connectOAuthSettings(ctx) {
2892
+ const row = ctx.rowsRaw(SETTINGS).find((r) => r.id === PLATFORM_CLIENT_ID);
2893
+ return { oauth_enabled: row?.oauth_enabled === true, redirect_uris: Array.isArray(row?.redirect_uris) ? row.redirect_uris : [] };
2894
+ }
2895
+ export const accountMissing = (ctx, id, status = 404) => fail(ctx, `No such account: '${id}'`, status, 'resource_missing');
2896
+ /** The account, tombstones included where the hand-written routes looked past a deletion. */
2897
+ export const accountRow = (ctx, id) => ctx.row('account', id, { withDeleted: true });
2898
+ /** Stripe's review of a Custom account once the platform gives it something: with nothing left due, its requested
2899
+ * capabilities become active and it can take charges and receive payouts; with something due again (its bank account
2900
+ * removed), they stop. Where the documentation stops and the twin decides: test-mode verification is immediate, and
2901
+ * it owes what customRequirements lists. An Express or Standard account is onboarded on Stripe's own pages. */
2902
+ export async function connectReview(ctx, id) {
2903
+ const account = ctx.get('account', id);
2904
+ if (account && account.type === 'custom')
2905
+ await reviewCustom(ctx, id, account);
2906
+ }
2907
+ /** The review of a Custom account (see review). */
2908
+ export async function reviewCustom(ctx, id, account) {
2909
+ const bank = ctx.rows('external_account').some((e) => e.account === id);
2910
+ const persons = ctx.rows('person').filter((p) => p.account === id);
2911
+ const { now: due, later } = customRequirements(account, bank, persons);
2912
+ // one account, one review: "If a connected account has both card_payments and transfers, and the status of either one
2913
+ // is inactive, then both capabilities are disabled" (docs.stripe.com/connect/account-capabilities)
2914
+ const ready = due.length === 0;
2915
+ const capabilities = {};
2916
+ for (const [key, was] of Object.entries(account.capabilities ?? {})) {
2917
+ const to = ready ? 'active' : 'inactive';
2918
+ // Stripe's review moves each capability, as the machine declares it may
2919
+ if (was !== to)
2920
+ ctx.legal('capability', 'status', ctx.call.operation.id, String(was), to, key, 'vendor');
2921
+ capabilities[key] = to;
2922
+ }
2923
+ const requirements = { ...(account.requirements ?? {}), currently_due: due, eventually_due: [...due, ...later], past_due: [], disabled_reason: ready ? null : 'requirements.past_due', current_deadline: null };
2924
+ await ctx.write('account', id, { capabilities, requirements, charges_enabled: ready, payouts_enabled: ready, details_submitted: ready }, ready ? 'account.updated' : 'account.update');
2925
+ }
2926
+ // ── persons on an account ──
2927
+ export const personMissing = (ctx) => fail(ctx, `No such person: '${at(ctx, 'person')}'`, 404, 'resource_missing');
2928
+ export const accountPerson = (ctx) => {
2929
+ const p = ctx.get('person', at(ctx, 'person'));
2930
+ return p && p.account === at(ctx, 'account') ? p : undefined;
2931
+ };
2932
+ // ── external accounts: a payout destination from a token or a bank_account hash ──
2933
+ export const externalMissing = (ctx) => fail(ctx, `No such external account: '${at(ctx, 'id')}'`, 404, 'resource_missing');
2934
+ export const accountExternal = (ctx) => {
2935
+ const e = ctx.get('external_account', at(ctx, 'id'));
2936
+ return e && e.account === at(ctx, 'account') ? e : undefined;
2937
+ };
2938
+ /** Whether the platform may create, update or delete an account's external accounts: "A platform can create, update, or
2939
+ * delete external accounts only for connected accounts without access to either the full Stripe or Express Dashboard
2940
+ * and where the platform is responsible for negative balances" (docs.stripe.com/connect/payouts-bank-accounts); it can
2941
+ * still view an Express account's ("A platform can view the external accounts of connected accounts that don’t have
2942
+ * access to the full Stripe Dashboard"). Where the documentation stops and the twin decides: the refusal. Stripe's
2943
+ * pages give no error code for it; the twin answers 403, which the errors page gives as "The API key doesn’t have
2944
+ * permissions to perform the request" (docs.stripe.com/api/errors), with the twin's own wording. The create page says
2945
+ * it too: "You can only specify connected accounts where account.controller.requirement_collection is `application`"
2946
+ * (docs.stripe.com/api/external_account_bank_accounts/create). */
2947
+ export function refuseExternalWrite(ctx, account) {
2948
+ const row = accountRow(ctx, account);
2949
+ const controller = row?.controller ?? controllerOf(String(row?.type ?? 'standard'));
2950
+ const dashboard = String((controller.stripe_dashboard?.type) ?? '');
2951
+ const losses = String((controller.losses?.payments) ?? '');
2952
+ if (dashboard === 'none' && losses === 'application')
2953
+ return undefined;
2954
+ return ctx.refuse({ status: 403, message: `This application does not have the required permissions to manage the external accounts of account '${account}': they can only be managed through the account's ${dashboard === 'express' ? 'Express' : 'Stripe'} Dashboard.` });
2955
+ }
2956
+ /** Update and delete by id, finding tombstones as the hand-written routes did, with Stripe's own operation names. */
2957
+ export function patch(resource, param, missing, op) {
2958
+ return (ctx) => patchRow(ctx, resource, param, missing, op);
2959
+ }
2960
+ export async function patchRow(ctx, resource, param, missing, op) {
2961
+ const id = at(ctx, param);
2962
+ if (!ctx.row(resource, id, { withDeleted: true }))
2963
+ return fail(ctx, `No such ${missing}: '${id}'`, 404, 'resource_missing');
2964
+ return ctx.reply(await ctx.write(resource, id, ctx.params, op));
2965
+ }
2966
+ // A reader's update takes its label and metadata only; it moves to another location by registering it
2967
+ // there again (https://docs.stripe.com/api/terminal/readers/update, docs.stripe.com/terminal/fleet/locations-and-zones)
2968
+ // (the parameter check refuses anything else, as Stripe does: ./params.ts)
2969
+ export const updateReader = patch(READER, 'reader', 'reader', 'terminal_reader.updated');
2970
+ export function remove(resource, param, missing, op, object) {
2971
+ return async (ctx) => {
2972
+ const id = at(ctx, param);
2973
+ if (!ctx.row(resource, id, { withDeleted: true }))
2974
+ return fail(ctx, `No such ${missing}: '${id}'`, 404, 'resource_missing');
2975
+ const row = ctx.get(resource, id);
2976
+ await ctx.write(resource, id, { deleted: true }, op);
2977
+ // a deleted reader still says what device it was
2978
+ return ctx.reply({ id, object, deleted: true, ...(object === 'terminal.reader' && row ? { device_type: row.device_type, serial_number: row.serial_number } : {}) });
2979
+ };
2980
+ }
2981
+ export const readerMissing = (ctx, id) => fail(ctx, `No such reader: '${id}'`, 404, 'resource_missing');
2982
+ /** A card presented to a reader with no payment in progress. */
2983
+ export function nothingToPresent(ctx) {
2984
+ return fail(ctx, 'This reader has no in-progress payment to present a payment method to.', 400, 'terminal_reader_invalid_state');
2985
+ }
2986
+ /** Under manual capture the presented card only holds the amount (docs.stripe.com/terminal/payments/collect-card-payment#capture-payment):
2987
+ * the hold is an uncaptured charge, as an online authorization's is (authorizeCharge), which the intent's capture takes. */
2988
+ export async function holdForCapture(ctx, piId, pi, pm, amount) {
2989
+ const charge = await authorizeCharge(ctx, piId, { ...pi, payment_method: pm }, amount);
2990
+ await ctx.write('payment_intent', piId, { status: 'requires_capture', payment_method: pm, amount_capturable: amount, latest_charge: charge }, 'payment_intent.amount_capturable_updated');
2991
+ }
2992
+ /** Time's settling of top-ups, caught up to the World's clock: a pending top-up succeeds on its expected_availability_date,
2993
+ * its funds entered then in the balance it names. */
2994
+ export async function settleTopups(ctx) {
2995
+ const now = Number(nowUnix(ctx.occurredAt));
2996
+ for (const t of ctx.rowsRaw('topup')) {
2997
+ if (t.status !== 'pending' || Number(t.expected_availability_date) > now)
2998
+ continue;
2999
+ const c = await at_(ctx)(Number(t.expected_availability_date));
3000
+ c.legal('topup', 'status', ctx.call.operation.id, 'pending', 'succeeded', String(t.id), 'vendor');
3001
+ const bt = await settleTopup(c, String(t.id), Number(t.amount), String(t.currency), t.destination_balance === 'issuing' ? 'issuing' : 'payments');
3002
+ await c.write('topup', String(t.id), { status: 'succeeded', balance_transaction: bt }, 'topup.succeeded');
3003
+ }
3004
+ }
3005
+ export const disputesDisputeMissing = (ctx, id) => fail(ctx, `No such dispute: '${id}'`, 404, 'resource_missing');
3006
+ /** Evidence staged without submitting it (submit=false), or on a dispute already under review or decided. */
3007
+ export async function keepEvidence(ctx, id, fields) {
3008
+ return ctx.reply(await ctx.write('dispute', id, fields, 'dispute.update'));
3009
+ }
3010
+ export const meterMissing = (ctx, id) => fail(ctx, `No such meter: '${id}'`, 404, 'resource_missing');
3011
+ export function activation(kind) {
3012
+ return async (ctx) => {
3013
+ const id = at(ctx, 'id');
3014
+ const m = ctx.get(METER, id);
3015
+ if (!m)
3016
+ return meterMissing(ctx, id);
3017
+ const refused = ctx.legal(METER, 'status', kind === 'deactivate' ? 'PostBillingMetersIdDeactivate' : 'PostBillingMetersIdReactivate', m.status, undefined, id);
3018
+ if (refused)
3019
+ return ctx.refuse(refused);
3020
+ const fields = { ...(kind === 'deactivate' ? { status: 'inactive', status_transitions: { deactivated_at: nowUnix(ctx.occurredAt) } } : { status: 'active', status_transitions: { deactivated_at: null } }), updated: nowUnix(ctx.occurredAt) };
3021
+ return ctx.reply(await ctx.write(METER, id, fields, `billing_meter.${kind}`));
3022
+ };
3023
+ }
3024
+ export const grantMissing = (ctx, id) => fail(ctx, `No such credit grant: '${id}'`, 404, 'resource_missing');
3025
+ // expiring or voiding a grant stamps the instant; it no longer counts toward the balance
3026
+ export function endGrant(kind) {
3027
+ return async (ctx) => {
3028
+ const id = at(ctx, 'id');
3029
+ if (!ctx.get(GRANT, id))
3030
+ return grantMissing(ctx, id);
3031
+ return ctx.reply(await ctx.write(GRANT, id, { ...(kind === 'void' ? { voided_at: nowUnix(ctx.occurredAt) } : { expires_at: nowUnix(ctx.occurredAt) }), updated: nowUnix(ctx.occurredAt) }, `credit_grant.${kind}`));
3032
+ };
3033
+ }
3034
+ export function alertMove(kind, operationId) {
3035
+ return async (ctx) => {
3036
+ const id = at(ctx, 'id');
3037
+ const a = ctx.get(ALERT, id);
3038
+ if (!a)
3039
+ return fail(ctx, `No such alert: '${id}'`, 404, 'resource_missing');
3040
+ const refused = ctx.legal(ALERT, 'status', operationId, a.status, undefined, id);
3041
+ if (refused)
3042
+ return ctx.refuse(refused);
3043
+ const status = kind === 'activate' ? 'active' : kind === 'deactivate' ? 'inactive' : 'archived';
3044
+ return ctx.reply(await ctx.write(ALERT, id, { status }, `billing_alert.${kind}`));
3045
+ };
3046
+ }
3047
+ export const transferMissing = (ctx, id) => fail(ctx, `No such transfer: '${id}'`, 404, 'resource_missing');
3048
+ /** A transfer to an account whose transfers capability is not active. */
3049
+ export function transfersInactive(ctx) {
3050
+ return ctx.refuse({ status: 400, code: 'insufficient_capabilities_for_transfer', param: 'destination', message: 'Your destination account needs to have at least one of the following capabilities enabled: transfers, crypto_transfers, legacy_payments' });
3051
+ }
3052
+ /** A transfer from a charge whose funds have settled: "With a source_transaction, the transfer request returns success
3053
+ * regardless of your available balance if the related charge hasn't settled yet" (docs.stripe.com/connect/separate-
3054
+ * charges-and-transfers), so a settled one comes out of the available balance. */
3055
+ export function settledSourceRefused(ctx, amount, currency) {
3056
+ return refusePayout(ctx, amount, currency, undefined);
3057
+ }
3058
+ /** An application fee refunded to its whole through its own refunds endpoint: the fee machine's move, or its refusal. */
3059
+ export function feeFullyRefunded(ctx, resource, flag, id) {
3060
+ const refused = ctx.legal(resource, flag, ctx.call.operation.id, 'false', 'true', id);
3061
+ return refused ? ctx.refuse(refused) : undefined;
3062
+ }
3063
+ /** Take `amount` (the remainder by default) back out of a parent's total, as a child row appended to its list. */
3064
+ export async function giveBack(ctx, o) {
3065
+ const id = String(o.parent.id);
3066
+ const total = Number(o.parent.amount ?? 0);
3067
+ const already = Number(o.parent[o.doneField] ?? 0);
3068
+ const remaining = total - already;
3069
+ const amount = ctx.params.amount !== undefined ? Math.trunc(Number(ctx.params.amount) || 0) : remaining;
3070
+ if (amount <= 0 || amount > remaining)
3071
+ return fail(ctx, o.tooLarge(remaining), 400, 'amount_too_large');
3072
+ const done = already + amount;
3073
+ // a fee refunded in full is `refunded`: the application fee machine's move (a transfer's `reversed` is no state)
3074
+ const feeRefusal = o.parentResource === 'application_fee' && done >= total && o.parent[o.flag] !== true ? feeFullyRefunded(ctx, o.parentResource, o.flag, id) : undefined;
3075
+ if (feeRefusal)
3076
+ return feeRefusal;
3077
+ // the `metadata` a reversal or a fee refund is created with is its own ("Set of key-value pairs that you can attach to
3078
+ // an object", docs.stripe.com/api/transfer_reversals/create, docs.stripe.com/api/fee_refunds/create)
3079
+ const metadata = ctx.params.metadata && typeof ctx.params.metadata === 'object' ? { metadata: ctx.params.metadata } : {};
3080
+ const child = await created(ctx, o.child, { ...o.childFields(amount), ...metadata }, o.childDefaults);
3081
+ if (o.settle)
3082
+ await o.settle(child, amount);
3083
+ const existing = o.parent[o.flag === 'reversed' ? 'reversals' : 'refunds'] ?? { object: 'list', data: [], has_more: false, total_count: 0 };
3084
+ const data = [...(existing.data ?? []), child];
3085
+ await ctx.write(o.parentResource, id, {
3086
+ [o.doneField]: done, [o.flag]: done >= total,
3087
+ [o.flag === 'reversed' ? 'reversals' : 'refunds']: { ...existing, data, total_count: data.length, url: o.listUrl },
3088
+ }, o.op);
3089
+ return ctx.reply(child);
3090
+ }
3091
+ export const feeMissing = (ctx, id) => fail(ctx, `No such application fee: '${id}'`, 404, 'resource_missing');
3092
+ /** An earlier version's payment_method_types refused: the first value that names no payment method type the
3093
+ * operation takes, in the intent's words or (a Checkout Session) the enum's. */
3094
+ export function unknownMethodType(ctx, opId, given) {
3095
+ const values = Array.isArray(given) ? given : given && typeof given === 'object' ? Object.values(given) : [given];
3096
+ const checkout = opId === 'PostCheckoutSessions';
3097
+ const known = checkout ? CHECKOUT_METHOD_TYPES : PAYMENT_METHOD_TYPES;
3098
+ const i = values.findIndex((v) => !known.includes(String(v)));
3099
+ if (i < 0)
3100
+ return undefined;
3101
+ if (checkout) {
3102
+ const param = `payment_method_types[${i}]`;
3103
+ return ctx.refuse({ status: 400, param, message: `Invalid ${param}: must be one of ${oneOf(known)}` });
3104
+ }
3105
+ const doc = TYPES_DOC[opId.startsWith('PostSetupIntents') ? 'setup_intents' : 'payment_intents'];
3106
+ return ctx.refuse({ status: 400, param: 'payment_method_types', message: `The payment method type "${String(values[i])}" is invalid. See ${doc} for the full list of supported payment method types.` });
3107
+ }
3108
+ /** payment_method_types refused where the caller's version removed it. */
3109
+ export const noLongerSupported = (ctx, version) => ctx.refuse({ status: 400, code: 'payment_method_types_no_longer_supported', param: 'payment_method_types', message: `payment_method_types is no longer supported in API version ${version}. Use allowed_payment_method_types, or dynamic payment methods, instead.` });
3110
+ /** Stripe's refusal of a request whose top-level parameters the operation does not take, or undefined. */
3111
+ export function refuseParameters(ctx) {
3112
+ const op = OPS.get(ctx.call.operation.id);
3113
+ if (!op)
3114
+ return undefined;
3115
+ const pinned = ctx.call.request.headers.get('stripe-version');
3116
+ const params = ctx.params;
3117
+ if (pinned && pinned < SERVED_VERSION) {
3118
+ if (!TYPED.has(op.id))
3119
+ return undefined;
3120
+ if (params.payment_method_types === undefined)
3121
+ return undefined;
3122
+ if (REMOVED_TYPES.has(op.id) && methodTypesRemoved(pinned))
3123
+ return noLongerSupported(ctx, pinned);
3124
+ return unknownMethodType(ctx, op.id, params.payment_method_types);
3125
+ }
3126
+ const method = ctx.call.request.method.toUpperCase();
3127
+ const known = new Set([...(op.query ?? []), ...(op.body ?? [])].map((p) => p.name).concat(DOCUMENTED[op.id] ?? []));
3128
+ if (REMOVED_TYPES.has(op.id) && params.payment_method_types !== undefined)
3129
+ return noLongerSupported(ctx, pinned ?? SERVED_VERSION);
3130
+ for (const name of Object.keys(params)) {
3131
+ if (!known.has(name))
3132
+ return ctx.refuse({ status: 400, code: 'parameter_unknown', param: name, message: `Received unknown parameter: ${name}` });
3133
+ }
3134
+ const required = method === 'GET' || method === 'DELETE' ? (op.query ?? []) : (op.body ?? []);
3135
+ for (const p of required) {
3136
+ if (p.required && params[p.name] === undefined)
3137
+ return ctx.refuse({ status: 400, code: 'parameter_missing', param: p.name, message: `Missing required param: ${p.name}.` });
3138
+ }
3139
+ return undefined;
3140
+ }
3141
+ /** A file's links, kept in step with the links made to it, at the address Stripe lists them
3142
+ * (/v1/file_links?file=…, the File object's example). */
3143
+ export async function syncLinks(ctx, fileId) {
3144
+ const data = newest(ctx, 'file_link').filter((l) => l.file === fileId);
3145
+ await ctx.write('file', fileId, { links: { object: 'list', data, has_more: false, url: `/v1/file_links?file=${fileId}` } }, 'file.links_synced');
3146
+ }
3147
+ // ── calculations: each line's tax on top of its amount; no `created`, a tax_date instead ──
3148
+ export const calcMissing = (ctx, id) => fail(ctx, `No such tax calculation: '${id}'`, 404, 'resource_missing');
3149
+ /** A page of the line items a stored object carries. */
3150
+ export function taxLinePage(ctx, holder) {
3151
+ const li = holder.line_items;
3152
+ const { page, hasMore } = paginate(Array.isArray(li?.data) ? li.data : [], ctx.params);
3153
+ return ctx.reply({ object: 'list', url: path(ctx), has_more: hasMore, data: page });
3154
+ }
3155
+ export const txMissing = (ctx, id, status = 404) => fail(ctx, `No such tax transaction: '${id}'`, status, 'resource_missing');
3156
+ /** An answer in the shape of the API version the caller is served (./version.ts), a webhook endpoint's `secret` shown
3157
+ * only at its creation ("Only returned at creation", docs.stripe.com/api/webhook_endpoints/object). */
3158
+ export async function rendered(request, res) {
3159
+ const creates = request.method === 'POST' && new URL(request.url).pathname.replace(/\/+$/, '') === '/v1/webhook_endpoints';
3160
+ const pinned = request.headers.get('stripe-version');
3161
+ if (!(res.headers.get('content-type') ?? '').includes('json'))
3162
+ return res;
3163
+ if (!servesCurrent(pinned) && creates)
3164
+ return res;
3165
+ const expand = await expandOf(request.clone());
3166
+ const body = await res.json();
3167
+ const answered = creates ? body : withoutEndpointSecret(body);
3168
+ return new Response(JSON.stringify(servesCurrent(pinned) ? render(answered, pinned, expand) : answered), { status: res.status, statusText: res.statusText, headers: res.headers });
3169
+ }