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