@volter/twin-stripe 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +51 -21
  2. package/dist/generated/pack-facts.json +3 -3
  3. package/dist/journeys/coverage.json +596 -0
  4. package/dist/journeys/customer-life.json +6108 -0
  5. package/dist/journeys/customer-life.outline.md +371 -0
  6. package/dist/journeys/decisions.json +677 -0
  7. package/dist/journeys/demand.json +706 -0
  8. package/dist/journeys/score.json +853 -0
  9. package/dist/journeys/unreachable.json +102 -0
  10. package/dist/journeys/vendor-examples.json +8010 -0
  11. package/dist/spec/SOURCE.md +5 -1
  12. package/dist/spec/doc-examples.json +445 -0
  13. package/dist/spec/sources.json +135 -207
  14. package/dist/src/cli.d.ts +0 -1
  15. package/dist/src/cli.js +0 -2
  16. package/dist/src/engine/after-payment.d.ts +0 -1
  17. package/dist/src/engine/after-payment.js +0 -1
  18. package/dist/src/engine/common.d.ts +5 -5
  19. package/dist/src/engine/common.js +13 -11
  20. package/dist/src/engine/connect.d.ts +0 -21
  21. package/dist/src/engine/connect.js +1 -82
  22. package/dist/src/engine/customers.d.ts +0 -8
  23. package/dist/src/engine/customers.js +0 -22
  24. package/dist/src/engine/display.d.ts +0 -3
  25. package/dist/src/engine/display.js +0 -3
  26. package/dist/src/engine/files.d.ts +0 -7
  27. package/dist/src/engine/files.js +0 -10
  28. package/dist/src/engine/invoices.d.ts +6 -9
  29. package/dist/src/engine/invoices.js +56 -29
  30. package/dist/src/engine/issuing.d.ts +6 -8
  31. package/dist/src/engine/issuing.js +19 -10
  32. package/dist/src/engine/ledger.d.ts +0 -1
  33. package/dist/src/engine/ledger.js +0 -1
  34. package/dist/src/engine/oauth.js +1 -2
  35. package/dist/src/engine/refunds.js +2 -27
  36. package/dist/src/engine/stripe.d.ts +2 -7
  37. package/dist/src/engine/stripe.js +34 -83
  38. package/dist/src/engine/subscriptions.d.ts +0 -1
  39. package/dist/src/engine/subscriptions.js +0 -1
  40. package/dist/src/engine/version.d.ts +0 -2
  41. package/dist/src/engine/version.js +17 -6
  42. package/dist/src/fetch.js +2 -2
  43. package/dist/src/generated/surface.gen.json +1 -1
  44. package/dist/src/generated/ui.gen.json +1 -1
  45. package/dist/src/index.js +5 -3
  46. package/dist/src/manifest.js +348 -220
  47. package/dist/src/screens/billing-portal.js +5 -5
  48. package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
  49. package/dist/src/screens/checkout-js.js +84 -0
  50. package/dist/src/screens/connect-onboarding.js +3 -3
  51. package/dist/src/screens/dashboard.d.ts +1 -1
  52. package/dist/src/screens/dashboard.js +52 -53
  53. package/dist/src/screens/stripe-js.js +53 -93
  54. package/dist/src/semantics/account.d.ts +2 -0
  55. package/dist/src/semantics/account.js +8 -0
  56. package/dist/src/semantics/accounts.js +3 -1
  57. package/dist/src/semantics/around.js +3 -6
  58. package/dist/src/semantics/billing_portal.d.ts +0 -1
  59. package/dist/src/semantics/billing_portal.js +1 -7
  60. package/dist/src/semantics/checkout.d.ts +1 -1
  61. package/dist/src/semantics/checkout.js +18 -15
  62. package/dist/src/semantics/clock.js +1 -7
  63. package/dist/src/semantics/customers.d.ts +1 -2
  64. package/dist/src/semantics/customers.js +32 -38
  65. package/dist/src/semantics/entitlements.d.ts +0 -2
  66. package/dist/src/semantics/entitlements.js +4 -50
  67. package/dist/src/semantics/events.d.ts +2 -3
  68. package/dist/src/semantics/events.js +6 -11
  69. package/dist/src/semantics/index.js +16 -38
  70. package/dist/src/semantics/invoices.d.ts +3 -8
  71. package/dist/src/semantics/invoices.js +134 -127
  72. package/dist/src/semantics/issuing.d.ts +6 -0
  73. package/dist/src/semantics/issuing.js +77 -0
  74. package/dist/src/semantics/payment_intents.d.ts +0 -1
  75. package/dist/src/semantics/payment_intents.js +5 -13
  76. package/dist/src/semantics/payment_methods.d.ts +0 -2
  77. package/dist/src/semantics/payment_methods.js +2 -26
  78. package/dist/src/semantics/payouts.d.ts +1 -0
  79. package/dist/src/semantics/payouts.js +8 -1
  80. package/dist/src/semantics/products.d.ts +1 -3
  81. package/dist/src/semantics/products.js +3 -14
  82. package/dist/src/semantics/refunds.d.ts +1 -2
  83. package/dist/src/semantics/refunds.js +1 -5
  84. package/dist/src/semantics/shared.d.ts +64 -336
  85. package/dist/src/semantics/shared.js +326 -1336
  86. package/dist/src/semantics/states.d.ts +20 -89
  87. package/dist/src/semantics/states.js +116 -334
  88. package/dist/src/semantics/subscriptions.d.ts +1 -2
  89. package/dist/src/semantics/subscriptions.js +23 -145
  90. package/dist/src/semantics/test_helpers.d.ts +2 -5
  91. package/dist/src/semantics/test_helpers.js +121 -43
  92. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  93. package/dist/src/semantics/webhook_endpoints.js +26 -0
  94. package/generated/pack-facts.json +3 -3
  95. package/journeys/coverage.json +596 -0
  96. package/journeys/customer-life.json +6108 -0
  97. package/journeys/customer-life.outline.md +371 -0
  98. package/journeys/decisions.json +677 -0
  99. package/journeys/demand.json +706 -0
  100. package/journeys/score.json +853 -0
  101. package/journeys/unreachable.json +102 -0
  102. package/journeys/vendor-examples.json +8010 -0
  103. package/package.json +4 -3
  104. package/spec/SOURCE.md +5 -1
  105. package/spec/doc-examples.json +445 -0
  106. package/spec/sources.json +135 -207
  107. package/src/cli.ts +0 -3
  108. package/src/engine/after-payment.ts +0 -2
  109. package/src/engine/balance.ts +0 -1
  110. package/src/engine/checkout.ts +0 -1
  111. package/src/engine/common.ts +19 -17
  112. package/src/engine/connect.ts +1 -79
  113. package/src/engine/customers.ts +0 -25
  114. package/src/engine/display.ts +2 -3
  115. package/src/engine/files.ts +0 -13
  116. package/src/engine/invoices.ts +64 -33
  117. package/src/engine/issuing.ts +16 -11
  118. package/src/engine/ledger.ts +0 -2
  119. package/src/engine/oauth.ts +1 -2
  120. package/src/engine/onboarding.ts +0 -1
  121. package/src/engine/params.ts +0 -1
  122. package/src/engine/payment-intents.ts +0 -1
  123. package/src/engine/payment-methods.ts +0 -1
  124. package/src/engine/refunds.ts +2 -28
  125. package/src/engine/renewals.ts +0 -1
  126. package/src/engine/stripe.ts +35 -83
  127. package/src/engine/subscriptions.ts +0 -3
  128. package/src/engine/transfers.ts +0 -1
  129. package/src/engine/version.ts +15 -7
  130. package/src/engine/webhooks.ts +0 -2
  131. package/src/fetch.ts +2 -2
  132. package/src/generated/surface.gen.json +1 -1
  133. package/src/generated/ui.gen.json +1 -1
  134. package/src/index.ts +6 -4
  135. package/src/manifest.ts +348 -220
  136. package/src/screens/billing-portal.tsx +5 -7
  137. package/src/screens/checkout-js.ts +80 -0
  138. package/src/screens/checkout.tsx +0 -2
  139. package/src/screens/connect-onboarding.tsx +3 -5
  140. package/src/screens/dashboard.tsx +53 -73
  141. package/src/screens/stripe-js.ts +57 -125
  142. package/src/semantics/account.ts +10 -0
  143. package/src/semantics/account_links.ts +0 -1
  144. package/src/semantics/accounts.ts +8 -7
  145. package/src/semantics/apps.ts +1 -2
  146. package/src/semantics/around.ts +3 -6
  147. package/src/semantics/balance.ts +0 -1
  148. package/src/semantics/balance_transactions.ts +1 -3
  149. package/src/semantics/billing_portal.ts +1 -7
  150. package/src/semantics/charges.ts +1 -6
  151. package/src/semantics/checkout.ts +16 -12
  152. package/src/semantics/clock.ts +1 -8
  153. package/src/semantics/coupons.ts +0 -1
  154. package/src/semantics/customers.ts +32 -35
  155. package/src/semantics/doors.ts +2 -3
  156. package/src/semantics/entitlements.ts +1 -45
  157. package/src/semantics/events.ts +7 -14
  158. package/src/semantics/index.ts +16 -38
  159. package/src/semantics/invoice_payments.ts +1 -3
  160. package/src/semantics/invoices.ts +124 -119
  161. package/src/semantics/issuing.ts +74 -0
  162. package/src/semantics/payment_intents.ts +10 -17
  163. package/src/semantics/payment_methods.ts +2 -25
  164. package/src/semantics/payouts.ts +9 -3
  165. package/src/semantics/prices.ts +0 -1
  166. package/src/semantics/products.ts +4 -14
  167. package/src/semantics/promotion_codes.ts +0 -1
  168. package/src/semantics/radar.ts +1 -3
  169. package/src/semantics/refunds.ts +2 -11
  170. package/src/semantics/shared.ts +350 -1382
  171. package/src/semantics/states.ts +109 -330
  172. package/src/semantics/subscriptions.ts +25 -145
  173. package/src/semantics/test_helpers.ts +109 -43
  174. package/src/semantics/transfers.ts +1 -3
  175. package/src/semantics/webhook_endpoints.ts +27 -0
  176. package/src/server.ts +0 -1
  177. package/dist/src/engine/billing.d.ts +0 -5
  178. package/dist/src/engine/billing.js +0 -6
  179. package/dist/src/engine/charges.d.ts +0 -6
  180. package/dist/src/engine/charges.js +0 -13
  181. package/dist/src/engine/connect-settings.d.ts +0 -16
  182. package/dist/src/engine/connect-settings.js +0 -26
  183. package/dist/src/engine/coupons.d.ts +0 -0
  184. package/dist/src/engine/coupons.js +0 -5
  185. package/dist/src/engine/credit-notes.d.ts +0 -1
  186. package/dist/src/engine/credit-notes.js +0 -5
  187. package/dist/src/engine/disputes.d.ts +0 -0
  188. package/dist/src/engine/disputes.js +0 -9
  189. package/dist/src/engine/entitlements.d.ts +0 -1
  190. package/dist/src/engine/entitlements.js +0 -4
  191. package/dist/src/engine/ephemeral-keys.d.ts +0 -0
  192. package/dist/src/engine/ephemeral-keys.js +0 -4
  193. package/dist/src/engine/front.d.ts +0 -5
  194. package/dist/src/engine/front.js +0 -25
  195. package/dist/src/engine/payment-links.d.ts +0 -1
  196. package/dist/src/engine/payment-links.js +0 -5
  197. package/dist/src/engine/plans.d.ts +0 -6
  198. package/dist/src/engine/plans.js +0 -26
  199. package/dist/src/engine/platform.d.ts +0 -39
  200. package/dist/src/engine/platform.js +0 -34
  201. package/dist/src/engine/products.d.ts +0 -0
  202. package/dist/src/engine/products.js +0 -4
  203. package/dist/src/engine/setup-intents.d.ts +0 -1
  204. package/dist/src/engine/setup-intents.js +0 -4
  205. package/dist/src/engine/subscription-schedules.d.ts +0 -3
  206. package/dist/src/engine/subscription-schedules.js +0 -1
  207. package/dist/src/engine/tax.d.ts +0 -7
  208. package/dist/src/engine/tax.js +0 -28
  209. package/dist/src/engine/terminal.d.ts +0 -4
  210. package/dist/src/engine/terminal.js +0 -20
  211. package/dist/src/engine/test-clocks.d.ts +0 -5
  212. package/dist/src/engine/test-clocks.js +0 -9
  213. package/dist/src/engine/tokens.d.ts +0 -7
  214. package/dist/src/engine/tokens.js +0 -20
  215. package/dist/src/engine/treasury.d.ts +0 -23
  216. package/dist/src/engine/treasury.js +0 -45
  217. package/dist/src/screens/connect-oauth.js +0 -333
  218. package/dist/src/semantics/disputes.d.ts +0 -2
  219. package/dist/src/semantics/disputes.js +0 -5
  220. package/dist/src/semantics/invoiceitems.d.ts +0 -3
  221. package/dist/src/semantics/invoiceitems.js +0 -59
  222. package/dist/src/semantics/setup_intents.d.ts +0 -3
  223. package/dist/src/semantics/setup_intents.js +0 -41
  224. package/dist/src/semantics/subscription_items.d.ts +0 -5
  225. package/dist/src/semantics/subscription_items.js +0 -55
  226. package/dist/src/semantics/subscription_schedules.d.ts +0 -4
  227. package/dist/src/semantics/subscription_schedules.js +0 -60
  228. package/src/engine/billing.ts +0 -16
  229. package/src/engine/charges.ts +0 -19
  230. package/src/engine/connect-settings.ts +0 -23
  231. package/src/engine/coupons.ts +0 -5
  232. package/src/engine/credit-notes.ts +0 -6
  233. package/src/engine/disputes.ts +0 -9
  234. package/src/engine/entitlements.ts +0 -5
  235. package/src/engine/ephemeral-keys.ts +0 -3
  236. package/src/engine/front.ts +0 -31
  237. package/src/engine/payment-links.ts +0 -6
  238. package/src/engine/plans.ts +0 -42
  239. package/src/engine/platform.ts +0 -47
  240. package/src/engine/products.ts +0 -3
  241. package/src/engine/setup-intents.ts +0 -5
  242. package/src/engine/subscription-schedules.ts +0 -10
  243. package/src/engine/tax.ts +0 -39
  244. package/src/engine/terminal.ts +0 -23
  245. package/src/engine/test-clocks.ts +0 -11
  246. package/src/engine/tokens.ts +0 -22
  247. package/src/engine/treasury.ts +0 -47
  248. package/src/screens/connect-oauth.tsx +0 -376
  249. package/src/semantics/disputes.ts +0 -12
  250. package/src/semantics/invoiceitems.ts +0 -59
  251. package/src/semantics/setup_intents.ts +0 -45
  252. package/src/semantics/subscription_items.ts +0 -54
  253. package/src/semantics/subscription_schedules.ts +0 -66
@@ -3,35 +3,24 @@ import { redactKey, requestKey } from "../engine/oauth.js";
3
3
  import { HOUR, INV, itemQuantity, itemsOf, keptAfter, oneTimeLines, RENEWING, SUB } from "../engine/renewals.js";
4
4
  import { DAY, payoutDay } from "../engine/balance.js";
5
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";
6
+ import { accountSettings, addBillingInterval, applyCouponDiscount, asBool, buildDiscount, buildSubscriptionItemsList, cardError, chargeDefaults, declineFor, disputeEvidence, microdepositsNextAction, mintClientSecret, nowUnix, OBJECT_NAME, paginate, paymentMethodSubObject, PLATFORM_ACCOUNT_ID, priceAmount, requiresAuthentication, requiresMicrodeposits, resolveSubscriptionBillingInterval, resolveTrial, searchOver, subscriptionItemEntries, subscriptionItemPrice, TEST_TOKEN_DECLINES, threeDsNextAction, view } from "../engine/stripe.js";
12
7
  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";
8
+ import { collectsThroughIntent, invoicePrefix, transitions } from "../engine/invoices.js";
15
9
  import { CS, isOneTime, nestedMetadata } from "../engine/checkout.js";
16
- import { externalList, refundDestination, SPEC_FIELDS } from "../engine/common.js";
10
+ import { externalList, SPEC_FIELDS } from "../engine/common.js";
17
11
  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
12
  import { intentCarries, PI, TYPES_GIVEN } from "../engine/payment-intents.js";
21
13
  import { BT, BYPASS_NUMBERS, feeOf, onAccount, UNSENT } from "../engine/ledger.js";
22
- import { withoutCard } from "../engine/charges.js";
23
14
  import { MAX_TRIAL, movedTrial } from "../engine/subscriptions.js";
24
15
  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";
16
+ import { AUTH, historyEntry, merchantData, REALTIME_WINDOW_SECONDS, TXN } from "../engine/issuing.js";
28
17
  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";
18
+ import { customRequirements } from "../engine/connect.js";
32
19
  import { expandOf, render, SERVED_VERSION, servesCurrent, withoutEndpointSecret } from "../engine/version.js";
33
- import { ALERT, GRANT, METER } from "../engine/billing.js";
34
20
  import { CHECKOUT_METHOD_TYPES, DOCUMENTED, methodTypesRemoved, oneOf, OPS, PAYMENT_METHOD_TYPES, REMOVED_TYPES, TYPED, TYPES_DOC } from "../engine/params.js";
21
+ import { linesOf, sumLines } from "../engine/invoices.js";
22
+ import { refundDestination } from "../engine/common.js";
23
+ import { REFUND_REASONS } from "../engine/refunds.js";
35
24
  /** Stripe's events: `Stripe-Signature: t=…,v1=…` over `${t}.${body}` keyed by the endpoint's secret
36
25
  * (docs.stripe.com/webhooks#verify-manually); the Event object (docs.stripe.com/api/events/object) kept as the account's
37
26
  * own (`GET /v1/events`), whether or not an endpoint takes it; an endpoint live while `enabled`, taking the types its
@@ -48,19 +37,6 @@ export const STRIPE_EVENTS = {
48
37
  },
49
38
  endpoints: { storedAs: 'webhook_endpoint', url: 'url', secret: 'secret', filter: 'enabled_events', status: { field: 'status', live: 'enabled' }, scope: { field: 'connect', value: '$account' } },
50
39
  };
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
40
  /** A customer's now: its test clock's frozen time when it is on one (docs.stripe.com/billing/testing/test-clocks), the
65
41
  * World's otherwise. */
66
42
  export function clockNow(ctx, customer) {
@@ -82,7 +58,7 @@ export function customerDefault(ctx, sub) {
82
58
  * it did not take (added after it was drafted) stays for the next (unspentDiscounts). */
83
59
  export async function spendOnceCoupon(ctx, invoice) {
84
60
  const subId = typeof invoice.subscription === 'string' ? invoice.subscription : undefined;
85
- const sub = subId ? ctx.get(SUB, subId) : undefined;
61
+ const sub = subId ? resourceView(ctx, SUB, subId) : undefined;
86
62
  if (!sub || !subId)
87
63
  return;
88
64
  const all = sub.discounts ?? [];
@@ -137,7 +113,7 @@ export async function draftSubscriptionInvoice(ctx, sub, o) {
137
113
  * whether it was paid. */
138
114
  export async function collectSubscriptionInvoice(ctx, invoiceId, at, actor, opts = {}) {
139
115
  const inv = ctx.get(INV, invoiceId);
140
- const sub = inv && typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
116
+ const sub = inv && typeof inv.subscription === 'string' ? resourceView(ctx, SUB, inv.subscription) : undefined;
141
117
  if (!inv || !sub || inv.status !== 'draft')
142
118
  return false;
143
119
  const total = Number(inv.total) || 0;
@@ -162,7 +138,7 @@ export async function collectSubscriptionInvoice(ctx, invoiceId, at, actor, opts
162
138
  const piId = ctx.mint('payment_intent');
163
139
  const currency = String(inv.currency ?? 'usd');
164
140
  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 }, {
141
+ const intent = await created(ctx, 'payment_intent', { id: piId, amount: total, currency, customer: inv.customer, invoice: invoiceId, payment_method: pays ? payer : null }, {
166
142
  status: pays ? 'succeeded' : 'requires_payment_method', client_secret: mintClientSecret(piId, await ctx.secret(`client-secret:${piId}`)), livemode: false,
167
143
  capture_method: 'automatic', amount_capturable: 0, amount_received: pays ? total : 0, next_action: null, latest_charge: chargeId,
168
144
  last_payment_error: decline ? { type: 'card_error', code: decline.code, decline_code: decline.decline_code ?? null, message: decline.message, payment_method: { id: payer } } : null,
@@ -170,6 +146,8 @@ export async function collectSubscriptionInvoice(ctx, invoiceId, at, actor, opts
170
146
  });
171
147
  await ctx.write(INV, invoiceId, {
172
148
  ...numbered, status: pays ? 'paid' : 'open', paid: pays, attempt_count: attempt ? 1 : 0, attempted: attempt, payment_intent: piId, charge: chargeId,
149
+ // source: spec:/components/schemas/invoice/properties/confirmation_secret/description "client_secret of the PaymentIntent"
150
+ confirmation_secret: { type: 'payment_intent', client_secret: intent.client_secret },
173
151
  amount_paid: pays ? total : 0, amount_remaining: pays ? 0 : total, auto_advance: attempt && !pays, next_payment_attempt: null,
174
152
  status_transitions: { ...numbered.status_transitions, paid_at: pays ? at : null },
175
153
  }, pays ? 'invoice.paid' : attempt ? 'invoice.payment_failed' : 'invoice.finalized');
@@ -205,7 +183,7 @@ export async function payOpenInvoice(ctx, invoiceId, paymentMethod) {
205
183
  attempt_count: (Number(inv.attempt_count) || 0) + 1, attempted: true,
206
184
  status_transitions: { ...inv.status_transitions, paid_at: now },
207
185
  }, 'invoice.paid');
208
- const sub = typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
186
+ const sub = typeof inv.subscription === 'string' ? resourceView(ctx, SUB, inv.subscription) : undefined;
209
187
  if (sub && sub.status === 'past_due' && !ctx.legal(SUB, 'status', op, 'past_due', 'active', String(sub.id), 'external')) {
210
188
  await ctx.write(SUB, String(sub.id), { status: 'active' }, 'customer.subscription.updated');
211
189
  }
@@ -226,7 +204,7 @@ export async function advanceBilling(ctx, only) {
226
204
  export async function billingStep(ctx, at, only) {
227
205
  // the earliest move that has come due, of any subscription
228
206
  let next;
229
- for (const sub of ctx.rows(SUB)) {
207
+ for (const sub of ctx.rows(SUB).map((s) => ctx.expand(SUB, s))) {
230
208
  if (only && !only(sub))
231
209
  continue;
232
210
  if (!RENEWING.includes(String(sub.status)) || typeof sub.current_period_end !== 'number' || sub.pause_collection)
@@ -237,7 +215,7 @@ export async function billingStep(ctx, at, only) {
237
215
  for (const inv of ctx.rows(INV)) {
238
216
  if (inv.status !== 'draft' || inv.billing_reason !== 'subscription_cycle' || inv.auto_advance !== true)
239
217
  continue;
240
- const sub = typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
218
+ const sub = typeof inv.subscription === 'string' ? resourceView(ctx, SUB, inv.subscription) : undefined;
241
219
  if (!sub || (only && !only(sub)))
242
220
  continue;
243
221
  const t = Number(inv.created) + HOUR;
@@ -252,7 +230,7 @@ export async function billingStep(ctx, at, only) {
252
230
  return true;
253
231
  }
254
232
  const id = next.id;
255
- const sub = c.get(SUB, id);
233
+ const sub = resourceView(c, SUB, id);
256
234
  const op = c.call.operation.id;
257
235
  // one set to cancel at its period's end is canceled then, with no renewal
258
236
  // (docs.stripe.com/billing/subscriptions/cancel#cancel-at-end-of-cycle)
@@ -273,7 +251,6 @@ export async function billingStep(ctx, at, only) {
273
251
  return true;
274
252
  }
275
253
  export const account = (ctx) => actingAccount(ctx);
276
- export const payoutMissing = (ctx, id) => fail(ctx, `No such payout: '${id}'`, 404, 'resource_missing');
277
254
  /** The acting account's Balance object (docs.stripe.com/api/balance/balance_object). */
278
255
  export function balanceBody(ctx, acct = account(ctx)) {
279
256
  const { available, pending, issuing } = balanceOf(ctx, acct);
@@ -447,118 +424,7 @@ export async function openReview(ctx, chargeId, pi) {
447
424
  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
425
  }, 'review.opened');
449
426
  }
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
427
  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
428
  /** The request event asked of the endpoint, signed with its secret and rendered in the event's API version, and its
563
429
  * answer read as the decision. */
564
430
  export async function askForAuthorization(ctx, url, event, secret) {
@@ -577,70 +443,19 @@ export function publishableKeyRefused(ctx) {
577
443
  // is still one
578
444
  if (!requestKey(ctx.call.request).startsWith('pk_'))
579
445
  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).
446
+ return ctx.refuse({ status: 403, code: 'secret_key_required', message: 'The API key provided is a publishable key, but a secret key is required.' });
447
+ }
616
448
  /** The platform's own keys, as the dashboard's API keys page makes them (the `appCredentials` door): a secret key and a
617
449
  * 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). */
450
+ * ("Invalid API Key provided"). */
619
451
  // source: https://docs.stripe.com/api/authentication "API requests without authentication also fail."
620
452
  export function platformKeyRefused(ctx, request) {
621
453
  const key = requestKey(request);
622
454
  if (!key)
623
455
  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
456
  const held = ctx.rowsRaw('_platform_key').some((k) => k._sha256 === ctx.crypto.sha256(key) && k.deleted !== true);
627
457
  return held ? undefined : ctx.refuse({ status: 401, message: `Invalid API Key provided: ${redactKey(key)}` });
628
458
  }
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
459
  // ── metadata ──
645
460
  // "This parameter uses a merge mechanism, which allows you to add new key-value pairs to an object in an update call
646
461
  // without affecting any existing metadata"; "Pass in the key with an empty string as the value to remove the key from the
@@ -667,8 +482,7 @@ export function withMetadata(ctx, request) {
667
482
  return request;
668
483
  }
669
484
  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;
485
+ const resource = op.resource;
672
486
  const id = Object.values(ctx.call.params).at(-1) ?? '';
673
487
  const held = ctx.get(resource, id)?.metadata;
674
488
  const prior = held && typeof held === 'object' ? held : {};
@@ -687,186 +501,6 @@ export function withMetadata(ctx, request) {
687
501
  headers.set('content-type', 'application/json');
688
502
  return new Request(request.url, { method: request.method, headers, body: JSON.stringify({ ...ctx.params, metadata }) });
689
503
  }
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
504
  export function scopeKeyOf(ctx) {
871
505
  const scope = ctx.params.scope && typeof ctx.params.scope === 'object' ? ctx.params.scope : undefined;
872
506
  const scopeType = scope && typeof scope.type === 'string' ? scope.type : 'account';
@@ -874,12 +508,6 @@ export function scopeKeyOf(ctx) {
874
508
  }
875
509
  export const named = (ctx, name) => ctx.rows(SECRET).find((s) => s.name === name && kept(ctx, SECRET, s, '_scope_key') === scopeKeyOf(ctx));
876
510
  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
511
  /** Paying an invoice with nothing to pay it: "If not set, defaults to the subscription's default payment method, if any,
884
512
  * or to the default payment method in the customer's invoice settings" (docs.stripe.com/api/invoices/object,
885
513
  * default_payment_method), and none of them is set. */
@@ -891,57 +519,6 @@ export function nothingToPayWith(ctx) {
891
519
  export async function activatedByPayment(ctx, subId) {
892
520
  await ctx.write('subscription', subId, { status: 'active' }, 'subscription.update');
893
521
  }
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
522
  /** The invoice the path names, and the machine's refusal when this operation may not move it. */
946
523
  export function invoicesLoad(ctx, operationId) {
947
524
  const id = at(ctx, 'invoice');
@@ -951,11 +528,22 @@ export function invoicesLoad(ctx, operationId) {
951
528
  const refused = ctx.legal(INV, 'status', operationId, inv.status, undefined, id);
952
529
  return refused ? { answer: ctx.refuse(refused) } : { inv };
953
530
  }
531
+ /** A draft finalized on its way to being paid: numbered, open, its PaymentIntent opened (PostInvoicesInvoiceFinalize). */
532
+ export async function finalizeForPayment(ctx, inv, id) {
533
+ const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
534
+ const now = nowUnix(ctx.occurredAt);
535
+ ctx.legal(INV, 'status', 'PostInvoicesInvoicePay', 'draft', 'open', id);
536
+ const body = await ctx.write(INV, id, { ...finalizedFields(ctx, inv, Number(now)), status: 'open', status_transitions: { ...transitions(inv), finalized_at: now }, payment_intent: piId }, 'invoice.finalize');
537
+ if (piId)
538
+ await openPaymentIntent(ctx, piId, inv, id);
539
+ await spendOnceCoupon(ctx, inv);
540
+ return ctx.get(INV, id) ?? body;
541
+ }
954
542
  /** The PaymentIntent a charge_automatically invoice with a balance gets when it is finalized; it
955
543
  * waits for confirmation when the invoice already names a default payment method. */
956
544
  export async function openPaymentIntent(ctx, piId, inv, invoiceId) {
957
545
  const defaultPaymentMethod = typeof inv.default_payment_method === 'string' ? inv.default_payment_method : undefined;
958
- await created(ctx, 'payment_intent', {
546
+ const intent = await created(ctx, 'payment_intent', {
959
547
  id: piId, amount: Number(inv.amount_due) || 0, currency: String(inv.currency ?? 'usd'), customer: inv.customer, invoice: invoiceId,
960
548
  ...(defaultPaymentMethod ? { payment_method: defaultPaymentMethod } : {}),
961
549
  }, {
@@ -964,6 +552,8 @@ export async function openPaymentIntent(ctx, piId, inv, invoiceId) {
964
552
  capture_method: 'automatic', amount_capturable: 0, amount_received: 0, next_action: null,
965
553
  automatic_payment_methods: null, payment_method_types: ['card', 'link'], payment_method_options: {},
966
554
  });
555
+ // source: spec:/components/schemas/invoice/properties/confirmation_secret/description "client_secret of the PaymentIntent"
556
+ await ctx.write(INV, invoiceId, { confirmation_secret: { type: 'payment_intent', client_secret: intent.client_secret } }, 'invoice.updated');
967
557
  }
968
558
  export function finalizedFields(ctx, inv, now) {
969
559
  const customerId = typeof inv.customer === 'string' ? inv.customer : '';
@@ -978,152 +568,6 @@ export function finalizedFields(ctx, inv, now) {
978
568
  fields.due_date = now + days * 86400;
979
569
  return fields;
980
570
  }
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
571
  /** A coupon for a fixed amount off, in its currency (docs.stripe.com/api/coupons/create#create_coupon-amount_off). */
1128
572
  export function amountOffOf(ctx, params) {
1129
573
  const amountOff = Number(params.amount_off);
@@ -1146,62 +590,6 @@ export async function lapseCoupons(ctx) {
1146
590
  await t.write('coupon', String(c.id), { valid: false }, 'coupon.lapsed');
1147
591
  }
1148
592
  }
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
593
  export const sessionMissing = (ctx, id) => fail(ctx, `No such checkout.session: '${id}'`, 404, 'resource_missing');
1206
594
  // Stripe normalizes the create input into the Session: line_items become the list envelope, and
1207
595
  // subscription_data (create-only) is kept aside for completion to copy onto the subscription
@@ -1395,46 +783,17 @@ export async function completeSession(ctx, id, existing, details = {}, card) {
1395
783
  if (d)
1396
784
  await redeemCoupon(ctx, d.coupon, d.promotion_code);
1397
785
  // the session's url "is only present when the session is active" (docs.stripe.com/api/checkout/sessions/object)
786
+ if (link.payment_status !== undefined)
787
+ ctx.legal(CS, 'payment_status', 'PostCheckoutSessionsSession', existing.payment_status, String(link.payment_status), id, 'external');
1398
788
  return ctx.write(CS, id, { ...details, ...link, url: null }, 'checkout.session.completed');
1399
789
  }
1400
790
  export const send = (ctx, r) => ctx.reply(r.body, r.status);
1401
791
  /** Stripe's error body: an invalid_request_error with the message, and the code when there is one. */
1402
792
  export const fail = (ctx, message, status = 404, code) => ctx.refuse({ status, message, ...(code ? { code } : {}) });
1403
- /** A file parameter whose description requires a purpose refuses a file of another one. The served spec requires it
1404
- * on every such parameter ("Must have a `purpose` value of `issuing_logo`", personalization designs' card_logo;
1405
- * "The file's `purpose` must be one of the following: …", file links' file). Where the documentation stops and the
1406
- * twin decides: Stripe documents no error for it (docs.stripe.com/error-codes names none), so the twin answers the
1407
- * 400 invalid_request_error it answers for an unknown file, naming the parameter and the purposes allowed. */
1408
- export function filePurposeRefused(ctx, param, allowed) {
1409
- const given = ctx.params[param];
1410
- if (given === undefined || given === '')
1411
- return undefined;
1412
- const file = ctx.get('file', String(given));
1413
- if (!file)
1414
- return fail(ctx, `No such file upload: '${String(given)}'`, 400, 'resource_missing');
1415
- if (allowed.includes(String(file.purpose)))
1416
- return undefined;
1417
- const need = allowed.length === 1 ? `a \`purpose\` value of \`${allowed[0]}\`` : `a \`purpose\` of one of: ${allowed.map((a) => `\`${a}\``).join(', ')}`;
1418
- return fail(ctx, `Invalid ${param}: the file '${String(given)}' has a \`purpose\` of \`${String(file.purpose)}\`, but it must have ${need}.`, 400);
1419
- }
1420
793
  /** The request's path, as the list envelope's `url` carries it. */
1421
794
  export const path = (ctx) => new URL(ctx.call.request.url).pathname.replace(/\/+$/, '') || '/';
1422
795
  /** A path parameter, by the name the spec gives it. */
1423
796
  export const at = (ctx, name) => ctx.call.params[name] ?? '';
1424
- /** An update's fields laid over what is stored, as the derived core does: a nested hash is merged into the stored one,
1425
- * not put in its place (Stripe updates only the keys a request names; docs.stripe.com/api/metadata and each update
1426
- * reference), and an empty string unsets a key. */
1427
- export function mergeUpdate(ctx, resource, id, fields) {
1428
- const current = ctx.get(resource, id) ?? {};
1429
- const out = {};
1430
- for (const [k, v] of Object.entries(fields)) {
1431
- const prior = current[k];
1432
- out[k] = v && typeof v === 'object' && !Array.isArray(v) && prior && typeof prior === 'object' && !Array.isArray(prior)
1433
- ? Object.fromEntries(Object.entries({ ...prior, ...v }).filter(([, x]) => x !== ''))
1434
- : v;
1435
- }
1436
- return out;
1437
- }
1438
797
  /** A resource's rows in Stripe's list order: newest first by `timeField`, ties by mint order (as the derived core breaks them). */
1439
798
  /** A subject's bookkeeping field (`_…`): kept on the stored row, never in the wire view ctx.get and ctx.rows answer. */
1440
799
  export const kept = (ctx, resource, row, key) => ctx.row(resource, String(row.id), { withDeleted: true })?.[key];
@@ -1469,7 +828,7 @@ export function newest(ctx, resource, timeField = 'created') {
1469
828
  if (tb !== ta)
1470
829
  return tb - ta;
1471
830
  return made.get(b) - made.get(a);
1472
- });
831
+ }).map((r) => ctx.expand(resource, r));
1473
832
  }
1474
833
  /** A context at the moment Stripe would have made a move time makes (a renewal, a payout on its schedule), so what
1475
834
  * the move writes carries that moment (its events' occurredAt, `created`, a charge's funds' available_on), not the
@@ -1503,8 +862,6 @@ export function commonAsOf(ctx, t) {
1503
862
  * it holds, one segment at a time; an id the tree lacks stays an id. An embedded balance transaction reads as the clock
1504
863
  * has it (asOf), as its own retrieve does. */
1505
864
  export function expandRow(ctx, resource, body, paths) {
1506
- if (paths.length === 0)
1507
- return body;
1508
865
  const asOf = (v) => {
1509
866
  if (Array.isArray(v))
1510
867
  return v.map(asOf);
@@ -1518,11 +875,15 @@ export function expandRow(ctx, resource, body, paths) {
1518
875
  }
1519
876
  /** One resource with the request's `expand[]` applied. */
1520
877
  export const expanded = (ctx, resource, body) => expandRow(ctx, resource, body, expandPaths(ctx));
878
+ export const resourceView = (ctx, resource, id) => {
879
+ const row = ctx.get(resource, id);
880
+ return row ? ctx.expand(resource, row) : undefined;
881
+ };
1521
882
  /** Stripe's list envelope over rows already in list order: a page, `has_more`, and `expand[]=data.*`. */
1522
883
  export function list(ctx, resource, items) {
1523
884
  const { page, hasMore } = paginate(items, ctx.params);
1524
885
  const paths = expandPaths(ctx).filter((p) => p[0] === 'data').map((p) => p.slice(1)).filter((p) => p.length > 0);
1525
- return ctx.reply({ object: 'list', url: path(ctx), has_more: hasMore, data: paths.length ? page.map((r) => expandRow(ctx, resource, r, paths)) : page });
886
+ return ctx.reply({ object: 'list', url: path(ctx), has_more: hasMore, data: page.map((r) => expandRow(ctx, resource, r, paths)) });
1526
887
  }
1527
888
  /** Stripe's search over a resource's rows (`query` in its search language). */
1528
889
  export const search = (ctx, resource, timeField = 'created') => send(ctx, searchOver(newest(ctx, resource, timeField), ctx.params, path(ctx)));
@@ -1539,11 +900,32 @@ export async function created(ctx, resource, params, defaults, opts = {}) {
1539
900
  const timeField = opts.timeField ?? 'created';
1540
901
  const stamped = !known || known.has(timeField) || timeField in defaults || timeField in rest ? timeField : '_created';
1541
902
  const fields = { object: OBJECT_NAME[type] ?? type, [stamped]: nowUnix(ctx.occurredAt), ...defaults, ...rest };
1542
- return ctx.write(resource, id, fields, opts.operation ?? `${type}.create`);
903
+ // source: spec:/components/schemas/subscription/properties/items/description "List of subscription items, each with an attached price."
904
+ const children = resource === SUB ? (fields.items?.data ?? []) : [];
905
+ if (resource === SUB)
906
+ delete fields.items;
907
+ // source: spec:/components/schemas/subscription_item/properties/price "price"
908
+ if (resource === 'subscription_item') {
909
+ const item = fields;
910
+ if (item.price && typeof item.price === 'object')
911
+ item.price = item.price.id;
912
+ delete item.plan; // the served subscription_item schema has no plan field
913
+ }
914
+ // source: spec:/components/schemas/account/properties/individual "person"
915
+ if (resource === 'account' && fields.individual && typeof fields.individual === 'object') {
916
+ const person = await created(ctx, 'person', { ...fields.individual, account: id }, { metadata: {}, relationship: { representative: true } });
917
+ fields.individual = person.id;
918
+ }
919
+ const row = await ctx.write(resource, id, fields, opts.operation ?? `${type}.create`);
920
+ if (children.length)
921
+ await storeItems(ctx, id, children);
922
+ return ctx.expand(resource, row);
1543
923
  }
1544
924
  export const finder = (ctx) => (storedType, id) => {
1545
925
  const r = ctx.rowsRaw(storedType, { withDeleted: true }).find((x) => x.id === id);
1546
- return r ? view(storedType, r) : undefined;
926
+ // source: https://docs.stripe.com/testing "Generic decline 4000 0000 0000 0002"
927
+ // Private test-card behavior is used for subsequent charges, never sent as a vendor field.
928
+ return r ? { ...view(storedType, r), ...(r._declineOutcome !== undefined ? { _declineOutcome: r._declineOutcome } : {}) } : undefined;
1547
929
  };
1548
930
  /** The account's external_accounts list, rewritten from its external accounts after one is added or removed. */
1549
931
  export async function syncExternals(ctx, account) {
@@ -1551,45 +933,6 @@ export async function syncExternals(ctx, account) {
1551
933
  return;
1552
934
  await ctx.write('account', account, { external_accounts: externalList(account, newest(ctx, 'external_account').filter((e) => e.account === account)) }, 'account.external_accounts');
1553
935
  }
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
936
  export const pmMissing = (ctx, id) => fail(ctx, `No such payment_method: '${id}'`, 404, 'resource_missing');
1594
937
  /** The card a charge was paid with, as its payment_method_details reports it
1595
938
  * (docs.stripe.com/api/charges/object#charge_object-payment_method_details): a stored PaymentMethod's card, a
@@ -1652,11 +995,18 @@ export async function methodFromData(ctx, data) {
1652
995
  const d = data;
1653
996
  const type = typeof d.type === 'string' ? d.type : 'card';
1654
997
  const made = await created(ctx, PM, {}, {
1655
- type, customer: null, livemode: false, metadata: {},
998
+ type, customer: null, livemode: false,
999
+ // source: spec:/paths/~1v1~1setup_intents~1{intent}~1confirm/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/payment_method_data/description "this hash creates a PaymentMethod"
1000
+ metadata: d.metadata && typeof d.metadata === 'object' ? d.metadata : {},
1656
1001
  billing_details: { ...noBilling, ...(d.billing_details ?? {}) },
1657
1002
  ...paymentMethodSubObject(type, d),
1658
1003
  // what follows its card's success (a dispute, funds straight to available), as a PaymentMethod created directly carries
1659
1004
  ...(type === 'card' && afterSuccessOf(ctx, d.card) ? { _afterSuccess: afterSuccessOf(ctx, d.card) } : {}),
1005
+ // and what a payment with its card answers: a test card the issuer declines, or one that asks the cardholder to
1006
+ // authenticate, entered as its number (Stripe.js sends it so), as the same card made a PaymentMethod directly does
1007
+ // source: https://docs.stripe.com/testing "Generic decline 4000 0000 0000 0002"
1008
+ // source: https://docs.stripe.com/testing "This card requires authentication on all transactions, regardless of how the card is set up."
1009
+ ...(type === 'card' ? { _declineOutcome: declineFor(() => undefined, { card: d.card }) ?? null, ...(requiresAuthentication({ card: d.card }) ? { _requiresAuthentication: true } : {}) } : {}),
1660
1010
  });
1661
1011
  return String(made.id);
1662
1012
  }
@@ -1667,94 +1017,7 @@ export function microdepositsAsked(ctx, params, intent, pm) {
1667
1017
  const options = (params.payment_method_options ?? intent.payment_method_options)?.us_bank_account;
1668
1018
  return options?.verification_method === 'microdeposits' && !!pm && ctx.get(PM, pm)?.type === 'us_bank_account';
1669
1019
  }
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
1020
  // ── 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
1021
  /** A tiered price: tiers_mode and tiers, each tier with up_to and a unit or flat amount; no single unit_amount, and
1759
1022
  * the last tier's up_to null (∞) (docs.stripe.com/products-prices/pricing-models#tiered-pricing). */
1760
1023
  export async function tieredPrice(ctx, params) {
@@ -1831,6 +1094,10 @@ export async function payInvoiceOf(ctx, pi, operationId, actor) {
1831
1094
  const invoice = invoiceId ? ctx.get('invoice', invoiceId) : undefined;
1832
1095
  if (!invoiceId || !invoice || invoice.status === 'paid' || invoice.status === 'void')
1833
1096
  return;
1097
+ await finishInvoiceIntent(ctx, invoice, invoiceId, operationId, actor);
1098
+ }
1099
+ /** An invoice-linked intent read back from a live account; the served create API cannot set invoice. */
1100
+ export async function finishInvoiceIntent(ctx, invoice, invoiceId, operationId, actor) {
1834
1101
  if (ctx.legal('invoice', 'status', operationId, invoice.status, 'paid', invoiceId, actor))
1835
1102
  return;
1836
1103
  const amount = Number(invoice.total ?? invoice.amount_due) || 0;
@@ -1840,19 +1107,11 @@ export async function payInvoiceOf(ctx, pi, operationId, actor) {
1840
1107
  status_transitions: { ...(invoice.status_transitions ?? {}), paid_at: nowUnix(ctx.occurredAt) },
1841
1108
  }, 'invoice.pay');
1842
1109
  const subId = typeof invoice.subscription === 'string' ? invoice.subscription : undefined;
1843
- const sub = subId ? ctx.get('subscription', subId) : undefined;
1110
+ const sub = subId ? resourceView(ctx, 'subscription', subId) : undefined;
1844
1111
  if (subId && sub && sub.status === 'incomplete' && !ctx.legal('subscription', 'status', operationId, sub.status, 'active', subId, actor)) {
1845
1112
  await ctx.write('subscription', subId, { status: 'active' }, 'subscription.update');
1846
1113
  }
1847
1114
  }
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
1115
  /** The type of the payment method an intent is paid with: its PaymentMethod's, or a documented test name's (pm_card_*,
1857
1116
  * tok_* a card; pm_usBankAccount_* a US bank account), else unknown. */
1858
1117
  export function methodType(ctx, method) {
@@ -1886,6 +1145,12 @@ export function methodNotAllowed(ctx, existing, params) {
1886
1145
  return undefined;
1887
1146
  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
1147
  }
1148
+ // Confirmation controls are request instructions, absent from the pinned PaymentIntent object.
1149
+ // source: spec:/paths/~1v1~1payment_intents~1{intent}~1confirm/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/return_url/description "The URL to redirect your customer back to after they authenticate or cancel their payment"
1150
+ export function intentFields(params) {
1151
+ const { confirm: _confirm, amount_to_confirm: _amount, confirmation_token: _token, error_on_requires_action: _error, expand: _expand, mandate: _mandate, mandate_data: _mandateData, off_session: _offSession, payment_method_data: _methodData, radar_options: _radar, return_url: _returnUrl, use_stripe_sdk: _sdk, ...fields } = params;
1152
+ return fields;
1153
+ }
1889
1154
  /** Confirm an intent the confirm machine allows to move: the confirm action, and a create with `confirm=true`. */
1890
1155
  export async function confirmIntent(ctx, existing, params) {
1891
1156
  const id = String(existing.id);
@@ -1916,7 +1181,7 @@ export async function confirmIntent(ctx, existing, params) {
1916
1181
  id: chargeId, amount, currency: existing.currency ?? 'usd', payment_intent: id,
1917
1182
  ...(typeof existing.customer === 'string' ? { customer: existing.customer } : {}),
1918
1183
  ...(pmId ? { payment_method: pmId } : {}),
1919
- ...intentCarries({ ...existing, ...params }),
1184
+ ...intentCarries({ ...existing, ...intentFields(params) }),
1920
1185
  }, {
1921
1186
  ...chargeDefaults(chargeId, amount, false, ctx.occurredAt), status: 'failed', paid: false, captured: false, capture_before: null,
1922
1187
  failure_code: decline.code, failure_message: decline.message, balance_transaction: null,
@@ -1925,56 +1190,38 @@ export async function confirmIntent(ctx, existing, params) {
1925
1190
  }, { operation: 'charge.failed' }); // "Occurs whenever a failed charge attempt occurs." (docs.stripe.com/api/events/types)
1926
1191
  const { payment_method: _pm, ...rest } = params;
1927
1192
  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');
1193
+ const failed = await ctx.write(PI, id, { ...intentFields(rest), payment_method: null, latest_charge: chargeId, status: 'requires_payment_method', last_payment_error }, 'payment_intent.payment_failed');
1929
1194
  return paymentIntentsSend(ctx, cardError(decline, { payment_intent: failed, charge: chargeId }));
1930
1195
  }
1931
1196
  // a bank debit that needs micro-deposit verification waits in requires_action
1932
1197
  const method = typeof params.payment_method === 'string' ? params.payment_method : typeof existing.payment_method === 'string' ? existing.payment_method : undefined;
1933
1198
  // a bank transfer is paid from the customer's cash balance, or waits for the transfer (fundFromCashBalance)
1934
1199
  if (method && ctx.get('payment_method', method)?.type === 'customer_balance')
1935
- return ctx.reply(ctx.expand(PI, await fundFromCashBalance(ctx, { ...existing, ...params }, params)));
1200
+ return ctx.reply(ctx.expand(PI, await fundFromCashBalance(ctx, { ...existing, ...intentFields(params) }, intentFields(params))));
1936
1201
  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')));
1202
+ return ctx.reply(ctx.expand(PI, await ctx.write(PI, id, { ...intentFields(params), status: 'requires_action', next_action: microdepositsNextAction(), last_payment_error: null }, 'payment_intent.requires_action')));
1938
1203
  }
1939
1204
  // 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')));
1205
+ if (existing.status !== 'requires_action' && (requiresAuthentication(params, existing) || (!!method && ctx.row(PM, method)?._requiresAuthentication === true))) {
1206
+ return ctx.reply(ctx.expand(PI, await ctx.write(PI, id, { ...intentFields(params), status: 'requires_action', next_action: threeDsNextAction(), last_payment_error: null }, 'payment_intent.requires_action')));
1942
1207
  }
1943
1208
  // manual capture authorizes and waits for /capture: the authorization is a charge, succeeded and not captured, the
1944
1209
  // intent's latest_charge
1945
1210
  if ((params.capture_method ?? existing.capture_method) === 'manual') {
1946
1211
  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')));
1212
+ const charge = await authorizeCharge(ctx, id, { ...existing, ...intentFields(params) }, amount);
1213
+ return ctx.reply(ctx.expand(PI, await ctx.write(PI, id, { ...intentFields(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
1214
  }
1950
1215
  // a bank debit is submitted and settles later (beginBankDebit)
1951
1216
  if (isBankDebit(ctx, method))
1952
- return ctx.reply(ctx.expand(PI, await beginBankDebit(ctx, id, params)));
1217
+ return ctx.reply(ctx.expand(PI, await beginBankDebit(ctx, id, intentFields(params))));
1953
1218
  const amount = Number(existing.amount ?? params.amount ?? 0);
1954
1219
  // 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');
1220
+ const charge = await mintCharge(ctx, id, { ...existing, ...intentFields(params) }, amount);
1221
+ const body = await ctx.write(PI, id, { ...intentFields(params), status: 'succeeded', amount_received: amount, next_action: null, last_payment_error: null, latest_charge: charge }, 'payment_intent.confirm');
1957
1222
  await payInvoiceOf(ctx, body, 'PostPaymentIntentsIntentConfirm');
1958
1223
  return ctx.reply(ctx.expand(PI, body));
1959
1224
  }
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
1225
  /** Whether the method an intent is confirmed with is a bank debit: a us_bank_account PaymentMethod, or one of Stripe's
1979
1226
  * test bank accounts named as one (pm_usBankAccount_*, pm_us_bank_account). */
1980
1227
  export function isBankDebit(ctx, method) {
@@ -1996,7 +1243,7 @@ export async function beginBankDebit(ctx, id, fields) {
1996
1243
  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
1244
  }
1998
1245
  /** 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. */
1246
+ * then, its Charge made and the invoice it collects for paid. */
2000
1247
  export async function settleBankDebits(ctx) {
2001
1248
  const now = Number(nowUnix(ctx.occurredAt));
2002
1249
  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));
@@ -2006,18 +1253,9 @@ export async function settleBankDebits(ctx) {
2006
1253
  c.legal(PI, 'status', ctx.call.operation.id, 'processing', 'succeeded', id, 'vendor');
2007
1254
  const amount = Number(pi.amount) || 0;
2008
1255
  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
1256
  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
- }
1257
+ // (a debit run under a mandate names it on its charge, and a single-use mandate is spent by it, at Stripe; the twin
1258
+ // makes no mandate: micro-deposit verification, which accepts one, is the gap here, ../manifest.ts unmodeled)
2021
1259
  await payInvoiceOf(c, body, ctx.call.operation.id, 'vendor');
2022
1260
  }
2023
1261
  }
@@ -2032,16 +1270,19 @@ export async function fundFromCashBalance(ctx, pi, fields = {}) {
2032
1270
  // what the customer's funded cash balance holds in this currency, from its ledger
2033
1271
  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
1272
  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
- }
1273
+ if (available >= amount)
1274
+ return payFromCashBalance(ctx, pi, fields, { customer, currency, available, amount });
2041
1275
  // not enough yet: the intent waits for the rest, with the instructions to send it
2042
1276
  const next_action = { type: 'display_bank_transfer_instructions', display_bank_transfer_instructions: { amount_remaining: amount - available, currency, type: 'us_bank_transfer' } };
2043
1277
  return ctx.write(PI, String(pi.id), { ...fields, status: 'requires_action', next_action }, 'payment_intent.requires_action');
2044
1278
  }
1279
+ /** A customer_balance intent the customer's funded cash balance pays: its ledger records what was applied, and the intent
1280
+ * succeeds with its charge. */
1281
+ export async function payFromCashBalance(ctx, pi, fields, o) {
1282
+ await created(ctx, 'customer_cash_balance_transaction', { customer: o.customer }, { currency: o.currency, type: 'applied_to_payment', net_amount: -o.amount, ending_balance: o.available - o.amount, applied_to_payment: { payment_intent: String(pi.id) }, livemode: false });
1283
+ const charge = await mintCharge(ctx, String(pi.id), { ...pi, ...fields }, o.amount);
1284
+ return ctx.write(PI, String(pi.id), { ...fields, status: 'succeeded', next_action: null, amount_received: o.amount, last_payment_error: null, latest_charge: charge }, 'payment_intent.succeeded');
1285
+ }
2045
1286
  /** Whether a charge's funds go straight to the available balance: a bypass card as a raw number, a test name (pm_card_*
2046
1287
  * or tok_*), or a stored PaymentMethod made from one (which records it as what follows its success, after-payment.ts);
2047
1288
  * or a US bank account, stored or one of Stripe's test bank accounts named as one (pm_usBankAccount_*). */
@@ -2057,7 +1298,8 @@ export function bypassesPending(ctx, card) {
2057
1298
  return BYPASS_NUMBERS.has(card.replace(/\D/g, '')) || card.replace(/^tok_/, 'pm_card_') in BYPASS_PENDING_CARDS;
2058
1299
  }
2059
1300
  /** 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;
1301
+ // The platform's own account ID denotes the same balance as an omitted Stripe-Account header.
1302
+ export const actingAccount = (ctx) => { const id = ctx.call.request.headers.get('stripe-account'); return id && id !== PLATFORM_ACCOUNT_ID ? id : undefined; };
2061
1303
  /** A ledger entry on an account's balance: the acting account's unless one is named (null names the platform). */
2062
1304
  export async function write(ctx, fields, account = actingAccount(ctx)) {
2063
1305
  // a credit available at once waits for the drain's balance.available (settleDueEntries)
@@ -2110,11 +1352,6 @@ export async function settleDispute(ctx, disputeId, amount, currency) {
2110
1352
  fee_details: [{ amount: fee, application: null, currency, description: 'Dispute fee', type: 'stripe_fee' }],
2111
1353
  });
2112
1354
  }
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
1355
  /** An account's ledger entries an automatic payout has not yet paid out: every entry on its payments balance that no
2119
1356
  * automatic payout took (the automatic payouts' own debits excluded), as stored. */
2120
1357
  export function unpaidEntries(ctx, account) {
@@ -2144,24 +1381,11 @@ export async function settleTransfer(ctx, transferId, amount, currency, destinat
2144
1381
  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
1382
  return platform;
2146
1383
  }
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
1384
  /** A direct charge's application fee: out of the connected account's balance, into the platform's. */
2154
1385
  export async function settleApplicationFee(ctx, feeId, amount, currency, account) {
2155
1386
  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
1387
  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
1388
  }
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
1389
  /** Time's settlements, written: each entry on the acting account's balance whose funds came due by the World's clock
2166
1390
  * moves pending → available (the clock's move the machine allows), so a settlement is recorded once. A read already
2167
1391
  * sees it (asOf); the record is what lets Stripe's balance.available be sent once (./front.ts, the drain).
@@ -2217,76 +1441,6 @@ export function chargeBody(ctx, c) {
2217
1441
  const data = newest(ctx, 'refund').filter((r) => r.charge === c.id);
2218
1442
  return { ...c, refunds: { object: 'list', data, has_more: false, total_count: data.length, url: `/v1/charges/${String(c.id)}/refunds` } };
2219
1443
  }
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
1444
  export const subMissing = (ctx, id) => fail(ctx, `No such subscription: '${id}'`, 404, 'resource_missing');
2291
1445
  /** 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
1446
  export function replacedDiscounts(ctx, couponId, sub) {
@@ -2341,7 +1495,7 @@ export async function applyTrialEnd(ctx, sub, fields, now) {
2341
1495
  if (value !== 'now' || sub.status !== 'trialing')
2342
1496
  return value === 'now' ? undefined : movedTrial(sub, fields, Number(value), now);
2343
1497
  // 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 };
1498
+ const next = { ...sub, ...fields, items: resourceView(ctx, SUB, id)?.items ?? sub.items };
2345
1499
  const { interval, interval_count } = resolveSubscriptionBillingInterval((next.items?.data) ?? [], finder(ctx));
2346
1500
  const period = { start: now, end: addBillingInterval(now, interval, interval_count) };
2347
1501
  const invoice = await draftSubscriptionInvoice(ctx, next, { ...period, reason: 'subscription_update' });
@@ -2483,7 +1637,7 @@ export async function writeItemChanges(ctx, subId, entries) {
2483
1637
  await ctx.write('subscription_item', entry.id, { deleted: true }, 'subscription_item.delete');
2484
1638
  continue;
2485
1639
  }
2486
- await ctx.write('subscription_item', entry.id, { ...(priceRef ? subscriptionItemPrice(priceRef, finder(ctx)) : {}), ...quantity, ...metadata }, 'subscription_item.update');
1640
+ await ctx.write('subscription_item', entry.id, { ...(priceRef ? { price: priceRef } : {}), ...quantity, ...metadata }, 'subscription_item.update');
2487
1641
  }
2488
1642
  else {
2489
1643
  await created(ctx, 'subscription_item', { subscription: subId, ...subscriptionItemPrice(priceRef, finder(ctx)), quantity: 1, ...quantity, ...metadata }, { livemode: false, metadata: {}, discounts: [], tax_rates: [], billing_thresholds: null });
@@ -2526,21 +1680,15 @@ export async function storeItems(ctx, subId, items) {
2526
1680
  for (const item of items) {
2527
1681
  if (typeof item.id !== 'string' || ctx.get('subscription_item', item.id))
2528
1682
  continue;
2529
- await ctx.write('subscription_item', item.id, { ...item, subscription: subId }, 'subscription_item.create');
1683
+ const { plan: _plan, price, ...fields } = item;
1684
+ await ctx.write('subscription_item', item.id, { ...fields, price: typeof price === 'object' && price ? price.id : price, subscription: subId }, 'subscription_item.create');
2530
1685
  }
2531
1686
  }
2532
1687
  export async function syncItems(ctx, subId) {
2533
1688
  if (!subId)
2534
1689
  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');
1690
+ await ctx.write(SUB, subId, {}, 'subscription.items_synced');
2537
1691
  }
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
1692
  export const listMissing = (ctx, id, status = 404) => fail(ctx, `No such radar value list: '${id}'`, status, 'resource_missing');
2545
1693
  /** A value list's list_items, "List of items contained within this value list" (the served spec), kept in step with
2546
1694
  * its items, at the address the fixture names (/v1/radar/value_list_items?value_list=…). */
@@ -2548,25 +1696,9 @@ export async function radarSyncItems(ctx, listId) {
2548
1696
  const data = newest(ctx, VLI).filter((i) => i.value_list === listId);
2549
1697
  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
1698
  }
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
1699
  /** An authorization as every egress carries it: its card embedded in full. */
2562
1700
  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;
1701
+ return ctx.expand(AUTH, a);
2570
1702
  }
2571
1703
  export const authMissing = (ctx, id) => fail(ctx, `No such authorization: '${id}'`, 404, 'resource_missing');
2572
1704
  /** A capture: an Issuing transaction (negative: it debits) and the issuing balance's debit. */
@@ -2616,7 +1748,7 @@ export function issuingDecide(approved) {
2616
1748
  return authMissing(ctx, id);
2617
1749
  const finalized = { status: 400, code: 'authorization_already_finalized', message: `This authorization has already been finalized (status ${String(auth.status)}).` };
2618
1750
  // a webhook-approved authorization is decided, though still pending
2619
- if (auth.approved === true)
1751
+ if (auth.approved === true && auth.status === 'pending')
2620
1752
  return ctx.refuse(finalized);
2621
1753
  const refused = ctx.legal(AUTH, 'status', approved ? 'PostIssuingAuthorizationsAuthorizationApprove' : 'PostIssuingAuthorizationsAuthorizationDecline', auth.status, undefined, id);
2622
1754
  if (refused)
@@ -2632,7 +1764,7 @@ export function issuingDecide(approved) {
2632
1764
  // (docs.stripe.com/issuing/purchases/authorizations, docs.stripe.com/issuing/funding/balance)
2633
1765
  const hold = approved ? await holdFor(ctx, id, decidedAmount, currency) : null;
2634
1766
  return ctx.reply(withCard(ctx, await ctx.write(AUTH, id, {
2635
- ...(approved ? {} : { status: 'closed' }), approved, pending_request: null,
1767
+ status: approved ? 'pending' : 'closed', approved, pending_request: null,
2636
1768
  ...(approved && ctx.params.amount !== undefined ? { amount: decidedAmount } : {}),
2637
1769
  request_history: [...history, historyEntry({
2638
1770
  amount: decidedAmount, currency, approved, reason: approved ? 'webhook_approved' : 'webhook_declined', reasonMessage: null,
@@ -2696,45 +1828,207 @@ export async function answeredByEndpoint(ctx, outcome, o) {
2696
1828
  }
2697
1829
  return outcome.kind === 'declined' ? o.decline('webhook_declined', null) : o.decline('webhook_error', outcome.message);
2698
1830
  }
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
- };
1831
+ /** Time's settling of held refunds, caught up to the World's clock: each refund held for want of balance, oldest first,
1832
+ * is made at the first moment the account's available balance covers it (the funds that come due then), its debit
1833
+ * written then, as `refund.updated`. Where the documentation stops and the twin decides: a held refund is made the
1834
+ * moment the balance covers it. */
1835
+ export async function settleHeldRefunds(ctx) {
1836
+ const now = Number(nowUnix(ctx.occurredAt));
1837
+ const heldRefunds = ctx.rowsRaw('refund').filter((r) => r.status === 'pending' && r.pending_reason === 'insufficient_funds').sort((a, b) => Number(a.created) - Number(b.created));
1838
+ for (const r of heldRefunds)
1839
+ await settleOneHeldRefund(ctx, r, now);
1840
+ }
1841
+ /** A pending refund can enter this surface only through the vendor-backed refund refresh. */
1842
+ export async function settleOneHeldRefund(ctx, r, now) {
1843
+ const account = typeof r._account === 'string' ? r._account : undefined;
1844
+ const currency = String(r.currency ?? 'usd');
1845
+ const amount = Number(r.amount) || 0;
1846
+ const when = [Number(r.created), ...fundsDueBetween(ctx, account, currency, Number(r.created), now)].find((t) => availableAt(ctx, account, currency, t) >= amount);
1847
+ if (when === undefined)
1848
+ return;
1849
+ const c = await at_(ctx)(when);
1850
+ c.legal('refund', 'status', ctx.call.operation.id, 'pending', 'succeeded', String(r.id), 'vendor');
1851
+ const bt = await settleRefund(c, String(r.id), amount, currency, account ?? null);
1852
+ await c.write('refund', String(r.id), { status: 'succeeded', pending_reason: null, balance_transaction: bt }, 'refund.updated');
2721
1853
  }
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');
1854
+ export const accountMissing = (ctx, id, status = 404) => fail(ctx, `No such account: '${id}'`, status, 'resource_missing');
1855
+ /** The account, tombstones included where the hand-written routes looked past a deletion. */
1856
+ export const accountRow = (ctx, id) => ctx.row('account', id, { withDeleted: true });
1857
+ /** Stripe's review of a Custom account once the platform gives it something: with nothing left due, its requested
1858
+ * capabilities become active and it can take charges and receive payouts; with something due again (its bank account
1859
+ * removed), they stop. Where the documentation stops and the twin decides: test-mode verification is immediate, and
1860
+ * it owes what customRequirements lists. An Express or Standard account is onboarded on Stripe's own pages. */
1861
+ export async function connectReview(ctx, id) {
1862
+ const account = resourceView(ctx, 'account', id);
1863
+ if (account && account.type === 'custom')
1864
+ await reviewCustom(ctx, id, account);
2730
1865
  }
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 });
1866
+ /** The review of a Custom account (see review). */
1867
+ export async function reviewCustom(ctx, id, account) {
1868
+ const bank = ctx.rows('external_account').some((e) => e.account === id);
1869
+ const persons = ctx.rows('person').filter((p) => p.account === id);
1870
+ const { now: due, later } = customRequirements(account, bank, persons);
1871
+ // one account, one review: "If a connected account has both card_payments and transfers, and the status of either one
1872
+ // is inactive, then both capabilities are disabled" (docs.stripe.com/connect/account-capabilities)
1873
+ const ready = due.length === 0;
1874
+ const capabilities = {};
1875
+ for (const [key, was] of Object.entries(account.capabilities ?? {})) {
1876
+ const to = ready ? 'active' : 'inactive';
1877
+ // Capabilities are entries of the account's map, not standalone stored subjects.
1878
+ capabilities[key] = to;
1879
+ }
1880
+ const requirements = { ...(account.requirements ?? {}), currently_due: due, eventually_due: [...due, ...later], past_due: [], disabled_reason: ready ? null : 'requirements.past_due', current_deadline: null };
1881
+ await ctx.write('account', id, { capabilities, requirements, charges_enabled: ready, payouts_enabled: ready, details_submitted: ready }, ready ? 'account.updated' : 'account.update');
2736
1882
  }
2737
- /** A capture=false charge's refund, which releases its whole authorization (see the header). */
1883
+ /** A transfer to an account whose transfers capability is not active. */
1884
+ export function transfersInactive(ctx) {
1885
+ 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' });
1886
+ }
1887
+ /** A transfer from a charge whose funds have settled: "With a source_transaction, the transfer request returns success
1888
+ * regardless of your available balance if the related charge hasn't settled yet" (docs.stripe.com/connect/separate-
1889
+ * charges-and-transfers), so a settled one comes out of the available balance. */
1890
+ export function settledSourceRefused(ctx, amount, currency) {
1891
+ return refusePayout(ctx, amount, currency, undefined);
1892
+ }
1893
+ /** An earlier version's payment_method_types refused: the first value that names no payment method type the
1894
+ * operation takes, in the intent's words or (a Checkout Session) the enum's. */
1895
+ export function unknownMethodType(ctx, opId, given) {
1896
+ const values = Array.isArray(given) ? given : given && typeof given === 'object' ? Object.values(given) : [given];
1897
+ const checkout = opId === 'PostCheckoutSessions';
1898
+ const known = checkout ? CHECKOUT_METHOD_TYPES : PAYMENT_METHOD_TYPES;
1899
+ const i = values.findIndex((v) => !known.includes(String(v)));
1900
+ if (i < 0)
1901
+ return undefined;
1902
+ if (checkout) {
1903
+ const param = `payment_method_types[${i}]`;
1904
+ return ctx.refuse({ status: 400, param, message: `Invalid ${param}: must be one of ${oneOf(known)}` });
1905
+ }
1906
+ const doc = TYPES_DOC[opId.startsWith('PostSetupIntents') ? 'setup_intents' : 'payment_intents'];
1907
+ 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.` });
1908
+ }
1909
+ /** payment_method_types refused where the caller's version removed it. */
1910
+ 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.` });
1911
+ /** Stripe's refusal of a request whose top-level parameters the operation does not take, or undefined. */
1912
+ export function refuseParameters(ctx) {
1913
+ const op = OPS.get(ctx.call.operation.id);
1914
+ if (!op)
1915
+ return undefined;
1916
+ const pinned = ctx.call.request.headers.get('stripe-version');
1917
+ const params = ctx.params;
1918
+ if (pinned && pinned < SERVED_VERSION) {
1919
+ if (!TYPED.has(op.id))
1920
+ return undefined;
1921
+ if (params.payment_method_types === undefined)
1922
+ return undefined;
1923
+ if (REMOVED_TYPES.has(op.id) && methodTypesRemoved(pinned))
1924
+ return noLongerSupported(ctx, pinned);
1925
+ return unknownMethodType(ctx, op.id, params.payment_method_types);
1926
+ }
1927
+ const method = ctx.call.request.method.toUpperCase();
1928
+ const known = new Set([...(op.query ?? []), ...(op.body ?? [])].map((p) => p.name).concat(DOCUMENTED[op.id] ?? []));
1929
+ if (REMOVED_TYPES.has(op.id) && params.payment_method_types !== undefined)
1930
+ return noLongerSupported(ctx, pinned ?? SERVED_VERSION);
1931
+ for (const name of Object.keys(params)) {
1932
+ if (!known.has(name))
1933
+ return ctx.refuse({ status: 400, code: 'parameter_unknown', param: name, message: `Received unknown parameter: ${name}` });
1934
+ }
1935
+ const required = method === 'GET' || method === 'DELETE' ? (op.query ?? []) : (op.body ?? []);
1936
+ for (const p of required) {
1937
+ if (p.required && params[p.name] === undefined)
1938
+ return ctx.refuse({ status: 400, code: 'parameter_missing', param: p.name, message: `Missing required param: ${p.name}.` });
1939
+ }
1940
+ return undefined;
1941
+ }
1942
+ /** An answer in the shape of the API version the caller is served (./version.ts), a webhook endpoint's `secret` shown
1943
+ * only at its creation ("Only returned at creation", docs.stripe.com/api/webhook_endpoints/object). */
1944
+ export async function rendered(request, res) {
1945
+ const creates = request.method === 'POST' && new URL(request.url).pathname.replace(/\/+$/, '') === '/v1/webhook_endpoints';
1946
+ const pinned = request.headers.get('stripe-version');
1947
+ if (!(res.headers.get('content-type') ?? '').includes('json'))
1948
+ return res;
1949
+ if (!servesCurrent(pinned) && creates)
1950
+ return res;
1951
+ const expand = await expandOf(request.clone());
1952
+ const body = await res.json();
1953
+ const answered = creates ? body : withoutEndpointSecret(body);
1954
+ return new Response(JSON.stringify(servesCurrent(pinned) ? render(answered, pinned, expand) : answered), { status: res.status, statusText: res.statusText, headers: res.headers });
1955
+ }
1956
+ /** Write a draft's line list and the totals derived from it. */
1957
+ export async function writeLines(ctx, invoiceId, next) {
1958
+ const subtotal = sumLines(next);
1959
+ const lines = { object: 'list', data: next, has_more: false, total_count: next.length, url: `/v1/invoices/${invoiceId}/lines` };
1960
+ return ctx.write(INV, invoiceId, { lines, subtotal, total: subtotal, amount_due: subtotal, amount_remaining: subtotal }, 'invoice.update');
1961
+ }
1962
+ /** 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). */
1963
+ export async function pullPendingItem(ctx, invoiceId, inv, item, now) {
1964
+ const current = ctx.get(INV, invoiceId) ?? inv;
1965
+ await writeLines(ctx, invoiceId, [...linesOf(current), {
1966
+ id: `il_${String(item.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(item.amount) || 0, currency: String(current.currency ?? item.currency ?? 'usd'),
1967
+ quantity: Number(item.quantity) || 1, proration: false, invoice_item: item.id, price: item.price ?? null,
1968
+ period: item.period ?? { start: now, end: now }, description: item.description ?? null,
1969
+ discountable: item.discountable ?? true, discounts: [], livemode: false, metadata: item.metadata ?? {}, tax_amounts: [], tax_rates: [],
1970
+ }]);
1971
+ await ctx.write('invoiceitem', String(item.id), { invoice: invoiceId }, 'invoiceitem.update');
1972
+ }
1973
+ /** The intent the path names, and the machine's refusal when this operation may not move it. */
1974
+ export function paymentIntentsLoad(ctx, operationId) {
1975
+ const pi = ctx.id ? ctx.get(PI, ctx.id) : undefined;
1976
+ if (!pi)
1977
+ return { answer: ctx.notFound(PI, String(ctx.id)) };
1978
+ const refusal = ctx.legal(PI, 'status', operationId, pi.status);
1979
+ return refusal ? { answer: ctx.refuse(refusal) } : { pi };
1980
+ }
1981
+ /** A transfer reversal's two entries: back into the platform's balance, out of the destination's. */
1982
+ export async function settleTransferReversal(ctx, reversalId, amount, currency, destination) {
1983
+ 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);
1984
+ 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);
1985
+ return platform;
1986
+ }
1987
+ /** A capture=false charge refunded while uncaptured: its authorization is released by the refund, as it would be
1988
+ * "automatically refunded if uncaptured" (spec/openapi.json.gz, `capture_before`). The charge stays uncaptured and
1989
+ * becomes refunded in full, a Refund records the release, and no money moves: none was ever received. Written as
1990
+ * charge.refunded. Where the documentation stops and the twin decides: Stripe's basil change ("Partially capturing or
1991
+ * canceling payments no longer creates a Refund", docs.stripe.com/changelog/basil/2025-03-31/remove-refund-from-partial-
1992
+ * capture-and-payment-cancellation-flow) names partial capture and cancellation, not a refund asked for, so a refund
1993
+ * still makes one; its reason is null and it has no balance transaction. Answers the Refund. */
1994
+ export async function releaseAuthorization(ctx, ch, operationId, given = {}) {
1995
+ const id = String(ch.id);
1996
+ const full = Number(ch.amount) || 0;
1997
+ if (ch.refunded !== true)
1998
+ ctx.legal('charge', 'refunded', operationId, 'false', 'true', id);
1999
+ const refund = await created(ctx, 'refund', { ...given, charge: id, amount: full - (Number(ch.amount_refunded) || 0), currency: String(ch.currency ?? 'usd') }, {
2000
+ status: 'succeeded', metadata: {}, reason: null, receipt_number: null,
2001
+ balance_transaction: null, source_transfer_reversal: null, transfer_reversal: null, ...refundDestination(ch, 'reversal'),
2002
+ ...(typeof ch.payment_intent === 'string' ? { payment_intent: ch.payment_intent } : {}),
2003
+ });
2004
+ await ctx.write('charge', id, { amount_refunded: full, refunded: true }, 'charge.refunded');
2005
+ return refund;
2006
+ }
2007
+ /** A PaymentIntent's authorization released by its cancel: "For PaymentIntents with a `status` of `requires_capture`, the
2008
+ * remaining `amount_capturable` is automatically refunded" (docs.stripe.com/api/payment_intents/cancel), and since basil
2009
+ * a cancellation makes no Refund: "`amount_captured` will be 0 instead of `nil` in payment cancellation flows",
2010
+ * "`amount_refunded` will no longer be updated by these actions", "`refunded` will no longer be `true` for payment
2011
+ * cancellation flows", and no charge.refunded is sent (docs.stripe.com/changelog/basil/2025-03-31/remove-refund-from-
2012
+ * partial-capture-and-payment-cancellation-flow). The charge stays uncaptured; nothing is written as an event. */
2013
+ export async function cancelAuthorization(ctx, ch) {
2014
+ return ctx.write('charge', String(ch.id), { amount_captured: 0 }, 'charge.authorization_released');
2015
+ }
2016
+ /**
2017
+ * Create a refund and land it on its charge. An omitted amount is the whole remaining balance. The
2018
+ * charge's write records `charge.refunded`, the event Stripe sends carrying the charge's new totals;
2019
+ * `refunded` is true only at the full amount.
2020
+ */
2021
+ /** A reason Stripe does not take (docs.stripe.com/api/refunds/create#create_refund-reason). */
2022
+ export function badReason(ctx) {
2023
+ return fail(ctx, `Invalid refund reason: must be one of ${[...REFUND_REASONS].join(', ')}.`, 400, 'parameter_invalid_string_enum');
2024
+ }
2025
+ /** A bank-transfer payment refunded with origin=customer_balance goes back to the cash balance
2026
+ * (docs.stripe.com/payments/customer-balance/refunding). */
2027
+ export async function backToCashBalance(ctx, customer, currency, amount, refundId) {
2028
+ 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);
2029
+ 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 });
2030
+ }
2031
+ /** A capture=false charge's refund, which releases its whole authorization (see the header). */
2738
2032
  export async function refundUncaptured(ctx, charge, amount, remaining, params) {
2739
2033
  if (amount < remaining)
2740
2034
  return fail(ctx, `Charge ${String(charge.id)} is uncaptured: refunding it releases the whole authorization (${remaining}).`, 400, 'amount_too_small');
@@ -2834,26 +2128,6 @@ export async function refundCharge(ctx, charge, params) {
2834
2128
  await ctx.write('charge', chargeId, { amount_refunded: refunded, refunded: refunded >= total }, 'charge.refunded');
2835
2129
  return ctx.reply(body);
2836
2130
  }
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
2131
  /** The charge a refund aims at: named directly, or reached through its PaymentIntent. */
2858
2132
  export function target(ctx) {
2859
2133
  const chargeId = typeof ctx.params.charge === 'string' ? ctx.params.charge : '';
@@ -2869,301 +2143,17 @@ export function target(ctx) {
2869
2143
  const ch = (latest ? ctx.get('charge', latest) : undefined) ?? newest(ctx, 'charge').find((c) => c.payment_intent === piId);
2870
2144
  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
2145
  }
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')
2146
+ /** A pending invoice item contributes to a preview only on its customer's next invoice. */
2147
+ // source: spec:PostInvoicesCreatePreview "including subscription renewal charges, invoice item charges, etc."
2148
+ export function pendingPreviewItem(ii, customer, currency, now) {
2149
+ if (ii.customer !== customer || ii.invoice)
2953
2150
  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}`));
2151
+ return {
2152
+ id: `il_${String(ii.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(ii.amount) || 0, currency,
2153
+ quantity: Number(ii.quantity) || 1, proration: false, invoice_item: ii.id,
2154
+ period: ii.period ?? { start: now, end: now }, description: ii.description ?? null,
3045
2155
  };
3046
2156
  }
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 });
2157
+ export function pendingPreviewLines(ctx, customer, currency, now) {
2158
+ return newest(ctx, 'invoiceitem', 'date').flatMap(ii => { const line = pendingPreviewItem(ii, customer, currency, now); return line ? [line] : []; });
3169
2159
  }