g1t/services/billing/src/margin.rs

1,672 lines75,360 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.
Margin alerts measure what is sold, and say dollars when a percentage would mislead408/// What the overall alert says: the money as money, and a percentage only
409/// while there is enough coming in for one to mean something (a few cents
410/// against dollars of cost reads as -8000%).
411pub(crate) fn overall_detail(took: i64, spent: i64, days: usize, floor: f64, worst: f64) -> String {
412 if took < 1_000_000 * days as i64 {
413 return format!(
414 "All of g1t, comped workspaces left out: took in {} against {} of Cloudflare's bill over {days} days.",
415 dollars(took),
416 dollars(spent)
417 );
418 }
419 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}%.")
420}
421
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily422pub(crate) fn anomalies(rows: &[(String, i64, i64)], factor: f64, floor_micros: i64) -> Vec<(String, i64, i64)> {
423 let mut out: Vec<(String, i64, i64)> = rows
424 .iter()
425 .filter(|(_, cost, revenue)| *cost >= floor_micros && *cost as f64 > *revenue as f64 * factor)
426 .cloned()
427 .collect();
428 out.sort_by(|a, b| (b.1 - b.2).cmp(&(a.1 - a.2)).then(a.0.cmp(&b.0)));
429 out
430}
431
432/// Cloudflare's marginal rate for one of its units: the median over the
433/// charged days of cost over quantity, in dollars. None while the included
434/// amounts still cover it. Each item is a day's (quantity, cost).
435pub(crate) fn billed_rate(days: &[(f64, f64)]) -> Option<f64> {
436 let mut rates: Vec<f64> = days.iter().filter(|(q, c)| *q > 0.0 && *c > 0.0).map(|(q, c)| c / q).collect();
437 if rates.is_empty() {
438 return None;
439 }
440 rates.sort_by(f64::total_cmp);
441 Some(rates[rates.len() / 2])
442}
443
444/// What one of g1t's units costs, from Cloudflare's rate per its own unit
445/// and how many of Cloudflare's units each of g1t's took: if Cloudflare
446/// counts three operations for every git operation g1t counts, a git
447/// operation costs three of Cloudflare's. None without enough of g1t's
448/// units to say.
449pub(crate) fn derived_unit_cost(rate_per_cf_unit: f64, cf_units: f64, own_units: f64) -> Option<f64> {
450 (own_units >= MIN_UNITS && cf_units > 0.0 && rate_per_cf_unit > 0.0).then(|| rate_per_cf_unit * cf_units / own_units)
451}
452
453/// How many units a price is per: `1,000 operations` → 1,000, `million
454/// requests` → 1,000,000, `second` → 1.
455pub(crate) fn unit_size(unit: &str) -> f64 {
456 let first = unit.split_whitespace().next().unwrap_or_default().replace(',', "");
457 match first.as_str() {
458 "million" => 1_000_000.0,
459 "thousand" => 1_000.0,
460 n => n.parse().unwrap_or(1.0),
461 }
462}
463
464fn day_before(day: &str, days: u64) -> String {
465 let ms = g1t_contracts::time::parse_rfc3339(&format!("{day}T00:00:00Z")).unwrap_or(0);
466 rfc3339(ms.saturating_sub(days * DAY_MS))[..10].to_owned()
467}
468
469/// Dollars to the cent from a dollar up, finer below: `$17.02`, `$0.063`.
470fn dollars(micros: i64) -> String {
471 if micros.abs() >= 1_000_000 {
472 let cents = (micros as f64 / 10_000.0).round() as i64;
473 format!("{}${}.{:02}", if cents < 0 { "-" } else { "" }, cents.abs() / 100, cents.abs() % 100)
474 } else {
475 crate::features::dollars(micros)
476 }
477}
478
479/// The days a plan payment is spread over.
480const PLAN_DAYS: u64 = 30;
481
482/// `micros` paid on `day` spread evenly over `days` days from it, in
483/// whole micros that add up to it (the first days take the remainder).
484pub(crate) fn spread(day: &str, micros: i64, days: u64) -> Vec<(String, i64)> {
485 if micros <= 0 || days == 0 {
486 return Vec::new();
487 }
488 let start = g1t_contracts::time::parse_rfc3339(&format!("{}T00:00:00Z", &day[..10.min(day.len())])).unwrap_or(0);
489 let each = micros / days as i64;
490 let rest = micros % days as i64;
491 (0..days)
492 .map(|n| (rfc3339(start + n * DAY_MS)[..10].to_owned(), each + i64::from((n as i64) < rest)))
493 .collect()
494}
495
496// ---------------------------------------------------------------------
497// The daily run, and what sudo reads.
498// ---------------------------------------------------------------------
499
500#[derive(Serialize)]
501struct Mail<'a> {
502 to: &'a str,
503 from: &'a str,
504 subject: &'a str,
505 text: String,
506 html: String,
507}
508
509fn escape(text: &str) -> String {
510 text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
511}
512
513/// Emails staff through Cloudflare Email Sending, the `EMAIL` binding.
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays514pub(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 daily515 let link = "https://sudo.g1t.sh/costs";
516 let text = format!("{}\n\nCosts & margin: {link}\n\nSent by g1t-billing's margin guard (COSTS_ALERT_EMAIL).\n", lines.join("\n\n"));
517 let mut html = String::from("<div style=\"font-family:system-ui,sans-serif;max-width:560px;margin:0 auto;padding:24px 16px;color:#16150f\">");
518 for line in lines {
519 html.push_str(&format!("<p style=\"font-size:15px;line-height:1.6\">{}</p>", escape(line)));
520 }
521 html.push_str(&format!(
522 "<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>"
523 ));
524 let mail = Mail { to, from: "g1t <noreply@g1t.sh>", subject, text, html };
525 let binding = g1t_kit::js::binding(env, "EMAIL")?;
526 g1t_kit::js::call(&binding, "send", &[g1t_kit::js::to_js(&mail)?]).await?;
527 Ok(())
528}
529
530#[derive(Deserialize)]
531struct AlertRow {
532 id: String,
533 kind: String,
534 subject: String,
535 detail: String,
536 since: String,
537 opened_at: String,
538 emailed_at: Option<String>,
539}
540
541impl From<AlertRow> for MarginAlert {
542 fn from(r: AlertRow) -> Self {
543 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 }
544 }
545}
546
547#[derive(Deserialize)]
548struct MarginRow {
549 day: String,
550 bucket: String,
551 cf_cost_micros: i64,
552 own_cost_micros: i64,
553 value_micros: i64,
554 cash_micros: i64,
555 cf_quantity: f64,
556 own_quantity: f64,
557}
558
559impl From<MarginRow> for ProductDay {
560 fn from(r: MarginRow) -> Self {
561 ProductDay {
562 day: r.day,
563 bucket: r.bucket,
564 cf_cost_micros: r.cf_cost_micros,
565 own_cost_micros: r.own_cost_micros,
566 value_micros: r.value_micros,
567 cash_micros: r.cash_micros,
568 cf_quantity: r.cf_quantity,
569 own_quantity: r.own_quantity,
570 }
571 }
572}
573
574impl Billing {
575 /// The day's work: read Cloudflare's bill and g1t's own counts,
576 /// reconcile, look for drift, measure unit costs, apply prices whose
577 /// day has come, and raise or clear alerts.
578 pub(crate) async fn costs_daily(&self, env: &Env, keeper: &crate::keeper::Keeper) -> Result<CostsRun> {
579 let mut run = CostsRun::default();
580 let (since, until) = match self.read_cloudflare(keeper, &mut run.problems).await? {
581 Some((since, until, lines)) => {
582 run.lines = lines;
583 (since, until)
584 }
585 // Without the bill, still reconcile what g1t knows itself, over
586 // the same days the bill would be read for.
587 None => {
588 #[derive(Deserialize)]
589 struct Last {
590 day: Option<String>,
591 }
592 let last = self.db.prepare("SELECT MAX(day) AS day FROM margin_days").first::<Last>(None).await?.and_then(|l| l.day);
593 costs::window(last.as_deref(), now_ms())
594 }
595 };
596 if let Err(error) = self.count_own(&since, &until).await {
597 run.problems.push(format!("g1t's own counts could not be read: {error}"));
598 }
599 self.snapshot_pending(&until).await?;
600 run.days = self.reconcile_range(&since, &until).await?;
601 let drift = self.find_drift(&until).await?;
602 run.proposals = self.measure_units(&until).await?;
603 self.apply_due_versions().await?;
604 run.alerts = self.raise_alerts(env, &until, &drift).await?;
605 if let Some(identity) = &self.identity
606 && let Err(error) = self.tell_owners_of_rises(identity).await
607 {
608 run.problems.push(format!("owners could not be told of a price rise: {error}"));
609 }
610 for problem in &run.problems {
611 worker::console_warn!("costs: {problem}");
612 }
613 Ok(run)
614 }
615
616 /// What each month-end source had come to by the end of `day`.
617 async fn snapshot_pending(&self, day: &str) -> Result<()> {
618 self.db
619 .prepare(
620 "INSERT INTO pending_days (day, workspace, source, cost_micros, charge_micros)
621 SELECT ?1, workspace, source, COALESCE(cost_micros, 0), COALESCE(charge_micros, 0) FROM pending_usage WHERE month = ?2
622 ON CONFLICT (day, workspace, source) DO UPDATE SET cost_micros = excluded.cost_micros, charge_micros = excluded.charge_micros",
623 )
624 .bind(&[day.into(), day[..7].into()])?
625 .run()
626 .await?;
627 Ok(())
628 }
629
630 /// What customers were charged on the days, by workspace and key.
631 async fn usage_rows(&self, since: &str, until: &str) -> Result<Vec<UsageRow>> {
632 #[derive(Deserialize)]
633 struct Row {
634 day: String,
635 workspace: String,
636 key: String,
637 internal: i64,
638 own_provider: i64,
639 cash: Option<i64>,
640 drawn: Option<i64>,
641 cost: Option<i64>,
642 }
643 let charged_here = crate::storage::CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
644 let end = format!("{until}T23:59:59.999Z");
645 let rows = self
646 .db
647 .prepare(format!(
648 "SELECT substr(created_at, 1, 10) AS day, workspace,
649 CASE WHEN task = 'deployments' AND reference LIKE 'deploy/%' THEN 'builds' ELSE COALESCE(task, 'other') END AS key,
650 CASE WHEN workspace IN ({internal}) THEN 1 ELSE 0 END AS internal,
651 CASE WHEN billed_to = 'workspace' THEN 1 ELSE 0 END AS own_provider,
652 -SUM(amount_micros) AS cash,
653 SUM(COALESCE(credit_micros, 0) + COALESCE(trial_micros, 0) + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0)) AS drawn,
654 SUM(COALESCE(cost_micros, 0)) AS cost
655 FROM ledger
656 WHERE kind = 'usage' AND created_at >= ?1 AND created_at <= ?2 AND COALESCE(task, '') NOT IN ({charged_here})
657 GROUP BY 1, 2, 3, 4, 5",
658 internal = crate::sales::INTERNAL_SQL
659 ))
660 .bind(&[since.into(), end.as_str().into()])?
661 .all()
662 .await?
663 .results::<Row>()?;
664 let mut out: Vec<UsageRow> = rows
665 .into_iter()
666 .map(|r| {
667 // A workspace's own model provider was paid there: no cost
668 // to g1t. g1t's own workspaces are valued at price.
669 let cost = if r.own_provider == 1 { 0 } else { r.cost.unwrap_or(0) };
670 let cash = r.cash.unwrap_or(0);
671 let value = if r.internal == 1 { crate::credits::with_margin(cost, self.margin_percent) } else { cash + r.drawn.unwrap_or(0) };
672 UsageRow { day: r.day, workspace: r.workspace, key: r.key, value, cash, cost }
673 })
674 .collect();
675 // Month-end sources, from their daily snapshots.
676 #[derive(Deserialize)]
677 struct Snap {
678 day: String,
679 workspace: String,
680 source: String,
681 cost_micros: i64,
682 charge_micros: i64,
683 }
684 let snaps = self
685 .db
686 .prepare("SELECT day, workspace, source, cost_micros, charge_micros FROM pending_days WHERE day >= ?1 AND day <= ?2")
687 .bind(&[day_before(since, 1).into(), until.into()])?
688 .all()
689 .await?
690 .results::<Snap>()?
691 .into_iter()
692 .filter(|s| crate::storage::CHARGED_HERE.contains(&s.source.as_str()) || s.source == "domains")
693 .map(|s| (s.day, s.workspace, s.source, s.cost_micros, s.charge_micros))
694 .collect::<Vec<_>>();
695 out.extend(pending_deltas(&snaps).into_iter().filter(|u| u.day.as_str() >= since));
696 // The plan's price, spread over the 30 days it pays for, so a month's
697 // payment does not read as one very good day and 29 bad ones.
698 #[derive(Deserialize)]
699 struct Plan {
700 day: String,
701 workspace: String,
702 micros: Option<i64>,
703 }
704 let plans = self
705 .db
706 .prepare(
707 "SELECT substr(paid_at, 1, 10) AS day, workspace, SUM(amount_micros) AS micros FROM plan_payments
708 WHERE paid_at >= ?1 AND paid_at <= ?2 GROUP BY 1, 2",
709 )
710 .bind(&[day_before(since, PLAN_DAYS - 1).into(), end.as_str().into()])?
711 .all()
712 .await?
713 .results::<Plan>()?;
714 for p in plans {
715 for (day, micros) in spread(&p.day, p.micros.unwrap_or(0), PLAN_DAYS) {
716 if day.as_str() >= since && day.as_str() <= until {
717 out.push(UsageRow { day, workspace: p.workspace.clone(), key: "plan".into(), value: micros, cash: micros, cost: 0 });
718 }
719 }
720 }
721 Ok(out)
722 }
723
724 /// Reconciles the days and writes `margin_days` and `workspace_costs`.
725 async fn reconcile_range(&self, since: &str, until: &str) -> Result<u32> {
726 let rules = self.rules().await?;
727 #[derive(Deserialize)]
728 struct Map {
729 key: String,
730 bucket: String,
731 }
732 let revenue_map: BTreeMap<String, String> = self
733 .db
734 .prepare("SELECT key, bucket FROM revenue_map")
735 .all()
736 .await?
737 .results::<Map>()?
738 .into_iter()
739 .map(|m| (m.key, m.bucket))
740 .collect();
741 let lines = self
742 .db
743 .prepare("SELECT day, source, product, meter, quantity, cost_usd FROM cost_lines WHERE day >= ?1 AND day <= ?2")
744 .bind(&[since.into(), until.into()])?
745 .all()
746 .await?
747 .results::<LineRow>()?;
748 let own = self
749 .db
750 .prepare("SELECT day, meter, workspace, quantity FROM own_counts WHERE day >= ?1 AND day <= ?2")
751 .bind(&[since.into(), until.into()])?
752 .all()
753 .await?
754 .results::<OwnRow>()?;
755 let usage = self.usage_rows(since, until).await?;
756 let (days, workspaces) = fold(&rules, &revenue_map, &lines, &own, &usage);
757 let now = rfc3339(now_ms());
758 self.db
759 .batch(vec![
760 self.db.prepare("DELETE FROM margin_days WHERE day >= ?1 AND day <= ?2").bind(&[since.into(), until.into()])?,
761 self.db.prepare("DELETE FROM workspace_costs WHERE day >= ?1 AND day <= ?2").bind(&[since.into(), until.into()])?,
762 ])
763 .await?;
764 for chunk in days.chunks(50) {
765 let mut statements = Vec::with_capacity(chunk.len());
766 for d in chunk {
767 statements.push(
768 self.db
769 .prepare(
770 "INSERT OR REPLACE INTO margin_days (day, bucket, cf_cost_micros, own_cost_micros, value_micros, cash_micros, cf_quantity, own_quantity, computed_at)
771 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
772 )
773 .bind(&[
774 d.day.as_str().into(),
775 d.bucket.as_str().into(),
776 (d.cf_cost_micros as f64).into(),
777 (d.own_cost_micros as f64).into(),
778 (d.value_micros as f64).into(),
779 (d.cash_micros as f64).into(),
780 d.cf_quantity.into(),
781 d.own_quantity.into(),
782 now.as_str().into(),
783 ])?,
784 );
785 }
786 self.db.batch(statements).await?;
787 }
788 for chunk in workspaces.chunks(50) {
789 let mut statements = Vec::with_capacity(chunk.len());
790 for w in chunk {
791 statements.push(
792 self.db
Margin alerts measure what is sold, and say dollars when a percentage would mislead793 .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 daily794 .bind(&[
795 w.day.as_str().into(),
796 w.workspace.as_str().into(),
797 w.bucket.as_str().into(),
798 (w.cost as f64).into(),
799 (w.revenue as f64).into(),
Margin alerts measure what is sold, and say dollars when a percentage would mislead800 (w.value as f64).into(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily801 ])?,
802 );
803 }
804 self.db.batch(statements).await?;
805 }
806 Ok(costs::days_between(since, until).len() as u32)
807 }
808
809 async fn margin_days(&self, since: &str, until: &str) -> Result<Vec<ProductDay>> {
810 Ok(self
811 .db
812 .prepare("SELECT * FROM margin_days WHERE day >= ?1 AND day <= ?2 ORDER BY day, bucket")
813 .bind(&[since.into(), until.into()])?
814 .all()
815 .await?
816 .results::<MarginRow>()?
817 .into_iter()
818 .map(ProductDay::from)
819 .collect())
820 }
821
822 /// Drift over the last week, written to `cost_drift` (replacing the
823 /// last run's), with unmapped Cloudflare meters as leaks.
824 async fn find_drift(&self, until: &str) -> Result<Vec<(Drift, String)>> {
825 let since = day_before(until, DRIFT_DAYS - 1);
826 let settings = self.cost_settings().await?;
827 let rules = self.rules().await?;
828 let days = self.margin_days(&since, until).await?;
829 let mut by: BTreeMap<String, Vec<ProductDay>> = BTreeMap::new();
830 for d in days {
831 by.entry(d.bucket.clone()).or_default().push(d);
832 }
833 let mut found = Vec::new();
834 for (bucket, days) in &by {
835 let bucket_rules: Vec<&Rule> = rules.iter().filter(|r| &r.bucket == bucket).collect();
836 let threshold = bucket_rules.iter().map(|r| r.drift_percent).fold(f64::INFINITY, f64::min);
837 let threshold = if threshold.is_finite() { threshold } else { 10.0 };
838 let counted = bucket_rules.iter().any(|r| r.own_meter.is_some());
839 for drift in drifts(bucket, days, threshold, counted, settings.min_daily_cost_micros) {
840 let title = costs::bucket_title(bucket);
841 let detail = match drift.kind {
842 DriftKind::Count => format!(
One operation mapping, owned by repos; billing reads it instead of keeping its own843 "{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 daily844 crate::features::thousands(drift.ours.max(0.0).round() as u64),
845 crate::features::thousands(drift.cloudflare.max(0.0).round() as u64),
846 drift.delta_percent.unwrap_or(0.0)
847 ),
848 DriftKind::Cost => format!(
849 "{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.",
850 dollars(drift.cloudflare as i64),
851 dollars(drift.ours as i64),
852 drift.delta_percent.unwrap_or(0.0)
853 ),
854 DriftKind::Leak if bucket == UNMAPPED => {
855 format!("Cloudflare charged {} for meters no mapping claims. Map them on Costs & margin.", dollars(drift.cloudflare as i64))
856 }
857 DriftKind::Leak => format!(
858 "{title}: Cloudflare charged {} over the last {DRIFT_DAYS} days and customers were charged nothing for it.",
859 dollars(drift.cloudflare as i64)
860 ),
861 };
862 found.push((drift, detail));
863 }
864 }
865 let now = rfc3339(now_ms());
866 let mut statements = vec![self.db.prepare("DELETE FROM cost_drift")];
867 for (drift, detail) in &found {
868 statements.push(
869 self.db
870 .prepare("INSERT OR REPLACE INTO cost_drift (bucket, kind, ours, cloudflare, delta_percent, detail, found_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
871 .bind(&[
872 drift.bucket.as_str().into(),
873 drift.kind.as_str().into(),
874 drift.ours.into(),
875 drift.cloudflare.into(),
876 drift.delta_percent.map_or(JsValue::NULL, JsValue::from),
877 detail.as_str().into(),
878 now.as_str().into(),
879 ])?,
880 );
881 }
882 self.db.batch(statements).await?;
883 Ok(found)
884 }
885
886 /// Unit costs from the bill for mappings that scale to g1t's own count
887 /// (git operations), proposed to the price book.
888 async fn measure_units(&self, until: &str) -> Result<u32> {
889 #[derive(Deserialize)]
890 struct Scaled {
891 product: String,
892 meter: String,
893 price_meter: String,
894 own_meter: String,
895 unit: Option<String>,
896 }
897 let scaled = self
898 .db
899 .prepare(
900 "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
901 WHERE m.scale_to_own = 1 AND m.price_meter IS NOT NULL AND m.own_meter IS NOT NULL",
902 )
903 .all()
904 .await?
905 .results::<Scaled>()?;
906 let since = day_before(until, MEASURE_DAYS - 1);
907 let rules = self.rules().await?;
908 let mut proposed = 0;
909 for s in scaled {
910 #[derive(Deserialize)]
911 struct Day {
912 product: String,
913 meter: String,
914 quantity: f64,
915 cost_usd: f64,
916 }
917 let lines = self
918 .db
919 .prepare("SELECT product, meter, quantity, cost_usd FROM cost_lines WHERE source = ?1 AND product = ?2 AND day >= ?3 AND day <= ?4")
920 .bind(&[SOURCE_BILLABLE.into(), s.product.as_str().into(), since.as_str().into(), until.into()])?
921 .all()
922 .await?
923 .results::<Day>()?;
924 // Only the lines this very mapping claims.
925 let mine: Vec<(f64, f64)> = lines
926 .iter()
927 .filter(|l| costs::classify(&rules, &l.product, &l.meter).is_some_and(|r| r.product == s.product && r.meter == s.meter))
928 .map(|l| (l.quantity, l.cost_usd))
929 .collect();
930 let Some(rate) = billed_rate(&mine) else { continue };
931 let cf_units: f64 = mine.iter().map(|(q, _)| q).sum();
932 #[derive(Deserialize)]
933 struct Own {
934 total: Option<f64>,
935 }
936 let own_units = self
937 .db
938 .prepare("SELECT SUM(quantity) AS total FROM own_counts WHERE meter = ?1 AND day >= ?2 AND day <= ?3")
939 .bind(&[s.own_meter.as_str().into(), since.as_str().into(), until.into()])?
940 .first::<Own>(None)
941 .await?
942 .and_then(|o| o.total)
943 .unwrap_or(0.0);
944 let Some(per_unit) = derived_unit_cost(rate, cf_units, own_units) else { continue };
945 let size = unit_size(s.unit.as_deref().unwrap_or("1"));
946 let measured = per_unit * size * 1_000_000.0;
947 let reason = format!(
948 "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 {})",
949 rate * 1000.0,
950 cf_units / own_units,
951 crate::features::thousands(cf_units.round() as u64),
952 crate::features::thousands(own_units.round() as u64)
953 );
954 if self.propose(&s.price_meter, measured, &reason, "reconciler").await?.is_some() {
955 proposed += 1;
956 }
957 }
958 Ok(proposed)
959 }
960
961 /// Opens, updates and closes margin alerts, and emails staff about new
962 /// ones (and open ones each week).
963 async fn raise_alerts(&self, env: &Env, until: &str, drift: &[(Drift, String)]) -> Result<u32> {
964 let settings = self.cost_settings().await?;
965 let since = day_before(until, u64::from(settings.alert_days.max(1)) - 1);
966 let days = self.margin_days(&since, until).await?;
967 let mut conditions: Vec<(String, String, String, String)> = Vec::new();
968 // Each product under the floor.
969 let mut by: BTreeMap<String, Vec<(String, i64, i64)>> = BTreeMap::new();
970 let mut all: BTreeMap<String, (i64, i64)> = BTreeMap::new();
971 for d in &days {
972 let overall = all.entry(d.day.clone()).or_default();
973 overall.0 += d.cash_micros;
974 overall.1 += d.cost();
975 if !OVERHEAD.contains(&d.bucket.as_str()) && d.bucket != UNMAPPED {
976 by.entry(d.bucket.clone()).or_default().push((d.day.clone(), d.value_micros, d.cost()));
977 }
978 }
979 let floor = settings.margin_floor_percent;
980 let n = settings.alert_days as usize;
981 for (bucket, series) in &by {
982 if let Some((from, worst)) = breach(series, floor, n, settings.min_daily_cost_micros) {
983 conditions.push((
984 "margin".into(),
985 bucket.clone(),
986 format!("{}: margin under {floor:.0}% for {n} days running, as low as {worst:.1}%.", costs::bucket_title(bucket)),
987 from,
988 ));
989 }
990 }
Margin alerts measure what is sold, and say dollars when a percentage would mislead991 // Comped workspaces' share is a budget g1t chose to spend, watched
992 // on its own (budget.rs): not part of whether what is sold pays.
993 for (day, cost) in self.comped_costs(&since, until).await? {
994 if let Some(overall) = all.get_mut(&day) {
995 overall.1 = (overall.1 - cost).max(0);
996 }
997 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily998 let series: Vec<(String, i64, i64)> = all.into_iter().map(|(day, (revenue, cost))| (day, revenue, cost)).collect();
999 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 mislead1000 let tail = &series[series.len().saturating_sub(n)..];
1001 let (took, spent) = tail.iter().fold((0i64, 0i64), |(r, c), (_, revenue, cost)| (r + revenue, c + cost));
1002 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 daily1003 }
1004 for (d, detail) in drift {
1005 let kind = if d.kind == DriftKind::Leak { "leak" } else { "drift" };
1006 conditions.push((kind.into(), format!("{}:{}", d.bucket, d.kind.as_str()), detail.clone(), until.to_owned()));
1007 }
1008 // Workspaces costing more than they pay.
1009 for (workspace, cost, revenue) in self.workspace_anomalies(until, &settings).await? {
1010 conditions.push((
1011 "workspace".into(),
1012 workspace.clone(),
Margin alerts measure what is sold, and say dollars when a percentage would mislead1013 format!(
1014 "{workspace} cost g1t {} on Cloudflare over {ANOMALY_DAYS} days, and its usage was priced at {}: its prices are below cost.",
1015 dollars(cost),
1016 dollars(revenue)
1017 ),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1018 day_before(until, ANOMALY_DAYS - 1),
1019 ));
1020 }
1021
1022 let open = self
1023 .db
1024 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL")
1025 .all()
1026 .await?
1027 .results::<AlertRow>()?;
1028 let now = now_ms();
1029 let stamp = rfc3339(now);
1030 let mut to_email: Vec<String> = Vec::new();
1031 let mut kept: BTreeSet<String> = BTreeSet::new();
1032 for (kind, subject, detail, from) in &conditions {
1033 match open.iter().find(|a| &a.kind == kind && &a.subject == subject) {
1034 Some(alert) => {
1035 kept.insert(alert.id.clone());
1036 self.db
1037 .prepare("UPDATE margin_alerts SET detail = ? WHERE id = ?")
1038 .bind(&[detail.as_str().into(), alert.id.as_str().into()])?
1039 .run()
1040 .await?;
1041 let stale = alert
1042 .emailed_at
1043 .as_deref()
1044 .and_then(g1t_contracts::time::parse_rfc3339)
1045 .is_none_or(|at| now.saturating_sub(at) >= REMIND_MS);
1046 if stale && kind != "workspace" {
1047 to_email.push(format!("Still open: {detail}"));
1048 kept.insert(format!("email:{}", alert.id));
1049 }
1050 }
1051 None => {
1052 let id = new_id("mal", now);
1053 self.db
1054 .prepare("INSERT INTO margin_alerts (id, kind, subject, detail, since, opened_at) VALUES (?, ?, ?, ?, ?, ?)")
1055 .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()])?
1056 .run()
1057 .await?;
1058 kept.insert(id.clone());
Margin alerts measure what is sold, and say dollars when a percentage would mislead1059 // A workspace's is for Reach out, not the inbox.
1060 if kind != "workspace" {
1061 to_email.push(detail.clone());
1062 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1063 kept.insert(format!("email:{id}"));
1064 }
1065 }
1066 }
1067 for alert in &open {
1068 if !kept.contains(&alert.id) {
1069 self.db
1070 .prepare("UPDATE margin_alerts SET resolved_at = ? WHERE id = ?")
1071 .bind(&[stamp.as_str().into(), alert.id.as_str().into()])?
1072 .run()
1073 .await?;
1074 }
1075 }
1076 let to = env.var("COSTS_ALERT_EMAIL").map(|v| v.to_string()).unwrap_or_default();
1077 if !to_email.is_empty() && !to.trim().is_empty() {
1078 let subject = format!("[g1t costs] {} margin alert{}", to_email.len(), if to_email.len() == 1 { "" } else { "s" });
1079 match email_staff(env, to.trim(), &subject, &to_email).await {
1080 Ok(()) => {
1081 for marker in kept.iter().filter_map(|k| k.strip_prefix("email:")) {
1082 self.db
1083 .prepare("UPDATE margin_alerts SET emailed_at = ? WHERE id = ?")
1084 .bind(&[stamp.as_str().into(), marker.into()])?
1085 .run()
1086 .await?;
1087 }
1088 }
1089 Err(error) => worker::console_error!("could not email the margin alerts: {error}"),
1090 }
1091 }
1092 Ok(conditions.len() as u32)
1093 }
1094
1095 /// 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 mislead1096 /// Each day's cost shared out to comped workspaces.
1097 async fn comped_costs(&self, since: &str, until: &str) -> Result<Vec<(String, i64)>> {
1098 #[derive(Deserialize)]
1099 struct Row {
1100 day: String,
1101 cost: Option<i64>,
1102 }
1103 Ok(self
1104 .db
1105 .prepare(format!(
1106 "SELECT day, SUM(cost_micros) AS cost FROM workspace_costs
1107 WHERE day >= ?1 AND day <= ?2 AND workspace IN ({}) GROUP BY day",
1108 crate::sales::INTERNAL_SQL
1109 ))
1110 .bind(&[since.into(), until.into()])?
1111 .all()
1112 .await?
1113 .results::<Row>()?
1114 .into_iter()
1115 .map(|r| (r.day, r.cost.unwrap_or(0)))
1116 .collect())
1117 }
1118
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1119 async fn workspace_anomalies(&self, until: &str, settings: &CostSettings) -> Result<Vec<(String, i64, i64)>> {
1120 #[derive(Deserialize)]
1121 struct Row {
1122 workspace: String,
1123 cost: Option<i64>,
1124 revenue: Option<i64>,
1125 }
1126 let rows = self
1127 .db
1128 .prepare(format!(
Margin alerts measure what is sold, and say dollars when a percentage would mislead1129 // Against what its usage was priced at, not the cash it
1130 // paid: a trial or a gift paying for usage is not a price
1131 // below cost.
1132 "SELECT workspace, SUM(cost_micros) AS cost, SUM(value_micros) AS revenue FROM workspace_costs
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1133 WHERE day >= ?1 AND day <= ?2 AND workspace NOT IN ({}) GROUP BY workspace",
1134 crate::sales::INTERNAL_SQL
1135 ))
1136 .bind(&[day_before(until, ANOMALY_DAYS - 1).into(), until.into()])?
1137 .all()
1138 .await?
1139 .results::<Row>()?;
1140 let rows: Vec<(String, i64, i64)> = rows.into_iter().map(|r| (r.workspace, r.cost.unwrap_or(0), r.revenue.unwrap_or(0))).collect();
1141 Ok(anomalies(&rows, settings.anomaly_factor, settings.anomaly_floor_micros))
1142 }
1143
1144 /// For Reach out: workspaces with an open cost-over-revenue alert,
1145 /// each with its detail and cost.
1146 pub(crate) async fn costing_more_than_they_pay(&self) -> Result<Vec<(String, String, i64)>> {
1147 let alerts = self
1148 .db
1149 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL AND kind = 'workspace' ORDER BY opened_at DESC LIMIT 50")
1150 .all()
1151 .await?
1152 .results::<AlertRow>()?;
1153 let mut out = Vec::new();
1154 for alert in alerts {
1155 #[derive(Deserialize)]
1156 struct Cost {
1157 cost: Option<i64>,
1158 }
1159 let cost = self
1160 .db
1161 .prepare("SELECT SUM(cost_micros) AS cost FROM workspace_costs WHERE workspace = ? AND day >= ?")
1162 .bind(&[alert.subject.as_str().into(), alert.since.as_str().into()])?
1163 .first::<Cost>(None)
1164 .await?
1165 .and_then(|c| c.cost)
1166 .unwrap_or(0);
1167 out.push((alert.subject, alert.detail, cost));
1168 }
1169 Ok(out)
1170 }
1171
1172 /// `admin_cost_alerts`: what sudo's banner says.
1173 pub(crate) async fn admin_cost_alerts(&self, _: AdminCostAlertsArgs) -> Result<Vec<MarginAlert>> {
1174 Ok(self
1175 .db
1176 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL ORDER BY opened_at DESC LIMIT 50")
1177 .all()
1178 .await?
1179 .results::<AlertRow>()?
1180 .into_iter()
1181 .map(MarginAlert::from)
1182 .collect())
1183 }
1184
1185 /// `admin_run_costs`: the daily run, now.
1186 pub(crate) async fn admin_run_costs(&self, env: &Env, a: AdminRunCostsArgs) -> Result<Outcome<CostsRun>> {
1187 let keeper = crate::keeper::Keeper::from_env(env);
1188 let run = self.costs_daily(env, &keeper).await?;
1189 if !a.by.is_empty() {
1190 self.audit(
1191 "costs",
1192 "costs_run",
1193 &format!("{} lines, {} days, {} proposals, {} alerts", run.lines, run.days, run.proposals, run.alerts),
1194 &a.by,
1195 )
1196 .await?;
1197 }
1198 Ok(Outcome::Ok(run))
1199 }
1200
1201 /// `admin_set_cost_mapping`.
1202 pub(crate) async fn admin_set_cost_mapping(&self, a: AdminSetCostMappingArgs) -> Result<Outcome<CostMapping>> {
1203 let product = costs::slug(&a.product);
1204 let meter = if a.meter.trim() == "*" { "*".to_owned() } else { costs::slug(&a.meter) };
1205 if product.is_empty() || meter.is_empty() {
1206 return Ok(Outcome::fail(FailureCode::Invalid, "Name Cloudflare's product and a meter (or * for all of it)."));
1207 }
1208 let now = rfc3339(now_ms());
1209 if a.remove {
1210 self.db
1211 .prepare("DELETE FROM cost_map WHERE product = ? AND meter = ?")
1212 .bind(&[product.as_str().into(), meter.as_str().into()])?
1213 .run()
1214 .await?;
1215 self.audit("costs", "cost_mapping_removed", &format!("{product}/{meter}"), &a.by).await?;
1216 return Ok(Outcome::Ok(CostMapping {
1217 product,
1218 meter,
1219 bucket: String::new(),
1220 price_meter: None,
1221 own_meter: None,
1222 scale_to_own: false,
1223 drift_percent: 0.0,
1224 note: String::new(),
1225 updated_at: now,
1226 updated_by: a.by,
1227 }));
1228 }
1229 let bucket = costs::slug(&a.bucket);
1230 if bucket.is_empty() {
1231 return Ok(Outcome::fail(FailureCode::Invalid, "Say which of g1t's products it is a cost of."));
1232 }
1233 let clean = |v: Option<String>| v.map(|v| v.trim().to_owned()).filter(|v| !v.is_empty());
1234 let (price_meter, own_meter) = (clean(a.price_meter), clean(a.own_meter));
1235 let drift = a.drift_percent.filter(|d| d.is_finite() && *d > 0.0).unwrap_or(10.0);
1236 self.db
1237 .prepare(
1238 "INSERT INTO cost_map (product, meter, bucket, price_meter, own_meter, scale_to_own, drift_percent, note, updated_at, updated_by)
1239 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1240 ON CONFLICT (product, meter) DO UPDATE SET bucket = ?3, price_meter = ?4, own_meter = ?5, scale_to_own = ?6,
1241 drift_percent = ?7, note = ?8, updated_at = ?9, updated_by = ?10",
1242 )
1243 .bind(&[
1244 product.as_str().into(),
1245 meter.as_str().into(),
1246 bucket.as_str().into(),
1247 crate::optional(price_meter.as_deref()),
1248 crate::optional(own_meter.as_deref()),
1249 i32::from(a.scale_to_own).into(),
1250 drift.into(),
1251 a.note.trim().into(),
1252 now.as_str().into(),
1253 a.by.as_str().into(),
1254 ])?
1255 .run()
1256 .await?;
1257 self.audit("costs", "cost_mapping", &format!("{product}/{meter} → {bucket}"), &a.by).await?;
1258 Ok(Outcome::Ok(CostMapping {
1259 product,
1260 meter,
1261 bucket,
1262 price_meter,
1263 own_meter,
1264 scale_to_own: a.scale_to_own,
1265 drift_percent: drift,
1266 note: a.note.trim().to_owned(),
1267 updated_at: now,
1268 updated_by: a.by,
1269 }))
1270 }
1271
1272 /// `admin_costs`: the Costs & margin page.
1273 pub(crate) async fn admin_costs(&self, a: AdminCostsArgs, configured: bool) -> Result<CostsReport> {
1274 let until = rfc3339(now_ms())[..10].to_owned();
1275 let span = u64::from(a.days.unwrap_or(30).clamp(7, 90));
1276 let since = day_before(&until, span - 1);
1277 let days = self.margin_days(&since, &until).await?;
1278 let rules = self.rules().await?;
1279
1280 let mut products: BTreeMap<String, ProductMargin> = BTreeMap::new();
1281 let mut overall = OverallMargin::default();
1282 for d in &days {
1283 let p = products.entry(d.bucket.clone()).or_insert_with(|| ProductMargin {
1284 bucket: d.bucket.clone(),
1285 title: costs::bucket_title(&d.bucket),
1286 cost_source: if NOT_CLOUDFLARE.contains(&d.bucket.as_str()) { "ledger" } else { "cloudflare" }.into(),
1287 overhead: OVERHEAD.contains(&d.bucket.as_str()),
1288 ..ProductMargin::default()
1289 });
1290 p.cf_cost_micros += d.cf_cost_micros;
1291 p.own_cost_micros += d.own_cost_micros;
1292 p.value_micros += d.value_micros;
1293 p.cost_micros += d.cost();
1294 overall.cost_micros += d.cost();
1295 if d.bucket == "platform" {
1296 overall.plans_micros += d.cash_micros;
1297 } else {
1298 overall.usage_micros += d.cash_micros;
1299 }
1300 }
1301 for p in products.values_mut() {
1302 p.margin_micros = p.value_micros - p.cost_micros;
1303 p.margin_percent = margin_percent(p.value_micros, p.cost_micros);
1304 }
1305 let revenue = overall.usage_micros + overall.plans_micros;
1306 overall.margin_micros = revenue - overall.cost_micros;
1307 overall.margin_percent = margin_percent(revenue, overall.cost_micros);
1308 let mut products: Vec<ProductMargin> = products.into_values().collect();
1309 products.sort_by_key(|p| std::cmp::Reverse(p.cost_micros.max(p.value_micros)));
1310
1311 #[derive(Deserialize)]
1312 struct DriftRow {
1313 bucket: String,
1314 kind: String,
1315 ours: f64,
1316 cloudflare: f64,
1317 delta_percent: Option<f64>,
1318 detail: String,
1319 found_at: String,
1320 }
1321 let drift = self
1322 .db
1323 .prepare("SELECT * FROM cost_drift ORDER BY kind, bucket")
1324 .all()
1325 .await?
1326 .results::<DriftRow>()?
1327 .into_iter()
1328 .map(|r| CostDrift {
1329 title: costs::bucket_title(&r.bucket),
1330 bucket: r.bucket,
1331 kind: r.kind,
1332 ours: r.ours,
1333 cloudflare: r.cloudflare,
1334 delta_percent: r.delta_percent,
1335 detail: r.detail,
1336 found_at: r.found_at,
1337 })
1338 .collect();
1339
1340 #[derive(Deserialize)]
1341 struct Top {
1342 workspace: String,
1343 cost: Option<i64>,
1344 revenue: Option<i64>,
1345 internal: i64,
1346 }
1347 let top_workspaces = self
1348 .db
1349 .prepare(format!(
1350 "SELECT workspace, SUM(cost_micros) AS cost, SUM(revenue_micros) AS revenue,
1351 CASE WHEN workspace IN ({}) THEN 1 ELSE 0 END AS internal
1352 FROM workspace_costs WHERE day >= ?1 AND day <= ?2 GROUP BY workspace ORDER BY cost DESC LIMIT 15",
1353 crate::sales::INTERNAL_SQL
1354 ))
1355 .bind(&[since.as_str().into(), until.as_str().into()])?
1356 .all()
1357 .await?
1358 .results::<Top>()?
1359 .into_iter()
1360 .map(|t| WorkspaceCost { workspace: t.workspace, cost_micros: t.cost.unwrap_or(0), revenue_micros: t.revenue.unwrap_or(0), internal: t.internal == 1 })
1361 .collect();
1362
1363 #[derive(Deserialize)]
1364 struct Summary {
1365 source: String,
1366 product: String,
1367 meter: String,
1368 raw_name: String,
1369 unit: String,
1370 quantity: f64,
1371 cost_usd: f64,
1372 }
1373 let lines = self
1374 .db
1375 .prepare(
1376 "SELECT source, product, meter, MAX(raw_name) AS raw_name, MAX(unit) AS unit, SUM(quantity) AS quantity, SUM(cost_usd) AS cost_usd
1377 FROM cost_lines WHERE day >= ?1 AND day <= ?2 GROUP BY source, product, meter ORDER BY cost_usd DESC, product, meter LIMIT 200",
1378 )
1379 .bind(&[since.as_str().into(), until.as_str().into()])?
1380 .all()
1381 .await?
1382 .results::<Summary>()?
1383 .into_iter()
1384 .map(|l| CostLineSummary {
1385 bucket: costs::classify(&rules, &l.product, &l.meter).map(|r| r.bucket.clone()),
1386 product: l.product,
1387 meter: l.meter,
1388 raw_name: l.raw_name,
1389 unit: l.unit,
1390 source: l.source,
1391 quantity: l.quantity,
1392 cost_micros: micros(l.cost_usd),
1393 })
1394 .collect();
1395
1396 #[derive(Deserialize)]
1397 struct MapRow {
1398 product: String,
1399 meter: String,
1400 bucket: String,
1401 price_meter: Option<String>,
1402 own_meter: Option<String>,
1403 scale_to_own: i64,
1404 drift_percent: f64,
1405 note: String,
1406 updated_at: String,
1407 updated_by: String,
1408 }
1409 let mappings = self
1410 .db
1411 .prepare("SELECT * FROM cost_map ORDER BY product, meter")
1412 .all()
1413 .await?
1414 .results::<MapRow>()?
1415 .into_iter()
1416 .map(|m| CostMapping {
1417 product: m.product,
1418 meter: m.meter,
1419 bucket: m.bucket,
1420 price_meter: m.price_meter,
1421 own_meter: m.own_meter,
1422 scale_to_own: m.scale_to_own == 1,
1423 drift_percent: m.drift_percent,
1424 note: m.note,
1425 updated_at: m.updated_at,
1426 updated_by: m.updated_by,
1427 })
1428 .collect();
1429
1430 #[derive(Deserialize)]
1431 struct Fetched {
1432 at: Option<String>,
1433 }
1434 let fetched_at = self.db.prepare("SELECT MAX(fetched_at) AS at FROM cost_lines").first::<Fetched>(None).await?.and_then(|f| f.at);
1435
1436 Ok(CostsReport {
1437 configured,
1438 fetched_at,
1439 days: days
1440 .iter()
1441 .map(|d| CostDay {
1442 day: d.day.clone(),
1443 bucket: d.bucket.clone(),
1444 cf_cost_micros: d.cf_cost_micros,
1445 own_cost_micros: d.own_cost_micros,
1446 value_micros: d.value_micros,
1447 cash_micros: d.cash_micros,
1448 })
1449 .collect(),
1450 since,
1451 until,
1452 products,
1453 overall,
1454 drift,
1455 alerts: self.admin_cost_alerts(AdminCostAlertsArgs {}).await?,
1456 proposals: self.proposals().await?,
1457 versions: self.versions().await?,
1458 top_workspaces,
1459 lines,
1460 mappings,
1461 settings: self.cost_settings().await?,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays1462 caps: self.spend_caps().await?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1463 })
1464 }
1465}
1466
1467#[cfg(test)]
1468mod tests {
1469 use super::*;
1470
Margin alerts measure what is sold, and say dollars when a percentage would mislead1471 #[test]
1472 fn the_overall_alert_says_dollars_while_little_comes_in() {
1473 let small = overall_detail(90_000, 7_500_000, 3, 10.0, -8239.7);
1474 assert!(small.contains("took in $0.09 against $7.50"), "{small}");
1475 assert!(!small.contains('%'), "{small}");
1476 let real = overall_detail(30_000_000, 40_000_000, 3, 10.0, -33.3);
1477 assert!(real.contains("as low as -33.3%"), "{real}");
1478 }
1479
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1480 fn rule(product: &str, meter: &str, bucket: &str, own: Option<&str>) -> Rule {
1481 Rule { product: product.into(), meter: meter.into(), bucket: bucket.into(), price_meter: None, own_meter: own.map(Into::into), drift_percent: 10.0 }
1482 }
1483
1484 fn rules() -> Vec<Rule> {
1485 vec![
1486 rule("containers", "*", "sandboxes", None),
1487 rule("workers", "*", "platform", None),
1488 rule("artifacts", "*", "git", Some("git_operations")),
1489 rule("artifacts", "events_", "git", Some("git_operations")),
1490 ]
1491 }
1492
1493 fn revenue_map() -> BTreeMap<String, String> {
1494 [("sandbox", "sandboxes"), ("git", "git"), ("plan", "platform")].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
1495 }
1496
1497 fn line(day: &str, source: &str, product: &str, meter: &str, quantity: f64, cost: f64) -> LineRow {
1498 LineRow { day: day.into(), source: source.into(), product: product.into(), meter: meter.into(), quantity, cost_usd: cost }
1499 }
1500
1501 fn usage(day: &str, workspace: &str, key: &str, value: i64, cash: i64, cost: i64) -> UsageRow {
1502 UsageRow { day: day.into(), workspace: workspace.into(), key: key.into(), value, cash, cost }
1503 }
1504
1505 #[test]
1506 fn a_day_puts_the_bill_g1ts_counts_and_charges_side_by_side() {
1507 let lines = vec![
1508 line("2026-10-15", SOURCE_BILLABLE, "containers", "container_memory", 1000.0, 2.00),
1509 line("2026-10-15", SOURCE_BILLABLE, "artifacts", "artifacts_operations", 30_000.0, 3.00),
1510 // Artifacts' own events: not used while the bill has a count.
1511 line("2026-10-15", SOURCE_ARTIFACTS, "artifacts", "events_pull", 29_000.0, 0.0),
1512 line("2026-10-15", SOURCE_BILLABLE, "workers", "workers_cpu_ms", 1.0, 0.50),
1513 line("2026-10-15", SOURCE_BILLABLE, "browser_rendering", "browser_hours", 2.0, 0.25),
1514 ];
1515 let own = vec![
1516 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "acme".into(), quantity: 7_500.0 },
1517 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "beta".into(), quantity: 2_500.0 },
1518 ];
1519 let usage = vec![
1520 usage("2026-10-15", "acme", "sandbox", 2_400_000, 1_000_000, 2_000_000),
1521 usage("2026-10-15", "beta", "sandbox", 1_200_000, 1_200_000, 1_000_000),
1522 usage("2026-10-15", "acme", "git", 600_000, 600_000, 500_000),
1523 usage("2026-10-15", "acme", "implement", 120_000, 120_000, 100_000),
1524 usage("2026-10-15", "beta", "plan", 20_000_000, 20_000_000, 0),
1525 ];
1526 let (days, workspaces) = fold(&rules(), &revenue_map(), &lines, &own, &usage);
1527 let get = |bucket: &str| days.iter().find(|d| d.bucket == bucket).unwrap();
1528 let sandboxes = get("sandboxes");
1529 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));
1530 let git = get("git");
1531 assert_eq!(git.cf_cost_micros, 3_000_000);
1532 assert_eq!((git.cf_quantity, git.own_quantity), (30_000.0, 10_000.0));
1533 assert_eq!(get("platform").value_micros, 20_000_000);
1534 // Not mapped: a leak until someone maps it.
1535 assert_eq!(get(UNMAPPED).cf_cost_micros, 250_000);
1536 // Models: no Cloudflare line, their cost is g1t's own.
1537 assert_eq!(get("models").cost(), 100_000);
1538 // Git's cost shared by g1t's own counts (Cloudflare gave none per
1539 // workspace here): three quarters to acme.
1540 let share = |ws: &str, bucket: &str| workspaces.iter().find(|w| w.workspace == ws && w.bucket == bucket).map(|w| (w.cost, w.revenue));
1541 assert_eq!(share("acme", "git"), Some((2_250_000, 600_000)));
1542 assert_eq!(share("beta", "git"), Some((750_000, 0)));
1543 // Every bucket's cost is shared out exactly.
1544 for d in &days {
1545 let shared: i64 = workspaces.iter().filter(|w| w.bucket == d.bucket).map(|w| w.cost).sum();
1546 assert_eq!(shared, d.cost(), "{}", d.bucket);
1547 }
1548 }
1549
1550 #[test]
1551 fn artifacts_events_count_when_the_bill_does_not() {
1552 let lines = vec![
1553 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_pull", 120.0, 0.0),
1554 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_push", 30.0, 0.0),
1555 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_ratelimited", 9.0, 0.0),
1556 ];
1557 let (days, _) = fold(&rules(), &revenue_map(), &lines, &[], &[]);
1558 assert_eq!(days[0].cf_quantity, 150.0);
1559 assert_eq!(days[0].cf_cost_micros, 0);
1560 }
1561
1562 #[test]
1563 fn month_end_meters_are_told_by_the_day_from_snapshots() {
1564 let snap = |day: &str, cost: i64, charge: i64| (day.to_string(), "acme".to_string(), "git".to_string(), cost, charge);
1565 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)]);
1566 assert_eq!(
1567 rows.iter().map(|r| (r.day.as_str(), r.cost, r.value)).collect::<Vec<_>>(),
1568 vec![("2026-10-30", 100, 120), ("2026-10-31", 150, 180), ("2026-11-01", 40, 48)]
1569 );
1570 }
1571
1572 #[test]
1573 fn a_plan_payment_is_spread_over_the_month_it_pays_for() {
1574 let days = spread("2026-10-01T00:00:00.000Z", 20_000_000, 30);
1575 assert_eq!(days.len(), 30);
1576 assert_eq!(days[0], ("2026-10-01".to_string(), 666_667));
1577 assert_eq!(days[29], ("2026-10-30".to_string(), 666_666));
1578 assert_eq!(days.iter().map(|d| d.1).sum::<i64>(), 20_000_000);
1579 assert!(spread("2026-10-01", 0, 30).is_empty());
1580 assert_eq!(dollars(17_024_000), "$17.02");
1581 assert_eq!(dollars(-27_668_620), "-$27.67");
1582 assert_eq!(dollars(63_000), "$0.063");
1583 }
1584
1585 #[test]
1586 fn margins_and_deltas() {
1587 assert_eq!(margin_percent(1_200_000, 1_000_000).map(|m| (m * 100.0).round() / 100.0), Some(16.67));
1588 assert_eq!(margin_percent(0, 5), None);
1589 assert_eq!(delta_percent(110.0, 100.0), Some(10.0));
1590 assert_eq!(delta_percent(1.0, 0.0), None);
1591 }
1592
1593 fn day(bucket: &str, cf: i64, own: i64, value: i64, cfq: f64, ownq: f64) -> ProductDay {
1594 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 }
1595 }
1596
1597 #[test]
1598 fn counts_more_than_the_threshold_apart_are_drift() {
1599 // Cloudflare counted 30,000 operations where g1t counted 10,000:
1600 // binding reads, perhaps. -66.7%.
1601 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);
1602 assert_eq!(drift.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Count, DriftKind::Cost]);
1603 assert!((drift[0].delta_percent.unwrap() + 66.666).abs() < 0.01);
1604 // 9% apart: within 10%.
1605 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());
1606 // Uncounted products have no count drift.
1607 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());
1608 }
1609
1610 #[test]
1611 fn cost_with_no_revenue_is_a_leak_but_not_for_running_g1t() {
1612 let leak = drifts("actions_cache", &[day("actions_cache", 400_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000);
1613 assert_eq!(leak.len(), 1);
1614 assert_eq!(leak[0].kind, DriftKind::Leak);
1615 assert!(drifts("platform", &[day("platform", 5_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
1616 // Pennies say nothing.
1617 assert!(drifts("actions_cache", &[day("actions_cache", 50_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
1618 assert!(drifts(UNMAPPED, &[day(UNMAPPED, 250_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000)[0].kind == DriftKind::Leak);
1619 }
1620
1621 #[test]
1622 fn a_margin_alert_needs_n_days_in_a_row_under_the_floor() {
1623 let s = |d: &str, revenue: i64, cost: i64| (d.to_string(), revenue, cost);
1624 // 5%, 0%, -20%: three days under 10%.
1625 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)];
1626 let (from, worst) = breach(&series, 10.0, 3, 100_000).unwrap();
1627 assert_eq!(from, "10-14");
1628 assert!((worst + 20.0).abs() < 1e-9);
1629 // A good day in the window clears it.
1630 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)];
1631 assert!(breach(&mended, 10.0, 3, 100_000).is_none());
1632 // Cost with no revenue at all is the worst margin there is.
1633 assert_eq!(breach(&[s("10-16", 0, 500_000)], 10.0, 1, 100_000).unwrap().1, -100.0);
1634 // Too little cost to judge.
1635 assert!(breach(&[s("10-16", 0, 5_000)], 10.0, 1, 100_000).is_none());
1636 assert!(breach(&series, 10.0, 9, 100_000).is_none());
1637 }
1638
1639 #[test]
1640 fn shared_costs_add_up_to_the_bill() {
1641 let w = |k: &str, v: f64| (k.to_string(), v);
1642 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)]);
1643 assert_eq!(attribute(10, &[w("a", 3.0), w("b", 1.0), w("a", 0.0)]), vec![("a".into(), 8), ("b".into(), 2)]);
1644 assert!(attribute(10, &[w("a", 0.0)]).is_empty());
1645 assert!(attribute(0, &[w("a", 1.0)]).is_empty());
1646 }
1647
1648 #[test]
1649 fn a_workspace_that_costs_more_than_it_pays_is_flagged() {
1650 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)];
1651 let found = anomalies(&rows, 1.0, 1_000_000);
1652 assert_eq!(found, vec![("acme".to_string(), 5_000_000, 1_000_000)]);
1653 // At twice its revenue as the threshold, $5 against $3 is fine.
1654 assert!(anomalies(&[("acme".to_string(), 5_000_000, 3_000_000)], 2.0, 1_000_000).is_empty());
1655 }
1656
1657 #[test]
1658 fn a_git_operation_costs_what_cloudflare_counts_for_it() {
1659 // $0.15 per 1,000 of Cloudflare's operations, on the charged days.
1660 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();
1661 assert!((rate - 0.000_15).abs() < 1e-12);
1662 // Cloudflare counted 3 for every 1 g1t did: binding reads count.
1663 let per_op = derived_unit_cost(rate, 300_000.0, 100_000.0).unwrap();
1664 let per_thousand_micros = per_op * unit_size("1,000 operations") * 1e6;
1665 assert!((per_thousand_micros - 450_000.0).abs() < 1e-6, "{per_thousand_micros}");
1666 // Too few of g1t's units to say.
1667 assert!(derived_unit_cost(rate, 3_000.0, 500.0).is_none());
1668 assert!(billed_rate(&[(10_000.0, 0.0)]).is_none());
1669 assert_eq!(unit_size("million requests"), 1e6);
1670 assert_eq!(unit_size("second"), 1.0);
1671 }
1672}