@volter/twin-stripe 0.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,404 +1,180 @@
1
- // Stripe MIRROR — structural DOM checklist (RUNG-5).
1
+ // The Stripe Dashboard — structural DOM checklist (RUNG-5).
2
2
  //
3
- // Completeness (stripe-ui-conformance.ts) asks "does the mirror SHOW the data?".
4
- // Structure asks a stricter, proximity question: "does the mirror's rendered DOM have
5
- // the same structural LANDMARKS a real Stripe dashboard screen has?" — a left nav with
6
- // an item (and count) per collection, a list pane that emits a row per object, a detail
7
- // pane with a scalar key/value grid, nested objects rendered as indented lines (NOT
8
- // "[object Object]"), status pills, a cross-reference "Related" block of links,
9
- // product image <img> thumbnails, a prominent payment-decline banner (last_payment_error)
10
- // and the synthesized balance available/pending summary buckets.
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.
11
9
  //
12
- // Each check's `present` is computed from the ACTUAL markup produced by
13
- // renderToStaticMarkup() of the mirror's own presentational components (exported from
14
- // client/stripe-mirror.tsx) over seeded, cross-referenced objects — so a check cannot
15
- // pass unless the component truly emits that structure. Read-only; deterministic.
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.
16
13
  import { createElement } from 'react';
17
14
  import { renderToStaticMarkup } from 'react-dom/server';
18
- import { checkUiStructure, type UiStructureReport } from '@volter/twin-tooling';
15
+ import { checkUiStructure, type UiStructureReport } from '@volter/world-tooling';
19
16
  import {
20
- SECTIONS, SideNav, ListPane, Detail, type Section,
17
+ SECTIONS, SideNav, ListPane, TestModeBand, PaymentsTable, PaymentDetailView, CustomerDetailView,
18
+ SubscriptionDetailView, BalanceSummary, ConnectAccountPanel,
21
19
  } from '../client/stripe-mirror.tsx';
22
- import {
23
- resolveCrossRefs, productImageUrls,
24
- type StripeRow, type CrossRefs,
25
- } from './stripe-mirror-ui.ts';
20
+ import type { CustomerDetail, Payment, PaymentDetail, SubscriptionDetail } from '../client/dashboard-api.ts';
21
+ import type { StripeRow } from './stripe-mirror-ui.ts';
26
22
 
27
- const SECTION_BY_KEY: Record<string, Section> = Object.fromEntries(SECTIONS.map((s) => [s.key, s]));
23
+ const noop = () => {};
24
+ const AT = 1790263345;
28
25
 
29
- // ── Seed: a small, cross-referenced slice of every shape a dashboard renders. ──────
30
- // One object per collection we assert on, wired with real Stripe reference fields so
31
- // the cross-ref resolver produces both outgoing and incoming "Related" links, and with
32
- // nested objects/arrays (subscription items, invoice lines, product images) so the
33
- // nested-line + thumbnail structure has something real to render.
34
- const CUSTOMER: StripeRow = {
35
- id: 'cus_twin001', object: 'customer', name: 'Ada Lovelace', email: 'ada@example.com', currency: 'usd',
36
- metadata: { plan: 'pro', seats: '5' },
37
- };
38
- const PRODUCT: StripeRow = {
39
- id: 'prod_twin001', object: 'product', name: 'Analytics Suite', active: true,
40
- images: ['https://files.stripe.com/img/one.png', 'https://files.stripe.com/img/two.png'],
41
- };
42
- const PRICE: StripeRow = {
43
- id: 'price_twin001', object: 'price', product: 'prod_twin001', currency: 'usd', unit_amount: 4200,
44
- recurring: { interval: 'month', interval_count: 1 },
45
- };
46
- const SUBSCRIPTION: StripeRow = {
47
- id: 'sub_twin001', object: 'subscription', status: 'active', customer: 'cus_twin001', currency: 'usd',
48
- items: {
49
- object: 'list',
50
- data: [
51
- { id: 'si_twin001', object: 'subscription_item', quantity: 5, price: 'price_twin001' },
52
- ],
53
- },
54
- };
55
- const INVOICE: StripeRow = {
56
- id: 'in_twin001', object: 'invoice', status: 'paid', customer: 'cus_twin001', subscription: 'sub_twin001',
57
- currency: 'usd', total: 21000, amount_due: 21000,
58
- lines: {
59
- object: 'list',
60
- data: [
61
- { id: 'il_twin001', object: 'line_item', amount: 21000, quantity: 5, price: 'price_twin001' },
62
- ],
63
- },
64
- };
65
- const DISPUTE: StripeRow = {
66
- id: 'dp_twin001', object: 'dispute', amount: 4200, currency: 'usd', reason: 'fraudulent',
67
- status: 'warning_needs_response', charge: 'ch_twin001',
68
- };
69
- const PAYOUT: StripeRow = {
70
- id: 'po_twin001', object: 'payout', amount: 16800, currency: 'usd', status: 'in_transit',
71
- arrival_date: 1718409600,
72
- };
73
- // A declined payment_intent carries the vendor-faithful last_payment_error (test-card
74
- // decline state) the mirror lifts into a prominent banner.
75
- const DECLINED_PAYMENT: StripeRow = {
76
- id: 'pi_twin009', object: 'payment_intent', amount: 4200, currency: 'usd', customer: 'cus_twin001',
77
- status: 'requires_payment_method',
78
- last_payment_error: { type: 'card_error', code: 'card_declined', decline_code: 'insufficient_funds', message: 'Your card has insufficient funds.', param: 'card' },
79
- };
80
- // The synthesized account balance summary (GET /v1/balance), presented as a one-row list.
81
- const BALANCE: StripeRow = {
82
- id: 'balance', object: 'balance', livemode: false,
83
- available: [{ amount: 16800, currency: 'usd', source_types: { card: 16800 } }],
84
- pending: [{ amount: 4200, currency: 'usd', source_types: { card: 4200 } }],
85
- };
86
- // A Connect connected account, freshly created (not yet onboarded): charges/payouts
87
- // disabled, details not submitted, with outstanding `requirements.currently_due`.
88
- const ACCOUNT: StripeRow = {
89
- id: 'acct_twin001', object: 'account', type: 'express', country: 'US', email: 'seller@example.com',
90
- charges_enabled: false, payouts_enabled: false, details_submitted: false,
91
- capabilities: { card_payments: 'inactive' },
92
- requirements: { currently_due: ['external_account', 'tos_acceptance.date'], eventually_due: [], past_due: [], pending_verification: [] },
93
- };
94
- // A transfer from the platform balance to the connected account above (destination cross-ref).
95
- const TRANSFER: StripeRow = {
96
- id: 'tr_twin001', object: 'transfer', amount: 16800, currency: 'usd', destination: 'acct_twin001',
97
- reversed: false, amount_reversed: 0,
98
- };
26
+ const CUSTOMER: StripeRow = { id: 'cus_twin001', object: 'customer', name: 'Ada Lovelace', email: 'ada@example.com', created: AT, invoice_settings: { default_payment_method: 'pm_twin001' }, metadata: {} };
27
+ const CARD: StripeRow = { 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: {} };
28
+ const INTENT: StripeRow = { 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: {} };
29
+ const CHARGE: StripeRow = { 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 };
30
+ const REFUND: StripeRow = { 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 };
31
+ const event = (id: string, type: string, object: StripeRow, created: number): StripeRow => ({ id, object: 'event', type, created, data: { object } });
99
32
 
100
- // A Stripe Tax rate (reusable) and a tax calculation with priced line_items (nested).
101
- const TAX_RATE: StripeRow = {
102
- id: 'txr_twin001', object: 'tax_rate', display_name: 'Sales Tax', percentage: 8.5,
103
- inclusive: false, active: true, jurisdiction: 'US',
33
+ const PARTIAL: PaymentDetail = {
34
+ id: 'pi_twin001', amount: 30000, currency: 'usd', status: 'partially_refunded', description: 'Annual workshop seat', created: AT,
35
+ customer: CUSTOMER, charge: CHARGE, intent: INTENT, paymentMethod: CARD, amountRefunded: 10000, refundedAt: AT + 60,
36
+ refunds: [REFUND],
37
+ events: [
38
+ event('evt_3', 'charge.refunded', CHARGE, AT + 60),
39
+ event('evt_2', 'payment_intent.succeeded', INTENT, AT + 1),
40
+ event('evt_1', 'payment_intent.created', INTENT, AT),
41
+ ],
42
+ };
43
+ const DECLINED_INTENT: StripeRow = {
44
+ id: 'pi_twin009', object: 'payment_intent', amount: 15000, currency: 'usd', status: 'requires_payment_method', customer: 'cus_twin001', created: AT,
45
+ last_payment_error: { type: 'card_error', code: 'card_declined', decline_code: 'insufficient_funds', message: 'Your card has insufficient funds.', param: 'card' },
104
46
  };
105
- const TAX_CALCULATION: StripeRow = {
106
- id: 'taxcalc_twin001', object: 'tax.calculation', currency: 'usd', amount_total: 1100,
107
- tax_amount_exclusive: 100, customer: 'cus_twin001',
108
- line_items: {
109
- object: 'list',
110
- data: [
111
- { id: 'tax_li_taxcalc_twin001_1', object: 'tax.calculation_line_item', amount: 1000, amount_tax: 100, reference: 'sku_1', tax_behavior: 'exclusive' },
112
- ],
113
- },
47
+ const DECLINED: PaymentDetail = {
48
+ id: 'pi_twin009', amount: 15000, currency: 'usd', status: 'failed', created: AT, customer: CUSTOMER, intent: DECLINED_INTENT,
49
+ amountRefunded: 0, declineReason: 'Insufficient funds', declineMessage: 'Your card has insufficient funds.', refunds: [],
50
+ events: [event('evt_9', 'payment_intent.payment_failed', DECLINED_INTENT, AT)],
114
51
  };
52
+ const REFUNDED: Payment = { id: 'pi_twin002', amount: 2500, currency: 'usd', status: 'refunded', description: 'Twin T-shirt', created: AT, customer: CUSTOMER, paymentMethod: CARD, amountRefunded: 2500, refundedAt: AT + 30 };
53
+ const PAYMENTS: Payment[] = [PARTIAL, DECLINED, REFUNDED, { ...REFUNDED, id: 'pi_twin003', status: 'succeeded', amountRefunded: 0, refundedAt: undefined, description: 'Consulting' }];
115
54
 
116
- // A credit note crediting back a paid invoice, with a nested credit_note_line_item, and
117
- // cross-refs to its invoice + customer. The mirror's Credit Notes section renders it.
118
- const CREDIT_NOTE: StripeRow = {
119
- id: 'cn_twin001', object: 'credit_note', amount: 5000, currency: 'usd', status: 'issued',
120
- invoice: 'in_twin001', customer: 'cus_twin001', type: 'post_payment', number: 'in_twin001-CN-01',
121
- lines: {
122
- object: 'list',
123
- data: [
124
- { id: 'cnli_cn_twin001_1', object: 'credit_note_line_item', type: 'custom_line_item', amount: 5000, description: 'goodwill credit' },
125
- ],
126
- },
127
- };
128
- // A customer tax ID (EU VAT) on the customer above (customer cross-ref).
129
- const TAX_ID: StripeRow = {
130
- id: 'txi_twin001', object: 'tax_id', type: 'eu_vat', value: 'DE123456789',
131
- customer: 'cus_twin001', country: 'DE', verification: { status: 'pending' },
132
- };
133
- // A customer balance transaction (a -2000 credit) with its running ending_balance.
134
- const CUSTOMER_BALANCE_TXN: StripeRow = {
135
- id: 'cbtxn_twin001', object: 'customer_balance_transaction', amount: -2000, currency: 'usd',
136
- ending_balance: -2000, type: 'adjustment', customer: 'cus_twin001', credit_note: 'cn_twin001',
55
+ const PRODUCT: StripeRow = { id: 'prod_twin001', object: 'product', name: 'Pro plan' };
56
+ const SUBSCRIPTION: StripeRow = {
57
+ id: 'sub_twin001', object: 'subscription', status: 'trialing', customer: 'cus_twin001', currency: 'usd', created: AT, start_date: AT,
58
+ current_period_start: AT, current_period_end: AT + 14 * 86400, trial_start: AT, trial_end: AT + 14 * 86400, collection_method: 'charge_automatically',
59
+ 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' } } }] },
137
60
  };
61
+ const INVOICE: StripeRow = { 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 };
62
+ const CUSTOMER_PAGE: CustomerDetail = { customer: CUSTOMER, paymentMethods: [CARD], subscriptions: [SUBSCRIPTION], invoices: [INVOICE], payments: PAYMENTS, products: new Map([[PRODUCT.id, PRODUCT]]), events: [] };
63
+ const SUBSCRIPTION_PAGE: SubscriptionDetail = { subscription: SUBSCRIPTION, customer: CUSTOMER, invoices: [INVOICE], products: new Map([[PRODUCT.id, PRODUCT]]), events: [event('evt_s', 'customer.subscription.created', SUBSCRIPTION, AT)], paymentMethod: CARD };
138
64
 
139
- const DATA: Record<string, StripeRow[]> = {
140
- customers: [CUSTOMER],
141
- credit_notes: [CREDIT_NOTE],
142
- tax_ids: [TAX_ID],
143
- customer_balance_transactions: [CUSTOMER_BALANCE_TXN],
144
- tax_rates: [TAX_RATE],
145
- 'tax/calculations': [TAX_CALCULATION],
146
- products: [PRODUCT],
147
- prices: [PRICE],
148
- subscriptions: [SUBSCRIPTION],
149
- invoices: [INVOICE],
150
- disputes: [DISPUTE],
151
- payouts: [PAYOUT],
152
- payment_intents: [DECLINED_PAYMENT],
153
- balance: [BALANCE],
154
- accounts: [ACCOUNT],
155
- transfers: [TRANSFER],
65
+ const BALANCE: StripeRow = { id: 'balance', object: 'balance', available: [{ amount: 16800, currency: 'usd' }], pending: [{ amount: 4200, currency: 'usd' }] };
66
+ const ACCOUNT: StripeRow = {
67
+ id: 'acct_twin001', object: 'account', type: 'express', country: 'US', charges_enabled: false, payouts_enabled: false, details_submitted: false,
68
+ requirements: { currently_due: ['external_account', 'tos_acceptance.date'] },
156
69
  };
70
+ const TAX_RATE: StripeRow = { id: 'txr_twin001', object: 'tax_rate', display_name: 'Sales Tax', percentage: 8.5, inclusive: false, active: true, jurisdiction: 'US', created: AT };
157
71
 
158
- const noop = () => {};
159
-
160
- /** renderToStaticMarkup the left nav (one item + count per collection). */
161
- function renderNav(): string {
162
- const counts = Object.fromEntries(SECTIONS.map((s) => [s.key, DATA[s.key]?.length ?? 0]));
163
- return renderToStaticMarkup(
164
- createElement(SideNav, { sections: SECTIONS, counts, activeKey: SECTIONS[0]!.key, onSelect: noop }),
165
- );
166
- }
72
+ const html = (el: Parameters<typeof renderToStaticMarkup>[0]) => renderToStaticMarkup(el);
73
+ const count = (markup: string, needle: RegExp) => (markup.match(needle) ?? []).length;
167
74
 
168
- /** renderToStaticMarkup the list pane for a collection (a row per object). */
169
- function renderList(collection: string, rows: StripeRow[]): string {
170
- return renderToStaticMarkup(
171
- createElement(ListPane, { section: SECTION_BY_KEY[collection]!, rows, currentId: rows[0]?.id, onSelect: noop }),
172
- );
173
- }
174
-
175
- /** renderToStaticMarkup the detail pane for a row, with its resolved cross-refs. */
176
- function renderDetail(collection: string, row: StripeRow): string {
177
- const refs: CrossRefs = resolveCrossRefs(collection, row, DATA);
178
- return renderToStaticMarkup(
179
- createElement(Detail, { collection, row, refs, onJump: noop }),
180
- );
181
- }
182
-
183
- /**
184
- * Build the structural checklist from the real rendered markup and run it.
185
- * ~10 checks, every one asserting a Stripe-dashboard structural landmark.
186
- */
75
+ /** Build the structural checklist from the real rendered markup and run it. */
187
76
  export function stripeUiStructure(): UiStructureReport {
188
- const nav = renderNav();
189
- const customersList = renderList('customers', DATA.customers!);
190
- const subscriptionDetail = renderDetail('subscriptions', SUBSCRIPTION);
191
- const invoiceDetail = renderDetail('invoices', INVOICE);
192
- const productDetail = renderDetail('products', PRODUCT);
193
- const customerDetail = renderDetail('customers', CUSTOMER);
194
- const declinedPaymentDetail = renderDetail('payment_intents', DECLINED_PAYMENT);
195
- const balanceDetail = renderDetail('balance', BALANCE);
196
- const accountDetail = renderDetail('accounts', ACCOUNT);
197
- const transferDetail = renderDetail('transfers', TRANSFER);
198
- const taxRatesList = renderList('tax_rates', DATA.tax_rates!);
199
- const taxCalculationDetail = renderDetail('tax/calculations', TAX_CALCULATION);
200
- const creditNotesList = renderList('credit_notes', DATA.credit_notes!);
201
- const creditNoteDetail = renderDetail('credit_notes', CREDIT_NOTE);
202
- const taxIdsList = renderList('tax_ids', DATA.tax_ids!);
203
- const customerBalanceTxnDetail = renderDetail('customer_balance_transactions', CUSTOMER_BALANCE_TXN);
204
-
205
- // How many collection nav items the markup emits. The left nav now also renders the Home
206
- // and Settings chrome buttons (which carry no per-collection count), so the per-collection
207
- // landmark counts the items that carry a nav-count — exactly one per collection.
208
- const navItems = (nav.match(/nav-count/g) ?? []).length;
209
- const customerRows = (customersList.match(/list-row/g) ?? []).length;
77
+ const nav = html(createElement(SideNav, { route: ['payments'], go: noop }));
78
+ const band = html(createElement(TestModeBand));
79
+ const payments = html(createElement(PaymentsTable, { payments: PAYMENTS, go: noop }));
80
+ const partial = html(createElement(PaymentDetailView, { p: PARTIAL, go: noop, onRefund: noop }));
81
+ const declined = html(createElement(PaymentDetailView, { p: DECLINED, go: noop, onRefund: noop }));
82
+ const customer = html(createElement(CustomerDetailView, { data: CUSTOMER_PAGE, go: noop }));
83
+ const subscription = html(createElement(SubscriptionDetailView, { data: SUBSCRIPTION_PAGE, go: noop, onCancel: noop }));
84
+ const balance = html(createElement(BalanceSummary, { row: BALANCE }));
85
+ const account = html(createElement(ConnectAccountPanel, { row: ACCOUNT }));
86
+ const taxRates = html(createElement(ListPane, { section: SECTIONS.find((s) => s.key === 'tax_rates')!, rows: [TAX_RATE], onSelect: noop }));
210
87
 
211
88
  const checks = [
212
89
  {
213
- key: 'navItemPerCollection',
214
- label: 'Left nav emits a nav item per collection (Customers/Payments/Subscriptions/Invoices/Products/…)',
215
- present: navItems === SECTIONS.length && nav.includes('class="side"'),
216
- },
217
- {
218
- key: 'navCounts',
219
- label: 'Each left-nav item carries a per-collection object count',
220
- present: (nav.match(/nav-count/g) ?? []).length === SECTIONS.length,
221
- },
222
- {
223
- key: 'listRowPerObject',
224
- label: 'List pane emits one row per object in the section',
225
- present: customerRows === DATA.customers!.length && customersList.includes('class="list"'),
90
+ key: 'navigation',
91
+ label: "Left navigation: Home, Balances, Transactions, Customers, Product catalog, Billing's Subscriptions and Invoices, Developers — at Stripe's /test/ paths",
92
+ present: ['>Home<', '>Balances<', '>Transactions<', '>Customers<', '>Product catalog<', '>Subscriptions<', '>Invoices<', '>Developers<'].every((l) => nav.includes(l))
93
+ && nav.includes('href="/test/payments"') && nav.includes('href="/test/customers"') && nav.includes('href="/test/subscriptions"'),
226
94
  },
227
95
  {
228
- key: 'detailScalarGrid',
229
- label: 'Detail pane renders a scalar field key/value grid (<dl>/<dt>/<dd>)',
230
- present: customerDetail.includes('<dl>') && customerDetail.includes('<dt>') && customerDetail.includes('<dd'),
96
+ key: 'navActive',
97
+ label: 'The navigation marks the section being viewed',
98
+ present: /class="nav-item [^"]*active"[^>]*href="\/test\/payments"|href="\/test\/payments" class="nav-item [^"]*active"/.test(nav),
231
99
  },
232
100
  {
233
- key: 'detailHeaderId',
234
- label: 'Detail pane shows the object id in its header',
235
- present: customerDetail.includes('detail-head') && customerDetail.includes(CUSTOMER.id),
101
+ key: 'testModeBand',
102
+ label: "The orange test-mode band across the top",
103
+ present: band.includes('class="test-band"') && band.includes('Test mode'),
236
104
  },
237
105
  {
238
- key: 'nestedLinesNotObjectObject',
239
- label: 'Nested objects/arrays render as indented nested lines, never "[object Object]"',
240
- present:
241
- subscriptionDetail.includes('class="nested"') &&
242
- subscriptionDetail.includes('nested-line') &&
243
- !subscriptionDetail.includes('[object Object]') &&
244
- !invoiceDetail.includes('[object Object]'),
106
+ key: 'paymentsColumns',
107
+ label: "Payments table carries Stripe's columns (Amount, Payment method, Description, Customer, Date, Refunded date, Decline reason)",
108
+ present: ['>Amount<', '>Payment method<', '>Description<', '>Customer<', '>Date<', '>Refunded date<', '>Decline reason<'].every((c) => payments.includes(c)),
245
109
  },
246
110
  {
247
- key: 'nestedListItems',
248
- label: 'Nested Stripe list wrappers (subscription items / invoice lines) flatten to their data rows',
249
- present:
250
- subscriptionDetail.includes('si_twin001') &&
251
- invoiceDetail.includes('il_twin001'),
111
+ key: 'paymentsRowPerPayment',
112
+ label: 'Payments table emits one row per payment, amount with its currency',
113
+ present: count(payments, /class="list-row"/g) === PAYMENTS.length && payments.includes('$300.00') && payments.includes('USD'),
252
114
  },
253
115
  {
254
- key: 'statusPill',
255
- label: 'A status pill element renders for objects with a status (active/paid/…)',
256
- present: subscriptionDetail.includes('class="pill') && invoiceDetail.includes('class="pill'),
116
+ key: 'paymentsStatusPills',
117
+ label: 'Status pills worded as Stripe words them (Succeeded, Refunded, Partial refund, Failed)',
118
+ present: ['>Succeeded<', '>Refunded<', '>Partial refund<', '>Failed<'].every((l) => payments.includes(l)),
257
119
  },
258
120
  {
259
- key: 'relatedCrossRefBlock',
260
- label: 'A cross-ref "Related" block renders outgoing reference links',
261
- present:
262
- invoiceDetail.includes('detail-block refs') &&
263
- invoiceDetail.includes('>Related<') &&
264
- invoiceDetail.includes('>References<') &&
265
- (invoiceDetail.match(/ref-chip/g) ?? []).length >= 1,
121
+ key: 'paymentsDeclineReason',
122
+ label: 'A failed payment shows its decline reason in the list',
123
+ present: payments.includes('Insufficient funds'),
266
124
  },
267
125
  {
268
- key: 'relatedIncomingRefs',
269
- label: 'The "Related" block renders incoming "Referenced by" links (objects pointing at this row)',
270
- // The customer is pointed AT by the subscription + invoice → incoming chips.
271
- present:
272
- customerDetail.includes('detail-block refs') &&
273
- customerDetail.includes('>Referenced by<') &&
274
- (customerDetail.match(/ref-chip/g) ?? []).length >= 1,
126
+ key: 'paymentHeader',
127
+ label: "A payment's page leads with its amount, currency and status, and a Refund control while refundable",
128
+ present: partial.includes('$300.00') && partial.includes('Partial refund') && partial.includes('Refund</button>'),
275
129
  },
276
130
  {
277
- key: 'crossRefResolvesTargetLabel',
278
- label: 'Cross-ref links resolve the target object label (not just a bare id)',
279
- // invoice → customer outgoing chip carries the resolved customer name.
280
- present: invoiceDetail.includes('Ada Lovelace'),
131
+ key: 'paymentTimeline',
132
+ label: "A payment's timeline lists its events (started, succeeded, refunded)",
133
+ present: partial.includes('class="timeline"') && partial.includes('Payment started') && partial.includes('Payment succeeded') && partial.includes('refunded'),
281
134
  },
282
135
  {
283
- key: 'productImageThumbs',
284
- label: 'Product images render as <img> thumbnails',
285
- present:
286
- productDetail.includes('class="thumbs"') &&
287
- (productDetail.match(/<img/g) ?? []).length === productImageUrls(PRODUCT.images).length &&
288
- productImageUrls(PRODUCT.images).length > 0,
136
+ key: 'paymentBreakdownAndRefunds',
137
+ label: "A payment's breakdown (amount, refunded, net) and its refunds",
138
+ present: partial.includes('Payment breakdown') && partial.includes('$200.00') && partial.includes('>Refunds<') && partial.includes('re_twin001'),
289
139
  },
290
140
  {
291
- key: 'paymentDeclineBanner',
292
- label: 'A declined payment_intent renders its last_payment_error as a prominent banner (decline reason + code)',
293
- present:
294
- declinedPaymentDetail.includes('class="pay-error"') &&
295
- declinedPaymentDetail.includes('Payment error') &&
296
- declinedPaymentDetail.includes('card_declined') &&
297
- declinedPaymentDetail.includes('insufficient_funds') &&
298
- // lifted out of the generic grid, not buried as a raw nested object
299
- !declinedPaymentDetail.includes('[object Object]'),
300
- },
301
- {
302
- key: 'balanceSummaryBuckets',
303
- label: 'The balance section renders available/pending summary buckets with formatted amounts',
304
- present:
305
- balanceDetail.includes('class="balance-summary"') &&
306
- balanceDetail.includes('balance-bucket available') &&
307
- (balanceDetail.match(/balance-bucket/g) ?? []).length >= 2 &&
308
- balanceDetail.includes('$168.00') &&
309
- balanceDetail.includes('$42.00'),
141
+ key: 'paymentMethodBlock',
142
+ label: "A payment's card: number's last four, expiry, type, CVC check",
143
+ present: partial.includes('4242') && partial.includes('12 / 2034') && partial.includes('Visa credit card') && partial.includes('Passed'),
310
144
  },
311
145
  {
312
- key: 'connectAccountFlags',
313
- label: 'A connected account renders its enablement flags (charges/payouts/details_submitted) as tone-carrying tiles',
314
- present:
315
- accountDetail.includes('class="connect-panel"') &&
316
- (accountDetail.match(/connect-flag /g) ?? []).length >= 3 &&
317
- accountDetail.includes('>Charges<') &&
318
- accountDetail.includes('>Payouts<') &&
319
- accountDetail.includes('>Details submitted<') &&
320
- // a freshly-created account is not enabled → disabled state shown
321
- accountDetail.includes('>disabled<'),
146
+ key: 'paymentDecline',
147
+ label: 'A declined payment states its error and decline code, and offers no refund',
148
+ present: declined.includes('class="pay-error"') && declined.includes('Your card has insufficient funds.') && declined.includes('insufficient_funds') && !declined.includes('Refund</button>'),
322
149
  },
323
150
  {
324
- key: 'connectAccountRequirements',
325
- label: 'A connected account renders its outstanding onboarding requirements (currently_due)',
326
- present:
327
- accountDetail.includes('class="connect-requirements"') &&
328
- accountDetail.includes('Requirements currently due') &&
329
- accountDetail.includes('external_account') &&
330
- accountDetail.includes('tos_acceptance.date'),
151
+ key: 'customerPage',
152
+ label: "A customer's page: name, email, Details, Subscriptions, Payments, Payment methods, Invoices",
153
+ present: customer.includes('Ada Lovelace') && customer.includes('ada@example.com')
154
+ && ['>Details<', '>Subscriptions<', '>Payments<', '>Payment methods<', '>Invoices<'].every((h) => customer.includes(h))
155
+ && customer.includes('Pro plan') && customer.includes('TWIN-0001') && customer.includes('>Default<'),
331
156
  },
332
157
  {
333
- key: 'transferDestinationCrossRef',
334
- label: 'A transfer renders a cross-ref link to its destination connected account',
335
- present:
336
- transferDetail.includes('detail-block refs') &&
337
- (transferDetail.match(/ref-chip/g) ?? []).length >= 1 &&
338
- transferDetail.includes('acct_twin001'),
158
+ key: 'subscriptionPage',
159
+ label: "A subscription's page: customer on product, status, period, pricing items, invoices and Cancel subscription",
160
+ present: subscription.includes('Ada Lovelace') && subscription.includes('Pro plan') && subscription.includes('>Trialing<')
161
+ && subscription.includes('Current period') && subscription.includes('>Pricing<') && subscription.includes('$147.00')
162
+ && subscription.includes('TWIN-0001') && subscription.includes('Cancel subscription'),
339
163
  },
340
164
  {
341
- key: 'taxRatesListRow',
342
- label: 'The Tax Rates section emits a list row per rate (display_name + percentage)',
343
- present:
344
- (taxRatesList.match(/list-row/g) ?? []).length === DATA.tax_rates!.length &&
345
- taxRatesList.includes('Sales Tax') &&
346
- taxRatesList.includes('8.5'),
347
- },
348
- {
349
- key: 'taxCalculationLineItems',
350
- label: 'A tax calculation renders its priced line_items as nested lines (per-item tax)',
351
- present:
352
- taxCalculationDetail.includes('class="nested"') &&
353
- taxCalculationDetail.includes('tax_li_taxcalc_twin001_1') &&
354
- !taxCalculationDetail.includes('[object Object]'),
355
- },
356
- {
357
- key: 'taxCalculationTotals',
358
- label: 'A tax calculation renders its amount_total and tax_amount_exclusive (currency-formatted)',
359
- present:
360
- taxCalculationDetail.includes('$11.00') &&
361
- taxCalculationDetail.includes('$1.00'),
362
- },
363
- {
364
- key: 'creditNotesListRow',
365
- label: 'The Credit Notes section emits a list row per credit note (credited amount + status)',
366
- present:
367
- (creditNotesList.match(/list-row/g) ?? []).length === DATA.credit_notes!.length &&
368
- creditNotesList.includes('$50.00') &&
369
- creditNotesList.includes('issued'),
370
- },
371
- {
372
- key: 'creditNoteLineItems',
373
- label: 'A credit note renders its nested line_items (per-line credited amount), never "[object Object]"',
374
- present:
375
- creditNoteDetail.includes('class="nested"') &&
376
- creditNoteDetail.includes('cnli_cn_twin001_1') &&
377
- !creditNoteDetail.includes('[object Object]'),
378
- },
379
- {
380
- key: 'creditNoteInvoiceCrossRef',
381
- label: 'A credit note renders a cross-ref link to the invoice it credits',
382
- present:
383
- creditNoteDetail.includes('detail-block refs') &&
384
- (creditNoteDetail.match(/ref-chip/g) ?? []).length >= 1 &&
385
- creditNoteDetail.includes('in_twin001'),
165
+ key: 'balanceSummaryBuckets',
166
+ label: 'The balance renders its incoming and available buckets, amounts formatted',
167
+ present: balance.includes('class="balance-summary"') && balance.includes('Incoming') && balance.includes('Available') && balance.includes('$168.00') && balance.includes('$42.00'),
386
168
  },
387
169
  {
388
- key: 'customerTaxIdsListRow',
389
- label: 'The Customer Tax IDs section emits a list row per tax ID (value + type)',
390
- present:
391
- (taxIdsList.match(/list-row/g) ?? []).length === DATA.tax_ids!.length &&
392
- taxIdsList.includes('DE123456789') &&
393
- taxIdsList.includes('eu_vat'),
170
+ key: 'connectAccountReadiness',
171
+ label: "A connected account's charges/payouts/details flags and what is currently due",
172
+ present: account.includes('class="connect-panel"') && count(account, /connect-flag /g) >= 3 && account.includes('Requirements currently due') && account.includes('external_account'),
394
173
  },
395
174
  {
396
- key: 'customerBalanceTxnDetail',
397
- label: 'A customer balance transaction renders its amount + running ending_balance (formatted) with a customer cross-ref',
398
- present:
399
- customerBalanceTxnDetail.includes('detail-block refs') &&
400
- customerBalanceTxnDetail.includes('-$20.00') &&
401
- (customerBalanceTxnDetail.match(/ref-chip/g) ?? []).length >= 1,
175
+ key: 'resourceListRow',
176
+ label: 'A resource list emits one row per object (Tax rates: name and percentage)',
177
+ present: count(taxRates, /list-row/g) === 1 && taxRates.includes('Sales Tax') && taxRates.includes('8.5'),
402
178
  },
403
179
  ];
404
180