@volter/twin-stripe 0.1.1 → 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,5 @@
|
|
|
1
|
+
import { type UiSurface, type UiCompletenessReport } from '@volter/world-tooling';
|
|
2
|
+
/** What Stripe's Dashboard shows, classified vs the twin + its Dashboard. */
|
|
3
|
+
export declare const STRIPE_UI_INVENTORY: UiSurface[];
|
|
4
|
+
/** Read-only completeness check over the declared Stripe UI inventory. */
|
|
5
|
+
export declare function stripeUiConformance(): UiCompletenessReport;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Stripe UI conformance — the declared surface inventory of Stripe's Dashboard, classified against THIS
|
|
2
|
+
// twin, and the read-only completeness check over it.
|
|
3
|
+
//
|
|
4
|
+
// Honesty: `status` is the current truth, verified by the structural cross-check in
|
|
5
|
+
// stripe-ui-conformance.test.ts (which builds the actual Dashboard bundle and asserts each 'rendered'
|
|
6
|
+
// surface's label/class is really present — downgrading to 'modeled' otherwise). `rendered` = the
|
|
7
|
+
// Dashboard shows it today; `modeled` = the twin has the data but the Dashboard doesn't show it; `unmodeled`
|
|
8
|
+
// = Stripe shows it but the twin doesn't model it.
|
|
9
|
+
//
|
|
10
|
+
// Classified against the Dashboard (client/stripe-mirror.tsx, reading through client/dashboard-api.ts):
|
|
11
|
+
// Home, Balances, Transactions > Payments and a payment's page, Customers and a customer's page, Product
|
|
12
|
+
// catalog, Billing's Subscriptions (and a subscription's page) and Invoices (and an invoice's page),
|
|
13
|
+
// Developers, Settings, and the resource lists under Connect, Payments, Reporting and More.
|
|
14
|
+
import { checkUiCompleteness } from '@volter/world-tooling';
|
|
15
|
+
/** What Stripe's Dashboard shows, classified vs the twin + its Dashboard. */
|
|
16
|
+
export const STRIPE_UI_INVENTORY = [
|
|
17
|
+
// --- shell ---
|
|
18
|
+
{ key: 'navigation', label: 'Left navigation (Home, Balances, Transactions, Customers, Product catalog, Billing, Developers)', status: 'rendered' },
|
|
19
|
+
{ key: 'testModeBand', label: 'Test-mode band', status: 'rendered' },
|
|
20
|
+
{ key: 'search', label: 'Search across customers, payments, subscriptions, invoices, products', status: 'rendered' },
|
|
21
|
+
{ key: 'statusPill', label: "Status pills in Stripe's words (Succeeded, Refunded, Failed, Active, Trialing, Paid, …)", status: 'rendered' },
|
|
22
|
+
{ key: 'amountFormatting', label: 'Currency-formatted amounts with the currency code', status: 'rendered' },
|
|
23
|
+
// --- Home and Balances ---
|
|
24
|
+
{ key: 'homeToday', label: "Home: today's gross volume, balance and payouts", status: 'rendered' },
|
|
25
|
+
{ key: 'homeOverview', label: 'Home: overview metrics (gross/net volume, successful and failed payments, customers, subscribers)', status: 'rendered' },
|
|
26
|
+
{ key: 'balanceSummary', label: 'Balances: incoming and available per currency', status: 'rendered' },
|
|
27
|
+
{ key: 'balanceTransactions', label: 'Balances: recent activity (balance transactions) and payouts', status: 'rendered' },
|
|
28
|
+
// --- Payments ---
|
|
29
|
+
{ key: 'paymentList', label: 'Payments list: amount + currency, status, payment method, description, customer, date', status: 'rendered' },
|
|
30
|
+
{ key: 'paymentFilters', label: 'Payments status tabs with counts, and filter chips', status: 'rendered' },
|
|
31
|
+
{ key: 'paymentRefundedDate', label: 'Payments list: refunded date and decline reason', status: 'rendered' },
|
|
32
|
+
{ key: 'paymentExport', label: 'Payments export', status: 'rendered' },
|
|
33
|
+
{ key: 'paymentTimeline', label: "A payment's timeline from its events", status: 'rendered' },
|
|
34
|
+
{ key: 'paymentBreakdown', label: "A payment's breakdown (amount, refunded, net)", status: 'rendered' },
|
|
35
|
+
{ key: 'paymentFees', label: "A payment's Stripe fees (balance transaction fee details)", status: 'unmodeled' },
|
|
36
|
+
{ key: 'paymentRefunds', label: "A payment's refunds", status: 'rendered' },
|
|
37
|
+
{ key: 'paymentMethodDetails', label: "A payment's card: last four, expiry, type, origin, CVC check", status: 'rendered' },
|
|
38
|
+
{ key: 'paymentDecline', label: 'A declined payment states its error and decline code', status: 'rendered' },
|
|
39
|
+
{ key: 'refundDialog', label: 'Refund a payment in full or in part, with a reason', status: 'rendered' },
|
|
40
|
+
{ key: 'disputes', label: 'Disputes', status: 'rendered' },
|
|
41
|
+
// --- Customers ---
|
|
42
|
+
{ key: 'customerList', label: 'Customers list: name, email, default payment method, created, total spend, payments, refunds', status: 'rendered' },
|
|
43
|
+
{ key: 'createCustomer', label: 'Create customer', status: 'rendered' },
|
|
44
|
+
{ key: 'customerDetails', label: "A customer's details (ID, since, email, description, billing details)", status: 'rendered' },
|
|
45
|
+
{ key: 'customerSubscriptions', label: "A customer's subscriptions", status: 'rendered' },
|
|
46
|
+
{ key: 'customerPayments', label: "A customer's payments", status: 'rendered' },
|
|
47
|
+
{ key: 'customerPaymentMethods', label: "A customer's saved payment methods, default marked", status: 'rendered' },
|
|
48
|
+
{ key: 'customerInvoices', label: "A customer's invoices", status: 'rendered' },
|
|
49
|
+
{ key: 'customerEvents', label: "A customer's events", status: 'rendered' },
|
|
50
|
+
// --- Billing ---
|
|
51
|
+
{ key: 'subscriptionList', label: 'Subscriptions list: customer, status, billing, product, amount, created', status: 'rendered' },
|
|
52
|
+
{ key: 'subscriptionDetails', label: "A subscription's details: customer, period, trial, billing method, payment method", status: 'rendered' },
|
|
53
|
+
{ key: 'subscriptionPricing', label: "A subscription's pricing items", status: 'rendered' },
|
|
54
|
+
{ key: 'cancelSubscription', label: 'Cancel a subscription now or at period end', status: 'rendered' },
|
|
55
|
+
{ key: 'invoiceList', label: 'Invoices list: amount, status, number, customer, due, created', status: 'rendered' },
|
|
56
|
+
{ key: 'invoiceDetail', label: "An invoice's summary lines and totals", status: 'rendered' },
|
|
57
|
+
{ key: 'quotes', label: 'Quotes', status: 'rendered' },
|
|
58
|
+
// --- Product catalog ---
|
|
59
|
+
{ key: 'productCatalog', label: 'Product catalog: name, pricing, created, updated', status: 'rendered' },
|
|
60
|
+
{ key: 'createProduct', label: 'Create product', status: 'rendered' },
|
|
61
|
+
{ key: 'coupons', label: 'Coupons', status: 'rendered' },
|
|
62
|
+
// --- Developers and Settings ---
|
|
63
|
+
{ key: 'apiKeys', label: 'API keys', status: 'rendered' },
|
|
64
|
+
{ key: 'webhooks', label: 'Webhook endpoints', status: 'rendered' },
|
|
65
|
+
{ key: 'eventsLog', label: 'Events', status: 'rendered' },
|
|
66
|
+
{ key: 'payoutSchedule', label: 'Settings: payout schedule', status: 'rendered' },
|
|
67
|
+
// --- under More ---
|
|
68
|
+
{ key: 'connectAccounts', label: 'Connect: connected accounts, readiness flags and requirements due', status: 'rendered' },
|
|
69
|
+
{ key: 'connectTransfers', label: 'Connect: transfers', status: 'rendered' },
|
|
70
|
+
{ key: 'radar', label: 'Radar: reviews, lists, rules', status: 'rendered' },
|
|
71
|
+
{ key: 'terminal', label: 'Terminal: readers, locations', status: 'rendered' },
|
|
72
|
+
{ key: 'issuing', label: 'Issuing: cards, cardholders', status: 'rendered' },
|
|
73
|
+
{ key: 'taxRates', label: 'Tax: tax rates', status: 'rendered' },
|
|
74
|
+
{ key: 'reports', label: 'Reporting: report runs', status: 'rendered' },
|
|
75
|
+
];
|
|
76
|
+
/** Read-only completeness check over the declared Stripe UI inventory. */
|
|
77
|
+
export function stripeUiConformance() {
|
|
78
|
+
return checkUiCompleteness('stripe', STRIPE_UI_INVENTORY);
|
|
79
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The Stripe Dashboard — structural DOM checklist (RUNG-5).
|
|
2
|
+
//
|
|
3
|
+
// Completeness (stripe-ui-conformance.ts) asks "does the Dashboard SHOW the data?". Structure asks
|
|
4
|
+
// "does its rendered DOM have the landmarks Stripe's Dashboard has?": the left navigation at Stripe's
|
|
5
|
+
// paths, the test-mode band, the Payments table with Stripe's columns and status pills, a payment's page
|
|
6
|
+
// (amount, status, timeline from its events, breakdown, refunds, card), a customer's page (details,
|
|
7
|
+
// subscriptions, payments, payment methods, invoices), a subscription's page (pricing, period, invoices,
|
|
8
|
+
// the cancel control), the balance buckets, a connected account's readiness and the resource lists.
|
|
9
|
+
//
|
|
10
|
+
// Each check's `present` is computed from the ACTUAL markup renderToStaticMarkup() produces from the
|
|
11
|
+
// Dashboard's own components (client/stripe-mirror.tsx) over objects shaped as the API returns them, so a
|
|
12
|
+
// check cannot pass unless the component truly emits that structure. Read-only; deterministic.
|
|
13
|
+
import { createElement } from 'react';
|
|
14
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
15
|
+
import { checkUiStructure } from '@volter/world-tooling';
|
|
16
|
+
import { SECTIONS, SideNav, ListPane, TestModeBand, PaymentsTable, PaymentDetailView, CustomerDetailView, SubscriptionDetailView, BalanceSummary, ConnectAccountPanel, } from "../client/stripe-mirror.js";
|
|
17
|
+
const noop = () => { };
|
|
18
|
+
const AT = 1790263345;
|
|
19
|
+
const CUSTOMER = { id: 'cus_twin001', object: 'customer', name: 'Ada Lovelace', email: 'ada@example.com', created: AT, invoice_settings: { default_payment_method: 'pm_twin001' }, metadata: {} };
|
|
20
|
+
const CARD = { id: 'pm_twin001', object: 'payment_method', type: 'card', customer: 'cus_twin001', card: { brand: 'visa', last4: '4242', exp_month: 12, exp_year: 2034, funding: 'credit', country: 'US', checks: { cvc_check: 'pass' } }, billing_details: {} };
|
|
21
|
+
const INTENT = { id: 'pi_twin001', object: 'payment_intent', amount: 30000, currency: 'usd', status: 'succeeded', customer: 'cus_twin001', payment_method: 'pm_twin001', latest_charge: 'ch_twin001', description: 'Annual workshop seat', created: AT, metadata: {} };
|
|
22
|
+
const CHARGE = { id: 'ch_twin001', object: 'charge', amount: 30000, currency: 'usd', status: 'succeeded', refunded: false, amount_refunded: 10000, payment_intent: 'pi_twin001', customer: 'cus_twin001', created: AT };
|
|
23
|
+
const REFUND = { id: 're_twin001', object: 'refund', amount: 10000, currency: 'usd', status: 'succeeded', charge: 'ch_twin001', payment_intent: 'pi_twin001', reason: 'requested_by_customer', created: AT + 60 };
|
|
24
|
+
const event = (id, type, object, created) => ({ id, object: 'event', type, created, data: { object } });
|
|
25
|
+
const PARTIAL = {
|
|
26
|
+
id: 'pi_twin001', amount: 30000, currency: 'usd', status: 'partially_refunded', description: 'Annual workshop seat', created: AT,
|
|
27
|
+
customer: CUSTOMER, charge: CHARGE, intent: INTENT, paymentMethod: CARD, amountRefunded: 10000, refundedAt: AT + 60,
|
|
28
|
+
refunds: [REFUND],
|
|
29
|
+
events: [
|
|
30
|
+
event('evt_3', 'charge.refunded', CHARGE, AT + 60),
|
|
31
|
+
event('evt_2', 'payment_intent.succeeded', INTENT, AT + 1),
|
|
32
|
+
event('evt_1', 'payment_intent.created', INTENT, AT),
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
const DECLINED_INTENT = {
|
|
36
|
+
id: 'pi_twin009', object: 'payment_intent', amount: 15000, currency: 'usd', status: 'requires_payment_method', customer: 'cus_twin001', created: AT,
|
|
37
|
+
last_payment_error: { type: 'card_error', code: 'card_declined', decline_code: 'insufficient_funds', message: 'Your card has insufficient funds.', param: 'card' },
|
|
38
|
+
};
|
|
39
|
+
const DECLINED = {
|
|
40
|
+
id: 'pi_twin009', amount: 15000, currency: 'usd', status: 'failed', created: AT, customer: CUSTOMER, intent: DECLINED_INTENT,
|
|
41
|
+
amountRefunded: 0, declineReason: 'Insufficient funds', declineMessage: 'Your card has insufficient funds.', refunds: [],
|
|
42
|
+
events: [event('evt_9', 'payment_intent.payment_failed', DECLINED_INTENT, AT)],
|
|
43
|
+
};
|
|
44
|
+
const REFUNDED = { id: 'pi_twin002', amount: 2500, currency: 'usd', status: 'refunded', description: 'Twin T-shirt', created: AT, customer: CUSTOMER, paymentMethod: CARD, amountRefunded: 2500, refundedAt: AT + 30 };
|
|
45
|
+
const PAYMENTS = [PARTIAL, DECLINED, REFUNDED, { ...REFUNDED, id: 'pi_twin003', status: 'succeeded', amountRefunded: 0, refundedAt: undefined, description: 'Consulting' }];
|
|
46
|
+
const PRODUCT = { id: 'prod_twin001', object: 'product', name: 'Pro plan' };
|
|
47
|
+
const SUBSCRIPTION = {
|
|
48
|
+
id: 'sub_twin001', object: 'subscription', status: 'trialing', customer: 'cus_twin001', currency: 'usd', created: AT, start_date: AT,
|
|
49
|
+
current_period_start: AT, current_period_end: AT + 14 * 86400, trial_start: AT, trial_end: AT + 14 * 86400, collection_method: 'charge_automatically',
|
|
50
|
+
items: { object: 'list', data: [{ id: 'si_twin001', object: 'subscription_item', quantity: 3, price: { id: 'price_twin001', product: 'prod_twin001', unit_amount: 4900, currency: 'usd', recurring: { interval: 'month' } } }] },
|
|
51
|
+
};
|
|
52
|
+
const INVOICE = { id: 'in_twin001', object: 'invoice', number: 'TWIN-0001', status: 'paid', customer: 'cus_twin001', subscription: 'sub_twin001', currency: 'usd', total: 14700, amount_due: 14700, created: AT };
|
|
53
|
+
const CUSTOMER_PAGE = { customer: CUSTOMER, paymentMethods: [CARD], subscriptions: [SUBSCRIPTION], invoices: [INVOICE], payments: PAYMENTS, products: new Map([[PRODUCT.id, PRODUCT]]), events: [] };
|
|
54
|
+
const SUBSCRIPTION_PAGE = { subscription: SUBSCRIPTION, customer: CUSTOMER, invoices: [INVOICE], products: new Map([[PRODUCT.id, PRODUCT]]), events: [event('evt_s', 'customer.subscription.created', SUBSCRIPTION, AT)], paymentMethod: CARD };
|
|
55
|
+
const BALANCE = { id: 'balance', object: 'balance', available: [{ amount: 16800, currency: 'usd' }], pending: [{ amount: 4200, currency: 'usd' }] };
|
|
56
|
+
const ACCOUNT = {
|
|
57
|
+
id: 'acct_twin001', object: 'account', type: 'express', country: 'US', charges_enabled: false, payouts_enabled: false, details_submitted: false,
|
|
58
|
+
requirements: { currently_due: ['external_account', 'tos_acceptance.date'] },
|
|
59
|
+
};
|
|
60
|
+
const TAX_RATE = { id: 'txr_twin001', object: 'tax_rate', display_name: 'Sales Tax', percentage: 8.5, inclusive: false, active: true, jurisdiction: 'US', created: AT };
|
|
61
|
+
const html = (el) => renderToStaticMarkup(el);
|
|
62
|
+
const count = (markup, needle) => (markup.match(needle) ?? []).length;
|
|
63
|
+
/** Build the structural checklist from the real rendered markup and run it. */
|
|
64
|
+
export function stripeUiStructure() {
|
|
65
|
+
const nav = html(createElement(SideNav, { route: ['payments'], go: noop }));
|
|
66
|
+
const band = html(createElement(TestModeBand));
|
|
67
|
+
const payments = html(createElement(PaymentsTable, { payments: PAYMENTS, go: noop }));
|
|
68
|
+
const partial = html(createElement(PaymentDetailView, { p: PARTIAL, go: noop, onRefund: noop }));
|
|
69
|
+
const declined = html(createElement(PaymentDetailView, { p: DECLINED, go: noop, onRefund: noop }));
|
|
70
|
+
const customer = html(createElement(CustomerDetailView, { data: CUSTOMER_PAGE, go: noop }));
|
|
71
|
+
const subscription = html(createElement(SubscriptionDetailView, { data: SUBSCRIPTION_PAGE, go: noop, onCancel: noop }));
|
|
72
|
+
const balance = html(createElement(BalanceSummary, { row: BALANCE }));
|
|
73
|
+
const account = html(createElement(ConnectAccountPanel, { row: ACCOUNT }));
|
|
74
|
+
const taxRates = html(createElement(ListPane, { section: SECTIONS.find((s) => s.key === 'tax_rates'), rows: [TAX_RATE], onSelect: noop }));
|
|
75
|
+
const checks = [
|
|
76
|
+
{
|
|
77
|
+
key: 'navigation',
|
|
78
|
+
label: "Left navigation: Home, Balances, Transactions, Customers, Product catalog, Billing's Subscriptions and Invoices, Developers — at Stripe's /test/ paths",
|
|
79
|
+
present: ['>Home<', '>Balances<', '>Transactions<', '>Customers<', '>Product catalog<', '>Subscriptions<', '>Invoices<', '>Developers<'].every((l) => nav.includes(l))
|
|
80
|
+
&& nav.includes('href="/test/payments"') && nav.includes('href="/test/customers"') && nav.includes('href="/test/subscriptions"'),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: 'navActive',
|
|
84
|
+
label: 'The navigation marks the section being viewed',
|
|
85
|
+
present: /class="nav-item [^"]*active"[^>]*href="\/test\/payments"|href="\/test\/payments" class="nav-item [^"]*active"/.test(nav),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: 'testModeBand',
|
|
89
|
+
label: "The orange test-mode band across the top",
|
|
90
|
+
present: band.includes('class="test-band"') && band.includes('Test mode'),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: 'paymentsColumns',
|
|
94
|
+
label: "Payments table carries Stripe's columns (Amount, Payment method, Description, Customer, Date, Refunded date, Decline reason)",
|
|
95
|
+
present: ['>Amount<', '>Payment method<', '>Description<', '>Customer<', '>Date<', '>Refunded date<', '>Decline reason<'].every((c) => payments.includes(c)),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: 'paymentsRowPerPayment',
|
|
99
|
+
label: 'Payments table emits one row per payment, amount with its currency',
|
|
100
|
+
present: count(payments, /class="list-row"/g) === PAYMENTS.length && payments.includes('$300.00') && payments.includes('USD'),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: 'paymentsStatusPills',
|
|
104
|
+
label: 'Status pills worded as Stripe words them (Succeeded, Refunded, Partial refund, Failed)',
|
|
105
|
+
present: ['>Succeeded<', '>Refunded<', '>Partial refund<', '>Failed<'].every((l) => payments.includes(l)),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'paymentsDeclineReason',
|
|
109
|
+
label: 'A failed payment shows its decline reason in the list',
|
|
110
|
+
present: payments.includes('Insufficient funds'),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
key: 'paymentHeader',
|
|
114
|
+
label: "A payment's page leads with its amount, currency and status, and a Refund control while refundable",
|
|
115
|
+
present: partial.includes('$300.00') && partial.includes('Partial refund') && partial.includes('Refund</button>'),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: 'paymentTimeline',
|
|
119
|
+
label: "A payment's timeline lists its events (started, succeeded, refunded)",
|
|
120
|
+
present: partial.includes('class="timeline"') && partial.includes('Payment started') && partial.includes('Payment succeeded') && partial.includes('refunded'),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: 'paymentBreakdownAndRefunds',
|
|
124
|
+
label: "A payment's breakdown (amount, refunded, net) and its refunds",
|
|
125
|
+
present: partial.includes('Payment breakdown') && partial.includes('$200.00') && partial.includes('>Refunds<') && partial.includes('re_twin001'),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: 'paymentMethodBlock',
|
|
129
|
+
label: "A payment's card: number's last four, expiry, type, CVC check",
|
|
130
|
+
present: partial.includes('4242') && partial.includes('12 / 2034') && partial.includes('Visa credit card') && partial.includes('Passed'),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: 'paymentDecline',
|
|
134
|
+
label: 'A declined payment states its error and decline code, and offers no refund',
|
|
135
|
+
present: declined.includes('class="pay-error"') && declined.includes('Your card has insufficient funds.') && declined.includes('insufficient_funds') && !declined.includes('Refund</button>'),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: 'customerPage',
|
|
139
|
+
label: "A customer's page: name, email, Details, Subscriptions, Payments, Payment methods, Invoices",
|
|
140
|
+
present: customer.includes('Ada Lovelace') && customer.includes('ada@example.com')
|
|
141
|
+
&& ['>Details<', '>Subscriptions<', '>Payments<', '>Payment methods<', '>Invoices<'].every((h) => customer.includes(h))
|
|
142
|
+
&& customer.includes('Pro plan') && customer.includes('TWIN-0001') && customer.includes('>Default<'),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'subscriptionPage',
|
|
146
|
+
label: "A subscription's page: customer on product, status, period, pricing items, invoices and Cancel subscription",
|
|
147
|
+
present: subscription.includes('Ada Lovelace') && subscription.includes('Pro plan') && subscription.includes('>Trialing<')
|
|
148
|
+
&& subscription.includes('Current period') && subscription.includes('>Pricing<') && subscription.includes('$147.00')
|
|
149
|
+
&& subscription.includes('TWIN-0001') && subscription.includes('Cancel subscription'),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
key: 'balanceSummaryBuckets',
|
|
153
|
+
label: 'The balance renders its incoming and available buckets, amounts formatted',
|
|
154
|
+
present: balance.includes('class="balance-summary"') && balance.includes('Incoming') && balance.includes('Available') && balance.includes('$168.00') && balance.includes('$42.00'),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: 'connectAccountReadiness',
|
|
158
|
+
label: "A connected account's charges/payouts/details flags and what is currently due",
|
|
159
|
+
present: account.includes('class="connect-panel"') && count(account, /connect-flag /g) >= 3 && account.includes('Requirements currently due') && account.includes('external_account'),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: 'resourceListRow',
|
|
163
|
+
label: 'A resource list emits one row per object (Tax rates: name and percentage)',
|
|
164
|
+
present: count(taxRates, /list-row/g) === 1 && taxRates.includes('Sales Tax') && taxRates.includes('8.5'),
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
return checkUiStructure('stripe', checks);
|
|
168
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The version the pack serves: its vendored spec's. */
|
|
2
|
+
export declare const SERVED_VERSION: string;
|
|
3
|
+
/** Whether a request is answered in the served version's shape. */
|
|
4
|
+
export declare const servesCurrent: (pinned?: string | null) => boolean;
|
|
5
|
+
/** The `expand[]` paths a request names, in its query or its body (JSON or form). */
|
|
6
|
+
export declare function expandOf(request: Request): Promise<string[]>;
|
|
7
|
+
/** An answer with every webhook endpoint's `secret` left out: Stripe answers it only when the endpoint is created. */
|
|
8
|
+
export declare function withoutEndpointSecret(value: unknown): unknown;
|
|
9
|
+
/** An answer (any JSON) rendered for the version a caller is served, with only the includable fields `expand` names. */
|
|
10
|
+
export declare function render(value: unknown, pinned?: string | null, expand?: string[]): unknown;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// STRIPE'S API VERSIONS — how an answer is rendered for the version a caller is served. Stripe keeps one
|
|
2
|
+
// account of an object and renders it per API version (docs.stripe.com/upgrades): a request pinning a version
|
|
3
|
+
// with `Stripe-Version` gets that version's shape, and one that pins none gets the account's. The twin keeps
|
|
4
|
+
// its objects in the shape its rules were written against (the 2024-06-20 one, with the request parameters
|
|
5
|
+
// those rules read), and renders them here: in the spec's version (the one this pack serves, surface.version)
|
|
6
|
+
// for a request that pins none or pins basil (2025-03-31) or later, and as kept for an earlier pin.
|
|
7
|
+
//
|
|
8
|
+
// Where the evidence stops: the changes below are the ones between the kept shape and the vendored spec that
|
|
9
|
+
// the pack's journeys reach, each from Stripe's changelog for basil (docs.stripe.com/changelog/basil); a version
|
|
10
|
+
// pinned between basil and the spec's is rendered in the spec's shape, not its own.
|
|
11
|
+
import surface from './generated/surface.gen.json' with { type: 'json' };
|
|
12
|
+
/** The version the pack serves: its vendored spec's. */
|
|
13
|
+
export const SERVED_VERSION = String(surface.version);
|
|
14
|
+
const BASIL = '2025-03-31';
|
|
15
|
+
/** Whether a request is answered in the served version's shape. */
|
|
16
|
+
export const servesCurrent = (pinned) => !pinned || pinned >= BASIL;
|
|
17
|
+
// What the twin keeps on an object for its rules that Stripe never answers: request parameters (a capture
|
|
18
|
+
// flag, the payment behavior a subscription was created with) and links it follows internally.
|
|
19
|
+
const KEPT = {
|
|
20
|
+
account: ['livemode'],
|
|
21
|
+
// a meter event is identified by its identifier; the twin's row id is its own
|
|
22
|
+
'billing.meter_event': ['id'],
|
|
23
|
+
// a card source's creation time is the twin's, for ordering
|
|
24
|
+
card: ['created'],
|
|
25
|
+
charge: ['capture', 'capture_before'],
|
|
26
|
+
dispute: ['submit'],
|
|
27
|
+
ephemeral_key: ['associated_objects'],
|
|
28
|
+
'financial_connections.account': ['session'],
|
|
29
|
+
'identity.verification_session': ['return_url'],
|
|
30
|
+
invoice: ['days_until_due', 'pending_invoice_items_behavior'],
|
|
31
|
+
'issuing.token': ['cardholder'],
|
|
32
|
+
payment_intent: ['mandate', 'off_session'],
|
|
33
|
+
subscription: ['payment_behavior'],
|
|
34
|
+
subscription_item: ['livemode'],
|
|
35
|
+
subscription_schedule: ['from_subscription', 'renewal_interval'],
|
|
36
|
+
'tax.transaction': ['calculation'],
|
|
37
|
+
'terminal.reader': ['registration_code'],
|
|
38
|
+
topup: ['destination_balance'],
|
|
39
|
+
'treasury.outbound_payment': ['destination_payment_method_data'],
|
|
40
|
+
'treasury.transaction_entry': ['amount'],
|
|
41
|
+
};
|
|
42
|
+
const omit = (o, keys) => Object.fromEntries(Object.entries(o).filter(([k]) => !keys.includes(k)));
|
|
43
|
+
const idOf = (v) => (typeof v === 'string' ? v : v && typeof v === 'object' && typeof v.id === 'string' ? String(v.id) : null);
|
|
44
|
+
/** A price (an id or an expanded object) as basil's pricing block names it. */
|
|
45
|
+
function pricing(price, amount, quantity) {
|
|
46
|
+
const id = idOf(price);
|
|
47
|
+
if (!id)
|
|
48
|
+
return null;
|
|
49
|
+
const product = price && typeof price === 'object' ? idOf(price.product) : null;
|
|
50
|
+
const unit = price && typeof price === 'object' && price.unit_amount !== undefined ? price.unit_amount : Number(amount) / (Number(quantity) || 1);
|
|
51
|
+
return { type: 'price_details', price_details: { price: id, product: product ?? '' }, unit_amount_decimal: String(unit ?? 0) };
|
|
52
|
+
}
|
|
53
|
+
// basil (2025-03-31): what moved, per object
|
|
54
|
+
const BASIL_CHANGES = {
|
|
55
|
+
// an invoice's subscription is its parent; its payments are the invoice's payments list (docs.stripe.com/changelog/basil/2025-03-31/add-support-for-multiple-partial-payments-on-invoices)
|
|
56
|
+
invoice: (o) => {
|
|
57
|
+
const subscription = idOf(o.subscription);
|
|
58
|
+
const parent = o.parent !== undefined ? o.parent : subscription ? { type: 'subscription_details', quote_details: null, subscription_details: { metadata: {}, subscription } } : null;
|
|
59
|
+
// "confirmation_secret … Currently, this contains the client_secret of the PaymentIntent that Stripe creates during
|
|
60
|
+
// invoice finalization" (docs.stripe.com/api/invoices/object; includable, answered only when expanded: INCLUDABLE);
|
|
61
|
+
// the intent's client_secret is the one stripe-twin.ts mintClientSecret gives it
|
|
62
|
+
const intent = typeof o.payment_intent === 'string' && o.payment_intent ? o.payment_intent : null;
|
|
63
|
+
const confirmation_secret = intent ? { type: 'payment_intent', client_secret: `${intent}_secret_twin` } : null;
|
|
64
|
+
return { ...omit(o, ['payment_intent', 'charge', 'paid', 'paid_out_of_band', 'subscription']), parent, confirmation_secret };
|
|
65
|
+
},
|
|
66
|
+
charge: (o) => omit(o, ['invoice', 'source']),
|
|
67
|
+
payment_intent: (o) => omit(o, ['invoice']),
|
|
68
|
+
// a subscription's billing period is its items' (docs.stripe.com/changelog/basil/2025-03-31/deprecate-subscription-current-period-start-and-end)
|
|
69
|
+
subscription: (o) => {
|
|
70
|
+
const period = { current_period_start: o.current_period_start, current_period_end: o.current_period_end };
|
|
71
|
+
const items = o.items && typeof o.items === 'object' ? o.items : undefined;
|
|
72
|
+
const data = Array.isArray(items?.data) ? items.data.map((it) => ({ ...it, current_period_start: it.current_period_start ?? period.current_period_start, current_period_end: it.current_period_end ?? period.current_period_end })) : undefined;
|
|
73
|
+
return { ...omit(o, ['current_period_start', 'current_period_end']), ...(items && data ? { items: { ...items, data } } : {}) };
|
|
74
|
+
},
|
|
75
|
+
subscription_item: (o) => ({ ...omit(o, ['plan']), discounts: o.discounts ?? [] }),
|
|
76
|
+
invoiceitem: (o) => ({ ...omit(o, ['price']), pricing: o.pricing ?? pricing(o.price, o.amount, o.quantity) }),
|
|
77
|
+
// a line names what generated it (its parent) and its price and taxes in basil's blocks
|
|
78
|
+
line_item: (o) => {
|
|
79
|
+
const proration = o.proration === true;
|
|
80
|
+
// a line an invoice item made names it; a subscription's line (invoice_item null) is its item's: "Details about the
|
|
81
|
+
// subscription item that generated this line item" (docs.stripe.com/api/invoice-line-item/object, parent)
|
|
82
|
+
const fromItem = o.type === 'invoiceitem' || (o.invoice_item !== undefined && o.invoice_item !== null && o.type !== 'subscription');
|
|
83
|
+
const parent = o.parent !== undefined ? o.parent : fromItem
|
|
84
|
+
? { type: 'invoice_item_details', invoice_item_details: { invoice_item: idOf(o.invoice_item) ?? String(o.id).replace(/^il_/, ''), proration, proration_details: null, subscription: idOf(o.subscription) }, subscription_item_details: null }
|
|
85
|
+
: { type: 'subscription_item_details', subscription_item_details: { subscription_item: idOf(o.subscription_item) ?? '', proration, proration_details: null, subscription: idOf(o.subscription), invoice_item: null }, invoice_item_details: null };
|
|
86
|
+
const taxes = Array.isArray(o.tax_amounts) ? o.tax_amounts.map((t) => ({ amount: t.amount, tax_behavior: 'exclusive', taxability_reason: t.taxability_reason ?? 'standard_rated', taxable_amount: t.taxable_amount ?? null, type: 'tax_rate_details', tax_rate_details: { tax_rate: idOf(t.tax_rate) } })) : [];
|
|
87
|
+
return {
|
|
88
|
+
...omit(o, ['price', 'invoice_item', 'proration', 'tax_amounts', 'tax_rates', 'type', 'subscription_item']),
|
|
89
|
+
parent, pricing: o.pricing ?? pricing(o.price, o.amount, o.quantity), taxes,
|
|
90
|
+
discountable: o.discountable ?? !proration, discounts: o.discounts ?? [], livemode: o.livemode ?? false, metadata: o.metadata ?? {},
|
|
91
|
+
period: o.period ?? { start: 0, end: 0 }, subtotal: o.subtotal ?? o.amount,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
// a promotion code promotes a coupon (docs.stripe.com/api/promotion_codes/object)
|
|
95
|
+
promotion_code: (o) => ({ ...omit(o, ['coupon']), promotion: o.promotion ?? { type: 'coupon', coupon: o.coupon ?? null } }),
|
|
96
|
+
};
|
|
97
|
+
// Stripe answers every field of an object, a nullable one it has no value for as null (the Product object's page,
|
|
98
|
+
// docs.stripe.com/api/products/object, lists `package_dimensions` as "(object, nullable)" and its example answers
|
|
99
|
+
// `"package_dimensions": null`): a nullable field of the served spec the twin never set is rendered null. The spec's
|
|
100
|
+
// top-level fields are read from the surface; a sub-object's nullable fields are listed below, each from its object's
|
|
101
|
+
// page, where a published example met one the twin left out.
|
|
102
|
+
const NULLABLE = new Map(surface.resources.map((r) => [r.schema, r.fields.filter((f) => f.nullable).map((f) => f.name)]));
|
|
103
|
+
const NESTED_NULLABLE = {
|
|
104
|
+
// docs.stripe.com/api/checkout/sessions/object?query=customer_details: business_name, individual_name "(string, nullable)"
|
|
105
|
+
'checkout.session': { customer_details: ['business_name', 'individual_name'] },
|
|
106
|
+
// docs.stripe.com/api/subscriptions/object: "billing_mode.flexible (object, nullable)", "automatic_tax.disabled_reason
|
|
107
|
+
// (enum, nullable)", invoice_settings.account_tax_ids, .custom_fields, .description, .footer each nullable
|
|
108
|
+
// (and cancellation_details.feedback_option, nullable in the served spec: "Customized feedback options that provide
|
|
109
|
+
// deeper insight into why the subscription was canceled")
|
|
110
|
+
subscription: { automatic_tax: ['disabled_reason'], billing_mode: ['flexible'], invoice_settings: ['account_tax_ids', 'custom_fields', 'description', 'footer'], cancellation_details: ['comment', 'feedback', 'feedback_option', 'reason'] },
|
|
111
|
+
// docs.stripe.com/api/payment_methods/object: billing_details.tax_id, card.fingerprint, card.generated_from,
|
|
112
|
+
// card.regulated_status each "(…, nullable)"
|
|
113
|
+
// docs.stripe.com/api/invoices/object: automatic_tax.disabled_reason "(enum, nullable)", automatic_tax.provider
|
|
114
|
+
// "(string, nullable)"
|
|
115
|
+
invoice: { automatic_tax: ['disabled_reason', 'provider'] },
|
|
116
|
+
// docs.stripe.com/api/customers/object: invoice_settings.custom_fields, .default_payment_method, .footer,
|
|
117
|
+
// .rendering_options each nullable
|
|
118
|
+
customer: { invoice_settings: ['custom_fields', 'default_payment_method', 'footer', 'rendering_options'] },
|
|
119
|
+
// docs.stripe.com/api/accounts/object: a fresh account's business_profile answers each of these null
|
|
120
|
+
account: { business_profile: ['annual_revenue', 'estimated_worker_count', 'mcc', 'minority_owned_business_designation', 'name', 'product_description', 'specified_commercial_transactions_act_url', 'support_address', 'support_email', 'support_phone', 'support_url', 'url'] },
|
|
121
|
+
// docs.stripe.com/api/charges/object: billing_details.tax_id and each of these payment_method_details.card fields
|
|
122
|
+
// "(…, nullable)"; extended_authorization, incremental_authorization, multicapture and overcapture are not nullable
|
|
123
|
+
// in the served spec, so an unmodelled one is left out rather than answered null
|
|
124
|
+
charge: {
|
|
125
|
+
billing_details: ['tax_id'],
|
|
126
|
+
'payment_method_details.card': ['amount_authorized', 'authorization_code', 'electronic_commerce_indicator', 'network_token', 'network_transaction_id', 'regulated_status', 'transaction_link_id'],
|
|
127
|
+
},
|
|
128
|
+
payment_method: { billing_details: ['tax_id'], card: ['fingerprint', 'generated_from', 'regulated_status'] },
|
|
129
|
+
// the served spec's person_relationship: legal_guardian and authorizer, each "(boolean, nullable)"
|
|
130
|
+
person: { relationship: ['legal_guardian', 'authorizer'] },
|
|
131
|
+
// the served spec's source_owner: each field "(…, nullable)"; the sources create page's example answers them null
|
|
132
|
+
source: { owner: ['address', 'email', 'name', 'phone', 'verified_address', 'verified_email', 'verified_name', 'verified_phone'] },
|
|
133
|
+
// the served spec's address_api_resource_terminal: each line "(string, nullable)"; the location fixture answers line2 null
|
|
134
|
+
'terminal.location': { address: ['city', 'country', 'line1', 'line2', 'postal_code', 'state'] },
|
|
135
|
+
// the served spec's issuing_cardholder_individual (dob, verification, card_issuing), its address and its authorization
|
|
136
|
+
// controls (allowed_card_presences, blocked_card_presences, spending_limits_currency): each "(…, nullable)"
|
|
137
|
+
'issuing.cardholder': { 'billing.address': ['city', 'country', 'line1', 'line2', 'postal_code', 'state'], individual: ['dob', 'verification', 'card_issuing'], spending_controls: ['allowed_card_presences', 'blocked_card_presences', 'spending_limits_currency'] },
|
|
138
|
+
// and a card's authorization controls (the served spec's issuing_card_authorization_controls), likewise nullable
|
|
139
|
+
'issuing.card': { spending_controls: ['allowed_card_presences', 'blocked_card_presences', 'spending_limits_currency'] },
|
|
140
|
+
// the served spec's issuing_dispute_fraudulent_evidence: additional_documentation and explanation, each nullable
|
|
141
|
+
'issuing.dispute': { 'evidence.fraudulent': ['additional_documentation', 'explanation'] },
|
|
142
|
+
// the served spec's issuing_personalization_design_carrier_text: its four texts, each nullable
|
|
143
|
+
'issuing.personalization_design': { carrier_text: ['footer_body', 'footer_title', 'header_body', 'header_title'] },
|
|
144
|
+
// the served spec's treasury_shared_resource_billing_details.address: each line "(string, nullable)"
|
|
145
|
+
'treasury.received_credit': { 'initiating_payment_method_details.billing_details.address': ['city', 'country', 'line1', 'line2', 'postal_code', 'state'], linked_flows: ['credit_reversal', 'issuing_authorization', 'issuing_transaction', 'source_flow', 'source_flow_details', 'source_flow_type'] },
|
|
146
|
+
// (and its linked flows: every one nullable in the served spec's treasury_received_debits_resource_linked_flows, and
|
|
147
|
+
// likewise the credit's)
|
|
148
|
+
'treasury.received_debit': { 'initiating_payment_method_details.billing_details.address': ['city', 'country', 'line1', 'line2', 'postal_code', 'state'], linked_flows: ['debit_reversal', 'inbound_transfer', 'issuing_authorization', 'issuing_transaction', 'payout', 'topup'] },
|
|
149
|
+
'treasury.inbound_transfer': { 'origin_payment_method_details.billing_details.address': ['city', 'country', 'line1', 'line2', 'postal_code', 'state'] },
|
|
150
|
+
};
|
|
151
|
+
// A field the vendor fills with its default when the request set none, by object, each from its object's page.
|
|
152
|
+
const CARD_DISPLAY = { amex: 'american_express', diners: 'diners_club', eftpos_au: 'eftpos_australia', unionpay: 'union_pay' };
|
|
153
|
+
const DEFAULTS = {
|
|
154
|
+
// docs.stripe.com/api/payment_methods/object: allow_redisplay "defaults to “unspecified”"; card.display_brand is "The
|
|
155
|
+
// brand to use when displaying the card … Can be `american_express`, …, `visa`", the brand's display name
|
|
156
|
+
payment_method: (o) => {
|
|
157
|
+
const card = o.card && typeof o.card === 'object' ? o.card : undefined;
|
|
158
|
+
const brand = typeof card?.brand === 'string' ? card.brand : undefined;
|
|
159
|
+
return {
|
|
160
|
+
...(o.allow_redisplay === undefined ? { allow_redisplay: 'unspecified' } : {}),
|
|
161
|
+
...(card && brand && card.display_brand === undefined ? { card: { ...card, display_brand: CARD_DISPLAY[brand] ?? (brand === 'unknown' ? 'other' : brand) } } : {}),
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
// a line names "The ID of the invoice that contains this line item" (docs.stripe.com/api/invoice-line-item/object)
|
|
165
|
+
invoice: (o) => {
|
|
166
|
+
const lines = o.lines && typeof o.lines === 'object' ? o.lines : undefined;
|
|
167
|
+
if (!Array.isArray(lines?.data) || typeof o.id !== 'string')
|
|
168
|
+
return {};
|
|
169
|
+
return { lines: { ...lines, data: lines.data.map((l) => (l && typeof l === 'object' && (l.invoice === undefined || l.invoice === null) ? { ...l, invoice: o.id } : l)) } };
|
|
170
|
+
},
|
|
171
|
+
// docs.stripe.com/api/invoiceitems/object: net_amount, "The amount after discounts, but before credits and taxes. This
|
|
172
|
+
// field is `null` for `discountable=true` items" (the twin puts no discount on an item itself)
|
|
173
|
+
invoiceitem: (o) => (o.net_amount === undefined ? { net_amount: o.discountable === false ? (typeof o.amount === 'number' ? o.amount : null) : null } : {}),
|
|
174
|
+
// docs.stripe.com/api/payment_intents/object: confirmation_method `automatic` "(Default)"; amount_details as its
|
|
175
|
+
// example answers it for a card payment, `{"tip": {}}`
|
|
176
|
+
payment_intent: (o) => ({
|
|
177
|
+
...(o.confirmation_method === undefined ? { confirmation_method: 'automatic' } : {}),
|
|
178
|
+
...(o.amount_details === undefined ? { amount_details: { tip: {} } } : {}),
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
const fill = (o, keys) => (keys.some((k) => !(k in o)) ? { ...o, ...Object.fromEntries(keys.filter((k) => !(k in o)).map((k) => [k, null])) } : o);
|
|
182
|
+
// every object whose served spec gives it a `metadata` it never answers null answers `{}` when none was set: "Set of
|
|
183
|
+
// key-value pairs that you can attach to an object" (docs.stripe.com/api/metadata), `metadata (map)` on each object's
|
|
184
|
+
// page, and its example `"metadata": {}` (the PaymentIntent object's, docs.stripe.com/api/payment_intents/object)
|
|
185
|
+
const METADATA = new Set(surface.resources.filter((r) => r.fields.some((f) => f.name === 'metadata' && !f.nullable)).map((r) => r.schema));
|
|
186
|
+
const withNulls = (o, kind) => {
|
|
187
|
+
let out = fill(DEFAULTS[kind] ? { ...o, ...DEFAULTS[kind](o) } : o, NULLABLE.get(kind) ?? []);
|
|
188
|
+
if (METADATA.has(kind) && (out.metadata === undefined || out.metadata === null))
|
|
189
|
+
out = { ...out, metadata: {} };
|
|
190
|
+
// a dotted path reaches a sub-object's own sub-object (a charge's payment_method_details.card)
|
|
191
|
+
const at = (o, path, keys) => {
|
|
192
|
+
const [head, ...rest] = path;
|
|
193
|
+
const sub = o[head];
|
|
194
|
+
if (!sub || typeof sub !== 'object' || Array.isArray(sub))
|
|
195
|
+
return o;
|
|
196
|
+
return { ...o, [head]: rest.length ? at(sub, rest, keys) : fill(sub, keys) };
|
|
197
|
+
};
|
|
198
|
+
for (const [path, keys] of Object.entries(NESTED_NULLABLE[kind] ?? {}))
|
|
199
|
+
out = at(out, path.split('.'), keys);
|
|
200
|
+
return out;
|
|
201
|
+
};
|
|
202
|
+
// A field Stripe includes only when the request expands it: the Checkout Session object's page
|
|
203
|
+
// (docs.stripe.com/api/checkout/sessions/object) marks `line_items` "includable (not returned by default; request it
|
|
204
|
+
// with the `expand` request parameter)". The twin keeps it on the object for its rules; the answer carries it only
|
|
205
|
+
// where the request's `expand[]` names it (`line_items`, or `data.line_items` on a list).
|
|
206
|
+
const INCLUDABLE = {
|
|
207
|
+
'checkout.session': ['line_items'],
|
|
208
|
+
// docs.stripe.com/api/charges/object: `refunds` "(object, nullable, includable (not returned by default; …))"
|
|
209
|
+
charge: ['refunds'],
|
|
210
|
+
// docs.stripe.com/api/payment-link/object: `line_items` "object Includable", and its example answers none
|
|
211
|
+
payment_link: ['line_items'],
|
|
212
|
+
// docs.stripe.com/api/quotes/object: `line_items` "object Includable", and its example answers none
|
|
213
|
+
quote: ['line_items'],
|
|
214
|
+
// docs.stripe.com/api/secret_management: `payload` "nullable string Includable": a secret's value is answered only
|
|
215
|
+
// when the request expands it
|
|
216
|
+
'apps.secret': ['payload'],
|
|
217
|
+
// docs.stripe.com/api/tax/calculations/object and /tax/transactions/object: `line_items` "nullable object Includable"
|
|
218
|
+
'tax.calculation': ['line_items'],
|
|
219
|
+
'tax.transaction': ['line_items'],
|
|
220
|
+
// docs.stripe.com/api/invoices/object: `confirmation_secret` "(object, nullable, includable (not returned by default;
|
|
221
|
+
// request it with the `expand` request parameter))"
|
|
222
|
+
invoice: ['confirmation_secret'],
|
|
223
|
+
};
|
|
224
|
+
/** The `expand[]` paths a request names, in its query or its body (JSON or form). */
|
|
225
|
+
export async function expandOf(request) {
|
|
226
|
+
const url = new URL(request.url);
|
|
227
|
+
const out = [...url.searchParams.entries()].filter(([k]) => /^expand(\[\d*\])?$/.test(k)).map(([, v]) => v);
|
|
228
|
+
if (request.method === 'GET' || request.method === 'HEAD')
|
|
229
|
+
return out;
|
|
230
|
+
const text = await request.text().catch(() => '');
|
|
231
|
+
if (!text)
|
|
232
|
+
return out;
|
|
233
|
+
if ((request.headers.get('content-type') ?? '').includes('json') || text.trim().startsWith('{')) {
|
|
234
|
+
try {
|
|
235
|
+
const e = JSON.parse(text).expand;
|
|
236
|
+
if (Array.isArray(e))
|
|
237
|
+
out.push(...e.map(String));
|
|
238
|
+
}
|
|
239
|
+
catch { /* not JSON */ }
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
for (const [k, v] of new URLSearchParams(text))
|
|
243
|
+
if (/^expand(\[\d*\])?$/.test(k))
|
|
244
|
+
out.push(v);
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/** An answer with every webhook endpoint's `secret` left out: Stripe answers it only when the endpoint is created. */
|
|
248
|
+
export function withoutEndpointSecret(value) {
|
|
249
|
+
if (Array.isArray(value))
|
|
250
|
+
return value.map(withoutEndpointSecret);
|
|
251
|
+
if (!value || typeof value !== 'object')
|
|
252
|
+
return value;
|
|
253
|
+
const o = value;
|
|
254
|
+
const out = Object.fromEntries(Object.entries(o).filter(([k]) => !(k === 'secret' && o.object === 'webhook_endpoint')).map(([k, v]) => [k, withoutEndpointSecret(v)]));
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/** An answer (any JSON) rendered for the version a caller is served, with only the includable fields `expand` names. */
|
|
258
|
+
export function render(value, pinned, expand = []) {
|
|
259
|
+
if (!servesCurrent(pinned))
|
|
260
|
+
return value;
|
|
261
|
+
const expands = (at) => expand.some((e) => e === at || e.startsWith(`${at}.`));
|
|
262
|
+
const walk = (v, at = '') => {
|
|
263
|
+
if (Array.isArray(v))
|
|
264
|
+
return v.map((x) => walk(x, at));
|
|
265
|
+
if (!v || typeof v !== 'object')
|
|
266
|
+
return v;
|
|
267
|
+
const kindOf = typeof v.object === 'string' ? String(v.object) : undefined;
|
|
268
|
+
// `expand` is a request parameter, never a field of any object (the served spec gives none one); a handler that
|
|
269
|
+
// keeps its parameters keeps it too, and the answer leaves it out
|
|
270
|
+
const hidden = [...((kindOf ? INCLUDABLE[kindOf]?.filter((k) => !expands(at ? `${at}.${k}` : k)) : undefined) ?? []), ...(kindOf ? ['expand'] : [])];
|
|
271
|
+
const o = Object.fromEntries(Object.entries(v).filter(([k]) => !hidden.includes(k)).map(([k, x]) => [k, walk(x, at ? `${at}.${k}` : k)]));
|
|
272
|
+
const kind = typeof o.object === 'string' ? o.object : undefined;
|
|
273
|
+
// a deleted object answers only that it is gone
|
|
274
|
+
if (!kind || o.deleted === true)
|
|
275
|
+
return o;
|
|
276
|
+
// metadata holds strings ("key-value pairs", each value up to 500 characters, docs.stripe.com/metadata); the form
|
|
277
|
+
// reader coerces a numeric-looking value to a number, which the answer gives back as the string it was sent as
|
|
278
|
+
if (o.metadata && typeof o.metadata === 'object' && !Array.isArray(o.metadata))
|
|
279
|
+
o.metadata = Object.fromEntries(Object.entries(o.metadata).map(([k, v]) => [k, typeof v === 'number' || typeof v === 'boolean' ? String(v) : v]));
|
|
280
|
+
const kept = KEPT[kind] ? omit(o, KEPT[kind]) : o;
|
|
281
|
+
// an includable field a version's change adds (an invoice's confirmation_secret) is hidden as its stored ones are
|
|
282
|
+
return omit(withNulls(BASIL_CHANGES[kind] ? BASIL_CHANGES[kind](kept) : kept, kind), hidden);
|
|
283
|
+
};
|
|
284
|
+
return walk(value);
|
|
285
|
+
}
|