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,000 linesCodeBlameRaw
1//! A workspace's invoices from g1t.
2//!
3//! Every time g1t charges a workspace's card, it is a real Stripe invoice:
4//! when each month closes, and when the workspace nears its limit mid-month
5//! (a threshold invoice, as Cloudflare and Fly do). Each is itemised by
6//! what was used since the last one, with any credit paid in advance taken
7//! off and anything left unpaid from before added, so its total is exactly
8//! what is owed. Stripe charges the card, emails the receipt, and keeps
9//! the invoice and its PDF in the workspace's billing page.
10
11use g1t_contracts::billing::{EntryKind, InvoiceItem, InvoicesArgs, WorkspaceInvoice};
12use g1t_contracts::time::rfc3339;
13use g1t_contracts::{FailureCode, Outcome};
14use g1t_kit::now_ms;
15use serde::Deserialize;
16use serde_json::Value;
17use worker::Result;
18use worker::wasm_bindgen::JsValue;
19
20use crate::Billing;
21
22/// The invoice's lines: what was used since the last one, by kind, then
23/// whatever makes the total what is owed.
24pub(crate) fn invoice_lines(used: &[(String, i64)], owed: i64) -> Vec<InvoiceItem> {
25 let mut lines: Vec<InvoiceItem> = used
26 .iter()
27 .filter(|(_, amount)| *amount > 0)
28 .map(|(kind, amount)| InvoiceItem { description: kind.clone(), amount_micros: *amount })
29 .collect();
30 let difference = owed - lines.iter().map(|l| l.amount_micros).sum::<i64>();
31 if difference < 0 {
32 lines.push(InvoiceItem { description: "Paid in advance".to_owned(), amount_micros: difference });
33 } else if difference > 0 {
34 lines.push(InvoiceItem { description: "Unpaid from earlier".to_owned(), amount_micros: difference });
35 }
36 lines
37}
38
39/// Each line in whole cents, for the card processor. Their sum is what is
40/// owed rounded up to the next cent, never down: cutting each line to a
41/// cent on its own would charge up to a cent less per line than is owed (and
42/// a credit line a cent less of a credit), and leave the rest stranded under
43/// the minimum charge. The cent each needs is given to the lines with the
44/// largest fractions first.
45pub(crate) fn line_cents(lines: &[InvoiceItem]) -> Vec<i64> {
46 const MICROS_PER_CENT: i64 = 10_000;
47 let total: i64 = lines.iter().map(|l| l.amount_micros).sum();
48 let total_cents = total.div_euclid(MICROS_PER_CENT) + i64::from(total.rem_euclid(MICROS_PER_CENT) > 0);
49 let mut cents: Vec<i64> = lines.iter().map(|l| l.amount_micros.div_euclid(MICROS_PER_CENT)).collect();
50 let mut short = total_cents - cents.iter().sum::<i64>();
51 let mut by_fraction: Vec<usize> = (0..lines.len()).collect();
52 by_fraction.sort_by_key(|&i| std::cmp::Reverse(lines[i].amount_micros.rem_euclid(MICROS_PER_CENT)));
53 for i in by_fraction {
54 if short <= 0 || lines[i].amount_micros.rem_euclid(MICROS_PER_CENT) == 0 {
55 break;
56 }
57 cents[i] += 1;
58 short -= 1;
59 }
60 cents
61}
62
63/// Whether paying an invoice failed because the card said no (Stripe's
64/// 402, a `card_error`), rather than because Stripe could not be reached,
65/// was busy or failed itself. Only a decline stops a workspace's work.
66pub(crate) fn is_decline(error: &str) -> bool {
67 error.contains("answered 402") || error.contains("\"card_error\"")
68}
69
70/// The HTTP status in an error from the card processor ("the card
71/// processor answered 400: …"), if it is one.
72fn answered(error: &str) -> Option<u16> {
73 let rest = error.split_once("the card processor answered ")?.1;
74 rest.get(..3)?.parse().ok()
75}
76
77/// Whether Stripe refused to charge for good, for want of a way to pay:
78/// a 400 or 404 `invalid_request_error` (no default payment method, a
79/// payment method gone or unusable off session, and the like). Trying
80/// again never changes that, so it stops like a decline, with Stripe's
81/// reason. A 5xx, a 429 or no answer at all is tried again, and so is a
82/// 401 or 403: g1t's key, never the payer's card.
83pub(crate) fn needs_card(error: &str) -> bool {
84 matches!(answered(error), Some(400 | 404)) && error.contains("\"invalid_request_error\"")
85}
86
87/// Stripe's own sentence in an error, else the error, short.
88pub(crate) fn stripe_reason(error: &str) -> String {
89 error
90 .split_once(": ")
91 .and_then(|(_, body)| serde_json::from_str::<Value>(body).ok())
92 .and_then(|body| body["error"]["message"].as_str().map(str::to_owned))
93 .unwrap_or_else(|| error.to_owned())
94 .chars()
95 .take(200)
96 .collect()
97}
98
99/// What charging an invoice came to.
100#[derive(Debug, PartialEq, Eq)]
101pub(crate) enum PayOutcome {
102 Paid,
103 /// The card said no: work stops until it is paid.
104 Declined,
105 /// Stripe will not charge it at all (no card it can use): work stops
106 /// until one is added and it is paid, as for a decline.
107 NeedsCard,
108 /// Voided or marked uncollectible at Stripe meanwhile: never to be paid.
109 Void,
110 /// Stripe did not answer, or failed: tried again, nobody stopped.
111 Retry,
112}
113
114/// What charging an invoice came to, from the invoice's status after the
115/// attempt and the error the attempt came back with, if any.
116pub(crate) fn pay_outcome(status: Option<&str>, error: Option<&str>) -> PayOutcome {
117 match status {
118 Some("paid") => PayOutcome::Paid,
119 Some("void" | "uncollectible") => PayOutcome::Void,
120 _ => match error {
121 Some(error) if is_decline(error) => PayOutcome::Declined,
122 Some(error) if needs_card(error) => PayOutcome::NeedsCard,
123 _ => PayOutcome::Retry,
124 },
125 }
126}
127
128/// The idempotency key of a workspace invoice's draft and lines: the same
129/// workspace, customer, reason, period and lines (each as the cents sent
130/// to Stripe) is the same invoice however often it is attempted, until one
131/// of its invoices is paid (`paid_today` counts the workspace's invoices
132/// paid that day) or voided (`voided` counts them), when the next is a new
133/// one. With the customer in it, a key never comes back with another
134/// customer's fields, which Stripe refuses. With every line's cents in it,
135/// a retry after usage moved a line (but not the total's cent) is a new
136/// draft, never the old key with other parameters, which Stripe refuses
137/// until the key expires.
138pub(crate) fn invoice_key(
139 workspace: &str,
140 customer: &str,
141 reason: &str,
142 period: &str,
143 lines: &[(&str, i64)],
144 paid_today: u32,
145 voided: u32,
146) -> String {
147 let total: i64 = lines.iter().map(|(_, cents)| cents).sum();
148 // FNV-1a over every line, so the key stays short however many there are.
149 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
150 for (description, cents) in lines {
151 for byte in format!("{description}={cents};").bytes() {
152 hash = (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3);
153 }
154 }
155 let key = format!("ws-invoice/{workspace}/{customer}/{reason}/{period}/{total}/{hash:016x}/p{paid_today}");
156 if voided == 0 { key } else { format!("{key}/v{voided}") }
157}
158
159/// Whether `viewer` may see a workspace's invoices: its owners and billing
160/// managers, as for its billing details; never any other member.
161pub(crate) fn may_see_invoices(viewer: &g1t_contracts::Viewer, workspace: &str) -> bool {
162 viewer.as_ref().is_some_and(|viewer| viewer.manages_billing(workspace))
163}
164
165/// What settling a workspace's unpaid invoices came to.
166pub(crate) enum Settled {
167 /// None is left unpaid; the last one paid now, if any.
168 Clear(Option<WorkspaceInvoice>),
169 /// One is still unpaid (declined, no card, or Stripe not finished):
170 /// nothing else is invoiced until it is paid.
171 Stop(std::result::Result<WorkspaceInvoice, String>),
172}
173
174#[derive(Deserialize)]
175struct Count {
176 n: Option<f64>,
177}
178
179/// A workspace invoice's draft: charged to the card, taxed by Stripe Tax,
180/// and holding only the lines put on it, never whatever is pending on the
181/// customer.
182fn draft_fields(customer: &str, workspace: &str, reason: &str, period: &str, description: String) -> Vec<(&'static str, String)> {
183 let mut fields = vec![
184 ("customer", customer.to_owned()),
185 ("collection_method", "charge_automatically".to_owned()),
186 ("auto_advance", "false".to_owned()),
187 ("pending_invoice_items_behavior", "exclude".to_owned()),
188 ("description", description),
189 ("metadata[g1t_workspace]", workspace.to_owned()),
190 ("metadata[reason]", reason.to_owned()),
191 ("metadata[period]", period.to_owned()),
192 ];
193 fields.extend(crate::stripe::invoice_tax_fields());
194 fields
195}
196
197/// One line, on the draft `invoice`, at g1t's tax code, excluding tax.
198fn item_fields(customer: &str, invoice: &str, workspace: &str, description: &str, cents: i64) -> Vec<(&'static str, String)> {
199 let mut fields = vec![
200 ("customer", customer.to_owned()),
201 ("invoice", invoice.to_owned()),
202 ("amount", cents.to_string()),
203 ("currency", "usd".to_owned()),
204 ("description", description.to_owned()),
205 ("metadata[workspace]", workspace.to_owned()),
206 ];
207 fields.extend(crate::stripe::item_tax_fields());
208 fields
209}
210
211/// Whether Stripe left an invoice a draft because Stripe Tax could not
212/// place the customer.
213pub(crate) fn needs_tax_location(invoice: &Value) -> bool {
214 invoice["automatic_tax"]["status"].as_str() == Some("requires_location_inputs")
215}
216
217#[derive(Deserialize)]
218struct InvoiceRow {
219 invoice_id: String,
220 workspace: String,
221 reason: String,
222 period: String,
223 amount_micros: i64,
224 status: String,
225 hosted_url: Option<String>,
226 pdf_url: Option<String>,
227 created_at: String,
228 #[serde(default)]
229 fee_micros: i64,
230 #[serde(default)]
231 tax_micros: i64,
232}
233
234#[derive(Deserialize)]
235struct LineRow {
236 description: String,
237 amount_micros: i64,
238}
239
240/// A Stripe invoice, as far as billing reads it.
241#[derive(Deserialize)]
242struct StripeInvoice {
243 id: String,
244 #[serde(default)]
245 status: Option<String>,
246 #[serde(default)]
247 hosted_invoice_url: Option<String>,
248 #[serde(default)]
249 invoice_pdf: Option<String>,
250 #[serde(default)]
251 amount_paid: i64,
252 #[serde(default)]
253 charge: Option<String>,
254 #[serde(default)]
255 payment_intent: Option<String>,
256 /// The tax Stripe added, in cents (`tax`, in this API version).
257 #[serde(default)]
258 tax: Option<i64>,
259}
260
261impl StripeInvoice {
262 fn tax_cents(&self) -> i64 {
263 self.tax.unwrap_or(0).max(0)
264 }
265}
266
267impl Billing {
268 /// Invoices the workspace for what it owes, charging its card. `Ok(Err)`
269 /// says why not, when there was nothing to do or no card.
270 pub(crate) async fn invoice_workspace(
271 &self,
272 workspace: &str,
273 reason: &str,
274 period: &str,
275 ) -> Result<std::result::Result<WorkspaceInvoice, String>> {
276 let Some(stripe) = &self.stripe else { return Ok(Err("Payments are not set up.".into())) };
277 if self.row(workspace).await?.and_then(|row| row.customer_id).is_none() {
278 return Ok(Err("No card on file.".into()));
279 }
280 // An invoice already sent and not paid is paid (or found paid, or
281 // found voided) before any other is made: two invoices for the same
282 // money would charge it twice. Once all are paid, what is owed
283 // beyond them is invoiced too; one still unpaid stops here.
284 let settled = match self.settle_unpaid_invoices(workspace).await? {
285 Settled::Clear(paid) => paid,
286 Settled::Stop(unpaid) => return Ok(unpaid),
287 };
288 // Read again: paying an earlier invoice credited the balance.
289 let Some(account) = self.row(workspace).await? else { return Ok(Err("Nothing billed yet.".into())) };
290 let Some(customer) = account.customer_id else { return Ok(Err("No card on file.".into())) };
291 // AI credit left (bought before g1t stopped selling it) props up the
292 // balance but pays only for models: it is not money for anything else.
293 let owed = self.owed_with(workspace, account.balance_micros).await?;
294 // Nothing owed is nothing to invoice, whatever the reason: a limit
295 // reached on usage not entered yet is not a charge. An earlier
296 // invoice paid just now is what this came to.
297 if owed <= 0 {
298 return Ok(settled.ok_or_else(|| "Nothing is owed.".to_owned()));
299 }
300 // Only a month's close charges no less than the minimum
301 // (`MIN_CHARGE_MICROS`), so a payment's fee is never most of it;
302 // less carries over. A charge because a limit was reached always
303 // goes through, whatever its size, so a new workspace's limit never
304 // strands it.
305 if reason == "month" && !crate::limits::worth_charging(owed, self.plans.min_charge_micros) {
306 if let Some(paid) = settled {
307 return Ok(Ok(paid));
308 }
309 return Ok(Err(format!(
310 "{} is owed, under the {} minimum charge; it carries over to the next invoice.",
311 crate::features::dollars(owed),
312 crate::features::dollars(self.plans.min_charge_micros)
313 )));
314 }
315 // What was used since the last invoice, by kind.
316 #[derive(Deserialize)]
317 struct Last {
318 through_at: Option<String>,
319 }
320 let since = self
321 .db
322 .prepare("SELECT MAX(through_at) AS through_at FROM workspace_invoices WHERE workspace = ? AND status <> 'void'")
323 .bind(&[workspace.into()])?
324 .first::<Last>(None)
325 .await?
326 .and_then(|l| l.through_at)
327 .unwrap_or_default();
328 #[derive(Deserialize)]
329 struct Used {
330 kind: String,
331 charged: Option<i64>,
332 }
333 let now = rfc3339(now_ms());
334 let used: Vec<(String, i64)> = self
335 .db
336 .prepare(
337 "SELECT CASE
338 WHEN task = 'sandbox' AND machine IS NOT NULL THEN 'Runner minutes, ' || machine
339 WHEN task = 'sandbox' THEN 'Sandbox time'
340 WHEN task = 'self_hosted' THEN 'Self-hosted runner time'
341 WHEN task = 'deployments' THEN 'Deployments: builds and usage past the plan'
342 WHEN task = 'security' THEN 'Security scans'
343 WHEN task = 'context' THEN 'Search embeddings'
344 WHEN task = 'storage' THEN 'Private repository storage'
345 WHEN task = 'git' THEN 'Git operations'
346 WHEN task = 'cache' THEN 'Actions cache storage'
347 WHEN task = 'domains' THEN 'Custom domains'
348 WHEN task = 'package_storage' THEN 'Package storage'
349 WHEN task = 'telemetry' THEN 'Observability data'
350 -- From when g1t ran agents and sold model access.
351 WHEN billed_to = 'workspace' THEN 'Runs on your own model provider'
352 WHEN reference LIKE 'run%' OR task = 'gateway' THEN 'Agents on g1t''s models'
353 ELSE 'Other usage' END AS kind,
354 -SUM(amount_micros) AS charged
355 FROM ledger WHERE workspace = ? AND kind = 'usage' AND created_at > ? AND created_at <= ?
356 GROUP BY 1 ORDER BY charged DESC",
357 )
358 .bind(&[workspace.into(), since.as_str().into(), now.as_str().into()])?
359 .all()
360 .await?
361 .results::<Used>()?
362 .into_iter()
363 .map(|u| (u.kind, u.charged.unwrap_or(0)))
364 .collect();
365 let lines = invoice_lines(&used, owed);
366 // Stripe Tax needs to know where the customer is. Without an
367 // address nothing is charged: the owners are asked for one, and the
368 // charge goes through once it is there.
369 match stripe.customer_placed(&customer).await {
370 Ok(true) => {}
371 Ok(false) => {
372 self.tax_address_needed(workspace).await?;
373 return Ok(Err(crate::tax::address_needed_message(workspace)));
374 }
375 // A customer Stripe no longer has: forgotten, so the next card
376 // saved makes a new one.
377 Err(error) if crate::stripe::is_missing(&error) => {
378 self.forget_customer(workspace).await?;
379 return Ok(Err("No card on file.".into()));
380 }
381 Err(error) => return Err(error),
382 }
383 // Each line in cents; no card fee on top, as g1t's prices include it.
384 let cents = line_cents(&lines);
385 // How many of its invoices were voided, and paid today, is part of
386 // the key: one voided at Stripe, or paid, is never handed back for
387 // the same total within the day.
388 let today = &now[..10];
389 let count = |sql: &'static str| async move {
390 Ok::<u32, worker::Error>(
391 self.db
392 .prepare(sql)
393 .bind(&[workspace.into(), today.into()])?
394 .first::<Count>(None)
395 .await?
396 .and_then(|c| c.n)
397 .unwrap_or(0.0) as u32,
398 )
399 };
400 let voided = count("SELECT COUNT(*) AS n FROM workspace_invoices WHERE workspace = ?1 AND status = 'void' AND ?2 IS NOT NULL").await?;
401 let paid_today =
402 count("SELECT COUNT(*) AS n FROM workspace_invoices WHERE workspace = ?1 AND status = 'paid' AND substr(paid_at, 1, 10) = ?2")
403 .await?;
404 let keyed: Vec<(&str, i64)> = lines.iter().zip(&cents).map(|(line, cents)| (line.description.as_str(), *cents)).collect();
405 let key = invoice_key(workspace, &customer, reason, period, &keyed, paid_today, voided);
406 let description = match reason {
407 "month" => format!("g1t usage for {workspace}, {period}"),
408 _ => format!("g1t usage for {workspace}, charged as it neared its limit"),
409 };
410 // The draft first, then its lines on it: lines left pending on the
411 // customer by an attempt that failed half way would otherwise be
412 // swept into the next invoice (this one's retry with a different
413 // total, or the plan's renewal) on top of their own new lines.
414 let draft: StripeInvoice =
415 stripe.post_idempotent("/invoices", &draft_fields(&customer, workspace, reason, period, description), &key).await?;
416 for (position, (line, cents)) in lines.iter().zip(&cents).enumerate() {
417 let fields = item_fields(&customer, &draft.id, workspace, &line.description, *cents);
418 let _: Value = stripe.post_idempotent("/invoiceitems", &fields, &format!("{key}/item/{position}")).await?;
419 }
420 // A retry finds it finalized already; that is fine.
421 let finalized = stripe.post::<Value>(&format!("/invoices/{}/finalize", draft.id), &[]).await;
422 if let Err(error) = &finalized
423 && crate::stripe::is_tax_location_error(error)
424 {
425 self.tax_address_needed(workspace).await?;
426 return Ok(Err(crate::tax::address_needed_message(workspace)));
427 }
428 if let Ok(left) = stripe.get::<Value>(&format!("/invoices/{}", draft.id)).await
429 && left["status"].as_str() == Some("draft")
430 && needs_tax_location(&left)
431 {
432 self.tax_address_needed(workspace).await?;
433 return Ok(Err(crate::tax::address_needed_message(workspace)));
434 }
435 self.tax_address_given(workspace).await?;
436 // Kept as open before the card is charged: whatever happens to the
437 // charge, the invoice is known here, so paying it later (on Stripe's
438 // page, or by the next attempt) is credited, and no second invoice
439 // is made for the same money while it is open.
440 let invoice: StripeInvoice = stripe.get(&format!("/invoices/{}", draft.id)).await?;
441 let total = lines.iter().map(|l| l.amount_micros).sum::<i64>();
442 let tax_micros = invoice.tax_cents() * 10_000;
443 let mut writes = vec![self
444 .db
445 .prepare(
446 "INSERT OR REPLACE INTO workspace_invoices
447 (invoice_id, workspace, reason, period, amount_micros, status, hosted_url, pdf_url, through_at, created_at, paid_at,
448 fee_micros, tax_micros)
449 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
450 )
451 .bind(&[
452 invoice.id.as_str().into(),
453 workspace.into(),
454 reason.into(),
455 period.into(),
456 (total as f64).into(),
457 "open".into(),
458 crate::optional(invoice.hosted_invoice_url.as_deref()),
459 crate::optional(invoice.invoice_pdf.as_deref()),
460 now.as_str().into(),
461 now.as_str().into(),
462 JsValue::NULL,
463 // No card fee on new invoices; the column keeps old ones'.
464 0.into(),
465 (tax_micros as f64).into(),
466 ])?];
467 for (position, line) in lines.iter().enumerate() {
468 writes.push(
469 self.db
470 .prepare("INSERT OR REPLACE INTO workspace_invoice_lines (invoice_id, position, description, amount_micros) VALUES (?, ?, ?, ?)")
471 .bind(&[invoice.id.as_str().into(), (position as u32).into(), line.description.as_str().into(), (line.amount_micros as f64).into()])?,
472 );
473 }
474 self.db.batch(writes).await?;
475 self.pay_invoice(workspace, &invoice.id, 0).await
476 }
477
478 /// Charges the card for an invoice kept as open (or declined before),
479 /// and records what came of it. Paid: credited, then marked paid. The
480 /// card saying no, or Stripe refusing for want of a card it can charge:
481 /// marked failed, and work stops until it is paid. Voided at Stripe
482 /// meanwhile: closed here.
483 /// Stripe failing to answer, or answering busy, is g1t's problem and
484 /// never stops a payer: the invoice stays open, an `Err` says so, and
485 /// the next attempt pays this same invoice.
486 async fn pay_invoice(&self, workspace: &str, invoice_id: &str, fee_cents: i64) -> Result<std::result::Result<WorkspaceInvoice, String>> {
487 let Some(stripe) = &self.stripe else { return Ok(Err("Payments are not set up.".into())) };
488 let paid = stripe.post::<StripeInvoice>(&format!("/invoices/{invoice_id}/pay"), &[("off_session", "true".to_owned())]).await;
489 let invoice: StripeInvoice = stripe.get(&format!("/invoices/{invoice_id}")).await?;
490 let error = paid.as_ref().err().map(ToString::to_string);
491 match pay_outcome(invoice.status.as_deref(), error.as_deref()) {
492 PayOutcome::Paid => {
493 self.credit_invoice(workspace, &invoice, fee_cents).await?;
494 self.db
495 .prepare("UPDATE workspace_invoices SET status = 'paid', paid_at = ?, tax_micros = ? WHERE invoice_id = ?")
496 .bind(&[rfc3339(now_ms()).into(), ((invoice.tax_cents() * 10_000) as f64).into(), invoice_id.into()])?
497 .run()
498 .await?;
499 }
500 PayOutcome::Declined => {
501 self.db
502 .prepare("UPDATE workspace_invoices SET status = 'failed' WHERE invoice_id = ? AND status <> 'paid'")
503 .bind(&[invoice_id.into()])?
504 .run()
505 .await?;
506 let error = error.map_or_else(|| "the card was declined".to_owned(), |e| e.chars().take(200).collect());
507 self.mark_declined(workspace, &error).await?;
508 }
509 PayOutcome::NeedsCard => {
510 self.db
511 .prepare("UPDATE workspace_invoices SET status = 'failed' WHERE invoice_id = ? AND status <> 'paid'")
512 .bind(&[invoice_id.into()])?
513 .run()
514 .await?;
515 let why = format!(
516 "Stripe could not charge the card on file ({}). Add a card on the Billing page.",
517 stripe_reason(error.as_deref().unwrap_or_default()).trim_end_matches('.')
518 );
519 self.mark_declined(workspace, &why).await?;
520 }
521 PayOutcome::Void => {
522 self.void_row(invoice_id).await?;
523 return Ok(Err(format!("Invoice {invoice_id} was voided at Stripe.")));
524 }
525 PayOutcome::Retry => {
526 let why = error.unwrap_or_else(|| format!("invoice is {}", invoice.status.as_deref().unwrap_or("unknown")));
527 return Err(worker::Error::RustError(format!(
528 "{workspace}: the card processor did not finish paying invoice {invoice_id}; it is tried again: {}",
529 why.chars().take(200).collect::<String>()
530 )));
531 }
532 }
533 Ok(self.workspace_invoice(invoice_id).await?.ok_or_else(|| "The invoice was not recorded.".to_owned()))
534 }
535
536 /// Settles the workspace's invoices that were sent but not paid
537 /// (`open`, or `failed` for a decline) before another is made, oldest
538 /// first: found paid at Stripe, credited; voided or gone, closed here;
539 /// still open, charged again. Each paid goes on to the next; the first
540 /// still unpaid after its charge stops it there.
541 async fn settle_unpaid_invoices(&self, workspace: &str) -> Result<Settled> {
542 let Some(stripe) = &self.stripe else { return Ok(Settled::Clear(None)) };
543 #[derive(Deserialize)]
544 struct Unpaid {
545 invoice_id: String,
546 #[serde(default)]
547 fee_micros: i64,
548 }
549 let unpaid = self
550 .db
551 .prepare("SELECT invoice_id, fee_micros FROM workspace_invoices WHERE workspace = ? AND status IN ('open', 'failed') ORDER BY created_at")
552 .bind(&[workspace.into()])?
553 .all()
554 .await?
555 .results::<Unpaid>()?;
556 let mut last_paid = None;
557 for row in unpaid {
558 let found = match stripe.get::<StripeInvoice>(&format!("/invoices/{}", row.invoice_id)).await {
559 Ok(found) => found,
560 // From another Stripe account: never to be paid here.
561 Err(error) if crate::stripe::is_missing(&error) => {
562 self.void_row(&row.invoice_id).await?;
563 continue;
564 }
565 Err(error) => return Err(error),
566 };
567 let charged = match found.status.as_deref() {
568 Some("paid") => {
569 self.workspace_invoice_paid(&row.invoice_id).await?;
570 last_paid = self.workspace_invoice(&row.invoice_id).await?.or(last_paid);
571 continue;
572 }
573 Some("void") | Some("uncollectible") => {
574 self.void_row(&row.invoice_id).await?;
575 continue;
576 }
577 Some("draft") => {
578 // Kept before it was finalized: finalize, then charge.
579 let _ = stripe.post::<Value>(&format!("/invoices/{}/finalize", row.invoice_id), &[]).await;
580 self.pay_invoice(workspace, &row.invoice_id, row.fee_micros / 10_000).await?
581 }
582 _ => self.pay_invoice(workspace, &row.invoice_id, row.fee_micros / 10_000).await?,
583 };
584 match charged {
585 Ok(invoice) if invoice.status == "paid" => last_paid = Some(invoice),
586 // Voided at Stripe as it was charged: closed, on to the next.
587 Err(_) if self.workspace_invoice(&row.invoice_id).await?.is_some_and(|i| i.status == "void") => {}
588 other => return Ok(Settled::Stop(other)),
589 }
590 }
591 Ok(Settled::Clear(last_paid))
592 }
593
594 async fn void_row(&self, invoice_id: &str) -> Result<()> {
595 self.db
596 .prepare("UPDATE workspace_invoices SET status = 'void' WHERE invoice_id = ? AND status <> 'paid'")
597 .bind(&[invoice_id.into()])?
598 .run()
599 .await?;
600 Ok(())
601 }
602
603 /// A late `invoice.payment_failed` for a workspace invoice: the invoice
604 /// is read as it is now, and a decline counts only if it is still
605 /// unpaid (a retry or Stripe's page may have paid it since).
606 pub(crate) async fn workspace_invoice_failed(&self, invoice_id: &str, tagged: &str) -> Result<String> {
607 let Some(stripe) = &self.stripe else { return Ok("ignored: payments off".to_owned()) };
608 let invoice: StripeInvoice = stripe.get(&format!("/invoices/{invoice_id}")).await?;
609 if matches!(invoice.status.as_deref(), Some("paid" | "void" | "uncollectible")) {
610 return Ok(format!("ignored: invoice {invoice_id} is {} now", invoice.status.as_deref().unwrap_or_default()));
611 }
612 // The invoice's own row names the workspace as it is now; the
613 // metadata keeps the slug it was sent under.
614 let workspace = self.workspace_of_invoice(invoice_id).await?.unwrap_or_else(|| tagged.to_owned());
615 self.db
616 .prepare("UPDATE workspace_invoices SET status = 'failed' WHERE invoice_id = ? AND status = 'open'")
617 .bind(&[invoice_id.into()])?
618 .run()
619 .await?;
620 self.mark_declined(&workspace, "the card was declined for an invoice").await?;
621 Ok(format!("{workspace}: invoice payment failed; work stopped"))
622 }
623
624 /// Enters an invoice's payment once, with the kind of card that paid:
625 /// what it paid for usage, never its tax or card fee, which are kept
626 /// apart (`tax_and_fees`).
627 async fn credit_invoice(&self, workspace: &str, invoice: &StripeInvoice, fee_cents: i64) -> Result<bool> {
628 let seen = self
629 .db
630 .prepare("SELECT id FROM ledger WHERE reference = ?")
631 .bind(&[invoice.id.as_str().into()])?
632 .first::<Value>(None)
633 .await?;
634 if seen.is_some() {
635 return Ok(false);
636 }
637 let extras = crate::tax::Extras { tax_cents: invoice.tax_cents(), fee_cents };
638 let amount = (invoice.amount_paid - extras.tax_cents - extras.fee_cents).max(0) * 10_000;
639 if amount <= 0 {
640 return Ok(false);
641 }
642 let description = format!("Paid invoice {}{}", invoice.id, crate::tax::paid_with(extras));
643 match self.enter(workspace, EntryKind::TopUp, amount, &description, &invoice.id, None, None, None).await {
644 Ok(()) => {}
645 // Credited a moment ago by the event or the charge alongside.
646 Err(error) if crate::is_duplicate(&error) => return Ok(false),
647 Err(error) => return Err(error),
648 }
649 self.record_extras(workspace, &invoice.id, invoice.payment_intent.as_deref(), extras, None).await?;
650 // Prepaid cards pay, but never raise the limit.
651 if let (Some(stripe), Some(charge)) = (&self.stripe, &invoice.charge)
652 && let Ok(charge) = stripe.get::<Value>(&format!("/charges/{charge}")).await
653 && let Some(funding) = charge["payment_method_details"]["card"]["funding"].as_str() {
654 self.db
655 .prepare("UPDATE ledger SET funding = ? WHERE reference = ?")
656 .bind(&[funding.into(), invoice.id.as_str().into()])?
657 .run()
658 .await?;
659 }
660 Ok(true)
661 }
662
663 pub(crate) async fn mark_declined(&self, workspace: &str, error: &str) -> Result<()> {
664 let now = rfc3339(now_ms());
665 self.db
666 .prepare(
667 "INSERT INTO limits (workspace, autopay_failed_at, autopay_error, updated_at) VALUES (?1, ?2, ?3, ?2)
668 ON CONFLICT (workspace) DO UPDATE SET autopay_failed_at = ?2, autopay_error = ?3, updated_at = ?2",
669 )
670 .bind(&[workspace.into(), now.as_str().into(), error.into()])?
671 .run()
672 .await?;
673 Ok(())
674 }
675
676 /// Why the workspace's card last could not be charged, while it still
677 /// cannot.
678 pub(crate) async fn autopay_error(&self, workspace: &str) -> Result<Option<String>> {
679 #[derive(Deserialize)]
680 struct Row {
681 autopay_error: Option<String>,
682 }
683 Ok(self
684 .db
685 .prepare("SELECT autopay_error FROM limits WHERE workspace = ? AND autopay_failed_at IS NOT NULL")
686 .bind(&[workspace.into()])?
687 .first::<Row>(None)
688 .await?
689 .and_then(|row| row.autopay_error))
690 }
691
692 /// A workspace invoice paid later, on Stripe's page or by a retry. One
693 /// g1t made but never kept (from before invoices were kept as open
694 /// before they were charged) is found by its `metadata[g1t_workspace]`
695 /// and kept now. Credited first, then marked paid. None when it is not
696 /// a workspace invoice.
697 pub(crate) async fn workspace_invoice_paid(&self, invoice_id: &str) -> Result<Option<String>> {
698 #[derive(Deserialize)]
699 struct Row {
700 workspace: String,
701 #[serde(default)]
702 fee_micros: i64,
703 }
704 let Some(stripe) = &self.stripe else { return Ok(None) };
705 let kept = self
706 .db
707 .prepare("SELECT workspace, fee_micros FROM workspace_invoices WHERE invoice_id = ?")
708 .bind(&[invoice_id.into()])?
709 .first::<Row>(None)
710 .await?;
711 // Read at billing's API version, whatever the event's was.
712 let found: Value = stripe.get(&format!("/invoices/{invoice_id}")).await?;
713 let invoice: StripeInvoice = serde_json::from_value(found.clone()).map_err(|e| worker::Error::RustError(e.to_string()))?;
714 let row = match kept {
715 Some(row) => row,
716 None => {
717 let Some(tagged) = found["metadata"]["g1t_workspace"].as_str().filter(|_| crate::webhooks::invoice_subscription(&found).is_none())
718 else {
719 return Ok(None);
720 };
721 // Under its slug now, by its customer; else as it was sent.
722 let workspace = match found["customer"].as_str() {
723 Some(customer) => self.workspace_of_customer(customer).await?,
724 None => None,
725 }
726 .unwrap_or_else(|| tagged.to_owned());
727 let split = crate::stripe::invoice_split(&found, invoice.amount_paid);
728 let created = rfc3339(found["created"].as_i64().unwrap_or(0).max(0) as u64 * 1000);
729 self.db
730 .prepare(
731 "INSERT OR IGNORE INTO workspace_invoices
732 (invoice_id, workspace, reason, period, amount_micros, status, hosted_url, pdf_url, through_at, created_at, paid_at,
733 fee_micros, tax_micros)
734 VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, NULL, ?, ?)",
735 )
736 .bind(&[
737 invoice_id.into(),
738 workspace.as_str().into(),
739 found["metadata"]["reason"].as_str().unwrap_or("month").into(),
740 found["metadata"]["period"].as_str().unwrap_or_default().into(),
741 ((split.net_cents * 10_000) as f64).into(),
742 crate::optional(invoice.hosted_invoice_url.as_deref()),
743 crate::optional(invoice.invoice_pdf.as_deref()),
744 created.as_str().into(),
745 created.as_str().into(),
746 ((split.fee_cents * 10_000) as f64).into(),
747 ((split.tax_cents * 10_000) as f64).into(),
748 ])?
749 .run()
750 .await?;
751 Row { workspace, fee_micros: split.fee_cents * 10_000 }
752 }
753 };
754 if invoice.status.as_deref() != Some("paid") {
755 return Ok(Some(format!("ignored: invoice {invoice_id} is {} now", invoice.status.as_deref().unwrap_or("unknown"))));
756 }
757 let credited = self.credit_invoice(&row.workspace, &invoice, row.fee_micros / 10_000).await?;
758 self.db
759 .prepare("UPDATE workspace_invoices SET status = 'paid', paid_at = ?, tax_micros = ? WHERE invoice_id = ?")
760 .bind(&[rfc3339(now_ms()).into(), ((invoice.tax_cents() * 10_000) as f64).into(), invoice_id.into()])?
761 .run()
762 .await?;
763 Ok(Some(format!(
764 "invoice {invoice_id} for {} paid{}",
765 row.workspace,
766 if credited { "" } else { " (already credited)" }
767 )))
768 }
769
770 pub(crate) async fn workspace_invoices(&self, workspace: &str) -> Result<Vec<WorkspaceInvoice>> {
771 let rows = self
772 .db
773 .prepare("SELECT * FROM workspace_invoices WHERE workspace = ? ORDER BY created_at DESC LIMIT 36")
774 .bind(&[workspace.into()])?
775 .all()
776 .await?
777 .results::<InvoiceRow>()?;
778 let mut invoices = vec![];
779 for row in rows {
780 invoices.push(self.with_lines(row).await?);
781 }
782 Ok(invoices)
783 }
784
785 /// One workspace invoice as kept, with its lines.
786 async fn workspace_invoice(&self, invoice_id: &str) -> Result<Option<WorkspaceInvoice>> {
787 let row = self
788 .db
789 .prepare("SELECT * FROM workspace_invoices WHERE invoice_id = ?")
790 .bind(&[invoice_id.into()])?
791 .first::<InvoiceRow>(None)
792 .await?;
793 Ok(match row {
794 Some(row) => Some(self.with_lines(row).await?),
795 None => None,
796 })
797 }
798
799 async fn with_lines(&self, row: InvoiceRow) -> Result<WorkspaceInvoice> {
800 let lines = self
801 .db
802 .prepare("SELECT description, amount_micros FROM workspace_invoice_lines WHERE invoice_id = ? ORDER BY position")
803 .bind(&[row.invoice_id.as_str().into()])?
804 .all()
805 .await?
806 .results::<LineRow>()?
807 .into_iter()
808 .map(|l| InvoiceItem { description: l.description, amount_micros: l.amount_micros })
809 .collect();
810 Ok(WorkspaceInvoice {
811 invoice_id: row.invoice_id,
812 workspace: row.workspace,
813 reason: row.reason,
814 period: row.period,
815 amount_micros: row.amount_micros,
816 status: row.status,
817 hosted_url: row.hosted_url,
818 pdf_url: row.pdf_url,
819 lines,
820 created_at: row.created_at,
821 fee_micros: row.fee_micros,
822 tax_micros: row.tax_micros,
823 })
824 }
825
826 /// `invoices`: for the workspace's owners and billing managers, as its
827 /// billing details are.
828 pub(crate) async fn invoices(&self, a: InvoicesArgs) -> Result<Outcome<Vec<WorkspaceInvoice>>> {
829 let workspace = a.workspace.to_lowercase();
830 if !may_see_invoices(&a.viewer, &workspace) {
831 return Ok(Outcome::fail(FailureCode::Forbidden, "Only an owner or a billing manager can see a workspace's invoices."));
832 }
833 Ok(Outcome::Ok(self.workspace_invoices(&workspace).await?))
834 }
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840
841 #[test]
842 fn an_invoice_adds_up_to_what_is_owed() {
843 let used = vec![("Agents on g1t's models".to_owned(), 40_000_000), ("Sandbox time".to_owned(), 10_000_000)];
844 // $10 of credit was paid in advance.
845 let lines = invoice_lines(&used, 40_000_000);
846 assert_eq!(lines.last().unwrap().description, "Paid in advance");
847 assert_eq!(lines.iter().map(|l| l.amount_micros).sum::<i64>(), 40_000_000);
848 // $5 was left unpaid from before.
849 let lines = invoice_lines(&used, 55_000_000);
850 assert_eq!(lines.last().unwrap().description, "Unpaid from earlier");
851 assert_eq!(lines.iter().map(|l| l.amount_micros).sum::<i64>(), 55_000_000);
852 // Exactly what was used.
853 assert_eq!(invoice_lines(&used, 50_000_000).len(), 2);
854 }
855
856 #[test]
857 fn an_invoice_holds_only_its_own_lines() {
858 let draft = draft_fields("cus_1", "acme", "month", "2026-09", "g1t usage".to_owned());
859 assert!(draft.contains(&("pending_invoice_items_behavior", "exclude".to_owned())));
860 // Taxed by Stripe Tax, every line at g1t's tax code, excluding tax.
861 assert!(draft.contains(&("automatic_tax[enabled]", "true".to_owned())));
862 let line = item_fields("cus_1", "in_1", "acme", "Sandbox time", 61);
863 assert!(line.contains(&("tax_code", crate::stripe::TAX_CODE.to_owned())));
864 assert!(line.contains(&("tax_behavior", "exclusive".to_owned())));
865 // Left a draft for want of an address: asked for, never charged.
866 assert!(needs_tax_location(&serde_json::json!({ "automatic_tax": { "status": "requires_location_inputs" } })));
867 assert!(!needs_tax_location(&serde_json::json!({ "automatic_tax": { "status": "complete" } })));
868 let item = item_fields("cus_1", "in_1", "acme", "Sandbox time", 1_234);
869 assert!(item.contains(&("invoice", "in_1".to_owned())));
870 assert!(item.contains(&("amount", "1234".to_owned())));
871 }
872
873 #[test]
874 fn only_the_card_saying_no_is_a_decline() {
875 let declined = r#"the card processor answered 402: {"error": {"code": "card_declined", "type": "card_error"}}"#;
876 assert!(is_decline(declined));
877 assert!(is_decline(r#"the card processor answered 400: {"error": {"type": "card_error"}}"#));
878 // Stripe down, busy or failing, or the network: tried again, nobody stopped.
879 assert!(!is_decline(r#"the card processor answered 500: {"error": {"type": "api_error"}}"#));
880 assert!(!is_decline(r#"the card processor answered 429: {"error": {"type": "rate_limit_error"}}"#));
881 assert!(!is_decline("Network connection lost."));
882 }
883
884 #[test]
885 fn only_owners_and_billing_managers_see_invoices() {
886 let user = |role: &str, org_roles: &[&str]| -> g1t_contracts::Viewer {
887 Some(
888 serde_json::from_value(serde_json::json!({
889 "id": "u_1", "username": "ana",
890 "workspaces": [{ "slug": "acme", "role": role, "org_roles": org_roles }],
891 }))
892 .unwrap(),
893 )
894 };
895 assert!(may_see_invoices(&user("owner", &[]), "acme"));
896 assert!(may_see_invoices(&user("member", &["billing_manager"]), "acme"));
897 assert!(!may_see_invoices(&user("member", &[]), "acme"));
898 assert!(!may_see_invoices(&user("member", &["security_manager"]), "acme"));
899 assert!(!may_see_invoices(&user("owner", &[]), "other"));
900 assert!(!may_see_invoices(&None, "acme"));
901 }
902
903 const DECLINED: &str = r#"the card processor answered 402: {"error": {"code": "card_declined", "type": "card_error"}}"#;
904 const NO_METHOD: &str = r#"the card processor answered 400: {"error": {"message": "This customer has no attached payment source or default payment method.", "type": "invalid_request_error"}}"#;
905
906 #[test]
907 fn only_a_paid_invoice_is_credited_and_only_a_decline_stops_work() {
908 assert_eq!(pay_outcome(Some("paid"), None), PayOutcome::Paid);
909 // Paid after all, whatever the attempt said.
910 assert_eq!(pay_outcome(Some("paid"), Some(DECLINED)), PayOutcome::Paid);
911 assert_eq!(pay_outcome(Some("open"), Some(DECLINED)), PayOutcome::Declined);
912 // Stripe did not answer: kept open, tried again, nobody stopped.
913 assert_eq!(pay_outcome(Some("open"), None), PayOutcome::Retry);
914 assert_eq!(pay_outcome(None, None), PayOutcome::Retry);
915 // Voided at Stripe meanwhile: closed, never charged.
916 assert_eq!(pay_outcome(Some("void"), Some(NO_METHOD)), PayOutcome::Void);
917 }
918
919 #[test]
920 fn a_refusal_for_want_of_a_card_stops_instead_of_retrying_for_ever() {
921 assert!(needs_card(NO_METHOD));
922 assert_eq!(pay_outcome(Some("open"), Some(NO_METHOD)), PayOutcome::NeedsCard);
923 assert_eq!(stripe_reason(NO_METHOD), "This customer has no attached payment source or default payment method.");
924 // Only 5xx, 429 and no answer are tried again.
925 for transient in [
926 r#"the card processor answered 500: {"error": {"type": "api_error"}}"#,
927 r#"the card processor answered 429: {"error": {"type": "invalid_request_error", "code": "rate_limit"}}"#,
928 "Network connection lost.",
929 ] {
930 assert!(!needs_card(transient), "{transient}");
931 assert_eq!(pay_outcome(Some("open"), Some(transient)), PayOutcome::Retry, "{transient}");
932 }
933 // A key problem (401) is g1t's, never the payer's: tried again.
934 assert!(!needs_card(r#"the card processor answered 401: {"error": {"type": "invalid_request_error"}}"#));
935 }
936
937 #[test]
938 fn an_invoice_attempted_again_is_the_same_until_one_is_paid_or_voided() {
939 let lines = [("Sandbox time", 3_500), ("Git operations", 501)];
940 let key = invoice_key("acme", "cus_1", "month", "2026-09", &lines, 0, 0);
941 assert!(key.starts_with("ws-invoice/acme/cus_1/month/2026-09/4001/"), "{key}");
942 assert!(key.ends_with("/p0"), "{key}");
943 assert_eq!(key, invoice_key("acme", "cus_1", "month", "2026-09", &lines, 0, 0));
944 // A voided invoice is never handed back for the next.
945 assert_ne!(key, invoice_key("acme", "cus_1", "month", "2026-09", &lines, 0, 1));
946 // Nor one paid earlier the same day for the same total.
947 assert_ne!(key, invoice_key("acme", "cus_1", "month", "2026-09", &lines, 1, 0));
948 // Nor one made for another customer.
949 assert_ne!(key, invoice_key("acme", "cus_2", "month", "2026-09", &lines, 0, 0));
950 }
951
952 #[test]
953 fn a_retry_whose_lines_moved_is_a_new_draft_not_the_old_key_with_other_amounts() {
954 // Owed 40.000000 then 40.000001 after a sliver more usage: the old
955 // key (whole cents of what was owed, rounded down) was 4000 for
956 // both, while the lines sent are 4000 and 4001 cents. Stripe refuses
957 // a key sent again with other parameters, so the retry was stuck
958 // until the key expired or the total crossed a cent.
959 let before = line_cents(&items(&[40_000_000]));
960 let after = line_cents(&items(&[40_000_001]));
961 assert_ne!(before, after);
962 let key = |cents: &[i64]| {
963 let lines: Vec<(&str, i64)> = cents.iter().map(|c| ("Sandbox time", *c)).collect();
964 invoice_key("acme", "cus_1", "threshold", "2026-10-14", &lines, 0, 0)
965 };
966 assert_ne!(key(&before), key(&after));
967 // The same total spread over other lines is another invoice too.
968 let a = invoice_key("acme", "cus_1", "month", "2026-09", &[("Sandbox time", 300), ("Git operations", 200)], 0, 0);
969 let b = invoice_key("acme", "cus_1", "month", "2026-09", &[("Sandbox time", 200), ("Git operations", 300)], 0, 0);
970 assert_ne!(a, b);
971 }
972
973 fn items(micros: &[i64]) -> Vec<InvoiceItem> {
974 micros.iter().map(|&amount_micros| InvoiceItem { description: String::new(), amount_micros }).collect()
975 }
976
977 #[test]
978 fn the_card_is_charged_what_is_owed_rounded_up_to_the_cent_never_down() {
979 // $1.234567 + $2.345678 = $3.580245 owed: 359 cents, where cutting
980 // each line would have charged 357.
981 let cents = line_cents(&items(&[1_234_567, 2_345_678]));
982 assert_eq!(cents.iter().sum::<i64>(), 359);
983 assert_eq!(cents, vec![124, 235]);
984 // Whole cents stay as they are.
985 assert_eq!(line_cents(&items(&[40_000_000, 10_000_000])), vec![4_000, 1_000]);
986 // A credit line keeps its full credit; the total still rounds up.
987 // $50.004 used, $10.0025 paid in advance: $40.0015 owed, 4,001 cents.
988 let cents = line_cents(&items(&[50_004_000, -10_002_500]));
989 assert_eq!(cents.iter().sum::<i64>(), 4_001);
990 // Lines under a cent each add up to the cents they make together.
991 let cents = line_cents(&items(&[4_000, 4_000, 4_000]));
992 assert_eq!(cents.iter().sum::<i64>(), 2);
993 // Every invoice the close makes: never less than owed, never a cent more.
994 for (used, owed) in [(vec![("a".to_owned(), 7_777_777), ("b".to_owned(), 3)], 6_000_001), (vec![("a".to_owned(), 5_000_001)], 5_000_001)] {
995 let lines = invoice_lines(&used, owed);
996 let charged = line_cents(&lines).iter().sum::<i64>() * 10_000;
997 assert!(charged >= owed && charged - owed < 10_000, "{charged} for {owed}");
998 }
999 }
1000}