Commit

Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running

syntaqxcommitted Parent1d51c60Browse files
6 files+390−1020/6 viewed
+152−40
11 import type { ReactNode } from "react";
2−import { Link, data, redirect } from "react-router";
2+import { Form, Link, data, redirect, useNavigation } from "react-router";
33
44 import type { CostsReport, PriceProposal } from "@g1t/contracts";
55
143143 // Margin under the floor at the top; drift and leaks are in their own table, workspaces on Reach out.
144144 const banner = report.alerts.filter((a) => a.kind === "overall" || a.kind === "margin");
145145 const elsewhere = report.alerts.length - banner.length;
146− // What g1t gave away on purpose (comped workspaces, free periods, the
147− // trial, the pools) is a budget, watched under g1t's own spend; the
148− // margin is measured on what was sold. Older reports lack the fields.
149− const given = report.overall.givenMicros ?? 0;
150− const soldMicros = report.overall.soldMarginMicros ?? report.overall.marginMicros;
151− const soldPercent = report.overall.soldMarginPercent !== undefined ? report.overall.soldMarginPercent : report.overall.marginPercent;
152146 return (
153147 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
154148 <PageHeader
155149 title="Costs & margin"
156− description="What Cloudflare charged g1t, day by day, against what g1t charged for the same things. Prices are cost plus 20%; this is where that is checked against the bill, where drift and leaks show, and where price changes wait for a decision."
157− actions={
158− <form method="post">
159− <input type="hidden" name="intent" value="run" />
160− <Button variant="quiet" type="submit">
161− Run the analysis now
162− </Button>
163− </form>
164− }
150+ description="What g1t cost to run (Cloudflare's bill and the model providers') against what workspaces paid. Prices are cost plus 20%; this is where that is checked against the bill, where drift and leaks show, and where price changes wait for a decision."
151+ actions={<RunButton />}
165152 />
166153 <nav aria-label="Range" className="mt-5 flex flex-wrap gap-2">
167154 {RANGES.map((days) => (
199186 )}
200187 </div>
201188
202− <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
203− <Stat
204− label="Money in"
205− value={usd(report.overall.usageMicros + report.overall.plansMicros)}
206− hint={`${usd(report.overall.usageMicros)} usage, ${usd(report.overall.plansMicros)} plans`}
207− />
208− <Stat
209− label="Cost"
210− value={usd(report.overall.costMicros)}
211− hint={given > 0 ? `${usd(given)} of it given away` : `${report.since} to ${report.until}`}
212− />
213− <Stat
214− label="Margin on what was sold"
215− value={percentLabel(soldPercent)}
216− hint={given > 0 ? `${usd(soldMicros)}; ${percentLabel(report.overall.marginPercent)} with what was given` : usd(soldMicros)}
217− tone={marginTone(soldPercent, floor)}
218− />
219− <Stat
220− label="Proposals waiting"
221− value={String(open.length)}
222− hint={report.fetchedAt ? <>Bill read <When at={report.fetchedAt} time /></> : "The bill has not been read yet"}
223− tone={open.length > 0 ? "warn" : undefined}
224− />
225− </div>
189+ <Statement report={report} floor={floor} range={range} proposals={open.length} />
226190
227191 <SpendSection caps={report.caps} error={failed?.section === "lift" ? failed.error : null} />
228192
649613 }
650614
651615 /** g1t's own spend: the daily breaker, comped budgets, and this month by what paid. */
616+/** The button that runs the nightly analysis now, saying so while it runs. */
617+function RunButton() {
618+ const navigation = useNavigation();
619+ const running = navigation.state !== "idle" && navigation.formData?.get("intent") === "run";
620+ return (
621+ <Form method="post" className="flex items-center gap-3">
622+ <input type="hidden" name="intent" value="run" />
623+ <Button variant="quiet" type="submit" disabled={running}>
624+ {running ? "Running the analysis…" : "Run the analysis now"}
625+ </Button>
626+ {running && <span className="text-xs text-muted">Reading the bill and reconciling 31 days; about a minute.</span>}
627+ </Form>
628+ );
629+}
630+
631+/**
632+ * Where the money went, as a short statement: usage sold and running g1t
633+ * (each against what paid for it), what g1t gave away on purpose (a budget,
634+ * never a loss), and who was paid.
635+ */
636+function Statement({ report, floor, range, proposals }: { report: CostsReport; floor: number; range: number; proposals: number }) {
637+ const o = report.overall;
638+ // Reports from before the statement have only the totals.
639+ const given = o.givenMicros ?? 0;
640+ const usageCost = o.usageCostMicros ?? o.costMicros - given;
641+ const usageMargin = o.usageMarginMicros ?? o.usageMicros - usageCost;
642+ const usagePercent = o.usageMarginPercent !== undefined ? o.usageMarginPercent : null;
643+ const running = o.runningCostMicros ?? 0;
644+ const unmapped = o.unmappedCostMicros ?? 0;
645+ // Cloudflare's subscriptions are not on the usage bill: the estimate, over the range.
646+ const subscriptions = Math.round((report.caps.fixedMonthlyMicros * range) / 30);
647+ const moneyIn = o.usageMicros + o.plansMicros;
648+ const spent = o.costMicros + subscriptions;
649+ const net = moneyIn - spent;
650+ const givenParts = [
651+ ["comped", o.givenCompedMicros ?? 0],
652+ ["free use", o.givenFreeMicros ?? 0],
653+ ["trial", o.givenTrialMicros ?? 0],
654+ ["open-source pool", o.givenPoolMicros ?? 0],
655+ ].filter(([, micros]) => (micros as number) > 0) as [string, number][];
656+ const rows: { title: string; note: string; in: number | null; cost: number; result: number | null; tone?: "danger" | "warn" | "muted" }[] = [
657+ {
658+ title: "Usage sold",
659+ note: "What workspaces paid for usage, against what that usage cost",
660+ in: o.usageMicros,
661+ cost: usageCost,
662+ result: usageMargin,
663+ tone: usageMargin < 0 ? "danger" : undefined,
664+ },
665+ {
666+ title: "Running g1t",
667+ note: "Plans, against Workers, D1, KV, Queues and the rest of the platform",
668+ in: o.plansMicros,
669+ cost: running,
670+ result: o.plansMicros - running,
671+ tone: o.plansMicros - running < 0 ? "warn" : undefined,
672+ },
673+ {
674+ title: "Cloudflare subscriptions",
675+ note: "Fixed, an estimate (CLOUDFLARE_FIXED_MONTHLY_MICROS), not on the usage bill",
676+ in: null,
677+ cost: subscriptions,
678+ result: -subscriptions,
679+ tone: "muted",
680+ },
681+ ...(unmapped > 0
682+ ? [{ title: "Not mapped", note: "Billed by Cloudflare, charged for by nothing yet", in: null, cost: unmapped, result: -unmapped, tone: "danger" as const }]
683+ : []),
684+ {
685+ title: "Given away",
686+ note: givenParts.length > 0 ? givenParts.map(([why, micros]) => `${why} ${usd(micros)}`).join(" · ") : "Nothing this range",
687+ in: null,
688+ cost: given,
689+ result: null,
690+ tone: "muted",
691+ },
692+ ];
693+ const resultClass = (tone?: string) => (tone === "danger" ? "text-danger" : tone === "warn" ? "text-warn" : tone === "muted" ? "text-muted" : "text-fg");
694+ return (
695+ <>
696+ <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
697+ <Stat
698+ label="Margin on usage sold"
699+ value={percentLabel(usagePercent)}
700+ hint={`${usd(o.usageMicros)} paid for usage that cost ${usd(usageCost)}`}
701+ tone={marginTone(usagePercent, floor)}
702+ />
703+ <Stat
704+ label="Given away on purpose"
705+ value={usd(given)}
706+ hint={givenParts.length > 0 ? givenParts.map(([why, micros]) => `${usd(micros)} ${why}`).join(", ") : "Nothing given this range"}
707+ />
708+ <Stat
709+ label="Who g1t paid"
710+ value={usd(o.costMicros)}
711+ hint={`Cloudflare ${usd(o.cloudflareCostMicros ?? o.costMicros - (o.modelsCostMicros ?? 0))}, model providers ${usd(o.modelsCostMicros ?? 0)}`}
712+ />
713+ <Stat
714+ label="Proposals waiting"
715+ value={String(proposals)}
716+ hint={report.fetchedAt ? <>Bill read <When at={report.fetchedAt} time /></> : "The bill has not been read yet"}
717+ tone={proposals > 0 ? "warn" : undefined}
718+ />
719+ </div>
720+ <div className="mt-3 overflow-x-auto rounded-lg border border-line bg-surface">
721+ <table className="w-full min-w-[36rem] text-sm">
722+ <thead>
723+ <tr className="border-b border-line text-left text-xs text-muted">
724+ <th className="px-4 py-2 font-medium sm:px-5">
725+ {report.since} to {report.until}
726+ </th>
727+ <th className="px-4 py-2 text-right font-medium">Paid in</th>
728+ <th className="px-4 py-2 text-right font-medium">Cost</th>
729+ <th className="px-4 py-2 text-right font-medium sm:pr-5">Result</th>
730+ </tr>
731+ </thead>
732+ <tbody>
733+ {rows.map((row) => (
734+ <tr key={row.title} className="border-b border-line">
735+ <td className="px-4 py-2.5 sm:px-5">
736+ <span className="text-fg">{row.title}</span>
737+ <span className="block text-xs text-faint">{row.note}</span>
738+ </td>
739+ <td className="tabular px-4 py-2.5 text-right">{row.in == null ? <span className="text-faint">—</span> : usd(row.in)}</td>
740+ <td className="tabular px-4 py-2.5 text-right">{usd(row.cost)}</td>
741+ <td className={`tabular px-4 py-2.5 text-right sm:pr-5 ${resultClass(row.tone)}`}>
742+ {row.result == null ? "a budget" : usd(row.result, { signed: true })}
743+ </td>
744+ </tr>
745+ ))}
746+ <tr>
747+ <td className="px-4 py-2.5 sm:px-5">
748+ <span className="font-medium text-fg">All in</span>
749+ <span className="block text-xs text-faint">
750+ {given > 0 ? `${usd(given)} of the cost was given away on purpose; without it, ${usd(net + given, { signed: true })}` : "Everything above"}
751+ </span>
752+ </td>
753+ <td className="tabular px-4 py-2.5 text-right font-medium">{usd(moneyIn)}</td>
754+ <td className="tabular px-4 py-2.5 text-right font-medium">{usd(spent)}</td>
755+ <td className="tabular px-4 py-2.5 text-right font-medium text-fg sm:pr-5">{usd(net, { signed: true })}</td>
756+ </tr>
757+ </tbody>
758+ </table>
759+ </div>
760+ </>
761+ );
762+}
763+
652764 function SpendSection({ caps, error }: { caps: CostsReport["caps"]; error: string | null }) {
653765 const { rows, totalMicros } = spendRows(caps);
654766 const net = caps.revenueMicros - totalMicros;
+30−0
24062406 pub sold_margin_micros: i64,
24072407 #[serde(default)]
24082408 pub sold_margin_percent: Option<f64>,
2409+ /// What was sold, apart: usage (`usage_micros` against what that usage
2410+ /// cost, less what was given), running g1t (`plans_micros` against the
2411+ /// platform's cost, less its given share) and what no mapping names.
2412+ #[serde(default)]
2413+ pub usage_cost_micros: i64,
2414+ #[serde(default)]
2415+ pub usage_margin_micros: i64,
2416+ #[serde(default)]
2417+ pub usage_margin_percent: Option<f64>,
2418+ #[serde(default)]
2419+ pub running_cost_micros: i64,
2420+ #[serde(default)]
2421+ pub unmapped_cost_micros: i64,
2422+ /// `given_micros` by why: comped workspaces, free use (free periods,
2423+ /// free allowances, overruns g1t covered), the trial, the open-source pool.
2424+ #[serde(default)]
2425+ pub given_comped_micros: i64,
2426+ #[serde(default)]
2427+ pub given_free_micros: i64,
2428+ #[serde(default)]
2429+ pub given_trial_micros: i64,
2430+ #[serde(default)]
2431+ pub given_pool_micros: i64,
2432+ /// `cost_micros` by who g1t pays: Cloudflare's bill (billed amounts,
2433+ /// after the included allowances), and model providers (the ledger's
2434+ /// cost of the tokens, which Cloudflare's bill does not show).
2435+ #[serde(default)]
2436+ pub cloudflare_cost_micros: i64,
2437+ #[serde(default)]
2438+ pub models_cost_micros: i64,
24092439 }
24102440
24112441 /// A count, cost or leak that does not add up.
+37−15
101101
102102 For each day and bucket:
103103
104−- **Cloudflare cost** = Σ the bucket's lines' cost. `models` uses the
105− ledger's cost instead.
104+- **Cloudflare cost** = Σ the bucket's lines' cost, as billed: after the
105+ included allowances, so a month inside them costs $0 here as on
106+ Cloudflare's Billable usage page. `models` uses the ledger's cost of the
107+ tokens instead; that is paid to the model providers and is not on
108+ Cloudflare's bill.
106109 - **Own cost** = Σ the ledger's `cost_micros` for the bucket's keys (the
107110 price book's cost when charged), plus month-end deltas. A workspace's own
108111 model provider is no cost to g1t.
111114 g1t's own (comped) workspaces are valued at cost plus the margin.
112115 - **Cash** = what workspaces paid: `-amount_micros`, and the plan's price.
113116 - **Given away** = the part of the cost that went on usage g1t paid for
114− itself on purpose: all of a comped workspace's, all of a free period's,
115− and what the trial and the open-source pool paid. Each workspace's day
116− is split by the share of its value at price that was given (value less
117− cash less the plan's included usage, which the plan's price paid for),
118− and that share of each of its buckets' cost is given, its part of
119− running g1t included. Stored as `given_micros` on `margin_days` and
120− `workspace_costs`.
117+ itself on purpose, by why:
118+ - **comped**: all of a comped workspace's cost, every bucket;
119+ - **free use**: a free period's usage, the overruns g1t covered
120+ (`ledger.given_micros`), and all of a workspace's cost on a day it had
121+ nothing priced (free allowances);
122+ - **trial** and **open-source pool**: what `trial_micros` and
123+ `oss_micros` paid.
124+
125+ Otherwise a workspace's day is split by those shares of its value at
126+ price, and the same shares of each of its buckets' cost are given, its
127+ part of running g1t included. The Team plan's included usage is sold:
128+ the plan's price paid for it. Stored on `margin_days` (`given_micros`
129+ and `given_<why>_micros`) and `workspace_costs` (`given_micros`).
121130 - **Month-end meters**: a day's figure is that day's `pending_days`
122131 snapshot less the day before's, within a month. Their month-end ledger
123132 entries are left out, so nothing is counted twice.
124−- **Product margin** = (value − cost) / value. **Margin on what was
125− sold**, sudo's headline and the overall alert = (Σ cash − (Σ cost − Σ
126− given)) / Σ cash. The margin with what was given, (Σ cash − Σ cost) / Σ
127− cash, shows under it. What was given is a budget, watched under g1t's own
128− spend, not a price below cost.
133+- **Product margin** = (value − cost) / value.
134+- **Sudo's statement** keeps apart:
135+ - **Usage sold**: cash for usage against the cost of the usage buckets
136+ less what was given. Its margin is the headline; at cost plus 20% it
137+ sits near 16.7%.
138+ - **Running g1t**: the plan's price against `platform` less its given
139+ share.
140+ - **Cloudflare subscriptions**: `CLOUDFLARE_FIXED_MONTHLY_MICROS` over
141+ the range, an estimate, since they are not on the usage bill.
142+ - **Not mapped**: billed, charged for by nothing.
143+ - **Given away**: by why. A budget, watched under g1t's own spend, never
144+ shown as a loss.
145+ - **All in**: money in against all of it, with the figure without what
146+ was given beside it. **Who g1t paid** splits the cost into Cloudflare
147+ and the model providers.
148+
149+ The overall alert is (Σ cash − (Σ cost − Σ given)) / Σ cash.
129150 - **Quantities**: where a mapping names an `own_meter`, Cloudflare's
130151 billed quantity of those lines (or, without one, Artifacts' operation
131152 events) against g1t's own count.
133154 **Shared costs to workspaces.** A bucket's cost is shared in proportion
134155 to, first available: Cloudflare's own per-workspace count
135156 (`cloudflare_<bucket>`, today the Artifacts events by repository), g1t's
136−own count, what each was charged for it, what its usage cost. `platform`
157+own count, what its usage cost (so free use carries its own cost), what
158+each was charged for it. `platform`
137159 and `unmapped` are shared by each workspace's share of all usage that
138160 day. Shares are whole micros that add up to the bill exactly (largest
139161 remainder).
+16−0
10671067 /** Money in against costMicros - givenMicros. */
10681068 soldMarginMicros?: number;
10691069 soldMarginPercent?: number | null;
1070+ /** Usage sold: usageMicros against what that usage cost, less what was given. */
1071+ usageCostMicros?: number;
1072+ usageMarginMicros?: number;
1073+ usageMarginPercent?: number | null;
1074+ /** Running g1t: plansMicros against the platform's cost, less its given share. */
1075+ runningCostMicros?: number;
1076+ /** Cost no mapping names, less its given share. */
1077+ unmappedCostMicros?: number;
1078+ /** givenMicros by why. Free is free periods, free allowances and overruns g1t covered. */
1079+ givenCompedMicros?: number;
1080+ givenFreeMicros?: number;
1081+ givenTrialMicros?: number;
1082+ givenPoolMicros?: number;
1083+ /** costMicros by who g1t pays: Cloudflare's bill (billed, after included allowances) and model providers (tokens, not on Cloudflare's bill). */
1084+ cloudflareCostMicros?: number;
1085+ modelsCostMicros?: number;
10701086 };
10711087
10721088 /** A count, cost or leak that does not add up. */
+7−0
1+-- What was given away, by why: comped workspaces, free use (free periods,
2+-- free allowances, overruns g1t covered), the trial and the open-source
3+-- pool. given_micros stays their total.
4+ALTER TABLE margin_days ADD COLUMN given_comped_micros INTEGER NOT NULL DEFAULT 0;
5+ALTER TABLE margin_days ADD COLUMN given_free_micros INTEGER NOT NULL DEFAULT 0;
6+ALTER TABLE margin_days ADD COLUMN given_trial_micros INTEGER NOT NULL DEFAULT 0;
7+ALTER TABLE margin_days ADD COLUMN given_pool_micros INTEGER NOT NULL DEFAULT 0;
+148−47
7878 pub own_quantity: f64,
7979 /// Of `cost()`, what went on usage g1t gave away (the workspaces'
8080 /// `WorkspaceDay::given`, added up).
81− pub given_micros: i64,
81+ pub given: Given,
8282 }
8383
8484 impl ProductDay {
109109 pub quantity: f64,
110110 }
111111
112+/// What g1t gave away, by why: its own comped workspaces, free use (a
113+/// free period, free allowances, overruns g1t covered), the trial, and the
114+/// open-source pool. The Team plan's included usage is paid for by the
115+/// plan's price, so it is sold, not given.
116+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
117+pub(crate) struct Given {
118+ pub comped: i64,
119+ pub free: i64,
120+ pub trial: i64,
121+ pub pool: i64,
122+}
123+
124+impl Given {
125+ pub fn total(&self) -> i64 {
126+ self.comped + self.free + self.trial + self.pool
127+ }
128+
129+ fn add(&mut self, other: &Given) {
130+ self.comped += other.comped;
131+ self.free += other.free;
132+ self.trial += other.trial;
133+ self.pool += other.pool;
134+ }
135+
136+ /// The same shares of `cost` as these are of `value`, at most all of it.
137+ fn of(&self, cost: i64, value: i64) -> Given {
138+ let total = self.total();
139+ if value <= 0 || cost <= 0 || total <= 0 {
140+ return Given::default();
141+ }
142+ let given = cost as i128 * total.min(value) as i128 / value as i128;
143+ let part = |x: i64| (given * x.max(0) as i128 / total as i128) as i64;
144+ Given { comped: part(self.comped), free: part(self.free), trial: part(self.trial), pool: part(self.pool) }
145+ }
146+}
147+
112148 /// What a workspace was charged for one key on one day.
113149 #[derive(Clone, Debug, Default, PartialEq)]
114150 pub(crate) struct UsageRow {
120156 pub cash: i64,
121157 pub cost: i64,
122158 /// Of `value`, what g1t gave away: all of it for g1t's own (comped)
123− /// workspaces and in a free period, else what the trial and the pools
124− /// paid. The Team plan's credit was paid for, so it is not given.
125− pub given: i64,
159+ /// workspaces and in a free period, else what the trial and the pool
160+ /// paid and the overruns g1t covered.
161+ pub given: Given,
126162 }
127163
128164 /// One workspace's share of a product's cost on one day.
136172 pub revenue: i64,
137173 /// What its usage was priced at, whoever paid for it.
138174 pub value: i64,
139− /// Of `cost`, the part g1t gave away: the cost times the share of the
140− /// workspace's usage that day that g1t paid for (see `UsageRow::given`).
141− pub given: i64,
175+ /// Of `cost`, the part g1t gave away: all of it for a comped workspace
176+ /// or one with nothing priced that day (free use), else the cost times
177+ /// the shares of its usage that day that g1t paid for.
178+ pub given: Given,
142179 }
143180
144181 fn micros(dollars: f64) -> i64 {
154191 lines: &[LineRow],
155192 own: &[OwnRow],
156193 usage: &[UsageRow],
194+ internal: &BTreeSet<String>,
157195 ) -> (Vec<ProductDay>, Vec<WorkspaceDay>) {
158196 let mut days: BTreeMap<(String, String), ProductDay> = BTreeMap::new();
159197 let entry = |day: &str, bucket: &str| -> ProductDay {
216254 let mut revenue: BTreeMap<(String, String, String), i64> = BTreeMap::new();
217255 let mut valued: BTreeMap<(String, String, String), i64> = BTreeMap::new();
218256 let mut active: BTreeMap<String, Vec<(String, f64)>> = BTreeMap::new();
219− let mut gave: BTreeMap<(String, String), (i64, i64)> = BTreeMap::new();
257+ let mut gave: BTreeMap<(String, String), (Given, i64)> = BTreeMap::new();
220258 for u in usage {
221259 let g = gave.entry((u.day.clone(), u.workspace.clone())).or_default();
222− g.0 += u.given;
260+ g.0.add(&u.given);
223261 g.1 += u.value;
224262 let bucket = bucket_of(&u.key);
225263 let key = (u.day.clone(), bucket.clone());
234272 active.entry(u.day.clone()).or_default().push((u.workspace.clone(), u.value.max(u.cost) as f64));
235273 }
236274 // Each bucket's cost shared out: by Cloudflare's own count per
237− // workspace, else by g1t's own count of its units, else
238− // by what each workspace was charged for it, else by what its usage
239− // cost; running g1t, and what no one mapped, by each workspace's share
240− // of all usage that day.
275+ // workspace, else by g1t's own count of its units, else by what its
276+ // usage cost (so free use carries its own cost), else by what it was
277+ // charged; running g1t, and what no one mapped, by each workspace's
278+ // share of all usage that day.
241279 let mut shares: BTreeMap<(String, String, String), i64> = BTreeMap::new();
242280 for ((day, bucket), row) in &days {
243281 let key = (day.clone(), bucket.clone());
245283 let weights = if OVERHEAD.contains(&bucket.as_str()) || bucket == UNMAPPED {
246284 active.get(day).cloned()
247285 } else {
248− weigh(&cf_by).or_else(|| weigh(&own_by)).or_else(|| weigh(&value_by)).or_else(|| weigh(&cost_by)).or_else(|| active.get(day).cloned())
286+ weigh(&cf_by).or_else(|| weigh(&own_by)).or_else(|| weigh(&cost_by)).or_else(|| weigh(&value_by)).or_else(|| active.get(day).cloned())
249287 };
250288 for (workspace, micros) in attribute(row.cost(), &weights.unwrap_or_default()) {
251289 *shares.entry((day.clone(), workspace, bucket.clone())).or_default() += micros;
256294 .into_iter()
257295 .map(|(day, workspace, bucket)| {
258296 let cost = shares.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0);
259− // The day's share given away applies to every bucket, so a
260− // comped workspace's part of running g1t is given too.
261− let given = match gave.get(&(day.clone(), workspace.clone())) {
262− Some(&(given, value)) if value > 0 => (cost as i128 * given.clamp(0, value) as i128 / value as i128) as i64,
263− _ => 0,
297+ // The day's shares given away apply to every bucket, so a
298+ // comped workspace's part of running g1t is given too. A
299+ // workspace with nothing priced that day used g1t for free.
300+ let given = if internal.contains(&workspace) {
301+ Given { comped: cost, ..Given::default() }
302+ } else {
303+ match gave.get(&(day.clone(), workspace.clone())) {
304+ Some((given, value)) if *value > 0 => given.of(cost, *value),
305+ _ => Given { free: cost.max(0), ..Given::default() },
306+ }
264307 };
265308 WorkspaceDay {
266309 cost,
275318 .collect();
276319 for w in &workspaces {
277320 if let Some(row) = days.get_mut(&(w.day.clone(), w.bucket.clone())) {
278− row.given_micros += w.given;
321+ row.given.add(&w.given);
279322 }
280323 }
281324 (days.into_values().collect(), workspaces)
298341 };
299342 let (cost, charge) = ((cost - before_cost).max(0), (charge - before_charge).max(0));
300343 if cost > 0 || charge > 0 {
301− out.push(UsageRow { day: day.clone(), workspace: workspace.clone(), key: source.clone(), value: charge, cash: charge, cost, given: 0 });
344+ out.push(UsageRow { day: day.clone(), workspace: workspace.clone(), key: source.clone(), value: charge, cash: charge, cost, given: Given::default() });
302345 }
303346 previous = Some(snap);
304347 }
596639 cf_quantity: f64,
597640 own_quantity: f64,
598641 #[serde(default)]
599− given_micros: Option<i64>,
642+ given_comped_micros: Option<i64>,
643+ #[serde(default)]
644+ given_free_micros: Option<i64>,
645+ #[serde(default)]
646+ given_trial_micros: Option<i64>,
647+ #[serde(default)]
648+ given_pool_micros: Option<i64>,
600649 }
601650
602651 impl From<MarginRow> for ProductDay {
610659 cash_micros: r.cash_micros,
611660 cf_quantity: r.cf_quantity,
612661 own_quantity: r.own_quantity,
613− given_micros: r.given_micros.unwrap_or(0),
662+ given: Given {
663+ comped: r.given_comped_micros.unwrap_or(0),
664+ free: r.given_free_micros.unwrap_or(0),
665+ trial: r.given_trial_micros.unwrap_or(0),
666+ pool: r.given_pool_micros.unwrap_or(0),
667+ },
614668 }
615669 }
616670 }
687741 own_provider: i64,
688742 cash: Option<i64>,
689743 drawn: Option<i64>,
690− credit: Option<i64>,
744+ trial: Option<i64>,
745+ oss: Option<i64>,
746+ covered: Option<i64>,
691747 cost: Option<i64>,
692748 }
693749 let charged_here = crate::storage::CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
701757 CASE WHEN billed_to = 'workspace' THEN 1 ELSE 0 END AS own_provider,
702758 -SUM(amount_micros) AS cash,
703759 SUM(COALESCE(credit_micros, 0) + COALESCE(trial_micros, 0) + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0)) AS drawn,
704− SUM(COALESCE(credit_micros, 0)) AS credit,
760+ SUM(COALESCE(trial_micros, 0)) AS trial,
761+ SUM(COALESCE(oss_micros, 0)) AS oss,
762+ SUM(COALESCE(given_micros, 0)) AS covered,
705763 SUM(COALESCE(cost_micros, 0)) AS cost
706764 FROM ledger
707765 WHERE kind = 'usage' AND created_at >= ?1 AND created_at <= ?2 AND COALESCE(task, '') NOT IN ({charged_here})
722780 let cash = r.cash.unwrap_or(0);
723781 let paid = cash + r.drawn.unwrap_or(0);
724782 let value = usage_value(r.internal == 1, cost, paid, self.margin_percent);
725− let given = if r.internal == 1 { value } else { (value - cash - r.credit.unwrap_or(0)).max(0) };
783+ let given = if r.internal == 1 {
784+ Given { comped: value, ..Given::default() }
785+ } else if paid == 0 && cost > 0 {
786+ Given { free: value, ..Given::default() }
787+ } else {
788+ Given { free: r.covered.unwrap_or(0), trial: r.trial.unwrap_or(0), pool: r.oss.unwrap_or(0), comped: 0 }
789+ };
726790 if r.internal == 1 {
727791 internal.insert(r.workspace.clone());
728792 }
751815 .collect::<Vec<_>>();
752816 out.extend(pending_deltas(&snaps).into_iter().filter(|u| u.day.as_str() >= since).map(|mut u| {
753817 if internal.contains(&u.workspace) {
754− u.given = u.value;
818+ u.given = Given { comped: u.value, ..Given::default() };
755819 }
756820 u
757821 }));
776840 for p in plans {
777841 for (day, micros) in spread(&p.day, p.micros.unwrap_or(0), PLAN_DAYS) {
778842 if day.as_str() >= since && day.as_str() <= until {
779− out.push(UsageRow { day, workspace: p.workspace.clone(), key: "plan".into(), value: micros, cash: micros, cost: 0, given: 0 });
843+ out.push(UsageRow { day, workspace: p.workspace.clone(), key: "plan".into(), value: micros, cash: micros, cost: 0, given: Given::default() });
780844 }
781845 }
782846 }
815879 .await?
816880 .results::<OwnRow>()?;
817881 let usage = self.usage_rows(since, until).await?;
818− let (days, workspaces) = fold(&rules, &revenue_map, &lines, &own, &usage);
882+ #[derive(Deserialize)]
883+ struct Internal {
884+ workspace: String,
885+ }
886+ let internal: BTreeSet<String> = self
887+ .db
888+ .prepare(format!("WITH i(workspace) AS ({}) SELECT DISTINCT workspace FROM i", crate::sales::INTERNAL_SQL))
889+ .all()
890+ .await?
891+ .results::<Internal>()?
892+ .into_iter()
893+ .map(|i| i.workspace)
894+ .collect();
895+ let (days, workspaces) = fold(&rules, &revenue_map, &lines, &own, &usage, &internal);
819896 let now = rfc3339(now_ms());
820897 self.db
821898 .batch(vec![
829906 statements.push(
830907 self.db
831908 .prepare(
832− "INSERT OR REPLACE INTO margin_days (day, bucket, cf_cost_micros, own_cost_micros, value_micros, cash_micros, cf_quantity, own_quantity, given_micros, computed_at)
833− VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
909+ "INSERT OR REPLACE INTO margin_days (day, bucket, cf_cost_micros, own_cost_micros, value_micros, cash_micros, cf_quantity, own_quantity, given_micros, given_comped_micros, given_free_micros, given_trial_micros, given_pool_micros, computed_at)
910+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
834911 )
835912 .bind(&[
836913 d.day.as_str().into(),
841918 (d.cash_micros as f64).into(),
842919 d.cf_quantity.into(),
843920 d.own_quantity.into(),
844− (d.given_micros as f64).into(),
921+ (d.given.total() as f64).into(),
922+ (d.given.comped as f64).into(),
923+ (d.given.free as f64).into(),
924+ (d.given.trial as f64).into(),
925+ (d.given.pool as f64).into(),
845926 now.as_str().into(),
846927 ])?,
847928 );
861942 (w.cost as f64).into(),
862943 (w.revenue as f64).into(),
863944 (w.value as f64).into(),
864− (w.given as f64).into(),
945+ (w.given.total() as f64).into(),
865946 ])?,
866947 );
867948 }
10381119 // trial and the pools) is a budget it chose to spend, watched on
10391120 // its own (budget.rs): not part of whether what is sold pays.
10401121 overall.0 += d.cash_micros;
1041− overall.1 += (d.cost() - d.given_micros).max(0);
1122+ overall.1 += (d.cost() - d.given.total()).max(0);
10421123 if !OVERHEAD.contains(&d.bucket.as_str()) && d.bucket != UNMAPPED {
10431124 by.entry(d.bucket.clone()).or_default().push((d.day.clone(), d.value_micros, d.cost()));
10441125 }
13341415 p.value_micros += d.value_micros;
13351416 p.cost_micros += d.cost();
13361417 overall.cost_micros += d.cost();
1337− overall.given_micros += d.given_micros;
1338− if d.bucket == "platform" {
1418+ overall.given_micros += d.given.total();
1419+ if NOT_CLOUDFLARE.contains(&d.bucket.as_str()) {
1420+ overall.models_cost_micros += d.cost();
1421+ } else {
1422+ overall.cloudflare_cost_micros += d.cost();
1423+ }
1424+ overall.given_comped_micros += d.given.comped;
1425+ overall.given_free_micros += d.given.free;
1426+ overall.given_trial_micros += d.given.trial;
1427+ overall.given_pool_micros += d.given.pool;
1428+ let sold = (d.cost() - d.given.total()).max(0);
1429+ if OVERHEAD.contains(&d.bucket.as_str()) {
13391430 overall.plans_micros += d.cash_micros;
1431+ overall.running_cost_micros += sold;
1432+ } else if d.bucket == UNMAPPED {
1433+ overall.usage_micros += d.cash_micros;
1434+ overall.unmapped_cost_micros += sold;
13401435 } else {
13411436 overall.usage_micros += d.cash_micros;
1437+ overall.usage_cost_micros += sold;
13421438 }
13431439 }
13441440 for p in products.values_mut() {
13511447 let sold = (overall.cost_micros - overall.given_micros).max(0);
13521448 overall.sold_margin_micros = revenue - sold;
13531449 overall.sold_margin_percent = margin_percent(revenue, sold);
1450+ overall.usage_margin_micros = overall.usage_micros - overall.usage_cost_micros;
1451+ overall.usage_margin_percent = margin_percent(overall.usage_micros, overall.usage_cost_micros);
13541452 let mut products: Vec<ProductMargin> = products.into_values().collect();
13551453 products.sort_by_key(|p| std::cmp::Reverse(p.cost_micros.max(p.value_micros)));
13561454
15591657 }
15601658
15611659 fn usage(day: &str, workspace: &str, key: &str, value: i64, cash: i64, cost: i64) -> UsageRow {
1562− UsageRow { day: day.into(), workspace: workspace.into(), key: key.into(), value, cash, cost, given: 0 }
1660+ UsageRow { day: day.into(), workspace: workspace.into(), key: key.into(), value, cash, cost, given: Given::default() }
15631661 }
15641662
15651663 #[test]
15831681 usage("2026-10-15", "acme", "implement", 120_000, 120_000, 100_000),
15841682 usage("2026-10-15", "beta", "plan", 20_000_000, 20_000_000, 0),
15851683 ];
1586− let (days, workspaces) = fold(&rules(), &revenue_map(), &lines, &own, &usage);
1684+ let (days, workspaces) = fold(&rules(), &revenue_map(), &lines, &own, &usage, &BTreeSet::new());
15871685 let get = |bucket: &str| days.iter().find(|d| d.bucket == bucket).unwrap();
15881686 let sandboxes = get("sandboxes");
15891687 assert_eq!((sandboxes.cf_cost_micros, sandboxes.own_cost_micros, sandboxes.value_micros, sandboxes.cash_micros), (2_000_000, 3_000_000, 3_600_000, 2_200_000));
16141712 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_push", 30.0, 0.0),
16151713 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_ratelimited", 9.0, 0.0),
16161714 ];
1617− let (days, _) = fold(&rules(), &revenue_map(), &lines, &[], &[]);
1715+ let (days, _) = fold(&rules(), &revenue_map(), &lines, &[], &[], &BTreeSet::new());
16181716 assert_eq!(days[0].cf_quantity, 150.0);
16191717 assert_eq!(days[0].cf_cost_micros, 0);
16201718 }
16511749 }
16521750
16531751 fn day(bucket: &str, cf: i64, own: i64, value: i64, cfq: f64, ownq: f64) -> ProductDay {
1654− ProductDay { day: "2026-10-15".into(), bucket: bucket.into(), cf_cost_micros: cf, own_cost_micros: own, value_micros: value, cash_micros: value, cf_quantity: cfq, own_quantity: ownq, given_micros: 0 }
1752+ ProductDay { day: "2026-10-15".into(), bucket: bucket.into(), cf_cost_micros: cf, own_cost_micros: own, value_micros: value, cash_micros: value, cf_quantity: cfq, own_quantity: ownq, given: Given::default() }
16551753 }
16561754
16571755 #[test]
17101808 let map = BTreeMap::new();
17111809 // A comped workspace (all of it given), one in its trial (half paid
17121810 // by the trial) and one paying in cash, all on models.
1713− let mut comped = usage("2026-10-15", "flagon", "agent", 1_200_000, 0, 1_000_000);
1714− comped.given = comped.value;
1811+ let comped = usage("2026-10-15", "flagon", "agent", 1_200_000, 0, 1_000_000);
17151812 let mut trial = usage("2026-10-15", "acme", "agent", 1_200_000, 600_000, 1_000_000);
1716− trial.given = 600_000;
1813+ trial.given = Given { trial: 600_000, ..Given::default() };
17171814 let paying = usage("2026-10-15", "beta", "agent", 1_200_000, 1_200_000, 1_000_000);
1718− let (days, workspaces) = fold(&[], &map, &[], &[], &[comped, trial, paying]);
1815+ // Nothing priced that day: free use.
1816+ let free = usage("2026-10-15", "gamma", "agent", 0, 0, 1_000_000);
1817+ let internal = BTreeSet::from(["flagon".to_string()]);
1818+ let (days, workspaces) = fold(&[], &map, &[], &[], &[comped, trial, paying, free], &internal);
17191819 let models = days.iter().find(|d| d.bucket == "models").unwrap();
1720− assert_eq!((models.cost(), models.given_micros), (3_000_000, 1_500_000));
1721− let given = |w: &str| workspaces.iter().find(|x| x.workspace == w).unwrap().given;
1722− assert_eq!((given("flagon"), given("acme"), given("beta")), (1_000_000, 500_000, 0));
1820+ assert_eq!(models.cost(), 4_000_000);
1821+ assert_eq!(models.given, Given { comped: 1_000_000, free: 1_000_000, trial: 500_000, pool: 0 });
1822+ let given = |w: &str| workspaces.iter().find(|x| x.workspace == w).unwrap().given.total();
1823+ assert_eq!((given("flagon"), given("acme"), given("beta"), given("gamma")), (1_000_000, 500_000, 0, 1_000_000));
17231824 }
17241825
17251826 #[test]