flagon-io/g1t

public

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/apps/sudo/app/lib/pricing.ts

233 lines10,086 bytesCodeBlame

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

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1/**
2 * The arithmetic and wording behind Requests, Overages, Velocity and the
3 * Overview's money: how long a request has waited against the promise of
4 * an answer within one business day, what a goodwill credit costs g1t,
5 * and margin kept apart from what g1t gives. Money is in micros. No
6 * Workers or React imports, so it can be tested under Node.
7 */
8import type { GivenFigures, MonthFigures } from "@g1t/contracts";
9
10// --- Requests -------------------------------------------------------------------
11
12export const REQUEST_STATUSES = ["open", "approved", "declined", "all"] as const;
13export type RequestStatus = (typeof REQUEST_STATUSES)[number];
14
15/** The status filter from the address: open unless another is named. */
16export function parseRequestStatus(value: string | null): RequestStatus {
17 return REQUEST_STATUSES.find((status) => status === value) ?? "open";
18}
19
20export function requestsHref(status: RequestStatus): string {
21 return status === "open" ? "/requests" : `/requests?status=${status}`;
22}
23
24const HOUR = 60 * 60 * 1000;
25const DAY = 24 * HOUR;
26
27function isWeekend(date: Date): boolean {
28 const day = date.getUTCDay();
29 return day === 0 || day === 6;
30}
31
32/**
33 * When an answer is due: one business day after the request, UTC. A
34 * request made on a Friday is due Monday at the same time; one made at
35 * the weekend, by the end of Monday.
36 */
37export function answerDueAt(createdAt: string): Date {
38 let start = new Date(createdAt);
39 if (Number.isNaN(start.getTime())) return start;
40 if (isWeekend(start)) {
41 // Asked at the weekend: the business day is Monday, all of it.
42 while (isWeekend(start)) start = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate() + 1));
43 return new Date(start.getTime() + DAY - 1000);
44 }
45 let due = new Date(start.getTime() + DAY);
46 while (isWeekend(due)) due = new Date(due.getTime() + DAY);
47 return due;
48}
49
50/** A span of time, short: `25 min`, `3 h`, `2 d 4 h`. */
51export function span(ms: number): string {
52 const minutes = Math.max(0, Math.floor(ms / 60_000));
53 if (minutes < 60) return `${minutes} min`;
54 const hours = Math.floor(minutes / 60);
55 if (hours < 24) return `${hours} h`;
56 const days = Math.floor(hours / 24);
57 const rest = hours % 24;
58 return rest > 0 ? `${days} d ${rest} h` : `${days} d`;
59}
60
61/** How long an open request has waited, and whether its one business day is up. */
62export function waiting(createdAt: string, now = new Date()): { label: string; overdue: boolean; dueAt: Date } {
63 const asked = new Date(createdAt);
64 const dueAt = answerDueAt(createdAt);
65 if (Number.isNaN(asked.getTime())) return { label: "—", overdue: false, dueAt };
66 return { label: span(now.getTime() - asked.getTime()), overdue: now.getTime() > dueAt.getTime(), dueAt };
67}
68
69/** How old a workspace is, in the largest whole unit: `12 days`, `3 months`, `2 years`. */
70export function age(createdAt: string | null | undefined, now = new Date()): string | null {
71 if (!createdAt) return null;
72 const then = new Date(createdAt);
73 if (Number.isNaN(then.getTime())) return null;
74 const days = Math.max(0, Math.floor((now.getTime() - then.getTime()) / DAY));
75 const plural = (count: number, unit: string) => `${count} ${unit}${count === 1 ? "" : "s"}`;
76 if (days < 1) return "less than a day";
77 if (days < 60) return plural(days, "day");
78 if (days < 730) return plural(Math.floor(days / 30), "month");
79 return plural(Math.floor(days / 365), "year");
80}
81
82// --- Goodwill -------------------------------------------------------------------
83
84/**
85 * What g1t absorbs of real cost on a goodwill credit: whatever is past its
86 * margin on the overage. The margin is money g1t never had; the rest it
87 * paid Cloudflare and model providers for.
88 */
89export function absorbedMicros(amountMicros: number, marginMicros: number): number {
90 return Math.max(0, amountMicros - Math.max(0, marginMicros));
91}
92
93/**
94 * Whether a goodwill credit needs a typed reason: more than the one-click
95 * credit, or a second within 12 months. No amount is the one-click credit.
96 * As billing decides it, so the form asks before billing refuses.
97 */
98export function goodwillNeedsReason(amountMicros: number | null, creditMicros: number, lastGoodwillAt: string | null): boolean {
99 const amount = amountMicros ?? creditMicros;
100 return amount > creditMicros || lastGoodwillAt != null;
101}
102
103/** The most of real cost a one-click goodwill credit absorbs, unless billing's price book says otherwise. */
104export const FORGIVE_COST_MICROS = 50_000_000;
105
106/**
107 * What a goodwill credit of `amountMicros` (null: the one-click credit)
108 * means: whether billing asks for a reason, what of it is real cost g1t
109 * absorbs, and whether that is past the cap, where the red warning shows.
110 */
111export function goodwillWarning(
112 amountMicros: number | null,
113 goodwill: { creditMicros: number; marginMicros: number },
114 lastGoodwillAt: string | null,
115 capMicros: number = FORGIVE_COST_MICROS,
116): { needsReason: boolean; absorbedMicros: number; overCap: boolean } {
117 const amount = amountMicros ?? goodwill.creditMicros;
118 const absorbed = absorbedMicros(amount, goodwill.marginMicros);
119 return {
120 needsReason: goodwillNeedsReason(amountMicros, goodwill.creditMicros, lastGoodwillAt),
121 absorbedMicros: absorbed,
122 overCap: absorbed > capMicros,
123 };
124}
125
126/**
127 * The amounts the Overages page offers beside the one-click credit: the
128 * whole overage (margin and all its cost), when that is more.
129 */
130export function goodwillChoices(goodwill: { overageMicros: number; creditMicros: number }): { label: string; micros: number | null }[] {
131 const choices: { label: string; micros: number | null }[] = [{ label: "One-click credit", micros: null }];
132 if (goodwill.overageMicros > goodwill.creditMicros) choices.push({ label: "The whole overage", micros: goodwill.overageMicros });
133 return choices;
134}
135
136/** The statement line a goodwill credit makes, as billing writes it. */
137export function goodwillLine(day: string): string {
138 return `Credit from g1t: accidental usage on ${day}`;
139}
140
141// --- Velocity -------------------------------------------------------------------
142
143/** A last hour this many times the usual hour is fast; at $5 and over, it pauses compute. */
144export const FAST_RATIO = 5;
145
146export function isFast(ratio: number): boolean {
147 return Number.isFinite(ratio) && ratio >= FAST_RATIO;
148}
149
150/** A ratio as staff read it: `7.2×`, `—` when there is no usual hour to compare. */
151export function ratioLabel(ratio: number, averageHourMicros: number): string {
152 if (averageHourMicros <= 0 || !Number.isFinite(ratio)) return "new";
153 return `${ratio >= 10 ? Math.round(ratio) : Math.round(ratio * 10) / 10}×`;
154}
155
156const SPIKE: Record<string, { label: string; tone: "danger" | "warn" | "plain" }> = {
157 open: { label: "Paused: waiting on the owner", tone: "danger" },
158 stopped: { label: "Stopped by the owner", tone: "danger" },
159 continued: { label: "Owner kept going", tone: "warn" },
160};
161
162export function spikeLabel(status: string): { label: string; tone: "danger" | "warn" | "plain" } {
163 return SPIKE[status] ?? { label: status, tone: "plain" };
164}
165
166// --- Money: margin apart from what g1t gives ------------------------------------
167
168/** What came in: usage charged plus the plan's price paid. */
169export function revenueOf(month: Pick<MonthFigures, "chargedMicros" | "plansMicros">): number {
170 return month.chargedMicros + (month.plansMicros ?? 0);
171}
172
173/**
174 * Margin: what came in less what usage cost g1t. What g1t gave is not a
175 * loss here; it is shown on its own.
176 */
177export function marginOfMonth(month: Pick<MonthFigures, "chargedMicros" | "plansMicros" | "costMicros">): { micros: number; percent: number | null } {
178 const revenue = revenueOf(month);
179 const micros = revenue - month.costMicros;
180 return { micros, percent: revenue > 0 ? Math.round((micros / revenue) * 100) : null };
181}
182
183const GIVEN_LABELS: Record<string, string> = {
184 internal: "Internal use",
185 trial: "Trials",
186 oss_pool: "Open-source pool",
187 covered: "Covered by g1t",
188 goodwill: "Goodwill credits",
189};
190
191/** What a source of given usage is called; billing's own label first. */
192export function givenLabel(row: Pick<GivenFigures, "source" | "label">): string {
193 return row.label || GIVEN_LABELS[row.source] || row.source;
194}
195
196/** Everything given, at price and at cost. */
197export function givenTotal(rows: Pick<GivenFigures, "micros" | "costMicros">[]): { micros: number; costMicros: number } {
198 return rows.reduce((sum, row) => ({ micros: sum.micros + row.micros, costMicros: sum.costMicros + row.costMicros }), { micros: 0, costMicros: 0 });
199}
200
201/**
202 * The parts of a ledger line g1t gave rather than charged: a trial, the
203 * open-source pool, or covered (internal use, the trial's last run).
204 */
205export function givenParts(entry: { trialMicros?: number; ossMicros?: number; givenMicros?: number }): { label: string; micros: number }[] {
206 return [
207 { label: "Trial", micros: entry.trialMicros ?? 0 },
208 { label: "Open-source pool", micros: entry.ossMicros ?? 0 },
209 { label: "Covered by g1t", micros: entry.givenMicros ?? 0 },
210 ].filter((part) => part.micros > 0);
211}
212
213/**
214 * A month's money with what g1t gave kept apart: revenue (usage charged
215 * and plans paid), margin on what was sold (revenue less the cost of the
216 * usage that was charged), and net (revenue less all cost, given included).
217 * `givenCostMicros` is what the given usage cost g1t.
218 */
219export function moneyApart(
220 month: Pick<MonthFigures, "chargedMicros" | "plansMicros" | "costMicros">,
221 givenCostMicros: number,
222): { revenueMicros: number; soldCostMicros: number; marginMicros: number; marginPercent: number | null; netMicros: number } {
223 const revenueMicros = revenueOf(month);
224 const soldCostMicros = Math.max(0, month.costMicros - Math.max(0, givenCostMicros));
225 const marginMicros = revenueMicros - soldCostMicros;
226 return {
227 revenueMicros,
228 soldCostMicros,
229 marginMicros,
230 marginPercent: revenueMicros > 0 ? Math.round((marginMicros / revenueMicros) * 100) : null,
231 netMicros: revenueMicros - month.costMicros,
232 };
233}