@volter/twin-stripe 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +51 -21
  2. package/dist/generated/pack-facts.json +3 -3
  3. package/dist/journeys/coverage.json +596 -0
  4. package/dist/journeys/customer-life.json +6108 -0
  5. package/dist/journeys/customer-life.outline.md +371 -0
  6. package/dist/journeys/decisions.json +677 -0
  7. package/dist/journeys/demand.json +706 -0
  8. package/dist/journeys/score.json +853 -0
  9. package/dist/journeys/unreachable.json +102 -0
  10. package/dist/journeys/vendor-examples.json +8010 -0
  11. package/dist/spec/SOURCE.md +5 -1
  12. package/dist/spec/doc-examples.json +445 -0
  13. package/dist/spec/sources.json +135 -207
  14. package/dist/src/cli.d.ts +0 -1
  15. package/dist/src/cli.js +0 -2
  16. package/dist/src/engine/after-payment.d.ts +0 -1
  17. package/dist/src/engine/after-payment.js +0 -1
  18. package/dist/src/engine/common.d.ts +5 -5
  19. package/dist/src/engine/common.js +13 -11
  20. package/dist/src/engine/connect.d.ts +0 -21
  21. package/dist/src/engine/connect.js +1 -82
  22. package/dist/src/engine/customers.d.ts +0 -8
  23. package/dist/src/engine/customers.js +0 -22
  24. package/dist/src/engine/display.d.ts +0 -3
  25. package/dist/src/engine/display.js +0 -3
  26. package/dist/src/engine/files.d.ts +0 -7
  27. package/dist/src/engine/files.js +0 -10
  28. package/dist/src/engine/invoices.d.ts +6 -9
  29. package/dist/src/engine/invoices.js +56 -29
  30. package/dist/src/engine/issuing.d.ts +6 -8
  31. package/dist/src/engine/issuing.js +19 -10
  32. package/dist/src/engine/ledger.d.ts +0 -1
  33. package/dist/src/engine/ledger.js +0 -1
  34. package/dist/src/engine/oauth.js +1 -2
  35. package/dist/src/engine/refunds.js +2 -27
  36. package/dist/src/engine/stripe.d.ts +2 -7
  37. package/dist/src/engine/stripe.js +34 -83
  38. package/dist/src/engine/subscriptions.d.ts +0 -1
  39. package/dist/src/engine/subscriptions.js +0 -1
  40. package/dist/src/engine/version.d.ts +0 -2
  41. package/dist/src/engine/version.js +17 -6
  42. package/dist/src/fetch.js +2 -2
  43. package/dist/src/generated/surface.gen.json +1 -1
  44. package/dist/src/generated/ui.gen.json +1 -1
  45. package/dist/src/index.js +5 -3
  46. package/dist/src/manifest.js +348 -220
  47. package/dist/src/screens/billing-portal.js +5 -5
  48. package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
  49. package/dist/src/screens/checkout-js.js +84 -0
  50. package/dist/src/screens/connect-onboarding.js +3 -3
  51. package/dist/src/screens/dashboard.d.ts +1 -1
  52. package/dist/src/screens/dashboard.js +52 -53
  53. package/dist/src/screens/stripe-js.js +53 -93
  54. package/dist/src/semantics/account.d.ts +2 -0
  55. package/dist/src/semantics/account.js +8 -0
  56. package/dist/src/semantics/accounts.js +3 -1
  57. package/dist/src/semantics/around.js +3 -6
  58. package/dist/src/semantics/billing_portal.d.ts +0 -1
  59. package/dist/src/semantics/billing_portal.js +1 -7
  60. package/dist/src/semantics/checkout.d.ts +1 -1
  61. package/dist/src/semantics/checkout.js +18 -15
  62. package/dist/src/semantics/clock.js +1 -7
  63. package/dist/src/semantics/customers.d.ts +1 -2
  64. package/dist/src/semantics/customers.js +32 -38
  65. package/dist/src/semantics/entitlements.d.ts +0 -2
  66. package/dist/src/semantics/entitlements.js +4 -50
  67. package/dist/src/semantics/events.d.ts +2 -3
  68. package/dist/src/semantics/events.js +6 -11
  69. package/dist/src/semantics/index.js +16 -38
  70. package/dist/src/semantics/invoices.d.ts +3 -8
  71. package/dist/src/semantics/invoices.js +134 -127
  72. package/dist/src/semantics/issuing.d.ts +6 -0
  73. package/dist/src/semantics/issuing.js +77 -0
  74. package/dist/src/semantics/payment_intents.d.ts +0 -1
  75. package/dist/src/semantics/payment_intents.js +5 -13
  76. package/dist/src/semantics/payment_methods.d.ts +0 -2
  77. package/dist/src/semantics/payment_methods.js +2 -26
  78. package/dist/src/semantics/payouts.d.ts +1 -0
  79. package/dist/src/semantics/payouts.js +8 -1
  80. package/dist/src/semantics/products.d.ts +1 -3
  81. package/dist/src/semantics/products.js +3 -14
  82. package/dist/src/semantics/refunds.d.ts +1 -2
  83. package/dist/src/semantics/refunds.js +1 -5
  84. package/dist/src/semantics/shared.d.ts +64 -336
  85. package/dist/src/semantics/shared.js +326 -1336
  86. package/dist/src/semantics/states.d.ts +20 -89
  87. package/dist/src/semantics/states.js +116 -334
  88. package/dist/src/semantics/subscriptions.d.ts +1 -2
  89. package/dist/src/semantics/subscriptions.js +23 -145
  90. package/dist/src/semantics/test_helpers.d.ts +2 -5
  91. package/dist/src/semantics/test_helpers.js +121 -43
  92. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  93. package/dist/src/semantics/webhook_endpoints.js +26 -0
  94. package/generated/pack-facts.json +3 -3
  95. package/journeys/coverage.json +596 -0
  96. package/journeys/customer-life.json +6108 -0
  97. package/journeys/customer-life.outline.md +371 -0
  98. package/journeys/decisions.json +677 -0
  99. package/journeys/demand.json +706 -0
  100. package/journeys/score.json +853 -0
  101. package/journeys/unreachable.json +102 -0
  102. package/journeys/vendor-examples.json +8010 -0
  103. package/package.json +4 -3
  104. package/spec/SOURCE.md +5 -1
  105. package/spec/doc-examples.json +445 -0
  106. package/spec/sources.json +135 -207
  107. package/src/cli.ts +0 -3
  108. package/src/engine/after-payment.ts +0 -2
  109. package/src/engine/balance.ts +0 -1
  110. package/src/engine/checkout.ts +0 -1
  111. package/src/engine/common.ts +19 -17
  112. package/src/engine/connect.ts +1 -79
  113. package/src/engine/customers.ts +0 -25
  114. package/src/engine/display.ts +2 -3
  115. package/src/engine/files.ts +0 -13
  116. package/src/engine/invoices.ts +64 -33
  117. package/src/engine/issuing.ts +16 -11
  118. package/src/engine/ledger.ts +0 -2
  119. package/src/engine/oauth.ts +1 -2
  120. package/src/engine/onboarding.ts +0 -1
  121. package/src/engine/params.ts +0 -1
  122. package/src/engine/payment-intents.ts +0 -1
  123. package/src/engine/payment-methods.ts +0 -1
  124. package/src/engine/refunds.ts +2 -28
  125. package/src/engine/renewals.ts +0 -1
  126. package/src/engine/stripe.ts +35 -83
  127. package/src/engine/subscriptions.ts +0 -3
  128. package/src/engine/transfers.ts +0 -1
  129. package/src/engine/version.ts +15 -7
  130. package/src/engine/webhooks.ts +0 -2
  131. package/src/fetch.ts +2 -2
  132. package/src/generated/surface.gen.json +1 -1
  133. package/src/generated/ui.gen.json +1 -1
  134. package/src/index.ts +6 -4
  135. package/src/manifest.ts +348 -220
  136. package/src/screens/billing-portal.tsx +5 -7
  137. package/src/screens/checkout-js.ts +80 -0
  138. package/src/screens/checkout.tsx +0 -2
  139. package/src/screens/connect-onboarding.tsx +3 -5
  140. package/src/screens/dashboard.tsx +53 -73
  141. package/src/screens/stripe-js.ts +57 -125
  142. package/src/semantics/account.ts +10 -0
  143. package/src/semantics/account_links.ts +0 -1
  144. package/src/semantics/accounts.ts +8 -7
  145. package/src/semantics/apps.ts +1 -2
  146. package/src/semantics/around.ts +3 -6
  147. package/src/semantics/balance.ts +0 -1
  148. package/src/semantics/balance_transactions.ts +1 -3
  149. package/src/semantics/billing_portal.ts +1 -7
  150. package/src/semantics/charges.ts +1 -6
  151. package/src/semantics/checkout.ts +16 -12
  152. package/src/semantics/clock.ts +1 -8
  153. package/src/semantics/coupons.ts +0 -1
  154. package/src/semantics/customers.ts +32 -35
  155. package/src/semantics/doors.ts +2 -3
  156. package/src/semantics/entitlements.ts +1 -45
  157. package/src/semantics/events.ts +7 -14
  158. package/src/semantics/index.ts +16 -38
  159. package/src/semantics/invoice_payments.ts +1 -3
  160. package/src/semantics/invoices.ts +124 -119
  161. package/src/semantics/issuing.ts +74 -0
  162. package/src/semantics/payment_intents.ts +10 -17
  163. package/src/semantics/payment_methods.ts +2 -25
  164. package/src/semantics/payouts.ts +9 -3
  165. package/src/semantics/prices.ts +0 -1
  166. package/src/semantics/products.ts +4 -14
  167. package/src/semantics/promotion_codes.ts +0 -1
  168. package/src/semantics/radar.ts +1 -3
  169. package/src/semantics/refunds.ts +2 -11
  170. package/src/semantics/shared.ts +350 -1382
  171. package/src/semantics/states.ts +109 -330
  172. package/src/semantics/subscriptions.ts +25 -145
  173. package/src/semantics/test_helpers.ts +109 -43
  174. package/src/semantics/transfers.ts +1 -3
  175. package/src/semantics/webhook_endpoints.ts +27 -0
  176. package/src/server.ts +0 -1
  177. package/dist/src/engine/billing.d.ts +0 -5
  178. package/dist/src/engine/billing.js +0 -6
  179. package/dist/src/engine/charges.d.ts +0 -6
  180. package/dist/src/engine/charges.js +0 -13
  181. package/dist/src/engine/connect-settings.d.ts +0 -16
  182. package/dist/src/engine/connect-settings.js +0 -26
  183. package/dist/src/engine/coupons.d.ts +0 -0
  184. package/dist/src/engine/coupons.js +0 -5
  185. package/dist/src/engine/credit-notes.d.ts +0 -1
  186. package/dist/src/engine/credit-notes.js +0 -5
  187. package/dist/src/engine/disputes.d.ts +0 -0
  188. package/dist/src/engine/disputes.js +0 -9
  189. package/dist/src/engine/entitlements.d.ts +0 -1
  190. package/dist/src/engine/entitlements.js +0 -4
  191. package/dist/src/engine/ephemeral-keys.d.ts +0 -0
  192. package/dist/src/engine/ephemeral-keys.js +0 -4
  193. package/dist/src/engine/front.d.ts +0 -5
  194. package/dist/src/engine/front.js +0 -25
  195. package/dist/src/engine/payment-links.d.ts +0 -1
  196. package/dist/src/engine/payment-links.js +0 -5
  197. package/dist/src/engine/plans.d.ts +0 -6
  198. package/dist/src/engine/plans.js +0 -26
  199. package/dist/src/engine/platform.d.ts +0 -39
  200. package/dist/src/engine/platform.js +0 -34
  201. package/dist/src/engine/products.d.ts +0 -0
  202. package/dist/src/engine/products.js +0 -4
  203. package/dist/src/engine/setup-intents.d.ts +0 -1
  204. package/dist/src/engine/setup-intents.js +0 -4
  205. package/dist/src/engine/subscription-schedules.d.ts +0 -3
  206. package/dist/src/engine/subscription-schedules.js +0 -1
  207. package/dist/src/engine/tax.d.ts +0 -7
  208. package/dist/src/engine/tax.js +0 -28
  209. package/dist/src/engine/terminal.d.ts +0 -4
  210. package/dist/src/engine/terminal.js +0 -20
  211. package/dist/src/engine/test-clocks.d.ts +0 -5
  212. package/dist/src/engine/test-clocks.js +0 -9
  213. package/dist/src/engine/tokens.d.ts +0 -7
  214. package/dist/src/engine/tokens.js +0 -20
  215. package/dist/src/engine/treasury.d.ts +0 -23
  216. package/dist/src/engine/treasury.js +0 -45
  217. package/dist/src/screens/connect-oauth.js +0 -333
  218. package/dist/src/semantics/disputes.d.ts +0 -2
  219. package/dist/src/semantics/disputes.js +0 -5
  220. package/dist/src/semantics/invoiceitems.d.ts +0 -3
  221. package/dist/src/semantics/invoiceitems.js +0 -59
  222. package/dist/src/semantics/setup_intents.d.ts +0 -3
  223. package/dist/src/semantics/setup_intents.js +0 -41
  224. package/dist/src/semantics/subscription_items.d.ts +0 -5
  225. package/dist/src/semantics/subscription_items.js +0 -55
  226. package/dist/src/semantics/subscription_schedules.d.ts +0 -4
  227. package/dist/src/semantics/subscription_schedules.js +0 -60
  228. package/src/engine/billing.ts +0 -16
  229. package/src/engine/charges.ts +0 -19
  230. package/src/engine/connect-settings.ts +0 -23
  231. package/src/engine/coupons.ts +0 -5
  232. package/src/engine/credit-notes.ts +0 -6
  233. package/src/engine/disputes.ts +0 -9
  234. package/src/engine/entitlements.ts +0 -5
  235. package/src/engine/ephemeral-keys.ts +0 -3
  236. package/src/engine/front.ts +0 -31
  237. package/src/engine/payment-links.ts +0 -6
  238. package/src/engine/plans.ts +0 -42
  239. package/src/engine/platform.ts +0 -47
  240. package/src/engine/products.ts +0 -3
  241. package/src/engine/setup-intents.ts +0 -5
  242. package/src/engine/subscription-schedules.ts +0 -10
  243. package/src/engine/tax.ts +0 -39
  244. package/src/engine/terminal.ts +0 -23
  245. package/src/engine/test-clocks.ts +0 -11
  246. package/src/engine/tokens.ts +0 -22
  247. package/src/engine/treasury.ts +0 -47
  248. package/src/screens/connect-oauth.tsx +0 -376
  249. package/src/semantics/disputes.ts +0 -12
  250. package/src/semantics/invoiceitems.ts +0 -59
  251. package/src/semantics/setup_intents.ts +0 -45
  252. package/src/semantics/subscription_items.ts +0 -54
  253. package/src/semantics/subscription_schedules.ts +0 -66
@@ -3,7 +3,7 @@ import { flowPage, Portal, PORTAL_CSS } from '@volter/world-ui';
3
3
  import { publicBusinessName } from "../engine/display.js";
4
4
  import { declineFor, nowUnix, paymentMethodSubObject, PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
5
5
  import { cardAnswer, COUNTRIES, formOf, money, notFound } from "./shared.js";
6
- import { afterSuccessOf, created, payerOf, payOpenInvoice } from "../semantics/shared.js";
6
+ import { resourceView, afterSuccessOf, created, payerOf, payOpenInvoice } from "../semantics/shared.js";
7
7
  const SUB = 'subscription';
8
8
  const PORTAL_OPERATION = { id: 'PostBillingPortalSessions' };
9
9
  // Stripe's skin: its type, its purple accent, the merchant's side tinted.
@@ -46,7 +46,7 @@ function planOf(ctx, sub) {
46
46
  function page(ctx, id, session, problem) {
47
47
  const features = featuresOf(ctx, session);
48
48
  const customer = ctx.get('customer', String(session.customer)) ?? {};
49
- const subs = ctx.rows(SUB).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
49
+ const subs = ctx.rows(SUB).map((s) => ctx.expand(SUB, s)).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
50
50
  const plans = subs.map((s) => {
51
51
  const plan = planOf(ctx, s);
52
52
  const ending = s.cancel_at_period_end === true;
@@ -105,7 +105,7 @@ function page(ctx, id, session, problem) {
105
105
  }
106
106
  /** The customer's move on one of their subscriptions, then back to the portal. */
107
107
  async function move(ctx, id, session, subscription, kind) {
108
- const sub = ctx.get(SUB, subscription);
108
+ const sub = resourceView(ctx, SUB, subscription);
109
109
  if (!sub || sub.customer !== session.customer || sub.status === 'canceled')
110
110
  return gone();
111
111
  const back = new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
@@ -146,7 +146,7 @@ async function updatePaymentMethod(ctx, id, session, v) {
146
146
  });
147
147
  const holder = ctx.get('customer', customer) ?? {};
148
148
  await ctx.write('customer', customer, { invoice_settings: { ...(holder.invoice_settings ?? {}), default_payment_method: pm.id } }, 'customer.update');
149
- for (const sub of ctx.rows(SUB).filter((s) => s.customer === customer && s.status !== 'canceled' && s.status !== 'incomplete_expired')) {
149
+ for (const sub of ctx.rows(SUB).map((s) => ctx.expand(SUB, s)).filter((s) => s.customer === customer && s.status !== 'canceled' && s.status !== 'incomplete_expired')) {
150
150
  await ctx.write(SUB, String(sub.id), { default_payment_method: pm.id }, 'subscription.update');
151
151
  }
152
152
  return new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
@@ -156,7 +156,7 @@ async function payInvoice(ctx, id, session, invoiceId) {
156
156
  const inv = ctx.get('invoice', invoiceId);
157
157
  if (!inv || inv.customer !== session.customer || inv.status !== 'open')
158
158
  return gone();
159
- const sub = typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
159
+ const sub = typeof inv.subscription === 'string' ? resourceView(ctx, SUB, inv.subscription) : undefined;
160
160
  const holder = ctx.get('customer', String(session.customer)) ?? {};
161
161
  const pm = (sub ? payerOf(ctx, sub) : undefined) ?? holder.invoice_settings?.default_payment_method;
162
162
  if (!pm)
@@ -1,3 +1,2 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
- /** connect.stripe.com's OAuth endpoints; Stripe's 404 for anything else there. */
3
2
  export declare function screen(ctx: HandlerContext): Promise<Response>;
@@ -0,0 +1,84 @@
1
+ import { newCustomer } from "../engine/customers.js";
2
+ import { CS } from "../engine/checkout.js";
3
+ import { applyCouponDiscount, nowUnix, resolveTrial } from "../engine/stripe.js";
4
+ import { cardAnswer, parseExpiry } from "./shared.js";
5
+ import { completeSession, discountOf, created } from "../semantics/shared.js";
6
+ function amount(value, currency) {
7
+ return { amount: new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(value / 100), minorUnitsAmount: value };
8
+ }
9
+ // source: archive:https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.0.tgz#sha256=13e61419e9076ab440cc636e34d3ea5df3fc62bbf2d6a80d3fd718e8cd47f0a2!/package/dist/stripe-js/checkout.d.ts "export interface StripeCheckoutSession"
10
+ // The SDK publishes its contract, not its private HTTP wire. This authored payment_pages wire uses the same session store and completion as hosted Checkout.
11
+ function view(ctx, session) {
12
+ const currency = String(session.currency ?? 'usd');
13
+ const lines = (session.line_items?.data ?? []);
14
+ const stored = ctx.row(CS, String(session.id)) ?? session;
15
+ const trial = session.mode === 'subscription' ? resolveTrial(stored._subscription_data ?? {}, nowUnix(ctx.occurredAt)) : undefined;
16
+ const customer = typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined;
17
+ const discount = stored._discount;
18
+ const promotion = typeof discount?.promotion_code === 'string' ? ctx.get('promotion_code', discount.promotion_code) : undefined;
19
+ const zero = amount(0, currency);
20
+ return {
21
+ id: session.id, billingAddress: null, businessName: null, canConfirm: session.status === 'open', currency,
22
+ currencyOptions: null, discountAmounts: promotion ? [{ ...amount(Number(session.total_details?.amount_discount ?? 0), currency), promotionCode: promotion.code }] : [],
23
+ email: customer?.email ?? session.customer_email ?? null, lastPaymentError: null,
24
+ lineItems: lines.map(line => ({ id: line.id, name: line.description, quantity: line.quantity, unitAmount: amount(Number(line.amount_subtotal) / Number(line.quantity || 1), currency), total: amount(Number(line.amount_total), currency) })),
25
+ livemode: false, minorUnitsAmountDivisor: 100, phoneNumber: null,
26
+ recurring: session.mode === 'subscription' ? { trial: trial ? { trialEnd: trial.end, trialPeriodDays: Math.ceil((trial.end - trial.start) / 86400) } : null } : null,
27
+ savedPaymentMethods: [], shipping: null, shippingAddress: null, shippingOptions: [],
28
+ status: session.status === 'complete' ? { type: 'complete', paymentStatus: session.payment_status } : { type: session.status },
29
+ tax: { status: 'ready' }, taxAmounts: [], taxIdInfo: null,
30
+ total: { appliedBalance: zero, balanceAppliedToNextInvoice: false, discount: amount(Number(session.total_details?.amount_discount ?? 0), currency), shippingRate: zero, subtotal: amount(Number(session.amount_subtotal ?? 0), currency), taxExclusive: zero, taxInclusive: zero, total: amount(trial ? 0 : Number(session.amount_total ?? 0), currency) },
31
+ };
32
+ }
33
+ const failed = (message, code = null) => Response.json({ type: 'error', error: { message, code } });
34
+ export async function screen(ctx) {
35
+ const path = new URL(ctx.call.request.url).pathname;
36
+ const id = path.split('/')[3];
37
+ const action = path.split('/')[4];
38
+ const params = ctx.params;
39
+ const session = ctx.get(CS, id);
40
+ // The private client secret is the browser's authority for this session; it never accepts a secret API key.
41
+ if (!session || session.ui_mode !== 'custom' || params.client_secret !== session.client_secret)
42
+ return failed('Invalid Checkout Session client secret.');
43
+ if (action === 'init')
44
+ return Response.json({ type: 'success', session: view(ctx, session) });
45
+ if (session.status !== 'open')
46
+ return failed('This Checkout Session is no longer active.');
47
+ if (action === 'apply_promotion_code' || action === 'remove_promotion_code') {
48
+ if (action === 'apply_promotion_code' && session.allow_promotion_codes !== true)
49
+ return failed('Promotion codes are not enabled for this Checkout Session.');
50
+ const promotion = action === 'apply_promotion_code' ? ctx.rows('promotion_code').find(p => p.code === params.promotion_code && p.active === true) : undefined;
51
+ if (action === 'apply_promotion_code' && !promotion)
52
+ return failed('This promotion code is invalid.', 'invalidPromotionCode');
53
+ const discount = promotion ? discountOf(ctx, { discounts: [{ promotion_code: promotion.id }] }, nowUnix(ctx.occurredAt)) : undefined;
54
+ if (discount instanceof Response)
55
+ return failed('This promotion code is invalid.', 'invalidPromotionCode');
56
+ const off = discount ? applyCouponDiscount(Number(session.amount_subtotal), discount.coupon) : 0;
57
+ const fields = { amount_total: Number(session.amount_subtotal) - off, total_details: { ...session.total_details, amount_discount: off }, discounts: discount ? [{ coupon: discount.coupon.id, promotion_code: promotion.id }] : [], _discount: discount ? { coupon: discount.coupon.id, promotion_code: promotion.id } : null };
58
+ const updated = await ctx.write(CS, id, fields, 'checkout.session.update');
59
+ return Response.json({ type: 'success', session: view(ctx, updated) });
60
+ }
61
+ if (action !== 'confirm')
62
+ return failed('Unrecognized Checkout action.');
63
+ const values = (params.payment_details ?? {});
64
+ const stored = ctx.row(CS, id) ?? session;
65
+ const trial = session.mode === 'subscription' ? resolveTrial(stored._subscription_data ?? {}, nowUnix(ctx.occurredAt)) : undefined;
66
+ const refusal = cardAnswer(values, nowUnix(ctx.occurredAt), !trial);
67
+ if (refusal)
68
+ return failed(refusal, 'paymentFailed');
69
+ const customer = typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined;
70
+ const email = String(customer?.email ?? params.email ?? session.customer_email ?? '');
71
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email))
72
+ return failed('Your email address is incomplete.');
73
+ let existing = session;
74
+ if (!customer && session.mode === 'subscription') {
75
+ const cid = ctx.mint('customer');
76
+ const made = await created(ctx, 'customer', { id: cid, email }, { livemode: false, ...newCustomer(cid) });
77
+ existing = { ...session, customer: made.id };
78
+ }
79
+ const expiry = parseExpiry(values.cardExpiry);
80
+ const completed = await completeSession(ctx, id, existing, { customer: existing.customer, customer_details: { email, name: values.billingName ?? null, address: null, phone: null, tax_exempt: 'none', tax_ids: [] } }, { number: values.cardNumber.replace(/\D/g, ''), exp_month: expiry.month, exp_year: expiry.year });
81
+ if (completed instanceof Response)
82
+ return failed('This Checkout Session is no longer active.');
83
+ return Response.json({ type: 'success', session: view(ctx, completed), return_url: session.return_url });
84
+ }
@@ -6,7 +6,6 @@ import { INDUSTRIES as MCCS } from "../engine/industries.js";
6
6
  import { currentlyDue, kindOf, OFFERED } from "../engine/onboarding.js";
7
7
  import { created, syncExternals } from "../semantics/shared.js";
8
8
  const LINK = 'account_link';
9
- const OPERATION = { id: 'PostAccountsAccount' };
10
9
  const gone = () => flowPage({ title: 'Stripe', status: 404, css: [CONSENT_CSS, STRIPE_CONSENT_SKIN], body: _jsx("main", { className: "consent", children: _jsx("h1", { className: "consent-heading", children: "This link is invalid." }) }) });
11
10
  /** A link that can still be used: not used and not past its expiry. */
12
11
  const usable = (ctx, link) => link.used !== true && Number(link.expires_at) > nowUnix(ctx.occurredAt);
@@ -81,7 +80,7 @@ async function submit(ctx, link, kind, v, ip) {
81
80
  return;
82
81
  const caps = { ...(account.capabilities ?? {}) };
83
82
  for (const [cap, status] of Object.entries(caps)) {
84
- if (status !== 'active' && !ctx.legal('capability', 'status', OPERATION.id, String(status), 'active', cap, 'vendor'))
83
+ if (status !== 'active')
85
84
  caps[cap] = 'active';
86
85
  }
87
86
  const due = currentlyDue(kind, v);
@@ -91,6 +90,7 @@ async function submit(ctx, link, kind, v, ip) {
91
90
  const person = { first_name: v.first_name, last_name: v.last_name, email: v.email, phone: v.phone, dob: { day, month, year }, address: addressOf(''), ssn_last_4_provided: true, id_number_provided: false };
92
91
  const company = kind === 'company';
93
92
  const settings = account.settings ?? {};
93
+ const individual = company ? null : await created(ctx, 'person', { ...person, account: id }, { relationship: { representative: true }, metadata: {} });
94
94
  await ctx.write('account', id, {
95
95
  details_submitted: true, charges_enabled: due.length === 0, payouts_enabled: due.length === 0, capabilities: caps, requirements,
96
96
  business_type: kind,
@@ -98,7 +98,7 @@ async function submit(ctx, link, kind, v, ip) {
98
98
  settings: { ...settings, payments: { ...(settings.payments ?? {}), statement_descriptor: v.statement_descriptor } },
99
99
  ...(company
100
100
  ? { company: { name: v.company_name, phone: v.company_phone, address: addressOf('company_'), tax_id_provided: true, owners_provided: true, directors_provided: false, executives_provided: false } }
101
- : { individual: { ...person, id: `person_${id}` } }),
101
+ : { individual: individual.id }),
102
102
  tos_acceptance: { date: nowUnix(ctx.occurredAt), ip, user_agent: null },
103
103
  }, 'account.updated');
104
104
  if (company) {
@@ -1,3 +1,3 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
- /** dashboard.stripe.com's pages: Public details and the Connect OAuth settings; Stripe's 404 for any other. */
2
+ /** Dashboard settings, Radar list creation and test Issuing funding; other pages answer Stripe's 404. */
3
3
  export declare function screen(ctx: HandlerContext): Promise<Response>;
@@ -1,42 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { flowPage } from '@volter/world-ui';
3
+ import { VL } from "../engine/radar.js";
4
+ import { nowUnix } from "../engine/stripe.js";
3
5
  import { platformAccountDefault } from "../engine/connect.js";
4
- import { CONNECT_SETTINGS_PATH, PLATFORM_CLIENT_ID, redirectUris, SETTINGS } from "../engine/connect-settings.js";
5
6
  import { publicBusinessName } from "../engine/display.js";
6
7
  import { PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
7
8
  import { formOf, notFound } from "./shared.js";
8
- import { connectOAuthSettings, created } from "../semantics/shared.js";
9
+ import { created } from "../semantics/shared.js";
10
+ const redirect = (path) => new Response(null, { status: 303, headers: { location: path } });
11
+ const invalid = (message) => flowPage({ css: [], title: 'Stripe', status: 400, body: _jsx("main", { children: _jsx("p", { role: "alert", children: message }) }) });
9
12
  const PUBLIC_DETAILS_PATH = '/settings/public';
10
13
  const MAX_NAME = 5000;
11
- const SETTINGS_CSS = `
12
- * { box-sizing: border-box; }
13
- body { margin: 0; background: #f6f8fa; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; font-size: 14px; }
14
- .co { max-width: 680px; margin: 48px auto; padding: 0 16px; }
15
- .co h1 { font-size: 28px; margin: 16px 0 4px; }
16
- .co-lead { color: #697386; margin: 0 0 24px; }
17
- .co-card { background: #ffffff; border: 1px solid #e3e8ee; border-radius: 8px; padding: 24px; margin-bottom: 16px; }
18
- .co-card h2 { font-size: 16px; margin: 0 0 12px; }
19
- .co-card label { display: block; font-weight: 600; margin-bottom: 6px; }
20
- .co-card small { display: block; color: #697386; margin-bottom: 8px; }
21
- .co-card textarea { width: 100%; min-height: 96px; padding: 8px 10px; border: 1px solid #c1c9d2; border-radius: 6px; font: 13px ui-monospace, Menlo, monospace; }
22
- .co-id { font: 14px ui-monospace, Menlo, monospace; background: #f6f8fa; border: 1px solid #e3e8ee; border-radius: 6px; padding: 6px 10px; display: inline-block; }
23
- .co-toggle { display: flex; gap: 8px; align-items: center; font-weight: 600; }
24
- .co-actions { display: flex; justify-content: flex-end; }
25
- .co-actions button { background: #635bff; color: #ffffff; border: 0; border-radius: 6px; padding: 8px 16px; font: inherit; font-weight: 600; cursor: pointer; }
26
- .co-notice { padding: 10px 12px; border-radius: 6px; margin-bottom: 16px; }
27
- .co-error { background: #fff0f3; color: #df1b41; }
28
- .co-saved { background: #e7f8ef; color: #0e6245; }
29
- .co-uris { margin: 0; padding-left: 18px; }
30
- `;
31
- function settingsPage(s, opts = {}) {
32
- const enabled = opts.enabled ?? s.oauth_enabled;
33
- return flowPage({
34
- title: 'OAuth – Connect settings – Stripe',
35
- css: [SETTINGS_CSS],
36
- ...(opts.error ? { status: 400 } : {}),
37
- body: (_jsxs("main", { className: "co", children: [_jsx("h1", { children: "OAuth" }), _jsx("p", { className: "co-lead", children: "Onboard Standard accounts by sending them to Stripe's OAuth flow. Test mode." }), opts.error ? _jsx("p", { className: "co-notice co-error", role: "alert", children: opts.error }) : null, opts.saved ? _jsx("p", { className: "co-notice co-saved", role: "status", children: "Your OAuth settings were saved." }) : null, _jsxs("section", { className: "co-card", children: [_jsx("h2", { children: "Test mode client ID" }), _jsx("small", { children: "The client_id your platform sends to connect.stripe.com/oauth/authorize." }), _jsx("code", { className: "co-id", "data-testid": "connect-client-id", children: PLATFORM_CLIENT_ID }), _jsx("p", { "data-testid": "connect-oauth-status", children: s.oauth_enabled ? 'OAuth is enabled.' : 'OAuth is disabled.' }), s.redirect_uris.length ? _jsx("ul", { className: "co-uris", children: s.redirect_uris.map((u) => _jsx("li", { "data-testid": "connect-redirect-uri", children: u }, u)) }) : _jsx("p", { children: "No redirect URIs." })] }), _jsxs("form", { className: "co-card", method: "post", children: [_jsxs("label", { className: "co-toggle", htmlFor: "oauth_enabled", children: [_jsx("input", { type: "checkbox", id: "oauth_enabled", name: "oauth_enabled", value: "on", defaultChecked: enabled }), " Enable OAuth for Standard accounts"] }), _jsx("label", { htmlFor: "redirect_uris", children: "Redirect URIs" }), _jsx("small", { children: "One per line. The first is used when a request names none. http and localhost are allowed in test mode." }), _jsx("textarea", { id: "redirect_uris", name: "redirect_uris", defaultValue: opts.value ?? s.redirect_uris.join('\n') }), _jsx("div", { className: "co-actions", children: _jsx("button", { type: "submit", children: "Save" }) })] })] })),
38
- });
39
- }
40
14
  const DETAILS_CSS = `
41
15
  * { box-sizing: border-box; }
42
16
  body { margin: 0; background: #f6f8fa; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; font-size: 14px; }
@@ -74,22 +48,6 @@ async function save(ctx, name) {
74
48
  function nameRefused(name) {
75
49
  return !name ? 'Enter your business name.' : name.length > MAX_NAME ? `Your business name must be at most ${MAX_NAME} characters.` : undefined;
76
50
  }
77
- async function settings(ctx) {
78
- const request = ctx.call.request;
79
- const url = new URL(request.url);
80
- if (request.method !== 'GET' && request.method !== 'POST')
81
- return notFound();
82
- const form = request.method === 'POST' ? await formOf(request.clone()) : {};
83
- const held = connectOAuthSettings(ctx);
84
- if (request.method === 'GET')
85
- return settingsPage(held, { saved: url.searchParams.has('saved') });
86
- const enabled = form.oauth_enabled === 'on' || form.oauth_enabled === 'true';
87
- const parsed = redirectUris(form.redirect_uris ?? '');
88
- if ('refused' in parsed)
89
- return settingsPage(held, { value: form.redirect_uris ?? '', enabled, error: parsed.refused });
90
- await ctx.record(SETTINGS, { oauth_enabled: enabled, redirect_uris: parsed.uris }, PLATFORM_CLIENT_ID);
91
- return new Response(null, { status: 303, headers: { location: '?saved=1' } });
92
- }
93
51
  async function publicDetails(ctx) {
94
52
  const request = ctx.call.request;
95
53
  const url = new URL(request.url);
@@ -106,12 +64,53 @@ async function publicDetails(ctx) {
106
64
  // a query-only reference: the page's own path, wherever it is mounted and with or without its trailing slash
107
65
  return new Response(null, { status: 303, headers: { location: '?saved=1' } });
108
66
  }
109
- /** dashboard.stripe.com's pages: Public details and the Connect OAuth settings; Stripe's 404 for any other. */
67
+ /** Dashboard settings, Radar list creation and test Issuing funding; other pages answer Stripe's 404. */
110
68
  export async function screen(ctx) {
111
69
  const path = new URL(ctx.call.request.url).pathname.replace(/\/+$/, '');
112
- if (path === PUBLIC_DETAILS_PATH)
113
- return publicDetails(ctx);
114
- if (CONNECT_SETTINGS_PATH.test(path))
115
- return settings(ctx);
116
- return notFound();
70
+ // source: https://docs.stripe.com/radar/lists "Use the Dashboard or the API to create lists."
71
+ // source: https://docs.stripe.com/radar/lists "Enter a name for the list"
72
+ // source: https://docs.stripe.com/radar/lists "Select the type of list to create."
73
+ // source: https://docs.stripe.com/radar/lists "Click Add to save your new list."
74
+ // Where the documentation stops: it names Dashboard controls, not their browser request routes or form wire.
75
+ // This authored page uses /radar/lists for GET and POST, /radar/lists/{id} for its redirect/detail address,
76
+ // and a 303 redirect after saving. The HTML field names name, alias and item_type are the twin's choices;
77
+ // item_type values are the pinned API's enums, not published Dashboard form values. The alias fallback's
78
+ // lowercase/underscore algorithm is also authored; Stripe says it generates an alias but gives no algorithm.
79
+ if (/^\/radar\/lists(?:\/[^/]+)?$/.test(path)) {
80
+ if (ctx.call.request.method === 'POST') {
81
+ const form = await formOf(ctx.call.request);
82
+ const types = ['string', 'case_sensitive_string', 'card_fingerprint', 'card_bin', 'customer_id', 'email', 'ip_address', 'country', 'sepa_debit_fingerprint', 'us_bank_account_fingerprint'];
83
+ if (!form.name?.trim() || !types.includes(form.item_type ?? ''))
84
+ return invalid('Enter a name and select a list type.');
85
+ const alias = form.alias?.trim() || form.name.toLowerCase().replace(/[^a-z0-9]+/g, '_');
86
+ // Where the page stops: an operator has no login identity in the World; created_by is its synthetic operator.
87
+ const list = await created(ctx, VL, { name: form.name.trim(), alias, item_type: form.item_type }, {
88
+ created_by: 'twin', livemode: false, metadata: {},
89
+ list_items: { object: 'list', data: [], has_more: false, url: '/v1/radar/value_list_items' },
90
+ });
91
+ await ctx.write(VL, String(list.id), { list_items: { object: 'list', data: [], has_more: false, url: `/v1/radar/value_list_items?value_list=${list.id}` } }, 'radar.value_list.updated');
92
+ return redirect(`/radar/lists/${list.id}`);
93
+ }
94
+ return flowPage({ css: [], title: 'Lists – Radar – Stripe', body: _jsxs("main", { children: [_jsx("h1", { children: "Radar Lists" }), _jsxs("form", { method: "post", action: "/radar/lists", children: [_jsx("h2", { children: "New list" }), _jsxs("label", { children: ["Name", _jsx("input", { name: "name", required: true })] }), _jsxs("label", { children: ["Alias", _jsx("input", { name: "alias" })] }), _jsxs("label", { children: ["List type", _jsxs("select", { name: "item_type", children: [_jsx("option", { value: "customer_id", children: "Customer ID" }), _jsx("option", { value: "email", children: "Email" }), _jsx("option", { value: "card_fingerprint", children: "Card fingerprint" }), _jsx("option", { value: "ip_address", children: "IP address" }), _jsx("option", { value: "string", children: "String" })] })] }), _jsx("button", { children: "Add" })] }), _jsx("ul", { children: ctx.rows(VL).map(row => _jsxs("li", { children: [String(row.name), " (", String(row.alias), ")"] }, String(row.id))) })] }) });
95
+ }
96
+ // source: https://docs.stripe.com/issuing/testing "Before you create test transactions, you must add test funds to the Issuing balance on your account."
97
+ // source: https://docs.stripe.com/issuing/testing "You can create test top-ups in the Dashboard, or with the Top-ups API"
98
+ // Where the documentation stops: it states the Dashboard funding act, not a browser request path or form wire.
99
+ // The authored GET/POST address and 303 redirect are /test/issuing/balance. The form name amount, its positive
100
+ // integer validation and its units (USD cents for this US sandbox) are the twin's choices, not published form
101
+ // fields. Its POST records the balance change in the vendor ledger, with source=null (permitted by the pinned
102
+ // schema) and immediate availability for this sandbox control; the page states neither that source nor timing.
103
+ // No private Topup object is seeded or answered, and the uncalled Top-ups API remains the gap.
104
+ if (path === '/test/issuing/balance') {
105
+ if (ctx.call.request.method === 'POST') {
106
+ const form = await formOf(ctx.call.request);
107
+ const amount = Number(form.amount);
108
+ if (!Number.isInteger(amount) || amount <= 0)
109
+ return invalid('Enter a positive amount in cents.');
110
+ await created(ctx, 'balance_transaction', {}, { amount, currency: 'usd', fee: 0, net: amount, type: 'topup', reporting_category: 'topup', source: null, status: 'available', balance_type: 'issuing', available_on: Number(nowUnix(ctx.occurredAt)), fee_details: [] });
111
+ return redirect('/test/issuing/balance');
112
+ }
113
+ return flowPage({ css: [], title: 'Test Issuing balance – Stripe', body: _jsxs("main", { children: [_jsx("h1", { children: "Test Issuing balance" }), _jsxs("form", { method: "post", children: [_jsxs("label", { children: ["Amount", _jsx("input", { name: "amount", type: "number", min: "1", required: true })] }), _jsx("button", { children: "Add test funds" })] })] }) });
114
+ }
115
+ return path === PUBLIC_DETAILS_PATH ? publicDetails(ctx) : notFound();
117
116
  }
@@ -1,115 +1,75 @@
1
1
  import { notFound } from "./shared.js";
2
2
  /** The release train a script path names: `/v3`, `/v3/` or `/v3/stripe.js` → 3, `/<train>/stripe.js` → the train. */
3
3
  function stripeJsTrain(pathname) {
4
- if (/^\/v3(\/|\/stripe\.js)?$/.test(pathname))
5
- return 3;
6
- return /^\/([a-z]+)\/stripe\.js$/.exec(pathname)?.[1];
4
+ return /^\/v3(\/|\/stripe\.js)?$/.test(pathname) ? 3 : /^\/([a-z]+)\/stripe\.js$/.exec(pathname)?.[1];
7
5
  }
6
+ // source: archive:https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.0.tgz#sha256=13e61419e9076ab440cc636e34d3ea5df3fc62bbf2d6a80d3fd718e8cd47f0a2!/package/dist/stripe-js/checkout.d.ts "loadActions: () => Promise<StripeCheckoutLoadActionsResult>"
8
7
  function script(version) {
9
8
  return `(function () {
10
9
  var version = ${JSON.stringify(version)};
11
10
  function gap(name) {
12
11
  return function () { throw new Error("Stripe.js (Volter twin): stripe." + name + " is not modelled by this twin yet"); };
13
12
  }
14
- function Stripe(publishableKey) {
15
- if (typeof publishableKey !== "string" || !publishableKey) throw new Error("Stripe.js (Volter twin): Stripe() needs a publishable key");
16
- var api = "https://api.stripe.com/v1";
17
- // the card fields every mounted element holds, the last mounted first
18
- var mounted = [];
19
- function cardOf(elements) {
20
- var el = (elements && elements._mounted && elements._mounted[elements._mounted.length - 1]) || mounted[mounted.length - 1];
21
- if (!el) return null;
22
- var exp = String(el.expiry.value || "").split("/");
23
- return { number: String(el.number.value || "").replace(/\s+/g, ""), exp_month: exp[0] ? String(Number(exp[0])) : "", exp_year: exp[1] ? String(2000 + Number(exp[1]) % 100) : "", cvc: String(el.cvc.value || "") };
24
- }
25
- function form(fields) {
26
- return Object.keys(fields).filter(function (k) { return fields[k] !== undefined && fields[k] !== ""; }).map(function (k) { return encodeURIComponent(k) + "=" + encodeURIComponent(fields[k]); }).join("&");
27
- }
28
- function confirm(kind, options) {
29
- options = options || {};
30
- var elements = options.elements;
31
- var secret = options.clientSecret || (elements && elements._options && elements._options.clientSecret);
32
- if (typeof secret !== "string" || secret.indexOf("_secret_") < 0) return Promise.resolve({ error: { type: "invalid_request_error", message: "A clientSecret is required." } });
13
+ function initCheckout(options) {
14
+ var session, root, fields = {}, listeners = {};
15
+ function emit(name, value) { (listeners[name] || []).forEach(function (fn) { fn(value); }); }
16
+ function call(action, values) {
17
+ var secret = options.clientSecret;
33
18
  var id = secret.split("_secret_")[0];
34
- var card = cardOf(elements);
35
- var params = options.confirmParams || {};
36
- var fields = { client_secret: secret };
37
- if (card) { fields["payment_method_data[type]"] = "card"; fields["payment_method_data[card][number]"] = card.number; fields["payment_method_data[card][exp_month]"] = card.exp_month; fields["payment_method_data[card][exp_year]"] = card.exp_year; fields["payment_method_data[card][cvc]"] = card.cvc; }
38
- if (params.return_url) fields.return_url = params.return_url;
39
- var path = kind === "payment" ? "/payment_intents/" : "/setup_intents/";
40
- return fetch(api + path + encodeURIComponent(id) + "/confirm", { method: "POST", headers: { authorization: "Bearer " + publishableKey, "content-type": "application/x-www-form-urlencoded" }, body: form(fields) })
41
- .then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); })
42
- .then(function (r) {
43
- if (!r.ok) return { error: r.body.error || { type: "api_error", message: "The confirmation failed." } };
44
- var key = kind === "payment" ? "paymentIntent" : "setupIntent";
45
- var out = {}; out[key] = r.body;
46
- if (options.redirect === "if_required" || !params.return_url) return out;
47
- var q = kind === "payment" ? "payment_intent" : "setup_intent";
48
- var target = new URL(params.return_url);
49
- target.searchParams.set(q, r.body.id);
50
- target.searchParams.set(q + "_client_secret", secret);
51
- target.searchParams.set("redirect_status", r.body.status === "succeeded" ? "succeeded" : r.body.status === "processing" ? "processing" : "failed");
52
- window.location.assign(target.toString());
53
- return new Promise(function () {});
54
- });
19
+ return fetch("https://api.stripe.com/v1/payment_pages/" + encodeURIComponent(id) + "/" + action, {
20
+ method: "POST", headers: { "content-type": "application/json" },
21
+ body: JSON.stringify(Object.assign({ client_secret: secret }, values || {}))
22
+ }).then(function (r) { return r.json(); }).then(function (result) {
23
+ if (result.type === "success") { session = result.session; emit("change", session); }
24
+ return result;
25
+ });
55
26
  }
56
- function element(type, group) {
57
- var handlers = {};
58
- function emit(name, event) { (handlers[name] || []).forEach(function (h) { h(event); }); }
59
- var el = {
60
- _type: type,
61
- mount: function (target) {
62
- var node = typeof target === "string" ? document.querySelector(target) : target;
63
- if (!node) throw new Error("Stripe.js (Volter twin): mount() found no element " + String(target));
64
- node.innerHTML = "";
65
- var wrap = document.createElement("div");
66
- wrap.setAttribute("data-volter-twin-element", type);
67
- function input(name, placeholder) { var i = document.createElement("input"); i.name = name; i.placeholder = placeholder; i.autocomplete = "off"; i.addEventListener("input", change); wrap.appendChild(i); return i; }
68
- el.number = input("cardnumber", "Card number");
69
- el.expiry = input("exp-date", "MM / YY");
70
- el.cvc = input("cvc", "CVC");
71
- node.appendChild(wrap);
72
- mounted.push(el); group._mounted.push(el);
73
- setTimeout(function () { emit("ready", { elementType: type }); }, 0);
74
- },
75
- on: function (name, handler) { (handlers[name] = handlers[name] || []).push(handler); return el; },
76
- off: function (name, handler) { handlers[name] = (handlers[name] || []).filter(function (h) { return h !== handler; }); return el; },
77
- update: function () {}, focus: function () { if (el.number) el.number.focus(); }, blur: function () {},
78
- clear: function () { [el.number, el.expiry, el.cvc].forEach(function (i) { if (i) i.value = ""; }); change(); },
79
- unmount: function () {}, destroy: function () { mounted = mounted.filter(function (m) { return m !== el; }); }
80
- };
81
- function change() {
82
- var c = { number: el.number && el.number.value, expiry: el.expiry && el.expiry.value, cvc: el.cvc && el.cvc.value };
83
- emit("change", { elementType: type, empty: !c.number && !c.expiry && !c.cvc, complete: !!(c.number && c.expiry && c.cvc), value: { type: "card" } });
27
+ var actions = {
28
+ getSession: function () { return session; },
29
+ applyPromotionCode: function (code) { return call("apply_promotion_code", { promotion_code: code }); },
30
+ removePromotionCode: function () { return call("remove_promotion_code"); },
31
+ confirm: function (params) {
32
+ return call("confirm", Object.assign({}, params || {}, { payment_details: fields })).then(function (result) {
33
+ if (result.type === "success" && (!params || params.redirect !== "if_required") && result.return_url) window.location.assign((params && params.returnUrl) || result.return_url);
34
+ return result;
35
+ });
84
36
  }
85
- return el;
86
- }
37
+ };
38
+ var elementListeners = {};
39
+ function elementEmit(name, value) { (elementListeners[name] || []).forEach(function (fn) { fn(value); }); }
40
+ var element = {
41
+ on: function (name, fn) { (elementListeners[name] || (elementListeners[name] = [])).push(fn); return element; },
42
+ off: function (name, fn) { elementListeners[name] = (elementListeners[name] || []).filter(function (f) { return f !== fn; }); return element; },
43
+ mount: function (target) {
44
+ root = typeof target === "string" ? document.querySelector(target) : target;
45
+ [["cardNumber", "Card number", "cc-number"], ["cardExpiry", "Expiration", "cc-exp"], ["cardCvc", "CVC", "cc-csc"], ["billingName", "Cardholder name", "cc-name"]].forEach(function (field) {
46
+ var label = document.createElement("label"), input = document.createElement("input");
47
+ label.textContent = field[1]; input.name = field[0]; input.autocomplete = field[2];
48
+ input.addEventListener("input", function () { fields[field[0]] = input.value; elementEmit("change", { empty: !fields.cardNumber, complete: !!(fields.cardNumber && fields.cardExpiry && fields.cardCvc), value: { type: "card" } }); });
49
+ label.appendChild(input); root.appendChild(label);
50
+ });
51
+ Promise.resolve().then(function () { elementEmit("ready", element); });
52
+ },
53
+ unmount: function () { if (root) root.replaceChildren(); root = null; },
54
+ destroy: function () { element.unmount(); elementListeners = {}; },
55
+ update: function () {}
56
+ };
57
+ return {
58
+ on: function (name, fn) { (listeners[name] || (listeners[name] = [])).push(fn); },
59
+ loadActions: function () { return call("init").then(function (r) { return r.type === "success" ? { type: "success", actions: actions } : r; }); },
60
+ createPaymentElement: function () { return element; }, getPaymentElement: function () { return element; },
61
+ changeAppearance: function () {}, loadFonts: function () {}
62
+ };
63
+ }
64
+ function Stripe(publishableKey) {
65
+ if (typeof publishableKey !== "string" || !publishableKey) throw new Error("Stripe.js (Volter twin): Stripe() needs a publishable key");
87
66
  var known = {
67
+ initCheckout: initCheckout,
88
68
  _registerWrapper: function () {},
89
- elements: function (options) {
90
- var group = { _options: options || {}, _mounted: [], _created: {} };
91
- group.create = function (type) { var e = element(type, group); group._created[type] = e; return e; };
92
- group.getElement = function (type) { return group._created[typeof type === "string" ? type : "card"] || null; };
93
- group.update = function (next) { group._options = Object.assign({}, group._options, next || {}); };
94
- group.fetchUpdates = function () { return Promise.resolve({}); };
95
- group.submit = function () {
96
- var card = cardOf(group);
97
- if (!card || !card.number || !card.exp_month || !card.cvc) return Promise.resolve({ error: { type: "validation_error", message: "Your card details are incomplete." } });
98
- return Promise.resolve({});
99
- };
100
- return group;
101
- },
102
- confirmPayment: function (options) { return confirm("payment", options); },
103
- confirmSetup: function (options) { return confirm("setup", options); },
104
69
  redirectToCheckout: function (options) {
105
70
  if (!options || typeof options.sessionId !== "string" || !options.sessionId) return gap("redirectToCheckout without a sessionId")();
106
71
  window.location.assign("https://checkout.stripe.com/c/pay/" + encodeURIComponent(options.sessionId));
107
72
  return new Promise(function () {});
108
- },
109
- collectFinancialConnectionsAccounts: function (options) {
110
- if (!options || typeof options.clientSecret !== "string" || !options.clientSecret) return gap("collectFinancialConnectionsAccounts without a clientSecret")();
111
- window.location.assign("https://js.stripe.com/v3/financial-connections/" + encodeURIComponent(options.clientSecret));
112
- return new Promise(function () {});
113
73
  }
114
74
  };
115
75
  return new Proxy(known, {
@@ -0,0 +1,2 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ export declare function GetAccount(ctx: HandlerContext): Promise<Response>;
@@ -0,0 +1,8 @@
1
+ import { PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
2
+ import { actingAccount, accountMissing } from "./shared.js";
3
+ // source: spec:GetAccount "Retrieves the details of an account."
4
+ export async function GetAccount(ctx) {
5
+ const id = actingAccount(ctx) ?? PLATFORM_ACCOUNT_ID;
6
+ const account = ctx.get('account', id);
7
+ return account ? ctx.reply(ctx.expand('account', account)) : accountMissing(ctx, id);
8
+ }
@@ -27,7 +27,7 @@ export async function PostAccounts(ctx) {
27
27
  tos_acceptance: { date: null, ip: null, user_agent: null },
28
28
  });
29
29
  await connectReview(ctx, String(made.id));
30
- return ctx.reply(ctx.get('account', String(made.id)));
30
+ return ctx.reply(ctx.expand('account', ctx.get('account', String(made.id))));
31
31
  }
32
32
  // a single-use Express dashboard link, not stored
33
33
  export async function PostAccountsAccountLoginLinks(ctx) {
@@ -36,3 +36,5 @@ export async function PostAccountsAccountLoginLinks(ctx) {
36
36
  return accountMissing(ctx, id);
37
37
  return ctx.reply({ object: 'login_link', created: nowUnix(ctx.occurredAt), url: `https://connect.twin.local/express/${id}` });
38
38
  }
39
+ // source: spec:PostAccountsAccount "left unchanged."
40
+ // source: https://docs.stripe.com/connect/account-capabilities "If a connected account has both card_payments and transfers"
@@ -1,16 +1,13 @@
1
- import { actingAsOAuthKey, platformKeyRefused, publishableKeyRefused, refuseParameters, rendered, revokedAccountRefused, withMetadata } from "./shared.js";
1
+ import { platformKeyRefused, publishableKeyRefused, refuseParameters, rendered, withMetadata } from "./shared.js";
2
2
  export async function around(ctx, next) {
3
3
  const arrived = ctx.call.request;
4
4
  const unkeyed = platformKeyRefused(ctx, arrived);
5
5
  if (unkeyed)
6
6
  return unkeyed;
7
- const acting = await actingAsOAuthKey(ctx, arrived.clone());
8
- if (acting instanceof Response)
9
- return acting;
10
7
  if (ctx.call.operation.id !== 'unmatched') {
11
- const refused = publishableKeyRefused(ctx) ?? revokedAccountRefused(ctx) ?? refuseParameters(ctx);
8
+ const refused = publishableKeyRefused(ctx) ?? refuseParameters(ctx);
12
9
  if (refused)
13
10
  return refused;
14
11
  }
15
- return rendered(arrived, await next(withMetadata(ctx, acting)));
12
+ return rendered(arrived, await next(withMetadata(ctx, arrived.clone())));
16
13
  }
@@ -1,5 +1,4 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
2
  export declare function GetBillingPortalConfigurations(ctx: HandlerContext): Promise<Response>;
3
3
  export declare function PostBillingPortalConfigurations(ctx: HandlerContext): Promise<Response>;
4
- export declare function PostBillingPortalConfigurationsConfiguration(ctx: HandlerContext): Promise<Response>;
5
4
  export declare function PostBillingPortalSessions(ctx: HandlerContext): Promise<Response>;
@@ -1,6 +1,6 @@
1
1
  import { filled, PBC, PORTAL_FEATURES } from "../engine/files.js";
2
2
  import { asBool, nowUnix } from "../engine/stripe.js";
3
- import { actingAccount, at, created, fail, list, mergeUpdate, newest, where } from "./shared.js";
3
+ import { actingAccount, created, fail, list, newest, where } from "./shared.js";
4
4
  export async function GetBillingPortalConfigurations(ctx) {
5
5
  return list(ctx, PBC, where(ctx, newest(ctx, PBC), {
6
6
  active: (c, v) => asBool(c.active) === asBool(v),
@@ -15,12 +15,6 @@ export async function PostBillingPortalConfigurations(ctx) {
15
15
  updated: nowUnix(ctx.occurredAt),
16
16
  }));
17
17
  }
18
- export async function PostBillingPortalConfigurationsConfiguration(ctx) {
19
- const id = at(ctx, 'configuration');
20
- if (!ctx.get(PBC, id))
21
- return ctx.notFound(PBC, id);
22
- return ctx.reply(await ctx.write(PBC, id, mergeUpdate(ctx, PBC, id, { ...ctx.params, updated: nowUnix(ctx.occurredAt) }), 'billing_portal.configuration.update'));
23
- }
24
18
  // ── the customer portal ──
25
19
  // the configuration defaults to the account's default one
26
20
  export async function PostBillingPortalSessions(ctx) {
@@ -1,3 +1,3 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
+ export declare function GetCheckoutSessionsSessionLineItems(ctx: HandlerContext): Promise<Response>;
2
3
  export declare function PostCheckoutSessions(ctx: HandlerContext): Promise<Response>;
3
- export declare function PostCheckoutSessionsSessionExpire(ctx: HandlerContext): Promise<Response>;