@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.
Files changed (219) hide show
  1. package/README.md +64 -27
  2. package/client/dashboard-api.ts +286 -0
  3. package/client/stripe-mirror.css +272 -159
  4. package/client/stripe-mirror.tsx +1384 -541
  5. package/dist/client/dashboard-api.d.ts +107 -0
  6. package/dist/client/dashboard-api.js +238 -0
  7. package/dist/client/dashboard-api.ts +286 -0
  8. package/dist/client/stripe-mirror.bundle.js +236 -0
  9. package/dist/client/stripe-mirror.css +275 -0
  10. package/dist/client/stripe-mirror.d.ts +134 -0
  11. package/dist/client/stripe-mirror.js +823 -0
  12. package/dist/client/stripe-mirror.tsx +1534 -0
  13. package/dist/src/cli.d.ts +2 -0
  14. package/dist/src/cli.js +39 -0
  15. package/dist/src/generated/events.gen.json +1 -0
  16. package/dist/src/generated/surface.gen.json +1 -0
  17. package/dist/src/generated/ui.gen.json +1 -0
  18. package/dist/src/index.d.ts +14 -0
  19. package/dist/src/index.js +73 -0
  20. package/dist/src/manifest.d.ts +2 -0
  21. package/dist/src/manifest.js +1065 -0
  22. package/dist/src/screens/checkout.d.ts +31 -0
  23. package/dist/src/screens/checkout.js +241 -0
  24. package/dist/src/screens/consent-skin.d.ts +4 -0
  25. package/dist/src/screens/consent-skin.js +18 -0
  26. package/dist/src/screens/financial-connections.d.ts +5 -0
  27. package/dist/src/screens/financial-connections.js +90 -0
  28. package/dist/src/screens/identity.d.ts +5 -0
  29. package/dist/src/screens/identity.js +86 -0
  30. package/dist/src/screens/industries.d.ts +1 -0
  31. package/dist/src/screens/industries.js +267 -0
  32. package/dist/src/screens/onboarding.d.ts +13 -0
  33. package/dist/src/screens/onboarding.js +225 -0
  34. package/dist/src/screens/portal.d.ts +5 -0
  35. package/dist/src/screens/portal.js +214 -0
  36. package/dist/src/screens/public-details.d.ts +5 -0
  37. package/dist/src/screens/public-details.js +90 -0
  38. package/dist/src/semantics/after-payment.d.ts +22 -0
  39. package/dist/src/semantics/after-payment.js +93 -0
  40. package/dist/src/semantics/apps-secrets.d.ts +2 -0
  41. package/dist/src/semantics/apps-secrets.js +54 -0
  42. package/dist/src/semantics/balance.d.ts +11 -0
  43. package/dist/src/semantics/balance.js +195 -0
  44. package/dist/src/semantics/billing.d.ts +2 -0
  45. package/dist/src/semantics/billing.js +220 -0
  46. package/dist/src/semantics/charges.d.ts +28 -0
  47. package/dist/src/semantics/charges.js +201 -0
  48. package/dist/src/semantics/checkout.d.ts +15 -0
  49. package/dist/src/semantics/checkout.js +303 -0
  50. package/dist/src/semantics/connect.d.ts +5 -0
  51. package/dist/src/semantics/connect.js +476 -0
  52. package/dist/src/semantics/coupons.d.ts +6 -0
  53. package/dist/src/semantics/coupons.js +92 -0
  54. package/dist/src/semantics/credit-notes.d.ts +2 -0
  55. package/dist/src/semantics/credit-notes.js +172 -0
  56. package/dist/src/semantics/customers.d.ts +6 -0
  57. package/dist/src/semantics/customers.js +429 -0
  58. package/dist/src/semantics/disputes.d.ts +2 -0
  59. package/dist/src/semantics/disputes.js +51 -0
  60. package/dist/src/semantics/entitlements.d.ts +2 -0
  61. package/dist/src/semantics/entitlements.js +95 -0
  62. package/dist/src/semantics/ephemeral-keys.d.ts +2 -0
  63. package/dist/src/semantics/ephemeral-keys.js +34 -0
  64. package/dist/src/semantics/files.d.ts +2 -0
  65. package/dist/src/semantics/files.js +125 -0
  66. package/dist/src/semantics/invoices.d.ts +18 -0
  67. package/dist/src/semantics/invoices.js +541 -0
  68. package/dist/src/semantics/issuing.d.ts +13 -0
  69. package/dist/src/semantics/issuing.js +570 -0
  70. package/dist/src/semantics/ledger.d.ts +54 -0
  71. package/dist/src/semantics/ledger.js +181 -0
  72. package/dist/src/semantics/payment-intents.d.ts +18 -0
  73. package/dist/src/semantics/payment-intents.js +404 -0
  74. package/dist/src/semantics/payment-links.d.ts +2 -0
  75. package/dist/src/semantics/payment-links.js +133 -0
  76. package/dist/src/semantics/payment-methods.d.ts +20 -0
  77. package/dist/src/semantics/payment-methods.js +138 -0
  78. package/dist/src/semantics/plans.d.ts +5 -0
  79. package/dist/src/semantics/plans.js +121 -0
  80. package/dist/src/semantics/platform.d.ts +9 -0
  81. package/dist/src/semantics/platform.js +206 -0
  82. package/dist/src/semantics/products.d.ts +2 -0
  83. package/dist/src/semantics/products.js +140 -0
  84. package/dist/src/semantics/radar.d.ts +2 -0
  85. package/dist/src/semantics/radar.js +83 -0
  86. package/dist/src/semantics/refunds.d.ts +9 -0
  87. package/dist/src/semantics/refunds.js +195 -0
  88. package/dist/src/semantics/renewals.d.ts +47 -0
  89. package/dist/src/semantics/renewals.js +251 -0
  90. package/dist/src/semantics/setup-intents.d.ts +2 -0
  91. package/dist/src/semantics/setup-intents.js +84 -0
  92. package/dist/src/semantics/shared.d.ts +78 -0
  93. package/dist/src/semantics/shared.js +192 -0
  94. package/dist/src/semantics/subscription-schedules.d.ts +2 -0
  95. package/dist/src/semantics/subscription-schedules.js +119 -0
  96. package/dist/src/semantics/subscriptions.d.ts +11 -0
  97. package/dist/src/semantics/subscriptions.js +605 -0
  98. package/dist/src/semantics/tax.d.ts +2 -0
  99. package/dist/src/semantics/tax.js +197 -0
  100. package/dist/src/semantics/terminal.d.ts +5 -0
  101. package/dist/src/semantics/terminal.js +182 -0
  102. package/dist/src/semantics/test-clocks.d.ts +6 -0
  103. package/dist/src/semantics/test-clocks.js +73 -0
  104. package/dist/src/semantics/tokens.d.ts +4 -0
  105. package/dist/src/semantics/tokens.js +44 -0
  106. package/dist/src/semantics/transfers.d.ts +2 -0
  107. package/dist/src/semantics/transfers.js +154 -0
  108. package/dist/src/semantics/treasury.d.ts +2 -0
  109. package/dist/src/semantics/treasury.js +377 -0
  110. package/dist/src/semantics/webhook-endpoints.d.ts +3 -0
  111. package/dist/src/semantics/webhook-endpoints.js +85 -0
  112. package/dist/src/stripe-budget.d.ts +55 -0
  113. package/dist/src/stripe-budget.js +155 -0
  114. package/dist/src/stripe-capabilities.d.ts +3 -0
  115. package/dist/src/stripe-capabilities.js +5052 -0
  116. package/dist/src/stripe-conformance.d.ts +41 -0
  117. package/dist/src/stripe-conformance.js +96 -0
  118. package/dist/src/stripe-connector.d.ts +161 -0
  119. package/dist/src/stripe-connector.js +414 -0
  120. package/dist/src/stripe-emit.d.ts +2 -0
  121. package/dist/src/stripe-emit.js +145 -0
  122. package/dist/src/stripe-events.d.ts +93 -0
  123. package/dist/src/stripe-events.js +388 -0
  124. package/dist/src/stripe-js.d.ts +4 -0
  125. package/dist/src/stripe-js.js +70 -0
  126. package/dist/src/stripe-mirror-ui.d.ts +15 -0
  127. package/dist/src/stripe-mirror-ui.js +87 -0
  128. package/dist/src/stripe-params.d.ts +3 -0
  129. package/dist/src/stripe-params.js +43 -0
  130. package/dist/src/stripe-perform-harness.d.ts +9 -0
  131. package/dist/src/stripe-perform-harness.js +26 -0
  132. package/dist/src/stripe-server.d.ts +33 -0
  133. package/dist/src/stripe-server.js +326 -0
  134. package/dist/src/stripe-shared.d.ts +106 -0
  135. package/dist/src/stripe-shared.js +273 -0
  136. package/dist/src/stripe-twin.d.ts +155 -0
  137. package/dist/src/stripe-twin.js +1226 -0
  138. package/dist/src/stripe-ui-conformance.d.ts +5 -0
  139. package/dist/src/stripe-ui-conformance.js +79 -0
  140. package/dist/src/stripe-ui-structure.d.ts +3 -0
  141. package/dist/src/stripe-ui-structure.js +168 -0
  142. package/dist/src/stripe-version.d.ts +10 -0
  143. package/dist/src/stripe-version.js +285 -0
  144. package/dist/test-fixtures/stripe-known-deviations.json +105 -0
  145. package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +14 -0
  146. package/dist/test-fixtures/stripe-openapi-operations.json +4717 -0
  147. package/dist/test-fixtures/stripe-schemas.SOURCE.md +35 -0
  148. package/dist/test-fixtures/stripe-schemas.json +3740 -0
  149. package/package.json +18 -10
  150. package/src/cli.ts +7 -7
  151. package/src/generated/events.gen.json +1 -0
  152. package/src/generated/surface.gen.json +1 -0
  153. package/src/generated/ui.gen.json +1 -0
  154. package/src/index.ts +31 -9
  155. package/src/manifest.ts +1097 -0
  156. package/src/screens/checkout.tsx +252 -0
  157. package/src/screens/consent-skin.ts +20 -0
  158. package/src/screens/financial-connections.tsx +101 -0
  159. package/src/screens/identity.tsx +96 -0
  160. package/src/screens/industries.ts +267 -0
  161. package/src/screens/onboarding.tsx +243 -0
  162. package/src/screens/portal.tsx +218 -0
  163. package/src/screens/public-details.tsx +105 -0
  164. package/src/semantics/after-payment.ts +113 -0
  165. package/src/semantics/apps-secrets.ts +58 -0
  166. package/src/semantics/balance.ts +209 -0
  167. package/src/semantics/billing.ts +216 -0
  168. package/src/semantics/charges.ts +211 -0
  169. package/src/semantics/checkout.ts +297 -0
  170. package/src/semantics/connect.ts +471 -0
  171. package/src/semantics/coupons.ts +97 -0
  172. package/src/semantics/credit-notes.ts +168 -0
  173. package/src/semantics/customers.ts +432 -0
  174. package/src/semantics/disputes.ts +62 -0
  175. package/src/semantics/entitlements.ts +94 -0
  176. package/src/semantics/ephemeral-keys.ts +34 -0
  177. package/src/semantics/files.ts +143 -0
  178. package/src/semantics/invoices.ts +541 -0
  179. package/src/semantics/issuing.ts +585 -0
  180. package/src/semantics/ledger.ts +216 -0
  181. package/src/semantics/payment-intents.ts +420 -0
  182. package/src/semantics/payment-links.ts +148 -0
  183. package/src/semantics/payment-methods.ts +143 -0
  184. package/src/semantics/plans.ts +131 -0
  185. package/src/semantics/platform.ts +220 -0
  186. package/src/semantics/products.ts +154 -0
  187. package/src/semantics/radar.ts +85 -0
  188. package/src/semantics/refunds.ts +218 -0
  189. package/src/semantics/renewals.ts +274 -0
  190. package/src/semantics/setup-intents.ts +87 -0
  191. package/src/semantics/shared.ts +215 -0
  192. package/src/semantics/subscription-schedules.ts +129 -0
  193. package/src/semantics/subscriptions.ts +610 -0
  194. package/src/semantics/tax.ts +220 -0
  195. package/src/semantics/terminal.ts +195 -0
  196. package/src/semantics/test-clocks.ts +77 -0
  197. package/src/semantics/tokens.ts +52 -0
  198. package/src/semantics/transfers.ts +174 -0
  199. package/src/semantics/treasury.ts +383 -0
  200. package/src/semantics/webhook-endpoints.ts +87 -0
  201. package/src/stripe-budget.ts +4 -4
  202. package/src/stripe-capabilities.ts +1456 -222
  203. package/src/stripe-conformance.ts +6 -5
  204. package/src/stripe-connector.ts +68 -40
  205. package/src/stripe-emit.ts +14 -7
  206. package/src/stripe-events.ts +94 -36
  207. package/src/stripe-js.ts +70 -0
  208. package/src/stripe-mirror-ui.ts +28 -298
  209. package/src/stripe-params.ts +44 -0
  210. package/src/stripe-perform-harness.ts +29 -0
  211. package/src/stripe-server.ts +263 -38
  212. package/src/stripe-shared.ts +294 -0
  213. package/src/stripe-twin.ts +429 -5325
  214. package/src/stripe-ui-conformance.ts +70 -107
  215. package/src/stripe-ui-structure.ts +124 -348
  216. package/src/stripe-version.ts +278 -0
  217. package/test-fixtures/stripe-known-deviations.json +2 -7
  218. package/test-fixtures/stripe-openapi-operations.json +1188 -2855
  219. 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,3 @@
1
+ import { type UiStructureReport } from '@volter/world-tooling';
2
+ /** Build the structural checklist from the real rendered markup and run it. */
3
+ export declare function stripeUiStructure(): UiStructureReport;
@@ -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
+ }