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.
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 1 | //! What a workspace is charged in a month: one definition, used by the |
| 2 | //! spend limit (`limits::limit_with`, the `spent_micros` every signed-in | |
| 3 | //! page shows), the Usage page and the Billing page's plan card | |
| 4 | //! (`report::usage_report`), and so by Spend's "Charged" tile and the top | |
| 5 | //! bar, which read the limit. | |
| 6 | //! | |
| 7 | //! **Charged** is usage at price, less the account's discount, less what | |
| 8 | //! the plan's included usage, the trial, the open-source pool or g1t paid, | |
| 9 | //! less what credit paid; never below zero. Every ledger line carries each | |
| 10 | //! part (`statement::PRICE_SQL` reads the price back from them), and usage | |
| 11 | //! metered through the month and charged when it closes (`pending_usage`: | |
| 12 | //! storage, git operations, scans, embeddings, domains, app traffic) counts | |
| 13 | //! now on the same terms it will be entered on: at cost plus the margin, | |
| 14 | //! with the discount off, and nothing for a source g1t covers on a free | |
| 15 | //! workspace. So a workspace with a 100% discount is charged exactly | |
| 16 | //! nothing, however much is pending. | |
| 17 | ||
| 18 | use g1t_contracts::billing::UsageTotals; | |
| 19 | use serde::Deserialize; | |
| 20 | use worker::wasm_bindgen::JsValue; | |
| 21 | use worker::Result; | |
| 22 | ||
| 23 | use crate::Billing; | |
| 24 | use crate::credits::with_margin; | |
| 25 | ||
| 26 | /// What is left to pay: usage at price less the discount, included usage | |
| 27 | /// and credit. The one arithmetic for "charged". | |
| 28 | pub(crate) fn charged(totals: &UsageTotals) -> i64 { | |
| 29 | (totals.price_micros - totals.discount_micros - totals.included_micros - totals.credits_micros).max(0) | |
| 30 | } | |
| 31 | ||
| 32 | /// A month-end source's usage so far this month, metered and not charged | |
| 33 | /// yet (`pending_usage`). | |
| 34 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] | |
| 35 | pub(crate) struct PendingLine { | |
| 36 | pub source: String, | |
| 37 | /// What it cost g1t so far. | |
| 38 | #[serde(default)] | |
| 39 | pub cost_micros: Option<f64>, | |
| 40 | /// What it will be charged before the account's terms: at cost plus | |
| 41 | /// the margin, or nothing for a source g1t covers on a free workspace | |
| 42 | /// (`storage::set_pending`). | |
| 43 | #[serde(default)] | |
| 44 | pub charge_micros: Option<f64>, | |
| 45 | } | |
| 46 | ||
| 47 | impl PendingLine { | |
| 48 | pub fn cost(&self) -> i64 { | |
| 49 | self.cost_micros.unwrap_or(0.0).round().max(0.0) as i64 | |
| 50 | } | |
| 51 | ||
| 52 | pub fn charge(&self) -> i64 { | |
| 53 | self.charge_micros.unwrap_or(0.0).round().max(0.0) as i64 | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | /// A pending line's parts, as the month's close will enter them: its | |
| 58 | /// price, what g1t covers of it, and what the discount takes off the rest. | |
| 59 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] | |
| 60 | pub(crate) struct PendingSplit { | |
| 61 | pub price: i64, | |
| 62 | pub covered: i64, | |
| 63 | pub discount: i64, | |
| 64 | } | |
| 65 | ||
| 66 | impl PendingSplit { | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 67 | /// What the workspace will be charged for it when the month closes. |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 68 | pub fn charged(&self) -> i64 { |
| 69 | self.price - self.covered - self.discount | |
| 70 | } | |
| 71 | } | |
| 72 | ||
| 73 | /// Splits a pending line: `cost` at cost plus `margin_percent` is its | |
| 74 | /// price; `charge` (what it will be charged before terms) below that is | |
| 75 | /// covered by g1t; `percent_off` comes off what is charged. | |
| 76 | pub(crate) fn pending_split(cost: i64, charge: i64, margin_percent: u32, percent_off: u32) -> PendingSplit { | |
| 77 | let price = with_margin(cost.max(0), margin_percent); | |
| 78 | let base = charge.clamp(0, price); | |
| 79 | PendingSplit { price, covered: price - base, discount: base * i64::from(percent_off.min(100)) / 100 } | |
| 80 | } | |
| 81 | ||
| 82 | /// Counts a pending line into the totals: at price (and as pending), with | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 83 | /// what g1t covers as included, the discount as discount, and what is |
| 84 | /// left as what the close will charge. | |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 85 | pub(crate) fn add_pending(totals: &mut UsageTotals, split: &PendingSplit) { |
| 86 | totals.price_micros += split.price; | |
| 87 | totals.pending_micros += split.price; | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 88 | totals.pending_charged_micros += split.charged(); |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 89 | totals.included_micros += split.covered; |
| 90 | totals.discount_micros += split.discount; | |
| 91 | } | |
| 92 | ||
| 93 | /// The month's totals from its ledger lines and pending usage, and what | |
| 94 | /// credit paid, with `charged_micros` worked out. | |
| 95 | pub(crate) fn month_totals(price: i64, discount: i64, covered: i64, credits: i64, pending: &[PendingSplit]) -> UsageTotals { | |
| 96 | let mut totals = UsageTotals { | |
| 97 | price_micros: price, | |
| 98 | discount_micros: discount, | |
| 99 | included_micros: covered, | |
| 100 | credits_micros: credits.max(0), | |
| 101 | ..UsageTotals::default() | |
| 102 | }; | |
| 103 | for split in pending { | |
| 104 | add_pending(&mut totals, split); | |
| 105 | } | |
| 106 | totals.charged_micros = charged(&totals); | |
| 107 | totals | |
| 108 | } | |
| 109 | ||
| 110 | impl Billing { | |
| 111 | /// What the workspaces have metered this month (`YYYY-MM`) that is | |
| 112 | /// charged when it closes, not charged yet. | |
| 113 | pub(crate) async fn pending_this_month(&self, members: &[JsValue], month: &str) -> Result<Vec<PendingLine>> { | |
| 114 | let marks = vec!["?"; members.len().max(1)].join(", "); | |
| 115 | let mut args = members.to_vec(); | |
| 116 | args.push(month.into()); | |
| 117 | self.db | |
| 118 | .prepare(format!( | |
| 119 | "SELECT source, cost_micros, charge_micros FROM pending_usage | |
| 120 | WHERE workspace IN ({marks}) AND month = ? AND charged_at IS NULL" | |
| 121 | )) | |
| 122 | .bind(&args)? | |
| 123 | .all() | |
| 124 | .await? | |
| 125 | .results::<PendingLine>() | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | #[cfg(test)] | |
| 130 | mod tests { | |
| 131 | use super::*; | |
| 132 | ||
| 133 | const MARGIN: u32 = 20; | |
| 134 | ||
| 135 | #[test] | |
| 136 | fn a_full_discount_is_charged_exactly_nothing_pending_usage_included() { | |
| 137 | // flagon-io, October 2026: $10.29 of usage at price on the ledger, | |
| 138 | // every line discounted in full; and $0.0075 of storage metered so | |
| 139 | // far, which the limit used to count at its charge, $0.009. | |
| 140 | let pending = [pending_split(7_500, with_margin(7_500, MARGIN), MARGIN, 100)]; | |
| 141 | assert_eq!(pending[0], PendingSplit { price: 9_000, covered: 0, discount: 9_000 }); | |
| 142 | let totals = month_totals(10_290_000, 10_290_000, 0, 0, &pending); | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 143 | assert_eq!((totals.pending_micros, totals.pending_charged_micros), (9_000, 0)); |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 144 | assert_eq!(totals.price_micros, 10_299_000); |
| 145 | assert_eq!(totals.discount_micros, 10_299_000); | |
| 146 | assert_eq!(totals.charged_micros, 0); | |
| 147 | assert_eq!(charged(&totals), 0); | |
| 148 | } | |
| 149 | ||
| 150 | #[test] | |
| 151 | fn included_usage_pays_first_and_only_the_rest_is_charged() { | |
| 152 | // The plan: $10 included. $12.50 of runs at price, $10 of it drawn | |
| 153 | // from the included usage on the lines themselves; $0.60 of storage | |
| 154 | // pending, which the close will charge in full (the included usage | |
| 155 | // is spent). | |
| 156 | let pending = [pending_split(500_000, with_margin(500_000, MARGIN), MARGIN, 0)]; | |
| 157 | assert_eq!(pending[0], PendingSplit { price: 600_000, covered: 0, discount: 0 }); | |
| 158 | let totals = month_totals(12_500_000, 0, 10_000_000, 0, &pending); | |
| 159 | assert_eq!(totals.included_micros, 10_000_000); | |
| 160 | assert_eq!(totals.charged_micros, 2_500_000 + 600_000); | |
| 161 | // Credit from g1t pays after that. | |
| 162 | let with_credit = month_totals(12_500_000, 0, 10_000_000, 1_000_000, &pending); | |
| 163 | assert_eq!(with_credit.charged_micros, 2_100_000); | |
| 164 | // Never below zero: more credit than usage charges nothing. | |
| 165 | assert_eq!(month_totals(1_000_000, 0, 0, 5_000_000, &[]).charged_micros, 0); | |
| 166 | } | |
| 167 | ||
| 168 | #[test] | |
| 169 | fn pay_as_you_go_is_charged_at_price() { | |
| 170 | // No discount, nothing included, no credit: price is what is | |
| 171 | // charged, pending usage counted at its price too. | |
| 172 | let pending = [pending_split(1_000_000, with_margin(1_000_000, MARGIN), MARGIN, 0), pending_split(250, 300, MARGIN, 0)]; | |
| 173 | let totals = month_totals(4_000_000, 0, 0, 0, &pending); | |
| 174 | assert_eq!(totals.pending_micros, 1_200_000 + 300); | |
| 175 | assert_eq!(totals.charged_micros, 4_000_000 + 1_200_000 + 300); | |
| 176 | } | |
| 177 | ||
| 178 | #[test] | |
| 179 | fn a_partial_discount_comes_off_pending_usage_as_the_close_will_take_it() { | |
| 180 | // 30% off: $0.009 of pending storage is charged $0.0063. | |
| 181 | let split = pending_split(7_500, 9_000, MARGIN, 30); | |
| 182 | assert_eq!(split, PendingSplit { price: 9_000, covered: 0, discount: 2_700 }); | |
| 183 | assert_eq!(split.charged(), 6_300); | |
| 184 | } | |
| 185 | ||
| 186 | #[test] | |
| 187 | fn what_g1t_covers_on_a_free_workspace_is_included_not_charged() { | |
| 188 | // Security scans on a free workspace: metered at cost, charged | |
| 189 | // nothing (`set_pending` writes a charge of 0), shown at price. | |
| 190 | let split = pending_split(2_000, 0, MARGIN, 0); | |
| 191 | assert_eq!(split, PendingSplit { price: 2_400, covered: 2_400, discount: 0 }); | |
| 192 | assert_eq!(split.charged(), 0); | |
| 193 | let totals = month_totals(0, 0, 0, 0, &[split]); | |
| 194 | assert_eq!(totals.price_micros, 2_400); | |
| 195 | assert_eq!(totals.included_micros, 2_400); | |
| 196 | assert_eq!(totals.charged_micros, 0); | |
| 197 | // A charge above the price (an older margin) is held to the price. | |
| 198 | assert_eq!(pending_split(1_000, 5_000, MARGIN, 0).covered, 0); | |
| 199 | } | |
| 200 | ||
| 201 | #[test] | |
| 202 | fn a_pending_row_reads_whole_micros_and_never_less_than_nothing() { | |
| 203 | let line = PendingLine { source: "storage".into(), cost_micros: Some(7_499.6), charge_micros: Some(-1.0) }; | |
| 204 | assert_eq!(line.cost(), 7_500); | |
| 205 | assert_eq!(line.charge(), 0); | |
| 206 | assert_eq!(PendingLine::default().cost(), 0); | |
| 207 | } | |
| 208 | } |