Skip to content

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

1,497 linesCodeBlameRaw
1//! Complimentary and custom terms: g1t staff enroll a workspace by hand in
2//! sudo (`admin_set_complimentary`); nothing enrolls a workspace on its own,
3//! and new workspaces stay free.
4//!
5//! **What it gives.** The plan's features; a monthly usage allowance valued
6//! at ordinary usage prices (not at cost, and not a discount); and a
7//! recurring monthly fee of its own, often waived. The fee and the
8//! allowance are separate amounts. Calendar months in UTC, no rollover.
9//!
10//! **Where it stops.** Past the allowance new billable work is refused
11//! (`reserve`, and `has_plan` false for storage and caches). There is never
12//! an overage, a saved card or not. Work already running when the allowance
13//! ran out finishes; what it cost past the allowance is g1t's
14//! (`ledger.given_micros`), recorded apart from the allowance
15//! (`ledger.gift_micros`) and never charged afterwards. Admission counts the
16//! month's used allowance, usage metered but not on the ledger yet (storage,
17//! git operations) and what running work holds.
18//!
19//! **Periods.** Terms are kept as periods (`complimentary_periods`). A usage
20//! line draws on the period covering its own time (`allocate_usage.sql`), so
21//! late usage lands where it happened. Changing the amount closes one period
22//! and opens the next; consumption is per month, so it is kept, and an amount
23//! lowered below it leaves nothing more this month, never a debt.
24//!
25//! **Stripe.** A real customer and subscription stand for the enrollment,
26//! with the grant's identity and terms in its metadata. The workspace sees
27//! the enrollment only once Stripe has the subscription: until then it is
28//! `pending` and keeps its plan. Each Stripe step is written down first
29//! (`pending_op`) with an idempotency key made from the grant, so a retry
30//! after a crash or a timeout repeats the same request; one older than
31//! Stripe keeps keys for is reconciled by looking for the grant at Stripe
32//! instead, never sent again under a new key. The fifteen-minute tick picks
33//! up any step still owed. A waived fee is a $0 price on the plan's
34//! product; g1t's own unlimited enrollment (flagon-io) is the plan's price
35//! with a 100% coupon, a fully discounted real subscription.
36//!
37//! The allowance is never a ledger top-up: it is not money and not credit
38//! on the balance. Money received (plan and fee invoices, payments in
39//! advance, bank transfers) and promotional credit are shown apart.
40//!
41//! **Converting.** An owner can start the plan on the same customer and
42//! subscription (`convert_to_paid`): its one item becomes the plan's price
43//! with `payment_behavior=pending_if_incomplete`, so Stripe changes nothing
44//! until the first payment succeeds. A decline, a bank asking for the person
45//! (3DS), an abandoned page or a second click leave the enrollment as it
46//! was, with one subscription. Only once Stripe shows the plan's price on
47//! the item does the enrollment end (`converted`). Usage up to that moment
48//! stays as the allowance paid for it.
49
50use g1t_contracts::billing::{
51 AdminComplimentaryArgs, AdminEndComplimentaryArgs, AdminSetComplimentaryArgs, CardCheckArgs, ComplimentaryMonth, ComplimentaryPeriod,
52 ComplimentaryStanding, Conversion, ConvertToPaidArgs, Feature, UsageAlert, MAX_COMPLIMENTARY_FEE_CENTS, MAX_COMPLIMENTARY_MICROS,
53};
54use g1t_contracts::time::rfc3339;
55use g1t_contracts::{FailureCode, Outcome, new_id};
56use g1t_kit::now_ms;
57use serde::{Deserialize, Serialize};
58use worker::Result;
59
60use crate::Billing;
61use crate::features::{cents, dollars};
62use crate::stripe::{Stripe, StripeSubscription, is_missing};
63
64/// Stripe keeps an idempotency key for 24 hours. An operation older than
65/// this is reconciled by looking for what it made, never sent again.
66const KEY_KEPT_MS: u64 = 23 * 60 * 60 * 1000;
67/// The coupon behind a fully discounted enrollment (g1t's own).
68pub(crate) const FULL_DISCOUNT_COUPON: &str = "g1t_complimentary_100";
69/// Pending Stripe steps tried each tick.
70const TICK_BATCH: u32 = 10;
71
72#[derive(Clone, Debug, Deserialize)]
73pub(crate) struct Row {
74 pub workspace: String,
75 pub grant_id: String,
76 pub status: String,
77 pub allowance_micros: i64,
78 pub unlimited: i64,
79 pub fee_cents: i64,
80 /// `stop` or `charge`: what happens past the allowance.
81 pub overage: String,
82 pub live: i64,
83 pub customer_id: Option<String>,
84 pub subscription_id: Option<String>,
85 pub item_id: Option<String>,
86 pub price_id: Option<String>,
87 pub stripe_status: Option<String>,
88 pub pending_op: Option<String>,
89 pub last_error: Option<String>,
90 pub note: String,
91 pub created_by: String,
92 pub created_at: String,
93 pub created_ms: i64,
94 pub updated_by: String,
95 pub updated_at: String,
96 pub ended_at: Option<String>,
97 pub ended_reason: Option<String>,
98}
99
100impl Row {
101 fn unlimited(&self) -> bool {
102 self.unlimited != 0
103 }
104
105 /// Past the allowance, the workspace's own card is charged.
106 pub(crate) fn charges_past(&self) -> bool {
107 self.overage == "charge"
108 }
109}
110
111/// Fully discounted: the plan's price under the 100% coupon. Only g1t's own
112/// workspaces (a 100% discount in their terms) with an unlimited allowance
113/// and no fee are; any other enrollment is its own price.
114pub(crate) fn fully_discounted(row: &Row, internal: bool) -> bool {
115 internal && row.unlimited() && row.fee_cents == 0
116}
117
118/// A Stripe step still owed.
119#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
120#[serde(tag = "op", rename_all = "snake_case")]
121pub(crate) enum Op {
122 /// Make the customer (if needed) and the subscription.
123 Create,
124 /// Bring the subscription's price and metadata to the open period's.
125 Terms { period: String },
126 /// Cancel the subscription now.
127 Cancel,
128 /// The owner's conversion to the plan, attempt `attempt` started at
129 /// `started_ms` (both in its idempotency keys, so a refused attempt is
130 /// never replayed by the next).
131 Convert { attempt: u32, by: String, started_ms: u64 },
132}
133
134impl Op {
135 pub(crate) fn parse(text: Option<&str>) -> Option<Op> {
136 serde_json::from_str(text?).ok()
137 }
138
139 fn text(&self) -> String {
140 serde_json::to_string(self).unwrap_or_default()
141 }
142}
143
144/// Why staff's terms cannot be saved, or None.
145pub(crate) fn invalid(a: &AdminSetComplimentaryArgs) -> Option<String> {
146 if a.workspace.trim().is_empty() {
147 return Some("Name the workspace.".into());
148 }
149 if a.note.trim().len() < 3 {
150 return Some("Say why, in a few words: it goes in the audit log.".into());
151 }
152 if a.by.trim().is_empty() {
153 return Some("Who is making the change is missing.".into());
154 }
155 if !(0..=MAX_COMPLIMENTARY_MICROS).contains(&a.allowance_micros) {
156 return Some(format!("The monthly allowance is between $0 and {}; for more, make it unlimited.", cents(MAX_COMPLIMENTARY_MICROS)));
157 }
158 if a.fee_cents > MAX_COMPLIMENTARY_FEE_CENTS {
159 return Some(format!("The monthly fee is at most {}.", cents(i64::from(MAX_COMPLIMENTARY_FEE_CENTS) * 10_000)));
160 }
161 if !a.unlimited && a.allowance_micros == 0 && a.fee_cents == 0 {
162 return Some("An enrollment with no allowance and no fee gives nothing: set an allowance, or end the enrollment.".into());
163 }
164 None
165}
166
167/// A month's standing from its figures. `remaining` is what new work may
168/// still hold: what is left after used, pending and held usage.
169pub(crate) fn month_standing(month: &str, row: &Row, used: i64, pending: i64, held: i64, overrun: i64, has_card: bool) -> ComplimentaryMonth {
170 let remaining = if row.unlimited() { crate::credits::left(g1t_contracts::billing::UNLIMITED_MICROS, used) } else {
171 crate::credits::left(row.allowance_micros, used + pending.max(0) + held.max(0))
172 };
173 ComplimentaryMonth {
174 month: month.to_owned(),
175 allowance_micros: row.allowance_micros,
176 unlimited: row.unlimited(),
177 used_micros: used,
178 pending_micros: pending.max(0),
179 held_micros: held.max(0),
180 remaining_micros: remaining,
181 overrun_micros: overrun,
182 resets_at: crate::credits::next_month_start(month),
183 fee_cents: row.fee_cents.max(0) as u32,
184 charge_past_allowance: row.charges_past(),
185 has_card,
186 }
187}
188
189/// The subscription's metadata: the grant and its terms, so Stripe shows
190/// what the $0 (or custom) price stands for.
191fn metadata_fields(row: &Row) -> Vec<(&'static str, String)> {
192 vec![
193 ("metadata[workspace]", row.workspace.clone()),
194 ("metadata[feature]", "complimentary".to_owned()),
195 ("metadata[g1t_grant]", row.grant_id.clone()),
196 ("metadata[g1t_allowance_micros]", row.allowance_micros.to_string()),
197 ("metadata[g1t_unlimited]", row.unlimited().to_string()),
198 ("metadata[g1t_fee_cents]", row.fee_cents.to_string()),
199 ("metadata[g1t_past_allowance]", row.overage.clone()),
200 ]
201}
202
203/// The one item's price: the fee (a $0 price when waived), or the plan's
204/// price under the 100% coupon for a fully discounted enrollment.
205fn price_fields(row: &Row, product: &str, plan_cents: u32, prefix: &'static str, fully: bool) -> Vec<(String, String)> {
206 let amount = if fully { i64::from(plan_cents) } else { row.fee_cents.max(0) };
207 vec![
208 (format!("{prefix}[price_data][currency]"), "usd".to_owned()),
209 (format!("{prefix}[price_data][product]"), product.to_owned()),
210 (format!("{prefix}[price_data][unit_amount]"), amount.to_string()),
211 (format!("{prefix}[price_data][recurring][interval]"), "month".to_owned()),
212 (format!("{prefix}[price_data][tax_behavior]"), "exclusive".to_owned()),
213 ]
214}
215
216/// A new enrollment's subscription. A fee is invoiced (sent, payable for 30
217/// days): negotiated terms are not charged to a card nobody gave. A waived
218/// fee has nothing to tax or collect; converting turns both on.
219pub(crate) fn create_fields(row: &Row, customer: &str, product: &str, plan_cents: u32, fully: bool) -> Vec<(String, String)> {
220 let mut fields: Vec<(String, String)> = vec![("customer".into(), customer.to_owned())];
221 fields.extend(price_fields(row, product, plan_cents, "items[0]", fully));
222 fields.extend(metadata_fields(row).into_iter().map(|(k, v)| (k.to_owned(), v)));
223 fields.push(("description".into(), format!("Complimentary g1t plan for {}", row.workspace)));
224 if fully {
225 fields.push(("discounts[0][coupon]".into(), FULL_DISCOUNT_COUPON.to_owned()));
226 }
227 if row.fee_cents > 0 {
228 fields.push(("collection_method".into(), "send_invoice".into()));
229 fields.push(("days_until_due".into(), "30".into()));
230 fields.push(("automatic_tax[enabled]".into(), "true".into()));
231 }
232 fields
233}
234
235/// Whether Stripe shows the conversion done: an owner's conversion is in
236/// progress (`converting`), nothing waits on a payment, and the item now has
237/// a new price at the plan's amount. A staff terms change also gives the
238/// item a new price, so the price alone never counts.
239pub(crate) fn converted(price_id: Option<&str>, subscription: &StripeSubscription, plan_cents: u32, converting: bool) -> bool {
240 let price = subscription.first_item().and_then(|item| item.price.as_ref());
241 converting
242 && subscription.pending_update.as_ref().is_none_or(serde_json::Value::is_null)
243 && matches!(subscription.status.as_str(), "active" | "trialing")
244 && price_id.is_some()
245 && price.is_some_and(|p| Some(p.id.as_str()) != price_id && p.unit_amount == Some(i64::from(plan_cents)))
246}
247
248fn owned(fields: &[(String, String)]) -> Vec<(&str, String)> {
249 fields.iter().map(|(k, v)| (k.as_str(), v.clone())).collect()
250}
251
252#[derive(Deserialize)]
253struct Sum {
254 micros: Option<i64>,
255}
256
257impl Billing {
258 pub(crate) async fn complimentary_row(&self, workspace: &str) -> Result<Option<Row>> {
259 self.db.prepare("SELECT * FROM complimentary WHERE workspace = ?").bind(&[workspace.into()])?.first::<Row>(None).await
260 }
261
262 /// The workspace's enrollment, when the workspace has it now.
263 pub(crate) async fn complimentary_active(&self, workspace: &str) -> Result<Option<Row>> {
264 Ok(self.complimentary_row(workspace).await?.filter(|row| row.status == "active"))
265 }
266
267 async fn sum(&self, sql: &str, binds: &[worker::wasm_bindgen::JsValue]) -> Result<i64> {
268 Ok(self.db.prepare(sql).bind(binds)?.first::<Sum>(None).await?.and_then(|s| s.micros).unwrap_or(0))
269 }
270
271 /// This month's standing of an active enrollment. `held` is what open
272 /// reservations hold, when the caller has read it.
273 pub(crate) async fn complimentary_month(&self, row: &Row, held: Option<i64>) -> Result<ComplimentaryMonth> {
274 let month = crate::credits::month_of(&rfc3339(now_ms()));
275 let used = self.allowance_used("complimentary", &row.workspace, &month).await?;
276 // Metered this month and not on the ledger yet: the allowance pays
277 // for it when the month closes.
278 let pending = self
279 .sum(
280 &format!(
281 "SELECT SUM({}) AS micros FROM pending_usage WHERE workspace = ?1 AND month = ?2 AND charged_at IS NULL",
282 crate::storage::NET_CHARGE
283 ),
284 &[row.workspace.as_str().into(), month.as_str().into()],
285 )
286 .await?;
287 let held = match held {
288 Some(held) => held,
289 None => self
290 .sum(
291 "SELECT SUM(hold_micros) AS micros FROM reservations WHERE workspace = ?1 AND settled_at IS NULL AND expires_at > ?2",
292 &[row.workspace.as_str().into(), rfc3339(now_ms()).into()],
293 )
294 .await?,
295 };
296 let overrun = self
297 .sum(
298 "SELECT SUM(given_micros) AS micros FROM ledger
299 WHERE workspace = ?1 AND kind = 'usage' AND created_at >= ?2 AND given_micros > 0 AND EXISTS (
300 SELECT 1 FROM complimentary_periods p WHERE p.workspace = ?1 AND p.starts_at <= ledger.created_at
301 AND (p.ends_at IS NULL OR p.ends_at > ledger.created_at))",
302 &[row.workspace.as_str().into(), format!("{month}-01").into()],
303 )
304 .await?;
305 let has_card = row.charges_past() && self.has_card(&row.workspace).await?;
306 Ok(month_standing(&month, row, used, pending, held, overrun, has_card))
307 }
308
309 /// Whether the workspace's own terms are a 100% discount: g1t's own.
310 async fn internal_terms(&self, workspace: &str) -> Result<bool> {
311 Ok(self.account_of(workspace).await?.terms.full_discount())
312 }
313
314 /// Whether the workspace has a card to charge: one saved by a card
315 /// check, or the customer's default card as last synced from Stripe.
316 pub(crate) async fn has_card(&self, workspace: &str) -> Result<bool> {
317 Ok(self
318 .db
319 .prepare(
320 "SELECT 1 AS found WHERE EXISTS (SELECT 1 FROM card_checks WHERE workspace = ?1)
321 OR EXISTS (SELECT 1 FROM accounts WHERE workspace = ?1 AND customer_id IS NOT NULL AND card_last4 IS NOT NULL)",
322 )
323 .bind(&[workspace.into()])?
324 .first::<serde_json::Value>(None)
325 .await?
326 .is_some())
327 }
328
329 async fn complimentary_standing(&self, row: Row) -> Result<ComplimentaryStanding> {
330 #[derive(Deserialize)]
331 struct PeriodRow {
332 id: String,
333 starts_at: String,
334 ends_at: Option<String>,
335 allowance_micros: i64,
336 unlimited: i64,
337 fee_cents: i64,
338 overage: String,
339 note: String,
340 created_by: String,
341 }
342 let periods = self
343 .db
344 .prepare("SELECT * FROM complimentary_periods WHERE workspace = ? ORDER BY starts_at DESC LIMIT 50")
345 .bind(&[row.workspace.as_str().into()])?
346 .all()
347 .await?
348 .results::<PeriodRow>()?
349 .into_iter()
350 .map(|p| ComplimentaryPeriod {
351 id: p.id,
352 starts_at: p.starts_at,
353 ends_at: p.ends_at,
354 allowance_micros: p.allowance_micros,
355 unlimited: p.unlimited != 0,
356 fee_cents: p.fee_cents.max(0) as u32,
357 charge_past_allowance: p.overage == "charge",
358 note: p.note,
359 created_by: p.created_by,
360 })
361 .collect();
362 let w: worker::wasm_bindgen::JsValue = row.workspace.as_str().into();
363 // Money in: plan and fee invoices, and top-ups that are payments
364 // (card prepayments, bank transfers), never credit from g1t.
365 let plans = self.sum("SELECT SUM(amount_micros) AS micros FROM plan_payments WHERE workspace = ?", &[w.clone()]).await?;
366 let paid = self
367 .sum(
368 "SELECT SUM(amount_micros) AS micros FROM ledger WHERE workspace = ? AND kind = 'top_up' AND credit_kind IS NULL AND reference NOT LIKE 'crd%'",
369 &[w.clone()],
370 )
371 .await?;
372 let promotional = self
373 .sum(
374 "SELECT SUM(amount_micros) AS micros FROM ledger WHERE workspace = ? AND kind = 'top_up' AND credit_kind IN ('promotional', 'goodwill')",
375 &[w.clone()],
376 )
377 .await?;
378 let gifted = self.sum("SELECT SUM(gift_micros) AS micros FROM ledger WHERE workspace = ? AND kind = 'usage'", &[w]).await?;
379 let month = if row.status == "active" { Some(self.complimentary_month(&row, None).await?) } else { None };
380 Ok(ComplimentaryStanding {
381 workspace: row.workspace.clone(),
382 grant_id: row.grant_id.clone(),
383 status: row.status.clone(),
384 allowance_micros: row.allowance_micros,
385 unlimited: row.unlimited(),
386 fee_cents: row.fee_cents.max(0) as u32,
387 charge_past_allowance: row.charges_past(),
388 live: row.live != 0,
389 customer_id: row.customer_id.clone(),
390 subscription_id: row.subscription_id.clone(),
391 stripe_status: row.stripe_status.clone(),
392 pending_op: Op::parse(row.pending_op.as_deref()).map(|op| match op {
393 Op::Create => "create".to_owned(),
394 Op::Terms { .. } => "terms".to_owned(),
395 Op::Cancel => "cancel".to_owned(),
396 Op::Convert { .. } => "convert".to_owned(),
397 }),
398 last_error: row.last_error.clone(),
399 note: row.note.clone(),
400 created_by: row.created_by.clone(),
401 created_at: row.created_at.clone(),
402 updated_by: row.updated_by.clone(),
403 updated_at: row.updated_at.clone(),
404 ended_at: row.ended_at.clone(),
405 ended_reason: row.ended_reason.clone(),
406 month,
407 periods,
408 money_received_micros: plans + paid,
409 promotional_credit_micros: promotional,
410 gifted_micros: gifted,
411 })
412 }
413
414 /// `admin_complimentary`.
415 pub(crate) async fn admin_complimentary(&self, a: AdminComplimentaryArgs) -> Result<Option<ComplimentaryStanding>> {
416 match self.complimentary_row(&a.workspace.trim().to_lowercase()).await? {
417 Some(row) => Ok(Some(self.complimentary_standing(row).await?)),
418 None => Ok(None),
419 }
420 }
421
422 /// `admin_set_complimentary`: enrolls, or changes the terms from now on.
423 pub(crate) async fn admin_set_complimentary(&self, a: AdminSetComplimentaryArgs) -> Result<Outcome<ComplimentaryStanding>> {
424 if let Some(why) = invalid(&a) {
425 return Ok(Outcome::fail(FailureCode::Invalid, why));
426 }
427 let Some(stripe) = &self.stripe else {
428 return Ok(Outcome::fail(FailureCode::Conflict, "Payments are not set up on this g1t: an enrollment needs a Stripe subscription."));
429 };
430 let workspace = a.workspace.trim().to_lowercase();
431 let by = a.by.trim();
432 let note = a.note.trim();
433 let account = self.account_of(&workspace).await?;
434 if account.kind == g1t_contracts::billing::AccountKind::Enterprise {
435 return Ok(Outcome::fail(FailureCode::Conflict, "An enterprise pays for this workspace. Detach it from the enterprise first."));
436 }
437 // Never two billable subscriptions: a paid plan ends (or the owner
438 // keeps it) before an enrollment starts.
439 let paying = self
440 .db
441 .prepare("SELECT subscription_id FROM subscriptions WHERE workspace = ? AND feature IN ('plan', 'deployments') AND status <> 'canceled' LIMIT 1")
442 .bind(&[workspace.as_str().into()])?
443 .first::<serde_json::Value>(None)
444 .await?;
445 if paying.is_some() {
446 return Ok(Outcome::fail(
447 FailureCode::Conflict,
448 "The workspace pays for the g1t plan. Cancel that subscription (or let it end) before enrolling it, so it is never billed twice.",
449 ));
450 }
451 // A plan's payment page still open would start a second subscription.
452 let checkout = self
453 .db
454 .prepare("SELECT id FROM checkouts WHERE workspace = ? AND feature IN ('plan', 'deployments') AND status = 'open' AND created_at > ? LIMIT 1")
455 .bind(&[workspace.as_str().into(), rfc3339(now_ms().saturating_sub(KEY_KEPT_MS + 60 * 60 * 1000)).into()])?
456 .first::<serde_json::Value>(None)
457 .await?;
458 if checkout.is_some() {
459 return Ok(Outcome::fail(
460 FailureCode::Conflict,
461 "An owner has a payment page open for the g1t plan. Wait for it to finish or expire (a day) before enrolling the workspace.",
462 ));
463 }
464 let now_ms = now_ms();
465 let now = rfc3339(now_ms);
466 let unlimited = i32::from(a.unlimited);
467 let fee = f64::from(a.fee_cents);
468 let overage = if a.charge_past_allowance { "charge" } else { "stop" };
469 let terms = format!(
470 "{} a month, fee {}, past it {}",
471 if a.unlimited { "unlimited allowance".to_owned() } else { format!("{} allowance", cents(a.allowance_micros)) },
472 if a.fee_cents == 0 { "waived".to_owned() } else { cents(i64::from(a.fee_cents) * 10_000) },
473 if a.charge_past_allowance { "charged to their card" } else { "work stops" }
474 );
475 let existing = self.complimentary_row(&workspace).await?;
476 match existing.as_ref().map(|row| row.status.as_str()) {
477 None | Some("ended") => {
478 // A canceled enrollment whose cancel is still owed at Stripe
479 // would be forgotten, still billing: finish it first.
480 if existing.as_ref().is_some_and(|row| row.pending_op.is_some()) {
481 return Ok(Outcome::fail(
482 FailureCode::Conflict,
483 "The last enrollment's subscription is still being canceled at Stripe; try again once it is (it is retried every fifteen minutes).",
484 ));
485 }
486 let grant = new_id("cmp", now_ms);
487 self.db
488 .prepare(
489 "INSERT INTO complimentary (workspace, grant_id, status, allowance_micros, unlimited, fee_cents, live, pending_op, note,
490 created_by, created_at, created_ms, updated_by, updated_at, overage)
491 VALUES (?1, ?2, 'pending', ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?9, ?10, ?12)
492 ON CONFLICT (workspace) DO UPDATE SET grant_id = ?2, status = 'pending', allowance_micros = ?3, unlimited = ?4,
493 fee_cents = ?5, pending_op = ?7, note = ?8, created_by = ?9, created_at = ?10, created_ms = ?11,
494 updated_by = ?9, updated_at = ?10, overage = ?12, subscription_id = NULL, item_id = NULL, price_id = NULL,
495 stripe_status = NULL, last_error = NULL, ended_at = NULL, ended_reason = NULL, converted_at = NULL,
496 -- A customer of the other Stripe mode is never reused.
497 customer_id = CASE WHEN complimentary.live = ?6 THEN complimentary.customer_id END, live = ?6
498 WHERE complimentary.status = 'ended' AND complimentary.pending_op IS NULL",
499 )
500 .bind(&[
501 workspace.as_str().into(),
502 grant.as_str().into(),
503 (a.allowance_micros as f64).into(),
504 unlimited.into(),
505 fee.into(),
506 i32::from(stripe.live()).into(),
507 Op::Create.text().into(),
508 note.into(),
509 by.into(),
510 now.as_str().into(),
511 (now_ms as f64).into(),
512 overage.into(),
513 ])?
514 .run()
515 .await?;
516 self.audit(&account.id, "complimentary", &format!("{workspace} enrolled ({grant}): {terms}: {note}"), by).await?;
517 }
518 Some("pending") => {
519 // Not visible yet: its first period takes these terms.
520 self.db
521 .prepare(
522 "UPDATE complimentary SET allowance_micros = ?2, unlimited = ?3, fee_cents = ?4, note = ?5, updated_by = ?6, updated_at = ?7,
523 overage = ?8
524 WHERE workspace = ?1 AND status = 'pending'",
525 )
526 .bind(&[
527 workspace.as_str().into(),
528 (a.allowance_micros as f64).into(),
529 unlimited.into(),
530 fee.into(),
531 note.into(),
532 by.into(),
533 now.as_str().into(),
534 overage.into(),
535 ])?
536 .run()
537 .await?;
538 self.audit(&account.id, "complimentary", &format!("{workspace} pending enrollment changed: {terms}: {note}"), by).await?;
539 }
540 Some(_) => {
541 let row = existing.as_ref().expect("matched an existing row");
542 if matches!(Op::parse(row.pending_op.as_deref()), Some(Op::Convert { .. } | Op::Create)) {
543 return Ok(Outcome::fail(FailureCode::Conflict, "The owner is converting this workspace to the plan; wait for that to finish."));
544 }
545 // One batch: the open period closes and the next opens with
546 // the new terms. What this month used stays used.
547 let period = new_id("cmpp", now_ms);
548 self.db
549 .batch(vec![
550 self.db
551 .prepare("UPDATE complimentary_periods SET ends_at = ?2 WHERE workspace = ?1 AND ends_at IS NULL")
552 .bind(&[workspace.as_str().into(), now.as_str().into()])?,
553 self.db
554 .prepare(
555 "INSERT INTO complimentary_periods (id, workspace, grant_id, starts_at, allowance_micros, unlimited, fee_cents, note, created_by, created_at, overage)
556 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?4, ?10)",
557 )
558 .bind(&[
559 period.as_str().into(),
560 workspace.as_str().into(),
561 row.grant_id.as_str().into(),
562 now.as_str().into(),
563 (a.allowance_micros as f64).into(),
564 unlimited.into(),
565 fee.into(),
566 note.into(),
567 by.into(),
568 overage.into(),
569 ])?,
570 self.db
571 .prepare(
572 "UPDATE complimentary SET allowance_micros = ?2, unlimited = ?3, fee_cents = ?4, note = ?5, updated_by = ?6, updated_at = ?7,
573 pending_op = ?8, overage = ?9 WHERE workspace = ?1 AND status = 'active'",
574 )
575 .bind(&[
576 workspace.as_str().into(),
577 (a.allowance_micros as f64).into(),
578 unlimited.into(),
579 fee.into(),
580 note.into(),
581 by.into(),
582 now.as_str().into(),
583 Op::Terms { period: period.clone() }.text().into(),
584 overage.into(),
585 ])?,
586 ])
587 .await?;
588 self.audit(
589 &account.id,
590 "complimentary",
591 &format!(
592 "{workspace} terms changed from {} allowance, fee {} to {terms}: {note}",
593 if row.unlimited() { "unlimited".to_owned() } else { cents(row.allowance_micros) },
594 cents(row.fee_cents * 10_000)
595 ),
596 by,
597 )
598 .await?;
599 }
600 }
601 self.drive_logged(&workspace).await;
602 match self.complimentary_row(&workspace).await? {
603 Some(row) => Ok(Outcome::Ok(self.complimentary_standing(row).await?)),
604 None => Ok(Outcome::fail(FailureCode::NotFound, "The enrollment was not saved.")),
605 }
606 }
607
608 /// `admin_end_complimentary`: the workspace goes back to Free now.
609 pub(crate) async fn admin_end_complimentary(&self, a: AdminEndComplimentaryArgs) -> Result<Outcome<ComplimentaryStanding>> {
610 let workspace = a.workspace.trim().to_lowercase();
611 if a.note.trim().len() < 3 || a.by.trim().is_empty() {
612 return Ok(Outcome::fail(FailureCode::Invalid, "Say why, in a few words: it goes in the audit log."));
613 }
614 let Some(row) = self.complimentary_row(&workspace).await?.filter(|row| row.status != "ended") else {
615 return Ok(Outcome::fail(FailureCode::NotFound, "The workspace has no enrollment to end."));
616 };
617 if matches!(Op::parse(row.pending_op.as_deref()), Some(Op::Convert { .. })) {
618 return Ok(Outcome::fail(FailureCode::Conflict, "The owner is converting this workspace to the plan; wait for that to finish."));
619 }
620 self.end(&row, "revoked", a.by.trim(), a.note.trim()).await?;
621 self.drive_logged(&workspace).await;
622 match self.complimentary_row(&workspace).await? {
623 Some(row) => Ok(Outcome::Ok(self.complimentary_standing(row).await?)),
624 None => Ok(Outcome::fail(FailureCode::NotFound, "The enrollment is gone.")),
625 }
626 }
627
628 /// Ends an enrollment: month-end usage so far is put on the ledger
629 /// under the allowance, the open period closes, and the subscription is
630 /// canceled (revoked) or left to the plan (converted).
631 async fn end(&self, row: &Row, reason: &str, by: &str, note: &str) -> Result<()> {
632 let now_ms = now_ms();
633 let now = rfc3339(now_ms);
634 if row.status == "active" {
635 self.post_pending_under_allowance(row, now_ms).await?;
636 }
637 let op = if reason == "revoked" && (row.subscription_id.is_some() || row.status == "pending") { Some(Op::Cancel.text()) } else { None };
638 self.db
639 .batch(vec![
640 self.db
641 .prepare("UPDATE complimentary_periods SET ends_at = ?2 WHERE workspace = ?1 AND ends_at IS NULL")
642 .bind(&[row.workspace.as_str().into(), now.as_str().into()])?,
643 self.db
644 .prepare(
645 "UPDATE complimentary SET status = 'ended', ended_at = ?2, ended_reason = ?3, pending_op = ?4, updated_by = ?5, updated_at = ?2,
646 converted_at = CASE WHEN ?3 = 'converted' THEN ?2 END
647 WHERE workspace = ?1 AND grant_id = ?6 AND status <> 'ended'",
648 )
649 .bind(&[
650 row.workspace.as_str().into(),
651 now.as_str().into(),
652 reason.into(),
653 crate::optional(op.as_deref()),
654 by.into(),
655 row.grant_id.as_str().into(),
656 ])?,
657 ])
658 .await?;
659 let account = self.account_of(&row.workspace).await?;
660 self.audit(&account.id, "complimentary", &format!("{} enrollment {} ended ({reason}): {note}", row.workspace, row.grant_id), by).await
661 }
662
663 /// Month-end meters (storage, git operations…) run up so far this month
664 /// go on the ledger now, timed just before the period closes, so the
665 /// allowance pays for them as it would have at the close. The month's
666 /// later charge takes only what is metered after (`pending_baselines`).
667 /// Never charges usage gifted while enrolled.
668 async fn post_pending_under_allowance(&self, row: &Row, now_ms: u64) -> Result<()> {
669 #[derive(Deserialize)]
670 struct Pending {
671 source: String,
672 month: String,
673 cost_micros: Option<i64>,
674 raw_cost: Option<i64>,
675 raw_charge: Option<i64>,
676 }
677 let at = rfc3339(now_ms.saturating_sub(1));
678 let month = crate::credits::month_of(&at);
679 let marks = crate::storage::CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
680 let rows = self
681 .db
682 .prepare(format!(
683 "SELECT source, month, {} AS cost_micros, cost_micros AS raw_cost, charge_micros AS raw_charge FROM pending_usage
684 WHERE workspace = ?1 AND month = ?2 AND charged_at IS NULL AND source IN ({marks})",
685 crate::storage::NET_COST
686 ))
687 .bind(&[row.workspace.as_str().into(), month.as_str().into()])?
688 .all()
689 .await?
690 .results::<Pending>()?;
691 for pending in rows {
692 let cost = pending.cost_micros.unwrap_or(0);
693 // The baseline moves to exactly the figure posted here, read
694 // above: a report landing meanwhile stays past it, charged at the
695 // month's close. An empty row still marks where the next starts.
696 let baseline = self
697 .db
698 .prepare(
699 "INSERT INTO pending_baselines (workspace, source, month, cost_micros, charge_micros, created_at)
700 VALUES (?1, ?2, ?3, ?5, ?6, ?4)
701 ON CONFLICT (workspace, source, month) DO UPDATE SET
702 cost_micros = MAX(pending_baselines.cost_micros, excluded.cost_micros),
703 charge_micros = MAX(pending_baselines.charge_micros, excluded.charge_micros), created_at = excluded.created_at",
704 )
705 .bind(&[
706 row.workspace.as_str().into(),
707 pending.source.as_str().into(),
708 pending.month.as_str().into(),
709 at.as_str().into(),
710 (pending.raw_cost.unwrap_or(0) as f64).into(),
711 (pending.raw_charge.unwrap_or(0) as f64).into(),
712 ])?;
713 if cost <= 0 {
714 baseline.run().await?;
715 continue;
716 }
717 let base = crate::credits::with_margin(cost, self.margin_percent);
718 let (charge, terms_note, discount) = self.charged(&row.workspace, base).await?;
719 let description = format!(
720 "{} in {} while enrolled{terms_note}",
721 crate::storage::title(&pending.source),
722 pending.month
723 );
724 let reference = format!("{}/{}/{}/{}", pending.source, row.workspace, pending.month, row.grant_id);
725 let eligible = crate::credits::Eligible::default();
726 let line = crate::storage::UsageLine {
727 workspace: &row.workspace,
728 charged: charge,
729 description: &description,
730 repo: None,
731 task: &pending.source,
732 cost,
733 reference: &reference,
734 created_at: &at,
735 drawn: crate::credits::Drawn::default(),
736 };
737 if self.post_usage_with(line, vec![baseline], Some((&pending.month, &eligible, false))).await? {
738 self.record_discount(&reference, discount).await?;
739 }
740 }
741 Ok(())
742 }
743
744 /// Runs the workspace's owed Stripe step, writing down an error for sudo
745 /// rather than failing the change staff made.
746 async fn drive_logged(&self, workspace: &str) {
747 if let Err(error) = self.drive(workspace).await {
748 worker::console_error!("{workspace}: complimentary Stripe step failed; tried again next tick: {error}");
749 let noted = match self.db.prepare("UPDATE complimentary SET last_error = ?2 WHERE workspace = ?1")
750 .bind(&[workspace.into(), crate::stripe::friendly(&error).into()]) {
751 Ok(statement) => statement.run().await.map(|_| ()),
752 Err(error) => Err(error),
753 };
754 if let Err(error) = noted {
755 worker::console_error!("{workspace}: could not note the error: {error}");
756 }
757 }
758 }
759
760 /// The fifteen-minute tick: every enrollment with a Stripe step owed.
761 pub(crate) async fn complimentary_tick(&self) -> Result<()> {
762 #[derive(Deserialize)]
763 struct Owed {
764 workspace: String,
765 }
766 let owed = self
767 .db
768 .prepare("SELECT workspace FROM complimentary WHERE pending_op IS NOT NULL ORDER BY updated_at LIMIT ?")
769 .bind(&[TICK_BATCH.into()])?
770 .all()
771 .await?
772 .results::<Owed>()?;
773 for row in owed {
774 if let Err(error) = self.drive(&row.workspace).await {
775 worker::console_error!("{}: complimentary Stripe step failed: {error}", row.workspace);
776 self.db
777 .prepare("UPDATE complimentary SET last_error = ?2 WHERE workspace = ?1")
778 .bind(&[row.workspace.as_str().into(), crate::stripe::friendly(&error).into()])?
779 .run()
780 .await?;
781 }
782 }
783 Ok(())
784 }
785
786 /// Does the owed Stripe step, if any.
787 async fn drive(&self, workspace: &str) -> Result<()> {
788 let Some(stripe) = &self.stripe else { return Ok(()) };
789 let Some(row) = self.complimentary_row(workspace).await? else { return Ok(()) };
790 let Some(op) = Op::parse(row.pending_op.as_deref()) else { return Ok(()) };
791 // Test objects are never carried into live mode, nor the reverse.
792 if (row.live != 0) != stripe.live() && (row.subscription_id.is_some() || row.customer_id.is_some()) {
793 return Err(worker::Error::RustError(format!(
794 "this enrollment's Stripe objects are {}-mode and billing now uses a {}-mode key: end it and enroll again",
795 if row.live != 0 { "live" } else { "test" },
796 if stripe.live() { "live" } else { "test" }
797 )));
798 }
799 match op {
800 Op::Create => self.drive_create(stripe, &row).await,
801 Op::Terms { period } => self.drive_terms(stripe, &row, &period).await,
802 Op::Cancel => self.drive_cancel(stripe, &row).await,
803 // A conversion moves on when its payment does: the webhook or
804 // the owner's next look settles it (`settle_complimentary`).
805 Op::Convert { started_ms, .. } => {
806 if let Some(id) = &row.subscription_id {
807 self.settle_complimentary(id).await?;
808 }
809 // A conversion Stripe gave up on (its pending update expires
810 // after 23 hours) stops waiting, and the enrollment stands.
811 let fresh = self.complimentary_row(workspace).await?;
812 if now_ms().saturating_sub(started_ms) > 2 * KEY_KEPT_MS && fresh.as_ref().is_some_and(|r| r.status == "active") {
813 // Preparing it charged the subscription automatically; a
814 // fee goes back to being invoiced.
815 if let (true, Some(id)) = (row.fee_cents > 0, &row.subscription_id) {
816 let _: serde_json::Value = stripe
817 .post_idempotent(
818 &format!("/subscriptions/{id}"),
819 &[("collection_method", "send_invoice".to_owned()), ("days_until_due", "30".to_owned())],
820 &format!("cmp/{}/convert/{started_ms}/undo", row.grant_id),
821 )
822 .await?;
823 }
824 self.db
825 .prepare("UPDATE complimentary SET pending_op = NULL WHERE workspace = ?1 AND pending_op = ?2 AND status = 'active'")
826 .bind(&[row.workspace.as_str().into(), row.pending_op.clone().unwrap_or_default().into()])?
827 .run()
828 .await?;
829 }
830 Ok(())
831 }
832 }
833 }
834
835 /// The customer for an enrollment: the workspace's own, or one made
836 /// under the grant's key.
837 async fn enrollment_customer(&self, stripe: &Stripe, row: &Row) -> Result<String> {
838 if let Some(customer) = &row.customer_id {
839 return Ok(customer.clone());
840 }
841 let existing = self.row(&row.workspace).await?.and_then(|account| account.customer_id);
842 let customer = match existing {
843 Some(customer) => customer,
844 None => {
845 let old = now_ms().saturating_sub(row.created_ms.max(0) as u64) > KEY_KEPT_MS;
846 let found = if old {
847 let query = crate::stripe::form(&[("query", format!("metadata['g1t_grant']:'{}'", row.grant_id))]);
848 let list: serde_json::Value = stripe.get(&format!("/customers/search?{query}")).await?;
849 list["data"].as_array().and_then(|d| d.first()).and_then(|c| c["id"].as_str()).map(str::to_owned)
850 } else {
851 None
852 };
853 match found {
854 Some(customer) => customer,
855 // Search sees a customer within about a minute; a day
856 // later, none found means none was made.
857 None => {
858 let email = self.owner_email(&row.workspace).await;
859 let mut fields = crate::stripe::customer_fields(&row.workspace, email.as_deref());
860 fields.push(("metadata[g1t_grant]", row.grant_id.clone()));
861 #[derive(Deserialize)]
862 struct Customer {
863 id: String,
864 }
865 let key = if old { format!("cmp/{}/customer/late", row.grant_id) } else { format!("cmp/{}/customer", row.grant_id) };
866 let made: Customer = stripe.post_idempotent("/customers", &fields, &key).await?;
867 made.id
868 }
869 }
870 }
871 };
872 self.db
873 .batch(vec![
874 self.db
875 .prepare("UPDATE complimentary SET customer_id = ?2 WHERE workspace = ?1 AND grant_id = ?3")
876 .bind(&[row.workspace.as_str().into(), customer.as_str().into(), row.grant_id.as_str().into()])?,
877 self.db
878 .prepare(
879 "INSERT INTO accounts (workspace, balance_micros, customer_id, created_at) VALUES (?1, 0, ?2, ?3)
880 ON CONFLICT (workspace) DO UPDATE SET customer_id = COALESCE(customer_id, ?2)",
881 )
882 .bind(&[row.workspace.as_str().into(), customer.as_str().into(), rfc3339(now_ms()).into()])?,
883 ])
884 .await?;
885 Ok(customer)
886 }
887
888 /// The 100% coupon, made once under its own id.
889 async fn ensure_coupon(&self, stripe: &Stripe) -> Result<()> {
890 match stripe.get::<serde_json::Value>(&format!("/coupons/{FULL_DISCOUNT_COUPON}")).await {
891 Ok(_) => Ok(()),
892 Err(error) if is_missing(&error) => {
893 let _: serde_json::Value = stripe
894 .post_idempotent(
895 "/coupons",
896 &[
897 ("id", FULL_DISCOUNT_COUPON.to_owned()),
898 ("percent_off", "100".to_owned()),
899 ("duration", "forever".to_owned()),
900 ("name", "g1t complimentary (100%)".to_owned()),
901 ],
902 &format!("coupon/{FULL_DISCOUNT_COUPON}"),
903 )
904 .await?;
905 Ok(())
906 }
907 Err(error) => Err(error),
908 }
909 }
910
911 /// A subscription Stripe already has for the grant, found by its
912 /// metadata: what an ambiguous creation made.
913 async fn grant_subscription(&self, stripe: &Stripe, customer: &str, grant: &str) -> Result<Option<StripeSubscription>> {
914 #[derive(Deserialize)]
915 struct List {
916 data: Vec<StripeSubscription>,
917 }
918 let query = crate::stripe::form(&[("customer", customer.to_owned()), ("status", "all".to_owned()), ("limit", "100".to_owned())]);
919 let list: List = stripe.get(&format!("/subscriptions?{query}")).await?;
920 Ok(list
921 .data
922 .into_iter()
923 .find(|s| s.metadata.as_ref().and_then(|m| m.get("g1t_grant")).map(String::as_str) == Some(grant) && s.status != "canceled"))
924 }
925
926 async fn drive_create(&self, stripe: &Stripe, row: &Row) -> Result<()> {
927 if row.status != "pending" {
928 return self.clear_op(row, &Op::Create).await;
929 }
930 let customer = self.enrollment_customer(stripe, row).await?;
931 let old = now_ms().saturating_sub(row.created_ms.max(0) as u64) > KEY_KEPT_MS;
932 let found = if old { self.grant_subscription(stripe, &customer, &row.grant_id).await? } else { None };
933 let subscription = match found {
934 Some(subscription) => subscription,
935 // Older than the key and nothing found: making it now is safe,
936 // as the search above saw none.
937 None => {
938 let fully = fully_discounted(row, self.internal_terms(&row.workspace).await?);
939 if fully {
940 self.ensure_coupon(stripe).await?;
941 }
942 let product = stripe.product(Feature::Plan.as_str(), &format!("{} plan", Feature::Plan.title())).await?;
943 let fields = create_fields(row, &customer, &product, self.plans.plan_monthly_cents, fully);
944 let key = if old { format!("cmp/{}/subscription/late", row.grant_id) } else { format!("cmp/{}/subscription", row.grant_id) };
945 stripe.post_idempotent("/subscriptions", &owned(&fields), &key).await?
946 }
947 };
948 let item = subscription.first_item();
949 let item_id = item.and_then(|i| i.id.clone());
950 let price_id = item.and_then(|i| i.price.as_ref()).map(|p| p.id.clone());
951 let now = rfc3339(now_ms());
952 let period = new_id("cmpp", now_ms());
953 // Stripe has it: the workspace sees the enrollment from now, with
954 // its first period, in one batch.
955 self.db
956 .batch(vec![
957 self.db
958 .prepare(
959 "UPDATE complimentary SET status = 'active', subscription_id = ?2, item_id = ?3, price_id = ?4, stripe_status = ?5,
960 pending_op = NULL, last_error = NULL, updated_at = ?6
961 WHERE workspace = ?1 AND grant_id = ?7 AND status = 'pending'",
962 )
963 .bind(&[
964 row.workspace.as_str().into(),
965 subscription.id.as_str().into(),
966 crate::optional(item_id.as_deref()),
967 crate::optional(price_id.as_deref()),
968 subscription.status.as_str().into(),
969 now.as_str().into(),
970 row.grant_id.as_str().into(),
971 ])?,
972 self.db
973 .prepare(
974 "INSERT INTO complimentary_periods (id, workspace, grant_id, starts_at, allowance_micros, unlimited, fee_cents, note, created_by, created_at, overage)
975 SELECT ?1, workspace, grant_id, ?2, allowance_micros, unlimited, fee_cents, note, updated_by, ?2, overage
976 FROM complimentary WHERE workspace = ?3 AND grant_id = ?4 AND status = 'active' AND subscription_id = ?5
977 AND NOT EXISTS (SELECT 1 FROM complimentary_periods WHERE workspace = ?3 AND ends_at IS NULL)",
978 )
979 .bind(&[period.as_str().into(), now.as_str().into(), row.workspace.as_str().into(), row.grant_id.as_str().into(), subscription.id.as_str().into()])?,
980 ])
981 .await?;
982 Ok(())
983 }
984
985 async fn drive_terms(&self, stripe: &Stripe, row: &Row, period: &str) -> Result<()> {
986 let (Some(subscription), Some(item)) = (&row.subscription_id, &row.item_id) else {
987 return self.clear_op(row, &Op::Terms { period: period.to_owned() }).await;
988 };
989 let fully = fully_discounted(row, self.internal_terms(&row.workspace).await?);
990 if fully {
991 self.ensure_coupon(stripe).await?;
992 }
993 let product = stripe.product(Feature::Plan.as_str(), &format!("{} plan", Feature::Plan.title())).await?;
994 let mut fields: Vec<(String, String)> = vec![("items[0][id]".into(), item.clone()), ("proration_behavior".into(), "none".into())];
995 fields.extend(price_fields(row, &product, self.plans.plan_monthly_cents, "items[0]", fully));
996 fields.extend(metadata_fields(row).into_iter().map(|(k, v)| (k.to_owned(), v)));
997 if fully {
998 fields.push(("discounts[0][coupon]".into(), FULL_DISCOUNT_COUPON.to_owned()));
999 } else {
1000 fields.push(("discounts".into(), String::new()));
1001 }
1002 // A fee is invoiced; with none there is nothing to collect, and
1003 // converting needs automatic collection.
1004 if row.fee_cents > 0 {
1005 fields.push(("collection_method".into(), "send_invoice".into()));
1006 fields.push(("days_until_due".into(), "30".into()));
1007 } else {
1008 fields.push(("collection_method".into(), "charge_automatically".into()));
1009 }
1010 let updated: StripeSubscription = stripe
1011 .post_idempotent(&format!("/subscriptions/{subscription}"), &owned(&fields), &format!("cmp/{}/terms/{period}", row.grant_id))
1012 .await?;
1013 let price_id = updated.first_item().and_then(|i| i.price.as_ref()).map(|p| p.id.clone());
1014 self.db
1015 .prepare(
1016 "UPDATE complimentary SET price_id = ?2, stripe_status = ?3, pending_op = NULL, last_error = NULL
1017 WHERE workspace = ?1 AND pending_op = ?4",
1018 )
1019 .bind(&[
1020 row.workspace.as_str().into(),
1021 crate::optional(price_id.as_deref()),
1022 updated.status.as_str().into(),
1023 Op::Terms { period: period.to_owned() }.text().into(),
1024 ])?
1025 .run()
1026 .await?;
1027 Ok(())
1028 }
1029
1030 async fn drive_cancel(&self, stripe: &Stripe, row: &Row) -> Result<()> {
1031 // A creation that may have happened is looked for first.
1032 let subscription = match (&row.subscription_id, &row.customer_id) {
1033 (Some(id), _) => Some(id.clone()),
1034 (None, Some(customer)) => self.grant_subscription(stripe, customer, &row.grant_id).await?.map(|s| s.id),
1035 (None, None) => None,
1036 };
1037 if let Some(id) = &subscription {
1038 match stripe.cancel_now(id).await {
1039 Ok(_) => {}
1040 Err(error) if is_missing(&error) || error.to_string().contains("canceled subscription") => {}
1041 Err(error) => return Err(error),
1042 }
1043 } else if row.status == "pending" && now_ms().saturating_sub(row.created_ms.max(0) as u64) <= KEY_KEPT_MS {
1044 // A creation may still land under its key: make it now so it can
1045 // be canceled, rather than leave one behind.
1046 if row.customer_id.is_some() {
1047 return Err(worker::Error::RustError("waiting for Stripe to show the enrollment's subscription before canceling it".into()));
1048 }
1049 }
1050 self.db
1051 .prepare("UPDATE complimentary SET stripe_status = 'canceled', pending_op = NULL, last_error = NULL WHERE workspace = ?1 AND pending_op = ?2")
1052 .bind(&[row.workspace.as_str().into(), Op::Cancel.text().into()])?
1053 .run()
1054 .await?;
1055 Ok(())
1056 }
1057
1058 async fn clear_op(&self, row: &Row, op: &Op) -> Result<()> {
1059 self.db
1060 .prepare("UPDATE complimentary SET pending_op = NULL WHERE workspace = ?1 AND pending_op = ?2")
1061 .bind(&[row.workspace.as_str().into(), op.text().into()])?
1062 .run()
1063 .await?;
1064 Ok(())
1065 }
1066
1067 /// A Stripe change to an enrollment's subscription: renewed, paid,
1068 /// canceled at Stripe, or converted to the plan. None when the
1069 /// subscription is no enrollment's.
1070 pub(crate) async fn settle_complimentary(&self, subscription_id: &str) -> Result<Option<String>> {
1071 let Some(row) = self
1072 .db
1073 .prepare("SELECT * FROM complimentary WHERE subscription_id = ?")
1074 .bind(&[subscription_id.into()])?
1075 .first::<Row>(None)
1076 .await?
1077 else {
1078 return Ok(None);
1079 };
1080 let Some(stripe) = &self.stripe else { return Ok(Some("ignored: payments off".into())) };
1081 let subscription = match stripe.subscription(subscription_id).await {
1082 Ok(subscription) => subscription,
1083 Err(error) if is_missing(&error) => {
1084 if row.status == "active" {
1085 self.end(&row, "revoked", "stripe", "its subscription is gone at Stripe").await?;
1086 }
1087 return Ok(Some(format!("{}: enrollment's subscription is gone at Stripe", row.workspace)));
1088 }
1089 Err(error) => return Err(error),
1090 };
1091 // Converting: an owner's attempt in progress, or (with no staff change
1092 // in flight) the plan's price on an enrollment whose own price
1093 // differs from it, which only a conversion puts there.
1094 let converting = match Op::parse(row.pending_op.as_deref()) {
1095 Some(Op::Convert { .. }) => true,
1096 None => row.fee_cents != i64::from(self.plans.plan_monthly_cents) && !self.internal_terms(&row.workspace).await?,
1097 _ => false,
1098 };
1099 if row.status == "active" && converted(row.price_id.as_deref(), &subscription, self.plans.plan_monthly_cents, converting) {
1100 let by = match Op::parse(row.pending_op.as_deref()) {
1101 Some(Op::Convert { by, .. }) => by,
1102 _ => "stripe".to_owned(),
1103 };
1104 self.finish_conversion(&row, &subscription, &by).await?;
1105 return Ok(Some(format!("{} converted to the plan", row.workspace)));
1106 }
1107 self.db
1108 .prepare("UPDATE complimentary SET stripe_status = ?2 WHERE workspace = ?1")
1109 .bind(&[row.workspace.as_str().into(), subscription.status.as_str().into()])?
1110 .run()
1111 .await?;
1112 // Canceled (or unpaid) at Stripe: the enrollment ends with it.
1113 if row.status == "active" && matches!(subscription.status.as_str(), "canceled" | "incomplete_expired" | "unpaid") {
1114 self.end(&row, "revoked", "stripe", &format!("its subscription is {} at Stripe", subscription.status)).await?;
1115 return Ok(Some(format!("{}: enrollment ended, subscription {}", row.workspace, subscription.status)));
1116 }
1117 Ok(Some(format!("{}: enrollment subscription is {}", row.workspace, subscription.status)))
1118 }
1119
1120 /// The conversion done at Stripe: the enrollment ends, the same
1121 /// subscription becomes the workspace's plan, and its first invoice is
1122 /// the plan's payment.
1123 async fn finish_conversion(&self, row: &Row, subscription: &StripeSubscription, by: &str) -> Result<()> {
1124 self.end(row, "converted", by, "the owner started the g1t plan").await?;
1125 self.record(&row.workspace, Feature::Plan, subscription, by).await?;
1126 // Best effort: Stripe's view of what the subscription is now.
1127 if let Some(stripe) = &self.stripe {
1128 let _ = stripe
1129 .post::<serde_json::Value>(
1130 &format!("/subscriptions/{}", subscription.id),
1131 &[("metadata[feature]", "plan".to_owned()), ("metadata[g1t_converted_from]", row.grant_id.clone())],
1132 )
1133 .await;
1134 }
1135 Ok(())
1136 }
1137
1138 /// `convert_to_paid`.
1139 pub(crate) async fn convert_to_paid(&self, a: ConvertToPaidArgs) -> Result<Outcome<Conversion>> {
1140 let workspace = a.workspace.trim().to_lowercase();
1141 if !a.actor.manages_billing(&workspace) {
1142 return Ok(Outcome::fail(FailureCode::Forbidden, "Only an owner or a billing manager can start the g1t plan."));
1143 }
1144 let Some(stripe) = &self.stripe else {
1145 return Ok(Outcome::fail(FailureCode::Conflict, "Payments are not set up on this g1t."));
1146 };
1147 let Some(row) = self.complimentary_active(&workspace).await? else {
1148 return Ok(Outcome::fail(FailureCode::NotFound, "The workspace has no complimentary enrollment to convert."));
1149 };
1150 if fully_discounted(&row, self.internal_terms(&workspace).await?) {
1151 return Ok(Outcome::fail(FailureCode::Conflict, "This enrollment is g1t's own and is not converted."));
1152 }
1153 let (Some(subscription_id), Some(item), Some(customer)) = (row.subscription_id.clone(), row.item_id.clone(), row.customer_id.clone()) else {
1154 return Ok(Outcome::fail(FailureCode::Conflict, "The enrollment's Stripe subscription is not set up yet; try again in a few minutes."));
1155 };
1156 // Where the subscription stands now decides: converted already, or
1157 // a payment waiting on the bank, is answered, never started again.
1158 let current = stripe.subscription(&subscription_id).await?;
1159 let converting = matches!(Op::parse(row.pending_op.as_deref()), Some(Op::Convert { .. }));
1160 if converted(row.price_id.as_deref(), &current, self.plans.plan_monthly_cents, converting) {
1161 self.finish_conversion(&row, &current, &a.actor.username).await?;
1162 return Ok(Outcome::Ok(Conversion { status: "converted".into(), url: None, message: "The g1t plan is on.".into() }));
1163 }
1164 let attempt = match Op::parse(row.pending_op.as_deref()) {
1165 Some(Op::Convert { attempt, .. }) => {
1166 if current.pending_update.as_ref().is_some_and(|p| !p.is_null()) {
1167 return self.waiting(stripe, &current).await;
1168 }
1169 attempt + 1
1170 }
1171 Some(_) => {
1172 return Ok(Outcome::fail(FailureCode::Conflict, "g1t is still updating this enrollment at Stripe; try again in a few minutes."));
1173 }
1174 None => 1,
1175 };
1176 // A card to charge, with its billing address for tax: none yet, the
1177 // card check page asks for both and charges nothing.
1178 let saved = stripe.default_payment_method(&customer).await?;
1179 let Some(saved) = saved else {
1180 return self.needs_card(&a, "Add a card to start the plan; nothing is charged until you confirm.").await;
1181 };
1182 stripe.set_default_card(&customer, &saved.id).await?;
1183 // Durable before Stripe is asked: a crash after this is picked up
1184 // with the same attempt and key.
1185 let started_ms = now_ms();
1186 let op = Op::Convert { attempt, by: a.actor.username.clone(), started_ms };
1187 let claimed = self
1188 .db
1189 .prepare(
1190 "UPDATE complimentary SET pending_op = ?2, updated_at = ?3
1191 WHERE workspace = ?1 AND status = 'active' AND (pending_op IS NULL OR pending_op LIKE '{\"op\":\"convert\"%')
1192 RETURNING workspace",
1193 )
1194 .bind(&[workspace.as_str().into(), op.text().into(), rfc3339(now_ms()).into()])?
1195 .first::<serde_json::Value>(None)
1196 .await?;
1197 if claimed.is_none() {
1198 return Ok(Outcome::fail(FailureCode::Conflict, "g1t is still updating this enrollment at Stripe; try again in a few minutes."));
1199 }
1200 // Charged automatically from now, taxed: both change nothing on a
1201 // $0 subscription, and neither can wait on a payment.
1202 let prepared = stripe
1203 .post_idempotent::<serde_json::Value>(
1204 &format!("/subscriptions/{subscription_id}"),
1205 &[("collection_method", "charge_automatically".to_owned()), ("automatic_tax[enabled]", "true".to_owned())],
1206 &format!("cmp/{}/convert/{attempt}/{started_ms}/prepare", row.grant_id),
1207 )
1208 .await;
1209 if let Err(error) = prepared {
1210 worker::console_error!("{workspace}: preparing the conversion failed: {error}");
1211 self.clear_op(&row, &op).await?;
1212 if crate::stripe::is_tax_location_error(&error) {
1213 return self.needs_card(&a, "Add your billing address with a card so tax can be worked out; nothing is charged until you confirm.").await;
1214 }
1215 return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error)));
1216 }
1217 let product = stripe.product(Feature::Plan.as_str(), &format!("{} plan", Feature::Plan.title())).await?;
1218 let fields = conversion_fields(&item, &product, self.plans.plan_monthly_cents);
1219 let changed: std::result::Result<StripeSubscription, _> = stripe
1220 .post_idempotent(
1221 &format!("/subscriptions/{subscription_id}"),
1222 &owned(&fields),
1223 &format!("cmp/{}/convert/{attempt}/{started_ms}", row.grant_id),
1224 )
1225 .await;
1226 let changed = match changed {
1227 Ok(changed) => changed,
1228 Err(error) => {
1229 worker::console_error!("{workspace}: converting to the plan failed: {error}");
1230 // A definite refusal with nothing waiting at Stripe leaves
1231 // nothing pending; a pending update (its invoice can still
1232 // be paid for 23 hours) or anything else is settled later.
1233 let refused = crate::invoices::is_decline(&error.to_string()) || error.to_string().contains("answered 400");
1234 let waiting = stripe.subscription(&subscription_id).await.map_or(true, |s| s.pending_update.as_ref().is_some_and(|p| !p.is_null()));
1235 if refused && !waiting {
1236 self.clear_op(&row, &op).await?;
1237 }
1238 return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error)));
1239 }
1240 };
1241 if converted(row.price_id.as_deref(), &changed, self.plans.plan_monthly_cents, true) {
1242 self.finish_conversion(&row, &changed, &a.actor.username).await?;
1243 return Ok(Outcome::Ok(Conversion { status: "converted".into(), url: None, message: "The g1t plan is on.".into() }));
1244 }
1245 self.waiting(stripe, &changed).await
1246 }
1247
1248 /// A conversion waiting on its first payment: where to finish it.
1249 async fn waiting(&self, stripe: &Stripe, subscription: &StripeSubscription) -> Result<Outcome<Conversion>> {
1250 let invoice: Option<serde_json::Value> = match &subscription.latest_invoice {
1251 Some(id) => Some(stripe.get(&format!("/invoices/{id}?expand[]=payment_intent")).await?),
1252 None => None,
1253 };
1254 let url = invoice.as_ref().and_then(|i| i["hosted_invoice_url"].as_str()).map(str::to_owned);
1255 let intent = invoice.as_ref().and_then(|i| i["payment_intent"]["status"].as_str()).unwrap_or_default().to_owned();
1256 Ok(Outcome::Ok(if intent == "requires_action" {
1257 Conversion {
1258 status: "needs_action".into(),
1259 url,
1260 message: "Your bank wants to confirm the payment. Finish it on Stripe's page; the plan starts once it is paid, and nothing changes until then.".into(),
1261 }
1262 } else {
1263 Conversion {
1264 status: "declined".into(),
1265 url,
1266 message: "The card was declined, so nothing changed: the complimentary terms stand. Pay with another card on Stripe's page to start the plan.".into(),
1267 }
1268 }))
1269 }
1270
1271 async fn needs_card(&self, a: &ConvertToPaidArgs, message: &str) -> Result<Outcome<Conversion>> {
1272 let page = self
1273 .card_check(CardCheckArgs { actor: a.actor.clone(), workspace: a.workspace.clone(), return_url: a.return_url.clone() })
1274 .await?;
1275 Ok(match page {
1276 Outcome::Ok(checkout) => Outcome::Ok(Conversion { status: "needs_card".into(), url: Some(checkout.url), message: message.to_owned() }),
1277 Outcome::Fail(failure) => Outcome::Fail(failure),
1278 })
1279 }
1280
1281}
1282
1283/// The conversion itself: the one item to the plan's price, a new cycle
1284/// from now, charged at once, applied only if the payment succeeds.
1285pub(crate) fn conversion_fields(item: &str, product: &str, plan_cents: u32) -> Vec<(String, String)> {
1286 vec![
1287 ("items[0][id]".into(), item.to_owned()),
1288 ("items[0][price_data][currency]".into(), "usd".into()),
1289 ("items[0][price_data][product]".into(), product.to_owned()),
1290 ("items[0][price_data][unit_amount]".into(), plan_cents.to_string()),
1291 ("items[0][price_data][recurring][interval]".into(), "month".into()),
1292 ("items[0][price_data][tax_behavior]".into(), "exclusive".into()),
1293 ("payment_behavior".into(), "pending_if_incomplete".into()),
1294 ("proration_behavior".into(), "always_invoice".into()),
1295 ("billing_cycle_anchor".into(), "now".into()),
1296 ]
1297}
1298
1299/// Where this month stands against the allowance, from 50%: used, metered
1300/// and held together, as admission counts them.
1301pub(crate) fn alert(workspace: &str, month: &ComplimentaryMonth) -> Option<UsageAlert> {
1302 if month.unlimited || month.allowance_micros <= 0 {
1303 return None;
1304 }
1305 let taken = month.used_micros + month.pending_micros + month.held_micros;
1306 let level = crate::limits::alert_level(taken, month.allowance_micros);
1307 (level > 0).then(|| UsageAlert {
1308 meter: "complimentary".into(),
1309 level,
1310 used_micros: taken,
1311 limit_micros: month.allowance_micros,
1312 message: if level >= 100 && month.charge_past_allowance {
1313 format!(
1314 "{workspace} has used all of this month's {} complimentary allowance. Usage from here is charged to the workspace's card at the usual prices, up to its spend limit{}.",
1315 dollars(month.allowance_micros),
1316 if month.has_card { "" } else { "; add a card on Billing to keep going" }
1317 )
1318 } else if level >= 100 {
1319 format!(
1320 "{workspace} has used all of this month's {} complimentary allowance. New work that costs money starts again on {}; an owner can start the g1t plan to keep going now.",
1321 dollars(month.allowance_micros),
1322 &month.resets_at[..10]
1323 )
1324 } else if month.charge_past_allowance {
1325 format!(
1326 "{workspace} has used {} of this month's {} complimentary allowance ({level}%). Past it, usage is charged to the workspace's card at the usual prices.",
1327 dollars(taken),
1328 dollars(month.allowance_micros)
1329 )
1330 } else {
1331 format!(
1332 "{workspace} has used {} of this month's {} complimentary allowance ({level}%). Past it, new work that costs money stops until {}; nothing is ever charged for it.",
1333 dollars(taken),
1334 dollars(month.allowance_micros),
1335 &month.resets_at[..10]
1336 )
1337 },
1338 })
1339}
1340
1341
1342#[cfg(test)]
1343mod tests {
1344 use super::*;
1345
1346 fn row(allowance: i64, unlimited: bool, fee: i64) -> Row {
1347 Row {
1348 workspace: "acme".into(),
1349 grant_id: "cmp_1".into(),
1350 status: "active".into(),
1351 allowance_micros: allowance,
1352 unlimited: i64::from(unlimited),
1353 fee_cents: fee,
1354 overage: "stop".into(),
1355 live: 0,
1356 customer_id: Some("cus_1".into()),
1357 subscription_id: Some("sub_1".into()),
1358 item_id: Some("si_1".into()),
1359 price_id: Some("price_comp".into()),
1360 stripe_status: Some("active".into()),
1361 pending_op: None,
1362 last_error: None,
1363 note: "partner".into(),
1364 created_by: "staff".into(),
1365 created_at: "2026-10-01T00:00:00.000Z".into(),
1366 created_ms: 0,
1367 updated_by: "staff".into(),
1368 updated_at: "2026-10-01T00:00:00.000Z".into(),
1369 ended_at: None,
1370 ended_reason: None,
1371 }
1372 }
1373
1374 fn args(allowance: i64, unlimited: bool, fee: u32) -> AdminSetComplimentaryArgs {
1375 AdminSetComplimentaryArgs {
1376 workspace: "acme".into(),
1377 allowance_micros: allowance,
1378 unlimited,
1379 fee_cents: fee,
1380 charge_past_allowance: false,
1381 note: "partner deal".into(),
1382 by: "staff".into(),
1383 }
1384 }
1385
1386 #[test]
1387 fn staff_terms_are_bounded_and_say_why() {
1388 assert!(invalid(&args(100_000_000, false, 0)).is_none());
1389 assert!(invalid(&args(500_000_000, false, 5_000)).is_none());
1390 assert!(invalid(&args(0, true, 0)).is_none());
1391 assert!(invalid(&args(0, false, 2_500)).is_none(), "a fee alone is custom terms");
1392 assert!(invalid(&args(0, false, 0)).unwrap().contains("gives nothing"));
1393 assert!(invalid(&args(-1, false, 0)).is_some());
1394 assert!(invalid(&args(MAX_COMPLIMENTARY_MICROS + 1, false, 0)).unwrap().contains("unlimited"));
1395 assert!(invalid(&args(1, false, MAX_COMPLIMENTARY_FEE_CENTS + 1)).is_some());
1396 let mut quiet = args(1, false, 0);
1397 quiet.note = " ".into();
1398 assert!(invalid(&quiet).unwrap().contains("why"));
1399 }
1400
1401 #[test]
1402 fn remaining_counts_used_pending_and_held_and_never_goes_below_nothing() {
1403 let r = row(100_000_000, false, 0);
1404 let m = month_standing("2026-10", &r, 60_000_000, 25_000_000, 10_000_000, 0, false);
1405 assert_eq!(m.remaining_micros, 5_000_000);
1406 assert_eq!(m.resets_at, "2026-11-01T00:00:00Z");
1407 // Lowered below what was used: nothing left, never a debt.
1408 let lowered = row(50_000_000, false, 0);
1409 assert_eq!(month_standing("2026-10", &lowered, 60_000_000, 0, 0, 0, false).remaining_micros, 0);
1410 // Unlimited ignores pending and held.
1411 let all = row(0, true, 0);
1412 assert!(month_standing("2026-12", &all, 9_000_000, 1, 1, 0, false).remaining_micros > 1_000_000_000);
1413 assert_eq!(month_standing("2026-12", &all, 0, 0, 0, 0, false).resets_at, "2027-01-01T00:00:00Z");
1414 }
1415
1416 #[test]
1417 fn a_waived_fee_is_a_zero_price_and_g1t_s_own_is_fully_discounted() {
1418 let waived = create_fields(&row(100_000_000, false, 0), "cus_1", "prod_plan", 2_100, false);
1419 let get = |fields: &[(String, String)], key: &str| fields.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone());
1420 assert_eq!(get(&waived, "items[0][price_data][unit_amount]").as_deref(), Some("0"));
1421 assert_eq!(get(&waived, "metadata[g1t_grant]").as_deref(), Some("cmp_1"));
1422 assert_eq!(get(&waived, "metadata[g1t_allowance_micros]").as_deref(), Some("100000000"));
1423 assert!(get(&waived, "discounts[0][coupon]").is_none());
1424 assert!(get(&waived, "collection_method").is_none(), "nothing to collect");
1425 let custom = create_fields(&row(500_000_000, false, 5_000), "cus_1", "prod_plan", 2_100, false);
1426 assert_eq!(get(&custom, "items[0][price_data][unit_amount]").as_deref(), Some("5000"));
1427 assert_eq!(get(&custom, "collection_method").as_deref(), Some("send_invoice"));
1428 assert_eq!(get(&custom, "automatic_tax[enabled]").as_deref(), Some("true"));
1429 // Only g1t's own (internal terms) is fully discounted; anyone else
1430 // unlimited with no fee is a $0 price, and may convert.
1431 assert!(fully_discounted(&row(0, true, 0), true));
1432 assert!(!fully_discounted(&row(0, true, 0), false));
1433 assert!(!fully_discounted(&row(0, true, 100), true));
1434 let own = create_fields(&row(0, true, 0), "cus_1", "prod_plan", 2_100, true);
1435 assert_eq!(get(&own, "items[0][price_data][unit_amount]").as_deref(), Some("2100"));
1436 assert_eq!(get(&own, "discounts[0][coupon]").as_deref(), Some(FULL_DISCOUNT_COUPON));
1437 }
1438
1439 fn subscription(json: serde_json::Value) -> StripeSubscription {
1440 serde_json::from_value(json).unwrap()
1441 }
1442
1443 #[test]
1444 fn a_conversion_counts_only_once_stripe_applied_it() {
1445 let item = |price: &str| serde_json::json!({"data": [{"id": "si_1", "price": {"id": price, "unit_amount": 2100}}]});
1446 let converted = |price: Option<&str>, s: &StripeSubscription| converted(price, s, 2_100, true);
1447 // Paid: the item has the plan's price and nothing waits.
1448 assert!(converted(Some("price_comp"), &subscription(serde_json::json!({"id": "sub_1", "status": "active", "items": item("price_plan")}))));
1449 // A staff terms change gives the item a new price too: never a
1450 // conversion without one in progress, nor at another amount.
1451 let terms = subscription(serde_json::json!({"id": "sub_1", "status": "active", "items": item("price_plan")}));
1452 assert!(!super::converted(Some("price_comp"), &terms, 2_100, false));
1453 let fee = subscription(serde_json::json!({"id": "sub_1", "status": "active", "items": {"data": [{"id": "si_1", "price": {"id": "price_fee", "unit_amount": 2500}}]}}));
1454 assert!(!super::converted(Some("price_comp"), &fee, 2_100, true));
1455 // 3DS or declined: Stripe keeps the old price behind a pending update.
1456 let waiting = subscription(serde_json::json!({"id": "sub_1", "status": "active", "items": item("price_comp"), "pending_update": {"subscription_items": []}}));
1457 assert!(!converted(Some("price_comp"), &waiting));
1458 let pending_new = subscription(serde_json::json!({"id": "sub_1", "status": "active", "items": item("price_plan"), "pending_update": {"x": 1}}));
1459 assert!(!converted(Some("price_comp"), &pending_new));
1460 // Unchanged, canceled, or nothing known to compare with.
1461 assert!(!converted(Some("price_comp"), &subscription(serde_json::json!({"id": "sub_1", "status": "active", "items": item("price_comp")}))));
1462 assert!(!converted(Some("price_comp"), &subscription(serde_json::json!({"id": "sub_1", "status": "canceled", "items": item("price_plan")}))));
1463 assert!(!converted(None, &subscription(serde_json::json!({"id": "sub_1", "status": "active", "items": item("price_plan")}))));
1464 }
1465
1466 #[test]
1467 fn a_conversion_waits_on_its_payment() {
1468 let fields = conversion_fields("si_1", "prod_plan", 2_100);
1469 let get = |key: &str| fields.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str());
1470 assert_eq!(get("payment_behavior"), Some("pending_if_incomplete"));
1471 assert_eq!(get("items[0][id]"), Some("si_1"));
1472 assert_eq!(get("items[0][price_data][unit_amount]"), Some("2100"));
1473 assert_eq!(get("billing_cycle_anchor"), Some("now"));
1474 }
1475
1476 #[test]
1477 fn alerts_count_what_admission_counts() {
1478 let r = row(100_000_000, false, 0);
1479 assert!(alert("acme", &month_standing("2026-10", &r, 40_000_000, 0, 0, 0, false)).is_none());
1480 let half = alert("acme", &month_standing("2026-10", &r, 30_000_000, 15_000_000, 5_000_000, 0, false)).unwrap();
1481 assert_eq!((half.meter.as_str(), half.level, half.used_micros), ("complimentary", 50, 50_000_000));
1482 let done = alert("acme", &month_standing("2026-10", &r, 100_000_000, 0, 0, 0, false)).unwrap();
1483 assert_eq!(done.level, 100);
1484 assert!(done.message.contains("2026-11-01") && !done.message.contains("charged at"));
1485 assert!(alert("acme", &month_standing("2026-10", &row(0, true, 0), 1, 0, 0, 0, false)).is_none());
1486 }
1487
1488 #[test]
1489 fn owed_steps_round_trip() {
1490 for op in [Op::Create, Op::Cancel, Op::Terms { period: "cmpp_1".into() }, Op::Convert { attempt: 2, by: "ana".into(), started_ms: 5 }] {
1491 assert_eq!(Op::parse(Some(&op.text())), Some(op));
1492 }
1493 assert_eq!(Op::Create.text(), r#"{"op":"create"}"#);
1494 assert!(Op::Convert { attempt: 1, by: "a".into(), started_ms: 0 }.text().starts_with(r#"{"op":"convert""#));
1495 assert_eq!(Op::parse(Some("nonsense")), None);
1496 }
1497}