@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,278 @@
|
|
|
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
|
+
|
|
13
|
+
type Row = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
/** The version the pack serves: its vendored spec's. */
|
|
16
|
+
export const SERVED_VERSION = String(surface.version);
|
|
17
|
+
const BASIL = '2025-03-31';
|
|
18
|
+
|
|
19
|
+
/** Whether a request is answered in the served version's shape. */
|
|
20
|
+
export const servesCurrent = (pinned?: string | null): boolean => !pinned || pinned >= BASIL;
|
|
21
|
+
|
|
22
|
+
// What the twin keeps on an object for its rules that Stripe never answers: request parameters (a capture
|
|
23
|
+
// flag, the payment behavior a subscription was created with) and links it follows internally.
|
|
24
|
+
const KEPT: Record<string, string[]> = {
|
|
25
|
+
account: ['livemode'],
|
|
26
|
+
// a meter event is identified by its identifier; the twin's row id is its own
|
|
27
|
+
'billing.meter_event': ['id'],
|
|
28
|
+
// a card source's creation time is the twin's, for ordering
|
|
29
|
+
card: ['created'],
|
|
30
|
+
charge: ['capture', 'capture_before'],
|
|
31
|
+
dispute: ['submit'],
|
|
32
|
+
ephemeral_key: ['associated_objects'],
|
|
33
|
+
'financial_connections.account': ['session'],
|
|
34
|
+
'identity.verification_session': ['return_url'],
|
|
35
|
+
invoice: ['days_until_due', 'pending_invoice_items_behavior'],
|
|
36
|
+
'issuing.token': ['cardholder'],
|
|
37
|
+
payment_intent: ['mandate', 'off_session'],
|
|
38
|
+
subscription: ['payment_behavior'],
|
|
39
|
+
subscription_item: ['livemode'],
|
|
40
|
+
subscription_schedule: ['from_subscription', 'renewal_interval'],
|
|
41
|
+
'tax.transaction': ['calculation'],
|
|
42
|
+
'terminal.reader': ['registration_code'],
|
|
43
|
+
topup: ['destination_balance'],
|
|
44
|
+
'treasury.outbound_payment': ['destination_payment_method_data'],
|
|
45
|
+
'treasury.transaction_entry': ['amount'],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const omit = (o: Row, keys: string[]): Row => Object.fromEntries(Object.entries(o).filter(([k]) => !keys.includes(k)));
|
|
49
|
+
const idOf = (v: unknown): string | null => (typeof v === 'string' ? v : v && typeof v === 'object' && typeof (v as Row).id === 'string' ? String((v as Row).id) : null);
|
|
50
|
+
|
|
51
|
+
/** A price (an id or an expanded object) as basil's pricing block names it. */
|
|
52
|
+
function pricing(price: unknown, amount: unknown, quantity: unknown): Row | null {
|
|
53
|
+
const id = idOf(price);
|
|
54
|
+
if (!id) return null;
|
|
55
|
+
const product = price && typeof price === 'object' ? idOf((price as Row).product) : null;
|
|
56
|
+
const unit = price && typeof price === 'object' && (price as Row).unit_amount !== undefined ? (price as Row).unit_amount : Number(amount) / (Number(quantity) || 1);
|
|
57
|
+
return { type: 'price_details', price_details: { price: id, product: product ?? '' }, unit_amount_decimal: String(unit ?? 0) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// basil (2025-03-31): what moved, per object
|
|
61
|
+
const BASIL_CHANGES: Record<string, (o: Row, parent?: Row) => Row> = {
|
|
62
|
+
// 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)
|
|
63
|
+
invoice: (o) => {
|
|
64
|
+
const subscription = idOf(o.subscription);
|
|
65
|
+
const parent = o.parent !== undefined ? o.parent : subscription ? { type: 'subscription_details', quote_details: null, subscription_details: { metadata: {}, subscription } } : null;
|
|
66
|
+
// "confirmation_secret … Currently, this contains the client_secret of the PaymentIntent that Stripe creates during
|
|
67
|
+
// invoice finalization" (docs.stripe.com/api/invoices/object; includable, answered only when expanded: INCLUDABLE);
|
|
68
|
+
// the intent's client_secret is the one stripe-twin.ts mintClientSecret gives it
|
|
69
|
+
const intent = typeof o.payment_intent === 'string' && o.payment_intent ? o.payment_intent : null;
|
|
70
|
+
const confirmation_secret = intent ? { type: 'payment_intent', client_secret: `${intent}_secret_twin` } : null;
|
|
71
|
+
return { ...omit(o, ['payment_intent', 'charge', 'paid', 'paid_out_of_band', 'subscription']), parent, confirmation_secret };
|
|
72
|
+
},
|
|
73
|
+
charge: (o) => omit(o, ['invoice', 'source']),
|
|
74
|
+
payment_intent: (o) => omit(o, ['invoice']),
|
|
75
|
+
// a subscription's billing period is its items' (docs.stripe.com/changelog/basil/2025-03-31/deprecate-subscription-current-period-start-and-end)
|
|
76
|
+
subscription: (o) => {
|
|
77
|
+
const period = { current_period_start: o.current_period_start, current_period_end: o.current_period_end };
|
|
78
|
+
const items = o.items && typeof o.items === 'object' ? (o.items as Row) : undefined;
|
|
79
|
+
const data = Array.isArray(items?.data) ? (items!.data as Row[]).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;
|
|
80
|
+
return { ...omit(o, ['current_period_start', 'current_period_end']), ...(items && data ? { items: { ...items, data } } : {}) };
|
|
81
|
+
},
|
|
82
|
+
subscription_item: (o) => ({ ...omit(o, ['plan']), discounts: o.discounts ?? [] }),
|
|
83
|
+
invoiceitem: (o) => ({ ...omit(o, ['price']), pricing: o.pricing ?? pricing(o.price, o.amount, o.quantity) }),
|
|
84
|
+
// a line names what generated it (its parent) and its price and taxes in basil's blocks
|
|
85
|
+
line_item: (o) => {
|
|
86
|
+
const proration = o.proration === true;
|
|
87
|
+
// a line an invoice item made names it; a subscription's line (invoice_item null) is its item's: "Details about the
|
|
88
|
+
// subscription item that generated this line item" (docs.stripe.com/api/invoice-line-item/object, parent)
|
|
89
|
+
const fromItem = o.type === 'invoiceitem' || (o.invoice_item !== undefined && o.invoice_item !== null && o.type !== 'subscription');
|
|
90
|
+
const parent = o.parent !== undefined ? o.parent : fromItem
|
|
91
|
+
? { 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 }
|
|
92
|
+
: { 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 };
|
|
93
|
+
const taxes = Array.isArray(o.tax_amounts) ? (o.tax_amounts as Row[]).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) } })) : [];
|
|
94
|
+
return {
|
|
95
|
+
...omit(o, ['price', 'invoice_item', 'proration', 'tax_amounts', 'tax_rates', 'type', 'subscription_item']),
|
|
96
|
+
parent, pricing: o.pricing ?? pricing(o.price, o.amount, o.quantity), taxes,
|
|
97
|
+
discountable: o.discountable ?? !proration, discounts: o.discounts ?? [], livemode: o.livemode ?? false, metadata: o.metadata ?? {},
|
|
98
|
+
period: o.period ?? { start: 0, end: 0 }, subtotal: o.subtotal ?? o.amount,
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
// a promotion code promotes a coupon (docs.stripe.com/api/promotion_codes/object)
|
|
102
|
+
promotion_code: (o) => ({ ...omit(o, ['coupon']), promotion: o.promotion ?? { type: 'coupon', coupon: o.coupon ?? null } }),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Stripe answers every field of an object, a nullable one it has no value for as null (the Product object's page,
|
|
106
|
+
// docs.stripe.com/api/products/object, lists `package_dimensions` as "(object, nullable)" and its example answers
|
|
107
|
+
// `"package_dimensions": null`): a nullable field of the served spec the twin never set is rendered null. The spec's
|
|
108
|
+
// top-level fields are read from the surface; a sub-object's nullable fields are listed below, each from its object's
|
|
109
|
+
// page, where a published example met one the twin left out.
|
|
110
|
+
const NULLABLE = new Map((surface.resources as Array<{ schema: string; fields: Array<{ name: string; nullable?: boolean }> }>).map((r) => [r.schema, r.fields.filter((f) => f.nullable).map((f) => f.name)]));
|
|
111
|
+
const NESTED_NULLABLE: Record<string, Record<string, string[]>> = {
|
|
112
|
+
// docs.stripe.com/api/checkout/sessions/object?query=customer_details: business_name, individual_name "(string, nullable)"
|
|
113
|
+
'checkout.session': { customer_details: ['business_name', 'individual_name'] },
|
|
114
|
+
// docs.stripe.com/api/subscriptions/object: "billing_mode.flexible (object, nullable)", "automatic_tax.disabled_reason
|
|
115
|
+
// (enum, nullable)", invoice_settings.account_tax_ids, .custom_fields, .description, .footer each nullable
|
|
116
|
+
// (and cancellation_details.feedback_option, nullable in the served spec: "Customized feedback options that provide
|
|
117
|
+
// deeper insight into why the subscription was canceled")
|
|
118
|
+
subscription: { automatic_tax: ['disabled_reason'], billing_mode: ['flexible'], invoice_settings: ['account_tax_ids', 'custom_fields', 'description', 'footer'], cancellation_details: ['comment', 'feedback', 'feedback_option', 'reason'] },
|
|
119
|
+
// docs.stripe.com/api/payment_methods/object: billing_details.tax_id, card.fingerprint, card.generated_from,
|
|
120
|
+
// card.regulated_status each "(…, nullable)"
|
|
121
|
+
// docs.stripe.com/api/invoices/object: automatic_tax.disabled_reason "(enum, nullable)", automatic_tax.provider
|
|
122
|
+
// "(string, nullable)"
|
|
123
|
+
invoice: { automatic_tax: ['disabled_reason', 'provider'] },
|
|
124
|
+
// docs.stripe.com/api/customers/object: invoice_settings.custom_fields, .default_payment_method, .footer,
|
|
125
|
+
// .rendering_options each nullable
|
|
126
|
+
customer: { invoice_settings: ['custom_fields', 'default_payment_method', 'footer', 'rendering_options'] },
|
|
127
|
+
// docs.stripe.com/api/accounts/object: a fresh account's business_profile answers each of these null
|
|
128
|
+
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'] },
|
|
129
|
+
// docs.stripe.com/api/charges/object: billing_details.tax_id and each of these payment_method_details.card fields
|
|
130
|
+
// "(…, nullable)"; extended_authorization, incremental_authorization, multicapture and overcapture are not nullable
|
|
131
|
+
// in the served spec, so an unmodelled one is left out rather than answered null
|
|
132
|
+
charge: {
|
|
133
|
+
billing_details: ['tax_id'],
|
|
134
|
+
'payment_method_details.card': ['amount_authorized', 'authorization_code', 'electronic_commerce_indicator', 'network_token', 'network_transaction_id', 'regulated_status', 'transaction_link_id'],
|
|
135
|
+
},
|
|
136
|
+
payment_method: { billing_details: ['tax_id'], card: ['fingerprint', 'generated_from', 'regulated_status'] },
|
|
137
|
+
// the served spec's person_relationship: legal_guardian and authorizer, each "(boolean, nullable)"
|
|
138
|
+
person: { relationship: ['legal_guardian', 'authorizer'] },
|
|
139
|
+
// the served spec's source_owner: each field "(…, nullable)"; the sources create page's example answers them null
|
|
140
|
+
source: { owner: ['address', 'email', 'name', 'phone', 'verified_address', 'verified_email', 'verified_name', 'verified_phone'] },
|
|
141
|
+
// the served spec's address_api_resource_terminal: each line "(string, nullable)"; the location fixture answers line2 null
|
|
142
|
+
'terminal.location': { address: ['city', 'country', 'line1', 'line2', 'postal_code', 'state'] },
|
|
143
|
+
// the served spec's issuing_cardholder_individual (dob, verification, card_issuing), its address and its authorization
|
|
144
|
+
// controls (allowed_card_presences, blocked_card_presences, spending_limits_currency): each "(…, nullable)"
|
|
145
|
+
'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'] },
|
|
146
|
+
// and a card's authorization controls (the served spec's issuing_card_authorization_controls), likewise nullable
|
|
147
|
+
'issuing.card': { spending_controls: ['allowed_card_presences', 'blocked_card_presences', 'spending_limits_currency'] },
|
|
148
|
+
// the served spec's issuing_dispute_fraudulent_evidence: additional_documentation and explanation, each nullable
|
|
149
|
+
'issuing.dispute': { 'evidence.fraudulent': ['additional_documentation', 'explanation'] },
|
|
150
|
+
// the served spec's issuing_personalization_design_carrier_text: its four texts, each nullable
|
|
151
|
+
'issuing.personalization_design': { carrier_text: ['footer_body', 'footer_title', 'header_body', 'header_title'] },
|
|
152
|
+
// the served spec's treasury_shared_resource_billing_details.address: each line "(string, nullable)"
|
|
153
|
+
'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'] },
|
|
154
|
+
// (and its linked flows: every one nullable in the served spec's treasury_received_debits_resource_linked_flows, and
|
|
155
|
+
// likewise the credit's)
|
|
156
|
+
'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'] },
|
|
157
|
+
'treasury.inbound_transfer': { 'origin_payment_method_details.billing_details.address': ['city', 'country', 'line1', 'line2', 'postal_code', 'state'] },
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// A field the vendor fills with its default when the request set none, by object, each from its object's page.
|
|
161
|
+
const CARD_DISPLAY: Record<string, string> = { amex: 'american_express', diners: 'diners_club', eftpos_au: 'eftpos_australia', unionpay: 'union_pay' };
|
|
162
|
+
const DEFAULTS: Record<string, (o: Row) => Row> = {
|
|
163
|
+
// docs.stripe.com/api/payment_methods/object: allow_redisplay "defaults to “unspecified”"; card.display_brand is "The
|
|
164
|
+
// brand to use when displaying the card … Can be `american_express`, …, `visa`", the brand's display name
|
|
165
|
+
payment_method: (o) => {
|
|
166
|
+
const card = o.card && typeof o.card === 'object' ? (o.card as Row) : undefined;
|
|
167
|
+
const brand = typeof card?.brand === 'string' ? card.brand : undefined;
|
|
168
|
+
return {
|
|
169
|
+
...(o.allow_redisplay === undefined ? { allow_redisplay: 'unspecified' } : {}),
|
|
170
|
+
...(card && brand && card.display_brand === undefined ? { card: { ...card, display_brand: CARD_DISPLAY[brand] ?? (brand === 'unknown' ? 'other' : brand) } } : {}),
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
// a line names "The ID of the invoice that contains this line item" (docs.stripe.com/api/invoice-line-item/object)
|
|
174
|
+
invoice: (o) => {
|
|
175
|
+
const lines = o.lines && typeof o.lines === 'object' ? (o.lines as Row) : undefined;
|
|
176
|
+
if (!Array.isArray(lines?.data) || typeof o.id !== 'string') return {};
|
|
177
|
+
return { lines: { ...lines, data: (lines!.data as Row[]).map((l) => (l && typeof l === 'object' && (l.invoice === undefined || l.invoice === null) ? { ...l, invoice: o.id } : l)) } };
|
|
178
|
+
},
|
|
179
|
+
// docs.stripe.com/api/invoiceitems/object: net_amount, "The amount after discounts, but before credits and taxes. This
|
|
180
|
+
// field is `null` for `discountable=true` items" (the twin puts no discount on an item itself)
|
|
181
|
+
invoiceitem: (o) => (o.net_amount === undefined ? { net_amount: o.discountable === false ? (typeof o.amount === 'number' ? o.amount : null) : null } : {}),
|
|
182
|
+
// docs.stripe.com/api/payment_intents/object: confirmation_method `automatic` "(Default)"; amount_details as its
|
|
183
|
+
// example answers it for a card payment, `{"tip": {}}`
|
|
184
|
+
payment_intent: (o) => ({
|
|
185
|
+
...(o.confirmation_method === undefined ? { confirmation_method: 'automatic' } : {}),
|
|
186
|
+
...(o.amount_details === undefined ? { amount_details: { tip: {} } } : {}),
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
const fill = (o: Row, keys: string[]): Row => (keys.some((k) => !(k in o)) ? { ...o, ...Object.fromEntries(keys.filter((k) => !(k in o)).map((k) => [k, null])) } : o);
|
|
190
|
+
// every object whose served spec gives it a `metadata` it never answers null answers `{}` when none was set: "Set of
|
|
191
|
+
// key-value pairs that you can attach to an object" (docs.stripe.com/api/metadata), `metadata (map)` on each object's
|
|
192
|
+
// page, and its example `"metadata": {}` (the PaymentIntent object's, docs.stripe.com/api/payment_intents/object)
|
|
193
|
+
const METADATA = new Set((surface.resources as Array<{ schema: string; fields: Array<{ name: string; nullable?: boolean }> }>).filter((r) => r.fields.some((f) => f.name === 'metadata' && !f.nullable)).map((r) => r.schema));
|
|
194
|
+
const withNulls = (o: Row, kind: string): Row => {
|
|
195
|
+
let out = fill(DEFAULTS[kind] ? { ...o, ...DEFAULTS[kind]!(o) } : o, NULLABLE.get(kind) ?? []);
|
|
196
|
+
if (METADATA.has(kind) && (out.metadata === undefined || out.metadata === null)) out = { ...out, metadata: {} };
|
|
197
|
+
// a dotted path reaches a sub-object's own sub-object (a charge's payment_method_details.card)
|
|
198
|
+
const at = (o: Row, path: string[], keys: string[]): Row => {
|
|
199
|
+
const [head, ...rest] = path;
|
|
200
|
+
const sub = o[head!];
|
|
201
|
+
if (!sub || typeof sub !== 'object' || Array.isArray(sub)) return o;
|
|
202
|
+
return { ...o, [head!]: rest.length ? at(sub as Row, rest, keys) : fill(sub as Row, keys) };
|
|
203
|
+
};
|
|
204
|
+
for (const [path, keys] of Object.entries(NESTED_NULLABLE[kind] ?? {})) out = at(out, path.split('.'), keys);
|
|
205
|
+
return out;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// A field Stripe includes only when the request expands it: the Checkout Session object's page
|
|
209
|
+
// (docs.stripe.com/api/checkout/sessions/object) marks `line_items` "includable (not returned by default; request it
|
|
210
|
+
// with the `expand` request parameter)". The twin keeps it on the object for its rules; the answer carries it only
|
|
211
|
+
// where the request's `expand[]` names it (`line_items`, or `data.line_items` on a list).
|
|
212
|
+
const INCLUDABLE: Record<string, string[]> = {
|
|
213
|
+
'checkout.session': ['line_items'],
|
|
214
|
+
// docs.stripe.com/api/charges/object: `refunds` "(object, nullable, includable (not returned by default; …))"
|
|
215
|
+
charge: ['refunds'],
|
|
216
|
+
// docs.stripe.com/api/payment-link/object: `line_items` "object Includable", and its example answers none
|
|
217
|
+
payment_link: ['line_items'],
|
|
218
|
+
// docs.stripe.com/api/quotes/object: `line_items` "object Includable", and its example answers none
|
|
219
|
+
quote: ['line_items'],
|
|
220
|
+
// docs.stripe.com/api/secret_management: `payload` "nullable string Includable": a secret's value is answered only
|
|
221
|
+
// when the request expands it
|
|
222
|
+
'apps.secret': ['payload'],
|
|
223
|
+
// docs.stripe.com/api/tax/calculations/object and /tax/transactions/object: `line_items` "nullable object Includable"
|
|
224
|
+
'tax.calculation': ['line_items'],
|
|
225
|
+
'tax.transaction': ['line_items'],
|
|
226
|
+
// docs.stripe.com/api/invoices/object: `confirmation_secret` "(object, nullable, includable (not returned by default;
|
|
227
|
+
// request it with the `expand` request parameter))"
|
|
228
|
+
invoice: ['confirmation_secret'],
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/** The `expand[]` paths a request names, in its query or its body (JSON or form). */
|
|
232
|
+
export async function expandOf(request: Request): Promise<string[]> {
|
|
233
|
+
const url = new URL(request.url);
|
|
234
|
+
const out = [...url.searchParams.entries()].filter(([k]) => /^expand(\[\d*\])?$/.test(k)).map(([, v]) => v);
|
|
235
|
+
if (request.method === 'GET' || request.method === 'HEAD') return out;
|
|
236
|
+
const text = await request.text().catch(() => '');
|
|
237
|
+
if (!text) return out;
|
|
238
|
+
if ((request.headers.get('content-type') ?? '').includes('json') || text.trim().startsWith('{')) {
|
|
239
|
+
try { const e = (JSON.parse(text) as { expand?: unknown }).expand; if (Array.isArray(e)) out.push(...e.map(String)); } catch { /* not JSON */ }
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
for (const [k, v] of new URLSearchParams(text)) if (/^expand(\[\d*\])?$/.test(k)) out.push(v);
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** An answer with every webhook endpoint's `secret` left out: Stripe answers it only when the endpoint is created. */
|
|
247
|
+
export function withoutEndpointSecret(value: unknown): unknown {
|
|
248
|
+
if (Array.isArray(value)) return value.map(withoutEndpointSecret);
|
|
249
|
+
if (!value || typeof value !== 'object') return value;
|
|
250
|
+
const o = value as Row;
|
|
251
|
+
const out = Object.fromEntries(Object.entries(o).filter(([k]) => !(k === 'secret' && o.object === 'webhook_endpoint')).map(([k, v]) => [k, withoutEndpointSecret(v)]));
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** An answer (any JSON) rendered for the version a caller is served, with only the includable fields `expand` names. */
|
|
256
|
+
export function render(value: unknown, pinned?: string | null, expand: string[] = []): unknown {
|
|
257
|
+
if (!servesCurrent(pinned)) return value;
|
|
258
|
+
const expands = (at: string): boolean => expand.some((e) => e === at || e.startsWith(`${at}.`));
|
|
259
|
+
const walk = (v: unknown, at = ''): unknown => {
|
|
260
|
+
if (Array.isArray(v)) return v.map((x) => walk(x, at));
|
|
261
|
+
if (!v || typeof v !== 'object') return v;
|
|
262
|
+
const kindOf = typeof (v as Row).object === 'string' ? String((v as Row).object) : undefined;
|
|
263
|
+
// `expand` is a request parameter, never a field of any object (the served spec gives none one); a handler that
|
|
264
|
+
// keeps its parameters keeps it too, and the answer leaves it out
|
|
265
|
+
const hidden: string[] = [...((kindOf ? INCLUDABLE[kindOf]?.filter((k) => !expands(at ? `${at}.${k}` : k)) : undefined) ?? []), ...(kindOf ? ['expand'] : [])];
|
|
266
|
+
const o = Object.fromEntries(Object.entries(v as Row).filter(([k]) => !hidden.includes(k)).map(([k, x]) => [k, walk(x, at ? `${at}.${k}` : k)]));
|
|
267
|
+
const kind = typeof o.object === 'string' ? o.object : undefined;
|
|
268
|
+
// a deleted object answers only that it is gone
|
|
269
|
+
if (!kind || o.deleted === true) return o;
|
|
270
|
+
// metadata holds strings ("key-value pairs", each value up to 500 characters, docs.stripe.com/metadata); the form
|
|
271
|
+
// reader coerces a numeric-looking value to a number, which the answer gives back as the string it was sent as
|
|
272
|
+
if (o.metadata && typeof o.metadata === 'object' && !Array.isArray(o.metadata)) o.metadata = Object.fromEntries(Object.entries(o.metadata as Row).map(([k, v]) => [k, typeof v === 'number' || typeof v === 'boolean' ? String(v) : v]));
|
|
273
|
+
const kept = KEPT[kind] ? omit(o, KEPT[kind]!) : o;
|
|
274
|
+
// an includable field a version's change adds (an invoice's confirmation_secret) is hidden as its stored ones are
|
|
275
|
+
return omit(withNulls(BASIL_CHANGES[kind] ? BASIL_CHANGES[kind]!(kept) : kept, kind), hidden);
|
|
276
|
+
};
|
|
277
|
+
return walk(value);
|
|
278
|
+
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
{
|
|
40
40
|
"path": "_card.declines-test-set-only",
|
|
41
41
|
"kind": "behavior-modeled-subset",
|
|
42
|
-
"reason": "Charge/PaymentIntent-confirm card declines are modeled for Stripe's DOCUMENTED TEST CARDS only (PAN 4242\u20264242 + pm_card_visa/tok_visa succeed; 4000\u20260002 generic_decline, 4000\u20269995 insufficient_funds, 4000\u20260069 expired_card, 4000\u20260127 incorrect_cvc, 4000\u20260119 processing_error, plus the matching pm_card_*/tok_* tokens). A known declining card returns the real Stripe card-error envelope (HTTP 402, error.type=card_error, code, decline_code for card_declined, message, param=card, and the charge/payment_intent id)
|
|
42
|
+
"reason": "Charge/PaymentIntent-confirm card declines are modeled for Stripe's DOCUMENTED TEST CARDS only (PAN 4242\u20264242 + pm_card_visa/tok_visa succeed; 4000\u20260002 generic_decline, 4000\u20269995 insufficient_funds, 4000\u20260069 expired_card, 4000\u20260127 incorrect_cvc, 4000\u20260119 processing_error, plus the matching pm_card_*/tok_* tokens). A known declining card returns the real Stripe card-error envelope (HTTP 402, error.type=card_error, code, decline_code for card_declined, message, param=card, and the charge/payment_intent id) records the attempt as a failed charge (failure_code, failure_message, outcome) that the error names and a confirmed PaymentIntent's latest_charge points at, leaving the intent at status=requires_payment_method with last_payment_error (carrying the declined card) set and no payment_method. ANY card the twin does not recognize (real PANs, unknown test tokens, or no card at all) succeeds, deterministically \u2014 the twin is not a risk/fraud engine and does not simulate issuer behavior beyond Stripe's published test set. Resolution reads card[number]/source[number] (raw PAN) or payment_method/source/card (token id). Leading underscore marks this as a twin-internal note (not a vendor schema path)."
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"path": "_idempotency.stored-per-root",
|
|
@@ -51,15 +51,10 @@
|
|
|
51
51
|
"kind": "resource-modeled-statefully",
|
|
52
52
|
"reason": "The Events API (GET /v1/events, GET /v1/events/:id) is modeled statefully via the action log. The twin separately emits live webhook events on writes (stripe-events.ts); the Events API here serves explicitly-recorded event objects with the faithful vendor shape (type, api_version, created, data.object, livemode, pending_webhooks). pending_webhooks defaults to 0 because the twin delivers webhooks synchronously. data.object defaults to an empty object when the caller does not supply a snapshot. Leading underscore marks this as a twin-internal note (not a vendor schema path)."
|
|
53
53
|
},
|
|
54
|
-
{
|
|
55
|
-
"path": "_checkout.hosted-page-pixels",
|
|
56
|
-
"kind": "non-goal-render-omitted",
|
|
57
|
-
"reason": "Checkout Sessions (POST/GET /v1/checkout/sessions, GET :id/line_items, POST :id/expire) and Customer Portal sessions (POST /v1/billing_portal/sessions, + billing_portal/configurations) are modeled as API OBJECTS with vendor-faithful shapes, ids (cs_/bps_/bpc_), status/payment_status enums, list envelope, and 400/404 errors. Rendering the Stripe-HOSTED Checkout / Customer Portal PAGE PIXELS is declared out of scope: the hosted HTML is Stripe's, not an API object. The twin models the Session object + redirect `url` (https://checkout.twin.local/... and https://billing.twin.local/...), which is the surface the unmodified `stripe` SDK and apps depend on. Leading underscore marks this as a twin-internal note (not a vendor schema path)."
|
|
58
|
-
},
|
|
59
54
|
{
|
|
60
55
|
"path": "_checkout.completion-modeled",
|
|
61
56
|
"kind": "behavior-modeled-statefully",
|
|
62
|
-
"reason": "Real Stripe completes a Checkout Session when the customer pays on the hosted page (there is no public REST verb to complete a session). The twin
|
|
57
|
+
"reason": "Real Stripe completes a Checkout Session when the customer pays on the hosted page (there is no public REST verb to complete a session). The twin serves that page at the session's url (/c/pay/:id, src/screens/checkout.tsx): paying there (only while `open`) completes the session and creates+links a real twin object: a succeeded payment_intent (mode=payment, amount = amount_total) with payment_status\u2192paid, an active subscription (mode=subscription, when a customer is present) with payment_status\u2192paid, or a succeeded setup_intent (mode=setup). POST :id/expire transitions open\u2192expired (terminal). amount_subtotal/amount_total are computed by summing resolved line_items (an existing Price's unit_amount, or inline price_data.unit_amount, \u00d7 quantity). Leading underscore marks this as a twin-internal note (not a vendor schema path)."
|
|
63
58
|
},
|
|
64
59
|
{
|
|
65
60
|
"path": "_tax.calculation-list-endpoint",
|