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