Skip to content
482 linesCodeBlameRaw

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.

The statement is a month at a time, a line per kind of charge1//! A workspace's statement: a month of its ledger, grouped so it can be
2//! read at a glance (by day or by project, a line per kind of charge),
3//! with each line's entries a page at a time.
4
5use g1t_contracts::billing::{
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas6 Covered, LedgerEntry, MeterUsage, Statement, StatementArgs, StatementEntriesArgs, StatementGroup, StatementLine, StatementTotals,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging7 UsageMetersArgs,
The statement is a month at a time, a line per kind of charge8};
9use g1t_contracts::time::rfc3339;
10use g1t_contracts::Outcome;
11use g1t_kit::now_ms;
12use serde::Deserialize;
13use worker::Result;
14use worker::wasm_bindgen::JsValue;
15
16use crate::{Billing, LedgerRow, members_only};
17
18/// Entries in one page of a line.
19const PAGE: u32 = 50;
20
21/// The kind of charge a ledger row is, as the statement names it. One SQL
22/// expression, so grouping and filtering agree.
Prices are what g1t pays plus 20%, from the first second23/// `Runs on your own model provider` is only on older months: those runs
24/// carried a flat fee then, and pay only their sandbox time now.
The statement is a month at a time, a line per kind of charge25pub(crate) const KIND_SQL: &str = "CASE
26 WHEN kind = 'top_up' AND reference LIKE 'crd%' THEN 'Credits from g1t'
27 WHEN kind = 'top_up' AND amount_micros < 0 THEN 'Refunds'
28 WHEN kind = 'top_up' THEN 'Payments'
29 WHEN task = 'sandbox' THEN 'Sandbox time'
Fast pages, required checks on the branch, self-hosted runners, honest incidents30 WHEN task = 'self_hosted' THEN 'Self-hosted runner time'
The statement is a month at a time, a line per kind of charge31 WHEN task = 'deployments' THEN 'Deployments'
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put32 WHEN task = 'security' THEN 'Security scans'
33 WHEN task = 'context' THEN 'Search embeddings'
34 WHEN task = 'storage' THEN 'Private storage'
Fast pages, required checks on the branch, self-hosted runners, honest incidents35 WHEN task = 'cache' THEN 'Actions cache storage'
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look36 WHEN task = 'git' THEN 'Git operations'
The statement is a month at a time, a line per kind of charge37 WHEN billed_to = 'workspace' THEN 'Runs on your own model provider'
38 ELSE 'Agent runs' END";
39
40/// The order lines appear in within a group.
41pub(crate) fn kind_order(kind: &str) -> u8 {
42 match kind {
43 "Agent runs" => 0,
44 "Runs on your own model provider" => 1,
45 "Sandbox time" => 2,
Fast pages, required checks on the branch, self-hosted runners, honest incidents46 "Self-hosted runner time" => 2,
The statement is a month at a time, a line per kind of charge47 "Deployments" => 3,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put48 "Private storage" => 4,
Fast pages, required checks on the branch, self-hosted runners, honest incidents49 "Actions cache storage" => 4,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50 "Git operations" => 5,
51 "Search embeddings" => 6,
52 "Security scans" => 7,
53 "Payments" => 8,
54 "Credits from g1t" => 9,
55 "Refunds" => 10,
56 _ => 11,
The statement is a month at a time, a line per kind of charge57 }
58}
59
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging60/// Payments, credits and refunds: money in, which has no price.
61pub(crate) fn is_money_in(kind: &str) -> bool {
62 matches!(kind, "Payments" | "Credits from g1t" | "Refunds")
63}
64
65/// A usage line at its price: what was charged, what paid for it first,
66/// and what the account's discount took off.
67pub(crate) fn line_price(charged: i64, covered: i64, discount: i64) -> i64 {
68 charged + covered + discount
69}
70
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas71/// The Billing page's meters, in order: key and label.
72pub(crate) const METERS: [(&str, &str); 6] = [
73 ("agents", "Agents & sandboxes"),
74 ("builds", "Builds"),
75 ("requests", "Requests & CPU"),
76 ("domains", "Custom domains"),
77 ("git_storage", "Git operations & storage"),
78 ("search_scans", "Search & security scans"),
79];
80
81/// The meter a ledger row's usage is on. One SQL expression, as `KIND_SQL`.
82pub(crate) const METER_SQL: &str = "CASE
83 WHEN task = 'deployments' AND reference LIKE 'deploy/%' THEN 'builds'
84 WHEN task = 'deployments' THEN 'requests'
Fast pages, required checks on the branch, self-hosted runners, honest incidents85 WHEN task IN ('storage', 'git', 'cache') THEN 'git_storage'
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas86 WHEN task IN ('security', 'context') THEN 'search_scans'
87 ELSE 'agents' END";
88
89/// The meter usage noted through the month (`pending_usage.source`) is on.
90pub(crate) fn meter_of_source(source: &str) -> &'static str {
91 match source {
92 "deployments" => "requests",
93 "domains" => "domains",
Fast pages, required checks on the branch, self-hosted runners, honest incidents94 "storage" | "git" | "cache" => "git_storage",
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas95 _ => "search_scans",
96 }
97}
98
99/// `41 build minutes`, from seconds, rounded up.
100pub(crate) fn build_minutes(seconds: i64) -> Option<String> {
101 let minutes = (seconds.max(0) + 59) / 60;
102 (seconds > 0).then(|| format!("{} build minute{}", crate::features::thousands(minutes as u64), if minutes == 1 { "" } else { "s" }))
103}
104
105/// `12 runs`.
106pub(crate) fn runs(count: u32) -> Option<String> {
107 (count > 0).then(|| format!("{} run{}", crate::features::thousands(count.into()), if count == 1 { "" } else { "s" }))
108}
109
110/// `12,345 git operations, 0.42 GB private`.
111pub(crate) fn git_and_storage(operations: u64, private_bytes: i64) -> Option<String> {
112 let mut parts = vec![];
113 if operations > 0 {
114 parts.push(format!("{} git operation{}", crate::features::thousands(operations), if operations == 1 { "" } else { "s" }));
115 }
116 if private_bytes > 0 {
117 parts.push(format!("{:.2} GB private", private_bytes as f64 / 1e9));
118 }
119 (!parts.is_empty()).then(|| parts.join(", "))
120}
121
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put122/// What paid for usage before it was charged, as the statement names it,
123/// with the ledger column that holds it.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look124pub(crate) const COVERED: [(&str, &str, &str); 4] = [
125 ("included", "credit_micros", "Paid by your plan's included usage"),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put126 ("trial", "trial_micros", "Paid by your trial credit"),
127 ("oss_pool", "oss_micros", "Paid by g1t's open-source pool"),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look128 ("given", "given_micros", "Covered by g1t"),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put129];
130
131/// The statement's lines for what paid: each source with anything to show.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look132pub(crate) fn covered_lines(sums: [i64; 4]) -> Vec<Covered> {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put133 COVERED
134 .iter()
135 .zip(sums)
136 .filter(|(_, micros)| *micros > 0)
137 .map(|((source, _, label), micros)| Covered { source: (*source).to_owned(), label: (*label).to_owned(), micros })
138 .collect()
139}
140
The statement is a month at a time, a line per kind of charge141/// `2026-10` and the first instant of the next month, for a range.
142pub(crate) fn month_range(month: &str) -> Option<(String, String)> {
143 let year: i32 = month.get(..4)?.parse().ok()?;
144 let number: u32 = month.get(5..7)?.parse().ok()?;
145 if month.len() != 7 || !(1..=12).contains(&number) {
146 return None;
147 }
148 let next = if number == 12 { format!("{}-01", year + 1) } else { format!("{year}-{:02}", number + 1) };
149 Some((format!("{month}-01"), format!("{next}-01")))
150}
151
152#[derive(Deserialize)]
153struct Row {
154 group_key: Option<String>,
155 kind: String,
156 count: u32,
157 amount: Option<i64>,
158 cost: Option<i64>,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put159 credit: Option<i64>,
160 trial: Option<i64>,
161 oss: Option<i64>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look162 given: Option<i64>,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging163 discount: Option<i64>,
The statement is a month at a time, a line per kind of charge164}
165
166#[derive(Deserialize)]
167struct Month {
168 month: String,
169}
170
171impl Billing {
172 pub(crate) async fn statement(&self, a: StatementArgs) -> Result<Outcome<Statement>> {
173 let workspace = a.workspace.to_lowercase();
174 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
175 return Ok(members_only());
176 }
177 let current = rfc3339(now_ms())[..7].to_owned();
178 let month = a.month.filter(|m| month_range(m).is_some()).unwrap_or(current);
179 let (from, until) = month_range(&month).expect("a valid month");
180 let by_project = a.group.as_deref() == Some("project");
181 let group_sql = if by_project { "COALESCE(repo, '')" } else { "substr(created_at, 1, 10)" };
182 let rows = self
183 .db
184 .prepare(format!(
185 "SELECT {group_sql} AS group_key, {KIND_SQL} AS kind, COUNT(*) AS count,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put186 SUM(amount_micros) AS amount, SUM(cost_micros) AS cost,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look187 SUM(credit_micros) AS credit, SUM(trial_micros) AS trial, SUM(oss_micros) AS oss,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging188 SUM(given_micros) AS given, SUM(discount_micros) AS discount
The statement is a month at a time, a line per kind of charge189 FROM ledger WHERE workspace = ?1 AND created_at >= ?2 AND created_at < ?3
190 GROUP BY 1, 2"
191 ))
192 .bind(&[workspace.as_str().into(), from.as_str().into(), until.as_str().into()])?
193 .all()
194 .await?
195 .results::<Row>()?;
196
197 let mut groups: Vec<StatementGroup> = vec![];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look198 let mut covered = [0i64; 4];
The statement is a month at a time, a line per kind of charge199 for row in rows {
200 let key = row.group_key.unwrap_or_default();
201 let amount = row.amount.unwrap_or(0);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look202 let paid_for = [row.credit.unwrap_or(0), row.trial.unwrap_or(0), row.oss.unwrap_or(0), row.given.unwrap_or(0)];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put203 for (total, micros) in covered.iter_mut().zip(paid_for) {
204 *total += micros;
205 }
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging206 let discount = row.discount.unwrap_or(0);
The statement is a month at a time, a line per kind of charge207 let line = StatementLine {
208 // Charges positive, money in negative, as a statement reads.
209 charged_micros: -amount,
210 cost_micros: row.cost.unwrap_or(0),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put211 covered_micros: paid_for.iter().sum(),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging212 price_micros: if is_money_in(&row.kind) { 0 } else { line_price(-amount, paid_for.iter().sum(), discount) },
213 discount_micros: discount,
214 kind: row.kind,
215 count: row.count,
The statement is a month at a time, a line per kind of charge216 };
217 match groups.iter_mut().find(|g| g.key == key) {
218 Some(group) => group.lines.push(line),
219 None => groups.push(StatementGroup {
220 label: if key.is_empty() { "Not one project".to_owned() } else { key.clone() },
221 key,
222 lines: vec![line],
223 charged_micros: 0,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging224 price_micros: 0,
225 discount_micros: 0,
The statement is a month at a time, a line per kind of charge226 }),
227 }
228 }
229 for group in &mut groups {
230 group.lines.sort_by_key(|line| kind_order(&line.kind));
231 group.charged_micros = group.lines.iter().filter(|l| l.charged_micros > 0).map(|l| l.charged_micros).sum();
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging232 group.price_micros = group.lines.iter().map(|l| l.price_micros).sum();
233 group.discount_micros = group.lines.iter().map(|l| l.discount_micros).sum();
The statement is a month at a time, a line per kind of charge234 }
235 if by_project {
Billing keeps Stripe's view itself: the saved card on the account, missed events replayed every 15 minutes, and the endpoint kept236 groups.sort_by_key(|a| std::cmp::Reverse(a.charged_micros));
The statement is a month at a time, a line per kind of charge237 } else {
238 groups.sort_by(|a, b| b.key.cmp(&a.key));
239 }
240 let lines = groups.iter().flat_map(|g| g.lines.iter());
241 let totals = StatementTotals {
242 charged_micros: lines.clone().filter(|l| l.charged_micros > 0).map(|l| l.charged_micros).sum(),
243 paid_micros: lines.clone().filter(|l| l.charged_micros < 0).map(|l| -l.charged_micros).sum(),
244 cost_micros: lines.clone().map(|l| l.cost_micros).sum(),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging245 price_micros: lines.clone().map(|l| l.price_micros).sum(),
246 discount_micros: lines.clone().map(|l| l.discount_micros).sum(),
247 discount_percent: Some(self.terms_of(&workspace).await?.percent_off()).filter(|p| *p > 0),
The statement is a month at a time, a line per kind of charge248 entries: lines.map(|l| l.count).sum(),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put249 covered: covered_lines(covered),
250 carried_micros: self.carried(&workspace, &month).await?,
The statement is a month at a time, a line per kind of charge251 };
252 let months = self
253 .db
254 .prepare("SELECT DISTINCT substr(created_at, 1, 7) AS month FROM ledger WHERE workspace = ? ORDER BY 1 DESC LIMIT 36")
255 .bind(&[workspace.as_str().into()])?
256 .all()
257 .await?
258 .results::<Month>()?
259 .into_iter()
260 .map(|m| m.month)
261 .collect();
262 Ok(Outcome::Ok(Statement { month, months, groups, totals }))
263 }
264
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas265 /// `usage_meters`: this month's usage by meter, at cost plus the margin
266 /// (with a custom discount, if the account has one) before the plan's
267 /// included usage or a pool paid for any of it. Comped workspaces see
268 /// what it would cost. Usage charged through the month is on the
269 /// ledger; usage charged when the month closes (app traffic, custom
270 /// domains, storage, git operations, search and scans) is what has been
271 /// noted so far.
272 pub(crate) async fn usage_meters(&self, a: UsageMetersArgs) -> Result<Outcome<Vec<MeterUsage>>> {
273 let workspace = a.workspace.to_lowercase();
274 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
275 return Ok(members_only());
276 }
277 let now = rfc3339(now_ms());
278 let month = now[..7].to_owned();
279 let (from, until) = month_range(&month).expect("a valid month");
280 #[derive(Deserialize)]
281 struct Used {
282 meter: String,
283 count: u32,
284 cost: Option<f64>,
285 }
286 // A month's close for deployments is charged in the next month;
287 // it is last month's, so it is left out here.
288 let ledger = self
289 .db
290 .prepare(format!(
291 "SELECT {METER_SQL} AS meter, COUNT(*) AS count, SUM(cost_micros) AS cost
292 FROM ledger WHERE workspace = ?1 AND kind = 'usage' AND created_at >= ?2 AND created_at < ?3
293 AND COALESCE(reference, '') NOT LIKE 'deployments/%'
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged294 -- Runs on the workspace's own model provider are its provider's to bill, never g1t's.
295 AND COALESCE(billed_to, 'g1t') = 'g1t'
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas296 GROUP BY 1"
297 ))
298 .bind(&[workspace.as_str().into(), from.as_str().into(), until.as_str().into()])?
299 .all()
300 .await?
301 .results::<Used>()?;
302 #[derive(Deserialize)]
303 struct Pending {
304 source: String,
305 cost_micros: Option<f64>,
306 detail: Option<String>,
307 }
308 let pending = self
309 .db
310 .prepare("SELECT source, cost_micros, detail FROM pending_usage WHERE workspace = ? AND month = ? AND charged_at IS NULL")
311 .bind(&[workspace.as_str().into(), month.as_str().into()])?
312 .all()
313 .await?
314 .results::<Pending>()?;
315 let terms = self.terms_of(&workspace).await?;
316 let price = |cost: i64| {
317 let charge = crate::credits::with_margin(cost, self.margin_percent);
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging318 if terms.full_discount() { charge } else { terms.apply(charge) }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas319 };
320 let mut meters: Vec<MeterUsage> = METERS
321 .iter()
322 .map(|(key, label)| MeterUsage { key: (*key).to_owned(), label: (*label).to_owned(), micros: 0, quantity: None })
323 .collect();
324 let mut agent_runs = 0;
325 for used in ledger {
326 if let Some(meter) = meters.iter_mut().find(|m| m.key == used.meter) {
327 meter.micros += price(used.cost.unwrap_or(0.0).round() as i64);
328 }
329 if used.meter == "agents" {
330 agent_runs = used.count;
331 }
332 }
333 for row in &pending {
334 let key = meter_of_source(&row.source);
335 if let Some(meter) = meters.iter_mut().find(|m| m.key == key) {
336 meter.micros += price(row.cost_micros.unwrap_or(0.0).round() as i64);
337 if matches!(key, "requests" | "domains") {
338 meter.quantity = row.detail.clone().filter(|d| !d.is_empty());
339 }
340 }
341 }
342 let build_seconds = self.allowance_used("build_seconds", &workspace, &month).await?;
343 let operations = self.git_operations_this_month(&workspace).await?;
344 let stored = self.private_storage(&workspace).await?;
345 for meter in &mut meters {
346 match meter.key.as_str() {
347 "agents" => meter.quantity = runs(agent_runs),
348 "builds" => meter.quantity = build_minutes(build_seconds),
349 "git_storage" => meter.quantity = git_and_storage(operations, stored),
350 _ => {}
351 }
352 }
353 Ok(Outcome::Ok(meters))
354 }
355
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put356 /// What was owed when `month` closed but was under the minimum charge,
357 /// and so carried over to the next invoice.
358 async fn carried(&self, workspace: &str, month: &str) -> Result<i64> {
359 #[derive(Deserialize)]
360 struct Row {
361 amount_micros: i64,
362 }
363 Ok(self
364 .db
365 .prepare("SELECT amount_micros FROM month_closes WHERE workspace = ? AND month = ? AND status = 'carried'")
366 .bind(&[workspace.into(), month.into()])?
367 .first::<Row>(None)
368 .await?
369 .map_or(0, |row| row.amount_micros))
370 }
371
The statement is a month at a time, a line per kind of charge372 /// One line's entries, newest first, a page at a time.
373 pub(crate) async fn statement_entries(&self, a: StatementEntriesArgs) -> Result<Outcome<Vec<LedgerEntry>>> {
374 let workspace = a.workspace.to_lowercase();
375 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
376 return Ok(members_only());
377 }
378 let Some((from, until)) = month_range(&a.month) else {
379 return Ok(Outcome::Ok(vec![]));
380 };
381 let mut filters = vec![format!("({KIND_SQL}) = ?4")];
382 let mut values: Vec<JsValue> = vec![
383 workspace.as_str().into(),
384 from.as_str().into(),
385 until.as_str().into(),
386 a.kind.as_str().into(),
387 ];
388 if let Some(day) = a.day.as_deref().filter(|d| d.len() == 10) {
389 values.push(day.into());
390 filters.push(format!("substr(created_at, 1, 10) = ?{}", values.len()));
391 }
392 if let Some(project) = &a.project {
393 values.push(project.as_str().into());
394 filters.push(format!("COALESCE(repo, '') = ?{}", values.len()));
395 }
396 if let Some(before) = &a.before {
397 values.push(before.as_str().into());
398 filters.push(format!("id < ?{}", values.len()));
399 }
400 let rows = self
401 .db
402 .prepare(format!(
403 "SELECT * FROM ledger WHERE workspace = ?1 AND created_at >= ?2 AND created_at < ?3 AND {}
404 ORDER BY id DESC LIMIT {PAGE}",
405 filters.join(" AND ")
406 ))
407 .bind(&values)?
408 .all()
409 .await?
410 .results::<LedgerRow>()?;
411 Ok(Outcome::Ok(rows.into_iter().map(LedgerEntry::from).collect()))
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn a_month_runs_to_the_first_of_the_next() {
421 assert_eq!(month_range("2026-10"), Some(("2026-10-01".into(), "2026-11-01".into())));
422 assert_eq!(month_range("2026-12"), Some(("2026-12-01".into(), "2027-01-01".into())));
423 assert_eq!(month_range("2026-13"), None);
424 assert_eq!(month_range("oops"), None);
425 }
426
427 #[test]
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging428 fn a_discounted_line_shows_its_price_and_the_discount() {
429 // $1.20 of usage on a 100% discount: charged nothing, all of it off.
430 assert_eq!(line_price(0, 0, 1_200_000), 1_200_000);
431 // 30% off, $0.20 of it paid by included usage first.
432 assert_eq!(line_price(700_000, 140_000, 360_000), 1_200_000);
433 assert!(is_money_in("Payments") && is_money_in("Credits from g1t") && is_money_in("Refunds"));
434 assert!(!is_money_in("Agent runs"));
435 }
436
437 #[test]
The statement is a month at a time, a line per kind of charge438 fn usage_lines_come_before_money_in() {
439 assert!(kind_order("Agent runs") < kind_order("Sandbox time"));
440 assert!(kind_order("Deployments") < kind_order("Payments"));
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put441 assert!(kind_order("Security scans") < kind_order("Payments"));
442 }
443
444 #[test]
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas445 fn usage_falls_on_the_meters_the_billing_page_shows() {
446 let keys: Vec<_> = METERS.iter().map(|(key, _)| *key).collect();
447 assert_eq!(keys, ["agents", "builds", "requests", "domains", "git_storage", "search_scans"]);
448 assert_eq!(meter_of_source("deployments"), "requests");
449 assert_eq!(meter_of_source("domains"), "domains");
450 assert_eq!(meter_of_source("git"), "git_storage");
451 assert_eq!(meter_of_source("storage"), "git_storage");
452 assert_eq!(meter_of_source("context"), "search_scans");
453 assert_eq!(meter_of_source("security"), "search_scans");
454 for key in ["builds", "requests", "git_storage", "search_scans", "agents"] {
455 assert!(METER_SQL.contains(&format!("'{key}'")));
456 }
457 }
458
459 #[test]
460 fn quantities_read_plainly() {
461 assert_eq!(build_minutes(0), None);
462 assert_eq!(build_minutes(1).as_deref(), Some("1 build minute"));
463 assert_eq!(build_minutes(2_461).as_deref(), Some("42 build minutes"));
464 assert_eq!(runs(1).as_deref(), Some("1 run"));
465 assert_eq!(runs(1_200).as_deref(), Some("1,200 runs"));
466 assert_eq!(git_and_storage(0, 0), None);
467 assert_eq!(git_and_storage(12_345, 420_000_000).as_deref(), Some("12,345 git operations, 0.42 GB private"));
468 assert_eq!(git_and_storage(1, 0).as_deref(), Some("1 git operation"));
469 }
470
471 #[test]
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put472 fn the_statement_says_what_paid_before_the_workspace_did() {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look473 let lines = covered_lines([0, 250_000, 120_000, 0]);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put474 assert_eq!(lines.len(), 2);
475 assert_eq!(lines[0].label, "Paid by your trial credit");
476 assert_eq!(lines[1], Covered { source: "oss_pool".into(), label: "Paid by g1t's open-source pool".into(), micros: 120_000 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look477 assert!(covered_lines([0, 0, 0, 0]).is_empty());
478 let lines = covered_lines([4_000_000, 0, 0, 30_000]);
479 assert_eq!(lines[0].label, "Paid by your plan's included usage");
480 assert_eq!(lines[1].label, "Covered by g1t");
The statement is a month at a time, a line per kind of charge481 }
482}