Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
432 linesCodeBlameRaw
1// Explicit sandbox-only integration checks against a locally running billing
2// Worker and isolated D1. Never accepts a live Stripe key or a remote Worker.
3import assert from "node:assert/strict";
4import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
5import { resolve } from "node:path";
6import { spawnSync } from "node:child_process";
7import { createHmac } from "node:crypto";
8
9const dir = resolve("target/billing-audit");
10mkdirSync(dir, { recursive: true });
11const key = readFileSync(".secrets/stripe-test-key.txt", "utf8").trim();
12assert.ok(/^(sk|rk)_test_/.test(key), 'Expected a test API key');
13const version = readFileSync("services/billing/src/stripe.rs", "utf8").match(/STRIPE_VERSION: &str = "([^"]+)"/)[1];
14const endpoint = "http://127.0.0.1:8797";
15const secret = "whsec_local_billing_audit_only";
16const fixturePath = resolve(dir, "fixture.json");
17const phase = process.argv[2];
18assert.ok(['seed', 'run', 'lifecycle', 'enterprise', 'protocol', 'complimentary', 'cleanup'].includes(phase), 'Use seed, run, lifecycle, enterprise, protocol, complimentary, or cleanup');
19// A local Worker is not enough isolation: production replays all events in
20// its Stripe account. The test and live keys below were verified to share
21// this account during the audit. Never seed or run fixtures there again.
22const productionStripeAccount = 'acct_1UMzU3CgDtq6trU5';
23const auditAccount = await stripe('/account');
24assert.match(auditAccount.id, /^acct_/);
25if (phase !== 'cleanup') {
26 assert.notEqual(auditAccount.id, productionStripeAccount,
27 'Use a separate Stripe Sandbox: this account is connected to production event replay. No test objects were created.');
28}
29async function stripe(path, fields, method = fields ? "POST" : "GET", apiVersion = version) {
30 const response = await fetch(`https://api.stripe.com/v1${path}`, {
31 method, headers: { Authorization: `Bearer ${key}`, "Stripe-Version": apiVersion, ...(fields ? { "Content-Type": "application/x-www-form-urlencoded" } : {}) },
32 body: fields ? new URLSearchParams(fields) : undefined,
33 });
34 const result = await response.json();
35 if (!response.ok) throw Error(`Stripe ${method} ${path}: ${response.status} ${result.error?.code ?? result.error?.type ?? "error"}`);
36 assert.notEqual(result.livemode, true, "Sandbox returned a live object");
37 return result;
38}
39async function rpc(method, args) {
40 const response = await fetch(`${endpoint}/rpc/${method}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(args) });
41 if (!response.ok) throw Error(`Local billing ${method}: HTTP ${response.status}: ${(await response.text()).slice(0, 500)}`);
42 return response.json();
43}
44function localSQL(sql) {
45 const file = resolve(dir, "fixture.sql"); writeFileSync(file, sql);
46 const result = spawnSync(process.execPath, ["node_modules/wrangler/bin/wrangler.js", "d1", "execute", "billing-audit", "--local", "--config", "target/billing-audit/wrangler.json", "--persist-to", "target/billing-audit/state", "--file", file, "--json"], { encoding: "utf8" });
47 if (result.status !== 0) throw Error(`Local fixture SQL failed: ${result.stderr.slice(0, 500)}`);
48}
49if (phase === 'protocol') {
50 assert.equal((await rpc('status', {})).live, false);
51 const event = { id: `evt_audit_mode_${Date.now()}`, type: 'audit.mode_check', livemode: true, data: { object: {} } };
52 const deliver = () => {
53 const payload = JSON.stringify(event), time = Math.floor(Date.now() / 1000);
54 const signature = `t=${time},v1=${createHmac('sha256', secret).update(`${time}.${payload}`).digest('hex')}`;
55 return rpc('stripe_webhook', { payload, signature });
56 };
57 await assert.rejects(deliver(), /HTTP 500/);
58 event.livemode = false;
59 assert.deepEqual(await deliver(), { ok: true, value: true });
60 assert.deepEqual(await deliver(), { ok: true, value: false });
61 console.log('PASS wrong-mode delivery remains unclaimed and can be processed once after mode correction');
62} else if (phase === "seed") {
63 const workspace = `audit-${Date.now()}`;
64 const customer = await stripe("/customers", { name: "Disposable g1t billing audit", "metadata[g1t_audit]": workspace, payment_method: "pm_card_visa", "invoice_settings[default_payment_method]": "pm_card_visa", "address[country]": "US", "address[state]": "CO", "address[postal_code]": "80202", "address[line1]": "123 Test Street", "address[city]": "Denver" });
65 assert.match(customer.id, /^cus_[a-zA-Z0-9]+$/);
66 writeFileSync(fixturePath, JSON.stringify({ workspace, customer: customer.id, stripeAccount: auditAccount.id }));
67 localSQL(`INSERT INTO accounts (workspace,balance_micros,customer_id,created_at) VALUES ('${workspace}',0,'${customer.id}','${new Date().toISOString()}');`);
68 console.log(JSON.stringify({ seeded: workspace, mode: "sandbox", database: "local" }));
69} else if (phase === "run") {
70 const fixture = JSON.parse(readFileSync(fixturePath, "utf8"));
71 assert.equal(fixture.stripeAccount, auditAccount.id, 'Fixture belongs to a different Stripe account; seed an isolated fixture');
72 const { workspace, customer } = fixture;
73 const actor = { id: "usr_audit", username: "audit-owner", verified: true, workspaces: [{ slug: workspace, role: "owner" }] };
74 const checks = [];
75 const passed = name => { checks.push(name); console.log(`PASS ${name}`); };
76 assert.equal((await rpc("status", {})).live, false);
77 const started = await rpc("subscribe", { workspace, actor, feature: "plan", returnUrl: `${endpoint}/return` });
78 assert.equal(started.ok, true, JSON.stringify(started));
79 const features = await rpc("features", { workspace, viewer: actor });
80 assert.ok(features.ok && features.value.some(f => f.plan.feature === "plan" && f.on), JSON.stringify(features));
81 passed("saved-card subscription activates through the real billing Worker");
82 const subscriptions = await stripe(`/subscriptions?customer=${customer}&status=all`);
83 assert.equal(subscriptions.data.length, 1);
84 fixture.subscription = subscriptions.data[0].id;
85 writeFileSync(fixturePath, JSON.stringify(fixture));
86 const again = await rpc("subscribe", { workspace, actor, feature: "plan", returnUrl: `${endpoint}/return` });
87 assert.equal(again.ok, false);
88 assert.equal((await stripe(`/subscriptions?customer=${customer}&status=all`)).data.length, 1);
89 passed("repeat subscribe cannot create a second active plan");
90 for (const resume of [false, true]) {
91 const changed = await rpc("cancel_subscription", { workspace, actor, feature: "plan", resume });
92 assert.equal(changed.ok, true, JSON.stringify(changed));
93 assert.equal((await stripe(`/subscriptions/${fixture.subscription}`)).cancel_at_period_end, !resume);
94 }
95 passed("period-end cancellation and resumption reach Stripe");
96 const charge = { workspace, feature: "deployments", costMicros: 15_000_000, description: "Sandbox audit usage", repo: null, reference: `audit/${workspace}` };
97 const posts = await Promise.all([rpc("charge_feature", charge), rpc("charge_feature", charge)]);
98 assert.equal(posts.filter(p => p.ok && p.value).length, 1);
99 const account = await rpc("account", { workspace, viewer: actor });
100 assert.equal(account.value.balanceMicros, -8_000_000, JSON.stringify(account));
101 passed("concurrent usage delivery consumes included credit and posts excess once");
102 const closed = await rpc("close_workspace", { workspace, actor, dryRun: false });
103 assert.equal(closed.ok, true, JSON.stringify(closed));
104 const invoices = await rpc("invoices", { workspace, viewer: actor });
105 assert.ok(invoices.ok && invoices.value.some(i => i.status === "paid"), JSON.stringify(invoices));
106 assert.equal((await rpc("account", { workspace, viewer: actor })).value.balanceMicros, 0);
107 const usageInvoice = (await stripe(`/invoices?customer=${customer}&limit=20`)).data.find(i => i.metadata?.g1t_workspace === workspace);
108 assert.ok(usageInvoice?.charge);
109 passed("closing collects exactly the excess usage on a paid Stripe invoice");
110 await stripe("/refunds", { charge: usageInvoice.charge, amount: "400" });
111 const chargeObject = await stripe(`/charges/${usageInvoice.charge}`);
112 const event = { id: `evt_local_audit_${workspace}`, type: "charge.refunded", livemode: false, data: { object: chargeObject } };
113 const payload = JSON.stringify(event), time = Math.floor(Date.now() / 1000);
114 const signature = `t=${time},v1=${createHmac("sha256", secret).update(`${time}.${payload}`).digest("hex")}`;
115 assert.equal((await rpc("stripe_webhook", { payload, signature })).ok, true);
116 assert.equal((await rpc("stripe_webhook", { payload, signature })).ok, true);
117 assert.equal((await rpc("account", { workspace, viewer: actor })).value.balanceMicros, -4_000_000);
118 passed("real sandbox refund and locally signed duplicate delivery reverse credit once");
119 const bad = await rpc("stripe_webhook", { payload, signature: "t=1,v1=00" });
120 assert.equal(bad.ok, false);
121 passed("invalid webhook signatures are rejected");
122 writeFileSync(resolve(dir, "sandbox-results.json"), JSON.stringify({ checkedAt: new Date().toISOString(), mode: "sandbox", worker: "local", checks, limitations: ["No public webhook delivery", "No browser Checkout or 3DS", "No renewal test clock", "No production deployment"] }, null, 2));
123} else if (phase === "lifecycle") {
124 assert.equal((await rpc('status', {})).live, false);
125 const workspace = `audit-cycle-${Date.now()}`;
126 const actor = { id: 'usr_audit', username: 'audit-owner', verified: true, workspaces: [{ slug: workspace, role: 'owner' }] };
127 const checks = [];
128 const passed = message => { checks.push(message); console.log(`PASS ${message}`); };
129 const clock = await stripe('/test_helpers/test_clocks', { frozen_time: String(Math.floor(Date.now() / 1000)), name: workspace });
130 const evidence = { workspace, clock: clock.id, stripeAccount: auditAccount.id, checks };
131 const save = () => writeFileSync(resolve(dir, 'lifecycle-results.json'), JSON.stringify({ ...evidence, checkedAt: new Date().toISOString() }, null, 2));
132 save();
133 const customer = await stripe('/customers', { name: workspace, test_clock: clock.id, payment_method: 'pm_card_visa', 'invoice_settings[default_payment_method]': 'pm_card_visa', 'metadata[workspace]': workspace, 'address[country]': 'US', 'address[state]': 'CO', 'address[postal_code]': '80202', 'address[line1]': '123 Test Street', 'address[city]': 'Denver' });
134 evidence.customer = customer.id; save();
135 localSQL(`INSERT INTO accounts (workspace,balance_micros,customer_id,created_at) VALUES ('${workspace}',0,'${customer.id}','${new Date().toISOString()}');`);
136 const started = await rpc('subscribe', { workspace, actor, feature: 'plan', returnUrl: `${endpoint}/return` });
137 assert.equal(started.ok, true, JSON.stringify(started));
138 const subscription = (await stripe(`/subscriptions?customer=${customer.id}&status=all`)).data[0];
139 assert.equal(subscription.status, 'active');
140 evidence.subscription = subscription.id; save();
141 const end = sub => sub.current_period_end ?? sub.items.data[0].current_period_end;
142 const advance = async frozenTime => {
143 await stripe(`/test_helpers/test_clocks/${clock.id}/advance`, { frozen_time: String(frozenTime) });
144 for (let attempt = 0; attempt < 30; attempt++) {
145 const current = await stripe(`/test_helpers/test_clocks/${clock.id}`);
146 if (current.status === 'ready') return;
147 await new Promise(resolve => setTimeout(resolve, 1000));
148 }
149 throw Error('Stripe clock did not become ready within 30 polls');
150 };
151 // Exercise the actual subscription handler and signed delivery protocol.
152 // These locally signed events deliberately fetch Stripe's current object;
153 // public webhook transport is a separate test, not claimed here.
154 const deliver = async (type, object, suffix) => {
155 // Read real objects in the production destination's schema while the
156 // Worker keeps using its independently pinned request API version.
157 const webhookVersion = '2026-08-26.dahlia';
158 const resource = type.startsWith('invoice.') ? 'invoices' : 'subscriptions';
159 object = await stripe(`/${resource}/${object.id}`, undefined, 'GET', webhookVersion);
160 if (resource === 'invoices') {
161 assert.equal(object.subscription, undefined);
162 assert.equal(object.parent?.subscription_details?.subscription, subscription.id);
163 }
164 const payload = JSON.stringify({ id: `evt_audit_${workspace}_${suffix}`, api_version: webhookVersion, type, livemode: false, data: { object } });
165 const time = Math.floor(Date.now() / 1000);
166 const signature = `t=${time},v1=${createHmac('sha256', secret).update(`${time}.${payload}`).digest('hex')}`;
167 const result = await rpc('stripe_webhook', { payload, signature });
168 assert.equal(result.ok, true, JSON.stringify(result));
169 };
170 const plan = async () => (await rpc('features', { workspace, viewer: actor })).value.find(f => f.plan.feature === 'plan');
171 try {
172 const waiver = await rpc('admin_set_allowances', { id: `ws_${workspace}`, allowances: { plan: true }, note: 'Audit: must not silently waive an active Stripe plan', by: 'audit' });
173 assert.equal(waiver.ok, false);
174 assert.match(waiver.error.message, /subscription/i);
175 assert.equal((await stripe(`/subscriptions/${subscription.id}`)).status, 'active');
176 passed('staff fee waiver refuses a still-billable Stripe subscription');
177 await advance(end(subscription) + 7200);
178 let renewed = await stripe(`/subscriptions/${subscription.id}`);
179 let renewalInvoice = await stripe(`/invoices/${renewed.latest_invoice}`);
180 // Stripe can leave the renewal draft until the next simulation advance.
181 if (renewalInvoice.status === 'draft') {
182 await stripe(`/invoices/${renewalInvoice.id}/finalize`, {});
183 renewalInvoice = await stripe(`/invoices/${renewalInvoice.id}`);
184 }
185 if (renewalInvoice.status === 'open') renewalInvoice = await stripe(`/invoices/${renewalInvoice.id}/pay`, {});
186 assert.equal(renewalInvoice.status, 'paid');
187 assert.notEqual(renewalInvoice.id, subscription.latest_invoice);
188 await deliver('invoice.paid', renewalInvoice, 'renewed');
189 await deliver('invoice.paid', renewalInvoice, 'renewed');
190 assert.equal((await plan()).on, true);
191 passed('test-clock renewal collects and duplicate delivery retains paid access'); save();
192 // Attach succeeds, subsequent charge fails: Stripe's documented test PM.
193 const declined = await stripe('/payment_methods/pm_card_chargeCustomerFail/attach', { customer: customer.id });
194 await stripe(`/subscriptions/${subscription.id}`, { default_payment_method: declined.id });
195 renewed = await stripe(`/subscriptions/${subscription.id}`);
196 await advance(end(renewed) + 7200);
197 let failedSub = await stripe(`/subscriptions/${subscription.id}`);
198 let failedInvoice = await stripe(`/invoices/${failedSub.latest_invoice}`);
199 if (failedInvoice.status === 'draft') {
200 await stripe(`/invoices/${failedInvoice.id}/finalize`, {});
201 failedInvoice = await stripe(`/invoices/${failedInvoice.id}`);
202 }
203 if (!failedInvoice.attempted) {
204 await assert.rejects(stripe(`/invoices/${failedInvoice.id}/pay`, {}), /Stripe/);
205 }
206 failedSub = await stripe(`/subscriptions/${subscription.id}`);
207 assert.equal(failedSub.status, 'past_due');
208 await deliver('invoice.payment_failed', failedInvoice, 'declined');
209 assert.equal((await plan()).on, false);
210 passed('declined renewal removes paid access after reconciliation'); save();
211 await stripe(`/subscriptions/${subscription.id}`, { default_payment_method: customer.invoice_settings.default_payment_method });
212 const recovered = await stripe(`/invoices/${failedInvoice.id}/pay`, {});
213 assert.equal(recovered.status, 'paid');
214 await deliver('invoice.paid', recovered, 'recovered');
215 await deliver('invoice.payment_failed', failedInvoice, 'late-decline');
216 assert.equal((await plan()).on, true);
217 passed('payment recovery restores access and stale decline cannot undo it'); save();
218 const cancel = await rpc('cancel_subscription', { workspace, actor, feature: 'plan', resume: false });
219 assert.equal(cancel.ok, true, JSON.stringify(cancel));
220 assert.equal((await plan()).on, true);
221 const beforeEnd = await stripe(`/subscriptions/${subscription.id}`);
222 await advance(end(beforeEnd) + 60);
223 const ended = await stripe(`/subscriptions/${subscription.id}`);
224 assert.equal(ended.status, 'canceled');
225 await deliver('customer.subscription.deleted', ended, 'ended');
226 assert.equal((await plan()).on, false);
227 passed('period-end cancellation retains access until expiry and then removes it');
228 evidence.webhookObjectVersion = '2026-08-26.dahlia';
229 evidence.transport = 'Locally signed RPC delivery; not public Stripe delivery';
230 evidence.complete = true; save();
231 } finally {
232 await stripe(`/test_helpers/test_clocks/${clock.id}`, undefined, 'DELETE');
233 evidence.cleaned = true; save();
234 }
235} else if (phase === 'enterprise') {
236 const workspace = `audit-enterprise-${Date.now()}`;
237 const enterprise = `ent_${workspace}`;
238 const customer = await stripe('/customers', { name: workspace, email: 'billing-audit@example.com', 'address[country]': 'US', 'address[state]': 'CO', 'address[postal_code]': '80202', 'address[line1]': '123 Test Street', 'address[city]': 'Denver' });
239 const now = new Date().toISOString();
240 localSQL(`INSERT INTO billing_accounts (id,kind,name,customer_id,created_by,created_at) VALUES ('${enterprise}','enterprise','Audit enterprise','${customer.id}','audit','${now}');
241 INSERT INTO account_members VALUES ('${workspace}','${enterprise}','audit','${now}');
242 INSERT INTO accounts (workspace,balance_micros,created_at) VALUES ('${workspace}',-10000001,'${now}');`);
243 const checks = [];
244 try {
245 // Wait for both requests before cleanup, even when Stripe returns its
246 // transient idempotency_key_in_use response to one concurrent caller.
247 const outcomes = await Promise.allSettled([rpc('admin_invoice_enterprise',{id:enterprise,by:'audit'}),rpc('admin_invoice_enterprise',{id:enterprise,by:'audit'})]);
248 const requests = outcomes.filter(r=>r.status==='fulfilled').map(r=>r.value);
249 for (const result of outcomes) assert.equal(result.status,'fulfilled', result.reason?.message);
250 assert.ok(requests.some(r => r.ok),JSON.stringify(requests));
251 const invoices = (await stripe(`/invoices?customer=${customer.id}`)).data;
252 assert.equal(invoices.length,1);
253 assert.equal(invoices[0].subtotal,1001);
254 for (const result of requests.filter(r => r.ok)) assert.equal(result.value.lines[0].amountMicros,10010000);
255 checks.push('concurrent enterprise requests create one invoice with exact cent-rounded line accounting');
256 console.log(`PASS ${checks.at(-1)}`);
257 const again = await rpc('admin_invoice_enterprise',{id:enterprise,by:'audit'});
258 assert.equal(again.ok,false);
259 assert.equal((await stripe(`/invoices?customer=${customer.id}`)).data.length,1);
260 checks.push('already-invoiced debt cannot be invoiced twice');
261 console.log(`PASS ${checks.at(-1)}`);
262 localSQL(`UPDATE accounts SET balance_micros=balance_micros-5000000 WHERE workspace='${workspace}';`);
263 const more = await rpc('admin_invoice_enterprise',{id:enterprise,by:'audit'});
264 assert.equal(more.ok,true,JSON.stringify(more));
265 const all = (await stripe(`/invoices?customer=${customer.id}`)).data;
266 assert.equal(all.length,2);
267 assert.equal(all.reduce((sum,i)=>sum+i.subtotal,0),1501);
268 checks.push('subsequent usage invoices only debt not covered by the first invoice');
269 console.log(`PASS ${checks.at(-1)}`);
270 writeFileSync(resolve(dir,'enterprise-results.json'),JSON.stringify({checkedAt:new Date().toISOString(),stripeAccount:auditAccount.id,workspace,checks},null,2));
271 } finally { await stripe(`/customers/${customer.id}`,undefined,'DELETE'); }
272} else if (phase === 'complimentary') {
273 // Staff enrollment, terms changes, admission, conversion (paid, 3DS,
274 // declined, no card, repeated) and revocation, through the real Worker
275 // against real sandbox subscriptions. Webhooks are not delivered: each
276 // step's synchronous path is what is exercised.
277 assert.equal((await rpc('status', {})).live, false);
278 const stamp = Date.now();
279 const checks = [];
280 const made = [];
281 const passed = message => { checks.push(message); console.log(`PASS ${message}`); };
282 const actorOf = workspace => ({ id: 'usr_audit', username: 'audit-owner', verified: true, workspaces: [{ slug: workspace, role: 'owner' }] });
283 const enroll = (workspace, allowance, fee = 0) =>
284 rpc('admin_set_complimentary', { workspace, allowance_micros: allowance, unlimited: false, fee_cents: fee, note: 'sandbox audit', by: 'audit' });
285 const standing = workspace => rpc('admin_complimentary', { workspace });
286 const reserve = (workspace, estimate) =>
287 rpc('reserve', { workspace, repo: { namespace: workspace, name: 'app' }, public: false, kind: 'workflow', estimateMicros: estimate });
288 const card = async (customer, method) => {
289 const pm = await stripe(`/payment_methods/${method}/attach`, { customer });
290 await stripe(`/customers/${customer}`, { 'invoice_settings[default_payment_method]': pm.id });
291 };
292 const convert = workspace => rpc('convert_to_paid', { actor: actorOf(workspace), workspace, returnUrl: `${endpoint}/return` });
293 try {
294 // Enrollment: a real customer and subscription, then visible.
295 const a = `cmp-a-${stamp}`;
296 const enrolled = await enroll(a, 100_000_000);
297 assert.equal(enrolled.ok, true, JSON.stringify(enrolled));
298 made.push(enrolled.value.customerId);
299 assert.equal(enrolled.value.status, 'active', JSON.stringify(enrolled.value));
300 const sub = await stripe(`/subscriptions/${enrolled.value.subscriptionId}`);
301 assert.equal(sub.status, 'active');
302 assert.equal(sub.metadata.g1t_grant, enrolled.value.grantId);
303 assert.equal(sub.metadata.g1t_allowance_micros, '100000000');
304 assert.equal(sub.items.data[0].price.unit_amount, 0);
305 const ent = await rpc('entitlements', { workspace: a });
306 assert.equal(ent.plan, 'complimentary');
307 assert.equal(ent.complimentary.allowanceMicros, 100_000_000);
308 assert.equal(ent.complimentary.remainingMicros, 100_000_000);
309 assert.equal(ent.hasPlan, true);
310 passed('staff enrollment creates a real $0 subscription with the grant in its metadata, then the workspace sees it');
311 const again = await enroll(a, 100_000_000);
312 assert.equal(again.value.subscriptionId, enrolled.value.subscriptionId);
313 assert.equal((await stripe(`/subscriptions?customer=${enrolled.value.customerId}&status=all`)).data.length, 1);
314 passed('changing terms keeps one subscription');
315 // Admission: the allowance bounds new work, card or not; no on demand.
316 await card(enrolled.value.customerId, 'pm_card_visa');
317 const big = await reserve(a, 200_000_000);
318 assert.equal(big.ok, false, JSON.stringify(big));
319 assert.match(big.error.message, /complimentary allowance/);
320 const small = await reserve(a, 10_000_000);
321 assert.equal(small.ok, true, JSON.stringify(small));
322 const held = await rpc('entitlements', { workspace: a });
323 assert.ok(held.complimentary.remainingMicros < 100_000_000 && held.complimentary.heldMicros > 0, JSON.stringify(held.complimentary));
324 passed('admission counts running work against the allowance and refuses past it even with a saved card');
325 // A custom fee is its own price on the same subscription.
326 const fee = await enroll(a, 50_000_000, 2_500);
327 assert.equal(fee.ok, true, JSON.stringify(fee));
328 const feeSub = await stripe(`/subscriptions/${enrolled.value.subscriptionId}`);
329 assert.equal(feeSub.items.data[0].price.unit_amount, 2_500);
330 assert.equal(feeSub.metadata.g1t_fee_cents, '2500');
331 assert.equal(feeSub.collection_method, 'send_invoice');
332 assert.equal((await standing(a)).periods.length, 3);
333 passed('a custom monthly fee changes the subscription price apart from the allowance, each term kept as a period');
334 assert.equal((await enroll(a, 50_000_000, 0)).ok, true);
335 // Conversion with a card that pays: the same subscription becomes the plan.
336 const converted = await convert(a);
337 assert.equal(converted.ok, true, JSON.stringify(converted));
338 assert.equal(converted.value.status, 'converted', JSON.stringify(converted.value));
339 const paid = await stripe(`/subscriptions/${enrolled.value.subscriptionId}`);
340 assert.equal(paid.items.data[0].price.unit_amount, 2_100);
341 assert.equal((await stripe(`/subscriptions?customer=${enrolled.value.customerId}&status=all`)).data.length, 1);
342 assert.equal((await rpc('entitlements', { workspace: a })).plan, 'paid');
343 const ended = await standing(a);
344 assert.equal(ended.status, 'ended');
345 assert.equal(ended.endedReason, 'converted');
346 assert.ok(ended.moneyReceivedMicros >= 21_000_000, JSON.stringify(ended));
347 passed('conversion with a paying card turns the same subscription into the plan and records its payment as money received');
348
349 // 3DS: nothing changes until the bank is answered; asking again is the same invoice.
350 const b = `cmp-b-${stamp}`;
351 const bEnrolled = await enroll(b, 100_000_000);
352 made.push(bEnrolled.value.customerId);
353 await card(bEnrolled.value.customerId, 'pm_card_authenticationRequired');
354 const needs = await convert(b);
355 assert.equal(needs.value?.status, 'needs_action', JSON.stringify(needs));
356 assert.match(needs.value.url, /^https:\/\/invoice\.stripe\.com\//);
357 const invoicesBefore = (await stripe(`/invoices?subscription=${bEnrolled.value.subscriptionId}`)).data.length;
358 const twice = await convert(b);
359 assert.equal(twice.value?.status, 'needs_action', JSON.stringify(twice));
360 // Hosted invoice links are re-signed on each read: the invoice count below is the check.
361 assert.equal((await stripe(`/invoices?subscription=${bEnrolled.value.subscriptionId}`)).data.length, invoicesBefore);
362 assert.equal((await rpc('entitlements', { workspace: b })).plan, 'complimentary');
363 assert.equal((await stripe(`/subscriptions/${bEnrolled.value.subscriptionId}`)).items.data[0].price.unit_amount, 0);
364 assert.equal((await standing(b)).status, 'active');
365 passed('3DS leaves the enrollment as it was, and asking again returns the same invoice rather than a new one');
366
367 // Declined: the enrollment stands, one subscription.
368 const c = `cmp-c-${stamp}`;
369 const cEnrolled = await enroll(c, 100_000_000);
370 made.push(cEnrolled.value.customerId);
371 await card(cEnrolled.value.customerId, 'pm_card_chargeCustomerFail');
372 const declined = await convert(c);
373 assert.equal(declined.value?.status, 'declined', JSON.stringify(declined));
374 assert.equal((await rpc('entitlements', { workspace: c })).plan, 'complimentary');
375 assert.equal((await stripe(`/subscriptions?customer=${cEnrolled.value.customerId}&status=all`)).data.length, 1);
376 passed('a declined card leaves the enrollment and its one subscription unchanged');
377
378 // No card at all: a card check page, nothing charged.
379 const d = `cmp-d-${stamp}`;
380 const dEnrolled = await enroll(d, 100_000_000);
381 made.push(dEnrolled.value.customerId);
382 const needsCard = await convert(d);
383 assert.equal(needsCard.value?.status, 'needs_card', JSON.stringify(needsCard));
384 assert.match(needsCard.value.url, /^https:\/\/checkout\.stripe\.com\//);
385 passed('without a saved card, conversion sends the owner to a card check first');
386 const member = { id: 'usr_member', username: 'member', verified: true, workspaces: [{ slug: d, role: 'member' }] };
387 assert.equal((await rpc('convert_to_paid', { actor: member, workspace: d, returnUrl: `${endpoint}/return` })).ok, false);
388 passed('a member who does not manage billing cannot convert');
389
390 // Charged past the allowance (employees, friends): refused without a
391 // card, then admitted under the workspace's limits once it has one.
392 const e = `cmp-e-${stamp}`;
393 const eEnrolled = await rpc('admin_set_complimentary', { workspace: e, allowance_micros: 5_000_000, unlimited: false, fee_cents: 0, charge_past_allowance: true, note: 'sandbox audit', by: 'audit' });
394 assert.equal(eEnrolled.ok, true, JSON.stringify(eEnrolled));
395 made.push(eEnrolled.value.customerId);
396 assert.equal(eEnrolled.value.chargePastAllowance, true);
397 assert.equal((await stripe(`/subscriptions/${eEnrolled.value.subscriptionId}`)).metadata.g1t_past_allowance, 'charge');
398 const noCard = await reserve(e, 20_000_000);
399 assert.equal(noCard.ok, false, JSON.stringify(noCard));
400 assert.match(noCard.error.message, /Add a card/);
401 const eEnt = await rpc('entitlements', { workspace: e });
402 assert.equal(eEnt.complimentary.chargePastAllowance, true);
403 assert.equal(eEnt.complimentary.hasCard, false);
404 passed('charged past the allowance, without a card: work past it is refused and the owner is told to add one');
405 await card(eEnrolled.value.customerId, 'pm_card_visa');
406 localSQL(`UPDATE accounts SET card_last4 = '4242' WHERE workspace = '${e}';`);
407 const withCard = await reserve(e, 20_000_000);
408 assert.equal(withCard.ok, true, JSON.stringify(withCard));
409 const eAfter = await rpc('entitlements', { workspace: e });
410 assert.equal(eAfter.hasPlan, true);
411 assert.ok(eAfter.ceilingMicros > 0, JSON.stringify(eAfter));
412 passed('charged past the allowance, with a card: work past it is admitted under the workspace\'s own limits');
413
414 // Revocation: back to Free now, subscription canceled.
415 const revoked = await rpc('admin_end_complimentary', { workspace: d, note: 'sandbox audit', by: 'audit' });
416 assert.equal(revoked.ok, true, JSON.stringify(revoked));
417 assert.equal(revoked.value.status, 'ended');
418 assert.equal((await stripe(`/subscriptions/${dEnrolled.value.subscriptionId}`)).status, 'canceled');
419 assert.equal((await rpc('entitlements', { workspace: d })).plan, 'free');
420 passed('ending an enrollment cancels its subscription and returns the workspace to Free');
421 writeFileSync(resolve(dir, 'complimentary-results.json'), JSON.stringify({ checkedAt: new Date().toISOString(), stripeAccount: auditAccount.id, mode: 'sandbox', worker: 'local', checks,
422 limitations: ['Webhooks not delivered publicly', '3DS not completed in a browser here', 'No test clock renewal of an enrollment'] }, null, 2));
423 } finally {
424 for (const customer of made.filter(Boolean)) {
425 try { await stripe(`/customers/${customer}`, undefined, 'DELETE'); } catch (error) { console.error(`cleanup ${customer}: ${error.message}`); }
426 }
427 }
428} else if (phase === "cleanup") {
429 const { customer } = JSON.parse(readFileSync(fixturePath, "utf8"));
430 await stripe(`/customers/${customer}`, undefined, "DELETE");
431 console.log("Disposable sandbox customer removed; live account untouched.");
432} else throw Error("Use seed, run or cleanup");