| 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 = [...source('detach').split('const STATEMENTS: &[&str] = &[')[1].split('\n];')[0].matchAll(/"([^"]*)"/g)].map(m => m[1]); |
| 18 | // 16 since migration 0064: test-mode complimentary enrollments end too. |
| 19 | assert.equal(fixed.length, 16, 'Review changed detach statements before running'); |
| 20 | const at = new Date().toISOString(); |
| 21 | const month = at.slice(0, 7); |
| 22 | const by = 'codex:user-authorized-demo-cutover'; |
| 23 | const dir = 'target/billing-downgrade'; |
| 24 | mkdirSync(dir, { recursive: true }); |
| 25 | const configPath = `${dir}/maintenance.json`; |
| 26 | writeFileSync(configPath, JSON.stringify({ name: 'g1t-demo-cutover', account_id: '1e6f2cffa3f445920836e8ebe446bb58', compatibility_date: '2026-09-26', |
| 27 | services: [{ binding: 'BILLING', service: 'g1t-billing', remote: true }], |
| 28 | d1_databases: [{ binding: 'DB', database_name: 'g1t-billing', database_id: '695a6979-fd07-4850-bc97-904a6b4b7a04', remote: true }], |
| 29 | })); |
| 30 | const proxy = await getPlatformProxy({ configPath, persist: false, remoteBindings: true, envFiles: [] }); |
| 31 | try { |
| 32 | const { DB, BILLING } = proxy.env; |
| 33 | const status = await (await BILLING.fetch('https://billing/rpc/status', { method: 'POST', body: '{}' })).json(); |
| 34 | assert.equal(status.live, false, 'Refusing to detach live billing'); |
| 35 | assert.equal(status.enabled, true); |
| 36 | const query = async (sql, args = []) => (await DB.prepare(sql).bind(...args).all()).results; |
| 37 | const snapshot = await query("SELECT * FROM billing_accounts WHERE id='ws_flagon-io'"); |
| 38 | assert.equal(snapshot.length, 1); |
| 39 | const tables = new Set((await query("SELECT name FROM sqlite_master WHERE type='table'")).map(r => r.name)); |
| 40 | const targets = (await query(found, [null, month])).filter(r => r.workspace !== 'flagon-io'); |
| 41 | for (const row of targets) { |
| 42 | assert.equal(row.comped + row.enterprise + row.ai_grants + row.pending_past, 0, `Manual review required: ${row.workspace}`); |
| 43 | assert.equal(row.balance - Math.max(0, row.paid), 0, `Unexpected balance: ${row.workspace}`); |
| 44 | } |
| 45 | const overrides = await query("SELECT id FROM billing_accounts WHERE id <> 'ws_flagon-io' AND (team_granted <> 0 OR terms_kind <> 'standard' OR discount_percent <> 0)"); |
| 46 | assert.equal(overrides.length, 0, 'Additional plan grants need review'); |
| 47 | writeFileSync(`${dir}/demo-${apply ? 'apply' : 'preview'}.json`, JSON.stringify({ at, status, targets, preserved: snapshot }, null, 2)); |
| 48 | console.log(JSON.stringify({ apply, preserved: 'flagon-io', targets })); |
| 49 | if (apply) { |
| 50 | const key = readFileSync('.secrets/stripe-test-key.txt', 'utf8').trim(); |
| 51 | assert.match(key, /^(sk|rk)_test_/); |
| 52 | const version = source('stripe').match(/STRIPE_VERSION: &str = "([^"]+)"/)[1]; |
| 53 | const stripe = async (path, method = 'GET') => { |
| 54 | const response = await fetch(`https://api.stripe.com/v1${path}`, { method, headers: { Authorization: `Bearer ${key}`, 'Stripe-Version': version } }); |
| 55 | const value = await response.json(); |
| 56 | assert.ok(response.ok, `Stripe ${method}: ${response.status} ${value.error?.code ?? ''}`); |
| 57 | assert.equal(value.livemode, false, 'Expected a test object'); |
| 58 | return value; |
| 59 | }; |
| 60 | for (const row of targets) { |
| 61 | const workspace = row.workspace; |
| 62 | const subscriptions = await query('SELECT subscription_id FROM subscriptions WHERE workspace=? AND status <> \'canceled\'', [workspace]); |
| 63 | const customer = (await query('SELECT customer_id FROM accounts WHERE workspace=?', [workspace]))[0]?.customer_id; |
| 64 | if (customer) await stripe(`/customers/${customer}`); |
| 65 | for (const { subscription_id: id } of subscriptions) { |
| 66 | const subscription = await stripe(`/subscriptions/${id}`); |
| 67 | assert.equal(subscription.customer, customer, 'Subscription/customer mismatch'); |
| 68 | if (subscription.status !== 'canceled') { |
| 69 | assert.equal((await stripe(`/subscriptions/${id}`, 'DELETE')).status, 'canceled'); |
| 70 | } |
| 71 | } |
| 72 | // Refresh after Stripe calls. Each workspace's local changes commit |
| 73 | // together; interruption is retryable, including canceled subscriptions. |
| 74 | const current = (await query(found, [workspace, month]))[0]; |
| 75 | assert.equal(current.balance - Math.max(0, current.paid), 0); |
| 76 | assert.equal(current.ai_grants + current.pending_past + current.enterprise + current.comped, 0); |
| 77 | const values = [workspace, at, by, month]; |
| 78 | const statements = fixed.filter(sql => tables.has('invoice_attempts') || !sql.includes('invoice_attempts')).map(sql => { |
| 79 | const count = Math.max(...[...sql.matchAll(/\?(\d+)/g)].map(m => Number(m[1]))); |
| 80 | return DB.prepare(sql).bind(...values.slice(0, count)); |
| 81 | }); |
| 82 | if (current.paid > 0) { |
| 83 | statements.push(DB.prepare(constant('detach', 'LEDGER')).bind(`led_${randomUUID()}`, workspace, -current.paid, 'Reverse demo payments before live billing', `test_detach/${workspace}/${at}`, by, at, null)); |
| 84 | statements.push(DB.prepare(constant('detach', 'BALANCE')).bind(workspace, -current.paid, at)); |
| 85 | } |
| 86 | statements.push(DB.prepare('INSERT INTO admin_actions (id, account, action, detail, by, created_at) VALUES (?, ?, ?, ?, ?, ?)').bind(`adm_${randomUUID()}`, `ws_${workspace}`, 'test_detach', 'User requested Free plan before live launch; Stripe test subscriptions canceled; demo payments reversed; history retained.', by, at)); |
| 87 | await DB.batch(statements); |
| 88 | console.log(JSON.stringify({ detached: workspace })); |
| 89 | } |
| 90 | assert.deepEqual(await query("SELECT * FROM billing_accounts WHERE id='ws_flagon-io'"), snapshot); |
| 91 | const remaining = (await query(found, [null, month])).filter(r => r.workspace !== 'flagon-io'); |
| 92 | for (const row of remaining) { |
| 93 | for (const field of ['subscriptions', 'customer', 'card_cached', 'balance', 'paid', 'plan_unreversed', 'plan_credit', 'pending_now', 'open_disputes']) assert.equal(row[field], 0, `${row.workspace}: ${field}`); |
| 94 | } |
| 95 | writeFileSync(`${dir}/demo-verified.json`, JSON.stringify({ at: new Date().toISOString(), rows: remaining, flagonPreserved: true }, null, 2)); |
| 96 | console.log('Verified: non-flagon workspaces detached; flagon-io terms preserved.'); |
| 97 | } |
| 98 | } finally { await proxy.dispose(); } |