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.
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 1 | //! What pays for usage before the workspace does. |
| 2 | //! | |
| 3 | //! Every charge is worked out the same way: its cost plus the margin, then | |
| 4 | //! the account's terms. What is left is drawn down, in this order, from: | |
| 5 | //! | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 6 | //! 1. **The plan's included usage** (`PLAN_INCLUDED_MICROS` a month, $10), |
| 7 | //! when the workspace has the g1t plan. Any usage draws on it. Unused | |
| 8 | //! included usage does not roll over. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 9 | //! 2. **The trial credit**: one grant per workspace |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 10 | //! (`TRIAL_WORKSPACE_MICROS`, $5), made once its card is checked (see |
| 11 | //! `cards`), out of a pool for everyone that resets each calendar month | |
| 12 | //! (`TRIAL_MONTHLY_POOL_MICROS`, $100). Never for deployments. | |
| 13 | //! 3. **g1t's open-source pool** (`OSS_POOL_MICROS` a month, $25, at most | |
| 14 | //! `OSS_REPO_MICROS`, $2, for any one repository): checks, workflows and | |
| 15 | //! the merge queue on a public repository. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 16 | //! |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 17 | //! Whatever is left is charged: from what was paid in advance first, since |
| 18 | //! a charge comes off the balance, and then owed. For a free workspace's | |
| 19 | //! compute, what is left past its trial is covered by g1t (`given`): a free | |
| 20 | //! workspace is never charged for compute, and `reserve` keeps that to the | |
| 21 | //! runs already in flight when the trial ran out. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 22 | //! |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 23 | //! Each source is a fixed, capped budget that something pays for: the |
| 24 | //! plan, or g1t. Nothing here is an open-ended allowance per workspace. | |
| 25 | //! | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 26 | //! Months are calendar months in UTC, the same as the limits'. Every draw |
| 27 | //! is one D1 batch, which runs as a transaction, so two charges at once | |
| 28 | //! never take more than a budget holds. | |
| 29 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 30 | use g1t_contracts::billing::{ComputeKind, Feature, PlanKind, Pools, TermsKind, Trial, TrialArgs}; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 31 | use g1t_contracts::time::rfc3339; |
| 32 | use g1t_kit::now_ms; | |
| 33 | use serde::Deserialize; | |
| 34 | use worker::{Env, Result}; | |
| 35 | ||
| 36 | use crate::Billing; | |
| 37 | use crate::features::dollars; | |
| 38 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 39 | /// Every number of the plan and the pools, from the billing service's |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 40 | /// variables, each with its default. |
| 41 | #[derive(Clone, Debug)] | |
| 42 | pub(crate) struct Config { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 43 | /// `PLAN_MONTHLY_CENTS`: the plan's price, per workspace: $20. |
| 44 | pub plan_monthly_cents: u32, | |
| 45 | /// `PLAN_INCLUDED_MICROS`: its included usage each month: $10. | |
| 46 | pub plan_included_micros: i64, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 47 | /// `OSS_POOL_MICROS`: g1t's open-source pool each month, in all. |
| 48 | pub oss_pool_micros: i64, | |
| 49 | /// `OSS_REPO_MICROS`: any one public repository's share of it. | |
| 50 | pub oss_repo_micros: i64, | |
| 51 | /// `TRIAL_WORKSPACE_MICROS`: each new workspace's trial credit. | |
| 52 | pub trial_workspace_micros: i64, | |
| 53 | /// `TRIAL_MONTHLY_POOL_MICROS`: trial grants each month, in all. | |
| 54 | pub trial_monthly_pool_micros: i64, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 55 | /// `MIN_CHARGE_MICROS`: a month's close charges no less; smaller |
| 56 | /// amounts carry over. Charges at a limit always go through. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 57 | pub min_charge_micros: i64, |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 58 | /// `FREE_PRIVATE_STORAGE_BYTES`: private repository storage that is |
| 59 | /// free for every workspace. Past it, the plan pays at cost plus the | |
| 60 | /// margin; a free workspace's pushes to private repositories stop. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 61 | pub free_storage_bytes: i64, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 62 | /// `AUDIT_RETENTION_DAYS`: the same on every plan. |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 63 | pub audit_days: u32, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 64 | /// `RUN_CAP_MICROS` and `ISSUE_CAP_MICROS`: one run's spend cap, and |
| 65 | /// agents' spend on one issue in all. | |
| 66 | pub run_cap_micros: i64, | |
| 67 | pub issue_cap_micros: i64, | |
| 68 | /// `LIMIT_PAID_START_MICROS`: a new paid workspace's ceiling in its | |
| 69 | /// first month. | |
| 70 | pub paid_start_micros: i64, | |
| 71 | /// `SPIKE_FACTOR` and `SPIKE_FLOOR_MICROS`: an hour above this many | |
| 72 | /// times the usual hour, and at least this much, is a spike. | |
| 73 | pub spike_factor: i64, | |
| 74 | pub spike_floor_micros: i64, | |
| 75 | /// `OVERAGE_FORGIVE_COST_MICROS`: the most of an overage's real cost a | |
| 76 | /// one-click goodwill credit covers. | |
| 77 | pub forgive_cost_micros: i64, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 78 | /// `GIT_OPERATIONS_INCLUDED`: git operations a month that are free for |
| 79 | /// every workspace. Past it, the plan pays at cost plus the margin and | |
| 80 | /// is never slowed; a free workspace is slowed down (the repos | |
| 81 | /// service's `GIT_OPERATIONS_FREE_CAP`, the same number), never charged. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 82 | pub git_included: u64, |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 83 | } |
| 84 | ||
| 85 | impl Default for Config { | |
| 86 | fn default() -> Self { | |
| 87 | Config { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 88 | plan_monthly_cents: 2_000, |
| 89 | plan_included_micros: 10_000_000, | |
| 90 | oss_pool_micros: 25_000_000, | |
| 91 | oss_repo_micros: 2_000_000, | |
| 92 | trial_workspace_micros: 5_000_000, | |
| 93 | trial_monthly_pool_micros: 100_000_000, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 94 | min_charge_micros: 5_000_000, |
| 95 | free_storage_bytes: 1_000_000_000, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 96 | audit_days: 90, |
| 97 | run_cap_micros: 2_000_000, | |
| 98 | issue_cap_micros: 10_000_000, | |
| 99 | paid_start_micros: 100_000_000, | |
| 100 | spike_factor: 5, | |
| 101 | spike_floor_micros: 5_000_000, | |
| 102 | forgive_cost_micros: 50_000_000, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 103 | git_included: 50_000, |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 104 | } |
| 105 | } | |
| 106 | } | |
| 107 | ||
| 108 | impl Config { | |
| 109 | pub(crate) fn from_env(env: &Env) -> Self { | |
| 110 | let d = Config::default(); | |
| 111 | let number = |name: &str, default: i64| -> i64 { | |
| 112 | env.var(name).ok().and_then(|v| v.to_string().trim().parse::<i64>().ok()).filter(|n| *n >= 0).unwrap_or(default) | |
| 113 | }; | |
| 114 | Config { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 115 | plan_monthly_cents: number("PLAN_MONTHLY_CENTS", d.plan_monthly_cents.into()) as u32, |
| 116 | plan_included_micros: number("PLAN_INCLUDED_MICROS", d.plan_included_micros), | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 117 | oss_pool_micros: number("OSS_POOL_MICROS", d.oss_pool_micros), |
| 118 | oss_repo_micros: number("OSS_REPO_MICROS", d.oss_repo_micros), | |
| 119 | trial_workspace_micros: number("TRIAL_WORKSPACE_MICROS", d.trial_workspace_micros), | |
| 120 | trial_monthly_pool_micros: number("TRIAL_MONTHLY_POOL_MICROS", d.trial_monthly_pool_micros), | |
| 121 | min_charge_micros: number("MIN_CHARGE_MICROS", d.min_charge_micros), | |
| 122 | free_storage_bytes: number("FREE_PRIVATE_STORAGE_BYTES", d.free_storage_bytes), | |
| 123 | audit_days: number("AUDIT_RETENTION_DAYS", d.audit_days.into()) as u32, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 124 | run_cap_micros: number("RUN_CAP_MICROS", d.run_cap_micros), |
| 125 | issue_cap_micros: number("ISSUE_CAP_MICROS", d.issue_cap_micros), | |
| 126 | paid_start_micros: number("LIMIT_PAID_START_MICROS", d.paid_start_micros), | |
| 127 | spike_factor: number("SPIKE_FACTOR", d.spike_factor).max(1), | |
| 128 | spike_floor_micros: number("SPIKE_FLOOR_MICROS", d.spike_floor_micros), | |
| 129 | forgive_cost_micros: number("OVERAGE_FORGIVE_COST_MICROS", d.forgive_cost_micros), | |
| 130 | git_included: number("GIT_OPERATIONS_INCLUDED", d.git_included as i64) as u64, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 131 | } |
| 132 | } | |
| 133 | } | |
| 134 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 135 | /// What may pay for a charge besides the plan's included usage, which any |
| 136 | /// usage may draw on. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 137 | #[derive(Clone, Debug, Default)] |
| 138 | pub(crate) struct Eligible { | |
| 139 | /// The trial credit: everything but deployments. | |
| 140 | pub trial: bool, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 141 | /// The open-source pool: this repository (`owner/name`), if it is |
| 142 | /// public. Only checks, workflows and the merge queue name one. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 143 | pub repo: Option<String>, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 144 | /// g1t covers what is left, rather than charging it, when the workspace |
| 145 | /// has no plan: a free workspace's compute. | |
| 146 | pub cover_rest: bool, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 147 | } |
| 148 | ||
| 149 | /// What paid for a charge before the workspace did. | |
| 150 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] | |
| 151 | pub(crate) struct Drawn { | |
| 152 | pub credit: i64, | |
| 153 | pub trial: i64, | |
| 154 | pub oss: i64, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 155 | /// What g1t covered itself. |
| 156 | pub given: i64, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 157 | } |
| 158 | ||
| 159 | impl Drawn { | |
| 160 | pub fn total(&self) -> i64 { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 161 | self.credit + self.trial + self.oss + self.given |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 162 | } |
| 163 | ||
| 164 | /// For the statement: what paid for the entry, e.g. ` ($0.12 paid by | |
| 165 | /// g1t's open-source pool)`. Empty when nothing did. | |
| 166 | pub fn note(&self) -> String { | |
| 167 | let parts: Vec<String> = [ | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 168 | (self.credit, "paid by your plan's included usage"), |
| 169 | (self.trial, "paid by your trial credit"), | |
| 170 | (self.oss, "paid by g1t's open-source pool"), | |
| 171 | (self.given, "covered by g1t"), | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 172 | ] |
| 173 | .iter() | |
| 174 | .filter(|(micros, _)| *micros > 0) | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 175 | .map(|(micros, by)| format!("{} {by}", dollars(*micros))) |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 176 | .collect(); |
| 177 | if parts.is_empty() { String::new() } else { format!(" ({})", parts.join(", ")) } | |
| 178 | } | |
| 179 | } | |
| 180 | ||
| 181 | /// How `gross` is paid for from sources with `available` left each, in | |
| 182 | /// order: each takes what it can of what is still unpaid. The rest is | |
| 183 | /// charged. | |
| 184 | pub(crate) fn split(gross: i64, available: &[i64]) -> Vec<i64> { | |
| 185 | let mut left = gross.max(0); | |
| 186 | available | |
| 187 | .iter() | |
| 188 | .map(|available| { | |
| 189 | let take = left.min((*available).max(0)); | |
| 190 | left -= take; | |
| 191 | take | |
| 192 | }) | |
| 193 | .collect() | |
| 194 | } | |
| 195 | ||
| 196 | /// What a budget with `cap` and `used` so far has left. | |
| 197 | pub(crate) fn left(cap: i64, used: i64) -> i64 { | |
| 198 | (cap - used).max(0) | |
| 199 | } | |
| 200 | ||
| 201 | /// `YYYY-MM` of an RFC 3339 time. | |
| 202 | pub(crate) fn month_of(timestamp: &str) -> String { | |
| 203 | timestamp[..7].to_owned() | |
| 204 | } | |
| 205 | ||
| 206 | /// The first instant of the month after `month`: when this month's pools | |
| 207 | /// reset. | |
| 208 | pub(crate) fn next_month_start(month: &str) -> String { | |
| 209 | let year: i32 = month[..4].parse().unwrap_or(1970); | |
| 210 | let number: u32 = month[5..7].parse().unwrap_or(1); | |
| 211 | if number == 12 { | |
| 212 | format!("{}-01-01T00:00:00Z", year + 1) | |
| 213 | } else { | |
| 214 | format!("{year}-{:02}-01T00:00:00Z", number + 1) | |
| 215 | } | |
| 216 | } | |
| 217 | ||
| 218 | /// The last second of `month`, for a charge that belongs to a month that | |
| 219 | /// is over. | |
| 220 | pub(crate) fn month_end(month: &str) -> String { | |
| 221 | let year: i32 = month[..4].parse().unwrap_or(1970); | |
| 222 | let number: u32 = month[5..7].parse().unwrap_or(1); | |
| 223 | let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; | |
| 224 | let days = match number { | |
| 225 | 2 if leap => 29, | |
| 226 | 2 => 28, | |
| 227 | 4 | 6 | 9 | 11 => 30, | |
| 228 | _ => 31, | |
| 229 | }; | |
| 230 | format!("{month}-{days:02}T23:59:59Z") | |
| 231 | } | |
| 232 | ||
| 233 | /// What a trial grant would be: the account's own amount from sudo, or the | |
| 234 | /// default. | |
| 235 | pub(crate) fn grant_size(config: &Config, staff: Option<i64>) -> i64 { | |
| 236 | staff.unwrap_or(config.trial_workspace_micros).max(0) | |
| 237 | } | |
| 238 | ||
| 239 | /// Whether this month's pool can still make a grant of `amount`. | |
| 240 | pub(crate) fn pool_has_room(pool: i64, granted_this_month: i64, amount: i64) -> bool { | |
| 241 | amount > 0 && granted_this_month + amount <= pool | |
| 242 | } | |
| 243 | ||
| 244 | #[derive(Deserialize)] | |
| 245 | struct Used { | |
| 246 | used: Option<i64>, | |
| 247 | } | |
| 248 | ||
| 249 | #[derive(Deserialize)] | |
| 250 | pub(crate) struct Grant { | |
| 251 | pub granted_micros: i64, | |
| 252 | pub used_micros: i64, | |
| 253 | } | |
| 254 | ||
| 255 | impl Billing { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 256 | /// The workspace's plan: comped terms are internal, an enterprise's |
| 257 | /// workspaces are invoiced, and otherwise the plan is paid for (or | |
| 258 | /// given by staff without its price) or not. A Deployments subscription | |
| 259 | /// from before the plan counts as the plan until its period ends. | |
| 260 | /// Without a card processor every workspace has the plan: a g1t that | |
| 261 | /// does not charge has nothing to gate. | |
| 262 | pub(crate) async fn plan_kind(&self, workspace: &str) -> Result<PlanKind> { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 263 | let account = self.account_of(workspace).await?; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 264 | if account.terms.kind == TermsKind::Comped { |
| 265 | return Ok(PlanKind::Internal); | |
| 266 | } | |
| 267 | if account.kind == g1t_contracts::billing::AccountKind::Enterprise { | |
| 268 | return Ok(PlanKind::Enterprise); | |
| 269 | } | |
| 270 | if self.stripe.is_none() || account.allowances.plan { | |
| 271 | return Ok(PlanKind::Paid); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 272 | } |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 273 | if self.plan_on(workspace, Feature::Plan).await? || self.plan_on(workspace, Feature::Deployments).await? { |
| 274 | return Ok(PlanKind::Paid); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 275 | } |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 276 | Ok(PlanKind::Free) |
| 277 | } | |
| 278 | ||
| 279 | /// Whether the workspace has the g1t plan now, whoever pays for it. | |
| 280 | pub(crate) async fn has_plan(&self, workspace: &str) -> Result<bool> { | |
| 281 | Ok(self.plan_kind(workspace).await? != PlanKind::Free) | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 282 | } |
| 283 | ||
| 284 | /// What one monthly allowance has used. | |
| 285 | pub(crate) async fn allowance_used(&self, kind: &str, scope: &str, month: &str) -> Result<i64> { | |
| 286 | Ok(self | |
| 287 | .db | |
| 288 | .prepare("SELECT used FROM allowance_use WHERE kind = ? AND scope = ? AND month = ?") | |
| 289 | .bind(&[kind.into(), scope.into(), month.into()])? | |
| 290 | .first::<Used>(None) | |
| 291 | .await? | |
| 292 | .and_then(|u| u.used) | |
| 293 | .unwrap_or(0)) | |
| 294 | } | |
| 295 | ||
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 296 | /// Adds `amount` to a monthly count with no cap, such as the month's |
| 297 | /// build seconds, which the Billing page shows beside what they cost. | |
| 298 | pub(crate) async fn tally(&self, kind: &str, scope: &str, month: &str, amount: i64) -> Result<()> { | |
| 299 | if amount <= 0 { | |
| 300 | return Ok(()); | |
| 301 | } | |
| 302 | self.db | |
| 303 | .prepare( | |
| 304 | "INSERT INTO allowance_use (kind, scope, month, used) VALUES (?1, ?2, ?3, ?4) | |
| 305 | ON CONFLICT (kind, scope, month) DO UPDATE SET used = used + ?4", | |
| 306 | ) | |
| 307 | .bind(&[kind.into(), scope.into(), month.into(), (amount as f64).into()])? | |
| 308 | .run() | |
| 309 | .await?; | |
| 310 | Ok(()) | |
| 311 | } | |
| 312 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 313 | /// Takes up to `want` from a monthly allowance with `cap`, as one |
| 314 | /// transaction. Returns what it took. | |
| 315 | pub(crate) async fn draw_allowance(&self, kind: &str, scope: &str, month: &str, want: i64, cap: i64) -> Result<i64> { | |
| 316 | if want <= 0 || cap <= 0 { | |
| 317 | return Ok(0); | |
| 318 | } | |
| 319 | let key = [kind.into(), scope.into(), month.into()]; | |
| 320 | let results = self | |
| 321 | .db | |
| 322 | .batch(vec![ | |
| 323 | self.db | |
| 324 | .prepare("INSERT OR IGNORE INTO allowance_use (kind, scope, month, used) VALUES (?1, ?2, ?3, 0)") | |
| 325 | .bind(&key)?, | |
| 326 | self.db | |
| 327 | .prepare("SELECT used FROM allowance_use WHERE kind = ?1 AND scope = ?2 AND month = ?3") | |
| 328 | .bind(&key)?, | |
| 329 | self.db | |
| 330 | .prepare( | |
| 331 | "UPDATE allowance_use SET used = MIN(?4, used + ?5) | |
| 332 | WHERE kind = ?1 AND scope = ?2 AND month = ?3 AND used < ?4", | |
| 333 | ) | |
| 334 | .bind(&[kind.into(), scope.into(), month.into(), (cap as f64).into(), (want as f64).into()])?, | |
| 335 | self.db | |
| 336 | .prepare("SELECT used FROM allowance_use WHERE kind = ?1 AND scope = ?2 AND month = ?3") | |
| 337 | .bind(&key)?, | |
| 338 | ]) | |
| 339 | .await?; | |
| 340 | let read = |i: usize| -> Result<i64> { | |
| 341 | Ok(results[i].results::<Used>()?.first().and_then(|u| u.used).unwrap_or(0)) | |
| 342 | }; | |
| 343 | Ok((read(3)? - read(1)?).max(0)) | |
| 344 | } | |
| 345 | ||
| 346 | /// Gives back what was drawn and not used. | |
| 347 | async fn return_allowance(&self, kind: &str, scope: &str, month: &str, amount: i64) -> Result<()> { | |
| 348 | if amount > 0 { | |
| 349 | self.db | |
| 350 | .prepare("UPDATE allowance_use SET used = MAX(0, used - ?4) WHERE kind = ?1 AND scope = ?2 AND month = ?3") | |
| 351 | .bind(&[kind.into(), scope.into(), month.into(), (amount as f64).into()])? | |
| 352 | .run() | |
| 353 | .await?; | |
| 354 | } | |
| 355 | Ok(()) | |
| 356 | } | |
| 357 | ||
| 358 | // --- Trials ----------------------------------------------------------- | |
| 359 | ||
| 360 | pub(crate) async fn grant_of(&self, workspace: &str) -> Result<Option<Grant>> { | |
| 361 | self.db | |
| 362 | .prepare("SELECT granted_micros, used_micros FROM trial_grants WHERE workspace = ?") | |
| 363 | .bind(&[workspace.into()])? | |
| 364 | .first::<Grant>(None) | |
| 365 | .await | |
| 366 | } | |
| 367 | ||
| 368 | /// Trial grants made this month, in all. | |
| 369 | pub(crate) async fn trial_granted(&self, month: &str) -> Result<(i64, u32)> { | |
| 370 | #[derive(Deserialize)] | |
| 371 | struct Row { | |
| 372 | micros: Option<i64>, | |
| 373 | n: Option<u32>, | |
| 374 | } | |
| 375 | let row = self | |
| 376 | .db | |
| 377 | .prepare("SELECT SUM(granted_micros) AS micros, COUNT(*) AS n FROM trial_grants WHERE month = ?") | |
| 378 | .bind(&[month.into()])? | |
| 379 | .first::<Row>(None) | |
| 380 | .await?; | |
| 381 | Ok(row.map_or((0, 0), |r| (r.micros.unwrap_or(0), r.n.unwrap_or(0)))) | |
| 382 | } | |
| 383 | ||
| 384 | /// The workspace's grant, made now out of this month's pool if it has | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 385 | /// none and the pool has room. Called once its card is checked, never |
| 386 | /// before: the trial needs a card check. A grant g1t staff set comes | |
| 387 | /// from no pool. | |
| 388 | pub(crate) async fn ensure_grant(&self, workspace: &str) -> Result<Option<Grant>> { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 389 | if let Some(grant) = self.grant_of(workspace).await? { |
| 390 | return Ok(Some(grant)); | |
| 391 | } | |
| 392 | if !self.trials_on { | |
| 393 | return Ok(None); | |
| 394 | } | |
| 395 | let staff = self.account_of(workspace).await?.allowances.trial_micros; | |
| 396 | let amount = grant_size(&self.plans, staff); | |
| 397 | if amount <= 0 { | |
| 398 | return Ok(None); | |
| 399 | } | |
| 400 | let now = rfc3339(now_ms()); | |
| 401 | let month = if staff.is_some() { "staff".to_owned() } else { month_of(&now) }; | |
| 402 | // One statement: the pool is checked and the grant made together. | |
| 403 | self.db | |
| 404 | .prepare( | |
| 405 | "INSERT INTO trial_grants (workspace, month, granted_micros, used_micros, created_at) | |
| 406 | SELECT ?1, ?2, ?3, 0, ?4 | |
| 407 | WHERE ?2 = 'staff' | |
| 408 | OR (SELECT COALESCE(SUM(granted_micros), 0) FROM trial_grants WHERE month = ?2) + ?3 <= ?5 | |
| 409 | ON CONFLICT (workspace) DO NOTHING", | |
| 410 | ) | |
| 411 | .bind(&[ | |
| 412 | workspace.into(), | |
| 413 | month.as_str().into(), | |
| 414 | (amount as f64).into(), | |
| 415 | now.as_str().into(), | |
| 416 | (self.plans.trial_monthly_pool_micros as f64).into(), | |
| 417 | ])? | |
| 418 | .run() | |
| 419 | .await?; | |
| 420 | self.grant_of(workspace).await | |
| 421 | } | |
| 422 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 423 | /// Takes up to `want` from the workspace's trial credit, if it has a |
| 424 | /// grant. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 425 | async fn draw_trial(&self, workspace: &str, want: i64) -> Result<i64> { |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 426 | if want <= 0 || self.grant_of(workspace).await?.is_none() { |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 427 | return Ok(0); |
| 428 | } | |
| 429 | #[derive(Deserialize)] | |
| 430 | struct Row { | |
| 431 | used_micros: i64, | |
| 432 | } | |
| 433 | let results = self | |
| 434 | .db | |
| 435 | .batch(vec![ | |
| 436 | self.db.prepare("SELECT used_micros FROM trial_grants WHERE workspace = ?1").bind(&[workspace.into()])?, | |
| 437 | self.db | |
| 438 | .prepare( | |
| 439 | "UPDATE trial_grants SET used_micros = MIN(granted_micros, used_micros + ?2) | |
| 440 | WHERE workspace = ?1 AND used_micros < granted_micros", | |
| 441 | ) | |
| 442 | .bind(&[workspace.into(), (want as f64).into()])?, | |
| 443 | self.db.prepare("SELECT used_micros FROM trial_grants WHERE workspace = ?1").bind(&[workspace.into()])?, | |
| 444 | ]) | |
| 445 | .await?; | |
| 446 | let read = |i: usize| -> Result<i64> { Ok(results[i].results::<Row>()?.first().map_or(0, |r| r.used_micros)) }; | |
| 447 | Ok((read(2)? - read(0)?).max(0)) | |
| 448 | } | |
| 449 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 450 | /// `trial`: where the workspace's trial credit stands. Not granted yet, |
| 451 | /// it waits for a card check (`verify`), or for next month's pool | |
| 452 | /// (`pool`). | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 453 | pub(crate) async fn trial(&self, a: TrialArgs) -> Result<Trial> { |
| 454 | let workspace = a.workspace.to_lowercase(); | |
| 455 | let closed = |reason: &str| Trial { | |
| 456 | open: false, | |
| 457 | used_micros: 0, | |
| 458 | limit_micros: 0, | |
| 459 | ends_at: None, | |
| 460 | reason: Some(reason.to_owned()), | |
| 461 | granted: false, | |
| 462 | waits_until: None, | |
| 463 | }; | |
| 464 | if let Some(grant) = self.grant_of(&workspace).await? { | |
| 465 | let open = grant.used_micros < grant.granted_micros; | |
| 466 | return Ok(Trial { | |
| 467 | open, | |
| 468 | used_micros: grant.used_micros, | |
| 469 | limit_micros: grant.granted_micros, | |
| 470 | ends_at: None, | |
| 471 | reason: (!open).then(|| "used".to_owned()), | |
| 472 | granted: true, | |
| 473 | waits_until: None, | |
| 474 | }); | |
| 475 | } | |
| 476 | if !self.trials_on { | |
| 477 | return Ok(closed("off")); | |
| 478 | } | |
| 479 | let staff = self.account_of(&workspace).await?.allowances.trial_micros; | |
| 480 | let amount = grant_size(&self.plans, staff); | |
| 481 | if amount <= 0 { | |
| 482 | return Ok(closed("off")); | |
| 483 | } | |
| 484 | let month = month_of(&rfc3339(now_ms())); | |
| 485 | let (granted, _) = self.trial_granted(&month).await?; | |
| 486 | let room = staff.is_some() || pool_has_room(self.plans.trial_monthly_pool_micros, granted, amount); | |
| 487 | Ok(Trial { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 488 | open: false, |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 489 | used_micros: 0, |
| 490 | limit_micros: amount, | |
| 491 | ends_at: None, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 492 | reason: Some(if room { "verify" } else { "pool" }.to_owned()), |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 493 | granted: false, |
| 494 | waits_until: (!room).then(|| next_month_start(&month)), | |
| 495 | }) | |
| 496 | } | |
| 497 | ||
| 498 | // --- The open-source pool --------------------------------------------- | |
| 499 | ||
| 500 | /// Whether `repo` (`owner/name`) is public, asked of the repos service. | |
| 501 | /// Unknown counts as private: the pool pays only for what is known to | |
| 502 | /// be open. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 503 | pub(crate) async fn is_public(&self, repo: &str) -> bool { |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 504 | let Some(repos) = &self.repos else { return false }; |
| 505 | let found: Result<Vec<g1t_contracts::repos::RepoVisibility>> = g1t_kit::call( | |
| 506 | repos, | |
| 507 | "visibility", | |
| 508 | &g1t_contracts::repos::VisibilityArgs { paths: vec![repo.to_owned()] }, | |
| 509 | ) | |
| 510 | .await; | |
| 511 | match found { | |
| 512 | Ok(list) => list.iter().any(|v| v.path.eq_ignore_ascii_case(repo) && !v.is_private), | |
| 513 | Err(error) => { | |
| 514 | worker::console_error!("could not ask whether {repo} is public: {error}"); | |
| 515 | false | |
| 516 | } | |
| 517 | } | |
| 518 | } | |
| 519 | ||
| 520 | /// A public repository's monthly cap on the pool: its account's own | |
| 521 | /// from sudo, or `OSS_REPO_MICROS`. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 522 | pub(crate) async fn oss_repo_cap(&self, workspace: &str) -> Result<i64> { |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 523 | Ok(self.account_of(workspace).await?.allowances.oss_repo_micros.unwrap_or(self.plans.oss_repo_micros)) |
| 524 | } | |
| 525 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 526 | /// What the open-source pool has left this month for `repo`: the |
| 527 | /// pool's and the repository's share, whichever is less. | |
| 528 | pub(crate) async fn oss_left(&self, workspace: &str, repo: &str, month: &str) -> Result<i64> { | |
| 529 | let pool = left(self.plans.oss_pool_micros, self.allowance_used("oss_pool", "", month).await?); | |
| 530 | let share = left(self.oss_repo_cap(workspace).await?, self.allowance_used("oss_repo", &repo.to_lowercase(), month).await?); | |
| 531 | Ok(pool.min(share)) | |
| 532 | } | |
| 533 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 534 | /// Takes up to `want` from the open-source pool for `repo`, within the |
| 535 | /// pool's cap and the repository's. | |
| 536 | async fn draw_oss(&self, workspace: &str, repo: &str, month: &str, want: i64) -> Result<i64> { | |
| 537 | let repo = repo.to_lowercase(); | |
| 538 | let cap = self.oss_repo_cap(workspace).await?; | |
| 539 | let room = left(cap, self.allowance_used("oss_repo", &repo, month).await?); | |
| 540 | let from_pool = self.draw_allowance("oss_pool", "", month, want.min(room), self.plans.oss_pool_micros).await?; | |
| 541 | let for_repo = self.draw_allowance("oss_repo", &repo, month, from_pool, cap).await?; | |
| 542 | // The repository's cap filled up meanwhile: give the pool back the rest. | |
| 543 | self.return_allowance("oss_pool", "", month, from_pool - for_repo).await?; | |
| 544 | Ok(for_repo) | |
| 545 | } | |
| 546 | ||
| 547 | // --- Drawing down ----------------------------------------------------- | |
| 548 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 549 | /// Pays for a `gross` charge from the plan's included usage, the trial |
| 550 | /// credit and the open-source pool, in that order, for usage in | |
| 551 | /// `month`; then, for a free workspace's compute, g1t covers the rest. | |
| 552 | /// Returns what each paid; the rest is the workspace's to pay. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 553 | pub(crate) async fn draw(&self, workspace: &str, gross: i64, month: &str, eligible: &Eligible) -> Result<Drawn> { |
| 554 | if gross <= 0 { | |
| 555 | return Ok(Drawn::default()); | |
| 556 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 557 | let plan = self.has_plan(workspace).await?; |
| 558 | let credit_left = if plan { | |
| 559 | left(self.plans.plan_included_micros, self.allowance_used("plan_credit", workspace, month).await?) | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 560 | } else { |
| 561 | 0 | |
| 562 | }; | |
| 563 | let trial_left = if eligible.trial { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 564 | self.grant_of(workspace).await?.map_or(0, |grant| left(grant.granted_micros, grant.used_micros)) |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 565 | } else { |
| 566 | 0 | |
| 567 | }; | |
| 568 | // Asked only when the rest has not paid for it all. | |
| 569 | let public_repo = match &eligible.repo { | |
| 570 | Some(repo) if gross > credit_left + trial_left && self.is_public(repo).await => Some(repo.clone()), | |
| 571 | _ => None, | |
| 572 | }; | |
| 573 | let oss_left = match &public_repo { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 574 | Some(repo) => self.oss_left(workspace, repo, month).await?, |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 575 | None => 0, |
| 576 | }; | |
| 577 | let planned = split(gross, &[credit_left, trial_left, oss_left]); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 578 | let mut drawn = Drawn { |
| 579 | credit: self.draw_allowance("plan_credit", workspace, month, planned[0], self.plans.plan_included_micros).await?, | |
| 580 | trial: self.draw_trial(workspace, planned[1]).await?, | |
| 581 | ..Drawn::default() | |
| 582 | }; | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 583 | if let Some(repo) = &public_repo { |
| 584 | drawn.oss = self.draw_oss(workspace, repo, month, planned[2]).await?; | |
| 585 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 586 | if eligible.cover_rest && !plan { |
| 587 | drawn.given = (gross - drawn.credit - drawn.trial - drawn.oss).max(0); | |
| 588 | } | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 589 | Ok(drawn) |
| 590 | } | |
| 591 | ||
| 592 | /// Writes down on a usage entry what paid for it. | |
| 593 | pub(crate) async fn record_drawn(&self, reference: &str, drawn: &Drawn) -> Result<()> { | |
| 594 | if drawn.total() == 0 { | |
| 595 | return Ok(()); | |
| 596 | } | |
| 597 | self.db | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 598 | .prepare("UPDATE ledger SET credit_micros = ?, trial_micros = ?, oss_micros = ?, given_micros = ? WHERE reference = ?") |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 599 | .bind(&[ |
| 600 | (drawn.credit as f64).into(), | |
| 601 | (drawn.trial as f64).into(), | |
| 602 | (drawn.oss as f64).into(), | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 603 | (drawn.given as f64).into(), |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 604 | reference.into(), |
| 605 | ])? | |
| 606 | .run() | |
| 607 | .await?; | |
| 608 | Ok(()) | |
| 609 | } | |
| 610 | ||
| 611 | /// g1t's pools this month, for sudo. | |
| 612 | pub(crate) async fn pools(&self) -> Result<Pools> { | |
| 613 | let month = month_of(&rfc3339(now_ms())); | |
| 614 | let (granted, grants) = self.trial_granted(&month).await?; | |
| 615 | Ok(Pools { | |
| 616 | oss_used_micros: self.allowance_used("oss_pool", "", &month).await?, | |
| 617 | oss_pool_micros: self.plans.oss_pool_micros, | |
| 618 | oss_repo_micros: self.plans.oss_repo_micros, | |
| 619 | trial_granted_micros: granted, | |
| 620 | trial_pool_micros: self.plans.trial_monthly_pool_micros, | |
| 621 | trial_grants: grants, | |
| 622 | month, | |
| 623 | }) | |
| 624 | } | |
| 625 | } | |
| 626 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 627 | /// What may pay for compute: the trial (never for deployments), the |
| 628 | /// open-source pool for checks, workflows and the merge queue on `repo`, | |
| 629 | /// and g1t for a free workspace's overrun. Work whose kind is not known is | |
| 630 | /// taken as an agent's: never the pool. | |
| 631 | pub(crate) fn eligible_for(kind: Option<ComputeKind>, repo: Option<&str>) -> Eligible { | |
| 632 | let kind = kind.unwrap_or(ComputeKind::Agent); | |
| 633 | Eligible { | |
| 634 | trial: kind != ComputeKind::Deploy, | |
| 635 | repo: repo.filter(|_| kind.open_source_pool()).map(str::to_owned), | |
| 636 | cover_rest: kind != ComputeKind::Deploy, | |
| 637 | } | |
| 638 | } | |
| 639 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 640 | /// A charge in millionths of a dollar for `micros` of cost plus `margin`. |
| 641 | pub(crate) fn with_margin(cost_micros: i64, margin_percent: u32) -> i64 { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 642 | crate::charge_micros(cost_micros.max(0) as f64 / g1t_contracts::billing::MICROS_PER_DOLLAR as f64, margin_percent) |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 643 | } |
| 644 | ||
| 645 | #[cfg(test)] | |
| 646 | mod tests { | |
| 647 | use super::*; | |
| 648 | ||
| 649 | #[test] | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 650 | fn included_usage_pays_first_then_the_trial_then_the_pool_then_the_workspace() { |
| 651 | // $0.50 of usage; $0.20 included, $1 of trial, $1 of pool. | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 652 | assert_eq!(split(500_000, &[200_000, 1_000_000, 1_000_000]), [200_000, 300_000, 0]); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 653 | // No plan: the trial pays all of it. |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 654 | assert_eq!(split(500_000, &[0, 1_000_000, 1_000_000]), [0, 500_000, 0]); |
| 655 | // Trial spent: the pool pays, where it applies. | |
| 656 | assert_eq!(split(500_000, &[0, 0, 1_000_000]), [0, 0, 500_000]); | |
| 657 | // Everything spent: the workspace pays all of it. | |
| 658 | let planned = split(500_000, &[0, 0, 0]); | |
| 659 | assert_eq!(planned, [0, 0, 0]); | |
| 660 | assert_eq!(500_000 - planned.iter().sum::<i64>(), 500_000); | |
| 661 | // Each pays what it can, and the rest is charged. | |
| 662 | let planned = split(500_000, &[100_000, 150_000, 50_000]); | |
| 663 | assert_eq!(planned, [100_000, 150_000, 50_000]); | |
| 664 | assert_eq!(500_000 - planned.iter().sum::<i64>(), 200_000); | |
| 665 | // Nothing is drawn for nothing, nor from a negative balance. | |
| 666 | assert_eq!(split(0, &[1, 1, 1]), [0, 0, 0]); | |
| 667 | assert_eq!(split(100, &[-5, 50, 100]), [0, 50, 50]); | |
| 668 | } | |
| 669 | ||
| 670 | #[test] | |
| 671 | fn a_budget_never_gives_more_than_its_cap() { | |
| 672 | assert_eq!(left(1_000_000, 400_000), 600_000); | |
| 673 | assert_eq!(left(1_000_000, 1_000_000), 0); | |
| 674 | assert_eq!(left(1_000_000, 1_200_000), 0); | |
| 675 | // The open-source pool: the repository's share and the pool's both bound it. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 676 | let pool = left(25_000_000, 24_900_000); |
| 677 | let repo = left(2_000_000, 300_000); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 678 | assert_eq!(split(800_000, &[pool.min(repo)]), [100_000]); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 679 | // A repository past its $2 share gets nothing, however full the pool. |
| 680 | assert_eq!(split(800_000, &[left(25_000_000, 0).min(left(2_000_000, 2_000_000))]), [0]); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 681 | } |
| 682 | ||
| 683 | #[test] | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 684 | fn the_open_source_pool_pays_only_for_checks_workflows_and_the_queue() { |
| 685 | assert_eq!(eligible_for(Some(ComputeKind::Check), Some("acme/web")).repo.as_deref(), Some("acme/web")); | |
| 686 | assert_eq!(eligible_for(Some(ComputeKind::Queue), Some("acme/web")).repo.as_deref(), Some("acme/web")); | |
| 687 | assert_eq!(eligible_for(Some(ComputeKind::Workflow), Some("acme/web")).repo.as_deref(), Some("acme/web")); | |
| 688 | // An agent on a public repository pays as any agent does. | |
| 689 | assert!(eligible_for(Some(ComputeKind::Agent), Some("acme/web")).repo.is_none()); | |
| 690 | // Unknown work is never the pool's. | |
| 691 | assert!(eligible_for(None, Some("acme/web")).repo.is_none()); | |
| 692 | // Deployments are never the trial's, and never covered. | |
| 693 | let deploy = eligible_for(Some(ComputeKind::Deploy), Some("acme/web")); | |
| 694 | assert!(!deploy.trial && !deploy.cover_rest && deploy.repo.is_none()); | |
| 695 | assert!(eligible_for(Some(ComputeKind::Agent), None).trial); | |
| 696 | } | |
| 697 | ||
| 698 | #[test] | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 699 | fn pools_reset_each_calendar_month() { |
| 700 | assert_eq!(month_of("2026-10-31T23:59:59Z"), "2026-10"); | |
| 701 | assert_eq!(month_of("2026-11-01T00:00:00Z"), "2026-11"); | |
| 702 | assert_eq!(next_month_start("2026-10"), "2026-11-01T00:00:00Z"); | |
| 703 | assert_eq!(next_month_start("2026-12"), "2027-01-01T00:00:00Z"); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 704 | // $100 a month in $5 grants: twenty trials, then the next month. |
| 705 | assert!(pool_has_room(100_000_000, 95_000_000, 5_000_000)); | |
| 706 | assert!(!pool_has_room(100_000_000, 100_000_000, 5_000_000)); | |
| 707 | assert!(!pool_has_room(100_000_000, 97_500_000, 5_000_000)); | |
| 708 | assert!(pool_has_room(100_000_000, 0, 5_000_000)); | |
| 709 | assert!(!pool_has_room(100_000_000, 0, 0)); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 710 | } |
| 711 | ||
| 712 | #[test] | |
| 713 | fn a_trial_grant_is_the_default_unless_staff_set_one() { | |
| 714 | let config = Config::default(); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 715 | assert_eq!(grant_size(&config, None), 5_000_000); |
| 716 | assert_eq!(grant_size(&config, Some(20_000_000)), 20_000_000); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 717 | assert_eq!(grant_size(&config, Some(-1)), 0); |
| 718 | } | |
| 719 | ||
| 720 | #[test] | |
| 721 | fn a_month_ends_on_its_last_day() { | |
| 722 | assert_eq!(month_end("2026-10"), "2026-10-31T23:59:59Z"); | |
| 723 | assert_eq!(month_end("2026-09"), "2026-09-30T23:59:59Z"); | |
| 724 | assert_eq!(month_end("2028-02"), "2028-02-29T23:59:59Z"); | |
| 725 | assert_eq!(month_end("2027-02"), "2027-02-28T23:59:59Z"); | |
| 726 | } | |
| 727 | ||
| 728 | #[test] | |
| 729 | fn what_paid_is_said_on_the_statement() { | |
| 730 | assert_eq!(Drawn::default().note(), ""); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 731 | let drawn = Drawn { oss: 120_000, ..Drawn::default() }; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 732 | assert_eq!(drawn.note(), " ($0.12 paid by g1t's open-source pool)"); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 733 | let drawn = Drawn { credit: 50_000, trial: 20_000, ..Drawn::default() }; |
| 734 | assert_eq!(drawn.note(), " ($0.05 paid by your plan's included usage, $0.02 paid by your trial credit)"); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 735 | assert_eq!(drawn.total(), 70_000); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 736 | let drawn = Drawn { trial: 300_000, given: 40_000, ..Drawn::default() }; |
| 737 | assert_eq!(drawn.note(), " ($0.30 paid by your trial credit, $0.04 covered by g1t)"); | |
| 738 | assert_eq!(drawn.total(), 340_000); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 739 | } |
| 740 | ||
| 741 | #[test] | |
| 742 | fn the_defaults_are_the_published_ones() { | |
| 743 | let c = Config::default(); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 744 | assert_eq!(c.plan_monthly_cents, 2_000); |
| 745 | assert_eq!(c.plan_included_micros, 10_000_000); | |
| 746 | assert_eq!(c.oss_pool_micros, 25_000_000); | |
| 747 | assert_eq!(c.oss_repo_micros, 2_000_000); | |
| 748 | assert_eq!(c.trial_workspace_micros, 5_000_000); | |
| 749 | assert_eq!(c.trial_monthly_pool_micros, 100_000_000); | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 750 | assert_eq!(c.min_charge_micros, 5_000_000); |
| 751 | assert_eq!(c.free_storage_bytes, 1_000_000_000); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 752 | assert_eq!(c.audit_days, 90); |
| 753 | assert_eq!(c.run_cap_micros, 2_000_000); | |
| 754 | assert_eq!(c.issue_cap_micros, 10_000_000); | |
| 755 | assert_eq!(c.paid_start_micros, 100_000_000); | |
| 756 | assert_eq!(c.forgive_cost_micros, 50_000_000); | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 757 | assert_eq!(c.git_included, 50_000); |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 758 | } |
| 759 | } |