Skip to content
268 linesCodeBlameRaw
1/**
2 * Costs & margin: the arithmetic behind the page, apart from the SVG and
3 * the Workers runtime so it can be tested under Node. Money is in micros.
4 */
5import type { CostDay, CostMappingInput, CostSettings, SpendCaps } from "@g1t/contracts";
6
7import { parseDollars, usd } from "./money.ts";
8
9/** Buckets Cloudflare does not bill: their cost is g1t's own figure. */
10export const NOT_CLOUDFLARE = new Set(["models"]);
11
12/** A day's cost: Cloudflare's bill, or g1t's own figure where Cloudflare does not bill it. */
13export function dayCost(day: CostDay): number {
14 return NOT_CLOUDFLARE.has(day.bucket) ? day.ownCostMicros : day.cfCostMicros;
15}
16
17export type DayFigures = { day: string; revenueMicros: number; costMicros: number };
18
19/** Every day from `since` to `until`, inclusive (YYYY-MM-DD, UTC). */
20export function daysBetween(since: string, until: string): string[] {
21 const start = Date.parse(`${since}T00:00:00Z`);
22 const end = Date.parse(`${until}T00:00:00Z`);
23 if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return [];
24 const out: string[] = [];
25 for (let at = start; at <= end && out.length < 400; at += 86_400_000) out.push(new Date(at).toISOString().slice(0, 10));
26 return out;
27}
28
29/**
30 * A day per day of the range, every day there even with nothing on it.
31 * All of g1t: money in (what workspaces paid, the plan too) against every
32 * cost. One product: what customers were charged for it at price against
33 * what it cost.
34 */
35export function daySeries(days: CostDay[], since: string, until: string, bucket: string | null): DayFigures[] {
36 const totals = new Map<string, DayFigures>(daysBetween(since, until).map((day) => [day, { day, revenueMicros: 0, costMicros: 0 }]));
37 for (const row of days) {
38 if (bucket && row.bucket !== bucket) continue;
39 const figures = totals.get(row.day);
40 if (!figures) continue;
41 figures.revenueMicros += bucket ? row.valueMicros : row.cashMicros;
42 figures.costMicros += dayCost(row);
43 }
44 return [...totals.values()];
45}
46
47/**
48 * The margin on the price a markup gives, in percent: cost plus 20% is a
49 * 16.7% margin, since the 20% is of the cost and the margin of the price.
50 */
51export function marginOnPrice(markupPercent: number): number {
52 return markupPercent > -100 ? (markupPercent / (100 + markupPercent)) * 100 : 0;
53}
54
55/**
56 * Who g1t paid over the range: Cloudflare's usage bill, its subscriptions
57 * (a month's, over the range) and the model providers. The same total as
58 * the statement's All in.
59 */
60export function whoPaid(
61 overall: { costMicros: number; cloudflareCostMicros?: number; modelsCostMicros?: number },
62 subscriptionsMicros: number,
63): { totalMicros: number; cloudflareMicros: number; subscriptionsMicros: number; modelsMicros: number } {
64 const modelsMicros = overall.modelsCostMicros ?? 0;
65 const cloudflareMicros = overall.cloudflareCostMicros ?? overall.costMicros - modelsMicros;
66 return { totalMicros: overall.costMicros + subscriptionsMicros, cloudflareMicros, subscriptionsMicros, modelsMicros };
67}
68
69/** Cloudflare's subscriptions over a range of days: a month's, pro rata. */
70export function subscriptionsOver(monthlyMicros: number, days: number): number {
71 return Math.round((monthlyMicros * days) / 30);
72}
73
74/**
75 * A price version's cost and price as the table shows them. A rate g1t
76 * sets has no cost behind it; a weight is a multiplier, not money.
77 */
78export function versionCells(v: { costMicros: number; priceMicros: number; basis?: string }): { cost: string; price: string; note: string | null } {
79 if (v.basis === "weight") {
80 const weight = Number((v.costMicros / 1_000_000).toFixed(6));
81 return { cost: "—", price: `×${weight}`, note: "A weight on the agent rate's tokens, not money" };
82 }
83 if (v.basis === "rate") return { cost: "—", price: unitDollars(v.priceMicros), note: "g1t's own rate: no cost behind it" };
84 return { cost: unitDollars(v.costMicros), price: unitDollars(v.priceMicros), note: null };
85}
86
87/**
88 * What became of a proposal, in words, for its line: who decided it, and
89 * when one never took effect because a later measurement replaced it.
90 */
91export function proposalOutcome(p: { status: string; decidedBy: string | null; effectiveAt: string | null }): string | null {
92 const by = p.decidedBy ? `${p.status === "superseded" ? "applied" : p.status} by ${p.decidedBy}` : null;
93 const replaced = "replaced by a later measurement before it took effect; nothing was charged at it";
94 if (p.status === "superseded") return by ? `${by}, then ${replaced}` : "replaced by a later measurement";
95 if ((p.status === "applied" || p.status === "approved") && !p.effectiveAt) return by ? `${by}, then ${replaced}` : replaced;
96 return by;
97}
98
99/** Margin as a whole percent of revenue; none when there was none. */
100export function marginPercent(revenueMicros: number, costMicros: number): number | null {
101 return revenueMicros > 0 ? ((revenueMicros - costMicros) / revenueMicros) * 100 : null;
102}
103
104/** `12.3%`, `−4.0%`, or a dash. */
105export function percentLabel(percent: number | null | undefined, { signed = false }: { signed?: boolean } = {}): string {
106 if (percent == null || !Number.isFinite(percent)) return "—";
107 const text = `${Math.abs(percent).toFixed(1)}%`;
108 if (percent < 0) return `−${text}`;
109 return signed && percent > 0 ? `+${text}` : text;
110}
111
112/** How a margin reads against the floor: under it is danger, close to it a warning. */
113export function marginTone(percent: number | null | undefined, floor: number): "danger" | "warn" | "mint" | undefined {
114 if (percent == null) return undefined;
115 if (percent < floor) return "danger";
116 if (percent < floor + 5) return "warn";
117 return "mint";
118}
119
120/** A count: `1,234` or `1.2M`. */
121export function countLabel(value: number): string {
122 if (!Number.isFinite(value)) return "—";
123 if (Math.abs(value) >= 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
124 return Math.round(value).toLocaleString("en-US");
125}
126
127/** What a kind of drift is called. */
128export function driftLabel(kind: string): string {
129 return { count: "Count", cost: "Cost", leak: "Leak" }[kind] ?? kind;
130}
131
132/** A cost per unit in micros, as dollars with as many places as it needs: `$0.000016`, `$0.15`. */
133export function unitDollars(micros: number): string {
134 const dollars = micros / 1_000_000;
135 if (dollars === 0) return "$0";
136 if (Math.abs(dollars) >= 1) return `$${dollars.toFixed(2)}`;
137 const places = Math.min(10, Math.max(2, 2 - Math.floor(Math.log10(Math.abs(dollars)))));
138 return `$${dollars.toFixed(places)}`;
139}
140
141/** The range shown: 7 to 90 days, 30 when not said. */
142export function parseRange(raw: string | null): number {
143 const days = Number(raw);
144 return Number.isInteger(days) && days >= 7 && days <= 90 ? days : 30;
145}
146
147/** The product to chart, if it is one the page has. */
148export function parseBucket(raw: string | null, known: string[]): string | null {
149 return raw && known.includes(raw) ? raw : null;
150}
151
152export type Parsed<T> = { ok: true; value: T } | { ok: false; error: string };
153
154function numberField(form: FormData, name: string): number {
155 const raw = String(form.get(name) ?? "").trim();
156 return raw === "" ? Number.NaN : Number(raw);
157}
158
159/** The guardrails from the settings form. */
160export function parseCostSettings(form: FormData): Parsed<CostSettings> {
161 const autoApplyPercent = numberField(form, "autoApplyPercent");
162 const noticeDays = numberField(form, "noticeDays");
163 const marginFloorPercent = numberField(form, "marginFloorPercent");
164 const alertDays = numberField(form, "alertDays");
165 const anomalyFactor = numberField(form, "anomalyFactor");
166 const minDaily = parseDollars(String(form.get("minDailyCost") ?? ""));
167 const anomalyFloor = parseDollars(String(form.get("anomalyFloor") ?? ""));
168 if (!(autoApplyPercent >= 0 && autoApplyPercent <= 100)) return { ok: false, error: "The guardrail is a percentage from 0 to 100." };
169 if (!(Number.isInteger(noticeDays) && noticeDays >= 0 && noticeDays <= 90)) return { ok: false, error: "Notice is 0 to 90 days." };
170 if (!(marginFloorPercent >= -100 && marginFloorPercent <= 100)) return { ok: false, error: "The margin floor is a percentage." };
171 if (!(Number.isInteger(alertDays) && alertDays >= 1 && alertDays <= 30)) return { ok: false, error: "Alert after 1 to 30 days." };
172 if (!(anomalyFactor > 0 && anomalyFactor <= 100)) return { ok: false, error: "The factor is a positive number." };
173 if (minDaily == null || anomalyFloor == null) return { ok: false, error: "Amounts are dollars to the cent." };
174 return {
175 ok: true,
176 value: {
177 autoApply: form.get("autoApply") === "on",
178 autoApplyPercent,
179 noticeDays,
180 marginFloorPercent,
181 alertDays,
182 minDailyCostMicros: minDaily,
183 anomalyFactor,
184 anomalyFloorMicros: anomalyFloor,
185 cardFee: form.get("cardFee") === "on",
186 },
187 };
188}
189
190const NAME = /^(\*|[a-z0-9][a-z0-9_]{0,79})$/;
191
192/** A mapping from the mapping form: Cloudflare's product and meter (or `*`) to one of g1t's products. */
193export function parseMapping(form: FormData): Parsed<CostMappingInput> {
194 const value = (name: string) => String(form.get(name) ?? "").trim();
195 const product = value("product").toLowerCase();
196 const meter = value("meter").toLowerCase() || "*";
197 const remove = form.get("remove") === "1";
198 if (!NAME.test(product) || product === "*") return { ok: false, error: "Cloudflare's product, as the lines table names it." };
199 if (!NAME.test(meter)) return { ok: false, error: "A meter prefix as the lines table names it, or * for all of the product." };
200 if (remove) return { ok: true, value: { product, meter, remove: true } };
201 const bucket = value("bucket").toLowerCase();
202 if (!NAME.test(bucket) || bucket === "*") return { ok: false, error: "Which of g1t's products it is a cost of." };
203 const drift = value("driftPercent");
204 const driftPercent = drift === "" ? null : Number(drift);
205 if (driftPercent != null && !(driftPercent > 0 && driftPercent <= 1000)) return { ok: false, error: "Drift is a percentage above zero." };
206 return {
207 ok: true,
208 value: {
209 product,
210 meter,
211 bucket,
212 priceMeter: value("priceMeter") || null,
213 ownMeter: value("ownMeter") || null,
214 scaleToOwn: form.get("scaleToOwn") === "on",
215 driftPercent,
216 note: value("note").slice(0, 200),
217 },
218 };
219}
220
221// --- g1t's own spend (billing's budget) ---------------------------------------
222
223/**
224 * The red bar on every sudo page: the daily breaker open, or a 100%-discount
225 * account's monthly budget used up. Null when neither.
226 */
227export function spendBanner(caps: SpendCaps): string | null {
228 const parts: string[] = [];
229 if (caps.tripped) {
230 parts.push(
231 `the daily breaker is open (${usd(caps.todayMicros)} of ${usd(caps.dailyCapMicros)} today), so new hosted-model agent runs g1t pays for wait until 00:00 UTC`,
232 );
233 }
234 for (const budget of caps.comped) {
235 if (budget.ceilingMicros > 0 && budget.usedMicros >= budget.ceilingMicros) {
236 parts.push(`${budget.name} used its ${usd(budget.ceilingMicros)} monthly budget, so new work on it is refused`);
237 }
238 }
239 if (parts.length === 0) return null;
240 const text = parts.join("; and ");
241 return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
242}
243
244/** What g1t paid this month, by bucket, with the free tier and Cloudflare's subscriptions; and the total. */
245export function spendRows(caps: SpendCaps): { rows: { key: string; title: string; micros: number; note: string }[]; totalMicros: number } {
246 const notes: Record<string, string> = {
247 comped: "Work on accounts with a 100% discount: what it cost g1t, not its price",
248 trial: "Trial credit, at what it cost g1t",
249 oss: "Checks and workflows on public repositories, at what they cost g1t",
250 given: "Free workspaces' overruns past their trial, at what they cost g1t",
251 unpaid: "Usage charged while payments are in Stripe's test mode: what it cost g1t, not what was charged",
252 };
253 const rows = caps.monthBuckets.map((b) => ({ key: b.bucket, title: b.title, micros: b.micros, note: notes[b.bucket] ?? "" }));
254 rows.push({ key: "free", title: "Free tier", micros: caps.freeTierMicros, note: "Free workspaces' share of git, storage and platform, as last reconciled" });
255 rows.push({
256 key: "fixed",
257 title: "Cloudflare subscriptions",
258 micros: caps.fixedMonthlyMicros,
259 note: caps.fixedSource === "cloudflare" ? "The whole month, as Cloudflare lists them" : "The whole month, estimated (CLOUDFLARE_FIXED_MONTHLY_MICROS)",
260 });
261 return { rows, totalMicros: rows.reduce((sum, row) => sum + row.micros, 0) };
262}
263
264/** How far a cap is used, 0 to 100, for a meter. */
265export function capPercent(usedMicros: number, capMicros: number): number {
266 if (capMicros <= 0) return 0;
267 return Math.max(0, Math.min(100, (usedMicros / capMicros) * 100));
268}