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
1,687 linesCodeBlameRaw
1//! How far a workspace can run up costs g1t has not been paid for, and how
2//! far its owners let it spend.
3//!
4//! Every sandbox second, build, app request and model token costs g1t
5//! money at Cloudflare or a model provider before the workspace pays for
6//! it. So each workspace has a ceiling on that unpaid usage:
7//!
8//! - **Free**: a few dollars (`LIMIT_NEW_MICROS`), for what a free
9//! workspace can owe at all (private storage past 1 GB). Free workspaces
10//! have no on-demand compute: the trial and g1t's pools pay for it.
11//! - **Paid, first month**: `LIMIT_PAID_START_MICROS` ($100) while the plan
12//! is in its first billing cycle.
13//! - **Paid, after**: twice what it has paid g1t once payments clear
14//! (`SETTLE_DAYS`), never less than the starting ceiling; after three
15//! steady months it follows the monthly spend, up to $10,000.
16//! - **Reviewed**: a ceiling g1t staff set by hand.
17//! - **Internal**: g1t's own workspaces, with none here: what their work
18//! costs g1t has a monthly budget instead (see `budget`).
19//!
20//! A ceiling g1t granted (an approved request, or the owners' one-time
21//! raise) is a floor under the trust ceiling. Money paid in advance raises
22//! what can be used before work stops by the same amount, at once: it comes
23//! off what is owed before anything counts against the ceiling.
24//!
25//! Owners also set a monthly **spend limit** on what is charged: the
26//! month's usage at price less the discount, included usage, pools and
27//! credit, with month-end usage counted on the same terms (`charged`, the
28//! one definition the Billing page and Usage share). They may
29//! put it anywhere up to the highest ceiling the workspace has ever had,
30//! plus what is prepaid, without asking anyone; once per workspace they may
31//! raise it to twice that highest ceiling themselves. Past that, they ask
32//! (see `requests`), and g1t answers within one business day.
33//!
34//! Alerts go out at 50, 75, 90 and 100% of the plan's included usage, the
35//! spend limit and the ceiling, in the app and by email. At the ceiling or
36//! the spend limit, new work stops: no new sandboxes, builds or app
37//! requests until it is paid, raised, or the month turns. Runs already
38//! under way finish.
39//!
40//! Usage counts at what it cost g1t or what it is charged, whichever is
41//! more. Test-mode payments are not money, so they do not raise trust.
42
43use std::collections::BTreeSet;
44
45use futures_util::future::{try_join, try_join3, try_join4, try_join_all};
46use g1t_contracts::billing::{
47 BillingAccount, CheckLimitArgs, Limit, LimitArgs, LimitState, NotePendingArgs, PlanKind, SetBudgetArgs, SetSpendLimitArgs, Trust,
48};
49use g1t_contracts::time::rfc3339;
50use g1t_contracts::{FailureCode, Outcome};
51use g1t_kit::now_ms;
52use serde::Deserialize;
53use worker::wasm_bindgen::JsValue;
54use worker::{Env, Result};
55
56use crate::charged::{PendingSplit, cover_with_included, month_totals, pending_split};
57use crate::features::dollars as dollars_plain;
58use crate::{Billing, members_only};
59
60/// A month's usage on the ledger, in the parts `charged` is made of.
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
62struct MonthCharges {
63 /// At price.
64 price: i64,
65 /// What the account's discount took off.
66 discount: i64,
67 /// What the plan's included usage, the trial, a pool or g1t paid.
68 covered: i64,
69 /// Last month's charge, as the ledger has it, for the automatic limit.
70 last_month: i64,
71}
72
73/// The ceilings, from the billing service's variables.
74pub(crate) struct Ceilings {
75 /// `LIMIT_NEW_MICROS`: a free workspace's.
76 pub new: i64,
77 /// `LIMIT_PAID_MIN_MICROS` and `LIMIT_PAID_MAX_MICROS`: the bounds of a
78 /// paid workspace's, from what it has paid.
79 pub paid_min: i64,
80 pub paid_max: i64,
81}
82
83impl Ceilings {
84 pub(crate) fn from_env(env: &Env) -> Self {
85 let number = |name: &str, default: i64| {
86 env.var(name).ok().and_then(|v| v.to_string().parse::<i64>().ok()).unwrap_or(default)
87 };
88 Ceilings {
89 new: number("LIMIT_NEW_MICROS", 3_000_000),
90 paid_min: number("LIMIT_PAID_MIN_MICROS", 25_000_000),
91 paid_max: number("LIMIT_PAID_MAX_MICROS", 1_000_000_000),
92 }
93 }
94
95 /// The ceiling for a workspace that has paid `paid` in live money.
96 pub(crate) fn for_paid(&self, paid: i64) -> i64 {
97 (paid * 2).clamp(self.paid_min, self.paid_max)
98 }
99}
100
101/// Where a workspace stands against its ceiling.
102pub(crate) fn state(exposure: i64, ceiling: Option<i64>) -> LimitState {
103 match ceiling {
104 Some(ceiling) if exposure >= ceiling => LimitState::Stopped,
105 Some(ceiling) if exposure * 5 >= ceiling * 4 => LimitState::Warning,
106 _ => LimitState::Ok,
107 }
108}
109
110/// A limit as the services that run things read it (`check_limit`): a
111/// staff hold (abuse, a suspension) stops the workspace's running apps as
112/// well as new compute, so it reads as stopped, with the reason.
113pub(crate) fn held(mut limit: Limit, hold: Option<&str>) -> Limit {
114 if let Some(why) = hold_message(&limit.workspace, hold) {
115 limit.state = LimitState::Stopped;
116 limit.message = Some(why);
117 }
118 limit
119}
120
121/// Why a hold stops the workspace, if there is one.
122pub(crate) fn hold_message(workspace: &str, hold: Option<&str>) -> Option<String> {
123 hold.map(str::trim).filter(|h| !h.is_empty()).map(|hold| {
124 format!("g1t staff put a hold on {workspace} ({hold}), so its sandboxes, builds and apps are stopped. Write to g1t support to have it lifted.")
125 })
126}
127
128/// Whether autopay charges the card now: something is owed on the ledger
129/// (`collectable`, the exposure less usage not entered until the month
130/// closes), and it is within a fifth of the ceiling.
131pub(crate) fn autopay_due(collectable: i64, ceiling: Option<i64>) -> bool {
132 collectable > 0 && ceiling.is_some_and(|ceiling| collectable * 5 >= ceiling * 4)
133}
134
135/// The automatic monthly spend limit's floor: $200.
136pub(crate) const DEFAULT_SPEND_MICROS: i64 = 200_000_000;
137/// Established workspaces' ceiling: three times their steady monthly
138/// spend, up to $10,000.
139const ESTABLISHED_FACTOR: i64 = 3;
140const ESTABLISHED_MAX_MICROS: i64 = 10_000_000_000;
141/// A month counts toward Established at this much spend or more.
142const ESTABLISHED_MONTH_MICROS: i64 = 20_000_000;
143/// Payments raise trust once this old: past the time most bad cards are
144/// caught.
145pub(crate) const SETTLE_DAYS: u64 = 7;
146
147/// The automatic spend limit: $200, or twice last month's spend.
148pub(crate) fn automatic_spend_limit(last_month_charged: i64) -> i64 {
149 DEFAULT_SPEND_MICROS.max(last_month_charged * 2)
150}
151
152/// An Established workspace's ceiling, from its last three months'
153/// charges, if each was steady enough.
154pub(crate) fn established_ceiling(months: &[i64]) -> Option<i64> {
155 if months.len() < 3 || months.iter().any(|m| *m < ESTABLISHED_MONTH_MICROS) {
156 return None;
157 }
158 let average = months.iter().sum::<i64>() / months.len() as i64;
159 Some((average * ESTABLISHED_FACTOR).min(ESTABLISHED_MAX_MICROS))
160}
161
162/// Days since 1970-01-01 of a `YYYY-MM-DD…` date, for comparing dates
163/// without a clock (Howard Hinnant's days-from-civil).
164pub(crate) fn days(date: &str) -> i64 {
165 let year: i64 = date.get(..4).and_then(|y| y.parse().ok()).unwrap_or(1970);
166 let month: i64 = date.get(5..7).and_then(|m| m.parse().ok()).unwrap_or(1);
167 let day: i64 = date.get(8..10).and_then(|d| d.parse().ok()).unwrap_or(1);
168 let y = if month <= 2 { year - 1 } else { year };
169 let era = y.div_euclid(400);
170 let yoe = y - era * 400;
171 let mp = (month + 9) % 12;
172 let doy = (153 * mp + 2) / 5 + day - 1;
173 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
174 era * 146_097 + doe - 719_468
175}
176
177/// Whether a plan that started at `started_at` is still in its first
178/// billing cycle: its paid period ends no more than a month after it
179/// started (a renewal moves the end a month on), or, with no period known,
180/// it started within the last 31 days.
181pub(crate) fn in_first_cycle(started_at: &str, period_end: Option<&str>, now: &str) -> bool {
182 match period_end {
183 Some(end) => days(end) - days(started_at) <= 32 && days(now) <= days(end),
184 None => days(now) - days(started_at) <= 31,
185 }
186}
187
188/// When a workspace's history as a payer starts: its first live plan
189/// payment ever (`first_paid`), or this plan's start if that is earlier or
190/// it has paid none yet.
191pub(crate) fn trust_start<'a>(started_at: &'a str, first_paid: Option<&'a str>) -> &'a str {
192 match first_paid {
193 Some(paid) if paid < started_at => paid,
194 _ => started_at,
195 }
196}
197
198/// g1t's ceiling for a workspace on the plan: the starting one in its
199/// first month; after it, what it has paid (or its Established ceiling),
200/// never less than the starting one.
201pub(crate) fn paid_ceiling(ceilings: &Ceilings, start: i64, first_month: bool, paid: i64, established: Option<i64>) -> i64 {
202 if first_month {
203 return start;
204 }
205 let from_paid = if paid > 0 { ceilings.for_paid(paid) } else { 0 };
206 start.max(from_paid).max(established.unwrap_or(0))
207}
208
209/// What the owners may set their spend limit to without asking, and the
210/// one-time raise if it is still theirs to use: up to the highest ceiling
211/// ever (or the current one, if higher) plus what is prepaid; once, twice
212/// the highest ceiling.
213pub(crate) fn spend_bounds(ceiling: i64, max_ever: i64, prepaid: i64, raised: bool) -> (i64, Option<i64>) {
214 let highest = ceiling.max(max_ever);
215 let available = highest + prepaid.max(0);
216 let once = (!raised).then(|| (highest * 2 + prepaid.max(0)).max(available));
217 (available, once)
218}
219
220/// Whether `requested` is a spend limit the owners may set themselves.
221/// `Ok(true)` when it takes the one-time raise.
222pub(crate) fn self_serve(requested: i64, available: i64, once: Option<i64>, raise_once: bool) -> std::result::Result<bool, String> {
223 if requested < 0 {
224 return Err("A spend limit cannot be negative.".to_owned());
225 }
226 if requested <= available {
227 return Ok(false);
228 }
229 match once {
230 Some(once) if raise_once && requested <= once => Ok(true),
231 Some(once) if raise_once => Err(format!(
232 "The one-time raise goes up to {}. For more, ask g1t with Raise my limit; the answer comes within one business day.",
233 dollars_plain(once)
234 )),
235 Some(once) => Err(format!(
236 "You can set up to {} yourself, or use your one-time raise to go up to {}. For more, ask g1t with Raise my limit.",
237 dollars_plain(available),
238 dollars_plain(once)
239 )),
240 None => Err(format!(
241 "You can set up to {} yourself, and the one-time raise is used. For more, ask g1t with Raise my limit; the answer comes within one business day.",
242 dollars_plain(available)
243 )),
244 }
245}
246
247/// Which alert a measure has reached: 100, 90, 75, 50, or none (0).
248pub(crate) fn alert_level(used: i64, limit: i64) -> u32 {
249 alert_level_in(used, limit, &ALERT_LEVELS)
250}
251
252/// Every alert a budget can have, highest first.
253pub(crate) const ALERT_LEVELS: [u32; 4] = [100, 90, 75, 50];
254
255/// The highest of `levels` a measure has reached, or 0.
256pub(crate) fn alert_level_in(used: i64, limit: i64, levels: &[u32]) -> u32 {
257 if limit <= 0 || used <= 0 {
258 return 0;
259 }
260 levels.iter().copied().filter(|level| used * 100 >= limit * i64::from(*level)).max().unwrap_or(0)
261}
262
263/// A budget's alerts as stored (`50,75,100`): every level when none were
264/// chosen, highest first.
265pub(crate) fn alert_levels(stored: Option<&str>) -> Vec<u32> {
266 let Some(stored) = stored else { return ALERT_LEVELS.to_vec() };
267 let mut levels: Vec<u32> = stored.split(',').filter_map(|l| l.trim().parse().ok()).filter(|l| ALERT_LEVELS.contains(l)).collect();
268 levels.sort_by(|a, b| b.cmp(a));
269 levels.dedup();
270 levels
271}
272
273/// Where spending stands against the budget: at 100% it stops work only
274/// when the budget pauses usage; otherwise it is a warning.
275pub(crate) fn budget_state(spent: i64, budget: Option<i64>, pause: bool) -> LimitState {
276 match state(spent, budget) {
277 LimitState::Stopped if !pause => LimitState::Warning,
278 other => other,
279 }
280}
281
282/// Whether a budget webhook is an address g1t will post to: HTTPS, not
283/// g1t's own, at most 500 characters.
284pub(crate) fn webhook_ok(url: &str) -> bool {
285 let url = url.trim();
286 url.len() <= 500
287 && url.starts_with("https://")
288 && url.len() > "https://".len() + 3
289 && !url.contains(char::is_whitespace)
290 && !url["https://".len()..].split('/').next().is_some_and(|host| host == "g1t.sh" || host.ends_with(".g1t.sh") || host.starts_with("localhost") || host.starts_with("127."))
291}
292
293/// What is owed and what is prepaid, from this month's usage and payments
294/// and the balance the month started with (positive: paid in advance;
295/// negative: owed from before).
296pub(crate) fn exposure(used: i64, paid_month: i64, balance_before: i64) -> (i64, i64) {
297 let prepaid_in = balance_before.max(0);
298 let carried = (-balance_before).max(0);
299 let net = used - paid_month - prepaid_in;
300 (net.max(0) + carried, (-net).max(0))
301}
302
303#[derive(Deserialize)]
304struct LimitRow {
305 spend_limit_micros: Option<i64>,
306 #[serde(default)]
307 spend_limit_full: Option<i64>,
308 autopay_failed_at: Option<String>,
309 autopay_error: Option<String>,
310 #[serde(default)]
311 max_ceiling_micros: Option<i64>,
312 #[serde(default)]
313 granted_ceiling_micros: Option<i64>,
314 #[serde(default)]
315 raised_at: Option<String>,
316 #[serde(default)]
317 alert_levels: Option<String>,
318 #[serde(default)]
319 pause_at_limit: Option<i64>,
320 #[serde(default)]
321 budget_webhook: Option<String>,
322}
323
324#[derive(Deserialize)]
325struct Month {
326 used: Option<i64>,
327 paid: Option<i64>,
328}
329
330#[derive(Deserialize)]
331struct Paid {
332 paid: Option<i64>,
333}
334
335impl Billing {
336 /// The workspace's limit, worked out from the ledger of the account
337 /// that pays for it: its own, or its enterprise's, whose workspaces'
338 /// usage and payments count together.
339 pub(crate) async fn limit_of(&self, workspace: &str) -> Result<Limit> {
340 let workspace = workspace.to_lowercase();
341 let account = self.account_of(&workspace).await?;
342 let plan = self.plan_kind_for(&workspace, &account).await?;
343 self.limit_with(&workspace, &account, plan).await
344 }
345
346 /// The workspace's limit, from the account and plan already read for
347 /// it, so a caller that has them does not read them again.
348 pub(crate) async fn limit_with(&self, workspace: &str, account: &BillingAccount, plan: PlanKind) -> Result<Limit> {
349 Ok(self.limit_parts(workspace, account, plan).await?.0)
350 }
351
352 /// `limit_with`, and how much of its exposure is usage metered this
353 /// month and not on the ledger yet: counted against the ceiling, but
354 /// nothing a card can be charged for until the month closes.
355 async fn limit_parts(&self, workspace: &str, account: &BillingAccount, plan: PlanKind) -> Result<(Limit, i64)> {
356 let workspace = workspace.to_lowercase();
357 let now = rfc3339(now_ms());
358 let month_start = format!("{}-01", &now[..7]);
359 let marks = vec!["?"; account.workspaces.len().max(1)].join(", ");
360 let names: Vec<String> = if account.workspaces.is_empty() { vec![workspace.clone()] } else { account.workspaces.clone() };
361 let members: Vec<JsValue> = names.iter().map(|w| JsValue::from(w.as_str())).collect();
362 let row = async {
363 self.db
364 .prepare(
365 "SELECT spend_limit_micros, spend_limit_full, autopay_failed_at, autopay_error,
366 max_ceiling_micros, granted_ceiling_micros, raised_at, alert_levels, pause_at_limit, budget_webhook
367 FROM limits WHERE workspace = ?",
368 )
369 .bind(&[workspace.as_str().into()])?
370 .first::<LimitRow>(None)
371 .await
372 };
373 let mut with_month = members.clone();
374 with_month.push(month_start.as_str().into());
375 // Each usage entry at its cost to g1t or its charge, whichever is
376 // more; on the workspace's own provider, only what g1t charged.
377 // What the plan's included usage, the trial, the open-source pool
378 // or g1t itself paid for is not unpaid: those are budgets already
379 // paid for.
380 let month = async {
381 self.db
382 .prepare(format!(
383 "SELECT
384 SUM(CASE WHEN kind = 'usage' THEN
385 CASE WHEN COALESCE(billed_to, 'g1t') = 'g1t'
386 THEN MAX(COALESCE(cost_micros, 0) - COALESCE(credit_micros, 0) - COALESCE(gift_micros, 0)
387 - COALESCE(trial_micros, 0) - COALESCE(oss_micros, 0)
388 - COALESCE(given_micros, 0),
389 -amount_micros)
390 ELSE -amount_micros END
391 END) AS used,
392 SUM(CASE WHEN kind = 'top_up' THEN amount_micros END) AS paid
393 FROM ledger WHERE workspace IN ({marks}) AND created_at >= ?"
394 ))
395 .bind(&with_month)?
396 .first::<Month>(None)
397 .await
398 };
399 // And what is metered but not charged until the month closes: it
400 // counts toward what is unpaid at its charge, and toward what is
401 // charged on the account's terms (see `charged`).
402 let pending = self.pending_this_month(&members, &month_start[..7]);
403 // What credit (AI credit, credit from g1t) paid this month: charged
404 // to no one, so not against the spend limit. Read only for
405 // workspaces with grants; the rest answer at once.
406 let next_month = crate::credits::next_month_start(&month_start[..7]);
407 let credits = try_join_all(names.iter().map(|name| {
408 let (from, until) = (month_start.as_str(), next_month.as_str());
409 async move { Ok::<i64, worker::Error>(self.credit_paid_between(name, from, until).await?.0) }
410 }));
411 // Test-mode payments are not money: they pay nothing off.
412 let live = self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live);
413 // The balance the month started with: owed from before (so a new
414 // month is not a fresh allowance for an account that never pays),
415 // or paid in advance. Credits g1t gave count; test-mode payments
416 // do not.
417 // Payments count only from the day payments went live
418 // (`payments_live_since`): test-mode ones from before the switch
419 // pay nothing off. None counts until that day is set.
420 let live_since = if live { self.payments_live_since().await? } else { None };
421 // In the statement's order: the day, the workspaces, the month.
422 let mut before: Vec<JsValue> = vec![live_since.as_deref().unwrap_or("9999").into()];
423 before.extend(members.iter().cloned());
424 before.push(month_start.as_str().into());
425 let balance_before = async {
426 Ok::<i64, worker::Error>(
427 self.db
428 .prepare(format!(
429 "SELECT SUM(CASE WHEN kind = 'usage' THEN amount_micros
430 WHEN kind = 'top_up' AND reference LIKE 'crd%' THEN amount_micros
431 WHEN kind = 'top_up' AND {live} = 1 AND created_at >= ? THEN amount_micros
432 ELSE 0 END) AS paid
433 FROM ledger WHERE workspace IN ({marks}) AND created_at < ?",
434 live = u8::from(live_since.is_some())
435 ))
436 .bind(&before)?
437 .first::<Paid>(None)
438 .await?
439 .and_then(|row| row.paid)
440 .unwrap_or(0),
441 )
442 };
443 // The trust ceiling, from what has been paid and how steadily. The
444 // first month is asked for beside it, since neither needs the other.
445 let trust = async {
446 Ok::<_, worker::Error>(match account.terms.kind {
447 _ if account.terms.full_discount() => (Trust::Internal, None, false),
448 _ if account.terms.ceiling_micros.is_some() => (Trust::Reviewed, account.terms.ceiling_micros, false),
449 _ => {
450 let standing = async {
451 let paid = self.live_paid(&members).await?;
452 let established = if paid > 0 { self.established(&members).await? } else { None };
453 Ok::<_, worker::Error>((paid, established))
454 };
455 let first = async {
456 if plan == PlanKind::Paid { self.first_month(&workspace).await } else { Ok(false) }
457 };
458 let ((paid, established), first_month) = try_join(standing, first).await?;
459 if plan == PlanKind::Free {
460 // Nothing on demand: only what a free workspace can owe.
461 (Trust::New, Some(self.ceilings.new), false)
462 } else {
463 let ceiling = paid_ceiling(&self.ceilings, self.plans.paid_start_micros, first_month, paid, established);
464 (if established.is_some() { Trust::Established } else { Trust::Paid }, Some(ceiling), first_month)
465 }
466 }
467 })
468 };
469 // This month's charges on the ledger, measured as every page
470 // measures them, and last month's, for the automatic spend limit.
471 let charges = self.month_charges(&members, &month_start);
472 // What each workspace's plan still has of its included usage this
473 // month: the close pays pending usage from it first.
474 let included = try_join_all(names.iter().map(|name| {
475 let month = &month_start[..7];
476 async move {
477 if plan == PlanKind::Free {
478 return Ok::<_, worker::Error>((name.clone(), 0));
479 }
480 let used = self.allowance_used("plan_credit", name, month).await?;
481 Ok((name.clone(), crate::credits::left(self.plans.plan_included_micros, used)))
482 }
483 }));
484 // None of these reads needs another's answer, so they go to D1 at
485 // once: the limit is on every signed-in page.
486 let ((row, month, balance_before, charges), (pending, credits, included), (trust, trust_ceiling, first_month)) =
487 try_join3(try_join4(row, month, balance_before, charges), try_join3(pending, credits, included), trust).await?;
488 // Charged this month: the one definition (`charged`), so this is
489 // the Billing page's and Usage's figure, to the micro.
490 let percent = account.terms.percent_off();
491 let mut splits: Vec<PendingSplit> = pending.iter().map(|p| pending_split(p.cost(), p.charge(), self.margin_percent, percent)).collect();
492 // Pending usage is paid from what is left of each workspace's
493 // included usage before anything of it is charged.
494 let mut paid_by_plan = vec![0; splits.len()];
495 for (name, left) in &included {
496 let at: Vec<usize> = (0..pending.len()).filter(|i| pending[*i].workspace == *name).collect();
497 let mut theirs: Vec<PendingSplit> = at.iter().map(|i| splits[*i]).collect();
498 for (k, pay) in cover_with_included(&mut theirs, *left).into_iter().enumerate() {
499 splits[at[k]] = theirs[k];
500 paid_by_plan[at[k]] = pay;
501 }
502 }
503 let totals = month_totals(charges.price, charges.discount, charges.covered, credits.into_iter().sum(), &splits);
504 let spent = totals.charged_micros;
505 let last_month = charges.last_month;
506 let (used, paid_month) = month.map_or((0, 0), |m| (m.used.unwrap_or(0), m.paid.unwrap_or(0)));
507 let pending_unpaid: i64 = pending.iter().zip(&paid_by_plan).map(|(p, plan_pays)| (p.charge() - plan_pays).max(0)).sum();
508 let used = used + pending_unpaid;
509 let (exposure, prepaid) = exposure(used, if live { paid_month } else { 0 }, balance_before);
510
511 // A ceiling g1t granted is a floor under the trust ceiling.
512 let granted = row.as_ref().and_then(|row| row.granted_ceiling_micros);
513 let ceiling = trust_ceiling.map(|c| c.max(granted.unwrap_or(0)));
514 // The highest ceiling ever, kept as it rises.
515 let stored_max = row.as_ref().and_then(|row| row.max_ceiling_micros);
516 let max_ever = match (stored_max, ceiling) {
517 (Some(stored), Some(now)) => Some(stored.max(now)),
518 (stored, now) => stored.or(now),
519 };
520 if let (Some(max), true) = (max_ever, ceiling.is_some() && max_ever != stored_max && plan != PlanKind::Free) {
521 self.db
522 .prepare(
523 "INSERT INTO limits (workspace, max_ceiling_micros, updated_at) VALUES (?1, ?2, ?3)
524 ON CONFLICT (workspace) DO UPDATE SET max_ceiling_micros = MAX(COALESCE(max_ceiling_micros, 0), ?2), updated_at = ?3",
525 )
526 .bind(&[workspace.as_str().into(), (max as f64).into(), now.as_str().into()])?
527 .run()
528 .await?;
529 }
530 let raised_at = row.as_ref().and_then(|row| row.raised_at.clone());
531 let self_serve = matches!(trust, Trust::New | Trust::Paid | Trust::Established) && plan != PlanKind::Free;
532 let (available, raise_once) = match (ceiling, self_serve) {
533 (Some(ceiling), true) => {
534 let (available, once) = spend_bounds(ceiling, max_ever.unwrap_or(ceiling), prepaid, raised_at.is_some());
535 (Some(available), once)
536 }
537 (ceiling, _) => (ceiling, None),
538 };
539 // The owners' own monthly limit: theirs, none, or the automatic one
540 // ($200, or twice last month), which self-serve workspaces start on.
541 let chosen = row.as_ref().and_then(|row| row.spend_limit_micros);
542 let full = row.as_ref().and_then(|row| row.spend_limit_full).unwrap_or(0) == 1;
543 let default_spend_limit = chosen.is_none() && !full && self_serve;
544 let spend_limit = match (chosen, full) {
545 (Some(own), _) => Some(own),
546 (None, true) => None,
547 (None, false) if self_serve => Some(automatic_spend_limit(last_month)),
548 _ => None,
549 };
550 // A card declined when g1t charged it at the limit stops work until
551 // it is paid; any payment clears it.
552 let declined = row.as_ref().and_then(|row| row.autopay_failed_at.clone().map(|at| (at, row.autopay_error.clone())));
553 // Two limits: g1t's on what is unpaid, the owners' on what is spent.
554 let risk = state(exposure, ceiling);
555 // A budget that does not pause usage only alerts: at 100% it is a
556 // warning, never a stop. g1t's own ceiling still stops work.
557 let pause = row.as_ref().and_then(|row| row.pause_at_limit).unwrap_or(1) != 0;
558 let budget = budget_state(spent, spend_limit, pause);
559 let over_budget = budget == LimitState::Stopped;
560 let state = if (declined.is_some() && exposure > 0) || risk == LimitState::Stopped || over_budget {
561 LimitState::Stopped
562 } else if risk == LimitState::Warning || budget == LimitState::Warning {
563 LimitState::Warning
564 } else {
565 LimitState::Ok
566 };
567 let who = if account.kind == g1t_contracts::billing::AccountKind::Enterprise {
568 format!("The {} enterprise, which pays for {workspace},", account.name)
569 } else {
570 format!("The {workspace} workspace")
571 };
572 let billing = format!("/{workspace}/-/billing");
573 let message = match state {
574 LimitState::Ok => None,
575 LimitState::Warning if budget == LimitState::Warning && !pause => Some(format!(
576 "{who} has spent {} of its {} monthly budget. Usage does not pause at the budget; an owner can change that at {billing}.",
577 dollars_plain(spent),
578 dollars_plain(spend_limit.unwrap_or_default()),
579 )),
580 LimitState::Warning if budget == LimitState::Warning => Some(format!(
581 "{who} has spent {} of its {} monthly spend limit. At the limit, its sandboxes, builds and apps stop until the month turns or an owner raises it at {billing}.",
582 dollars_plain(spent),
583 dollars_plain(spend_limit.unwrap_or_default()),
584 )),
585 LimitState::Warning => Some(format!(
586 "{who} has {} of usage not yet paid for, of the {} g1t allows. With a card on file g1t charges it now; prepaying at {billing} raises what it can use at once.",
587 dollars_plain(exposure),
588 dollars_plain(ceiling.unwrap_or_default()),
589 )),
590 LimitState::Stopped if declined.is_some() => Some(format!(
591 "{who} could not be charged for its usage ({}), so its sandboxes, builds and apps are stopped. An owner can pay with another card at {billing}.",
592 declined.as_ref().and_then(|(_, error)| error.clone()).unwrap_or_else(|| "the card was declined".to_owned()),
593 )),
594 LimitState::Stopped => Some(if over_budget {
595 format!(
596 "{who} reached its {} monthly spend limit, so its sandboxes, builds and apps are stopped until the month turns. An owner can raise it at {billing}.",
597 dollars_plain(spend_limit.unwrap_or_default()),
598 )
599 } else {
600 format!(
601 "{who} reached its {} limit for usage not yet paid for, so its sandboxes, builds and apps are stopped. An owner can pay or prepay, or ask for a higher limit, at {billing}.",
602 dollars_plain(ceiling.unwrap_or_default()),
603 )
604 }),
605 };
606 let growth = match trust {
607 Trust::New => Some("Free workspaces have no on-demand usage: the g1t plan starts at a $100 limit.".to_owned()),
608 Trust::Paid if first_month => Some(format!(
609 "Your first month's limit is {}. After it, the limit grows to twice what you have paid as payments clear ({SETTLE_DAYS} days), up to $1,000. Prepaying raises it at once, and you can ask for more.",
610 dollars_plain(self.plans.paid_start_micros)
611 )),
612 Trust::Paid => Some(format!(
613 "Grows to twice what you have paid, as payments clear ({SETTLE_DAYS} days), up to $1,000. After three steady months it follows your monthly spend, up to $10,000, by itself. Prepaying raises it at once."
614 )),
615 Trust::Established => Some("Follows your monthly spend, up to $10,000, by itself. Prepaying raises it at once, and you can ask for more.".to_owned()),
616 Trust::Reviewed | Trust::Internal => None,
617 };
618 Ok((Limit {
619 workspace,
620 account: account.id.clone(),
621 account_name: account.name.clone(),
622 spent_micros: spent,
623 default_spend_limit,
624 available_micros: available,
625 growth,
626 trust,
627 exposure_micros: exposure,
628 ceiling_micros: ceiling,
629 trust_ceiling_micros: trust_ceiling,
630 spend_limit_micros: spend_limit,
631 state,
632 message,
633 prepaid_micros: prepaid,
634 max_ceiling_micros: max_ever.filter(|_| plan != PlanKind::Free),
635 raise_once_micros: raise_once,
636 raised_at,
637 first_month,
638 alert_levels: alert_levels(row.as_ref().and_then(|row| row.alert_levels.as_deref())),
639 pause_at_limit: pause,
640 budget_webhook: row.as_ref().and_then(|row| row.budget_webhook.clone()),
641 }, pending_unpaid.min(exposure)))
642 }
643
644 /// Whether the workspace's plan is in its first billing cycle.
645 /// Counted from the first live plan payment ever, not from this
646 /// subscription's start: a workspace that paid before, ended its plan
647 /// and came back keeps its history rather than starting over.
648 pub(crate) async fn first_month(&self, workspace: &str) -> Result<bool> {
649 let Some((started_at, period_end)) = self.plan_cycle(workspace).await? else { return Ok(false) };
650 let first_paid = if self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live) {
651 #[derive(Deserialize)]
652 struct First {
653 paid_at: Option<String>,
654 }
655 self.db
656 .prepare("SELECT MIN(paid_at) AS paid_at FROM plan_payments WHERE workspace = ? AND amount_micros > 0")
657 .bind(&[workspace.into()])?
658 .first::<First>(None)
659 .await?
660 .and_then(|first| first.paid_at)
661 } else {
662 None
663 };
664 Ok(in_first_cycle(trust_start(&started_at, first_paid.as_deref()), period_end.as_deref(), &rfc3339(now_ms())))
665 }
666
667 /// Real money the workspaces have paid g1t, cleared: usage payments and
668 /// the plan's price. Nothing in test mode, and credits g1t gave are not
669 /// payments.
670 pub(crate) async fn live_paid(&self, members: &[JsValue]) -> Result<i64> {
671 if !self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live) {
672 return Ok(0);
673 }
674 // Only what was paid since payments went live: test-mode payments
675 // from before the switch are not money. Nothing until the day is set.
676 let Some(live_since) = self.payments_live_since().await? else { return Ok(0) };
677 let marks = vec!["?"; members.len().max(1)].join(", ");
678 let settled = rfc3339(now_ms() - SETTLE_DAYS * 24 * 60 * 60 * 1000);
679 let mut bound = members.to_vec();
680 bound.push(live_since.as_str().into());
681 bound.extend_from_slice(members);
682 bound.push(live_since.as_str().into());
683 Ok(self
684 .db
685 .prepare(format!(
686 "SELECT
687 (SELECT COALESCE(SUM(amount_micros), 0) FROM ledger
688 WHERE workspace IN ({marks}) AND kind = 'top_up' AND reference NOT LIKE 'crd%' AND created_at >= ?
689 AND (amount_micros < 0
690 OR (disputed = 0 AND COALESCE(funding, '') <> 'prepaid'
691 AND created_at <= '{settled}')))
692 + (SELECT COALESCE(SUM(amount_micros), 0) FROM plan_payments
693 WHERE workspace IN ({marks}) AND paid_at >= ?
694 AND (amount_micros < 0 OR paid_at <= '{settled}')) AS paid"
695 ))
696 .bind(&bound)?
697 .first::<Paid>(None)
698 .await?
699 .and_then(|row| row.paid)
700 .unwrap_or(0))
701 }
702
703 /// This month's usage on the ledger across the workspaces, in the parts
704 /// `charged` is made of (at price, the discount, what included usage,
705 /// the trial, a pool or g1t paid), and last month's charge.
706 async fn month_charges(&self, members: &[JsValue], month_start: &str) -> Result<MonthCharges> {
707 #[derive(Deserialize)]
708 struct Row {
709 price: Option<f64>,
710 discount: Option<f64>,
711 covered: Option<f64>,
712 last_month: Option<f64>,
713 }
714 let last_start = format!("{}-01", previous_month(&month_start[..7]));
715 let marks = vec!["?"; members.len().max(1)].join(", ");
716 let row = self
717 .db
718 .prepare(format!(
719 "SELECT
720 SUM(CASE WHEN created_at >= '{month_start}' THEN {price} END) AS price,
721 SUM(CASE WHEN created_at >= '{month_start}' THEN COALESCE(discount_micros, 0) END) AS discount,
722 SUM(CASE WHEN created_at >= '{month_start}' THEN COALESCE(credit_micros, 0) + COALESCE(gift_micros, 0) + COALESCE(trial_micros, 0)
723 + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0) END) AS covered,
724 -SUM(CASE WHEN created_at >= '{last_start}' AND created_at < '{month_start}' THEN amount_micros END) AS last_month
725 FROM ledger WHERE kind = 'usage' AND workspace IN ({marks}) AND created_at >= '{last_start}'",
726 price = crate::statement::PRICE_SQL
727 ))
728 .bind(members)?
729 .first::<Row>(None)
730 .await?;
731 let whole = |n: Option<f64>| n.unwrap_or(0.0).round() as i64;
732 Ok(row.map_or(MonthCharges::default(), |r| MonthCharges {
733 price: whole(r.price),
734 discount: whole(r.discount),
735 covered: whole(r.covered),
736 last_month: whole(r.last_month).max(0),
737 }))
738 }
739
740 /// An Established ceiling, if the workspaces have paid steadily: three
741 /// full months of real spend, each invoiced and paid, nothing declined
742 /// in 90 days and nothing ever disputed.
743 async fn established(&self, members: &[JsValue]) -> Result<Option<i64>> {
744 let marks = vec!["?"; members.len().max(1)].join(", ");
745 let now = rfc3339(now_ms());
746 let mut months = vec![];
747 let mut month = previous_month(&now[..7]);
748 for _ in 0..3 {
749 months.push(month.clone());
750 month = previous_month(&month);
751 }
752 #[derive(Deserialize)]
753 struct Count {
754 n: Option<i64>,
755 }
756 let troubled = async {
757 Ok::<i64, worker::Error>(
758 self.db
759 .prepare(format!(
760 "SELECT (SELECT COUNT(*) FROM ledger WHERE workspace IN ({marks}) AND disputed = 1)
761 + (SELECT COUNT(*) FROM limits WHERE workspace IN ({marks}) AND autopay_failed_at >= '{since}') AS n",
762 since = rfc3339(now_ms() - 90 * 24 * 60 * 60 * 1000)
763 ))
764 .bind(&[members, members].concat())?
765 .first::<Count>(None)
766 .await?
767 .and_then(|c| c.n)
768 .unwrap_or(0),
769 )
770 };
771 #[derive(Deserialize)]
772 struct Month {
773 charged: Option<i64>,
774 unpaid: Option<i64>,
775 }
776 let read_month = |month: &String| {
777 let next = {
778 let year: i32 = month[..4].parse().unwrap_or(1970);
779 let number: u32 = month[5..7].parse().unwrap_or(1);
780 if number == 12 { format!("{}-01", year + 1) } else { format!("{year}-{:02}", number + 1) }
781 };
782 let sql = format!(
783 "SELECT
784 (SELECT -SUM(amount_micros) FROM ledger WHERE kind = 'usage' AND workspace IN ({marks})
785 AND created_at >= '{month}-01' AND created_at < '{next}-01') AS charged,
786 (SELECT COUNT(*) FROM workspace_invoices WHERE workspace IN ({marks}) AND reason = 'month'
787 AND period = '{month}' AND status <> 'paid') AS unpaid"
788 );
789 async move { self.db.prepare(sql).bind(&[members, members].concat())?.first::<Month>(None).await }
790 };
791 // The check for trouble and the three months are read at once; the
792 // answer is the one reading them in turn and stopping early gives.
793 let (troubled, rows) = try_join(troubled, try_join_all(months.iter().map(read_month))).await?;
794 if troubled > 0 {
795 return Ok(None);
796 }
797 let mut charged = vec![];
798 for row in rows {
799 let Some(row) = row else { return Ok(None) };
800 if row.unpaid.unwrap_or(0) > 0 {
801 return Ok(None);
802 }
803 charged.push(row.charged.unwrap_or(0));
804 }
805 Ok(established_ceiling(&charged))
806 }
807
808 pub(crate) async fn limit(&self, a: LimitArgs) -> Result<Outcome<Limit>> {
809 let workspace = a.workspace.to_lowercase();
810 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
811 return Ok(members_only());
812 }
813 Ok(Outcome::Ok(self.limit_of(&workspace).await?))
814 }
815
816 /// What a source cost so far in a month: the month the reporter names
817 /// (this one, or the one before while it is still open, see
818 /// `pending_month`), else this one. `security`, `context`, `storage`
819 /// and `git` are charged by billing once the month is over (see
820 /// `storage`); `deployments` charges its own. A report refused is
821 /// logged and answered `false`; nothing else is touched.
822 pub(crate) async fn note_pending(&self, a: NotePendingArgs) -> Result<bool> {
823 // The actions cache is the plan's to pay for; free workspaces are
824 // held to its quota instead.
825 if crate::storage::PLAN_ONLY.contains(&a.source.as_str()) && !self.has_plan(&a.workspace.to_lowercase()).await? {
826 return Ok(false);
827 }
828 let now = rfc3339(now_ms());
829 let (month, previous) = match pending_month(a.month.as_deref(), &now[..7]) {
830 Ok(month) => month,
831 Err(why) => {
832 worker::console_warn!("note_pending refused for {} ({}): {why}", a.workspace, a.source);
833 return Ok(false);
834 }
835 };
836 if previous {
837 let workspace = a.workspace.to_lowercase();
838 #[derive(Deserialize)]
839 struct Open {
840 charged: Option<i64>,
841 closed: Option<i64>,
842 }
843 let open = self
844 .db
845 .prepare(PREVIOUS_OPEN)
846 .bind(&[workspace.as_str().into(), a.source.as_str().into(), month.as_str().into()])?
847 .first::<Open>(None)
848 .await?;
849 let (charged, closed) = open.map_or((0, 0), |o| (o.charged.unwrap_or(0), o.closed.unwrap_or(0)));
850 if charged > 0 || closed > 0 {
851 // Late: what it adds is charged as a correction on that month,
852 // once for any figure (`late_correction`).
853 let detail = a.detail.as_deref().map(str::trim).filter(|d| !d.is_empty()).map(|d| d.chars().take(200).collect::<String>());
854 let taken = self.late_correction(&workspace, &a.source, &month, a.cost_micros, detail.as_deref()).await?;
855 if !taken {
856 let why = if closed > 0 { "the month is closed" } else { "the month's figure is charged already" };
857 worker::console_warn!("note_pending refused for {workspace} ({}) in {month}: {why}", a.source);
858 }
859 return Ok(taken);
860 }
861 }
862 let detail = a.detail.as_deref().map(str::trim).filter(|d| !d.is_empty()).map(|d| d.chars().take(200).collect::<String>());
863 self.set_pending(&a.workspace, &a.source, &month, a.cost_micros, detail.as_deref()).await?;
864 Ok(true)
865 }
866
867 /// The workspaces with usage on the ledger this month: autopay's and
868 /// the limit warnings' candidates, read once a tick for both. Served by
869 /// `ledger_usage_by_time` (migration 0050), not a scan of the ledger.
870 pub(crate) async fn month_users(&self) -> Result<BTreeSet<String>> {
871 #[derive(Deserialize)]
872 struct User {
873 workspace: String,
874 }
875 let month_start = format!("{}-01", &rfc3339(now_ms())[..7]);
876 Ok(self
877 .db
878 .prepare("SELECT DISTINCT workspace FROM ledger WHERE kind = 'usage' AND created_at >= ?")
879 .bind(&[month_start.into()])?
880 .all()
881 .await?
882 .results::<User>()?
883 .into_iter()
884 .map(|user| user.workspace)
885 .collect())
886 }
887
888 /// Charges the saved card of each workspace nearing its limit, for what
889 /// it owes, so that a workspace that pays never has its work stopped.
890 /// Only with live payments: test-mode payments are not money and lower
891 /// nothing. Not for a workspace's own spend limit, which means stop, nor
892 /// for enterprises, which are invoiced. A charge at the limit always
893 /// goes through, whatever the minimum charge.
894 ///
895 /// `users` are the workspaces with usage this month ([`Self::month_users`]).
896 pub(crate) async fn autopay(&self, users: &BTreeSet<String>) -> Result<()> {
897 if !self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live) {
898 return Ok(());
899 }
900 #[derive(Deserialize)]
901 struct Candidate {
902 workspace: String,
903 }
904 // With a card, and not already declined: a declined card waits for
905 // the owners, rather than being tried again every few minutes.
906 let candidates = self
907 .db
908 .prepare(
909 "SELECT accounts.workspace AS workspace
910 FROM accounts LEFT JOIN limits ON limits.workspace = accounts.workspace
911 WHERE accounts.customer_id IS NOT NULL AND limits.autopay_failed_at IS NULL
912 ORDER BY accounts.workspace",
913 )
914 .all()
915 .await?
916 .results::<Candidate>()?
917 .into_iter()
918 .filter(|candidate| users.contains(&candidate.workspace));
919 // Each workspace on its own: one the card processor fails for is
920 // logged and tried again next run, and never stops the rest.
921 for candidate in candidates {
922 let workspace = candidate.workspace.as_str();
923 let charged = async {
924 let account = self.account_of(workspace).await?;
925 let plan = self.plan_kind_for(workspace, &account).await?;
926 let (limit, pending) = self.limit_parts(workspace, &account, plan).await?;
927 // Near g1t's ceiling on what is unpaid; the spend limit is the
928 // owners' and stops work by itself, but what is owed is still owed.
929 // Only what is on the ledger can be charged now: usage metered
930 // and not entered until the month closes does not count toward
931 // it, and with nothing owed there is nothing to charge.
932 let collectable = limit.exposure_micros - pending;
933 if !autopay_due(collectable, limit.ceiling_micros) || limit.trust == Trust::Internal || limit.account.starts_with("ent_") {
934 return Ok(());
935 }
936 let today = rfc3339(now_ms())[..10].to_owned();
937 if let Err(why) = self.invoice_workspace(workspace, "threshold", &today).await? {
938 worker::console_log!("{workspace}: no threshold invoice: {why}");
939 }
940 Ok::<(), worker::Error>(())
941 };
942 if let Err(error) = charged.await {
943 worker::console_error!("{workspace}: paying at the limit failed; tried again next run: {error}");
944 }
945 }
946 Ok(())
947 }
948
949 /// Closes last month for each workspace with a card on file: charges
950 /// what it owed when the month ended. Live payments only, once per
951 /// workspace and month; a declined card stops work until it is paid.
952 /// Comped workspaces owe nothing, and enterprises are invoiced. Only
953 /// here does the minimum charge apply: less carries over.
954 pub(crate) async fn close_months(&self) -> Result<()> {
955 if !self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live) {
956 return Ok(());
957 }
958 // Not before last month's final count of git operations: a
959 // workspace whose usage passed the free amount in the month's last
960 // hours has no pending row yet, so nothing else would hold it.
961 if !self.last_month_final().await? {
962 return Ok(());
963 }
964 let now = rfc3339(now_ms());
965 let month_start = format!("{}-01", &now[..7]);
966 let closing = previous_month(&now[..7]);
967 #[derive(Deserialize)]
968 struct Open {
969 workspace: String,
970 balance: Option<i64>,
971 }
972 // Twenty a run, in a stable order: those never tried first, then
973 // those retrying, tried longest ago first, so one the card processor
974 // keeps failing for never holds the others back. Not while a month
975 // that is over still has metered usage to go on the ledger
976 // (`charge_pending`, a few at a tick): the close would charge
977 // without it and the rest would wait a month.
978 let sources = crate::storage::CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
979 let open = self
980 .db
981 .prepare(format!(
982 "SELECT accounts.workspace AS workspace,
983 (SELECT SUM(amount_micros) FROM ledger
984 WHERE ledger.workspace = accounts.workspace AND ledger.created_at < ?1) AS balance
985 FROM accounts
986 LEFT JOIN month_closes retrying ON retrying.workspace = accounts.workspace AND retrying.month = ?2
987 AND retrying.status = 'retrying'
988 WHERE accounts.customer_id IS NOT NULL
989 AND NOT EXISTS (SELECT 1 FROM month_closes
990 WHERE month_closes.workspace = accounts.workspace AND month_closes.month = ?2
991 AND month_closes.status <> 'retrying')
992 AND NOT EXISTS (SELECT 1 FROM pending_usage
993 WHERE pending_usage.workspace = accounts.workspace AND pending_usage.month < ?3
994 AND pending_usage.charged_at IS NULL AND pending_usage.source IN ({sources}))
995 ORDER BY retrying.closed_at IS NOT NULL, retrying.closed_at, accounts.workspace
996 LIMIT 20"
997 ))
998 .bind(&[month_start.as_str().into(), closing.as_str().into(), now[..7].into()])?
999 .all()
1000 .await?
1001 .results::<Open>()?;
1002 for account in open {
1003 // Once a month is closed it stays as closed; only a retry is
1004 // replaced, by the next attempt's outcome.
1005 let record = |status: &str, amount: i64, payment: Option<&str>, error: Option<&str>| {
1006 self.db
1007 .prepare(
1008 "INSERT INTO month_closes (workspace, month, status, amount_micros, payment_id, error, closed_at)
1009 VALUES (?, ?, ?, ?, ?, ?, ?)
1010 ON CONFLICT (workspace, month) DO UPDATE SET status = excluded.status, amount_micros = excluded.amount_micros,
1011 payment_id = excluded.payment_id, error = excluded.error, closed_at = excluded.closed_at
1012 WHERE month_closes.status = 'retrying'",
1013 )
1014 .bind(&[
1015 account.workspace.as_str().into(),
1016 closing.as_str().into(),
1017 status.into(),
1018 (amount as f64).into(),
1019 crate::optional(payment),
1020 crate::optional(error),
1021 now.as_str().into(),
1022 ])
1023 };
1024 // Each workspace on its own: a failure is logged and kept as a
1025 // retry, and the rest still close.
1026 let closed = async {
1027 let payer = self.account_of(&account.workspace).await?;
1028 if payer.terms.full_discount() || payer.id.starts_with("ent_") {
1029 record("skipped", 0, None, None)?.run().await?;
1030 return Ok(());
1031 }
1032 // AI credit left at the month's end pays only for models: not
1033 // money for anything else that is owed (ai.rs).
1034 let ai_left = self.models_left_before(&account.workspace, &month_start).await?;
1035 let owed = (ai_left - account.balance.unwrap_or(0)).max(0);
1036 if owed == 0 {
1037 record("nothing", 0, None, None)?.run().await?;
1038 return Ok(());
1039 }
1040 if !worth_charging(owed, self.plans.min_charge_micros) {
1041 // Under the minimum charge: a card payment's fee would be
1042 // too much of it. It stays owed and goes on the next
1043 // invoice that reaches the minimum.
1044 record("carried", owed, None, None)?.run().await?;
1045 return Ok(());
1046 }
1047 // Stripe failing to answer is an `Err`: the month is not
1048 // recorded as owing nothing, and the next run tries again.
1049 match self.invoice_workspace(&account.workspace, "month", &closing).await? {
1050 Ok(invoice) if invoice.status == "paid" => {
1051 record("paid", invoice.amount_micros, Some(&invoice.invoice_id), None)?.run().await?;
1052 }
1053 Ok(invoice) => {
1054 // Declined, or no card Stripe can charge: the
1055 // reason as the decline recorded it.
1056 let why = self.autopay_error(&account.workspace).await?.unwrap_or_else(|| "the card was declined".to_owned());
1057 record("failed", invoice.amount_micros, Some(&invoice.invoice_id), Some(&why))?.run().await?;
1058 }
1059 Err(why) => {
1060 record("nothing", 0, None, Some(&why))?.run().await?;
1061 }
1062 }
1063 Ok::<(), worker::Error>(())
1064 };
1065 if let Err(error) = closed.await {
1066 worker::console_error!("{}: closing {closing} failed; tried again next run: {error}", account.workspace);
1067 let why: String = error.to_string().chars().take(300).collect();
1068 if let Err(error) = async { record("retrying", 0, None, Some(&why))?.run().await }.await {
1069 worker::console_error!("{}: the retry for {closing} was not kept: {error}", account.workspace);
1070 }
1071 }
1072 }
1073 Ok(())
1074 }
1075
1076 /// Emails a workspace's owners as it passes 50, 75, 90 and 100% of its
1077 /// plan's included usage, its spend limit and g1t's ceiling, once each a
1078 /// month; when its card was declined; and when a spend spike paused it.
1079 /// The same alerts show in the app (`entitlements`).
1080 ///
1081 /// `users` are the workspaces with usage this month ([`Self::month_users`]).
1082 pub(crate) async fn warn_limits(&self, identity: &worker::Fetcher, users: &BTreeSet<String>) -> Result<()> {
1083 if !self.charging {
1084 return Ok(());
1085 }
1086 let now = rfc3339(now_ms());
1087 let month = &now[..7];
1088 #[derive(Deserialize)]
1089 struct Candidate {
1090 workspace: String,
1091 }
1092 let mut candidates = users.clone();
1093 candidates.extend(
1094 self.db
1095 .prepare("SELECT workspace FROM limits WHERE autopay_failed_at IS NOT NULL")
1096 .all()
1097 .await?
1098 .results::<Candidate>()?
1099 .into_iter()
1100 .map(|candidate| candidate.workspace),
1101 );
1102 #[derive(Deserialize)]
1103 struct Told {
1104 autopay_failed_at: Option<String>,
1105 declined_told_at: Option<String>,
1106 }
1107 #[derive(Deserialize)]
1108 struct Sent {
1109 meter: String,
1110 level: Option<i64>,
1111 }
1112 for workspace in candidates {
1113 let told = self
1114 .db
1115 .prepare("SELECT autopay_failed_at, declined_told_at FROM limits WHERE workspace = ?")
1116 .bind(&[workspace.as_str().into()])?
1117 .first::<Told>(None)
1118 .await?;
1119 let billing = format!("https://g1t.sh/{workspace}/-/billing");
1120
1121 // A declined card, once per decline.
1122 if let Some(Told { autopay_failed_at: Some(failed), declined_told_at }) = &told
1123 && declined_told_at.as_deref().is_none_or(|at| at < failed.as_str()) {
1124 let limit = self.limit_of(&workspace).await?;
1125 let sent = notify(
1126 identity,
1127 &workspace,
1128 &format!("g1t: the card for {workspace} was declined"),
1129 &limit.message.clone().unwrap_or_else(|| format!("g1t could not charge the card on file for {workspace}.")),
1130 "Update the card",
1131 &billing,
1132 )
1133 .await;
1134 if sent {
1135 self.db
1136 .prepare("UPDATE limits SET declined_told_at = ? WHERE workspace = ?")
1137 .bind(&[now.as_str().into(), workspace.as_str().into()])?
1138 .run()
1139 .await?;
1140 }
1141 }
1142
1143 // 50, 75, 90 and 100%, once each a month and meter: only the
1144 // highest new level is emailed.
1145 let alerts = self.alerts_for(&workspace).await?;
1146 if alerts.is_empty() {
1147 continue;
1148 }
1149 let sent: Vec<Sent> = self
1150 .db
1151 .prepare("SELECT meter, MAX(level) AS level FROM alerts_sent WHERE workspace = ? AND month = ? GROUP BY meter")
1152 .bind(&[workspace.as_str().into(), month.into()])?
1153 .all()
1154 .await?
1155 .results::<Sent>()?;
1156 for alert in alerts {
1157 let already = sent.iter().find(|s| s.meter == alert.meter).and_then(|s| s.level).unwrap_or(0);
1158 if i64::from(alert.level) <= already {
1159 continue;
1160 }
1161 let subject = match alert.meter.as_str() {
1162 "included" => format!("g1t: {workspace} has used {}% of its included usage", alert.level),
1163 "spend_limit" => format!("g1t: {workspace} has used {}% of its spend limit", alert.level),
1164 _ => format!("g1t: {workspace} has used {}% of its usage limit", alert.level),
1165 };
1166 // The budget's webhook hears of its alerts too, once each.
1167 if alert.meter == "spend_limit"
1168 && let Some(url) = self.budget_webhook_of(&workspace).await?
1169 {
1170 self.post_budget_webhook(&url, &workspace, alert.level, alert.used_micros, alert.limit_micros).await;
1171 }
1172 if notify(identity, &workspace, &subject, &alert.message, "Open billing", &billing).await {
1173 self.db
1174 .prepare(
1175 "INSERT OR IGNORE INTO alerts_sent (workspace, month, meter, level, sent_at) VALUES (?1, ?2, ?3, ?4, ?5)",
1176 )
1177 .bind(&[workspace.as_str().into(), month.into(), alert.meter.as_str().into(), alert.level.into(), now.as_str().into()])?
1178 .run()
1179 .await?;
1180 }
1181 }
1182 }
1183 self.tell_spikes(identity).await?;
1184 Ok(())
1185 }
1186
1187 pub(crate) async fn check_limit(&self, a: CheckLimitArgs) -> Result<Outcome<Limit>> {
1188 let workspace = a.workspace.to_lowercase();
1189 let account = self.account_of(&workspace).await?;
1190 let plan = self.plan_kind_for(&workspace, &account).await?;
1191 let limit = self.limit_with(&workspace, &account, plan).await?;
1192 Ok(Outcome::Ok(held(limit, account.allowances.hold.as_deref())))
1193 }
1194
1195 /// The owners' spend limit: anywhere up to what is available without
1196 /// asking; once, up to twice the highest ceiling (`raise_once`), which
1197 /// also raises g1t's ceiling to match.
1198 pub(crate) async fn set_spend_limit(&self, a: SetSpendLimitArgs) -> Result<Outcome<Limit>> {
1199 let workspace = a.workspace.to_lowercase();
1200 if !a.actor.manages_billing(&workspace) {
1201 return Ok(Outcome::fail(
1202 FailureCode::Forbidden,
1203 "Only an owner or a billing manager can set the workspace's spend limit.",
1204 ));
1205 }
1206 let before = self.limit_of(&workspace).await?;
1207 // A free workspace has no on-demand usage to limit: its ceiling is
1208 // only what it can owe for storage, and no raise applies to it.
1209 if before.trust == Trust::New {
1210 return Ok(Outcome::fail(
1211 FailureCode::Conflict,
1212 format!("{workspace} is not on the g1t plan, so it has no usage to put a spend limit on. The plan starts with a {} limit for its first month.", dollars_plain(self.plans.paid_start_micros)),
1213 ));
1214 }
1215 let mut raising = false;
1216 if let (Some(requested), false) = (a.spend_limit_micros, a.use_full_limit) {
1217 match (before.available_micros, matches!(before.trust, Trust::Internal | Trust::Reviewed)) {
1218 (_, true) | (None, _) => {
1219 if requested < 0 {
1220 return Ok(Outcome::fail(FailureCode::Invalid, "A spend limit cannot be negative."));
1221 }
1222 }
1223 (Some(available), false) => match self_serve(requested, available, before.raise_once_micros, a.raise_once) {
1224 Ok(uses_raise) => raising = uses_raise,
1225 Err(why) => return Ok(Outcome::fail(FailureCode::Invalid, why)),
1226 },
1227 }
1228 }
1229 let now = rfc3339(now_ms());
1230 let limit = if a.use_full_limit { JsValue::NULL } else { a.spend_limit_micros.map_or(JsValue::NULL, |limit| (limit as f64).into()) };
1231 self.db
1232 .prepare(
1233 "INSERT INTO limits (workspace, spend_limit_micros, spend_limit_full, updated_at) VALUES (?1, ?2, ?3, ?4)
1234 ON CONFLICT (workspace) DO UPDATE SET spend_limit_micros = ?2, spend_limit_full = ?3, updated_at = ?4",
1235 )
1236 .bind(&[workspace.as_str().into(), limit, (if a.use_full_limit { 1 } else { 0 }).into(), now.as_str().into()])?
1237 .run()
1238 .await?;
1239 if raising {
1240 let raised = a.spend_limit_micros.unwrap_or_default();
1241 // The ceiling rises with it, once.
1242 self.db
1243 .prepare(
1244 "UPDATE limits SET granted_ceiling_micros = MAX(COALESCE(granted_ceiling_micros, 0), ?2),
1245 raised_at = ?3, updated_at = ?3 WHERE workspace = ?1 AND raised_at IS NULL",
1246 )
1247 .bind(&[workspace.as_str().into(), (raised as f64).into(), now.as_str().into()])?
1248 .run()
1249 .await?;
1250 let account = self.account_of(&workspace).await?;
1251 self.audit(&account.id, "raise_once", &format!("{workspace} used its one-time raise: {}", dollars_plain(raised)), &a.actor.username)
1252 .await?;
1253 }
1254 Ok(Outcome::Ok(self.limit_of(&workspace).await?))
1255 }
1256
1257 /// `set_budget`: the owners' monthly budget on usage after what the
1258 /// plan includes: the spend limit (set as `set_spend_limit` sets it,
1259 /// with the same bounds), which alerts to send, whether usage pauses at
1260 /// 100%, and a webhook told at each alert.
1261 pub(crate) async fn set_budget(&self, a: SetBudgetArgs) -> Result<Outcome<Limit>> {
1262 let workspace = a.workspace.to_lowercase();
1263 if !a.actor.manages_billing(&workspace) {
1264 return Ok(Outcome::fail(FailureCode::Forbidden, "Only an owner or a billing manager can set the workspace's budget."));
1265 }
1266 if a.alerts.iter().any(|level| !ALERT_LEVELS.contains(level)) {
1267 return Ok(Outcome::fail(FailureCode::Invalid, "Alerts are at 50, 75, 90 or 100% of the budget."));
1268 }
1269 let webhook = a.webhook.as_deref().map(str::trim).filter(|url| !url.is_empty());
1270 if let Some(url) = webhook
1271 && !webhook_ok(url)
1272 {
1273 return Ok(Outcome::fail(FailureCode::Invalid, "The webhook is an https:// address of your own, at most 500 characters."));
1274 }
1275 if !a.keep_limit {
1276 let set = self
1277 .set_spend_limit(SetSpendLimitArgs {
1278 actor: a.actor.clone(),
1279 workspace: workspace.clone(),
1280 spend_limit_micros: a.amount_micros,
1281 use_full_limit: false,
1282 raise_once: false,
1283 })
1284 .await?;
1285 if let Outcome::Fail(failure) = set {
1286 return Ok(Outcome::Fail(failure));
1287 }
1288 } else if self.limit_of(&workspace).await?.trust == Trust::New {
1289 return Ok(Outcome::fail(FailureCode::Conflict, format!("{workspace} is not on the g1t plan, so it has no budget to alert on.")));
1290 } else {
1291 // The row the alerts are kept on, if the workspace has none yet.
1292 self.db
1293 .prepare("INSERT OR IGNORE INTO limits (workspace, updated_at) VALUES (?, ?)")
1294 .bind(&[workspace.as_str().into(), rfc3339(now_ms()).into()])?
1295 .run()
1296 .await?;
1297 }
1298 let mut levels = a.alerts.clone();
1299 levels.sort_by(|x, y| y.cmp(x));
1300 levels.dedup();
1301 let stored = levels.iter().map(u32::to_string).collect::<Vec<_>>().join(",");
1302 self.db
1303 .prepare(
1304 "UPDATE limits SET alert_levels = ?2, pause_at_limit = ?3, budget_webhook = ?4, updated_at = ?5 WHERE workspace = ?1",
1305 )
1306 .bind(&[
1307 workspace.as_str().into(),
1308 stored.as_str().into(),
1309 i32::from(a.pause_at_limit).into(),
1310 crate::optional(webhook),
1311 rfc3339(now_ms()).into(),
1312 ])?
1313 .run()
1314 .await?;
1315 let account = self.account_of(&workspace).await?;
1316 self.audit(
1317 &account.id,
1318 "budget",
1319 &format!(
1320 "{workspace}: budget {}, alerts at {}, {}{}",
1321 a.amount_micros.map_or_else(|| "automatic".to_owned(), dollars_plain),
1322 if stored.is_empty() { "none".to_owned() } else { format!("{stored}%") },
1323 if a.pause_at_limit { "pauses usage at 100%" } else { "alerts only" },
1324 if webhook.is_some() { ", with a webhook" } else { "" }
1325 ),
1326 &a.actor.username,
1327 )
1328 .await?;
1329 Ok(Outcome::Ok(self.limit_of(&workspace).await?))
1330 }
1331
1332
1333 /// The budget's webhook, if the workspace has one.
1334 async fn budget_webhook_of(&self, workspace: &str) -> Result<Option<String>> {
1335 #[derive(Deserialize)]
1336 struct Row {
1337 budget_webhook: Option<String>,
1338 }
1339 Ok(self
1340 .db
1341 .prepare("SELECT budget_webhook FROM limits WHERE workspace = ?")
1342 .bind(&[workspace.into()])?
1343 .first::<Row>(None)
1344 .await?
1345 .and_then(|row| row.budget_webhook)
1346 .filter(|url| webhook_ok(url)))
1347 }
1348 /// Posts a budget alert to the workspace's webhook, if it has one.
1349 /// Never fails the alert: a receiver that does not answer is logged.
1350 pub(crate) async fn post_budget_webhook(&self, url: &str, workspace: &str, level: u32, spent: i64, budget: i64) {
1351 let body = serde_json::json!({
1352 "event": "budget.alert",
1353 "workspace": workspace,
1354 "level_percent": level,
1355 "spent_micros": spent,
1356 "budget_micros": budget,
1357 "sent_at": rfc3339(now_ms()),
1358 });
1359 let headers = worker::Headers::new();
1360 let _ = headers.set("content-type", "application/json");
1361 let _ = headers.set("user-agent", "g1t-billing");
1362 let mut init = worker::RequestInit::new();
1363 init.with_method(worker::Method::Post).with_headers(headers).with_body(Some(body.to_string().into()));
1364 let sent = match worker::Request::new_with_init(url, &init) {
1365 Ok(request) => worker::Fetch::Request(request).send().await.map(|r| r.status_code()),
1366 Err(error) => Err(error),
1367 };
1368 match sent {
1369 Ok(status) if (200..300).contains(&status) => {}
1370 Ok(status) => worker::console_warn!("{workspace}'s budget webhook answered {status}"),
1371 Err(error) => worker::console_warn!("{workspace}'s budget webhook could not be reached: {error}"),
1372 }
1373 }
1374}
1375
1376/// Whether `owed` is enough to charge a card when a month closes: at least
1377/// the minimum charge (`MIN_CHARGE_MICROS`). Less carries over to the next
1378/// invoice. Charges at a limit do not ask.
1379pub(crate) fn worth_charging(owed: i64, min_charge: i64) -> bool {
1380 owed > 0 && owed >= min_charge
1381}
1382
1383/// Emails the workspace's owners through identity. False if nothing was sent.
1384pub(crate) async fn notify(identity: &worker::Fetcher, workspace: &str, subject: &str, intro: &str, action: &str, link: &str) -> bool {
1385 let footer = "You get this because you own this workspace on g1t. Limits and alerts are explained at https://docs.g1t.sh/guides/usage-and-billing/#limits";
1386 notify_with(identity, workspace, subject, intro, action, link, footer).await
1387}
1388
1389/// `notify`, with a footer of its own.
1390pub(crate) async fn notify_with(
1391 identity: &worker::Fetcher,
1392 workspace: &str,
1393 subject: &str,
1394 intro: &str,
1395 action: &str,
1396 link: &str,
1397 footer: &str,
1398) -> bool {
1399 let args = g1t_contracts::identity::NotifyOwnersArgs {
1400 workspace: workspace.to_owned(),
1401 subject: subject.to_owned(),
1402 intro: intro.to_owned(),
1403 action: action.to_owned(),
1404 link: link.to_owned(),
1405 footer: footer.to_owned(),
1406 };
1407 match g1t_kit::call::<_, u32>(identity, "notify_owners", &args).await {
1408 Ok(sent) => sent > 0,
1409 Err(error) => {
1410 worker::console_error!("could not tell {workspace}'s owners: {error}");
1411 false
1412 }
1413 }
1414}
1415
1416/// Whether a source's figure for the month before is still open: charged
1417/// already (`charged`) or the month closed for the workspace (`closed`).
1418/// `?1` workspace, `?2` source, `?3` month.
1419pub(crate) const PREVIOUS_OPEN: &str = "SELECT
1420 (SELECT COUNT(*) FROM pending_usage WHERE workspace = ?1 AND source = ?2 AND month = ?3 AND charged_at IS NOT NULL) AS charged,
1421 (SELECT COUNT(*) FROM month_closes WHERE workspace = ?1 AND month = ?3) AS closed";
1422
1423/// The month a pending report is filed under, from the month the reporter
1424/// says its total belongs to and this month (`YYYY-MM`, UTC), with whether
1425/// it is the month before (still to be checked open, `PREVIOUS_OPEN`).
1426/// None is this month, as before reporters named one; any other month is
1427/// refused, so a stale or wrong report never lands in this month.
1428pub(crate) fn pending_month(given: Option<&str>, current: &str) -> std::result::Result<(String, bool), String> {
1429 let Some(given) = given.map(str::trim).filter(|m| !m.is_empty()) else {
1430 return Ok((current.to_owned(), false));
1431 };
1432 let b = given.as_bytes();
1433 let shaped = b.len() == 7 && b[4] == b'-' && b.iter().enumerate().all(|(i, c)| i == 4 || c.is_ascii_digit());
1434 if !shaped || !(1..=12).contains(&given[5..7].parse::<u32>().unwrap_or(0)) {
1435 return Err(format!("{given:?} is not a month (YYYY-MM)"));
1436 }
1437 if given == current {
1438 return Ok((current.to_owned(), false));
1439 }
1440 if given == previous_month(current) {
1441 return Ok((given.to_owned(), true));
1442 }
1443 Err(format!("{given} is neither this month ({current}) nor the one before"))
1444}
1445
1446/// `2026-09` for `2026-10`, and `2025-12` for `2026-01`.
1447pub(crate) fn previous_month(month: &str) -> String {
1448 let year: i32 = month[..4].parse().unwrap_or(1970);
1449 let number: u32 = month[5..7].parse().unwrap_or(1);
1450 if number == 1 {
1451 format!("{}-12", year - 1)
1452 } else {
1453 format!("{year}-{:02}", number - 1)
1454 }
1455}
1456
1457#[cfg(test)]
1458mod tests {
1459 use super::*;
1460
1461 #[test]
1462 fn a_pending_report_lands_in_its_own_month_or_none() {
1463 // Unnamed: this month, as before reporters named one.
1464 assert_eq!(pending_month(None, "2026-11"), Ok(("2026-11".to_owned(), false)));
1465 assert_eq!(pending_month(Some(" "), "2026-11"), Ok(("2026-11".to_owned(), false)));
1466 assert_eq!(pending_month(Some("2026-11"), "2026-11"), Ok(("2026-11".to_owned(), false)));
1467 // The month before: only once it is checked open.
1468 assert_eq!(pending_month(Some("2026-10"), "2026-11"), Ok(("2026-10".to_owned(), true)));
1469 assert_eq!(pending_month(Some("2025-12"), "2026-01"), Ok(("2025-12".to_owned(), true)));
1470 // Anything else is refused, never filed under this month.
1471 for bad in ["2026-09", "2026-12", "2026-1", "2026/10", "2026-13", "2026-00", "abcd-ef", "2026-10-01"] {
1472 assert!(pending_month(Some(bad), "2026-11").is_err(), "{bad}");
1473 }
1474 // Charged or closed: refused; a charged row is never rewritten.
1475 assert!(PREVIOUS_OPEN.contains("charged_at IS NOT NULL") && PREVIOUS_OPEN.contains("FROM month_closes"));
1476 assert!(crate::storage::SET_PENDING.contains("WHERE pending_usage.charged_at IS NULL"));
1477 assert!(crate::storage::SET_PENDING.contains("NOT EXISTS (SELECT 1 FROM month_closes"));
1478 }
1479
1480 #[test]
1481 fn a_budget_alerts_at_the_levels_chosen_and_pauses_only_if_asked() {
1482 // Every level when none were chosen; the chosen ones, highest first.
1483 assert_eq!(alert_levels(None), [100, 90, 75, 50]);
1484 assert_eq!(alert_levels(Some("50,100,75")), [100, 75, 50]);
1485 assert_eq!(alert_levels(Some("")), Vec::<u32>::new());
1486 assert_eq!(alert_levels(Some("40, 50")), [50]);
1487 // The highest chosen level reached.
1488 assert_eq!(alert_level_in(80, 100, &[100, 75, 50]), 75);
1489 assert_eq!(alert_level_in(80, 100, &[100, 90]), 0);
1490 assert_eq!(alert_level_in(100, 100, &[100, 75, 50]), 100);
1491 assert_eq!(alert_level_in(10, 0, &[50]), 0);
1492 // At 100%: a stop when the budget pauses usage, else a warning.
1493 assert_eq!(budget_state(100, Some(100), true), LimitState::Stopped);
1494 assert_eq!(budget_state(100, Some(100), false), LimitState::Warning);
1495 assert_eq!(budget_state(10, Some(100), false), LimitState::Ok);
1496 assert_eq!(budget_state(10, None, true), LimitState::Ok);
1497 }
1498
1499 #[test]
1500 fn a_budget_webhook_is_someone_elses_https_address() {
1501 assert!(webhook_ok("https://hooks.acme.test/g1t"));
1502 assert!(!webhook_ok("http://hooks.acme.test/g1t"));
1503 assert!(!webhook_ok("https://g1t.sh/x"));
1504 assert!(!webhook_ok("https://api.g1t.sh/x"));
1505 assert!(!webhook_ok("https://localhost:3000/x"));
1506 assert!(!webhook_ok("https://a b.test/"));
1507 assert!(!webhook_ok(&format!("https://acme.test/{}", "x".repeat(600))));
1508 }
1509 #[test]
1510 fn autopay_charges_only_what_is_owed_on_the_ledger() {
1511 let ceiling = Some(100_000_000);
1512 // $85 owed on the ledger, near the $100 ceiling: charged.
1513 assert!(autopay_due(85_000_000, ceiling));
1514 // $85 of exposure that is all metered usage not entered until the
1515 // month closes: nothing to charge, so no invoice.
1516 assert!(!autopay_due(85_000_000 - 85_000_000, ceiling));
1517 assert!(!autopay_due(-5, ceiling));
1518 // Owed but well under the ceiling, or no ceiling: not yet.
1519 assert!(!autopay_due(10_000_000, ceiling));
1520 assert!(!autopay_due(85_000_000, None));
1521 }
1522
1523 #[test]
1524 fn a_staff_hold_stops_running_apps_with_its_reason() {
1525 let why = hold_message("acme", Some(" mining ")).unwrap();
1526 assert!(why.contains("acme") && why.contains("(mining)") && why.contains("stopped"), "{why}");
1527 assert_eq!(hold_message("acme", Some(" ")), None);
1528 assert_eq!(hold_message("acme", None), None);
1529 }
1530
1531 #[test]
1532 fn a_returning_payer_keeps_its_history() {
1533 let now = "2026-10-13T00:00:00Z";
1534 // New: first paid as the plan started: still its first month.
1535 let start = trust_start("2026-10-01T00:00:00Z", Some("2026-10-01T00:05:00Z"));
1536 assert_eq!(start, "2026-10-01T00:00:00Z");
1537 assert!(in_first_cycle(start, Some("2026-11-01T00:00:00Z"), now));
1538 // Paid first in March, left, back this month: not a first month.
1539 let start = trust_start("2026-10-01T00:00:00Z", Some("2026-03-04T00:00:00Z"));
1540 assert_eq!(start, "2026-03-04T00:00:00Z");
1541 assert!(!in_first_cycle(start, Some("2026-11-01T00:00:00Z"), now));
1542 assert!(!in_first_cycle(start, None, now));
1543 // Never paid yet: from the plan's start, as before.
1544 assert_eq!(trust_start("2026-10-01T00:00:00Z", None), "2026-10-01T00:00:00Z");
1545 }
1546
1547 #[test]
1548 fn the_automatic_spend_limit_follows_last_month() {
1549 assert_eq!(automatic_spend_limit(0), 200_000_000);
1550 assert_eq!(automatic_spend_limit(50_000_000), 200_000_000);
1551 assert_eq!(automatic_spend_limit(900_000_000), 1_800_000_000);
1552 }
1553
1554 #[test]
1555 fn three_steady_months_make_a_workspace_established() {
1556 assert_eq!(established_ceiling(&[900_000_000, 850_000_000, 950_000_000]), Some(2_700_000_000));
1557 assert_eq!(established_ceiling(&[5_000_000_000, 5_000_000_000, 5_000_000_000]), Some(10_000_000_000));
1558 assert_eq!(established_ceiling(&[900_000_000, 10_000_000, 950_000_000]), None);
1559 assert_eq!(established_ceiling(&[900_000_000, 900_000_000]), None);
1560 }
1561
1562 #[test]
1563 fn alerts_come_at_half_three_quarters_ninety_and_the_limit() {
1564 assert_eq!(alert_level(0, 10_000_000), 0);
1565 assert_eq!(alert_level(4_999_999, 10_000_000), 0);
1566 assert_eq!(alert_level(5_000_000, 10_000_000), 50);
1567 assert_eq!(alert_level(7_500_000, 10_000_000), 75);
1568 assert_eq!(alert_level(8_999_999, 10_000_000), 75);
1569 assert_eq!(alert_level(9_000_000, 10_000_000), 90);
1570 assert_eq!(alert_level(10_000_000, 10_000_000), 100);
1571 assert_eq!(alert_level(25_000_000, 10_000_000), 100);
1572 // Nothing to measure against: no alert.
1573 assert_eq!(alert_level(5, 0), 0);
1574 }
1575
1576 #[test]
1577 fn amounts_under_the_minimum_carry_over_only_at_the_month_close() {
1578 let min = 5_000_000;
1579 assert!(!worth_charging(0, min));
1580 assert!(!worth_charging(4_990_000, min));
1581 assert!(worth_charging(5_000_000, min));
1582 assert!(worth_charging(12_000_000, min));
1583 // $3 carried from last month and $2.50 this month: charged together.
1584 let carried = 3_000_000;
1585 assert!(!worth_charging(carried, min));
1586 assert!(worth_charging(carried + 2_500_000, min));
1587 }
1588
1589 #[test]
1590 fn the_month_before_wraps_the_year() {
1591 assert_eq!(previous_month("2026-10"), "2026-09");
1592 assert_eq!(previous_month("2026-01"), "2025-12");
1593 }
1594
1595 fn ceilings() -> Ceilings {
1596 Ceilings { new: 3_000_000, paid_min: 25_000_000, paid_max: 1_000_000_000 }
1597 }
1598
1599 #[test]
1600 fn trust_grows_with_what_was_paid_within_bounds() {
1601 assert_eq!(ceilings().for_paid(5_000_000), 25_000_000);
1602 assert_eq!(ceilings().for_paid(100_000_000), 200_000_000);
1603 assert_eq!(ceilings().for_paid(10_000_000_000), 1_000_000_000);
1604 }
1605
1606 #[test]
1607 fn a_new_paid_workspace_starts_at_a_hundred_dollars_and_only_goes_up() {
1608 let start = 100_000_000;
1609 // The first month: the starting ceiling, whatever was paid.
1610 assert_eq!(paid_ceiling(&ceilings(), start, true, 900_000_000, None), start);
1611 // After it, with little paid: never below the start.
1612 assert_eq!(paid_ceiling(&ceilings(), start, false, 20_000_000, None), start);
1613 assert_eq!(paid_ceiling(&ceilings(), start, false, 0, None), start);
1614 // Payments that cleared raise it: twice what was paid.
1615 assert_eq!(paid_ceiling(&ceilings(), start, false, 300_000_000, None), 600_000_000);
1616 // Established follows the monthly spend.
1617 assert_eq!(paid_ceiling(&ceilings(), start, false, 300_000_000, Some(2_700_000_000)), 2_700_000_000);
1618 }
1619
1620 #[test]
1621 fn the_first_billing_cycle_is_the_first_month() {
1622 // A plan that started on the 5th, paid through the 5th of next month.
1623 assert!(in_first_cycle("2026-10-05T10:00:00Z", Some("2026-11-05T10:00:00Z"), "2026-10-20T00:00:00Z"));
1624 // Renewed: the period now ends two months after the start.
1625 assert!(!in_first_cycle("2026-10-05T10:00:00Z", Some("2026-12-05T10:00:00Z"), "2026-11-20T00:00:00Z"));
1626 // No period known yet: the first 31 days.
1627 assert!(in_first_cycle("2026-10-05T10:00:00Z", None, "2026-11-04T00:00:00Z"));
1628 assert!(!in_first_cycle("2026-10-05T10:00:00Z", None, "2026-11-10T00:00:00Z"));
1629 // Day counting is exact across months and years.
1630 assert_eq!(days("1970-01-01"), 0);
1631 assert_eq!(days("2026-11-01") - days("2026-10-01"), 31);
1632 assert_eq!(days("2028-03-01") - days("2028-02-28"), 2);
1633 assert_eq!(days("2027-01-01") - days("2026-12-31"), 1);
1634 }
1635
1636 #[test]
1637 fn owners_set_their_limit_up_to_the_highest_ceiling_without_asking() {
1638 // First month at $100; the highest ever is $100.
1639 let (available, once) = spend_bounds(100_000_000, 100_000_000, 0, false);
1640 assert_eq!(available, 100_000_000);
1641 assert_eq!(once, Some(200_000_000));
1642 assert_eq!(self_serve(80_000_000, available, once, false), Ok(false));
1643 assert_eq!(self_serve(100_000_000, available, once, false), Ok(false));
1644 // Above it without the raise: refused, saying what to do.
1645 assert!(self_serve(150_000_000, available, once, false).unwrap_err().contains("one-time raise"));
1646 // With the raise: up to twice the highest ceiling, once.
1647 assert_eq!(self_serve(200_000_000, available, once, true), Ok(true));
1648 assert!(self_serve(200_000_001, available, once, true).unwrap_err().contains("Raise my limit"));
1649 // Once used, it is gone.
1650 let (available, once) = spend_bounds(200_000_000, 200_000_000, 0, true);
1651 assert_eq!(once, None);
1652 assert_eq!(self_serve(200_000_000, available, once, false), Ok(false));
1653 assert!(self_serve(300_000_000, available, once, true).unwrap_err().contains("is used"));
1654 // A ceiling that came down still leaves the highest one available.
1655 let (available, _) = spend_bounds(100_000_000, 400_000_000, 0, true);
1656 assert_eq!(available, 400_000_000);
1657 assert!(self_serve(-1, available, None, false).is_err());
1658 }
1659
1660 #[test]
1661 fn prepaying_raises_what_can_be_used_at_once() {
1662 // $500 prepaid this month, $120 used: nothing owed, $380 left.
1663 assert_eq!(exposure(120_000_000, 500_000_000, 0), (0, 380_000_000));
1664 // Prepaid last month and carried in.
1665 assert_eq!(exposure(120_000_000, 0, 500_000_000), (0, 380_000_000));
1666 // Used past the prepayment: the rest is owed.
1667 assert_eq!(exposure(620_000_000, 500_000_000, 0), (120_000_000, 0));
1668 // Owed from before adds to this month's.
1669 assert_eq!(exposure(10_000_000, 0, -4_000_000), (14_000_000, 0));
1670 // With a $100 ceiling and $500 prepaid, work stops at $600 of use,
1671 // not at $100.
1672 let ceiling = 100_000_000;
1673 assert_eq!(state(exposure(599_000_000, 500_000_000, 0).0, Some(ceiling)), LimitState::Warning);
1674 assert_eq!(state(exposure(600_000_000, 500_000_000, 0).0, Some(ceiling)), LimitState::Stopped);
1675 // And the owners may set their spend limit that much higher.
1676 assert_eq!(spend_bounds(ceiling, ceiling, 500_000_000, true).0, 600_000_000);
1677 }
1678
1679 #[test]
1680 fn work_warns_at_eighty_percent_and_stops_at_the_ceiling() {
1681 assert_eq!(state(0, Some(100)), LimitState::Ok);
1682 assert_eq!(state(79, Some(100)), LimitState::Ok);
1683 assert_eq!(state(80, Some(100)), LimitState::Warning);
1684 assert_eq!(state(100, Some(100)), LimitState::Stopped);
1685 assert_eq!(state(1_000_000, None), LimitState::Ok);
1686 }
1687}