Skip to content
507 linesCodeBlameRaw
1//! The Usage page: a workspace's usage over a range of days, at price, by
2//! product family, meter, project and day, with what paid for it.
3//!
4//! Every figure comes from the ledger's own lines, measured as the
5//! statement measures them (`statement::PRICE_SQL`: what was charged, plus
6//! what included usage, the trial, a pool or g1t paid, plus what a discount
7//! took off), so the Usage page and Billing show the same numbers. Usage metered through the month and charged
8//! when it closes (storage, git operations, scans, embeddings, domains,
9//! app traffic) is added from `pending_usage`, at cost plus the margin, and
10//! said to be pending.
11//!
12//! Lines from before g1t stopped running agents and selling model access
13//! (agent runs, the agent rate, AI Gateway requests), and any line on a
14//! task this page does not know, are kept on one meter, Other, in a
15//! product of their own: nothing charged is ever left out.
16
17use std::collections::{BTreeMap, BTreeSet};
18
19use futures_util::future::{try_join, try_join3};
20use g1t_contracts::billing::{
21 Allowance, MeterLine, PlanKind, ProductUsage, ProjectUsage, UsageDay, UsageReport, UsageReportArgs, UsageTotals, PRODUCTS,
22};
23use g1t_contracts::time::{parse_rfc3339, rfc3339};
24use g1t_contracts::{FailureCode, Outcome};
25use g1t_kit::now_ms;
26use serde::Deserialize;
27use worker::Result;
28
29use crate::charged::{charged, pending_split};
30use crate::statement::PRICE_SQL;
31use crate::{Billing, members_only};
32
33/// The longest range the page reads at once.
34pub(crate) const MAX_DAYS: u64 = 400;
35const DAY_MS: u64 = 86_400_000;
36
37/// Which meter a ledger line is on. One SQL expression, so every query
38/// agrees with `METERS`.
39pub(crate) const METER_KEY_SQL: &str = "CASE
40 WHEN task = 'sandbox' THEN 'sandbox'
41 WHEN task = 'self_hosted' THEN 'self_hosted'
42 WHEN task = 'deployments' AND reference LIKE 'deploy/%' THEN 'builds'
43 WHEN task = 'deployments' THEN 'requests'
44 WHEN task = 'domains' THEN 'domains'
45 WHEN task = 'git' THEN 'git'
46 WHEN task = 'storage' THEN 'storage'
47 WHEN task = 'cache' THEN 'cache'
48 WHEN task = 'package_storage' THEN 'packages'
49 WHEN task = 'security' THEN 'security'
50 WHEN task = 'context' THEN 'context'
51 ELSE 'other' END";
52
53/// Every meter: key, name, product family and the unit of its quantity.
54pub(crate) const METERS: [(&str, &str, &str, &str); 12] = [
55 ("sandbox", "Sandbox time", "sandboxes", "seconds"),
56 ("self_hosted", "Self-hosted runner time", "sandboxes", "seconds"),
57 ("builds", "Builds", "deployments", "entries"),
58 ("requests", "Requests & CPU", "deployments", "entries"),
59 ("domains", "Custom domains", "deployments", "entries"),
60 ("git", "Git operations", "git_storage", "operations"),
61 ("storage", "Private storage", "git_storage", "bytes"),
62 ("cache", "Actions cache", "git_storage", "entries"),
63 ("packages", "Package storage", "packages", "entries"),
64 ("security", "Security scans", "security", "entries"),
65 ("context", "Search embeddings", "search", "entries"),
66 // Kept apart so nothing is ever lost: a line whose meter is unknown,
67 // and agents' and models' usage from before they were retired.
68 ("other", "Other", OTHER, "entries"),
69];
70
71/// The product of the Other meter: not one of `PRODUCTS`, listed last and
72/// only when it has something.
73pub(crate) const OTHER: &str = "other";
74
75/// The meter month-end usage noted so far (`pending_usage.source`) is on.
76pub(crate) fn meter_of_pending(source: &str) -> &'static str {
77 match source {
78 "deployments" => "requests",
79 "domains" => "domains",
80 "storage" => "storage",
81 "git" => "git",
82 "cache" => "cache",
83 "package_storage" => "packages",
84 "security" => "security",
85 "context" => "context",
86 _ => "other",
87 }
88}
89
90/// Every day from `from` to `until`, both included, or why not.
91pub(crate) fn days_between(from: &str, until: &str) -> std::result::Result<Vec<String>, &'static str> {
92 let (Some(start), Some(end)) = (parse_rfc3339(&format!("{from}T00:00:00Z")), parse_rfc3339(&format!("{until}T00:00:00Z"))) else {
93 return Err("Give the range as days, YYYY-MM-DD.");
94 };
95 if end < start {
96 return Err("The range ends before it starts.");
97 }
98 if (end - start) / DAY_MS + 1 > MAX_DAYS {
99 return Err("Ask for at most 400 days at a time.");
100 }
101 Ok((0..=(end - start) / DAY_MS).map(|n| rfc3339(start + n * DAY_MS)[..10].to_owned()).collect())
102}
103
104/// One group of ledger lines: a day, a meter and a project.
105#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
106pub(crate) struct Cell {
107 pub day: String,
108 pub meter: String,
109 #[serde(default)]
110 pub project: String,
111 #[serde(default)]
112 pub price: Option<f64>,
113 #[serde(default)]
114 pub quantity: Option<f64>,
115 #[serde(default)]
116 pub entries: Option<f64>,
117 #[serde(default)]
118 pub discount: Option<f64>,
119 #[serde(default)]
120 pub covered: Option<f64>,
121 #[serde(default)]
122 pub cost: Option<f64>,
123}
124
125/// What is metered this month and charged when it closes, by meter: at
126/// price, with what g1t covers of it and what the discount takes off, as
127/// the close will enter it (`charged::pending_split`).
128#[derive(Clone, Debug, Default, PartialEq)]
129pub(crate) struct Pending {
130 pub meter: &'static str,
131 pub micros: i64,
132 pub covered: i64,
133 pub discount: i64,
134}
135
136/// Whether a range of days reaches into the current month, so the month's
137/// usage not yet closed (`pending_usage`, which has no day of its own) is
138/// counted in it. `today` is billing's own UTC day, so the top bar, Spend,
139/// Home and Usage, which all ask for the first of the month to today, get
140/// the same answer whatever `until` they send: a range need not reach
141/// month end to include what the month has metered so far.
142pub(crate) fn reaches_this_month(from: &str, until: &str, today: &str) -> bool {
143 from <= today && until >= &format!("{}-01", &today[..7])[..]
144}
145
146/// Shapes the groups into the page: products in order, each with its
147/// meters, each meter with every day of the range and its projects. Only
148/// the products and projects asked for (all, when none are).
149pub(crate) fn shape(
150 days: &[String],
151 cells: &[Cell],
152 pending: &[Pending],
153 products: &[String],
154 projects: &[String],
155) -> (Vec<ProductUsage>, Vec<UsageDay>, UsageTotals, Vec<String>) {
156 let wanted_product = |product: &str| products.is_empty() || products.iter().any(|p| p == product);
157 let wanted_project = |project: &str| projects.is_empty() || projects.iter().any(|p| p == project);
158 let index: BTreeMap<&str, usize> = days.iter().enumerate().map(|(i, d)| (d.as_str(), i)).collect();
159 let product_of = |meter: &str| METERS.iter().find(|m| m.0 == meter).map_or(OTHER, |m| m.2);
160 let mut all_projects = BTreeSet::new();
161 let mut lines: Vec<MeterLine> = METERS
162 .iter()
163 .map(|(key, label, product, unit)| MeterLine {
164 key: (*key).to_owned(),
165 label: (*label).to_owned(),
166 product: (*product).to_owned(),
167 unit: (*unit).to_owned(),
168 quantity: 0.0,
169 micros: 0,
170 pending_micros: 0,
171 daily: vec![0; days.len()],
172 allowance: None,
173 by_project: vec![],
174 note: None,
175 })
176 .collect();
177 let mut by_day: BTreeMap<(String, String), i64> = BTreeMap::new();
178 let mut totals = UsageTotals::default();
179 let mut projects_of: BTreeMap<(String, String), (i64, f64)> = BTreeMap::new();
180 for cell in cells {
181 if !cell.project.is_empty() {
182 all_projects.insert(cell.project.clone());
183 }
184 let product = product_of(&cell.meter);
185 if !wanted_product(product) || !wanted_project(&cell.project) {
186 continue;
187 }
188 // A meter this page does not know is Other's: never dropped.
189 let meter = if METERS.iter().any(|m| m.0 == cell.meter) { cell.meter.as_str() } else { "other" };
190 let Some(line) = lines.iter_mut().find(|l| l.key == meter) else { continue };
191 let price = cell.price.unwrap_or(0.0).round() as i64;
192 line.micros += price;
193 line.quantity += if line.unit == "entries" { cell.entries.unwrap_or(0.0) } else { cell.quantity.unwrap_or(0.0) };
194 if let Some(i) = index.get(cell.day.as_str()) {
195 line.daily[*i] += price;
196 }
197 *by_day.entry((cell.day.clone(), product.to_owned())).or_default() += price;
198 let part = projects_of.entry((meter.to_owned(), cell.project.clone())).or_default();
199 part.0 += price;
200 part.1 += if line.unit == "entries" { cell.entries.unwrap_or(0.0) } else { cell.quantity.unwrap_or(0.0) };
201 totals.price_micros += price;
202 totals.discount_micros += cell.discount.unwrap_or(0.0).round() as i64;
203 totals.included_micros += cell.covered.unwrap_or(0.0).round() as i64;
204 totals.cost_micros += cell.cost.unwrap_or(0.0).round() as i64;
205 }
206 // Pending usage is no one project's: only when all projects are shown.
207 if projects.is_empty() {
208 for p in pending {
209 let product = product_of(p.meter);
210 if !wanted_product(product) {
211 continue;
212 }
213 if let Some(line) = lines.iter_mut().find(|l| l.key == p.meter) {
214 line.micros += p.micros;
215 line.pending_micros += p.micros;
216 }
217 totals.price_micros += p.micros;
218 totals.pending_micros += p.micros;
219 totals.pending_charged_micros += p.micros - p.covered - p.discount;
220 totals.included_micros += p.covered;
221 totals.discount_micros += p.discount;
222 }
223 }
224 for ((meter, project), (micros, quantity)) in projects_of {
225 if let Some(line) = lines.iter_mut().find(|l| l.key == meter) {
226 line.by_project.push(ProjectUsage { project, micros, quantity });
227 }
228 }
229 for line in &mut lines {
230 line.by_project.sort_by(|a, b| b.micros.cmp(&a.micros).then(a.project.cmp(&b.project)));
231 }
232 // The products that still have meters, in order, then Other when it
233 // has something.
234 let other_used = lines.iter().any(|l| l.product == OTHER && (l.micros != 0 || l.quantity != 0.0));
235 let products_out = PRODUCTS
236 .iter()
237 .copied()
238 .filter(|(key, _)| METERS.iter().any(|m| m.2 == *key))
239 .chain(other_used.then_some((OTHER, "Other")))
240 .filter(|(key, _)| wanted_product(key))
241 .map(|(key, label)| {
242 let meters: Vec<MeterLine> = lines.iter().filter(|l| l.product == key).cloned().collect();
243 ProductUsage {
244 key: key.to_owned(),
245 label: label.to_owned(),
246 micros: meters.iter().map(|m| m.micros).sum(),
247 meters,
248 features: vec![],
249 }
250 })
251 .collect();
252 let days_out = by_day.into_iter().filter(|(_, micros)| *micros != 0).map(|((day, product), micros)| UsageDay { day, product, micros }).collect();
253 (products_out, days_out, totals, all_projects.into_iter().collect())
254}
255
256impl Billing {
257 /// `usage_report`: members only.
258 pub(crate) async fn usage_report(&self, a: UsageReportArgs) -> Result<Outcome<UsageReport>> {
259 let workspace = a.workspace.to_lowercase();
260 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
261 return Ok(members_only());
262 }
263 let days = match days_between(&a.from, &a.until) {
264 Ok(days) => days,
265 Err(why) => return Ok(Outcome::fail(FailureCode::Invalid, why)),
266 };
267 let from = days[0].clone();
268 let until = days[days.len() - 1].clone();
269 // Lines entered on the last day count, whatever the hour.
270 let end = rfc3339(parse_rfc3339(&format!("{until}T00:00:00Z")).unwrap_or(0) + DAY_MS);
271 let measure = if self.free { "COALESCE(cost_micros, 0)" } else { PRICE_SQL };
272 let now = rfc3339(now_ms());
273 let month = now[..7].to_owned();
274 let this_month = reaches_this_month(&from, &until, &now[..10]);
275
276 let cells = async {
277 self.db
278 .prepare(format!(
279 "SELECT substr(created_at, 1, 10) AS day, {meter} AS meter, COALESCE(repo, '') AS project,
280 SUM({measure}) AS price, SUM(COALESCE(quantity, 0)) AS quantity, COUNT(*) AS entries,
281 SUM(COALESCE(discount_micros, 0)) AS discount,
282 SUM(COALESCE(credit_micros, 0) + COALESCE(trial_micros, 0) + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0)) AS covered,
283 SUM(COALESCE(cost_micros, 0)) AS cost
284 FROM ledger WHERE workspace = ?1 AND kind = 'usage' AND created_at >= ?2 AND created_at < ?3
285 GROUP BY 1, 2, 3 LIMIT 20000",
286 meter = METER_KEY_SQL
287 ))
288 .bind(&[workspace.as_str().into(), from.as_str().into(), end.as_str().into()])?
289 .all()
290 .await?
291 .results::<Cell>()
292 };
293 let pending = async {
294 if !this_month {
295 return Ok(vec![]);
296 }
297 self.pending_this_month(&[workspace.as_str().into()], &month).await
298 };
299 let credits = self.credit_paid_between(&workspace, &from, &end);
300 let ((cells, pending, (credits_paid, credits)), account) =
301 try_join(try_join3(cells, pending, credits), self.account_of(&workspace)).await?;
302 let percent = account.terms.percent_off();
303 // What is metered so far is entered on the account's terms when the
304 // month closes: its discount, and what g1t covers, come off it now.
305 let pending: Vec<Pending> = pending
306 .into_iter()
307 .map(|p| {
308 let split = pending_split(p.cost(), p.charge(), self.margin_percent, percent);
309 Pending { meter: meter_of_pending(&p.source), micros: split.price, covered: split.covered, discount: split.discount }
310 })
311 .filter(|p| p.micros > 0)
312 .collect();
313 let (mut products, days_out, mut totals, all_projects) = shape(&days, &cells, &pending, &a.products, &a.projects);
314 let filtered = !a.products.is_empty() || !a.projects.is_empty();
315 // Credit is the workspace's, not a product's or a project's: only
316 // without a filter is it taken off.
317 totals.credits_micros = if filtered { 0 } else { credits_paid.max(0) };
318 totals.charged_micros = charged(&totals);
319 let plan = self.plan_kind_for(&workspace, &account).await?;
320 // Allowances, for the range that includes this month.
321 let mut included = None;
322 let mut trial = None;
323 if this_month {
324 let (used, stored, operations) = futures_util::future::try_join3(
325 async {
326 if plan == PlanKind::Paid { self.allowance_used("plan_credit", &workspace, &month).await } else { Ok(0) }
327 },
328 self.private_storage(&workspace),
329 self.git_operations_this_month(&workspace),
330 )
331 .await?;
332 if plan == PlanKind::Paid {
333 included = Some(Allowance { used: used as f64, of: self.plans.plan_included_micros as f64, unit: "micros".to_owned() });
334 }
335 for product in &mut products {
336 for meter in &mut product.meters {
337 match meter.key.as_str() {
338 "storage" => {
339 meter.quantity = stored as f64;
340 meter.allowance = Some(Allowance { used: stored as f64, of: self.plans.free_storage_bytes as f64, unit: "bytes".to_owned() });
341 }
342 "git" => {
343 meter.quantity = operations as f64;
344 meter.allowance = Some(Allowance { used: operations as f64, of: self.plans.git_included as f64, unit: "operations".to_owned() });
345 }
346 _ => {}
347 }
348 }
349 }
350 if plan == PlanKind::Free
351 && let Some(grant) = self.grant_of(&workspace).await?
352 {
353 trial = Some(crate::credits::left(grant.granted_micros, grant.used_micros));
354 }
355 }
356 // AI credit bought before g1t stopped selling it: still the
357 // workspace's, and kept apart from credit for everything.
358 let ai: i64 = credits.grants.iter().filter(|g| g.scope == "models").map(|g| g.left_micros).sum();
359 Ok(Outcome::Ok(UsageReport {
360 from,
361 until,
362 totals,
363 days: days_out,
364 products,
365 projects: all_projects,
366 models: vec![],
367 by_agent: vec![],
368 by_person: vec![],
369 included,
370 discount_percent: (percent > 0).then_some(percent),
371 ai_credit_micros: ai,
372 credit_micros: credits.left_micros - ai,
373 trial_micros: trial,
374 plan,
375 free: self.free,
376 }))
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 fn cell(day: &str, meter: &str, project: &str, price: f64) -> Cell {
385 Cell { day: day.into(), meter: meter.into(), project: project.into(), price: Some(price), entries: Some(1.0), ..Cell::default() }
386 }
387
388 #[test]
389 fn a_range_is_every_day_in_it() {
390 let days = days_between("2026-10-01", "2026-10-03").unwrap();
391 assert_eq!(days, ["2026-10-01", "2026-10-02", "2026-10-03"]);
392 assert_eq!(days_between("2026-10-01", "2026-10-01").unwrap().len(), 1);
393 assert!(days_between("2026-10-03", "2026-10-01").is_err());
394 assert!(days_between("2025-01-01", "2026-10-01").is_err());
395 assert!(days_between("Oct 1", "2026-10-01").is_err());
396 }
397
398 #[test]
399 fn usage_is_shaped_into_products_meters_days_and_projects() {
400 let days = days_between("2026-10-01", "2026-10-03").unwrap();
401 let mut sandbox = cell("2026-10-02", "sandbox", "acme/web", 120_000.0);
402 sandbox.quantity = Some(600.0);
403 let cells = vec![
404 cell("2026-10-01", "requests", "acme/web", 2_000_000.0),
405 cell("2026-10-03", "requests", "acme/api", 1_000_000.0),
406 sandbox,
407 Cell { covered: Some(1_000_000.0), discount: Some(0.0), ..cell("2026-10-03", "storage", "", 30_000.0) },
408 ];
409 let pending = vec![Pending { meter: "git", micros: 40_000, ..Pending::default() }];
410 let (products, by_day, totals, projects) = shape(&days, &cells, &pending, &[], &[]);
411 // Every product family with meters, in order; no Other without usage on it.
412 assert_eq!(products.iter().map(|p| p.key.as_str()).collect::<Vec<_>>(), ["sandboxes", "deployments", "git_storage", "packages", "security", "search"]);
413 let deployments = &products[1];
414 assert_eq!(deployments.micros, 3_000_000);
415 let requests = deployments.meters.iter().find(|m| m.key == "requests").unwrap();
416 assert_eq!(requests.daily, [2_000_000, 0, 1_000_000]);
417 assert_eq!(requests.by_project[0].project, "acme/web");
418 let sandbox = products[0].meters.iter().find(|m| m.key == "sandbox").unwrap();
419 assert_eq!((sandbox.micros, sandbox.quantity), (120_000, 600.0));
420 // Pending usage counts in the totals and its meter, not on a day.
421 let git = products[2].meters.iter().find(|m| m.key == "git").unwrap();
422 assert_eq!((git.micros, git.pending_micros), (40_000, 40_000));
423 assert_eq!(totals.price_micros, 3_000_000 + 120_000 + 30_000 + 40_000);
424 assert_eq!(totals.pending_micros, 40_000);
425 assert_eq!(totals.included_micros, 1_000_000);
426 assert_eq!(by_day.iter().filter(|d| d.product == "deployments").map(|d| d.micros).sum::<i64>(), 3_000_000);
427 assert_eq!(projects, ["acme/api", "acme/web"]);
428 }
429
430 #[test]
431 fn usage_from_retired_or_unknown_meters_is_other_and_never_lost() {
432 let days = days_between("2026-10-01", "2026-10-02").unwrap();
433 // Lines from when g1t ran agents, as the ledger still has them, and
434 // a meter nothing knows.
435 let cells = vec![
436 cell("2026-10-01", "other", "acme/web", 2_000_000.0),
437 cell("2026-10-01", "agent_rate", "acme/web", 500_000.0),
438 cell("2026-10-02", "gateway", "", 70_000.0),
439 cell("2026-10-02", "sandbox", "acme/web", 10_000.0),
440 ];
441 let (products, by_day, totals, _) = shape(&days, &cells, &[], &[], &[]);
442 let other = products.last().unwrap();
443 assert_eq!((other.key.as_str(), other.label.as_str(), other.micros), ("other", "Other", 2_570_000));
444 assert_eq!(other.meters.len(), 1);
445 assert_eq!(other.meters[0].daily, [2_500_000, 70_000]);
446 // Everything charged is in the totals and on its day.
447 assert_eq!(totals.price_micros, 2_580_000);
448 assert_eq!(products.iter().map(|p| p.micros).sum::<i64>(), totals.price_micros);
449 assert_eq!(by_day.iter().map(|d| d.micros).sum::<i64>(), totals.price_micros);
450 // Asked for by name, Other is shown alone.
451 let (only, _, totals, _) = shape(&days, &cells, &[], &["other".into()], &[]);
452 assert_eq!(only.len(), 1);
453 assert_eq!(totals.price_micros, 2_570_000);
454 }
455
456 #[test]
457 fn filters_keep_only_the_products_and_projects_asked_for() {
458 let days = days_between("2026-10-01", "2026-10-01").unwrap();
459 let cells = vec![cell("2026-10-01", "sandbox", "acme/web", 100.0), cell("2026-10-01", "sandbox", "acme/api", 50.0), cell("2026-10-01", "requests", "acme/web", 10.0)];
460 let pending = vec![Pending { meter: "git", micros: 7, ..Pending::default() }];
461 let (products, _, totals, projects) = shape(&days, &cells, &pending, &["sandboxes".into()], &["acme/web".into()]);
462 assert_eq!(products.len(), 1);
463 assert_eq!(totals.price_micros, 100);
464 // Every project stays listed, for the filter itself.
465 assert_eq!(projects, ["acme/api", "acme/web"]);
466 }
467
468 #[test]
469 fn pending_usage_counts_at_price_with_what_is_covered_and_discounted() {
470 let days = days_between("2026-10-01", "2026-10-02").unwrap();
471 // Storage on a 100% discount, and scans g1t covers on a free workspace.
472 let pending = [
473 Pending { meter: "storage", micros: 9_000, covered: 0, discount: 9_000 },
474 Pending { meter: "security", micros: 2_400, covered: 2_400, discount: 0 },
475 ];
476 let (_, _, totals, _) = shape(&days, &[], &pending, &[], &[]);
477 assert_eq!(totals.price_micros, 11_400);
478 assert_eq!(totals.pending_micros, 11_400);
479 assert_eq!(totals.pending_charged_micros, 11_400 - totals.included_micros - totals.discount_micros);
480 assert_eq!(totals.included_micros, 2_400);
481 assert_eq!(totals.discount_micros, 9_000);
482 assert_eq!(charged(&totals), 0);
483 // With a project chosen, pending usage (no project's) is left out.
484 let (_, _, filtered, _) = shape(&days, &[], &pending, &[], &["acme/api".to_owned()]);
485 assert_eq!(filtered.pending_micros, 0);
486 }
487
488 #[test]
489 fn pending_usage_is_on_its_meter() {
490 assert_eq!(meter_of_pending("package_storage"), "packages");
491 assert_eq!(meter_of_pending("nonsense"), "other");
492 }
493
494 #[test]
495 fn pending_usage_counts_in_any_range_that_reaches_into_this_month() {
496 // The first of the month to today, as every page asks: counted.
497 assert!(reaches_this_month("2026-10-01", "2026-10-10", "2026-10-10"));
498 // To month end, or only the last week: counted too.
499 assert!(reaches_this_month("2026-10-01", "2026-10-31", "2026-10-10"));
500 assert!(reaches_this_month("2026-10-04", "2026-10-10", "2026-10-10"));
501 // Last month, or a month that has not started: not this month's.
502 assert!(!reaches_this_month("2026-09-01", "2026-09-30", "2026-10-10"));
503 assert!(!reaches_this_month("2026-11-01", "2026-11-30", "2026-10-10"));
504 // The month has turned on billing's clock: September's range no longer has it.
505 assert!(!reaches_this_month("2026-09-01", "2026-09-30", "2026-10-01"));
506 }
507}