Skip to content

g1t/services/billing/src/reset.rs

202 lines9,940 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.
11
12use g1t_contracts::billing::{AdminResetBillingArgs, BillingReset};
13use g1t_contracts::{FailureCode, Outcome};
14use serde::Deserialize;
15use worker::Result;
16use worker::wasm_bindgen::JsValue;
17
18use crate::Billing;
19use crate::accounts::own_account;
20
21/// The statements, in order. Parameters: `?1` the workspace, `?2` its own
22/// billing account (`ws_<slug>`).
23pub(crate) const STATEMENTS: &[&str] = &[
24 "DELETE FROM workspace_invoice_lines WHERE invoice_id IN (SELECT invoice_id FROM workspace_invoices WHERE workspace = ?1)",
25 "DELETE FROM workspace_invoices WHERE workspace = ?1",
26 "DELETE FROM ledger WHERE workspace = ?1",
27 "DELETE FROM runs WHERE workspace = ?1",
28 "DELETE FROM reservations WHERE workspace = ?1",
29 "DELETE FROM checkouts WHERE workspace = ?1",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit30 "DELETE FROM ai_reload WHERE workspace = ?1",
31 "DELETE FROM ai_reloads WHERE workspace = ?1",
Merge Stripe Tax, the card fee on card payments, and one free workspace per person32 "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's33 "DELETE FROM accounts WHERE workspace = ?1",
34 "DELETE FROM plan_payments WHERE workspace = ?1",
35 "DELETE FROM subscriptions WHERE workspace = ?1",
36 "DELETE FROM limits WHERE workspace = ?1",
37 "DELETE FROM limit_requests WHERE workspace = ?1",
38 "DELETE FROM trial_grants WHERE workspace = ?1",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging39 "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's40 "DELETE FROM card_checks WHERE workspace = ?1",
41 "DELETE FROM alerts_sent WHERE workspace = ?1",
42 "DELETE FROM price_notices WHERE workspace = ?1",
43 "DELETE FROM pending_usage WHERE workspace = ?1",
44 "DELETE FROM pending_days WHERE workspace = ?1",
45 "DELETE FROM month_closes WHERE workspace = ?1",
46 "DELETE FROM storage_days WHERE workspace = ?1",
47 "DELETE FROM package_storage_days WHERE workspace = ?1",
48 "DELETE FROM token_usage WHERE workspace = ?1",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens49 "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's50 "DELETE FROM spikes WHERE workspace = ?1",
51 "DELETE FROM closed_workspaces WHERE workspace = ?1",
52 "DELETE FROM sales_records WHERE workspace = ?1",
53 "DELETE FROM sales_notes WHERE workspace = ?1",
54 "DELETE FROM workspace_costs WHERE workspace = ?1",
55 "DELETE FROM margin_alerts WHERE kind = 'workspace' AND subject = ?1",
56 // Allowances drawn by the workspace, and its repositories' shares of
57 // the open-source pool (`<slug>/<name>`, compared exactly).
58 "DELETE FROM allowance_use WHERE scope = ?1 OR (kind = 'oss_repo' AND substr(scope, 1, length(?1) + 1) = ?1 || '/')",
59 "DELETE FROM budget_alerts WHERE account = ?2",
60 "DELETE FROM billing_accounts WHERE id = ?2",
61];
62
63impl 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 sold64 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's65 let workspace = a.workspace.trim().to_lowercase();
66 if workspace.is_empty() || a.by.trim().is_empty() {
67 return Ok(Outcome::fail(FailureCode::Invalid, "A reset needs a workspace and who did it."));
68 }
69 if a.note.trim().len() < 5 {
70 return Ok(Outcome::fail(FailureCode::Invalid, "Say why it is reset, for whoever looks next."));
71 }
72 if a.confirm.trim() != workspace {
73 return Ok(Outcome::fail(FailureCode::Invalid, format!("Type the workspace's slug, {workspace}, exactly, to reset it.")));
74 }
75 if self.stripe.as_ref().is_some_and(|s| s.live()) {
76 return Ok(Outcome::fail(FailureCode::Forbidden, "Billing takes real cards: a workspace's billing is never wiped."));
77 }
78 #[derive(Deserialize)]
79 struct Found {
80 comped: i64,
81 enterprise: i64,
82 }
83 let found = self
84 .db
85 .prepare(format!(
86 "SELECT CASE WHEN ?1 IN ({}) THEN 1 ELSE 0 END AS comped,
87 (SELECT COUNT(*) FROM account_members WHERE workspace = ?1) AS enterprise",
88 crate::sales::INTERNAL_SQL
89 ))
90 .bind(&[workspace.as_str().into()])?
91 .first::<Found>(None)
92 .await?;
93 if let Some(found) = found {
94 if found.comped > 0 {
95 return Ok(Outcome::fail(FailureCode::Forbidden, format!("{workspace} is comped (g1t's own): its spend is a budget, kept.")));
96 }
97 if found.enterprise > 0 {
98 return Ok(Outcome::fail(FailureCode::Forbidden, format!("An enterprise pays for {workspace}: move it off first.")));
99 }
100 }
101 let account = own_account(&workspace);
102 let mut batch = Vec::with_capacity(STATEMENTS.len());
103 for sql in STATEMENTS {
104 let values: Vec<JsValue> =
105 [workspace.as_str(), account.as_str()][..crate::rename::parameters(sql)].iter().map(|v| (*v).into()).collect();
106 batch.push(self.db.prepare(*sql).bind(&values)?);
107 }
108 let mut rows = 0usize;
109 for result in self.db.batch(batch).await? {
110 rows += result.meta()?.and_then(|m| m.changes).unwrap_or(0);
111 }
112 self.audit(&account, "reset", &format!("billing of {workspace} reset ({rows} rows): {}", a.note.trim()), &a.by).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 sold113 // The margin figures still hold the workspace's past usage: redo
114 // them now (the day's analysis: the bill, 31 days, the alerts), so
115 // the pages show the reset at once.
116 let refreshed = match self.costs_daily(env, &crate::keeper::Keeper::from_env(env)).await {
117 Ok(run) => run.problems.is_empty(),
118 Err(error) => {
119 worker::console_error!("costs after a reset of {workspace}: {error}");
120 false
121 }
122 };
123 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's124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn every_table_with_a_workspace_is_wiped_or_kept_on_purpose() {
133 let all = STATEMENTS.join("\n");
134 // What is kept: the audit log, and g1t's own counts compared with
135 // Cloudflare's bill.
136 let kept = ["admin_actions", "own_counts"];
137 for table in [
138 "ledger", "runs", "checkouts", "workspace_invoices", "workspace_invoice_lines", "sales_notes", "accounts",
139 "pending_usage", "pending_days", "limits", "subscriptions", "month_closes", "sales_records",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging140 "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 page141 "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's142 "card_checks", "alerts_sent", "price_notices", "closed_workspaces", "workspace_costs",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens143 "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's144 ] {
145 assert!(!kept.contains(&table));
146 assert!(all.contains(&format!("DELETE FROM {table} WHERE")), "{table}");
147 }
148 }
149
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 page150 /// The tables the migrations leave: every one made, less those dropped.
151 fn live_tables() -> std::collections::BTreeSet<String> {
152 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("migrations");
153 let mut files: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().path()).collect();
154 files.sort();
155 let mut live = std::collections::BTreeSet::new();
156 for file in files {
157 let sql = std::fs::read_to_string(file).unwrap();
158 for line in sql.lines().map(str::trim) {
159 let words: Vec<&str> = line.split(|c: char| c.is_whitespace() || c == '(' || c == ';').filter(|w| !w.is_empty()).collect();
160 let name = |at: usize| words.get(at).map(|w| w.to_string());
161 match words.as_slice() {
162 ["CREATE", "TABLE", "IF", "NOT", "EXISTS", ..] => live.extend(name(5)),
163 ["CREATE", "TABLE", ..] => live.extend(name(2)),
164 ["DROP", "TABLE", "IF", "EXISTS", ..] => {
165 name(4).map(|n| live.remove(&n));
166 }
167 ["DROP", "TABLE", ..] => {
168 name(2).map(|n| live.remove(&n));
169 }
170 _ => {}
171 }
172 }
173 }
174 live
175 }
176
177 #[test]
178 fn every_table_wiped_is_one_the_migrations_leave() {
179 let live = live_tables();
180 assert!(live.contains("ledger") && !live.contains("sandbox_months"), "{live:?}");
181 for sql in STATEMENTS {
182 let table = sql.split_whitespace().nth(2).unwrap();
183 assert!(live.contains(table), "{table} is not a table after the migrations");
184 }
185 }
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 #[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 tables188 fn a_rename_moves_every_table_a_reset_wipes() {
189 let moved = crate::rename::STATEMENTS.join("\n");
190 // Lines follow their invoice, which carries the workspace.
191 for sql in STATEMENTS.iter().filter(|sql| !sql.contains("workspace_invoice_lines")) {
192 let table = sql.split_whitespace().nth(2).unwrap();
193 assert!(moved.contains(&format!(" {table} ")), "{table} is wiped on a reset but not moved on a rename");
194 }
195 }
196
197 #[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's198 fn statements_name_at_most_the_workspace_and_its_account() {
199 assert!(STATEMENTS.iter().all(|sql| crate::rename::parameters(sql) <= 2));
200 assert_eq!(crate::rename::parameters(STATEMENTS.last().unwrap()), 2);
201 }
202}

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