Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Staff can enroll a workspace in complimentary terms from sudo, with a monthly usage allowance at ordinary prices and a monthly fee of its own, and choose what happens past the allowance: new paid work stops, or (for employees and friends g1t covers up to an amount) the workspace's own card is charged at the usual prices under its limits. Every workspace but flagon-io is Free when this deploys (#47).

Complimentary (billing 0064, src/complimentary.rs). A real Stripe customer and subscription stand for each enrollment: a $0 price when the fee is waived, the fee's price (invoiced, 30 days) when there is one, the plan's price under a 100% coupon only for g1t's own (100% terms) unlimited enrollment; the grant, allowance, fee and past-allowance choice are in its metadata. The workspace sees the enrollment (PlanKind::Complimentary) only once Stripe has the subscription. Every Stripe step is written down first (pending_op) under idempotency keys made from the grant, the tick retries what is owed, and one older than Stripe keeps keys looks for what it made rather than sending again. Terms are kept as periods: the allowance is drawn in the same D1 batch that posts the usage (allocate_usage.sql), from the period covering the usage's own time, per UTC month (allowance_use complimentary), so late usage lands where it happened, a changed amount keeps the month's consumption and a lowered one never makes a debt. Past it, reserve refuses and entitlements say hasPlan false (repos, caches and packages stop billable storage), and what running work cost past it is g1t's (given_micros); in charge mode with a card it is charged under the plan's spend limit and ceiling, and without a card it stops as stop mode does. Pending month-end usage and open reservations count against what is left. The allowance is its own ledger column (gift_micros, "Paid by your complimentary allowance"), promotional in margin, sales and g1t's spend, never revenue; sudo shows money received (plan and fee invoices, prepayments, bank transfers), promotional credit and gifted usage apart. Billing shows the allowance, used, metered, remaining and when it starts again. Owners and billing managers can start the plan instead (convert_to_paid) on the same customer and subscription: the item becomes the plan's price with payment_behavior=pending_if_incomplete, so a decline, a bank's 3DS check, a closed page or a second click leave the enrollment and its one subscription as they were; it ends only once Stripe shows the plan applied, by the answer, the owner's next look, the tick or the webhook. Month-end meters run up so far are put on the ledger under the allowance when an enrollment ends. A staff terms change is never mistaken for a conversion, a refused conversion is never replayed by the next, and an abandoned one puts a fee back to being invoiced. Late month-end figures. A source's figure for a month already charged or closed charges only what it adds, on that month and its allowances, keyed by the new total and guarded against a stale writer, keeping the highest figure; a lower one is never refunded on its own (one audit row for staff). An uncharged row of a closed month takes the higher figure for the close. Also: migration 0065 returns every account but flagon-io to standard terms and detaches enterprise members (with audit rows; production had none on 2026-10-14). The Stripe Tax guard covers subscription updates. Repos treats a workspace that may meter nothing more as free for storage and git operations. The earlier, uncommitted launch-readiness work rides along: durable workspace and enterprise invoice attempts (billing 0062, 0063), atomic allowance and usage posting, refund and payment-stop transactions, replay pagination, wrong-mode events left unclaimed, the 5 GB public storage cap on every plan with atomic reservations and native-copy locks (repos 0021), and the ops scripts (read-only live preflight, isolated sandbox harness, demo detach). Deploy: billing 0062-0065 and repos 0021 apply before the Workers (all additive; 0062's unique index checked against production: one trial grant, no shared card). Live charging stays off: billing is on Stripe's test key. Not yet: public webhook delivery, disputes end to end, test-clock renewal of an enrollment, the live cutover and a small live rehearsal, storage interrupted-write recovery tooling.

Checked:
cargo test for g1t-billing (290), g1t-contracts (312) and g1t-repos (357); cargo check of the workspace and of billing for wasm32; the billing Worker built; SQL conformance against the migrated schema (53, new: complimentary allocation, charge and stop modes, terms changes, late usage, 0065); TypeScript checks and production builds of the site and sudo, the site's tests (950), sudo's (156), contracts' (48), the deploy tool's (92), ops (34); against the real Worker and an isolated Stripe sandbox, every harness phase (protocol, run 7, enterprise 3, complimentary 12) and a 3DS challenge completed in headless Chrome on Stripe's hosted invoice, then settled as a conversion. Two independent reviews; every defect they found is fixed.
G1t-Session:
ses_01m4y8v7hae5csmqcavv6wprjy
syntaqxcommitted Parentda6fe51Browse files
73 files+4396−43225 files not counted0/73 viewed
+58−11
99 deployed apps, and, on the plan, storage and git traffic past the free
1010 amounts. Each is priced from what it costs g1t, plus 20%: runner minutes
1111 at a fixed rate for each machine, everything else at its cost plus 20%.
12−It is charged to the workspace that owns the repository. There is no seat price, and on the plan there
13−are no quotas: only your spend limit stops anything.
12+It is charged to the workspace that owns the repository. There is no seat price.
13+Your spend limit bounds paid usage; public storage and service size limits still apply.
1414
1515 Wherever this page says an owner can do something to a workspace's
1616 billing, so can its [billing managers](/guides/workspaces/#roles-that-add-to-a-member):
4141 them are refused at the start of the push, with what to do: make the
4242 repository public, delete what you do not need, or start the plan, where
4343 storage past 1 GB is usage and pushes never stop. Public repositories are
44−never charged for storage, on any plan; a free workspace's public
45−repositories hold up to 5 GB together, and past it pushes to them are
46−refused the same way. On the plan, pushes to public repositories never
47−stop.
44+never charged for storage. Every workspace, including paid workspaces,
45+has a **5 GB public repository storage cap**. Pushes, imports, forks and
46+changes that would add public storage need room within that cap.
47+Recently deleted repositories still count until permanently deleted.
48+In-flight writes reserve capacity; an interrupted write may retain its
49+reservation until it is reconciled.
4850
4951 Compute is not free. Workflows, the merge queue,
5052 deployments and semantic search need [the g1t plan](#the-g1t-plan), or a
112114 - Usage past $10 is charged the same way, **up to your
113115 [spend limit](#limits)**.
114116
115−There are **no quotas** on the plan: no count of projects, previews,
116−build minutes, requests, custom domains, git operations or gigabytes
117−stops a workspace. Everything that costs g1t money is metered from the
117+The plan has no count limit on projects or previews. Public repository
118+storage has the 5 GB cap above, and repository and request size limits
119+still apply. Paid usage is metered from the
118120 first unit (past the free 1 GB of private storage and 50,000 git
119−operations that every workspace has), and only your spend limit stops
120−work.
121+operations that every workspace has). Your spend limit also stops new
122+paid work.
121123
122124 **Included usage** pays for the month's usage first, at the same prices as
123125 everything else: runner minutes, builds, app traffic, custom domains,
845847 have a 100% discount. Their usage is shown at price and kept apart from
846848 what customers pay.
847849
850+### Complimentary terms
851+
852+g1t can enroll a workspace in **complimentary** terms, such as for a
853+design partner. A new workspace is never enrolled on its own: g1t staff
854+choose each one. An enrollment gives:
855+
856+- **The plan's features**, without the plan's $21 a month.
857+- **A monthly usage allowance**, such as $100. Usage counts against it at
858+ its ordinary price, the same prices everyone pays: it is an amount of
859+ usage g1t pays for, not a discount. It starts again on the 1st of each
860+ month (UTC), and what is unused does not carry over.
861+- **A monthly fee of its own**, often none. When there is one, Stripe
862+ invoices it each month, apart from the allowance.
863+
864+**Billing** shows the allowance, what is used, what is metered but not
865+billed yet, what is left, and when it starts again. Usage that is measured
866+at the end of the month, such as storage and git operations, and work
867+running now count against what is left.
868+
869+What happens at the allowance is part of the terms, and **Billing** says
870+which:
871+
872+- **New work that costs money stops** until the month turns: new runner
873+ jobs, builds, deployments and other compute, and storage and caches past
874+ the free amounts. Nothing is ever charged for usage past it, whether or
875+ not a card is saved. Work already running when the allowance runs out
876+ finishes, and g1t covers what it cost past the allowance.
877+- **Or usage past it is charged to your card**, such as when g1t covers a
878+ set amount of an employee's or a friend's usage. Past the allowance,
879+ usage is charged at the same prices as on the plan, up to your
880+ [spend limit](#limits) and the [ceiling](#the-ceiling), and invoiced
881+ when the month closes. Add a card on **Billing** first: without one,
882+ work stops at the allowance until you do.
883+
884+If g1t changes the amount, what the month already used still counts:
885+raising it adds only the difference, and lowering it below what was used
886+leaves nothing more this month, never a bill.
887+
888+An owner or billing manager can **start the g1t plan instead** from the
889+allowance card on **Billing**. The plan uses the same Stripe customer and
890+subscription, and nothing changes until its first payment goes through:
891+if the card is declined, the bank asks to confirm the payment, or the page
892+is closed, the complimentary terms stay as they were. Usage the allowance
893+paid for before the plan started is never charged afterwards.
894+
848895 Each change is made by g1t staff and recorded with who made it and why. To
849896 ask for one, write to [support@g1t.sh](mailto:support@g1t.sh).
850897
+2−2
245245 Custom
246246 </Segment>
247247 </div>
248− <p className="mt-1.5 text-xs text-faint">Off every usage charge. 100%: nothing is charged, and the statement shows usage at price with the discount beside it.</p>
248+ <p className="mt-1.5 text-xs text-faint">Off usage charges. This does not change an existing Stripe subscription's monthly fee. A full discount requires ending that subscription first.</p>
249249 </fieldset>
250250 <div className="when-custom">
251251 <Field label="Custom discount %" hint="A whole percent, 1 to 100.">
253253 </Field>
254254 </div>
255255 <div className="grid gap-4 sm:grid-cols-2">
256− <Field label="Limit $" hint="Unpaid usage allowed; blank: trust decides. At 100%: g1t's monthly budget for it, at cost; blank: the default ($150).">
256+ <Field label="Limit $" hint="Unpaid usage allowed; blank: trust decides. At 100%: g1t's monthly budget at cost; blank: $150; zero: unlimited. This is not a monthly usage credit.">
257257 <Input name="ceiling" inputMode="decimal" placeholder="By trust" defaultValue={values?.ceiling ?? dollarsField(terms.ceilingMicros)} />
258258 </Field>
259259 <Field label="Until" hint="Blank: no end. UTC.">
+213−0
1+/**
2+ * A workspace's complimentary enrollment: the monthly usage allowance (at
3+ * ordinary prices), the recurring monthly fee apart from it, this month's
4+ * standing, the Stripe subscription that stands for it, and its terms over
5+ * time. Staff enroll each workspace by hand; nothing enrolls one on its own.
6+ */
7+import type { ComplimentaryStanding } from "@g1t/contracts";
8+
9+import { Badge, Button, Field, Input, Notice, Section, Textarea, When } from "~/components/ui";
10+import { dollarsField, usd } from "~/lib/money";
11+import type { SectionError } from "~/lib/review";
12+
13+const STATUS: Record<ComplimentaryStanding["status"], { label: string; tone: "mint" | "warn" | "plain" }> = {
14+ active: { label: "Active", tone: "mint" },
15+ pending: { label: "Waiting for Stripe", tone: "warn" },
16+ ended: { label: "Ended", tone: "plain" },
17+};
18+
19+function allowanceText(micros: number, unlimited?: boolean) {
20+ return unlimited ? "Unlimited" : `${usd(micros, { cents: true })} a month`;
21+}
22+
23+function feeText(cents?: number) {
24+ return cents ? `${usd(cents * 10_000, { cents: true })} a month` : "Waived";
25+}
26+
27+export function ComplimentarySection({
28+ workspace,
29+ standing,
30+ unavailable,
31+ pathname,
32+ error,
33+}: {
34+ workspace: string;
35+ standing: ComplimentaryStanding | null;
36+ unavailable: string | null;
37+ pathname: string;
38+ error: SectionError;
39+}) {
40+ const values = error?.values;
41+ const open = standing && standing.status !== "ended" ? standing : null;
42+ const month = open?.month ?? null;
43+ return (
44+ <Section
45+ id="complimentary"
46+ title="Complimentary"
47+ description="The plan's features, a monthly usage allowance at ordinary prices, and a monthly fee of its own. Past the allowance new work that costs money stops; nothing is ever charged for it. No rollover."
48+ >
49+ {unavailable && <Notice tone="error">Billing could not say: {unavailable}</Notice>}
50+ {open ? (
51+ <div className="mb-4 space-y-3">
52+ <div className="flex flex-wrap items-center gap-2 text-sm">
53+ <Badge tone={STATUS[open.status].tone}>{STATUS[open.status].label}</Badge>
54+ <span className="text-fg-soft">{allowanceText(open.allowanceMicros, open.unlimited)} allowance</span>
55+ <span className="text-faint">·</span>
56+ <span className="text-fg-soft">Fee {feeText(open.feeCents)}</span>
57+ <span className="text-faint">·</span>
58+ <span className="text-fg-soft">{open.chargePastAllowance ? "Past it: their card is charged" : "Past it: work stops"}</span>
59+ </div>
60+ {month && (
61+ <dl className="grid grid-cols-2 gap-x-4 gap-y-1.5 rounded-md border border-line bg-bg px-3.5 py-2.5 text-sm sm:grid-cols-3">
62+ <Stat label="Used this month" value={usd(month.usedMicros, { cents: true })} />
63+ <Stat label="Metered, not billed yet" value={usd(month.pendingMicros ?? 0, { cents: true })} />
64+ <Stat label="Held by running work" value={usd(month.heldMicros ?? 0, { cents: true })} />
65+ <Stat label="Remaining" value={month.unlimited ? "Unlimited" : usd(month.remainingMicros, { cents: true })} />
66+ <Stat label="Covered past it by g1t" value={usd(month.overrunMicros ?? 0, { cents: true })} />
67+ <Stat label="Resets" value={<When at={month.resetsAt} />} />
68+ </dl>
69+ )}
70+ <dl className="grid grid-cols-2 gap-x-4 gap-y-1.5 text-xs sm:grid-cols-3">
71+ <Stat label="Money received, ever" value={usd(open.moneyReceivedMicros ?? 0, { cents: true })} />
72+ <Stat label="Promotional credit, ever" value={usd(open.promotionalCreditMicros ?? 0, { cents: true })} />
73+ <Stat label="Gifted usage, ever" value={usd(open.giftedMicros ?? 0, { cents: true })} />
74+ </dl>
75+ <p className="text-xs text-muted">
76+ Stripe {open.live ? "live" : "test"}: {open.customerId ?? "no customer yet"}, {open.subscriptionId ?? "no subscription yet"}
77+ {open.stripeStatus ? ` (${open.stripeStatus})` : ""}. Grant {open.grantId}.
78+ </p>
79+ {open.pendingOp && (
80+ <Notice tone="info">
81+ Still owed at Stripe: {open.pendingOp}. Tried again every fifteen minutes{open.lastError ? `; last error: ${open.lastError}` : "."}
82+ </Notice>
83+ )}
84+ </div>
85+ ) : (
86+ <p className="mb-4 text-sm text-muted">
87+ {standing?.status === "ended"
88+ ? `Not enrolled. The last enrollment ended ${standing.endedReason === "converted" ? "when the owner started the plan" : "by staff"}.`
89+ : "Not enrolled: the workspace is on its own plan."}
90+ </p>
91+ )}
92+
93+ <form method="post" action={`${pathname}#complimentary`} className="space-y-4">
94+ <input type="hidden" name="intent" value="complimentary" />
95+ {error && <Notice tone="error">{error.error}</Notice>}
96+ <div className="grid gap-4 sm:grid-cols-2">
97+ <Field label="Monthly allowance $" hint="Usage at ordinary prices, each calendar month (UTC). Changing it keeps what this month used.">
98+ <Input
99+ name="allowance"
100+ inputMode="decimal"
101+ placeholder="100.00"
102+ defaultValue={values?.allowance ?? (open ? dollarsField(open.allowanceMicros) : "100.00")}
103+ />
104+ </Field>
105+ <Field label="Monthly fee $" hint="Recurring, invoiced by Stripe, apart from the allowance. Blank or 0: waived.">
106+ <Input name="fee" inputMode="decimal" placeholder="Waived" defaultValue={values?.fee ?? (open?.feeCents ? dollarsField(open.feeCents * 10_000) : "")} />
107+ </Field>
108+ </div>
109+ <fieldset className="space-y-2 text-sm">
110+ <legend className="mb-1 text-xs text-faint">Past the allowance</legend>
111+ <label className="flex cursor-pointer items-start gap-2.5">
112+ <input
113+ type="radio"
114+ name="past"
115+ value="stop"
116+ defaultChecked={values ? values.past !== "charge" : !open?.chargePastAllowance}
117+ className="mt-0.5"
118+ />
119+ <span>
120+ <span className="block font-medium">New paid work stops</span>
121+ <span className="block text-xs text-muted">Nothing is ever charged. For partners g1t carries entirely.</span>
122+ </span>
123+ </label>
124+ <label className="flex cursor-pointer items-start gap-2.5">
125+ <input
126+ type="radio"
127+ name="past"
128+ value="charge"
129+ defaultChecked={values ? values.past === "charge" : Boolean(open?.chargePastAllowance)}
130+ className="mt-0.5"
131+ />
132+ <span>
133+ <span className="block font-medium">Their card is charged</span>
134+ <span className="block text-xs text-muted">
135+ g1t covers the allowance; past it, usage is charged to the workspace's own card at the usual prices, up to its spend
136+ limit. For employees and friends. Without a card, work stops until they add one.
137+ </span>
138+ </span>
139+ </label>
140+ </fieldset>
141+ <label className="flex cursor-pointer items-start gap-2.5 text-sm">
142+ <input type="checkbox" name="unlimited" defaultChecked={values ? values.unlimited === "on" : Boolean(open?.unlimited)} className="mt-0.5" />
143+ <span>
144+ <span className="block font-medium">Unlimited allowance</span>
145+ <span className="block text-xs text-muted">For g1t's own workspaces. Infrastructure limits still apply.</span>
146+ </span>
147+ </label>
148+ <Field label="Note" hint="Required. Why, for whoever looks next.">
149+ <Textarea name="note" rows={2} required maxLength={500} defaultValue={values?.note ?? ""} placeholder="e.g. Design partner, $500 a month through Q1" />
150+ </Field>
151+ <Field label={`Type ${workspace} to confirm`}>
152+ <Input name="confirmation" autoComplete="off" defaultValue="" />
153+ </Field>
154+ <div className="flex justify-end">
155+ <Button type="submit">{open ? "Change terms" : "Enroll"}</Button>
156+ </div>
157+ </form>
158+
159+ {open && (
160+ <form method="post" action={`${pathname}#complimentary`} className="mt-6 space-y-3 border-t border-line pt-4">
161+ <input type="hidden" name="intent" value="end-complimentary" />
162+ <p className="text-sm text-muted">
163+ Ending it puts the workspace on Free now and cancels its Stripe subscription. Usage the allowance paid for stays paid for.
164+ </p>
165+ <div className="grid gap-3 sm:grid-cols-2">
166+ <Field label="Note">
167+ <Input name="note" required maxLength={500} placeholder="Why" />
168+ </Field>
169+ <Field label={`Type ${workspace} to end`}>
170+ <Input name="confirmation" autoComplete="off" />
171+ </Field>
172+ </div>
173+ <div className="flex justify-end">
174+ <Button type="submit" variant="danger">
175+ End enrollment
176+ </Button>
177+ </div>
178+ </form>
179+ )}
180+
181+ {standing?.periods && standing.periods.length > 0 && (
182+ <div className="mt-6 border-t border-line pt-4">
183+ <p className="text-xs text-faint">Terms over time</p>
184+ <ul className="mt-2 space-y-1 text-xs">
185+ {standing.periods.map((period) => (
186+ <li key={period.id} className="flex flex-wrap gap-x-2 text-fg-soft">
187+ <When at={period.startsAt} />
188+ <span>→</span>
189+ {period.endsAt ? <When at={period.endsAt} /> : <span>now</span>}
190+ <span>
191+ {allowanceText(period.allowanceMicros, period.unlimited)}, fee {feeText(period.feeCents)},{" "}
192+ {period.chargePastAllowance ? "then their card" : "then stops"}
193+ </span>
194+ <span className="text-muted">
195+ {period.createdBy}: {period.note}
196+ </span>
197+ </li>
198+ ))}
199+ </ul>
200+ </div>
201+ )}
202+ </Section>
203+ );
204+}
205+
206+function Stat({ label, value }: { label: string; value: React.ReactNode }) {
207+ return (
208+ <div>
209+ <dt className="text-xs text-faint">{label}</dt>
210+ <dd className="tabular-nums text-fg-soft">{value}</dd>
211+ </div>
212+ );
213+}
+27−1
99 import { data, redirect } from "react-router";
1010
1111 import { parseCreditForm } from "./credits";
12−import { fields, parseAllowances, parseEmail, parseGoodwill, parseNote, parsePayment, parseSlug, parseTerms, text } from "./forms";
12+import { fields, parseAllowances, parseComplimentary, parseEmail, parseGoodwill, parseNote, parsePayment, parseSlug, parseTerms, text } from "./forms";
1313 import { FORGIVE_COST_MICROS, goodwillWarning } from "./pricing";
1414 import type { ActionData } from "./review";
1515 import { admin, identity, priceBook } from "./services.server";
6161 return back("allowances");
6262 }
6363
64+ if (intent === "complimentary" || intent === "end-complimentary") {
65+ // Enrollment is per workspace, never an enterprise's.
66+ const values = fields(form, "allowance", "unlimited", "fee", "past", "note", "confirmation");
67+ if (subject.kind !== "workspace") return failed("complimentary", "Enroll each workspace on its own page.", values);
68+ if (subject.billedTo) return failed("complimentary", `${subject.billedTo.name} pays for this workspace. Detach it first.`, values);
69+ if (intent === "end-complimentary") {
70+ const note = parseNote(values.note);
71+ if (!note.ok) return failed("complimentary", note.error, values);
72+ if (values.confirmation !== subject.slug) {
73+ return failed("complimentary", `Type the workspace's slug, ${subject.slug}, to end its enrollment. It goes back to Free now.`, values);
74+ }
75+ const ended = await admin.endComplimentary(subject.slug, note.value, staff.email);
76+ if (!ended.ok) return failed("complimentary", ended.error.message, values);
77+ return back("complimentary-ended");
78+ }
79+ const terms = parseComplimentary(form);
80+ if (!terms.ok) return failed("complimentary", terms.error, values);
81+ if (values.confirmation !== subject.slug) {
82+ return failed("complimentary", `Type the workspace's slug, ${subject.slug}, to confirm these terms.`, values);
83+ }
84+ const { note, ...chosen } = terms.value;
85+ const saved = await admin.setComplimentary(subject.slug, chosen, note, staff.email);
86+ if (!saved.ok) return failed("complimentary", saved.error.message, values);
87+ return back("complimentary");
88+ }
89+
6490 if (intent === "attach") {
6591 const values = fields(form, "workspace", "target");
6692 const section = isEnterprise ? "members" : "billed-to";
+33−0
235235 };
236236 }
237237
238+/** The most a complimentary allowance or fee can be: billing's caps. */
239+export const MAX_COMPLIMENTARY_MICROS = 100_000 * MICROS_PER_DOLLAR;
240+
241+export type ComplimentaryInput = { allowanceMicros: number; unlimited: boolean; feeCents: number; chargePastAllowance: boolean; note: string };
242+
243+/**
244+ * A complimentary enrollment's terms, from its form: the monthly usage
245+ * allowance (at ordinary prices) or unlimited, and the monthly fee (blank or
246+ * 0: waived). The two are separate amounts. A note is required.
247+ */
248+export function parseComplimentary(form: FormData): Parsed<ComplimentaryInput> {
249+ const unlimited = text(form, "unlimited") === "on";
250+ const rawAllowance = text(form, "allowance");
251+ const allowanceMicros = unlimited && !rawAllowance ? 0 : parseDollars(rawAllowance);
252+ if (allowanceMicros == null || allowanceMicros < 0) {
253+ return { ok: false, error: "The monthly allowance is dollars and cents, such as 100 or 500, valued at ordinary usage prices." };
254+ }
255+ if (allowanceMicros > MAX_COMPLIMENTARY_MICROS) return { ok: false, error: "The allowance is at most $100,000 a month; for more, make it unlimited." };
256+ const rawFee = text(form, "fee");
257+ const feeMicros = rawFee ? parseDollars(rawFee) : 0;
258+ if (feeMicros == null || feeMicros < 0) return { ok: false, error: "The monthly fee is dollars and cents, or blank to waive it." };
259+ if (feeMicros > MAX_COMPLIMENTARY_MICROS) return { ok: false, error: "The monthly fee is at most $100,000." };
260+ if (feeMicros % 10_000 !== 0) return { ok: false, error: "The monthly fee is whole cents." };
261+ if (!unlimited && allowanceMicros === 0 && feeMicros === 0) {
262+ return { ok: false, error: "No allowance and no fee gives nothing: set an allowance, or end the enrollment." };
263+ }
264+ const note = parseNote(text(form, "note"));
265+ if (!note.ok) return note;
266+ const chargePastAllowance = text(form, "past") === "charge";
267+ if (unlimited && chargePastAllowance) return { ok: false, error: "An unlimited allowance has nothing past it to charge." };
268+ return { ok: true, value: { allowanceMicros, unlimited, feeCents: feeMicros / 10_000, chargePastAllowance, note: note.value } };
269+}
270+
238271 /** The most one recorded payment can be; billing refuses more. */
239272 export const MAX_PAYMENT_MICROS = 100_000 * MICROS_PER_DOLLAR;
240273 const MAX_REFERENCE = 100;
+2−0
2525 export const DONE: Record<string, string> = {
2626 terms: "Terms saved. They apply to charges from now on.",
2727 allowances: "Plan, pools and caps saved. They apply from now on.",
28+ complimentary: "Complimentary terms saved. A new enrollment shows to the workspace once Stripe has its subscription, usually within seconds.",
29+ "complimentary-ended": "Enrollment ended. The workspace is on Free now; its Stripe subscription is canceled.",
2830 attach: "Workspace moved onto the enterprise.",
2931 detach: "Workspace moved off the enterprise. It pays for itself again.",
3032 credit: "Credit given. It is on the balance and the statement, and the owners were emailed.",
+21−2
4141 trustAbout,
4242 } from "~/components/ui";
4343 import { type Subject, billingAction } from "~/lib/billing-actions.server";
44+import { ComplimentarySection } from "~/components/complimentary";
4445 import { parseSlug, text } from "~/lib/forms";
4546 import { usd } from "~/lib/money";
4647 import { type ActionData, DONE, type SectionError, doneKey } from "~/lib/review";
9394 const figures = billedTo
9495 ? { charged: share?.chargedMicros ?? 0, cost: share?.costMicros ?? 0, paid: share?.paidMicros ?? 0 }
9596 : { charged: summary.chargedMicros, cost: summary.costMicros, paid: summary.paidMicros };
96− const [enterprises, sales, invoices, plan, overages, book, credits] = await Promise.all([
97+ const [enterprises, sales, invoices, plan, overages, book, credits, complimentary] = await Promise.all([
9798 billedTo
9899 ? Promise.resolve([])
99100 : admin
109110 settle(admin.overages()),
110111 settle(priceBook()),
111112 settle(admin.credits({ workspace: slug })),
113+ settle(admin.complimentary(slug)),
112114 ]);
113115 const done = doneKey(request.url);
114116 return {
135137 ? { grants: credits.value.grants, leftMicros: credits.value.grants.reduce((sum, grant) => sum + grant.leftMicros, 0) }
136138 : null,
137139 creditsError: credits.ok ? null : credits.error,
140+ complimentary: complimentary.ok ? complimentary.value : null,
141+ complimentaryError: complimentary.ok ? null : complimentary.error,
138142 ledger: billedTo ? detail.ledger.filter((entry) => !entry.workspace || entry.workspace === slug) : detail.ledger,
139143 audit: billedTo ? detail.audit.filter((entry) => mentions(entry, slug)) : detail.audit,
140144 done: done && !SALES_DONE.has(done) ? DONE[done] : null,
187191 forgiveCap,
188192 credits,
189193 creditsError,
194+ complimentary,
195+ complimentaryError,
190196 ledger,
191197 audit,
192198 done,
355361 </Section>
356362 ) : (
357363 <>
364+ <ComplimentarySection
365+ workspace={slug}
366+ standing={complimentary}
367+ unavailable={complimentaryError}
368+ pathname={pathname}
369+ error={error("complimentary")}
370+ />
358371 <TermsForm terms={terms} pathname={pathname} error={error("terms")} />
359372 <AllowancesForm allowances={allowances} comped={fullDiscount(terms)} pathname={pathname} error={error("allowances")} />
360373 </>
523536 );
524537 }
525538
526−const PLAN_LABEL: Record<string, string> = { free: "Free", paid: "The g1t plan", internal: "Internal (100% discount)", enterprise: "Enterprise" };
539+const PLAN_LABEL: Record<string, string> = {
540+ free: "Free",
541+ paid: "The g1t plan",
542+ internal: "Internal (100% discount)",
543+ enterprise: "Enterprise",
544+ complimentary: "Complimentary",
545+};
527546
528547 function gigabytes(bytes: number): string {
529548 return `${Math.round((bytes / 1e9) * 100) / 100} GB`;
+106−3
1010 import type { ReactNode } from "react";
1111 import { Form, Link } from "react-router";
1212
13−import type { BillingDetails, FeatureState, Limit, UsageReport } from "@g1t/contracts";
13+import type { BillingDetails, ComplimentaryMonth, FeatureState, Limit, UsageReport } from "@g1t/contracts";
1414
1515 import type { PlanStatus } from "../lib/billing";
1616 import { money, wholeDollars } from "../lib/money";
2222 const FIELD =
2323 "w-full rounded-md border border-line bg-bg px-2.5 py-1.5 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
2424
25+// --- Complimentary ------------------------------------------------------------------------------
26+
27+/**
28+ * A complimentary enrollment's month: the allowance g1t gives, what is used,
29+ * metered and held against it, what is left and when it starts again. Past
30+ * it, new work that costs money stops: nothing is charged, card or not. An
31+ * owner can start the plan on the same billing from here.
32+ */
33+export function ComplimentaryCard({
34+ month,
35+ owner,
36+ enabled,
37+ error,
38+}: {
39+ month: ComplimentaryMonth;
40+ owner: boolean;
41+ enabled: boolean;
42+ error?: string;
43+}) {
44+ const taken = month.usedMicros + (month.pendingMicros ?? 0) + (month.heldMicros ?? 0);
45+ const resets = new Date(month.resetsAt).toLocaleDateString("en-US", { month: "long", day: "numeric", timeZone: "UTC" });
46+ const charged = Boolean(month.chargePastAllowance);
47+ const used = month.unlimited ? 0 : Math.min(100, (taken / Math.max(1, month.allowanceMicros)) * 100);
48+ return (
49+ <Card
50+ id="complimentary"
51+ tone={!month.unlimited && month.remainingMicros <= 0 ? "warn" : "accent"}
52+ icon={<Sparkles size={16} />}
53+ title="Complimentary allowance"
54+ about={
55+ charged
56+ ? `Usage at its ordinary price, paid by g1t up to the allowance each calendar month (UTC). Past it, usage is charged to this workspace's card at the same prices, up to your spend limit. Unused allowance does not carry over.`
57+ : `Usage at its ordinary price, paid by g1t up to the allowance each calendar month (UTC). Past it, new work that costs money stops until ${resets}; nothing is ever charged for it, and unused allowance does not carry over.`
58+ }
59+ aside={
60+ <p className="shrink-0 sm:text-right">
61+ <span className="text-2xl font-semibold tabular-nums tracking-tight">{month.unlimited ? "Unlimited" : money(month.allowanceMicros)}</span>
62+ {!month.unlimited && <span className="text-sm text-muted"> / month</span>}
63+ <span className="block text-xs text-faint">{month.feeCents ? `Fee ${money(month.feeCents * 10_000)} a month, invoiced` : "No monthly fee"}</span>
64+ </p>
65+ }
66+ >
67+ {!month.unlimited && (
68+ <div className="mt-4">
69+ <div className="h-1.5 overflow-hidden rounded-full bg-line" role="presentation">
70+ <div className={`h-full rounded-full ${month.remainingMicros <= 0 ? "bg-warn" : "bg-accent"}`} style={{ width: `${used}%` }} />
71+ </div>
72+ </div>
73+ )}
74+ <dl className="mt-3 grid grid-cols-2 gap-3 text-sm sm:grid-cols-4">
75+ <div>
76+ <dt className="text-xs text-faint">Used</dt>
77+ <dd className="tabular-nums">{money(month.usedMicros)}</dd>
78+ </div>
79+ <div>
80+ <dt className="text-xs text-faint">Metered, not billed yet</dt>
81+ <dd className="tabular-nums">{money((month.pendingMicros ?? 0) + (month.heldMicros ?? 0))}</dd>
82+ </div>
83+ <div>
84+ <dt className="text-xs text-faint">Remaining</dt>
85+ <dd className="tabular-nums">{month.unlimited ? "Unlimited" : money(month.remainingMicros)}</dd>
86+ </div>
87+ <div>
88+ <dt className="text-xs text-faint">Starts again</dt>
89+ <dd>{resets}</dd>
90+ </div>
91+ </dl>
92+ {charged && !month.hasCard && (
93+ <p className="mt-4 text-sm text-muted">
94+ No card yet: past the allowance, work stops until one is added.{" "}
95+ {owner && enabled && (
96+ <Form method="post" className="inline">
97+ <SubmitButton variant="outline" name="intent" value="card-check" pending="Opening Stripe…">
98+ <CreditCard size={14} />
99+ Add a card
100+ </SubmitButton>
101+ </Form>
102+ )}
103+ </p>
104+ )}
105+ {owner && enabled && !month.unlimited && (
106+ <Form method="post" className="mt-4 flex flex-wrap items-center gap-3">
107+ <SubmitButton variant="outline" name="intent" value="convert" pending="Starting the plan…">
108+ <CreditCard size={14} />
109+ Start the g1t plan instead
110+ </SubmitButton>
111+ <span className="text-xs text-faint">
112+ $21 a month with usage past what is included charged, on this workspace's billing. Nothing changes until the first payment
113+ goes through.
114+ </span>
115+ </Form>
116+ )}
117+ {error && (
118+ <div className="mt-2">
119+ <ErrorText>{error}</ErrorText>
120+ </div>
121+ )}
122+ </Card>
123+ );
124+}
125+
25126 // --- The plan ---------------------------------------------------------------------------------
26127
27128 /**
57158 }) {
58159 const plan = state?.plan;
59160 const subscription = state?.subscription ?? null;
60− const on = ["paid", "canceling", "comped", "enterprise", "past_due"].includes(status.kind);
161+ const on = ["paid", "canceling", "comped", "enterprise", "past_due", "complimentary"].includes(status.kind);
61162 const now = new Date();
62163 const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
63164 const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0));
86187 <span className="text-2xl font-semibold tabular-nums tracking-tight">${((plan?.monthlyCents ?? 2100) / 100).toFixed(0)}</span>
87188 <span className="text-sm text-muted"> / month</span>
88189 <span className="block text-xs text-faint">with {wholeDollars(report?.included?.of ?? 10_000_000)} of usage included</span>
89− {status.kind !== "comped" && status.kind !== "enterprise" && <span className="block text-xs text-faint">Plus tax where it applies</span>}
190+ {status.kind !== "comped" && status.kind !== "enterprise" && status.kind !== "complimentary" && (
191+ <span className="block text-xs text-faint">Plus tax where it applies</span>
192+ )}
90193 </p>
91194 }
92195 >
+2−1
125125
126126 /** Where the workspace stands with the g1t plan, for the plan card. */
127127 export type PlanStatus = {
128− kind: "free" | "trial" | "paid" | "canceling" | "past_due" | "comped" | "enterprise";
128+ kind: "free" | "trial" | "paid" | "canceling" | "past_due" | "comped" | "enterprise" | "complimentary";
129129 label: string;
130130 };
131131
135135 ): PlanStatus {
136136 if (entitlements?.plan === "internal") return { kind: "comped", label: "100% discount from g1t" };
137137 if (entitlements?.plan === "enterprise") return { kind: "enterprise", label: "Paid by an enterprise" };
138+ if (entitlements?.plan === "complimentary") return { kind: "complimentary", label: "Complimentary from g1t" };
138139 if (plan?.included) return { kind: "comped", label: "Included by g1t" };
139140 const subscription = plan?.subscription;
140141 if (subscription?.status === "past_due") return { kind: "past_due", label: "Payment failed" };
+1−1
238238 what: "Private storage",
239239 free: `${gigabytes(tier.freePrivateStorageBytes)}, never charged. Past it, pushes to private repositories stop until you make room or start the plan.`,
240240 plan: `${gigabytes(tier.freePrivateStorageBytes)} free, then at cost plus 20%. Pushes never stop.`,
241− note: "Public repositories are never charged for storage.",
241+ note: "Public repository storage is free, with a 5 GB limit per workspace on every plan.",
242242 },
243243 {
244244 what: "Git operations",
+18−2
1616 SpikeBanner,
1717 TrialCard,
1818 } from "../../components/billing";
19−import { AddOns, BillingOnStripeCard, BudgetAlerts, PlanSummary, StripeSkeleton } from "../../components/billing-settings";
19+import { AddOns, BillingOnStripeCard, BudgetAlerts, ComplimentaryCard, PlanSummary, StripeSkeleton } from "../../components/billing-settings";
2020 import { StatementView } from "../../components/statement";
2121 import {
2222 cardCheckResult,
4444 prepaid_pending:
4545 "Payment pending. Your prepaid balance goes up once the payment clears, and this page shows it the next time it loads. A bank transfer can take a few business days.",
4646 budget: "Budget alerts saved.",
47+ converted: "The g1t plan is on, on the same billing. Usage from now on draws on its included usage first.",
4748 limit: "Spend limit saved.",
4849 requested: "Sent. g1t answers within one business day, here and by email.",
4950 told: "Thanks. g1t looks at it and answers within one business day, here and by email.",
183184 if (!started.ok) return fail(feature, started.error.message);
184185 throw redirect(started.value.url);
185186 }
187+ case "convert": {
188+ // From complimentary to the plan, on the same customer and subscription:
189+ // a card page, or the bank's confirmation, when one is needed first.
190+ const converted = await billing.convertToPaid(user, slug, here);
191+ if (!converted.ok) return fail("complimentary", converted.error.message);
192+ if (converted.value.status === "converted") throw done("converted");
193+ if (converted.value.url) throw redirect(converted.value.url);
194+ return fail("complimentary", converted.value.message);
195+ }
186196 case "card-check": {
187197 const started = await billing.cardCheck(user, slug, here);
188198 if (!started.ok) return fail("trial", started.error.message);
248258 const error = actionData as SectionError;
249259 const err = (section: string) => (error && error.section === section ? error.error : undefined);
250260 const standing = planStatus(plan, entitlements);
251− const paying = standing.kind === "paid" || standing.kind === "canceling" || standing.kind === "past_due";
261+ // A complimentary workspace charged past its allowance has the plan's limits too.
262+ const chargedPast = standing.kind === "complimentary" && Boolean(entitlements?.complimentary?.chargePastAllowance);
263+ const paying = standing.kind === "paid" || standing.kind === "canceling" || standing.kind === "past_due" || chargedPast;
252264 const free = standing.kind === "free" || standing.kind === "trial";
253265 const prepaid = limit?.prepaidMicros ?? entitlements?.prepaidMicros ?? 0;
254266 // The spend limit and its budget are the owners' to set only where
318330 <Await resolve={details}>{(loaded) => summary(loaded)}</Await>
319331 </Suspense>
320332
333+ {entitlements?.complimentary && standing.kind === "complimentary" && (
334+ <ComplimentaryCard month={entitlements.complimentary} owner={owner} enabled={status.enabled} error={err("complimentary")} />
335+ )}
336+
321337 {free && status.enabled && (
322338 <TrialCard entitlements={entitlements} trialMicros={trialMicros} owner={owner} enabled={status.enabled} error={err("trial")} />
323339 )}
+287−0
1+import assert from "node:assert/strict";
2+import { readFileSync, readdirSync } from "node:fs";
3+import { DatabaseSync } from "node:sqlite";
4+import { test } from "node:test";
5+
6+// Execute production SQL against the complete migrated schema. These verify
7+// transaction/retry contracts; Stripe HTTP and Worker execution need staging.
8+const source = name => readFileSync(new URL(`../services/billing/src/${name}.rs`, import.meta.url), "utf8");
9+const constant = (file, name) => {
10+ const match = source(file).match(new RegExp(`const ${name}: &str = "([\\s\\S]*?)";`));
11+ assert.ok(match, `${file}::${name}`);
12+ return match[1];
13+};
14+const prepares = (file, from, to) => {
15+ const text = source(file).split(from)[1].split(to)[0];
16+ return [...text.matchAll(/\.prepare\(\s*"([\s\S]*?)"\s*,?\s*\)/g)].map(m => m[1]);
17+};
18+function database() {
19+ const db = new DatabaseSync(":memory:");
20+ const dir = new URL("../services/billing/migrations/", import.meta.url);
21+ for (const name of readdirSync(dir).filter(n => n.endsWith(".sql")).sort()) db.exec(readFileSync(new URL(name, dir), "utf8"));
22+ return db;
23+}
24+const at = "2026-10-14T12:00:00Z";
25+test('late enterprise overdue writes cannot reopen terminal invoices or pause paid workspaces', () => {
26+ const db = database();
27+ db.prepare("INSERT INTO accounts(workspace,balance_micros,created_at) VALUES ('fixture',0,?)").run(at);
28+ db.prepare("INSERT INTO enterprise_invoices(invoice_id,account_id,period,amount_micros,status,created_by,created_at) VALUES ('in_fixture','ent_fixture','now',10000000,'open','staff',?)").run(at);
29+ const update = db.prepare(constant('webhooks','ENTERPRISE_STATUS'));
30+ const stop = db.prepare(constant('webhooks','ENTERPRISE_OVERDUE_STOP'));
31+ update.get('overdue','in_fixture');
32+ stop.run('fixture',at,'overdue','in_fixture');
33+ assert.equal(db.prepare("SELECT autopay_error FROM limits WHERE workspace='fixture'").get().autopay_error,'overdue');
34+ batch(db, [["UPDATE enterprise_invoices SET status='paid' WHERE invoice_id='in_fixture'", []], [constant('lib','CLEAR_STOP'), ['fixture']]]);
35+ assert.equal(update.get('overdue','in_fixture'), undefined);
36+ stop.run('fixture',at,'late overdue','in_fixture');
37+ assert.equal(db.prepare("SELECT autopay_error FROM limits WHERE workspace='fixture'").get().autopay_error,null);
38+ db.exec("UPDATE enterprise_invoices SET status='void'");
39+ assert.equal(update.get('overdue','in_fixture'), undefined);
40+ stop.run('fixture',at,'late overdue','in_fixture');
41+ assert.equal(db.prepare("SELECT autopay_error FROM limits WHERE workspace='fixture'").get().autopay_error,null);
42+});
43+test("enterprise invoice acquisition freezes one payload and rejects stale balances after completion", () => {
44+ const db = database();
45+ db.prepare("INSERT INTO accounts(workspace,balance_micros,created_at) VALUES ('fixture',-10000000,?)").run(at);
46+ db.prepare("INSERT INTO account_members(workspace,account_id,added_by,added_at) VALUES ('fixture','ent_fixture','staff',?)").run(at);
47+ const snapshots = JSON.stringify([{workspace:'fixture',balance:-10000000,invoiced:0}]);
48+ const acquire = db.prepare(constant('enterprise_attempt', 'INSERT'));
49+ const payload = JSON.stringify({lines:[{workspace:'fixture',amountMicros:10000000}]});
50+ acquire.run('ent_fixture','first',payload,1,snapshots);
51+ acquire.run('ent_fixture','second','{"amount":20000000}',2,snapshots);
52+ assert.deepEqual({...db.prepare('SELECT id,payload FROM enterprise_invoice_attempts').get()}, {id:'first',payload});
53+ assert.throws(() => db.exec("UPDATE account_members SET account_id='ent_other' WHERE workspace='fixture'"), /pending enterprise invoice/);
54+ assert.throws(() => db.exec("DELETE FROM account_members WHERE workspace='fixture'"), /pending enterprise invoice/);
55+ db.prepare("INSERT INTO enterprise_invoices(invoice_id,account_id,period,amount_micros,status,created_by,created_at) VALUES ('in_first','ent_fixture','now',10000000,'open','staff',?)").run(at);
56+ db.exec("INSERT INTO enterprise_invoice_lines VALUES ('in_first','fixture',10000000); UPDATE enterprise_invoice_attempts SET completed=1");
57+ db.prepare(constant('enterprise_attempt','RETIRE')).run('ent_fixture');
58+ acquire.run('ent_fixture','stale','{}',3,snapshots);
59+ assert.equal(db.prepare('SELECT COUNT(*) n FROM enterprise_invoice_attempts').get().n,0);
60+ db.exec("UPDATE accounts SET balance_micros=-15000000 WHERE workspace='fixture'");
61+ const fresh = JSON.stringify([{workspace:'fixture',balance:-15000000,invoiced:10000000}]);
62+ acquire.run('ent_fixture','additional','{"amount":5000000}',4,fresh);
63+ assert.equal(db.prepare('SELECT id FROM enterprise_invoice_attempts').get().id,'additional');
64+});
65+test("a late enterprise invoice writer cannot recreate old lines after completion and rename", () => {
66+ const db = database();
67+ const writes = prepares('webhooks', 'async fn finish_enterprise_attempt(', '/// An enterprise invoice paid:')
68+ .filter(sql => sql.startsWith('INSERT INTO enterprise_invoice'));
69+ assert.equal(writes.length,2);
70+ db.prepare("INSERT INTO enterprise_invoice_attempts(account_id,id,payload,created_ms) VALUES ('ent_fixture','attempt','{}',1)").run();
71+ const invoice = ['in_fixture','ent_fixture','now',10000000,null,'staff',at,'attempt'];
72+ const line = ['in_fixture','old',10000000,'attempt'];
73+ batch(db,[[writes[0],invoice],[writes[1],line]]);
74+ db.exec("UPDATE enterprise_invoice_attempts SET completed=1; UPDATE enterprise_invoice_lines SET workspace='renamed'");
75+ batch(db,[[writes[0],invoice],[writes[1],line]]);
76+ assert.deepEqual(db.prepare('SELECT workspace FROM enterprise_invoice_lines').all().map(r=>r.workspace),['renamed']);
77+ db.prepare(constant('enterprise_attempt','RETIRE')).run('ent_fixture');
78+ batch(db,[[writes[0],invoice],[writes[1],line]]);
79+ assert.equal(db.prepare('SELECT SUM(amount_micros) n FROM enterprise_invoice_lines').get().n,10000000);
80+});
81+test("fee waivers refuse subscriptions that may still charge, including delinquent plans", () => {
82+ const db = database();
83+ const insert = db.prepare("INSERT INTO subscriptions (workspace, feature, subscription_id, status, started_by, started_at, updated_at) VALUES (?, 'plan', ?, ?, 'staff', ?, ?)");
84+ for (const status of ['active', 'past_due', 'incomplete', 'unpaid', 'canceled']) {
85+ insert.run(status, `sub_${status}`, status, at, at);
86+ const found = db.prepare(constant('accounts', 'FEE_WAIVER_SUBSCRIPTIONS')).get(status);
87+ assert.equal(Boolean(found), status !== 'canceled', status);
88+ }
89+ assert.equal(db.prepare(constant('accounts', 'FEE_WAIVER_SUBSCRIPTIONS')).get('new-workspace'), undefined);
90+});
91+test("demo detach finds all sources through bounded compound selects", () => {
92+ const db = database();
93+ db.prepare("INSERT INTO accounts (workspace,balance_micros,created_at) VALUES ('demo',8000000,?)").run(at);
94+ db.prepare("INSERT INTO allowance_use (kind,scope,month,used) VALUES ('plan_credit','allowance-only','2026-10',500)").run();
95+ const sql = constant('detach', 'FOUND').replace('{internal}', constant('sales', 'INTERNAL_SQL'));
96+ const rows = db.prepare(sql).all(null, '2026-10');
97+ assert.deepEqual(rows.map(r => r.workspace), ['allowance-only', 'demo']);
98+ assert.equal(rows[0].plan_credit, 500);
99+ assert.equal(rows[1].balance, 8000000);
100+ assert.deepEqual(db.prepare(sql).all('demo', '2026-10').map(r => r.workspace), ['demo']);
101+ // D1 rejects the former long UNION. Each candidate CTE stays small.
102+ for (const cte of sql.split('AS MATERIALIZED (').slice(1)) {
103+ assert.ok((cte.split(')')[0].match(/UNION/g) ?? []).length <= 3);
104+ }
105+});
106+function batch(db, writes) {
107+ db.exec("BEGIN");
108+ try { for (const [sql, args] of writes) db.prepare(sql).run(...args); db.exec("COMMIT"); }
109+ catch (e) { db.exec("ROLLBACK"); throw e; }
110+}
111+const entries = prepares("lib", "pub(crate) fn entry_statements(", "/// What follows an entry");
112+const stops = prepares("lib", "fn after_entry_statements(", "async fn account(");
113+const extras = prepares("tax", "pub(crate) fn extras_statements(", "/// A payment's tax and fee");
114+function payment(reference, amount = 25_000_000) {
115+ assert.equal(entries.length, 2);
116+ assert.equal(stops.length, 1);
117+ assert.equal(extras.length, 1);
118+ return [
119+ [entries[0], [reference, "fixture", "top_up", amount, "Paid in advance", null, reference, null, at]],
120+ [entries[1], ["fixture", amount, "cus_fixture", at]],
121+ [stops[0], ["fixture", amount]],
122+ [constant("lib", "CLEAR_STOP"), ["fixture"]],
123+ [extras[0], [`${reference}/tax`, "fixture", "tax", 2_000_000, reference, "pi_fixture", null, at]],
124+ ];
125+}
126+test("payment, balance, dispute repayment and tax commit once together", () => {
127+ const db = database();
128+ db.prepare("INSERT INTO limits (workspace, updated_at, dispute_owed_micros, autopay_failed_at) VALUES (?, ?, ?, ?)").run("fixture", at, 30_000_000, at);
129+ batch(db, payment("cs_fixture"));
130+ assert.equal(db.prepare("SELECT balance_micros FROM accounts WHERE workspace='fixture'").get().balance_micros, 25_000_000);
131+ assert.equal(db.prepare("SELECT dispute_owed_micros FROM limits WHERE workspace='fixture'").get().dispute_owed_micros, 5_000_000);
132+ assert.throws(() => batch(db, payment("cs_fixture")), /UNIQUE/);
133+ assert.equal(db.prepare("SELECT dispute_owed_micros FROM limits WHERE workspace='fixture'").get().dispute_owed_micros, 5_000_000);
134+ assert.equal(db.prepare("SELECT COUNT(*) n FROM tax_and_fees").get().n, 1);
135+ batch(db, payment("cs_second", 5_000_000));
136+ assert.equal(db.prepare("SELECT autopay_failed_at FROM limits WHERE workspace='fixture'").get().autopay_failed_at, null);
137+ db.close();
138+});
139+test("a failure writing tax rolls back the credit and payment-stop updates", () => {
140+ const db = database();
141+ db.prepare("INSERT INTO limits (workspace, updated_at, dispute_owed_micros, autopay_failed_at) VALUES (?, ?, ?, ?)").run("fixture", at, 25_000_000, at);
142+ db.exec("CREATE TRIGGER fail_tax BEFORE INSERT ON tax_and_fees BEGIN SELECT RAISE(ABORT, 'injected failure'); END");
143+ assert.throws(() => batch(db, payment("cs_fixture")), /injected failure/);
144+ assert.equal(db.prepare("SELECT COUNT(*) n FROM ledger").get().n, 0);
145+ assert.equal(db.prepare("SELECT COUNT(*) n FROM accounts WHERE workspace='fixture'").get().n, 0);
146+ assert.equal(db.prepare("SELECT dispute_owed_micros FROM limits WHERE workspace='fixture'").get().dispute_owed_micros, 25_000_000);
147+ db.exec("DROP TRIGGER fail_tax");
148+ batch(db, payment("cs_fixture"));
149+ assert.equal(db.prepare("SELECT COUNT(*) n FROM tax_and_fees").get().n, 1);
150+ db.close();
151+});
152+test("payment never clears an open dispute", () => {
153+ const db = database();
154+ db.prepare("INSERT INTO limits (workspace, updated_at, autopay_failed_at) VALUES (?, ?, ?)").run("fixture", at, at);
155+ db.prepare("INSERT INTO disputes (dispute_id, workspace, charge_id, status, updated_at) VALUES ('dp_fixture', 'fixture', 'ch_fixture', 'open', ?)").run(at);
156+ batch(db, payment("cs_fixture"));
157+ assert.equal(db.prepare("SELECT autopay_failed_at FROM limits WHERE workspace='fixture'").get().autopay_failed_at, at);
158+ db.close();
159+});
160+test("concurrent invoice requests freeze one payload even as usage changes", () => {
161+ const db = database();
162+ db.prepare("INSERT INTO accounts (workspace, balance_micros, created_at) VALUES ('fixture', -10000000, ?)").run(at);
163+ const insert = db.prepare(constant("invoice_attempt", "INSERT"));
164+ insert.run("attempt_a", "fixture", '{"amount":1000}', 1000, -10_000_000);
165+ db.exec("UPDATE accounts SET balance_micros=-20000000 WHERE workspace='fixture'");
166+ insert.run("attempt_b", "fixture", '{"amount":2000}', 2000, -20_000_000);
167+ assert.deepEqual({ ...db.prepare("SELECT id, payload FROM invoice_attempts").get() }, { id: "attempt_a", payload: '{"amount":1000}' });
168+ assert.equal(db.prepare(constant("invoice_attempt", "RETIRE")).run("fixture").changes, 0);
169+ assert.equal(db.prepare(constant("closing", "UNPAID_INVOICES_SQL")).get("fixture").n, 1);
170+ db.close();
171+});
172+test("a stale invoice calculation cannot create an attempt after payment changes the balance", () => {
173+ const db = database();
174+ db.prepare("INSERT INTO accounts (workspace, balance_micros, created_at) VALUES ('fixture', 0, ?)").run(at);
175+ const result = db.prepare(constant("invoice_attempt", "INSERT")).run("attempt_a", "fixture", "{}", 1000, -10_000_000);
176+ assert.equal(result.changes, 0);
177+ db.close();
178+});
179+test("the migration registry covers every billing migration", () => {
180+ const dir = new URL("../services/billing/migrations/", import.meta.url);
181+ const lib = source("lib");
182+ for (const name of readdirSync(dir).filter(n => n.endsWith(".sql"))) assert.ok(lib.includes(`include_str!("../migrations/${name}")`), name);
183+});
184+
185+const sqlFile = name => readFileSync(new URL(`../services/billing/src/${name}.sql`, import.meta.url), "utf8");
186+function refund(db, cents, tax, plan = false) {
187+ const reference = `refund/ch_fixture/${cents + tax}`;
188+ const writes = [[sqlFile(plan ? "refund_plan" : "refund_ledger"), [reference, "fixture", cents * 10_000, reference, at, "refund/ch_fixture/%"]]];
189+ if (!plan) writes.push([constant("webhooks", "REFUND_BALANCE"), [reference, "fixture"]]);
190+ writes.push([sqlFile("refund_extras"), [`${reference}/tax`, "fixture", "tax", tax * 10_000, reference, "pi_fixture", at, "refund/ch_fixture/%"]]);
191+ batch(db, writes);
192+}
193+test("partial refunds, duplicate deliveries and reversed ordering debit only the cumulative total", () => {
194+ for (const order of [[1000, 2500, 2500], [2500, 1000, 2500]]) {
195+ const db = database();
196+ batch(db, payment("cs_fixture"));
197+ for (const cents of order) refund(db, cents, cents * 0.08);
198+ assert.equal(db.prepare("SELECT balance_micros FROM accounts WHERE workspace='fixture'").get().balance_micros, 0);
199+ assert.equal(db.prepare("SELECT SUM(amount_micros) n FROM tax_and_fees WHERE workspace='fixture'").get().n, 0);
200+ db.close();
201+ }
202+});
203+test("refund tax failure rolls back the reversal and retries without a second debit", () => {
204+ const db = database();
205+ batch(db, payment("cs_fixture"));
206+ db.exec("CREATE TRIGGER fail_refund_tax BEFORE INSERT ON tax_and_fees WHEN NEW.amount_micros < 0 BEGIN SELECT RAISE(ABORT, 'injected failure'); END");
207+ assert.throws(() => refund(db, 2500, 200), /injected failure/);
208+ assert.equal(db.prepare("SELECT balance_micros FROM accounts WHERE workspace='fixture'").get().balance_micros, 25_000_000);
209+ db.exec("DROP TRIGGER fail_refund_tax");
210+ refund(db, 2500, 200);
211+ refund(db, 2500, 200);
212+ assert.equal(db.prepare("SELECT balance_micros FROM accounts WHERE workspace='fixture'").get().balance_micros, 0);
213+ db.close();
214+});
215+test("plan refunds reduce revenue without debiting prepaid usage credit", () => {
216+ const db = database();
217+ batch(db, payment("cs_fixture"));
218+ refund(db, 1000, 80, true);
219+ refund(db, 2500, 200, true);
220+ refund(db, 1000, 80, true);
221+ assert.equal(db.prepare("SELECT SUM(amount_micros) n FROM plan_payments WHERE workspace='fixture'").get().n, -25_000_000);
222+ assert.equal(db.prepare("SELECT balance_micros FROM accounts WHERE workspace='fixture'").get().balance_micros, 25_000_000);
223+ db.close();
224+});
225+
226+test("one card cannot fund concurrent trials or another trial after being replaced", () => {
227+ const db = database();
228+ const grantSql = prepares("credits", "pub(crate) async fn ensure_grant(", "/// Takes up to")[0];
229+ const card = db.prepare("INSERT INTO card_checks (workspace, setup_intent, payment_method, fingerprint, funding, checked_by, checked_at) VALUES (?, ?, ?, ?, 'credit', 'fixture', ?)");
230+ card.run("trial_a", "seti_a", "pm_a", "same_card", at);
231+ card.run("trial_b", "seti_b", "pm_b", "same_card", at);
232+ const grant = db.prepare(grantSql);
233+ grant.run("trial_a", "2026-10", 5_000_000, at, 100_000_000);
234+ grant.run("trial_b", "2026-10", 5_000_000, at, 100_000_000);
235+ assert.equal(db.prepare("SELECT COUNT(*) n FROM trial_grants WHERE workspace IN ('trial_a','trial_b')").get().n, 1);
236+ db.exec("UPDATE card_checks SET fingerprint='replacement_card' WHERE workspace='trial_a'");
237+ grant.run("trial_b", "2026-10", 5_000_000, at, 100_000_000);
238+ assert.equal(db.prepare("SELECT COUNT(*) n FROM trial_grants WHERE workspace='trial_b'").get().n, 0);
239+ db.close();
240+});
241+
242+function usage(id, reference, workspace, gross, { plan = 10_000_000, trial = 1, pool = 1_000_000, repo = 1_000_000, given = 0, when = at } = {}) {
243+ const insert = prepares("storage", "pub(crate) async fn post_usage_with(", "/// Takes `charged`")[0];
244+ const balance = prepares("storage", "pub(crate) fn balance_if_posted(", "/// Writes down")[0];
245+ const month = when.slice(0, 7);
246+ return [
247+ [insert, [id, workspace, -gross, "Usage", `${workspace}/repo`, "sandbox", gross, reference, when, 0, 0, 0, 0, null]],
248+ [sqlFile("allocate_usage"), [id, workspace, month, plan, trial, `${workspace}/repo`, pool, repo, given]],
249+ [constant("allocation", "PLAN"), [id, month]],
250+ [constant("allocation", "GIFT"), [id, month]],
251+ [constant("allocation", "TRIAL"), [id]],
252+ [constant("allocation", "OSS_POOL"), [id, month]],
253+ [constant("allocation", "OSS_REPO"), [id, month, `${workspace}/repo`]],
254+ [balance, [workspace, at, id]],
255+ ];
256+}
257+test("usage allocation, source debits and ledger posting conserve credit across retries", () => {
258+ const db = database();
259+ db.prepare("INSERT INTO trial_grants (workspace, month, granted_micros, used_micros, created_at) VALUES ('fixture', '2026-10', 1000000, 0, ?)").run(at);
260+ batch(db, usage("usage_a", "run/a", "fixture", 12_000_000));
261+ const read = () => ({ ...db.prepare("SELECT amount_micros, credit_micros, trial_micros, oss_micros FROM ledger WHERE id='usage_a'").get() });
262+ assert.deepEqual(read(), { amount_micros: 0, credit_micros: 10_000_000, trial_micros: 1_000_000, oss_micros: 1_000_000 });
263+ batch(db, usage("duplicate", "run/a", "fixture", 12_000_000));
264+ assert.equal(db.prepare("SELECT SUM(used) n FROM allowance_use WHERE kind='plan_credit'").get().n, 10_000_000);
265+ batch(db, usage("usage_b", "run/b", "fixture", 2_000_000));
266+ assert.equal(db.prepare("SELECT balance_micros FROM accounts WHERE workspace='fixture'").get().balance_micros, -2_000_000);
267+ db.close();
268+});
269+test("an interrupted usage transaction consumes no allowance and a retry posts once", () => {
270+ const db = database();
271+ db.exec("CREATE TRIGGER fail_usage_balance BEFORE INSERT ON accounts WHEN NEW.workspace='fixture' BEGIN SELECT RAISE(ABORT, 'injected failure'); END");
272+ assert.throws(() => batch(db, usage("usage_a", "run/a", "fixture", 5_000_000)), /injected failure/);
273+ assert.equal(db.prepare("SELECT COUNT(*) n FROM ledger WHERE id='usage_a'").get().n, 0);
274+ assert.equal(db.prepare("SELECT COUNT(*) n FROM allowance_use WHERE scope='fixture'").get().n, 0);
275+ db.exec("DROP TRIGGER fail_usage_balance");
276+ batch(db, usage("usage_b", "run/a", "fixture", 5_000_000));
277+ assert.equal(db.prepare("SELECT used FROM allowance_use WHERE scope='fixture' AND kind='plan_credit'").get().used, 5_000_000);
278+ db.close();
279+});
280+test("different workspaces share one bounded OSS pool and free overruns remain explicit", () => {
281+ const db = database();
282+ batch(db, usage("a", "run/a", "a", 800_000, { plan: 0, trial: 0 }));
283+ batch(db, usage("b", "run/b", "b", 800_000, { plan: 0, trial: 0, given: 1 }));
284+ assert.equal(db.prepare("SELECT used FROM allowance_use WHERE kind='oss_pool'").get().used, 1_000_000);
285+ assert.deepEqual({ ...db.prepare("SELECT amount_micros, oss_micros, given_micros FROM ledger WHERE id='b'").get() }, { amount_micros: 0, oss_micros: 200_000, given_micros: 600_000 });
286+ db.close();
287+});
+219−0
1+import assert from "node:assert/strict";
2+import { readFileSync, readdirSync } from "node:fs";
3+import { DatabaseSync } from "node:sqlite";
4+import { test } from "node:test";
5+
6+// Complimentary enrollment against the complete migrated schema, using the
7+// production SQL: the allocation that posts usage, and the terms change as
8+// `admin_set_complimentary` writes it.
9+const source = name => readFileSync(new URL(`../services/billing/src/${name}.rs`, import.meta.url), "utf8");
10+const sqlFile = name => readFileSync(new URL(`../services/billing/src/${name}.sql`, import.meta.url), "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 prepares = (file, from, to) => {
17+ const text = source(file).split(from)[1].split(to)[0];
18+ return [...text.matchAll(/\.prepare\(\s*"([\s\S]*?)"\s*,?\s*\)/g)].map(m => m[1]);
19+};
20+const migrations = new URL("../services/billing/migrations/", import.meta.url);
21+const names = readdirSync(migrations).filter(n => n.endsWith(".sql")).sort();
22+function database(before = null) {
23+ const db = new DatabaseSync(":memory:");
24+ for (const name of names.filter(n => before === null || n < before)) db.exec(readFileSync(new URL(name, migrations), "utf8"));
25+ return db;
26+}
27+function batch(db, writes) {
28+ db.exec("BEGIN");
29+ try { for (const [sql, args] of writes) db.prepare(sql).run(...args); db.exec("COMMIT"); }
30+ catch (e) { db.exec("ROLLBACK"); throw e; }
31+}
32+const at = "2026-10-14T12:00:00.000Z";
33+function usage(id, reference, workspace, gross, { plan = 10_000_000, trial = 1, given = 0, when = at } = {}) {
34+ const insert = prepares("storage", "pub(crate) async fn post_usage_with(", "/// Takes `charged`")[0];
35+ const balance = prepares("storage", "pub(crate) fn balance_if_posted(", "/// Writes down")[0];
36+ const month = when.slice(0, 7);
37+ return [
38+ [insert, [id, workspace, -gross, "Usage", `${workspace}/repo`, "sandbox", gross, reference, when, 0, 0, 0, 0, null]],
39+ [sqlFile("allocate_usage"), [id, workspace, month, plan, trial, `${workspace}/repo`, 1_000_000, 1_000_000, given]],
40+ [constant("allocation", "PLAN"), [id, month]],
41+ [constant("allocation", "GIFT"), [id, month]],
42+ [constant("allocation", "TRIAL"), [id]],
43+ [constant("allocation", "OSS_POOL"), [id, month]],
44+ [constant("allocation", "OSS_REPO"), [id, month, `${workspace}/repo`]],
45+ [balance, [workspace, when, id]],
46+ ];
47+}
48+const enrolledAt = "2026-10-01T00:00:00.000Z";
49+function enroll(db, workspace, allowance, { unlimited = 0, overage = "stop" } = {}) {
50+ db.prepare(`INSERT INTO complimentary (workspace, grant_id, status, allowance_micros, unlimited, fee_cents, note, created_by, created_at, created_ms, updated_by, updated_at, subscription_id, overage)
51+ VALUES (?, ?, 'active', ?, ?, 0, 'partner', 'staff', ?, 0, 'staff', ?, ?, ?)`).run(workspace, `cmp_${workspace}`, allowance, unlimited, enrolledAt, enrolledAt, `sub_${workspace}`, overage);
52+ db.prepare(`INSERT INTO complimentary_periods (id, workspace, grant_id, starts_at, allowance_micros, unlimited, fee_cents, note, created_by, created_at, overage)
53+ VALUES (?, ?, ?, ?, ?, ?, 0, 'partner', 'staff', ?, ?)`).run(`p_${workspace}`, workspace, `cmp_${workspace}`, enrolledAt, allowance, unlimited, enrolledAt, overage);
54+}
55+const termsChange = prepares("complimentary", "// One batch: the open period closes", "self.audit(");
56+function changeTerms(db, workspace, allowance, when, overage = "stop") {
57+ assert.equal(termsChange.length, 3);
58+ batch(db, [
59+ [termsChange[0], [workspace, when]],
60+ [termsChange[1], [`p_${when}`, workspace, `cmp_${workspace}`, when, allowance, 0, 0, "changed", "staff", overage]],
61+ [termsChange[2], [workspace, allowance, 0, 0, "changed", "staff", when, JSON.stringify({ op: "terms", period: `p_${when}` }), overage]],
62+ ]);
63+}
64+const line = (db, id) => ({ ...db.prepare("SELECT amount_micros, credit_micros, gift_micros, trial_micros, oss_micros, given_micros FROM ledger WHERE id=?").get(id) });
65+const giftUsed = (db, workspace, month = "2026-10") =>
66+ db.prepare("SELECT used FROM allowance_use WHERE kind='complimentary' AND scope=? AND month=?").get(workspace, month)?.used ?? 0;
67+
68+test("a complimentary allowance pays first and alone, and past it g1t covers rather than charging", () => {
69+ const db = database();
70+ enroll(db, "acme", 10_000_000);
71+ db.prepare("INSERT INTO trial_grants (workspace, month, granted_micros, used_micros, created_at) VALUES ('acme', '2026-10', 5000000, 0, ?)").run(at);
72+ batch(db, usage("a", "run/a", "acme", 6_000_000));
73+ assert.deepEqual(line(db, "a"), { amount_micros: 0, credit_micros: 0, gift_micros: 6_000_000, trial_micros: 0, oss_micros: 0, given_micros: 0 });
74+ // Running work that outlasts the allowance: the rest is g1t's, recorded
75+ // apart from the allowance, and nothing is owed.
76+ batch(db, usage("b", "run/b", "acme", 6_000_000));
77+ assert.deepEqual(line(db, "b"), { amount_micros: 0, credit_micros: 0, gift_micros: 4_000_000, trial_micros: 0, oss_micros: 0, given_micros: 2_000_000 });
78+ assert.equal(giftUsed(db, "acme"), 10_000_000);
79+ assert.equal(db.prepare("SELECT balance_micros FROM accounts WHERE workspace='acme'").get().balance_micros, 0);
80+ // Plan credit, the trial and the pools were never touched.
81+ assert.equal(db.prepare("SELECT COUNT(*) n FROM allowance_use WHERE kind IN ('plan_credit','oss_pool','oss_repo')").get().n, 0);
82+ assert.equal(db.prepare("SELECT used_micros FROM trial_grants WHERE workspace='acme'").get().used_micros, 0);
83+ // A duplicate delivery consumes nothing more.
84+ batch(db, usage("dup", "run/b", "acme", 6_000_000));
85+ assert.equal(giftUsed(db, "acme"), 10_000_000);
86+ assert.equal(db.prepare("SELECT COUNT(*) n FROM ledger").get().n, 2);
87+ db.close();
88+});
89+
90+test("changing the amount keeps the month's consumption; lowering it never creates a debt", () => {
91+ const db = database();
92+ enroll(db, "acme", 10_000_000);
93+ batch(db, usage("a", "run/a", "acme", 8_000_000, { when: "2026-10-10T00:00:00.000Z" }));
94+ changeTerms(db, "acme", 5_000_000, "2026-10-11T00:00:00.000Z");
95+ assert.equal(db.prepare("SELECT COUNT(*) n FROM complimentary_periods WHERE workspace='acme' AND ends_at IS NULL").get().n, 1);
96+ assert.equal(db.prepare("SELECT allowance_micros FROM complimentary WHERE workspace='acme'").get().allowance_micros, 5_000_000);
97+ // Lowered below what was used: nothing more from it, nothing owed.
98+ batch(db, usage("b", "run/b", "acme", 1_000_000, { when: "2026-10-12T00:00:00.000Z" }));
99+ assert.deepEqual(line(db, "b"), { amount_micros: 0, credit_micros: 0, gift_micros: 0, trial_micros: 0, oss_micros: 0, given_micros: 1_000_000 });
100+ assert.equal(giftUsed(db, "acme"), 8_000_000, "the earlier draw is kept, not reset");
101+ // Raised: only what is over the month's use is left.
102+ changeTerms(db, "acme", 20_000_000, "2026-10-13T00:00:00.000Z");
103+ batch(db, usage("c", "run/c", "acme", 15_000_000, { when: "2026-10-14T00:00:00.000Z" }));
104+ assert.equal(line(db, "c").gift_micros, 12_000_000);
105+ assert.equal(line(db, "c").given_micros, 3_000_000);
106+ assert.equal(giftUsed(db, "acme"), 20_000_000);
107+ // A new month starts the allowance again, with no rollover.
108+ batch(db, usage("d", "run/d", "acme", 25_000_000, { when: "2026-11-01T00:00:01.000Z" }));
109+ assert.equal(line(db, "d").gift_micros, 20_000_000);
110+ assert.equal(giftUsed(db, "acme", "2026-11"), 20_000_000);
111+ assert.equal(db.prepare("SELECT SUM(amount_micros) n FROM ledger").get().n, 0, "nothing was ever charged");
112+ db.close();
113+});
114+
115+test("late usage draws on the period it happened in, not the one it is posted in", () => {
116+ const db = database();
117+ enroll(db, "acme", 10_000_000);
118+ // Converted to the plan on the 15th.
119+ db.exec("UPDATE complimentary_periods SET ends_at='2026-10-15T00:00:00.000Z'; UPDATE complimentary SET status='ended', ended_reason='converted'");
120+ batch(db, usage("before", "run/before", "acme", 3_000_000, { when: "2026-10-14T23:00:00.000Z" }));
121+ assert.equal(line(db, "before").gift_micros, 3_000_000, "gifted usage is never repriced after conversion");
122+ assert.equal(line(db, "before").amount_micros, 0);
123+ batch(db, usage("after", "run/after", "acme", 3_000_000, { when: "2026-10-15T01:00:00.000Z" }));
124+ assert.deepEqual(line(db, "after"), { amount_micros: 0, credit_micros: 3_000_000, gift_micros: 0, trial_micros: 0, oss_micros: 0, given_micros: 0 });
125+ db.close();
126+});
127+
128+test("charged past the allowance: g1t pays the allowance, the workspace's own card the rest", () => {
129+ const db = database();
130+ enroll(db, "friend", 10_000_000, { overage: "charge" });
131+ // No card yet: past the allowance it is g1t's, never a debt.
132+ batch(db, usage("z", "run/z", "friend", 12_000_000, { when: "2026-11-02T00:00:00.000Z" }));
133+ assert.deepEqual(line(db, "z"), { amount_micros: 0, credit_micros: 0, gift_micros: 10_000_000, trial_micros: 0, oss_micros: 0, given_micros: 2_000_000 });
134+ db.prepare("INSERT INTO card_checks (workspace, setup_intent, payment_method, checked_by, checked_at) VALUES ('friend', 'seti_1', 'pm_1', 'owner', ?)").run(at);
135+ batch(db, usage("a", "run/a", "friend", 6_000_000));
136+ batch(db, usage("b", "run/b", "friend", 7_000_000));
137+ // 4 of 7 from the allowance, 3 charged; nothing covered by g1t.
138+ assert.deepEqual(line(db, "b"), { amount_micros: -3_000_000, credit_micros: 0, gift_micros: 4_000_000, trial_micros: 0, oss_micros: 0, given_micros: 0 });
139+ assert.equal(db.prepare("SELECT balance_micros FROM accounts WHERE workspace='friend'").get().balance_micros, -3_000_000);
140+ // Switched to stop mid-month: from then, past the allowance is g1t's.
141+ changeTerms(db, "friend", 10_000_000, "2026-10-15T00:00:00.000Z", "stop");
142+ batch(db, usage("c", "run/c", "friend", 1_000_000, { when: "2026-10-16T00:00:00.000Z" }));
143+ assert.deepEqual(line(db, "c"), { amount_micros: 0, credit_micros: 0, gift_micros: 0, trial_micros: 0, oss_micros: 0, given_micros: 1_000_000 });
144+ // Usage from before the switch, posted late, keeps the period it happened in.
145+ batch(db, usage("d", "run/d", "friend", 2_000_000, { when: "2026-10-14T00:00:00.000Z" }));
146+ assert.equal(line(db, "d").amount_micros, -2_000_000);
147+ db.close();
148+});
149+
150+test("an enrollment's fee paid while enrolled is not evidence of the plan", () => {
151+ const db = database();
152+ enroll(db, "acme", 10_000_000);
153+ db.exec("UPDATE complimentary_periods SET ends_at='2026-10-15T00:00:00.000Z'");
154+ const evidence = db.prepare(constant("storage", "PLAN_EVIDENCE"));
155+ db.exec("UPDATE complimentary_periods SET fee_cents = 2500");
156+ db.prepare("INSERT INTO plan_payments (invoice_id, workspace, amount_micros, paid_at) VALUES ('in_fee', 'acme', 25000000, '2026-10-02T00:00:00.000Z')").run();
157+ assert.equal(evidence.get("acme", "2026-10-01", "2026-11-01", "2026-10").had, 0);
158+ // The conversion's first plan payment, made while the period is still open, is the plan.
159+ db.prepare("INSERT INTO plan_payments (invoice_id, workspace, amount_micros, paid_at) VALUES ('in_plan', 'acme', 21000000, '2026-10-14T23:59:00.000Z')").run();
160+ assert.equal(evidence.get("acme", "2026-10-01", "2026-11-01", "2026-10").had, 1);
161+ db.close();
162+});
163+
164+test("an unlimited enrollment pays everything and still records what it used", () => {
165+ const db = database();
166+ enroll(db, "flagon-io", 0, { unlimited: 1 });
167+ batch(db, usage("a", "run/a", "flagon-io", 900_000_000));
168+ assert.equal(line(db, "a").gift_micros, 900_000_000);
169+ assert.equal(line(db, "a").given_micros, 0);
170+ assert.equal(giftUsed(db, "flagon-io"), 900_000_000);
171+ db.close();
172+});
173+
174+test("an interrupted posting consumes no complimentary allowance", () => {
175+ const db = database();
176+ enroll(db, "acme", 10_000_000);
177+ db.exec("CREATE TRIGGER fail_balance BEFORE INSERT ON accounts WHEN NEW.workspace='acme' BEGIN SELECT RAISE(ABORT, 'injected failure'); END");
178+ assert.throws(() => batch(db, usage("a", "run/a", "acme", 4_000_000)), /injected failure/);
179+ assert.equal(giftUsed(db, "acme"), 0);
180+ db.exec("DROP TRIGGER fail_balance");
181+ batch(db, usage("b", "run/a", "acme", 4_000_000));
182+ assert.equal(giftUsed(db, "acme"), 4_000_000);
183+ db.close();
184+});
185+
186+test("one open period per workspace", () => {
187+ const db = database();
188+ enroll(db, "acme", 1);
189+ assert.throws(() => db.prepare(`INSERT INTO complimentary_periods (id, workspace, grant_id, starts_at, allowance_micros, note, created_by, created_at)
190+ VALUES ('second', 'acme', 'cmp_acme', ?, 1, 'x', 'staff', ?)`).run(at, at), /UNIQUE/);
191+ db.close();
192+});
193+
194+test("migration 0065 returns every workspace but flagon-io to Free, with an audit entry", () => {
195+ const db = database("0065");
196+ const account = db.prepare("INSERT INTO billing_accounts (id, kind, name, terms_kind, discount_percent, team_granted, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, 'staff', ?)");
197+ account.run("ws_flagon-io-x", "workspace", "flagon-io-x", "comped", 100, 0, at);
198+ db.prepare("UPDATE billing_accounts SET terms_kind='custom' WHERE id='ws_flagon-io'").run();
199+ account.run("ws_partner", "workspace", "partner", "comped", 100, 0, at);
200+ account.run("ws_given", "workspace", "given", "standard", 0, 1, at);
201+ account.run("ws_plain", "workspace", "plain", "standard", 0, 0, at);
202+ db.prepare("INSERT INTO account_members (workspace, account_id, added_by, added_at) VALUES ('member', 'ent_x', 'staff', ?)").run(at);
203+ // One on an enterprise invoice still being made: kept, not a failed deploy.
204+ db.prepare("INSERT INTO account_members (workspace, account_id, added_by, added_at) VALUES ('invoicing', 'ent_y', 'staff', ?)").run(at);
205+ db.prepare("INSERT INTO enterprise_invoice_attempts (account_id, id, payload, created_ms) VALUES ('ent_y', 'att', ?, 1)")
206+ .run(JSON.stringify({ lines: [{ workspace: "invoicing", amountMicros: 1 }] }));
207+ const flagonBefore = db.prepare("SELECT * FROM billing_accounts WHERE id='ws_flagon-io'").get();
208+ db.exec(readFileSync(new URL(names.find(n => n.startsWith("0065")), migrations), "utf8"));
209+ const terms = Object.fromEntries(db.prepare("SELECT id, terms_kind, discount_percent, team_granted FROM billing_accounts").all().map(r => [r.id, [r.terms_kind, r.discount_percent, r.team_granted]]));
210+ assert.deepEqual({ ...db.prepare("SELECT * FROM billing_accounts WHERE id='ws_flagon-io'").get() }, { ...flagonBefore }, "flagon-io is untouched");
211+ assert.deepEqual(terms["ws_flagon-io-x"], ["standard", 0, 0], "only flagon-io itself is kept");
212+ assert.deepEqual(terms.ws_partner, ["standard", 0, 0]);
213+ assert.deepEqual(terms.ws_given, ["standard", 0, 0]);
214+ assert.deepEqual(terms.ws_plain, ["standard", 0, 0]);
215+ assert.deepEqual(db.prepare("SELECT workspace FROM account_members").all().map(r => r.workspace), ["invoicing"]);
216+ const audited = db.prepare("SELECT account FROM admin_actions WHERE by='billing' ORDER BY account").all().map(r => r.account);
217+ assert.deepEqual(audited, ["ent_x", "ws_flagon-io-x", "ws_given", "ws_partner"]);
218+ db.close();
219+});
+107−0
1+import assert from "node:assert/strict";
2+import { readFileSync, readdirSync } from "node:fs";
3+import { DatabaseSync } from "node:sqlite";
4+import { test } from "node:test";
5+
6+// A month-end source's figure reported after its month was charged or
7+// closed: the production SQL of `late_correction` (src/storage.rs).
8+const source = name => readFileSync(new URL(`../services/billing/src/${name}.rs`, import.meta.url), "utf8");
9+const sqlFile = name => readFileSync(new URL(`../services/billing/src/${name}.sql`, import.meta.url), "utf8");
10+const constant = (file, name) => {
11+ const match = source(file).match(new RegExp(`const ${name}: &str = "([\\s\\S]*?)";`));
12+ assert.ok(match, `${file}::${name}`);
13+ return match[1];
14+};
15+function database() {
16+ const db = new DatabaseSync(":memory:");
17+ const dir = new URL("../services/billing/migrations/", import.meta.url);
18+ for (const name of readdirSync(dir).filter(n => n.endsWith(".sql")).sort()) db.exec(readFileSync(new URL(name, dir), "utf8"));
19+ return db;
20+}
21+function batch(db, writes) {
22+ db.exec("BEGIN");
23+ try {
24+ const rows = writes.map(([sql, args]) => db.prepare(sql).all(...args));
25+ db.exec("COMMIT");
26+ return rows;
27+ } catch (e) {
28+ db.exec("ROLLBACK");
29+ throw e;
30+ }
31+}
32+const now = "2026-11-03T10:00:00.000Z";
33+// One late report, as `late_correction` sends it: the raise, the guarded
34+// ledger line, the allocation and the balance, in one batch.
35+function late(db, id, workspace, newCost, charge, oldRaw, { plan = 0 } = {}) {
36+ const raise = constant("storage", "LATE_RAISE");
37+ const ledger = constant("storage", "LATE_LEDGER");
38+ const balanceSql = source("storage").split("pub(crate) fn balance_if_posted(")[1].match(/\.prepare\(\s*"([\s\S]*?)"\s*,?\s*\)/)[1];
39+ const reference = `storage/${workspace}/2026-10/late/${newCost}`;
40+ const rows = batch(db, [
41+ [raise, [workspace, "storage", "2026-10", newCost, newCost, now, null, oldRaw]],
42+ [ledger, [id, workspace, -charge, "Late", "storage", newCost - oldRaw, reference, "2026-10-31T23:59:59Z", null, "2026-10", newCost]],
43+ [sqlFile("allocate_usage"), [id, workspace, "2026-10", plan, 0, "", 0, 0, 0]],
44+ [constant("allocation", "PLAN"), [id, "2026-10"]],
45+ [constant("allocation", "GIFT"), [id, "2026-10"]],
46+ [balanceSql, [workspace, now, id]],
47+ ]);
48+ return rows[1].length === 1;
49+}
50+function charged(db, workspace, cost) {
51+ db.prepare("INSERT INTO pending_usage (workspace, source, month, charge_micros, cost_micros, updated_at, charged_at) VALUES (?, 'storage', '2026-10', ?, ?, ?, ?)")
52+ .run(workspace, cost, cost, now, now);
53+}
54+const owed = (db, workspace) => db.prepare("SELECT balance_micros FROM accounts WHERE workspace=?").get(workspace)?.balance_micros ?? 0;
55+
56+test("a late higher figure charges only what it adds, once", () => {
57+ const db = database();
58+ charged(db, "acme", 5_000_000);
59+ assert.equal(late(db, "a", "acme", 7_000_000, 2_400_000, 5_000_000), true);
60+ assert.equal(owed(db, "acme"), -2_400_000);
61+ assert.equal(db.prepare("SELECT cost_micros FROM pending_usage WHERE workspace='acme'").get().cost_micros, 7_000_000);
62+ // The same report again: worked out from the new figure, nothing changes.
63+ assert.equal(late(db, "b", "acme", 7_000_000, 2_400_000, 5_000_000), false);
64+ assert.equal(owed(db, "acme"), -2_400_000);
65+ assert.equal(db.prepare("SELECT COUNT(*) n FROM ledger").get().n, 1);
66+ db.close();
67+});
68+
69+test("of two late reports worked out from the same figure, only one is charged", () => {
70+ const db = database();
71+ charged(db, "acme", 5_000_000);
72+ assert.equal(late(db, "a", "acme", 8_000_000, 3_600_000, 5_000_000), true);
73+ // A stale writer that read 5,000,000 before the first moved it.
74+ assert.equal(late(db, "b", "acme", 6_000_000, 1_200_000, 5_000_000), false);
75+ assert.equal(owed(db, "acme"), -3_600_000);
76+ assert.equal(db.prepare("SELECT cost_micros FROM pending_usage WHERE workspace='acme'").get().cost_micros, 8_000_000);
77+ db.close();
78+});
79+
80+test("a closed month with no row yet takes the late figure as a charged row", () => {
81+ const db = database();
82+ db.prepare("INSERT INTO month_closes (workspace, month, status, amount_micros, closed_at) VALUES ('acme', '2026-10', 'charged', 0, ?)").run(now);
83+ // The ordinary report is refused for a closed month…
84+ db.prepare(constant("storage", "SET_PENDING")).run("acme", "storage", "2026-10", 1, 1, now, null);
85+ assert.equal(db.prepare("SELECT COUNT(*) n FROM pending_usage").get().n, 0);
86+ // …and the late one charges it, and marks the row charged.
87+ assert.equal(late(db, "a", "acme", 4_000_000, 4_800_000, 0), true);
88+ assert.equal(owed(db, "acme"), -4_800_000);
89+ assert.ok(db.prepare("SELECT charged_at FROM pending_usage WHERE workspace='acme'").get().charged_at.startsWith("late: "));
90+ db.close();
91+});
92+
93+test("a late correction draws on that month's allowances first", () => {
94+ const db = database();
95+ charged(db, "acme", 1_000_000);
96+ db.prepare("INSERT INTO allowance_use (kind, scope, month, used) VALUES ('plan_credit', 'acme', '2026-10', 9000000)").run();
97+ assert.equal(late(db, "a", "acme", 3_000_000, 2_400_000, 1_000_000, { plan: 10_000_000 }), true);
98+ assert.deepEqual({ ...db.prepare("SELECT amount_micros, credit_micros FROM ledger").get() }, { amount_micros: -1_400_000, credit_micros: 1_000_000 });
99+ assert.equal(db.prepare("SELECT used FROM allowance_use WHERE kind='plan_credit'").get().used, 10_000_000);
100+ db.close();
101+});
102+
103+test("only a raise is charged: a lower late figure changes nothing", () => {
104+ const src = source("storage");
105+ assert.match(src, /pub\(crate\) fn late_delta\(charged_net: i64, new_net: i64\) -> i64 \{\s*\(new_net - charged_net\)\.max\(0\)/);
106+ assert.match(src, /if new_cost < raw_old \{[\s\S]*?return Ok\(true\);/);
107+});
+72−0
1+import assert from "node:assert/strict";
2+import { readFileSync, readdirSync } from "node:fs";
3+import { DatabaseSync } from "node:sqlite";
4+import { test } from "node:test";
5+
6+const source = readFileSync(new URL("../services/repos/src/public_storage.rs", import.meta.url), "utf8");
7+const sql = name => source.match(new RegExp(`const ${name}: &str = "([\\s\\S]*?)";`))[1];
8+function database() {
9+ const db = new DatabaseSync(":memory:");
10+ const dir = new URL("../services/repos/migrations/", import.meta.url);
11+ for (const name of readdirSync(dir).filter(n => n.endsWith(".sql")).sort()) db.exec(readFileSync(new URL(name, dir), "utf8"));
12+ return db;
13+}
14+function repo(db, id, bytes, workspace = "team", privateRepo = 0) {
15+ db.prepare("INSERT INTO repos(id,namespace,name,owner_id,stored_bytes,is_private,created_at) VALUES (?,?,?,'owner',?,?,'2026-10-14T00:00:00Z')")
16+ .run(id, workspace, id, bytes, privateRepo);
17+}
18+const reserve = (db, id, bytes, cap = 5_000_000_000) => db.prepare(sql("RESERVE")).get({ 1: id, 2: bytes, 3: cap });
19+
20+test("overlapping pushes cannot reserve the same final capacity", () => {
21+ const db = database();
22+ repo(db, "first", 4_900_000_000); repo(db, "second", 0);
23+ assert.ok(reserve(db, "first", 80_000_000));
24+ assert.equal(reserve(db, "second", 80_000_000), undefined);
25+ assert.ok(reserve(db, "second", 20_000_000));
26+ assert.equal(reserve(db, "first", 1), undefined);
27+ assert.equal(db.prepare("SELECT SUM(stored_bytes + storage_reserved_bytes) n FROM repos").get().n, 5_000_000_000);
28+ assert.equal(db.prepare("SELECT SUM(stored_bytes) n FROM repos").get().n, 4_900_000_000, "reservations are not measured storage billed to customers");
29+ db.close();
30+});
31+
32+test("deleted retained data and interrupted write reservations still consume capacity", () => {
33+ const db = database();
34+ repo(db, "retained", 4_950_000_000); repo(db, "active", 0);
35+ db.exec("UPDATE repos SET deleted_at = '2026-10-14T00:00:00Z' WHERE id = 'retained'");
36+ assert.ok(reserve(db, "active", 50_000_000));
37+ assert.equal(reserve(db, "active", 1), undefined);
38+ db.exec("DELETE FROM repos WHERE id = 'retained'");
39+ assert.ok(reserve(db, "active", 1));
40+ db.close();
41+});
42+
43+test("concurrent visibility changes cannot bypass public capacity", () => {
44+ const db = database();
45+ repo(db, "public", 4_000_000_000); repo(db, "private_a", 750_000_000, "team", 1); repo(db, "private_b", 750_000_000, "team", 1);
46+ assert.ok(db.prepare(sql("MAKE_PUBLIC")).get({ 1: "private_a", 2: 5_000_000_000 }));
47+ assert.equal(db.prepare(sql("MAKE_PUBLIC")).get({ 1: "private_b", 2: 5_000_000_000 }), undefined);
48+ assert.ok(reserve(db, "private_b", 100_000_000));
49+ assert.equal(db.prepare("SELECT is_private FROM repos WHERE id='private_b'").get().is_private, 1);
50+ db.close();
51+});
52+
53+test("transfer admission uses destination capacity at the moment of mutation", () => {
54+ const db = database();
55+ repo(db, "existing", 4_500_000_000, "destination"); repo(db, "moving", 600_000_000);
56+ assert.equal(db.prepare(sql("TRANSFER")).get({ 1: "moving", 2: "destination", 3: "team", 4: 5_000_000_000 }), undefined);
57+ assert.equal(db.prepare("SELECT namespace FROM repos WHERE id='moving'").get().namespace, "team");
58+ db.exec("DELETE FROM repos WHERE id='existing'");
59+ assert.ok(db.prepare(sql("TRANSFER")).get({ 1: "moving", 2: "destination", 3: "team", 4: 5_000_000_000 }));
60+ db.close();
61+});
62+
63+test("native copy freezes new source writes until its storage snapshot is taken", () => {
64+ const db = database();
65+ repo(db, "source", 100); repo(db, "fork", 0);
66+ db.exec("INSERT INTO storage_copy_locks VALUES ('source','fork','2026-10-14T00:00:00Z')");
67+ assert.equal(reserve(db, "source", 20), undefined);
68+ assert.ok(reserve(db, "fork", 100));
69+ db.exec("DELETE FROM storage_copy_locks WHERE repo_id='source'");
70+ assert.ok(reserve(db, "source", 20));
71+ db.close();
72+});
+214−1
188188 /// workspace's last trial run that went past its trial credit.
189189 #[serde(default)]
190190 pub given_micros: i64,
191+ /// For usage: what a complimentary allowance from g1t paid of it.
192+ #[serde(default)]
193+ pub gift_micros: i64,
191194 /// For usage: what the account's discount took off its price. The
192195 /// price is `-amount_micros` plus this and what paid for it.
193196 #[serde(default)]
861864 Internal,
862865 /// Paid for by an enterprise account, invoiced.
863866 Enterprise,
867+ /// Enrolled by g1t staff (`admin_set_complimentary`): the plan's
868+ /// features, a monthly usage allowance valued at ordinary prices, and a
869+ /// monthly fee of its own (often none). Never charged on demand: past
870+ /// the allowance new billable work stops.
871+ Complimentary,
864872 }
865873
866874 impl PlanKind {
870878 PlanKind::Paid => "paid",
871879 PlanKind::Internal => "internal",
872880 PlanKind::Enterprise => "enterprise",
881+ PlanKind::Complimentary => "complimentary",
873882 }
874883 }
875884
876885 /// Whether usage past what is included may be charged (on demand).
877886 pub fn on_demand(self) -> bool {
878− !matches!(self, PlanKind::Free)
887+ !matches!(self, PlanKind::Free | PlanKind::Complimentary)
879888 }
880889 }
881890
10461055 /// Where usage stands against what is included and the limits, from 50%.
10471056 #[serde(default)]
10481057 pub alerts: Vec<UsageAlert>,
1058+ /// A complimentary enrollment's allowance this month, when the plan is
1059+ /// `complimentary` (or g1t's own with one).
1060+ #[serde(default, skip_serializing_if = "Option::is_none")]
1061+ pub complimentary: Option<ComplimentaryMonth>,
1062+}
1063+
1064+/// A complimentary allowance this calendar month (UTC), as the workspace's
1065+/// Billing page and sudo show it. Usage counts at its ordinary price.
1066+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1067+#[serde(rename_all = "camelCase")]
1068+pub struct ComplimentaryMonth {
1069+ /// `YYYY-MM`.
1070+ pub month: String,
1071+ /// This month's allowance; `unlimited` ignores it.
1072+ pub allowance_micros: i64,
1073+ #[serde(default)]
1074+ pub unlimited: bool,
1075+ /// Usage this month the allowance paid for.
1076+ pub used_micros: i64,
1077+ /// Usage this month not on the ledger yet (storage, git operations and
1078+ /// other month-end meters), which the allowance will pay for.
1079+ #[serde(default)]
1080+ pub pending_micros: i64,
1081+ /// What work running now holds against it.
1082+ #[serde(default)]
1083+ pub held_micros: i64,
1084+ /// What is left after all three; 0 at most stops new billable work.
1085+ pub remaining_micros: i64,
1086+ /// What g1t covered this month past the allowance (work already
1087+ /// running when it ran out): never charged to the workspace.
1088+ #[serde(default)]
1089+ pub overrun_micros: i64,
1090+ /// When the next month's allowance starts: RFC 3339.
1091+ pub resets_at: String,
1092+ /// The recurring monthly fee, in cents; 0: waived.
1093+ #[serde(default)]
1094+ pub fee_cents: u32,
1095+ /// Past the allowance, usage is charged to the workspace's own card at
1096+ /// ordinary prices, under its limits; false: new paid work stops.
1097+ #[serde(default)]
1098+ pub charge_past_allowance: bool,
1099+ /// A card is on file to charge past the allowance.
1100+ #[serde(default)]
1101+ pub has_card: bool,
10491102 }
10501103
10511104 /// One level reached: 50, 75, 90 or 100 percent of something.
13051358 pub by: String,
13061359 }
13071360
1361+// --- Complimentary enrollment (sudo) -----------------------------------------
1362+//
1363+// Staff enroll a workspace by hand; no workspace is ever enrolled on its
1364+// own. The enrollment is the plan's features, a monthly usage allowance at
1365+// ordinary prices, and a recurring monthly fee of its own, each set apart.
1366+// A real Stripe customer and subscription stand for it.
1367+
1368+/// The most a monthly complimentary allowance can be, against a slipped
1369+/// finger: $100,000. `unlimited` is how to give more.
1370+pub const MAX_COMPLIMENTARY_MICROS: i64 = 100_000 * MICROS_PER_DOLLAR;
1371+/// The most a complimentary enrollment's monthly fee can be: $100,000.
1372+pub const MAX_COMPLIMENTARY_FEE_CENTS: u32 = 10_000_000;
1373+
1374+/// `admin_set_complimentary`: enrolls a workspace, or changes the terms of
1375+/// its enrollment from now on. Changing the amount keeps what this month
1376+/// already used; lowering it below that leaves nothing more this month and
1377+/// never a debt. Recorded with who and why. Returns
1378+/// `Outcome<ComplimentaryStanding>`.
1379+#[derive(Debug, Serialize, Deserialize)]
1380+pub struct AdminSetComplimentaryArgs {
1381+ pub workspace: String,
1382+ /// The monthly usage allowance, valued at ordinary prices.
1383+ pub allowance_micros: i64,
1384+ /// No monthly cap on the allowance (g1t's own workspaces). g1t's
1385+ /// infrastructure limits still apply.
1386+ #[serde(default)]
1387+ pub unlimited: bool,
1388+ /// The recurring monthly fee, in cents, before tax; 0 waives it.
1389+ #[serde(default)]
1390+ pub fee_cents: u32,
1391+ /// Past the allowance, charge the workspace's own card at ordinary
1392+ /// prices under its limits (an employee's or a friend's workspace g1t
1393+ /// covers up to an amount); false: new paid work stops there.
1394+ #[serde(default)]
1395+ pub charge_past_allowance: bool,
1396+ pub note: String,
1397+ pub by: String,
1398+}
1399+
1400+/// `admin_end_complimentary`: ends an enrollment now. The workspace goes
1401+/// back to Free; usage the allowance paid for stays paid for. Its Stripe
1402+/// subscription is canceled. Returns `Outcome<ComplimentaryStanding>`.
1403+#[derive(Debug, Serialize, Deserialize)]
1404+pub struct AdminEndComplimentaryArgs {
1405+ pub workspace: String,
1406+ pub note: String,
1407+ pub by: String,
1408+}
1409+
1410+/// `admin_complimentary`: a workspace's enrollment, if it ever had one.
1411+/// Returns `Option<ComplimentaryStanding>`.
1412+#[derive(Debug, Serialize, Deserialize)]
1413+pub struct AdminComplimentaryArgs {
1414+ pub workspace: String,
1415+}
1416+
1417+/// A complimentary enrollment, as sudo shows it.
1418+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1419+#[serde(rename_all = "camelCase")]
1420+pub struct ComplimentaryStanding {
1421+ pub workspace: String,
1422+ pub grant_id: String,
1423+ /// `pending` (Stripe not confirmed yet), `active` or `ended`.
1424+ pub status: String,
1425+ pub allowance_micros: i64,
1426+ #[serde(default)]
1427+ pub unlimited: bool,
1428+ #[serde(default)]
1429+ pub fee_cents: u32,
1430+ #[serde(default)]
1431+ pub charge_past_allowance: bool,
1432+ /// Whether its Stripe objects are live-mode ones.
1433+ #[serde(default)]
1434+ pub live: bool,
1435+ #[serde(default)]
1436+ pub customer_id: Option<String>,
1437+ #[serde(default)]
1438+ pub subscription_id: Option<String>,
1439+ #[serde(default)]
1440+ pub stripe_status: Option<String>,
1441+ /// A Stripe step still owed (`create`, `terms`, `cancel`, `convert`),
1442+ /// tried again every fifteen minutes.
1443+ #[serde(default)]
1444+ pub pending_op: Option<String>,
1445+ #[serde(default)]
1446+ pub last_error: Option<String>,
1447+ pub note: String,
1448+ pub created_by: String,
1449+ pub created_at: String,
1450+ pub updated_by: String,
1451+ pub updated_at: String,
1452+ #[serde(default)]
1453+ pub ended_at: Option<String>,
1454+ /// `revoked` or `converted`.
1455+ #[serde(default)]
1456+ pub ended_reason: Option<String>,
1457+ /// This month, while it is active.
1458+ #[serde(default)]
1459+ pub month: Option<ComplimentaryMonth>,
1460+ /// Its terms over time, newest first.
1461+ #[serde(default)]
1462+ pub periods: Vec<ComplimentaryPeriod>,
1463+ /// Money the workspace actually paid g1t, all time: plan and fee
1464+ /// invoices, payments in advance and bank transfers, net of refunds.
1465+ #[serde(default)]
1466+ pub money_received_micros: i64,
1467+ /// Credit g1t gave it (promotional and goodwill), all time: never money.
1468+ #[serde(default)]
1469+ pub promotional_credit_micros: i64,
1470+ /// Usage complimentary allowances paid for, all time: never money.
1471+ #[serde(default)]
1472+ pub gifted_micros: i64,
1473+}
1474+
1475+/// One stretch of an enrollment's terms.
1476+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1477+#[serde(rename_all = "camelCase")]
1478+pub struct ComplimentaryPeriod {
1479+ pub id: String,
1480+ pub starts_at: String,
1481+ #[serde(default)]
1482+ pub ends_at: Option<String>,
1483+ pub allowance_micros: i64,
1484+ #[serde(default)]
1485+ pub unlimited: bool,
1486+ #[serde(default)]
1487+ pub fee_cents: u32,
1488+ #[serde(default)]
1489+ pub charge_past_allowance: bool,
1490+ pub note: String,
1491+ pub created_by: String,
1492+}
1493+
1494+/// `convert_to_paid`: an owner or billing manager of a complimentary
1495+/// workspace starts the g1t plan on the same Stripe customer and
1496+/// subscription. The plan, and usage past the included amount, start only
1497+/// once its first payment succeeds; until then the enrollment stands. Safe
1498+/// to call again: a conversion waiting on the bank is answered, never
1499+/// started twice. Returns `Outcome<Conversion>`.
1500+#[derive(Debug, Serialize, Deserialize)]
1501+#[serde(rename_all = "camelCase")]
1502+pub struct ConvertToPaidArgs {
1503+ pub actor: crate::User,
1504+ pub workspace: String,
1505+ pub return_url: String,
1506+}
1507+
1508+/// Where a conversion stands.
1509+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1510+#[serde(rename_all = "camelCase")]
1511+pub struct Conversion {
1512+ /// `converted`; `needs_card` (no saved card: `url` checks one);
1513+ /// `needs_action` (the bank wants the person: `url` is the invoice to
1514+ /// pay); `declined` (nothing changed; the enrollment stands).
1515+ pub status: String,
1516+ #[serde(default)]
1517+ pub url: Option<String>,
1518+ pub message: String,
1519+}
1520+
13081521 // --- Overages and goodwill (sudo) --------------------------------------------
13091522
13101523 /// What a one-time goodwill credit would come to: g1t's margin on the
+100−1
6969 ossMicros?: number;
7070 /** For usage: what g1t covered itself, such as a trial's last run past its credit. */
7171 givenMicros?: number;
72+ /** For usage: what a complimentary allowance from g1t paid of it. */
73+ giftMicros?: number;
7274 /** For usage: what the account's discount took off its price. */
7375 discountMicros?: number;
7476 /** For a credit from g1t, and for what of one expired or was revoked: its kind. */
206208 export const UNLIMITED_MICROS = 1_000_000_000_000_000;
207209
208210 /** What a workspace pays g1t on, as far as compute is concerned. Mirrors `PlanKind`. */
209−export type PlanKind = "free" | "paid" | "internal" | "enterprise";
211+export type PlanKind = "free" | "paid" | "internal" | "enterprise" | "complimentary";
210212
211213 // `ComputeKind` (what compute is for), `PaidBy` (who pays first: credit, trial, oss, on_demand) and
212214 // `Reservation` are in `./compute`, with the gate that calls `reserve` and `settle`.
292294 spike?: Spike | null;
293295 /** Where usage stands against what is included and the limits, from 50%. */
294296 alerts?: UsageAlert[];
297+ /** A complimentary enrollment's allowance this month. */
298+ complimentary?: ComplimentaryMonth | null;
295299 };
296300
301+/** A complimentary allowance this calendar month (UTC). Usage counts at its ordinary price. Mirrors `ComplimentaryMonth`. */
302+export type ComplimentaryMonth = {
303+ /** `YYYY-MM`. */
304+ month: string;
305+ allowanceMicros: number;
306+ unlimited?: boolean;
307+ /** Usage this month the allowance paid for. */
308+ usedMicros: number;
309+ /** Metered this month and not on the ledger yet (storage, git operations), which the allowance will pay for. */
310+ pendingMicros?: number;
311+ /** What running work holds against it. */
312+ heldMicros?: number;
313+ /** What is left after all three: at 0, new work that costs money stops. */
314+ remainingMicros: number;
315+ /** What g1t covered past the allowance this month: never charged. */
316+ overrunMicros?: number;
317+ /** RFC 3339: when next month's allowance starts. */
318+ resetsAt: string;
319+ /** The recurring monthly fee, in cents; 0: waived. */
320+ feeCents?: number;
321+ /** Past the allowance, usage is charged to the workspace's own card under its limits; otherwise new paid work stops. */
322+ chargePastAllowance?: boolean;
323+ /** A card is on file to charge past the allowance. */
324+ hasCard?: boolean;
325+};
326+
327+/** A complimentary enrollment, as sudo shows it. Mirrors `ComplimentaryStanding`. */
328+export type ComplimentaryStanding = {
329+ workspace: string;
330+ grantId: string;
331+ /** `pending` (Stripe not confirmed yet), `active` or `ended`. */
332+ status: "pending" | "active" | "ended";
333+ allowanceMicros: number;
334+ unlimited?: boolean;
335+ feeCents?: number;
336+ chargePastAllowance?: boolean;
337+ live?: boolean;
338+ customerId?: string | null;
339+ subscriptionId?: string | null;
340+ stripeStatus?: string | null;
341+ /** A Stripe step still owed, tried again every fifteen minutes. */
342+ pendingOp?: "create" | "terms" | "cancel" | "convert" | null;
343+ lastError?: string | null;
344+ note: string;
345+ createdBy: string;
346+ createdAt: string;
347+ updatedBy: string;
348+ updatedAt: string;
349+ endedAt?: string | null;
350+ endedReason?: "revoked" | "converted" | null;
351+ month?: ComplimentaryMonth | null;
352+ periods?: ComplimentaryPeriod[];
353+ /** Money actually paid to g1t, all time: plan and fee invoices, prepayments, bank transfers. */
354+ moneyReceivedMicros?: number;
355+ /** Promotional and goodwill credit from g1t, all time: never money. */
356+ promotionalCreditMicros?: number;
357+ /** Usage complimentary allowances paid for, all time: never money. */
358+ giftedMicros?: number;
359+};
360+
361+export type ComplimentaryPeriod = {
362+ id: string;
363+ startsAt: string;
364+ endsAt?: string | null;
365+ allowanceMicros: number;
366+ unlimited?: boolean;
367+ feeCents?: number;
368+ chargePastAllowance?: boolean;
369+ note: string;
370+ createdBy: string;
371+};
372+
373+/** Where an owner's conversion to the plan stands. */
374+export type Conversion = {
375+ status: "converted" | "needs_card" | "needs_action" | "declined";
376+ url?: string | null;
377+ message: string;
378+};
379+
297380 /** A hold on a start's estimated cost, as billing answers it (`Reservation` in `./compute`, and more). */
298381 export type ReservationHeld = Reservation & {
299382 /** What is held, at cost; may be less than the estimate for a free workspace's last bit of trial. */
606689 setTerms(id: string, terms: Terms, by: string): Promise<Result<PayingAccount>>;
607690 /** Team on or off without charge, and the account's share of the pools. Needs a note. */
608691 setAllowances(id: string, allowances: Allowances, note: string, by: string): Promise<Result<PayingAccount>>;
692+ /** A workspace's complimentary enrollment, if it ever had one. */
693+ complimentary(workspace: string): Promise<ComplimentaryStanding | null>;
694+ /**
695+ * Enrolls a workspace, or changes its terms from now on (the month's use is kept). Needs a note.
696+ * `chargePastAllowance`: past the allowance, its own card is charged (employees, friends); otherwise work stops.
697+ */
698+ setComplimentary(
699+ workspace: string,
700+ terms: { allowanceMicros: number; unlimited: boolean; feeCents: number; chargePastAllowance: boolean },
701+ note: string,
702+ by: string,
703+ ): Promise<Result<ComplimentaryStanding>>;
704+ /** Ends an enrollment now: back to Free, its Stripe subscription canceled. */
705+ endComplimentary(workspace: string, note: string, by: string): Promise<Result<ComplimentaryStanding>>;
609706 createEnterprise(name: string, workspaces: string[], by: string): Promise<Result<PayingAccount>>;
610707 attach(workspace: string, account: string | null, by: string): Promise<Result<PayingAccount>>;
611708 /** Credit for a workspace: promotional, goodwill or a refund; the owners are emailed. */
9671064 confirmSubscription(workspace: string, viewer: Viewer, session: string): Promise<Result<FeatureState>>;
9681065 /** Ends a plan at the end of its period, or (`resume`) takes that back. Owners only. */
9691066 cancelSubscription(actor: User, workspace: string, feature: Feature, resume?: boolean): Promise<Result<FeatureState>>;
1067+ /** A complimentary workspace starts the plan on its own subscription; nothing changes until the first payment succeeds. Owners and billing managers. */
1068+ convertToPaid(actor: User, workspace: string, returnUrl: string): Promise<Result<Conversion>>;
9701069 /** Which of the workspaces are free (on no paid plan); none where payments are not set up. */
9711070 freeWorkspaces(workspaces: string[]): Promise<string[]>;
9721071 /** Whether a feature works for a workspace now; a failure with the reason when not. */
+13−0
598598 call("confirm_subscription", { workspace, viewer, session }),
599599 cancelSubscription: (actor, workspace, feature, resume = false) =>
600600 call("cancel_subscription", { actor, workspace, feature, resume }),
601+ convertToPaid: (actor, workspace, returnUrl) => call("convert_to_paid", { actor, workspace, returnUrl }),
601602 hasFeature: (workspace, feature) => call("has_feature", { workspace, feature }),
602603 freeWorkspaces: (workspaces) => call("free_workspaces", { workspaces }),
603604 chargeFeature: (charge) => call("charge_feature", charge),
635636 account: (id) => call("admin_account", { id }),
636637 setTerms: (id, terms, by) => call("admin_set_terms", { id, terms, by }),
637638 setAllowances: (id, allowances, note, by) => call("admin_set_allowances", { id, allowances, note, by }),
639+ complimentary: (workspace) => call("admin_complimentary", { workspace }),
640+ setComplimentary: (workspace, terms, note, by) =>
641+ call("admin_set_complimentary", {
642+ workspace,
643+ allowance_micros: terms.allowanceMicros,
644+ unlimited: terms.unlimited,
645+ fee_cents: terms.feeCents,
646+ charge_past_allowance: terms.chargePastAllowance,
647+ note,
648+ by,
649+ }),
650+ endComplimentary: (workspace, note, by) => call("admin_end_complimentary", { workspace, note, by }),
638651 createEnterprise: (name, workspaces, by) => call("admin_create_enterprise", { name, workspaces, by }),
639652 attach: (workspace, account, by) => call("admin_attach", { workspace, account, by }),
640653 credit: (workspace, amountMicros, note, by, options = { kind: "goodwill" }) =>
+2−2
3434 import type { Result } from "./result";
3535
3636 /** What a workspace pays for. */
37−export type ComputePlan = "free" | "paid" | "internal" | "enterprise";
37+export type ComputePlan = "free" | "paid" | "internal" | "enterprise" | "complimentary";
3838
3939 /**
4040 * What kind of compute is being started. Billing also knows `agent` and
9494
9595 const num = (value: unknown): number => (typeof value === "number" && Number.isFinite(value) ? value : 0);
9696
97−const PLANS: ReadonlySet<string> = new Set(["free", "paid", "internal", "enterprise"]);
97+const PLANS: ReadonlySet<string> = new Set(["free", "paid", "internal", "enterprise", "complimentary"]);
9898
9999 /**
100100 * Billing's `entitlements`, as this module reads them. Null when they do
+43−0
1+// Read-only live configuration check. Never installs secrets or creates charges.
2+import assert from 'node:assert/strict';
3+import { readFileSync } from 'node:fs';
4+
5+const key = readFileSync('.secrets/stripe-live-key.txt', 'utf8').trim();
6+const signingSecret = readFileSync('.secrets/stripe-live-webhook.txt', 'utf8').trim();
7+assert.ok(/^(sk|rk)_live_/.test(key), 'Expected a live API key');
8+assert.ok(/^whsec_\S+$/.test(signingSecret), 'Expected a webhook signing secret');
9+const source = readFileSync('services/billing/src/webhooks.rs', 'utf8');
10+const url = source.match(/WEBHOOK_URL: &str = "([^"]+)"/)[1];
11+const required = [...source.match(/const EVENTS: &\[&str\] = &\[([\s\S]*?)\];/)[1].matchAll(/"([a-z_.]+)"/g)].map(m => m[1]);
12+const version = readFileSync('services/billing/src/stripe.rs', 'utf8').match(/STRIPE_VERSION: &str = "([^"]+)"/)[1];
13+async function get(path) {
14+ const response = await fetch(`https://api.stripe.com/v1${path}`, {
15+ headers: { Authorization: `Bearer ${key}`, 'Stripe-Version': version },
16+ });
17+ if (!response.ok) throw Error(`Stripe read failed: HTTP ${response.status}`);
18+ return response.json();
19+}
20+const [account, tax] = await Promise.all([get('/account'), get('/tax/settings')]);
21+const endpoints = [];
22+let after;
23+do {
24+ const page = await get(`/webhook_endpoints?limit=100${after ? `&starting_after=${encodeURIComponent(after)}` : ''}`);
25+ endpoints.push(...page.data.filter(e => e.url === url));
26+ after = page.has_more ? page.data.at(-1)?.id : undefined;
27+ assert.ok(!page.has_more || after, 'Malformed endpoint pagination');
28+} while (after);
29+const matching = endpoints.map(e => ({
30+ id: e.id, url: e.url, status: e.status, live: e.livemode, apiVersion: e.api_version,
31+ missingEvents: e.enabled_events.includes('*') ? [] : required.filter(t => !e.enabled_events.includes(t)),
32+}));
33+const ready = account.charges_enabled && tax.status === 'active'
34+ && matching.some(e => e.live && e.status === 'enabled' && e.apiVersion === '2026-08-26.dahlia' && !e.missingEvents.length);
35+console.log(JSON.stringify({
36+ checkedAt: new Date().toISOString(), account: account.id,
37+ chargesEnabled: account.charges_enabled, payoutsEnabled: account.payouts_enabled,
38+ taxStatus: tax.status, endpoints: matching, configurationReady: ready,
39+ signingSecretFilePresent: true,
40+ limitations: ['Stored signing secret cannot be matched to an existing endpoint through the read API',
41+ 'Does not verify deployed secrets, delivery, application readiness, or settlement'],
42+}, null, 2));
43+if (!ready) process.exitCode = 1;
+432−0
Loading the diff…
+98−0
Loading the diff…
+27−0
Loading the diff…
+23−0
Loading the diff…
+93−0
Loading the diff…
+41−0
Loading the diff…
+23−0
Loading the diff…
+50−0
Loading the diff…
+52−0
Loading the diff…
+10−2
Loading the diff…
+7−10
Loading the diff…
+4−1
Loading the diff…
+1497−0
A large diff (1,497 lines) is not shown until you ask for it.
+123−11
Loading the diff…
+19−166
Loading the diff…
+22−5
Loading the diff…
+59−0
Loading the diff…
+28−55
Loading the diff…
+67−0
Loading the diff…
+54−106
Loading the diff…
+52−40
Loading the diff…
+11−5
Loading the diff…
+42−3
Loading the diff…
+1−1
Loading the diff…
+4−0
Loading the diff…
+5−0
Loading the diff…
+5−0
Loading the diff…
+7−0
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…

Comments

Sign in to comment.