g1t/apps/sudo/app/lib/pricing.ts
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1 | /** |
| 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 | */ | |
| 8 | import type { GivenFigures, MonthFigures } from "@g1t/contracts"; | |
| 9 | ||
| 10 | // --- Requests ------------------------------------------------------------------- | |
| 11 | ||
| 12 | export const REQUEST_STATUSES = ["open", "approved", "declined", "all"] as const; | |
| 13 | export type RequestStatus = (typeof REQUEST_STATUSES)[number]; | |
| 14 | ||
| 15 | /** The status filter from the address: open unless another is named. */ | |
| 16 | export function parseRequestStatus(value: string | null): RequestStatus { | |
| 17 | return REQUEST_STATUSES.find((status) => status === value) ?? "open"; | |
| 18 | } | |
| 19 | ||
| 20 | export function requestsHref(status: RequestStatus): string { | |
| 21 | return status === "open" ? "/requests" : `/requests?status=${status}`; | |
| 22 | } | |
| 23 | ||
| 24 | const HOUR = 60 * 60 * 1000; | |
| 25 | const DAY = 24 * HOUR; | |
| 26 | ||
| 27 | function 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 | */ | |
| 37 | export 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`. */ | |
| 51 | export 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. */ | |
| 62 | export 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`. */ | |
| 70 | export 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 | */ | |
| 89 | export 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 | */ | |
| 98 | export 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. */ | |
| 104 | export 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 | */ | |
| 111 | export 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 | */ | |
| 130 | export 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. */ | |
| 137 | export 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. */ | |
| 144 | export const FAST_RATIO = 5; | |
| 145 | ||
| 146 | export 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. */ | |
| 151 | export 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 | ||
| 156 | const 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 | ||
| 162 | export 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. */ | |
| 169 | export 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 | */ | |
| 177 | export 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 | ||
| 183 | const 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. */ | |
| 192 | export 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. */ | |
| 197 | export 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 | */ | |
| 205 | export 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 | */ | |
| 219 | export 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 | } |