@volter/twin-stripe 0.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -27
- package/client/dashboard-api.ts +286 -0
- package/client/stripe-mirror.css +272 -159
- package/client/stripe-mirror.tsx +1384 -541
- package/dist/client/dashboard-api.d.ts +107 -0
- package/dist/client/dashboard-api.js +238 -0
- package/dist/client/dashboard-api.ts +286 -0
- package/dist/client/stripe-mirror.bundle.js +236 -0
- package/dist/client/stripe-mirror.css +275 -0
- package/dist/client/stripe-mirror.d.ts +134 -0
- package/dist/client/stripe-mirror.js +823 -0
- package/dist/client/stripe-mirror.tsx +1534 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +39 -0
- package/dist/src/generated/events.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +73 -0
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +1065 -0
- package/dist/src/screens/checkout.d.ts +31 -0
- package/dist/src/screens/checkout.js +241 -0
- package/dist/src/screens/consent-skin.d.ts +4 -0
- package/dist/src/screens/consent-skin.js +18 -0
- package/dist/src/screens/financial-connections.d.ts +5 -0
- package/dist/src/screens/financial-connections.js +90 -0
- package/dist/src/screens/identity.d.ts +5 -0
- package/dist/src/screens/identity.js +86 -0
- package/dist/src/screens/industries.d.ts +1 -0
- package/dist/src/screens/industries.js +267 -0
- package/dist/src/screens/onboarding.d.ts +13 -0
- package/dist/src/screens/onboarding.js +225 -0
- package/dist/src/screens/portal.d.ts +5 -0
- package/dist/src/screens/portal.js +214 -0
- package/dist/src/screens/public-details.d.ts +5 -0
- package/dist/src/screens/public-details.js +90 -0
- package/dist/src/semantics/after-payment.d.ts +22 -0
- package/dist/src/semantics/after-payment.js +93 -0
- package/dist/src/semantics/apps-secrets.d.ts +2 -0
- package/dist/src/semantics/apps-secrets.js +54 -0
- package/dist/src/semantics/balance.d.ts +11 -0
- package/dist/src/semantics/balance.js +195 -0
- package/dist/src/semantics/billing.d.ts +2 -0
- package/dist/src/semantics/billing.js +220 -0
- package/dist/src/semantics/charges.d.ts +28 -0
- package/dist/src/semantics/charges.js +201 -0
- package/dist/src/semantics/checkout.d.ts +15 -0
- package/dist/src/semantics/checkout.js +303 -0
- package/dist/src/semantics/connect.d.ts +5 -0
- package/dist/src/semantics/connect.js +476 -0
- package/dist/src/semantics/coupons.d.ts +6 -0
- package/dist/src/semantics/coupons.js +92 -0
- package/dist/src/semantics/credit-notes.d.ts +2 -0
- package/dist/src/semantics/credit-notes.js +172 -0
- package/dist/src/semantics/customers.d.ts +6 -0
- package/dist/src/semantics/customers.js +429 -0
- package/dist/src/semantics/disputes.d.ts +2 -0
- package/dist/src/semantics/disputes.js +51 -0
- package/dist/src/semantics/entitlements.d.ts +2 -0
- package/dist/src/semantics/entitlements.js +95 -0
- package/dist/src/semantics/ephemeral-keys.d.ts +2 -0
- package/dist/src/semantics/ephemeral-keys.js +34 -0
- package/dist/src/semantics/files.d.ts +2 -0
- package/dist/src/semantics/files.js +125 -0
- package/dist/src/semantics/invoices.d.ts +18 -0
- package/dist/src/semantics/invoices.js +541 -0
- package/dist/src/semantics/issuing.d.ts +13 -0
- package/dist/src/semantics/issuing.js +570 -0
- package/dist/src/semantics/ledger.d.ts +54 -0
- package/dist/src/semantics/ledger.js +181 -0
- package/dist/src/semantics/payment-intents.d.ts +18 -0
- package/dist/src/semantics/payment-intents.js +404 -0
- package/dist/src/semantics/payment-links.d.ts +2 -0
- package/dist/src/semantics/payment-links.js +133 -0
- package/dist/src/semantics/payment-methods.d.ts +20 -0
- package/dist/src/semantics/payment-methods.js +138 -0
- package/dist/src/semantics/plans.d.ts +5 -0
- package/dist/src/semantics/plans.js +121 -0
- package/dist/src/semantics/platform.d.ts +9 -0
- package/dist/src/semantics/platform.js +206 -0
- package/dist/src/semantics/products.d.ts +2 -0
- package/dist/src/semantics/products.js +140 -0
- package/dist/src/semantics/radar.d.ts +2 -0
- package/dist/src/semantics/radar.js +83 -0
- package/dist/src/semantics/refunds.d.ts +9 -0
- package/dist/src/semantics/refunds.js +195 -0
- package/dist/src/semantics/renewals.d.ts +47 -0
- package/dist/src/semantics/renewals.js +251 -0
- package/dist/src/semantics/setup-intents.d.ts +2 -0
- package/dist/src/semantics/setup-intents.js +84 -0
- package/dist/src/semantics/shared.d.ts +78 -0
- package/dist/src/semantics/shared.js +192 -0
- package/dist/src/semantics/subscription-schedules.d.ts +2 -0
- package/dist/src/semantics/subscription-schedules.js +119 -0
- package/dist/src/semantics/subscriptions.d.ts +11 -0
- package/dist/src/semantics/subscriptions.js +605 -0
- package/dist/src/semantics/tax.d.ts +2 -0
- package/dist/src/semantics/tax.js +197 -0
- package/dist/src/semantics/terminal.d.ts +5 -0
- package/dist/src/semantics/terminal.js +182 -0
- package/dist/src/semantics/test-clocks.d.ts +6 -0
- package/dist/src/semantics/test-clocks.js +73 -0
- package/dist/src/semantics/tokens.d.ts +4 -0
- package/dist/src/semantics/tokens.js +44 -0
- package/dist/src/semantics/transfers.d.ts +2 -0
- package/dist/src/semantics/transfers.js +154 -0
- package/dist/src/semantics/treasury.d.ts +2 -0
- package/dist/src/semantics/treasury.js +377 -0
- package/dist/src/semantics/webhook-endpoints.d.ts +3 -0
- package/dist/src/semantics/webhook-endpoints.js +85 -0
- package/dist/src/stripe-budget.d.ts +55 -0
- package/dist/src/stripe-budget.js +155 -0
- package/dist/src/stripe-capabilities.d.ts +3 -0
- package/dist/src/stripe-capabilities.js +5052 -0
- package/dist/src/stripe-conformance.d.ts +41 -0
- package/dist/src/stripe-conformance.js +96 -0
- package/dist/src/stripe-connector.d.ts +161 -0
- package/dist/src/stripe-connector.js +414 -0
- package/dist/src/stripe-emit.d.ts +2 -0
- package/dist/src/stripe-emit.js +145 -0
- package/dist/src/stripe-events.d.ts +93 -0
- package/dist/src/stripe-events.js +388 -0
- package/dist/src/stripe-js.d.ts +4 -0
- package/dist/src/stripe-js.js +70 -0
- package/dist/src/stripe-mirror-ui.d.ts +15 -0
- package/dist/src/stripe-mirror-ui.js +87 -0
- package/dist/src/stripe-params.d.ts +3 -0
- package/dist/src/stripe-params.js +43 -0
- package/dist/src/stripe-perform-harness.d.ts +9 -0
- package/dist/src/stripe-perform-harness.js +26 -0
- package/dist/src/stripe-server.d.ts +33 -0
- package/dist/src/stripe-server.js +326 -0
- package/dist/src/stripe-shared.d.ts +106 -0
- package/dist/src/stripe-shared.js +273 -0
- package/dist/src/stripe-twin.d.ts +155 -0
- package/dist/src/stripe-twin.js +1226 -0
- package/dist/src/stripe-ui-conformance.d.ts +5 -0
- package/dist/src/stripe-ui-conformance.js +79 -0
- package/dist/src/stripe-ui-structure.d.ts +3 -0
- package/dist/src/stripe-ui-structure.js +168 -0
- package/dist/src/stripe-version.d.ts +10 -0
- package/dist/src/stripe-version.js +285 -0
- package/dist/test-fixtures/stripe-known-deviations.json +105 -0
- package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +14 -0
- package/dist/test-fixtures/stripe-openapi-operations.json +4717 -0
- package/dist/test-fixtures/stripe-schemas.SOURCE.md +35 -0
- package/dist/test-fixtures/stripe-schemas.json +3740 -0
- package/package.json +18 -10
- package/src/cli.ts +7 -7
- package/src/generated/events.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/index.ts +31 -9
- package/src/manifest.ts +1097 -0
- package/src/screens/checkout.tsx +252 -0
- package/src/screens/consent-skin.ts +20 -0
- package/src/screens/financial-connections.tsx +101 -0
- package/src/screens/identity.tsx +96 -0
- package/src/screens/industries.ts +267 -0
- package/src/screens/onboarding.tsx +243 -0
- package/src/screens/portal.tsx +218 -0
- package/src/screens/public-details.tsx +105 -0
- package/src/semantics/after-payment.ts +113 -0
- package/src/semantics/apps-secrets.ts +58 -0
- package/src/semantics/balance.ts +209 -0
- package/src/semantics/billing.ts +216 -0
- package/src/semantics/charges.ts +211 -0
- package/src/semantics/checkout.ts +297 -0
- package/src/semantics/connect.ts +471 -0
- package/src/semantics/coupons.ts +97 -0
- package/src/semantics/credit-notes.ts +168 -0
- package/src/semantics/customers.ts +432 -0
- package/src/semantics/disputes.ts +62 -0
- package/src/semantics/entitlements.ts +94 -0
- package/src/semantics/ephemeral-keys.ts +34 -0
- package/src/semantics/files.ts +143 -0
- package/src/semantics/invoices.ts +541 -0
- package/src/semantics/issuing.ts +585 -0
- package/src/semantics/ledger.ts +216 -0
- package/src/semantics/payment-intents.ts +420 -0
- package/src/semantics/payment-links.ts +148 -0
- package/src/semantics/payment-methods.ts +143 -0
- package/src/semantics/plans.ts +131 -0
- package/src/semantics/platform.ts +220 -0
- package/src/semantics/products.ts +154 -0
- package/src/semantics/radar.ts +85 -0
- package/src/semantics/refunds.ts +218 -0
- package/src/semantics/renewals.ts +274 -0
- package/src/semantics/setup-intents.ts +87 -0
- package/src/semantics/shared.ts +215 -0
- package/src/semantics/subscription-schedules.ts +129 -0
- package/src/semantics/subscriptions.ts +610 -0
- package/src/semantics/tax.ts +220 -0
- package/src/semantics/terminal.ts +195 -0
- package/src/semantics/test-clocks.ts +77 -0
- package/src/semantics/tokens.ts +52 -0
- package/src/semantics/transfers.ts +174 -0
- package/src/semantics/treasury.ts +383 -0
- package/src/semantics/webhook-endpoints.ts +87 -0
- package/src/stripe-budget.ts +4 -4
- package/src/stripe-capabilities.ts +1456 -222
- package/src/stripe-conformance.ts +6 -5
- package/src/stripe-connector.ts +68 -40
- package/src/stripe-emit.ts +14 -7
- package/src/stripe-events.ts +94 -36
- package/src/stripe-js.ts +70 -0
- package/src/stripe-mirror-ui.ts +28 -298
- package/src/stripe-params.ts +44 -0
- package/src/stripe-perform-harness.ts +29 -0
- package/src/stripe-server.ts +263 -38
- package/src/stripe-shared.ts +294 -0
- package/src/stripe-twin.ts +429 -5325
- package/src/stripe-ui-conformance.ts +70 -107
- package/src/stripe-ui-structure.ts +124 -348
- package/src/stripe-version.ts +278 -0
- package/test-fixtures/stripe-known-deviations.json +2 -7
- package/test-fixtures/stripe-openapi-operations.json +1188 -2855
- package/src/stripe-form.ts +0 -35
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// STRIPE'S CUSTOMER PORTAL — a hosted flow (docs/contributing/architecture.md, "Screens"): an application creates
|
|
3
|
+
// a portal session for its customer and sends them to the session's `url` at billing.stripe.com/p/session/{id};
|
|
4
|
+
// there the customer sees their plans, payment method, billing details and invoices, and makes the moves the
|
|
5
|
+
// portal configuration allows, then returns to return_url (docs.stripe.com/customer-management). Canceling is the
|
|
6
|
+
// customer's own move (the subscription machine's external actor): at the period's end by default, or at once
|
|
7
|
+
// when the configuration's subscription_cancel.mode says immediately, and a plan set to cancel can be renewed.
|
|
8
|
+
// Each move goes through the same handler context the API's handlers use, so the application sees it as it
|
|
9
|
+
// would from Stripe: on the subscription, and in its events. Authored from @volter/world-ui's portal piece under
|
|
10
|
+
// Stripe's skin; nothing of Stripe's page is copied.
|
|
11
|
+
//
|
|
12
|
+
// The customer also replaces their card, which becomes the default for their invoices and subscriptions, and pays
|
|
13
|
+
// an open invoice with it, which makes a subscription waiting on it (past_due) active again
|
|
14
|
+
// (docs.stripe.com/customer-management/configure-portal: payment_method_update, invoice_history).
|
|
15
|
+
//
|
|
16
|
+
// Where the documentation stops and the twin decides: a session's configuration that the World never created
|
|
17
|
+
// (the account's default) enables cancellation at period end, updating the payment method and invoice history;
|
|
18
|
+
// changing the plan from the portal is not offered yet. A new card is saved without a charge, so a card that
|
|
19
|
+
// declines only when charged (4000000000000341) is saved and declines when the invoice is paid.
|
|
20
|
+
import { semanticsContext } from '@volter/world-core';
|
|
21
|
+
import { flowPage, Portal, PORTAL_CSS } from '@volter/world-ui';
|
|
22
|
+
import surface from '../generated/surface.gen.json' with { type: 'json' };
|
|
23
|
+
import { manifest } from "../manifest.js";
|
|
24
|
+
import { payerOf, payOpenInvoice } from "../semantics/renewals.js";
|
|
25
|
+
import { created } from "../semantics/shared.js";
|
|
26
|
+
import { publicBusinessName } from "../stripe-shared.js";
|
|
27
|
+
import { declineFor, paymentMethodSubObject, PLATFORM_ACCOUNT_ID } from "../stripe-twin.js";
|
|
28
|
+
import { cardAnswer, COUNTRIES, money } from "./checkout.js";
|
|
29
|
+
import { formOf } from "./consent-skin.js";
|
|
30
|
+
const SUB = 'subscription';
|
|
31
|
+
const PORTAL_OPERATION = surface.operations.find((o) => o.id === 'PostBillingPortalSessions');
|
|
32
|
+
// Stripe's skin: its type, its purple accent, the merchant's side tinted.
|
|
33
|
+
const SKIN = `
|
|
34
|
+
body { color: #1a1a1a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; }
|
|
35
|
+
.portal-side { background: #f7f7f9; }
|
|
36
|
+
.portal-back { color: #635bff; }
|
|
37
|
+
.portal-section h2 { color: #6b7280; border-color: #e5e7eb; }
|
|
38
|
+
.portal-item { border-bottom: 1px solid #f0f0f3; }
|
|
39
|
+
.portal-detail, .portal-note { color: #6b7280; }
|
|
40
|
+
.portal-badge { background: #eef0f4; color: #414552; }
|
|
41
|
+
.portal-button { border-color: #d5dbe1; color: #1a1a1a; }
|
|
42
|
+
.portal-primary { background: #635bff; border-color: #635bff; color: #ffffff; }
|
|
43
|
+
.portal-danger { color: #df1b41; border-color: #f4c7d0; }
|
|
44
|
+
.portal-empty { color: #6b7280; }
|
|
45
|
+
`;
|
|
46
|
+
const day = (epoch) => new Date(Number(epoch) * 1000).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', timeZone: 'UTC' });
|
|
47
|
+
const gone = () => flowPage({ title: 'Billing', status: 404, css: [PORTAL_CSS, SKIN], body: _jsx("main", { className: "portal-main", children: _jsx("h1", { children: "This link has expired." }) }) });
|
|
48
|
+
function featuresOf(ctx, session) {
|
|
49
|
+
const f = (ctx.get('billing_portal.configuration', String(session.configuration))?.features ?? undefined);
|
|
50
|
+
const cancel = (f?.subscription_cancel ?? {});
|
|
51
|
+
const invoices = (f?.invoice_history ?? {});
|
|
52
|
+
const update = (f?.payment_method_update ?? {});
|
|
53
|
+
return {
|
|
54
|
+
cancel: { enabled: f ? cancel.enabled === true : true, mode: cancel.mode === 'immediately' ? 'immediately' : 'at_period_end' },
|
|
55
|
+
invoices: f ? invoices.enabled !== false : true,
|
|
56
|
+
paymentMethod: f ? update.enabled === true : true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function planOf(ctx, sub) {
|
|
60
|
+
const item = ((sub.items?.data) ?? [])[0];
|
|
61
|
+
const price = (typeof item?.price === 'object' ? item.price : ctx.get('price', String(item?.price ?? '')));
|
|
62
|
+
const product = typeof price?.product === 'string' ? ctx.get('product', price.product) : price?.product;
|
|
63
|
+
const recurring = price?.recurring;
|
|
64
|
+
return {
|
|
65
|
+
name: String(product?.name ?? price?.nickname ?? 'Plan'),
|
|
66
|
+
price: price ? `${money(price.unit_amount, price.currency)}${recurring?.interval ? ` per ${String(recurring.interval)}` : ''}` : '',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function page(ctx, id, session, problem) {
|
|
70
|
+
const features = featuresOf(ctx, session);
|
|
71
|
+
const customer = ctx.get('customer', String(session.customer)) ?? {};
|
|
72
|
+
const subs = ctx.rows(SUB).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
|
|
73
|
+
const plans = subs.map((s) => {
|
|
74
|
+
const plan = planOf(ctx, s);
|
|
75
|
+
const ending = s.cancel_at_period_end === true;
|
|
76
|
+
return {
|
|
77
|
+
title: plan.name,
|
|
78
|
+
detail: plan.price,
|
|
79
|
+
badge: s.status === 'trialing' ? 'Trial' : s.status === 'past_due' ? 'Past due' : ending ? 'Cancels' : undefined,
|
|
80
|
+
note: ending ? `Your plan will be canceled on ${day(s.current_period_end)}.` : s.status === 'trialing' ? `Trial ends ${day(s.trial_end)}.` : `Renews on ${day(s.current_period_end)}.`,
|
|
81
|
+
actions: ending
|
|
82
|
+
? [{ label: 'Renew plan', action: `/p/session/${id}/renew`, fields: { subscription: String(s.id) }, tone: 'primary' }]
|
|
83
|
+
: features.cancel.enabled ? [{ label: 'Cancel plan', action: `/p/session/${id}/cancel`, fields: { subscription: String(s.id) }, tone: 'danger' }] : [],
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
const defaultPm = customer.invoice_settings?.default_payment_method ?? undefined;
|
|
87
|
+
const pms = ctx.rows('payment_method').filter((p) => p.customer === session.customer);
|
|
88
|
+
const subDefault = subs.map((sub) => payerOf(ctx, sub)).find((p) => typeof p === 'string');
|
|
89
|
+
const pm = (defaultPm ? pms.find((p) => p.id === defaultPm) : undefined) ?? (subDefault ? pms.find((p) => p.id === subDefault) : undefined) ?? pms[0];
|
|
90
|
+
const card = pm?.card;
|
|
91
|
+
const methods = pm ? [{ title: `${String(card?.brand ?? 'card').replace(/^./, (c) => c.toUpperCase())} •••• ${String(card?.last4 ?? '')}`, detail: card ? `Expires ${String(card.exp_month).padStart(2, '0')}/${String(card.exp_year)}` : undefined, badge: 'Default' }] : [];
|
|
92
|
+
const address = customer.address;
|
|
93
|
+
const details = [{ title: String(customer.name ?? customer.email ?? ''), detail: String(customer.email ?? ''), ...(address?.country ? { note: [address.line1, address.city, address.postal_code, address.country].filter(Boolean).join(', ') } : {}) }];
|
|
94
|
+
const invoices = features.invoices
|
|
95
|
+
? ctx.rows('invoice').filter((i) => i.customer === session.customer && i.status !== 'draft').sort((a, b) => Number(b.created) - Number(a.created))
|
|
96
|
+
.map((i) => ({
|
|
97
|
+
title: day(i.created), detail: money(i.amount_due ?? i.total, i.currency), badge: String(i.status ?? '').replace(/^./, (c) => c.toUpperCase()),
|
|
98
|
+
...(i.status === 'open' && i.collection_method !== 'send_invoice' ? { actions: [{ label: 'Pay', action: `/p/session/${id}/pay`, fields: { invoice: String(i.id) }, tone: 'primary' }] } : {}),
|
|
99
|
+
}))
|
|
100
|
+
: [];
|
|
101
|
+
const back = typeof session.return_url === 'string' ? session.return_url : undefined;
|
|
102
|
+
// the portal shows the public business name (stripe-shared.ts, publicBusinessName) of the session's on_behalf_of
|
|
103
|
+
// account ("Use the Accounts API to modify the on_behalf_of account's branding settings, which the portal displays",
|
|
104
|
+
// docs.stripe.com/api/customer_portal/sessions/object), else of the account it was made on (Stripe-Account), else the
|
|
105
|
+
// platform's
|
|
106
|
+
const owner = session.on_behalf_of ?? ctx.row('billing_portal.session', String(session.id))?._brand_account;
|
|
107
|
+
const merchant = publicBusinessName(ctx.get('account', typeof owner === 'string' ? owner : PLATFORM_ACCOUNT_ID));
|
|
108
|
+
const v = problem?.values ?? {};
|
|
109
|
+
const cardFields = [
|
|
110
|
+
{ id: 'cardNumber', label: 'Card number', autoComplete: 'cc-number', placeholder: '1234 1234 1234 1234', value: v.cardNumber ?? '' },
|
|
111
|
+
{ id: 'cardExpiry', label: 'Expiration', autoComplete: 'cc-exp', placeholder: 'MM / YY', value: v.cardExpiry ?? '' },
|
|
112
|
+
{ id: 'cardCvc', label: 'CVC', autoComplete: 'cc-csc', placeholder: 'CVC', value: v.cardCvc ?? '' },
|
|
113
|
+
{ id: 'billingName', label: 'Name on card', autoComplete: 'cc-name', value: v.billingName ?? '' },
|
|
114
|
+
{ id: 'billingCountry', label: 'Country or region', options: COUNTRIES, value: v.billingCountry ?? 'US' },
|
|
115
|
+
{ id: 'billingPostalCode', label: 'ZIP', autoComplete: 'postal-code', value: v.billingPostalCode ?? '' },
|
|
116
|
+
];
|
|
117
|
+
return flowPage({
|
|
118
|
+
title: 'Billing',
|
|
119
|
+
css: [PORTAL_CSS, SKIN],
|
|
120
|
+
...(problem ? { status: 402 } : {}),
|
|
121
|
+
body: (_jsx(Portal, { merchant: merchant, ...(problem ? { notice: problem.error } : {}), ...(features.paymentMethod ? { forms: [{ heading: pm ? 'Update payment method' : 'Add payment method', action: `/p/session/${id}/payment_method`, fields: cardFields, submit: { label: pm ? 'Update' : 'Add' } }] } : {}), ...(back ? { back: { href: back, label: `← Return to ${merchant}` } } : {}), sections: [
|
|
122
|
+
{ heading: 'Current plan', items: plans, empty: 'You have no active plans.' },
|
|
123
|
+
{ heading: 'Payment method', items: methods, empty: 'No payment method on file.' },
|
|
124
|
+
{ heading: 'Billing information', items: details },
|
|
125
|
+
...(features.invoices ? [{ heading: 'Invoice history', items: invoices, empty: 'No invoices yet.' }] : []),
|
|
126
|
+
] })),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** The customer's move on one of their subscriptions, then back to the portal. */
|
|
130
|
+
async function move(ctx, id, session, subscription, kind) {
|
|
131
|
+
const sub = ctx.get(SUB, subscription);
|
|
132
|
+
if (!sub || sub.customer !== session.customer || sub.status === 'canceled')
|
|
133
|
+
return gone();
|
|
134
|
+
const back = new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
135
|
+
if (kind === 'renew') {
|
|
136
|
+
await ctx.write(SUB, subscription, { cancel_at_period_end: false, cancel_at: null, canceled_at: null, cancellation_details: { comment: null, feedback: null, reason: null } }, 'subscription.update');
|
|
137
|
+
return back;
|
|
138
|
+
}
|
|
139
|
+
const features = featuresOf(ctx, session);
|
|
140
|
+
if (!features.cancel.enabled)
|
|
141
|
+
return gone();
|
|
142
|
+
const now = Number(ctx.now());
|
|
143
|
+
const details = { comment: null, feedback: null, reason: 'cancellation_requested' };
|
|
144
|
+
if (features.cancel.mode === 'at_period_end') {
|
|
145
|
+
await ctx.write(SUB, subscription, { cancel_at_period_end: true, cancel_at: sub.current_period_end ?? null, canceled_at: now, cancellation_details: details }, 'subscription.update');
|
|
146
|
+
return back;
|
|
147
|
+
}
|
|
148
|
+
const refused = ctx.legal(SUB, 'status', PORTAL_OPERATION.id, sub.status, 'canceled', subscription, 'external');
|
|
149
|
+
if (refused)
|
|
150
|
+
return gone();
|
|
151
|
+
await ctx.write(SUB, subscription, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details: details }, 'subscription.cancel');
|
|
152
|
+
return back;
|
|
153
|
+
}
|
|
154
|
+
/** The customer's new card: saved on them (no charge) as the default their invoices and subscriptions charge. */
|
|
155
|
+
async function updatePaymentMethod(ctx, id, session, v) {
|
|
156
|
+
if (!featuresOf(ctx, session).paymentMethod)
|
|
157
|
+
return gone();
|
|
158
|
+
const refused = cardAnswer(v, Number(ctx.now()), false);
|
|
159
|
+
if (refused)
|
|
160
|
+
return page(ctx, id, session, { error: refused, values: v });
|
|
161
|
+
const customer = String(session.customer);
|
|
162
|
+
const [month, year] = (v.cardExpiry ?? '').split('/').map((x) => Number(x.trim()));
|
|
163
|
+
const card = { number: (v.cardNumber ?? '').replace(/\D/g, ''), exp_month: month ?? 12, exp_year: (year ?? 0) < 100 ? 2000 + (year ?? 0) : (year ?? 0) };
|
|
164
|
+
const pm = await created(ctx, 'payment_method', {}, {
|
|
165
|
+
type: 'card', customer, livemode: false,
|
|
166
|
+
billing_details: { address: { country: v.billingCountry || null, postal_code: v.billingPostalCode || null, city: null, line1: null, line2: null, state: null }, email: null, name: v.billingName || null, phone: null },
|
|
167
|
+
...paymentMethodSubObject('card', { card }), _declineOutcome: declineFor(() => undefined, { card }) ?? null,
|
|
168
|
+
});
|
|
169
|
+
const holder = ctx.get('customer', customer) ?? {};
|
|
170
|
+
await ctx.write('customer', customer, { invoice_settings: { ...(holder.invoice_settings ?? {}), default_payment_method: pm.id } }, 'customer.update');
|
|
171
|
+
for (const sub of ctx.rows(SUB).filter((s) => s.customer === customer && s.status !== 'canceled' && s.status !== 'incomplete_expired')) {
|
|
172
|
+
await ctx.write(SUB, String(sub.id), { default_payment_method: pm.id }, 'subscription.update');
|
|
173
|
+
}
|
|
174
|
+
return new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
175
|
+
}
|
|
176
|
+
/** The customer pays one of their open invoices with the card on file. */
|
|
177
|
+
async function payInvoice(ctx, id, session, invoiceId) {
|
|
178
|
+
const inv = ctx.get('invoice', invoiceId);
|
|
179
|
+
if (!inv || inv.customer !== session.customer || inv.status !== 'open')
|
|
180
|
+
return gone();
|
|
181
|
+
const sub = typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
|
|
182
|
+
const holder = ctx.get('customer', String(session.customer)) ?? {};
|
|
183
|
+
const pm = (sub ? payerOf(ctx, sub) : undefined) ?? holder.invoice_settings?.default_payment_method;
|
|
184
|
+
if (!pm)
|
|
185
|
+
return page(ctx, id, session, { error: 'Add a payment method to pay this invoice.', values: {} });
|
|
186
|
+
const declined = await payOpenInvoice(ctx, invoiceId, pm);
|
|
187
|
+
if (declined)
|
|
188
|
+
return page(ctx, id, session, { error: declined.message, values: {} });
|
|
189
|
+
return new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
190
|
+
}
|
|
191
|
+
/** billing.stripe.com's portal pages, or undefined for any other request. */
|
|
192
|
+
export function stripePortalFlow(scope) {
|
|
193
|
+
return async (request) => {
|
|
194
|
+
const m = /^\/p\/session\/(bps_[A-Za-z0-9_]+)(?:\/(cancel|renew|payment_method|pay))?\/?$/.exec(new URL(request.url).pathname);
|
|
195
|
+
if (!m)
|
|
196
|
+
return undefined;
|
|
197
|
+
const [, id, kind] = m;
|
|
198
|
+
if ((kind && request.method !== 'POST') || (!kind && request.method !== 'GET'))
|
|
199
|
+
return undefined;
|
|
200
|
+
// the form is read before the context, which reads the request's body itself
|
|
201
|
+
const form = kind ? await formOf(request.clone()) : {};
|
|
202
|
+
const ctx = await semanticsContext(manifest, request, PORTAL_OPERATION, scope);
|
|
203
|
+
const session = ctx.get('billing_portal.session', id);
|
|
204
|
+
if (!session)
|
|
205
|
+
return gone();
|
|
206
|
+
if (!kind)
|
|
207
|
+
return page(ctx, id, session);
|
|
208
|
+
if (kind === 'payment_method')
|
|
209
|
+
return updatePaymentMethod(ctx, id, session, form);
|
|
210
|
+
if (kind === 'pay')
|
|
211
|
+
return payInvoice(ctx, id, session, form.invoice ?? '');
|
|
212
|
+
return move(ctx, id, session, form.subscription ?? '', kind);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// THE DASHBOARD'S PUBLIC DETAILS — a workspace page (docs/contributing/architecture.md, "Screens") of the Dashboard
|
|
3
|
+
// (manifest screen `dashboard`), at dashboard.stripe.com/settings/public: Settings → Business → Public details, where
|
|
4
|
+
// the operator sets what their customers see ("Public Details → Business Settings",
|
|
5
|
+
// support.stripe.com/questions/update-account-name-shown-in-the-dashboard). Its business name is the account's
|
|
6
|
+
// `business_profile.name`, "The customer-facing business name" (docs.stripe.com/api/accounts/object), and it is the
|
|
7
|
+
// name Checkout shows: "You can change a Checkout page's name by modifying the Business name field"
|
|
8
|
+
// (docs.stripe.com/payments/checkout/customization/appearance). Stripe's API has no call that changes the platform's
|
|
9
|
+
// own account (POST /v1/accounts/{account} updates a connected one), so this page is the only way to the act, and
|
|
10
|
+
// saving it writes the platform account's stored record (semantics/connect.ts `platform`), which GET /v1/account,
|
|
11
|
+
// Checkout and the customer portal then read. The Dashboard's account name (`settings.dashboard.display_name`,
|
|
12
|
+
// Settings → Account) is a separate, internal setting, "for internal use only" (the same support article), and this
|
|
13
|
+
// page does not touch it. Authored from plain markup under Stripe's type; nothing of Stripe's page is copied.
|
|
14
|
+
//
|
|
15
|
+
// Where the documentation stops and the twin decides: the page holds the business name only (the real page's
|
|
16
|
+
// support details, website and statement descriptor are not modelled here); the name is required and at most 5000
|
|
17
|
+
// characters (the spec's bound on business_profile.name), which the browser holds the field to and the page checks
|
|
18
|
+
// again for a client that is not a browser; no sign-in guards the page, as none guards the Dashboard mirror. The page
|
|
19
|
+
// is matched by its path on whatever host reaches the twin (the World routes only dashboard.stripe.com's
|
|
20
|
+
// /settings/public here; the local mirror serves it on its own origin), and the form posts to the page's own address
|
|
21
|
+
// and the save redirects to its own path, so it also works under the hosted mirror's path prefix. Saving is an update of an account that exists (the first save included, though
|
|
22
|
+
// the twin stores the platform's record only then), so every save is sent as account.updated.
|
|
23
|
+
import { semanticsContext } from '@volter/world-core';
|
|
24
|
+
import { flowPage } from '@volter/world-ui';
|
|
25
|
+
import surface from '../generated/surface.gen.json' with { type: 'json' };
|
|
26
|
+
import { manifest } from "../manifest.js";
|
|
27
|
+
import { platformAccountDefault } from "../semantics/connect.js";
|
|
28
|
+
import { created } from "../semantics/shared.js";
|
|
29
|
+
import { publicBusinessName } from "../stripe-shared.js";
|
|
30
|
+
import { PLATFORM_ACCOUNT_ID } from "../stripe-twin.js";
|
|
31
|
+
import { formOf } from "./consent-skin.js";
|
|
32
|
+
export const PUBLIC_DETAILS_PATH = '/settings/public';
|
|
33
|
+
const MAX_NAME = 5000;
|
|
34
|
+
const OPERATION = surface.operations.find((o) => o.id === 'PostAccountsAccount');
|
|
35
|
+
const CSS = `
|
|
36
|
+
* { box-sizing: border-box; }
|
|
37
|
+
body { margin: 0; background: #f6f8fa; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; font-size: 14px; }
|
|
38
|
+
.pd { max-width: 640px; margin: 48px auto; padding: 0 16px; }
|
|
39
|
+
.pd h1 { font-size: 28px; margin: 16px 0 4px; }
|
|
40
|
+
.pd-lead { color: #697386; margin: 0 0 24px; }
|
|
41
|
+
.pd-card { background: #ffffff; border: 1px solid #e3e8ee; border-radius: 8px; padding: 24px; }
|
|
42
|
+
.pd-card label { display: block; font-weight: 600; margin-bottom: 6px; }
|
|
43
|
+
.pd-card small { display: block; color: #697386; margin-bottom: 8px; }
|
|
44
|
+
.pd-card input { width: 100%; padding: 8px 10px; border: 1px solid #c1c9d2; border-radius: 6px; font: inherit; }
|
|
45
|
+
.pd-actions { margin-top: 20px; display: flex; justify-content: flex-end; }
|
|
46
|
+
.pd-actions button { background: #635bff; color: #ffffff; border: 0; border-radius: 6px; padding: 8px 16px; font: inherit; font-weight: 600; cursor: pointer; }
|
|
47
|
+
.pd-notice { padding: 10px 12px; border-radius: 6px; margin-bottom: 16px; }
|
|
48
|
+
.pd-error { background: #fff0f3; color: #df1b41; }
|
|
49
|
+
.pd-saved { background: #e7f8ef; color: #0e6245; }
|
|
50
|
+
`;
|
|
51
|
+
function page(account, opts = {}) {
|
|
52
|
+
const named = account?.business_profile?.name;
|
|
53
|
+
const value = opts.value ?? (typeof named === 'string' ? named : '');
|
|
54
|
+
return flowPage({
|
|
55
|
+
title: 'Public details – Stripe',
|
|
56
|
+
css: [CSS],
|
|
57
|
+
...(opts.error ? { status: 400 } : {}),
|
|
58
|
+
body: (_jsxs("main", { className: "pd", children: [_jsx("h1", { children: "Public details" }), _jsxs("p", { className: "pd-lead", children: ["What your customers see on Checkout, the customer portal, receipts and invoices. Currently shown: ", _jsx("strong", { "data-testid": "public-business-name", children: publicBusinessName(account) })] }), opts.error ? _jsx("p", { className: "pd-notice pd-error", role: "alert", children: opts.error }) : null, opts.saved ? _jsx("p", { className: "pd-notice pd-saved", role: "status", children: "Your public details were saved." }) : null, _jsxs("form", { className: "pd-card", method: "post", children: [_jsx("label", { htmlFor: "business_name", children: "Business name" }), _jsx("small", { children: "The name customers see when they pay you." }), _jsx("input", { id: "business_name", name: "business_name", autoComplete: "organization", defaultValue: value, required: true, maxLength: MAX_NAME }), _jsx("div", { className: "pd-actions", children: _jsx("button", { type: "submit", children: "Save" }) })] })] })),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Save the business name on the platform's own account: the stored record, made from Stripe's default the first
|
|
62
|
+
* time anything is saved, and otherwise changed in place (its other public details kept). */
|
|
63
|
+
async function save(ctx, name) {
|
|
64
|
+
const held = ctx.get('account', PLATFORM_ACCOUNT_ID);
|
|
65
|
+
const profile = { business_profile: { ...((held ?? platformAccountDefault()).business_profile ?? {}), name } };
|
|
66
|
+
await (held ? ctx.write('account', PLATFORM_ACCOUNT_ID, profile, 'account.update') : created(ctx, 'account', { ...platformAccountDefault(), ...profile }, {}, { operation: 'account.update' }));
|
|
67
|
+
}
|
|
68
|
+
/** What the page says of a name no browser sends (the field is required and bounded), for a client that sends it. */
|
|
69
|
+
function nameRefused(name) {
|
|
70
|
+
return !name ? 'Enter your business name.' : name.length > MAX_NAME ? `Your business name must be at most ${MAX_NAME} characters.` : undefined;
|
|
71
|
+
}
|
|
72
|
+
export function stripePublicDetailsFlow(scope) {
|
|
73
|
+
return async (request) => {
|
|
74
|
+
const url = new URL(request.url);
|
|
75
|
+
if (url.pathname.replace(/\/+$/, '') !== PUBLIC_DETAILS_PATH || (request.method !== 'GET' && request.method !== 'POST'))
|
|
76
|
+
return undefined;
|
|
77
|
+
// the form is read before the context, which reads the request's body itself
|
|
78
|
+
const form = request.method === 'POST' ? await formOf(request.clone()) : {};
|
|
79
|
+
const ctx = await semanticsContext(manifest, request, OPERATION, scope);
|
|
80
|
+
if (request.method === 'GET')
|
|
81
|
+
return page(ctx.get('account', PLATFORM_ACCOUNT_ID), { saved: url.searchParams.has('saved') });
|
|
82
|
+
const name = (form.business_name ?? '').trim();
|
|
83
|
+
const refused = nameRefused(name);
|
|
84
|
+
if (refused)
|
|
85
|
+
return page(ctx.get('account', PLATFORM_ACCOUNT_ID), { value: form.business_name ?? '', error: refused });
|
|
86
|
+
await save(ctx, name);
|
|
87
|
+
// a query-only reference: the page's own path, wherever it is mounted and with or without its trailing slash
|
|
88
|
+
return new Response(null, { status: 303, headers: { location: '?saved=1' } });
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
2
|
+
type Outcome = 'dispute' | 'dispute_not_received' | 'inquiry' | 'review';
|
|
3
|
+
/** The test cards whose success Stripe follows with its own act, by number and by their documented test names. */
|
|
4
|
+
export declare const AFTER_SUCCESS_CARDS: Record<string, {
|
|
5
|
+
brand: string;
|
|
6
|
+
number: string;
|
|
7
|
+
outcome: Outcome;
|
|
8
|
+
}>;
|
|
9
|
+
/** What Stripe does after a card succeeds: from a raw number, a test name (pm_card_* or tok_*), or a stored
|
|
10
|
+
* PaymentMethod that recorded it when it was made. */
|
|
11
|
+
export declare function afterSuccessOf(ctx: SemanticsContext, ref: unknown): Outcome | undefined;
|
|
12
|
+
/** Stripe's acts on a charge that just succeeded: a dispute or a review its card brings, and the platform's fee. */
|
|
13
|
+
export declare function afterCharge(ctx: SemanticsContext, chargeId: string, charge: {
|
|
14
|
+
amount: number;
|
|
15
|
+
currency: string;
|
|
16
|
+
payment_intent?: string | undefined;
|
|
17
|
+
application_fee_amount?: unknown;
|
|
18
|
+
destination?: unknown;
|
|
19
|
+
}, ref: unknown): Promise<void>;
|
|
20
|
+
/** The mandate a verified bank account carries for a payment (single use) or a setup (multi use). */
|
|
21
|
+
export declare function mintMandate(ctx: SemanticsContext, paymentMethod: unknown, use: 'single_use' | 'multi_use', amount?: number, currency?: string): Promise<string>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { disputeEvidence } from "../stripe-twin.js";
|
|
2
|
+
import { actingAccount, settleApplicationFee, settleDispute, settleTransfer } from "./ledger.js";
|
|
3
|
+
import { created } from "./shared.js";
|
|
4
|
+
const DAY = 86_400;
|
|
5
|
+
/** The test cards whose success Stripe follows with its own act, by number and by their documented test names. */
|
|
6
|
+
export const AFTER_SUCCESS_CARDS = {
|
|
7
|
+
pm_card_createDispute: { brand: 'visa', number: '4000000000000259', outcome: 'dispute' },
|
|
8
|
+
pm_card_createDisputeProductNotReceived: { brand: 'visa', number: '4000000000002685', outcome: 'dispute_not_received' },
|
|
9
|
+
pm_card_createDisputeInquiry: { brand: 'visa', number: '4000000000001976', outcome: 'inquiry' },
|
|
10
|
+
pm_card_riskLevelElevated: { brand: 'visa', number: '4000000000009235', outcome: 'review' },
|
|
11
|
+
};
|
|
12
|
+
const BY_NUMBER = Object.fromEntries(Object.values(AFTER_SUCCESS_CARDS).map((c) => [c.number, c.outcome]));
|
|
13
|
+
/** What Stripe does after a card succeeds: from a raw number, a test name (pm_card_* or tok_*), or a stored
|
|
14
|
+
* PaymentMethod that recorded it when it was made. */
|
|
15
|
+
export function afterSuccessOf(ctx, ref) {
|
|
16
|
+
if (ref && typeof ref === 'object')
|
|
17
|
+
return BY_NUMBER[String(ref.number ?? '').replace(/\D/g, '')];
|
|
18
|
+
if (typeof ref !== 'string' || !ref)
|
|
19
|
+
return undefined;
|
|
20
|
+
const stored = ctx.row('payment_method', ref);
|
|
21
|
+
if (stored)
|
|
22
|
+
return stored._afterSuccess ?? undefined;
|
|
23
|
+
return AFTER_SUCCESS_CARDS[ref.replace(/^tok_/, 'pm_card_')]?.outcome ?? BY_NUMBER[ref.replace(/\D/g, '')];
|
|
24
|
+
}
|
|
25
|
+
/** Stripe's acts on a charge that just succeeded: a dispute or a review its card brings, and the platform's fee. */
|
|
26
|
+
export async function afterCharge(ctx, chargeId, charge, ref) {
|
|
27
|
+
const outcome = afterSuccessOf(ctx, ref);
|
|
28
|
+
const pi = charge.payment_intent ? { payment_intent: charge.payment_intent } : { payment_intent: null };
|
|
29
|
+
const now = Number(ctx.now());
|
|
30
|
+
if (outcome === 'dispute' || outcome === 'dispute_not_received' || outcome === 'inquiry') {
|
|
31
|
+
const dispute = await created(ctx, 'dispute', { charge: chargeId, ...pi, amount: charge.amount, currency: charge.currency }, {
|
|
32
|
+
reason: outcome === 'dispute_not_received' ? 'product_not_received' : 'fraudulent', status: outcome === 'inquiry' ? 'warning_needs_response' : 'needs_response', is_charge_refundable: false,
|
|
33
|
+
evidence: disputeEvidence(), evidence_details: { due_by: now + 7 * DAY, has_evidence: false, past_due: false, submission_count: 0, enhanced_eligibility: {} },
|
|
34
|
+
balance_transactions: [], livemode: false, metadata: {}, enhanced_eligibility_types: [],
|
|
35
|
+
});
|
|
36
|
+
// an inquiry moves no money; a dispute takes the amount and its fee until it is decided
|
|
37
|
+
if (outcome !== 'inquiry') {
|
|
38
|
+
const bt = await settleDispute(ctx, String(dispute.id), charge.amount, charge.currency);
|
|
39
|
+
await ctx.write('dispute', String(dispute.id), { balance_transactions: [ctx.get('balance_transaction', bt) ?? bt] }, 'dispute.funds_withdrawn');
|
|
40
|
+
}
|
|
41
|
+
await ctx.write('charge', chargeId, { disputed: true }, 'charge.dispute.created');
|
|
42
|
+
}
|
|
43
|
+
if (outcome === 'review')
|
|
44
|
+
await openReview(ctx, chargeId, pi);
|
|
45
|
+
const feeAmount = Math.trunc(Number(charge.application_fee_amount) || 0);
|
|
46
|
+
const destination = typeof charge.destination === 'string' && charge.destination ? charge.destination : undefined;
|
|
47
|
+
const direct = actingAccount(ctx);
|
|
48
|
+
const account = destination ?? direct;
|
|
49
|
+
// a destination charge transfers what it collected, less the platform's fee, available when the charge's funds are
|
|
50
|
+
if (destination) {
|
|
51
|
+
const amount = charge.amount - feeAmount;
|
|
52
|
+
const chargeBt = ctx.get('charge', chargeId)?.balance_transaction;
|
|
53
|
+
const availableOn = Number(ctx.get('balance_transaction', String(chargeBt))?.available_on ?? ctx.now());
|
|
54
|
+
const tr = await created(ctx, 'transfer', { amount, currency: charge.currency, destination }, {
|
|
55
|
+
amount_reversed: 0, balance_transaction: null, livemode: false, metadata: {}, reversed: false, source_type: 'card', source_transaction: chargeId,
|
|
56
|
+
reversals: { object: 'list', data: [], has_more: false, total_count: 0, url: '' },
|
|
57
|
+
});
|
|
58
|
+
const bt = await settleTransfer(ctx, String(tr.id), amount, charge.currency, destination, availableOn);
|
|
59
|
+
await ctx.write('transfer', String(tr.id), { balance_transaction: bt, destination_payment: `py_${String(tr.id).replace(/^tr_/, '')}`, reversals: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/transfers/${String(tr.id)}/reversals` } }, 'transfer.created');
|
|
60
|
+
await ctx.write('charge', chargeId, { transfer: tr.id }, 'charge.updated');
|
|
61
|
+
}
|
|
62
|
+
if (feeAmount > 0 && account) {
|
|
63
|
+
const fee = await created(ctx, 'application_fee', { account, amount: feeAmount, charge: chargeId, currency: charge.currency }, {
|
|
64
|
+
amount_refunded: 0, application: 'ca_twin', balance_transaction: null, originating_transaction: null, refunded: false, livemode: false,
|
|
65
|
+
});
|
|
66
|
+
// a direct charge's fee moves from the connected account to the platform (a destination charge kept it back)
|
|
67
|
+
const bt = !destination && direct ? await settleApplicationFee(ctx, String(fee.id), feeAmount, charge.currency, direct) : null;
|
|
68
|
+
await ctx.write('application_fee', String(fee.id), { balance_transaction: bt, refunds: { object: 'list', data: [], has_more: false, total_count: 0, url: `/v1/application_fees/${String(fee.id)}/refunds` } }, 'application_fee.created');
|
|
69
|
+
await ctx.write('charge', chargeId, { application_fee: fee.id, application_fee_amount: feeAmount }, 'charge.updated');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Radar places an elevated-risk card's charge in review (docs.stripe.com/radar/reviews). */
|
|
73
|
+
async function openReview(ctx, chargeId, pi) {
|
|
74
|
+
const review = await created(ctx, 'review', { charge: chargeId, ...pi }, {
|
|
75
|
+
open: true, opened_reason: 'rule', reason: 'rule', closed_reason: null, billing_zip: null, ip_address: null, ip_address_location: null, session: null, livemode: false,
|
|
76
|
+
});
|
|
77
|
+
const existing = ctx.get('charge', chargeId)?.outcome ?? {};
|
|
78
|
+
await ctx.write('charge', chargeId, {
|
|
79
|
+
review: review.id,
|
|
80
|
+
outcome: { ...existing, type: 'manual_review', risk_level: 'elevated', risk_score: 67, seller_message: 'Stripe evaluated this payment as having elevated risk, and placed it in review.' },
|
|
81
|
+
}, 'review.opened');
|
|
82
|
+
}
|
|
83
|
+
/** The mandate a verified bank account carries for a payment (single use) or a setup (multi use). */
|
|
84
|
+
export async function mintMandate(ctx, paymentMethod, use, amount, currency) {
|
|
85
|
+
const now = Number(ctx.now());
|
|
86
|
+
const mandate = await created(ctx, 'mandate', { payment_method: typeof paymentMethod === 'string' ? paymentMethod : null }, {
|
|
87
|
+
status: 'active', type: use, livemode: false,
|
|
88
|
+
customer_acceptance: { type: 'online', accepted_at: now, online: { ip_address: '127.0.0.1', user_agent: 'twin' } },
|
|
89
|
+
payment_method_details: { type: 'us_bank_account', us_bank_account: { collection_method: 'paper' } },
|
|
90
|
+
...(use === 'single_use' ? { single_use: { amount: amount ?? 0, currency: currency ?? 'usd' } } : { multi_use: {} }),
|
|
91
|
+
}, { timeField: '_created' });
|
|
92
|
+
return String(mandate.id);
|
|
93
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { created, fail, list, newest, kept } from "./shared.js";
|
|
2
|
+
const SECRET = 'apps.secret';
|
|
3
|
+
function scopeKeyOf(ctx) {
|
|
4
|
+
const scope = ctx.params.scope && typeof ctx.params.scope === 'object' ? ctx.params.scope : undefined;
|
|
5
|
+
const scopeType = scope && typeof scope.type === 'string' ? scope.type : 'account';
|
|
6
|
+
return scopeType === 'user' ? `user:${String(scope.user)}` : 'account';
|
|
7
|
+
}
|
|
8
|
+
const named = (ctx, name) => ctx.rows(SECRET).find((s) => s.name === name && kept(ctx, SECRET, s, '_scope_key') === scopeKeyOf(ctx));
|
|
9
|
+
const set = async (ctx) => {
|
|
10
|
+
const params = ctx.params;
|
|
11
|
+
const name = typeof params.name === 'string' ? params.name : '';
|
|
12
|
+
if (!name)
|
|
13
|
+
return fail(ctx, 'Missing required param: name.', 400, 'parameter_missing');
|
|
14
|
+
const scope = params.scope && typeof params.scope === 'object' ? params.scope : undefined;
|
|
15
|
+
const scopeType = scope && typeof scope.type === 'string' ? scope.type : '';
|
|
16
|
+
if (scopeType !== 'account' && scopeType !== 'user')
|
|
17
|
+
return fail(ctx, 'Invalid scope[type]: must be account or user.', 400, 'parameter_invalid_string_enum');
|
|
18
|
+
if (scopeType === 'user' && typeof scope.user !== 'string')
|
|
19
|
+
return fail(ctx, 'Missing required param: scope[user].', 400, 'parameter_missing');
|
|
20
|
+
if (params.payload === undefined)
|
|
21
|
+
return fail(ctx, 'Missing required param: payload.', 400, 'parameter_missing');
|
|
22
|
+
const expiresAt = params.expires_at !== undefined ? Math.trunc(Number(params.expires_at)) : null;
|
|
23
|
+
const existing = named(ctx, name);
|
|
24
|
+
if (existing)
|
|
25
|
+
return ctx.reply(await ctx.write(SECRET, String(existing.id), { payload: params.payload, expires_at: expiresAt, _updated: ctx.now() }, 'apps_secret.update'));
|
|
26
|
+
return ctx.reply(await created(ctx, SECRET, { name }, {
|
|
27
|
+
livemode: false, deleted: false, expires_at: expiresAt, payload: params.payload,
|
|
28
|
+
scope: scopeType === 'user' ? { type: 'user', user: scope.user } : { type: 'account' },
|
|
29
|
+
_scope_key: scopeKeyOf(ctx),
|
|
30
|
+
}));
|
|
31
|
+
};
|
|
32
|
+
const find = async (ctx) => {
|
|
33
|
+
const name = typeof ctx.params.name === 'string' ? ctx.params.name : '';
|
|
34
|
+
if (!name)
|
|
35
|
+
return fail(ctx, 'Missing required param: name.', 400, 'parameter_missing');
|
|
36
|
+
const s = named(ctx, name);
|
|
37
|
+
return s ? ctx.reply(s) : fail(ctx, `No such secret: '${name}'`, 404, 'resource_missing');
|
|
38
|
+
};
|
|
39
|
+
const remove = async (ctx) => {
|
|
40
|
+
const name = typeof ctx.params.name === 'string' ? ctx.params.name : '';
|
|
41
|
+
if (!name)
|
|
42
|
+
return fail(ctx, 'Missing required param: name.', 400, 'parameter_missing');
|
|
43
|
+
const s = named(ctx, name);
|
|
44
|
+
if (!s)
|
|
45
|
+
return fail(ctx, `No such secret: '${name}'`, 404, 'resource_missing');
|
|
46
|
+
return ctx.reply(await ctx.write(SECRET, String(s.id), { deleted: true }, 'apps_secret.delete'));
|
|
47
|
+
};
|
|
48
|
+
const listSecrets = async (ctx) => list(ctx, SECRET, newest(ctx, SECRET).filter((s) => kept(ctx, SECRET, s, '_scope_key') === scopeKeyOf(ctx)));
|
|
49
|
+
export const appsSecrets = {
|
|
50
|
+
PostAppsSecrets: set,
|
|
51
|
+
GetAppsSecretsFind: find,
|
|
52
|
+
PostAppsSecretsDelete: remove,
|
|
53
|
+
GetAppsSecrets: listSecrets,
|
|
54
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Semantics, SemanticsContext } from '@volter/world-core';
|
|
2
|
+
import { type Row } from './shared.js';
|
|
3
|
+
/** The acting account's Balance object (docs.stripe.com/api/balance/balance_object). */
|
|
4
|
+
export declare function balanceBody(ctx: SemanticsContext): Row;
|
|
5
|
+
/** Time's arrivals, written: each of the acting account's payouts whose arrival date has come moves pending → paid
|
|
6
|
+
* (the clock's move, as asOf reads it), written as `payout.paid`, the event Stripe sends for it. Answers their ids. */
|
|
7
|
+
export declare function payDuePayouts(ctx: SemanticsContext): Promise<string[]>;
|
|
8
|
+
/** Time's payouts, caught up to the World's clock: each account on an automatic schedule is paid out, on each of its
|
|
9
|
+
* scheduled days that has come, what became available by then. */
|
|
10
|
+
export declare function advancePayouts(ctx: SemanticsContext): Promise<void>;
|
|
11
|
+
export declare const balances: Record<string, Semantics>;
|