@volter/twin-stripe 0.1.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.
@@ -0,0 +1,3304 @@
1
+ // Stripe capability manifest — the EXPECTED REAL-PRODUCT SURFACE (the target), authored
2
+ // top-down from what Stripe actually does — NOT from what this twin has built. This is the
3
+ // honest denominator: most entries start as `todo` and coverage reads LOW until the twin
4
+ // truly reaches 100% of Stripe. `verify()` (required to count as done) is ground truth;
5
+ // `expected: 'done'` only on capabilities we genuinely claim, so a broken one shows as a
6
+ // regression. Grow this toward Stripe's *full* surface every cycle — a missing entry is a
7
+ // hidden gap, and a high % against a thin list is a misleading metric (the bug this fixes).
8
+ import { mkdtempSync, rmSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { createElement } from 'react';
12
+ import { renderToStaticMarkup } from 'react-dom/server';
13
+ import { checkCapabilities, type CapabilityReport, type CapabilitySpec } from '@volter/twin-tooling';
14
+ import { ListPane, SECTIONS, PaymentErrorBanner, ConnectAccountPanel, BalanceSummary, type Section } from '../client/stripe-mirror.tsx';
15
+ import type { StripeRow } from './stripe-mirror-ui.ts';
16
+ import {
17
+ clearStripeWebhooks,
18
+ computeStripeSignature,
19
+ constructEvent,
20
+ generateTestHeaderString,
21
+ registerStripeWebhook,
22
+ setStripeEventDelivery,
23
+ type StripeEvent,
24
+ } from './stripe-events.ts';
25
+ import { fullSyncStripe } from './stripe-connector.ts';
26
+ import { buildStripeMirrorClient, createStripeMirrorServer, formatStripeAmount } from './stripe-mirror-ui.ts';
27
+ import { handleStripeTwinRequest, type StripeResponse } from './stripe-twin.ts';
28
+
29
+ // ── UI verify: the built mirror bundle must contain the screen's load-bearing markers ──
30
+ let bundle: Promise<string> | null = null;
31
+ const mirrorBundle = (): Promise<string> => (bundle ??= buildStripeMirrorClient());
32
+
33
+ // ── DATA-COUPLED UI verify ───────────────────────────────────────────────────
34
+ // A screen passes only when BOTH hold: (1) its load-bearing markers are in the built
35
+ // bundle (the screen is wired in), AND (2) the SAME data source the screen reads — served
36
+ // by the real mirror server over the twin's projection — reflects seeded twin state. This
37
+ // proves the screen renders REAL twin data, not just that the markers exist. `seed` writes
38
+ // state into the isolated root via the twin handler; `check` fetches the screen's endpoint(s)
39
+ // off the running mirror server and asserts the seeded state is present.
40
+ type ServerFetch = (path: string) => Promise<Body>;
41
+ // A small client against the running mirror server: GET a screen's data source, or POST the
42
+ // exact write a screen's form issues — both over the SAME HTTP the React client uses.
43
+ type ServerClient = { get: ServerFetch; post: (path: string, body?: string) => Promise<Body> };
44
+ function uiDataCoupled(opts: {
45
+ markers: string[];
46
+ seed: (h: (s: Step) => Promise<StripeResponse>) => Promise<void>;
47
+ check: (client: ServerClient) => Promise<boolean>;
48
+ }): () => Promise<boolean> {
49
+ return async () => {
50
+ const js = await mirrorBundle();
51
+ if (!opts.markers.every((m) => js.includes(m))) return false;
52
+ const root = mkdtempSync(join(tmpdir(), 'stp-ui-'));
53
+ const server = createStripeMirrorServer({ root, port: 0 });
54
+ try {
55
+ const h = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root });
56
+ await opts.seed(h);
57
+ const base = `http://127.0.0.1:${server.port}`;
58
+ const get: ServerFetch = (path) => fetch(`${base}${path}`).then((r) => r.json() as Promise<Body>);
59
+ const post = (path: string, body?: string) => fetch(`${base}${path}`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: body ?? '' }).then((r) => r.json() as Promise<Body>);
60
+ return await opts.check({ get, post });
61
+ } catch {
62
+ return false;
63
+ } finally {
64
+ server.stop();
65
+ rmSync(root, { recursive: true, force: true });
66
+ }
67
+ };
68
+ }
69
+
70
+ // ── RUNG-5 data-coupled UI verify: SEED → SERVE → RENDER → STRUCTURAL DOM ASSERT ─────────
71
+ // Stronger than uiDataCoupled (which proves the screen's data source reflects seeded state):
72
+ // here we additionally render the mirror's OWN ListPane component (the same React component
73
+ // the dashboard ships) over the rows the running mirror server returns, then assert the
74
+ // rendered DOM emits one `list-row` landmark per seeded object carrying that object's id.
75
+ // This fails on an empty workspace (zero rows → zero list-rows) and would fail if rendering
76
+ // were hardcoded/markers-only (the ids must actually flow into the markup).
77
+ const SECTION_BY_KEY: Record<string, Section> = Object.fromEntries(SECTIONS.map((s) => [s.key, s]));
78
+ const noop = () => {};
79
+ /** renderToStaticMarkup the mirror's ListPane for a section over real rows; return the markup. */
80
+ function renderSectionList(sectionKey: string, rows: StripeRow[]): string {
81
+ const section = SECTION_BY_KEY[sectionKey];
82
+ if (!section) throw new Error(`unknown mirror section ${sectionKey}`);
83
+ return renderToStaticMarkup(
84
+ createElement(ListPane, { section, rows, currentId: rows[0]?.id, onSelect: noop }),
85
+ );
86
+ }
87
+ /** Count `list-row` landmarks in rendered ListPane markup. */
88
+ const listRows = (markup: string) => (markup.match(/list-row/g) ?? []).length;
89
+
90
+ // ── API verify: drive REAL requests against a fresh temp root, then assert status/shape ──
91
+ type Step = { m: string; p: string; b?: string };
92
+ type Body = Record<string, unknown>;
93
+
94
+ /** Run a sequence of real Stripe requests against an isolated root; return all responses. */
95
+ async function withRoot(steps: (h: (s: Step) => Promise<StripeResponse>) => Promise<boolean>): Promise<boolean> {
96
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
97
+ const h = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root });
98
+ try {
99
+ return await steps(h);
100
+ } catch {
101
+ return false;
102
+ } finally {
103
+ rmSync(root, { recursive: true, force: true });
104
+ }
105
+ }
106
+
107
+ const ok = (r: StripeResponse) => r.status >= 200 && r.status < 300;
108
+ const id = (r: StripeResponse) => (r.body as Body)?.id as string;
109
+ const field = (r: StripeResponse, k: string) => (r.body as Body)?.[k];
110
+
111
+ /** A GET list endpoint that returns a Stripe list envelope (object:'list', data[]). */
112
+ const listOk = (path: string) => () =>
113
+ withRoot(async (h) => {
114
+ const r = await h({ m: 'GET', p: path });
115
+ return ok(r) && field(r, 'object') === 'list' && Array.isArray((r.body as Body).data);
116
+ });
117
+
118
+ // ── shorthands (mirror the linear manifest) ──
119
+ const done = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], verify: CapabilitySpec['verify']): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'done', verify });
120
+ const todo = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier']): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'todo' });
121
+ const outOfScope = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], reason: string): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'todo', outOfScope: reason });
122
+
123
+ // The real Stripe surface (the target). Entries with verify() + expected:'done' are what we
124
+ // currently claim are working against THIS twin; everything else (the majority) is a gap.
125
+ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
126
+ // ── Customers ───────────────────────────────────────────────────────────────────
127
+ done('stripe.customers.crud', 'customers', 'Customers: create / retrieve / update / list', 'api', 'core', () =>
128
+ withRoot(async (h) => {
129
+ const c = await h({ m: 'POST', p: '/v1/customers', b: 'email=ada@twin.test&name=Ada' });
130
+ if (!ok(c)) return false;
131
+ const g = await h({ m: 'GET', p: `/v1/customers/${id(c)}` });
132
+ const u = await h({ m: 'POST', p: `/v1/customers/${id(c)}`, b: 'name=Ada Lovelace' });
133
+ const l = await h({ m: 'GET', p: '/v1/customers' });
134
+ return ok(g) && id(g) === id(c) && ok(u) && field(u, 'name') === 'Ada Lovelace' && field(l, 'object') === 'list';
135
+ }),
136
+ ),
137
+ done('stripe.customers.list_filter', 'customers', 'Customer list filter by email', 'api', 'core', () =>
138
+ withRoot(async (h) => {
139
+ await h({ m: 'POST', p: '/v1/customers', b: 'email=match@twin.test' });
140
+ await h({ m: 'POST', p: '/v1/customers', b: 'email=other@twin.test' });
141
+ const r = await h({ m: 'GET', p: '/v1/customers?email=match@twin.test' });
142
+ const data = (r.body as Body).data as Body[];
143
+ return ok(r) && data.length === 1 && data[0]!.email === 'match@twin.test';
144
+ }),
145
+ ),
146
+ // Customer credit balance: a -2000 adjustment lowers the running ending_balance to -2000
147
+ // AND syncs customer.balance; a second +500 nets to -1500; the per-customer ledger lists
148
+ // both newest-first; retrieve round-trips. A missing customer 404s; missing amount 400s.
149
+ // (ending_balance/customer.balance sync is produced ONLY by this feature.)
150
+ done('stripe.customers.balance', 'customers', 'Customer credit balance + balance transactions', 'api', 'common', () =>
151
+ withRoot(async (h) => {
152
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=bal@twin.test' });
153
+ const t1 = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/balance_transactions`, b: 'amount=-2000&currency=usd' });
154
+ if (!ok(t1) || field(t1, 'object') !== 'customer_balance_transaction' || field(t1, 'ending_balance') !== -2000 || field(t1, 'amount') !== -2000) return false;
155
+ const t2 = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/balance_transactions`, b: 'amount=500&currency=usd' });
156
+ if (!ok(t2) || field(t2, 'ending_balance') !== -1500) return false;
157
+ // the customer's running balance is kept in sync with the ledger
158
+ const c = await h({ m: 'GET', p: `/v1/customers/${id(cust)}` });
159
+ if (!ok(c) || field(c, 'balance') !== -1500) return false;
160
+ const g = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/balance_transactions/${id(t1)}` });
161
+ const l = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/balance_transactions` });
162
+ const data = (l.body as Body).data as Body[];
163
+ const missingCust = await h({ m: 'POST', p: '/v1/customers/cus_nope/balance_transactions', b: 'amount=100&currency=usd' });
164
+ const missingAmt = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/balance_transactions`, b: 'currency=usd' });
165
+ return ok(g) && id(g) === id(t1) && field(g, 'ending_balance') === -2000 &&
166
+ field(l, 'object') === 'list' && data.length === 2 &&
167
+ missingCust.status === 404 && missingAmt.status === 400;
168
+ }),
169
+ ),
170
+ // Customer tax IDs: create eu_vat on a customer, retrieve/list it (scoped to the customer),
171
+ // delete returns the deleted stub and the tax id then 404s + drops from the list. Missing
172
+ // type/value 400; missing customer 404. type round-trips via the _stripe_type stash.
173
+ done('stripe.customers.tax_ids', 'customers', 'Customer tax IDs', 'api', 'common', () =>
174
+ withRoot(async (h) => {
175
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=txi@twin.test' });
176
+ const t = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/tax_ids`, b: 'type=eu_vat&value=DE123456789' });
177
+ if (!ok(t) || field(t, 'object') !== 'tax_id' || field(t, 'type') !== 'eu_vat' || field(t, 'value') !== 'DE123456789') return false;
178
+ const g = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/tax_ids/${id(t)}` });
179
+ const l1 = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/tax_ids` });
180
+ if (!ok(g) || id(g) !== id(t) || ((l1.body as Body).data as Body[]).length !== 1) return false;
181
+ const del = await h({ m: 'DELETE', p: `/v1/customers/${id(cust)}/tax_ids/${id(t)}` });
182
+ const gone = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/tax_ids/${id(t)}` });
183
+ const l2 = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/tax_ids` });
184
+ const noType = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/tax_ids`, b: 'value=X' });
185
+ const missingCust = await h({ m: 'POST', p: '/v1/customers/cus_nope/tax_ids', b: 'type=eu_vat&value=X' });
186
+ return ok(del) && field(del, 'deleted') === true && gone.status === 404 &&
187
+ ((l2.body as Body).data as Body[]).length === 0 && noType.status === 400 && missingCust.status === 404;
188
+ }),
189
+ ),
190
+ // Cash balance: GET /v1/customers/:id/cash_balance returns the cash_balance summary
191
+ // (available currency→amount map, derived from the cash-balance ledger). Funding via
192
+ // .../cash_balance_transactions accrues into `available`; POST updates reconciliation_mode.
193
+ // An unknown customer 404s; an invalid reconciliation_mode 400s. (Only this feature produces
194
+ // the available map keyed by the funded currency.)
195
+ done('stripe.customers.cash_balance', 'customers', 'Customer cash balance', 'api', 'niche', () =>
196
+ withRoot(async (h) => {
197
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=cash@twin.test' });
198
+ const empty = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/cash_balance` });
199
+ if (!ok(empty) || field(empty, 'object') !== 'cash_balance' || field(empty, 'available') !== null) return false;
200
+ const fund = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/cash_balance_transactions`, b: 'amount=5000&currency=usd' });
201
+ if (!ok(fund) || field(fund, 'type') !== 'funded' || field(fund, 'ending_balance') !== 5000) return false;
202
+ const more = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/cash_balance_transactions`, b: 'amount=2500&currency=usd' });
203
+ if (field(more, 'ending_balance') !== 7500) return false;
204
+ const bal = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/cash_balance` });
205
+ if ((field(bal, 'available') as Body)?.usd !== 7500) return false;
206
+ const txns = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/cash_balance_transactions` });
207
+ if (((txns.body as Body).data as Body[]).length !== 2) return false;
208
+ const set = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/cash_balance`, b: 'settings[reconciliation_mode]=manual' });
209
+ if (!ok(set) || (field(set, 'settings') as Body)?.reconciliation_mode !== 'manual') return false;
210
+ const badMode = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/cash_balance`, b: 'settings[reconciliation_mode]=bogus' });
211
+ const noCust = await h({ m: 'GET', p: '/v1/customers/cus_nope/cash_balance' });
212
+ return badMode.status === 400 && noCust.status === 404;
213
+ }),
214
+ ),
215
+ // Legacy sources: POST /v1/customers/:id/sources attaches a tokenized source (the pre-PM
216
+ // flow). The FIRST source becomes default_source; deleting the default promotes the next.
217
+ // The card carries only safe last4 fields (never the raw PAN). Unknown source/customer 404.
218
+ done('stripe.customers.sources', 'customers', 'Legacy customer sources / default_source mgmt', 'api', 'niche', () =>
219
+ withRoot(async (h) => {
220
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=src@twin.test' });
221
+ const s1 = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/sources`, b: 'source=tok_visa' });
222
+ if (!ok(s1) || field(s1, 'object') !== 'card' || typeof field(s1, 'last4') !== 'string') return false;
223
+ // first attached source becomes default_source.
224
+ const c1 = await h({ m: 'GET', p: `/v1/customers/${id(cust)}` });
225
+ if (field(c1, 'default_source') !== id(s1)) return false;
226
+ const s2 = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/sources`, b: 'source=tok_mastercard' });
227
+ const list = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/sources` });
228
+ if (((list.body as Body).data as Body[]).length !== 2) return false;
229
+ const g = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/sources/${id(s1)}` });
230
+ if (!ok(g) || id(g) !== id(s1)) return false;
231
+ // deleting the default_source promotes the remaining one.
232
+ const del = await h({ m: 'DELETE', p: `/v1/customers/${id(cust)}/sources/${id(s1)}` });
233
+ if (!ok(del) || field(del, 'deleted') !== true) return false;
234
+ const c2 = await h({ m: 'GET', p: `/v1/customers/${id(cust)}` });
235
+ if (field(c2, 'default_source') !== id(s2)) return false;
236
+ const gone = await h({ m: 'GET', p: `/v1/customers/${id(cust)}/sources/${id(s1)}` });
237
+ const noSrc = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/sources` });
238
+ return gone.status === 404 && noSrc.status === 400;
239
+ }),
240
+ ),
241
+ // Customer search: GET /v1/customers/search?query=email:"…" returns the Stripe search_result
242
+ // envelope filtered by the query language; metadata["k"]:"v" narrows further. A missing query
243
+ // 400s; a deleted customer is excluded. (search_result envelope produced ONLY by this feature.)
244
+ done('stripe.customers.search', 'customers', 'Customer search (query language)', 'api', 'common', () =>
245
+ withRoot(async (h) => {
246
+ await h({ m: 'POST', p: '/v1/customers', b: 'email=find@twin.test&metadata[tier]=gold' });
247
+ await h({ m: 'POST', p: '/v1/customers', b: 'email=other@twin.test&metadata[tier]=silver' });
248
+ const r = await h({ m: 'GET', p: `/v1/customers/search?query=${encodeURIComponent('email:"find@twin.test"')}` });
249
+ if (!ok(r) || field(r, 'object') !== 'search_result') return false;
250
+ const data = (r.body as Body).data as Body[];
251
+ if (data.length !== 1 || data[0]!.email !== 'find@twin.test') return false;
252
+ const meta = await h({ m: 'GET', p: `/v1/customers/search?query=${encodeURIComponent('metadata["tier"]:"gold"')}` });
253
+ const metaData = (meta.body as Body).data as Body[];
254
+ const noQuery = await h({ m: 'GET', p: '/v1/customers/search' });
255
+ return metaData.length === 1 && metaData[0]!.email === 'find@twin.test' && noQuery.status === 400;
256
+ }),
257
+ ),
258
+ // Customer delete: soft-deletes — DELETE returns { deleted:true }, retrieve then 404s and
259
+ // the customer drops from the list. A second delete 404s.
260
+ done('stripe.customers.delete', 'customers', 'Customer delete', 'api', 'core', () =>
261
+ withRoot(async (h) => {
262
+ const c = await h({ m: 'POST', p: '/v1/customers', b: 'email=del@twin.test' });
263
+ const before = await h({ m: 'GET', p: '/v1/customers' });
264
+ const del = await h({ m: 'DELETE', p: `/v1/customers/${id(c)}` });
265
+ if (!ok(del) || field(del, 'deleted') !== true || field(del, 'object') !== 'customer') return false;
266
+ const gone = await h({ m: 'GET', p: `/v1/customers/${id(c)}` });
267
+ const after = await h({ m: 'GET', p: '/v1/customers' });
268
+ const beforeN = ((before.body as Body).data as Body[]).length;
269
+ const afterN = ((after.body as Body).data as Body[]).length;
270
+ const again = await h({ m: 'DELETE', p: `/v1/customers/${id(c)}` });
271
+ return gone.status === 404 && afterN === beforeN - 1 && again.status === 404;
272
+ }),
273
+ ),
274
+
275
+ // ── PaymentIntents ───────────────────────────────────────────────────────────────
276
+ done('stripe.payment_intents.create', 'payment_intents', 'PaymentIntents: create + retrieve + list', 'api', 'core', () =>
277
+ withRoot(async (h) => {
278
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=1000&currency=usd' });
279
+ if (!ok(pi) || field(pi, 'object') !== 'payment_intent') return false;
280
+ const g = await h({ m: 'GET', p: `/v1/payment_intents/${id(pi)}` });
281
+ const l = await h({ m: 'GET', p: '/v1/payment_intents' });
282
+ return ok(g) && id(g) === id(pi) && field(l, 'object') === 'list';
283
+ }),
284
+ ),
285
+ done('stripe.payment_intents.confirm', 'payment_intents', 'PaymentIntent confirm → succeeded', 'api', 'core', () =>
286
+ withRoot(async (h) => {
287
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=1000&currency=usd' });
288
+ const c = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm`, b: 'payment_method=pm_card_visa' });
289
+ return ok(c) && field(c, 'status') === 'succeeded';
290
+ }),
291
+ ),
292
+ done('stripe.payment_intents.validation', 'payment_intents', 'PaymentIntent rejects missing/invalid amount+currency', 'api', 'core', () =>
293
+ withRoot(async (h) => {
294
+ const noAmt = await h({ m: 'POST', p: '/v1/payment_intents', b: 'currency=usd' });
295
+ const noCur = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=1000' });
296
+ return noAmt.status === 400 && noCur.status === 400;
297
+ }),
298
+ ),
299
+ // Manual capture: a manual-capture PI confirmed lands in requires_capture (funds authorized,
300
+ // amount_capturable=amount), then /capture → succeeded (amount_received set, capturable cleared).
301
+ // Capturing an already-captured PI is the vendor 400 payment_intent_unexpected_state; unknown id 404.
302
+ done('stripe.payment_intents.capture', 'payment_intents', 'PaymentIntent manual capture (capture_method=manual + /capture)', 'api', 'core', () =>
303
+ withRoot(async (h) => {
304
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=2000&currency=usd&capture_method=manual' });
305
+ if (!ok(pi)) return false;
306
+ const conf = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm`, b: 'payment_method=pm_card_visa' });
307
+ if (!ok(conf) || field(conf, 'status') !== 'requires_capture' || field(conf, 'amount_capturable') !== 2000) return false;
308
+ const cap = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/capture` });
309
+ if (!ok(cap) || field(cap, 'status') !== 'succeeded' || field(cap, 'amount_received') !== 2000 || field(cap, 'amount_capturable') !== 0) return false;
310
+ // re-read persists; double-capture is the vendor unexpected-state 400; unknown id 404
311
+ const g = await h({ m: 'GET', p: `/v1/payment_intents/${id(pi)}` });
312
+ const again = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/capture` });
313
+ const missing = await h({ m: 'POST', p: '/v1/payment_intents/pi_nope/capture' });
314
+ return ok(g) && field(g, 'status') === 'succeeded' &&
315
+ again.status === 400 && ((again.body as Body).error as Body)?.code === 'payment_intent_unexpected_state' &&
316
+ missing.status === 404;
317
+ }),
318
+ ),
319
+ // Cancel: a non-terminal PI cancels → status canceled (+ cancellation_reason). Canceling a
320
+ // succeeded PI is the vendor 400 payment_intent_unexpected_state; an unknown id is 404.
321
+ done('stripe.payment_intents.cancel', 'payment_intents', 'PaymentIntent cancel', 'api', 'core', () =>
322
+ withRoot(async (h) => {
323
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=1000&currency=usd' });
324
+ if (!ok(pi)) return false;
325
+ const can = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/cancel`, b: 'cancellation_reason=abandoned' });
326
+ if (!ok(can) || field(can, 'status') !== 'canceled' || field(can, 'cancellation_reason') !== 'abandoned') return false;
327
+ const g = await h({ m: 'GET', p: `/v1/payment_intents/${id(pi)}` });
328
+ // a succeeded PI cannot be canceled (unexpected state 400); unknown id 404
329
+ const pi2 = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=500&currency=usd' });
330
+ await h({ m: 'POST', p: `/v1/payment_intents/${id(pi2)}/confirm`, b: 'payment_method=pm_card_visa' });
331
+ const bad = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi2)}/cancel` });
332
+ const missing = await h({ m: 'POST', p: '/v1/payment_intents/pi_nope/cancel' });
333
+ return ok(g) && field(g, 'status') === 'canceled' &&
334
+ bad.status === 400 && ((bad.body as Body).error as Body)?.code === 'payment_intent_unexpected_state' &&
335
+ missing.status === 404;
336
+ }),
337
+ ),
338
+ // 3DS / SCA: confirming with a 3DS-test PM puts the PI in `requires_action` with a
339
+ // next_action (use_stripe_sdk) and does NOT succeed; a SECOND confirm completes the
340
+ // challenge → succeeded (next_action cleared). The interim state fires
341
+ // payment_intent.requires_action (NOT succeeded). (requires_action + next_action produced
342
+ // ONLY by this feature.)
343
+ done('stripe.payment_intents.sca_3ds', 'payment_intents', 'PaymentIntent 3DS / SCA requires_action + next_action', 'api', 'common', () =>
344
+ withRoot(async (h) => {
345
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=5000&currency=usd' });
346
+ if (!ok(pi)) return false;
347
+ const c1 = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm`, b: 'payment_method=pm_card_authenticationRequired' });
348
+ if (!ok(c1) || field(c1, 'status') !== 'requires_action') return false;
349
+ const na = field(c1, 'next_action') as Body;
350
+ if (!na || na.type !== 'use_stripe_sdk') return false;
351
+ // the interim transition recorded a requires_action event, not a succeeded one.
352
+ const events = await h({ m: 'GET', p: '/v1/events' });
353
+ const types = ((events.body as Body).data as Body[]).map((e) => e.type);
354
+ if (!types.includes('payment_intent.requires_action') || types.includes('payment_intent.succeeded')) return false;
355
+ // completing the challenge (a second confirm) succeeds and clears next_action.
356
+ const c2 = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm` });
357
+ return ok(c2) && field(c2, 'status') === 'succeeded' && field(c2, 'next_action') === null;
358
+ }),
359
+ ),
360
+ // (increment_auth upgraded to done() in the AUDIT GROWTH block below.)
361
+ // automatic_payment_methods: enabling it on create normalizes to { enabled, allow_redirects }
362
+ // and derives a payment_method_types list; payment_method_options pass through. A PI without
363
+ // it reports automatic_payment_methods null. (the normalized APM object is produced ONLY by
364
+ // this feature.)
365
+ done('stripe.payment_intents.automatic_pm', 'payment_intents', 'automatic_payment_methods + payment_method options', 'api', 'common', () =>
366
+ withRoot(async (h) => {
367
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=1200&currency=usd&automatic_payment_methods[enabled]=true&payment_method_options[card][request_three_d_secure]=automatic' });
368
+ if (!ok(pi)) return false;
369
+ const apm = field(pi, 'automatic_payment_methods') as Body;
370
+ if (!apm || apm.enabled !== true || apm.allow_redirects !== 'always') return false;
371
+ if (!Array.isArray(field(pi, 'payment_method_types')) || !(field(pi, 'payment_method_types') as string[]).includes('card')) return false;
372
+ const pmo = field(pi, 'payment_method_options') as Body;
373
+ if (!pmo || (pmo.card as Body)?.request_three_d_secure !== 'automatic') return false;
374
+ // a plain PI reports automatic_payment_methods null (not fabricated).
375
+ const plain = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=500&currency=usd' });
376
+ return field(plain, 'automatic_payment_methods') === null;
377
+ }),
378
+ ),
379
+ // PaymentIntent search: GET /v1/payment_intents/search?query=… returns the search_result
380
+ // envelope; supports AND-combined clauses (currency:"usd" AND amount>=num). Missing query 400s.
381
+ done('stripe.payment_intents.search', 'payment_intents', 'PaymentIntent search', 'api', 'common', () =>
382
+ withRoot(async (h) => {
383
+ await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=8000&currency=usd' });
384
+ await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=200&currency=usd' });
385
+ await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=9000&currency=eur' });
386
+ const r = await h({ m: 'GET', p: `/v1/payment_intents/search?query=${encodeURIComponent('currency:"usd" AND amount>=5000')}` });
387
+ if (!ok(r) || field(r, 'object') !== 'search_result') return false;
388
+ const data = (r.body as Body).data as Body[];
389
+ const noQuery = await h({ m: 'GET', p: '/v1/payment_intents/search' });
390
+ return data.length === 1 && data[0]!.amount === 8000 && noQuery.status === 400;
391
+ }),
392
+ ),
393
+
394
+ // ── Charges ─────────────────────────────────────────────────────────────────────
395
+ done('stripe.charges.create', 'charges', 'Charges: create (succeeded) + retrieve + list', 'api', 'core', () =>
396
+ withRoot(async (h) => {
397
+ const ch = await h({ m: 'POST', p: '/v1/charges', b: 'amount=2000&currency=usd' });
398
+ if (!ok(ch) || field(ch, 'status') !== 'succeeded' || field(ch, 'paid') !== true) return false;
399
+ const g = await h({ m: 'GET', p: `/v1/charges/${id(ch)}` });
400
+ const l = await h({ m: 'GET', p: '/v1/charges' });
401
+ return ok(g) && id(g) === id(ch) && field(l, 'object') === 'list';
402
+ }),
403
+ ),
404
+ done('stripe.charges.test_declines', 'charges', 'Test-card declines → 402 typed card_error (no charge persisted)', 'api', 'core', () =>
405
+ withRoot(async (h) => {
406
+ const dec = await h({ m: 'POST', p: '/v1/charges', b: 'amount=1000&currency=usd&source[number]=4000000000000002' });
407
+ const ins = await h({ m: 'POST', p: '/v1/charges', b: 'amount=1000&currency=usd&source[number]=4000000000009995' });
408
+ const decErr = (dec.body as Body).error as Body;
409
+ const insErr = (ins.body as Body).error as Body;
410
+ return dec.status === 402 && decErr?.code === 'card_declined' && ins.status === 402 && insErr?.decline_code === 'insufficient_funds';
411
+ }),
412
+ ),
413
+ // Charge capture: a charge created with capture=false is an uncaptured auth (captured:false,
414
+ // amount_captured:0); /capture captures it (captured:true, amount_captured set). Re-capture
415
+ // is the vendor 400 charge_already_captured; an immediately-captured charge (default) cannot
416
+ // be captured again. (captured toggle produced ONLY by this feature.)
417
+ done('stripe.charges.capture', 'charges', 'Charge capture (uncaptured auth)', 'api', 'common', () =>
418
+ withRoot(async (h) => {
419
+ const auth = await h({ m: 'POST', p: '/v1/charges', b: 'amount=3000&currency=usd&capture=false' });
420
+ if (!ok(auth) || field(auth, 'captured') !== false || field(auth, 'amount_captured') !== 0) return false;
421
+ const cap = await h({ m: 'POST', p: `/v1/charges/${id(auth)}/capture` });
422
+ if (!ok(cap) || field(cap, 'captured') !== true || field(cap, 'amount_captured') !== 3000) return false;
423
+ const again = await h({ m: 'POST', p: `/v1/charges/${id(auth)}/capture` });
424
+ const def = await h({ m: 'POST', p: '/v1/charges', b: 'amount=1000&currency=usd' });
425
+ const capDef = await h({ m: 'POST', p: `/v1/charges/${id(def)}/capture` });
426
+ const missing = await h({ m: 'POST', p: '/v1/charges/ch_nope/capture' });
427
+ return field(def, 'captured') === true && again.status === 400 &&
428
+ ((again.body as Body).error as Body)?.code === 'charge_already_captured' &&
429
+ capDef.status === 400 && missing.status === 404;
430
+ }),
431
+ ),
432
+ // Charge search: GET /v1/charges/search?query=amount>num returns the search_result envelope
433
+ // of matching charges (numeric comparison via the query language). Missing query 400s.
434
+ done('stripe.charges.search', 'charges', 'Charge search', 'api', 'common', () =>
435
+ withRoot(async (h) => {
436
+ await h({ m: 'POST', p: '/v1/charges', b: 'amount=5000&currency=usd' });
437
+ await h({ m: 'POST', p: '/v1/charges', b: 'amount=100&currency=usd' });
438
+ const r = await h({ m: 'GET', p: `/v1/charges/search?query=${encodeURIComponent('amount>1000')}` });
439
+ if (!ok(r) || field(r, 'object') !== 'search_result') return false;
440
+ const data = (r.body as Body).data as Body[];
441
+ const noQuery = await h({ m: 'GET', p: '/v1/charges/search' });
442
+ return data.length === 1 && data[0]!.amount === 5000 && noQuery.status === 400;
443
+ }),
444
+ ),
445
+ // Charge fraud-marking (Radar): POST /v1/charges/:id with fraud_details[user_report]=safe|
446
+ // fraudulent stores the canonical fraud_details { user_report, stripe_report } on the charge;
447
+ // it round-trips on retrieve. An invalid user_report 400s; unknown charge 404.
448
+ done('stripe.charges.fraud_details', 'charges', 'Charge fraud_details / mark safe/fraudulent', 'api', 'niche', () =>
449
+ withRoot(async (h) => {
450
+ const ch = await h({ m: 'POST', p: '/v1/charges', b: 'amount=2000&currency=usd' });
451
+ if (!ok(ch)) return false;
452
+ const marked = await h({ m: 'POST', p: `/v1/charges/${id(ch)}`, b: 'fraud_details[user_report]=fraudulent' });
453
+ if (!ok(marked) || (field(marked, 'fraud_details') as Body)?.user_report !== 'fraudulent') return false;
454
+ const g = await h({ m: 'GET', p: `/v1/charges/${id(ch)}` });
455
+ if ((field(g, 'fraud_details') as Body)?.user_report !== 'fraudulent') return false;
456
+ const safe = await h({ m: 'POST', p: `/v1/charges/${id(ch)}`, b: 'fraud_details[user_report]=safe' });
457
+ const bad = await h({ m: 'POST', p: `/v1/charges/${id(ch)}`, b: 'fraud_details[user_report]=maybe' });
458
+ const missing = await h({ m: 'POST', p: '/v1/charges/ch_nope', b: 'fraud_details[user_report]=safe' });
459
+ return ok(safe) && (field(safe, 'fraud_details') as Body)?.user_report === 'safe' && bad.status === 400 && missing.status === 404;
460
+ }),
461
+ ),
462
+
463
+ // ── Refunds ─────────────────────────────────────────────────────────────────────
464
+ done('stripe.refunds.crud', 'refunds', 'Refunds: create + retrieve + list', 'api', 'core', () =>
465
+ withRoot(async (h) => {
466
+ const r = await h({ m: 'POST', p: '/v1/refunds', b: 'charge=ch_twin&amount=500' });
467
+ if (!ok(r) || field(r, 'object') !== 'refund') return false;
468
+ const g = await h({ m: 'GET', p: `/v1/refunds/${id(r)}` });
469
+ const l = await h({ m: 'GET', p: '/v1/refunds' });
470
+ return ok(g) && id(g) === id(r) && field(l, 'object') === 'list';
471
+ }),
472
+ ),
473
+ // Refund update: POST /v1/refunds/:id persists metadata; retrieve round-trips it. Unknown id 404.
474
+ done('stripe.refunds.update', 'refunds', 'Refund update (metadata)', 'api', 'common', () =>
475
+ withRoot(async (h) => {
476
+ const r = await h({ m: 'POST', p: '/v1/refunds', b: 'charge=ch_twin&amount=500' });
477
+ const u = await h({ m: 'POST', p: `/v1/refunds/${id(r)}`, b: 'metadata[reason]=duplicate' });
478
+ if (!ok(u) || ((field(u, 'metadata') as Body)?.reason) !== 'duplicate') return false;
479
+ const g = await h({ m: 'GET', p: `/v1/refunds/${id(r)}` });
480
+ const missing = await h({ m: 'POST', p: '/v1/refunds/re_nope', b: 'metadata[x]=1' });
481
+ return ok(g) && ((field(g, 'metadata') as Body)?.reason) === 'duplicate' && missing.status === 404;
482
+ }),
483
+ ),
484
+ // (refunds.cancel upgraded to done() in the AUDIT GROWTH block below.)
485
+
486
+ // ── Disputes ────────────────────────────────────────────────────────────────────
487
+ done('stripe.disputes.crud', 'disputes', 'Disputes: create + retrieve + list', 'api', 'common', () =>
488
+ withRoot(async (h) => {
489
+ const d = await h({ m: 'POST', p: '/v1/disputes', b: 'charge=ch_twin&amount=1000&currency=usd' });
490
+ if (!ok(d) || field(d, 'object') !== 'dispute') return false;
491
+ const g = await h({ m: 'GET', p: `/v1/disputes/${id(d)}` });
492
+ const l = await h({ m: 'GET', p: '/v1/disputes' });
493
+ return ok(g) && id(g) === id(d) && field(l, 'object') === 'list';
494
+ }),
495
+ ),
496
+ done('stripe.disputes.evidence', 'disputes', 'Dispute evidence submission (update) + close', 'api', 'common', () =>
497
+ withRoot(async (h) => {
498
+ const d = await h({ m: 'POST', p: '/v1/disputes', b: 'charge=ch_twin&amount=1000&currency=usd' });
499
+ const ev = await h({ m: 'POST', p: `/v1/disputes/${id(d)}`, b: 'evidence[uncategorized_text]=we shipped it' });
500
+ const cl = await h({ m: 'POST', p: `/v1/disputes/${id(d)}/close` });
501
+ return ok(ev) && ok(cl) && field(cl, 'status') === 'lost';
502
+ }),
503
+ ),
504
+
505
+ // ── Balance / BalanceTransactions / Payouts ───────────────────────────────────────
506
+ done('stripe.balance.retrieve', 'balance', 'Balance retrieve (available/pending)', 'api', 'core', () =>
507
+ withRoot(async (h) => {
508
+ const r = await h({ m: 'GET', p: '/v1/balance' });
509
+ return ok(r) && field(r, 'object') === 'balance';
510
+ }),
511
+ ),
512
+ done('stripe.balance_transactions.crud', 'balance', 'BalanceTransactions: create + retrieve + list', 'api', 'core', () =>
513
+ withRoot(async (h) => {
514
+ const t = await h({ m: 'POST', p: '/v1/balance_transactions', b: 'amount=1000&currency=usd' });
515
+ if (!ok(t)) return false;
516
+ const g = await h({ m: 'GET', p: `/v1/balance_transactions/${id(t)}` });
517
+ const l = await h({ m: 'GET', p: '/v1/balance_transactions' });
518
+ return ok(g) && id(g) === id(t) && field(l, 'object') === 'list';
519
+ }),
520
+ ),
521
+ done('stripe.payouts.crud', 'payouts', 'Payouts: create + retrieve + list + cancel', 'api', 'core', () =>
522
+ withRoot(async (h) => {
523
+ const p = await h({ m: 'POST', p: '/v1/payouts', b: 'amount=1000&currency=usd' });
524
+ if (!ok(p) || field(p, 'object') !== 'payout') return false;
525
+ const g = await h({ m: 'GET', p: `/v1/payouts/${id(p)}` });
526
+ const c = await h({ m: 'POST', p: `/v1/payouts/${id(p)}/cancel` });
527
+ return ok(g) && id(g) === id(p) && ok(c);
528
+ }),
529
+ ),
530
+ // Payout reverse: POST /v1/payouts/:id/reverse materializes the reversal as a NEW payout
531
+ // (carrying original_payout) and marks the original canceled + reversed_by. Reversing twice
532
+ // 400s; unknown id 404. (Only this feature sets original_payout / reversed_by.)
533
+ done('stripe.payouts.reverse', 'payouts', 'Payout reverse', 'api', 'niche', () =>
534
+ withRoot(async (h) => {
535
+ const p = await h({ m: 'POST', p: '/v1/payouts', b: 'amount=4000&currency=usd' });
536
+ if (!ok(p)) return false;
537
+ const rev = await h({ m: 'POST', p: `/v1/payouts/${id(p)}/reverse` });
538
+ if (!ok(rev) || field(rev, 'original_payout') !== id(p) || field(rev, 'amount') !== 4000) return false;
539
+ const orig = await h({ m: 'GET', p: `/v1/payouts/${id(p)}` });
540
+ if (field(orig, 'status') !== 'canceled' || field(orig, 'reversed_by') !== id(rev)) return false;
541
+ const twice = await h({ m: 'POST', p: `/v1/payouts/${id(p)}/reverse` });
542
+ const nope = await h({ m: 'POST', p: '/v1/payouts/po_nope/reverse' });
543
+ return twice.status === 400 && nope.status === 404;
544
+ }),
545
+ ),
546
+ // Payout schedule: the automatic-payout cadence lives in account.settings.payouts.schedule.
547
+ // GET /v1/account returns the platform account with a default daily schedule; POST updates
548
+ // it to e.g. weekly+anchor (anchors normalized — weekly_anchor set, monthly_anchor null) and
549
+ // it persists. A connected account also carries its own schedule. (settings.payouts.schedule
550
+ // produced ONLY by this feature.)
551
+ done('stripe.payouts.schedule', 'payouts', 'Automatic payout schedule settings', 'api', 'common', () =>
552
+ withRoot(async (h) => {
553
+ const acct = await h({ m: 'GET', p: '/v1/account' });
554
+ if (!ok(acct)) return false;
555
+ const sched0 = ((field(acct, 'settings') as Body)?.payouts as Body)?.schedule as Body;
556
+ if (!sched0 || sched0.interval !== 'daily') return false;
557
+ const upd = await h({ m: 'POST', p: '/v1/account', b: 'settings[payouts][schedule][interval]=weekly&settings[payouts][schedule][weekly_anchor]=friday' });
558
+ const sched1 = ((field(upd, 'settings') as Body)?.payouts as Body)?.schedule as Body;
559
+ if (!ok(upd) || sched1.interval !== 'weekly' || sched1.weekly_anchor !== 'friday' || sched1.monthly_anchor !== null) return false;
560
+ // persists across a re-read.
561
+ const acct2 = await h({ m: 'GET', p: '/v1/account' });
562
+ const sched2 = ((field(acct2, 'settings') as Body)?.payouts as Body)?.schedule as Body;
563
+ if (sched2.interval !== 'weekly') return false;
564
+ // a connected account carries its own schedule.
565
+ const conn = await h({ m: 'POST', p: '/v1/accounts', b: 'type=express&settings[payouts][schedule][interval]=monthly&settings[payouts][schedule][monthly_anchor]=15' });
566
+ const cs = ((field(conn, 'settings') as Body)?.payouts as Body)?.schedule as Body;
567
+ return ok(conn) && cs.interval === 'monthly' && cs.monthly_anchor === 15;
568
+ }),
569
+ ),
570
+
571
+ // ── Products / Prices ─────────────────────────────────────────────────────────────
572
+ done('stripe.products.crud', 'catalog', 'Products: create + retrieve + update + list', 'api', 'core', () =>
573
+ withRoot(async (h) => {
574
+ const p = await h({ m: 'POST', p: '/v1/products', b: 'name=Widget' });
575
+ if (!ok(p) || field(p, 'object') !== 'product') return false;
576
+ const g = await h({ m: 'GET', p: `/v1/products/${id(p)}` });
577
+ const u = await h({ m: 'POST', p: `/v1/products/${id(p)}`, b: 'name=Widget Pro' });
578
+ const l = await h({ m: 'GET', p: '/v1/products' });
579
+ return ok(g) && ok(u) && field(u, 'name') === 'Widget Pro' && field(l, 'object') === 'list';
580
+ }),
581
+ ),
582
+ done('stripe.prices.crud', 'catalog', 'Prices: create (referential to product) + retrieve + list', 'api', 'core', () =>
583
+ withRoot(async (h) => {
584
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Widget' });
585
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=1500&currency=usd&recurring[interval]=month&product=${id(prod)}` });
586
+ if (!ok(price) || field(price, 'object') !== 'price') return false;
587
+ const g = await h({ m: 'GET', p: `/v1/prices/${id(price)}` });
588
+ const l = await h({ m: 'GET', p: '/v1/prices' });
589
+ // referential integrity: a bad product is rejected
590
+ const bad = await h({ m: 'POST', p: '/v1/prices', b: 'unit_amount=100&currency=usd&product=prod_does_not_exist' });
591
+ return ok(g) && field(l, 'object') === 'list' && bad.status >= 400;
592
+ }),
593
+ ),
594
+ // Tiered pricing: a billing_scheme=tiered price requires tiers_mode (graduated|volume) +
595
+ // tiers[] (each up_to + unit_amount/flat_amount); the twin normalizes the tiers, forces
596
+ // unit_amount null, and round-trips them. currency_options pass through (a per-currency
597
+ // unit_amount). A tiered price missing tiers_mode/tiers 400s. (tiers/tiers_mode produced
598
+ // ONLY by this feature.)
599
+ done('stripe.prices.tiers', 'catalog', 'Tiered / graduated / volume prices + currency_options', 'api', 'common', () =>
600
+ withRoot(async (h) => {
601
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Tiered' });
602
+ const body = `product=${id(prod)}&currency=usd&recurring[interval]=month&billing_scheme=tiered&tiers_mode=graduated`
603
+ + '&tiers[0][up_to]=10&tiers[0][unit_amount]=100'
604
+ + '&tiers[1][up_to]=inf&tiers[1][unit_amount]=50'
605
+ + '&currency_options[eur][unit_amount]=90';
606
+ const p = await h({ m: 'POST', p: '/v1/prices', b: body });
607
+ if (!ok(p) || field(p, 'billing_scheme') !== 'tiered' || field(p, 'tiers_mode') !== 'graduated' || field(p, 'unit_amount') !== null) return false;
608
+ const tiers = field(p, 'tiers') as Body[];
609
+ if (!Array.isArray(tiers) || tiers.length !== 2 || tiers[0]!.up_to !== 10 || tiers[0]!.unit_amount !== 100 || tiers[1]!.up_to !== null || tiers[1]!.unit_amount !== 50) return false;
610
+ const co = field(p, 'currency_options') as Body;
611
+ if (!co || (co.eur as Body)?.unit_amount !== 90) return false;
612
+ const g = await h({ m: 'GET', p: `/v1/prices/${id(p)}` });
613
+ // a tiered price WITHOUT tiers_mode / tiers is rejected like Stripe.
614
+ const noMode = await h({ m: 'POST', p: '/v1/prices', b: `product=${id(prod)}&currency=usd&billing_scheme=tiered&tiers[0][up_to]=inf&tiers[0][unit_amount]=10` });
615
+ const noTiers = await h({ m: 'POST', p: '/v1/prices', b: `product=${id(prod)}&currency=usd&billing_scheme=tiered&tiers_mode=volume` });
616
+ return ok(g) && (field(g, 'tiers') as Body[]).length === 2 && noMode.status === 400 && noTiers.status === 400;
617
+ }),
618
+ ),
619
+ // Product delete: a product with NO prices hard-deletes (→ deleted stub, 404 on retrieve,
620
+ // dropped from list). A product WITH a price cannot be deleted — the vendor 400 (deactivate
621
+ // instead). Unknown id 404. (the price-guard is produced ONLY by this feature.)
622
+ done('stripe.products.delete', 'catalog', 'Product delete (guarded by attached prices)', 'api', 'common', () =>
623
+ withRoot(async (h) => {
624
+ const p1 = await h({ m: 'POST', p: '/v1/products', b: 'name=Deletable' });
625
+ const before = ((await h({ m: 'GET', p: '/v1/products' })).body as Body).data as Body[];
626
+ const del = await h({ m: 'DELETE', p: `/v1/products/${id(p1)}` });
627
+ if (!ok(del) || field(del, 'deleted') !== true) return false;
628
+ const gone = await h({ m: 'GET', p: `/v1/products/${id(p1)}` });
629
+ const after = ((await h({ m: 'GET', p: '/v1/products' })).body as Body).data as Body[];
630
+ if (gone.status !== 404 || after.length !== before.length - 1) return false;
631
+ // a product with a price cannot be deleted
632
+ const p2 = await h({ m: 'POST', p: '/v1/products', b: 'name=HasPrice' });
633
+ await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=100&currency=usd&product=${id(p2)}` });
634
+ const guarded = await h({ m: 'DELETE', p: `/v1/products/${id(p2)}` });
635
+ const missing = await h({ m: 'DELETE', p: '/v1/products/prod_nope' });
636
+ return guarded.status === 400 && missing.status === 404;
637
+ }),
638
+ ),
639
+
640
+ // ── Coupons / PromotionCodes ──────────────────────────────────────────────────────
641
+ // Pure list: the twin has no PromotionCode create path, so this is honestly scoped to
642
+ // the list endpoint only (a real GET asserting the Stripe list envelope). Create/filter
643
+ // round-trips are tracked as the separate `promotion_codes.create` todo below.
644
+ done('stripe.promotion_codes.list', 'discounts', 'PromotionCodes list (object:list envelope)', 'api', 'common', listOk('/v1/promotion_codes')),
645
+ // Coupons: create a percent_off coupon (valid:true), retrieve + update its name, list, and
646
+ // delete (→ deleted stub, then 404 + dropped from list). Vendor errors: passing BOTH
647
+ // percent_off and amount_off 400s; an out-of-range percent_off 400s; amount_off without
648
+ // currency 400s. (valid/duration/percent_off are produced ONLY by this feature.)
649
+ done('stripe.coupons.crud', 'discounts', 'Coupons: create / retrieve / update / delete / list', 'api', 'common', () =>
650
+ withRoot(async (h) => {
651
+ const c = await h({ m: 'POST', p: '/v1/coupons', b: 'percent_off=25&duration=once&name=Quarter Off' });
652
+ if (!ok(c) || field(c, 'object') !== 'coupon' || field(c, 'percent_off') !== 25 || field(c, 'valid') !== true || field(c, 'duration') !== 'once') return false;
653
+ const g = await h({ m: 'GET', p: `/v1/coupons/${id(c)}` });
654
+ const u = await h({ m: 'POST', p: `/v1/coupons/${id(c)}`, b: 'name=Renamed' });
655
+ const l = await h({ m: 'GET', p: '/v1/coupons' });
656
+ if (!ok(g) || id(g) !== id(c) || !ok(u) || field(u, 'name') !== 'Renamed' || field(l, 'object') !== 'list') return false;
657
+ const del = await h({ m: 'DELETE', p: `/v1/coupons/${id(c)}` });
658
+ const gone = await h({ m: 'GET', p: `/v1/coupons/${id(c)}` });
659
+ // vendor errors
660
+ const both = await h({ m: 'POST', p: '/v1/coupons', b: 'percent_off=10&amount_off=500&currency=usd&duration=once' });
661
+ const oob = await h({ m: 'POST', p: '/v1/coupons', b: 'percent_off=150&duration=once' });
662
+ const noCur = await h({ m: 'POST', p: '/v1/coupons', b: 'amount_off=500&duration=once' });
663
+ return ok(del) && field(del, 'deleted') === true && gone.status === 404 &&
664
+ both.status === 400 && oob.status === 400 && noCur.status === 400;
665
+ }),
666
+ ),
667
+ // PromotionCode: create against a coupon (auto/explicit code, active:true), retrieve + list,
668
+ // then deactivate (active:false) and reactivate via update. Vendor errors: missing coupon 400;
669
+ // unknown coupon 400 resource_missing. (code/active toggle produced ONLY by this feature.)
670
+ done('stripe.promotion_codes.create', 'discounts', 'PromotionCode create + activate/deactivate', 'api', 'common', () =>
671
+ withRoot(async (h) => {
672
+ const coupon = await h({ m: 'POST', p: '/v1/coupons', b: 'percent_off=10&duration=once' });
673
+ const pc = await h({ m: 'POST', p: '/v1/promotion_codes', b: `coupon=${id(coupon)}&code=SAVE10` });
674
+ if (!ok(pc) || field(pc, 'object') !== 'promotion_code' || field(pc, 'code') !== 'SAVE10' || field(pc, 'active') !== true || field(pc, 'coupon') !== id(coupon)) return false;
675
+ const g = await h({ m: 'GET', p: `/v1/promotion_codes/${id(pc)}` });
676
+ const off = await h({ m: 'POST', p: `/v1/promotion_codes/${id(pc)}`, b: 'active=false' });
677
+ const on = await h({ m: 'POST', p: `/v1/promotion_codes/${id(pc)}`, b: 'active=true' });
678
+ const lst = await h({ m: 'GET', p: `/v1/promotion_codes?code=SAVE10` });
679
+ if (!ok(g) || id(g) !== id(pc) || !ok(off) || field(off, 'active') !== false || !ok(on) || field(on, 'active') !== true) return false;
680
+ if (((lst.body as Body).data as Body[]).length !== 1) return false;
681
+ const noCoupon = await h({ m: 'POST', p: '/v1/promotion_codes', b: 'code=X' });
682
+ const badCoupon = await h({ m: 'POST', p: '/v1/promotion_codes', b: 'coupon=coupon_nope&code=Y' });
683
+ return noCoupon.status === 400 && badCoupon.status === 400 && ((badCoupon.body as Body).error as Body)?.code === 'resource_missing';
684
+ }),
685
+ ),
686
+
687
+ // ── SetupIntents / PaymentMethods ─────────────────────────────────────────────────
688
+ done('stripe.setup_intents.confirm', 'payment_methods', 'SetupIntents: create + confirm → succeeded', 'api', 'common', () =>
689
+ withRoot(async (h) => {
690
+ const si = await h({ m: 'POST', p: '/v1/setup_intents' });
691
+ if (!ok(si) || field(si, 'object') !== 'setup_intent') return false;
692
+ const c = await h({ m: 'POST', p: `/v1/setup_intents/${id(si)}/confirm`, b: 'payment_method=pm_card_visa' });
693
+ return ok(c) && field(c, 'status') === 'succeeded';
694
+ }),
695
+ ),
696
+ done('stripe.payment_methods.list', 'payment_methods', 'PaymentMethods list (object:list envelope)', 'api', 'core', listOk('/v1/payment_methods')),
697
+ // Create a card PM (detached: customer null), attach it to a customer, re-read to prove the
698
+ // attach persisted, and confirm it surfaces in the customer-scoped list. Vendor errors:
699
+ // attach to an unknown PM → 404; attach with an unknown customer → 400 resource_missing.
700
+ done('stripe.payment_methods.attach', 'payment_methods', 'PaymentMethod create + attach to customer', 'api', 'core', () =>
701
+ withRoot(async (h) => {
702
+ const cus = await h({ m: 'POST', p: '/v1/customers', b: 'email=pm@example.com' });
703
+ const pm = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2030&card[cvc]=314' });
704
+ if (!ok(cus) || !ok(pm) || field(pm, 'type') !== 'card' || field(pm, 'customer') !== null) return false;
705
+ const card = (pm.body as Body).card as Body | undefined;
706
+ if (!card || card.last4 !== '4242') return false;
707
+ const att = await h({ m: 'POST', p: `/v1/payment_methods/${id(pm)}/attach`, b: `customer=${id(cus)}` });
708
+ if (!ok(att) || field(att, 'customer') !== id(cus)) return false;
709
+ const g = await h({ m: 'GET', p: `/v1/payment_methods/${id(pm)}` });
710
+ if (!ok(g) || field(g, 'customer') !== id(cus)) return false;
711
+ const lst = await h({ m: 'GET', p: `/v1/payment_methods?customer=${id(cus)}&type=card` });
712
+ if (!ok(lst) || (lst.body as Body).object !== 'list' || ((lst.body as Body).data as unknown[]).length !== 1) return false;
713
+ // vendor errors: unknown PM → 404; unknown customer → 400 resource_missing
714
+ const missPm = await h({ m: 'POST', p: '/v1/payment_methods/pm_nope/attach', b: `customer=${id(cus)}` });
715
+ const missCus = await h({ m: 'POST', p: `/v1/payment_methods/${id(pm)}/attach`, b: 'customer=cus_nope' });
716
+ return missPm.status === 404 &&
717
+ missCus.status === 400 && ((missCus.body as Body).error as Body)?.code === 'resource_missing';
718
+ }),
719
+ ),
720
+ // Non-card PaymentMethods: creating a sepa_debit / us_bank_account / link PM synthesizes
721
+ // the canonical type-specific sub-object (never echoing the raw IBAN/account number — only
722
+ // last4-style fields), and the `type` round-trips. attach to a customer still works. (The
723
+ // sepa_debit/us_bank_account sub-objects are produced ONLY by this feature.)
724
+ done('stripe.payment_methods.types', 'payment_methods', 'Non-card PM types (sepa/ach/link/wallets)', 'api', 'common', () =>
725
+ withRoot(async (h) => {
726
+ const sepa = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=sepa_debit&sepa_debit[iban]=DE89370400440532013000' });
727
+ if (!ok(sepa) || field(sepa, 'type') !== 'sepa_debit') return false;
728
+ const sd = field(sepa, 'sepa_debit') as Body;
729
+ if (!sd || sd.last4 !== '3000' || sd.country !== 'DE' || 'iban' in sd) return false;
730
+ const ach = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=us_bank_account&us_bank_account[account_number]=000123456789&us_bank_account[routing_number]=110000000' });
731
+ const ab = field(ach, 'us_bank_account') as Body;
732
+ if (!ok(ach) || field(ach, 'type') !== 'us_bank_account' || ab?.last4 !== '6789' || ab?.routing_number !== '110000000') return false;
733
+ const link = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=link' });
734
+ if (!ok(link) || field(link, 'type') !== 'link' || typeof field(link, 'link') !== 'object') return false;
735
+ // attach a non-card PM to a customer (the same flow as cards).
736
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=sepa@twin.test' });
737
+ const att = await h({ m: 'POST', p: `/v1/payment_methods/${id(sepa)}/attach`, b: `customer=${id(cust)}` });
738
+ // a card PM still produces its card sub-object (no regression).
739
+ const card = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=4242424242424242' });
740
+ return ok(att) && field(att, 'customer') === id(cust) && (field(card, 'card') as Body)?.last4 === '4242';
741
+ }),
742
+ ),
743
+ // EphemeralKeys: POST /v1/ephemeral_keys mints a customer-scoped short-lived key for the
744
+ // mobile SDK. Requires the Stripe-Version header (apiVersion) + exactly one scope; the key
745
+ // is stateful (DELETE expires it). associated_objects reflects the scope. Missing version or
746
+ // unknown customer → 400. (Only this feature returns object 'ephemeral_key' with a secret.)
747
+ done('stripe.ephemeral_keys', 'payment_methods', 'EphemeralKeys (mobile SDK)', 'api', 'niche', async () => {
748
+ const root = mkdtempSync(join(tmpdir(), 'stp-ek-'));
749
+ try {
750
+ const hv = (s: Step & { v?: string }) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root, ...(s.v ? { apiVersion: s.v } : {}) });
751
+ const cust = await hv({ m: 'POST', p: '/v1/customers', b: 'email=ek@twin.test' });
752
+ // missing Stripe-Version → 400 (the SDK always sends it).
753
+ const noVer = await hv({ m: 'POST', p: '/v1/ephemeral_keys', b: `customer=${id(cust)}` });
754
+ if (noVer.status !== 400) return false;
755
+ const key = await hv({ m: 'POST', p: '/v1/ephemeral_keys', b: `customer=${id(cust)}`, v: '2024-06-20' });
756
+ if (!ok(key) || field(key, 'object') !== 'ephemeral_key' || typeof field(key, 'secret') !== 'string') return false;
757
+ if (((field(key, 'associated_objects') as Body[])[0]?.id) !== id(cust)) return false;
758
+ // unknown customer → 400; no scope → 400.
759
+ const badCust = await hv({ m: 'POST', p: '/v1/ephemeral_keys', b: 'customer=cus_nope', v: '2024-06-20' });
760
+ const noScope = await hv({ m: 'POST', p: '/v1/ephemeral_keys', b: '', v: '2024-06-20' });
761
+ // DELETE expires it early (retrievable shape; expires bumped).
762
+ const del = await hv({ m: 'DELETE', p: `/v1/ephemeral_keys/${id(key)}` });
763
+ return badCust.status === 400 && noScope.status === 400 && ok(del);
764
+ } catch {
765
+ return false;
766
+ } finally {
767
+ rmSync(root, { recursive: true, force: true });
768
+ }
769
+ }),
770
+
771
+ // ── Subscriptions ─────────────────────────────────────────────────────────────────
772
+ done('stripe.subscriptions.lifecycle', 'subscriptions', 'Subscriptions: create (active) + retrieve + update + cancel', 'api', 'core', () =>
773
+ withRoot(async (h) => {
774
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=sub@twin.test' });
775
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Plan' });
776
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=1500&currency=usd&recurring[interval]=month&product=${id(prod)}` });
777
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}&items[0][price]=${id(price)}` });
778
+ if (!ok(sub) || field(sub, 'status') !== 'active') return false;
779
+ const g = await h({ m: 'GET', p: `/v1/subscriptions/${id(sub)}` });
780
+ const cancel = await h({ m: 'DELETE', p: `/v1/subscriptions/${id(sub)}` });
781
+ return ok(g) && ok(cancel) && field(cancel, 'status') === 'canceled';
782
+ }),
783
+ ),
784
+ done('stripe.subscriptions.list', 'subscriptions', 'Subscription list + filter by customer', 'api', 'core', () =>
785
+ withRoot(async (h) => {
786
+ const r = await h({ m: 'GET', p: '/v1/subscriptions' });
787
+ return ok(r) && field(r, 'object') === 'list';
788
+ }),
789
+ ),
790
+ // Proration + trials: a subscription created with trial_period_days lands in `trialing`
791
+ // with trial_start/trial_end set; the upcoming invoice (GET /v1/invoices/upcoming) computes
792
+ // a proration_date credit+charge split off the sub's item price. (trialing status +
793
+ // proration lines produced ONLY by this feature.)
794
+ done('stripe.subscriptions.proration', 'subscriptions', 'Proration / upcoming invoice / trials', 'api', 'common', () =>
795
+ withRoot(async (h) => {
796
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=trial@twin.test' });
797
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Plan' });
798
+ const price = await h({ m: 'POST', p: `/v1/prices`, b: `unit_amount=2000&currency=usd&recurring[interval]=month&product=${id(prod)}` });
799
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}&items[0][price]=${id(price)}&trial_period_days=14` });
800
+ if (!ok(sub) || field(sub, 'status') !== 'trialing' || typeof field(sub, 'trial_end') !== 'number') return false;
801
+ // upcoming invoice WITH a proration_date splits the period (credit + charge proration lines).
802
+ const up = await h({ m: 'GET', p: `/v1/invoices/upcoming?subscription=${id(sub)}&proration_date=100` });
803
+ if (!ok(up) || field(up, 'object') !== 'invoice' || field(up, 'id') !== null) return false;
804
+ const lines = (field(up, 'lines') as Body).data as Body[];
805
+ const prorations = lines.filter((l) => l.proration === true);
806
+ return prorations.length === 2 && prorations.some((l) => Number(l.amount) < 0) && prorations.some((l) => Number(l.amount) > 0);
807
+ }),
808
+ ),
809
+ // Pause/resume: set pause_collection[behavior]=void → the sub records pause_collection (and
810
+ // the customer.subscription.paused event fires); clearing it (pause_collection="") resumes
811
+ // (pause_collection null). cancel_at_period_end toggles a scheduled cancel and reactivation.
812
+ // (pause_collection persistence produced ONLY by this feature.)
813
+ done('stripe.subscriptions.pause', 'subscriptions', 'Pause collection + cancel_at_period_end resume', 'api', 'common', () =>
814
+ withRoot(async (h) => {
815
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=pause@twin.test' });
816
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}` });
817
+ if (!ok(sub)) return false;
818
+ const paused = await h({ m: 'POST', p: `/v1/subscriptions/${id(sub)}`, b: 'pause_collection[behavior]=void' });
819
+ if (!ok(paused) || (field(paused, 'pause_collection') as Body)?.behavior !== 'void') return false;
820
+ const resumed = await h({ m: 'POST', p: `/v1/subscriptions/${id(sub)}`, b: 'pause_collection=' });
821
+ if (!ok(resumed) || field(resumed, 'pause_collection') !== null) return false;
822
+ const sched = await h({ m: 'POST', p: `/v1/subscriptions/${id(sub)}`, b: 'cancel_at_period_end=true' });
823
+ const react = await h({ m: 'POST', p: `/v1/subscriptions/${id(sub)}`, b: 'cancel_at_period_end=false' });
824
+ return ok(sched) && field(sched, 'cancel_at_period_end') === true && ok(react) && field(react, 'cancel_at_period_end') === false;
825
+ }),
826
+ ),
827
+ // SubscriptionSchedules: create from a customer with phases[] (each phase = items +
828
+ // duration); the twin normalizes phases (start_date/end_date timeline) + current_phase,
829
+ // round-trips retrieve/list, and release/cancel are terminal transitions (a second one
830
+ // 400s). Missing customer+from_subscription 400. (phases/current_phase + status transitions
831
+ // produced ONLY by this feature.)
832
+ done('stripe.subscription_schedules', 'subscriptions', 'SubscriptionSchedules (phases)', 'api', 'common', () =>
833
+ withRoot(async (h) => {
834
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=sched@twin.test' });
835
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Plan' });
836
+ const price = await h({ m: 'POST', p: `/v1/prices`, b: `unit_amount=1000&currency=usd&recurring[interval]=month&product=${id(prod)}` });
837
+ const sched = await h({ m: 'POST', p: '/v1/subscription_schedules', b: `customer=${id(cust)}&phases[0][items][0][price]=${id(price)}&phases[0][iterations]=3&phases[1][items][0][price]=${id(price)}&phases[1][iterations]=1` });
838
+ if (!ok(sched) || field(sched, 'object') !== 'subscription_schedule' || field(sched, 'status') !== 'active') return false;
839
+ const phases = field(sched, 'phases') as Body[];
840
+ if (!Array.isArray(phases) || phases.length !== 2 || (phases[0]!.items as Body[]).length !== 1) return false;
841
+ // current_phase must be coupled to the real first phase (not a fabricated value).
842
+ const cp = field(sched, 'current_phase') as Body | null;
843
+ if (!cp || cp.start_date !== (phases[0] as Body).start_date || cp.end_date !== (phases[0] as Body).end_date) return false;
844
+ const g = await h({ m: 'GET', p: `/v1/subscription_schedules/${id(sched)}` });
845
+ const l = await h({ m: 'GET', p: `/v1/subscription_schedules?customer=${id(cust)}` });
846
+ const rel = await h({ m: 'POST', p: `/v1/subscription_schedules/${id(sched)}/release` });
847
+ const relAgain = await h({ m: 'POST', p: `/v1/subscription_schedules/${id(sched)}/cancel` });
848
+ const noTarget = await h({ m: 'POST', p: '/v1/subscription_schedules', b: 'end_behavior=release' });
849
+ return ok(g) && id(g) === id(sched) && ((l.body as Body).data as Body[]).length === 1 &&
850
+ ok(rel) && field(rel, 'status') === 'released' && relAgain.status === 400 && noTarget.status === 400;
851
+ }),
852
+ ),
853
+ // Metered/usage billing: a Billing Meter defines event_name + aggregation; usage is reported
854
+ // via meter_events (and the legacy subscription_items usage_records). The twin requires
855
+ // display_name/event_name/default_aggregation[formula], lists/retrieves meters, deactivate
856
+ // transitions active→inactive, and usage record summaries total the reported quantity.
857
+ done('stripe.billing.metered_usage', 'subscriptions', 'Usage / metered billing (meters + usage records)', 'api', 'common', () =>
858
+ withRoot(async (h) => {
859
+ const meter = await h({ m: 'POST', p: '/v1/billing/meters', b: 'display_name=API calls&event_name=api_request&default_aggregation[formula]=sum&value_settings[event_payload_key]=value' });
860
+ if (!ok(meter) || field(meter, 'object') !== 'billing.meter' || field(meter, 'status') !== 'active' || field(meter, 'event_name') !== 'api_request') return false;
861
+ const noAgg = await h({ m: 'POST', p: '/v1/billing/meters', b: 'display_name=X&event_name=y' });
862
+ if (noAgg.status !== 400) return false;
863
+ const evt = await h({ m: 'POST', p: '/v1/billing/meter_events', b: 'event_name=api_request&payload[value]=10&payload[stripe_customer_id]=cus_x' });
864
+ if (!ok(evt) || field(evt, 'object') !== 'billing.meter_event' || field(evt, 'event_name') !== 'api_request') return false;
865
+ const g = await h({ m: 'GET', p: `/v1/billing/meters/${id(meter)}` });
866
+ const deact = await h({ m: 'POST', p: `/v1/billing/meters/${id(meter)}/deactivate` });
867
+ // legacy per-item usage records SUM in the summary — post two (7 + 3) so the assertion
868
+ // proves real aggregation (a single-value / canned impl would not total to 10).
869
+ const ur = await h({ m: 'POST', p: '/v1/subscription_items/si_twin/usage_records', b: 'quantity=7' });
870
+ const ur2 = await h({ m: 'POST', p: '/v1/subscription_items/si_twin/usage_records', b: 'quantity=3' });
871
+ const noQty = await h({ m: 'POST', p: '/v1/subscription_items/si_twin/usage_records' });
872
+ const summ = await h({ m: 'GET', p: '/v1/subscription_items/si_twin/usage_record_summaries' });
873
+ const sd = (summ.body as Body).data as Body[];
874
+ return ok(g) && ok(deact) && field(deact, 'status') === 'inactive' && ok(ur) && ok(ur2) && noQty.status === 400 &&
875
+ sd.length === 1 && sd[0]!.total_usage === 10;
876
+ }),
877
+ ),
878
+ // Subscription discounts: a `coupon` on create attaches a discount (validated to exist),
879
+ // materialized as the canonical discount object on the sub; update can attach/replace, and
880
+ // DELETE /v1/subscriptions/:id/discount removes it. An unknown coupon 400s. (discount object
881
+ // produced ONLY by this feature.)
882
+ done('stripe.subscriptions.discounts', 'subscriptions', 'Subscription discounts / add coupon', 'api', 'common', () =>
883
+ withRoot(async (h) => {
884
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=disc@twin.test' });
885
+ const coupon = await h({ m: 'POST', p: '/v1/coupons', b: 'percent_off=25&duration=forever' });
886
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}&coupon=${id(coupon)}` });
887
+ if (!ok(sub)) return false;
888
+ const discs = (field(sub, 'discounts') as Body[]) ?? [];
889
+ if (discs.length !== 1 || discs[0]!.object !== 'discount' || (discs[0]!.coupon as Body)?.id !== id(coupon)) return false;
890
+ // unknown coupon on a fresh sub 400s.
891
+ const bad = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}&coupon=coupon_nope` });
892
+ if (bad.status !== 400) return false;
893
+ // remove the discount.
894
+ const del = await h({ m: 'DELETE', p: `/v1/subscriptions/${id(sub)}/discount` });
895
+ const g = await h({ m: 'GET', p: `/v1/subscriptions/${id(sub)}` });
896
+ return ok(del) && field(del, 'deleted') === true && ((field(g, 'discounts') as Body[]) ?? []).length === 0;
897
+ }),
898
+ ),
899
+
900
+ // ── Invoices / InvoiceItems / CreditNotes ─────────────────────────────────────────
901
+ done('stripe.invoices.lifecycle', 'invoices', 'Invoices: create + finalize + pay (+ void) + list', 'api', 'core', () =>
902
+ withRoot(async (h) => {
903
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=inv@twin.test' });
904
+ await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=2500&currency=usd` });
905
+ const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
906
+ if (!ok(inv) || field(inv, 'object') !== 'invoice') return false;
907
+ const fin = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
908
+ const pay = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay` });
909
+ const l = await h({ m: 'GET', p: '/v1/invoices' });
910
+ return ok(fin) && field(fin, 'status') === 'open' && ok(pay) && field(pay, 'status') === 'paid' && field(l, 'object') === 'list';
911
+ }),
912
+ ),
913
+ done('stripe.invoiceitems.crud', 'invoices', 'InvoiceItems: create + list', 'api', 'core', () =>
914
+ withRoot(async (h) => {
915
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=ii@twin.test' });
916
+ const ii = await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=400&currency=usd` });
917
+ const l = await h({ m: 'GET', p: '/v1/invoiceitems' });
918
+ return ok(ii) && field(ii, 'object') === 'invoiceitem' && field(l, 'object') === 'list';
919
+ }),
920
+ ),
921
+ // Invoice send / mark_uncollectible / pay-out-of-band: send auto-finalizes a draft (→ open);
922
+ // pay with paid_out_of_band=true marks it paid out of band; a fresh open invoice can be
923
+ // marked uncollectible (write-off). Unknown invoice 404. (status transitions + paid_out_of_band
924
+ // produced ONLY by this feature.)
925
+ done('stripe.invoices.send', 'invoices', 'Invoice send / mark_uncollectible / pay out-of-band', 'api', 'common', () =>
926
+ withRoot(async (h) => {
927
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=send@twin.test' });
928
+ await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=4000&currency=usd` });
929
+ const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
930
+ const sent = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/send` });
931
+ if (!ok(sent) || field(sent, 'status') !== 'open') return false;
932
+ const oob = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay`, b: 'paid_out_of_band=true' });
933
+ if (!ok(oob) || field(oob, 'status') !== 'paid' || field(oob, 'paid_out_of_band') !== true) return false;
934
+ // a second invoice can be marked uncollectible
935
+ const inv2 = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
936
+ await h({ m: 'POST', p: `/v1/invoices/${id(inv2)}/finalize` });
937
+ const unc = await h({ m: 'POST', p: `/v1/invoices/${id(inv2)}/mark_uncollectible` });
938
+ const missing = await h({ m: 'POST', p: '/v1/invoices/in_nope/send' });
939
+ return ok(unc) && field(unc, 'status') === 'uncollectible' && missing.status === 404;
940
+ }),
941
+ ),
942
+ // Upcoming invoice preview: GET /v1/invoices/upcoming computes the next invoice for a
943
+ // subscription (items × price → lines + totals) WITHOUT persisting it (id null), applying
944
+ // the sub's coupon discount; pending invoice items fold in. Missing customer/sub 404.
945
+ // (the non-persisted upcoming invoice is produced ONLY by this feature.)
946
+ done('stripe.invoices.upcoming', 'invoices', 'Upcoming invoice preview', 'api', 'common', () =>
947
+ withRoot(async (h) => {
948
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=up@twin.test' });
949
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Plan' });
950
+ const price = await h({ m: 'POST', p: `/v1/prices`, b: `unit_amount=3000&currency=usd&recurring[interval]=month&product=${id(prod)}` });
951
+ const coupon = await h({ m: 'POST', p: '/v1/coupons', b: 'percent_off=10&duration=forever' });
952
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}&items[0][price]=${id(price)}&coupon=${id(coupon)}` });
953
+ const up = await h({ m: 'GET', p: `/v1/invoices/upcoming?subscription=${id(sub)}` });
954
+ if (!ok(up) || field(up, 'object') !== 'invoice' || field(up, 'id') !== null) return false;
955
+ // subtotal 3000, 10% off → total 2700.
956
+ if (field(up, 'subtotal') !== 3000 || field(up, 'total') !== 2700) return false;
957
+ const lines = (field(up, 'lines') as Body).data as Body[];
958
+ if (lines.length !== 1 || lines[0]!.amount !== 3000) return false;
959
+ const noCust = await h({ m: 'GET', p: '/v1/invoices/upcoming?customer=cus_nope' });
960
+ return noCust.status === 404;
961
+ }),
962
+ ),
963
+ // CreditNotes: preview (non-persisted) then create against a finalized invoice → status
964
+ // issued, type post_payment (the invoice is paid), a nested credit_note_line_item, and
965
+ // the credited amount; retrieve + /lines round-trip; void is terminal (status void) and a
966
+ // second void 400s. Missing invoice 404; bad amount 400. (status/type/lines produced ONLY
967
+ // by this feature.)
968
+ done('stripe.credit_notes', 'invoices', 'CreditNotes (create/preview/void/list)', 'api', 'common', () =>
969
+ withRoot(async (h) => {
970
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=cn@twin.test' });
971
+ await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=5000&currency=usd` });
972
+ const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
973
+ await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
974
+ await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay` });
975
+ // preview computes the object WITHOUT persisting it
976
+ const prev = await h({ m: 'GET', p: `/v1/credit_notes/preview?invoice=${id(inv)}&amount=2000` });
977
+ if (!ok(prev) || field(prev, 'object') !== 'credit_note' || field(prev, 'amount') !== 2000) return false;
978
+ const cn = await h({ m: 'POST', p: '/v1/credit_notes', b: `invoice=${id(inv)}&amount=2000&reason=order_change` });
979
+ if (!ok(cn) || field(cn, 'object') !== 'credit_note' || field(cn, 'status') !== 'issued' || field(cn, 'type') !== 'post_payment' || field(cn, 'amount') !== 2000) return false;
980
+ const g = await h({ m: 'GET', p: `/v1/credit_notes/${id(cn)}` });
981
+ const lines = await h({ m: 'GET', p: `/v1/credit_notes/${id(cn)}/lines` });
982
+ const lineData = (lines.body as Body).data as Body[];
983
+ if (!ok(g) || id(g) !== id(cn) || !ok(lines) || lineData.length !== 1 || lineData[0]!.amount !== 2000) return false;
984
+ const l = await h({ m: 'GET', p: `/v1/credit_notes?invoice=${id(inv)}` });
985
+ const voided = await h({ m: 'POST', p: `/v1/credit_notes/${id(cn)}/void` });
986
+ const voidAgain = await h({ m: 'POST', p: `/v1/credit_notes/${id(cn)}/void` });
987
+ const noInv = await h({ m: 'POST', p: '/v1/credit_notes', b: 'invoice=in_nope&amount=100' });
988
+ const badAmt = await h({ m: 'POST', p: '/v1/credit_notes', b: `invoice=${id(inv)}&amount=0` });
989
+ return field(l, 'object') === 'list' && ((l.body as Body).data as Body[]).length === 1 &&
990
+ ok(voided) && field(voided, 'status') === 'void' && voidAgain.status === 400 &&
991
+ noInv.status === 404 && badAmt.status === 400;
992
+ }),
993
+ ),
994
+
995
+ // ── Checkout Sessions / Billing Portal ────────────────────────────────────────────
996
+ done('stripe.checkout.sessions', 'checkout', 'Checkout Sessions: create (referential to price) + retrieve + line_items + expire', 'api', 'core', () =>
997
+ withRoot(async (h) => {
998
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Plan' });
999
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=1500&currency=usd&product=${id(prod)}` });
1000
+ const cs = await h({ m: 'POST', p: '/v1/checkout/sessions', b: `mode=payment&line_items[0][price]=${id(price)}&line_items[0][quantity]=1&success_url=https://x.test` });
1001
+ if (!ok(cs) || field(cs, 'object') !== 'checkout.session' || !field(cs, 'url')) return false;
1002
+ const g = await h({ m: 'GET', p: `/v1/checkout/sessions/${id(cs)}` });
1003
+ const li = await h({ m: 'GET', p: `/v1/checkout/sessions/${id(cs)}/line_items` });
1004
+ const exp = await h({ m: 'POST', p: `/v1/checkout/sessions/${id(cs)}/expire` });
1005
+ return ok(g) && ok(li) && field(li, 'object') === 'list' && ok(exp);
1006
+ }),
1007
+ ),
1008
+ done('stripe.billing_portal.session', 'checkout', 'Billing Portal Session create (referential to customer)', 'api', 'common', () =>
1009
+ withRoot(async (h) => {
1010
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=portal@twin.test' });
1011
+ const bps = await h({ m: 'POST', p: '/v1/billing_portal/sessions', b: `customer=${id(cust)}` });
1012
+ return ok(bps) && field(bps, 'object') === 'billing_portal.session' && !!field(bps, 'url');
1013
+ }),
1014
+ ),
1015
+ done('stripe.billing_portal.configuration', 'checkout', 'Billing Portal Configurations: create + retrieve + update + list', 'api', 'common', () =>
1016
+ withRoot(async (h) => {
1017
+ const c = await h({ m: 'POST', p: '/v1/billing_portal/configurations', b: 'business_profile[headline]=Hi&features[customer_update][enabled]=true' });
1018
+ if (!ok(c)) return false;
1019
+ const g = await h({ m: 'GET', p: `/v1/billing_portal/configurations/${id(c)}` });
1020
+ const l = await h({ m: 'GET', p: '/v1/billing_portal/configurations' });
1021
+ return ok(g) && id(g) === id(c) && field(l, 'object') === 'list';
1022
+ }),
1023
+ ),
1024
+ outOfScope('stripe.checkout.hosted_page', 'checkout', 'Hosted Checkout PAGE pixel rendering', 'ui', 'niche', 'Out of scope: the Session/API is modeled; the hosted PAGE pixels are out of scope.'),
1025
+ outOfScope('stripe.billing_portal.hosted_page', 'checkout', 'Hosted Customer Portal PAGE pixel rendering', 'ui', 'niche', 'Out of scope: the Session/API is modeled; the hosted PAGE pixels are out of scope.'),
1026
+
1027
+ // ── Payment Links / Quotes ────────────────────────────────────────────────────────
1028
+ // Payment Links: create (referential to a Price) → active link with a share url + line_items
1029
+ // sub-list; retrieve, list, /line_items round-trip; deactivate via update. Vendor errors:
1030
+ // no line_items 400; an unknown price 400 resource_missing.
1031
+ done('stripe.payment_links', 'checkout', 'Payment Links (create/retrieve/list/line_items)', 'api', 'common', () =>
1032
+ withRoot(async (h) => {
1033
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Linkable' });
1034
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=2500&currency=usd&product=${id(prod)}` });
1035
+ const pl = await h({ m: 'POST', p: '/v1/payment_links', b: `line_items[0][price]=${id(price)}&line_items[0][quantity]=2` });
1036
+ if (!ok(pl) || field(pl, 'object') !== 'payment_link' || field(pl, 'active') !== true || !field(pl, 'url')) return false;
1037
+ const g = await h({ m: 'GET', p: `/v1/payment_links/${id(pl)}` });
1038
+ const li = await h({ m: 'GET', p: `/v1/payment_links/${id(pl)}/line_items` });
1039
+ const liData = (li.body as Body).data as Body[];
1040
+ const l = await h({ m: 'GET', p: '/v1/payment_links' });
1041
+ const off = await h({ m: 'POST', p: `/v1/payment_links/${id(pl)}`, b: 'active=false' });
1042
+ if (!ok(g) || id(g) !== id(pl) || !ok(li) || liData.length !== 1 || liData[0]!.quantity !== 2 || field(l, 'object') !== 'list' || field(off, 'active') !== false) return false;
1043
+ const noItems = await h({ m: 'POST', p: '/v1/payment_links', b: 'metadata[x]=1' });
1044
+ const badPrice = await h({ m: 'POST', p: '/v1/payment_links', b: 'line_items[0][price]=price_nope&line_items[0][quantity]=1' });
1045
+ return noItems.status === 400 && badPrice.status === 400 && ((badPrice.body as Body).error as Body)?.code === 'resource_missing';
1046
+ }),
1047
+ ),
1048
+ // Quotes: create (referential to customer + price) → draft with amount_total + line_items;
1049
+ // finalize (→ open), accept (→ accepted), PDF + line_items sub-list. Status gates: accepting a
1050
+ // draft 400s (quote_invalid_status). Vendor errors: missing customer 400; unknown customer 400.
1051
+ done('stripe.quotes', 'checkout', 'Quotes (create/finalize/accept/PDF/line_items)', 'api', 'common', () =>
1052
+ withRoot(async (h) => {
1053
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=quote@twin.test' });
1054
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Quotable' });
1055
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=4000&currency=usd&product=${id(prod)}` });
1056
+ const q = await h({ m: 'POST', p: '/v1/quotes', b: `customer=${id(cust)}&line_items[0][price]=${id(price)}&line_items[0][quantity]=3` });
1057
+ if (!ok(q) || field(q, 'object') !== 'quote' || field(q, 'status') !== 'draft' || field(q, 'amount_total') !== 12000) return false;
1058
+ // can't accept a draft
1059
+ const earlyAccept = await h({ m: 'POST', p: `/v1/quotes/${id(q)}/accept` });
1060
+ if (earlyAccept.status !== 400) return false;
1061
+ const fin = await h({ m: 'POST', p: `/v1/quotes/${id(q)}/finalize` });
1062
+ if (!ok(fin) || field(fin, 'status') !== 'open') return false;
1063
+ const acc = await h({ m: 'POST', p: `/v1/quotes/${id(q)}/accept` });
1064
+ if (!ok(acc) || field(acc, 'status') !== 'accepted') return false;
1065
+ const li = await h({ m: 'GET', p: `/v1/quotes/${id(q)}/line_items` });
1066
+ const pdf = await h({ m: 'GET', p: `/v1/quotes/${id(q)}/pdf` });
1067
+ const l = await h({ m: 'GET', p: '/v1/quotes' });
1068
+ if (!ok(li) || ((li.body as Body).data as Body[]).length !== 1 || !ok(pdf) || !field(pdf, 'url') || field(l, 'object') !== 'list') return false;
1069
+ const noCust = await h({ m: 'POST', p: '/v1/quotes', b: 'metadata[x]=1' });
1070
+ const badCust = await h({ m: 'POST', p: '/v1/quotes', b: 'customer=cus_nope' });
1071
+ return noCust.status === 400 && badCust.status === 400;
1072
+ }),
1073
+ ),
1074
+
1075
+ // ── Connect ────────────────────────────────────────────────────────────────────
1076
+ done('stripe.connect.accounts', 'connect', 'Connect Accounts: create + retrieve + update + delete + login_links', 'api', 'niche', () =>
1077
+ withRoot(async (h) => {
1078
+ const a = await h({ m: 'POST', p: '/v1/accounts', b: 'type=express' });
1079
+ if (!ok(a) || field(a, 'object') !== 'account') return false;
1080
+ const g = await h({ m: 'GET', p: `/v1/accounts/${id(a)}` });
1081
+ const u = await h({ m: 'POST', p: `/v1/accounts/${id(a)}`, b: 'business_profile[url]=https://x.test' });
1082
+ const ll = await h({ m: 'POST', p: `/v1/accounts/${id(a)}/login_links` });
1083
+ const del = await h({ m: 'DELETE', p: `/v1/accounts/${id(a)}` });
1084
+ return ok(g) && ok(u) && ok(ll) && field(ll, 'object') === 'login_link' && ok(del) && field(del, 'deleted') === true;
1085
+ }),
1086
+ ),
1087
+ done('stripe.connect.transfers', 'connect', 'Connect Transfers: create (referential to dest account) + retrieve + list', 'api', 'niche', () =>
1088
+ withRoot(async (h) => {
1089
+ const a = await h({ m: 'POST', p: '/v1/accounts', b: 'type=express' });
1090
+ const t = await h({ m: 'POST', p: '/v1/transfers', b: `amount=500&currency=usd&destination=${id(a)}` });
1091
+ if (!ok(t) || field(t, 'object') !== 'transfer') return false;
1092
+ const g = await h({ m: 'GET', p: `/v1/transfers/${id(t)}` });
1093
+ const l = await h({ m: 'GET', p: '/v1/transfers' });
1094
+ // referential integrity: unknown destination is rejected
1095
+ const bad = await h({ m: 'POST', p: '/v1/transfers', b: 'amount=500&currency=usd&destination=acct_nope' });
1096
+ return ok(g) && field(l, 'object') === 'list' && bad.status >= 400;
1097
+ }),
1098
+ ),
1099
+ // (account_links, persons, external_accounts, transfer_reversals, application_fees,
1100
+ // capabilities upgraded to done() in the AUDIT GROWTH block below.)
1101
+ // Connect payouts (Stripe-Account): a payout created WITH a Stripe-Account header is attributed
1102
+ // to that connected account; GET /v1/payouts with the same header returns only that account's
1103
+ // payouts, and the platform's own payouts (no header) are scoped out. An unknown account 400s.
1104
+ done('stripe.connect.connect_payouts', 'connect', 'Payouts on connected accounts (Stripe-Account)', 'api', 'niche', async () => {
1105
+ const root = mkdtempSync(join(tmpdir(), 'stp-cpo-'));
1106
+ try {
1107
+ const h = (s: Step & { acct?: string }) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root, ...(s.acct ? { stripeAccount: s.acct } : {}) });
1108
+ const acct = await h({ m: 'POST', p: '/v1/accounts', b: 'type=express&country=US' });
1109
+ // a platform payout (no header) and a connected-account payout (with header).
1110
+ const plat = await h({ m: 'POST', p: '/v1/payouts', b: 'amount=1000&currency=usd' });
1111
+ const conn = await h({ m: 'POST', p: '/v1/payouts', b: 'amount=2000&currency=usd', acct: id(acct) });
1112
+ if (!ok(plat) || !ok(conn)) return false;
1113
+ // scoped list: with the header → only the connected account's payout.
1114
+ const connList = await h({ m: 'GET', p: '/v1/payouts', acct: id(acct) });
1115
+ const connData = (connList.body as Body).data as Body[];
1116
+ if (connData.length !== 1 || connData[0]!.id !== id(conn)) return false;
1117
+ // without the header → only the platform's payout.
1118
+ const platList = await h({ m: 'GET', p: '/v1/payouts' });
1119
+ const platData = (platList.body as Body).data as Body[];
1120
+ if (platData.length !== 1 || platData[0]!.id !== id(plat)) return false;
1121
+ const badAcct = await h({ m: 'POST', p: '/v1/payouts', b: 'amount=500&currency=usd', acct: 'acct_nope' });
1122
+ return badAcct.status === 400;
1123
+ } catch {
1124
+ return false;
1125
+ } finally {
1126
+ rmSync(root, { recursive: true, force: true });
1127
+ }
1128
+ }),
1129
+ // Top-ups: POST /v1/topups funds the platform balance (amount + currency required); the twin
1130
+ // succeeds a test top-up immediately. retrieve/list/update round-trip; a succeeded top-up
1131
+ // cannot be canceled (400); missing money 400; unknown id 404.
1132
+ done('stripe.connect.top_ups', 'connect', 'Top-ups (fund platform balance)', 'api', 'niche', () =>
1133
+ withRoot(async (h) => {
1134
+ const tu = await h({ m: 'POST', p: '/v1/topups', b: 'amount=50000&currency=usd&statement_descriptor=Top up' });
1135
+ if (!ok(tu) || field(tu, 'object') !== 'topup' || field(tu, 'status') !== 'succeeded') return false;
1136
+ const g = await h({ m: 'GET', p: `/v1/topups/${id(tu)}` });
1137
+ const u = await h({ m: 'POST', p: `/v1/topups/${id(tu)}`, b: 'metadata[ref]=q3' });
1138
+ const l = await h({ m: 'GET', p: '/v1/topups?status=succeeded' });
1139
+ // a succeeded top-up cannot be canceled.
1140
+ const cancel = await h({ m: 'POST', p: `/v1/topups/${id(tu)}/cancel` });
1141
+ const noMoney = await h({ m: 'POST', p: '/v1/topups', b: 'currency=usd' });
1142
+ const nope = await h({ m: 'GET', p: '/v1/topups/tu_nope' });
1143
+ return ok(g) && id(g) === id(tu) && ok(u) && ((l.body as Body).data as Body[]).length === 1 &&
1144
+ cancel.status === 400 && noMoney.status === 400 && nope.status === 404;
1145
+ }),
1146
+ ),
1147
+
1148
+ // ── Identity / Files ──────────────────────────────────────────────────────────────
1149
+ done('stripe.identity.verification_sessions', 'identity', 'Identity VerificationSessions: create + retrieve', 'api', 'niche', () =>
1150
+ withRoot(async (h) => {
1151
+ const v = await h({ m: 'POST', p: '/v1/identity/verification_sessions', b: 'type=document' });
1152
+ if (!ok(v) || field(v, 'object') !== 'verification_session') return false;
1153
+ const g = await h({ m: 'GET', p: `/v1/identity/verification_sessions/${id(v)}` });
1154
+ return ok(g) && id(g) === id(v);
1155
+ }),
1156
+ ),
1157
+ // create requires `file` (the link target) — with it, a file_link is returned; WITHOUT it
1158
+ // the twin 400s parameter_missing exactly like Stripe (no fabricated success). Failable:
1159
+ // the old fabricating path returned 200 for a missing file and would fail this assertion.
1160
+ done('stripe.file_links.create', 'files', 'FileLinks: create (requires file)', 'api', 'niche', () =>
1161
+ withRoot(async (h) => {
1162
+ const f = await h({ m: 'POST', p: '/v1/file_links', b: 'file=file_twin' });
1163
+ if (!ok(f) || field(f, 'object') !== 'file_link' || field(f, 'file') !== 'file_twin') return false;
1164
+ const missing = await h({ m: 'POST', p: '/v1/file_links', b: 'expires_at=0' });
1165
+ return missing.status === 400 && ((missing.body as Body).error as Body)?.code === 'parameter_missing';
1166
+ }),
1167
+ ),
1168
+ // (files.upload upgraded to done() in the AUDIT GROWTH block below.)
1169
+ // Identity VerificationReports: materialize from a verified session (test helper
1170
+ // POST .../verify), then list (filterable by verification_session) + retrieve. Unknown id 404.
1171
+ done('stripe.identity.verification_reports', 'identity', 'Identity VerificationReports', 'api', 'niche', () =>
1172
+ withRoot(async (h) => {
1173
+ const vs = await h({ m: 'POST', p: '/v1/identity/verification_sessions', b: 'type=document' });
1174
+ const ver = await h({ m: 'POST', p: `/v1/identity/verification_sessions/${id(vs)}/verify` });
1175
+ if (!ok(ver) || field(ver, 'status') !== 'verified') return false;
1176
+ const reportId = field(ver, 'last_verification_report') as string;
1177
+ if (!reportId?.startsWith('vr_')) return false;
1178
+ const list = await h({ m: 'GET', p: `/v1/identity/verification_reports?verification_session=${id(vs)}` });
1179
+ const data = (list.body as Body).data as Body[];
1180
+ if (data.length !== 1 || data[0]!.object !== 'identity.verification_report' || data[0]!.type !== 'document') return false;
1181
+ if (data[0]!.verification_session !== id(vs) || (data[0]!.document as Body)?.status !== 'verified') return false;
1182
+ const get = await h({ m: 'GET', p: `/v1/identity/verification_reports/${reportId}` });
1183
+ const nope = await h({ m: 'GET', p: '/v1/identity/verification_reports/vr_nope' });
1184
+ return ok(get) && field(get, 'id') === reportId && nope.status === 404;
1185
+ }),
1186
+ ),
1187
+
1188
+ // ── Events / Webhooks / Idempotency ───────────────────────────────────────────────
1189
+ done('stripe.events.list', 'events', 'Events: create + retrieve + list', 'api', 'core', () =>
1190
+ withRoot(async (h) => {
1191
+ const e = await h({ m: 'POST', p: '/v1/events', b: 'type=twin.synthetic' });
1192
+ if (!ok(e) || field(e, 'object') !== 'event') return false;
1193
+ const g = await h({ m: 'GET', p: `/v1/events/${id(e)}` });
1194
+ const l = await h({ m: 'GET', p: '/v1/events' });
1195
+ return ok(g) && id(g) === id(e) && field(l, 'object') === 'list';
1196
+ }),
1197
+ ),
1198
+ done('stripe.idempotency', 'core', 'Idempotency-Key: POST replay returns the same resource', 'api', 'core', async () => {
1199
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1200
+ const H = (idk: string) => handleStripeTwinRequest({ method: 'POST', path: '/v1/customers', body: 'email=idem@twin.test', root, idempotencyKey: idk });
1201
+ try {
1202
+ // sequential: the second call must replay the first (concurrent calls would race the store)
1203
+ const a = await H('k-1');
1204
+ const b = await H('k-1');
1205
+ // a THIRD call under a DIFFERENT key must mint its OWN distinct customer — proves the
1206
+ // replay above is genuine dedup on a real resource, not two empty bodies matching by luck.
1207
+ const c = await H('k-2');
1208
+ const aId = (a.body as Body).id;
1209
+ return ok(a) && ok(b) && ok(c)
1210
+ && typeof aId === 'string' && aId.startsWith('cus_')
1211
+ && aId === (b.body as Body).id
1212
+ && aId !== (c.body as Body).id;
1213
+ } finally {
1214
+ rmSync(root, { recursive: true, force: true });
1215
+ }
1216
+ }),
1217
+ done('stripe.read_only_guard', 'core', 'Read-only mode rejects mutations (405)', 'api', 'common', () => {
1218
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1219
+ return handleStripeTwinRequest({ method: 'POST', path: '/v1/customers', body: 'email=z', root, readOnly: true })
1220
+ .then((r) => r.status === 405)
1221
+ .finally(() => rmSync(root, { recursive: true, force: true }));
1222
+ }),
1223
+ done('stripe.errors.resource_missing', 'core', 'Stripe-faithful 404 resource_missing on unknown id', 'api', 'core', () =>
1224
+ withRoot(async (h) => {
1225
+ const r = await h({ m: 'GET', p: '/v1/customers/cus_does_not_exist' });
1226
+ return r.status === 404 && ((r.body as Body).error as Body)?.code === 'resource_missing';
1227
+ }),
1228
+ ),
1229
+ done('stripe.webhooks.emit', 'webhooks', 'Webhook event emission on state changes (R17)', 'connector', 'core', async () => {
1230
+ // Round-trip, fully OFFLINE/deterministic: install an INJECTED fake deliverer (no real
1231
+ // sockets, no network), register an endpoint URL, drive a state change
1232
+ // (payment_intent.confirm → succeeded), and assert the payment_intent.succeeded event was
1233
+ // actually emitted to the deliverer carrying the succeeded resource — not just that a write
1234
+ // returned 2xx. The write path threads its emission through setStripeEventDelivery, so this
1235
+ // exercises the twin's real emission in-process (D5: failable, no real vendor/network).
1236
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1237
+ const hits: StripeEvent[] = [];
1238
+ try {
1239
+ setStripeEventDelivery((_url, event) => { hits.push(event); });
1240
+ registerStripeWebhook('https://hooks.twin.local/webhook');
1241
+ const H = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root, occurredAt: '2026-06-14T00:00:00Z' });
1242
+ const pi = await H({ m: 'POST', p: '/v1/payment_intents', b: 'amount=1000&currency=usd' });
1243
+ if (!ok(pi)) return false;
1244
+ const c = await H({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm`, b: 'payment_method=pm_card_visa' });
1245
+ if (!ok(c) || field(c, 'status') !== 'succeeded') return false;
1246
+ return hits.some((e) => e.type === 'payment_intent.succeeded' && (e.data.object as Body)?.status === 'succeeded');
1247
+ } catch {
1248
+ return false;
1249
+ } finally {
1250
+ setStripeEventDelivery(null);
1251
+ clearStripeWebhooks();
1252
+ rmSync(root, { recursive: true, force: true });
1253
+ }
1254
+ }),
1255
+ // Webhook signature scheme (offline, deterministic, no sockets). generateTestHeaderString
1256
+ // builds Stripe's `t=<ts>,v1=<hmac>` header; constructEvent recomputes the HMAC and returns
1257
+ // the parsed event on a match. Failable: a wrong secret, a tampered payload, a malformed
1258
+ // header, and a stale timestamp (with tolerance) must ALL throw.
1259
+ done('stripe.webhooks.signature', 'webhooks', 'Webhook signature verification (constructEvent / generateTestHeaderString)', 'connector', 'core', () => {
1260
+ try {
1261
+ const secret = 'whsec_test_secret';
1262
+ const ts = 1_700_000_000;
1263
+ const event: StripeEvent = { id: 'evt_sig_1', object: 'event', type: 'payment_intent.succeeded', created: ts, livemode: false, data: { object: { id: 'pi_1', status: 'succeeded' } } };
1264
+ const payload = JSON.stringify(event);
1265
+ const header = generateTestHeaderString({ payload, secret, timestamp: ts });
1266
+ // header is the documented shape and embeds the recomputed signature
1267
+ const expectedSig = computeStripeSignature(payload, secret, ts);
1268
+ if (header !== `t=${ts},v1=${expectedSig}`) return false;
1269
+ // valid → returns the parsed event unchanged
1270
+ const parsed = constructEvent(payload, header, secret);
1271
+ if (parsed.id !== 'evt_sig_1' || parsed.type !== 'payment_intent.succeeded') return false;
1272
+ // within tolerance using an explicit `now` (deterministic) → OK
1273
+ constructEvent(payload, header, secret, { tolerance: 300, now: ts + 100 });
1274
+ // wrong secret → throws
1275
+ let rejWrongSecret = false; try { constructEvent(payload, header, 'whsec_wrong'); } catch { rejWrongSecret = true; }
1276
+ // tampered payload (same header) → throws
1277
+ let rejTampered = false; try { constructEvent(payload + ' ', header, secret); } catch { rejTampered = true; }
1278
+ // malformed header → throws
1279
+ let rejMalformed = false; try { constructEvent(payload, 'garbage', secret); } catch { rejMalformed = true; }
1280
+ // stale timestamp beyond tolerance → throws
1281
+ let rejStale = false; try { constructEvent(payload, header, secret, { tolerance: 300, now: ts + 1000 }); } catch { rejStale = true; }
1282
+ return rejWrongSecret && rejTampered && rejMalformed && rejStale;
1283
+ } catch {
1284
+ return false;
1285
+ }
1286
+ }),
1287
+ // WebhookEndpoint CRUD: register a URL + enabled_events → an enabled endpoint with a
1288
+ // synthesized signing secret; retrieve, list, update enabled_events, delete (→ deleted stub,
1289
+ // 404 + dropped from list). Vendor errors: missing url 400; missing enabled_events 400.
1290
+ done('stripe.webhook_endpoints', 'webhooks', 'WebhookEndpoint CRUD (register/list/delete)', 'api', 'common', () =>
1291
+ withRoot(async (h) => {
1292
+ const we = await h({ m: 'POST', p: '/v1/webhook_endpoints', b: 'url=https://app.twin.test/hook&enabled_events[]=payment_intent.succeeded&enabled_events[]=invoice.paid' });
1293
+ if (!ok(we) || field(we, 'object') !== 'webhook_endpoint' || field(we, 'status') !== 'enabled' || !field(we, 'secret')) return false;
1294
+ const evs = field(we, 'enabled_events') as string[];
1295
+ if (!Array.isArray(evs) || evs.length !== 2) return false;
1296
+ const g = await h({ m: 'GET', p: `/v1/webhook_endpoints/${id(we)}` });
1297
+ const u = await h({ m: 'POST', p: `/v1/webhook_endpoints/${id(we)}`, b: 'enabled_events[]=charge.refunded' });
1298
+ const l = await h({ m: 'GET', p: '/v1/webhook_endpoints' });
1299
+ if (!ok(g) || id(g) !== id(we) || !ok(u) || (field(u, 'enabled_events') as string[]).length !== 1 || field(l, 'object') !== 'list') return false;
1300
+ const del = await h({ m: 'DELETE', p: `/v1/webhook_endpoints/${id(we)}` });
1301
+ const gone = await h({ m: 'GET', p: `/v1/webhook_endpoints/${id(we)}` });
1302
+ const noUrl = await h({ m: 'POST', p: '/v1/webhook_endpoints', b: 'enabled_events[]=invoice.paid' });
1303
+ const noEvents = await h({ m: 'POST', p: '/v1/webhook_endpoints', b: 'url=https://x.test/h' });
1304
+ return ok(del) && field(del, 'deleted') === true && gone.status === 404 && noUrl.status === 400 && noEvents.status === 400;
1305
+ }),
1306
+ ),
1307
+ // Events store (offline): write operations persist the Stripe `event` envelope into
1308
+ // GET /v1/events independent of any webhook registration — exactly like Stripe's stored
1309
+ // Events API. Drive several DISTINCT state changes, then assert each mapped event type is
1310
+ // present, the envelope is well-shaped (object 'event', data.object snapshot), `type=`
1311
+ // filtering narrows the list, and a single event retrieves by id (unknown id → 404).
1312
+ done('stripe.events.full_types', 'webhooks', 'Full event-type coverage + events stored in /v1/events list', 'api', 'core', () =>
1313
+ withRoot(async (h) => {
1314
+ const cus = await h({ m: 'POST', p: '/v1/customers', b: 'email=ev@example.com' });
1315
+ if (!ok(cus)) return false;
1316
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=2000&currency=usd' });
1317
+ const conf = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm`, b: 'payment_method=pm_card_visa' });
1318
+ const pm = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2030' });
1319
+ const att = await h({ m: 'POST', p: `/v1/payment_methods/${id(pm)}/attach`, b: `customer=${id(cus)}` });
1320
+ if (![pi, conf, att].every(ok)) return false;
1321
+ const list = await h({ m: 'GET', p: '/v1/events' });
1322
+ if (!ok(list) || (list.body as Body).object !== 'list') return false;
1323
+ const events = (list.body as Body).data as Array<Record<string, unknown>>;
1324
+ const types = new Set(events.map((e) => e.type as string));
1325
+ const wanted = ['customer.created', 'payment_intent.created', 'payment_intent.succeeded', 'payment_method.attached'];
1326
+ if (!wanted.every((t) => types.has(t))) return false;
1327
+ // envelope shape: object 'event', carries the resource snapshot under data.object
1328
+ const succeeded = events.find((e) => e.type === 'payment_intent.succeeded');
1329
+ if (!succeeded || succeeded.object !== 'event' || ((succeeded.data as Body)?.object as Body)?.status !== 'succeeded') return false;
1330
+ // type filter narrows; single retrieve by id; unknown id → 404
1331
+ const filtered = await h({ m: 'GET', p: '/v1/events?type=payment_intent.succeeded' });
1332
+ const fdata = (filtered.body as Body).data as Array<Record<string, unknown>>;
1333
+ if (!ok(filtered) || fdata.length !== 1 || fdata[0]!.type !== 'payment_intent.succeeded') return false;
1334
+ const one = await h({ m: 'GET', p: `/v1/events/${succeeded.id}` });
1335
+ const missing = await h({ m: 'GET', p: '/v1/events/evt_nope' });
1336
+ return ok(one) && field(one, 'id') === succeeded.id && missing.status === 404;
1337
+ }),
1338
+ ),
1339
+ // Deep expand[]: a dotted path (expand[]=latest_charge / data.customer on a list) walks the
1340
+ // reference chain, replacing id strings with the full sub-object where the twin has it, and
1341
+ // leaving the id when it can't (no fabrication). One shared expander handles every resource +
1342
+ // list. (the expanded nested objects are produced ONLY by this feature.)
1343
+ done('stripe.api.expand', 'core', 'Deep `expand[]` across all resources', 'api', 'common', () =>
1344
+ withRoot(async (h) => {
1345
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=exp@twin.test' });
1346
+ const pi = await h({ m: 'POST', p: `/v1/payment_intents`, b: `amount=1000&currency=usd&customer=${id(cust)}` });
1347
+ // single-resource expand of a reference field.
1348
+ const e1 = await h({ m: 'GET', p: `/v1/payment_intents/${id(pi)}?expand[]=customer` });
1349
+ if (!ok(e1) || ((field(e1, 'customer') as Body)?.object) !== 'customer' || (field(e1, 'customer') as Body)?.id !== id(cust)) return false;
1350
+ // a charge whose customer + payment_intent chain expands deeply (charge.payment_intent.customer).
1351
+ const ch = await h({ m: 'POST', p: `/v1/charges`, b: `amount=1000&currency=usd&customer=${id(cust)}&payment_intent=${id(pi)}` });
1352
+ const e2 = await h({ m: 'GET', p: `/v1/charges/${id(ch)}?expand[]=payment_intent.customer` });
1353
+ const expandedPi = field(e2, 'payment_intent') as Body;
1354
+ if (!expandedPi || expandedPi.object !== 'payment_intent' || (expandedPi.customer as Body)?.id !== id(cust)) return false;
1355
+ // list-level expand (expand[]=data.customer) expands the field on every row.
1356
+ const l = await h({ m: 'GET', p: '/v1/charges?expand[]=data.customer' });
1357
+ const row = ((l.body as Body).data as Body[])[0]!;
1358
+ if ((row.customer as Body)?.object !== 'customer') return false;
1359
+ // an unresolvable reference is left as the id string (no fabricated object).
1360
+ const ch2 = await h({ m: 'POST', p: '/v1/charges', b: 'amount=500&currency=usd&customer=cus_missing' });
1361
+ const e3 = await h({ m: 'GET', p: `/v1/charges/${id(ch2)}?expand[]=customer` });
1362
+ return field(e3, 'customer') === 'cus_missing';
1363
+ }),
1364
+ ),
1365
+ // Cursor pagination: seed 5 customers, then walk the list with limit + starting_after and
1366
+ // back with ending_before, asserting page contents, ordering (newest-first), has_more, and
1367
+ // that the two cursors are inverses. Failable: a broken paginator would mis-slice or
1368
+ // mis-compute has_more / leave duplicates across pages.
1369
+ done('stripe.api.pagination', 'core', 'Cursor pagination (starting_after/ending_before, auto-paging)', 'api', 'core', () =>
1370
+ withRoot(async (h) => {
1371
+ const ids: string[] = [];
1372
+ for (let i = 0; i < 5; i++) {
1373
+ const c = await h({ m: 'POST', p: '/v1/customers', b: `email=p${i}@example.com`, });
1374
+ if (!ok(c)) return false;
1375
+ ids.push(id(c));
1376
+ }
1377
+ // newest-first: the last-created id is first in the unpaged list
1378
+ const full = await h({ m: 'GET', p: '/v1/customers?limit=100' });
1379
+ const fullIds = ((full.body as Body).data as Array<Body>).map((r) => r.id as string);
1380
+ if (fullIds.length !== 5 || fullIds[0] !== ids[4] || fullIds[4] !== ids[0]) return false;
1381
+ // page 1 (limit 2) → first two of fullIds, has_more true
1382
+ const p1 = await h({ m: 'GET', p: '/v1/customers?limit=2' });
1383
+ const p1d = ((p1.body as Body).data as Array<Body>).map((r) => r.id as string);
1384
+ if (!ok(p1) || p1d.length !== 2 || p1d[0] !== fullIds[0] || p1d[1] !== fullIds[1] || (p1.body as Body).has_more !== true) return false;
1385
+ // page 2 via starting_after = last id of page 1 → next two, no overlap
1386
+ const p2 = await h({ m: 'GET', p: `/v1/customers?limit=2&starting_after=${p1d[1]}` });
1387
+ const p2d = ((p2.body as Body).data as Array<Body>).map((r) => r.id as string);
1388
+ if (!ok(p2) || p2d.length !== 2 || p2d[0] !== fullIds[2] || p2d[1] !== fullIds[3] || (p2.body as Body).has_more !== true) return false;
1389
+ // page 3 → last one, has_more false
1390
+ const p3 = await h({ m: 'GET', p: `/v1/customers?limit=2&starting_after=${p2d[1]}` });
1391
+ const p3d = ((p3.body as Body).data as Array<Body>).map((r) => r.id as string);
1392
+ if (!ok(p3) || p3d.length !== 1 || p3d[0] !== fullIds[4] || (p3.body as Body).has_more !== false) return false;
1393
+ // ending_before is the inverse: page just before fullIds[2] == page 1
1394
+ const back = await h({ m: 'GET', p: `/v1/customers?limit=2&ending_before=${fullIds[2]}` });
1395
+ const backd = ((back.body as Body).data as Array<Body>).map((r) => r.id as string);
1396
+ return ok(back) && backd.length === 2 && backd[0] === fullIds[0] && backd[1] === fullIds[1];
1397
+ }),
1398
+ ),
1399
+ // API version pinning (Stripe-Version header): a malformed version is rejected with a 400
1400
+ // (invalid_api_version); a valid override is echoed onto the events the request produces
1401
+ // (Stripe's stored Event.api_version reflects the version in force). A request with no
1402
+ // version uses the twin's default. (version-pinned events produced ONLY by this feature.)
1403
+ done('stripe.api.versioning', 'core', 'Stripe-Version header / API version pinning', 'api', 'common', async () => {
1404
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1405
+ try {
1406
+ const bad = await handleStripeTwinRequest({ method: 'POST', path: '/v1/customers', body: 'email=v@twin.test', apiVersion: 'not-a-date', root });
1407
+ if (bad.status !== 400 || ((bad.body as Body).error as Body)?.code !== 'invalid_api_version') return false;
1408
+ // a valid override is reflected on the event the request produced (PI create →
1409
+ // payment_intent.created, which carries the version in force).
1410
+ const pi = await handleStripeTwinRequest({ method: 'POST', path: '/v1/payment_intents', body: 'amount=1000&currency=usd', apiVersion: '2022-11-15', root });
1411
+ if (!ok(pi)) return false;
1412
+ const events = await handleStripeTwinRequest({ method: 'GET', path: '/v1/events', root });
1413
+ const ev = ((events.body as Body).data as Body[]).find((e) => e.type === 'payment_intent.created');
1414
+ if (!ev || ev.api_version !== '2022-11-15') return false;
1415
+ // no version → the twin default.
1416
+ const pi2 = await handleStripeTwinRequest({ method: 'POST', path: '/v1/payment_intents', body: 'amount=500&currency=usd', root });
1417
+ const ev2 = (((await handleStripeTwinRequest({ method: 'GET', path: '/v1/events', root })).body as Body).data as Body[])
1418
+ .find((e) => e.type === 'payment_intent.created' && ((e.data as Body)?.object as Body)?.id === id(pi2));
1419
+ return ev2 ? ev2.api_version === '2024-06-20' : false;
1420
+ } catch { return false; } finally { rmSync(root, { recursive: true, force: true }); }
1421
+ }),
1422
+
1423
+ // ── Test helpers ──────────────────────────────────────────────────────────────────
1424
+ // Test clocks: create a clock at a frozen_time, then advance it; advancing past a trialing
1425
+ // subscription's trial_end transitions that sub trialing→active (the real clock-tick effect).
1426
+ // Advancing backwards 400s; missing frozen_time 400s; delete removes it. (the clock object +
1427
+ // the trial→active transition on advance are produced ONLY by this feature.)
1428
+ done('stripe.test_clocks', 'test-helpers', 'Test clocks (advance time for subscription/invoice)', 'api', 'common', () =>
1429
+ withRoot(async (h) => {
1430
+ const clock = await h({ m: 'POST', p: '/v1/test_helpers/test_clocks', b: 'frozen_time=1000&name=T' });
1431
+ if (!ok(clock) || field(clock, 'object') !== 'test_helpers.test_clock' || field(clock, 'frozen_time') !== 1000) return false;
1432
+ // a trialing subscription whose trial_end is 5000.
1433
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=clock@twin.test' });
1434
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}&trial_end=5000` });
1435
+ if (field(sub, 'status') !== 'trialing') return false;
1436
+ // advancing PAST trial_end transitions the sub to active.
1437
+ const adv = await h({ m: 'POST', p: `/v1/test_helpers/test_clocks/${id(clock)}/advance`, b: 'frozen_time=6000' });
1438
+ if (!ok(adv) || field(adv, 'frozen_time') !== 6000) return false;
1439
+ const subAfter = await h({ m: 'GET', p: `/v1/subscriptions/${id(sub)}` });
1440
+ if (field(subAfter, 'status') !== 'active') return false;
1441
+ const back = await h({ m: 'POST', p: `/v1/test_helpers/test_clocks/${id(clock)}/advance`, b: 'frozen_time=1' });
1442
+ const noTime = await h({ m: 'POST', p: '/v1/test_helpers/test_clocks' });
1443
+ const del = await h({ m: 'DELETE', p: `/v1/test_helpers/test_clocks/${id(clock)}` });
1444
+ const gone = await h({ m: 'GET', p: `/v1/test_helpers/test_clocks/${id(clock)}` });
1445
+ return back.status === 400 && noTime.status === 400 && ok(del) && field(del, 'deleted') === true && gone.status === 404;
1446
+ }),
1447
+ ),
1448
+ // Test-helper Issuing endpoints: fund_balance accrues the issuing balance; present an
1449
+ // authorization (the test-mode way to simulate card usage) → a 'pending' authorization
1450
+ // awaiting approve/decline; create_force_capture lands a settled transaction directly.
1451
+ // Missing required params 400; unknown card 400. (Exercised together with the issuing family.)
1452
+ done('stripe.test_helpers.issuing', 'test-helpers', 'Test-helper endpoints (fund balance, present authorization)', 'api', 'niche', () =>
1453
+ withRoot(async (h) => {
1454
+ const fund = await h({ m: 'POST', p: '/v1/test_helpers/issuing/fund_balance', b: 'amount=100000&currency=usd' });
1455
+ if (!ok(fund) || ((field(fund, 'issuing') as Body)?.available as Body[])[0]?.amount !== 100000) return false;
1456
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jane&type=individual&billing[address][line1]=1 Main&billing[address][city]=SF&billing[address][country]=US&billing[address][postal_code]=94105&billing[address][state]=CA' });
1457
+ const card = await h({ m: 'POST', p: `/v1/issuing/cards`, b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
1458
+ const auth = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(card)}&amount=2500` });
1459
+ if (!ok(auth) || field(auth, 'status') !== 'pending' || field(auth, 'card') !== id(card) || field(auth, 'cardholder') !== id(ch)) return false;
1460
+ const forced = await h({ m: 'POST', p: '/v1/test_helpers/issuing/transactions/create_force_capture', b: `card=${id(card)}&amount=1000` });
1461
+ if (!ok(forced) || field(forced, 'type') !== 'capture' || field(forced, 'amount') !== -1000) return false;
1462
+ const noAmt = await h({ m: 'POST', p: '/v1/test_helpers/issuing/fund_balance', b: 'currency=usd' });
1463
+ const badCard = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: 'card=ic_nope&amount=100' });
1464
+ return noAmt.status === 400 && badCard.status === 400;
1465
+ }),
1466
+ ),
1467
+
1468
+ // ── Tax ────────────────────────────────────────────────────────────────────────
1469
+ done('stripe.tax.rates', 'tax', 'TaxRates: create / retrieve / update / list', 'api', 'common', () =>
1470
+ withRoot(async (h) => {
1471
+ const c = await h({ m: 'POST', p: '/v1/tax_rates', b: 'display_name=Sales Tax&percentage=8.5&inclusive=false&jurisdiction=US' });
1472
+ if (!ok(c) || field(c, 'object') !== 'tax_rate' || field(c, 'percentage') !== 8.5 || field(c, 'inclusive') !== false) return false;
1473
+ const g = await h({ m: 'GET', p: `/v1/tax_rates/${id(c)}` });
1474
+ if (!ok(g) || id(g) !== id(c) || field(g, 'percentage') !== 8.5) return false;
1475
+ const u = await h({ m: 'POST', p: `/v1/tax_rates/${id(c)}`, b: 'active=false' });
1476
+ const l = await h({ m: 'GET', p: '/v1/tax_rates' });
1477
+ // missing required → 400; unknown id → 404 resource_missing
1478
+ const bad = await h({ m: 'POST', p: '/v1/tax_rates', b: 'percentage=5&inclusive=false' });
1479
+ const missing = await h({ m: 'GET', p: '/v1/tax_rates/txr_nope' });
1480
+ return ok(u) && field(u, 'active') === false && field(l, 'object') === 'list' &&
1481
+ bad.status === 400 && missing.status === 404 && ((missing.body as Body).error as Body)?.code === 'resource_missing';
1482
+ }),
1483
+ ),
1484
+ done('stripe.tax.calculations', 'tax', 'Tax calculations: create (amount_total + tax_breakdown) + retrieve + line_items', 'api', 'common', () =>
1485
+ withRoot(async (h) => {
1486
+ const calc = await h({ m: 'POST', p: '/v1/tax/calculations', b: 'currency=usd&line_items[0][amount]=1000&line_items[0][reference]=sku_1&customer_details[address][country]=US' });
1487
+ if (!ok(calc) || field(calc, 'object') !== 'tax.calculation') return false;
1488
+ // line_items[0].amount=1000 at the twin's 10% exclusive rate → tax 100, total 1100.
1489
+ if (field(calc, 'tax_amount_exclusive') !== 100 || field(calc, 'amount_total') !== 1100) return false;
1490
+ const g = await h({ m: 'GET', p: `/v1/tax/calculations/${id(calc)}` });
1491
+ const li = await h({ m: 'GET', p: `/v1/tax/calculations/${id(calc)}/line_items` });
1492
+ const liData = (li.body as Body).data as Body[];
1493
+ const noCur = await h({ m: 'POST', p: '/v1/tax/calculations', b: 'line_items[0][amount]=1000' });
1494
+ return ok(g) && id(g) === id(calc) && ok(li) && field(li, 'object') === 'list' &&
1495
+ liData.length === 1 && liData[0]!.amount_tax === 100 && noCur.status === 400;
1496
+ }),
1497
+ ),
1498
+ done('stripe.tax.registrations', 'tax', 'Tax registrations: create (active) + retrieve + list', 'api', 'common', () =>
1499
+ withRoot(async (h) => {
1500
+ const reg = await h({ m: 'POST', p: '/v1/tax/registrations', b: 'country=US&active_from=now&country_options[us][type]=state_sales_tax' });
1501
+ if (!ok(reg) || field(reg, 'object') !== 'tax.registration' || field(reg, 'status') !== 'active' || field(reg, 'country') !== 'US') return false;
1502
+ const g = await h({ m: 'GET', p: `/v1/tax/registrations/${id(reg)}` });
1503
+ const l = await h({ m: 'GET', p: '/v1/tax/registrations' });
1504
+ const bad = await h({ m: 'POST', p: '/v1/tax/registrations', b: 'active_from=now' });
1505
+ return ok(g) && id(g) === id(reg) && field(l, 'object') === 'list' && bad.status === 400;
1506
+ }),
1507
+ ),
1508
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'Tax Rates', 'Tax
1509
+ // Calculations'; now data-coupled (this file's in-house `uiDataCoupled`, reused from the
1510
+ // Radar/Reports precedent above): seed a tax rate + a tax calculation with distinct values →
1511
+ // fetch the SAME `/v1/tax_rates` + `/v1/tax/calculations` projections those screens read off
1512
+ // the running mirror server → render the mirror's OWN ListPane over both → assert one
1513
+ // list-row per seeded object carrying the seeded name. Failable: an empty workspace yields
1514
+ // zero rows → zero list-rows.
1515
+ done('stripe.ui.tax', 'ui-dashboard', 'Dashboard: Tax (Tax Rates + Tax Calculations) screens (data-coupled)', 'ui', 'common', uiDataCoupled({
1516
+ markers: ['Tax Rates', 'Tax Calculations', 'list-row'],
1517
+ seed: async (h) => {
1518
+ await h({ m: 'POST', p: '/v1/tax_rates', b: 'display_name=UI Check Tax&percentage=12.5&inclusive=false&jurisdiction=US' });
1519
+ await h({ m: 'POST', p: '/v1/tax/calculations', b: 'currency=usd&line_items[0][amount]=1000&line_items[0][reference]=ui_check_sku&customer_details[address][country]=US' });
1520
+ },
1521
+ check: async ({ get }) => {
1522
+ const rates = ((await get('/v1/tax_rates')).data as Body[]) as unknown as StripeRow[];
1523
+ const calcs = ((await get('/v1/tax/calculations')).data as Body[]) as unknown as StripeRow[];
1524
+ if (!rates.length || !calcs.length) return false;
1525
+ const ratesMarkup = renderSectionList('tax_rates', rates);
1526
+ const calcsMarkup = renderSectionList('tax/calculations', calcs);
1527
+ return listRows(ratesMarkup) === rates.length && ratesMarkup.includes('UI Check Tax') &&
1528
+ listRows(calcsMarkup) === calcs.length;
1529
+ },
1530
+ })),
1531
+
1532
+ // ── Issuing ──────────────────────────────────────────────────────────────────────
1533
+ // Cardholders: create requires name + type (individual|company) + billing[address]. update
1534
+ // round-trips; missing required + invalid type 400; unknown id 404.
1535
+ done('stripe.issuing.cardholders', 'issuing', 'Issuing Cardholders', 'api', 'niche', () =>
1536
+ withRoot(async (h) => {
1537
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jane&type=individual&billing[address][line1]=1 Main&billing[address][city]=SF&billing[address][country]=US&billing[address][postal_code]=94105&billing[address][state]=CA' });
1538
+ if (!ok(ch) || field(ch, 'object') !== 'issuing.cardholder' || field(ch, 'type') !== 'individual' || field(ch, 'status') !== 'active') return false;
1539
+ const g = await h({ m: 'GET', p: `/v1/issuing/cardholders/${id(ch)}` });
1540
+ if (!ok(g) || id(g) !== id(ch)) return false;
1541
+ const u = await h({ m: 'POST', p: `/v1/issuing/cardholders/${id(ch)}`, b: 'status=inactive' });
1542
+ const l = await h({ m: 'GET', p: '/v1/issuing/cardholders?status=inactive' });
1543
+ const noName = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'type=individual&billing[address][country]=US' });
1544
+ const badType = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=X&type=robot&billing[address][country]=US' });
1545
+ const noAddr = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=X&type=individual' });
1546
+ const nope = await h({ m: 'GET', p: '/v1/issuing/cardholders/ich_nope' });
1547
+ return ok(u) && field(u, 'status') === 'inactive' && ((l.body as Body).data as Body[]).length === 1 &&
1548
+ noName.status === 400 && badType.status === 400 && noAddr.status === 400 && nope.status === 404;
1549
+ }),
1550
+ ),
1551
+ // Cards: create requires cardholder (must exist) + currency + type (virtual|physical). A
1552
+ // virtual card is 'active'; physical is 'inactive'. Update accepts only active|inactive|canceled.
1553
+ done('stripe.issuing.cards', 'issuing', 'Issuing Cards', 'api', 'niche', () =>
1554
+ withRoot(async (h) => {
1555
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jane&type=individual&billing[address][country]=US' });
1556
+ const card = await h({ m: 'POST', p: `/v1/issuing/cards`, b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
1557
+ if (!ok(card) || field(card, 'object') !== 'issuing.card' || field(card, 'status') !== 'active' || field(card, 'type') !== 'virtual' || typeof field(card, 'last4') !== 'string') return false;
1558
+ const phys = await h({ m: 'POST', p: `/v1/issuing/cards`, b: `cardholder=${id(ch)}&currency=usd&type=physical` });
1559
+ if (field(phys, 'status') !== 'inactive') return false;
1560
+ const g = await h({ m: 'GET', p: `/v1/issuing/cards/${id(card)}` });
1561
+ const u = await h({ m: 'POST', p: `/v1/issuing/cards/${id(card)}`, b: 'status=canceled' });
1562
+ const l = await h({ m: 'GET', p: `/v1/issuing/cards?cardholder=${id(ch)}` });
1563
+ const badCh = await h({ m: 'POST', p: '/v1/issuing/cards', b: 'cardholder=ich_nope&currency=usd&type=virtual' });
1564
+ const badStatus = await h({ m: 'POST', p: `/v1/issuing/cards/${id(card)}`, b: 'status=frozen' });
1565
+ return ok(g) && id(g) === id(card) && ok(u) && field(u, 'status') === 'canceled' &&
1566
+ ((l.body as Body).data as Body[]).length === 2 && badCh.status === 400 && badStatus.status === 400;
1567
+ }),
1568
+ ),
1569
+ // Authorizations (+approve/decline): an authorization is PRESENTED via the test helper (pending),
1570
+ // then /approve closes it (approved:true) AND materializes a captured transaction, or /decline
1571
+ // closes it (approved:false). Acting on a finalized authorization 400s; unknown id 404.
1572
+ done('stripe.issuing.authorizations', 'issuing', 'Issuing Authorizations (+approve/decline)', 'api', 'niche', () =>
1573
+ withRoot(async (h) => {
1574
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jane&type=individual&billing[address][country]=US' });
1575
+ const card = await h({ m: 'POST', p: `/v1/issuing/cards`, b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
1576
+ const auth = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(card)}&amount=3000` });
1577
+ if (field(auth, 'status') !== 'pending') return false;
1578
+ const appr = await h({ m: 'POST', p: `/v1/issuing/authorizations/${id(auth)}/approve` });
1579
+ if (!ok(appr) || field(appr, 'status') !== 'closed' || field(appr, 'approved') !== true) return false;
1580
+ // approve materialized a captured transaction.
1581
+ const txns = await h({ m: 'GET', p: '/v1/issuing/transactions' });
1582
+ const txnData = (txns.body as Body).data as Body[];
1583
+ if (txnData.length !== 1 || txnData[0]!.authorization !== id(auth) || txnData[0]!.amount !== -3000) return false;
1584
+ // approving again 400s (already finalized).
1585
+ const again = await h({ m: 'POST', p: `/v1/issuing/authorizations/${id(auth)}/approve` });
1586
+ if (again.status !== 400) return false;
1587
+ // a second authorization can be declined.
1588
+ const auth2 = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(card)}&amount=500` });
1589
+ const dec = await h({ m: 'POST', p: `/v1/issuing/authorizations/${id(auth2)}/decline` });
1590
+ if (!ok(dec) || field(dec, 'approved') !== false || field(dec, 'status') !== 'closed') return false;
1591
+ const g = await h({ m: 'GET', p: `/v1/issuing/authorizations/${id(auth)}` });
1592
+ const nope = await h({ m: 'POST', p: '/v1/issuing/authorizations/iauth_nope/approve' });
1593
+ return ok(g) && id(g) === id(auth) && nope.status === 404;
1594
+ }),
1595
+ ),
1596
+ // Transactions + disputes: a settled transaction (from an approved authorization) can be
1597
+ // retrieved/updated and disputed. A dispute requires transaction + evidence[reason]; /submit
1598
+ // transitions unsubmitted→submitted; submitting twice 400s; unknown transaction 400.
1599
+ done('stripe.issuing.transactions', 'issuing', 'Issuing Transactions + disputes', 'api', 'niche', () =>
1600
+ withRoot(async (h) => {
1601
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jane&type=individual&billing[address][country]=US' });
1602
+ const card = await h({ m: 'POST', p: `/v1/issuing/cards`, b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
1603
+ const auth = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(card)}&amount=4200` });
1604
+ await h({ m: 'POST', p: `/v1/issuing/authorizations/${id(auth)}/approve` });
1605
+ const txns = await h({ m: 'GET', p: '/v1/issuing/transactions' });
1606
+ const txn = ((txns.body as Body).data as Body[])[0]!;
1607
+ const g = await h({ m: 'GET', p: `/v1/issuing/transactions/${txn.id}` });
1608
+ if (!ok(g) || field(g, 'object') !== 'issuing.transaction') return false;
1609
+ const u = await h({ m: 'POST', p: `/v1/issuing/transactions/${txn.id}`, b: 'metadata[note]=checked' });
1610
+ if (!ok(u)) return false;
1611
+ const disp = await h({ m: 'POST', p: '/v1/issuing/disputes', b: `transaction=${txn.id}&evidence[reason]=fraudulent` });
1612
+ if (!ok(disp) || field(disp, 'object') !== 'issuing.dispute' || field(disp, 'status') !== 'unsubmitted' || field(disp, 'amount') !== 4200) return false;
1613
+ const sub = await h({ m: 'POST', p: `/v1/issuing/disputes/${id(disp)}/submit` });
1614
+ if (!ok(sub) || field(sub, 'status') !== 'submitted') return false;
1615
+ const twice = await h({ m: 'POST', p: `/v1/issuing/disputes/${id(disp)}/submit` });
1616
+ const noReason = await h({ m: 'POST', p: '/v1/issuing/disputes', b: `transaction=${txn.id}` });
1617
+ const badTxn = await h({ m: 'POST', p: '/v1/issuing/disputes', b: 'transaction=ipi_nope&evidence[reason]=fraudulent' });
1618
+ return twice.status === 400 && noReason.status === 400 && badTxn.status === 400;
1619
+ }),
1620
+ ),
1621
+
1622
+ // ── Terminal ─────────────────────────────────────────────────────────────────────
1623
+ // Readers (+process_payment_intent): a reader registers with a registration_code (status
1624
+ // 'online'); handing it a PaymentIntent sets reader.action to process_payment_intent
1625
+ // in_progress; cancel_action clears it. Connection tokens mint a secret. Missing code 400.
1626
+ done('stripe.terminal.readers', 'terminal', 'Terminal Readers (+process_payment_intent)', 'api', 'niche', () =>
1627
+ withRoot(async (h) => {
1628
+ const loc = await h({ m: 'POST', p: '/v1/terminal/locations', b: 'display_name=HQ&address[country]=US&address[line1]=1 Main&address[city]=SF&address[postal_code]=94105&address[state]=CA' });
1629
+ const reader = await h({ m: 'POST', p: '/v1/terminal/readers', b: `registration_code=simulated-wpe&location=${id(loc)}&label=Front` });
1630
+ if (!ok(reader) || field(reader, 'object') !== 'terminal.reader' || field(reader, 'status') !== 'online') return false;
1631
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=1500&currency=usd' });
1632
+ const proc = await h({ m: 'POST', p: `/v1/terminal/readers/${id(reader)}/process_payment_intent`, b: `payment_intent=${id(pi)}` });
1633
+ if (!ok(proc) || (field(proc, 'action') as Body)?.type !== 'process_payment_intent' || (field(proc, 'action') as Body)?.status !== 'in_progress') return false;
1634
+ const cancel = await h({ m: 'POST', p: `/v1/terminal/readers/${id(reader)}/cancel_action` });
1635
+ if (!ok(cancel) || field(cancel, 'action') !== null) return false;
1636
+ const ct = await h({ m: 'POST', p: '/v1/terminal/connection_tokens' });
1637
+ if (!ok(ct) || field(ct, 'object') !== 'terminal.connection_token' || typeof field(ct, 'secret') !== 'string') return false;
1638
+ const g = await h({ m: 'GET', p: `/v1/terminal/readers/${id(reader)}` });
1639
+ const l = await h({ m: 'GET', p: `/v1/terminal/readers?location=${id(loc)}` });
1640
+ const noCode = await h({ m: 'POST', p: '/v1/terminal/readers', b: 'label=X' });
1641
+ const badPi = await h({ m: 'POST', p: `/v1/terminal/readers/${id(reader)}/process_payment_intent`, b: 'payment_intent=pi_nope' });
1642
+ return ok(g) && id(g) === id(reader) && ((l.body as Body).data as Body[]).length === 1 && noCode.status === 400 && badPi.status === 400;
1643
+ }),
1644
+ ),
1645
+ // Locations + configurations: a location requires display_name + address[country]; CRUD +
1646
+ // delete round-trip. A reader configuration is created/retrieved/deleted. Missing required 400.
1647
+ done('stripe.terminal.locations', 'terminal', 'Terminal Locations + connection tokens', 'api', 'niche', () =>
1648
+ withRoot(async (h) => {
1649
+ const loc = await h({ m: 'POST', p: '/v1/terminal/locations', b: 'display_name=Store&address[country]=US&address[line1]=1 Main&address[city]=SF&address[postal_code]=94105&address[state]=CA' });
1650
+ if (!ok(loc) || field(loc, 'object') !== 'terminal.location' || field(loc, 'display_name') !== 'Store') return false;
1651
+ const g = await h({ m: 'GET', p: `/v1/terminal/locations/${id(loc)}` });
1652
+ const u = await h({ m: 'POST', p: `/v1/terminal/locations/${id(loc)}`, b: 'display_name=Store 2' });
1653
+ const l = await h({ m: 'GET', p: '/v1/terminal/locations' });
1654
+ // reader configuration CRUD.
1655
+ const cfg = await h({ m: 'POST', p: '/v1/terminal/configurations', b: 'name=Default' });
1656
+ if (!ok(cfg) || field(cfg, 'object') !== 'terminal.configuration') return false;
1657
+ const cfgG = await h({ m: 'GET', p: `/v1/terminal/configurations/${id(cfg)}` });
1658
+ const cfgDel = await h({ m: 'DELETE', p: `/v1/terminal/configurations/${id(cfg)}` });
1659
+ const del = await h({ m: 'DELETE', p: `/v1/terminal/locations/${id(loc)}` });
1660
+ const gone = await h({ m: 'GET', p: `/v1/terminal/locations/${id(loc)}` });
1661
+ const noName = await h({ m: 'POST', p: '/v1/terminal/locations', b: 'address[country]=US' });
1662
+ const noCountry = await h({ m: 'POST', p: '/v1/terminal/locations', b: 'display_name=X' });
1663
+ return ok(g) && ok(u) && field(u, 'display_name') === 'Store 2' && field(l, 'object') === 'list' &&
1664
+ ok(cfgG) && ok(cfgDel) && field(cfgDel, 'deleted') === true && ok(del) && field(del, 'deleted') === true &&
1665
+ gone.status === 404 && noName.status === 400 && noCountry.status === 400;
1666
+ }),
1667
+ ),
1668
+
1669
+ // ── Radar (fraud prevention) — built deeply this cycle ───────────────────────────
1670
+ // Radar Reviews: a flagged payment opens a Review (seeded, since reviews arise from charges,
1671
+ // not a public create). open is the only approvable state → /approve closes it (open:false,
1672
+ // closed_reason 'approved'); a second approve 400s. retrieve + list round-trip; unknown id 404.
1673
+ // (the open→closed approve transition is produced ONLY by this feature.)
1674
+ done('stripe.radar.reviews', 'radar', 'Radar Reviews (approve/list)', 'api', 'niche', () =>
1675
+ withRoot(async (h) => {
1676
+ const rv = await h({ m: 'POST', p: '/v1/radar/reviews', b: 'charge=ch_twin&payment_intent=pi_twin' });
1677
+ if (!ok(rv) || field(rv, 'object') !== 'review' || field(rv, 'open') !== true) return false;
1678
+ const g = await h({ m: 'GET', p: `/v1/reviews/${id(rv)}` });
1679
+ const l = await h({ m: 'GET', p: '/v1/reviews' });
1680
+ if (!ok(g) || id(g) !== id(rv) || field(l, 'object') !== 'list') return false;
1681
+ const ap = await h({ m: 'POST', p: `/v1/reviews/${id(rv)}/approve` });
1682
+ if (!ok(ap) || field(ap, 'open') !== false || field(ap, 'closed_reason') !== 'approved') return false;
1683
+ const apAgain = await h({ m: 'POST', p: `/v1/reviews/${id(rv)}/approve` });
1684
+ const missing = await h({ m: 'POST', p: '/v1/reviews/prv_nope/approve' });
1685
+ return apAgain.status === 400 && missing.status === 404;
1686
+ }),
1687
+ ),
1688
+ // Radar Value Lists (+ items): create a list (requires alias+name), add items (requires
1689
+ // value_list + value), list items (scoped), retrieve, delete item, delete list. Missing
1690
+ // alias/name 400; item against unknown list 400; missing value 400. (the list+item store is
1691
+ // produced ONLY by this feature.)
1692
+ done('stripe.radar.value_lists', 'radar', 'Radar value lists + items', 'api', 'niche', () =>
1693
+ withRoot(async (h) => {
1694
+ const vl = await h({ m: 'POST', p: '/v1/radar/value_lists', b: 'alias=block_ips&name=Blocked IPs&item_type=ip_address' });
1695
+ if (!ok(vl) || field(vl, 'object') !== 'radar.value_list' || field(vl, 'alias') !== 'block_ips') return false;
1696
+ const item = await h({ m: 'POST', p: '/v1/radar/value_list_items', b: `value_list=${id(vl)}&value=1.2.3.4` });
1697
+ if (!ok(item) || field(item, 'object') !== 'radar.value_list_item' || field(item, 'value') !== '1.2.3.4') return false;
1698
+ const g = await h({ m: 'GET', p: `/v1/radar/value_lists/${id(vl)}` });
1699
+ const items = await h({ m: 'GET', p: `/v1/radar/value_list_items?value_list=${id(vl)}` });
1700
+ if (!ok(g) || ((items.body as Body).data as Body[]).length !== 1) return false;
1701
+ const delItem = await h({ m: 'DELETE', p: `/v1/radar/value_list_items/${id(item)}` });
1702
+ const itemsAfter = await h({ m: 'GET', p: `/v1/radar/value_list_items?value_list=${id(vl)}` });
1703
+ const delList = await h({ m: 'DELETE', p: `/v1/radar/value_lists/${id(vl)}` });
1704
+ const gone = await h({ m: 'GET', p: `/v1/radar/value_lists/${id(vl)}` });
1705
+ // vendor errors.
1706
+ const noAlias = await h({ m: 'POST', p: '/v1/radar/value_lists', b: 'name=X' });
1707
+ const badList = await h({ m: 'POST', p: '/v1/radar/value_list_items', b: 'value_list=rsl_nope&value=x' });
1708
+ const noValue = await h({ m: 'POST', p: '/v1/radar/value_list_items', b: `value_list=${id(vl)}` });
1709
+ return ok(delItem) && field(delItem, 'deleted') === true && ((itemsAfter.body as Body).data as Body[]).length === 0 &&
1710
+ ok(delList) && field(delList, 'deleted') === true && gone.status === 404 &&
1711
+ noAlias.status === 400 && badList.status === 400 && noValue.status === 400;
1712
+ }),
1713
+ ),
1714
+ // Radar Rules: create a custom rule (requires action ∈ {block,review,allow} + predicate),
1715
+ // retrieve, list (filterable by action), delete. Missing action/predicate 400; bad action 400.
1716
+ done('stripe.radar.rules', 'radar', 'Radar rules', 'api', 'niche', () =>
1717
+ withRoot(async (h) => {
1718
+ const rule = await h({ m: 'POST', p: '/v1/radar/rules', b: 'action=block&predicate=:risk_level: = "highest"' });
1719
+ if (!ok(rule) || field(rule, 'object') !== 'radar.rule' || field(rule, 'action') !== 'block') return false;
1720
+ const g = await h({ m: 'GET', p: `/v1/radar/rules/${id(rule)}` });
1721
+ const l = await h({ m: 'GET', p: '/v1/radar/rules?action=block' });
1722
+ if (!ok(g) || id(g) !== id(rule) || ((l.body as Body).data as Body[]).length !== 1) return false;
1723
+ const del = await h({ m: 'DELETE', p: `/v1/radar/rules/${id(rule)}` });
1724
+ const gone = await h({ m: 'GET', p: `/v1/radar/rules/${id(rule)}` });
1725
+ const noAction = await h({ m: 'POST', p: '/v1/radar/rules', b: 'predicate=x' });
1726
+ const badAction = await h({ m: 'POST', p: '/v1/radar/rules', b: 'action=nuke&predicate=x' });
1727
+ const noPred = await h({ m: 'POST', p: '/v1/radar/rules', b: 'action=block' });
1728
+ return ok(del) && field(del, 'deleted') === true && gone.status === 404 &&
1729
+ noAction.status === 400 && badAction.status === 400 && noPred.status === 400;
1730
+ }),
1731
+ ),
1732
+ // Dashboard: Radar (Reviews + Value Lists + Rules) screens — rendered surfaces in the mirror.
1733
+ // Radar (Reviews + Value Lists + Rules) — RUNG-5 data-coupled: the three section markers are
1734
+ // bundled (the screens are wired in) AND, for each of the three Radar collections, we seed
1735
+ // real twin state (a review, a value-list + item, a rule), fetch the SAME /v1 endpoint the
1736
+ // mirror's React client reads off the running mirror server, render the mirror's OWN ListPane
1737
+ // component over those rows, and assert the rendered DOM emits exactly one `list-row` landmark
1738
+ // per seeded object carrying that object's id. Failable: an empty workspace yields zero rows →
1739
+ // zero list-rows → false; and a marker-only/hardcoded render could not surface the seeded ids.
1740
+ done('stripe.ui.radar', 'ui-dashboard', 'Dashboard: Radar (Reviews + Value Lists + Rules) screens', 'ui', 'niche', uiDataCoupled({
1741
+ markers: ['Radar Reviews', 'Radar Value Lists', 'Radar Rules', 'list-row'],
1742
+ seed: async (h) => {
1743
+ // a flagged payment opens a Review; a value list + one item; one custom rule.
1744
+ await h({ m: 'POST', p: '/v1/radar/reviews', b: 'charge=ch_radar_ui&payment_intent=pi_radar_ui' });
1745
+ const vl = await h({ m: 'POST', p: '/v1/radar/value_lists', b: 'alias=ui_block_ips&name=UI Blocked IPs&item_type=ip_address' });
1746
+ await h({ m: 'POST', p: '/v1/radar/value_list_items', b: `value_list=${(vl.body as Body).id}&value=9.9.9.9` });
1747
+ await h({ m: 'POST', p: '/v1/radar/rules', b: 'action=block&predicate=:risk_level: = "highest"' });
1748
+ },
1749
+ check: async ({ get }) => {
1750
+ const reviews = ((await get('/v1/reviews')).data as Body[]) ?? [];
1751
+ const valueLists = ((await get('/v1/radar/value_lists')).data as Body[]) ?? [];
1752
+ const rules = ((await get('/v1/radar/rules')).data as Body[]) ?? [];
1753
+ // the seeded objects must be present in the projections the screen reads
1754
+ const review = reviews.find((r) => r.charge === 'ch_radar_ui');
1755
+ const vl = valueLists.find((l) => l.alias === 'ui_block_ips');
1756
+ const rule = rules.find((r) => r.predicate === ':risk_level: = "highest"');
1757
+ if (!review || !vl || !rule) return false;
1758
+ // RENDER the mirror's own ListPane over the fetched rows and assert the DOM emits one
1759
+ // list-row per object, each carrying the seeded object's id (subtitle renders r.id).
1760
+ const reviewsMarkup = renderSectionList('reviews', reviews as StripeRow[]);
1761
+ const valueListsMarkup = renderSectionList('radar/value_lists', valueLists as StripeRow[]);
1762
+ const rulesMarkup = renderSectionList('radar/rules', rules as StripeRow[]);
1763
+ return (
1764
+ // reviews + rules render their id in the row subtitle; the value-list row renders its
1765
+ // name + alias (the id is not surfaced in that section's row), so assert those instead.
1766
+ listRows(reviewsMarkup) === reviews.length && reviewsMarkup.includes(String(review.id)) &&
1767
+ listRows(valueListsMarkup) === valueLists.length && valueListsMarkup.includes('UI Blocked IPs') && valueListsMarkup.includes('ui_block_ips') &&
1768
+ listRows(rulesMarkup) === rules.length && rulesMarkup.includes(String(rule.id)) && rulesMarkup.includes('block')
1769
+ );
1770
+ },
1771
+ })),
1772
+
1773
+ // ── Reporting ────────────────────────────────────────────────────────────────────
1774
+ // Reporting report_runs + report_types: POST /v1/reporting/report_runs requires
1775
+ // parameters[report_type] (a valid type from the catalog); the twin completes the run
1776
+ // synchronously (status 'succeeded') with a result File ref. Unknown/missing report_type 400.
1777
+ // The report-type catalog is listable/retrievable.
1778
+ done('stripe.reporting.report_runs', 'reporting', 'Reporting (report_runs + report_types)', 'api', 'niche', () =>
1779
+ withRoot(async (h) => {
1780
+ const types = await h({ m: 'GET', p: '/v1/reporting/report_types' });
1781
+ if (!ok(types) || ((types.body as Body).data as Body[]).length === 0) return false;
1782
+ const one = await h({ m: 'GET', p: '/v1/reporting/report_types/balance.summary.1' });
1783
+ if (!ok(one) || field(one, 'object') !== 'reporting.report_type') return false;
1784
+ const run = await h({ m: 'POST', p: '/v1/reporting/report_runs', b: 'parameters[report_type]=balance.summary.1&parameters[interval_start]=0&parameters[interval_end]=100' });
1785
+ if (!ok(run) || field(run, 'object') !== 'reporting.report_run' || field(run, 'status') !== 'succeeded' || field(run, 'report_type') !== 'balance.summary.1') return false;
1786
+ if ((field(run, 'result') as Body)?.object !== 'file') return false;
1787
+ const g = await h({ m: 'GET', p: `/v1/reporting/report_runs/${id(run)}` });
1788
+ const l = await h({ m: 'GET', p: '/v1/reporting/report_runs' });
1789
+ const noType = await h({ m: 'POST', p: '/v1/reporting/report_runs', b: 'parameters[interval_start]=0' });
1790
+ const badType = await h({ m: 'POST', p: '/v1/reporting/report_runs', b: 'parameters[report_type]=bogus.report' });
1791
+ const nope = await h({ m: 'GET', p: '/v1/reporting/report_runs/frr_nope' });
1792
+ const badTypeRetrieve = await h({ m: 'GET', p: '/v1/reporting/report_types/bogus.report' });
1793
+ return ok(g) && id(g) === id(run) && ((l.body as Body).data as Body[]).length === 1 &&
1794
+ noType.status === 400 && badType.status === 400 && nope.status === 404 && badTypeRetrieve.status === 404;
1795
+ }),
1796
+ ),
1797
+ // Reporting API (the modellable part of Sigma/Financial reports): report_types catalog +
1798
+ // report_runs create→result. A run requires a valid parameters[report_type] (else 400),
1799
+ // starts and completes to 'succeeded' with a result File ref. The Sigma SQL warehouse
1800
+ // itself is out-of-scope (see the out-of-scope entry below). Unknown type 400.
1801
+ done('stripe.sigma_financial', 'reporting', 'Financial reports / Sigma (Reporting API)', 'api', 'niche', () =>
1802
+ withRoot(async (h) => {
1803
+ const types = await h({ m: 'GET', p: '/v1/reporting/report_types' });
1804
+ if (!ok(types) || field(types, 'object') !== 'list' || ((types.body as Body).data as Body[]).length === 0) return false;
1805
+ const run = await h({ m: 'POST', p: '/v1/reporting/report_runs', b: 'parameters[report_type]=balance.summary.1' });
1806
+ if (!ok(run) || field(run, 'object') !== 'reporting.report_run' || field(run, 'status') !== 'succeeded') return false;
1807
+ if ((field(run, 'result') as Body)?.object !== 'file') return false;
1808
+ const get = await h({ m: 'GET', p: `/v1/reporting/report_runs/${id(run)}` });
1809
+ const list = await h({ m: 'GET', p: '/v1/reporting/report_runs' });
1810
+ const badType = await h({ m: 'POST', p: '/v1/reporting/report_runs', b: 'parameters[report_type]=not.a.type' });
1811
+ const missing = await h({ m: 'POST', p: '/v1/reporting/report_runs', b: 'metadata[x]=1' });
1812
+ return ok(get) && field(list, 'object') === 'list' && badType.status === 400 && missing.status === 400;
1813
+ }),
1814
+ ),
1815
+ outOfScope('stripe.sigma_financial.warehouse', 'reporting', 'Sigma SQL query warehouse (scheduled SQL queries)', 'api', 'niche',
1816
+ 'Out of scope: Sigma is a hosted SQL analytics warehouse over your account data — running arbitrary SQL against a managed data lake is not locally reproducible. The Reporting API (report_types + report_runs) IS modeled (stripe.sigma_financial).'),
1817
+
1818
+ // ── Connector (full pull/push) ────────────────────────────────────────────────────
1819
+ done('stripe.connector.read_surface', 'connector', 'Connector read surface (list core collections in one pass)', 'connector', 'core', () =>
1820
+ withRoot(async (h) => {
1821
+ for (const p of ['/v1/customers', '/v1/charges', '/v1/products', '/v1/invoices', '/v1/subscriptions']) {
1822
+ const r = await h({ m: 'GET', p });
1823
+ if (!ok(r) || field(r, 'object') !== 'list') return false;
1824
+ }
1825
+ return true;
1826
+ }),
1827
+ ),
1828
+ // Full bi-directional sync over the injected client: PUSH every pending local action to
1829
+ // real Stripe (confirming each), then PULL every collection + webhook endpoints back and
1830
+ // fold them into the event log. A re-sync of identical state appends nothing (shadow-diff).
1831
+ // Exercised fully offline via a fake executor (no network) — the same code path as live.
1832
+ done('stripe.connector.full_sync', 'connector', 'Connector full bi-directional sync (all collections + webhooks)', 'connector', 'common', async () => {
1833
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1834
+ try {
1835
+ // a pending local write to push.
1836
+ const cust = await handleStripeTwinRequest({ method: 'POST', path: '/v1/customers', body: 'email=sync@twin.test', root });
1837
+ if (!ok(cust)) return false;
1838
+ // a fake real-Stripe executor: records pushes; returns canned lists on the pull pass.
1839
+ const pushed: string[] = [];
1840
+ const lists: Record<string, any[]> = {
1841
+ '/v1/customers': [{ id: 'cus_real', object: 'customer', email: 'real@x.co' }],
1842
+ '/v1/products': [{ id: 'prod_real', object: 'product', name: 'Real' }],
1843
+ '/v1/webhook_endpoints': [{ id: 'we_real', object: 'webhook_endpoint', url: 'https://x/hook', status: 'enabled' }],
1844
+ };
1845
+ const execute = (async (method: string, path: string, params?: Record<string, unknown>) => {
1846
+ if (method === 'GET') return { object: 'list', data: lists[path] ?? [] };
1847
+ pushed.push(`${method} ${path}`);
1848
+ return { id: 'ext_pushed', object: 'customer' };
1849
+ }) as unknown as Parameters<typeof fullSyncStripe>[0];
1850
+ const res = await fullSyncStripe(execute, { root, occurredAt: '2024-01-01T00:00:00.000Z' });
1851
+ if (res.pushed < 1 || pushed.length < 1) return false; // the pending create was pushed
1852
+ if (res.observed < 1 || res.collections < 11) return false; // all collections + webhooks pulled
1853
+ // the pulled real customer is now in the twin's projection.
1854
+ const list = await handleStripeTwinRequest({ method: 'GET', path: '/v1/customers', root });
1855
+ const emails = ((list.body as Body).data as Body[]).map((c) => c.email);
1856
+ if (!emails.includes('real@x.co')) return false;
1857
+ // re-sync of identical state appends no deltas (idempotent shadow-diff).
1858
+ const again = await fullSyncStripe(execute, { root, occurredAt: '2024-01-01T00:00:00.000Z' });
1859
+ return again.deltasAppended === 0;
1860
+ } catch { return false; } finally { rmSync(root, { recursive: true, force: true }); }
1861
+ }),
1862
+
1863
+ // ── DASHBOARD UI screens (the real Stripe Dashboard) ─────────────────────────────
1864
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Customers'; now data-coupled:
1865
+ // seed a customer with a distinct email → fetch the SAME `/v1/customers` projection the
1866
+ // screen reads → render the mirror's OWN ListPane over the rows → assert the seeded email
1867
+ // survives. Failable: an empty workspace yields zero rows.
1868
+ done('stripe.ui.customers', 'ui-dashboard', 'Dashboard: Customers screen (data-coupled)', 'ui', 'core', uiDataCoupled({
1869
+ markers: ['Customers', 'list-row'],
1870
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/customers', b: 'email=ui-customers-check@twin.test&name=UI Customer Check' }); },
1871
+ check: async ({ get }) => {
1872
+ const rows = ((await get('/v1/customers')).data as Body[]) as unknown as StripeRow[];
1873
+ if (!rows.length) return false;
1874
+ const markup = renderSectionList('customers', rows);
1875
+ return listRows(markup) === rows.length && markup.includes('UI Customer Check') && rows.some((c) => c.email === 'ui-customers-check@twin.test');
1876
+ },
1877
+ })),
1878
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Payments'; now data-coupled:
1879
+ // seed a PaymentIntent with a distinct amount → fetch the SAME `/v1/payment_intents`
1880
+ // projection the screen reads → render the mirror's OWN ListPane → assert the seeded amount
1881
+ // survives. Failable: an empty workspace yields zero rows.
1882
+ done('stripe.ui.payments', 'ui-dashboard', 'Dashboard: Payments (PaymentIntents) screen (data-coupled)', 'ui', 'core', uiDataCoupled({
1883
+ markers: ['Payments', 'list-row'],
1884
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=4321&currency=usd' }); },
1885
+ check: async ({ get }) => {
1886
+ const rows = ((await get('/v1/payment_intents')).data as Body[]) as unknown as StripeRow[];
1887
+ if (!rows.length) return false;
1888
+ const markup = renderSectionList('payment_intents', rows);
1889
+ return listRows(markup) === rows.length && rows.some((p) => p.amount === 4321) && markup.includes(formatStripeAmount(4321, 'usd'));
1890
+ },
1891
+ })),
1892
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Charges'; now data-coupled:
1893
+ // seed a charge with a distinct amount → fetch the SAME `/v1/charges` projection the screen
1894
+ // reads → render the mirror's OWN ListPane → assert the seeded amount survives.
1895
+ done('stripe.ui.charges', 'ui-dashboard', 'Dashboard: Charges screen (data-coupled)', 'ui', 'core', uiDataCoupled({
1896
+ markers: ['Charges', 'list-row'],
1897
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/charges', b: 'amount=1234&currency=usd' }); },
1898
+ check: async ({ get }) => {
1899
+ const rows = ((await get('/v1/charges')).data as Body[]) as unknown as StripeRow[];
1900
+ if (!rows.length) return false;
1901
+ const markup = renderSectionList('charges', rows);
1902
+ return listRows(markup) === rows.length && rows.some((c) => c.amount === 1234) && markup.includes(formatStripeAmount(1234, 'usd'));
1903
+ },
1904
+ })),
1905
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Refunds'; now data-coupled:
1906
+ // seed a refund against a distinct charge id → fetch the SAME `/v1/refunds` projection the
1907
+ // screen reads → render the mirror's OWN ListPane → assert the seeded charge id survives.
1908
+ done('stripe.ui.refunds', 'ui-dashboard', 'Dashboard: Refunds screen (data-coupled)', 'ui', 'core', uiDataCoupled({
1909
+ markers: ['Refunds', 'list-row'],
1910
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/refunds', b: 'charge=ch_ui_check_refund&amount=555' }); },
1911
+ check: async ({ get }) => {
1912
+ const rows = ((await get('/v1/refunds')).data as Body[]) as unknown as StripeRow[];
1913
+ if (!rows.length) return false;
1914
+ const markup = renderSectionList('refunds', rows);
1915
+ return listRows(markup) === rows.length && rows.some((r) => r.charge === 'ch_ui_check_refund' && r.amount === 555);
1916
+ },
1917
+ })),
1918
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Subscriptions'; now
1919
+ // data-coupled: seed a customer + product + price + subscription → fetch the SAME
1920
+ // `/v1/subscriptions` projection the screen reads → render the mirror's OWN ListPane →
1921
+ // assert the seeded customer id survives (rendered in the row subtitle).
1922
+ done('stripe.ui.subscriptions', 'ui-dashboard', 'Dashboard: Subscriptions screen (data-coupled)', 'ui', 'core', uiDataCoupled({
1923
+ markers: ['Subscriptions', 'list-row'],
1924
+ seed: async (h) => {
1925
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=sub-ui-check@twin.test' });
1926
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=UI Check Plan' });
1927
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=1999&currency=usd&recurring[interval]=month&product=${(prod.body as Body).id}` });
1928
+ await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${(cust.body as Body).id}&items[0][price]=${(price.body as Body).id}` });
1929
+ },
1930
+ check: async ({ get }) => {
1931
+ const rows = ((await get('/v1/subscriptions')).data as Body[]) as unknown as StripeRow[];
1932
+ if (!rows.length) return false;
1933
+ const markup = renderSectionList('subscriptions', rows);
1934
+ return listRows(markup) === rows.length && rows.some((s) => (s.customer as string)?.startsWith('cus_')) && markup.includes(String(rows[0]!.customer));
1935
+ },
1936
+ })),
1937
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Invoices'; now data-coupled:
1938
+ // seed a customer + invoice → fetch the SAME `/v1/invoices` projection the screen reads →
1939
+ // render the mirror's OWN ListPane → assert the seeded customer id survives.
1940
+ done('stripe.ui.invoices', 'ui-dashboard', 'Dashboard: Invoices screen (data-coupled)', 'ui', 'core', uiDataCoupled({
1941
+ markers: ['Invoices', 'list-row'],
1942
+ seed: async (h) => {
1943
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=invoices-ui-check@twin.test' });
1944
+ await h({ m: 'POST', p: '/v1/invoices', b: `customer=${(cust.body as Body).id}` });
1945
+ },
1946
+ check: async ({ get }) => {
1947
+ const rows = ((await get('/v1/invoices')).data as Body[]) as unknown as StripeRow[];
1948
+ if (!rows.length) return false;
1949
+ const markup = renderSectionList('invoices', rows);
1950
+ return listRows(markup) === rows.length && rows.some((i) => (i.customer as string)?.startsWith('cus_'));
1951
+ },
1952
+ })),
1953
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'Products', 'Prices'; now
1954
+ // data-coupled: seed a product with a distinct name + a price → fetch the SAME
1955
+ // `/v1/products` + `/v1/prices` projections those screens read → render the mirror's OWN
1956
+ // ListPane over both → assert the seeded name survives.
1957
+ done('stripe.ui.products', 'ui-dashboard', 'Dashboard: Products + Prices screens (data-coupled)', 'ui', 'core', uiDataCoupled({
1958
+ markers: ['Products', 'Prices', 'list-row'],
1959
+ seed: async (h) => {
1960
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=UI Check Product' });
1961
+ await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=2599&currency=usd&product=${(prod.body as Body).id}` });
1962
+ },
1963
+ check: async ({ get }) => {
1964
+ const products = ((await get('/v1/products')).data as Body[]) as unknown as StripeRow[];
1965
+ const prices = ((await get('/v1/prices')).data as Body[]) as unknown as StripeRow[];
1966
+ if (!products.length || !prices.length) return false;
1967
+ const productsMarkup = renderSectionList('products', products);
1968
+ const pricesMarkup = renderSectionList('prices', prices);
1969
+ return listRows(productsMarkup) === products.length && productsMarkup.includes('UI Check Product') &&
1970
+ listRows(pricesMarkup) === prices.length && prices.some((p) => p.unit_amount === 2599);
1971
+ },
1972
+ })),
1973
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Payment Methods'; now
1974
+ // data-coupled: seed a card payment method → fetch the SAME `/v1/payment_methods`
1975
+ // projection the screen reads → shape through the mirror's OWN `ListPane` (which titles
1976
+ // each row via the section's `formatPaymentMethod`-backed title()) → assert the seeded
1977
+ // card's last4 survives.
1978
+ done('stripe.ui.payment_methods', 'ui-dashboard', 'Dashboard: Payment Methods screen (data-coupled)', 'ui', 'core', uiDataCoupled({
1979
+ markers: ['Payment Methods', 'list-row'],
1980
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2030&card[cvc]=314' }); },
1981
+ check: async ({ get }) => {
1982
+ const rows = ((await get('/v1/payment_methods')).data as Body[]) as unknown as StripeRow[];
1983
+ if (!rows.length) return false;
1984
+ const markup = renderSectionList('payment_methods', rows);
1985
+ return listRows(markup) === rows.length && markup.includes('4242');
1986
+ },
1987
+ })),
1988
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Disputes'; now data-coupled:
1989
+ // seed a dispute against a distinct charge id → fetch the SAME `/v1/disputes` projection the
1990
+ // screen reads → render the mirror's OWN ListPane → assert the seeded charge id survives.
1991
+ done('stripe.ui.disputes', 'ui-dashboard', 'Dashboard: Disputes screen (data-coupled)', 'ui', 'common', uiDataCoupled({
1992
+ markers: ['Disputes', 'list-row'],
1993
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/disputes', b: 'charge=ch_ui_check_dispute&amount=1000&currency=usd' }); },
1994
+ check: async ({ get }) => {
1995
+ const rows = ((await get('/v1/disputes')).data as Body[]) as unknown as StripeRow[];
1996
+ if (!rows.length) return false;
1997
+ const markup = renderSectionList('disputes', rows);
1998
+ return listRows(markup) === rows.length && rows.some((d) => d.charge === 'ch_ui_check_dispute' && d.amount === 1000);
1999
+ },
2000
+ })),
2001
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Payouts'; now data-coupled:
2002
+ // seed a payout with a distinct amount → fetch the SAME `/v1/payouts` projection the screen
2003
+ // reads → render the mirror's OWN ListPane → assert the seeded amount survives.
2004
+ done('stripe.ui.payouts', 'ui-dashboard', 'Dashboard: Payouts screen (data-coupled)', 'ui', 'core', uiDataCoupled({
2005
+ markers: ['Payouts', 'list-row'],
2006
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/payouts', b: 'amount=7777&currency=usd' }); },
2007
+ check: async ({ get }) => {
2008
+ const rows = ((await get('/v1/payouts')).data as Body[]) as unknown as StripeRow[];
2009
+ if (!rows.length) return false;
2010
+ const markup = renderSectionList('payouts', rows);
2011
+ return listRows(markup) === rows.length && rows.some((p) => p.amount === 7777) && markup.includes(formatStripeAmount(7777, 'usd'));
2012
+ },
2013
+ })),
2014
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'Balance', 'Balance
2015
+ // Transactions', 'balance-summary'; now data-coupled: seed a balance transaction with a
2016
+ // distinct amount → fetch the SAME `/v1/balance` summary + `/v1/balance_transactions` list
2017
+ // those screens read → render the mirror's OWN `BalanceSummary` component over the balance
2018
+ // object (the exact component the Balance detail shows) AND `ListPane` over the
2019
+ // transactions → assert the seeded amount's formatted text appears in both. Failable: an
2020
+ // empty workspace's balance has no buckets → `BalanceSummary` renders nothing.
2021
+ done('stripe.ui.balance', 'ui-dashboard', 'Dashboard: Balance + Balance Transactions screens (data-coupled)', 'ui', 'core', uiDataCoupled({
2022
+ markers: ['Balance', 'Balance Transactions', 'balance-summary'],
2023
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/balance_transactions', b: 'amount=424200&currency=usd' }); },
2024
+ check: async ({ get }) => {
2025
+ const balance = await get('/v1/balance');
2026
+ const txns = ((await get('/v1/balance_transactions')).data as Body[]) as unknown as StripeRow[];
2027
+ if (!txns.length) return false;
2028
+ const summaryMarkup = renderToStaticMarkup(createElement(BalanceSummary, { row: { id: 'balance', ...balance } as StripeRow }));
2029
+ const txnsMarkup = renderSectionList('balance_transactions', txns);
2030
+ return summaryMarkup.includes('balance-summary') && summaryMarkup.includes(formatStripeAmount(424200, 'usd')) &&
2031
+ listRows(txnsMarkup) === txns.length && txns.some((t) => t.amount === 424200);
2032
+ },
2033
+ })),
2034
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'Connected Accounts',
2035
+ // 'Transfers', 'connect-flag'; now data-coupled: seed a Connect account + a transfer to it →
2036
+ // fetch the SAME `/v1/accounts` + `/v1/transfers` projections those screens read → render the
2037
+ // mirror's OWN `ConnectAccountPanel` over the account row (the exact enablement-flags panel)
2038
+ // AND `ListPane` over the transfers → assert the seeded destination id survives. Failable: no
2039
+ // seeded account ⇒ `ConnectAccountPanel` renders nothing (empty flags).
2040
+ done('stripe.ui.connect', 'ui-dashboard', 'Dashboard: Connect (Connected Accounts + Transfers) screens (data-coupled)', 'ui', 'niche', uiDataCoupled({
2041
+ markers: ['Connected Accounts', 'Transfers', 'connect-flag'],
2042
+ seed: async (h) => {
2043
+ const acct = await h({ m: 'POST', p: '/v1/accounts', b: 'type=express&country=US' });
2044
+ await h({ m: 'POST', p: '/v1/transfers', b: `amount=999&currency=usd&destination=${(acct.body as Body).id}` });
2045
+ },
2046
+ check: async ({ get }) => {
2047
+ const accounts = ((await get('/v1/accounts')).data as Body[]) as unknown as StripeRow[];
2048
+ const transfers = ((await get('/v1/transfers')).data as Body[]) as unknown as StripeRow[];
2049
+ const account = accounts.find((a) => a.country === 'US');
2050
+ if (!account || !transfers.length) return false;
2051
+ const panelMarkup = renderToStaticMarkup(createElement(ConnectAccountPanel, { row: account }));
2052
+ const transfersMarkup = renderSectionList('transfers', transfers);
2053
+ return panelMarkup.includes('connect-flag') && listRows(transfersMarkup) === transfers.length &&
2054
+ transfers.some((t) => t.destination === account!.id && t.amount === 999);
2055
+ },
2056
+ })),
2057
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Events'; now data-coupled:
2058
+ // seed a synthetic event with a distinct type → fetch the SAME `/v1/events` projection the
2059
+ // screen reads → render the mirror's OWN ListPane → assert the seeded type survives.
2060
+ done('stripe.ui.events', 'ui-dashboard', 'Dashboard: Events log screen (data-coupled)', 'ui', 'core', uiDataCoupled({
2061
+ markers: ['Events', 'list-row'],
2062
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/events', b: 'type=ui.check.event' }); },
2063
+ check: async ({ get }) => {
2064
+ const rows = ((await get('/v1/events')).data as Body[]) as unknown as StripeRow[];
2065
+ if (!rows.length) return false;
2066
+ const markup = renderSectionList('events', rows);
2067
+ return listRows(markup) === rows.length && rows.some((e) => e.type === 'ui.check.event') && markup.includes('ui.check.event');
2068
+ },
2069
+ })),
2070
+ // TWIN-14 (B4) migration — was a marker-grep over the literals 'Credit Notes', 'Customer Tax
2071
+ // IDs', 'Customer Balance'; now data-coupled: seed a credit note, a customer tax id, and a
2072
+ // customer balance transaction, each with a distinct value → fetch the SAME `/v1/credit_notes`
2073
+ // + `/v1/tax_ids` + `/v1/customer_balance_transactions` projections those screens read →
2074
+ // render the mirror's OWN ListPane over each → assert every seeded value survives.
2075
+ done('stripe.ui.billing_credit', 'ui-dashboard', 'Dashboard: Credit Notes + Customer Tax IDs + Customer Balance screens (data-coupled)', 'ui', 'common', uiDataCoupled({
2076
+ markers: ['Credit Notes', 'Customer Tax IDs', 'Customer Balance', 'list-row'],
2077
+ seed: async (h) => {
2078
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=billing-credit-check@twin.test' });
2079
+ const custId = (cust.body as Body).id;
2080
+ const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${custId}` });
2081
+ await h({ m: 'POST', p: '/v1/credit_notes', b: `invoice=${(inv.body as Body).id}&amount=2500&reason=order_change` });
2082
+ await h({ m: 'POST', p: `/v1/customers/${custId}/tax_ids`, b: 'type=eu_vat&value=DE999999999' });
2083
+ await h({ m: 'POST', p: `/v1/customers/${custId}/balance_transactions`, b: 'amount=-750&currency=usd' });
2084
+ },
2085
+ check: async ({ get }) => {
2086
+ const notes = ((await get('/v1/credit_notes')).data as Body[]) as unknown as StripeRow[];
2087
+ const taxIds = ((await get('/v1/tax_ids')).data as Body[]) as unknown as StripeRow[];
2088
+ const custBal = ((await get('/v1/customer_balance_transactions')).data as Body[]) as unknown as StripeRow[];
2089
+ if (!notes.length || !taxIds.length || !custBal.length) return false;
2090
+ const notesMarkup = renderSectionList('credit_notes', notes);
2091
+ const taxMarkup = renderSectionList('tax_ids', taxIds);
2092
+ const balMarkup = renderSectionList('customer_balance_transactions', custBal);
2093
+ return listRows(notesMarkup) === notes.length && notes.some((n) => n.amount === 2500) &&
2094
+ listRows(taxMarkup) === taxIds.length && taxIds.some((t) => t.value === 'DE999999999') &&
2095
+ listRows(balMarkup) === custBal.length && custBal.some((b) => b.amount === -750);
2096
+ },
2097
+ })),
2098
+ // TWIN-14 (B4) migration — was a marker-grep over the literal 'Payment error'; now
2099
+ // data-coupled: seed a PaymentIntent confirmed with a REAL declining test card (a genuine
2100
+ // 402 + populated `last_payment_error`, not a fabricated fixture) → fetch the SAME
2101
+ // `/v1/payment_intents` projection the detail screen reads → render the mirror's OWN
2102
+ // `PaymentErrorBanner` over the declined row's `last_payment_error` → assert the seeded
2103
+ // decline code/reason survive. Failable: a succeeded (non-declined) PI has no
2104
+ // `last_payment_error` ⇒ the banner renders nothing.
2105
+ done('stripe.ui.payment_error', 'ui-dashboard', 'Dashboard: surfaces payment errors on detail (data-coupled)', 'ui', 'common', uiDataCoupled({
2106
+ markers: ['Payment error', 'pay-error'],
2107
+ seed: async (h) => {
2108
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=3300&currency=usd' });
2109
+ await h({ m: 'POST', p: `/v1/payment_intents/${(pi.body as Body).id}/confirm`, b: 'payment_method=pm_card_chargeDeclinedInsufficientFunds' });
2110
+ },
2111
+ check: async ({ get }) => {
2112
+ const rows = ((await get('/v1/payment_intents')).data as Body[]) as unknown as StripeRow[];
2113
+ const declined = rows.find((p) => p.amount === 3300);
2114
+ if (!declined || !declined.last_payment_error) return false;
2115
+ const markup = renderToStaticMarkup(createElement(PaymentErrorBanner, { error: declined.last_payment_error }));
2116
+ return markup.includes('Payment error') && markup.includes('card_declined') && markup.includes('insufficient_funds');
2117
+ },
2118
+ })),
2119
+ // Global search / command bar — data-coupled: the command-bar markers are bundled AND a
2120
+ // seeded customer is findable via the SAME /v1/customers projection the search reads (the
2121
+ // search filters cached collection data client-side; we assert the data source carries the
2122
+ // seeded row so the search has real state to match).
2123
+ done('stripe.ui.search', 'ui-dashboard', 'Dashboard: global search / command bar', 'ui', 'common', uiDataCoupled({
2124
+ markers: ['cmdk', 'cmdk-input', 'cmdk-hit', 'globalSearch'],
2125
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/customers', b: 'email=searchme@twin.test&name=Searchable' }); },
2126
+ check: async ({ get }) => {
2127
+ const list = await get('/v1/customers');
2128
+ const rows = (list.data as Body[]) ?? [];
2129
+ return rows.some((c) => c.email === 'searchme@twin.test');
2130
+ },
2131
+ })),
2132
+ // Home / overview — data-coupled: the metric-card markers are bundled AND the metrics the
2133
+ // Home screen computes are derived from the SAME projections it reads (a seeded succeeded
2134
+ // payment shows up in /v1/payment_intents so gross volume is non-zero).
2135
+ done('stripe.ui.home', 'ui-dashboard', 'Dashboard: Home / overview metrics', 'ui', 'common', uiDataCoupled({
2136
+ markers: ['home-screen', 'metric-card', 'data-metric', 'Gross volume'],
2137
+ seed: async (h) => {
2138
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=4200&currency=usd' });
2139
+ await h({ m: 'POST', p: `/v1/payment_intents/${(pi.body as Body).id}/confirm`, b: 'payment_method=pm_card_visa' });
2140
+ },
2141
+ check: async ({ get }) => {
2142
+ const pis = ((await get('/v1/payment_intents')).data as Body[]) ?? [];
2143
+ const succeeded = pis.filter((p) => p.status === 'succeeded');
2144
+ return succeeded.length >= 1 && succeeded.some((p) => Number(p.amount_received ?? p.amount) === 4200);
2145
+ },
2146
+ })),
2147
+ // Reports screen — data-coupled: the Reports section markers are bundled AND the screen's
2148
+ // data source (GET /v1/reporting/report_runs) reflects a seeded report run; we render the
2149
+ // mirror's OWN ListPane over the rows the running server returns and assert one list-row
2150
+ // per seeded run carrying its report_type (fails on an empty workspace).
2151
+ done('stripe.ui.reports', 'ui-dashboard', 'Dashboard: Reports / analytics screen', 'ui', 'niche', uiDataCoupled({
2152
+ markers: ['Reports', 'reporting/report_runs', 'list-row'],
2153
+ seed: async (h) => { await h({ m: 'POST', p: '/v1/reporting/report_runs', b: 'parameters[report_type]=balance.summary.1' }); },
2154
+ check: async ({ get }) => {
2155
+ const rows = ((await get('/v1/reporting/report_runs')).data as Body[]) as unknown as StripeRow[];
2156
+ if (!rows || rows.length < 1) return false;
2157
+ const markup = renderSectionList('reporting/report_runs', rows);
2158
+ return listRows(markup) === rows.length && markup.includes('balance.summary.1') && rows[0]!.status === 'succeeded';
2159
+ },
2160
+ })),
2161
+ // Settings — data-coupled: the settings-block markers (account/api-keys/webhooks) are bundled
2162
+ // AND the screen's data sources reflect seeded twin state: the platform account's payout
2163
+ // schedule (GET /v1/account) and a registered webhook endpoint (GET /v1/webhook_endpoints).
2164
+ done('stripe.ui.settings', 'ui-dashboard', 'Dashboard: Settings (account/team/api keys/webhooks)', 'ui', 'common', uiDataCoupled({
2165
+ markers: ['settings-block', 'data-settings', 'API keys', 'settings-payout-schedule', 'payoutScheduleText'],
2166
+ seed: async (h) => {
2167
+ await h({ m: 'POST', p: '/v1/account', b: 'settings[payouts][schedule][interval]=weekly&settings[payouts][schedule][weekly_anchor]=monday' });
2168
+ await h({ m: 'POST', p: '/v1/webhook_endpoints', b: 'url=https://twin.test/hook&enabled_events[]=charge.succeeded' });
2169
+ },
2170
+ check: async ({ get }) => {
2171
+ const acct = await get('/v1/account');
2172
+ const sched = (((acct.settings as Body)?.payouts as Body)?.schedule as Body);
2173
+ const hooks = ((await get('/v1/webhook_endpoints')).data as Body[]) ?? [];
2174
+ return sched?.interval === 'weekly' && hooks.some((w) => w.url === 'https://twin.test/hook');
2175
+ },
2176
+ })),
2177
+ // Create/edit modals (write forms) — data-coupled: the create-modal markers + per-collection
2178
+ // forms are bundled AND the form POSTs the SAME /v1/<collection> create endpoint the SDK
2179
+ // uses; we exercise that real endpoint through the mirror server and assert the row appears
2180
+ // in the projection the list re-reads (the round-trip the modal drives).
2181
+ done('stripe.ui.create_modals', 'ui-dashboard', 'Dashboard: create/edit modals (write forms)', 'ui', 'common', uiDataCoupled({
2182
+ markers: ['create-modal', 'create-field', 'create-submit', 'CREATE_FORMS'],
2183
+ seed: async () => { /* the create flow IS the write; nothing to pre-seed */ },
2184
+ check: async ({ get, post }) => {
2185
+ // drive the exact request the product create modal issues (POST /v1/products) through
2186
+ // the running mirror server, then assert the new row appears in the list re-read.
2187
+ const created = await post('/v1/products', 'name=Modal Product&description=via form');
2188
+ if (created.object !== 'product' || created.name !== 'Modal Product') return false;
2189
+ const list = ((await get('/v1/products')).data as Body[]) ?? [];
2190
+ return list.some((p) => p.id === created.id && p.name === 'Modal Product');
2191
+ },
2192
+ })),
2193
+ // Terminal / Issuing / Tax / Radar screens — data-coupled: the section markers are bundled
2194
+ // (Tax + Radar screens already existed; this adds Terminal + Issuing) AND the screens' data
2195
+ // sources reflect seeded twin state. We seed a terminal location + reader and an issuing
2196
+ // cardholder + card, then render the mirror's OWN ListPane over the rows the running server
2197
+ // returns and assert one list-row per seeded object (fails on an empty workspace).
2198
+ done('stripe.ui.terminal', 'ui-dashboard', 'Dashboard: Terminal / Issuing / Tax / Radar screens', 'ui', 'niche', uiDataCoupled({
2199
+ markers: ['Terminal Readers', 'Terminal Locations', 'Issuing Cards', 'Issuing Cardholders', 'Tax Rates', 'Radar Reviews', 'list-row'],
2200
+ seed: async (h) => {
2201
+ const loc = await h({ m: 'POST', p: '/v1/terminal/locations', b: 'display_name=Front Desk&address[line1]=1 A St&address[city]=SF&address[country]=US&address[postal_code]=94103&address[state]=CA' });
2202
+ await h({ m: 'POST', p: '/v1/terminal/readers', b: `registration_code=puppies-plug-could&label=Lane 1&location=${(loc.body as Body).id}` });
2203
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jenny Rosen&type=individual&billing[address][line1]=1 A St&billing[address][city]=SF&billing[address][country]=US&billing[address][postal_code]=94103&billing[address][state]=CA' });
2204
+ await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${(ch.body as Body).id}&currency=usd&type=virtual` });
2205
+ },
2206
+ check: async ({ get }) => {
2207
+ const readers = ((await get('/v1/terminal/readers')).data as Body[]) as unknown as StripeRow[];
2208
+ const cards = ((await get('/v1/issuing/cards')).data as Body[]) as unknown as StripeRow[];
2209
+ if (!readers?.length || !cards?.length) return false;
2210
+ const readerMarkup = renderSectionList('terminal/readers', readers);
2211
+ const cardMarkup = renderSectionList('issuing/cards', cards);
2212
+ return listRows(readerMarkup) === readers.length && readerMarkup.includes('Lane 1') &&
2213
+ listRows(cardMarkup) === cards.length && Boolean(cards[0]!.last4);
2214
+ },
2215
+ })),
2216
+
2217
+ // ════════════════════════════════════════════════════════════════════════════════════
2218
+ // AUDIT GROWTH (this cycle) — the manifest above under-enumerated the REAL Stripe surface.
2219
+ // The entries below grow the denominator toward the full API; the NEWLY-BUILT ones carry a
2220
+ // failable verify(); the rest are honest todos. Coverage % DROPS as a result — expected.
2221
+ // ════════════════════════════════════════════════════════════════════════════════════
2222
+
2223
+ // ── SetupIntents full lifecycle (only confirm was tracked before) ──
2224
+ done('stripe.setup_intents.lifecycle', 'payment_methods', 'SetupIntents: create / retrieve / list / cancel', 'api', 'common', () =>
2225
+ withRoot(async (h) => {
2226
+ const c = await h({ m: 'POST', p: '/v1/setup_intents', b: 'usage=off_session' });
2227
+ if (!ok(c) || field(c, 'status') !== 'requires_confirmation') return false;
2228
+ const g = await h({ m: 'GET', p: `/v1/setup_intents/${id(c)}` });
2229
+ const l = await h({ m: 'GET', p: '/v1/setup_intents' });
2230
+ const x = await h({ m: 'POST', p: `/v1/setup_intents/${id(c)}/cancel` });
2231
+ // cancelling a succeeded SI is rejected (negative path)
2232
+ const conf = await h({ m: 'POST', p: '/v1/setup_intents', b: 'usage=off_session' });
2233
+ await h({ m: 'POST', p: `/v1/setup_intents/${id(conf)}/confirm` });
2234
+ const badCancel = await h({ m: 'POST', p: `/v1/setup_intents/${id(conf)}/cancel` });
2235
+ return ok(g) && id(g) === id(c) && field(l, 'object') === 'list'
2236
+ && ok(x) && field(x, 'status') === 'canceled' && badCancel.status === 400;
2237
+ }),
2238
+ ),
2239
+
2240
+ // ── Tokens API (legacy/older-integration tokenization) ──
2241
+ done('stripe.tokens.create', 'payment_methods', 'Tokens: create card/bank_account + retrieve', 'api', 'common', () =>
2242
+ withRoot(async (h) => {
2243
+ const cardTok = await h({ m: 'POST', p: '/v1/tokens', b: 'card[number]=4242424242424242&card[exp_month]=12&card[exp_year]=2034&card[cvc]=123' });
2244
+ if (!ok(cardTok) || field(cardTok, 'type') !== 'card') return false;
2245
+ const card = (cardTok.body as Body).card as Body;
2246
+ if (!card || card.last4 !== '4242') return false;
2247
+ const baTok = await h({ m: 'POST', p: '/v1/tokens', b: 'bank_account[country]=US&bank_account[account_number]=000123456789&bank_account[routing_number]=110000000' });
2248
+ if (!ok(baTok) || field(baTok, 'type') !== 'bank_account') return false;
2249
+ const g = await h({ m: 'GET', p: `/v1/tokens/${id(cardTok)}` });
2250
+ // missing instrument → 400
2251
+ const bad = await h({ m: 'POST', p: '/v1/tokens', b: '' });
2252
+ return ok(g) && id(g) === id(cardTok) && bad.status === 400;
2253
+ }),
2254
+ ),
2255
+
2256
+ // ── Mandates retrieve ──
2257
+ done('stripe.mandates.retrieve', 'payment_methods', 'Mandates: retrieve (404 unknown)', 'api', 'niche', () =>
2258
+ withRoot(async (h) => {
2259
+ const m = await h({ m: 'POST', p: '/v1/mandates', b: 'payment_method=pm_card_visa' });
2260
+ if (!ok(m) || field(m, 'object') !== 'mandate') return false;
2261
+ const g = await h({ m: 'GET', p: `/v1/mandates/${id(m)}` });
2262
+ const miss = await h({ m: 'GET', p: '/v1/mandates/mandate_nope' });
2263
+ return ok(g) && id(g) === id(m) && field(g, 'status') === 'active' && miss.status === 404;
2264
+ }),
2265
+ ),
2266
+
2267
+ // ── Refund cancel ──
2268
+ done('stripe.refunds.cancel', 'refunds', 'Refunds: cancel a pending refund (400 if not pending)', 'api', 'niche', () =>
2269
+ withRoot(async (h) => {
2270
+ const pending = await h({ m: 'POST', p: '/v1/refunds', b: 'amount=500&currency=usd&status=pending' });
2271
+ if (!ok(pending)) return false;
2272
+ const x = await h({ m: 'POST', p: `/v1/refunds/${id(pending)}/cancel` });
2273
+ const succeeded = await h({ m: 'POST', p: '/v1/refunds', b: 'amount=500&currency=usd' });
2274
+ const bad = await h({ m: 'POST', p: `/v1/refunds/${id(succeeded)}/cancel` });
2275
+ const miss = await h({ m: 'POST', p: '/v1/refunds/re_nope/cancel' });
2276
+ return ok(x) && field(x, 'status') === 'canceled' && bad.status === 400 && miss.status === 404;
2277
+ }),
2278
+ ),
2279
+
2280
+ // ── PaymentIntents increment authorization (manual-capture extended auth) ──
2281
+ done('stripe.payment_intents.increment_auth', 'payment_intents', 'PaymentIntents: increment_authorization', 'api', 'niche', () =>
2282
+ withRoot(async (h) => {
2283
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=1000&currency=usd&capture_method=manual&payment_method=pm_card_visa' });
2284
+ if (!ok(pi)) return false;
2285
+ await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm` });
2286
+ const inc = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/increment_authorization`, b: 'amount=1500' });
2287
+ // must be greater than current → reject lowering
2288
+ const bad = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/increment_authorization`, b: 'amount=1000' });
2289
+ return ok(inc) && field(inc, 'amount') === 1500 && field(inc, 'amount_capturable') === 1500 && field(inc, 'status') === 'requires_capture' && bad.status === 400;
2290
+ }),
2291
+ ),
2292
+
2293
+ // ── Connect: account links (hosted onboarding) ──
2294
+ done('stripe.connect.account_links', 'connect', 'Connect: account_links (onboarding URL)', 'api', 'niche', () =>
2295
+ withRoot(async (h) => {
2296
+ const acct = await h({ m: 'POST', p: '/v1/accounts', b: 'type=express' });
2297
+ if (!ok(acct)) return false;
2298
+ const link = await h({ m: 'POST', p: '/v1/account_links', b: `account=${id(acct)}&type=account_onboarding&refresh_url=https://x&return_url=https://y` });
2299
+ const badType = await h({ m: 'POST', p: '/v1/account_links', b: `account=${id(acct)}&type=bogus` });
2300
+ const noAcct = await h({ m: 'POST', p: '/v1/account_links', b: 'account=acct_nope&type=account_onboarding' });
2301
+ return ok(link) && field(link, 'object') === 'account_link' && typeof field(link, 'url') === 'string' && badType.status === 400 && noAcct.status === 400;
2302
+ }),
2303
+ ),
2304
+
2305
+ // ── Connect: persons (beneficial owners) ──
2306
+ done('stripe.connect.persons', 'connect', 'Connect: persons CRUD on a connected account', 'api', 'niche', () =>
2307
+ withRoot(async (h) => {
2308
+ const acct = await h({ m: 'POST', p: '/v1/accounts', b: 'type=custom' });
2309
+ if (!ok(acct)) return false;
2310
+ const p = await h({ m: 'POST', p: `/v1/accounts/${id(acct)}/persons`, b: 'first_name=Ada&last_name=Lovelace' });
2311
+ if (!ok(p) || field(p, 'object') !== 'person') return false;
2312
+ const g = await h({ m: 'GET', p: `/v1/accounts/${id(acct)}/persons/${id(p)}` });
2313
+ const l = await h({ m: 'GET', p: `/v1/accounts/${id(acct)}/persons` });
2314
+ const u = await h({ m: 'POST', p: `/v1/accounts/${id(acct)}/persons/${id(p)}`, b: 'last_name=Byron' });
2315
+ const d = await h({ m: 'DELETE', p: `/v1/accounts/${id(acct)}/persons/${id(p)}` });
2316
+ // unknown account / unknown person fail like Stripe (404) — makes this verify failable.
2317
+ const badAcct = await h({ m: 'POST', p: `/v1/accounts/acct_nope/persons`, b: 'first_name=X' });
2318
+ const badPerson = await h({ m: 'GET', p: `/v1/accounts/${id(acct)}/persons/person_nope` });
2319
+ return ok(g) && id(g) === id(p) && field(l, 'object') === 'list' && ok(u) && field(u, 'last_name') === 'Byron' && ok(d) && field(d, 'deleted') === true
2320
+ && badAcct.status === 404 && badPerson.status === 404;
2321
+ }),
2322
+ ),
2323
+
2324
+ // ── Connect: external accounts (payout destinations) ──
2325
+ done('stripe.connect.external_accounts', 'connect', 'Connect: external_accounts (bank accounts)', 'api', 'niche', () =>
2326
+ withRoot(async (h) => {
2327
+ const acct = await h({ m: 'POST', p: '/v1/accounts', b: 'type=custom' });
2328
+ if (!ok(acct)) return false;
2329
+ const ba = await h({ m: 'POST', p: `/v1/accounts/${id(acct)}/external_accounts`, b: 'external_account[country]=US&external_account[account_number]=000123456789&external_account[routing_number]=110000000' });
2330
+ if (!ok(ba) || field(ba, 'object') !== 'bank_account' || field(ba, 'last4') !== '6789') return false;
2331
+ const g = await h({ m: 'GET', p: `/v1/accounts/${id(acct)}/external_accounts/${id(ba)}` });
2332
+ const l = await h({ m: 'GET', p: `/v1/accounts/${id(acct)}/external_accounts` });
2333
+ const d = await h({ m: 'DELETE', p: `/v1/accounts/${id(acct)}/external_accounts/${id(ba)}` });
2334
+ const miss = await h({ m: 'POST', p: `/v1/accounts/${id(acct)}/external_accounts`, b: '' });
2335
+ // unknown account fails like Stripe (404) — makes this verify failable.
2336
+ const badAcct = await h({ m: 'POST', p: `/v1/accounts/acct_nope/external_accounts`, b: 'external_account[country]=US&external_account[account_number]=000123456789&external_account[routing_number]=110000000' });
2337
+ return ok(g) && id(g) === id(ba) && field(l, 'object') === 'list' && ok(d) && field(d, 'deleted') === true && miss.status === 400
2338
+ && badAcct.status === 404;
2339
+ }),
2340
+ ),
2341
+
2342
+ // ── Connect: account capabilities ──
2343
+ done('stripe.connect.capabilities', 'connect', 'Connect: account capabilities (list/retrieve/request)', 'api', 'niche', () =>
2344
+ withRoot(async (h) => {
2345
+ const acct = await h({ m: 'POST', p: '/v1/accounts', b: 'type=custom&capabilities[card_payments][requested]=true&capabilities[transfers][requested]=true' });
2346
+ if (!ok(acct)) return false;
2347
+ const l = await h({ m: 'GET', p: `/v1/accounts/${id(acct)}/capabilities` });
2348
+ if (field(l, 'object') !== 'list') return false;
2349
+ const g = await h({ m: 'GET', p: `/v1/accounts/${id(acct)}/capabilities/card_payments` });
2350
+ const req = await h({ m: 'POST', p: `/v1/accounts/${id(acct)}/capabilities/card_payments`, b: 'requested=true' });
2351
+ const miss = await h({ m: 'GET', p: `/v1/accounts/${id(acct)}/capabilities/bogus` });
2352
+ return ok(g) && field(g, 'object') === 'capability' && ok(req) && field(req, 'status') === 'pending' && miss.status === 404;
2353
+ }),
2354
+ ),
2355
+
2356
+ // ── Connect: transfer reversals ──
2357
+ done('stripe.connect.transfer_reversals', 'connect', 'Connect: transfer reversals (partial + full)', 'api', 'niche', () =>
2358
+ withRoot(async (h) => {
2359
+ const acct = await h({ m: 'POST', p: '/v1/accounts', b: 'type=standard' });
2360
+ const tr = await h({ m: 'POST', p: '/v1/transfers', b: `amount=1000&currency=usd&destination=${id(acct)}` });
2361
+ if (!ok(tr)) return false;
2362
+ const rev1 = await h({ m: 'POST', p: `/v1/transfers/${id(tr)}/reversals`, b: 'amount=400' });
2363
+ if (!ok(rev1) || field(rev1, 'object') !== 'transfer_reversal') return false;
2364
+ const mid = await h({ m: 'GET', p: `/v1/transfers/${id(tr)}` });
2365
+ if (field(mid, 'amount_reversed') !== 400 || field(mid, 'reversed') !== false) return false;
2366
+ const rev2 = await h({ m: 'POST', p: `/v1/transfers/${id(tr)}/reversals`, b: 'amount=600' });
2367
+ const done2 = await h({ m: 'GET', p: `/v1/transfers/${id(tr)}` });
2368
+ const tooMuch = await h({ m: 'POST', p: `/v1/transfers/${id(tr)}/reversals`, b: 'amount=100' });
2369
+ const l = await h({ m: 'GET', p: `/v1/transfers/${id(tr)}/reversals` });
2370
+ return ok(rev2) && field(done2, 'reversed') === true && field(done2, 'amount_reversed') === 1000 && tooMuch.status === 400 && field(l, 'object') === 'list';
2371
+ }),
2372
+ ),
2373
+
2374
+ // ── Connect: application fees + refunds ──
2375
+ done('stripe.connect.application_fees', 'connect', 'Connect: application fees + fee refunds', 'api', 'niche', () =>
2376
+ withRoot(async (h) => {
2377
+ const fee = await h({ m: 'POST', p: '/v1/application_fees', b: 'amount=1000&currency=usd&charge=ch_twin_1&account=acct_1' });
2378
+ if (!ok(fee) || field(fee, 'object') !== 'application_fee') return false;
2379
+ const g = await h({ m: 'GET', p: `/v1/application_fees/${id(fee)}` });
2380
+ const l = await h({ m: 'GET', p: '/v1/application_fees' });
2381
+ const ref = await h({ m: 'POST', p: `/v1/application_fees/${id(fee)}/refunds`, b: 'amount=400' });
2382
+ const after = await h({ m: 'GET', p: `/v1/application_fees/${id(fee)}` });
2383
+ const tooMuch = await h({ m: 'POST', p: `/v1/application_fees/${id(fee)}/refunds`, b: 'amount=700' });
2384
+ return ok(g) && field(l, 'object') === 'list' && ok(ref) && field(ref, 'object') === 'fee_refund' && field(after, 'amount_refunded') === 400 && tooMuch.status === 400;
2385
+ }),
2386
+ ),
2387
+
2388
+ // ── Files (uploads) + retrieve ──
2389
+ done('stripe.files.upload', 'files', 'Files: upload (purpose required) + retrieve/list', 'api', 'niche', () =>
2390
+ withRoot(async (h) => {
2391
+ const f = await h({ m: 'POST', p: '/v1/files', b: 'purpose=dispute_evidence&filename=evidence.png' });
2392
+ if (!ok(f) || field(f, 'object') !== 'file' || field(f, 'purpose') !== 'dispute_evidence') return false;
2393
+ const g = await h({ m: 'GET', p: `/v1/files/${id(f)}` });
2394
+ const l = await h({ m: 'GET', p: '/v1/files' });
2395
+ const bad = await h({ m: 'POST', p: '/v1/files', b: 'filename=x.png' });
2396
+ return ok(g) && id(g) === id(f) && field(l, 'object') === 'list' && bad.status === 400;
2397
+ }),
2398
+ ),
2399
+
2400
+ // ── HONEST TODOS: the rest of the real Stripe surface, tiered honestly. ──
2401
+ // Connect (remaining)
2402
+ // Connect account sessions: POST /v1/account_sessions mints a client_secret for embedded
2403
+ // components. Requires account (must exist, else 400) + a non-empty components map (else
2404
+ // 400); each requested component is normalized to {enabled, features}. (the client_secret +
2405
+ // normalized components are produced ONLY by this feature.)
2406
+ done('stripe.connect.account_sessions', 'connect', 'Connect: account_sessions (embedded components)', 'api', 'niche', () =>
2407
+ withRoot(async (h) => {
2408
+ const acct = await h({ m: 'POST', p: '/v1/accounts', b: 'type=express&country=US&email=conn@twin.test' });
2409
+ const s = await h({ m: 'POST', p: '/v1/account_sessions', b: `account=${id(acct)}&components[payments][enabled]=true&components[payouts][enabled]=false` });
2410
+ if (!ok(s) || field(s, 'object') !== 'account_session' || typeof field(s, 'client_secret') !== 'string') return false;
2411
+ const comps = field(s, 'components') as Body;
2412
+ if ((comps.payments as Body).enabled !== true || (comps.payouts as Body).enabled !== false) return false;
2413
+ if (field(s, 'account') !== id(acct) || typeof field(s, 'expires_at') !== 'number') return false;
2414
+ const noAcct = await h({ m: 'POST', p: '/v1/account_sessions', b: 'components[payments][enabled]=true' });
2415
+ const badAcct = await h({ m: 'POST', p: '/v1/account_sessions', b: 'account=acct_nope&components[payments][enabled]=true' });
2416
+ const noComps = await h({ m: 'POST', p: '/v1/account_sessions', b: `account=${id(acct)}` });
2417
+ return noAcct.status === 400 && badAcct.status === 400 && noComps.status === 400;
2418
+ }),
2419
+ ),
2420
+ // Apps secret store: POST /v1/apps/secrets sets a secret (requires name + scope[type] +
2421
+ // payload). find/list are scope-scoped; re-setting the same name+scope OVERWRITES (no
2422
+ // duplicate); /delete deletes it. A user-scope secret requires scope[user]. (the
2423
+ // scoped overwrite + find/delete are produced ONLY by this feature.)
2424
+ done('stripe.connect.secrets', 'connect', 'Connect: apps secret store', 'api', 'niche', () =>
2425
+ withRoot(async (h) => {
2426
+ const set = await h({ m: 'POST', p: '/v1/apps/secrets', b: 'name=api_key&scope[type]=account&payload=sk_secret_1' });
2427
+ if (!ok(set) || field(set, 'object') !== 'apps.secret' || field(set, 'payload') !== 'sk_secret_1') return false;
2428
+ const find = await h({ m: 'GET', p: '/v1/apps/secrets/find?name=api_key&scope[type]=account' });
2429
+ if (!ok(find) || field(find, 'payload') !== 'sk_secret_1') return false;
2430
+ // overwrite: same name+scope updates in place (no duplicate in the list).
2431
+ const set2 = await h({ m: 'POST', p: '/v1/apps/secrets', b: 'name=api_key&scope[type]=account&payload=sk_secret_2' });
2432
+ const find2 = await h({ m: 'GET', p: '/v1/apps/secrets/find?name=api_key&scope[type]=account' });
2433
+ const list = await h({ m: 'GET', p: '/v1/apps/secrets?scope[type]=account' });
2434
+ if (!ok(set2) || field(find2, 'payload') !== 'sk_secret_2' || ((list.body as Body).data as Body[]).length !== 1) return false;
2435
+ // delete → find 404s.
2436
+ const del = await h({ m: 'POST', p: '/v1/apps/secrets/delete', b: 'name=api_key&scope[type]=account' });
2437
+ const findGone = await h({ m: 'GET', p: '/v1/apps/secrets/find?name=api_key&scope[type]=account' });
2438
+ if (!ok(del) || field(del, 'deleted') !== true || findGone.status !== 404) return false;
2439
+ // vendor errors.
2440
+ const noName = await h({ m: 'POST', p: '/v1/apps/secrets', b: 'scope[type]=account&payload=x' });
2441
+ const noPayload = await h({ m: 'POST', p: '/v1/apps/secrets', b: 'name=k&scope[type]=account' });
2442
+ const noUser = await h({ m: 'POST', p: '/v1/apps/secrets', b: 'name=k&scope[type]=user&payload=x' });
2443
+ return noName.status === 400 && noPayload.status === 400 && noUser.status === 400;
2444
+ }),
2445
+ ),
2446
+ // PaymentIntents / Charges (remaining)
2447
+ // PaymentIntents apply_customer_balance: reconcile a customer_balance (bank-transfer) PI
2448
+ // against the customer's funded cash balance. With sufficient funds → succeeded
2449
+ // (amount_received set); with insufficient funds → stays requires_action with a
2450
+ // display_bank_transfer_instructions next_action carrying amount_remaining. A succeeded PI
2451
+ // 400s; unknown id 404. (the balance-reconciliation transition is produced ONLY here.)
2452
+ done('stripe.payment_intents.apply_customer_balance', 'payment_intents', 'PaymentIntents: apply_customer_balance', 'api', 'niche', () =>
2453
+ withRoot(async (h) => {
2454
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=acb@twin.test' });
2455
+ // partial funds first.
2456
+ await h({ m: 'POST', p: `/v1/customers/${id(cust)}/cash_balance_transactions`, b: 'amount=600&currency=usd' });
2457
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: `amount=1000&currency=usd&customer=${id(cust)}&payment_method_types[]=customer_balance` });
2458
+ if (!ok(pi)) return false;
2459
+ const partial = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/apply_customer_balance` });
2460
+ if (!ok(partial) || field(partial, 'status') !== 'requires_action') return false;
2461
+ const na = field(partial, 'next_action') as Body;
2462
+ if (na?.type !== 'display_bank_transfer_instructions' || (na.display_bank_transfer_instructions as Body).amount_remaining !== 400) return false;
2463
+ // top up the rest → succeeds.
2464
+ await h({ m: 'POST', p: `/v1/customers/${id(cust)}/cash_balance_transactions`, b: 'amount=400&currency=usd' });
2465
+ const full = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/apply_customer_balance` });
2466
+ if (!ok(full) || field(full, 'status') !== 'succeeded' || field(full, 'amount_received') !== 1000) return false;
2467
+ // a succeeded PI 400s; unknown id 404.
2468
+ const again = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/apply_customer_balance` });
2469
+ const missing = await h({ m: 'POST', p: '/v1/payment_intents/pi_nope/apply_customer_balance' });
2470
+ return again.status === 400 && missing.status === 404;
2471
+ }),
2472
+ ),
2473
+ // ACH/SEPA micro-deposit verification: confirming a PI with a us_bank_account PM that needs
2474
+ // verification leaves it in requires_action with a verify_with_microdeposits next_action (NOT
2475
+ // succeeded); /verify_microdeposits with the WRONG amounts 400s, the RIGHT amounts (32,45) →
2476
+ // succeeded. Missing/both inputs 400; unknown id 404. (the requires_action microdeposit state
2477
+ // + the amount-match check are produced ONLY by this feature.)
2478
+ done('stripe.payment_intents.verify_microdeposits', 'payment_intents', 'PaymentIntents: verify_microdeposits (ACH)', 'api', 'common', () =>
2479
+ withRoot(async (h) => {
2480
+ const pi = await h({ m: 'POST', p: '/v1/payment_intents', b: 'amount=5000&currency=usd&payment_method_types[]=us_bank_account' });
2481
+ if (!ok(pi)) return false;
2482
+ const conf = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm`, b: 'payment_method=pm_us_bank_account' });
2483
+ if (!ok(conf) || field(conf, 'status') !== 'requires_action') return false;
2484
+ if ((field(conf, 'next_action') as Body)?.type !== 'verify_with_microdeposits') return false;
2485
+ // wrong amounts → 400 mismatch; still requires_action afterwards.
2486
+ const bad = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/verify_microdeposits`, b: 'amounts[]=1&amounts[]=2' });
2487
+ if (bad.status !== 400) return false;
2488
+ const both = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/verify_microdeposits`, b: 'amounts[]=32&amounts[]=45&descriptor_code=SM11AA' });
2489
+ const none = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/verify_microdeposits` });
2490
+ if (both.status !== 400 || none.status !== 400) return false;
2491
+ // correct amounts → succeeded, next_action cleared.
2492
+ const good = await h({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/verify_microdeposits`, b: 'amounts[]=32&amounts[]=45' });
2493
+ const missing = await h({ m: 'POST', p: '/v1/payment_intents/pi_nope/verify_microdeposits', b: 'amounts[]=32&amounts[]=45' });
2494
+ return ok(good) && field(good, 'status') === 'succeeded' && field(good, 'next_action') === null && missing.status === 404;
2495
+ }),
2496
+ ),
2497
+ // SetupIntent micro-deposit verification: same flow for saving an ACH bank account — confirm
2498
+ // with the verification PM → requires_action; descriptor_code SM11AA verifies → succeeded; a
2499
+ // wrong descriptor 400s. (the requires_action microdeposit state is produced ONLY by this feature.)
2500
+ done('stripe.setup_intents.verify_microdeposits', 'payment_methods', 'SetupIntents: verify_microdeposits (ACH)', 'api', 'common', () =>
2501
+ withRoot(async (h) => {
2502
+ const si = await h({ m: 'POST', p: '/v1/setup_intents', b: 'payment_method_types[]=us_bank_account' });
2503
+ if (!ok(si)) return false;
2504
+ const conf = await h({ m: 'POST', p: `/v1/setup_intents/${id(si)}/confirm`, b: 'payment_method=pm_us_bank_account' });
2505
+ if (!ok(conf) || field(conf, 'status') !== 'requires_action' || (field(conf, 'next_action') as Body)?.type !== 'verify_with_microdeposits') return false;
2506
+ const bad = await h({ m: 'POST', p: `/v1/setup_intents/${id(si)}/verify_microdeposits`, b: 'descriptor_code=WRONG1' });
2507
+ if (bad.status !== 400) return false;
2508
+ const good = await h({ m: 'POST', p: `/v1/setup_intents/${id(si)}/verify_microdeposits`, b: 'descriptor_code=SM11AA' });
2509
+ const missing = await h({ m: 'POST', p: '/v1/setup_intents/seti_nope/verify_microdeposits', b: 'descriptor_code=SM11AA' });
2510
+ return ok(good) && field(good, 'status') === 'succeeded' && missing.status === 404;
2511
+ }),
2512
+ ),
2513
+ // Sources (legacy)
2514
+ // Legacy top-level Sources: POST /v1/sources requires `type`; a card source is chargeable
2515
+ // immediately, a non-instant type is pending. retrieve round-trips the type; update patches
2516
+ // metadata; the source attaches to a customer via the customer-scoped sources endpoint.
2517
+ // Missing type 400; unknown id 404. (the standalone Source object is produced ONLY here.)
2518
+ done('stripe.sources.crud', 'payment_methods', 'Sources: create / retrieve / attach (legacy)', 'api', 'niche', () =>
2519
+ withRoot(async (h) => {
2520
+ const src = await h({ m: 'POST', p: '/v1/sources', b: 'type=card&currency=usd' });
2521
+ if (!ok(src) || field(src, 'object') !== 'source' || field(src, 'type') !== 'card' || field(src, 'status') !== 'chargeable') return false;
2522
+ const pending = await h({ m: 'POST', p: '/v1/sources', b: 'type=ach_credit_transfer&currency=usd' });
2523
+ if (field(pending, 'status') !== 'pending') return false;
2524
+ const get = await h({ m: 'GET', p: `/v1/sources/${id(src)}` });
2525
+ const upd = await h({ m: 'POST', p: `/v1/sources/${id(src)}`, b: 'metadata[label]=primary' });
2526
+ if (!ok(get) || id(get) !== id(src) || !ok(upd) || ((field(upd, 'metadata') as Body)?.label) !== 'primary') return false;
2527
+ // attach to a customer via the customer-scoped sources endpoint.
2528
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=src@twin.test' });
2529
+ const attach = await h({ m: 'POST', p: `/v1/customers/${id(cust)}/sources`, b: 'source=tok_visa' });
2530
+ if (!ok(attach)) return false;
2531
+ const noType = await h({ m: 'POST', p: '/v1/sources', b: 'currency=usd' });
2532
+ const missing = await h({ m: 'GET', p: '/v1/sources/src_nope' });
2533
+ return noType.status === 400 && missing.status === 404;
2534
+ }),
2535
+ ),
2536
+ // Customer.list_payment_methods: GET /v1/customers/:id/payment_methods lists the PMs attached
2537
+ // to the customer (with an optional type filter), and GET …/payment_methods/:pmId retrieves one.
2538
+ // A detached PM is excluded; an unknown customer 404s. (the customer-scoped PM list is produced
2539
+ // ONLY by this feature.)
2540
+ done('stripe.customers.payment_methods', 'customers', 'Customer.list_payment_methods convenience endpoint', 'api', 'common', () =>
2541
+ withRoot(async (h) => {
2542
+ const cus = await h({ m: 'POST', p: '/v1/customers', b: 'email=clpm@twin.test' });
2543
+ const pm = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=4242424242424242' });
2544
+ const detached = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=4242424242424242' });
2545
+ await h({ m: 'POST', p: `/v1/payment_methods/${id(pm)}/attach`, b: `customer=${id(cus)}` });
2546
+ const list = await h({ m: 'GET', p: `/v1/customers/${id(cus)}/payment_methods?type=card` });
2547
+ const data = (list.body as Body).data as Body[];
2548
+ if (!ok(list) || field(list, 'object') !== 'list' || data.length !== 1 || data[0]!.id !== id(pm)) return false;
2549
+ const one = await h({ m: 'GET', p: `/v1/customers/${id(cus)}/payment_methods/${id(pm)}` });
2550
+ // the detached PM is NOT in the customer-scoped list; unknown customer 404s.
2551
+ const detachedInList = data.some((m) => m.id === id(detached));
2552
+ const missing = await h({ m: 'GET', p: '/v1/customers/cus_nope/payment_methods' });
2553
+ return ok(one) && id(one) === id(pm) && !detachedInList && missing.status === 404;
2554
+ }),
2555
+ ),
2556
+ // Subscriptions / Billing (remaining)
2557
+ // SubscriptionItem CRUD: create against a subscription+price (404 unknown sub / 400 unknown
2558
+ // price), retrieve/list (scoped to the sub), update quantity, delete. Each mutation keeps the
2559
+ // parent subscription's items.data list in sync (retrieving the sub reflects it). Missing
2560
+ // subscription/price 400. (the synced sub.items list is produced ONLY by this feature.)
2561
+ done('stripe.subscription_items.crud', 'subscriptions', 'Subscription items: create / update / delete', 'api', 'common', () =>
2562
+ withRoot(async (h) => {
2563
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=si@twin.test' });
2564
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Plan' });
2565
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=1000&currency=usd&recurring[interval]=month&product=${id(prod)}` });
2566
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}` });
2567
+ if (!ok(sub)) return false;
2568
+ const si = await h({ m: 'POST', p: '/v1/subscription_items', b: `subscription=${id(sub)}&price=${id(price)}&quantity=2` });
2569
+ if (!ok(si) || field(si, 'object') !== 'subscription_item' || field(si, 'quantity') !== 2) return false;
2570
+ // the parent subscription's items list now reflects it.
2571
+ const sub1 = await h({ m: 'GET', p: `/v1/subscriptions/${id(sub)}` });
2572
+ const items1 = ((field(sub1, 'items') as Body).data as Body[]) ?? [];
2573
+ if (items1.length !== 1 || items1[0]!.id !== id(si)) return false;
2574
+ const g = await h({ m: 'GET', p: `/v1/subscription_items/${id(si)}` });
2575
+ const l = await h({ m: 'GET', p: `/v1/subscription_items?subscription=${id(sub)}` });
2576
+ const u = await h({ m: 'POST', p: `/v1/subscription_items/${id(si)}`, b: 'quantity=5' });
2577
+ if (!ok(g) || ((l.body as Body).data as Body[]).length !== 1 || !ok(u) || field(u, 'quantity') !== 5) return false;
2578
+ const del = await h({ m: 'DELETE', p: `/v1/subscription_items/${id(si)}` });
2579
+ const gone = await h({ m: 'GET', p: `/v1/subscription_items/${id(si)}` });
2580
+ const sub2 = await h({ m: 'GET', p: `/v1/subscriptions/${id(sub)}` });
2581
+ const items2 = ((field(sub2, 'items') as Body).data as Body[]) ?? [];
2582
+ // vendor errors: unknown sub 404; unknown price 400; missing subscription 400.
2583
+ const badSub = await h({ m: 'POST', p: '/v1/subscription_items', b: `subscription=sub_nope&price=${id(price)}` });
2584
+ const badPrice = await h({ m: 'POST', p: '/v1/subscription_items', b: `subscription=${id(sub)}&price=price_nope` });
2585
+ const noSub = await h({ m: 'POST', p: '/v1/subscription_items', b: `price=${id(price)}` });
2586
+ return ok(del) && field(del, 'deleted') === true && gone.status === 404 && items2.length === 0 &&
2587
+ badSub.status === 404 && badPrice.status === 400 && noSub.status === 400;
2588
+ }),
2589
+ ),
2590
+ // Subscription search: GET /v1/subscriptions/search?query=status:"active" returns the
2591
+ // search_result envelope filtered by the query language; missing query 400s.
2592
+ done('stripe.subscriptions.search', 'subscriptions', 'Subscriptions: search', 'api', 'common', () =>
2593
+ withRoot(async (h) => {
2594
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=subs@twin.test' });
2595
+ await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}` });
2596
+ const sub2 = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}` });
2597
+ await h({ m: 'DELETE', p: `/v1/subscriptions/${id(sub2)}` }); // canceled
2598
+ const r = await h({ m: 'GET', p: `/v1/subscriptions/search?query=${encodeURIComponent('status:"active"')}` });
2599
+ if (!ok(r) || field(r, 'object') !== 'search_result') return false;
2600
+ const data = (r.body as Body).data as Body[];
2601
+ const noQuery = await h({ m: 'GET', p: '/v1/subscriptions/search' });
2602
+ return data.length === 1 && data[0]!.status === 'active' && noQuery.status === 400;
2603
+ }),
2604
+ ),
2605
+ // Invoice search: GET /v1/invoices/search?query=status:"draft" returns the search_result
2606
+ // envelope of matching invoices; missing query 400s.
2607
+ done('stripe.invoices.search', 'invoices', 'Invoices: search', 'api', 'common', () =>
2608
+ withRoot(async (h) => {
2609
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=invs@twin.test' });
2610
+ const draft = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
2611
+ const open = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
2612
+ await h({ m: 'POST', p: `/v1/invoices/${id(open)}/finalize` });
2613
+ const r = await h({ m: 'GET', p: `/v1/invoices/search?query=${encodeURIComponent('status:"draft"')}` });
2614
+ if (!ok(r) || field(r, 'object') !== 'search_result') return false;
2615
+ const data = (r.body as Body).data as Body[];
2616
+ const noQuery = await h({ m: 'GET', p: '/v1/invoices/search' });
2617
+ return data.length === 1 && data[0]!.id === id(draft) && noQuery.status === 400;
2618
+ }),
2619
+ ),
2620
+ // Invoice line items: GET /v1/invoices/:id/lines lists the lines; add_lines appends (amount
2621
+ // or price-derived) and recomputes subtotal/total; update_lines patches a line; remove_lines
2622
+ // drops it. Editing a non-draft invoice 400s. Missing lines 400; unknown invoice 404.
2623
+ // (the in-place line edits + recomputed totals are produced ONLY by this feature.)
2624
+ done('stripe.invoices.line_items', 'invoices', 'Invoice line items list + add/remove/update lines', 'api', 'common', () =>
2625
+ withRoot(async (h) => {
2626
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=invlines@twin.test' });
2627
+ const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
2628
+ if (!ok(inv)) return false;
2629
+ const add = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/add_lines`, b: 'lines[0][amount]=1500&lines[0][description]=Setup&lines[1][amount]=500' });
2630
+ if (!ok(add) || field(add, 'subtotal') !== 2000 || field(add, 'total') !== 2000) return false;
2631
+ const lines = await h({ m: 'GET', p: `/v1/invoices/${id(inv)}/lines` });
2632
+ const lineData = (lines.body as Body).data as Body[];
2633
+ if (!ok(lines) || field(lines, 'object') !== 'list' || lineData.length !== 2) return false;
2634
+ const firstId = lineData[0]!.id as string;
2635
+ const upd = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/update_lines`, b: `lines[0][id]=${firstId}&lines[0][amount]=3000` });
2636
+ if (!ok(upd) || field(upd, 'subtotal') !== 3500) return false;
2637
+ const rem = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/remove_lines`, b: `lines[0][id]=${firstId}` });
2638
+ if (!ok(rem) || field(rem, 'subtotal') !== 500) return false;
2639
+ // editing a finalized (non-draft) invoice 400s; missing lines 400; unknown invoice 404.
2640
+ await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
2641
+ const afterFinal = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/add_lines`, b: 'lines[0][amount]=100' });
2642
+ const noLines = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/add_lines`, b: 'metadata[x]=1' });
2643
+ const missing = await h({ m: 'GET', p: '/v1/invoices/in_nope/lines' });
2644
+ return afterFinal.status === 400 && (noLines.status === 400) && missing.status === 404;
2645
+ }),
2646
+ ),
2647
+ // Invoice void/pay edge actions: status-gated transitions. finalizing a non-draft 400s;
2648
+ // paying an already-paid invoice 400s; marking a draft uncollectible 400s; voiding a paid
2649
+ // invoice 400s. (the state-machine gates are produced ONLY by this feature.)
2650
+ done('stripe.invoices.void_pay_actions', 'invoices', 'Invoices: pay/mark_uncollectible/finalize edge actions', 'api', 'common', () =>
2651
+ withRoot(async (h) => {
2652
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=invedge@twin.test' });
2653
+ await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=2000&currency=usd` });
2654
+ const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
2655
+ // marking a DRAFT uncollectible is rejected (must be open).
2656
+ const earlyUnc = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/mark_uncollectible` });
2657
+ if (earlyUnc.status !== 400) return false;
2658
+ const fin = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
2659
+ if (!ok(fin) || field(fin, 'status') !== 'open') return false;
2660
+ // finalizing an already-finalized invoice 400s.
2661
+ const finAgain = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
2662
+ if (finAgain.status !== 400) return false;
2663
+ const pay = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay` });
2664
+ if (!ok(pay) || field(pay, 'status') !== 'paid') return false;
2665
+ // paying an already-paid invoice 400s; voiding a paid invoice 400s.
2666
+ const payAgain = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay` });
2667
+ const voidPaid = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/void` });
2668
+ const missing = await h({ m: 'POST', p: '/v1/invoices/in_nope/void' });
2669
+ return payAgain.status === 400 && voidPaid.status === 400 && missing.status === 404;
2670
+ }),
2671
+ ),
2672
+ // Credit notes preview + void + lines: GET /v1/credit_notes/preview computes the credit
2673
+ // note object WITHOUT persisting (no id is created); POST /v1/credit_notes persists one
2674
+ // against a finalized invoice; /lines lists its line items; /void is terminal (a second
2675
+ // void 400s). Missing invoice/amount 400. (the non-persisted preview + the void gate are
2676
+ // produced ONLY by this feature.)
2677
+ done('stripe.credit_notes.preview', 'invoices', 'Credit notes: preview + void + lines', 'api', 'niche', () =>
2678
+ withRoot(async (h) => {
2679
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=cnprev@twin.test' });
2680
+ await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=5000&currency=usd` });
2681
+ const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
2682
+ await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
2683
+ // preview computes the object without persisting.
2684
+ const prev = await h({ m: 'GET', p: `/v1/credit_notes/preview?invoice=${id(inv)}&amount=2000` });
2685
+ if (!ok(prev) || field(prev, 'object') !== 'credit_note' || field(prev, 'amount') !== 2000) return false;
2686
+ const listBefore = await h({ m: 'GET', p: `/v1/credit_notes?invoice=${id(inv)}` });
2687
+ if (((listBefore.body as Body).data as Body[]).length !== 0) return false; // preview did NOT persist
2688
+ // persist + lines + void.
2689
+ const cn = await h({ m: 'POST', p: '/v1/credit_notes', b: `invoice=${id(inv)}&amount=2000&memo=Goodwill` });
2690
+ if (!ok(cn) || field(cn, 'status') !== 'issued') return false;
2691
+ const lines = await h({ m: 'GET', p: `/v1/credit_notes/${id(cn)}/lines` });
2692
+ if (!ok(lines) || ((lines.body as Body).data as Body[]).length !== 1) return false;
2693
+ const voided = await h({ m: 'POST', p: `/v1/credit_notes/${id(cn)}/void` });
2694
+ const voidAgain = await h({ m: 'POST', p: `/v1/credit_notes/${id(cn)}/void` });
2695
+ // vendor errors.
2696
+ const noInv = await h({ m: 'GET', p: '/v1/credit_notes/preview?amount=100' });
2697
+ const badInv = await h({ m: 'POST', p: '/v1/credit_notes', b: 'invoice=in_nope&amount=100' });
2698
+ const missing = await h({ m: 'GET', p: '/v1/credit_notes/cn_nope' });
2699
+ return ok(voided) && field(voided, 'status') === 'void' && voidAgain.status === 400 &&
2700
+ noInv.status === 400 && badInv.status === 404 && missing.status === 404;
2701
+ }),
2702
+ ),
2703
+ // Billing credit grants: a prepaid credit balance a customer draws down. create requires
2704
+ // customer (404 unknown) + category (paid|promotional, else 400) + amount[monetary]
2705
+ // (value+currency, else 400). retrieve/list (filter by customer), update (expires_at), and
2706
+ // the /expire + /void terminal transitions. credit_balance_summary sums the customer's
2707
+ // active (non-voided, non-expired) grants per currency. (the prepaid balance + draw-down
2708
+ // summary are produced ONLY by this feature.)
2709
+ done('stripe.billing.credit_grants', 'billing', 'Billing credit grants (prepaid credits)', 'api', 'niche', () =>
2710
+ withRoot(async (h) => {
2711
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=credgr@twin.test' });
2712
+ const g = await h({ m: 'POST', p: '/v1/billing/credit_grants', b: `customer=${id(cust)}&category=promotional&name=Welcome&amount[type]=monetary&amount[monetary][value]=1000&amount[monetary][currency]=usd` });
2713
+ if (!ok(g) || field(g, 'object') !== 'billing.credit_grant' || field(g, 'category') !== 'promotional') return false;
2714
+ if (((field(g, 'amount') as Body)?.monetary as Body)?.value !== 1000) return false;
2715
+ const get = await h({ m: 'GET', p: `/v1/billing/credit_grants/${id(g)}` });
2716
+ const list = await h({ m: 'GET', p: `/v1/billing/credit_grants?customer=${id(cust)}` });
2717
+ if (!ok(get) || id(get) !== id(g) || ((list.body as Body).data as Body[]).length !== 1) return false;
2718
+ // balance summary reflects the active grant.
2719
+ const sum = await h({ m: 'GET', p: `/v1/billing/credit_balance_summary?customer=${id(cust)}` });
2720
+ const balances = (sum.body as Body).balances as Body[];
2721
+ if (!ok(sum) || (((balances[0]!.available_balance as Body).monetary as Body).value) !== 1000) return false;
2722
+ // void clears it from the balance.
2723
+ const voided = await h({ m: 'POST', p: `/v1/billing/credit_grants/${id(g)}/void` });
2724
+ if (!ok(voided) || field(voided, 'voided_at') === null) return false;
2725
+ const sum2 = await h({ m: 'GET', p: `/v1/billing/credit_balance_summary?customer=${id(cust)}` });
2726
+ if (((sum2.body as Body).balances as Body[]).length !== 0) return false;
2727
+ // vendor errors.
2728
+ const noCust = await h({ m: 'POST', p: '/v1/billing/credit_grants', b: 'customer=cus_nope&category=paid&amount[type]=monetary&amount[monetary][value]=10&amount[monetary][currency]=usd' });
2729
+ const badCat = await h({ m: 'POST', p: '/v1/billing/credit_grants', b: `customer=${id(cust)}&category=bogus&amount[type]=monetary&amount[monetary][value]=10&amount[monetary][currency]=usd` });
2730
+ const noAmt = await h({ m: 'POST', p: '/v1/billing/credit_grants', b: `customer=${id(cust)}&category=paid` });
2731
+ const missing = await h({ m: 'GET', p: '/v1/billing/credit_grants/credgr_nope' });
2732
+ return noCust.status === 404 && badCat.status === 400 && noAmt.status === 400 && missing.status === 404;
2733
+ }),
2734
+ ),
2735
+ // Billing meters lifecycle: create→list (status filter)→deactivate (active→inactive)→
2736
+ // reactivate, plus meter event_summaries that aggregate the meter's events for a customer
2737
+ // over a [start_time, end_time) window applying default_aggregation.formula. (deactivate
2738
+ // gate + bucketed summary are produced ONLY by this feature; the deactivated_at transition
2739
+ // round-trips.)
2740
+ done('stripe.billing.meters_lifecycle', 'billing', 'Billing meters: list/deactivate/reactivate + meter event summaries', 'api', 'niche', () =>
2741
+ withRoot(async (h) => {
2742
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=mtr@twin.test' });
2743
+ const m = await h({ m: 'POST', p: '/v1/billing/meters', b: 'display_name=API calls&event_name=api_request&default_aggregation[formula]=sum&value_settings[event_payload_key]=value&customer_mapping[type]=by_id&customer_mapping[event_payload_key]=stripe_customer_id' });
2744
+ if (!ok(m) || field(m, 'status') !== 'active') return false;
2745
+ // report two usage events for the customer.
2746
+ const e1 = await h({ m: 'POST', p: '/v1/billing/meter_events', b: `event_name=api_request&timestamp=100&payload[value]=3&payload[stripe_customer_id]=${id(cust)}` });
2747
+ const e2 = await h({ m: 'POST', p: '/v1/billing/meter_events', b: `event_name=api_request&timestamp=200&payload[value]=4&payload[stripe_customer_id]=${id(cust)}` });
2748
+ if (!ok(e1) || !ok(e2) || field(e1, 'object') !== 'billing.meter_event') return false;
2749
+ const summary = await h({ m: 'GET', p: `/v1/billing/meters/${id(m)}/event_summaries?customer=${id(cust)}&start_time=0&end_time=1000` });
2750
+ const sData = (summary.body as Body).data as Body[];
2751
+ if (!ok(summary) || sData[0]!.aggregated_value !== 7) return false;
2752
+ // an out-of-window event is excluded.
2753
+ const summaryNarrow = await h({ m: 'GET', p: `/v1/billing/meters/${id(m)}/event_summaries?customer=${id(cust)}&start_time=150&end_time=1000` });
2754
+ if (((summaryNarrow.body as Body).data as Body[])[0]!.aggregated_value !== 4) return false;
2755
+ // deactivate → inactive; the active filter excludes it; reactivate → active.
2756
+ const off = await h({ m: 'POST', p: `/v1/billing/meters/${id(m)}/deactivate` });
2757
+ if (!ok(off) || field(off, 'status') !== 'inactive' || (field(off, 'status_transitions') as Body).deactivated_at === null) return false;
2758
+ const activeList = await h({ m: 'GET', p: '/v1/billing/meters?status=active' });
2759
+ if (((activeList.body as Body).data as Body[]).some((x) => x.id === id(m))) return false;
2760
+ const on = await h({ m: 'POST', p: `/v1/billing/meters/${id(m)}/reactivate` });
2761
+ const summaryMissing = await h({ m: 'GET', p: '/v1/billing/meters/mtr_nope/event_summaries?customer=x&start_time=0&end_time=1' });
2762
+ const noCust = await h({ m: 'GET', p: `/v1/billing/meters/${id(m)}/event_summaries?start_time=0&end_time=1` });
2763
+ return ok(on) && field(on, 'status') === 'active' && summaryMissing.status === 404 && noCust.status === 400;
2764
+ }),
2765
+ ),
2766
+ // Billing usage alerts: create requires alert_type=usage_threshold + title +
2767
+ // usage_threshold[gte]+[meter] (meter must exist, else 400). starts active; /deactivate→
2768
+ // inactive, /activate→active, /archive→archived. retrieve/list (filter by meter).
2769
+ // (the threshold object + status machine are produced ONLY by this feature.)
2770
+ done('stripe.billing.alerts', 'billing', 'Billing usage alerts (thresholds)', 'api', 'niche', () =>
2771
+ withRoot(async (h) => {
2772
+ const m = await h({ m: 'POST', p: '/v1/billing/meters', b: 'display_name=Calls&event_name=calls&default_aggregation[formula]=count' });
2773
+ const a = await h({ m: 'POST', p: `/v1/billing/alerts`, b: `alert_type=usage_threshold&title=Heavy use&usage_threshold[gte]=100&usage_threshold[meter]=${id(m)}` });
2774
+ if (!ok(a) || field(a, 'object') !== 'billing.alert' || field(a, 'status') !== 'active') return false;
2775
+ if ((field(a, 'usage_threshold') as Body).gte !== 100) return false;
2776
+ const get = await h({ m: 'GET', p: `/v1/billing/alerts/${id(a)}` });
2777
+ const list = await h({ m: 'GET', p: `/v1/billing/alerts?meter=${id(m)}` });
2778
+ if (!ok(get) || ((list.body as Body).data as Body[]).length !== 1) return false;
2779
+ const off = await h({ m: 'POST', p: `/v1/billing/alerts/${id(a)}/deactivate` });
2780
+ if (!ok(off) || field(off, 'status') !== 'inactive') return false;
2781
+ const on = await h({ m: 'POST', p: `/v1/billing/alerts/${id(a)}/activate` });
2782
+ const arch = await h({ m: 'POST', p: `/v1/billing/alerts/${id(a)}/archive` });
2783
+ // vendor errors.
2784
+ const badType = await h({ m: 'POST', p: '/v1/billing/alerts', b: 'alert_type=bogus&title=x&usage_threshold[gte]=1&usage_threshold[meter]=' + id(m) });
2785
+ const badMeter = await h({ m: 'POST', p: '/v1/billing/alerts', b: 'alert_type=usage_threshold&title=x&usage_threshold[gte]=1&usage_threshold[meter]=mtr_nope' });
2786
+ const missing = await h({ m: 'GET', p: '/v1/billing/alerts/alert_nope' });
2787
+ return ok(on) && field(on, 'status') === 'active' && ok(arch) && field(arch, 'status') === 'archived' &&
2788
+ badType.status === 400 && badMeter.status === 400 && missing.status === 404;
2789
+ }),
2790
+ ),
2791
+ // Entitlements features: create requires name + lookup_key (unique, else 400). list (filter
2792
+ // by lookup_key), retrieve, update (name/metadata/active). A product feature attaches a
2793
+ // feature to a product (POST /v1/products/:id/features, requires entitlement_feature; dup
2794
+ // 400; unknown feature 400), listed/deleted under the product. (the feature catalog + the
2795
+ // product↔feature grant are produced ONLY by this feature.)
2796
+ done('stripe.entitlements.features', 'billing', 'Entitlements: features + product features', 'api', 'niche', () =>
2797
+ withRoot(async (h) => {
2798
+ const f = await h({ m: 'POST', p: '/v1/entitlements/features', b: 'name=Premium reports&lookup_key=premium_reports' });
2799
+ if (!ok(f) || field(f, 'object') !== 'entitlements.feature' || field(f, 'lookup_key') !== 'premium_reports') return false;
2800
+ const dup = await h({ m: 'POST', p: '/v1/entitlements/features', b: 'name=Other&lookup_key=premium_reports' });
2801
+ if (dup.status !== 400) return false;
2802
+ const get = await h({ m: 'GET', p: `/v1/entitlements/features/${id(f)}` });
2803
+ const list = await h({ m: 'GET', p: '/v1/entitlements/features?lookup_key=premium_reports' });
2804
+ const upd = await h({ m: 'POST', p: `/v1/entitlements/features/${id(f)}`, b: 'name=Premium' });
2805
+ if (!ok(get) || ((list.body as Body).data as Body[]).length !== 1 || !ok(upd) || field(upd, 'name') !== 'Premium') return false;
2806
+ // attach to a product.
2807
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Pro plan' });
2808
+ const pf = await h({ m: 'POST', p: `/v1/products/${id(prod)}/features`, b: `entitlement_feature=${id(f)}` });
2809
+ if (!ok(pf) || field(pf, 'object') !== 'product_feature') return false;
2810
+ const dupPf = await h({ m: 'POST', p: `/v1/products/${id(prod)}/features`, b: `entitlement_feature=${id(f)}` });
2811
+ const pfList = await h({ m: 'GET', p: `/v1/products/${id(prod)}/features` });
2812
+ const pfGet = await h({ m: 'GET', p: `/v1/products/${id(prod)}/features/${id(pf)}` });
2813
+ if (dupPf.status !== 400 || ((pfList.body as Body).data as Body[]).length !== 1 || !ok(pfGet)) return false;
2814
+ const pfDel = await h({ m: 'DELETE', p: `/v1/products/${id(prod)}/features/${id(pf)}` });
2815
+ const pfList2 = await h({ m: 'GET', p: `/v1/products/${id(prod)}/features` });
2816
+ // vendor errors.
2817
+ const noName = await h({ m: 'POST', p: '/v1/entitlements/features', b: 'lookup_key=x' });
2818
+ const badFeat = await h({ m: 'POST', p: `/v1/products/${id(prod)}/features`, b: 'entitlement_feature=feat_nope' });
2819
+ const missing = await h({ m: 'GET', p: '/v1/entitlements/features/feat_nope' });
2820
+ return ok(pfDel) && field(pfDel, 'deleted') === true && ((pfList2.body as Body).data as Body[]).length === 0 &&
2821
+ noName.status === 400 && badFeat.status === 400 && missing.status === 404;
2822
+ }),
2823
+ ),
2824
+ // Active entitlements: GET /v1/entitlements/active_entitlements?customer= derives the
2825
+ // features a customer is entitled to from the products of their active subscriptions that
2826
+ // grant a feature. A customer with no granting subscription has none; granting one via a
2827
+ // subscribed product's feature yields exactly that feature's active entitlement. Missing
2828
+ // customer 400; unknown customer 404. (the derived per-customer entitlement is produced
2829
+ // ONLY by this feature.)
2830
+ done('stripe.entitlements.active', 'billing', 'Entitlements: active entitlements per customer', 'api', 'niche', () =>
2831
+ withRoot(async (h) => {
2832
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=ent@twin.test' });
2833
+ const f = await h({ m: 'POST', p: '/v1/entitlements/features', b: 'name=API access&lookup_key=api_access' });
2834
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Plan' });
2835
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=1000&currency=usd&recurring[interval]=month&product=${id(prod)}` });
2836
+ await h({ m: 'POST', p: `/v1/products/${id(prod)}/features`, b: `entitlement_feature=${id(f)}` });
2837
+ // before subscribing: no active entitlements.
2838
+ const before = await h({ m: 'GET', p: `/v1/entitlements/active_entitlements?customer=${id(cust)}` });
2839
+ if (!ok(before) || ((before.body as Body).data as Body[]).length !== 0) return false;
2840
+ // subscribe → entitled to the product's feature.
2841
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}&items[0][price]=${id(price)}` });
2842
+ if (!ok(sub)) return false;
2843
+ const after = await h({ m: 'GET', p: `/v1/entitlements/active_entitlements?customer=${id(cust)}` });
2844
+ const data = (after.body as Body).data as Body[];
2845
+ if (data.length !== 1 || data[0]!.feature !== id(f) || data[0]!.lookup_key !== 'api_access' || data[0]!.object !== 'entitlements.active_entitlement') return false;
2846
+ const noCust = await h({ m: 'GET', p: '/v1/entitlements/active_entitlements' });
2847
+ const badCust = await h({ m: 'GET', p: '/v1/entitlements/active_entitlements?customer=cus_nope' });
2848
+ return noCust.status === 400 && badCust.status === 404;
2849
+ }),
2850
+ ),
2851
+ // Tax (remaining)
2852
+ // Tax transactions: commit a calculation (create_from_calculation, requires calculation +
2853
+ // reference) into a persisted transaction copying its totals/line_items; record a refund
2854
+ // (create_reversal, requires original_transaction + reference + mode) with NEGATED amounts.
2855
+ // retrieve + /line_items round-trip. Missing calculation/reference 400; unknown calc 400.
2856
+ // (the committed transaction + negated reversal lines are produced ONLY by this feature.)
2857
+ done('stripe.tax.transactions', 'tax', 'Tax transactions (create from calculation, reversals)', 'api', 'common', () =>
2858
+ withRoot(async (h) => {
2859
+ const calc = await h({ m: 'POST', p: '/v1/tax/calculations', b: 'currency=usd&line_items[0][amount]=1000&line_items[0][reference]=sku_1&customer_details[address][country]=US' });
2860
+ if (!ok(calc)) return false;
2861
+ const txn = await h({ m: 'POST', p: '/v1/tax/transactions/create_from_calculation', b: `calculation=${id(calc)}&reference=order_123` });
2862
+ if (!ok(txn) || field(txn, 'object') !== 'tax.transaction' || field(txn, 'type') !== 'transaction' || field(txn, 'reference') !== 'order_123') return false;
2863
+ const txnLines = ((field(txn, 'line_items') as Body).data as Body[]) ?? [];
2864
+ if (txnLines.length !== 1 || txnLines[0]!.amount_tax !== 100) return false;
2865
+ const g = await h({ m: 'GET', p: `/v1/tax/transactions/${id(txn)}` });
2866
+ const li = await h({ m: 'GET', p: `/v1/tax/transactions/${id(txn)}/line_items` });
2867
+ if (!ok(g) || id(g) !== id(txn) || ((li.body as Body).data as Body[]).length !== 1) return false;
2868
+ // a reversal NEGATES the amounts.
2869
+ const rev = await h({ m: 'POST', p: '/v1/tax/transactions/create_reversal', b: `original_transaction=${id(txn)}&reference=refund_123&mode=full` });
2870
+ if (!ok(rev) || field(rev, 'type') !== 'reversal') return false;
2871
+ const revLines = ((field(rev, 'line_items') as Body).data as Body[]) ?? [];
2872
+ if (revLines[0]!.amount_tax !== -100 || (field(rev, 'reversal') as Body)?.original_transaction !== id(txn)) return false;
2873
+ // vendor errors.
2874
+ const noCalc = await h({ m: 'POST', p: '/v1/tax/transactions/create_from_calculation', b: 'reference=x' });
2875
+ const noRef = await h({ m: 'POST', p: '/v1/tax/transactions/create_from_calculation', b: `calculation=${id(calc)}` });
2876
+ const badCalc = await h({ m: 'POST', p: '/v1/tax/transactions/create_from_calculation', b: 'calculation=taxcalc_nope&reference=x' });
2877
+ return noCalc.status === 400 && noRef.status === 400 && badCalc.status === 400;
2878
+ }),
2879
+ ),
2880
+ // Tax settings (singleton): GET returns the account-wide config; with no head office set
2881
+ // the status is 'pending' (missing_fields lists head_office). POST updates defaults
2882
+ // (tax_behavior) + head_office; once a head office is set the status flips to 'active'.
2883
+ // The object has no id (singleton). (the status derivation + persisted defaults are
2884
+ // produced ONLY by this feature.)
2885
+ done('stripe.tax.settings', 'tax', 'Tax settings (head office, default tax behavior)', 'api', 'niche', () =>
2886
+ withRoot(async (h) => {
2887
+ const initial = await h({ m: 'GET', p: '/v1/tax/settings' });
2888
+ if (!ok(initial) || field(initial, 'object') !== 'tax.settings' || field(initial, 'status') !== 'pending') return false;
2889
+ if (!((field(initial, 'status_details') as Body).pending)) return false;
2890
+ // set default behavior (still pending — no head office yet).
2891
+ const setDefault = await h({ m: 'POST', p: '/v1/tax/settings', b: 'defaults[tax_behavior]=inclusive' });
2892
+ if (!ok(setDefault) || (field(setDefault, 'defaults') as Body).tax_behavior !== 'inclusive' || field(setDefault, 'status') !== 'pending') return false;
2893
+ // set head office → active; the earlier default persists.
2894
+ const setHead = await h({ m: 'POST', p: '/v1/tax/settings', b: 'head_office[address][country]=US&head_office[address][postal_code]=94103' });
2895
+ if (!ok(setHead) || field(setHead, 'status') !== 'active') return false;
2896
+ if ((field(setHead, 'defaults') as Body).tax_behavior !== 'inclusive') return false;
2897
+ const get = await h({ m: 'GET', p: '/v1/tax/settings' });
2898
+ return ok(get) && field(get, 'status') === 'active' && (field(get, 'head_office') as Body) !== null;
2899
+ }),
2900
+ ),
2901
+ // Quotes / discounts (remaining)
2902
+ // Quote lifecycle gates: a draft can be canceled directly; finalize→open, then cancel from
2903
+ // open; accepting a draft 400s (quote_invalid_status), finalizing an open quote 400s. line_items
2904
+ // round-trip. (the cancel-from-draft/open transitions complete the status machine.)
2905
+ done('stripe.quotes.lifecycle', 'checkout', 'Quotes: finalize / accept / cancel + line items', 'api', 'common', () =>
2906
+ withRoot(async (h) => {
2907
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=qtlc@twin.test' });
2908
+ const prod = await h({ m: 'POST', p: '/v1/products', b: 'name=Q' });
2909
+ const price = await h({ m: 'POST', p: '/v1/prices', b: `unit_amount=2000&currency=usd&product=${id(prod)}` });
2910
+ // a draft quote can be canceled directly.
2911
+ const draft = await h({ m: 'POST', p: '/v1/quotes', b: `customer=${id(cust)}&line_items[0][price]=${id(price)}&line_items[0][quantity]=2` });
2912
+ if (!ok(draft) || field(draft, 'amount_total') !== 4000) return false;
2913
+ const cancelDraft = await h({ m: 'POST', p: `/v1/quotes/${id(draft)}/cancel` });
2914
+ if (!ok(cancelDraft) || field(cancelDraft, 'status') !== 'canceled') return false;
2915
+ // a second quote: finalize → open, then cancel from open.
2916
+ const q2 = await h({ m: 'POST', p: '/v1/quotes', b: `customer=${id(cust)}&line_items[0][price]=${id(price)}` });
2917
+ const earlyAccept = await h({ m: 'POST', p: `/v1/quotes/${id(q2)}/accept` });
2918
+ if (earlyAccept.status !== 400) return false;
2919
+ const fin = await h({ m: 'POST', p: `/v1/quotes/${id(q2)}/finalize` });
2920
+ if (!ok(fin) || field(fin, 'status') !== 'open' || !field(fin, 'number')) return false;
2921
+ const finAgain = await h({ m: 'POST', p: `/v1/quotes/${id(q2)}/finalize` });
2922
+ const cancelOpen = await h({ m: 'POST', p: `/v1/quotes/${id(q2)}/cancel` });
2923
+ const li = await h({ m: 'GET', p: `/v1/quotes/${id(draft)}/line_items` });
2924
+ return finAgain.status === 400 && ok(cancelOpen) && field(cancelOpen, 'status') === 'canceled' &&
2925
+ ok(li) && ((li.body as Body).data as Body[]).length === 1;
2926
+ }),
2927
+ ),
2928
+ // Discount delete: attach a coupon to a customer (materializes the discount object) and to a
2929
+ // subscription, then DELETE …/discount on each removes it (→ {object:'discount',deleted:true})
2930
+ // and the resource's discount is cleared. Unknown customer 404. (the customer discount apply +
2931
+ // both deletes are produced ONLY by this feature.)
2932
+ done('stripe.discounts.delete', 'discounts', 'Customer/Subscription discount delete', 'api', 'common', () =>
2933
+ withRoot(async (h) => {
2934
+ const coupon = await h({ m: 'POST', p: '/v1/coupons', b: 'percent_off=20&duration=forever' });
2935
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: `email=disc-del@twin.test&coupon=${id(coupon)}` });
2936
+ if (!ok(cust) || (field(cust, 'discount') as Body)?.object !== 'discount') return false;
2937
+ const delCust = await h({ m: 'DELETE', p: `/v1/customers/${id(cust)}/discount` });
2938
+ if (!ok(delCust) || field(delCust, 'deleted') !== true) return false;
2939
+ const c = await h({ m: 'GET', p: `/v1/customers/${id(cust)}` });
2940
+ if (field(c, 'discount') !== null) return false;
2941
+ // subscription discount delete.
2942
+ const sub = await h({ m: 'POST', p: '/v1/subscriptions', b: `customer=${id(cust)}&coupon=${id(coupon)}` });
2943
+ if (((field(sub, 'discounts') as Body[]) ?? []).length !== 1) return false;
2944
+ const delSub = await h({ m: 'DELETE', p: `/v1/subscriptions/${id(sub)}/discount` });
2945
+ const s = await h({ m: 'GET', p: `/v1/subscriptions/${id(sub)}` });
2946
+ const badCoupon = await h({ m: 'POST', p: '/v1/customers', b: 'email=x@x.co&coupon=coupon_nope' });
2947
+ const missing = await h({ m: 'DELETE', p: '/v1/customers/cus_nope/discount' });
2948
+ return ok(delSub) && field(delSub, 'deleted') === true && ((field(s, 'discounts') as Body[]) ?? []).length === 0 &&
2949
+ badCoupon.status === 400 && missing.status === 404;
2950
+ }),
2951
+ ),
2952
+ // PromotionCode update: flip active (deactivate → reactivate) + metadata; the change round-trips
2953
+ // on retrieve. Unknown id 404. (the active toggle persistence is produced ONLY by this feature.)
2954
+ done('stripe.promotion_codes.update', 'discounts', 'Promotion codes: update (active toggle)', 'api', 'common', () =>
2955
+ withRoot(async (h) => {
2956
+ const coupon = await h({ m: 'POST', p: '/v1/coupons', b: 'percent_off=10&duration=once' });
2957
+ const pc = await h({ m: 'POST', p: '/v1/promotion_codes', b: `coupon=${id(coupon)}&code=UPD10` });
2958
+ if (!ok(pc) || field(pc, 'active') !== true) return false;
2959
+ const off = await h({ m: 'POST', p: `/v1/promotion_codes/${id(pc)}`, b: 'active=false&metadata[campaign]=spring' });
2960
+ if (!ok(off) || field(off, 'active') !== false || ((field(off, 'metadata') as Body)?.campaign) !== 'spring') return false;
2961
+ const g = await h({ m: 'GET', p: `/v1/promotion_codes/${id(pc)}` });
2962
+ if (field(g, 'active') !== false) return false;
2963
+ const on = await h({ m: 'POST', p: `/v1/promotion_codes/${id(pc)}`, b: 'active=true' });
2964
+ const missing = await h({ m: 'POST', p: '/v1/promotion_codes/promo_nope', b: 'active=true' });
2965
+ return ok(on) && field(on, 'active') === true && missing.status === 404;
2966
+ }),
2967
+ ),
2968
+ // Reporting / Sigma
2969
+ // Report types catalog: list returns the available reporting.report_type objects; retrieve
2970
+ // by id returns one; an unknown id 404s.
2971
+ done('stripe.reporting.report_types', 'reporting', 'Reporting: report types list/retrieve', 'api', 'niche', () =>
2972
+ withRoot(async (h) => {
2973
+ const l = await h({ m: 'GET', p: '/v1/reporting/report_types' });
2974
+ if (!ok(l) || field(l, 'object') !== 'list') return false;
2975
+ const data = (l.body as Body).data as Body[];
2976
+ if (data.length === 0 || data[0]!.object !== 'reporting.report_type') return false;
2977
+ const g = await h({ m: 'GET', p: `/v1/reporting/report_types/${data[0]!.id}` });
2978
+ const nope = await h({ m: 'GET', p: '/v1/reporting/report_types/nope.report' });
2979
+ return ok(g) && (g.body as Body).id === data[0]!.id && nope.status === 404;
2980
+ }),
2981
+ ),
2982
+ // Issuing (remaining sub-resources)
2983
+ // Issuing disputes: open against a settled transaction (requires transaction + evidence[reason]);
2984
+ // /submit transitions unsubmitted→submitted (twice 400s); unknown id 404. (The dispute lifecycle
2985
+ // is also exercised by stripe.issuing.transactions; this entry pins disputes specifically.)
2986
+ done('stripe.issuing.disputes', 'issuing', 'Issuing: disputes', 'api', 'niche', () =>
2987
+ withRoot(async (h) => {
2988
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jane&type=individual&billing[address][country]=US' });
2989
+ const card = await h({ m: 'POST', p: `/v1/issuing/cards`, b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
2990
+ const txn = await h({ m: 'POST', p: '/v1/test_helpers/issuing/transactions/create_force_capture', b: `card=${id(card)}&amount=1200` });
2991
+ const disp = await h({ m: 'POST', p: '/v1/issuing/disputes', b: `transaction=${id(txn)}&evidence[reason]=merchandise_not_as_described` });
2992
+ if (!ok(disp) || field(disp, 'status') !== 'unsubmitted') return false;
2993
+ const g = await h({ m: 'GET', p: `/v1/issuing/disputes/${id(disp)}` });
2994
+ const l = await h({ m: 'GET', p: '/v1/issuing/disputes?status=unsubmitted' });
2995
+ const sub = await h({ m: 'POST', p: `/v1/issuing/disputes/${id(disp)}/submit` });
2996
+ const twice = await h({ m: 'POST', p: `/v1/issuing/disputes/${id(disp)}/submit` });
2997
+ const nope = await h({ m: 'GET', p: '/v1/issuing/disputes/idp_nope' });
2998
+ return ok(g) && ((l.body as Body).data as Body[]).length === 1 && ok(sub) && field(sub, 'status') === 'submitted' &&
2999
+ twice.status === 400 && nope.status === 404;
3000
+ }),
3001
+ ),
3002
+ // Issuing network tokens: tokens are minted by the card network (wallet provisioning), not
3003
+ // via a public create — so the twin exposes the READ surface (list, filterable by card/
3004
+ // status) + the status update Stripe allows (active|deleted|suspended, else 400). To prove
3005
+ // that surface FAILABLY, this verify SEEDS a real token by minting it via the test_helpers
3006
+ // route (the network-simulation namespace the pack already uses for authorizations) against
3007
+ // a created cardholder+card, then asserts retrieve / status-transition (persisted on re-GET)
3008
+ // / list+filter. An empty account still lists none; unknown ids still 404. This fails if
3009
+ // retrieve returned nothing, if a status update didn't persist, or if filtering were broken.
3010
+ done('stripe.issuing.tokens', 'issuing', 'Issuing: network tokens', 'api', 'niche', () =>
3011
+ withRoot(async (h) => {
3012
+ // empty account: lists none, unknown ids 404 on retrieve + update.
3013
+ const empty = await h({ m: 'GET', p: '/v1/issuing/tokens' });
3014
+ if (!ok(empty) || field(empty, 'object') !== 'list' || ((empty.body as Body).data as Body[]).length !== 0) return false;
3015
+ const missing = await h({ m: 'GET', p: '/v1/issuing/tokens/iss_tok_nope' });
3016
+ const badUpd = await h({ m: 'POST', p: '/v1/issuing/tokens/iss_tok_nope', b: 'status=active' });
3017
+ if (missing.status !== 404 || badUpd.status !== 404) return false;
3018
+ // seed: a token is network-minted for a real card (no public create) via test_helpers.
3019
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jane&type=individual&billing[address][country]=US' });
3020
+ const card = await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
3021
+ const seeded = await h({ m: 'POST', p: '/v1/test_helpers/issuing/tokens', b: `card=${id(card)}` });
3022
+ if (!ok(seeded) || field(seeded, 'object') !== 'issuing.token' || field(seeded, 'status') !== 'active' || field(seeded, 'card') !== id(card)) return false;
3023
+ // minting requires a real card.
3024
+ const badCard = await h({ m: 'POST', p: '/v1/test_helpers/issuing/tokens', b: 'card=ic_nope' });
3025
+ const noCard = await h({ m: 'POST', p: '/v1/test_helpers/issuing/tokens' });
3026
+ if (badCard.status !== 400 || noCard.status !== 400) return false;
3027
+ // retrieve the seeded token.
3028
+ const get = await h({ m: 'GET', p: `/v1/issuing/tokens/${id(seeded)}` });
3029
+ if (!ok(get) || id(get) !== id(seeded) || field(get, 'card') !== id(card) || field(get, 'status') !== 'active') return false;
3030
+ // status transition suspended → persists on re-GET.
3031
+ const susp = await h({ m: 'POST', p: `/v1/issuing/tokens/${id(seeded)}`, b: 'status=suspended' });
3032
+ if (!ok(susp) || field(susp, 'status') !== 'suspended') return false;
3033
+ const reGet = await h({ m: 'GET', p: `/v1/issuing/tokens/${id(seeded)}` });
3034
+ if (!ok(reGet) || field(reGet, 'status') !== 'suspended') return false;
3035
+ // reactivate.
3036
+ const react = await h({ m: 'POST', p: `/v1/issuing/tokens/${id(seeded)}`, b: 'status=active' });
3037
+ if (!ok(react) || field(react, 'status') !== 'active') return false;
3038
+ // invalid status 400s.
3039
+ const badStatus = await h({ m: 'POST', p: `/v1/issuing/tokens/${id(seeded)}`, b: 'status=frozen' });
3040
+ if (badStatus.status !== 400) return false;
3041
+ // list + filter: by card returns it; by status=active returns it; wrong filters exclude it.
3042
+ const byCard = await h({ m: 'GET', p: `/v1/issuing/tokens?card=${id(card)}` });
3043
+ if (((byCard.body as Body).data as Body[]).length !== 1 || ((byCard.body as Body).data as Body[])[0]!.id !== id(seeded)) return false;
3044
+ const byStatus = await h({ m: 'GET', p: `/v1/issuing/tokens?status=active&card=${id(card)}` });
3045
+ if (((byStatus.body as Body).data as Body[]).length !== 1) return false;
3046
+ const wrongStatus = await h({ m: 'GET', p: `/v1/issuing/tokens?status=suspended&card=${id(card)}` });
3047
+ if (((wrongStatus.body as Body).data as Body[]).length !== 0) return false;
3048
+ const wrongCard = await h({ m: 'GET', p: '/v1/issuing/tokens?card=ic_other' });
3049
+ return ((wrongCard.body as Body).data as Body[]).length === 0;
3050
+ }),
3051
+ ),
3052
+ // Issuing personalization designs (physical-card art): create requires physical_bundle
3053
+ // (else 400); the design starts 'inactive' (pending review). list (status filter), retrieve,
3054
+ // update. test_helpers activate/deactivate/reject transition the status. Unknown id 404.
3055
+ // (the design object + review status machine are produced ONLY by this feature.)
3056
+ done('stripe.issuing.personalization_designs', 'issuing', 'Issuing: card personalization designs', 'api', 'niche', () =>
3057
+ withRoot(async (h) => {
3058
+ const d = await h({ m: 'POST', p: '/v1/issuing/personalization_designs', b: 'physical_bundle=ipb_test&name=Gold card' });
3059
+ if (!ok(d) || field(d, 'object') !== 'issuing.personalization_design' || field(d, 'status') !== 'inactive') return false;
3060
+ const get = await h({ m: 'GET', p: `/v1/issuing/personalization_designs/${id(d)}` });
3061
+ const upd = await h({ m: 'POST', p: `/v1/issuing/personalization_designs/${id(d)}`, b: 'name=Platinum' });
3062
+ if (!ok(get) || !ok(upd) || field(upd, 'name') !== 'Platinum') return false;
3063
+ // test_helpers activate → active; the status filter then includes it.
3064
+ const act = await h({ m: 'POST', p: `/v1/test_helpers/issuing/personalization_designs/${id(d)}/activate` });
3065
+ if (!ok(act) || field(act, 'status') !== 'active') return false;
3066
+ const list = await h({ m: 'GET', p: '/v1/issuing/personalization_designs?status=active' });
3067
+ if (((list.body as Body).data as Body[]).length !== 1) return false;
3068
+ const rej = await h({ m: 'POST', p: `/v1/test_helpers/issuing/personalization_designs/${id(d)}/reject` });
3069
+ const noBundle = await h({ m: 'POST', p: '/v1/issuing/personalization_designs', b: 'name=x' });
3070
+ const missing = await h({ m: 'GET', p: '/v1/issuing/personalization_designs/pd_nope' });
3071
+ return ok(rej) && field(rej, 'status') === 'rejected' && noBundle.status === 400 && missing.status === 404;
3072
+ }),
3073
+ ),
3074
+ // Terminal (remaining)
3075
+ // Connection tokens: POST /v1/terminal/connection_tokens mints a short-lived secret for the
3076
+ // reader SDK (optionally scoped to a location).
3077
+ done('stripe.terminal.connection_tokens', 'terminal', 'Terminal: connection tokens', 'api', 'niche', () =>
3078
+ withRoot(async (h) => {
3079
+ const ct = await h({ m: 'POST', p: '/v1/terminal/connection_tokens' });
3080
+ if (!ok(ct) || field(ct, 'object') !== 'terminal.connection_token' || typeof field(ct, 'secret') !== 'string') return false;
3081
+ const loc = await h({ m: 'POST', p: '/v1/terminal/locations', b: 'display_name=HQ&address[country]=US' });
3082
+ const scoped = await h({ m: 'POST', p: '/v1/terminal/connection_tokens', b: `location=${id(loc)}` });
3083
+ return ok(scoped) && field(scoped, 'location') === id(loc);
3084
+ }),
3085
+ ),
3086
+ // Reader configurations: create/retrieve/update/delete a configuration profile. Unknown id 404.
3087
+ done('stripe.terminal.configurations', 'terminal', 'Terminal: reader configurations', 'api', 'niche', () =>
3088
+ withRoot(async (h) => {
3089
+ const cfg = await h({ m: 'POST', p: '/v1/terminal/configurations', b: 'name=Lane 1' });
3090
+ if (!ok(cfg) || field(cfg, 'object') !== 'terminal.configuration') return false;
3091
+ const g = await h({ m: 'GET', p: `/v1/terminal/configurations/${id(cfg)}` });
3092
+ const u = await h({ m: 'POST', p: `/v1/terminal/configurations/${id(cfg)}`, b: 'name=Lane 2' });
3093
+ const l = await h({ m: 'GET', p: '/v1/terminal/configurations' });
3094
+ const del = await h({ m: 'DELETE', p: `/v1/terminal/configurations/${id(cfg)}` });
3095
+ const gone = await h({ m: 'GET', p: `/v1/terminal/configurations/${id(cfg)}` });
3096
+ const nope = await h({ m: 'GET', p: '/v1/terminal/configurations/tmc_nope' });
3097
+ return ok(g) && ok(u) && field(u, 'name') === 'Lane 2' && field(l, 'object') === 'list' &&
3098
+ ok(del) && field(del, 'deleted') === true && gone.status === 404 && nope.status === 404;
3099
+ }),
3100
+ ),
3101
+ // Treasury (financial accounts) — whole product
3102
+ // Treasury financial accounts: open one with supported_currencies (else 400) + features to
3103
+ // enable; it starts status 'open' with a zero balance and the requested features active
3104
+ // (others restricted). list (status filter), retrieve, update (metadata), GET /features.
3105
+ // Unknown id 404. (the opened account + per-currency zero balance + feature map are
3106
+ // produced ONLY by this feature.)
3107
+ done('stripe.treasury.financial_accounts', 'treasury', 'Treasury: financial accounts', 'api', 'niche', () =>
3108
+ withRoot(async (h) => {
3109
+ const fa = await h({ m: 'POST', p: '/v1/treasury/financial_accounts', b: 'supported_currencies[]=usd&features[card_issuing][requested]=true&features[outbound_payments][requested]=true' });
3110
+ if (!ok(fa) || field(fa, 'object') !== 'treasury.financial_account' || field(fa, 'status') !== 'open') return false;
3111
+ const bal = field(fa, 'balance') as Body;
3112
+ if ((bal.cash as Body).usd !== 0) return false;
3113
+ if ((((field(fa, 'features') as Body).card_issuing) as Body).status !== 'active') return false;
3114
+ if ((((field(fa, 'features') as Body).deposit_insurance) as Body).status !== 'restricted') return false;
3115
+ const get = await h({ m: 'GET', p: `/v1/treasury/financial_accounts/${id(fa)}` });
3116
+ const list = await h({ m: 'GET', p: '/v1/treasury/financial_accounts?status=open' });
3117
+ const feats = await h({ m: 'GET', p: `/v1/treasury/financial_accounts/${id(fa)}/features` });
3118
+ if (!ok(get) || ((list.body as Body).data as Body[]).length !== 1 || !ok(feats) || field(feats, 'object') !== 'treasury.financial_account_features') return false;
3119
+ const upd = await h({ m: 'POST', p: `/v1/treasury/financial_accounts/${id(fa)}`, b: 'metadata[team]=ops' });
3120
+ if (!ok(upd) || ((field(upd, 'metadata') as Body)?.team) !== 'ops') return false;
3121
+ const noCur = await h({ m: 'POST', p: '/v1/treasury/financial_accounts', b: 'features[card_issuing][requested]=true' });
3122
+ const missing = await h({ m: 'GET', p: '/v1/treasury/financial_accounts/fa_nope' });
3123
+ return noCur.status === 400 && missing.status === 404;
3124
+ }),
3125
+ ),
3126
+ // Treasury transactions + transaction entries: the FinancialAccount ledger. Every flow
3127
+ // posts a Transaction (object treasury.transaction) + a TransactionEntry. The list REQUIRES
3128
+ // financial_account (400 without). The ledger reflects the flow's balance_impact.
3129
+ done('stripe.treasury.transactions', 'treasury', 'Treasury: transactions / transaction entries', 'api', 'niche', () =>
3130
+ withRoot(async (h) => {
3131
+ const fa = await h({ m: 'POST', p: '/v1/treasury/financial_accounts', b: 'supported_currencies[]=usd&features[outbound_payments][requested]=true' });
3132
+ const op = await h({ m: 'POST', p: '/v1/treasury/outbound_payments', b: `amount=5000&currency=usd&financial_account=${id(fa)}&destination_payment_method=pm_x` });
3133
+ if (!ok(op)) return false;
3134
+ const txns = await h({ m: 'GET', p: `/v1/treasury/transactions?financial_account=${id(fa)}` });
3135
+ const tdata = (txns.body as Body).data as Body[];
3136
+ if (tdata.length !== 1 || tdata[0]!.object !== 'treasury.transaction' || tdata[0]!.flow !== id(op)) return false;
3137
+ if ((tdata[0]!.balance_impact as Body)?.cash !== -5000 || (tdata[0]!.balance_impact as Body)?.outbound_pending !== 5000) return false;
3138
+ const getT = await h({ m: 'GET', p: `/v1/treasury/transactions/${tdata[0]!.id}` });
3139
+ const entries = await h({ m: 'GET', p: `/v1/treasury/transaction_entries?financial_account=${id(fa)}` });
3140
+ const edata = (entries.body as Body).data as Body[];
3141
+ if (edata.length !== 1 || edata[0]!.object !== 'treasury.transaction_entry' || edata[0]!.flow_type !== 'outbound_payment') return false;
3142
+ const noFa = await h({ m: 'GET', p: '/v1/treasury/transactions' });
3143
+ const noFaE = await h({ m: 'GET', p: '/v1/treasury/transaction_entries' });
3144
+ const nope = await h({ m: 'GET', p: '/v1/treasury/transactions/trxn_nope' });
3145
+ return ok(getT) && noFa.status === 400 && noFaE.status === 400 && nope.status === 404;
3146
+ }),
3147
+ ),
3148
+ // Treasury OutboundPayments (+ OutboundTransfers): create→processing (cancelable), cancel→
3149
+ // canceled. Requires amount/currency/financial_account + a destination. Cancel only while
3150
+ // processing (else 400). Bad financial_account 400. Unknown id 404. id obp_ / obt_.
3151
+ done('stripe.treasury.outbound_payments', 'treasury', 'Treasury: outbound payments / transfers', 'api', 'niche', () =>
3152
+ withRoot(async (h) => {
3153
+ const fa = await h({ m: 'POST', p: '/v1/treasury/financial_accounts', b: 'supported_currencies[]=usd&features[outbound_payments][requested]=true' });
3154
+ const op = await h({ m: 'POST', p: `/v1/treasury/outbound_payments`, b: `amount=5000&currency=usd&financial_account=${id(fa)}&destination_payment_method=pm_x` });
3155
+ if (!ok(op) || field(op, 'object') !== 'treasury.outbound_payment' || field(op, 'status') !== 'processing' || !id(op).startsWith('obp_')) return false;
3156
+ const get = await h({ m: 'GET', p: `/v1/treasury/outbound_payments/${id(op)}` });
3157
+ const list = await h({ m: 'GET', p: `/v1/treasury/outbound_payments?financial_account=${id(fa)}` });
3158
+ if (!ok(get) || ((list.body as Body).data as Body[]).length !== 1) return false;
3159
+ const cancel = await h({ m: 'POST', p: `/v1/treasury/outbound_payments/${id(op)}/cancel` });
3160
+ if (!ok(cancel) || field(cancel, 'status') !== 'canceled' || field(cancel, 'cancelable') !== false) return false;
3161
+ if ((field(cancel, 'status_transitions') as Body)?.canceled_at == null) return false;
3162
+ const recancel = await h({ m: 'POST', p: `/v1/treasury/outbound_payments/${id(op)}/cancel` });
3163
+ // outbound_transfers share the lifecycle.
3164
+ const ot = await h({ m: 'POST', p: `/v1/treasury/outbound_transfers`, b: `amount=2000&currency=usd&financial_account=${id(fa)}&destination_payment_method=pm_y` });
3165
+ if (!ok(ot) || field(ot, 'object') !== 'treasury.outbound_transfer' || !id(ot).startsWith('obt_')) return false;
3166
+ const otCancel = await h({ m: 'POST', p: `/v1/treasury/outbound_transfers/${id(ot)}/cancel` });
3167
+ const badFa = await h({ m: 'POST', p: `/v1/treasury/outbound_payments`, b: 'amount=100&currency=usd&financial_account=fa_nope&destination_payment_method=pm_x' });
3168
+ const noDest = await h({ m: 'POST', p: `/v1/treasury/outbound_payments`, b: `amount=100&currency=usd&financial_account=${id(fa)}` });
3169
+ const nope = await h({ m: 'GET', p: '/v1/treasury/outbound_payments/obp_nope' });
3170
+ return recancel.status === 400 && ok(otCancel) && field(otCancel, 'status') === 'canceled' &&
3171
+ badFa.status === 400 && noDest.status === 400 && nope.status === 404;
3172
+ }),
3173
+ ),
3174
+ // Treasury InboundTransfers: pull funds into a FinancialAccount. create→succeeded (test-mode
3175
+ // synchronous settle), posting a ledger Transaction (cash↑). Requires amount/currency/
3176
+ // financial_account/origin_payment_method. Unknown id 404. id ibt_.
3177
+ done('stripe.treasury.inbound_transfers', 'treasury', 'Treasury: inbound transfers / received credits', 'api', 'niche', () =>
3178
+ withRoot(async (h) => {
3179
+ const fa = await h({ m: 'POST', p: '/v1/treasury/financial_accounts', b: 'supported_currencies[]=usd&features[inbound_transfers][requested]=true' });
3180
+ const it = await h({ m: 'POST', p: `/v1/treasury/inbound_transfers`, b: `amount=3000&currency=usd&financial_account=${id(fa)}&origin_payment_method=pm_y` });
3181
+ if (!ok(it) || field(it, 'object') !== 'treasury.inbound_transfer' || field(it, 'status') !== 'succeeded' || !id(it).startsWith('ibt_')) return false;
3182
+ if ((field(it, 'status_transitions') as Body)?.succeeded_at == null) return false;
3183
+ const get = await h({ m: 'GET', p: `/v1/treasury/inbound_transfers/${id(it)}` });
3184
+ const list = await h({ m: 'GET', p: `/v1/treasury/inbound_transfers?financial_account=${id(fa)}` });
3185
+ if (!ok(get) || ((list.body as Body).data as Body[]).length !== 1) return false;
3186
+ // a ledger transaction was posted with cash↑.
3187
+ const txns = await h({ m: 'GET', p: `/v1/treasury/transactions?financial_account=${id(fa)}&flow_type=inbound_transfer` });
3188
+ const tdata = (txns.body as Body).data as Body[];
3189
+ if (tdata.length !== 1 || (tdata[0]!.balance_impact as Body)?.cash !== 3000 || tdata[0]!.status !== 'posted') return false;
3190
+ // received_credits/debits read surface exists (empty until a test flow materializes one).
3191
+ const rc = await h({ m: 'GET', p: `/v1/treasury/received_credits?financial_account=${id(fa)}` });
3192
+ const noOrigin = await h({ m: 'POST', p: `/v1/treasury/inbound_transfers`, b: `amount=100&currency=usd&financial_account=${id(fa)}` });
3193
+ const nope = await h({ m: 'GET', p: '/v1/treasury/inbound_transfers/ibt_nope' });
3194
+ return field(rc, 'object') === 'list' && noOrigin.status === 400 && nope.status === 404;
3195
+ }),
3196
+ ),
3197
+ // Climate (carbon removal): products/suppliers catalog (read-only) + orders. An order needs
3198
+ // a valid product + exactly one of amount/metric_tons (else 400); starts awaiting_funds;
3199
+ // cancel→canceled. amount_total = subtotal+fees. Unknown product 400; unknown order 404.
3200
+ done('stripe.climate.orders', 'climate', 'Climate: orders / products / suppliers', 'api', 'niche', () =>
3201
+ withRoot(async (h) => {
3202
+ const products = await h({ m: 'GET', p: '/v1/climate/products' });
3203
+ const pdata = (products.body as Body).data as Body[];
3204
+ if (pdata.length === 0 || pdata[0]!.object !== 'climate.product') return false;
3205
+ const suppliers = await h({ m: 'GET', p: '/v1/climate/suppliers' });
3206
+ if (((suppliers.body as Body).data as Body[])[0]?.object !== 'climate.supplier') return false;
3207
+ const order = await h({ m: 'POST', p: '/v1/climate/orders', b: 'product=climsku_direct_air_capture&metric_tons=2' });
3208
+ if (!ok(order) || field(order, 'object') !== 'climate.order' || field(order, 'status') !== 'awaiting_funds' || !id(order).startsWith('climorder_')) return false;
3209
+ if (field(order, 'amount_total') !== (field(order, 'amount_subtotal') as number) + (field(order, 'amount_fees') as number)) return false;
3210
+ const get = await h({ m: 'GET', p: `/v1/climate/orders/${id(order)}` });
3211
+ const list = await h({ m: 'GET', p: '/v1/climate/orders' });
3212
+ if (!ok(get) || ((list.body as Body).data as Body[]).length !== 1) return false;
3213
+ const cancel = await h({ m: 'POST', p: `/v1/climate/orders/${id(order)}/cancel` });
3214
+ if (!ok(cancel) || field(cancel, 'status') !== 'canceled') return false;
3215
+ const badProduct = await h({ m: 'POST', p: '/v1/climate/orders', b: 'product=climsku_nope&metric_tons=1' });
3216
+ const bothAmounts = await h({ m: 'POST', p: '/v1/climate/orders', b: 'product=climsku_direct_air_capture&metric_tons=1&amount=1000' });
3217
+ const nope = await h({ m: 'GET', p: '/v1/climate/orders/climorder_nope' });
3218
+ return badProduct.status === 400 && bothAmounts.status === 400 && nope.status === 404;
3219
+ }),
3220
+ ),
3221
+ // Financial Connections: create a session (requires account_holder + permissions), link an
3222
+ // account via the hosted-flow test helper, then read accounts + the session's accounts
3223
+ // sub-list. id fcsess_ / fca_. Missing permissions 400; unknown id 404.
3224
+ done('stripe.financial_connections.sessions', 'financial_connections', 'Financial Connections: sessions + accounts', 'api', 'niche', () =>
3225
+ withRoot(async (h) => {
3226
+ const sess = await h({ m: 'POST', p: '/v1/financial_connections/sessions', b: 'account_holder[type]=customer&account_holder[customer]=cus_1&permissions[]=transactions&permissions[]=balances' });
3227
+ if (!ok(sess) || field(sess, 'object') !== 'financial_connections.session' || !id(sess).startsWith('fcsess_')) return false;
3228
+ if (typeof field(sess, 'client_secret') !== 'string') return false;
3229
+ const acc = await h({ m: 'POST', p: `/v1/financial_connections/sessions/${id(sess)}/link_account` });
3230
+ if (!ok(acc) || field(acc, 'object') !== 'financial_connections.account' || !id(acc).startsWith('fca_') || field(acc, 'status') !== 'active') return false;
3231
+ const getSess = await h({ m: 'GET', p: `/v1/financial_connections/sessions/${id(sess)}` });
3232
+ if (((field(getSess, 'accounts') as Body)?.data as Body[]).length !== 1) return false;
3233
+ const accounts = await h({ m: 'GET', p: '/v1/financial_connections/accounts' });
3234
+ if (((accounts.body as Body).data as Body[]).length !== 1) return false;
3235
+ const getAcc = await h({ m: 'GET', p: `/v1/financial_connections/accounts/${id(acc)}` });
3236
+ const disc = await h({ m: 'POST', p: `/v1/financial_connections/accounts/${id(acc)}/disconnect` });
3237
+ const noPerms = await h({ m: 'POST', p: '/v1/financial_connections/sessions', b: 'account_holder[type]=customer&account_holder[customer]=cus_1' });
3238
+ const nope = await h({ m: 'GET', p: '/v1/financial_connections/sessions/fcsess_nope' });
3239
+ return ok(getAcc) && ok(disc) && field(disc, 'status') === 'disconnected' && noPerms.status === 400 && nope.status === 404;
3240
+ }),
3241
+ ),
3242
+ // Financial Connections account transactions: list a linked account's transactions
3243
+ // (also via /v1/financial_connections/transactions?account=). Requires account; 404 unknown.
3244
+ done('stripe.financial_connections.transactions', 'financial_connections', 'Financial Connections: account transactions', 'api', 'niche', () =>
3245
+ withRoot(async (h) => {
3246
+ const sess = await h({ m: 'POST', p: '/v1/financial_connections/sessions', b: 'account_holder[type]=customer&account_holder[customer]=cus_1&permissions[]=transactions' });
3247
+ const acc = await h({ m: 'POST', p: `/v1/financial_connections/sessions/${id(sess)}/link_account` });
3248
+ const txns = await h({ m: 'GET', p: `/v1/financial_connections/accounts/${id(acc)}/transactions` });
3249
+ const tdata = (txns.body as Body).data as Body[];
3250
+ if (tdata.length !== 2 || tdata[0]!.object !== 'financial_connections.transaction' || tdata[0]!.account !== id(acc)) return false;
3251
+ const flat = await h({ m: 'GET', p: `/v1/financial_connections/transactions?account=${id(acc)}` });
3252
+ if (((flat.body as Body).data as Body[]).length !== 2) return false;
3253
+ const noAcct = await h({ m: 'GET', p: '/v1/financial_connections/transactions' });
3254
+ const nope = await h({ m: 'GET', p: '/v1/financial_connections/accounts/fca_nope/transactions' });
3255
+ return noAcct.status === 400 && nope.status === 404;
3256
+ }),
3257
+ ),
3258
+ // Forwarding: PAN forwarding requests. create requires payment_method + url; stores the
3259
+ // (redacted) request + a synthesized response. id fwdr_. Missing url/pm 400; unknown id 404.
3260
+ done('stripe.forwarding.requests', 'core', 'Forwarding: requests (PAN forwarding)', 'api', 'niche', () =>
3261
+ withRoot(async (h) => {
3262
+ const fr = await h({ m: 'POST', p: '/v1/forwarding/requests', b: 'payment_method=pm_z&url=https://api.example.com/charge&request[headers][Idempotency-Key]=abc&replacements[]=card_number&replacements[]=card_expiry' });
3263
+ if (!ok(fr) || field(fr, 'object') !== 'forwarding.request' || !id(fr).startsWith('fwdr_')) return false;
3264
+ if ((field(fr, 'response_details') as Body)?.status !== 200 || (field(fr, 'request_details') as Body)?.http_method !== 'POST') return false;
3265
+ const get = await h({ m: 'GET', p: `/v1/forwarding/requests/${id(fr)}` });
3266
+ const list = await h({ m: 'GET', p: '/v1/forwarding/requests' });
3267
+ if (!ok(get) || ((list.body as Body).data as Body[]).length !== 1) return false;
3268
+ const noPm = await h({ m: 'POST', p: '/v1/forwarding/requests', b: 'url=https://x.test' });
3269
+ const noUrl = await h({ m: 'POST', p: '/v1/forwarding/requests', b: 'payment_method=pm_z' });
3270
+ const nope = await h({ m: 'GET', p: '/v1/forwarding/requests/fwdr_nope' });
3271
+ return noPm.status === 400 && noUrl.status === 400 && nope.status === 404;
3272
+ }),
3273
+ ),
3274
+ // Crypto onramp sessions: create an initialized session with a client_secret + transaction
3275
+ // details, then retrieve. id cos_. Unknown id 404. (The hosted purchase widget is OOS.)
3276
+ done('stripe.crypto.onramp', 'core', 'Crypto onramp sessions', 'api', 'niche', () =>
3277
+ withRoot(async (h) => {
3278
+ const cos = await h({ m: 'POST', p: '/v1/crypto/onramp_sessions', b: 'transaction_details[destination_currency]=eth&transaction_details[destination_network]=ethereum&transaction_details[source_amount]=100' });
3279
+ if (!ok(cos) || field(cos, 'object') !== 'crypto.onramp_session' || field(cos, 'status') !== 'initialized' || !id(cos).startsWith('cos_')) return false;
3280
+ if (typeof field(cos, 'client_secret') !== 'string') return false;
3281
+ if ((field(cos, 'transaction_details') as Body)?.destination_currency !== 'eth') return false;
3282
+ const get = await h({ m: 'GET', p: `/v1/crypto/onramp_sessions/${id(cos)}` });
3283
+ const nope = await h({ m: 'GET', p: '/v1/crypto/onramp_sessions/cos_nope' });
3284
+ return ok(get) && field(get, 'id') === id(cos) && nope.status === 404;
3285
+ }),
3286
+ ),
3287
+
3288
+ // ── HONEST DENOMINATOR GROWTH (real Stripe surfaces NOT yet modeled) ────────────────
3289
+ // The gaps above were closed this cycle; these enumerate genuine remaining surface so the
3290
+ // % stays honest (a missing entry is a hidden gap). Each is a real Stripe API.
3291
+ todo('stripe.treasury.reversals', 'treasury', 'Treasury: credit reversals / debit reversals', 'api', 'niche'),
3292
+ todo('stripe.treasury.financial_addresses', 'treasury', 'Treasury: financial-account ABA/address activation', 'api', 'niche'),
3293
+ todo('stripe.financial_connections.refresh', 'financial_connections', 'Financial Connections: balance/ownership/transaction refresh + inferred balances', 'api', 'niche'),
3294
+ // ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos so
3295
+ // scripts/seed-conformance-issues.ts (A1) turns them into real backlog work. See
3296
+ // pull-audit.json (repo root) for the per-pack pull-vs-read-surface evidence.
3297
+ todo('stripe.connector.pull_payment_methods', 'connector', 'Connector: pull payment methods from the real account (already pushable via COLLECTION, never pulled)', 'connector', 'core'),
3298
+ todo('stripe.connector.pull_setup_intents', 'connector', 'Connector: pull setup intents from the real account (already pushable via COLLECTION, never pulled)', 'connector', 'common'),
3299
+
3300
+ ];
3301
+
3302
+ export function stripeCapabilities(): Promise<CapabilityReport> {
3303
+ return checkCapabilities('stripe', STRIPE_CAPABILITIES);
3304
+ }