Skip to content

g1t/services/billing/src/reset.rs

295 lines14,700 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's1//! A test workspace's billing, wiped: `admin_reset_billing`.
2//!
3//! While billing runs on Stripe's test key, staff can return a workspace
4//! used for testing to how a new customer starts: no ledger, balance,
5//! plan, limits, trial grant, invoices, holds, signals or cost rows. Its
6//! workspace, members and repositories are not billing's and stay. Never
7//! with a live Stripe key, never for a comped workspace, and never for one
8//! an enterprise pays for. The reset itself is kept in the audit log, and
9//! g1t's own counts of the workspace's git operations stay: they are what
10//! Cloudflare's bill is compared with, not what the workspace owes.
Merge main: Deployments panel in the About, project homepage, both sides' operations11//!
12//! What the wiped usage cost g1t is kept too (`reset_costs`): the model
13//! calls and Cloudflare usage still happened, and AI Gateway and the bill
14//! still show them. The costs run counts that as given away on purpose
15//! ("testing resets"), so it is neither drift nor a leak.
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's16
17use g1t_contracts::billing::{AdminResetBillingArgs, BillingReset};
Merge main: Deployments panel in the About, project homepage, both sides' operations18use g1t_contracts::time::rfc3339;
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's19use g1t_contracts::{FailureCode, Outcome};
Merge main: Deployments panel in the About, project homepage, both sides' operations20use g1t_kit::now_ms;
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's21use serde::Deserialize;
22use worker::Result;
23use worker::wasm_bindgen::JsValue;
24
25use crate::Billing;
26use crate::accounts::own_account;
27
28/// The statements, in order. Parameters: `?1` the workspace, `?2` its own
29/// billing account (`ws_<slug>`).
30pub(crate) const STATEMENTS: &[&str] = &[
31 "DELETE FROM workspace_invoice_lines WHERE invoice_id IN (SELECT invoice_id FROM workspace_invoices WHERE workspace = ?1)",
32 "DELETE FROM workspace_invoices WHERE workspace = ?1",
33 "DELETE FROM ledger WHERE workspace = ?1",
34 "DELETE FROM runs WHERE workspace = ?1",
35 "DELETE FROM reservations WHERE workspace = ?1",
36 "DELETE FROM checkouts WHERE workspace = ?1",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit37 "DELETE FROM ai_reload WHERE workspace = ?1",
38 "DELETE FROM ai_reloads WHERE workspace = ?1",
Merge Stripe Tax, the card fee on card payments, and one free workspace per person39 "DELETE FROM tax_and_fees WHERE workspace = ?1",
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's40 "DELETE FROM accounts WHERE workspace = ?1",
41 "DELETE FROM plan_payments WHERE workspace = ?1",
42 "DELETE FROM subscriptions WHERE workspace = ?1",
43 "DELETE FROM limits WHERE workspace = ?1",
44 "DELETE FROM limit_requests WHERE workspace = ?1",
45 "DELETE FROM trial_grants WHERE workspace = ?1",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging46 "DELETE FROM credit_grants WHERE workspace = ?1",
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's47 "DELETE FROM card_checks WHERE workspace = ?1",
48 "DELETE FROM alerts_sent WHERE workspace = ?1",
49 "DELETE FROM price_notices WHERE workspace = ?1",
50 "DELETE FROM pending_usage WHERE workspace = ?1",
51 "DELETE FROM pending_days WHERE workspace = ?1",
52 "DELETE FROM month_closes WHERE workspace = ?1",
53 "DELETE FROM storage_days WHERE workspace = ?1",
54 "DELETE FROM package_storage_days WHERE workspace = ?1",
55 "DELETE FROM token_usage WHERE workspace = ?1",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens56 "DELETE FROM gateway_requests WHERE workspace = ?1",
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's57 "DELETE FROM spikes WHERE workspace = ?1",
58 "DELETE FROM closed_workspaces WHERE workspace = ?1",
59 "DELETE FROM sales_records WHERE workspace = ?1",
60 "DELETE FROM sales_notes WHERE workspace = ?1",
61 "DELETE FROM workspace_costs WHERE workspace = ?1",
62 "DELETE FROM margin_alerts WHERE kind = 'workspace' AND subject = ?1",
63 // Allowances drawn by the workspace, and its repositories' shares of
64 // the open-source pool (`<slug>/<name>`, compared exactly).
65 "DELETE FROM allowance_use WHERE scope = ?1 OR (kind = 'oss_repo' AND substr(scope, 1, length(?1) + 1) = ?1 || '/')",
66 "DELETE FROM budget_alerts WHERE account = ?2",
67 "DELETE FROM billing_accounts WHERE id = ?2",
68];
69
Merge main: Deployments panel in the About, project homepage, both sides' operations70/// Keeps one row of what a reset wiped that g1t paid for. Parameters: the
71/// workspace, day, bucket, cost, value, when and who.
72pub(crate) const KEEP: &str = "INSERT OR REPLACE INTO reset_costs (workspace, day, bucket, cost_micros, value_micros, reset_at, reset_by) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)";
73
74/// The `reset_costs` rows a reset at `at` writes, each (day, bucket, cost,
75/// value): what it wiped that g1t paid for, and one for the reset itself
76/// (bucket '', nothing in it), so every reset is on record even when it
77/// wiped nothing.
78pub(crate) fn kept_rows(at: &str, wiped: &[crate::margin::Wiped]) -> Vec<(String, String, i64, i64)> {
79 let mut rows = vec![(at[..10.min(at.len())].to_owned(), String::new(), 0, 0)];
80 rows.extend(wiped.iter().map(|w| (w.day.clone(), w.bucket.clone(), w.cost, w.value)));
81 rows
82}
83
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's84impl Billing {
sudo: a billing reset runs the costs analysis again so every figure is fresh; every submit button shows it is working (CSS only); no margin percentage on less than a cent sold85 pub(crate) async fn admin_reset_billing(&self, env: &worker::Env, a: AdminResetBillingArgs) -> Result<Outcome<BillingReset>> {
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's86 let workspace = a.workspace.trim().to_lowercase();
87 if workspace.is_empty() || a.by.trim().is_empty() {
88 return Ok(Outcome::fail(FailureCode::Invalid, "A reset needs a workspace and who did it."));
89 }
90 if a.note.trim().len() < 5 {
91 return Ok(Outcome::fail(FailureCode::Invalid, "Say why it is reset, for whoever looks next."));
92 }
93 if a.confirm.trim() != workspace {
94 return Ok(Outcome::fail(FailureCode::Invalid, format!("Type the workspace's slug, {workspace}, exactly, to reset it.")));
95 }
96 if self.stripe.as_ref().is_some_and(|s| s.live()) {
97 return Ok(Outcome::fail(FailureCode::Forbidden, "Billing takes real cards: a workspace's billing is never wiped."));
98 }
99 #[derive(Deserialize)]
100 struct Found {
101 comped: i64,
102 enterprise: i64,
103 }
104 let found = self
105 .db
106 .prepare(format!(
107 "SELECT CASE WHEN ?1 IN ({}) THEN 1 ELSE 0 END AS comped,
108 (SELECT COUNT(*) FROM account_members WHERE workspace = ?1) AS enterprise",
109 crate::sales::INTERNAL_SQL
110 ))
111 .bind(&[workspace.as_str().into()])?
112 .first::<Found>(None)
113 .await?;
114 if let Some(found) = found {
115 if found.comped > 0 {
116 return Ok(Outcome::fail(FailureCode::Forbidden, format!("{workspace} is comped (g1t's own): its spend is a budget, kept.")));
117 }
118 if found.enterprise > 0 {
119 return Ok(Outcome::fail(FailureCode::Forbidden, format!("An enterprise pays for {workspace}: move it off first.")));
120 }
121 }
122 let account = own_account(&workspace);
Merge main: Deployments panel in the About, project homepage, both sides' operations123 // What g1t paid for is kept before it is wiped, in the same batch,
124 // so the costs run counts it as given away (testing resets) rather
125 // than finding AI Gateway's and Cloudflare's figures unexplained.
126 let at_ms = now_ms();
127 let at = rfc3339(at_ms);
128 let wiped = self.wiped_by_reset(&workspace).await?;
129 let kept = kept_rows(&at, &wiped);
130 let mut batch = Vec::with_capacity(kept.len() + STATEMENTS.len());
131 for (day, bucket, cost, value) in &kept {
132 batch.push(self.db.prepare(KEEP).bind(&[
133 workspace.as_str().into(),
134 day.as_str().into(),
135 bucket.as_str().into(),
136 (*cost as f64).into(),
137 (*value as f64).into(),
138 at.as_str().into(),
139 a.by.as_str().into(),
140 ])?);
141 }
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's142 for sql in STATEMENTS {
143 let values: Vec<JsValue> =
144 [workspace.as_str(), account.as_str()][..crate::rename::parameters(sql)].iter().map(|v| (*v).into()).collect();
145 batch.push(self.db.prepare(*sql).bind(&values)?);
146 }
147 let mut rows = 0usize;
Merge main: Deployments panel in the About, project homepage, both sides' operations148 for result in self.db.batch(batch).await?.into_iter().skip(kept.len()) {
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's149 rows += result.meta()?.and_then(|m| m.changes).unwrap_or(0);
150 }
Merge main: Deployments panel in the About, project homepage, both sides' operations151 let paid: i64 = wiped.iter().map(|w| w.cost).sum();
152 let detail = format!(
153 "billing of {workspace} reset ({rows} rows; {} g1t paid for kept as given away): {}",
154 crate::features::dollars(paid),
155 a.note.trim()
156 );
157 // At the same instant as the kept rows: that is how the costs run
158 // tells a reset that kept its costs from one before resets did.
159 self.audit_at(&account, "reset", &detail, &a.by, at_ms).await?;
sudo: a billing reset runs the costs analysis again so every figure is fresh; every submit button shows it is working (CSS only); no margin percentage on less than a cent sold160 // The margin figures still hold the workspace's past usage: redo
161 // them now (the day's analysis: the bill, 31 days, the alerts), so
162 // the pages show the reset at once.
163 let refreshed = match self.costs_daily(env, &crate::keeper::Keeper::from_env(env)).await {
164 Ok(run) => run.problems.is_empty(),
165 Err(error) => {
166 worker::console_error!("costs after a reset of {workspace}: {error}");
167 false
168 }
169 };
170 Ok(Outcome::Ok(BillingReset { workspace, rows: rows as u32, refreshed }))
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn every_table_with_a_workspace_is_wiped_or_kept_on_purpose() {
180 let all = STATEMENTS.join("\n");
Merge main: Deployments panel in the About, project homepage, both sides' operations181 // What is kept: the audit log, g1t's own counts compared with
182 // Cloudflare's bill, and what resets wiped that g1t paid for.
183 let kept = ["admin_actions", "own_counts", "reset_costs"];
184 for table in kept {
185 assert!(!all.contains(&format!("DELETE FROM {table} ")), "{table} is kept on purpose");
186 }
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's187 for table in [
188 "ledger", "runs", "checkouts", "workspace_invoices", "workspace_invoice_lines", "sales_notes", "accounts",
189 "pending_usage", "pending_days", "limits", "subscriptions", "month_closes", "sales_records",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging190 "billing_accounts", "allowance_use", "trial_grants", "credit_grants", "storage_days", "package_storage_days",
sudo: the billing reset no longer names sandbox_months (dropped in 0015), checked against the migrations by a test; a failed reset says why instead of an error page191 "token_usage", "reservations", "spikes", "limit_requests", "plan_payments",
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's192 "card_checks", "alerts_sent", "price_notices", "closed_workspaces", "workspace_costs",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens193 "margin_alerts", "budget_alerts", "ai_reload", "ai_reloads", "tax_and_fees", "gateway_requests",
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's194 ] {
195 assert!(!kept.contains(&table));
196 assert!(all.contains(&format!("DELETE FROM {table} WHERE")), "{table}");
197 }
198 }
199
sudo: the billing reset no longer names sandbox_months (dropped in 0015), checked against the migrations by a test; a failed reset says why instead of an error page200 /// The tables the migrations leave: every one made, less those dropped.
201 fn live_tables() -> std::collections::BTreeSet<String> {
202 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("migrations");
203 let mut files: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().path()).collect();
204 files.sort();
205 let mut live = std::collections::BTreeSet::new();
206 for file in files {
207 let sql = std::fs::read_to_string(file).unwrap();
208 for line in sql.lines().map(str::trim) {
209 let words: Vec<&str> = line.split(|c: char| c.is_whitespace() || c == '(' || c == ';').filter(|w| !w.is_empty()).collect();
210 let name = |at: usize| words.get(at).map(|w| w.to_string());
211 match words.as_slice() {
212 ["CREATE", "TABLE", "IF", "NOT", "EXISTS", ..] => live.extend(name(5)),
213 ["CREATE", "TABLE", ..] => live.extend(name(2)),
214 ["DROP", "TABLE", "IF", "EXISTS", ..] => {
215 name(4).map(|n| live.remove(&n));
216 }
217 ["DROP", "TABLE", ..] => {
218 name(2).map(|n| live.remove(&n));
219 }
220 _ => {}
221 }
222 }
223 }
224 live
225 }
226
227 #[test]
228 fn every_table_wiped_is_one_the_migrations_leave() {
229 let live = live_tables();
230 assert!(live.contains("ledger") && !live.contains("sandbox_months"), "{live:?}");
231 for sql in STATEMENTS {
232 let table = sql.split_whitespace().nth(2).unwrap();
233 assert!(live.contains(table), "{table} is not a table after the migrations");
234 }
235 }
236
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's237 #[test]
Billing: a workspace rename moves its token usage, package storage, month-end snapshots, price notices, cost and count rows, margin alert and budget alerts too; a test keeps the rename and the reset naming the same tables238 fn a_rename_moves_every_table_a_reset_wipes() {
239 let moved = crate::rename::STATEMENTS.join("\n");
240 // Lines follow their invoice, which carries the workspace.
241 for sql in STATEMENTS.iter().filter(|sql| !sql.contains("workspace_invoice_lines")) {
242 let table = sql.split_whitespace().nth(2).unwrap();
243 assert!(moved.contains(&format!(" {table} ")), "{table} is wiped on a reset but not moved on a rename");
244 }
245 }
246
247 #[test]
Merge main: Deployments panel in the About, project homepage, both sides' operations248 fn a_reset_keeps_what_g1t_paid_for_and_a_row_for_itself() {
249 use crate::margin::{UsageRow, Wiped, wiped};
250 let map: std::collections::BTreeMap<String, String> =
251 [("sandbox", "sandboxes"), ("git", "git"), ("plan", "platform")].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
252 let row = |day: &str, key: &str, value: i64, cash: i64, cost: i64| UsageRow {
253 day: day.into(),
254 workspace: "syntaqx".into(),
255 key: key.into(),
256 value,
257 cash,
258 cost,
259 ..UsageRow::default()
260 };
261 // Two agent runs and a sandbox on Oct 2, a run in a free period on
262 // Oct 7 (valued at price), the plan's payment and a run on the
263 // workspace's own key (no cost to g1t): only what g1t paid for.
264 let rows = vec![
265 row("2026-10-02", "implement", 3_600_000, 3_600_000, 3_000_000),
266 row("2026-10-02", "review", 1_200_000, 0, 1_000_000),
267 row("2026-10-02", "sandbox", 240_000, 240_000, 200_000),
268 row("2026-10-07", "implement", 0, 0, 4_600_000),
269 row("2026-10-07", "plan", 20_000_000, 20_000_000, 0),
270 row("2026-10-07", "own_key", 50_000, 50_000, 0),
271 ];
272 let kept = wiped(&rows, &map, 20);
273 assert_eq!(
274 kept,
275 vec![
276 Wiped { day: "2026-10-02".into(), bucket: "models".into(), cost: 4_000_000, value: 4_800_000 },
277 Wiped { day: "2026-10-02".into(), bucket: "sandboxes".into(), cost: 200_000, value: 240_000 },
278 Wiped { day: "2026-10-07".into(), bucket: "models".into(), cost: 4_600_000, value: 5_520_000 },
279 ]
280 );
281 let rows = kept_rows("2026-10-07T09:12:00.000Z", &kept);
282 assert_eq!(rows[0], ("2026-10-07".to_string(), String::new(), 0, 0));
283 assert_eq!(rows.len(), 4);
284 // Nothing paid for: still on record.
285 assert_eq!(kept_rows("2026-10-08T00:00:00.000Z", &[]), vec![("2026-10-08".to_string(), String::new(), 0, 0)]);
286 assert_eq!(crate::rename::parameters(KEEP), 7);
287 assert!(live_tables().contains("reset_costs"));
288 }
289
290 #[test]
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's291 fn statements_name_at_most_the_workspace_and_its_account() {
292 assert!(STATEMENTS.iter().all(|sql| crate::rename::parameters(sql) <= 2));
293 assert_eq!(crate::rename::parameters(STATEMENTS.last().unwrap()), 2);
294 }
295}

This file's history is long; its oldest lines are credited to the oldest commit read.