Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
717 linesCodeBlameRaw
1//! Usage other services meter through the month, charged once it is over:
2//! security scans, the actions cache, and two billing measures itself each
3//! day: private repository storage and git operations. `context` (search
4//! embeddings) is no longer reported, since the context service was
5//! retired; it stays a source so usage noted before is still charged and
6//! closed.
7//!
8//! Each reports what it cost g1t so far this month (`note_pending`), so the
9//! workspace's limit counts it as it happens. When the month is over,
10//! billing charges it once: at cost plus the margin, on the account's
11//! terms, after the plan's included usage and the trial credit (see
12//! `credits`), dated the month's last second so it falls in that month's
13//! statement and invoice.
14//!
15//! The forge is free for every workspace up to the same amounts, on the
16//! plan or not; past them, a workspace on the plan pays at cost plus the
17//! margin and is never refused or slowed, and a free workspace is never
18//! charged but is held to them.
19//!
20//! **Git operations.** Cloudflare Artifacts charges g1t $0.15 per 1,000
21//! operations (clones, fetches, pushes) from 2026-10-14. The repos service
22//! counts those through g1t's git endpoints. Every workspace has
23//! `GIT_OPERATIONS_INCLUDED` (50,000) a month free; past it, the plan pays
24//! at cost plus the margin, and a free workspace is slowed down instead
25//! (the repos service's `GIT_OPERATIONS_FREE_CAP`, the same number).
26//!
27//! **Actions cache.** The actions service reports what each workspace's
28//! `actions/cache` entries held each day (source `cache`), at what R2
29//! charges g1t to store them ($0.015 a GB-month). Only the plan is charged
30//! for it, from the first byte; a free workspace is never charged, and its
31//! repositories are held to the cache's quota like everyone's.
32//!
33//! **Storage.** The git store does not report a repository's size, so the
34//! repos service counts the packs pushed through g1t's git endpoints (see
35//! `g1t_contracts::repos::StorageArgs`): a lower bound. Each day billing
36//! records what each workspace's private repositories hold and what is
37//! free that day (`FREE_PRIVATE_STORAGE_BYTES`, 1 GB, for everyone). Like
38//! Cloudflare's own storage billing, a month's GB-months are the days'
39//! amounts past the free one, added up and divided by the month's days
40//! (28 to 31), and only the
41//! plan is charged for them. A free workspace is never charged for
42//! storage: pushes to its private repositories stop once they hold its
43//! free amount. Public repositories are never charged.
44
45use g1t_contracts::billing::PlanKind;
46use g1t_contracts::new_id;
47use g1t_contracts::repos::{GitOperationsArgs, StorageArgs, WorkspaceGitOperations, WorkspaceStorage};
48use g1t_contracts::time::rfc3339;
49use g1t_kit::now_ms;
50use serde::Deserialize;
51use worker::Result;
52
53use crate::credits::{self, Drawn, Eligible};
54use crate::{Billing, optional};
55
56/// Sources billing charges itself when the month is over: those billing
57/// measures (storage, git operations, package storage) and those other
58/// services note through the month (`note_pending`: security scans, search
59/// embeddings, the actions cache, observability data).
60pub(crate) const CHARGED_HERE: [&str; 7] = ["security", "context", "storage", "git", "cache", "package_storage", "telemetry"];
61
62/// A `pending_usage` column less what the workspace had of it when its
63/// test-mode data was detached (`pending_baselines`, migration 0060),
64/// never below nothing. Reads `pending_usage` unaliased.
65macro_rules! net_of_baseline {
66 ($column:literal) => {
67 concat!(
68 "MAX(0, COALESCE(pending_usage.", $column, ", 0) - COALESCE((SELECT b.", $column,
69 " FROM pending_baselines b WHERE b.workspace = pending_usage.workspace AND b.source = pending_usage.source",
70 " AND b.month = pending_usage.month), 0))"
71 )
72 };
73}
74
75/// What a pending row cost g1t since the detach (all of it without one):
76/// what everything that values or charges pending usage reads.
77pub(crate) const NET_COST: &str = net_of_baseline!("cost_micros");
78/// What a pending row will be charged since the detach, before terms.
79pub(crate) const NET_CHARGE: &str = net_of_baseline!("charge_micros");
80
81/// A month's figure for a source, as `note_pending` writes it. Never into
82/// a month closed for the workspace, and the update leaves a row already
83/// charged as it was: a late report of a month that is over never
84/// rewrites what was charged.
85pub(crate) const SET_PENDING: &str = "INSERT INTO pending_usage (workspace, source, month, charge_micros, cost_micros, updated_at, detail)
86 SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7
87 WHERE NOT EXISTS (SELECT 1 FROM month_closes WHERE month_closes.workspace = ?1 AND month_closes.month = ?3)
88 ON CONFLICT (workspace, source, month) DO UPDATE SET
89 charge_micros = ?4, cost_micros = ?5, updated_at = ?6, detail = COALESCE(?7, detail)
90 WHERE pending_usage.charged_at IS NULL";
91
92/// Whether a workspace had the plan in a month by what it paid or drew:
93/// a plan payment in the month that a test-mode detach has not reversed
94/// (its `_test_detach` row nets it to nothing), or the plan's included
95/// usage drawn in it. `?1` workspace, `?2` the month's first day, `?3` the
96/// next month's, `?4` the month.
97pub(crate) const PLAN_EVIDENCE: &str = "SELECT (EXISTS (SELECT 1 FROM plan_payments p WHERE p.workspace = ?1 AND p.paid_at >= ?2 AND p.paid_at < ?3
98 AND p.amount_micros > 0 AND substr(p.invoice_id, -12) <> '_test_detach'
99 AND p.amount_micros + COALESCE((SELECT SUM(q.amount_micros) FROM plan_payments q
100 WHERE q.invoice_id = p.invoice_id || '_test_detach'), 0) > 0)
101 OR EXISTS (SELECT 1 FROM allowance_use WHERE kind = 'plan_credit' AND scope = ?1 AND month = ?4 AND used > 0)) AS had";
102
103/// Sources only the plan is charged for: a free workspace's are not kept.
104pub(crate) const PLAN_ONLY: [&str; 1] = ["cache"];
105
106/// Sources g1t pays for itself on a free workspace: security scans and
107/// (from before the context service was retired) search embeddings. No security feature is held back for the plan, and
108/// a free workspace never runs up a bill, so on a workspace without the
109/// plan they are recorded at cost, covered by g1t, and charged nothing
110/// (not even from the trial). On the plan they are charged like any usage.
111pub(crate) const COVERED_FOR_FREE: [&str; 2] = ["security", "context"];
112
113/// What pays for a month-end source's charge: the plan's included usage
114/// and the trial, and for a covered source on a free workspace, g1t.
115pub(crate) fn month_end_eligible(source: &str, plan: bool) -> Eligible {
116 let covered = !plan && COVERED_FOR_FREE.contains(&source);
117 Eligible { trial: !covered, repo: None, cover_rest: covered }
118}
119
120/// What Artifacts charges g1t, when the price book cannot be read: $0.50
121/// a GB-month of storage, and $0.15 per 1,000 git operations.
122pub(crate) const STORAGE_MICROS_PER_GB_MONTH: i64 = 500_000;
123pub(crate) const GIT_MICROS_PER_THOUSAND: i64 = 150_000;
124
125/// When Cloudflare starts charging for Artifacts operations: none before
126/// count.
127pub(crate) const GIT_BILLING_STARTS: &str = "2026-10-14T00";
128
129/// What git operations past the free amount cost g1t, at
130/// `micros_per_thousand`: nothing up to it.
131pub(crate) fn git_cost(operations: u64, included: u64, micros_per_thousand: f64) -> i64 {
132 let past = operations.saturating_sub(included);
133 (past as f64 * micros_per_thousand / 1000.0).ceil() as i64
134}
135
136/// The first hour of `month` to count git operations from.
137pub(crate) fn git_since(month: &str) -> String {
138 let start = format!("{month}-01T00");
139 if start.as_str() < GIT_BILLING_STARTS { GIT_BILLING_STARTS.to_owned() } else { start }
140}
141
142/// A gigabyte, as Cloudflare bills storage.
143pub(crate) const GB: f64 = 1_000_000_000.0;
144
145/// GB-months from a month's daily measures, each `(private, free)` bytes:
146/// what was past the free amount each day, over the month's `month_days`.
147pub(crate) fn storage_gb_months(days: &[(i64, i64)], month_days: u32) -> f64 {
148 days.iter().map(|(private, free)| (private - free).max(0) as f64).sum::<f64>() / GB / f64::from(month_days.max(1))
149}
150
151/// GB-months of package storage past the free amounts from a month's
152/// daily measures, each `(public, private, public_free, private_free)`:
153/// public and private past their own amounts, over the month's
154/// `month_days`.
155pub(crate) fn package_gb_months(days: &[(i64, i64, i64, i64)], month_days: u32) -> f64 {
156 days.iter()
157 .map(|(public, private, public_free, private_free)| ((public - public_free).max(0) + (private - private_free).max(0)) as f64)
158 .sum::<f64>()
159 / GB
160 / f64::from(month_days.max(1))
161}
162
163/// What R2 charges g1t a GB-month, when the price book cannot be read.
164pub(crate) const PACKAGE_MICROS_PER_GB_MONTH: i64 = 15_000;
165
166/// What `gb_months` cost g1t at `micros_per_gb_month`, rounded up.
167pub(crate) fn storage_cost(gb_months: f64, micros_per_gb_month: f64) -> i64 {
168 (gb_months * micros_per_gb_month).ceil() as i64
169}
170
171/// What a source is called on the statement.
172pub(crate) fn title(source: &str) -> &'static str {
173 match source {
174 "security" => "Security scans",
175 "context" => "Search embeddings",
176 "storage" => "Private repository storage past the free amount",
177 "package_storage" => "Package storage past the free amounts",
178 "git" => "Git operations past the free amount",
179 "cache" => "Actions cache storage",
180 "domains" => "Custom domains",
181 "telemetry" => "Observability data past the free amount",
182 _ => "Metered usage",
183 }
184}
185
186/// Whether a batch's first statement, an insert `RETURNING id`, wrote its
187/// row: none comes back when `ON CONFLICT … DO NOTHING` skipped it.
188pub(crate) fn posted(results: &[worker::D1Result]) -> Result<bool> {
189 #[derive(Deserialize)]
190 struct Row {
191 #[allow(dead_code)]
192 id: String,
193 }
194 match results.first() {
195 Some(result) => Ok(!result.results::<Row>()?.is_empty()),
196 None => Ok(false),
197 }
198}
199
200/// A usage entry to put on the ledger.
201pub(crate) struct UsageLine<'a> {
202 pub workspace: &'a str,
203 /// What the workspace is charged, after terms and what paid for it.
204 pub charged: i64,
205 pub description: &'a str,
206 pub repo: Option<&'a str>,
207 pub task: &'a str,
208 pub cost: i64,
209 pub reference: &'a str,
210 pub created_at: &'a str,
211 pub drawn: Drawn,
212}
213
214impl Billing {
215 /// Puts a usage entry on the ledger and takes it off the balance, as
216 /// one write. False when an entry with its reference is already there
217 /// (another delivery of the same usage wrote it first): nothing is
218 /// written, and the caller gives back what it drew (`undraw`).
219 pub(crate) async fn post_usage(&self, line: UsageLine<'_>) -> Result<bool> {
220 self.post_usage_with(line, vec![]).await
221 }
222
223 /// `post_usage`, with `also` written in the same batch, whether or not
224 /// the entry was new.
225 pub(crate) async fn post_usage_with(&self, line: UsageLine<'_>, also: Vec<worker::D1PreparedStatement>) -> Result<bool> {
226 // The price versions it was charged at (pricing.rs).
227 let mut versions = Vec::new();
228 for meter in crate::pricing::meters_of(line.task) {
229 versions.extend(self.version_now(meter).await?);
230 }
231 let price_version = versions.join(",");
232 let id = new_id("led", now_ms());
233 // The ledger's unique reference is the guard: the entry goes in
234 // only if no other has it, and the balance moves only if it did.
235 let mut statements = vec![
236 self.db
237 .prepare(
238 "INSERT INTO ledger
239 (id, workspace, kind, amount_micros, description, repo, task, cost_micros, reference,
240 created_at, billed_to, credit_micros, trial_micros, oss_micros, given_micros, price_version)
241 VALUES (?, ?, 'usage', ?, ?, ?, ?, ?, ?, ?, 'g1t', ?, ?, ?, ?, ?)
242 ON CONFLICT (reference) DO NOTHING RETURNING id",
243 )
244 .bind(&[
245 id.as_str().into(),
246 line.workspace.into(),
247 (-(line.charged as f64)).into(),
248 line.description.into(),
249 optional(line.repo),
250 line.task.into(),
251 (line.cost as f64).into(),
252 line.reference.into(),
253 line.created_at.into(),
254 (line.drawn.credit as f64).into(),
255 (line.drawn.trial as f64).into(),
256 (line.drawn.oss as f64).into(),
257 // What g1t covered itself (a free workspace's scans and
258 // embeddings): given, never margin lost.
259 (line.drawn.given as f64).into(),
260 optional(Some(price_version.as_str()).filter(|v| !v.is_empty())),
261 ])?,
262 self.balance_if_posted(line.workspace, line.charged, &id)?,
263 ];
264 statements.extend(also);
265 let results = self.db.batch(statements).await?;
266 posted(&results)
267 }
268
269 /// Takes `charged` off the workspace's balance, only if the ledger entry
270 /// `id` was written in the same batch.
271 pub(crate) fn balance_if_posted(&self, workspace: &str, charged: i64, id: &str) -> Result<worker::D1PreparedStatement> {
272 self.db
273 .prepare(
274 "INSERT INTO accounts (workspace, balance_micros, created_at)
275 SELECT ?1, ?2, ?3 WHERE EXISTS (SELECT 1 FROM ledger WHERE id = ?4)
276 ON CONFLICT (workspace) DO UPDATE SET balance_micros = balance_micros + ?2",
277 )
278 .bind(&[workspace.into(), (-(charged as f64)).into(), rfc3339(now_ms()).into(), id.into()])
279 }
280
281 /// Writes down what a source cost g1t so far in `month`, what it will
282 /// be charged, and how much of it there was (`detail`, for the Billing
283 /// page), replacing the last figure.
284 pub(crate) async fn set_pending(
285 &self,
286 workspace: &str,
287 source: &str,
288 month: &str,
289 cost_micros: i64,
290 detail: Option<&str>,
291 ) -> Result<()> {
292 // A covered source on a free workspace is never charged, so it does
293 // not count toward the workspace's limit either.
294 let covered = COVERED_FOR_FREE.contains(&source) && !self.has_plan(&workspace.to_lowercase()).await?;
295 let charge = if covered { 0 } else { credits::with_margin(cost_micros, self.margin_percent) };
296 self.db
297 .prepare(SET_PENDING)
298 .bind(&[
299 workspace.to_lowercase().into(),
300 source.into(),
301 month.into(),
302 (charge as f64).into(),
303 (cost_micros.max(0) as f64).into(),
304 rfc3339(now_ms()).into(),
305 optional(detail),
306 ])?
307 .run()
308 .await?;
309 Ok(())
310 }
311
312 /// Charges every month that is over for the usage billing charges
313 /// itself, once each.
314 pub(crate) async fn charge_pending(&self) -> Result<()> {
315 if !self.charging {
316 return Ok(());
317 }
318 let now = rfc3339(now_ms());
319 let current = credits::month_of(&now);
320 #[derive(Deserialize)]
321 struct Row {
322 workspace: String,
323 source: String,
324 month: String,
325 cost_micros: Option<i64>,
326 }
327 let marks = CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
328 let due = self
329 .db
330 // Less what was metered before a test-mode detach: not charged.
331 .prepare(format!(
332 "SELECT workspace, source, month, {NET_COST} AS cost_micros FROM pending_usage
333 WHERE month < ? AND charged_at IS NULL AND source IN ({marks}) ORDER BY month LIMIT 50"
334 ))
335 .bind(&[current.as_str().into()])?
336 .all()
337 .await?
338 .results::<Row>()?;
339 for row in due {
340 // Marked charged in the same batch as its ledger entry, never
341 // before: a failure in between leaves it to the next tick, and
342 // the ledger's unique reference keeps two crons from charging
343 // it twice.
344 let mark = || {
345 self.db
346 .prepare(
347 "UPDATE pending_usage SET charged_at = ?1
348 WHERE workspace = ?2 AND source = ?3 AND month = ?4 AND charged_at IS NULL",
349 )
350 .bind(&[now.as_str().into(), row.workspace.as_str().into(), row.source.as_str().into(), row.month.as_str().into()])
351 };
352 let cost = row.cost_micros.unwrap_or(0);
353 if cost <= 0 {
354 mark()?.run().await?;
355 continue;
356 }
357 let base = credits::with_margin(cost, self.margin_percent);
358 let (charge, terms_note, discount) = self.charged(&row.workspace, base).await?;
359 // The plan as it was in that month, not as it is now: a plan
360 // started since does not pay for it, and one ended since still
361 // does.
362 let plan = self.had_plan_in(&row.workspace, &row.month).await?;
363 let eligible = month_end_eligible(&row.source, plan);
364 let drawn = self.draw_as(&row.workspace, charge, &row.month, &eligible, plan).await?;
365 let detail = if row.source == "storage" {
366 let gb_months = self.gb_months(&row.workspace, &row.month).await?;
367 format!(": {gb_months:.2} GB-months")
368 } else {
369 String::new()
370 };
371 let description = format!("{} in {}{detail}{terms_note}{}", title(&row.source), row.month, drawn.note());
372 let reference = format!("{}/{}/{}", row.source, row.workspace, row.month);
373 let created_at = credits::month_end(&row.month);
374 let line = UsageLine {
375 workspace: &row.workspace,
376 charged: charge - drawn.total(),
377 description: &description,
378 repo: None,
379 task: &row.source,
380 cost,
381 reference: &reference,
382 created_at: &created_at,
383 drawn,
384 };
385 let posted = match self.post_usage_with(line, vec![mark()?]).await {
386 Ok(posted) => posted,
387 Err(error) => {
388 // Nothing was written: what was drawn goes back.
389 self.undraw(&row.workspace, &row.month, &drawn, None).await?;
390 return Err(error);
391 }
392 };
393 if !posted {
394 // Charged already (another tick won): give back the draw.
395 self.undraw(&row.workspace, &row.month, &drawn, None).await?;
396 continue;
397 }
398 self.record_discount(&reference, discount).await?;
399 }
400 Ok(())
401 }
402
403 /// Whether the workspace had the plan in `month`: g1t's own,
404 /// enterprises and a plan staff gave by their terms; otherwise a plan
405 /// invoice paid in the month and not reversed by a test-mode detach,
406 /// the plan's included usage drawn in it, or a plan on now that
407 /// started before the month ended (`PLAN_EVIDENCE`).
408 pub(crate) async fn had_plan_in(&self, workspace: &str, month: &str) -> Result<bool> {
409 let account = self.account_of(workspace).await?;
410 let now = self.plan_kind_for(workspace, &account).await?;
411 if matches!(now, PlanKind::Internal | PlanKind::Enterprise) || account.allowances.plan || !self.charging {
412 return Ok(now != PlanKind::Free);
413 }
414 let next = credits::next_month_start(month);
415 if now == PlanKind::Paid
416 && let Some((started_at, _)) = self.plan_cycle(workspace).await?
417 && started_at.as_str() < next.as_str()
418 {
419 return Ok(true);
420 }
421 #[derive(Deserialize)]
422 struct Evidence {
423 had: Option<i64>,
424 }
425 Ok(self
426 .db
427 .prepare(PLAN_EVIDENCE)
428 .bind(&[workspace.into(), format!("{month}-01").into(), next.as_str().into(), month.into()])?
429 .first::<Evidence>(None)
430 .await?
431 .and_then(|e| e.had)
432 .unwrap_or(0)
433 == 1)
434 }
435
436 /// A workspace's private storage past the free amount in `month`.
437 async fn gb_months(&self, workspace: &str, month: &str) -> Result<f64> {
438 #[derive(Deserialize)]
439 struct Day {
440 private_bytes: i64,
441 free_bytes: i64,
442 }
443 let days = self
444 .db
445 .prepare("SELECT private_bytes, free_bytes FROM storage_days WHERE workspace = ? AND substr(day, 1, 7) = ?")
446 .bind(&[workspace.into(), month.into()])?
447 .all()
448 .await?
449 .results::<Day>()?;
450 Ok(storage_gb_months(&days.iter().map(|d| (d.private_bytes, d.free_bytes)).collect::<Vec<_>>(), credits::days_in(month)))
451 }
452
453 /// Once a day: what each workspace's private repositories hold, and
454 /// what this month's storage past the free amount comes to so far.
455 pub(crate) async fn measure_storage(&self) -> Result<()> {
456 let Some(repos) = &self.repos else { return Ok(()) };
457 let list: Vec<WorkspaceStorage> = g1t_kit::call(repos, "storage", &StorageArgs {}).await?;
458 let now = rfc3339(now_ms());
459 let (day, month) = (&now[..10], credits::month_of(&now));
460 let price = self.price("private_storage").await?.map_or(STORAGE_MICROS_PER_GB_MONTH as f64, |(cost, _)| cost);
461 for workspace in list {
462 let slug = workspace.namespace.to_lowercase();
463 let plan = self.has_plan(&slug).await?;
464 // The same free amount for everyone: the plan pays past it.
465 let free = self.plans.free_storage_bytes;
466 self.db
467 .prepare(
468 "INSERT INTO storage_days (workspace, day, private_bytes, free_bytes) VALUES (?1, ?2, ?3, ?4)
469 ON CONFLICT (workspace, day) DO UPDATE SET private_bytes = ?3, free_bytes = ?4",
470 )
471 .bind(&[slug.as_str().into(), day.into(), (workspace.private_bytes as f64).into(), (free as f64).into()])?
472 .run()
473 .await?;
474 // Only the plan pays for storage past its amount. A free
475 // workspace is never charged: the repos service stops its pushes
476 // to private repositories once it is full (see git_ops.rs there).
477 let gb_months = if plan { self.gb_months(&slug, &month).await? } else { 0.0 };
478 if gb_months > 0.0 {
479 let detail = format!("{gb_months:.2} GB-months past the free {}", crate::features::bytes(free));
480 self.set_pending(&slug, "storage", &month, storage_cost(gb_months, price), Some(&detail)).await?;
481 }
482 }
483 Ok(())
484 }
485
486 /// Once a day: what each workspace's packages hold, public and private,
487 /// from the packages service, and for the plan what this month's
488 /// storage past the free amounts comes to so far. A free workspace is
489 /// never charged: the packages service refuses its pushes past them.
490 pub(crate) async fn measure_packages(&self) -> Result<()> {
491 #[derive(Deserialize)]
492 struct Held {
493 workspace: String,
494 public_bytes: i64,
495 private_bytes: i64,
496 }
497 #[derive(Deserialize)]
498 struct Day {
499 public_bytes: i64,
500 private_bytes: i64,
501 public_free_bytes: i64,
502 private_free_bytes: i64,
503 }
504 let Some(packages) = &self.packages else { return Ok(()) };
505 let list: Vec<Held> = g1t_kit::call(packages, "storage_all", &serde_json::json!({})).await?;
506 let now = rfc3339(now_ms());
507 let (day, month) = (&now[..10], credits::month_of(&now));
508 let price = self.price("package_storage").await?.map_or(PACKAGE_MICROS_PER_GB_MONTH as f64, |(cost, _)| cost);
509 let (public_free, private_free) = (self.plans.public_package_free_bytes, self.plans.private_package_free_bytes);
510 for held in list {
511 let slug = held.workspace.to_lowercase();
512 self.db
513 .prepare(
514 "INSERT INTO package_storage_days (workspace, day, public_bytes, private_bytes, public_free_bytes, private_free_bytes)
515 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
516 ON CONFLICT (workspace, day) DO UPDATE SET public_bytes = ?3, private_bytes = ?4, public_free_bytes = ?5, private_free_bytes = ?6",
517 )
518 .bind(&[
519 slug.as_str().into(),
520 day.into(),
521 (held.public_bytes as f64).into(),
522 (held.private_bytes as f64).into(),
523 (public_free as f64).into(),
524 (private_free as f64).into(),
525 ])?
526 .run()
527 .await?;
528 if !self.has_plan(&slug).await? {
529 continue;
530 }
531 let days = self
532 .db
533 .prepare(
534 "SELECT public_bytes, private_bytes, public_free_bytes, private_free_bytes FROM package_storage_days
535 WHERE workspace = ? AND substr(day, 1, 7) = ?",
536 )
537 .bind(&[slug.as_str().into(), month.as_str().into()])?
538 .all()
539 .await?
540 .results::<Day>()?;
541 let gb_months = package_gb_months(
542 &days.iter().map(|d| (d.public_bytes, d.private_bytes, d.public_free_bytes, d.private_free_bytes)).collect::<Vec<_>>(),
543 credits::days_in(&month),
544 );
545 if gb_months > 0.0 {
546 let detail = format!(
547 "{gb_months:.2} GB-months past the free {} public and {} private",
548 crate::features::bytes(public_free),
549 crate::features::bytes(private_free)
550 );
551 self.set_pending(&slug, "package_storage", &month, storage_cost(gb_months, price), Some(&detail)).await?;
552 }
553 }
554 Ok(())
555 }
556
557 /// Git operations this month for each workspace, from the repos
558 /// service: what is past the free amount goes to the month's pending
559 /// usage for workspaces on the plan, which are never slowed or refused
560 /// for them. Free workspaces are never charged for them.
561 pub(crate) async fn measure_git(&self) -> Result<()> {
562 let Some(repos) = &self.repos else { return Ok(()) };
563 let month = credits::month_of(&rfc3339(now_ms()));
564 let list: Vec<WorkspaceGitOperations> =
565 g1t_kit::call(repos, "git_operations", &GitOperationsArgs { since: Some(git_since(&month)), month: month.clone(), namespace: None }).await?;
566 let price = self.price("git_operations").await?.map_or(GIT_MICROS_PER_THOUSAND as f64, |(cost, _)| cost);
567 for workspace in list {
568 let slug = workspace.namespace.to_lowercase();
569 if self.plan_kind(&slug).await? == PlanKind::Free {
570 continue;
571 }
572 let cost = git_cost(workspace.operations, self.plans.git_included, price);
573 if cost > 0 {
574 let detail = format!(
575 "{} operations, {} of them free",
576 crate::features::thousands(workspace.operations),
577 crate::features::thousands(self.plans.git_included)
578 );
579 self.set_pending(&slug, "git", &month, cost, Some(&detail)).await?;
580 }
581 }
582 Ok(())
583 }
584
585 /// The workspace's git operations this month, as last measured.
586 pub(crate) async fn git_operations_this_month(&self, workspace: &str) -> Result<u64> {
587 let Some(repos) = &self.repos else { return Ok(0) };
588 let month = credits::month_of(&rfc3339(now_ms()));
589 let list: Result<Vec<WorkspaceGitOperations>> =
590 g1t_kit::call(repos, "git_operations", &GitOperationsArgs { since: Some(format!("{month}-01T00")), month, namespace: Some(workspace.to_lowercase()) }).await;
591 Ok(list
592 .ok()
593 .and_then(|list| list.into_iter().find(|w| w.namespace.eq_ignore_ascii_case(workspace)))
594 .map_or(0, |w| w.operations))
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 #[test]
603 fn scans_and_embeddings_on_a_free_workspace_are_covered_by_g1t() {
604 // No plan: g1t pays, and the trial is left alone.
605 let free = month_end_eligible("security", false);
606 assert!(free.cover_rest && !free.trial);
607 assert!(month_end_eligible("context", false).cover_rest);
608 // On the plan: charged like any usage, from its included usage first.
609 let plan = month_end_eligible("security", true);
610 assert!(!plan.cover_rest && plan.trial);
611 // Storage, git and the cache are never kept for a free workspace;
612 // were one at the close, it would not be covered either.
613 assert!(!month_end_eligible("storage", false).cover_rest);
614 assert!(!month_end_eligible("git", false).cover_rest);
615 // A covered source is still a source billing charges at the close.
616 assert!(COVERED_FOR_FREE.iter().all(|s| CHARGED_HERE.contains(s)));
617 }
618
619 #[test]
620 fn storage_past_the_free_amount_is_counted_by_the_day() {
621 // 3 GB private with 1 GB free, every day of a 30-day month: 2 GB-months.
622 let month = vec![(3_000_000_000, 1_000_000_000); 30];
623 assert!((storage_gb_months(&month, 30) - 2.0).abs() < 1e-9);
624 // Under the free amount: nothing.
625 assert_eq!(storage_gb_months(&[(500_000_000, 1_000_000_000); 30], 30), 0.0);
626 // The plan has the same 1 GB free: 11 GB on it all month is 10
627 // GB-months, $5.00 to g1t, $6.00 charged, from its included usage.
628 let days = vec![(11_000_000_000, 1_000_000_000); 30];
629 assert!((storage_gb_months(&days, 30) - 10.0).abs() < 1e-9);
630 assert_eq!(credits::with_margin(storage_cost(storage_gb_months(&days, 30), STORAGE_MICROS_PER_GB_MONTH as f64), 20), 6_000_000);
631 // Ten days of 4 GB past it: a third of 4 GB-months.
632 let days = vec![(5_000_000_000, 1_000_000_000); 10];
633 assert!((storage_gb_months(&days, 30) - 4.0 / 3.0).abs() < 1e-9);
634 }
635
636 #[test]
637 fn a_month_of_storage_is_its_own_days_not_thirty() {
638 // 2 GB past the free amount every day of a 31-day month is 2
639 // GB-months, not 2.07; every day of February, 2 too, not 1.87.
640 let october = vec![(3_000_000_000, 1_000_000_000); 31];
641 assert!((storage_gb_months(&october, credits::days_in("2026-10")) - 2.0).abs() < 1e-9);
642 let february = vec![(3_000_000_000, 1_000_000_000); 28];
643 assert!((storage_gb_months(&february, credits::days_in("2026-02")) - 2.0).abs() < 1e-9);
644 assert_eq!(credits::days_in("2028-02"), 29);
645 let gb = GB as i64;
646 let packages = vec![(11 * gb, 0, 10 * gb, 0); 31];
647 assert!((package_gb_months(&packages, 31) - 1.0).abs() < 1e-9);
648 }
649
650 #[test]
651 fn package_storage_counts_public_and_private_past_their_own_free_amounts() {
652 let gb = GB as i64;
653 // 12 GB public with 10 free, 0.5 GB private with 0.5 free: 2 GB past, for 15 days.
654 let days = vec![(12 * gb, gb / 2, 10 * gb, gb / 2); 15];
655 assert!((package_gb_months(&days, 30) - 1.0).abs() < 1e-9);
656 // Under both amounts: nothing.
657 assert_eq!(package_gb_months(&[(gb, gb / 4, 10 * gb, gb / 2)], 30), 0.0);
658 // At R2's rate, a GB-month is $0.015.
659 assert_eq!(storage_cost(1.0, PACKAGE_MICROS_PER_GB_MONTH as f64), 15_000);
660 }
661
662 #[test]
663 fn storage_is_priced_at_cloudflares_rate_plus_the_margin() {
664 // $0.50 a GB-month to g1t: 2 GB-months cost $1.00, charged $1.20.
665 let cost = storage_cost(2.0, 500_000.0);
666 assert_eq!(cost, 1_000_000);
667 assert_eq!(credits::with_margin(cost, 20), 1_200_000);
668 // A fraction of a millionth rounds up.
669 assert_eq!(storage_cost(0.000_000_001, 500_000.0), 1);
670 }
671
672 #[test]
673 fn embeddings_and_scans_are_charged_at_cost_plus_the_margin() {
674 // 10 million tokens at $0.067 a million: $0.67, charged $0.804.
675 assert_eq!(credits::with_margin(670_000, 20), 804_000);
676 assert_eq!(title("context"), "Search embeddings");
677 assert_eq!(title("security"), "Security scans");
678 assert!(CHARGED_HERE.contains(&"storage") && !CHARGED_HERE.contains(&"deployments"));
679 assert!(CHARGED_HERE.contains(&"git"));
680 // Package storage and observability data are charged when the month
681 // closes, like storage, and each is named on the statement.
682 for source in ["package_storage", "telemetry"] {
683 assert!(CHARGED_HERE.contains(&source), "{source}");
684 assert_ne!(title(source), "Metered usage", "{source}");
685 assert!(!crate::pricing::meters_of(source).is_empty(), "{source}");
686 assert!(crate::pricing::meters_of(source).iter().all(|m| crate::pricing::MONTHLY.contains(m)), "{source}");
687 assert!(!COVERED_FOR_FREE.contains(&source) && !PLAN_ONLY.contains(&source), "{source}");
688 }
689 assert_eq!(title("telemetry"), "Observability data past the free amount");
690 }
691
692 #[test]
693 fn git_operations_are_charged_past_the_free_amount_at_cloudflares_price() {
694 // $0.15 per 1,000 to g1t; 50,000 a month free for everyone.
695 let per_thousand = GIT_MICROS_PER_THOUSAND as f64;
696 let free = crate::credits::Config::default().git_included;
697 assert_eq!(free, 50_000);
698 assert_eq!(git_cost(49_000, free, per_thousand), 0);
699 assert_eq!(git_cost(50_000, free, per_thousand), 0);
700 // A workspace on the plan pushes on past it, and pays: 70,000
701 // operations are 20,000 past it, $3.00 to g1t, $3.60 charged.
702 assert_eq!(git_cost(70_000, free, per_thousand), 3_000_000);
703 assert_eq!(credits::with_margin(git_cost(70_000, free, per_thousand), 20), 3_600_000);
704 // A million in a month: no cap, $142.50 to g1t.
705 assert_eq!(git_cost(1_000_000, free, per_thousand), 142_500_000);
706 // One past it: a fraction of a cent, rounded up to a millionth.
707 assert_eq!(git_cost(50_001, free, per_thousand), 150);
708 assert_eq!(title("git"), "Git operations past the free amount");
709 }
710
711 #[test]
712 fn git_operations_count_from_when_cloudflare_starts_charging() {
713 assert_eq!(git_since("2026-10"), "2026-10-14T00");
714 assert_eq!(git_since("2026-11"), "2026-11-01T00");
715 assert_eq!(git_since("2026-09"), "2026-10-14T00");
716 }
717}