g1t/apps/web/app/lib/billing.test.ts
| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { |
| 5 | gigabytes, |
| 6 | shownMeters, |
| 7 | alertText, |
| 8 | alertTone, |
| 9 | cardCheckResult, |
| 10 | dollars, |
| 11 | needsAttention, |
| 12 | usageGlance, |
| 13 | parseCaps, |
| 14 | parseLimitRequest, |
| 15 | parsePrepay, |
| 16 | parseSpendLimit, |
| 17 | planStatus, |
| 18 | readDollars, |
| 19 | requestStatus, |
| 20 | share, |
| 21 | spendPath, |
| 22 | spendRange, |
| 23 | wholeDollars, |
| 24 | } from "./billing.ts"; |
| 25 | |
| 26 | test("money reads as dollars", () => { |
| 27 | assert.equal(dollars(9_500_000), "$9.50"); |
| 28 | assert.equal(dollars(-1_250_000), "−$1.25"); |
| 29 | assert.equal(wholeDollars(1_000_000_000), "$1,000"); |
| 30 | assert.equal(wholeDollars(100_000), "$0.10"); |
| 31 | assert.equal(readDollars("$1,000"), 1_000_000_000); |
| 32 | assert.equal(readDollars(""), null); |
| 33 | assert.equal(readDollars("lots"), null); |
| 34 | }); |
| 35 | |
| 36 | test("a prepayment is $25 or more, by card up to $10,000, by bank transfer from $1,000", () => { |
| 37 | assert.deepEqual(parsePrepay({ amount: "100" }), { ok: true, value: { amountCents: 10_000, method: "card" } }); |
| 38 | assert.deepEqual(parsePrepay({ amount: "100", custom: "250" }), { |
| 39 | ok: true, |
| 40 | value: { amountCents: 25_000, method: "card" }, |
| 41 | }); |
| 42 | assert.equal(parsePrepay({ custom: "10" }).ok, false); |
| 43 | assert.equal(parsePrepay({ custom: "20000" }).ok, false); |
| 44 | assert.equal(parsePrepay({ custom: "500", method: "bank_transfer" }).ok, false); |
| 45 | assert.deepEqual(parsePrepay({ custom: "20000", method: "bank_transfer" }), { |
| 46 | ok: true, |
| 47 | value: { amountCents: 2_000_000, method: "bank_transfer" }, |
| 48 | }); |
| 49 | assert.equal(parsePrepay({}).ok, false); |
| 50 | }); |
| 51 | |
| 52 | test("caps stay in range, and empty goes back to the default", () => { |
| 53 | assert.deepEqual(parseCaps({ run: "", issue: "" }), { ok: true, value: { runCapMicros: null, issueCapMicros: null } }); |
| 54 | assert.deepEqual(parseCaps({ run: "0.10", issue: "1000" }), { |
| 55 | ok: true, |
| 56 | value: { runCapMicros: 100_000, issueCapMicros: 1_000_000_000 }, |
| 57 | }); |
| 58 | assert.equal(parseCaps({ run: "0.05" }).ok, false); |
| 59 | assert.equal(parseCaps({ issue: "2000" }).ok, false); |
| 60 | assert.equal(parseCaps({ run: "two" }).ok, false); |
| 61 | }); |
| 62 | |
| 63 | test("a limit request needs an amount, a reason and a monthly spend; an overage only a reason", () => { |
| 64 | assert.deepEqual(parseLimitRequest({ kind: "limit", amount: "5000", reason: " A launch ", expected: "3000" }), { |
| 65 | ok: true, |
| 66 | value: { kind: "limit", amountMicros: 5_000_000_000, reason: "A launch", expectedMonthlyMicros: 3_000_000_000 }, |
| 67 | }); |
| 68 | assert.equal(parseLimitRequest({ kind: "limit", amount: "5000", reason: "" , expected: "1" }).ok, false); |
| 69 | assert.equal(parseLimitRequest({ kind: "limit", reason: "x", expected: "1" }).ok, false); |
| 70 | assert.deepEqual(parseLimitRequest({ kind: "overage", reason: "A loop ran all night" }), { |
| 71 | ok: true, |
| 72 | value: { kind: "overage", amountMicros: 0, reason: "A loop ran all night", expectedMonthlyMicros: 0 }, |
| 73 | }); |
| 74 | }); |
| 75 | |
| 76 | test("a spend limit is automatic, fixed, the full amount available, or the one-time raise", () => { |
| 77 | assert.deepEqual(parseSpendLimit({ mode: "automatic" }), { ok: true, value: { micros: null, useFull: false, raiseOnce: false } }); |
| 78 | assert.deepEqual(parseSpendLimit({ mode: "fixed", limit: "500" }), { |
| 79 | ok: true, |
| 80 | value: { micros: 500_000_000, useFull: false, raiseOnce: false }, |
| 81 | }); |
| 82 | assert.equal(parseSpendLimit({ mode: "fixed", limit: "0.5" }).ok, false); |
| 83 | assert.deepEqual(parseSpendLimit({ mode: "full" }), { ok: true, value: { micros: null, useFull: true, raiseOnce: false } }); |
| 84 | assert.deepEqual(parseSpendLimit({ mode: "raise" }), { ok: true, value: { micros: null, useFull: false, raiseOnce: true } }); |
| 85 | assert.deepEqual(parseSpendLimit({ mode: "raise", limit: "150" }), { |
| 86 | ok: true, |
| 87 | value: { micros: 150_000_000, useFull: false, raiseOnce: true }, |
| 88 | }); |
| 89 | }); |
| 90 | |
| 91 | test("the spend limit's range: up to the highest ceiling plus prepaid, then once to twice it, then ask", () => { |
| 92 | // A first month at $100, nothing prepaid, the raise unused: $100 alone, $200 once. |
| 93 | const range = spendRange({ availableMicros: 100_000_000, ceilingMicros: 100_000_000, raiseOnceMicros: 200_000_000, raisedAt: null }); |
| 94 | assert.deepEqual(range, { selfServeMicros: 100_000_000, raiseOnceMicros: 200_000_000, raisedAt: null }); |
| 95 | assert.equal(spendPath(80_000_000, range), "self"); |
| 96 | assert.equal(spendPath(100_000_000, range), "self"); |
| 97 | assert.equal(spendPath(150_000_000, range), "raise"); |
| 98 | assert.equal(spendPath(200_000_000, range), "raise"); |
| 99 | assert.equal(spendPath(200_000_001, range), "ask"); |
| 100 | // The raise used: only asking goes further. |
| 101 | const used = spendRange({ availableMicros: 200_000_000, ceilingMicros: 200_000_000, raiseOnceMicros: null, raisedAt: "2026-10-01T00:00:00Z" }); |
| 102 | assert.equal(used.raiseOnceMicros, null); |
| 103 | assert.equal(spendPath(300_000_000, used), "ask"); |
| 104 | // No ceiling: anything goes. |
| 105 | assert.equal(spendPath(1e12, spendRange({ availableMicros: null, ceilingMicros: null, raiseOnceMicros: null })), "self"); |
| 106 | // A raise no higher than what is available is no raise. |
| 107 | assert.equal(spendRange({ availableMicros: 300_000_000, ceilingMicros: 100_000_000, raiseOnceMicros: 300_000_000 }).raiseOnceMicros, null); |
| 108 | }); |
| 109 | |
| 110 | test("the plan card says where the workspace stands", () => { |
| 111 | const free = { plan: "free" as const, trialMicrosLeft: 0, trialVerified: false, firstMonth: false }; |
| 112 | assert.deepEqual(planStatus(null, free), { kind: "free", label: "Free" }); |
| 113 | assert.equal(planStatus(null, { ...free, trialVerified: true, trialMicrosLeft: 5_000_000 }).kind, "trial"); |
| 114 | assert.equal(planStatus(null, { ...free, trialVerified: true }).kind, "free"); |
| 115 | const subscription = { feature: "plan" as const, status: "active" as const, periodEnd: null, startedBy: "a", startedAt: "" }; |
| 116 | assert.deepEqual(planStatus({ on: true, subscription }, { ...free, plan: "paid", firstMonth: true }), { |
| 117 | kind: "paid", |
| 118 | label: "On the g1t plan, first month", |
| 119 | }); |
| 120 | assert.equal(planStatus({ on: true, subscription: { ...subscription, status: "canceling" } }, { ...free, plan: "paid" }).kind, "canceling"); |
| 121 | assert.equal(planStatus({ on: false, subscription: { ...subscription, status: "past_due" } }, free).kind, "past_due"); |
| 122 | assert.equal(planStatus({ on: true, included: true, subscription: null }, free).kind, "comped"); |
| 123 | assert.equal(planStatus(null, { ...free, plan: "internal" }).kind, "comped"); |
| 124 | assert.equal(planStatus(null, { ...free, plan: "enterprise" }).kind, "enterprise"); |
| 125 | }); |
| 126 | |
| 127 | test("the overview's Usage card reads the month by what pays", () => { |
| 128 | const usage = { |
| 129 | free: false, |
| 130 | spentMicros: 14_000_000, |
| 131 | usedMicros: 11_000_000, |
| 132 | byTask: [ |
| 133 | { key: "review", micros: 2_000_000, runs: 4 }, |
| 134 | { key: "implement", micros: 12_000_000, runs: 9 }, |
| 135 | { key: "plan", micros: 0, runs: 1 }, |
| 136 | ], |
| 137 | }; |
| 138 | const entitlements = { includedMicros: 10_000_000, includedUsedMicros: 10_000_000, trialMicrosLeft: 0 }; |
| 139 | const plan = usageGlance({ usage, status: { kind: "paid", label: "" }, entitlements, limit: { spentMicros: 4_000_000, spendLimitMicros: 200_000_000 }, trialMicros: 5_000_000 }); |
| 140 | assert.equal(plan.kind, "plan"); |
| 141 | assert.deepEqual(plan.credit, { label: "Included usage", usedMicros: 10_000_000, ofMicros: 10_000_000 }); |
| 142 | assert.deepEqual(plan.onDemand, { micros: 4_000_000, limitMicros: 200_000_000 }); |
| 143 | // Most first, and nothing that cost nothing. |
| 144 | assert.deepEqual(plan.lines.map((line) => line.label), ["Making changes", "Reviews"]); |
| 145 | const trial = usageGlance({ usage, status: { kind: "trial", label: "" }, entitlements: { ...entitlements, trialMicrosLeft: 1_500_000 }, limit: null, trialMicros: 5_000_000 }); |
| 146 | assert.deepEqual(trial.credit, { label: "Trial credit", usedMicros: 3_500_000, ofMicros: 5_000_000 }); |
| 147 | assert.equal(trial.onDemand, null); |
| 148 | assert.equal(usageGlance({ usage, status: { kind: "comped", label: "" }, entitlements, limit: null, trialMicros: 0 }).kind, "comped"); |
| 149 | assert.equal(usageGlance({ usage, status: { kind: "free", label: "" }, entitlements, limit: null, trialMicros: 0 }).kind, "forge"); |
| 150 | // While g1t charges nothing, usage is shown at cost. |
| 151 | const beta = usageGlance({ usage: { ...usage, free: true }, status: { kind: "paid", label: "" }, entitlements, limit: null, trialMicros: 0 }); |
| 152 | assert.equal(beta.kind, "beta"); |
| 153 | assert.equal(beta.spentMicros, 11_000_000); |
| 154 | }); |
| 155 | |
| 156 | test("a meter's share stays between 0 and 1", () => { |
| 157 | assert.equal(share(5, 10), 0.5); |
| 158 | assert.equal(share(15, 10), 1); |
| 159 | assert.equal(share(5, 0), 0); |
| 160 | assert.equal(share(5, null), 0); |
| 161 | }); |
| 162 | |
| 163 | test("after a card check the owner reads whether the trial started", () => { |
| 164 | assert.match(cardCheckResult({ trialVerified: true, trialMicrosLeft: 5_000_000 }, 5_000_000), /[$]5 trial is ready/); |
| 165 | assert.match(cardCheckResult({ trialVerified: true, trialMicrosLeft: 0 }, 5_000_000), /Prepaid cards can still pay for the plan/); |
| 166 | assert.match(cardCheckResult({ trialVerified: false, trialMicrosLeft: 0 }, 5_000_000), /did not finish/); |
| 167 | }); |
| 168 | |
| 169 | test("alerts read as sentences", () => { |
| 170 | const alert = { meter: "included", level: 90, usedMicros: 9_000_000, limitMicros: 10_000_000, message: "" }; |
| 171 | assert.equal(alertText(alert), "90% of the plan's included usage: $9.00 of $10.00."); |
| 172 | assert.equal(alertText({ ...alert, meter: "spend_limit", level: 100 }), "All of your spend limit: $9.00 of $10.00."); |
| 173 | assert.equal(alertTone(50), "ok"); |
| 174 | assert.equal(alertTone(75), "warning"); |
| 175 | assert.equal(alertTone(100), "stopped"); |
| 176 | }); |
| 177 | |
| 178 | test("a workspace needs attention when paused, on a spike, or at 90%", () => { |
| 179 | assert.equal(needsAttention(null), false); |
| 180 | assert.equal(needsAttention({ paused: null, spike: null, alerts: [] }), false); |
| 181 | assert.equal(needsAttention({ paused: "Spend spike", spike: null, alerts: [] }), true); |
| 182 | const spike = { id: "s", status: "continued", hourMicros: 1, averageMicros: 1, detectedAt: "" }; |
| 183 | assert.equal(needsAttention({ paused: null, spike, alerts: [] }), false); |
| 184 | assert.equal(needsAttention({ paused: null, spike: { ...spike, status: "open" }, alerts: [] }), true); |
| 185 | const at = (level: number) => ({ meter: "spend_limit", level, usedMicros: 0, limitMicros: 0, message: "" }); |
| 186 | assert.equal(needsAttention({ paused: null, spike: null, alerts: [at(75)] }), false); |
| 187 | assert.equal(needsAttention({ paused: null, spike: null, alerts: [at(90)] }), true); |
| 188 | }); |
| 189 | |
| 190 | test("a request says where it stands", () => { |
| 191 | const request = { |
| 192 | id: "r", |
| 193 | workspace: "acme", |
| 194 | kind: "limit", |
| 195 | amountMicros: 1, |
| 196 | reason: "", |
| 197 | expectedMonthlyMicros: 0, |
| 198 | status: "open", |
| 199 | createdBy: "a", |
| 200 | createdAt: "", |
| 201 | }; |
| 202 | assert.equal(requestStatus(request), "Waiting for an answer"); |
| 203 | assert.equal(requestStatus({ ...request, status: "approved", decidedMicros: 2_000_000_000 }), "Approved at $2,000"); |
| 204 | assert.equal(requestStatus({ ...request, status: "declined" }), "Declined"); |
| 205 | }); |
| 206 | |
| 207 | const METERS = [ |
| 208 | { key: "agents", label: "Agents & sandboxes", micros: 1_200_000, quantity: "4 runs" }, |
| 209 | { key: "builds", label: "Builds", micros: 0, quantity: null }, |
| 210 | { key: "requests", label: "Requests & CPU", micros: 0, quantity: null }, |
| 211 | { key: "domains", label: "Custom domains", micros: 120_000, quantity: "1 custom domain" }, |
| 212 | { key: "git_storage", label: "Git operations & storage", micros: 0, quantity: "1,200 git operations" }, |
| 213 | { key: "search_scans", label: "Search & security scans", micros: 3_000, quantity: null }, |
| 214 | ]; |
| 215 | |
| 216 | test("the plan shows every meter, with no quota beside any of them", () => { |
| 217 | const shown = shownMeters(METERS, true); |
| 218 | assert.deepEqual( |
| 219 | shown.map((m) => m.key), |
| 220 | ["agents", "builds", "requests", "domains", "git_storage", "search_scans"], |
| 221 | ); |
| 222 | for (const meter of shown) assert.ok(!/ of |apps up/i.test(`${meter.label} ${meter.quantity ?? ""}`)); |
| 223 | }); |
| 224 | |
| 225 | test("a free workspace sees only what it can use, and anything it used anyway", () => { |
| 226 | assert.deepEqual( |
| 227 | shownMeters(METERS, false).map((m) => m.key), |
| 228 | ["agents", "domains", "git_storage", "search_scans"], |
| 229 | ); |
| 230 | }); |
| 231 | |
| 232 | test("storage reads in powers of ten", () => { |
| 233 | assert.equal(gigabytes(1_000_000_000), "1 GB"); |
| 234 | assert.equal(gigabytes(1_500_000_000), "1.5 GB"); |
| 235 | assert.equal(gigabytes(500_000_000), "500 MB"); |
| 236 | }); |