| 1 | // One-time demo cutover. Preview by default; --apply detaches test billing. |
| 2 | // Keep usage history and flagon-io. Stop on data requiring the general service |
| 3 | // workflow (enterprise, purchased grants, debt, or historical month closes). |
| 4 | import assert from "node:assert/strict"; |
| 5 | import { readFileSync, mkdirSync, writeFileSync } from "node:fs"; |
| 6 | import { randomUUID } from "node:crypto"; |
| 7 | import { getPlatformProxy } from "wrangler"; |
| 8 | |
| 9 | const apply = process.argv.includes("--apply"); |
| 10 | const source = (name) => readFileSync(`services/billing/src/${name}.rs`, "utf8"); |
| 11 | const constant = (file, name) => { |
| 12 | const match = source(file).match(new RegExp(`const ${name}: &str = "([\\s\\S]*?)";`)); |
| 13 | assert.ok(match, `${file}::${name}`); |
| 14 | return match[1]; |
| 15 | }; |
| 16 | const found = constant("detach", "FOUND").replace("{internal}", constant("sales", "INTERNAL_SQL")); |
| 17 | const fixed = [ |
| 18 | ...source("detach") |
| 19 | .split("const STATEMENTS: &[&str] = &[")[1] |
| 20 | .split("\n];")[0] |
| 21 | .matchAll(/"([^"]*)"/g), |
| 22 | ].map((m) => m[1]); |
| 23 | // 16 since migration 0064: test-mode complimentary enrollments end too. |
| 24 | assert.equal(fixed.length, 16, "Review changed detach statements before running"); |
| 25 | const at = new Date().toISOString(); |
| 26 | const month = at.slice(0, 7); |
| 27 | const by = "codex:user-authorized-demo-cutover"; |
| 28 | const dir = "target/billing-downgrade"; |
| 29 | mkdirSync(dir, { recursive: true }); |
| 30 | const configPath = `${dir}/maintenance.json`; |
| 31 | writeFileSync( |
| 32 | configPath, |
| 33 | JSON.stringify({ |
| 34 | name: "g1t-demo-cutover", |
| 35 | account_id: "1e6f2cffa3f445920836e8ebe446bb58", |
| 36 | compatibility_date: "2026-09-26", |
| 37 | services: [{ binding: "BILLING", service: "g1t-billing", remote: true }], |
| 38 | d1_databases: [{ binding: "DB", database_name: "g1t-billing", database_id: "695a6979-fd07-4850-bc97-904a6b4b7a04", remote: true }], |
| 39 | }), |
| 40 | ); |
| 41 | const proxy = await getPlatformProxy({ configPath, persist: false, remoteBindings: true, envFiles: [] }); |
| 42 | try { |
| 43 | const { DB, BILLING } = proxy.env; |
| 44 | const status = await (await BILLING.fetch("https://billing/rpc/status", { method: "POST", body: "{}" })).json(); |
| 45 | assert.equal(status.live, false, "Refusing to detach live billing"); |
| 46 | assert.equal(status.enabled, true); |
| 47 | const query = async (sql, args = []) => |
| 48 | ( |
| 49 | await DB.prepare(sql) |
| 50 | .bind(...args) |
| 51 | .all() |
| 52 | ).results; |
| 53 | const snapshot = await query("SELECT * FROM billing_accounts WHERE id='ws_flagon-io'"); |
| 54 | assert.equal(snapshot.length, 1); |
| 55 | const tables = new Set((await query("SELECT name FROM sqlite_master WHERE type='table'")).map((r) => r.name)); |
| 56 | const targets = (await query(found, [null, month])).filter((r) => r.workspace !== "flagon-io"); |
| 57 | for (const row of targets) { |
| 58 | assert.equal(row.comped + row.enterprise + row.ai_grants + row.pending_past, 0, `Manual review required: ${row.workspace}`); |
| 59 | assert.equal(row.balance - Math.max(0, row.paid), 0, `Unexpected balance: ${row.workspace}`); |
| 60 | } |
| 61 | const overrides = await query( |
| 62 | "SELECT id FROM billing_accounts WHERE id <> 'ws_flagon-io' AND (team_granted <> 0 OR terms_kind <> 'standard' OR discount_percent <> 0)", |
| 63 | ); |
| 64 | assert.equal(overrides.length, 0, "Additional plan grants need review"); |
| 65 | writeFileSync(`${dir}/demo-${apply ? "apply" : "preview"}.json`, JSON.stringify({ at, status, targets, preserved: snapshot }, null, 2)); |
| 66 | console.log(JSON.stringify({ apply, preserved: "flagon-io", targets })); |
| 67 | if (apply) { |
| 68 | const key = readFileSync(".secrets/stripe-test-key.txt", "utf8").trim(); |
| 69 | assert.match(key, /^(sk|rk)_test_/); |
| 70 | const version = source("stripe").match(/STRIPE_VERSION: &str = "([^"]+)"/)[1]; |
| 71 | const stripe = async (path, method = "GET") => { |
| 72 | const response = await fetch(`https://api.stripe.com/v1${path}`, { method, headers: { Authorization: `Bearer ${key}`, "Stripe-Version": version } }); |
| 73 | const value = await response.json(); |
| 74 | assert.ok(response.ok, `Stripe ${method}: ${response.status} ${value.error?.code ?? ""}`); |
| 75 | assert.equal(value.livemode, false, "Expected a test object"); |
| 76 | return value; |
| 77 | }; |
| 78 | for (const row of targets) { |
| 79 | const workspace = row.workspace; |
| 80 | const subscriptions = await query("SELECT subscription_id FROM subscriptions WHERE workspace=? AND status <> 'canceled'", [workspace]); |
| 81 | const customer = (await query("SELECT customer_id FROM accounts WHERE workspace=?", [workspace]))[0]?.customer_id; |
| 82 | if (customer) await stripe(`/customers/${customer}`); |
| 83 | for (const { subscription_id: id } of subscriptions) { |
| 84 | const subscription = await stripe(`/subscriptions/${id}`); |
| 85 | assert.equal(subscription.customer, customer, "Subscription/customer mismatch"); |
| 86 | if (subscription.status !== "canceled") { |
| 87 | assert.equal((await stripe(`/subscriptions/${id}`, "DELETE")).status, "canceled"); |
| 88 | } |
| 89 | } |
| 90 | // Refresh after Stripe calls. Each workspace's local changes commit |
| 91 | // together; interruption is retryable, including canceled subscriptions. |
| 92 | const current = (await query(found, [workspace, month]))[0]; |
| 93 | assert.equal(current.balance - Math.max(0, current.paid), 0); |
| 94 | assert.equal(current.ai_grants + current.pending_past + current.enterprise + current.comped, 0); |
| 95 | const values = [workspace, at, by, month]; |
| 96 | const statements = fixed |
| 97 | .filter((sql) => tables.has("invoice_attempts") || !sql.includes("invoice_attempts")) |
| 98 | .map((sql) => { |
| 99 | const count = Math.max(...[...sql.matchAll(/\?(\d+)/g)].map((m) => Number(m[1]))); |
| 100 | return DB.prepare(sql).bind(...values.slice(0, count)); |
| 101 | }); |
| 102 | if (current.paid > 0) { |
| 103 | statements.push( |
| 104 | DB.prepare(constant("detach", "LEDGER")).bind( |
| 105 | `led_${randomUUID()}`, |
| 106 | workspace, |
| 107 | -current.paid, |
| 108 | "Reverse demo payments before live billing", |
| 109 | `test_detach/${workspace}/${at}`, |
| 110 | by, |
| 111 | at, |
| 112 | null, |
| 113 | ), |
| 114 | ); |
| 115 | statements.push(DB.prepare(constant("detach", "BALANCE")).bind(workspace, -current.paid, at)); |
| 116 | } |
| 117 | statements.push( |
| 118 | DB.prepare("INSERT INTO admin_actions (id, account, action, detail, by, created_at) VALUES (?, ?, ?, ?, ?, ?)").bind( |
| 119 | `adm_${randomUUID()}`, |
| 120 | `ws_${workspace}`, |
| 121 | "test_detach", |
| 122 | "User requested Free plan before live launch; Stripe test subscriptions canceled; demo payments reversed; history retained.", |
| 123 | by, |
| 124 | at, |
| 125 | ), |
| 126 | ); |
| 127 | await DB.batch(statements); |
| 128 | console.log(JSON.stringify({ detached: workspace })); |
| 129 | } |
| 130 | assert.deepEqual(await query("SELECT * FROM billing_accounts WHERE id='ws_flagon-io'"), snapshot); |
| 131 | const remaining = (await query(found, [null, month])).filter((r) => r.workspace !== "flagon-io"); |
| 132 | for (const row of remaining) { |
| 133 | for (const field of ["subscriptions", "customer", "card_cached", "balance", "paid", "plan_unreversed", "plan_credit", "pending_now", "open_disputes"]) |
| 134 | assert.equal(row[field], 0, `${row.workspace}: ${field}`); |
| 135 | } |
| 136 | writeFileSync(`${dir}/demo-verified.json`, JSON.stringify({ at: new Date().toISOString(), rows: remaining, flagonPreserved: true }, null, 2)); |
| 137 | console.log("Verified: non-flagon workspaces detached; flagon-io terms preserved."); |
| 138 | } |
| 139 | } finally { |
| 140 | await proxy.dispose(); |
| 141 | } |