Skip to content

g1t/apps/web/app/lib/billing.test.ts

328 lines16,756 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
Usage lists Other once: kinds of work without words of their own are added into it5 foldTasks,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas6 gigabytes,
7 shownMeters,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look8 alertText,
9 alertTone,
10 cardCheckResult,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person11 cardFeeCents,
12 feeAndTax,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging13 creditLine,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look14 dollars,
15 needsAttention,
16 usageGlance,
17 parseCaps,
18 parseLimitRequest,
19 parsePrepay,
20 parseSpendLimit,
21 planStatus,
22 readDollars,
23 requestStatus,
24 share,
25 spendPath,
26 spendRange,
27 wholeDollars,
28} from "./billing.ts";
29
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging30test("a credit from g1t reads in a line, with what is left and when it expires", () => {
31 const now = new Date("2026-10-07T12:00:00Z");
32 const grant = { amountMicros: 25_000_000, leftMicros: 12_400_000, expiresAt: "2027-01-05T23:59:59Z", state: "open" as const };
33 assert.equal(creditLine(grant, now), "$25.00 credit, $12.40 left, expires Jan 5, 2027");
34 assert.equal(creditLine({ ...grant, expiresAt: "2026-12-05T23:59:59Z" }, now), "$25.00 credit, $12.40 left, expires Dec 5");
35 assert.equal(creditLine({ ...grant, expiresAt: null }, now), "$25.00 credit, $12.40 left");
36 assert.equal(creditLine({ ...grant, state: "used", leftMicros: 0 }, now), "$25.00 credit, all used");
37 assert.equal(creditLine({ ...grant, state: "revoked", leftMicros: 0 }, now), "$25.00 credit, withdrawn");
38});
39
Merge Stripe Tax, the card fee on card payments, and one free workspace per person40test("the card fee shown before paying is the one billing charges, and prices exclude tax", () => {
41 const fee = { on: true, percentMicros: 29_000, fixedCents: 30 };
42 // The same figures as billing's own tests (ai.rs, tax.rs).
43 assert.equal(cardFeeCents(2_500, fee), 106);
44 assert.equal(cardFeeCents(2_000, fee), 91);
45 assert.equal(cardFeeCents(2_000, { ...fee, on: false }), 0);
46 assert.equal(cardFeeCents(0, fee), 0);
47 assert.equal(cardFeeCents(2_000, null), 0);
48 assert.equal(feeAndTax(59), "Card processing fee $0.59, plus tax where it applies");
49 assert.equal(feeAndTax(0), "Plus tax where it applies");
50});
51
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look52test("money reads as dollars", () => {
53 assert.equal(dollars(9_500_000), "$9.50");
54 assert.equal(dollars(-1_250_000), "−$1.25");
55 assert.equal(wholeDollars(1_000_000_000), "$1,000");
56 assert.equal(wholeDollars(100_000), "$0.10");
57 assert.equal(readDollars("$1,000"), 1_000_000_000);
58 assert.equal(readDollars(""), null);
59 assert.equal(readDollars("lots"), null);
60});
61
62test("a prepayment is $25 or more, by card up to $10,000, by bank transfer from $1,000", () => {
63 assert.deepEqual(parsePrepay({ amount: "100" }), { ok: true, value: { amountCents: 10_000, method: "card" } });
64 assert.deepEqual(parsePrepay({ amount: "100", custom: "250" }), {
65 ok: true,
66 value: { amountCents: 25_000, method: "card" },
67 });
68 assert.equal(parsePrepay({ custom: "10" }).ok, false);
69 assert.equal(parsePrepay({ custom: "20000" }).ok, false);
70 assert.equal(parsePrepay({ custom: "500", method: "bank_transfer" }).ok, false);
71 assert.deepEqual(parsePrepay({ custom: "20000", method: "bank_transfer" }), {
72 ok: true,
73 value: { amountCents: 2_000_000, method: "bank_transfer" },
74 });
75 assert.equal(parsePrepay({}).ok, false);
76});
77
78test("caps stay in range, and empty goes back to the default", () => {
79 assert.deepEqual(parseCaps({ run: "", issue: "" }), { ok: true, value: { runCapMicros: null, issueCapMicros: null } });
80 assert.deepEqual(parseCaps({ run: "0.10", issue: "1000" }), {
81 ok: true,
82 value: { runCapMicros: 100_000, issueCapMicros: 1_000_000_000 },
83 });
84 assert.equal(parseCaps({ run: "0.05" }).ok, false);
85 assert.equal(parseCaps({ issue: "2000" }).ok, false);
86 assert.equal(parseCaps({ run: "two" }).ok, false);
87});
88
89test("a limit request needs an amount, a reason and a monthly spend; an overage only a reason", () => {
90 assert.deepEqual(parseLimitRequest({ kind: "limit", amount: "5000", reason: " A launch ", expected: "3000" }), {
91 ok: true,
92 value: { kind: "limit", amountMicros: 5_000_000_000, reason: "A launch", expectedMonthlyMicros: 3_000_000_000 },
93 });
94 assert.equal(parseLimitRequest({ kind: "limit", amount: "5000", reason: "" , expected: "1" }).ok, false);
95 assert.equal(parseLimitRequest({ kind: "limit", reason: "x", expected: "1" }).ok, false);
96 assert.deepEqual(parseLimitRequest({ kind: "overage", reason: "A loop ran all night" }), {
97 ok: true,
98 value: { kind: "overage", amountMicros: 0, reason: "A loop ran all night", expectedMonthlyMicros: 0 },
99 });
100});
101
102test("a spend limit is automatic, fixed, the full amount available, or the one-time raise", () => {
103 assert.deepEqual(parseSpendLimit({ mode: "automatic" }), { ok: true, value: { micros: null, useFull: false, raiseOnce: false } });
104 assert.deepEqual(parseSpendLimit({ mode: "fixed", limit: "500" }), {
105 ok: true,
106 value: { micros: 500_000_000, useFull: false, raiseOnce: false },
107 });
108 assert.equal(parseSpendLimit({ mode: "fixed", limit: "0.5" }).ok, false);
109 assert.deepEqual(parseSpendLimit({ mode: "full" }), { ok: true, value: { micros: null, useFull: true, raiseOnce: false } });
110 assert.deepEqual(parseSpendLimit({ mode: "raise" }), { ok: true, value: { micros: null, useFull: false, raiseOnce: true } });
111 assert.deepEqual(parseSpendLimit({ mode: "raise", limit: "150" }), {
112 ok: true,
113 value: { micros: 150_000_000, useFull: false, raiseOnce: true },
114 });
115});
116
117test("the spend limit's range: up to the highest ceiling plus prepaid, then once to twice it, then ask", () => {
118 // A first month at $100, nothing prepaid, the raise unused: $100 alone, $200 once.
119 const range = spendRange({ availableMicros: 100_000_000, ceilingMicros: 100_000_000, raiseOnceMicros: 200_000_000, raisedAt: null });
120 assert.deepEqual(range, { selfServeMicros: 100_000_000, raiseOnceMicros: 200_000_000, raisedAt: null });
121 assert.equal(spendPath(80_000_000, range), "self");
122 assert.equal(spendPath(100_000_000, range), "self");
123 assert.equal(spendPath(150_000_000, range), "raise");
124 assert.equal(spendPath(200_000_000, range), "raise");
125 assert.equal(spendPath(200_000_001, range), "ask");
126 // The raise used: only asking goes further.
127 const used = spendRange({ availableMicros: 200_000_000, ceilingMicros: 200_000_000, raiseOnceMicros: null, raisedAt: "2026-10-01T00:00:00Z" });
128 assert.equal(used.raiseOnceMicros, null);
129 assert.equal(spendPath(300_000_000, used), "ask");
130 // No ceiling: anything goes.
131 assert.equal(spendPath(1e12, spendRange({ availableMicros: null, ceilingMicros: null, raiseOnceMicros: null })), "self");
132 // A raise no higher than what is available is no raise.
133 assert.equal(spendRange({ availableMicros: 300_000_000, ceilingMicros: 100_000_000, raiseOnceMicros: 300_000_000 }).raiseOnceMicros, null);
134});
135
136test("the plan card says where the workspace stands", () => {
137 const free = { plan: "free" as const, trialMicrosLeft: 0, trialVerified: false, firstMonth: false };
138 assert.deepEqual(planStatus(null, free), { kind: "free", label: "Free" });
139 assert.equal(planStatus(null, { ...free, trialVerified: true, trialMicrosLeft: 5_000_000 }).kind, "trial");
140 assert.equal(planStatus(null, { ...free, trialVerified: true }).kind, "free");
141 const subscription = { feature: "plan" as const, status: "active" as const, periodEnd: null, startedBy: "a", startedAt: "" };
142 assert.deepEqual(planStatus({ on: true, subscription }, { ...free, plan: "paid", firstMonth: true }), {
143 kind: "paid",
144 label: "On the g1t plan, first month",
145 });
146 assert.equal(planStatus({ on: true, subscription: { ...subscription, status: "canceling" } }, { ...free, plan: "paid" }).kind, "canceling");
147 assert.equal(planStatus({ on: false, subscription: { ...subscription, status: "past_due" } }, free).kind, "past_due");
148 assert.equal(planStatus({ on: true, included: true, subscription: null }, free).kind, "comped");
149 assert.equal(planStatus(null, { ...free, plan: "internal" }).kind, "comped");
150 assert.equal(planStatus(null, { ...free, plan: "enterprise" }).kind, "enterprise");
151});
152
153test("the overview's Usage card reads the month by what pays", () => {
154 const usage = {
155 free: false,
156 spentMicros: 14_000_000,
157 usedMicros: 11_000_000,
158 byTask: [
159 { key: "review", micros: 2_000_000, runs: 4 },
160 { key: "implement", micros: 12_000_000, runs: 9 },
161 { key: "plan", micros: 0, runs: 1 },
162 ],
163 };
164 const entitlements = { includedMicros: 10_000_000, includedUsedMicros: 10_000_000, trialMicrosLeft: 0 };
165 const plan = usageGlance({ usage, status: { kind: "paid", label: "" }, entitlements, limit: { spentMicros: 4_000_000, spendLimitMicros: 200_000_000 }, trialMicros: 5_000_000 });
166 assert.equal(plan.kind, "plan");
167 assert.deepEqual(plan.credit, { label: "Included usage", usedMicros: 10_000_000, ofMicros: 10_000_000 });
168 assert.deepEqual(plan.onDemand, { micros: 4_000_000, limitMicros: 200_000_000 });
169 // Most first, and nothing that cost nothing.
170 assert.deepEqual(plan.lines.map((line) => line.label), ["Making changes", "Reviews"]);
171 const trial = usageGlance({ usage, status: { kind: "trial", label: "" }, entitlements: { ...entitlements, trialMicrosLeft: 1_500_000 }, limit: null, trialMicros: 5_000_000 });
172 assert.deepEqual(trial.credit, { label: "Trial credit", usedMicros: 3_500_000, ofMicros: 5_000_000 });
173 assert.equal(trial.onDemand, null);
174 assert.equal(usageGlance({ usage, status: { kind: "comped", label: "" }, entitlements, limit: null, trialMicros: 0 }).kind, "comped");
175 assert.equal(usageGlance({ usage, status: { kind: "free", label: "" }, entitlements, limit: null, trialMicros: 0 }).kind, "forge");
176 // While g1t charges nothing, usage is shown at cost.
177 const beta = usageGlance({ usage: { ...usage, free: true }, status: { kind: "paid", label: "" }, entitlements, limit: null, trialMicros: 0 });
178 assert.equal(beta.kind, "beta");
179 assert.equal(beta.spentMicros, 11_000_000);
180});
181
182test("a meter's share stays between 0 and 1", () => {
183 assert.equal(share(5, 10), 0.5);
184 assert.equal(share(15, 10), 1);
185 assert.equal(share(5, 0), 0);
186 assert.equal(share(5, null), 0);
187});
188
189test("after a card check the owner reads whether the trial started", () => {
190 assert.match(cardCheckResult({ trialVerified: true, trialMicrosLeft: 5_000_000 }, 5_000_000), /[$]5 trial is ready/);
191 assert.match(cardCheckResult({ trialVerified: true, trialMicrosLeft: 0 }, 5_000_000), /Prepaid cards can still pay for the plan/);
192 assert.match(cardCheckResult({ trialVerified: false, trialMicrosLeft: 0 }, 5_000_000), /did not finish/);
193});
194
195test("alerts read as sentences", () => {
196 const alert = { meter: "included", level: 90, usedMicros: 9_000_000, limitMicros: 10_000_000, message: "" };
197 assert.equal(alertText(alert), "90% of the plan's included usage: $9.00 of $10.00.");
198 assert.equal(alertText({ ...alert, meter: "spend_limit", level: 100 }), "All of your spend limit: $9.00 of $10.00.");
199 assert.equal(alertTone(50), "ok");
200 assert.equal(alertTone(75), "warning");
201 assert.equal(alertTone(100), "stopped");
202});
203
204test("a workspace needs attention when paused, on a spike, or at 90%", () => {
205 assert.equal(needsAttention(null), false);
206 assert.equal(needsAttention({ paused: null, spike: null, alerts: [] }), false);
207 assert.equal(needsAttention({ paused: "Spend spike", spike: null, alerts: [] }), true);
208 const spike = { id: "s", status: "continued", hourMicros: 1, averageMicros: 1, detectedAt: "" };
209 assert.equal(needsAttention({ paused: null, spike, alerts: [] }), false);
210 assert.equal(needsAttention({ paused: null, spike: { ...spike, status: "open" }, alerts: [] }), true);
211 const at = (level: number) => ({ meter: "spend_limit", level, usedMicros: 0, limitMicros: 0, message: "" });
212 assert.equal(needsAttention({ paused: null, spike: null, alerts: [at(75)] }), false);
213 assert.equal(needsAttention({ paused: null, spike: null, alerts: [at(90)] }), true);
214});
215
216test("a request says where it stands", () => {
217 const request = {
218 id: "r",
219 workspace: "acme",
220 kind: "limit",
221 amountMicros: 1,
222 reason: "",
223 expectedMonthlyMicros: 0,
224 status: "open",
225 createdBy: "a",
226 createdAt: "",
227 };
228 assert.equal(requestStatus(request), "Waiting for an answer");
229 assert.equal(requestStatus({ ...request, status: "approved", decidedMicros: 2_000_000_000 }), "Approved at $2,000");
230 assert.equal(requestStatus({ ...request, status: "declined" }), "Declined");
231});
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas232
233const METERS = [
234 { key: "agents", label: "Agents & sandboxes", micros: 1_200_000, quantity: "4 runs" },
235 { key: "builds", label: "Builds", micros: 0, quantity: null },
236 { key: "requests", label: "Requests & CPU", micros: 0, quantity: null },
237 { key: "domains", label: "Custom domains", micros: 120_000, quantity: "1 custom domain" },
238 { key: "git_storage", label: "Git operations & storage", micros: 0, quantity: "1,200 git operations" },
239 { key: "search_scans", label: "Search & security scans", micros: 3_000, quantity: null },
240];
241
242test("the plan shows every meter, with no quota beside any of them", () => {
243 const shown = shownMeters(METERS, true);
244 assert.deepEqual(
245 shown.map((m) => m.key),
246 ["agents", "builds", "requests", "domains", "git_storage", "search_scans"],
247 );
248 for (const meter of shown) assert.ok(!/ of |apps up/i.test(`${meter.label} ${meter.quantity ?? ""}`));
249});
250
251test("a free workspace sees only what it can use, and anything it used anyway", () => {
252 assert.deepEqual(
253 shownMeters(METERS, false).map((m) => m.key),
254 ["agents", "domains", "git_storage", "search_scans"],
255 );
256});
257
258test("storage reads in powers of ten", () => {
259 assert.equal(gigabytes(1_000_000_000), "1 GB");
260 assert.equal(gigabytes(1_500_000_000), "1.5 GB");
261 assert.equal(gigabytes(500_000_000), "500 MB");
262});
Usage lists Other once: kinds of work without words of their own are added into it263
264test("kinds of work without words of their own are one Other, listed once and last", () => {
265 const slices = [
266 { key: "check", micros: 5, runs: 50 },
267 { key: "review", micros: 3, runs: 5 },
268 { key: "other", micros: 1, runs: 7 },
269 { key: "implement", micros: 9, runs: 4 },
270 { key: "answer", micros: 2, runs: 11 },
271 ];
272 assert.deepEqual(foldTasks(slices), [
273 { key: "review", micros: 3, runs: 5 },
274 { key: "implement", micros: 9, runs: 4 },
275 { key: "other", micros: 8, runs: 68 },
276 ]);
277 assert.deepEqual(foldTasks([{ key: "plan", micros: 1, runs: 1 }]), [{ key: "plan", micros: 1, runs: 1 }]);
278});
Usage, Billing settings and prepaid AI credit; fixes from the UX audit279
280test("AI credit is bought in whole dollars from $10 to $1,000", async () => {
281 const { parseAiPurchase } = await import("./billing.ts");
282 assert.deepEqual(parseAiPurchase({ amount: "25" }), { ok: true, value: { amountCents: 2_500 } });
283 assert.deepEqual(parseAiPurchase({ amount: "custom", custom: "$150" }), { ok: true, value: { amountCents: 15_000 } });
284 assert.equal(parseAiPurchase({ amount: "custom", custom: "5" }).ok, false);
285 assert.equal(parseAiPurchase({ amount: "custom", custom: "12.50" }).ok, false);
286 assert.equal(parseAiPurchase({ amount: "custom", custom: "" }).ok, false);
287});
288
289test("auto-reload's amounts are checked before billing sees them", async () => {
290 const { parseAiReload } = await import("./billing.ts");
291 assert.deepEqual(parseAiReload({ enabled: "on", threshold: "10", target: "25", monthly: "100" }), {
292 ok: true,
293 value: { enabled: true, thresholdMicros: 10_000_000, targetMicros: 25_000_000, monthlyMaxMicros: 100_000_000 },
294 });
295 assert.equal(parseAiReload({ threshold: "10", target: "15", monthly: "100" }).ok, false);
296 assert.equal(parseAiReload({ threshold: "10", target: "25", monthly: "10" }).ok, false);
297 assert.equal(parseAiReload({ threshold: "10", target: "25.5", monthly: "100" }).ok, false);
298});
299
300test("budget alerts keep the levels ticked, highest first", async () => {
301 const { parseBudgetAlerts } = await import("./billing.ts");
302 assert.deepEqual(parseBudgetAlerts({ alerts: ["50", "100", "75", "12"], pause: "on", webhook: "" }), {
303 ok: true,
304 value: { alerts: [100, 75, 50], pauseAtLimit: true, webhook: null },
305 });
306 assert.equal(parseBudgetAlerts({ alerts: [], webhook: "http://x.test" }).ok, false);
307 assert.deepEqual(parseBudgetAlerts({ alerts: [], webhook: "https://hooks.acme.test/g1t" }).ok, true);
308});
309
310test("invoice details read from the form, with the country in capitals", async () => {
311 const { parseInvoiceDetails } = await import("./billing.ts");
312 const form = new FormData();
313 form.set("email", "billing@acme.test");
314 form.set("country", "de");
315 form.set("taxIdType", "eu_vat");
316 form.set("taxId", "DE123456789");
317 const parsed = parseInvoiceDetails(form);
318 assert.ok(parsed.ok);
319 assert.equal(parsed.value.address.country, "DE");
320 form.set("taxId", "");
321 assert.equal(parseInvoiceDetails(form).ok, false);
Merge Stripe Tax, the card fee on card payments, and one free workspace per person322 // Tax is worked out from the address: in the US, the ZIP code is needed.
323 form.set("taxIdType", "");
324 form.set("country", "us");
325 assert.equal(parseInvoiceDetails(form).ok, false);
326 form.set("postalCode", "94107");
327 assert.ok(parseInvoiceDetails(form).ok);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit328});

This file's history is long; its oldest lines are credited to the oldest commit read.