g1t/services/billing/src/sales.rs

766 lines33,764 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.

Two limits, real invoices, trust that grows by itself, sales signals1//! What g1t's team needs to sell and support: month-by-month figures, the
2//! signals that say a workspace is worth a call, and what was done about
3//! it. Staff only, through sudo.g1t.sh.
4
5use g1t_contracts::billing::{
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look6 AdminAddNoteArgs, AdminOverviewArgs, AdminOveragesArgs, AdminSalesArgs, AdminSetSalesArgs, AdminSignalsArgs, GivenFigures,
7 InternalUse, KindFigures, LimitState, MonthFigures, Overview, SalesNote, SalesRecord, Signal, SignalKind, TermsKind, Trust,
Two limits, real invoices, trust that grows by itself, sales signals8};
9use g1t_contracts::time::rfc3339;
10use g1t_contracts::{FailureCode, Outcome, new_id};
11use g1t_kit::now_ms;
12use serde::Deserialize;
13use worker::Result;
14use worker::wasm_bindgen::JsValue;
15
16use crate::Billing;
17use crate::features::dollars;
18use crate::limits::previous_month;
19
20pub(crate) const STAGES: &[&str] = &["none", "lead", "contacted", "negotiating", "won", "lost", "churn_risk"];
21
22/// A workspace this much ahead of last month's pace is growing.
23const GROWING_FACTOR: f64 = 1.5;
24/// Below this last month, growth is noise.
25const GROWING_FROM_MICROS: i64 = 10_000_000;
26/// Spending this much a month may suit custom terms or an enterprise.
27const HIGH_SPEND_MICROS: i64 = 500_000_000;
28
29/// The six months ending with `month`, oldest first.
30pub(crate) fn last_months(month: &str, count: usize) -> Vec<String> {
31 let mut months = vec![month.to_owned()];
32 while months.len() < count {
33 let earlier = previous_month(months.last().unwrap());
34 months.push(earlier);
35 }
36 months.reverse();
37 months
38}
39
40/// How urgent a kind of signal is: lower first.
41fn urgency(kind: SignalKind) -> u8 {
42 match kind {
43 SignalKind::AtLimit => 0,
44 SignalKind::Declined => 1,
45 SignalKind::NearCeiling => 2,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily46 SignalKind::CostOverRevenue => 2,
Two limits, real invoices, trust that grows by itself, sales signals47 SignalKind::HighSpend => 3,
48 SignalKind::Growing => 4,
49 SignalKind::Established => 5,
50 SignalKind::FirstPayment => 6,
51 }
52}
53
54/// Whether this month, at its pace so far, is well ahead of last month.
55pub(crate) fn growing(this_month: i64, last_month: i64, day: u32, days_in_month: u32) -> bool {
56 if last_month < GROWING_FROM_MICROS || day == 0 {
57 return false;
58 }
59 let pace = this_month as f64 * f64::from(days_in_month) / f64::from(day);
60 pace >= last_month as f64 * GROWING_FACTOR
61}
62
63fn days_in(month: &str) -> u32 {
64 let year: i32 = month[..4].parse().unwrap_or(1970);
65 match month[5..7].parse::<u32>().unwrap_or(1) {
66 2 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 => 29,
67 2 => 28,
68 4 | 6 | 9 | 11 => 30,
69 _ => 31,
70 }
71}
72
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73/// The workspaces g1t does not charge (comped terms): its own and
74/// Flagon's, and any enterprise on comped terms. Their usage is recorded at
75/// what it cost and shown as given, apart from margin.
76pub(crate) const INTERNAL_SQL: &str = "SELECT substr(id, 4) FROM billing_accounts WHERE kind = 'workspace' AND terms_kind = 'comped'
77 UNION SELECT m.workspace FROM account_members m JOIN billing_accounts b ON b.id = m.account_id WHERE b.terms_kind = 'comped'";
78
79/// One group of ledger rows in a month, as sudo's figures add them up.
80#[derive(Clone, Debug, Default, Deserialize)]
81pub(crate) struct LedgerGroup {
82 pub month: String,
83 /// `usage` or `top_up`.
84 pub kind: String,
85 /// `g1t`, or `workspace` for a run on the workspace's own provider.
86 pub billed_to: Option<String>,
87 /// A credit from g1t rather than a payment (`crd…`), and of those, a
88 /// goodwill credit.
89 pub credit: i64,
90 pub goodwill: i64,
91 /// One of g1t's own workspaces (comped).
92 pub internal: i64,
93 pub amount: Option<i64>,
94 pub cost: Option<i64>,
95 /// What the trial, the open-source pool and g1t itself paid.
96 pub pools: Option<i64>,
Two limits, real invoices, trust that grows by itself, sales signals97}
98
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look99/// A month's figures from its ledger groups and the plan's price paid.
100///
101/// - **Cost** is only what g1t paid: a run on the workspace's own model
102/// provider was paid for there, so its cost is never g1t's.
103/// - **Given** is what g1t gave at price, apart from its margin: internal
104/// use (its cost plus the margin, since nothing was charged), what the
105/// trial, the open-source pool and g1t itself paid, and goodwill credits.
106/// - **Paid** is money in: payments, never credits from g1t.
107pub(crate) fn fold_month(month: &str, groups: &[LedgerGroup], plans: i64, margin_percent: u32) -> MonthFigures {
108 let mut figures = MonthFigures { month: month.to_owned(), plans_micros: plans, ..MonthFigures::default() };
109 for g in groups.iter().filter(|g| g.month == month) {
110 let amount = g.amount.unwrap_or(0);
111 let own_provider = g.billed_to.as_deref() == Some("workspace");
112 if g.kind == "usage" {
113 figures.charged_micros += -amount;
114 if !own_provider {
115 let cost = g.cost.unwrap_or(0);
116 figures.cost_micros += cost;
117 if g.internal == 1 {
118 figures.given_micros += crate::credits::with_margin(cost, margin_percent);
119 }
120 }
121 figures.given_micros += g.pools.unwrap_or(0);
122 } else if g.credit == 0 {
123 figures.paid_micros += amount;
124 } else if g.goodwill == 1 {
125 figures.given_micros += amount;
126 }
127 }
128 figures
129}
130
Two limits, real invoices, trust that grows by itself, sales signals131#[derive(Deserialize)]
132struct RecordRow {
133 stage: String,
134 owner: Option<String>,
135 next_step: Option<String>,
136 next_at: Option<String>,
137 updated_at: String,
138}
139
140#[derive(Deserialize)]
141struct NoteRow {
142 id: String,
143 text: String,
144 by: String,
145 created_at: String,
146}
147
148impl Billing {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look149 /// Month-by-month figures for some workspaces (all, when empty): usage
150 /// charged, what it cost g1t (never a workspace's own provider), money
151 /// paid, the plan's price paid, and what g1t gave.
Two limits, real invoices, trust that grows by itself, sales signals152 pub(crate) async fn months_for(&self, workspaces: &[String], count: usize) -> Result<Vec<MonthFigures>> {
153 let months = last_months(&rfc3339(now_ms())[..7], count);
154 let since = format!("{}-01", months[0]);
155 let (filter, values): (String, Vec<JsValue>) = if workspaces.is_empty() {
156 (String::new(), vec![])
157 } else {
158 let marks = vec!["?"; workspaces.len()].join(", ");
159 (format!("AND workspace IN ({marks})"), workspaces.iter().map(|w| JsValue::from(w.as_str())).collect())
160 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look161 let groups = self
Two limits, real invoices, trust that grows by itself, sales signals162 .db
163 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look164 "SELECT substr(created_at, 1, 7) AS month, kind, COALESCE(billed_to, 'g1t') AS billed_to,
165 CASE WHEN reference LIKE 'crd%' THEN 1 ELSE 0 END AS credit,
166 CASE WHEN reference LIKE '{goodwill}%' THEN 1 ELSE 0 END AS goodwill,
167 CASE WHEN workspace IN ({INTERNAL_SQL}) THEN 1 ELSE 0 END AS internal,
168 SUM(amount_micros) AS amount, SUM(cost_micros) AS cost,
169 SUM(trial_micros + oss_micros + given_micros) AS pools
170 FROM ledger WHERE created_at >= '{since}' {filter} GROUP BY 1, 2, 3, 4, 5, 6",
171 goodwill = crate::overages::GOODWILL_PREFIX
Two limits, real invoices, trust that grows by itself, sales signals172 ))
173 .bind(&values)?
174 .all()
175 .await?
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look176 .results::<LedgerGroup>()?;
177 #[derive(Deserialize)]
178 struct Plans {
179 month: String,
180 micros: Option<i64>,
181 }
182 let plans = self
183 .db
184 .prepare(format!(
185 "SELECT substr(paid_at, 1, 7) AS month, SUM(amount_micros) AS micros FROM plan_payments
186 WHERE paid_at >= '{since}' {filter} GROUP BY 1"
187 ))
188 .bind(&values)?
189 .all()
190 .await?
191 .results::<Plans>()?;
Two limits, real invoices, trust that grows by itself, sales signals192 Ok(months
193 .into_iter()
194 .map(|month| {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look195 let paid_plans = plans.iter().find(|p| p.month == month).and_then(|p| p.micros).unwrap_or(0);
196 fold_month(&month, &groups, paid_plans, self.margin_percent)
Two limits, real invoices, trust that grows by itself, sales signals197 })
198 .collect())
199 }
200
201 async fn record_row(&self, workspace: &str) -> Result<Option<RecordRow>> {
202 self.db
203 .prepare("SELECT * FROM sales_records WHERE workspace = ?")
204 .bind(&[workspace.into()])?
205 .first::<RecordRow>(None)
206 .await
207 }
208
209 pub(crate) async fn admin_sales(&self, a: AdminSalesArgs) -> Result<SalesRecord> {
210 let workspace = a.workspace.trim().to_lowercase();
211 let row = self.record_row(&workspace).await?;
212 let notes = self
213 .db
214 .prepare("SELECT id, text, by, created_at FROM sales_notes WHERE workspace = ? ORDER BY created_at DESC LIMIT 100")
215 .bind(&[workspace.as_str().into()])?
216 .all()
217 .await?
218 .results::<NoteRow>()?
219 .into_iter()
220 .map(|n| SalesNote { id: n.id, text: n.text, by: n.by, created_at: n.created_at })
221 .collect();
222 Ok(match row {
223 Some(row) => SalesRecord {
224 workspace,
225 stage: row.stage,
226 owner: row.owner,
227 next_step: row.next_step,
228 next_at: row.next_at,
229 notes,
230 updated_at: Some(row.updated_at),
231 },
232 None => SalesRecord {
233 workspace,
234 stage: "none".to_owned(),
235 owner: None,
236 next_step: None,
237 next_at: None,
238 notes,
239 updated_at: None,
240 },
241 })
242 }
243
244 pub(crate) async fn admin_set_sales(&self, a: AdminSetSalesArgs) -> Result<Outcome<SalesRecord>> {
245 let workspace = a.workspace.trim().to_lowercase();
246 if workspace.is_empty() || a.by.trim().is_empty() {
247 return Ok(Outcome::fail(FailureCode::Invalid, "Name the workspace, and who is making the change."));
248 }
249 if !STAGES.contains(&a.stage.as_str()) {
250 return Ok(Outcome::fail(FailureCode::Invalid, format!("A stage is one of: {}.", STAGES.join(", "))));
251 }
252 let clean = |value: &Option<String>| value.as_deref().map(str::trim).filter(|v| !v.is_empty()).map(str::to_owned);
253 let (owner, next_step, next_at) = (clean(&a.owner), clean(&a.next_step), clean(&a.next_at));
254 let before = self.record_row(&workspace).await?;
255 self.db
256 .prepare(
257 "INSERT INTO sales_records (workspace, stage, owner, next_step, next_at, updated_by, updated_at)
258 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
259 ON CONFLICT (workspace) DO UPDATE SET stage = ?2, owner = ?3, next_step = ?4, next_at = ?5,
260 updated_by = ?6, updated_at = ?7",
261 )
262 .bind(&[
263 workspace.as_str().into(),
264 a.stage.as_str().into(),
265 crate::optional(owner.as_deref()),
266 crate::optional(next_step.as_deref()),
267 crate::optional(next_at.as_deref()),
268 a.by.as_str().into(),
269 rfc3339(now_ms()).into(),
270 ])?
271 .run()
272 .await?;
273 let account = self.account_of(&workspace).await?;
274 let was = before.map_or_else(|| "none".to_owned(), |b| b.stage);
275 let detail = format!(
276 "{workspace}: stage {was} → {}{}{}",
277 a.stage,
278 owner.as_deref().map(|o| format!(", owner {o}")).unwrap_or_default(),
279 next_step.as_deref().map(|s| format!(", next: {s}")).unwrap_or_default(),
280 );
281 self.audit(&account.id, "sales", &detail, &a.by).await?;
282 Ok(Outcome::Ok(self.admin_sales(AdminSalesArgs { workspace }).await?))
283 }
284
285 pub(crate) async fn admin_add_note(&self, a: AdminAddNoteArgs) -> Result<Outcome<SalesRecord>> {
286 let workspace = a.workspace.trim().to_lowercase();
287 let text = a.text.trim();
288 if workspace.is_empty() || text.is_empty() || a.by.trim().is_empty() {
289 return Ok(Outcome::fail(FailureCode::Invalid, "A note needs a workspace, some words, and who wrote it."));
290 }
291 if text.chars().count() > 4000 {
292 return Ok(Outcome::fail(FailureCode::Invalid, "Keep a note under 4,000 characters."));
293 }
294 let now = now_ms();
295 self.db
296 .prepare("INSERT INTO sales_notes (id, workspace, text, by, created_at) VALUES (?, ?, ?, ?, ?)")
297 .bind(&[new_id("note", now).into(), workspace.as_str().into(), text.into(), a.by.as_str().into(), rfc3339(now).into()])?
298 .run()
299 .await?;
300 Ok(Outcome::Ok(self.admin_sales(AdminSalesArgs { workspace }).await?))
301 }
302
303 /// Every workspace worth reaching out to, most urgent first.
304 pub(crate) async fn admin_signals(&self, _: AdminSignalsArgs) -> Result<Vec<Signal>> {
305 let now = rfc3339(now_ms());
306 let month = &now[..7];
307 let last = previous_month(month);
308 #[derive(Deserialize)]
309 struct Active {
310 workspace: String,
311 this_month: Option<i64>,
312 last_month: Option<i64>,
313 first_paid: Option<String>,
314 }
315 let active = self
316 .db
317 .prepare(
318 "SELECT workspace,
319 -SUM(CASE WHEN kind = 'usage' AND created_at >= ?1 THEN amount_micros END) AS this_month,
320 -SUM(CASE WHEN kind = 'usage' AND created_at >= ?2 AND created_at < ?1 THEN amount_micros END) AS last_month,
321 MIN(CASE WHEN kind = 'top_up' AND amount_micros > 0 AND reference NOT LIKE 'crd%' THEN created_at END) AS first_paid
322 FROM ledger GROUP BY workspace
323 HAVING MAX(created_at) >= ?2
324 LIMIT 500",
325 )
326 .bind(&[format!("{month}-01").into(), format!("{last}-01").into()])?
327 .all()
328 .await?
329 .results::<Active>()?;
330 let day: u32 = now[8..10].parse().unwrap_or(1);
331 let fortnight_ago = rfc3339(now_ms() - 14 * 24 * 60 * 60 * 1000);
332 let mut signals = vec![];
333 for row in active {
334 let limit = self.limit_of(&row.workspace).await?;
335 if limit.trust == Trust::Internal {
336 continue;
337 }
338 let this_month = row.this_month.unwrap_or(0).max(0);
339 let last_month = row.last_month.unwrap_or(0).max(0);
340 let record = self.record_row(&row.workspace).await?;
Billing lists every invoice and every staff change; signals carry follow-ups341 let (stage, owner, next_step, next_at) =
342 record.map_or((None, None, None, None), |r| (Some(r.stage), r.owner, r.next_step, r.next_at));
Two limits, real invoices, trust that grows by itself, sales signals343 let mut push = |kind: SignalKind, detail: String, value: i64| {
344 signals.push(Signal {
345 workspace: row.workspace.clone(),
346 kind,
347 detail,
348 value_micros: value,
349 stage: stage.clone(),
350 owner: owner.clone(),
Billing lists every invoice and every staff change; signals carry follow-ups351 next_step: next_step.clone(),
352 next_at: next_at.clone(),
Two limits, real invoices, trust that grows by itself, sales signals353 });
354 };
355 let declined = limit.message.as_deref().is_some_and(|m| m.contains("could not be charged"));
356 if declined {
357 push(SignalKind::Declined, limit.message.clone().unwrap_or_default(), limit.exposure_micros);
358 } else if limit.state == LimitState::Stopped {
359 push(SignalKind::AtLimit, limit.message.clone().unwrap_or_default(), limit.exposure_micros.max(limit.spent_micros));
Billing keeps Stripe's view itself: the saved card on the account, missed events replayed every 15 minutes, and the endpoint kept360 } else if let Some(available) = limit.available_micros.filter(|a| *a > 0)
361 && limit.exposure_micros * 5 >= available * 4 {
Two limits, real invoices, trust that grows by itself, sales signals362 push(
363 SignalKind::NearCeiling,
364 format!(
365 "{} unpaid of the {} g1t allows it ({:?}); a call could raise it before it stops.",
366 dollars(limit.exposure_micros),
367 dollars(available),
368 limit.trust
369 ),
370 limit.exposure_micros,
371 );
372 }
373 if last_month >= HIGH_SPEND_MICROS {
374 let terms = self.terms_of(&row.workspace).await?;
375 if terms.kind == TermsKind::Standard && !limit.account.starts_with("ent_") {
376 push(
377 SignalKind::HighSpend,
378 format!("Spent {} last month on standard terms: worth offering custom terms or an enterprise.", dollars(last_month)),
379 last_month,
380 );
381 }
382 }
383 if growing(this_month, last_month, day, days_in(month)) {
384 push(
385 SignalKind::Growing,
386 format!(
387 "{} so far this month, on pace for about {}, against {} last month.",
388 dollars(this_month),
389 dollars((this_month as f64 * f64::from(days_in(month)) / f64::from(day.max(1))) as i64),
390 dollars(last_month)
391 ),
392 this_month,
393 );
394 }
395 if limit.trust == Trust::Established {
396 push(
397 SignalKind::Established,
398 format!(
399 "A steady customer: its limit now follows its spend ({} available).",
400 limit.available_micros.map(dollars).unwrap_or_default()
401 ),
402 last_month,
403 );
404 }
405 if row.first_paid.as_deref().is_some_and(|at| at >= fortnight_ago.as_str()) {
406 push(SignalKind::FirstPayment, "Paid g1t for the first time in the last two weeks: say hello.".to_owned(), this_month);
407 }
408 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily409 // Workspaces costing g1t more than they pay (margin.rs), for a look
410 // before they cost more: a pricing gap, or abuse.
411 for (workspace, detail, value) in self.costing_more_than_they_pay().await? {
412 let record = self.record_row(&workspace).await?;
413 let (stage, owner, next_step, next_at) =
414 record.map_or((None, None, None, None), |r| (Some(r.stage), r.owner, r.next_step, r.next_at));
415 signals.push(Signal { workspace, kind: SignalKind::CostOverRevenue, detail, value_micros: value, stage, owner, next_step, next_at });
416 }
Two limits, real invoices, trust that grows by itself, sales signals417 signals.sort_by(|a, b| urgency(a.kind).cmp(&urgency(b.kind)).then(b.value_micros.cmp(&a.value_micros)));
418 Ok(signals)
419 }
420
Billing lists every invoice and every staff change; signals carry follow-ups421 /// Every invoice g1t has sent, workspaces' and enterprises'.
422 pub(crate) async fn admin_invoices(&self, a: g1t_contracts::billing::AdminInvoicesArgs) -> Result<Vec<g1t_contracts::billing::InvoiceSummary>> {
423 #[derive(Deserialize)]
424 struct Row {
425 invoice_id: String,
426 kind: String,
427 account: String,
428 name: String,
429 reason: String,
430 period: String,
431 amount_micros: i64,
432 status: String,
433 hosted_url: Option<String>,
434 created_at: String,
435 paid_at: Option<String>,
436 }
437 let status = a.status.filter(|s| ["paid", "open", "failed", "overdue", "void"].contains(&s.as_str()));
438 let month = a.month.filter(|m| m.len() == 7 && m.chars().all(|c| c.is_ascii_digit() || c == '-'));
439 let rows = self
440 .db
441 .prepare(
442 "SELECT * FROM (
443 SELECT invoice_id, 'workspace' AS kind, workspace AS account, workspace AS name, reason, period,
444 amount_micros, status, hosted_url, created_at, paid_at
445 FROM workspace_invoices
446 UNION ALL
447 SELECT i.invoice_id, 'enterprise', i.account_id, COALESCE(b.name, i.account_id), 'enterprise', i.period,
448 i.amount_micros, i.status, i.hosted_url, i.created_at, i.paid_at
449 FROM enterprise_invoices i LEFT JOIN billing_accounts b ON b.id = i.account_id
450 )
451 WHERE (?1 IS NULL OR status = ?1) AND (?2 IS NULL OR substr(created_at, 1, 7) = ?2)
452 ORDER BY created_at DESC LIMIT 200",
453 )
454 .bind(&[crate::optional(status.as_deref()), crate::optional(month.as_deref())])?
455 .all()
456 .await?
457 .results::<Row>()?;
458 Ok(rows
459 .into_iter()
460 .map(|r| g1t_contracts::billing::InvoiceSummary {
461 invoice_id: r.invoice_id,
462 kind: r.kind,
463 account: r.account,
464 name: r.name,
465 reason: r.reason,
466 period: r.period,
467 amount_micros: r.amount_micros,
468 status: r.status,
469 hosted_url: r.hosted_url,
470 created_at: r.created_at,
471 paid_at: r.paid_at,
472 })
473 .collect())
474 }
475
476 /// Every change made in sudo and by Stripe, newest first.
477 pub(crate) async fn admin_audit(&self, a: g1t_contracts::billing::AdminAuditArgs) -> Result<Vec<g1t_contracts::billing::AdminAction>> {
478 #[derive(Deserialize)]
479 struct Row {
480 id: String,
481 account: String,
482 action: String,
483 detail: String,
484 by: String,
485 created_at: String,
486 }
487 let rows = self
488 .db
489 .prepare(
490 "SELECT id, account, action, detail, by, created_at FROM admin_actions
491 WHERE (?1 IS NULL OR by = ?1) AND (?2 IS NULL OR action = ?2) AND (?3 IS NULL OR created_at < ?3)
492 ORDER BY created_at DESC LIMIT 100",
493 )
494 .bind(&[
495 crate::optional(a.by.as_deref().map(str::trim).filter(|s| !s.is_empty())),
496 crate::optional(a.action.as_deref().map(str::trim).filter(|s| !s.is_empty())),
497 crate::optional(a.before.as_deref()),
498 ])?
499 .all()
500 .await?
501 .results::<Row>()?;
502 Ok(rows
503 .into_iter()
504 .map(|r| g1t_contracts::billing::AdminAction {
505 id: r.id,
506 account: r.account,
507 action: r.action,
508 detail: r.detail,
509 by: r.by,
510 created_at: r.created_at,
511 })
512 .collect())
513 }
514
Two limits, real invoices, trust that grows by itself, sales signals515 /// The business at a glance.
516 pub(crate) async fn admin_overview(&self, _: AdminOverviewArgs) -> Result<Overview> {
517 let now = rfc3339(now_ms());
518 let month = now[..7].to_owned();
519 let months = self.months_for(&[], 6).await?;
520 #[derive(Deserialize)]
521 struct KindRow {
522 kind: Option<String>,
523 charged: Option<i64>,
524 cost: Option<i64>,
525 }
526 let by_kind = self
527 .db
528 .prepare(
529 "SELECT CASE
530 WHEN task = 'sandbox' THEN 'Sandbox time'
Fast pages, required checks on the branch, self-hosted runners, honest incidents531 WHEN task = 'self_hosted' THEN 'Self-hosted runner time'
Two limits, real invoices, trust that grows by itself, sales signals532 WHEN task = 'deployments' THEN 'Deployments'
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put533 WHEN task = 'security' THEN 'Security scans'
534 WHEN task = 'context' THEN 'Search embeddings'
535 WHEN task = 'storage' THEN 'Private storage'
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look536 WHEN task = 'git' THEN 'Git operations'
Two limits, real invoices, trust that grows by itself, sales signals537 WHEN billed_to = 'workspace' THEN 'Own-provider runs'
538 ELSE 'Models' END AS kind,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look539 -SUM(amount_micros) AS charged,
540 SUM(CASE WHEN COALESCE(billed_to, 'g1t') = 'g1t' THEN cost_micros ELSE 0 END) AS cost
Two limits, real invoices, trust that grows by itself, sales signals541 FROM ledger WHERE kind = 'usage' AND created_at >= ? GROUP BY 1 ORDER BY charged DESC",
542 )
543 .bind(&[format!("{month}-01").into()])?
544 .all()
545 .await?
546 .results::<KindRow>()?
547 .into_iter()
548 .map(|r| KindFigures {
549 kind: r.kind.unwrap_or_else(|| "Other".to_owned()),
550 charged_micros: r.charged.unwrap_or(0),
551 cost_micros: r.cost.unwrap_or(0),
552 })
553 .collect();
554 #[derive(Deserialize)]
555 struct Count {
556 n: Option<i64>,
557 }
558 let count = |sql: &'static str, args: Vec<JsValue>| {
559 let db = &self.db;
560 async move {
561 Ok::<i64, worker::Error>(db.prepare(sql).bind(&args)?.first::<Count>(None).await?.and_then(|c| c.n).unwrap_or(0))
562 }
563 };
564 let paying = count(
565 "SELECT COUNT(DISTINCT workspace) AS n FROM ledger
566 WHERE kind = 'top_up' AND amount_micros > 0 AND reference NOT LIKE 'crd%' AND created_at >= ?",
567 vec![rfc3339(now_ms() - 60 * 24 * 60 * 60 * 1000).into()],
568 )
569 .await?;
570 let open_invoices = count(
571 "SELECT (SELECT COALESCE(SUM(amount_micros), 0) FROM workspace_invoices WHERE status IN ('open', 'failed'))
572 + (SELECT COALESCE(SUM(amount_micros), 0) FROM enterprise_invoices WHERE status IN ('open', 'overdue')) AS n",
573 vec![],
574 )
575 .await?;
576 let follow_ups = count(
577 "SELECT COUNT(*) AS n FROM sales_records WHERE next_at IS NOT NULL AND next_at <= ? AND stage NOT IN ('won', 'lost')",
578 vec![now[..10].into()],
579 )
580 .await?;
581 let signals = self.admin_signals(AdminSignalsArgs {}).await?;
582 let tally = |kind: SignalKind| signals.iter().filter(|s| s.kind == kind).count() as u32;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look583 let this = months.last().cloned().unwrap_or_default();
584 let active_plans = count(
585 "SELECT COUNT(*) AS n FROM subscriptions WHERE feature = 'plan' AND status IN ('active', 'canceling')",
586 vec![],
587 )
588 .await?;
589 let open_requests = count("SELECT COUNT(*) AS n FROM limit_requests WHERE status = 'open'", vec![]).await?;
590 let open_spikes = count("SELECT COUNT(*) AS n FROM spikes WHERE status = 'open'", vec![]).await?;
591 let overages = self.admin_overages(AdminOveragesArgs {}).await?.len() as u32;
Two limits, real invoices, trust that grows by itself, sales signals592 Ok(Overview {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look593 revenue_micros: this.charged_micros + this.plans_micros,
594 active_plans: active_plans as u32,
595 plan_mrr_micros: active_plans * i64::from(self.plans.plan_monthly_cents) * 10_000,
596 given: self.given_this_month(&month).await?,
597 internal: self.internal_use(&month).await?,
598 open_requests: open_requests as u32,
599 open_spikes: open_spikes as u32,
600 overages,
Two limits, real invoices, trust that grows by itself, sales signals601 month,
602 months,
603 by_kind,
604 paying_workspaces: paying as u32,
605 stopped: tally(SignalKind::AtLimit),
606 near_ceiling: tally(SignalKind::NearCeiling),
607 declined: tally(SignalKind::Declined),
608 open_invoices_micros: open_invoices,
609 follow_ups_due: follow_ups as u32,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put610 pools: Some(self.pools().await?),
Two limits, real invoices, trust that grows by itself, sales signals611 })
612 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look613
614 /// What g1t gave in `month`, by source, at price and at cost.
615 async fn given_this_month(&self, month: &str) -> Result<Vec<GivenFigures>> {
616 #[derive(Deserialize)]
617 struct Row {
618 internal_cost: Option<i64>,
619 trial: Option<i64>,
620 oss: Option<i64>,
621 covered: Option<i64>,
622 goodwill: Option<i64>,
623 }
624 let row = self
625 .db
626 .prepare(format!(
627 "SELECT
628 SUM(CASE WHEN kind = 'usage' AND COALESCE(billed_to, 'g1t') = 'g1t' AND workspace IN ({INTERNAL_SQL})
629 THEN cost_micros END) AS internal_cost,
630 SUM(CASE WHEN kind = 'usage' THEN trial_micros END) AS trial,
631 SUM(CASE WHEN kind = 'usage' THEN oss_micros END) AS oss,
632 SUM(CASE WHEN kind = 'usage' THEN given_micros END) AS covered,
633 SUM(CASE WHEN kind = 'top_up' AND reference LIKE '{goodwill}%' THEN amount_micros END) AS goodwill
634 FROM ledger WHERE created_at >= ?",
635 goodwill = crate::overages::GOODWILL_PREFIX
636 ))
637 .bind(&[format!("{month}-01").into()])?
638 .first::<Row>(None)
639 .await?;
640 let Some(row) = row else { return Ok(vec![]) };
641 let at_cost = |price: i64| price * 100 / i64::from(100 + self.margin_percent);
642 let internal_cost = row.internal_cost.unwrap_or(0);
643 Ok([
644 ("internal", "g1t's and Flagon's own use", crate::credits::with_margin(internal_cost, self.margin_percent), internal_cost),
645 ("trial", "Trials", row.trial.unwrap_or(0), at_cost(row.trial.unwrap_or(0))),
646 ("oss_pool", "Open-source pool", row.oss.unwrap_or(0), at_cost(row.oss.unwrap_or(0))),
647 ("covered", "Covered past a trial's end", row.covered.unwrap_or(0), at_cost(row.covered.unwrap_or(0))),
648 ("goodwill", "Goodwill credits", row.goodwill.unwrap_or(0), row.goodwill.unwrap_or(0)),
649 ]
650 .into_iter()
651 .map(|(source, label, micros, cost)| GivenFigures { source: source.to_owned(), label: label.to_owned(), micros, cost_micros: cost })
652 .collect())
653 }
654
655 /// g1t's own workspaces' use in `month`, with why each is not charged.
656 async fn internal_use(&self, month: &str) -> Result<Vec<InternalUse>> {
657 #[derive(Deserialize)]
658 struct Row {
659 workspace: String,
660 cost: Option<i64>,
661 entries: Option<u32>,
662 }
663 let rows = self
664 .db
665 .prepare(format!(
666 "SELECT workspace, SUM(cost_micros) AS cost, COUNT(*) AS entries FROM ledger
667 WHERE kind = 'usage' AND COALESCE(billed_to, 'g1t') = 'g1t' AND created_at >= ? AND workspace IN ({INTERNAL_SQL})
668 GROUP BY workspace ORDER BY cost DESC"
669 ))
670 .bind(&[format!("{month}-01").into()])?
671 .all()
672 .await?
673 .results::<Row>()?;
674 let mut list = vec![];
675 for row in rows {
676 let terms = self.terms_of(&row.workspace).await?;
677 list.push(InternalUse {
678 reason: if terms.note.is_empty() { "Comped".to_owned() } else { terms.note },
679 cost_micros: row.cost.unwrap_or(0),
680 entries: row.entries.unwrap_or(0),
681 workspace: row.workspace,
682 });
683 }
684 Ok(list)
685 }
Two limits, real invoices, trust that grows by itself, sales signals686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look692 fn group(kind: &str, billed_to: &str, amount: i64, cost: i64) -> LedgerGroup {
693 LedgerGroup {
694 month: "2026-10".into(),
695 kind: kind.into(),
696 billed_to: Some(billed_to.into()),
697 amount: Some(amount),
698 cost: Some(cost),
699 ..LedgerGroup::default()
700 }
701 }
702
703 #[test]
704 fn a_workspaces_own_provider_is_never_g1ts_cost() {
705 let groups = [
706 // Runs on g1t's models: charged $12, cost g1t $10.
707 group("usage", "g1t", -12_000_000, 10_000_000),
708 // A run on the workspace's own provider: it paid that $0.21 itself.
709 group("usage", "workspace", -100_000, 209_700),
710 ];
711 let month = fold_month("2026-10", &groups, 0, 20);
712 assert_eq!(month.cost_micros, 10_000_000);
713 assert_eq!(month.charged_micros, 12_100_000);
714 }
715
716 #[test]
717 fn plan_revenue_counts_and_what_g1t_gives_is_kept_apart() {
718 let groups = [
719 group("usage", "g1t", -12_000_000, 10_000_000),
720 // Paid for by the trial and the open-source pool: $3 at price.
721 LedgerGroup { pools: Some(3_000_000), ..group("usage", "g1t", 0, 2_500_000) },
722 // g1t's own workspace: nothing charged, $5 of cost.
723 LedgerGroup { internal: 1, ..group("usage", "g1t", 0, 5_000_000) },
724 // A payment, a goodwill credit, and another credit from g1t.
725 group("top_up", "g1t", 40_000_000, 0),
726 LedgerGroup { credit: 1, goodwill: 1, ..group("top_up", "g1t", 15_000_000, 0) },
727 LedgerGroup { credit: 1, ..group("top_up", "g1t", 2_000_000, 0) },
728 // Another month is not this one.
729 LedgerGroup { month: "2026-09".into(), ..group("usage", "g1t", -1, 1) },
730 ];
731 let month = fold_month("2026-10", &groups, 20_000_000, 20);
732 assert_eq!(month.plans_micros, 20_000_000);
733 assert_eq!(month.paid_micros, 40_000_000);
734 assert_eq!(month.charged_micros, 12_000_000);
735 assert_eq!(month.cost_micros, 17_500_000);
736 // $6 of internal use at price, $3 of pools, $15 of goodwill.
737 assert_eq!(month.given_micros, 6_000_000 + 3_000_000 + 15_000_000);
738 }
739
Two limits, real invoices, trust that grows by itself, sales signals740 #[test]
741 fn six_months_end_with_this_one() {
742 assert_eq!(last_months("2026-02", 3), vec!["2025-12", "2026-01", "2026-02"]);
743 }
744
745 #[test]
746 fn growth_is_judged_on_pace_not_on_the_month_so_far() {
747 // Ten days in, $20 on a 30-day month is a $60 pace against $30.
748 assert!(growing(20_000_000, 30_000_000, 10, 30));
749 assert!(!growing(10_000_000, 30_000_000, 10, 30));
750 // Too small last month to say.
751 assert!(!growing(9_000_000, 1_000_000, 10, 30));
752 }
753
754 #[test]
755 fn the_most_urgent_comes_first() {
756 assert!(urgency(SignalKind::AtLimit) < urgency(SignalKind::NearCeiling));
757 assert!(urgency(SignalKind::Declined) < urgency(SignalKind::Growing));
758 }
759
760 #[test]
761 fn february_knows_its_leap_years() {
762 assert_eq!(days_in("2028-02"), 29);
763 assert_eq!(days_in("2026-02"), 28);
764 assert_eq!(days_in("2026-10"), 31);
765 }
766}