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's | 1 | //! 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' operations | 11 | //! |
| 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's | 16 | |
| 17 | use g1t_contracts::billing::{AdminResetBillingArgs, BillingReset}; | |
| Merge main: Deployments panel in the About, project homepage, both sides' operations | 18 | use 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's | 19 | use g1t_contracts::{FailureCode, Outcome}; |
| Merge main: Deployments panel in the About, project homepage, both sides' operations | 20 | use 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's | 21 | use serde::Deserialize; |
| 22 | use worker::Result; | |
| 23 | use worker::wasm_bindgen::JsValue; | |
| 24 | ||
| 25 | use crate::Billing; | |
| 26 | use crate::accounts::own_account; | |
| 27 | ||
| 28 | /// The statements, in order. Parameters: `?1` the workspace, `?2` its own | |
| 29 | /// billing account (`ws_<slug>`). | |
| 30 | pub(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 audit | 37 | "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 person | 39 | "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's | 40 | "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 charging | 46 | "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's | 47 | "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 tokens | 56 | "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's | 57 | "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' operations | 70 | /// Keeps one row of what a reset wiped that g1t paid for. Parameters: the |
| 71 | /// workspace, day, bucket, cost, value, when and who. | |
| 72 | pub(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. | |
| 78 | pub(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's | 84 | impl 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 sold | 85 | 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's | 86 | 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' operations | 123 | // 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's | 142 | 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' operations | 148 | 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's | 149 | rows += result.meta()?.and_then(|m| m.changes).unwrap_or(0); |
| 150 | } | |
| Merge main: Deployments panel in the About, project homepage, both sides' operations | 151 | 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 sold | 160 | // 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's | 171 | } |
| 172 | } | |
| 173 | ||
| 174 | #[cfg(test)] | |
| 175 | mod 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' operations | 181 | // 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's | 187 | 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 charging | 190 | "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 page | 191 | "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's | 192 | "card_checks", "alerts_sent", "price_notices", "closed_workspaces", "workspace_costs", |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 193 | "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's | 194 | ] { |
| 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 page | 200 | /// 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's | 237 | #[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 tables | 238 | 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' operations | 248 | 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's | 291 | 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.