@volter/twin-stripe 3.0.0 → 3.0.2

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 +75 -21
  2. package/dist/journeys/coverage.json +596 -0
  3. package/dist/journeys/customer-life.json +6108 -0
  4. package/dist/journeys/customer-life.outline.md +371 -0
  5. package/dist/journeys/decisions.json +677 -0
  6. package/dist/journeys/demand.json +706 -0
  7. package/dist/journeys/score.json +853 -0
  8. package/dist/journeys/unreachable.json +102 -0
  9. package/dist/journeys/vendor-examples.json +8010 -0
  10. package/dist/spec/SOURCE.md +5 -1
  11. package/dist/spec/doc-examples.json +445 -0
  12. package/dist/spec/sources.json +135 -207
  13. package/dist/src/cli.d.ts +0 -1
  14. package/dist/src/cli.js +0 -2
  15. package/dist/src/engine/after-payment.d.ts +0 -1
  16. package/dist/src/engine/after-payment.js +0 -1
  17. package/dist/src/engine/common.d.ts +5 -5
  18. package/dist/src/engine/common.js +13 -11
  19. package/dist/src/engine/connect.d.ts +0 -21
  20. package/dist/src/engine/connect.js +1 -82
  21. package/dist/src/engine/customers.d.ts +0 -8
  22. package/dist/src/engine/customers.js +0 -22
  23. package/dist/src/engine/display.d.ts +0 -3
  24. package/dist/src/engine/display.js +0 -3
  25. package/dist/src/engine/files.d.ts +0 -7
  26. package/dist/src/engine/files.js +0 -10
  27. package/dist/src/engine/invoices.d.ts +6 -9
  28. package/dist/src/engine/invoices.js +56 -29
  29. package/dist/src/engine/issuing.d.ts +6 -8
  30. package/dist/src/engine/issuing.js +19 -10
  31. package/dist/src/engine/ledger.d.ts +0 -1
  32. package/dist/src/engine/ledger.js +0 -1
  33. package/dist/src/engine/oauth.js +1 -2
  34. package/dist/src/engine/refunds.js +2 -27
  35. package/dist/src/engine/stripe.d.ts +2 -7
  36. package/dist/src/engine/stripe.js +34 -83
  37. package/dist/src/engine/subscriptions.d.ts +0 -1
  38. package/dist/src/engine/subscriptions.js +0 -1
  39. package/dist/src/engine/version.d.ts +0 -2
  40. package/dist/src/engine/version.js +17 -6
  41. package/dist/src/fetch.js +2 -2
  42. package/dist/src/generated/surface.gen.json +1 -1
  43. package/dist/src/generated/ui.gen.json +1 -1
  44. package/dist/src/index.js +5 -3
  45. package/dist/src/manifest.js +348 -220
  46. package/dist/src/screens/billing-portal.js +5 -5
  47. package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
  48. package/dist/src/screens/checkout-js.js +84 -0
  49. package/dist/src/screens/connect-onboarding.js +3 -3
  50. package/dist/src/screens/dashboard.d.ts +1 -1
  51. package/dist/src/screens/dashboard.js +56 -53
  52. package/dist/src/screens/stripe-js.js +53 -93
  53. package/dist/src/semantics/account.d.ts +2 -0
  54. package/dist/src/semantics/account.js +8 -0
  55. package/dist/src/semantics/accounts.js +3 -1
  56. package/dist/src/semantics/around.js +3 -6
  57. package/dist/src/semantics/billing_portal.d.ts +0 -1
  58. package/dist/src/semantics/billing_portal.js +1 -7
  59. package/dist/src/semantics/checkout.d.ts +1 -1
  60. package/dist/src/semantics/checkout.js +18 -15
  61. package/dist/src/semantics/clock.js +1 -7
  62. package/dist/src/semantics/customers.d.ts +1 -2
  63. package/dist/src/semantics/customers.js +32 -38
  64. package/dist/src/semantics/entitlements.d.ts +0 -2
  65. package/dist/src/semantics/entitlements.js +4 -50
  66. package/dist/src/semantics/events.d.ts +2 -3
  67. package/dist/src/semantics/events.js +6 -11
  68. package/dist/src/semantics/index.js +16 -38
  69. package/dist/src/semantics/invoices.d.ts +3 -8
  70. package/dist/src/semantics/invoices.js +134 -127
  71. package/dist/src/semantics/issuing.d.ts +6 -0
  72. package/dist/src/semantics/issuing.js +77 -0
  73. package/dist/src/semantics/payment_intents.d.ts +0 -1
  74. package/dist/src/semantics/payment_intents.js +5 -13
  75. package/dist/src/semantics/payment_methods.d.ts +0 -2
  76. package/dist/src/semantics/payment_methods.js +2 -26
  77. package/dist/src/semantics/payouts.d.ts +1 -0
  78. package/dist/src/semantics/payouts.js +8 -1
  79. package/dist/src/semantics/products.d.ts +1 -3
  80. package/dist/src/semantics/products.js +3 -14
  81. package/dist/src/semantics/refunds.d.ts +1 -2
  82. package/dist/src/semantics/refunds.js +1 -5
  83. package/dist/src/semantics/shared.d.ts +64 -336
  84. package/dist/src/semantics/shared.js +326 -1336
  85. package/dist/src/semantics/states.d.ts +20 -89
  86. package/dist/src/semantics/states.js +116 -334
  87. package/dist/src/semantics/subscriptions.d.ts +1 -2
  88. package/dist/src/semantics/subscriptions.js +23 -145
  89. package/dist/src/semantics/test_helpers.d.ts +2 -5
  90. package/dist/src/semantics/test_helpers.js +121 -43
  91. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  92. package/dist/src/semantics/webhook_endpoints.js +26 -0
  93. package/generated/pack-facts.json +3 -3
  94. package/journeys/coverage.json +596 -0
  95. package/journeys/customer-life.json +6108 -0
  96. package/journeys/customer-life.outline.md +371 -0
  97. package/journeys/decisions.json +677 -0
  98. package/journeys/demand.json +706 -0
  99. package/journeys/score.json +853 -0
  100. package/journeys/unreachable.json +102 -0
  101. package/journeys/vendor-examples.json +8010 -0
  102. package/package.json +11 -10
  103. package/spec/SOURCE.md +5 -1
  104. package/spec/doc-examples.json +445 -0
  105. package/spec/sources.json +135 -207
  106. package/src/cli.ts +0 -3
  107. package/src/engine/after-payment.ts +0 -2
  108. package/src/engine/balance.ts +0 -1
  109. package/src/engine/checkout.ts +0 -1
  110. package/src/engine/common.ts +19 -17
  111. package/src/engine/connect.ts +1 -79
  112. package/src/engine/customers.ts +0 -25
  113. package/src/engine/display.ts +2 -3
  114. package/src/engine/files.ts +0 -13
  115. package/src/engine/invoices.ts +64 -33
  116. package/src/engine/issuing.ts +16 -11
  117. package/src/engine/ledger.ts +0 -2
  118. package/src/engine/oauth.ts +1 -2
  119. package/src/engine/onboarding.ts +0 -1
  120. package/src/engine/params.ts +0 -1
  121. package/src/engine/payment-intents.ts +0 -1
  122. package/src/engine/payment-methods.ts +0 -1
  123. package/src/engine/refunds.ts +2 -28
  124. package/src/engine/renewals.ts +0 -1
  125. package/src/engine/stripe.ts +35 -83
  126. package/src/engine/subscriptions.ts +0 -3
  127. package/src/engine/transfers.ts +0 -1
  128. package/src/engine/version.ts +15 -7
  129. package/src/engine/webhooks.ts +0 -2
  130. package/src/fetch.ts +2 -2
  131. package/src/generated/surface.gen.json +1 -1
  132. package/src/generated/ui.gen.json +1 -1
  133. package/src/index.ts +6 -4
  134. package/src/manifest.ts +348 -220
  135. package/src/screens/billing-portal.tsx +5 -7
  136. package/src/screens/checkout-js.ts +80 -0
  137. package/src/screens/checkout.tsx +0 -2
  138. package/src/screens/connect-onboarding.tsx +3 -5
  139. package/src/screens/dashboard.tsx +55 -73
  140. package/src/screens/stripe-js.ts +57 -125
  141. package/src/semantics/account.ts +10 -0
  142. package/src/semantics/account_links.ts +0 -1
  143. package/src/semantics/accounts.ts +8 -7
  144. package/src/semantics/apps.ts +1 -2
  145. package/src/semantics/around.ts +3 -6
  146. package/src/semantics/balance.ts +0 -1
  147. package/src/semantics/balance_transactions.ts +1 -3
  148. package/src/semantics/billing_portal.ts +1 -7
  149. package/src/semantics/charges.ts +1 -6
  150. package/src/semantics/checkout.ts +16 -12
  151. package/src/semantics/clock.ts +1 -8
  152. package/src/semantics/coupons.ts +0 -1
  153. package/src/semantics/customers.ts +32 -35
  154. package/src/semantics/doors.ts +2 -3
  155. package/src/semantics/entitlements.ts +1 -45
  156. package/src/semantics/events.ts +7 -14
  157. package/src/semantics/index.ts +16 -38
  158. package/src/semantics/invoice_payments.ts +1 -3
  159. package/src/semantics/invoices.ts +124 -119
  160. package/src/semantics/issuing.ts +74 -0
  161. package/src/semantics/payment_intents.ts +10 -17
  162. package/src/semantics/payment_methods.ts +2 -25
  163. package/src/semantics/payouts.ts +9 -3
  164. package/src/semantics/prices.ts +0 -1
  165. package/src/semantics/products.ts +4 -14
  166. package/src/semantics/promotion_codes.ts +0 -1
  167. package/src/semantics/radar.ts +1 -3
  168. package/src/semantics/refunds.ts +2 -11
  169. package/src/semantics/shared.ts +350 -1382
  170. package/src/semantics/states.ts +109 -330
  171. package/src/semantics/subscriptions.ts +25 -145
  172. package/src/semantics/test_helpers.ts +109 -43
  173. package/src/semantics/transfers.ts +1 -3
  174. package/src/semantics/webhook_endpoints.ts +27 -0
  175. package/src/server.ts +0 -1
  176. package/dist/generated/pack-facts.json +0 -94
  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
@@ -2,34 +2,79 @@
2
2
  // handler contract's context, computing with the engine (../engine/).
3
3
  import type { HandlerContext } from '@volter/world-core';
4
4
  import type { Row } from '../engine/common.ts';
5
- import { collectsThroughIntent, draft, INV, linesOf, prorationLines, sumLines, transitions } from '../engine/invoices.ts';
6
- import { addBillingInterval, applyCouponDiscount, asBool, cardError, declineFor, nowUnix, paginate, priceAmount, subscriptionItemPairs } from '../engine/stripe.ts';
7
- import { activatedByPayment, at, created, editLines, expanded, fail, finalizedFields, finder, invoiceMissing, invoicesLoad, mintCharge, newest, nothingToPayWith, openPaymentIntent, path, pullPendingItem, search, send, spendOnceCoupon } from './shared.ts';
8
-
9
- // only a draft is deleted; a finalized invoice is voided instead
10
- export async function DeleteInvoicesInvoice(ctx: HandlerContext): Promise<Response> {
11
- const loaded = invoicesLoad(ctx, 'DeleteInvoicesInvoice');
12
- if ('answer' in loaded) return loaded.answer;
13
- const id = String(loaded.inv.id);
14
- await ctx.write(INV, id, { deleted: true }, 'invoice.deleted');
15
- return ctx.reply({ id, object: 'invoice', deleted: true });
16
- }
17
-
5
+ import { INV, transitions } from '../engine/invoices.ts';
6
+ import { asBool, cardError, declineFor, nowUnix } from '../engine/stripe.ts';
7
+ import { resourceView, activatedByPayment, at, expanded, finalizeForPayment, finder, invoiceMissing, invoicesLoad, mintCharge, nothingToPayWith, send } from './shared.ts';
8
+ import { draft, prorationLines, sumLines } from '../engine/invoices.ts';
9
+ import { addBillingInterval, applyCouponDiscount, priceAmount } from '../engine/stripe.ts';
10
+ import { created, fail, newest, pendingPreviewLines, pullPendingItem } from './shared.ts';
18
11
  export async function GetInvoicesInvoice(ctx: HandlerContext): Promise<Response> {
19
12
  const id = at(ctx, 'invoice');
20
13
  const inv = ctx.get(INV, id);
21
14
  return inv ? ctx.reply(expanded(ctx, INV, inv)) : invoiceMissing(ctx, id);
22
15
  }
23
16
 
24
- // the invoice's lines, a page at a time
25
- export async function GetInvoicesInvoiceLines(ctx: HandlerContext): Promise<Response> {
17
+ // paying charges a payment method (the one passed, the invoice's default, or the customer's) unless
18
+ // the payment is recorded out of band; a declining card answers 402 and leaves the invoice open. A
19
+ // paid invoice settles the PaymentIntent finalize opened, and names the charge that took the money.
20
+ export async function PostInvoicesInvoicePay(ctx: HandlerContext): Promise<Response> {
26
21
  const id = at(ctx, 'invoice');
27
- const inv = ctx.get(INV, id);
28
- if (!inv) return invoiceMissing(ctx, id);
29
- const { page, hasMore } = paginate(linesOf(inv), ctx.params);
30
- return ctx.reply({ object: 'list', url: path(ctx), has_more: hasMore, data: page.map((l) => ({ invoice: id, ...l })) });
22
+ const found = ctx.get(INV, id);
23
+ if (!found) return invoiceMissing(ctx, id);
24
+ const refused = ctx.legal(INV, 'status', 'PostInvoicesInvoicePay', found.status, undefined, id);
25
+ if (refused) return ctx.refuse(refused);
26
+ // paying a draft finalizes it first, as its finalize does (its number, and the PaymentIntent it collects through)
27
+ // source: https://docs.stripe.com/invoicing/integration "To finalize a draft invoice, use the Dashboard, send it to the customer, or pay it."
28
+ const inv = found.status === 'draft' ? await finalizeForPayment(ctx, found, id) : found;
29
+ const params = ctx.params;
30
+ const paidOutOfBand = params.paid_out_of_band !== undefined && asBool(params.paid_out_of_band);
31
+ const payer = ctx.get('customer', String(inv.customer ?? ''));
32
+ const customerDefault = (payer?.invoice_settings as Row | undefined)?.default_payment_method || payer?.default_source;
33
+ if (!paidOutOfBand && !params.payment_method && !params.source && !inv.default_payment_method && !inv.default_source && !customerDefault) return nothingToPayWith(ctx);
34
+ // what it charges: the method passed, else the invoice's default, its subscription's, then its customer's
35
+ // (docs.stripe.com/api/invoices/pay#pay_invoice-payment_method)
36
+ const subscription = typeof inv.subscription === 'string' ? resourceView(ctx, 'subscription', inv.subscription) : undefined;
37
+ const decline = declineFor(finder(ctx), params, { payment_method: inv.default_payment_method ?? subscription?.default_payment_method ?? customerDefault });
38
+ const intent = typeof inv.payment_intent === 'string' ? ctx.get('payment_intent', inv.payment_intent) : undefined;
39
+ if (decline) return send(ctx, cardError(decline, intent ? { payment_intent: intent } : {}));
40
+ const amountPaid = Number(inv.total ?? inv.amount_due) || 0;
41
+ let body = await ctx.write(INV, id, {
42
+ // paid outside Stripe, the invoice counts it as such: `amount_paid_off_stripe`, "Amount, in cents (or local
43
+ // equivalent), that was paid on the invoice outside of Stripe" (served spec, invoice), the only place the served
44
+ // version shows it now that basil renders no `paid_out_of_band` (../engine/version.ts)
45
+ status: 'paid', paid: true, paid_out_of_band: paidOutOfBand, amount_paid: amountPaid, amount_remaining: 0,
46
+ ...(paidOutOfBand ? { amount_paid_off_stripe: amountPaid } : {}),
47
+ status_transitions: { ...transitions(inv), paid_at: nowUnix(ctx.occurredAt) },
48
+ }, 'invoice.pay');
49
+ if (!paidOutOfBand && typeof inv.payment_intent === 'string' && inv.payment_intent) {
50
+ const pi = ctx.get('payment_intent', inv.payment_intent);
51
+ if (pi && pi.status !== 'succeeded' && !ctx.legal('payment_intent', 'status', 'PostInvoicesInvoicePay', pi.status)) {
52
+ const chargeId = await mintCharge(ctx, inv.payment_intent, { ...pi, invoice: id, ...(typeof params.payment_method === 'string' ? { payment_method: params.payment_method } : {}) }, amountPaid);
53
+ await ctx.write('payment_intent', inv.payment_intent, { status: 'succeeded', amount_received: amountPaid, amount_capturable: 0, next_action: null, latest_charge: chargeId }, 'payment_intent.succeeded');
54
+ body = await ctx.write(INV, id, { charge: chargeId }, 'invoice.update');
55
+ }
56
+ }
57
+ // a subscription waiting on this invoice (incomplete on its first, past_due on a failed renewal) is active once it is paid
58
+ const subId = typeof inv.subscription === 'string' ? inv.subscription : undefined;
59
+ const sub = subId ? resourceView(ctx, 'subscription', subId) : undefined;
60
+ if (subId && sub && (sub.status === 'incomplete' || sub.status === 'past_due') && !ctx.legal('subscription', 'status', 'PostInvoicesInvoicePay', sub.status, 'active', subId)) await activatedByPayment(ctx, subId);
61
+ return ctx.reply(expanded(ctx, INV, ctx.get(INV, id) ?? body));
31
62
  }
32
63
 
64
+ // sending a draft finalizes it; any other invoice keeps its status and records the send
65
+
66
+ export async function PostInvoicesInvoiceVoid(ctx: HandlerContext): Promise<Response> {
67
+ const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceVoid');
68
+ if ('answer' in loaded) return loaded.answer;
69
+ const inv = loaded.inv;
70
+ const body = await ctx.write(INV, String(inv.id), { status: 'void', status_transitions: { ...transitions(inv), voided_at: nowUnix(ctx.occurredAt) } }, 'invoice.void');
71
+ return ctx.reply(expanded(ctx, INV, body));
72
+ }
73
+
74
+
75
+
76
+ // the invoice's lines, a page at a time
77
+
33
78
  // a new invoice pulls in the customer's pending items only when asked: pending_invoice_items_behavior "Defaults to
34
79
  // `exclude` if the parameter is omitted" (docs.stripe.com/api/invoices/create)
35
80
  export async function PostInvoices(ctx: HandlerContext): Promise<Response> {
@@ -53,21 +98,21 @@ export async function PostInvoices(ctx: HandlerContext): Promise<Response> {
53
98
  return ctx.reply(pending.length > 0 ? (ctx.get(INV, invoiceId) ?? inv) : inv);
54
99
  }
55
100
 
101
+
102
+
56
103
  /**
57
104
  * A preview of the next invoice for a customer or subscription (create_preview), computed and never
58
105
  * stored: the subscription's items at their prices, the customer's pending items, the subscription's
59
- * discount, and with `subscription_details.proration_date` a credit for the unused period against a
60
- * charge for the new one (split evenly: there is no clock here to measure the remaining fraction).
106
+ * discount, and an update's unused-time credit and remaining-time charge at the old and new prices.
61
107
  */
62
108
  export async function PostInvoicesCreatePreview(ctx: HandlerContext): Promise<Response> {
63
109
  const params = ctx.params;
64
- const prorationDate = (params.subscription_details as Row | undefined)?.proration_date;
65
110
  const now = Number(nowUnix(ctx.occurredAt));
66
111
  let subId = typeof params.subscription === 'string' ? params.subscription : '';
67
112
  let customer = typeof params.customer === 'string' ? params.customer : '';
68
113
  let sub: Row | undefined;
69
114
  if (subId) {
70
- sub = ctx.get('subscription', subId);
115
+ sub = resourceView(ctx, 'subscription', subId);
71
116
  if (!sub) return fail(ctx, `No such subscription: '${subId}'`, 404, 'resource_missing');
72
117
  customer = customer || String(sub.customer ?? '');
73
118
  } else if (customer) {
@@ -81,17 +126,48 @@ export async function PostInvoicesCreatePreview(ctx: HandlerContext): Promise<Re
81
126
  // the upcoming invoice bills the subscription's next period, its "subscription renewal charges"
82
127
  // (docs.stripe.com/api/invoices/create_preview): from the current period's end, one billing interval long
83
128
  const renewsAt = sub && Number(sub.current_period_end) > 0 ? Number(sub.current_period_end) : now;
84
- const itemRows = sub ? ctx.rows('subscription_item').filter((r) => r.subscription === subId) : [];
129
+ let itemRows = sub ? ctx.rows('subscription_item').filter((r) => r.subscription === subId) : [];
130
+ const details = params.subscription_details as Row | undefined;
131
+ const previousItems = itemRows;
132
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "The default value is"
133
+ const behavior = details?.proration_behavior ?? 'create_prorations';
134
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "The prorated amount is calculated as soon as the API updates the subscription."
135
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "forces the proration to be calculated as though the update was done at the specified time."
136
+ const prorationTime = details?.proration_date === undefined ? now : Number(details.proration_date);
137
+ const periodStart = Number(sub?.current_period_start ?? now);
138
+ const periodEnd = Number(sub?.current_period_end ?? now);
139
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "The time given must be within the current subscription period"
140
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "cannot be set to"
141
+ if (details?.proration_date !== undefined && (behavior === 'none' || !sub || !Array.isArray(details.items) || prorationTime < periodStart || prorationTime > periodEnd)) return fail(ctx, 'Invalid subscription_details[proration_date] for this subscription preview.', 400, 'parameter_invalid_integer');
142
+ // source: spec:/paths/~1v1~1invoices~1create_preview/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/subscription_details/description "The subscription creation or modification params to apply as a preview."
143
+ // Preview changes are local to this calculation: the subscription and its stored items are never changed.
144
+ if (Array.isArray(details?.items)) {
145
+ itemRows = itemRows.map(item => ({ ...item }));
146
+ for (const change of details.items as Row[]) {
147
+ const index = change.id ? itemRows.findIndex(item => item.id === change.id) : -1;
148
+ if (change.id && index < 0) return fail(ctx, `No such subscription_item: '${change.id}'`, 400, 'resource_missing');
149
+ if (asBool(change.deleted)) { if (index >= 0) itemRows.splice(index, 1); continue; }
150
+ const previous = index >= 0 ? itemRows[index]! : {};
151
+ const priceId = typeof change.price === 'string' ? change.price : typeof previous.price === 'string' ? previous.price : (previous.price as Row | undefined)?.id;
152
+ const price = typeof priceId === 'string' ? ctx.get('price', priceId) : undefined;
153
+ if (!price) return fail(ctx, `No such price: '${priceId ?? ''}'`, 400, 'resource_missing');
154
+ const quantity = change.quantity === undefined ? Number(previous.quantity ?? 1) : Number(change.quantity);
155
+ const previewItem = { ...previous, price, quantity };
156
+ if (index >= 0) itemRows[index] = previewItem; else itemRows.push(previewItem);
157
+ }
158
+ }
85
159
  const out: Row[] = [];
86
160
  let subtotal = 0;
87
161
  let period = now + 30 * 24 * 3600;
88
162
  if (sub) {
89
- for (const { price: priceId, quantity } of subscriptionItemPairs(sub)) {
163
+ for (const previewItem of itemRows) {
164
+ const priceId = typeof previewItem.price === 'string' ? previewItem.price : String((previewItem.price as Row | undefined)?.id ?? '');
165
+ const quantity = Number(previewItem.quantity) || 0;
90
166
  const price = ctx.get('price', priceId);
91
167
  const amount = priceAmount(price, quantity);
92
168
  const recurring = (price?.recurring as Row | undefined) ?? {};
93
169
  period = recurring.interval ? addBillingInterval(renewsAt, recurring.interval as Parameters<typeof addBillingInterval>[1], Number(recurring.interval_count) || 1) : period;
94
- const item = itemRows.find((r) => (typeof r.price === 'string' ? r.price : (r.price as Row | undefined)?.id) === priceId);
170
+ const item = previewItem;
95
171
  subtotal += amount;
96
172
  out.push({
97
173
  id: `il_twin_${priceId}`, object: 'line_item', type: 'subscription', amount, currency,
@@ -99,17 +175,30 @@ export async function PostInvoicesCreatePreview(ctx: HandlerContext): Promise<Re
99
175
  period: { start: renewsAt, end: period }, description: `${quantity} × ${(price?.nickname as string) ?? priceId}`,
100
176
  });
101
177
  }
102
- if (prorationDate !== undefined && subtotal > 0) prorationLines(out, subtotal, currency, now, period);
103
178
  }
104
- for (const ii of newest(ctx, 'invoiceitem', 'date')) {
105
- if (ii.customer !== customer || ii.invoice) continue;
106
- out.push({ id: `il_${String(ii.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(ii.amount) || 0, currency, quantity: Number(ii.quantity) || 1, proration: false, invoice_item: ii.id, period: ii.period ?? { start: now, end: now }, description: (ii.description as string) ?? null });
107
- }
108
- subtotal = sumLines(out);
109
179
  const subDiscount = Array.isArray(sub?.discounts) && sub!.discounts.length ? (sub!.discounts as Row[])[0] : undefined;
110
180
  const coupon = subDiscount && typeof subDiscount.coupon === 'object' ? (subDiscount.coupon as Row) : undefined;
111
- const discountAmount = applyCouponDiscount(Math.max(0, subtotal), coupon);
112
- const total = Math.max(0, subtotal - discountAmount);
181
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "Disable creating prorations in this request."
182
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "creates a non-proration debit, even when the debit covers a partial billing period."
183
+ if (sub && Array.isArray(details?.items) && behavior !== 'none' && sub.status !== 'trialing') {
184
+ const before = previousItems.map(item => {
185
+ const priceId = typeof item.price === 'string' ? item.price : String((item.price as Row).id);
186
+ const price = ctx.get('price', priceId);
187
+ return { id: `il_twin_${priceId}`, object: 'line_item', type: 'subscription', amount: priceAmount(price, Number(item.quantity ?? 1)), currency,
188
+ price: price ?? priceId, quantity: Number(item.quantity ?? 1), subscription: subId, subscription_item: item.id,
189
+ period: { start: item.current_period_start ?? periodStart, end: item.current_period_end ?? periodEnd } };
190
+ });
191
+ const after = out.map(line => ({ ...line, period: { start: periodStart, end: periodEnd } }));
192
+ const prorations = prorationLines(before, after, prorationTime, periodStart, periodEnd, coupon);
193
+ // source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "Always invoice immediately for prorations."
194
+ if (behavior === 'always_invoice' && prorations.length) out.splice(0, out.length, ...prorations);
195
+ else out.unshift(...prorations);
196
+ }
197
+ out.push(...pendingPreviewLines(ctx, customer, currency, now));
198
+ subtotal = sumLines(out);
199
+ // source: https://docs.stripe.com/billing/subscriptions/prorations "no additional discounts are applied to the proration line items themselves"
200
+ const discountAmount = applyCouponDiscount(Math.max(0, sumLines(out.filter(line => !line.proration))), coupon);
201
+ const total = subtotal - discountAmount;
113
202
  return ctx.reply({
114
203
  ...draft(now),
115
204
  // "For preview invoices created using the create preview endpoint, this id will be prefixed with `upcoming_in`"
@@ -118,94 +207,10 @@ export async function PostInvoicesCreatePreview(ctx: HandlerContext): Promise<Re
118
207
  object: 'invoice', id: `upcoming_${ctx.mint(INV)}`, customer, subscription: subId || null, currency,
119
208
  billing_reason: subId ? 'upcoming' : 'manual',
120
209
  created: now, period_start: now, period_end: period,
121
- subtotal, total, amount_due: total, amount_paid: 0, amount_remaining: total,
210
+ subtotal, total, amount_due: Math.max(0, total), amount_paid: 0, amount_remaining: Math.max(0, total),
122
211
  starting_balance: 0, ending_balance: null,
123
212
  discounts: discountAmount > 0 && subDiscount ? [subDiscount] : [],
124
213
  total_discount_amounts: discountAmount > 0 ? [{ amount: discountAmount, discount: subDiscount?.id ?? null }] : [],
125
214
  lines: { object: 'list', url: '/v1/invoices/create_preview/lines', has_more: false, total_count: out.length, data: out },
126
215
  });
127
216
  }
128
-
129
- export async function PostInvoicesInvoiceFinalize(ctx: HandlerContext): Promise<Response> {
130
- const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceFinalize');
131
- if ('answer' in loaded) return loaded.answer;
132
- const inv = loaded.inv;
133
- const id = String(inv.id);
134
- const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
135
- const body = await ctx.write(INV, id, { ...finalizedFields(ctx, inv, Number(nowUnix(ctx.occurredAt))), status: 'open', status_transitions: { ...transitions(inv), finalized_at: nowUnix(ctx.occurredAt) }, payment_intent: piId }, 'invoice.finalize');
136
- if (piId) await openPaymentIntent(ctx, piId, inv, id);
137
- // a subscription's invoice finalized spends the once coupon it took (renewals.ts spendOnceCoupon)
138
- await spendOnceCoupon(ctx, inv);
139
- return ctx.reply(expanded(ctx, INV, body));
140
- }
141
-
142
- // paying charges a payment method (the one passed, the invoice's default, or the customer's) unless
143
- // the payment is recorded out of band; a declining card answers 402 and leaves the invoice open. A
144
- // paid invoice settles the PaymentIntent finalize opened, and names the charge that took the money.
145
- export async function PostInvoicesInvoicePay(ctx: HandlerContext): Promise<Response> {
146
- const id = at(ctx, 'invoice');
147
- const inv = ctx.get(INV, id);
148
- if (!inv) return invoiceMissing(ctx, id);
149
- const refused = ctx.legal(INV, 'status', 'PostInvoicesInvoicePay', inv.status, undefined, id);
150
- if (refused) return ctx.refuse(refused);
151
- const params = ctx.params;
152
- const paidOutOfBand = params.paid_out_of_band !== undefined && asBool(params.paid_out_of_band);
153
- const payer = ctx.get('customer', String(inv.customer ?? ''));
154
- const customerDefault = (payer?.invoice_settings as Row | undefined)?.default_payment_method || payer?.default_source;
155
- if (!paidOutOfBand && !params.payment_method && !params.source && !inv.default_payment_method && !inv.default_source && !customerDefault) return nothingToPayWith(ctx);
156
- // what it charges: the method passed, else the invoice's default, its subscription's, then its customer's
157
- // (docs.stripe.com/api/invoices/pay#pay_invoice-payment_method)
158
- const subscription = typeof inv.subscription === 'string' ? ctx.get('subscription', inv.subscription) : undefined;
159
- const decline = declineFor(finder(ctx), params, { payment_method: inv.default_payment_method ?? subscription?.default_payment_method ?? customerDefault });
160
- const intent = typeof inv.payment_intent === 'string' ? ctx.get('payment_intent', inv.payment_intent) : undefined;
161
- if (decline) return send(ctx, cardError(decline, intent ? { payment_intent: intent } : {}));
162
- const amountPaid = Number(inv.total ?? inv.amount_due) || 0;
163
- let body = await ctx.write(INV, id, {
164
- // paid outside Stripe, the invoice counts it as such: `amount_paid_off_stripe`, "Amount, in cents (or local
165
- // equivalent), that was paid on the invoice outside of Stripe" (served spec, invoice), the only place the served
166
- // version shows it now that basil renders no `paid_out_of_band` (../engine/version.ts)
167
- status: 'paid', paid: true, paid_out_of_band: paidOutOfBand, amount_paid: amountPaid, amount_remaining: 0,
168
- ...(paidOutOfBand ? { amount_paid_off_stripe: amountPaid } : {}),
169
- status_transitions: { ...transitions(inv), paid_at: nowUnix(ctx.occurredAt) },
170
- }, 'invoice.pay');
171
- if (!paidOutOfBand && typeof inv.payment_intent === 'string' && inv.payment_intent) {
172
- const pi = ctx.get('payment_intent', inv.payment_intent);
173
- if (pi && pi.status !== 'succeeded' && !ctx.legal('payment_intent', 'status', 'PostInvoicesInvoicePay', pi.status)) {
174
- const chargeId = await mintCharge(ctx, inv.payment_intent, { ...pi, invoice: id, ...(typeof params.payment_method === 'string' ? { payment_method: params.payment_method } : {}) }, amountPaid);
175
- await ctx.write('payment_intent', inv.payment_intent, { status: 'succeeded', amount_received: amountPaid, amount_capturable: 0, next_action: null, latest_charge: chargeId }, 'payment_intent.succeeded');
176
- body = await ctx.write(INV, id, { charge: chargeId }, 'invoice.update');
177
- }
178
- }
179
- // a subscription waiting on this invoice (incomplete on its first, past_due on a failed renewal) is active once it is paid
180
- const subId = typeof inv.subscription === 'string' ? inv.subscription : undefined;
181
- const sub = subId ? ctx.get('subscription', subId) : undefined;
182
- if (subId && sub && (sub.status === 'incomplete' || sub.status === 'past_due') && !ctx.legal('subscription', 'status', 'PostInvoicesInvoicePay', sub.status, 'active', subId)) await activatedByPayment(ctx, subId);
183
- return ctx.reply(expanded(ctx, INV, body));
184
- }
185
-
186
- // sending a draft finalizes it; any other invoice keeps its status and records the send
187
- export async function PostInvoicesInvoiceSend(ctx: HandlerContext): Promise<Response> {
188
- const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceSend');
189
- if ('answer' in loaded) return loaded.answer;
190
- const inv = loaded.inv;
191
- const id = String(inv.id);
192
- const becameOpen = inv.status === 'draft';
193
- const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
194
- const now = nowUnix(ctx.occurredAt);
195
- const body = await ctx.write(INV, id, {
196
- status: becameOpen ? 'open' : (inv.status as string), _sent: true, _sent_at: now,
197
- status_transitions: { ...transitions(inv), ...(becameOpen ? { finalized_at: now } : {}) },
198
- ...(becameOpen ? { payment_intent: piId } : {}),
199
- }, 'invoice.sent');
200
- if (piId) await openPaymentIntent(ctx, piId, inv, id);
201
- if (becameOpen) await spendOnceCoupon(ctx, inv);
202
- return ctx.reply(expanded(ctx, INV, body));
203
- }
204
-
205
- export async function PostInvoicesInvoiceVoid(ctx: HandlerContext): Promise<Response> {
206
- const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceVoid');
207
- if ('answer' in loaded) return loaded.answer;
208
- const inv = loaded.inv;
209
- const body = await ctx.write(INV, String(inv.id), { status: 'void', status_transitions: { ...transitions(inv), voided_at: nowUnix(ctx.occurredAt) } }, 'invoice.void');
210
- return ctx.reply(expanded(ctx, INV, body));
211
- }
@@ -0,0 +1,74 @@
1
+ // Stripe's issuing operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
2
+ // handler contract's context, computing with the engine (../engine/).
3
+ import type { HandlerContext } from '@volter/world-core';
4
+ import type { Row } from '../engine/common.ts';
5
+ import { CARD, CH, cardNumberOf, cvcOf } from '../engine/issuing.ts';
6
+ import { emptySpendingControls, normalizeSpendingControls, nowUnix } from '../engine/stripe.ts';
7
+ import { at, created, fail, issuingDecide, issuingRefusal, send } from './shared.ts';
8
+
9
+ export async function GetIssuingCardsCard(ctx: HandlerContext): Promise<Response> {
10
+ const id = at(ctx, 'card');
11
+ const card = ctx.get(CARD, id);
12
+ if (!card) return fail(ctx, `No such card: '${id}'`, 404, 'resource_missing');
13
+ const expand = Array.isArray(ctx.params.expand) ? ctx.params.expand.map(String) : [];
14
+ // source: spec:/components/schemas/issuing.card/properties/number/description "For security reasons, this is only available for virtual cards, and will be omitted unless you explicitly request it with"
15
+ const virtual = card.type === 'virtual';
16
+ const shown: Row = { ...card };
17
+ if (virtual && expand.includes('number')) shown.number = cardNumberOf(await ctx.secret(`issuing-card-number:${id}`));
18
+ // source: spec:/components/schemas/issuing.card/properties/cvc/description "The card's CVC. For security reasons, this is only available for virtual cards"
19
+ if (virtual && expand.includes('cvc')) shown.cvc = cvcOf(await ctx.secret(`issuing-card-cvc:${id}`));
20
+ return ctx.reply(ctx.expand(CARD, shown));
21
+ }
22
+
23
+ export async function PostIssuingAuthorizationsAuthorizationApprove(ctx: HandlerContext): Promise<Response> {
24
+ return issuingDecide(true)(ctx);
25
+ }
26
+
27
+ export async function PostIssuingAuthorizationsAuthorizationDecline(ctx: HandlerContext): Promise<Response> {
28
+ return issuingDecide(false)(ctx);
29
+ }
30
+
31
+ export async function PostIssuingCards(ctx: HandlerContext): Promise<Response> {
32
+ const params = ctx.params;
33
+ // source: spec:PostIssuingCards "The currency for the card."
34
+ if (params.currency === undefined || params.currency === '') return fail(ctx, 'Missing required param: currency.', 400, 'parameter_missing');
35
+ const cardType = typeof params.type === 'string' ? params.type : '';
36
+ if (!cardType) return fail(ctx, 'Missing required param: type.', 400, 'parameter_missing');
37
+ // source: spec:PostIssuingCards "The type of card to issue."
38
+ if (cardType !== 'virtual' && cardType !== 'physical') return fail(ctx, "Invalid type: must be one of 'virtual' or 'physical'.", 400, 'parameter_invalid_string_enum');
39
+ // the card answers its cardholder in full (the served spec's issuing.card.cardholder is the Cardholder object)
40
+ const holder = typeof params.cardholder === 'string' ? params.cardholder : '';
41
+ const cardholder = holder ? ctx.get(CH, holder) : undefined;
42
+ if (holder && !cardholder) return fail(ctx, `No such cardholder: '${holder}'`, 400, 'resource_missing');
43
+ const controls = normalizeSpendingControls(params.spending_controls, typeof params.currency === 'string' ? params.currency : null);
44
+ if (controls.error) return send(ctx, controls.error);
45
+ const id = ctx.mint(CARD);
46
+ const number = cardNumberOf(await ctx.secret(`issuing-card-number:${id}`));
47
+ // Where the documentation stops and the twin decides: a card expires three years after it is issued, at the end of
48
+ // the month it was issued in.
49
+ const issued = new Date(Number(nowUnix(ctx.occurredAt)) * 1000);
50
+ return ctx.reply(ctx.expand(CARD, await created(ctx, CARD, { ...params, id, cardholder: holder || null, spending_controls: controls.controls ?? emptySpendingControls() }, {
51
+ // source: https://docs.stripe.com/api/issuing/cards/create "Whether authorizations can be approved on this card. May be blocked from activating cards depending on past-due Cardholder requirements. Defaults to inactive."
52
+ status: typeof params.status === 'string' ? params.status : 'inactive',
53
+ livemode: false, metadata: {}, brand: 'Visa', last4: number.slice(-4),
54
+ exp_month: issued.getUTCMonth() + 1, exp_year: issued.getUTCFullYear() + 3,
55
+ cancellation_reason: null, replacement_for: null, replacement_reason: null, shipping: null, wallets: null,
56
+ })));
57
+ }
58
+
59
+ // source: spec:PostIssuingCardsCard "Any parameters not provided will be left unchanged."
60
+ export async function PostIssuingCardsCard(ctx: HandlerContext): Promise<Response> {
61
+ const id = at(ctx, 'card');
62
+ const card = ctx.get(CARD, id);
63
+ if (!card) return fail(ctx, `No such card: '${id}'`, 404, 'resource_missing');
64
+ const { expand: _expand, ...fields } = ctx.params;
65
+ // source: spec:/paths/~1v1~1issuing~1cards~1{card}/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/status "Dictates whether authorizations can be approved on this card."
66
+ if (fields.status !== undefined && !['active', 'inactive', 'canceled'].includes(String(fields.status))) return issuingRefusal(ctx, 'card_status');
67
+ if (fields.status !== undefined) {
68
+ const refused = ctx.legal(CARD, 'status', 'PostIssuingCardsCard', card.status, String(fields.status), id);
69
+ if (refused) return ctx.refuse(refused);
70
+ }
71
+ const controls = normalizeSpendingControls(fields.spending_controls, String(card.currency), card.spending_controls as Row);
72
+ if (controls.error) return send(ctx, controls.error);
73
+ return ctx.reply(ctx.expand(CARD, await ctx.write(CARD, id, { ...fields, ...(controls.controls ? { spending_controls: controls.controls } : {}) }, 'issuing.card.update')));
74
+ }
@@ -1,11 +1,10 @@
1
1
  // Stripe's payment_intents operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
2
2
  // handler contract's context, computing with the engine (../engine/).
3
3
  import type { HandlerContext } from '@volter/world-core';
4
- import type { Row } from '../engine/common.ts';
5
4
  import { PI, TYPES_GIVEN } from '../engine/payment-intents.ts';
6
- import { asBool, mintClientSecret, nowUnix, validateMoney, verifyMicrodeposits } from '../engine/stripe.ts';
7
- import { afterCharge, beginBankDebit, cancelAuthorization, captureAuthorization, confirmIntent, confirmOnly, fundFromCashBalance, incrementTooSmall, methodFromData, mintCharge, mintMandate, payInvoiceOf, paymentIntentsLoad, paymentIntentsSend, search } from './shared.ts';
8
-
5
+ import { asBool, mintClientSecret, nowUnix, validateMoney } from '../engine/stripe.ts';
6
+ import { confirmIntent, intentFields, confirmOnly, methodFromData, paymentIntentsSend } from './shared.ts';
7
+ import { cancelAuthorization, paymentIntentsLoad } from './shared.ts';
9
8
  export async function PostPaymentIntents(ctx: HandlerContext): Promise<Response> {
10
9
  const params = ctx.params;
11
10
  const bad = validateMoney(params);
@@ -51,15 +50,20 @@ export async function PostPaymentIntents(ctx: HandlerContext): Promise<Response>
51
50
  payment_method_types,
52
51
  ...(Array.isArray(params.allowed_payment_method_types) ? { allowed_payment_method_types: payment_method_types } : {}),
53
52
  payment_method_options: (params.payment_method_options as object) ?? {},
54
- ...rest,
53
+ ...intentFields(rest),
55
54
  },
56
55
  'payment_intent.create',
57
56
  );
58
57
  if (Array.isArray(given)) await ctx.record(TYPES_GIVEN, { payment_intent: id }, id);
59
- if (asBool(confirmNow)) return confirmIntent(ctx, body, {});
58
+ if (asBool(confirmNow)) return confirmIntent(ctx, body, { ...params, payment_method: rest.payment_method });
60
59
  return ctx.reply(ctx.expand(PI, body));
61
60
  }
62
61
 
62
+
63
+ // source: spec:GetPaymentIntentsSearch "Search for PaymentIntents"
64
+
65
+
66
+
63
67
  export async function PostPaymentIntentsIntentCancel(ctx: HandlerContext): Promise<Response> {
64
68
  const loaded = paymentIntentsLoad(ctx, 'PostPaymentIntentsIntentCancel');
65
69
  if ('answer' in loaded) return loaded.answer;
@@ -70,14 +74,3 @@ export async function PostPaymentIntentsIntentCancel(ctx: HandlerContext): Promi
70
74
  if (held && held.captured === false && held.refunded !== true) await cancelAuthorization(ctx, held);
71
75
  return ctx.reply(ctx.expand(PI, await ctx.write(PI, String(loaded.pi.id), { status: 'canceled', cancellation_reason, amount_capturable: 0 }, 'payment_intent.canceled')));
72
76
  }
73
-
74
- export async function PostPaymentIntentsIntentConfirm(ctx: HandlerContext): Promise<Response> {
75
- const loaded = paymentIntentsLoad(ctx, 'PostPaymentIntentsIntentConfirm');
76
- if ('answer' in loaded) return loaded.answer;
77
- const given = ctx.params;
78
- const { expand: _e, payment_method_data: data, mandate_data: _mandate, ...params } = given;
79
- // a confirm's payment_method_data makes the method it pays with; neither it nor mandate_data is a field of the intent
80
- const made = await methodFromData(ctx, data);
81
- return confirmIntent(ctx, loaded.pi, made ? { ...params, payment_method: made } : params);
82
- }
83
-
@@ -1,11 +1,8 @@
1
1
  // Stripe's payment_methods operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
2
2
  // handler contract's context, computing with the engine (../engine/).
3
3
  import type { HandlerContext } from '@volter/world-core';
4
- import type { Row } from '../engine/common.ts';
5
- import { noBilling, PM } from '../engine/payment-methods.ts';
6
- import { declineFor, paymentMethodSubObject } from '../engine/stripe.ts';
7
- import { afterSuccessOf, at, attachable, attachPaymentMethod, created, fail, pmMissing } from './shared.ts';
8
-
4
+ import { PM } from '../engine/payment-methods.ts';
5
+ import { at, pmMissing } from './shared.ts';
9
6
  export async function GetPaymentMethodsPaymentMethod(ctx: HandlerContext): Promise<Response> {
10
7
  const m = ctx.get(PM, at(ctx, 'payment_method'));
11
8
  return m ? ctx.reply(m) : pmMissing(ctx, at(ctx, 'payment_method'));
@@ -14,27 +11,7 @@ export async function GetPaymentMethodsPaymentMethod(ctx: HandlerContext): Promi
14
11
  // the raw card number, cvc and bank account number are never echoed: the modeled sub-object is
15
12
  // what the PaymentMethod carries, with the decline outcome its card implies, so a later charge
16
13
  // naming it only by id declines the same way
17
- export async function PostPaymentMethods(ctx: HandlerContext): Promise<Response> {
18
- const params = ctx.params;
19
- const pmType = typeof params.type === 'string' ? params.type : 'card';
20
- const { card: _card, sepa_debit: _sepa, us_bank_account: _ach, type: _t, ...rest } = params;
21
- const sub = paymentMethodSubObject(pmType, params);
22
- const declineOutcome = pmType === 'card' ? (declineFor(() => undefined, params) ?? null) : null;
23
- // and what Stripe does after its card succeeds (a dispute, a review), which a later charge naming it by id brings
24
- const afterSuccess = pmType === 'card' ? afterSuccessOf(ctx, params.card) : undefined;
25
- // the billing details the create gives are the method's (docs.stripe.com/api/payment_methods/create#create_payment_method-billing_details)
26
- const given = rest.billing_details && typeof rest.billing_details === 'object' ? (rest.billing_details as Row) : {};
27
- return ctx.reply(await created(ctx, PM, { ...rest, type: pmType, customer: null, livemode: false, billing_details: { ...noBilling, ...given }, ...sub, _declineOutcome: declineOutcome, ...(afterSuccess ? { _afterSuccess: afterSuccess } : {}) }, {}));
28
- }
29
14
 
30
- export async function PostPaymentMethodsPaymentMethodAttach(ctx: HandlerContext): Promise<Response> {
31
- const id = at(ctx, 'payment_method');
32
- const customer = typeof ctx.params.customer === 'string' ? ctx.params.customer : '';
33
- if (!customer) return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
34
- if (!ctx.get('customer', customer)) return fail(ctx, `No such customer: '${customer}'`, 400, 'resource_missing');
35
- if (!attachable(ctx, id)) return pmMissing(ctx, id);
36
- return ctx.reply(await attachPaymentMethod(ctx, id, customer));
37
- }
38
15
 
39
16
  export async function PostPaymentMethodsPaymentMethodDetach(ctx: HandlerContext): Promise<Response> {
40
17
  const id = at(ctx, 'payment_method');
@@ -1,10 +1,8 @@
1
1
  // Stripe's payouts operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
2
2
  // handler contract's context, computing with the engine (../engine/).
3
3
  import type { HandlerContext } from '@volter/world-core';
4
- import { inRange } from '../engine/common.ts';
5
4
  import { validateMoney } from '../engine/stripe.ts';
6
- import { account, asOf, at, created, fail, kept, list, newest, noExternalAccount, payoutBank, payoutDefaults, payoutMissing, refusePayout, send, settlePayout, where } from './shared.ts';
7
-
5
+ import { account, asOf, created, kept, list, newest, fail, noExternalAccount, payoutBank, payoutDefaults, refusePayout, send, settlePayout } from './shared.ts';
8
6
  export async function PostPayouts(ctx: HandlerContext): Promise<Response> {
9
7
  const bad = validateMoney(ctx.params);
10
8
  if (bad) return send(ctx, bad);
@@ -21,3 +19,11 @@ export async function PostPayouts(ctx: HandlerContext): Promise<Response> {
21
19
  return ctx.reply(await created(ctx, 'payout', { ...ctx.params, id }, { status: 'pending', ...payoutDefaults(ctx), balance_transaction: bt, destination: acct ? payoutBank(ctx, acct, currency) : null, ...(acct ? { _account: acct } : {}) }));
22
20
  }
23
21
 
22
+
23
+ // source: https://docs.stripe.com/connect/authentication "Stripe-Account header"
24
+ // source: https://docs.stripe.com/payouts "paid"
25
+ export async function GetPayouts(ctx: HandlerContext): Promise<Response> {
26
+ const acct = account(ctx);
27
+ const payouts = newest(ctx, 'payout').filter((p) => (kept(ctx, 'payout', p, '_account') ?? null) === (acct ?? null));
28
+ return list(ctx, 'payout', payouts.map((p) => asOf(ctx, p)));
29
+ }
@@ -4,7 +4,6 @@ import type { HandlerContext } from '@volter/world-core';
4
4
  import type { Row } from '../engine/common.ts';
5
5
  import { asBool } from '../engine/stripe.ts';
6
6
  import { at, created, fail, list, newest, tieredPrice, where } from './shared.ts';
7
-
8
7
  // `type` is not stored (it collides with the tree's discriminator), so `recurring` stands for it
9
8
  export async function GetPrices(ctx: HandlerContext): Promise<Response> {
10
9
  return list(ctx, 'price', where(ctx, newest(ctx, 'price'), {
@@ -2,16 +2,7 @@
2
2
  // handler contract's context, computing with the engine (../engine/).
3
3
  import type { HandlerContext } from '@volter/world-core';
4
4
  import { asBool, nowUnix } from '../engine/stripe.ts';
5
- import { alreadyAttached, at, created, fail, search, featureMissing, kept, list, newest, pricedProductRefused, productFeature, productMissing, where } from './shared.ts';
6
-
7
- export async function DeleteProductsId(ctx: HandlerContext): Promise<Response> {
8
- const id = at(ctx, 'id');
9
- if (!ctx.get('product', id)) return productMissing(ctx, id);
10
- if (ctx.rows('price').some((p) => p.product === id)) return pricedProductRefused(ctx);
11
- await ctx.write('product', id, { deleted: true }, 'product.delete');
12
- return ctx.reply({ id, object: 'product', deleted: true });
13
- }
14
-
5
+ import { created, list, newest, where } from './shared.ts';
15
6
  export async function GetProducts(ctx: HandlerContext): Promise<Response> {
16
7
  return list(ctx, 'product', where(ctx, newest(ctx, 'product'), {
17
8
  active: (p, v) => asBool(p.active) === asBool(v),
@@ -23,8 +14,7 @@ export async function PostProducts(ctx: HandlerContext): Promise<Response> {
23
14
  return ctx.reply(await created(ctx, 'product', ctx.params, { active: true, livemode: false, images: [], marketing_features: [], metadata: {}, updated: nowUnix(ctx.occurredAt) }));
24
15
  }
25
16
 
26
- /** `GET /v1/products/search?query=`: the products Stripe's search language selects (Ponder's catalog probes). */
17
+ /** `GET /v1/products/search?query=`: the products Stripe's search language selects. */
27
18
  // source: spec:GetProductsSearch "Search for products you’ve previously created using Stripe’s"
28
- export async function GetProductsSearch(ctx: HandlerContext): Promise<Response> {
29
- return search(ctx, 'product');
30
- }
19
+
20
+ // source: spec:PostProductsProductFeatures "Creates a product_feature"
@@ -3,7 +3,6 @@
3
3
  import type { HandlerContext } from '@volter/world-core';
4
4
  import { asBool } from '../engine/stripe.ts';
5
5
  import { created, fail, list, newest, where } from './shared.ts';
6
-
7
6
  export async function GetPromotionCodes(ctx: HandlerContext): Promise<Response> {
8
7
  return list(ctx, 'promotion_code', where(ctx, newest(ctx, 'promotion_code'), {
9
8
  code: (p, v) => p.code === v,
@@ -2,8 +2,7 @@
2
2
  // handler contract's context, computing with the engine (../engine/).
3
3
  import type { HandlerContext } from '@volter/world-core';
4
4
  import { VL, VLI } from '../engine/radar.ts';
5
- import { at, created, fail, list, listMissing, newest, radarSyncItems } from './shared.ts';
6
-
5
+ import { created, fail, listMissing, radarSyncItems } from './shared.ts';
7
6
  export async function PostRadarValueListItems(ctx: HandlerContext): Promise<Response> {
8
7
  const listId = typeof ctx.params.value_list === 'string' ? ctx.params.value_list : '';
9
8
  if (!listId) return fail(ctx, 'Missing required param: value_list.', 400, 'parameter_missing');
@@ -13,4 +12,3 @@ export async function PostRadarValueListItems(ctx: HandlerContext): Promise<Resp
13
12
  await radarSyncItems(ctx, listId);
14
13
  return ctx.reply(item);
15
14
  }
16
-
@@ -1,15 +1,6 @@
1
- // Stripe's refunds operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
2
- // handler contract's context, computing with the engine (../engine/).
3
- import type { HandlerContext } from '@volter/world-core';
4
- import { at, refundCharge, refundMissing, target } from './shared.ts';
5
-
6
- export async function GetRefundsRefund(ctx: HandlerContext): Promise<Response> {
7
- const r = ctx.get('refund', at(ctx, 'refund'));
8
- return r ? ctx.reply(r) : refundMissing(ctx, at(ctx, 'refund'));
9
- }
10
-
1
+ import { type HandlerContext } from '@volter/world-core';
2
+ import { refundCharge, target } from './shared.ts';
11
3
  export async function PostRefunds(ctx: HandlerContext): Promise<Response> {
12
4
  const ch = target(ctx);
13
5
  return ch instanceof Response ? ch : refundCharge(ctx, ch, ctx.params);
14
6
  }
15
-