@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,107 @@
1
+ export type StripeObject = Record<string, any>;
2
+ export declare function wireBaseFrom(hasBaseElement: boolean, baseURI: string): string;
3
+ export declare const API_BASE: string;
4
+ export declare class StripeApiError extends Error {
5
+ readonly code?: string | undefined;
6
+ constructor(message: string, code?: string | undefined);
7
+ }
8
+ export declare const retrieve: (path: string) => Promise<StripeObject>;
9
+ /** A list endpoint's `data`, newest first as Stripe returns it (one page of up to 100). */
10
+ export declare function list(path: string): Promise<StripeObject[]>;
11
+ /** One row of the Payments list: a PaymentIntent (or a Charge made without one) with what the Dashboard
12
+ * shows beside it — its charge, refunds, card and customer. */
13
+ export type Payment = {
14
+ id: string;
15
+ amount: number;
16
+ currency: string;
17
+ status: PaymentStatus;
18
+ description?: string;
19
+ created: number;
20
+ customer?: StripeObject;
21
+ charge?: StripeObject;
22
+ intent?: StripeObject;
23
+ paymentMethod?: StripeObject;
24
+ amountRefunded: number;
25
+ refundedAt?: number;
26
+ declineReason?: string;
27
+ declineMessage?: string;
28
+ };
29
+ export type PaymentStatus = 'succeeded' | 'refunded' | 'partially_refunded' | 'failed' | 'incomplete' | 'uncaptured' | 'canceled' | 'disputed' | 'processing';
30
+ export declare function paymentStatusOf(intent: StripeObject | undefined, charge: StripeObject | undefined): PaymentStatus;
31
+ export declare const declineReasonText: (code: string | undefined) => string | undefined;
32
+ /** The Payments list: every PaymentIntent, and every Charge made without one. */
33
+ export declare function loadPayments(): Promise<Payment[]>;
34
+ export type PaymentDetail = Payment & {
35
+ refunds: StripeObject[];
36
+ events: StripeObject[];
37
+ invoice?: StripeObject;
38
+ };
39
+ /** One payment's page: the intent (or charge), its charge, refunds, card, customer and the events about it. */
40
+ export declare function loadPayment(id: string): Promise<PaymentDetail>;
41
+ export type CustomerRow = StripeObject & {
42
+ defaultCard?: StripeObject;
43
+ totalSpend: number;
44
+ payments: number;
45
+ refunds: number;
46
+ currency: string;
47
+ };
48
+ export declare function loadCustomers(): Promise<CustomerRow[]>;
49
+ export type CustomerDetail = {
50
+ customer: StripeObject;
51
+ paymentMethods: StripeObject[];
52
+ subscriptions: StripeObject[];
53
+ invoices: StripeObject[];
54
+ payments: Payment[];
55
+ products: Map<string, StripeObject>;
56
+ events: StripeObject[];
57
+ };
58
+ export declare function loadCustomer(id: string): Promise<CustomerDetail>;
59
+ export type Billing = {
60
+ subscriptions: StripeObject[];
61
+ customers: Map<string, StripeObject>;
62
+ products: Map<string, StripeObject>;
63
+ };
64
+ export declare function loadSubscriptions(): Promise<Billing>;
65
+ export type SubscriptionDetail = {
66
+ subscription: StripeObject;
67
+ customer?: StripeObject;
68
+ invoices: StripeObject[];
69
+ products: Map<string, StripeObject>;
70
+ events: StripeObject[];
71
+ paymentMethod?: StripeObject;
72
+ };
73
+ export declare function loadSubscription(id: string): Promise<SubscriptionDetail>;
74
+ export declare function loadInvoices(): Promise<{
75
+ invoices: StripeObject[];
76
+ customers: Map<string, StripeObject>;
77
+ }>;
78
+ export declare function loadProducts(): Promise<{
79
+ products: StripeObject[];
80
+ prices: StripeObject[];
81
+ }>;
82
+ export type HomeData = {
83
+ balance: StripeObject;
84
+ payments: Payment[];
85
+ customers: StripeObject[];
86
+ subscriptions: StripeObject[];
87
+ invoices: StripeObject[];
88
+ };
89
+ export declare function loadHome(): Promise<HomeData>;
90
+ export declare function loadBalances(): Promise<{
91
+ balance: StripeObject;
92
+ transactions: StripeObject[];
93
+ payouts: StripeObject[];
94
+ }>;
95
+ export declare function loadDevelopers(): Promise<{
96
+ events: StripeObject[];
97
+ webhooks: StripeObject[];
98
+ account?: StripeObject;
99
+ }>;
100
+ /** Refund a payment in full (no amount) or in part. */
101
+ export declare function createRefund(payment: {
102
+ intent?: StripeObject;
103
+ charge?: StripeObject;
104
+ }, amount: number | undefined, reason: string | undefined): Promise<StripeObject>;
105
+ /** Cancel now (DELETE) or at the end of the current period (cancel_at_period_end). */
106
+ export declare function cancelSubscription(id: string, when: 'now' | 'period_end'): Promise<StripeObject>;
107
+ export declare const createObject: (collection: string, fields: Record<string, string>) => Promise<StripeObject>;
@@ -0,0 +1,238 @@
1
+ // The Stripe Dashboard's reads and writes, through Stripe's own API on this origin (docs/contributing/
2
+ // architecture.md, "Screens": a workspace reads and writes only through the pack's operations). The calls
3
+ // the Dashboard makes for the pages it shows: the balance, payments (PaymentIntents and the Charges they
4
+ // made), refunds, customers and their saved payment methods, subscriptions, invoices, products and prices,
5
+ // and events for a detail page's timeline. The writes a person makes here: refund a payment
6
+ // (POST /v1/refunds), cancel a subscription (DELETE /v1/subscriptions/{id}, or schedule it with
7
+ // cancel_at_period_end), create a customer or a product. Bodies are form-encoded as Stripe's API takes them;
8
+ // an `error` envelope, never the HTTP status alone, is what refuses.
9
+ export function wireBaseFrom(hasBaseElement, baseURI) {
10
+ return hasBaseElement ? new URL('.', baseURI).pathname.replace(/\/$/, '') : '';
11
+ }
12
+ // Served at the origin root the base is ''; served under a path prefix (an embedder's earlier <base>) every
13
+ // call resolves inside that prefix.
14
+ export const API_BASE = typeof document === 'undefined' ? '' : wireBaseFrom(document.querySelector('base[href]') !== null, document.baseURI);
15
+ export class StripeApiError extends Error {
16
+ code;
17
+ constructor(message, code) {
18
+ super(message);
19
+ this.code = code;
20
+ }
21
+ }
22
+ async function call(method, path, body) {
23
+ const init = { method, headers: { accept: 'application/json' } };
24
+ if (body) {
25
+ const form = Object.entries(body).filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
26
+ init.body = form;
27
+ init.headers['content-type'] = 'application/x-www-form-urlencoded';
28
+ }
29
+ const res = await fetch(`${API_BASE}${path}`, init);
30
+ const json = await res.json().catch(() => ({ error: { message: `${res.status} ${res.statusText}` } }));
31
+ if (json && json.error)
32
+ throw new StripeApiError(String(json.error.message ?? 'Request failed'), json.error.code);
33
+ return json;
34
+ }
35
+ export const retrieve = (path) => call('GET', path);
36
+ /** A list endpoint's `data`, newest first as Stripe returns it (one page of up to 100). */
37
+ export async function list(path) {
38
+ const sep = path.includes('?') ? '&' : '?';
39
+ const body = await call('GET', `${path}${sep}limit=100`);
40
+ return Array.isArray(body?.data) ? body.data : [];
41
+ }
42
+ const optional = (p, fallback) => p.catch(() => fallback);
43
+ export function paymentStatusOf(intent, charge) {
44
+ if (charge?.disputed)
45
+ return 'disputed';
46
+ if (charge?.refunded)
47
+ return 'refunded';
48
+ if (charge && Number(charge.amount_refunded) > 0)
49
+ return 'partially_refunded';
50
+ if (charge?.status === 'failed')
51
+ return 'failed';
52
+ if (intent) {
53
+ if (intent.status === 'succeeded')
54
+ return 'succeeded';
55
+ if (intent.status === 'canceled')
56
+ return 'canceled';
57
+ if (intent.status === 'requires_capture')
58
+ return 'uncaptured';
59
+ if (intent.status === 'processing')
60
+ return 'processing';
61
+ if (intent.last_payment_error)
62
+ return 'failed';
63
+ return 'incomplete';
64
+ }
65
+ return charge?.status === 'succeeded' ? 'succeeded' : charge?.status === 'pending' ? 'processing' : 'failed';
66
+ }
67
+ const DECLINE_REASONS = {
68
+ generic_decline: 'Generic decline', insufficient_funds: 'Insufficient funds', lost_card: 'Lost card', stolen_card: 'Stolen card',
69
+ expired_card: 'Expired card', incorrect_cvc: 'Incorrect CVC', processing_error: 'Processing error', card_declined: 'Card declined',
70
+ };
71
+ export const declineReasonText = (code) => (code ? DECLINE_REASONS[code] ?? code.replace(/_/g, ' ') : undefined);
72
+ async function paymentMethodsById(ids) {
73
+ const unique = [...new Set([...ids].filter((id) => typeof id === 'string' && id.startsWith('pm_')))];
74
+ const found = await Promise.all(unique.map((id) => optional(retrieve(`/v1/payment_methods/${id}`), undefined)));
75
+ return new Map(found.filter((pm) => Boolean(pm?.id)).map((pm) => [pm.id, pm]));
76
+ }
77
+ function paymentOf(intent, charge, refunds, dir) {
78
+ const base = intent ?? charge;
79
+ const customerId = typeof base.customer === 'string' ? base.customer : base.customer?.id;
80
+ const pmId = typeof base.payment_method === 'string' ? base.payment_method : charge?.payment_method;
81
+ const error = intent?.last_payment_error;
82
+ const refundsOf = refunds.filter((r) => (intent && r.payment_intent === intent.id) || (charge && r.charge === charge.id));
83
+ return {
84
+ id: String(base.id),
85
+ amount: Number(base.amount) || 0,
86
+ currency: String(base.currency ?? 'usd'),
87
+ status: paymentStatusOf(intent, charge),
88
+ description: base.description ?? charge?.description ?? undefined,
89
+ created: Number(base.created) || 0,
90
+ customer: customerId ? dir.customers.get(customerId) ?? { id: customerId } : undefined,
91
+ charge, intent,
92
+ paymentMethod: pmId ? dir.paymentMethods.get(pmId) : undefined,
93
+ amountRefunded: Number(charge?.amount_refunded) || refundsOf.reduce((n, r) => n + (r.status === 'failed' ? 0 : Number(r.amount) || 0), 0),
94
+ refundedAt: refundsOf.length ? Math.max(...refundsOf.map((r) => Number(r.created) || 0)) : undefined,
95
+ declineReason: declineReasonText(error?.decline_code ?? error?.code ?? charge?.failure_code),
96
+ declineMessage: error?.message ?? charge?.failure_message ?? undefined,
97
+ };
98
+ }
99
+ /** The Payments list: every PaymentIntent, and every Charge made without one. */
100
+ export async function loadPayments() {
101
+ const [intents, charges, refunds, customers] = await Promise.all([
102
+ list('/v1/payment_intents'), list('/v1/charges'), optional(list('/v1/refunds'), []), list('/v1/customers'),
103
+ ]);
104
+ const chargeById = new Map(charges.map((c) => [c.id, c]));
105
+ const pmIds = [...intents.map((i) => i.payment_method), ...charges.map((c) => c.payment_method)];
106
+ const dir = { customers: new Map(customers.map((c) => [c.id, c])), paymentMethods: await paymentMethodsById(pmIds) };
107
+ const rows = intents.map((pi) => {
108
+ const charge = (typeof pi.latest_charge === 'string' ? chargeById.get(pi.latest_charge) : pi.latest_charge) ?? charges.find((c) => c.payment_intent === pi.id);
109
+ return paymentOf(pi, charge, refunds, dir);
110
+ });
111
+ for (const ch of charges)
112
+ if (!ch.payment_intent)
113
+ rows.push(paymentOf(undefined, ch, refunds, dir));
114
+ return rows.sort((a, b) => b.created - a.created);
115
+ }
116
+ /** One payment's page: the intent (or charge), its charge, refunds, card, customer and the events about it. */
117
+ export async function loadPayment(id) {
118
+ let intent;
119
+ let charge;
120
+ if (id.startsWith('ch_') || id.startsWith('py_')) {
121
+ charge = await retrieve(`/v1/charges/${id}`);
122
+ if (typeof charge.payment_intent === 'string')
123
+ intent = await optional(retrieve(`/v1/payment_intents/${charge.payment_intent}`), undefined);
124
+ }
125
+ else {
126
+ intent = await retrieve(`/v1/payment_intents/${id}`);
127
+ const chargeId = typeof intent.latest_charge === 'string' ? intent.latest_charge : intent.latest_charge?.id;
128
+ if (chargeId)
129
+ charge = await optional(retrieve(`/v1/charges/${chargeId}`), undefined);
130
+ }
131
+ const base = intent ?? charge;
132
+ const customerId = typeof base.customer === 'string' ? base.customer : undefined;
133
+ const pmId = typeof base.payment_method === 'string' ? base.payment_method : charge?.payment_method;
134
+ const refundPath = intent ? `/v1/refunds?payment_intent=${intent.id}` : `/v1/refunds?charge=${charge.id}`;
135
+ const [customer, pms, refunds, events, invoice] = await Promise.all([
136
+ customerId ? optional(retrieve(`/v1/customers/${customerId}`), { id: customerId }) : Promise.resolve(undefined),
137
+ paymentMethodsById(pmId ? [pmId] : []),
138
+ optional(list(refundPath), []),
139
+ optional(list('/v1/events'), []),
140
+ typeof base.invoice === 'string' ? optional(retrieve(`/v1/invoices/${base.invoice}`), undefined) : Promise.resolve(undefined),
141
+ ]);
142
+ const dir = { customers: new Map(customer ? [[customer.id, customer]] : []), paymentMethods: pms };
143
+ const related = new Set([base.id, charge?.id, intent?.id, ...refunds.map((r) => r.id)].filter(Boolean));
144
+ return {
145
+ ...paymentOf(intent, charge, refunds, dir),
146
+ refunds,
147
+ events: events.filter((e) => related.has(e?.data?.object?.id)).sort((a, b) => b.created - a.created),
148
+ invoice,
149
+ };
150
+ }
151
+ export async function loadCustomers() {
152
+ const [customers, charges, intents] = await Promise.all([list('/v1/customers'), list('/v1/charges'), list('/v1/payment_intents')]);
153
+ const pmIds = customers.map((c) => c.invoice_settings?.default_payment_method).filter((x) => typeof x === 'string');
154
+ const pms = await paymentMethodsById(pmIds);
155
+ return customers.map((c) => {
156
+ const mine = charges.filter((ch) => ch.customer === c.id && ch.status === 'succeeded');
157
+ const pmId = c.invoice_settings?.default_payment_method;
158
+ return {
159
+ ...c,
160
+ defaultCard: typeof pmId === 'string' ? pms.get(pmId) : undefined,
161
+ totalSpend: mine.reduce((n, ch) => n + (Number(ch.amount_captured ?? ch.amount) || 0) - (Number(ch.amount_refunded) || 0), 0),
162
+ payments: intents.filter((i) => i.customer === c.id && i.status === 'succeeded').length || mine.length,
163
+ refunds: mine.reduce((n, ch) => n + (Number(ch.amount_refunded) || 0), 0),
164
+ currency: String(mine[0]?.currency ?? c.currency ?? 'usd'),
165
+ };
166
+ });
167
+ }
168
+ export async function loadCustomer(id) {
169
+ const customer = await retrieve(`/v1/customers/${id}`);
170
+ const [paymentMethods, subscriptions, invoices, payments, products, events] = await Promise.all([
171
+ optional(list(`/v1/customers/${id}/payment_methods`), []),
172
+ optional(list(`/v1/subscriptions?customer=${id}&status=all`), []),
173
+ optional(list(`/v1/invoices?customer=${id}`), []),
174
+ loadPayments().then((all) => all.filter((p) => p.customer?.id === id)),
175
+ optional(list('/v1/products'), []),
176
+ optional(list('/v1/events'), []),
177
+ ]);
178
+ const mine = new Set([id, ...subscriptions.map((s) => s.id), ...invoices.map((i) => i.id), ...payments.map((p) => p.id)]);
179
+ return {
180
+ customer, paymentMethods, subscriptions, invoices, payments,
181
+ products: new Map(products.map((p) => [p.id, p])),
182
+ events: events.filter((e) => mine.has(e?.data?.object?.id)).sort((a, b) => b.created - a.created),
183
+ };
184
+ }
185
+ export async function loadSubscriptions() {
186
+ const [subscriptions, customers, products] = await Promise.all([list('/v1/subscriptions?status=all'), list('/v1/customers'), list('/v1/products')]);
187
+ return { subscriptions, customers: new Map(customers.map((c) => [c.id, c])), products: new Map(products.map((p) => [p.id, p])) };
188
+ }
189
+ export async function loadSubscription(id) {
190
+ const subscription = await retrieve(`/v1/subscriptions/${id}`);
191
+ const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id;
192
+ const [customer, invoices, products, events] = await Promise.all([
193
+ customerId ? optional(retrieve(`/v1/customers/${customerId}`), undefined) : Promise.resolve(undefined),
194
+ optional(list(`/v1/invoices?subscription=${id}`), []),
195
+ optional(list('/v1/products'), []),
196
+ optional(list('/v1/events'), []),
197
+ ]);
198
+ const pmId = subscription.default_payment_method ?? customer?.invoice_settings?.default_payment_method;
199
+ const pms = await paymentMethodsById(typeof pmId === 'string' ? [pmId] : []);
200
+ return {
201
+ subscription, customer, invoices, products: new Map(products.map((p) => [p.id, p])),
202
+ events: events.filter((e) => e?.data?.object?.id === id).sort((a, b) => b.created - a.created),
203
+ paymentMethod: typeof pmId === 'string' ? pms.get(pmId) : undefined,
204
+ };
205
+ }
206
+ export async function loadInvoices() {
207
+ const [invoices, customers] = await Promise.all([list('/v1/invoices'), list('/v1/customers')]);
208
+ return { invoices, customers: new Map(customers.map((c) => [c.id, c])) };
209
+ }
210
+ export async function loadProducts() {
211
+ const [products, prices] = await Promise.all([list('/v1/products'), list('/v1/prices')]);
212
+ return { products, prices };
213
+ }
214
+ export async function loadHome() {
215
+ const [balance, payments, customers, subscriptions, invoices] = await Promise.all([
216
+ retrieve('/v1/balance'), loadPayments(), list('/v1/customers'), optional(list('/v1/subscriptions?status=all'), []), optional(list('/v1/invoices'), []),
217
+ ]);
218
+ return { balance, payments, customers, subscriptions, invoices };
219
+ }
220
+ export async function loadBalances() {
221
+ const [balance, transactions, payouts] = await Promise.all([retrieve('/v1/balance'), optional(list('/v1/balance_transactions'), []), optional(list('/v1/payouts'), [])]);
222
+ return { balance, transactions, payouts };
223
+ }
224
+ export async function loadDevelopers() {
225
+ const [events, webhooks, account] = await Promise.all([optional(list('/v1/events'), []), optional(list('/v1/webhook_endpoints'), []), optional(retrieve('/v1/account'), undefined)]);
226
+ return { events, webhooks, account };
227
+ }
228
+ // ── writes ───────────────────────────────────────────────────────────────────
229
+ /** Refund a payment in full (no amount) or in part. */
230
+ export function createRefund(payment, amount, reason) {
231
+ const target = payment.intent ? { payment_intent: String(payment.intent.id) } : { charge: String(payment.charge?.id) };
232
+ return call('POST', '/v1/refunds', { ...target, amount, reason });
233
+ }
234
+ /** Cancel now (DELETE) or at the end of the current period (cancel_at_period_end). */
235
+ export function cancelSubscription(id, when) {
236
+ return when === 'now' ? call('DELETE', `/v1/subscriptions/${id}`) : call('POST', `/v1/subscriptions/${id}`, { cancel_at_period_end: true });
237
+ }
238
+ export const createObject = (collection, fields) => call('POST', `/v1/${collection}`, fields);
@@ -0,0 +1,286 @@
1
+ // The Stripe Dashboard's reads and writes, through Stripe's own API on this origin (docs/contributing/
2
+ // architecture.md, "Screens": a workspace reads and writes only through the pack's operations). The calls
3
+ // the Dashboard makes for the pages it shows: the balance, payments (PaymentIntents and the Charges they
4
+ // made), refunds, customers and their saved payment methods, subscriptions, invoices, products and prices,
5
+ // and events for a detail page's timeline. The writes a person makes here: refund a payment
6
+ // (POST /v1/refunds), cancel a subscription (DELETE /v1/subscriptions/{id}, or schedule it with
7
+ // cancel_at_period_end), create a customer or a product. Bodies are form-encoded as Stripe's API takes them;
8
+ // an `error` envelope, never the HTTP status alone, is what refuses.
9
+
10
+ type Json = any;
11
+ export type StripeObject = Record<string, any>;
12
+
13
+ export function wireBaseFrom(hasBaseElement: boolean, baseURI: string): string {
14
+ return hasBaseElement ? new URL('.', baseURI).pathname.replace(/\/$/, '') : '';
15
+ }
16
+ // Served at the origin root the base is ''; served under a path prefix (an embedder's earlier <base>) every
17
+ // call resolves inside that prefix.
18
+ export const API_BASE = typeof document === 'undefined' ? '' : wireBaseFrom(document.querySelector('base[href]') !== null, document.baseURI);
19
+
20
+ export class StripeApiError extends Error {
21
+ constructor(message: string, readonly code?: string) { super(message); }
22
+ }
23
+
24
+ async function call(method: string, path: string, body?: Record<string, string | number | boolean | undefined>): Promise<Json> {
25
+ const init: RequestInit = { method, headers: { accept: 'application/json' } };
26
+ if (body) {
27
+ const form = Object.entries(body).filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
28
+ init.body = form;
29
+ (init.headers as Record<string, string>)['content-type'] = 'application/x-www-form-urlencoded';
30
+ }
31
+ const res = await fetch(`${API_BASE}${path}`, init);
32
+ const json = await res.json().catch(() => ({ error: { message: `${res.status} ${res.statusText}` } }));
33
+ if (json && json.error) throw new StripeApiError(String(json.error.message ?? 'Request failed'), json.error.code);
34
+ return json;
35
+ }
36
+
37
+ export const retrieve = (path: string): Promise<StripeObject> => call('GET', path);
38
+ /** A list endpoint's `data`, newest first as Stripe returns it (one page of up to 100). */
39
+ export async function list(path: string): Promise<StripeObject[]> {
40
+ const sep = path.includes('?') ? '&' : '?';
41
+ const body = await call('GET', `${path}${sep}limit=100`);
42
+ return Array.isArray(body?.data) ? body.data : [];
43
+ }
44
+ const optional = <T>(p: Promise<T>, fallback: T): Promise<T> => p.catch(() => fallback);
45
+
46
+ // ── what a payment row is ────────────────────────────────────────────────────
47
+ /** One row of the Payments list: a PaymentIntent (or a Charge made without one) with what the Dashboard
48
+ * shows beside it — its charge, refunds, card and customer. */
49
+ export type Payment = {
50
+ id: string;
51
+ amount: number;
52
+ currency: string;
53
+ status: PaymentStatus;
54
+ description?: string;
55
+ created: number;
56
+ customer?: StripeObject;
57
+ charge?: StripeObject;
58
+ intent?: StripeObject;
59
+ paymentMethod?: StripeObject;
60
+ amountRefunded: number;
61
+ refundedAt?: number;
62
+ declineReason?: string;
63
+ declineMessage?: string;
64
+ };
65
+ export type PaymentStatus = 'succeeded' | 'refunded' | 'partially_refunded' | 'failed' | 'incomplete' | 'uncaptured' | 'canceled' | 'disputed' | 'processing';
66
+
67
+ export function paymentStatusOf(intent: StripeObject | undefined, charge: StripeObject | undefined): PaymentStatus {
68
+ if (charge?.disputed) return 'disputed';
69
+ if (charge?.refunded) return 'refunded';
70
+ if (charge && Number(charge.amount_refunded) > 0) return 'partially_refunded';
71
+ if (charge?.status === 'failed') return 'failed';
72
+ if (intent) {
73
+ if (intent.status === 'succeeded') return 'succeeded';
74
+ if (intent.status === 'canceled') return 'canceled';
75
+ if (intent.status === 'requires_capture') return 'uncaptured';
76
+ if (intent.status === 'processing') return 'processing';
77
+ if (intent.last_payment_error) return 'failed';
78
+ return 'incomplete';
79
+ }
80
+ return charge?.status === 'succeeded' ? 'succeeded' : charge?.status === 'pending' ? 'processing' : 'failed';
81
+ }
82
+
83
+ const DECLINE_REASONS: Record<string, string> = {
84
+ generic_decline: 'Generic decline', insufficient_funds: 'Insufficient funds', lost_card: 'Lost card', stolen_card: 'Stolen card',
85
+ expired_card: 'Expired card', incorrect_cvc: 'Incorrect CVC', processing_error: 'Processing error', card_declined: 'Card declined',
86
+ };
87
+ export const declineReasonText = (code: string | undefined): string | undefined => (code ? DECLINE_REASONS[code] ?? code.replace(/_/g, ' ') : undefined);
88
+
89
+ type Directory = { customers: Map<string, StripeObject>; paymentMethods: Map<string, StripeObject> };
90
+
91
+ async function paymentMethodsById(ids: Iterable<string>): Promise<Map<string, StripeObject>> {
92
+ const unique = [...new Set([...ids].filter((id) => typeof id === 'string' && id.startsWith('pm_')))];
93
+ const found = await Promise.all(unique.map((id) => optional(retrieve(`/v1/payment_methods/${id}`), undefined as StripeObject | undefined)));
94
+ return new Map(found.filter((pm): pm is StripeObject => Boolean(pm?.id)).map((pm) => [pm.id, pm]));
95
+ }
96
+
97
+ function paymentOf(intent: StripeObject | undefined, charge: StripeObject | undefined, refunds: StripeObject[], dir: Directory): Payment {
98
+ const base = intent ?? charge!;
99
+ const customerId = typeof base.customer === 'string' ? base.customer : base.customer?.id;
100
+ const pmId = typeof base.payment_method === 'string' ? base.payment_method : charge?.payment_method;
101
+ const error = intent?.last_payment_error;
102
+ const refundsOf = refunds.filter((r) => (intent && r.payment_intent === intent.id) || (charge && r.charge === charge.id));
103
+ return {
104
+ id: String(base.id),
105
+ amount: Number(base.amount) || 0,
106
+ currency: String(base.currency ?? 'usd'),
107
+ status: paymentStatusOf(intent, charge),
108
+ description: base.description ?? charge?.description ?? undefined,
109
+ created: Number(base.created) || 0,
110
+ customer: customerId ? dir.customers.get(customerId) ?? { id: customerId } : undefined,
111
+ charge, intent,
112
+ paymentMethod: pmId ? dir.paymentMethods.get(pmId) : undefined,
113
+ amountRefunded: Number(charge?.amount_refunded) || refundsOf.reduce((n, r) => n + (r.status === 'failed' ? 0 : Number(r.amount) || 0), 0),
114
+ refundedAt: refundsOf.length ? Math.max(...refundsOf.map((r) => Number(r.created) || 0)) : undefined,
115
+ declineReason: declineReasonText(error?.decline_code ?? error?.code ?? charge?.failure_code),
116
+ declineMessage: error?.message ?? charge?.failure_message ?? undefined,
117
+ };
118
+ }
119
+
120
+ /** The Payments list: every PaymentIntent, and every Charge made without one. */
121
+ export async function loadPayments(): Promise<Payment[]> {
122
+ const [intents, charges, refunds, customers] = await Promise.all([
123
+ list('/v1/payment_intents'), list('/v1/charges'), optional(list('/v1/refunds'), []), list('/v1/customers'),
124
+ ]);
125
+ const chargeById = new Map(charges.map((c) => [c.id, c]));
126
+ const pmIds = [...intents.map((i) => i.payment_method), ...charges.map((c) => c.payment_method)];
127
+ const dir: Directory = { customers: new Map(customers.map((c) => [c.id, c])), paymentMethods: await paymentMethodsById(pmIds) };
128
+ const rows = intents.map((pi) => {
129
+ const charge = (typeof pi.latest_charge === 'string' ? chargeById.get(pi.latest_charge) : pi.latest_charge) ?? charges.find((c) => c.payment_intent === pi.id);
130
+ return paymentOf(pi, charge, refunds, dir);
131
+ });
132
+ for (const ch of charges) if (!ch.payment_intent) rows.push(paymentOf(undefined, ch, refunds, dir));
133
+ return rows.sort((a, b) => b.created - a.created);
134
+ }
135
+
136
+ export type PaymentDetail = Payment & { refunds: StripeObject[]; events: StripeObject[]; invoice?: StripeObject };
137
+
138
+ /** One payment's page: the intent (or charge), its charge, refunds, card, customer and the events about it. */
139
+ export async function loadPayment(id: string): Promise<PaymentDetail> {
140
+ let intent: StripeObject | undefined;
141
+ let charge: StripeObject | undefined;
142
+ if (id.startsWith('ch_') || id.startsWith('py_')) {
143
+ charge = await retrieve(`/v1/charges/${id}`);
144
+ if (typeof charge.payment_intent === 'string') intent = await optional(retrieve(`/v1/payment_intents/${charge.payment_intent}`), undefined);
145
+ } else {
146
+ intent = await retrieve(`/v1/payment_intents/${id}`);
147
+ const chargeId = typeof intent.latest_charge === 'string' ? intent.latest_charge : intent.latest_charge?.id;
148
+ if (chargeId) charge = await optional(retrieve(`/v1/charges/${chargeId}`), undefined);
149
+ }
150
+ const base = intent ?? charge!;
151
+ const customerId = typeof base.customer === 'string' ? base.customer : undefined;
152
+ const pmId = typeof base.payment_method === 'string' ? base.payment_method : charge?.payment_method;
153
+ const refundPath = intent ? `/v1/refunds?payment_intent=${intent.id}` : `/v1/refunds?charge=${charge!.id}`;
154
+ const [customer, pms, refunds, events, invoice] = await Promise.all([
155
+ customerId ? optional(retrieve(`/v1/customers/${customerId}`), { id: customerId }) : Promise.resolve(undefined),
156
+ paymentMethodsById(pmId ? [pmId] : []),
157
+ optional(list(refundPath), []),
158
+ optional(list('/v1/events'), []),
159
+ typeof base.invoice === 'string' ? optional(retrieve(`/v1/invoices/${base.invoice}`), undefined) : Promise.resolve(undefined),
160
+ ]);
161
+ const dir: Directory = { customers: new Map(customer ? [[customer.id, customer]] : []), paymentMethods: pms };
162
+ const related = new Set<string>([base.id, charge?.id, intent?.id, ...refunds.map((r) => r.id)].filter(Boolean) as string[]);
163
+ return {
164
+ ...paymentOf(intent, charge, refunds, dir),
165
+ refunds,
166
+ events: events.filter((e) => related.has(e?.data?.object?.id)).sort((a, b) => b.created - a.created),
167
+ invoice,
168
+ };
169
+ }
170
+
171
+ // ── customers ────────────────────────────────────────────────────────────────
172
+ export type CustomerRow = StripeObject & { defaultCard?: StripeObject; totalSpend: number; payments: number; refunds: number; currency: string };
173
+
174
+ export async function loadCustomers(): Promise<CustomerRow[]> {
175
+ const [customers, charges, intents] = await Promise.all([list('/v1/customers'), list('/v1/charges'), list('/v1/payment_intents')]);
176
+ const pmIds = customers.map((c) => c.invoice_settings?.default_payment_method).filter((x) => typeof x === 'string');
177
+ const pms = await paymentMethodsById(pmIds);
178
+ return customers.map((c) => {
179
+ const mine = charges.filter((ch) => ch.customer === c.id && ch.status === 'succeeded');
180
+ const pmId = c.invoice_settings?.default_payment_method;
181
+ return {
182
+ ...c,
183
+ defaultCard: typeof pmId === 'string' ? pms.get(pmId) : undefined,
184
+ totalSpend: mine.reduce((n, ch) => n + (Number(ch.amount_captured ?? ch.amount) || 0) - (Number(ch.amount_refunded) || 0), 0),
185
+ payments: intents.filter((i) => i.customer === c.id && i.status === 'succeeded').length || mine.length,
186
+ refunds: mine.reduce((n, ch) => n + (Number(ch.amount_refunded) || 0), 0),
187
+ currency: String(mine[0]?.currency ?? c.currency ?? 'usd'),
188
+ };
189
+ });
190
+ }
191
+
192
+ export type CustomerDetail = {
193
+ customer: StripeObject;
194
+ paymentMethods: StripeObject[];
195
+ subscriptions: StripeObject[];
196
+ invoices: StripeObject[];
197
+ payments: Payment[];
198
+ products: Map<string, StripeObject>;
199
+ events: StripeObject[];
200
+ };
201
+
202
+ export async function loadCustomer(id: string): Promise<CustomerDetail> {
203
+ const customer = await retrieve(`/v1/customers/${id}`);
204
+ const [paymentMethods, subscriptions, invoices, payments, products, events] = await Promise.all([
205
+ optional(list(`/v1/customers/${id}/payment_methods`), []),
206
+ optional(list(`/v1/subscriptions?customer=${id}&status=all`), []),
207
+ optional(list(`/v1/invoices?customer=${id}`), []),
208
+ loadPayments().then((all) => all.filter((p) => p.customer?.id === id)),
209
+ optional(list('/v1/products'), []),
210
+ optional(list('/v1/events'), []),
211
+ ]);
212
+ const mine = new Set([id, ...subscriptions.map((s) => s.id), ...invoices.map((i) => i.id), ...payments.map((p) => p.id)]);
213
+ return {
214
+ customer, paymentMethods, subscriptions, invoices, payments,
215
+ products: new Map(products.map((p) => [p.id, p])),
216
+ events: events.filter((e) => mine.has(e?.data?.object?.id)).sort((a, b) => b.created - a.created),
217
+ };
218
+ }
219
+
220
+ // ── billing ──────────────────────────────────────────────────────────────────
221
+ export type Billing = { subscriptions: StripeObject[]; customers: Map<string, StripeObject>; products: Map<string, StripeObject> };
222
+
223
+ export async function loadSubscriptions(): Promise<Billing> {
224
+ const [subscriptions, customers, products] = await Promise.all([list('/v1/subscriptions?status=all'), list('/v1/customers'), list('/v1/products')]);
225
+ return { subscriptions, customers: new Map(customers.map((c) => [c.id, c])), products: new Map(products.map((p) => [p.id, p])) };
226
+ }
227
+
228
+ export type SubscriptionDetail = { subscription: StripeObject; customer?: StripeObject; invoices: StripeObject[]; products: Map<string, StripeObject>; events: StripeObject[]; paymentMethod?: StripeObject };
229
+
230
+ export async function loadSubscription(id: string): Promise<SubscriptionDetail> {
231
+ const subscription = await retrieve(`/v1/subscriptions/${id}`);
232
+ const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id;
233
+ const [customer, invoices, products, events] = await Promise.all([
234
+ customerId ? optional(retrieve(`/v1/customers/${customerId}`), undefined) : Promise.resolve(undefined),
235
+ optional(list(`/v1/invoices?subscription=${id}`), []),
236
+ optional(list('/v1/products'), []),
237
+ optional(list('/v1/events'), []),
238
+ ]);
239
+ const pmId = subscription.default_payment_method ?? customer?.invoice_settings?.default_payment_method;
240
+ const pms = await paymentMethodsById(typeof pmId === 'string' ? [pmId] : []);
241
+ return {
242
+ subscription, customer, invoices, products: new Map(products.map((p) => [p.id, p])),
243
+ events: events.filter((e) => e?.data?.object?.id === id).sort((a, b) => b.created - a.created),
244
+ paymentMethod: typeof pmId === 'string' ? pms.get(pmId) : undefined,
245
+ };
246
+ }
247
+
248
+ export async function loadInvoices(): Promise<{ invoices: StripeObject[]; customers: Map<string, StripeObject> }> {
249
+ const [invoices, customers] = await Promise.all([list('/v1/invoices'), list('/v1/customers')]);
250
+ return { invoices, customers: new Map(customers.map((c) => [c.id, c])) };
251
+ }
252
+
253
+ export async function loadProducts(): Promise<{ products: StripeObject[]; prices: StripeObject[] }> {
254
+ const [products, prices] = await Promise.all([list('/v1/products'), list('/v1/prices')]);
255
+ return { products, prices };
256
+ }
257
+
258
+ export type HomeData = { balance: StripeObject; payments: Payment[]; customers: StripeObject[]; subscriptions: StripeObject[]; invoices: StripeObject[] };
259
+ export async function loadHome(): Promise<HomeData> {
260
+ const [balance, payments, customers, subscriptions, invoices] = await Promise.all([
261
+ retrieve('/v1/balance'), loadPayments(), list('/v1/customers'), optional(list('/v1/subscriptions?status=all'), []), optional(list('/v1/invoices'), []),
262
+ ]);
263
+ return { balance, payments, customers, subscriptions, invoices };
264
+ }
265
+
266
+ export async function loadBalances(): Promise<{ balance: StripeObject; transactions: StripeObject[]; payouts: StripeObject[] }> {
267
+ const [balance, transactions, payouts] = await Promise.all([retrieve('/v1/balance'), optional(list('/v1/balance_transactions'), []), optional(list('/v1/payouts'), [])]);
268
+ return { balance, transactions, payouts };
269
+ }
270
+
271
+ export async function loadDevelopers(): Promise<{ events: StripeObject[]; webhooks: StripeObject[]; account?: StripeObject }> {
272
+ const [events, webhooks, account] = await Promise.all([optional(list('/v1/events'), []), optional(list('/v1/webhook_endpoints'), []), optional(retrieve('/v1/account'), undefined)]);
273
+ return { events, webhooks, account };
274
+ }
275
+
276
+ // ── writes ───────────────────────────────────────────────────────────────────
277
+ /** Refund a payment in full (no amount) or in part. */
278
+ export function createRefund(payment: { intent?: StripeObject; charge?: StripeObject }, amount: number | undefined, reason: string | undefined): Promise<StripeObject> {
279
+ const target = payment.intent ? { payment_intent: String(payment.intent.id) } : { charge: String(payment.charge?.id) };
280
+ return call('POST', '/v1/refunds', { ...target, amount, reason });
281
+ }
282
+ /** Cancel now (DELETE) or at the end of the current period (cancel_at_period_end). */
283
+ export function cancelSubscription(id: string, when: 'now' | 'period_end'): Promise<StripeObject> {
284
+ return when === 'now' ? call('DELETE', `/v1/subscriptions/${id}`) : call('POST', `/v1/subscriptions/${id}`, { cancel_at_period_end: true });
285
+ }
286
+ export const createObject = (collection: string, fields: Record<string, string>): Promise<StripeObject> => call('POST', `/v1/${collection}`, fields);