Skip to content
714 linesCodeBlameRaw
1//! What g1t pays for itself, and two caps on it.
2//!
3//! Every charge that settles (an agent run, sandbox time, a build) is
4//! split by what paid for it, at cost: a customer's real money, or g1t's.
5//! g1t's part goes to `g1t_spend` by day, bucket and billing account:
6//!
7//! - `comped`: work on a comped account (g1t's own, Flagon's), all of it.
8//! - `trial`, `oss`: the trial credit and the open-source pool.
9//! - `given`: a free workspace's overrun past its last bit of trial.
10//! - `unpaid`: charged, but with no real money behind it: Stripe's test
11//! key, or `FREE_WHILE_BUILDING`.
12//!
13//! The plan's included usage and on-demand charges with live payments are
14//! revenue, not g1t's. A workspace's own model provider costs g1t nothing.
15//!
16//! Two caps read it:
17//!
18//! 1. **A comped account's monthly budget**: `COMPED_MONTHLY_CEILING_MICROS`
19//! ($150), or the account's own limit in its terms (sudo, Accounts →
20//! Terms → Limit). Staff are emailed at 50, 75, 90 and 100%, once each a
21//! month; at 100% new work on it is refused until staff raise it or the
22//! month turns. Work already running finishes.
23//! 2. **The daily breaker**: when g1t's part across every workspace today
24//! (UTC) reaches `PLATFORM_DAILY_SPEND_CAP_MICROS` ($75), new agent runs
25//! on g1t's hosted models that g1t would pay for are paused for the rest
26//! of the day: everyone's except workspaces paying with real money on
27//! the plan or an enterprise contract. Staff are emailed at once and sudo
28//! shows a red bar; staff can lift it for the day.
29//!
30//! Zero for either variable turns that cap off. See
31//! docs/BILLING_OPERATIONS.md.
32
33use g1t_contracts::billing::{
34 AdminLiftBreakerArgs, BillingAccount, CompedBudget, ComputeKind, PlanKind, SpendBucket, SpendCaps,
35};
36use g1t_contracts::time::rfc3339;
37use g1t_contracts::{FailureCode, Outcome};
38use g1t_kit::now_ms;
39use serde::Deserialize;
40use worker::{Env, Result};
41
42use crate::Billing;
43use crate::credits::Drawn;
44use crate::limits::alert_level;
45
46/// The caps, from the billing service's variables.
47#[derive(Clone, Debug)]
48pub(crate) struct Caps {
49 /// `COMPED_MONTHLY_CEILING_MICROS`: a comped account's monthly budget
50 /// at cost, unless its terms set one. Zero: none.
51 pub comped_monthly: i64,
52 /// `PLATFORM_DAILY_SPEND_CAP_MICROS`: g1t's own spend a day before the
53 /// breaker trips. Zero: no breaker.
54 pub daily: i64,
55 /// `CLOUDFLARE_FIXED_MONTHLY_MICROS`: Cloudflare's subscriptions, an
56 /// estimate for sudo.
57 pub fixed_monthly: i64,
58 /// `COSTS_ALERT_EMAIL`. Empty: nothing is emailed.
59 pub alert_to: String,
60}
61
62impl Caps {
63 pub(crate) fn from_env(env: &Env) -> Self {
64 let number = |name: &str, default: i64| {
65 env.var(name).ok().and_then(|v| v.to_string().trim().parse::<i64>().ok()).unwrap_or(default).max(0)
66 };
67 Caps {
68 comped_monthly: number("COMPED_MONTHLY_CEILING_MICROS", 150_000_000),
69 daily: number("PLATFORM_DAILY_SPEND_CAP_MICROS", 75_000_000),
70 fixed_monthly: number("CLOUDFLARE_FIXED_MONTHLY_MICROS", 30_000_000),
71 alert_to: env.var("COSTS_ALERT_EMAIL").map(|v| v.to_string().trim().to_owned()).unwrap_or_default(),
72 }
73 }
74}
75
76/// g1t's part of one charge, at cost, by what paid for it.
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
78pub(crate) struct Share {
79 pub comped: i64,
80 pub trial: i64,
81 pub oss: i64,
82 pub given: i64,
83 pub unpaid: i64,
84}
85
86impl Share {
87 pub fn total(&self) -> i64 {
88 self.comped + self.trial + self.oss + self.given + self.unpaid
89 }
90
91 pub fn parts(&self) -> [(&'static str, i64); 5] {
92 [("comped", self.comped), ("trial", self.trial), ("oss", self.oss), ("given", self.given), ("unpaid", self.unpaid)]
93 }
94}
95
96/// What of a charge costing g1t `cost` g1t paid itself. `charged` is what
97/// the workspace was charged after `drawn` paid its part (both at price);
98/// `real_money` is whether payments are live. The plan's included usage
99/// and what the workspace is charged are revenue only with real money.
100pub(crate) fn share(cost: i64, charged: i64, drawn: &Drawn, comped: bool, real_money: bool) -> Share {
101 if cost <= 0 {
102 return Share::default();
103 }
104 if comped {
105 return Share { comped: cost, ..Share::default() };
106 }
107 let gross = charged.max(0) + drawn.total();
108 if gross <= 0 {
109 // Charged nothing at all (free while g1t is being built out).
110 return Share { unpaid: cost, ..Share::default() };
111 }
112 let part = |paid: i64| (i128::from(cost) * i128::from(paid.max(0)) / i128::from(gross)) as i64;
113 let (trial, oss, given) = (part(drawn.trial), part(drawn.oss), part(drawn.given));
114 let unpaid = if real_money { 0 } else { (cost - trial - oss - given).max(0) };
115 Share { comped: 0, trial, oss, given, unpaid }
116}
117
118/// A comped account's monthly budget: its own (terms' limit) or the
119/// default; and whether it is the default. Zero: none.
120pub(crate) fn comped_ceiling(own: Option<i64>, default: i64) -> (i64, bool) {
121 match own {
122 Some(own) => (own.max(0), false),
123 None => (default.max(0), true),
124 }
125}
126
127/// Whether a budget is used up.
128pub(crate) fn used_up(used: i64, ceiling: i64) -> bool {
129 ceiling > 0 && used >= ceiling
130}
131
132/// Whether the breaker stops new runs: on, reached, and not lifted today.
133pub(crate) fn breaker_open(today: i64, cap: i64, lifted: bool) -> bool {
134 cap > 0 && today >= cap && !lifted
135}
136
137/// Whether the breaker is about this start: an agent run on g1t's hosted
138/// models (an agent run that does not say is taken to be one).
139pub(crate) fn breaker_applies(kind: ComputeKind, hosted_model: Option<bool>) -> bool {
140 kind == ComputeKind::Agent && hosted_model.unwrap_or(true)
141}
142
143/// Whether a workspace's spend is covered by revenue, so the breaker
144/// leaves it alone: live payments, not comped, and on the plan it pays for
145/// (not given it by staff) or an enterprise contract.
146pub(crate) fn covered_by_revenue(plan: PlanKind, comped: bool, plan_given: bool, live: bool) -> bool {
147 live && !comped && match plan {
148 PlanKind::Enterprise => true,
149 PlanKind::Paid => !plan_given,
150 _ => false,
151 }
152}
153
154/// The alert to send now: the level reached, if higher than any sent this
155/// month.
156pub(crate) fn alert_to_send(level: u32, sent: u32) -> Option<u32> {
157 (level > 0 && level > sent).then_some(level)
158}
159
160/// `$150.00`: whole cents.
161pub(crate) fn cents(micros: i64) -> String {
162 let cents = (micros as f64 / 10_000.0).round() as i64;
163 format!("{}${}.{:02}", if cents < 0 { "-" } else { "" }, cents.abs() / 100, cents.abs() % 100)
164}
165
166/// What a start on a comped account past its budget is told. Staff-only:
167/// only comped (g1t's own) accounts see it.
168pub(crate) fn comped_refusal(name: &str, used: i64, ceiling: i64) -> String {
169 format!(
170 "{name}'s monthly budget for g1t's own agents is used up ({} of {} this month at cost), so new runs wait. Staff can raise it in sudo: Accounts, {name}, Terms, Limit.",
171 cents(used),
172 cents(ceiling)
173 )
174}
175
176/// What a hosted-model start is told while the breaker is open.
177pub(crate) fn breaker_refusal(today: i64, cap: i64) -> String {
178 format!(
179 "g1t's daily spend breaker is open: g1t has paid {} of its {} a day for work today, so new agent runs on g1t's hosted models wait until 00:00 UTC. Agents on the workspace's own model provider still run, and so does work on the paid plan.",
180 cents(today),
181 cents(cap)
182 )
183}
184
185#[derive(Deserialize)]
186struct Sum {
187 micros: Option<i64>,
188}
189
190#[derive(Deserialize)]
191struct BreakerRow {
192 tripped_at: Option<String>,
193 told_at: Option<String>,
194 lifted_by: Option<String>,
195 lifted_at: Option<String>,
196 lift_note: Option<String>,
197}
198
199fn today() -> String {
200 rfc3339(now_ms())[..10].to_owned()
201}
202
203impl Billing {
204 fn live(&self) -> bool {
205 self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live)
206 }
207
208 /// Counts g1t's part of a charge that just settled, and trips the
209 /// breaker if today reached its cap. Never fails the charge: a problem
210 /// here is logged.
211 pub(crate) async fn count_spend(&self, workspace: &str, cost: i64, charged: i64, drawn: &Drawn) {
212 if let Err(error) = self.try_count_spend(workspace, cost, charged, drawn).await {
213 worker::console_error!("could not count g1t's spend for {workspace}: {error}");
214 }
215 }
216
217 async fn try_count_spend(&self, workspace: &str, cost: i64, charged: i64, drawn: &Drawn) -> Result<()> {
218 if cost <= 0 {
219 return Ok(());
220 }
221 let account = self.account_of(workspace).await?;
222 let paid = share(cost, charged, drawn, account.terms.full_discount(), self.live());
223 if paid.total() == 0 {
224 return Ok(());
225 }
226 let day = today();
227 let mut writes = vec![];
228 for (bucket, micros) in paid.parts() {
229 if micros > 0 {
230 writes.push(
231 self.db
232 .prepare(
233 "INSERT INTO g1t_spend (day, bucket, account, micros) VALUES (?1, ?2, ?3, ?4)
234 ON CONFLICT (day, bucket, account) DO UPDATE SET micros = micros + ?4",
235 )
236 .bind(&[day.as_str().into(), bucket.into(), account.id.as_str().into(), (micros as f64).into()])?,
237 );
238 }
239 }
240 self.db.batch(writes).await?;
241 if self.caps.daily <= 0 {
242 return Ok(());
243 }
244 let total = self.spent_on(&day).await?;
245 if total < self.caps.daily {
246 return Ok(());
247 }
248 // Tripped: recorded once a day, and staff told at once.
249 let now = rfc3339(now_ms());
250 let tripped = self
251 .db
252 .prepare(
253 "INSERT INTO spend_breaker (day, tripped_at, tripped_micros) VALUES (?1, ?2, ?3)
254 ON CONFLICT (day) DO UPDATE SET tripped_at = ?2, tripped_micros = ?3 WHERE spend_breaker.tripped_at IS NULL
255 RETURNING day",
256 )
257 .bind(&[day.as_str().into(), now.as_str().into(), (total as f64).into()])?
258 .first::<serde_json::Value>(None)
259 .await?;
260 if tripped.is_some() {
261 self.tell_breaker(&day, total).await?;
262 }
263 Ok(())
264 }
265
266 /// g1t's own spend on `day`, across every workspace.
267 async fn spent_on(&self, day: &str) -> Result<i64> {
268 Ok(self
269 .db
270 .prepare("SELECT SUM(micros) AS micros FROM g1t_spend WHERE day = ?")
271 .bind(&[day.into()])?
272 .first::<Sum>(None)
273 .await?
274 .and_then(|s| s.micros)
275 .unwrap_or(0))
276 }
277
278 async fn breaker_row(&self, day: &str) -> Result<Option<BreakerRow>> {
279 self.db
280 .prepare("SELECT tripped_at, told_at, lifted_by, lifted_at, lift_note FROM spend_breaker WHERE day = ?")
281 .bind(&[day.into()])?
282 .first::<BreakerRow>(None)
283 .await
284 }
285
286 /// Emails staff that the breaker tripped, and notes it was told.
287 async fn tell_breaker(&self, day: &str, total: i64) -> Result<()> {
288 if self.caps.alert_to.is_empty() {
289 return Ok(());
290 }
291 let lifted = self.breaker_row(day).await?.and_then(|row| row.lifted_by);
292 let mut lines = vec![
293 format!(
294 "g1t paid {} for work today ({day}, UTC), its daily cap of {} (PLATFORM_DAILY_SPEND_CAP_MICROS). New agent runs on g1t's hosted models that g1t pays for are paused until 00:00 UTC; workspaces paying with real money, and agents on their own model provider, are not affected. Runs already going finish.",
295 cents(total),
296 cents(self.caps.daily)
297 ),
298 "To let them start again today: sudo, Costs & margin, Lift for today. To change the cap: PLATFORM_DAILY_SPEND_CAP_MICROS in services/billing/wrangler.jsonc.".to_owned(),
299 ];
300 if let Some(by) = lifted {
301 lines.insert(1, format!("{by} had already lifted it for today, so nothing is paused."));
302 }
303 match crate::margin::email_staff(&self.env, &self.caps.alert_to, &format!("g1t: the daily spend breaker tripped at {}", cents(total)), &lines).await {
304 Ok(()) => {
305 self.db
306 .prepare("UPDATE spend_breaker SET told_at = ? WHERE day = ?")
307 .bind(&[rfc3339(now_ms()).into(), day.into()])?
308 .run()
309 .await?;
310 }
311 Err(error) => worker::console_error!("could not email the breaker: {error}"),
312 }
313 Ok(())
314 }
315
316 /// Why a start is refused by the breaker, if it is.
317 pub(crate) async fn breaker_refuses(
318 &self,
319 plan: PlanKind,
320 account: &BillingAccount,
321 kind: ComputeKind,
322 hosted_model: Option<bool>,
323 ) -> Result<Option<String>> {
324 if self.caps.daily <= 0 || !breaker_applies(kind, hosted_model) {
325 return Ok(None);
326 }
327 let comped = account.terms.full_discount();
328 if covered_by_revenue(plan, comped, account.allowances.plan, self.live()) {
329 return Ok(None);
330 }
331 let day = today();
332 let spent = self.spent_on(&day).await?;
333 if spent < self.caps.daily {
334 return Ok(None);
335 }
336 let lifted = self.breaker_row(&day).await?.is_some_and(|row| row.lifted_at.is_some());
337 Ok(breaker_open(spent, self.caps.daily, lifted).then(|| breaker_refusal(spent, self.caps.daily)))
338 }
339
340 /// A comped account's budget this month.
341 pub(crate) async fn comped_budget(&self, account: &BillingAccount) -> Result<CompedBudget> {
342 let month = &rfc3339(now_ms())[..7];
343 let used = self
344 .db
345 .prepare("SELECT SUM(micros) AS micros FROM g1t_spend WHERE account = ? AND bucket = 'comped' AND day >= ?")
346 .bind(&[account.id.as_str().into(), format!("{month}-01").into()])?
347 .first::<Sum>(None)
348 .await?
349 .and_then(|s| s.micros)
350 .unwrap_or(0);
351 let (ceiling, default_ceiling) = comped_ceiling(account.terms.ceiling_micros, self.caps.comped_monthly);
352 Ok(CompedBudget {
353 account: account.id.clone(),
354 name: account.name.clone(),
355 used_micros: used,
356 ceiling_micros: ceiling,
357 default_ceiling,
358 level: alert_level(used, ceiling),
359 })
360 }
361
362 /// Why new work on a comped account is refused, if its budget is used
363 /// up. None for every other account.
364 pub(crate) async fn comped_stop(&self, account: &BillingAccount) -> Result<Option<String>> {
365 if !account.terms.full_discount() {
366 return Ok(None);
367 }
368 let budget = self.comped_budget(account).await?;
369 Ok(used_up(budget.used_micros, budget.ceiling_micros).then(|| comped_refusal(&account.name, budget.used_micros, budget.ceiling_micros)))
370 }
371
372 /// Every 15 minutes: comped budgets' alerts, once each level a month,
373 /// and a tripped breaker staff were not yet told about.
374 pub(crate) async fn watch_spend(&self) -> Result<()> {
375 if self.caps.alert_to.is_empty() {
376 return Ok(());
377 }
378 let month = rfc3339(now_ms())[..7].to_owned();
379 #[derive(Deserialize)]
380 struct Id {
381 id: String,
382 }
383 let comped = self
384 .db
385 .prepare(format!("SELECT id FROM billing_accounts WHERE {}", crate::sales::FULL_DISCOUNT_SQL))
386 .all()
387 .await?
388 .results::<Id>()?;
389 #[derive(Deserialize)]
390 struct Sent {
391 level: Option<i64>,
392 }
393 for Id { id } in comped {
394 let Some(account) = self.find_account(&id).await? else { continue };
395 let budget = self.comped_budget(&account).await?;
396 let sent = self
397 .db
398 .prepare("SELECT MAX(level) AS level FROM budget_alerts WHERE account = ? AND month = ?")
399 .bind(&[id.as_str().into(), month.as_str().into()])?
400 .first::<Sent>(None)
401 .await?
402 .and_then(|s| s.level)
403 .unwrap_or(0);
404 let Some(level) = alert_to_send(budget.level, u32::try_from(sent).unwrap_or(0)) else { continue };
405 let name = &account.name;
406 let mut lines = vec![format!(
407 "{name}'s work has cost g1t {} this month, {level}% of its {} monthly budget ({}).",
408 cents(budget.used_micros),
409 cents(budget.ceiling_micros),
410 if budget.default_ceiling { "COMPED_MONTHLY_CEILING_MICROS" } else { "its own limit, in its terms" }
411 )];
412 lines.push(if level >= 100 {
413 format!("New agent runs, checks and builds on {name} are refused until the budget is raised or the month turns. Runs already going finish. To raise it: sudo, Accounts, {name}, Terms, Limit.")
414 } else {
415 format!("At 100%, new work on {name} is refused until staff raise the budget. To raise it now: sudo, Accounts, {name}, Terms, Limit.")
416 });
417 let subject = format!("g1t: {name} has used {level}% of its monthly budget");
418 match crate::margin::email_staff(&self.env, &self.caps.alert_to, &subject, &lines).await {
419 Ok(()) => {
420 self.db
421 .prepare("INSERT OR IGNORE INTO budget_alerts (account, month, level, sent_at) VALUES (?, ?, ?, ?)")
422 .bind(&[id.as_str().into(), month.as_str().into(), level.into(), rfc3339(now_ms()).into()])?
423 .run()
424 .await?;
425 }
426 Err(error) => worker::console_error!("could not email {name}'s budget alert: {error}"),
427 }
428 }
429 // A trip whose email did not go out when it happened.
430 let day = today();
431 if self.breaker_row(&day).await?.is_some_and(|row| row.tripped_at.is_some() && row.told_at.is_none()) {
432 let total = self.spent_on(&day).await?;
433 self.tell_breaker(&day, total).await?;
434 }
435 Ok(())
436 }
437
438 /// `admin_spend_caps`: g1t's own spend against its caps.
439 pub(crate) async fn spend_caps(&self) -> Result<SpendCaps> {
440 let day = today();
441 let month = day[..7].to_owned();
442 let month_start = format!("{month}-01");
443 let today_micros = self.spent_on(&day).await?;
444 let row = self.breaker_row(&day).await?;
445 let lifted = row.as_ref().is_some_and(|r| r.lifted_at.is_some());
446 #[derive(Deserialize)]
447 struct Bucket {
448 bucket: String,
449 micros: Option<i64>,
450 }
451 let rows = self
452 .db
453 .prepare("SELECT bucket, SUM(micros) AS micros FROM g1t_spend WHERE day >= ? GROUP BY bucket")
454 .bind(&[month_start.as_str().into()])?
455 .all()
456 .await?
457 .results::<Bucket>()?;
458 let month_buckets = ["comped", "trial", "oss", "given", "unpaid"]
459 .iter()
460 .map(|bucket| SpendBucket {
461 bucket: (*bucket).to_owned(),
462 title: bucket_title(bucket).to_owned(),
463 micros: rows.iter().find(|r| r.bucket == *bucket).and_then(|r| r.micros).unwrap_or(0),
464 })
465 .collect();
466 #[derive(Deserialize)]
467 struct Id {
468 id: String,
469 }
470 let ids = self
471 .db
472 .prepare(format!("SELECT id FROM billing_accounts WHERE {} ORDER BY id", crate::sales::FULL_DISCOUNT_SQL))
473 .all()
474 .await?
475 .results::<Id>()?;
476 let mut comped = vec![];
477 for Id { id } in ids {
478 if let Some(account) = self.find_account(&id).await? {
479 comped.push(self.comped_budget(&account).await?);
480 }
481 }
482 // Free workspaces' share of the reconciled costs that are not on
483 // the ledger (models, sandboxes and builds are, above).
484 let free_tier_micros = self
485 .db
486 .prepare(format!(
487 "SELECT SUM(cost_micros) AS micros FROM workspace_costs
488 WHERE day >= ?1 AND bucket NOT IN ('models', 'sandboxes', 'deployments')
489 AND workspace NOT IN ({internal})
490 AND workspace NOT IN (SELECT workspace FROM workspace_costs WHERE day >= ?1 GROUP BY workspace HAVING SUM(revenue_micros) > 0)",
491 internal = crate::sales::INTERNAL_SQL
492 ))
493 .bind(&[month_start.as_str().into()])?
494 .first::<Sum>(None)
495 .await?
496 .and_then(|s| s.micros)
497 .unwrap_or(0);
498 let revenue_micros = self
499 .db
500 .prepare("SELECT SUM(cash_micros) AS micros FROM margin_days WHERE day >= ?")
501 .bind(&[month_start.as_str().into()])?
502 .first::<Sum>(None)
503 .await?
504 .and_then(|s| s.micros)
505 .unwrap_or(0);
506 // What a testing reset wiped from the ledger is still here.
507 #[derive(Deserialize)]
508 struct Reset {
509 account: String,
510 micros: Option<i64>,
511 }
512 let reset = self
513 .db
514 .prepare(RESET_SPEND_SQL)
515 .bind(&[month_start.as_str().into()])?
516 .all()
517 .await?
518 .results::<Reset>()?;
519 let reset_micros = reset.iter().filter_map(|r| r.micros).sum();
520 let reset_workspaces = reset.into_iter().map(|r| r.account.strip_prefix("ws_").unwrap_or(&r.account).to_owned()).collect();
521 let fixed = self.fixed_monthly(self.caps.fixed_monthly).await?;
522 Ok(SpendCaps {
523 reset_micros,
524 reset_workspaces,
525 day,
526 month,
527 today_micros,
528 daily_cap_micros: self.caps.daily,
529 tripped: breaker_open(today_micros, self.caps.daily, lifted),
530 tripped_at: row.as_ref().and_then(|r| r.tripped_at.clone()),
531 lifted_by: row.as_ref().and_then(|r| r.lifted_by.clone()),
532 lifted_at: row.as_ref().and_then(|r| r.lifted_at.clone()),
533 lift_note: row.as_ref().and_then(|r| r.lift_note.clone()),
534 month_buckets,
535 comped,
536 free_tier_micros,
537 fixed_monthly_micros: fixed.monthly_micros,
538 fixed_source: fixed.source.into(),
539 fixed_read_at: fixed.read_at,
540 fixed_items: fixed.items,
541 revenue_micros,
542 })
543 }
544
545 /// `admin_lift_breaker`: hosted-model runs start again for the rest of
546 /// today (UTC).
547 pub(crate) async fn admin_lift_breaker(&self, a: AdminLiftBreakerArgs) -> Result<Outcome<SpendCaps>> {
548 let (by, note) = (a.by.trim(), a.note.trim());
549 if by.is_empty() || note.len() < 5 {
550 return Ok(Outcome::fail(FailureCode::Invalid, "Say who is lifting it, and why, in the note."));
551 }
552 let day = today();
553 let now = rfc3339(now_ms());
554 let note: String = note.chars().take(500).collect();
555 self.db
556 .prepare(
557 "INSERT INTO spend_breaker (day, lifted_by, lifted_at, lift_note) VALUES (?1, ?2, ?3, ?4)
558 ON CONFLICT (day) DO UPDATE SET lifted_by = ?2, lifted_at = ?3, lift_note = ?4",
559 )
560 .bind(&[day.as_str().into(), by.into(), now.as_str().into(), note.as_str().into()])?
561 .run()
562 .await?;
563 let spent = self.spent_on(&day).await?;
564 self.audit("costs", "breaker_lifted", &format!("{day}: lifted at {} of {}: {note}", cents(spent), cents(self.caps.daily)), by)
565 .await?;
566 Ok(Outcome::Ok(self.spend_caps().await?))
567 }
568}
569
570/// g1t's own spend since `?1` on accounts a testing reset wiped later than
571/// the day it was spent (`admin_actions`, action `reset`), by account.
572pub(crate) const RESET_SPEND_SQL: &str = "SELECT s.account, SUM(s.micros) AS micros FROM g1t_spend s
573 WHERE s.day >= ?1 AND EXISTS (SELECT 1 FROM admin_actions a WHERE a.action = 'reset' AND a.account = s.account AND substr(a.created_at, 1, 10) >= s.day)
574 GROUP BY s.account HAVING SUM(s.micros) > 0 ORDER BY s.account";
575
576/// How sudo names a bucket of g1t's own spend.
577pub(crate) fn bucket_title(bucket: &str) -> &'static str {
578 match bucket {
579 "comped" => "100% discount (g1t's own)",
580 "trial" => "Trial pool",
581 "oss" => "Open-source pool",
582 "given" => "Free overruns g1t covered",
583 "unpaid" => "Charged without real money",
584 _ => "Other",
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 fn drawn(credit: i64, trial: i64, oss: i64, given: i64) -> Drawn {
593 Drawn { credit, trial, oss, given }
594 }
595
596 #[test]
597 fn comped_work_is_all_g1ts_at_cost() {
598 let s = share(1_000_000, 0, &Drawn::default(), true, true);
599 assert_eq!(s, Share { comped: 1_000_000, ..Share::default() });
600 // Nothing that cost nothing is counted.
601 assert_eq!(share(0, 0, &Drawn::default(), true, true).total(), 0);
602 assert_eq!(share(-5, 0, &Drawn::default(), false, false).total(), 0);
603 }
604
605 #[test]
606 fn pools_pay_their_share_of_the_cost_not_the_price() {
607 // $1 of cost charged at $1.20, all from the trial: $1 is g1t's.
608 assert_eq!(share(1_000_000, 0, &drawn(0, 1_200_000, 0, 0), false, true), Share { trial: 1_000_000, ..Share::default() });
609 // Half the open-source pool, half charged on a live card: half is g1t's.
610 assert_eq!(share(1_000_000, 600_000, &drawn(0, 0, 600_000, 0), false, true), Share { oss: 500_000, ..Share::default() });
611 // A free workspace's overrun past its trial.
612 let s = share(1_000_000, 0, &drawn(0, 300_000, 0, 900_000), false, true);
613 assert_eq!((s.trial, s.given), (250_000, 750_000));
614 }
615
616 #[test]
617 fn revenue_is_only_revenue_with_real_money() {
618 // Plan credit and an on-demand charge, live: none of it is g1t's.
619 assert_eq!(share(1_000_000, 600_000, &drawn(600_000, 0, 0, 0), false, true).total(), 0);
620 // The same in test mode: all of it.
621 assert_eq!(share(1_000_000, 600_000, &drawn(600_000, 0, 0, 0), false, false), Share { unpaid: 1_000_000, ..Share::default() });
622 // Free while building: charged nothing, all g1t's.
623 assert_eq!(share(1_000_000, 0, &Drawn::default(), false, true), Share { unpaid: 1_000_000, ..Share::default() });
624 }
625
626 #[test]
627 fn a_comped_account_gets_the_default_budget_unless_its_terms_set_one() {
628 assert_eq!(comped_ceiling(None, 150_000_000), (150_000_000, true));
629 assert_eq!(comped_ceiling(Some(400_000_000), 150_000_000), (400_000_000, false));
630 // Zero: no budget.
631 assert_eq!(comped_ceiling(None, 0), (0, true));
632 assert!(!used_up(1_000_000_000, 0));
633 }
634
635 #[test]
636 fn a_comped_budget_refuses_new_work_at_one_hundred_percent() {
637 let ceiling = 150_000_000;
638 assert!(!used_up(149_999_999, ceiling));
639 assert!(used_up(150_000_000, ceiling));
640 assert!(used_up(151_000_000, ceiling));
641 let message = comped_refusal("flagon-io", 150_000_000, ceiling);
642 assert!(message.contains("used up") && message.contains("$150.00 of $150.00") && message.contains("sudo"), "{message}");
643 }
644
645 #[test]
646 fn budget_alerts_go_once_per_level_each_month() {
647 let ceiling = 150_000_000;
648 let mut sent = 0;
649 let mut emailed = vec![];
650 // Spend climbs through the month, checked every 15 minutes.
651 for used in [10_000_000, 74_000_000, 75_000_000, 80_000_000, 112_500_000, 120_000_000, 135_000_000, 140_000_000, 150_000_000, 170_000_000] {
652 if let Some(level) = alert_to_send(alert_level(used, ceiling), sent) {
653 emailed.push(level);
654 sent = level;
655 }
656 }
657 assert_eq!(emailed, vec![50, 75, 90, 100]);
658 // A jump straight past several levels sends only the highest.
659 assert_eq!(alert_to_send(alert_level(140_000_000, ceiling), 0), Some(90));
660 // A new month starts from nothing sent.
661 assert_eq!(alert_to_send(alert_level(80_000_000, ceiling), 0), Some(50));
662 }
663
664 #[test]
665 fn the_breaker_trips_at_the_cap_and_staff_can_lift_it_for_the_day() {
666 let cap = 75_000_000;
667 assert!(!breaker_open(74_999_999, cap, false));
668 assert!(breaker_open(75_000_000, cap, false));
669 // Lifted: open no more today.
670 assert!(!breaker_open(90_000_000, cap, true));
671 // Off.
672 assert!(!breaker_open(1_000_000_000, 0, false));
673 // Tomorrow's total starts at zero: the breaker resets by itself.
674 assert!(!breaker_open(0, cap, false));
675 let message = breaker_refusal(80_000_000, cap);
676 assert!(message.contains("$80.00 of its $75.00") && message.contains("00:00 UTC"), "{message}");
677 }
678
679 #[test]
680 fn the_breaker_is_about_hosted_model_agent_runs() {
681 assert!(breaker_applies(ComputeKind::Agent, Some(true)));
682 assert!(breaker_applies(ComputeKind::Agent, None));
683 assert!(!breaker_applies(ComputeKind::Agent, Some(false)));
684 assert!(!breaker_applies(ComputeKind::Check, None));
685 assert!(!breaker_applies(ComputeKind::Workflow, Some(true)));
686 }
687
688 #[test]
689 fn workspaces_paying_with_real_money_are_never_paused_by_the_breaker() {
690 assert!(covered_by_revenue(PlanKind::Paid, false, false, true));
691 assert!(covered_by_revenue(PlanKind::Enterprise, false, false, true));
692 // Test-mode payments are not money.
693 assert!(!covered_by_revenue(PlanKind::Paid, false, false, false));
694 // The plan given by staff, comped, free: g1t pays.
695 assert!(!covered_by_revenue(PlanKind::Paid, false, true, true));
696 assert!(!covered_by_revenue(PlanKind::Internal, true, false, true));
697 assert!(!covered_by_revenue(PlanKind::Free, false, false, true));
698 }
699
700 #[test]
701 fn spend_a_testing_reset_wiped_is_found_by_the_reset_after_it() {
702 // syntaqx's $7.41 of 2026-10-02 to 10-05, reset on 10-07: still
703 // g1t's spend, gone from its ledger.
704 assert_eq!(crate::rename::parameters(RESET_SPEND_SQL), 1);
705 assert!(RESET_SPEND_SQL.contains("a.action = 'reset'") && RESET_SPEND_SQL.contains("substr(a.created_at, 1, 10) >= s.day"));
706 }
707
708 #[test]
709 fn amounts_read_in_cents() {
710 assert_eq!(cents(150_000_000), "$150.00");
711 assert_eq!(cents(1_234_567), "$1.23");
712 assert_eq!(cents(5_000), "$0.01");
713 }
714}