Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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