g1t/services/billing/src/margin.rs

1,707 lines77,207 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.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1//! What g1t earns on each thing it sells, measured against what
2//! Cloudflare actually charged for it.
3//!
4//! Once a day, after `costs` has read Cloudflare's bill, the reconciler
5//! puts three figures side by side for every day and each of g1t's
6//! products (a "bucket": sandboxes, deployments, git, repository storage,
7//! …):
8//!
9//! 1. **What Cloudflare charged**: the day's cost lines `cost_map` gives
10//! the bucket.
11//! 2. **What g1t's meters recorded**: the cost on the ledger's entries for
12//! it (the price book's cost at the time) and, where a mapping names
13//! one, g1t's own count of the same units (git operations).
14//! 3. **What customers were charged**: the entries' value at price, before
15//! the plan's included usage, a trial or a pool paid part of it; and of
16//! that, what workspaces paid. Month-end meters (git, storage, scans,
17//! embeddings, the actions cache) come from daily snapshots of what they
18//! had come to (`pending_days`). The plan's price is the `platform`
19//! bucket's: the plan pays for running g1t.
20//!
21//! From those: margin per product (value against cost) and for all of g1t
22//! (money in against every cost); drift (counts or costs that disagree past
23//! a mapping's threshold, and leaks: cost with no revenue, or a Cloudflare
24//! meter no one mapped); each workspace's cost, Cloudflare's figure shared
25//! out by each workspace's own meters; and price proposals when a unit's
26//! real cost has moved (`pricing`). Alerts go to staff by email and as a
27//! banner in sudo. See docs/BILLING_OPERATIONS.md.
28
29use std::collections::{BTreeMap, BTreeSet};
30
31use g1t_contracts::billing::*;
32use g1t_contracts::{FailureCode, Outcome, new_id};
33use g1t_contracts::time::rfc3339;
34use g1t_kit::now_ms;
35use serde::{Deserialize, Serialize};
36use worker::wasm_bindgen::JsValue;
37use worker::{Env, Result};
38
39use crate::Billing;
40use crate::costs::{self, ARTIFACTS_OPERATIONS, DAY_MS, Rule, SOURCE_ARTIFACTS, SOURCE_BILLABLE, UNMAPPED};
41
42/// Buckets that are the cost of running g1t, paid by the plan rather than
43/// sold by the unit: never a leak for having no revenue of their own.
44pub(crate) const OVERHEAD: [&str; 1] = ["platform"];
45/// Buckets Cloudflare does not bill: their cost is g1t's own figure.
46pub(crate) const NOT_CLOUDFLARE: [&str; 1] = ["models"];
47/// The days drift is judged over.
48const DRIFT_DAYS: u64 = 7;
49/// The days a workspace's cost is set against its revenue.
50const ANOMALY_DAYS: u64 = 30;
51/// The days a unit's cost is measured over.
52const MEASURE_DAYS: u64 = 30;
53/// Fewer of g1t's units than this say nothing about cost per unit.
54const MIN_UNITS: f64 = 1_000.0;
55/// An open alert is emailed again after this long.
56const REMIND_MS: u64 = 7 * DAY_MS;
57
58// ---------------------------------------------------------------------
59// The arithmetic, apart from the database so it can be tested.
60// ---------------------------------------------------------------------
61
62/// One of g1t's products on one day.
63#[derive(Clone, Debug, Default, PartialEq)]
64pub(crate) struct ProductDay {
65 pub day: String,
66 pub bucket: String,
67 /// What Cloudflare charged g1t, in millionths of a dollar.
68 pub cf_cost_micros: i64,
69 /// What g1t's meters recorded it cost (the price book's cost).
70 pub own_cost_micros: i64,
71 /// What customers were charged for it at price, before what paid.
72 pub value_micros: i64,
73 /// Of that, what workspaces paid themselves.
74 pub cash_micros: i64,
75 /// Units Cloudflare counted and units g1t counted, where a mapping
76 /// says they are the same units.
77 pub cf_quantity: f64,
78 pub own_quantity: f64,
79}
80
81impl ProductDay {
82 /// What it cost: Cloudflare's figure where Cloudflare bills it, else
83 /// g1t's own (models are billed by their providers, through the gateway).
84 pub fn cost(&self) -> i64 {
85 if NOT_CLOUDFLARE.contains(&self.bucket.as_str()) { self.own_cost_micros } else { self.cf_cost_micros }
86 }
87}
88
89/// A line of Cloudflare's bill, as stored.
90#[derive(Clone, Debug, Deserialize)]
91pub(crate) struct LineRow {
92 pub day: String,
93 pub source: String,
94 pub product: String,
95 pub meter: String,
96 pub quantity: f64,
97 pub cost_usd: f64,
98}
99
100/// A count of g1t's own, as stored.
101#[derive(Clone, Debug, Deserialize)]
102pub(crate) struct OwnRow {
103 pub day: String,
104 pub meter: String,
105 pub workspace: String,
106 pub quantity: f64,
107}
108
109/// What a workspace was charged for one key on one day.
110#[derive(Clone, Debug, Default, PartialEq)]
111pub(crate) struct UsageRow {
112 pub day: String,
113 pub workspace: String,
114 /// A ledger task (or `builds`), a month-end source, or `plan`.
115 pub key: String,
116 pub value: i64,
117 pub cash: i64,
118 pub cost: i64,
119}
120
121/// One workspace's share of a product's cost on one day.
122#[derive(Clone, Debug, PartialEq)]
123pub(crate) struct WorkspaceDay {
124 pub day: String,
125 pub workspace: String,
126 pub bucket: String,
127 pub cost: i64,
Margin alerts measure what is sold, and say dollars when a percentage would mislead128 /// What the workspace paid in cash.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily129 pub revenue: i64,
Margin alerts measure what is sold, and say dollars when a percentage would mislead130 /// What its usage was priced at, whoever paid for it.
131 pub value: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily132}
133
134fn micros(dollars: f64) -> i64 {
135 (dollars * 1_000_000.0).round() as i64
136}
137
138/// Puts the day's bill, g1t's counts and what customers were charged side
139/// by side, a row per day and bucket, and shares each bucket's cost out
140/// to workspaces.
141pub(crate) fn fold(
142 rules: &[Rule],
143 revenue_map: &BTreeMap<String, String>,
144 lines: &[LineRow],
145 own: &[OwnRow],
146 usage: &[UsageRow],
147) -> (Vec<ProductDay>, Vec<WorkspaceDay>) {
148 let mut days: BTreeMap<(String, String), ProductDay> = BTreeMap::new();
149 let entry = |day: &str, bucket: &str| -> ProductDay {
150 ProductDay { day: day.to_owned(), bucket: bucket.to_owned(), ..ProductDay::default() }
151 };
152 // Which of g1t's own meters count each bucket's units.
153 let mut own_meters: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
154 for rule in rules {
155 if let Some(meter) = &rule.own_meter {
156 own_meters.entry(rule.bucket.as_str()).or_default().insert(meter.as_str());
157 }
158 }
159 let mut events: BTreeMap<(String, String), f64> = BTreeMap::new();
160 for line in lines {
161 let rule = costs::classify(rules, &line.product, &line.meter);
162 let bucket = rule.map_or(UNMAPPED, |r| r.bucket.as_str());
163 let key = (line.day.clone(), bucket.to_owned());
164 if line.source == SOURCE_ARTIFACTS {
165 // What Artifacts counted: operations only, and only where the
166 // bill does not count them itself.
167 if ARTIFACTS_OPERATIONS.contains(&line.meter.as_str()) {
168 *events.entry(key).or_default() += line.quantity;
169 }
170 continue;
171 }
172 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
173 row.cf_cost_micros += micros(line.cost_usd);
174 if line.source == SOURCE_BILLABLE && rule.is_some_and(|r| r.own_meter.is_some()) {
175 row.cf_quantity += line.quantity;
176 }
177 }
178 for (key, quantity) in events {
179 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
180 if row.cf_quantity == 0.0 {
181 row.cf_quantity = quantity;
182 }
183 }
184 // g1t's own counts of the same units, by bucket and by workspace.
185 let mut own_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
186 // Cloudflare's own count by workspace, where it gives one
187 // (`cloudflare_<bucket>`): the best way to share its cost.
188 let mut cf_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
189 for count in own {
190 if let Some(bucket) = count.meter.strip_prefix("cloudflare_") {
191 cf_by.entry((count.day.clone(), bucket.to_owned())).or_default().push((count.workspace.clone(), count.quantity));
192 continue;
193 }
194 for (bucket, meters) in &own_meters {
195 if meters.contains(count.meter.as_str()) {
196 let key = (count.day.clone(), (*bucket).to_owned());
197 days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1)).own_quantity += count.quantity;
198 own_by.entry(key).or_default().push((count.workspace.clone(), count.quantity));
199 }
200 }
201 }
202 // What customers were charged.
203 let bucket_of = |key: &str| revenue_map.get(key).cloned().unwrap_or_else(|| "models".to_owned());
204 let mut value_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
205 let mut cost_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
206 let mut revenue: BTreeMap<(String, String, String), i64> = BTreeMap::new();
Margin alerts measure what is sold, and say dollars when a percentage would mislead207 let mut valued: BTreeMap<(String, String, String), i64> = BTreeMap::new();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily208 let mut active: BTreeMap<String, Vec<(String, f64)>> = BTreeMap::new();
209 for u in usage {
210 let bucket = bucket_of(&u.key);
211 let key = (u.day.clone(), bucket.clone());
212 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
213 row.own_cost_micros += u.cost;
214 row.value_micros += u.value;
215 row.cash_micros += u.cash;
216 value_by.entry(key.clone()).or_default().push((u.workspace.clone(), u.value as f64));
217 cost_by.entry(key).or_default().push((u.workspace.clone(), u.cost as f64));
Margin alerts measure what is sold, and say dollars when a percentage would mislead218 *revenue.entry((u.day.clone(), u.workspace.clone(), bucket.clone())).or_default() += u.cash;
219 *valued.entry((u.day.clone(), u.workspace.clone(), bucket)).or_default() += u.value;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily220 active.entry(u.day.clone()).or_default().push((u.workspace.clone(), u.value.max(u.cost) as f64));
221 }
222 // Each bucket's cost shared out: by Cloudflare's own count per
223 // workspace, else by g1t's own count of its units, else
224 // by what each workspace was charged for it, else by what its usage
225 // cost; running g1t, and what no one mapped, by each workspace's share
226 // of all usage that day.
227 let mut shares: BTreeMap<(String, String, String), i64> = BTreeMap::new();
228 for ((day, bucket), row) in &days {
229 let key = (day.clone(), bucket.clone());
230 let weigh = |m: &BTreeMap<(String, String), Vec<(String, f64)>>| m.get(&key).filter(|w| w.iter().any(|(_, v)| *v > 0.0)).cloned();
231 let weights = if OVERHEAD.contains(&bucket.as_str()) || bucket == UNMAPPED {
232 active.get(day).cloned()
233 } else {
234 weigh(&cf_by).or_else(|| weigh(&own_by)).or_else(|| weigh(&value_by)).or_else(|| weigh(&cost_by)).or_else(|| active.get(day).cloned())
235 };
236 for (workspace, micros) in attribute(row.cost(), &weights.unwrap_or_default()) {
237 *shares.entry((day.clone(), workspace, bucket.clone())).or_default() += micros;
238 }
239 }
240 let keys: BTreeSet<(String, String, String)> = shares.keys().chain(revenue.keys()).cloned().collect();
241 let workspaces = keys
242 .into_iter()
243 .map(|(day, workspace, bucket)| WorkspaceDay {
244 cost: shares.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0),
245 revenue: revenue.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0),
Margin alerts measure what is sold, and say dollars when a percentage would mislead246 value: valued.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily247 day,
248 workspace,
249 bucket,
250 })
251 .collect();
252 (days.into_values().collect(), workspaces)
253}
254
255/// A month-end source's day, from the snapshots of what it had come to:
256/// each day's figure less the day before's in the same month (the first
257/// day of a month, or the first snapshot, is its own).
258pub(crate) fn pending_deltas(snapshots: &[(String, String, String, i64, i64)]) -> Vec<UsageRow> {
259 // (day, workspace, source, cost, charge), any order.
260 let mut sorted = snapshots.to_vec();
261 sorted.sort_by(|a, b| (&a.1, &a.2, &a.0).cmp(&(&b.1, &b.2, &b.0)));
262 let mut out = Vec::new();
263 let mut previous: Option<&(String, String, String, i64, i64)> = None;
264 for snap in &sorted {
265 let (day, workspace, source, cost, charge) = snap;
266 let (before_cost, before_charge) = match previous {
267 Some(p) if p.1 == *workspace && p.2 == *source && p.0[..7] == day[..7] => (p.3, p.4),
268 _ => (0, 0),
269 };
270 let (cost, charge) = ((cost - before_cost).max(0), (charge - before_charge).max(0));
271 if cost > 0 || charge > 0 {
272 out.push(UsageRow { day: day.clone(), workspace: workspace.clone(), key: source.clone(), value: charge, cash: charge, cost });
273 }
274 previous = Some(snap);
275 }
276 out
277}
278
279/// Margin as a share of what was charged, in percent; None when nothing was.
280pub(crate) fn margin_percent(revenue_micros: i64, cost_micros: i64) -> Option<f64> {
281 (revenue_micros > 0).then(|| (revenue_micros - cost_micros) as f64 * 100.0 / revenue_micros as f64)
282}
283
284/// How far `ours` is from `theirs`, in percent of theirs; None when theirs
285/// is nothing.
286pub(crate) fn delta_percent(ours: f64, theirs: f64) -> Option<f64> {
287 (theirs > 0.0).then(|| (ours - theirs) * 100.0 / theirs)
288}
289
290#[derive(Clone, Copy, Debug, PartialEq, Eq)]
291pub(crate) enum DriftKind {
292 /// g1t counted a different number of units than Cloudflare did.
293 Count,
294 /// What Cloudflare charged differs from what the price book says the
295 /// same usage cost.
296 Cost,
297 /// Cloudflare charged for something nothing charges customers for.
298 Leak,
299}
300
301impl DriftKind {
302 pub fn as_str(self) -> &'static str {
303 match self {
304 DriftKind::Count => "count",
305 DriftKind::Cost => "cost",
306 DriftKind::Leak => "leak",
307 }
308 }
309}
310
311#[derive(Clone, Debug, PartialEq)]
312pub(crate) struct Drift {
313 pub bucket: String,
314 pub kind: DriftKind,
315 pub ours: f64,
316 pub cloudflare: f64,
317 pub delta_percent: Option<f64>,
318}
319
320/// Drift over a window for one bucket: counts more than `threshold`
321/// percent apart, a bill that far from the price book's cost of the same
322/// usage, and cost with nothing charged for it. Under `min_cost_micros`
323/// in all, cost says nothing.
324pub(crate) fn drifts(bucket: &str, days: &[ProductDay], threshold: f64, counted: bool, min_cost_micros: i64) -> Vec<Drift> {
325 let overhead = OVERHEAD.contains(&bucket);
326 let sum = |f: &dyn Fn(&ProductDay) -> f64| days.iter().map(f).sum::<f64>();
327 let cf_cost = sum(&|d| d.cf_cost_micros as f64);
328 let own_cost = sum(&|d| d.own_cost_micros as f64);
329 let value = sum(&|d| d.value_micros as f64);
330 let (cf_quantity, own_quantity) = (sum(&|d| d.cf_quantity), sum(&|d| d.own_quantity));
331 let mut out = Vec::new();
332 if counted && cf_quantity > 0.0 {
333 let delta = delta_percent(own_quantity, cf_quantity);
334 if delta.is_some_and(|d| d.abs() > threshold) {
335 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Count, ours: own_quantity, cloudflare: cf_quantity, delta_percent: delta });
336 }
337 }
338 let enough = cf_cost.max(own_cost) >= min_cost_micros as f64;
339 if enough && !overhead && cf_cost > 0.0 && own_cost > 0.0 && !NOT_CLOUDFLARE.contains(&bucket) {
340 let delta = delta_percent(own_cost, cf_cost);
341 if delta.is_some_and(|d| d.abs() > threshold) {
342 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Cost, ours: own_cost, cloudflare: cf_cost, delta_percent: delta });
343 }
344 }
345 if !overhead && cf_cost >= min_cost_micros as f64 && value <= 0.0 {
346 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Leak, ours: value, cloudflare: cf_cost, delta_percent: None });
347 }
348 out
349}
350
351/// When the last `days` in a row (each with enough cost to say something)
352/// were all under the floor: the first of them and the worst margin.
353/// Each item is a day's (day, revenue, cost).
354pub(crate) fn breach(series: &[(String, i64, i64)], floor_percent: f64, days: usize, min_cost_micros: i64) -> Option<(String, f64)> {
355 if days == 0 || series.len() < days {
356 return None;
357 }
358 let tail = &series[series.len() - days..];
359 let mut worst = f64::INFINITY;
360 for (_, revenue, cost) in tail {
361 if *cost < min_cost_micros {
362 return None;
363 }
364 let margin = margin_percent(*revenue, *cost).unwrap_or(-100.0);
365 if margin >= floor_percent {
366 return None;
367 }
368 worst = worst.min(margin);
369 }
370 Some((tail[0].0.clone(), worst))
371}
372
373/// `total` shared out in proportion to `weights`, in whole millionths that
374/// add up to it exactly (largest remainder first). Nothing to share, or no
375/// weight, shares nothing.
376pub(crate) fn attribute(total: i64, weights: &[(String, f64)]) -> Vec<(String, i64)> {
377 let mut merged: BTreeMap<String, f64> = BTreeMap::new();
378 for (key, w) in weights {
379 *merged.entry(key.clone()).or_default() += w.max(0.0);
380 }
381 let sum: f64 = merged.values().sum();
382 if total <= 0 || sum <= 0.0 {
383 return Vec::new();
384 }
385 let mut shares: Vec<(String, i64, f64)> = merged
386 .into_iter()
387 .map(|(key, w)| {
388 let exact = total as f64 * w / sum;
389 (key, exact.floor() as i64, exact - exact.floor())
390 })
391 .collect();
392 let mut left = total - shares.iter().map(|s| s.1).sum::<i64>();
393 let mut order: Vec<usize> = (0..shares.len()).collect();
394 order.sort_by(|a, b| shares[*b].2.total_cmp(&shares[*a].2).then(shares[*a].0.cmp(&shares[*b].0)));
395 for index in order {
396 if left <= 0 {
397 break;
398 }
399 shares[index].1 += 1;
400 left -= 1;
401 }
402 shares.into_iter().filter(|s| s.1 > 0).map(|(key, micros, _)| (key, micros)).collect()
403}
404
405/// Workspaces that cost g1t more than `factor` times what they paid, with
406/// at least `floor_micros` of cost: each (workspace, cost, revenue), the
407/// biggest gap first.
Models' margin read -14%: usage nothing paid for is valued at price, not $0408/// What a day's usage was worth at price. g1t's own workspaces are valued
409/// at price. So is usage nothing paid for, neither charged nor drawn from
410/// the plan, a trial, a pool or a gift (a free period): it was given away at
411/// its price, not sold for nothing. Anything paid keeps what it was paid, so
412/// a discount still shows as one.
413pub(crate) fn usage_value(internal: bool, cost: i64, paid: i64, margin_percent: u32) -> i64 {
414 if internal || (paid == 0 && cost > 0) {
415 return crate::credits::with_margin(cost, margin_percent);
416 }
417 paid
418}
419
Margin alerts measure what is sold, and say dollars when a percentage would mislead420/// What the overall alert says: the money as money, and a percentage only
421/// while there is enough coming in for one to mean something (a few cents
422/// against dollars of cost reads as -8000%).
423pub(crate) fn overall_detail(took: i64, spent: i64, days: usize, floor: f64, worst: f64) -> String {
424 if took < 1_000_000 * days as i64 {
425 return format!(
426 "All of g1t, comped workspaces left out: took in {} against {} of Cloudflare's bill over {days} days.",
427 dollars(took),
428 dollars(spent)
429 );
430 }
431 format!("All of g1t, comped workspaces left out: money in against Cloudflare's bill under {floor:.0}% for {days} days running, as low as {worst:.1}%.")
432}
433
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily434pub(crate) fn anomalies(rows: &[(String, i64, i64)], factor: f64, floor_micros: i64) -> Vec<(String, i64, i64)> {
435 let mut out: Vec<(String, i64, i64)> = rows
436 .iter()
437 .filter(|(_, cost, revenue)| *cost >= floor_micros && *cost as f64 > *revenue as f64 * factor)
438 .cloned()
439 .collect();
440 out.sort_by(|a, b| (b.1 - b.2).cmp(&(a.1 - a.2)).then(a.0.cmp(&b.0)));
441 out
442}
443
444/// Cloudflare's marginal rate for one of its units: the median over the
445/// charged days of cost over quantity, in dollars. None while the included
446/// amounts still cover it. Each item is a day's (quantity, cost).
447pub(crate) fn billed_rate(days: &[(f64, f64)]) -> Option<f64> {
448 let mut rates: Vec<f64> = days.iter().filter(|(q, c)| *q > 0.0 && *c > 0.0).map(|(q, c)| c / q).collect();
449 if rates.is_empty() {
450 return None;
451 }
452 rates.sort_by(f64::total_cmp);
453 Some(rates[rates.len() / 2])
454}
455
456/// What one of g1t's units costs, from Cloudflare's rate per its own unit
457/// and how many of Cloudflare's units each of g1t's took: if Cloudflare
458/// counts three operations for every git operation g1t counts, a git
459/// operation costs three of Cloudflare's. None without enough of g1t's
460/// units to say.
461pub(crate) fn derived_unit_cost(rate_per_cf_unit: f64, cf_units: f64, own_units: f64) -> Option<f64> {
462 (own_units >= MIN_UNITS && cf_units > 0.0 && rate_per_cf_unit > 0.0).then(|| rate_per_cf_unit * cf_units / own_units)
463}
464
465/// How many units a price is per: `1,000 operations` → 1,000, `million
466/// requests` → 1,000,000, `second` → 1.
467pub(crate) fn unit_size(unit: &str) -> f64 {
468 let first = unit.split_whitespace().next().unwrap_or_default().replace(',', "");
469 match first.as_str() {
470 "million" => 1_000_000.0,
471 "thousand" => 1_000.0,
472 n => n.parse().unwrap_or(1.0),
473 }
474}
475
476fn day_before(day: &str, days: u64) -> String {
477 let ms = g1t_contracts::time::parse_rfc3339(&format!("{day}T00:00:00Z")).unwrap_or(0);
478 rfc3339(ms.saturating_sub(days * DAY_MS))[..10].to_owned()
479}
480
481/// Dollars to the cent from a dollar up, finer below: `$17.02`, `$0.063`.
482fn dollars(micros: i64) -> String {
483 if micros.abs() >= 1_000_000 {
484 let cents = (micros as f64 / 10_000.0).round() as i64;
485 format!("{}${}.{:02}", if cents < 0 { "-" } else { "" }, cents.abs() / 100, cents.abs() % 100)
486 } else {
487 crate::features::dollars(micros)
488 }
489}
490
491/// The days a plan payment is spread over.
492const PLAN_DAYS: u64 = 30;
493
494/// `micros` paid on `day` spread evenly over `days` days from it, in
495/// whole micros that add up to it (the first days take the remainder).
496pub(crate) fn spread(day: &str, micros: i64, days: u64) -> Vec<(String, i64)> {
497 if micros <= 0 || days == 0 {
498 return Vec::new();
499 }
500 let start = g1t_contracts::time::parse_rfc3339(&format!("{}T00:00:00Z", &day[..10.min(day.len())])).unwrap_or(0);
501 let each = micros / days as i64;
502 let rest = micros % days as i64;
503 (0..days)
504 .map(|n| (rfc3339(start + n * DAY_MS)[..10].to_owned(), each + i64::from((n as i64) < rest)))
505 .collect()
506}
507
508// ---------------------------------------------------------------------
509// The daily run, and what sudo reads.
510// ---------------------------------------------------------------------
511
512#[derive(Serialize)]
513struct Mail<'a> {
514 to: &'a str,
515 from: &'a str,
516 subject: &'a str,
517 text: String,
518 html: String,
519}
520
521fn escape(text: &str) -> String {
522 text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
523}
524
525/// Emails staff through Cloudflare Email Sending, the `EMAIL` binding.
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays526pub(crate) async fn email_staff(env: &Env, to: &str, subject: &str, lines: &[String]) -> Result<()> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily527 let link = "https://sudo.g1t.sh/costs";
528 let text = format!("{}\n\nCosts & margin: {link}\n\nSent by g1t-billing's margin guard (COSTS_ALERT_EMAIL).\n", lines.join("\n\n"));
529 let mut html = String::from("<div style=\"font-family:system-ui,sans-serif;max-width:560px;margin:0 auto;padding:24px 16px;color:#16150f\">");
530 for line in lines {
531 html.push_str(&format!("<p style=\"font-size:15px;line-height:1.6\">{}</p>", escape(line)));
532 }
533 html.push_str(&format!(
534 "<p><a href=\"{link}\">Open Costs &amp; margin in sudo</a></p><p style=\"font-size:13px;color:#6e6a5e\">Sent by g1t-billing's margin guard (COSTS_ALERT_EMAIL).</p></div>"
535 ));
536 let mail = Mail { to, from: "g1t <noreply@g1t.sh>", subject, text, html };
537 let binding = g1t_kit::js::binding(env, "EMAIL")?;
538 g1t_kit::js::call(&binding, "send", &[g1t_kit::js::to_js(&mail)?]).await?;
539 Ok(())
540}
541
542#[derive(Deserialize)]
543struct AlertRow {
544 id: String,
545 kind: String,
546 subject: String,
547 detail: String,
548 since: String,
549 opened_at: String,
550 emailed_at: Option<String>,
551}
552
553impl From<AlertRow> for MarginAlert {
554 fn from(r: AlertRow) -> Self {
555 MarginAlert { id: r.id, kind: r.kind, subject: r.subject, detail: r.detail, since: r.since, opened_at: r.opened_at, emailed_at: r.emailed_at }
556 }
557}
558
559#[derive(Deserialize)]
560struct MarginRow {
561 day: String,
562 bucket: String,
563 cf_cost_micros: i64,
564 own_cost_micros: i64,
565 value_micros: i64,
566 cash_micros: i64,
567 cf_quantity: f64,
568 own_quantity: f64,
569}
570
571impl From<MarginRow> for ProductDay {
572 fn from(r: MarginRow) -> Self {
573 ProductDay {
574 day: r.day,
575 bucket: r.bucket,
576 cf_cost_micros: r.cf_cost_micros,
577 own_cost_micros: r.own_cost_micros,
578 value_micros: r.value_micros,
579 cash_micros: r.cash_micros,
580 cf_quantity: r.cf_quantity,
581 own_quantity: r.own_quantity,
582 }
583 }
584}
585
586impl Billing {
587 /// The day's work: read Cloudflare's bill and g1t's own counts,
588 /// reconcile, look for drift, measure unit costs, apply prices whose
589 /// day has come, and raise or clear alerts.
590 pub(crate) async fn costs_daily(&self, env: &Env, keeper: &crate::keeper::Keeper) -> Result<CostsRun> {
591 let mut run = CostsRun::default();
592 let (since, until) = match self.read_cloudflare(keeper, &mut run.problems).await? {
593 Some((since, until, lines)) => {
594 run.lines = lines;
595 (since, until)
596 }
597 // Without the bill, still reconcile what g1t knows itself, over
598 // the same days the bill would be read for.
599 None => {
600 #[derive(Deserialize)]
601 struct Last {
602 day: Option<String>,
603 }
604 let last = self.db.prepare("SELECT MAX(day) AS day FROM margin_days").first::<Last>(None).await?.and_then(|l| l.day);
605 costs::window(last.as_deref(), now_ms())
606 }
607 };
608 if let Err(error) = self.count_own(&since, &until).await {
609 run.problems.push(format!("g1t's own counts could not be read: {error}"));
610 }
611 self.snapshot_pending(&until).await?;
Models' margin read -14%: usage nothing paid for is valued at price, not $0612 // Reconciled over the whole window sudo shows, not only the days the
613 // bill was read for: it reads only what is already kept, so a change
614 // in how a day is valued reaches every day shown at the next run.
615 let window = day_before(&until, costs::BACKFILL_DAYS - 1);
616 let reconcile_from = if window < since { window } else { since.clone() };
617 run.days = self.reconcile_range(&reconcile_from, &until).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily618 let drift = self.find_drift(&until).await?;
619 run.proposals = self.measure_units(&until).await?;
620 self.apply_due_versions().await?;
621 run.alerts = self.raise_alerts(env, &until, &drift).await?;
622 if let Some(identity) = &self.identity
623 && let Err(error) = self.tell_owners_of_rises(identity).await
624 {
625 run.problems.push(format!("owners could not be told of a price rise: {error}"));
626 }
627 for problem in &run.problems {
628 worker::console_warn!("costs: {problem}");
629 }
630 Ok(run)
631 }
632
633 /// What each month-end source had come to by the end of `day`.
634 async fn snapshot_pending(&self, day: &str) -> Result<()> {
635 self.db
636 .prepare(
637 "INSERT INTO pending_days (day, workspace, source, cost_micros, charge_micros)
638 SELECT ?1, workspace, source, COALESCE(cost_micros, 0), COALESCE(charge_micros, 0) FROM pending_usage WHERE month = ?2
639 ON CONFLICT (day, workspace, source) DO UPDATE SET cost_micros = excluded.cost_micros, charge_micros = excluded.charge_micros",
640 )
641 .bind(&[day.into(), day[..7].into()])?
642 .run()
643 .await?;
644 Ok(())
645 }
646
647 /// What customers were charged on the days, by workspace and key.
648 async fn usage_rows(&self, since: &str, until: &str) -> Result<Vec<UsageRow>> {
649 #[derive(Deserialize)]
650 struct Row {
651 day: String,
652 workspace: String,
653 key: String,
654 internal: i64,
655 own_provider: i64,
656 cash: Option<i64>,
657 drawn: Option<i64>,
658 cost: Option<i64>,
659 }
660 let charged_here = crate::storage::CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
661 let end = format!("{until}T23:59:59.999Z");
662 let rows = self
663 .db
664 .prepare(format!(
665 "SELECT substr(created_at, 1, 10) AS day, workspace,
666 CASE WHEN task = 'deployments' AND reference LIKE 'deploy/%' THEN 'builds' ELSE COALESCE(task, 'other') END AS key,
667 CASE WHEN workspace IN ({internal}) THEN 1 ELSE 0 END AS internal,
668 CASE WHEN billed_to = 'workspace' THEN 1 ELSE 0 END AS own_provider,
669 -SUM(amount_micros) AS cash,
670 SUM(COALESCE(credit_micros, 0) + COALESCE(trial_micros, 0) + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0)) AS drawn,
671 SUM(COALESCE(cost_micros, 0)) AS cost
672 FROM ledger
673 WHERE kind = 'usage' AND created_at >= ?1 AND created_at <= ?2 AND COALESCE(task, '') NOT IN ({charged_here})
674 GROUP BY 1, 2, 3, 4, 5",
675 internal = crate::sales::INTERNAL_SQL
676 ))
677 .bind(&[since.into(), end.as_str().into()])?
678 .all()
679 .await?
680 .results::<Row>()?;
681 let mut out: Vec<UsageRow> = rows
682 .into_iter()
683 .map(|r| {
684 // A workspace's own model provider was paid there: no cost
685 // to g1t. g1t's own workspaces are valued at price.
686 let cost = if r.own_provider == 1 { 0 } else { r.cost.unwrap_or(0) };
687 let cash = r.cash.unwrap_or(0);
Models' margin read -14%: usage nothing paid for is valued at price, not $0688 let paid = cash + r.drawn.unwrap_or(0);
689 let value = usage_value(r.internal == 1, cost, paid, self.margin_percent);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily690 UsageRow { day: r.day, workspace: r.workspace, key: r.key, value, cash, cost }
691 })
692 .collect();
693 // Month-end sources, from their daily snapshots.
694 #[derive(Deserialize)]
695 struct Snap {
696 day: String,
697 workspace: String,
698 source: String,
699 cost_micros: i64,
700 charge_micros: i64,
701 }
702 let snaps = self
703 .db
704 .prepare("SELECT day, workspace, source, cost_micros, charge_micros FROM pending_days WHERE day >= ?1 AND day <= ?2")
705 .bind(&[day_before(since, 1).into(), until.into()])?
706 .all()
707 .await?
708 .results::<Snap>()?
709 .into_iter()
710 .filter(|s| crate::storage::CHARGED_HERE.contains(&s.source.as_str()) || s.source == "domains")
711 .map(|s| (s.day, s.workspace, s.source, s.cost_micros, s.charge_micros))
712 .collect::<Vec<_>>();
713 out.extend(pending_deltas(&snaps).into_iter().filter(|u| u.day.as_str() >= since));
714 // The plan's price, spread over the 30 days it pays for, so a month's
715 // payment does not read as one very good day and 29 bad ones.
716 #[derive(Deserialize)]
717 struct Plan {
718 day: String,
719 workspace: String,
720 micros: Option<i64>,
721 }
722 let plans = self
723 .db
724 .prepare(
725 "SELECT substr(paid_at, 1, 10) AS day, workspace, SUM(amount_micros) AS micros FROM plan_payments
726 WHERE paid_at >= ?1 AND paid_at <= ?2 GROUP BY 1, 2",
727 )
728 .bind(&[day_before(since, PLAN_DAYS - 1).into(), end.as_str().into()])?
729 .all()
730 .await?
731 .results::<Plan>()?;
732 for p in plans {
733 for (day, micros) in spread(&p.day, p.micros.unwrap_or(0), PLAN_DAYS) {
734 if day.as_str() >= since && day.as_str() <= until {
735 out.push(UsageRow { day, workspace: p.workspace.clone(), key: "plan".into(), value: micros, cash: micros, cost: 0 });
736 }
737 }
738 }
739 Ok(out)
740 }
741
742 /// Reconciles the days and writes `margin_days` and `workspace_costs`.
743 async fn reconcile_range(&self, since: &str, until: &str) -> Result<u32> {
744 let rules = self.rules().await?;
745 #[derive(Deserialize)]
746 struct Map {
747 key: String,
748 bucket: String,
749 }
750 let revenue_map: BTreeMap<String, String> = self
751 .db
752 .prepare("SELECT key, bucket FROM revenue_map")
753 .all()
754 .await?
755 .results::<Map>()?
756 .into_iter()
757 .map(|m| (m.key, m.bucket))
758 .collect();
759 let lines = self
760 .db
761 .prepare("SELECT day, source, product, meter, quantity, cost_usd FROM cost_lines WHERE day >= ?1 AND day <= ?2")
762 .bind(&[since.into(), until.into()])?
763 .all()
764 .await?
765 .results::<LineRow>()?;
766 let own = self
767 .db
768 .prepare("SELECT day, meter, workspace, quantity FROM own_counts WHERE day >= ?1 AND day <= ?2")
769 .bind(&[since.into(), until.into()])?
770 .all()
771 .await?
772 .results::<OwnRow>()?;
773 let usage = self.usage_rows(since, until).await?;
774 let (days, workspaces) = fold(&rules, &revenue_map, &lines, &own, &usage);
775 let now = rfc3339(now_ms());
776 self.db
777 .batch(vec![
778 self.db.prepare("DELETE FROM margin_days WHERE day >= ?1 AND day <= ?2").bind(&[since.into(), until.into()])?,
779 self.db.prepare("DELETE FROM workspace_costs WHERE day >= ?1 AND day <= ?2").bind(&[since.into(), until.into()])?,
780 ])
781 .await?;
782 for chunk in days.chunks(50) {
783 let mut statements = Vec::with_capacity(chunk.len());
784 for d in chunk {
785 statements.push(
786 self.db
787 .prepare(
788 "INSERT OR REPLACE INTO margin_days (day, bucket, cf_cost_micros, own_cost_micros, value_micros, cash_micros, cf_quantity, own_quantity, computed_at)
789 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
790 )
791 .bind(&[
792 d.day.as_str().into(),
793 d.bucket.as_str().into(),
794 (d.cf_cost_micros as f64).into(),
795 (d.own_cost_micros as f64).into(),
796 (d.value_micros as f64).into(),
797 (d.cash_micros as f64).into(),
798 d.cf_quantity.into(),
799 d.own_quantity.into(),
800 now.as_str().into(),
801 ])?,
802 );
803 }
804 self.db.batch(statements).await?;
805 }
806 for chunk in workspaces.chunks(50) {
807 let mut statements = Vec::with_capacity(chunk.len());
808 for w in chunk {
809 statements.push(
810 self.db
Margin alerts measure what is sold, and say dollars when a percentage would mislead811 .prepare("INSERT OR REPLACE INTO workspace_costs (day, workspace, bucket, cost_micros, revenue_micros, value_micros) VALUES (?, ?, ?, ?, ?, ?)")
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily812 .bind(&[
813 w.day.as_str().into(),
814 w.workspace.as_str().into(),
815 w.bucket.as_str().into(),
816 (w.cost as f64).into(),
817 (w.revenue as f64).into(),
Margin alerts measure what is sold, and say dollars when a percentage would mislead818 (w.value as f64).into(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily819 ])?,
820 );
821 }
822 self.db.batch(statements).await?;
823 }
824 Ok(costs::days_between(since, until).len() as u32)
825 }
826
827 async fn margin_days(&self, since: &str, until: &str) -> Result<Vec<ProductDay>> {
828 Ok(self
829 .db
830 .prepare("SELECT * FROM margin_days WHERE day >= ?1 AND day <= ?2 ORDER BY day, bucket")
831 .bind(&[since.into(), until.into()])?
832 .all()
833 .await?
834 .results::<MarginRow>()?
835 .into_iter()
836 .map(ProductDay::from)
837 .collect())
838 }
839
840 /// Drift over the last week, written to `cost_drift` (replacing the
841 /// last run's), with unmapped Cloudflare meters as leaks.
842 async fn find_drift(&self, until: &str) -> Result<Vec<(Drift, String)>> {
843 let since = day_before(until, DRIFT_DAYS - 1);
844 let settings = self.cost_settings().await?;
845 let rules = self.rules().await?;
846 let days = self.margin_days(&since, until).await?;
847 let mut by: BTreeMap<String, Vec<ProductDay>> = BTreeMap::new();
848 for d in days {
849 by.entry(d.bucket.clone()).or_default().push(d);
850 }
851 let mut found = Vec::new();
852 for (bucket, days) in &by {
853 let bucket_rules: Vec<&Rule> = rules.iter().filter(|r| &r.bucket == bucket).collect();
854 let threshold = bucket_rules.iter().map(|r| r.drift_percent).fold(f64::INFINITY, f64::min);
855 let threshold = if threshold.is_finite() { threshold } else { 10.0 };
856 let counted = bucket_rules.iter().any(|r| r.own_meter.is_some());
857 for drift in drifts(bucket, days, threshold, counted, settings.min_daily_cost_micros) {
858 let title = costs::bucket_title(bucket);
859 let detail = match drift.kind {
860 DriftKind::Count => format!(
One operation mapping, owned by repos; billing reads it instead of keeping its own861 "{title}: g1t counted {}, Cloudflare {} over the last {DRIFT_DAYS} days ({:+.1}%). Customers are charged for what g1t counts; check what Cloudflare counts as a unit and change the repos service's operation_mapping (set_operation_mapping).",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily862 crate::features::thousands(drift.ours.max(0.0).round() as u64),
863 crate::features::thousands(drift.cloudflare.max(0.0).round() as u64),
864 drift.delta_percent.unwrap_or(0.0)
865 ),
866 DriftKind::Cost => format!(
867 "{title}: Cloudflare charged {} over the last {DRIFT_DAYS} days; the price book's cost of the same usage is {} ({:+.1}%). A price may be stale: see the proposals.",
868 dollars(drift.cloudflare as i64),
869 dollars(drift.ours as i64),
870 drift.delta_percent.unwrap_or(0.0)
871 ),
872 DriftKind::Leak if bucket == UNMAPPED => {
873 format!("Cloudflare charged {} for meters no mapping claims. Map them on Costs & margin.", dollars(drift.cloudflare as i64))
874 }
875 DriftKind::Leak => format!(
876 "{title}: Cloudflare charged {} over the last {DRIFT_DAYS} days and customers were charged nothing for it.",
877 dollars(drift.cloudflare as i64)
878 ),
879 };
880 found.push((drift, detail));
881 }
882 }
883 let now = rfc3339(now_ms());
884 let mut statements = vec![self.db.prepare("DELETE FROM cost_drift")];
885 for (drift, detail) in &found {
886 statements.push(
887 self.db
888 .prepare("INSERT OR REPLACE INTO cost_drift (bucket, kind, ours, cloudflare, delta_percent, detail, found_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
889 .bind(&[
890 drift.bucket.as_str().into(),
891 drift.kind.as_str().into(),
892 drift.ours.into(),
893 drift.cloudflare.into(),
894 drift.delta_percent.map_or(JsValue::NULL, JsValue::from),
895 detail.as_str().into(),
896 now.as_str().into(),
897 ])?,
898 );
899 }
900 self.db.batch(statements).await?;
901 Ok(found)
902 }
903
904 /// Unit costs from the bill for mappings that scale to g1t's own count
905 /// (git operations), proposed to the price book.
906 async fn measure_units(&self, until: &str) -> Result<u32> {
907 #[derive(Deserialize)]
908 struct Scaled {
909 product: String,
910 meter: String,
911 price_meter: String,
912 own_meter: String,
913 unit: Option<String>,
914 }
915 let scaled = self
916 .db
917 .prepare(
918 "SELECT m.product, m.meter, m.price_meter, m.own_meter, p.unit FROM cost_map m LEFT JOIN prices p ON p.meter = m.price_meter
919 WHERE m.scale_to_own = 1 AND m.price_meter IS NOT NULL AND m.own_meter IS NOT NULL",
920 )
921 .all()
922 .await?
923 .results::<Scaled>()?;
924 let since = day_before(until, MEASURE_DAYS - 1);
925 let rules = self.rules().await?;
926 let mut proposed = 0;
927 for s in scaled {
928 #[derive(Deserialize)]
929 struct Day {
930 product: String,
931 meter: String,
932 quantity: f64,
933 cost_usd: f64,
934 }
935 let lines = self
936 .db
937 .prepare("SELECT product, meter, quantity, cost_usd FROM cost_lines WHERE source = ?1 AND product = ?2 AND day >= ?3 AND day <= ?4")
938 .bind(&[SOURCE_BILLABLE.into(), s.product.as_str().into(), since.as_str().into(), until.into()])?
939 .all()
940 .await?
941 .results::<Day>()?;
942 // Only the lines this very mapping claims.
943 let mine: Vec<(f64, f64)> = lines
944 .iter()
945 .filter(|l| costs::classify(&rules, &l.product, &l.meter).is_some_and(|r| r.product == s.product && r.meter == s.meter))
946 .map(|l| (l.quantity, l.cost_usd))
947 .collect();
948 let Some(rate) = billed_rate(&mine) else { continue };
949 let cf_units: f64 = mine.iter().map(|(q, _)| q).sum();
950 #[derive(Deserialize)]
951 struct Own {
952 total: Option<f64>,
953 }
954 let own_units = self
955 .db
956 .prepare("SELECT SUM(quantity) AS total FROM own_counts WHERE meter = ?1 AND day >= ?2 AND day <= ?3")
957 .bind(&[s.own_meter.as_str().into(), since.as_str().into(), until.into()])?
958 .first::<Own>(None)
959 .await?
960 .and_then(|o| o.total)
961 .unwrap_or(0.0);
962 let Some(per_unit) = derived_unit_cost(rate, cf_units, own_units) else { continue };
963 let size = unit_size(s.unit.as_deref().unwrap_or("1"));
964 let measured = per_unit * size * 1_000_000.0;
965 let reason = format!(
966 "Cloudflare billed ${:.4} per 1,000 of its units and counted {:.2} of them for each one g1t counted over the last {MEASURE_DAYS} days ({} against {})",
967 rate * 1000.0,
968 cf_units / own_units,
969 crate::features::thousands(cf_units.round() as u64),
970 crate::features::thousands(own_units.round() as u64)
971 );
972 if self.propose(&s.price_meter, measured, &reason, "reconciler").await?.is_some() {
973 proposed += 1;
974 }
975 }
976 Ok(proposed)
977 }
978
979 /// Opens, updates and closes margin alerts, and emails staff about new
980 /// ones (and open ones each week).
981 async fn raise_alerts(&self, env: &Env, until: &str, drift: &[(Drift, String)]) -> Result<u32> {
982 let settings = self.cost_settings().await?;
983 let since = day_before(until, u64::from(settings.alert_days.max(1)) - 1);
984 let days = self.margin_days(&since, until).await?;
985 let mut conditions: Vec<(String, String, String, String)> = Vec::new();
986 // Each product under the floor.
987 let mut by: BTreeMap<String, Vec<(String, i64, i64)>> = BTreeMap::new();
988 let mut all: BTreeMap<String, (i64, i64)> = BTreeMap::new();
989 for d in &days {
990 let overall = all.entry(d.day.clone()).or_default();
991 overall.0 += d.cash_micros;
992 overall.1 += d.cost();
993 if !OVERHEAD.contains(&d.bucket.as_str()) && d.bucket != UNMAPPED {
994 by.entry(d.bucket.clone()).or_default().push((d.day.clone(), d.value_micros, d.cost()));
995 }
996 }
997 let floor = settings.margin_floor_percent;
998 let n = settings.alert_days as usize;
999 for (bucket, series) in &by {
1000 if let Some((from, worst)) = breach(series, floor, n, settings.min_daily_cost_micros) {
1001 conditions.push((
1002 "margin".into(),
1003 bucket.clone(),
1004 format!("{}: margin under {floor:.0}% for {n} days running, as low as {worst:.1}%.", costs::bucket_title(bucket)),
1005 from,
1006 ));
1007 }
1008 }
Margin alerts measure what is sold, and say dollars when a percentage would mislead1009 // Comped workspaces' share is a budget g1t chose to spend, watched
1010 // on its own (budget.rs): not part of whether what is sold pays.
1011 for (day, cost) in self.comped_costs(&since, until).await? {
1012 if let Some(overall) = all.get_mut(&day) {
1013 overall.1 = (overall.1 - cost).max(0);
1014 }
1015 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1016 let series: Vec<(String, i64, i64)> = all.into_iter().map(|(day, (revenue, cost))| (day, revenue, cost)).collect();
1017 if let Some((from, worst)) = breach(&series, floor, n, settings.min_daily_cost_micros) {
Margin alerts measure what is sold, and say dollars when a percentage would mislead1018 let tail = &series[series.len().saturating_sub(n)..];
1019 let (took, spent) = tail.iter().fold((0i64, 0i64), |(r, c), (_, revenue, cost)| (r + revenue, c + cost));
1020 conditions.push(("overall".into(), "g1t".into(), overall_detail(took, spent, n, floor, worst), from));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1021 }
1022 for (d, detail) in drift {
1023 let kind = if d.kind == DriftKind::Leak { "leak" } else { "drift" };
1024 conditions.push((kind.into(), format!("{}:{}", d.bucket, d.kind.as_str()), detail.clone(), until.to_owned()));
1025 }
1026 // Workspaces costing more than they pay.
1027 for (workspace, cost, revenue) in self.workspace_anomalies(until, &settings).await? {
1028 conditions.push((
1029 "workspace".into(),
1030 workspace.clone(),
Margin alerts measure what is sold, and say dollars when a percentage would mislead1031 format!(
1032 "{workspace} cost g1t {} on Cloudflare over {ANOMALY_DAYS} days, and its usage was priced at {}: its prices are below cost.",
1033 dollars(cost),
1034 dollars(revenue)
1035 ),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1036 day_before(until, ANOMALY_DAYS - 1),
1037 ));
1038 }
1039
1040 let open = self
1041 .db
1042 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL")
1043 .all()
1044 .await?
1045 .results::<AlertRow>()?;
1046 let now = now_ms();
1047 let stamp = rfc3339(now);
1048 let mut to_email: Vec<String> = Vec::new();
1049 let mut kept: BTreeSet<String> = BTreeSet::new();
1050 for (kind, subject, detail, from) in &conditions {
1051 match open.iter().find(|a| &a.kind == kind && &a.subject == subject) {
1052 Some(alert) => {
1053 kept.insert(alert.id.clone());
1054 self.db
1055 .prepare("UPDATE margin_alerts SET detail = ? WHERE id = ?")
1056 .bind(&[detail.as_str().into(), alert.id.as_str().into()])?
1057 .run()
1058 .await?;
1059 let stale = alert
1060 .emailed_at
1061 .as_deref()
1062 .and_then(g1t_contracts::time::parse_rfc3339)
1063 .is_none_or(|at| now.saturating_sub(at) >= REMIND_MS);
1064 if stale && kind != "workspace" {
1065 to_email.push(format!("Still open: {detail}"));
1066 kept.insert(format!("email:{}", alert.id));
1067 }
1068 }
1069 None => {
1070 let id = new_id("mal", now);
1071 self.db
1072 .prepare("INSERT INTO margin_alerts (id, kind, subject, detail, since, opened_at) VALUES (?, ?, ?, ?, ?, ?)")
1073 .bind(&[id.as_str().into(), kind.as_str().into(), subject.as_str().into(), detail.as_str().into(), from.as_str().into(), stamp.as_str().into()])?
1074 .run()
1075 .await?;
1076 kept.insert(id.clone());
Margin alerts measure what is sold, and say dollars when a percentage would mislead1077 // A workspace's is for Reach out, not the inbox.
1078 if kind != "workspace" {
1079 to_email.push(detail.clone());
1080 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1081 kept.insert(format!("email:{id}"));
1082 }
1083 }
1084 }
1085 for alert in &open {
1086 if !kept.contains(&alert.id) {
1087 self.db
1088 .prepare("UPDATE margin_alerts SET resolved_at = ? WHERE id = ?")
1089 .bind(&[stamp.as_str().into(), alert.id.as_str().into()])?
1090 .run()
1091 .await?;
1092 }
1093 }
1094 let to = env.var("COSTS_ALERT_EMAIL").map(|v| v.to_string()).unwrap_or_default();
1095 if !to_email.is_empty() && !to.trim().is_empty() {
1096 let subject = format!("[g1t costs] {} margin alert{}", to_email.len(), if to_email.len() == 1 { "" } else { "s" });
1097 match email_staff(env, to.trim(), &subject, &to_email).await {
1098 Ok(()) => {
1099 for marker in kept.iter().filter_map(|k| k.strip_prefix("email:")) {
1100 self.db
1101 .prepare("UPDATE margin_alerts SET emailed_at = ? WHERE id = ?")
1102 .bind(&[stamp.as_str().into(), marker.into()])?
1103 .run()
1104 .await?;
1105 }
1106 }
1107 Err(error) => worker::console_error!("could not email the margin alerts: {error}"),
1108 }
1109 }
1110 Ok(conditions.len() as u32)
1111 }
1112
1113 /// Workspaces costing g1t more than they pay over 30 days, not g1t's own.
Margin alerts measure what is sold, and say dollars when a percentage would mislead1114 /// Each day's cost shared out to comped workspaces.
1115 async fn comped_costs(&self, since: &str, until: &str) -> Result<Vec<(String, i64)>> {
1116 #[derive(Deserialize)]
1117 struct Row {
1118 day: String,
1119 cost: Option<i64>,
1120 }
1121 Ok(self
1122 .db
1123 .prepare(format!(
1124 "SELECT day, SUM(cost_micros) AS cost FROM workspace_costs
1125 WHERE day >= ?1 AND day <= ?2 AND workspace IN ({}) GROUP BY day",
1126 crate::sales::INTERNAL_SQL
1127 ))
1128 .bind(&[since.into(), until.into()])?
1129 .all()
1130 .await?
1131 .results::<Row>()?
1132 .into_iter()
1133 .map(|r| (r.day, r.cost.unwrap_or(0)))
1134 .collect())
1135 }
1136
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1137 async fn workspace_anomalies(&self, until: &str, settings: &CostSettings) -> Result<Vec<(String, i64, i64)>> {
1138 #[derive(Deserialize)]
1139 struct Row {
1140 workspace: String,
1141 cost: Option<i64>,
1142 revenue: Option<i64>,
1143 }
1144 let rows = self
1145 .db
1146 .prepare(format!(
Margin alerts measure what is sold, and say dollars when a percentage would mislead1147 // Against what its usage was priced at, not the cash it
1148 // paid: a trial or a gift paying for usage is not a price
1149 // below cost.
The workspace cost alert compares only days that carry their value, not the days before it was kept1150 // Days from before value_micros was kept have none: only days
1151 // since the first one that does are compared.
Margin alerts measure what is sold, and say dollars when a percentage would mislead1152 "SELECT workspace, SUM(cost_micros) AS cost, SUM(value_micros) AS revenue FROM workspace_costs
The workspace cost alert compares only days that carry their value, not the days before it was kept1153 WHERE day >= ?1 AND day <= ?2 AND workspace NOT IN ({})
1154 AND day >= (SELECT MIN(day) FROM workspace_costs WHERE value_micros > 0)
1155 GROUP BY workspace",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1156 crate::sales::INTERNAL_SQL
1157 ))
1158 .bind(&[day_before(until, ANOMALY_DAYS - 1).into(), until.into()])?
1159 .all()
1160 .await?
1161 .results::<Row>()?;
1162 let rows: Vec<(String, i64, i64)> = rows.into_iter().map(|r| (r.workspace, r.cost.unwrap_or(0), r.revenue.unwrap_or(0))).collect();
1163 Ok(anomalies(&rows, settings.anomaly_factor, settings.anomaly_floor_micros))
1164 }
1165
1166 /// For Reach out: workspaces with an open cost-over-revenue alert,
1167 /// each with its detail and cost.
1168 pub(crate) async fn costing_more_than_they_pay(&self) -> Result<Vec<(String, String, i64)>> {
1169 let alerts = self
1170 .db
1171 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL AND kind = 'workspace' ORDER BY opened_at DESC LIMIT 50")
1172 .all()
1173 .await?
1174 .results::<AlertRow>()?;
1175 let mut out = Vec::new();
1176 for alert in alerts {
1177 #[derive(Deserialize)]
1178 struct Cost {
1179 cost: Option<i64>,
1180 }
1181 let cost = self
1182 .db
1183 .prepare("SELECT SUM(cost_micros) AS cost FROM workspace_costs WHERE workspace = ? AND day >= ?")
1184 .bind(&[alert.subject.as_str().into(), alert.since.as_str().into()])?
1185 .first::<Cost>(None)
1186 .await?
1187 .and_then(|c| c.cost)
1188 .unwrap_or(0);
1189 out.push((alert.subject, alert.detail, cost));
1190 }
1191 Ok(out)
1192 }
1193
1194 /// `admin_cost_alerts`: what sudo's banner says.
1195 pub(crate) async fn admin_cost_alerts(&self, _: AdminCostAlertsArgs) -> Result<Vec<MarginAlert>> {
1196 Ok(self
1197 .db
1198 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL ORDER BY opened_at DESC LIMIT 50")
1199 .all()
1200 .await?
1201 .results::<AlertRow>()?
1202 .into_iter()
1203 .map(MarginAlert::from)
1204 .collect())
1205 }
1206
1207 /// `admin_run_costs`: the daily run, now.
1208 pub(crate) async fn admin_run_costs(&self, env: &Env, a: AdminRunCostsArgs) -> Result<Outcome<CostsRun>> {
1209 let keeper = crate::keeper::Keeper::from_env(env);
1210 let run = self.costs_daily(env, &keeper).await?;
1211 if !a.by.is_empty() {
1212 self.audit(
1213 "costs",
1214 "costs_run",
1215 &format!("{} lines, {} days, {} proposals, {} alerts", run.lines, run.days, run.proposals, run.alerts),
1216 &a.by,
1217 )
1218 .await?;
1219 }
1220 Ok(Outcome::Ok(run))
1221 }
1222
1223 /// `admin_set_cost_mapping`.
1224 pub(crate) async fn admin_set_cost_mapping(&self, a: AdminSetCostMappingArgs) -> Result<Outcome<CostMapping>> {
1225 let product = costs::slug(&a.product);
1226 let meter = if a.meter.trim() == "*" { "*".to_owned() } else { costs::slug(&a.meter) };
1227 if product.is_empty() || meter.is_empty() {
1228 return Ok(Outcome::fail(FailureCode::Invalid, "Name Cloudflare's product and a meter (or * for all of it)."));
1229 }
1230 let now = rfc3339(now_ms());
1231 if a.remove {
1232 self.db
1233 .prepare("DELETE FROM cost_map WHERE product = ? AND meter = ?")
1234 .bind(&[product.as_str().into(), meter.as_str().into()])?
1235 .run()
1236 .await?;
1237 self.audit("costs", "cost_mapping_removed", &format!("{product}/{meter}"), &a.by).await?;
1238 return Ok(Outcome::Ok(CostMapping {
1239 product,
1240 meter,
1241 bucket: String::new(),
1242 price_meter: None,
1243 own_meter: None,
1244 scale_to_own: false,
1245 drift_percent: 0.0,
1246 note: String::new(),
1247 updated_at: now,
1248 updated_by: a.by,
1249 }));
1250 }
1251 let bucket = costs::slug(&a.bucket);
1252 if bucket.is_empty() {
1253 return Ok(Outcome::fail(FailureCode::Invalid, "Say which of g1t's products it is a cost of."));
1254 }
1255 let clean = |v: Option<String>| v.map(|v| v.trim().to_owned()).filter(|v| !v.is_empty());
1256 let (price_meter, own_meter) = (clean(a.price_meter), clean(a.own_meter));
1257 let drift = a.drift_percent.filter(|d| d.is_finite() && *d > 0.0).unwrap_or(10.0);
1258 self.db
1259 .prepare(
1260 "INSERT INTO cost_map (product, meter, bucket, price_meter, own_meter, scale_to_own, drift_percent, note, updated_at, updated_by)
1261 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1262 ON CONFLICT (product, meter) DO UPDATE SET bucket = ?3, price_meter = ?4, own_meter = ?5, scale_to_own = ?6,
1263 drift_percent = ?7, note = ?8, updated_at = ?9, updated_by = ?10",
1264 )
1265 .bind(&[
1266 product.as_str().into(),
1267 meter.as_str().into(),
1268 bucket.as_str().into(),
1269 crate::optional(price_meter.as_deref()),
1270 crate::optional(own_meter.as_deref()),
1271 i32::from(a.scale_to_own).into(),
1272 drift.into(),
1273 a.note.trim().into(),
1274 now.as_str().into(),
1275 a.by.as_str().into(),
1276 ])?
1277 .run()
1278 .await?;
1279 self.audit("costs", "cost_mapping", &format!("{product}/{meter} → {bucket}"), &a.by).await?;
1280 Ok(Outcome::Ok(CostMapping {
1281 product,
1282 meter,
1283 bucket,
1284 price_meter,
1285 own_meter,
1286 scale_to_own: a.scale_to_own,
1287 drift_percent: drift,
1288 note: a.note.trim().to_owned(),
1289 updated_at: now,
1290 updated_by: a.by,
1291 }))
1292 }
1293
1294 /// `admin_costs`: the Costs & margin page.
1295 pub(crate) async fn admin_costs(&self, a: AdminCostsArgs, configured: bool) -> Result<CostsReport> {
1296 let until = rfc3339(now_ms())[..10].to_owned();
1297 let span = u64::from(a.days.unwrap_or(30).clamp(7, 90));
1298 let since = day_before(&until, span - 1);
1299 let days = self.margin_days(&since, &until).await?;
1300 let rules = self.rules().await?;
1301
1302 let mut products: BTreeMap<String, ProductMargin> = BTreeMap::new();
1303 let mut overall = OverallMargin::default();
1304 for d in &days {
1305 let p = products.entry(d.bucket.clone()).or_insert_with(|| ProductMargin {
1306 bucket: d.bucket.clone(),
1307 title: costs::bucket_title(&d.bucket),
1308 cost_source: if NOT_CLOUDFLARE.contains(&d.bucket.as_str()) { "ledger" } else { "cloudflare" }.into(),
1309 overhead: OVERHEAD.contains(&d.bucket.as_str()),
1310 ..ProductMargin::default()
1311 });
1312 p.cf_cost_micros += d.cf_cost_micros;
1313 p.own_cost_micros += d.own_cost_micros;
1314 p.value_micros += d.value_micros;
1315 p.cost_micros += d.cost();
1316 overall.cost_micros += d.cost();
1317 if d.bucket == "platform" {
1318 overall.plans_micros += d.cash_micros;
1319 } else {
1320 overall.usage_micros += d.cash_micros;
1321 }
1322 }
1323 for p in products.values_mut() {
1324 p.margin_micros = p.value_micros - p.cost_micros;
1325 p.margin_percent = margin_percent(p.value_micros, p.cost_micros);
1326 }
1327 let revenue = overall.usage_micros + overall.plans_micros;
1328 overall.margin_micros = revenue - overall.cost_micros;
1329 overall.margin_percent = margin_percent(revenue, overall.cost_micros);
1330 let mut products: Vec<ProductMargin> = products.into_values().collect();
1331 products.sort_by_key(|p| std::cmp::Reverse(p.cost_micros.max(p.value_micros)));
1332
1333 #[derive(Deserialize)]
1334 struct DriftRow {
1335 bucket: String,
1336 kind: String,
1337 ours: f64,
1338 cloudflare: f64,
1339 delta_percent: Option<f64>,
1340 detail: String,
1341 found_at: String,
1342 }
1343 let drift = self
1344 .db
1345 .prepare("SELECT * FROM cost_drift ORDER BY kind, bucket")
1346 .all()
1347 .await?
1348 .results::<DriftRow>()?
1349 .into_iter()
1350 .map(|r| CostDrift {
1351 title: costs::bucket_title(&r.bucket),
1352 bucket: r.bucket,
1353 kind: r.kind,
1354 ours: r.ours,
1355 cloudflare: r.cloudflare,
1356 delta_percent: r.delta_percent,
1357 detail: r.detail,
1358 found_at: r.found_at,
1359 })
1360 .collect();
1361
1362 #[derive(Deserialize)]
1363 struct Top {
1364 workspace: String,
1365 cost: Option<i64>,
1366 revenue: Option<i64>,
1367 internal: i64,
1368 }
1369 let top_workspaces = self
1370 .db
1371 .prepare(format!(
1372 "SELECT workspace, SUM(cost_micros) AS cost, SUM(revenue_micros) AS revenue,
1373 CASE WHEN workspace IN ({}) THEN 1 ELSE 0 END AS internal
1374 FROM workspace_costs WHERE day >= ?1 AND day <= ?2 GROUP BY workspace ORDER BY cost DESC LIMIT 15",
1375 crate::sales::INTERNAL_SQL
1376 ))
1377 .bind(&[since.as_str().into(), until.as_str().into()])?
1378 .all()
1379 .await?
1380 .results::<Top>()?
1381 .into_iter()
1382 .map(|t| WorkspaceCost { workspace: t.workspace, cost_micros: t.cost.unwrap_or(0), revenue_micros: t.revenue.unwrap_or(0), internal: t.internal == 1 })
1383 .collect();
1384
1385 #[derive(Deserialize)]
1386 struct Summary {
1387 source: String,
1388 product: String,
1389 meter: String,
1390 raw_name: String,
1391 unit: String,
1392 quantity: f64,
1393 cost_usd: f64,
1394 }
1395 let lines = self
1396 .db
1397 .prepare(
1398 "SELECT source, product, meter, MAX(raw_name) AS raw_name, MAX(unit) AS unit, SUM(quantity) AS quantity, SUM(cost_usd) AS cost_usd
1399 FROM cost_lines WHERE day >= ?1 AND day <= ?2 GROUP BY source, product, meter ORDER BY cost_usd DESC, product, meter LIMIT 200",
1400 )
1401 .bind(&[since.as_str().into(), until.as_str().into()])?
1402 .all()
1403 .await?
1404 .results::<Summary>()?
1405 .into_iter()
1406 .map(|l| CostLineSummary {
1407 bucket: costs::classify(&rules, &l.product, &l.meter).map(|r| r.bucket.clone()),
1408 product: l.product,
1409 meter: l.meter,
1410 raw_name: l.raw_name,
1411 unit: l.unit,
1412 source: l.source,
1413 quantity: l.quantity,
1414 cost_micros: micros(l.cost_usd),
1415 })
1416 .collect();
1417
1418 #[derive(Deserialize)]
1419 struct MapRow {
1420 product: String,
1421 meter: String,
1422 bucket: String,
1423 price_meter: Option<String>,
1424 own_meter: Option<String>,
1425 scale_to_own: i64,
1426 drift_percent: f64,
1427 note: String,
1428 updated_at: String,
1429 updated_by: String,
1430 }
1431 let mappings = self
1432 .db
1433 .prepare("SELECT * FROM cost_map ORDER BY product, meter")
1434 .all()
1435 .await?
1436 .results::<MapRow>()?
1437 .into_iter()
1438 .map(|m| CostMapping {
1439 product: m.product,
1440 meter: m.meter,
1441 bucket: m.bucket,
1442 price_meter: m.price_meter,
1443 own_meter: m.own_meter,
1444 scale_to_own: m.scale_to_own == 1,
1445 drift_percent: m.drift_percent,
1446 note: m.note,
1447 updated_at: m.updated_at,
1448 updated_by: m.updated_by,
1449 })
1450 .collect();
1451
1452 #[derive(Deserialize)]
1453 struct Fetched {
1454 at: Option<String>,
1455 }
1456 let fetched_at = self.db.prepare("SELECT MAX(fetched_at) AS at FROM cost_lines").first::<Fetched>(None).await?.and_then(|f| f.at);
1457
1458 Ok(CostsReport {
1459 configured,
1460 fetched_at,
1461 days: days
1462 .iter()
1463 .map(|d| CostDay {
1464 day: d.day.clone(),
1465 bucket: d.bucket.clone(),
1466 cf_cost_micros: d.cf_cost_micros,
1467 own_cost_micros: d.own_cost_micros,
1468 value_micros: d.value_micros,
1469 cash_micros: d.cash_micros,
1470 })
1471 .collect(),
1472 since,
1473 until,
1474 products,
1475 overall,
1476 drift,
1477 alerts: self.admin_cost_alerts(AdminCostAlertsArgs {}).await?,
1478 proposals: self.proposals().await?,
1479 versions: self.versions().await?,
1480 top_workspaces,
1481 lines,
1482 mappings,
1483 settings: self.cost_settings().await?,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays1484 caps: self.spend_caps().await?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1485 })
1486 }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use super::*;
1492
Margin alerts measure what is sold, and say dollars when a percentage would mislead1493 #[test]
Models' margin read -14%: usage nothing paid for is valued at price, not $01494 fn usage_nothing_paid_for_is_valued_at_price_and_paid_usage_at_what_was_paid() {
1495 // A free period: charged nothing, drawn from nothing.
1496 assert_eq!(usage_value(false, 1_000_000, 0, 20), 1_200_000);
1497 // Charged, or drawn from a trial: what was paid.
1498 assert_eq!(usage_value(false, 1_000_000, 1_200_000, 20), 1_200_000);
1499 assert_eq!(usage_value(false, 1_000_000, 900_000, 20), 900_000);
1500 // g1t's own: at price.
1501 assert_eq!(usage_value(true, 1_000_000, 0, 20), 1_200_000);
1502 // No cost, nothing paid: nothing.
1503 assert_eq!(usage_value(false, 0, 0, 20), 0);
1504 }
1505
1506 #[test]
Margin alerts measure what is sold, and say dollars when a percentage would mislead1507 fn the_overall_alert_says_dollars_while_little_comes_in() {
1508 let small = overall_detail(90_000, 7_500_000, 3, 10.0, -8239.7);
1509 assert!(small.contains("took in $0.09 against $7.50"), "{small}");
1510 assert!(!small.contains('%'), "{small}");
1511 let real = overall_detail(30_000_000, 40_000_000, 3, 10.0, -33.3);
1512 assert!(real.contains("as low as -33.3%"), "{real}");
1513 }
1514
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1515 fn rule(product: &str, meter: &str, bucket: &str, own: Option<&str>) -> Rule {
1516 Rule { product: product.into(), meter: meter.into(), bucket: bucket.into(), price_meter: None, own_meter: own.map(Into::into), drift_percent: 10.0 }
1517 }
1518
1519 fn rules() -> Vec<Rule> {
1520 vec![
1521 rule("containers", "*", "sandboxes", None),
1522 rule("workers", "*", "platform", None),
1523 rule("artifacts", "*", "git", Some("git_operations")),
1524 rule("artifacts", "events_", "git", Some("git_operations")),
1525 ]
1526 }
1527
1528 fn revenue_map() -> BTreeMap<String, String> {
1529 [("sandbox", "sandboxes"), ("git", "git"), ("plan", "platform")].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
1530 }
1531
1532 fn line(day: &str, source: &str, product: &str, meter: &str, quantity: f64, cost: f64) -> LineRow {
1533 LineRow { day: day.into(), source: source.into(), product: product.into(), meter: meter.into(), quantity, cost_usd: cost }
1534 }
1535
1536 fn usage(day: &str, workspace: &str, key: &str, value: i64, cash: i64, cost: i64) -> UsageRow {
1537 UsageRow { day: day.into(), workspace: workspace.into(), key: key.into(), value, cash, cost }
1538 }
1539
1540 #[test]
1541 fn a_day_puts_the_bill_g1ts_counts_and_charges_side_by_side() {
1542 let lines = vec![
1543 line("2026-10-15", SOURCE_BILLABLE, "containers", "container_memory", 1000.0, 2.00),
1544 line("2026-10-15", SOURCE_BILLABLE, "artifacts", "artifacts_operations", 30_000.0, 3.00),
1545 // Artifacts' own events: not used while the bill has a count.
1546 line("2026-10-15", SOURCE_ARTIFACTS, "artifacts", "events_pull", 29_000.0, 0.0),
1547 line("2026-10-15", SOURCE_BILLABLE, "workers", "workers_cpu_ms", 1.0, 0.50),
1548 line("2026-10-15", SOURCE_BILLABLE, "browser_rendering", "browser_hours", 2.0, 0.25),
1549 ];
1550 let own = vec![
1551 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "acme".into(), quantity: 7_500.0 },
1552 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "beta".into(), quantity: 2_500.0 },
1553 ];
1554 let usage = vec![
1555 usage("2026-10-15", "acme", "sandbox", 2_400_000, 1_000_000, 2_000_000),
1556 usage("2026-10-15", "beta", "sandbox", 1_200_000, 1_200_000, 1_000_000),
1557 usage("2026-10-15", "acme", "git", 600_000, 600_000, 500_000),
1558 usage("2026-10-15", "acme", "implement", 120_000, 120_000, 100_000),
1559 usage("2026-10-15", "beta", "plan", 20_000_000, 20_000_000, 0),
1560 ];
1561 let (days, workspaces) = fold(&rules(), &revenue_map(), &lines, &own, &usage);
1562 let get = |bucket: &str| days.iter().find(|d| d.bucket == bucket).unwrap();
1563 let sandboxes = get("sandboxes");
1564 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));
1565 let git = get("git");
1566 assert_eq!(git.cf_cost_micros, 3_000_000);
1567 assert_eq!((git.cf_quantity, git.own_quantity), (30_000.0, 10_000.0));
1568 assert_eq!(get("platform").value_micros, 20_000_000);
1569 // Not mapped: a leak until someone maps it.
1570 assert_eq!(get(UNMAPPED).cf_cost_micros, 250_000);
1571 // Models: no Cloudflare line, their cost is g1t's own.
1572 assert_eq!(get("models").cost(), 100_000);
1573 // Git's cost shared by g1t's own counts (Cloudflare gave none per
1574 // workspace here): three quarters to acme.
1575 let share = |ws: &str, bucket: &str| workspaces.iter().find(|w| w.workspace == ws && w.bucket == bucket).map(|w| (w.cost, w.revenue));
1576 assert_eq!(share("acme", "git"), Some((2_250_000, 600_000)));
1577 assert_eq!(share("beta", "git"), Some((750_000, 0)));
1578 // Every bucket's cost is shared out exactly.
1579 for d in &days {
1580 let shared: i64 = workspaces.iter().filter(|w| w.bucket == d.bucket).map(|w| w.cost).sum();
1581 assert_eq!(shared, d.cost(), "{}", d.bucket);
1582 }
1583 }
1584
1585 #[test]
1586 fn artifacts_events_count_when_the_bill_does_not() {
1587 let lines = vec![
1588 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_pull", 120.0, 0.0),
1589 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_push", 30.0, 0.0),
1590 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_ratelimited", 9.0, 0.0),
1591 ];
1592 let (days, _) = fold(&rules(), &revenue_map(), &lines, &[], &[]);
1593 assert_eq!(days[0].cf_quantity, 150.0);
1594 assert_eq!(days[0].cf_cost_micros, 0);
1595 }
1596
1597 #[test]
1598 fn month_end_meters_are_told_by_the_day_from_snapshots() {
1599 let snap = |day: &str, cost: i64, charge: i64| (day.to_string(), "acme".to_string(), "git".to_string(), cost, charge);
1600 let rows = pending_deltas(&[snap("2026-10-30", 100, 120), snap("2026-10-31", 250, 300), snap("2026-11-01", 40, 48), snap("2026-11-02", 40, 48)]);
1601 assert_eq!(
1602 rows.iter().map(|r| (r.day.as_str(), r.cost, r.value)).collect::<Vec<_>>(),
1603 vec![("2026-10-30", 100, 120), ("2026-10-31", 150, 180), ("2026-11-01", 40, 48)]
1604 );
1605 }
1606
1607 #[test]
1608 fn a_plan_payment_is_spread_over_the_month_it_pays_for() {
1609 let days = spread("2026-10-01T00:00:00.000Z", 20_000_000, 30);
1610 assert_eq!(days.len(), 30);
1611 assert_eq!(days[0], ("2026-10-01".to_string(), 666_667));
1612 assert_eq!(days[29], ("2026-10-30".to_string(), 666_666));
1613 assert_eq!(days.iter().map(|d| d.1).sum::<i64>(), 20_000_000);
1614 assert!(spread("2026-10-01", 0, 30).is_empty());
1615 assert_eq!(dollars(17_024_000), "$17.02");
1616 assert_eq!(dollars(-27_668_620), "-$27.67");
1617 assert_eq!(dollars(63_000), "$0.063");
1618 }
1619
1620 #[test]
1621 fn margins_and_deltas() {
1622 assert_eq!(margin_percent(1_200_000, 1_000_000).map(|m| (m * 100.0).round() / 100.0), Some(16.67));
1623 assert_eq!(margin_percent(0, 5), None);
1624 assert_eq!(delta_percent(110.0, 100.0), Some(10.0));
1625 assert_eq!(delta_percent(1.0, 0.0), None);
1626 }
1627
1628 fn day(bucket: &str, cf: i64, own: i64, value: i64, cfq: f64, ownq: f64) -> ProductDay {
1629 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 }
1630 }
1631
1632 #[test]
1633 fn counts_more_than_the_threshold_apart_are_drift() {
1634 // Cloudflare counted 30,000 operations where g1t counted 10,000:
1635 // binding reads, perhaps. -66.7%.
1636 let drift = drifts("git", &[day("git", 3_000_000, 1_500_000, 1_800_000, 30_000.0, 10_000.0)], 10.0, true, 100_000);
1637 assert_eq!(drift.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Count, DriftKind::Cost]);
1638 assert!((drift[0].delta_percent.unwrap() + 66.666).abs() < 0.01);
1639 // 9% apart: within 10%.
1640 assert!(drifts("git", &[day("git", 1_000_000, 1_000_000, 1_200_000, 10_000.0, 10_900.0)], 10.0, true, 100_000).is_empty());
1641 // Uncounted products have no count drift.
1642 assert!(drifts("sandboxes", &[day("sandboxes", 1_000_000, 1_050_000, 1_200_000, 5.0, 0.0)], 10.0, false, 100_000).is_empty());
1643 }
1644
1645 #[test]
1646 fn cost_with_no_revenue_is_a_leak_but_not_for_running_g1t() {
1647 let leak = drifts("actions_cache", &[day("actions_cache", 400_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000);
1648 assert_eq!(leak.len(), 1);
1649 assert_eq!(leak[0].kind, DriftKind::Leak);
1650 assert!(drifts("platform", &[day("platform", 5_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
1651 // Pennies say nothing.
1652 assert!(drifts("actions_cache", &[day("actions_cache", 50_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
1653 assert!(drifts(UNMAPPED, &[day(UNMAPPED, 250_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000)[0].kind == DriftKind::Leak);
1654 }
1655
1656 #[test]
1657 fn a_margin_alert_needs_n_days_in_a_row_under_the_floor() {
1658 let s = |d: &str, revenue: i64, cost: i64| (d.to_string(), revenue, cost);
1659 // 5%, 0%, -20%: three days under 10%.
1660 let series = vec![s("10-13", 1_200_000, 1_000_000), s("10-14", 1_050_000, 1_000_000), s("10-15", 1_000_000, 1_000_000), s("10-16", 1_000_000, 1_200_000)];
1661 let (from, worst) = breach(&series, 10.0, 3, 100_000).unwrap();
1662 assert_eq!(from, "10-14");
1663 assert!((worst + 20.0).abs() < 1e-9);
1664 // A good day in the window clears it.
1665 let mended = vec![s("10-14", 1_050_000, 1_000_000), s("10-15", 1_300_000, 1_000_000), s("10-16", 1_000_000, 1_200_000)];
1666 assert!(breach(&mended, 10.0, 3, 100_000).is_none());
1667 // Cost with no revenue at all is the worst margin there is.
1668 assert_eq!(breach(&[s("10-16", 0, 500_000)], 10.0, 1, 100_000).unwrap().1, -100.0);
1669 // Too little cost to judge.
1670 assert!(breach(&[s("10-16", 0, 5_000)], 10.0, 1, 100_000).is_none());
1671 assert!(breach(&series, 10.0, 9, 100_000).is_none());
1672 }
1673
1674 #[test]
1675 fn shared_costs_add_up_to_the_bill() {
1676 let w = |k: &str, v: f64| (k.to_string(), v);
1677 assert_eq!(attribute(100, &[w("a", 1.0), w("b", 1.0), w("c", 1.0)]), vec![("a".into(), 34), ("b".into(), 33), ("c".into(), 33)]);
1678 assert_eq!(attribute(10, &[w("a", 3.0), w("b", 1.0), w("a", 0.0)]), vec![("a".into(), 8), ("b".into(), 2)]);
1679 assert!(attribute(10, &[w("a", 0.0)]).is_empty());
1680 assert!(attribute(0, &[w("a", 1.0)]).is_empty());
1681 }
1682
1683 #[test]
1684 fn a_workspace_that_costs_more_than_it_pays_is_flagged() {
1685 let rows = vec![("acme".to_string(), 5_000_000, 1_000_000), ("beta".to_string(), 900_000, 0), ("gamma".to_string(), 2_000_000, 3_000_000)];
1686 let found = anomalies(&rows, 1.0, 1_000_000);
1687 assert_eq!(found, vec![("acme".to_string(), 5_000_000, 1_000_000)]);
1688 // At twice its revenue as the threshold, $5 against $3 is fine.
1689 assert!(anomalies(&[("acme".to_string(), 5_000_000, 3_000_000)], 2.0, 1_000_000).is_empty());
1690 }
1691
1692 #[test]
1693 fn a_git_operation_costs_what_cloudflare_counts_for_it() {
1694 // $0.15 per 1,000 of Cloudflare's operations, on the charged days.
1695 let rate = billed_rate(&[(10_000.0, 0.0), (20_000.0, 3.0), (30_000.0, 4.5), (5_000.0, 0.75)]).unwrap();
1696 assert!((rate - 0.000_15).abs() < 1e-12);
1697 // Cloudflare counted 3 for every 1 g1t did: binding reads count.
1698 let per_op = derived_unit_cost(rate, 300_000.0, 100_000.0).unwrap();
1699 let per_thousand_micros = per_op * unit_size("1,000 operations") * 1e6;
1700 assert!((per_thousand_micros - 450_000.0).abs() < 1e-6, "{per_thousand_micros}");
1701 // Too few of g1t's units to say.
1702 assert!(derived_unit_cost(rate, 3_000.0, 500.0).is_none());
1703 assert!(billed_rate(&[(10_000.0, 0.0)]).is_none());
1704 assert_eq!(unit_size("million requests"), 1e6);
1705 assert_eq!(unit_size("second"), 1.0);
1706 }
1707}