g1t/services/billing/src/margin.rs

1,676 lines75,611 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.
The workspace cost alert compares only days that carry their value, not the days before it was kept1132 // Days from before value_micros was kept have none: only days
1133 // since the first one that does are compared.
Margin alerts measure what is sold, and say dollars when a percentage would mislead1134 "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 kept1135 WHERE day >= ?1 AND day <= ?2 AND workspace NOT IN ({})
1136 AND day >= (SELECT MIN(day) FROM workspace_costs WHERE value_micros > 0)
1137 GROUP BY workspace",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1138 crate::sales::INTERNAL_SQL
1139 ))
1140 .bind(&[day_before(until, ANOMALY_DAYS - 1).into(), until.into()])?
1141 .all()
1142 .await?
1143 .results::<Row>()?;
1144 let rows: Vec<(String, i64, i64)> = rows.into_iter().map(|r| (r.workspace, r.cost.unwrap_or(0), r.revenue.unwrap_or(0))).collect();
1145 Ok(anomalies(&rows, settings.anomaly_factor, settings.anomaly_floor_micros))
1146 }
1147
1148 /// For Reach out: workspaces with an open cost-over-revenue alert,
1149 /// each with its detail and cost.
1150 pub(crate) async fn costing_more_than_they_pay(&self) -> Result<Vec<(String, String, i64)>> {
1151 let alerts = self
1152 .db
1153 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL AND kind = 'workspace' ORDER BY opened_at DESC LIMIT 50")
1154 .all()
1155 .await?
1156 .results::<AlertRow>()?;
1157 let mut out = Vec::new();
1158 for alert in alerts {
1159 #[derive(Deserialize)]
1160 struct Cost {
1161 cost: Option<i64>,
1162 }
1163 let cost = self
1164 .db
1165 .prepare("SELECT SUM(cost_micros) AS cost FROM workspace_costs WHERE workspace = ? AND day >= ?")
1166 .bind(&[alert.subject.as_str().into(), alert.since.as_str().into()])?
1167 .first::<Cost>(None)
1168 .await?
1169 .and_then(|c| c.cost)
1170 .unwrap_or(0);
1171 out.push((alert.subject, alert.detail, cost));
1172 }
1173 Ok(out)
1174 }
1175
1176 /// `admin_cost_alerts`: what sudo's banner says.
1177 pub(crate) async fn admin_cost_alerts(&self, _: AdminCostAlertsArgs) -> Result<Vec<MarginAlert>> {
1178 Ok(self
1179 .db
1180 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL ORDER BY opened_at DESC LIMIT 50")
1181 .all()
1182 .await?
1183 .results::<AlertRow>()?
1184 .into_iter()
1185 .map(MarginAlert::from)
1186 .collect())
1187 }
1188
1189 /// `admin_run_costs`: the daily run, now.
1190 pub(crate) async fn admin_run_costs(&self, env: &Env, a: AdminRunCostsArgs) -> Result<Outcome<CostsRun>> {
1191 let keeper = crate::keeper::Keeper::from_env(env);
1192 let run = self.costs_daily(env, &keeper).await?;
1193 if !a.by.is_empty() {
1194 self.audit(
1195 "costs",
1196 "costs_run",
1197 &format!("{} lines, {} days, {} proposals, {} alerts", run.lines, run.days, run.proposals, run.alerts),
1198 &a.by,
1199 )
1200 .await?;
1201 }
1202 Ok(Outcome::Ok(run))
1203 }
1204
1205 /// `admin_set_cost_mapping`.
1206 pub(crate) async fn admin_set_cost_mapping(&self, a: AdminSetCostMappingArgs) -> Result<Outcome<CostMapping>> {
1207 let product = costs::slug(&a.product);
1208 let meter = if a.meter.trim() == "*" { "*".to_owned() } else { costs::slug(&a.meter) };
1209 if product.is_empty() || meter.is_empty() {
1210 return Ok(Outcome::fail(FailureCode::Invalid, "Name Cloudflare's product and a meter (or * for all of it)."));
1211 }
1212 let now = rfc3339(now_ms());
1213 if a.remove {
1214 self.db
1215 .prepare("DELETE FROM cost_map WHERE product = ? AND meter = ?")
1216 .bind(&[product.as_str().into(), meter.as_str().into()])?
1217 .run()
1218 .await?;
1219 self.audit("costs", "cost_mapping_removed", &format!("{product}/{meter}"), &a.by).await?;
1220 return Ok(Outcome::Ok(CostMapping {
1221 product,
1222 meter,
1223 bucket: String::new(),
1224 price_meter: None,
1225 own_meter: None,
1226 scale_to_own: false,
1227 drift_percent: 0.0,
1228 note: String::new(),
1229 updated_at: now,
1230 updated_by: a.by,
1231 }));
1232 }
1233 let bucket = costs::slug(&a.bucket);
1234 if bucket.is_empty() {
1235 return Ok(Outcome::fail(FailureCode::Invalid, "Say which of g1t's products it is a cost of."));
1236 }
1237 let clean = |v: Option<String>| v.map(|v| v.trim().to_owned()).filter(|v| !v.is_empty());
1238 let (price_meter, own_meter) = (clean(a.price_meter), clean(a.own_meter));
1239 let drift = a.drift_percent.filter(|d| d.is_finite() && *d > 0.0).unwrap_or(10.0);
1240 self.db
1241 .prepare(
1242 "INSERT INTO cost_map (product, meter, bucket, price_meter, own_meter, scale_to_own, drift_percent, note, updated_at, updated_by)
1243 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1244 ON CONFLICT (product, meter) DO UPDATE SET bucket = ?3, price_meter = ?4, own_meter = ?5, scale_to_own = ?6,
1245 drift_percent = ?7, note = ?8, updated_at = ?9, updated_by = ?10",
1246 )
1247 .bind(&[
1248 product.as_str().into(),
1249 meter.as_str().into(),
1250 bucket.as_str().into(),
1251 crate::optional(price_meter.as_deref()),
1252 crate::optional(own_meter.as_deref()),
1253 i32::from(a.scale_to_own).into(),
1254 drift.into(),
1255 a.note.trim().into(),
1256 now.as_str().into(),
1257 a.by.as_str().into(),
1258 ])?
1259 .run()
1260 .await?;
1261 self.audit("costs", "cost_mapping", &format!("{product}/{meter} → {bucket}"), &a.by).await?;
1262 Ok(Outcome::Ok(CostMapping {
1263 product,
1264 meter,
1265 bucket,
1266 price_meter,
1267 own_meter,
1268 scale_to_own: a.scale_to_own,
1269 drift_percent: drift,
1270 note: a.note.trim().to_owned(),
1271 updated_at: now,
1272 updated_by: a.by,
1273 }))
1274 }
1275
1276 /// `admin_costs`: the Costs & margin page.
1277 pub(crate) async fn admin_costs(&self, a: AdminCostsArgs, configured: bool) -> Result<CostsReport> {
1278 let until = rfc3339(now_ms())[..10].to_owned();
1279 let span = u64::from(a.days.unwrap_or(30).clamp(7, 90));
1280 let since = day_before(&until, span - 1);
1281 let days = self.margin_days(&since, &until).await?;
1282 let rules = self.rules().await?;
1283
1284 let mut products: BTreeMap<String, ProductMargin> = BTreeMap::new();
1285 let mut overall = OverallMargin::default();
1286 for d in &days {
1287 let p = products.entry(d.bucket.clone()).or_insert_with(|| ProductMargin {
1288 bucket: d.bucket.clone(),
1289 title: costs::bucket_title(&d.bucket),
1290 cost_source: if NOT_CLOUDFLARE.contains(&d.bucket.as_str()) { "ledger" } else { "cloudflare" }.into(),
1291 overhead: OVERHEAD.contains(&d.bucket.as_str()),
1292 ..ProductMargin::default()
1293 });
1294 p.cf_cost_micros += d.cf_cost_micros;
1295 p.own_cost_micros += d.own_cost_micros;
1296 p.value_micros += d.value_micros;
1297 p.cost_micros += d.cost();
1298 overall.cost_micros += d.cost();
1299 if d.bucket == "platform" {
1300 overall.plans_micros += d.cash_micros;
1301 } else {
1302 overall.usage_micros += d.cash_micros;
1303 }
1304 }
1305 for p in products.values_mut() {
1306 p.margin_micros = p.value_micros - p.cost_micros;
1307 p.margin_percent = margin_percent(p.value_micros, p.cost_micros);
1308 }
1309 let revenue = overall.usage_micros + overall.plans_micros;
1310 overall.margin_micros = revenue - overall.cost_micros;
1311 overall.margin_percent = margin_percent(revenue, overall.cost_micros);
1312 let mut products: Vec<ProductMargin> = products.into_values().collect();
1313 products.sort_by_key(|p| std::cmp::Reverse(p.cost_micros.max(p.value_micros)));
1314
1315 #[derive(Deserialize)]
1316 struct DriftRow {
1317 bucket: String,
1318 kind: String,
1319 ours: f64,
1320 cloudflare: f64,
1321 delta_percent: Option<f64>,
1322 detail: String,
1323 found_at: String,
1324 }
1325 let drift = self
1326 .db
1327 .prepare("SELECT * FROM cost_drift ORDER BY kind, bucket")
1328 .all()
1329 .await?
1330 .results::<DriftRow>()?
1331 .into_iter()
1332 .map(|r| CostDrift {
1333 title: costs::bucket_title(&r.bucket),
1334 bucket: r.bucket,
1335 kind: r.kind,
1336 ours: r.ours,
1337 cloudflare: r.cloudflare,
1338 delta_percent: r.delta_percent,
1339 detail: r.detail,
1340 found_at: r.found_at,
1341 })
1342 .collect();
1343
1344 #[derive(Deserialize)]
1345 struct Top {
1346 workspace: String,
1347 cost: Option<i64>,
1348 revenue: Option<i64>,
1349 internal: i64,
1350 }
1351 let top_workspaces = self
1352 .db
1353 .prepare(format!(
1354 "SELECT workspace, SUM(cost_micros) AS cost, SUM(revenue_micros) AS revenue,
1355 CASE WHEN workspace IN ({}) THEN 1 ELSE 0 END AS internal
1356 FROM workspace_costs WHERE day >= ?1 AND day <= ?2 GROUP BY workspace ORDER BY cost DESC LIMIT 15",
1357 crate::sales::INTERNAL_SQL
1358 ))
1359 .bind(&[since.as_str().into(), until.as_str().into()])?
1360 .all()
1361 .await?
1362 .results::<Top>()?
1363 .into_iter()
1364 .map(|t| WorkspaceCost { workspace: t.workspace, cost_micros: t.cost.unwrap_or(0), revenue_micros: t.revenue.unwrap_or(0), internal: t.internal == 1 })
1365 .collect();
1366
1367 #[derive(Deserialize)]
1368 struct Summary {
1369 source: String,
1370 product: String,
1371 meter: String,
1372 raw_name: String,
1373 unit: String,
1374 quantity: f64,
1375 cost_usd: f64,
1376 }
1377 let lines = self
1378 .db
1379 .prepare(
1380 "SELECT source, product, meter, MAX(raw_name) AS raw_name, MAX(unit) AS unit, SUM(quantity) AS quantity, SUM(cost_usd) AS cost_usd
1381 FROM cost_lines WHERE day >= ?1 AND day <= ?2 GROUP BY source, product, meter ORDER BY cost_usd DESC, product, meter LIMIT 200",
1382 )
1383 .bind(&[since.as_str().into(), until.as_str().into()])?
1384 .all()
1385 .await?
1386 .results::<Summary>()?
1387 .into_iter()
1388 .map(|l| CostLineSummary {
1389 bucket: costs::classify(&rules, &l.product, &l.meter).map(|r| r.bucket.clone()),
1390 product: l.product,
1391 meter: l.meter,
1392 raw_name: l.raw_name,
1393 unit: l.unit,
1394 source: l.source,
1395 quantity: l.quantity,
1396 cost_micros: micros(l.cost_usd),
1397 })
1398 .collect();
1399
1400 #[derive(Deserialize)]
1401 struct MapRow {
1402 product: String,
1403 meter: String,
1404 bucket: String,
1405 price_meter: Option<String>,
1406 own_meter: Option<String>,
1407 scale_to_own: i64,
1408 drift_percent: f64,
1409 note: String,
1410 updated_at: String,
1411 updated_by: String,
1412 }
1413 let mappings = self
1414 .db
1415 .prepare("SELECT * FROM cost_map ORDER BY product, meter")
1416 .all()
1417 .await?
1418 .results::<MapRow>()?
1419 .into_iter()
1420 .map(|m| CostMapping {
1421 product: m.product,
1422 meter: m.meter,
1423 bucket: m.bucket,
1424 price_meter: m.price_meter,
1425 own_meter: m.own_meter,
1426 scale_to_own: m.scale_to_own == 1,
1427 drift_percent: m.drift_percent,
1428 note: m.note,
1429 updated_at: m.updated_at,
1430 updated_by: m.updated_by,
1431 })
1432 .collect();
1433
1434 #[derive(Deserialize)]
1435 struct Fetched {
1436 at: Option<String>,
1437 }
1438 let fetched_at = self.db.prepare("SELECT MAX(fetched_at) AS at FROM cost_lines").first::<Fetched>(None).await?.and_then(|f| f.at);
1439
1440 Ok(CostsReport {
1441 configured,
1442 fetched_at,
1443 days: days
1444 .iter()
1445 .map(|d| CostDay {
1446 day: d.day.clone(),
1447 bucket: d.bucket.clone(),
1448 cf_cost_micros: d.cf_cost_micros,
1449 own_cost_micros: d.own_cost_micros,
1450 value_micros: d.value_micros,
1451 cash_micros: d.cash_micros,
1452 })
1453 .collect(),
1454 since,
1455 until,
1456 products,
1457 overall,
1458 drift,
1459 alerts: self.admin_cost_alerts(AdminCostAlertsArgs {}).await?,
1460 proposals: self.proposals().await?,
1461 versions: self.versions().await?,
1462 top_workspaces,
1463 lines,
1464 mappings,
1465 settings: self.cost_settings().await?,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays1466 caps: self.spend_caps().await?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1467 })
1468 }
1469}
1470
1471#[cfg(test)]
1472mod tests {
1473 use super::*;
1474
Margin alerts measure what is sold, and say dollars when a percentage would mislead1475 #[test]
1476 fn the_overall_alert_says_dollars_while_little_comes_in() {
1477 let small = overall_detail(90_000, 7_500_000, 3, 10.0, -8239.7);
1478 assert!(small.contains("took in $0.09 against $7.50"), "{small}");
1479 assert!(!small.contains('%'), "{small}");
1480 let real = overall_detail(30_000_000, 40_000_000, 3, 10.0, -33.3);
1481 assert!(real.contains("as low as -33.3%"), "{real}");
1482 }
1483
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1484 fn rule(product: &str, meter: &str, bucket: &str, own: Option<&str>) -> Rule {
1485 Rule { product: product.into(), meter: meter.into(), bucket: bucket.into(), price_meter: None, own_meter: own.map(Into::into), drift_percent: 10.0 }
1486 }
1487
1488 fn rules() -> Vec<Rule> {
1489 vec![
1490 rule("containers", "*", "sandboxes", None),
1491 rule("workers", "*", "platform", None),
1492 rule("artifacts", "*", "git", Some("git_operations")),
1493 rule("artifacts", "events_", "git", Some("git_operations")),
1494 ]
1495 }
1496
1497 fn revenue_map() -> BTreeMap<String, String> {
1498 [("sandbox", "sandboxes"), ("git", "git"), ("plan", "platform")].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
1499 }
1500
1501 fn line(day: &str, source: &str, product: &str, meter: &str, quantity: f64, cost: f64) -> LineRow {
1502 LineRow { day: day.into(), source: source.into(), product: product.into(), meter: meter.into(), quantity, cost_usd: cost }
1503 }
1504
1505 fn usage(day: &str, workspace: &str, key: &str, value: i64, cash: i64, cost: i64) -> UsageRow {
1506 UsageRow { day: day.into(), workspace: workspace.into(), key: key.into(), value, cash, cost }
1507 }
1508
1509 #[test]
1510 fn a_day_puts_the_bill_g1ts_counts_and_charges_side_by_side() {
1511 let lines = vec![
1512 line("2026-10-15", SOURCE_BILLABLE, "containers", "container_memory", 1000.0, 2.00),
1513 line("2026-10-15", SOURCE_BILLABLE, "artifacts", "artifacts_operations", 30_000.0, 3.00),
1514 // Artifacts' own events: not used while the bill has a count.
1515 line("2026-10-15", SOURCE_ARTIFACTS, "artifacts", "events_pull", 29_000.0, 0.0),
1516 line("2026-10-15", SOURCE_BILLABLE, "workers", "workers_cpu_ms", 1.0, 0.50),
1517 line("2026-10-15", SOURCE_BILLABLE, "browser_rendering", "browser_hours", 2.0, 0.25),
1518 ];
1519 let own = vec![
1520 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "acme".into(), quantity: 7_500.0 },
1521 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "beta".into(), quantity: 2_500.0 },
1522 ];
1523 let usage = vec![
1524 usage("2026-10-15", "acme", "sandbox", 2_400_000, 1_000_000, 2_000_000),
1525 usage("2026-10-15", "beta", "sandbox", 1_200_000, 1_200_000, 1_000_000),
1526 usage("2026-10-15", "acme", "git", 600_000, 600_000, 500_000),
1527 usage("2026-10-15", "acme", "implement", 120_000, 120_000, 100_000),
1528 usage("2026-10-15", "beta", "plan", 20_000_000, 20_000_000, 0),
1529 ];
1530 let (days, workspaces) = fold(&rules(), &revenue_map(), &lines, &own, &usage);
1531 let get = |bucket: &str| days.iter().find(|d| d.bucket == bucket).unwrap();
1532 let sandboxes = get("sandboxes");
1533 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));
1534 let git = get("git");
1535 assert_eq!(git.cf_cost_micros, 3_000_000);
1536 assert_eq!((git.cf_quantity, git.own_quantity), (30_000.0, 10_000.0));
1537 assert_eq!(get("platform").value_micros, 20_000_000);
1538 // Not mapped: a leak until someone maps it.
1539 assert_eq!(get(UNMAPPED).cf_cost_micros, 250_000);
1540 // Models: no Cloudflare line, their cost is g1t's own.
1541 assert_eq!(get("models").cost(), 100_000);
1542 // Git's cost shared by g1t's own counts (Cloudflare gave none per
1543 // workspace here): three quarters to acme.
1544 let share = |ws: &str, bucket: &str| workspaces.iter().find(|w| w.workspace == ws && w.bucket == bucket).map(|w| (w.cost, w.revenue));
1545 assert_eq!(share("acme", "git"), Some((2_250_000, 600_000)));
1546 assert_eq!(share("beta", "git"), Some((750_000, 0)));
1547 // Every bucket's cost is shared out exactly.
1548 for d in &days {
1549 let shared: i64 = workspaces.iter().filter(|w| w.bucket == d.bucket).map(|w| w.cost).sum();
1550 assert_eq!(shared, d.cost(), "{}", d.bucket);
1551 }
1552 }
1553
1554 #[test]
1555 fn artifacts_events_count_when_the_bill_does_not() {
1556 let lines = vec![
1557 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_pull", 120.0, 0.0),
1558 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_push", 30.0, 0.0),
1559 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_ratelimited", 9.0, 0.0),
1560 ];
1561 let (days, _) = fold(&rules(), &revenue_map(), &lines, &[], &[]);
1562 assert_eq!(days[0].cf_quantity, 150.0);
1563 assert_eq!(days[0].cf_cost_micros, 0);
1564 }
1565
1566 #[test]
1567 fn month_end_meters_are_told_by_the_day_from_snapshots() {
1568 let snap = |day: &str, cost: i64, charge: i64| (day.to_string(), "acme".to_string(), "git".to_string(), cost, charge);
1569 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)]);
1570 assert_eq!(
1571 rows.iter().map(|r| (r.day.as_str(), r.cost, r.value)).collect::<Vec<_>>(),
1572 vec![("2026-10-30", 100, 120), ("2026-10-31", 150, 180), ("2026-11-01", 40, 48)]
1573 );
1574 }
1575
1576 #[test]
1577 fn a_plan_payment_is_spread_over_the_month_it_pays_for() {
1578 let days = spread("2026-10-01T00:00:00.000Z", 20_000_000, 30);
1579 assert_eq!(days.len(), 30);
1580 assert_eq!(days[0], ("2026-10-01".to_string(), 666_667));
1581 assert_eq!(days[29], ("2026-10-30".to_string(), 666_666));
1582 assert_eq!(days.iter().map(|d| d.1).sum::<i64>(), 20_000_000);
1583 assert!(spread("2026-10-01", 0, 30).is_empty());
1584 assert_eq!(dollars(17_024_000), "$17.02");
1585 assert_eq!(dollars(-27_668_620), "-$27.67");
1586 assert_eq!(dollars(63_000), "$0.063");
1587 }
1588
1589 #[test]
1590 fn margins_and_deltas() {
1591 assert_eq!(margin_percent(1_200_000, 1_000_000).map(|m| (m * 100.0).round() / 100.0), Some(16.67));
1592 assert_eq!(margin_percent(0, 5), None);
1593 assert_eq!(delta_percent(110.0, 100.0), Some(10.0));
1594 assert_eq!(delta_percent(1.0, 0.0), None);
1595 }
1596
1597 fn day(bucket: &str, cf: i64, own: i64, value: i64, cfq: f64, ownq: f64) -> ProductDay {
1598 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 }
1599 }
1600
1601 #[test]
1602 fn counts_more_than_the_threshold_apart_are_drift() {
1603 // Cloudflare counted 30,000 operations where g1t counted 10,000:
1604 // binding reads, perhaps. -66.7%.
1605 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);
1606 assert_eq!(drift.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Count, DriftKind::Cost]);
1607 assert!((drift[0].delta_percent.unwrap() + 66.666).abs() < 0.01);
1608 // 9% apart: within 10%.
1609 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());
1610 // Uncounted products have no count drift.
1611 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());
1612 }
1613
1614 #[test]
1615 fn cost_with_no_revenue_is_a_leak_but_not_for_running_g1t() {
1616 let leak = drifts("actions_cache", &[day("actions_cache", 400_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000);
1617 assert_eq!(leak.len(), 1);
1618 assert_eq!(leak[0].kind, DriftKind::Leak);
1619 assert!(drifts("platform", &[day("platform", 5_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
1620 // Pennies say nothing.
1621 assert!(drifts("actions_cache", &[day("actions_cache", 50_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
1622 assert!(drifts(UNMAPPED, &[day(UNMAPPED, 250_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000)[0].kind == DriftKind::Leak);
1623 }
1624
1625 #[test]
1626 fn a_margin_alert_needs_n_days_in_a_row_under_the_floor() {
1627 let s = |d: &str, revenue: i64, cost: i64| (d.to_string(), revenue, cost);
1628 // 5%, 0%, -20%: three days under 10%.
1629 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)];
1630 let (from, worst) = breach(&series, 10.0, 3, 100_000).unwrap();
1631 assert_eq!(from, "10-14");
1632 assert!((worst + 20.0).abs() < 1e-9);
1633 // A good day in the window clears it.
1634 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)];
1635 assert!(breach(&mended, 10.0, 3, 100_000).is_none());
1636 // Cost with no revenue at all is the worst margin there is.
1637 assert_eq!(breach(&[s("10-16", 0, 500_000)], 10.0, 1, 100_000).unwrap().1, -100.0);
1638 // Too little cost to judge.
1639 assert!(breach(&[s("10-16", 0, 5_000)], 10.0, 1, 100_000).is_none());
1640 assert!(breach(&series, 10.0, 9, 100_000).is_none());
1641 }
1642
1643 #[test]
1644 fn shared_costs_add_up_to_the_bill() {
1645 let w = |k: &str, v: f64| (k.to_string(), v);
1646 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)]);
1647 assert_eq!(attribute(10, &[w("a", 3.0), w("b", 1.0), w("a", 0.0)]), vec![("a".into(), 8), ("b".into(), 2)]);
1648 assert!(attribute(10, &[w("a", 0.0)]).is_empty());
1649 assert!(attribute(0, &[w("a", 1.0)]).is_empty());
1650 }
1651
1652 #[test]
1653 fn a_workspace_that_costs_more_than_it_pays_is_flagged() {
1654 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)];
1655 let found = anomalies(&rows, 1.0, 1_000_000);
1656 assert_eq!(found, vec![("acme".to_string(), 5_000_000, 1_000_000)]);
1657 // At twice its revenue as the threshold, $5 against $3 is fine.
1658 assert!(anomalies(&[("acme".to_string(), 5_000_000, 3_000_000)], 2.0, 1_000_000).is_empty());
1659 }
1660
1661 #[test]
1662 fn a_git_operation_costs_what_cloudflare_counts_for_it() {
1663 // $0.15 per 1,000 of Cloudflare's operations, on the charged days.
1664 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();
1665 assert!((rate - 0.000_15).abs() < 1e-12);
1666 // Cloudflare counted 3 for every 1 g1t did: binding reads count.
1667 let per_op = derived_unit_cost(rate, 300_000.0, 100_000.0).unwrap();
1668 let per_thousand_micros = per_op * unit_size("1,000 operations") * 1e6;
1669 assert!((per_thousand_micros - 450_000.0).abs() < 1e-6, "{per_thousand_micros}");
1670 // Too few of g1t's units to say.
1671 assert!(derived_unit_cost(rate, 3_000.0, 500.0).is_none());
1672 assert!(billed_rate(&[(10_000.0, 0.0)]).is_none());
1673 assert_eq!(unit_size("million requests"), 1e6);
1674 assert_eq!(unit_size("second"), 1.0);
1675 }
1676}