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