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.

g1t.sh
1,501 linesCodeBlameRaw
1//! Stripe telling billing what happened, and enterprise invoices.
2//!
3//! Most of billing asks Stripe when it needs to know: a payment page is
4//! confirmed when the person comes back, a plan is checked when its period
5//! ends. That misses whatever happens while no one is looking: a page paid
6//! for and closed, a renewal that failed, a refund, a dispute, an invoice
7//! paid by bank transfer a week later. Stripe sends each as an event to
8//! `https://api.g1t.sh/stripe/webhook`; the API passes the raw body and its
9//! signature here, untouched.
10//!
11//! - The endpoint is registered by billing itself, from sudo, once per
12//! mode. Its signing secret is the worker's `STRIPE_WEBHOOK_SECRET`
13//! environment secret, never kept in the database or shown, and nothing
14//! can be posted here without it.
15//! - An event whose `livemode` is not the key's mode is ignored (and
16//! logged): test events never move live balances, nor the reverse.
17//! - Each event is handled once, by id, and recorded with what was done.
18//! - Every handler is safe alongside the paths that ask Stripe directly:
19//! both claim the same rows.
20//!
21//! Enterprises are invoiced: one Stripe invoice per month (or sooner, from
22//! sudo), with a line per workspace for what it owes, sent to the
23//! enterprise's billing email and paid on Stripe's hosted invoice page.
24//! When it is paid, each workspace is credited its line; when it goes
25//! overdue, their work stops until it is paid.
26
27use g1t_contracts::billing::{
28 AdminEnterpriseAddressArgs, AdminEnterpriseBillingArgs, AdminInvoiceEnterpriseArgs, AdminStripeArgs, BillingAccount, EnterpriseInvoice,
29 EntryKind, InvoiceLine, StripeEventSummary, StripePortalStatus, StripeStatus, StripeTaxStatus, StripeWebhook, StripeWebhookArgs,
30};
31use g1t_contracts::time::rfc3339;
32use g1t_contracts::{FailureCode, Outcome};
33use g1t_kit::now_ms;
34use hmac::{Hmac, Mac};
35use serde::Deserialize;
36use serde_json::Value;
37use sha2::Sha256;
38use worker::Result;
39use worker::wasm_bindgen::JsValue;
40
41use crate::Billing;
42
43/// Where Stripe sends events.
44pub(crate) const WEBHOOK_URL: &str = "https://api.g1t.sh/stripe/webhook";
45
46/// The events billing acts on.
47pub(crate) const EVENTS: &[&str] = &[
48 "checkout.session.completed",
49 // A prepayment by bank transfer: the page completes when the transfer
50 // is set up, and this comes when the money arrives.
51 "checkout.session.async_payment_succeeded",
52 "customer.subscription.updated",
53 "customer.subscription.deleted",
54 "invoice.paid",
55 "invoice.payment_failed",
56 "invoice.overdue",
57 "invoice.voided",
58 "charge.refunded",
59 "charge.dispute.created",
60 "charge.dispute.closed",
61 // The saved card, kept on the account row (stripe_sync.rs).
62 "customer.updated",
63 "payment_method.attached",
64 "payment_method.detached",
65 "payment_method.updated",
66 "payment_method.automatically_updated",
67 "setup_intent.succeeded",
68];
69
70/// The subscription an invoice is for: `subscription` at the API version
71/// billing asks in, `parent.subscription_details.subscription` in events
72/// sent at a newer one (a destination keeps the version it was made at).
73pub(crate) fn invoice_subscription(invoice: &Value) -> Option<&str> {
74 invoice["subscription"].as_str().or_else(|| invoice["parent"]["subscription_details"]["subscription"].as_str())
75}
76
77/// Why an event is not for this key, when its `livemode` disagrees with the
78/// key's mode (`live` or `test`): a test event never moves real balances,
79/// nor a live one test balances. None when it agrees, or says nothing.
80pub(crate) fn wrong_mode(livemode: Option<bool>, mode: &str) -> Option<String> {
81 let live = livemode?;
82 let key_live = mode == "live";
83 (live != key_live).then(|| format!("a {} event, and the key is in {mode} mode", if live { "live" } else { "test" }))
84}
85
86/// What earlier refunds of a charge (`?1`, `refund/<charge>/%`) took back,
87/// as a positive amount: from the plan's payments and from the ledger alike.
88pub(crate) const ALREADY_REFUNDED_SQL: &str = "SELECT -(COALESCE((SELECT SUM(amount_micros) FROM plan_payments WHERE invoice_id LIKE ?1), 0)
89 + COALESCE((SELECT SUM(amount_micros) FROM ledger WHERE reference LIKE ?1), 0)) AS micros";
90
91/// Why a workspace's work is stopped while one of its payments is disputed.
92const DISPUTE_STOP: &str = "a payment was disputed with the card's bank";
93
94/// Where a dispute stands, from Stripe's `status`: `lost`, `won` (a
95/// warning closed without a dispute counts as won), else `open`.
96pub(crate) fn dispute_state(status: &str) -> &'static str {
97 match status {
98 "lost" => "lost",
99 "won" | "warning_closed" | "prevented" => "won",
100 _ => "open",
101 }
102}
103
104/// Whether a dispute event moves the dispute from where billing has it
105/// (`known`, none for a dispute not seen before) to `state`: an event that
106/// says what billing knows already does nothing. Once lost, only a win
107/// (Stripe reversing it) moves it again; once detached (a test-mode
108/// dispute), nothing does.
109pub(crate) fn dispute_moves(known: Option<&str>, state: &str) -> bool {
110 match known {
111 None => true,
112 Some(known) if known == state => false,
113 // Closed by a test-mode detach: a test dispute, never acted on.
114 Some("detached") => false,
115 Some("lost") => state == "won",
116 Some(_) => true,
117 }
118}
119
120/// What a dispute won after it was lost gives back: what the loss took
121/// (`taken`), only on that move. Nothing for any other.
122pub(crate) fn restore_on_win(known: Option<&str>, state: &str, taken: i64) -> i64 {
123 if known == Some("lost") && state == "won" { taken.max(0) } else { 0 }
124}
125
126/// Where a lost dispute's reversal is given back: `dispute/<id>/won`,
127/// unique, so it is given back once.
128pub(crate) fn won_reference(dispute_id: &str) -> String {
129 format!("dispute/{dispute_id}/won")
130}
131
132/// What a dispute won after it was lost no longer leaves owed: `?1` the
133/// workspace, `?2` what the loss took. Never below nothing.
134pub(crate) const LOWER_DISPUTE_OWED: &str =
135 "UPDATE limits SET dispute_owed_micros = MAX(0, dispute_owed_micros - ?2) WHERE workspace = ?1";
136
137/// Events billing handles that `has` does not include, in billing's order.
138pub(crate) fn missing_events(has: &[String]) -> Vec<String> {
139 // `*` is every event.
140 if has.iter().any(|event| event == "*") {
141 return Vec::new();
142 }
143 EVENTS.iter().filter(|event| !has.iter().any(|h| h == *event)).map(|event| (*event).to_owned()).collect()
144}
145
146/// How old a signed event may be, so a captured one cannot be replayed.
147const TOLERANCE_SECONDS: i64 = 5 * 60;
148
149/// Whether `header` (`t=…,v1=…`) signs `payload` with `secret`, within the
150/// tolerance of `now_seconds`.
151pub(crate) fn verify(payload: &str, header: &str, secret: &str, now_seconds: i64) -> bool {
152 let mut timestamp = None;
153 let mut signatures = vec![];
154 for part in header.split(',') {
155 match part.trim().split_once('=') {
156 Some(("t", value)) => timestamp = value.parse::<i64>().ok(),
157 Some(("v1", value)) => signatures.push(value.to_owned()),
158 _ => {}
159 }
160 }
161 let Some(timestamp) = timestamp else { return false };
162 if (now_seconds - timestamp).abs() > TOLERANCE_SECONDS {
163 return false;
164 }
165 let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(secret.as_bytes()) else { return false };
166 mac.update(format!("{timestamp}.{payload}").as_bytes());
167 let expected = mac.finalize().into_bytes();
168 signatures.iter().any(|signature| {
169 hex::decode(signature).is_ok_and(|given| {
170 // Constant time: compare every byte whatever the first difference.
171 given.len() == expected.len() && given.iter().zip(expected.iter()).fold(0u8, |acc, (a, b)| acc | (a ^ b)) == 0
172 })
173 })
174}
175
176/// The destination at billing's address, as Stripe lists it.
177#[derive(Deserialize)]
178pub(crate) struct Destination {
179 pub(crate) id: String,
180 url: String,
181 /// `enabled` or `disabled`.
182 pub(crate) status: String,
183 pub(crate) enabled_events: Vec<String>,
184 created: i64,
185}
186
187#[derive(Deserialize)]
188struct EventRow {
189 id: String,
190 r#type: String,
191 outcome: String,
192 received_at: String,
193}
194
195#[derive(Deserialize)]
196struct InvoiceRow {
197 invoice_id: String,
198 period: String,
199 amount_micros: i64,
200 status: String,
201 hosted_url: Option<String>,
202 created_at: String,
203}
204
205#[derive(Deserialize)]
206struct LineRow {
207 workspace: String,
208 amount_micros: i64,
209}
210
211/// The idempotency key of an enterprise's invoice: the same enterprise,
212/// period and amounts are the same invoice however often it is attempted.
213pub(crate) fn enterprise_invoice_key(id: &str, period: &str, lines: &[(&str, i64)]) -> String {
214 // FNV-1a over every line, so the key stays short however many there are.
215 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
216 for (workspace, cents) in lines {
217 for byte in format!("{workspace}={cents};").bytes() {
218 hash = (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3);
219 }
220 }
221 format!("ent-invoice/{id}/{period}/{hash:016x}")
222}
223
224impl Billing {
225 pub(crate) fn mode(&self) -> &'static str {
226 match &self.stripe {
227 None => "off",
228 Some(stripe) if stripe.live() => "live",
229 Some(_) => "test",
230 }
231 }
232
233 /// The destination at billing's address in Stripe, for this key's
234 /// mode: an enabled one first. None when there is none.
235 pub(crate) async fn destination(&self) -> Result<Option<Destination>> {
236 let Some(stripe) = &self.stripe else { return Ok(None) };
237 #[derive(Deserialize)]
238 struct List {
239 data: Vec<Destination>,
240 }
241 let mut ours: Vec<Destination> =
242 stripe.get::<List>("/webhook_endpoints?limit=100").await?.data.into_iter().filter(|d| d.url == WEBHOOK_URL).collect();
243 ours.sort_by_key(|d| d.status != "enabled");
244 Ok(ours.into_iter().next())
245 }
246
247 // --- Staff ------------------------------------------------------------
248
249 pub(crate) async fn admin_stripe(&self, a: AdminStripeArgs) -> Result<StripeStatus> {
250 let mut error = None;
251 if a.fix
252 && let Err(e) = self.keep_endpoint(a.by.as_deref().unwrap_or("sudo")).await
253 {
254 error = Some(e.to_string());
255 }
256 let (webhook, missing_events) = match self.destination().await {
257 Ok(Some(d)) => {
258 let missing = missing_events(&d.enabled_events);
259 let webhook = StripeWebhook {
260 url: d.url,
261 endpoint_id: d.id,
262 status: d.status,
263 events: d.enabled_events,
264 created_at: rfc3339(d.created.max(0) as u64 * 1000),
265 };
266 (Some(webhook), missing)
267 }
268 Ok(None) => (None, Vec::new()),
269 Err(e) => {
270 error = error.or(Some(format!("Stripe could not be read: {e}")));
271 (None, Vec::new())
272 }
273 };
274 let recent_events = self
275 .db
276 .prepare("SELECT * FROM stripe_events ORDER BY received_at DESC LIMIT 25")
277 .all()
278 .await?
279 .results::<EventRow>()?
280 .into_iter()
281 .map(|row| StripeEventSummary { id: row.id, kind: row.r#type, outcome: row.outcome, received_at: row.received_at })
282 .collect();
283 // Stripe Tax and the customer portal, for the go-live checklist:
284 // read as Stripe has them, never made or changed here.
285 let (mut tax, mut portal) = (None, None);
286 if let Some(stripe) = &self.stripe {
287 match stripe.tax_settings().await {
288 Ok(settings) => {
289 tax = Some(StripeTaxStatus {
290 status: settings.status,
291 missing_fields: settings.status_details.pending.and_then(|p| p.missing_fields).unwrap_or_default(),
292 })
293 }
294 Err(e) => error = error.or(Some(format!("Stripe Tax's settings could not be read: {e}"))),
295 }
296 match stripe.our_portal().await {
297 Ok(found) => portal = found.map(|c| StripePortalStatus { configuration_id: c.id, active: c.active }),
298 Err(e) => error = error.or(Some(format!("The customer portal's settings could not be read: {e}"))),
299 }
300 }
301 Ok(StripeStatus {
302 mode: self.mode().to_owned(),
303 secret_set: self.webhook_secret.is_some(),
304 webhook,
305 missing_events,
306 recent_events,
307 error,
308 tax,
309 portal,
310 })
311 }
312
313 // --- Events -----------------------------------------------------------
314
315 pub(crate) async fn stripe_webhook(&self, a: StripeWebhookArgs) -> Result<Outcome<bool>> {
316 let Some(secret) = &self.webhook_secret else {
317 return Ok(Outcome::fail(
318 FailureCode::Conflict,
319 "STRIPE_WEBHOOK_SECRET is not set, so billing cannot check that events come from Stripe.",
320 ));
321 };
322 let now_seconds = (now_ms() / 1000) as i64;
323 if !verify(&a.payload, &a.signature, secret, now_seconds) {
324 return Ok(Outcome::fail(FailureCode::Forbidden, "The signature does not match."));
325 }
326 let event: Value = serde_json::from_str(&a.payload).map_err(|e| worker::Error::RustError(e.to_string()))?;
327 if event["id"].as_str().unwrap_or_default().is_empty() {
328 return Ok(Outcome::fail(FailureCode::Invalid, "Not an event."));
329 }
330 Ok(Outcome::Ok(self.process_event(&event).await?))
331 }
332
333 /// Handles a Stripe event once, however often it arrives: by webhook,
334 /// or again from the event list (stripe_sync.rs). False when it was
335 /// seen before.
336 pub(crate) async fn process_event(&self, event: &Value) -> Result<bool> {
337 let id = event["id"].as_str().unwrap_or_default().to_owned();
338 let kind = event["type"].as_str().unwrap_or_default().to_owned();
339 // Once each: the first to record it handles it.
340 let claimed = self
341 .db
342 .prepare("INSERT OR IGNORE INTO stripe_events (id, type, outcome, received_at) VALUES (?, ?, 'handling', ?) RETURNING id")
343 .bind(&[id.as_str().into(), kind.as_str().into(), rfc3339(now_ms()).into()])?
344 .first::<Value>(None)
345 .await?;
346 if claimed.is_none() {
347 return Ok(false);
348 }
349 let outcome = match self.handle(&kind, event).await {
350 Ok(outcome) => outcome,
351 Err(error) => {
352 // Let Stripe send it again: forget it was seen.
353 self.db.prepare("DELETE FROM stripe_events WHERE id = ?").bind(&[id.as_str().into()])?.run().await?;
354 return Err(error);
355 }
356 };
357 self.db
358 .prepare("UPDATE stripe_events SET outcome = ? WHERE id = ?")
359 .bind(&[outcome.as_str().into(), id.as_str().into()])?
360 .run()
361 .await?;
362 Ok(true)
363 }
364
365 async fn handle(&self, kind: &str, event: &Value) -> Result<String> {
366 if let Some(why) = wrong_mode(event["livemode"].as_bool(), self.mode()) {
367 worker::console_error!("Stripe event {} ({kind}) ignored: {why}", event["id"].as_str().unwrap_or_default());
368 return Ok(format!("ignored: {why}"));
369 }
370 let object = &event["data"]["object"];
371 let text = |key: &str| object[key].as_str().unwrap_or_default().to_owned();
372 // The customer whose saved card may have changed. A detached card
373 // has no customer any more; the event says whose it was.
374 let card_owner = match kind {
375 "customer.updated" => object["id"].as_str(),
376 "payment_method.detached" => event["data"]["previous_attributes"]["customer"].as_str(),
377 _ => object["customer"].as_str(),
378 };
379 Ok(match kind {
380 "checkout.session.completed" | "checkout.session.async_payment_succeeded" => self.settle_checkout(&text("id")).await?,
381 "customer.subscription.updated" | "customer.subscription.deleted" => {
382 self.settle_subscription(&text("id")).await?
383 }
384 "invoice.paid" => {
385 if let Some(subscription) = invoice_subscription(object) {
386 self.plan_paid(subscription, &text("id")).await?;
387 self.settle_subscription(subscription).await?
388 } else if let Some(done) = self.workspace_invoice_paid(&text("id")).await? {
389 done
390 } else {
391 self.enterprise_invoice_paid(&text("id")).await?
392 }
393 }
394 "invoice.payment_failed" => match (invoice_subscription(object), object["metadata"]["g1t_workspace"].as_str()) {
395 (Some(subscription), _) => self.settle_subscription(subscription).await?,
396 (None, Some(tagged)) => self.workspace_invoice_failed(&text("id"), tagged).await?,
397 _ => "ignored: not g1t's".to_owned(),
398 },
399 "invoice.overdue" => self.enterprise_invoice_status(&text("id"), "overdue").await?,
400 "invoice.voided" => {
401 // A workspace invoice voided at Stripe stops being open here.
402 self.db
403 .prepare("UPDATE workspace_invoices SET status = 'void' WHERE invoice_id = ? AND status IN ('open', 'failed')")
404 .bind(&[text("id").into()])?
405 .run()
406 .await?;
407 self.enterprise_invoice_status(&text("id"), "void").await?
408 }
409 "charge.refunded" => self.refunded(&text("id")).await?,
410 // Created or closed, the dispute is read as it stands now, so
411 // events late or out of order act on where it is.
412 "charge.dispute.created" | "charge.dispute.closed" => self.disputed(&text("id")).await?,
413 "customer.updated"
414 | "payment_method.attached"
415 | "payment_method.detached"
416 | "payment_method.updated"
417 | "payment_method.automatically_updated"
418 | "setup_intent.succeeded" => match card_owner {
419 Some(customer) => self.sync_card_of(customer).await?,
420 None => "ignored: no customer".to_owned(),
421 },
422 _ => "ignored".to_owned(),
423 })
424 }
425
426 /// A payment page done, whether or not the person came back to g1t.
427 async fn settle_checkout(&self, session_id: &str) -> Result<String> {
428 #[derive(Deserialize)]
429 struct Open {
430 workspace: String,
431 created_by: String,
432 feature: Option<String>,
433 #[serde(default)]
434 fee_cents: Option<u32>,
435 }
436 let Some(open) = self
437 .db
438 .prepare("SELECT workspace, created_by, feature, fee_cents FROM checkouts WHERE id = ? AND status = 'open'")
439 .bind(&[session_id.into()])?
440 .first::<Open>(None)
441 .await?
442 else {
443 return Ok("ignored: already settled or not g1t's".to_owned());
444 };
445 // A card check: saved and verified, never charged.
446 if open.feature.as_deref() == Some(crate::cards::CARD_CHECK) {
447 return Ok(match self.settle_card_check(session_id).await? {
448 Ok(done) => done,
449 Err(why) => format!("card check not passed: {why}"),
450 });
451 }
452 // AI credit, from before g1t stopped selling it: a page left open
453 // then is never credited as anything else.
454 if open.feature.as_deref() == Some("ai_credit") {
455 return Ok("ignored: AI credit is no longer sold".to_owned());
456 }
457 let Some(stripe) = &self.stripe else { return Ok("ignored: payments off".to_owned()) };
458 let session = stripe.session(session_id).await?;
459 if session.payment_status != "paid" && open.feature.is_none() {
460 return Ok("ignored: not paid".to_owned());
461 }
462 // What the page paid for first, then the page marked paid: a
463 // failure between the two leaves it open, and settling it again
464 // finds the credit (the ledger's reference) or the plan entered.
465 let mark = self.db.prepare("UPDATE checkouts SET status = 'paid' WHERE id = ? AND status = 'open'").bind(&[session_id.into()])?;
466 match open.feature.as_deref() {
467 None => {
468 let cents = self.credit_prepayment(&open.workspace, &session, open.fee_cents.unwrap_or(0), &open.created_by).await?;
469 mark.run().await?;
470 if cents == 0 {
471 return Ok("ignored: credited meanwhile".to_owned());
472 }
473 Ok(format!("credited {} to {}", crate::features::dollars(cents * 10_000), open.workspace))
474 }
475 Some(feature) => {
476 let Some(feature) = g1t_contracts::billing::Feature::parse(feature) else {
477 return Ok("ignored: unknown feature".to_owned());
478 };
479 if let Some(subscription_id) = &session.subscription {
480 let subscription = stripe.subscription(subscription_id).await?;
481 self.record(&open.workspace, feature, &subscription, &open.created_by).await?;
482 }
483 self.db
484 .prepare(
485 "INSERT INTO accounts (workspace, balance_micros, customer_id, created_at) VALUES (?1, 0, ?2, ?3)
486 ON CONFLICT (workspace) DO UPDATE SET customer_id = COALESCE(customer_id, ?2)",
487 )
488 .bind(&[open.workspace.as_str().into(), crate::optional(session.customer.as_deref()), rfc3339(now_ms()).into()])?
489 .run()
490 .await?;
491 mark.run().await?;
492 Ok(format!("{} plan started for {}", feature.title(), open.workspace))
493 }
494 }
495 }
496
497 /// The plan's monthly price, paid: revenue that never goes through the
498 /// ledger, recorded once per invoice for sudo's figures and for trust.
499 /// Only the plan's own price is revenue: the invoice's tax and card fee
500 /// are kept apart (`tax_and_fees`).
501 ///
502 /// The invoice is read again at billing's API version: the event's copy
503 /// is at the destination's, where `tax` and `payment_intent` may be
504 /// gone.
505 async fn plan_paid(&self, subscription_id: &str, invoice_id: &str) -> Result<()> {
506 let Some(stripe) = &self.stripe else { return Ok(()) };
507 if invoice_id.is_empty() {
508 return Ok(());
509 }
510 let invoice: Value = stripe.get(&format!("/invoices/{invoice_id}")).await?;
511 let invoice = &invoice;
512 let split = crate::stripe::invoice_split(invoice, invoice["amount_paid"].as_i64().unwrap_or(0));
513 let amount_cents = split.net_cents;
514 if amount_cents <= 0 {
515 return Ok(());
516 }
517 #[derive(Deserialize)]
518 struct Owner {
519 workspace: String,
520 }
521 if let Some(owner) = self
522 .db
523 .prepare("SELECT workspace FROM subscriptions WHERE subscription_id = ?")
524 .bind(&[subscription_id.into()])?
525 .first::<Owner>(None)
526 .await?
527 {
528 let extras = crate::tax::Extras { tax_cents: split.tax_cents, fee_cents: split.fee_cents };
529 self.record_extras(&owner.workspace, invoice_id, invoice["payment_intent"].as_str(), extras, None).await?;
530 }
531 self.db
532 .prepare(
533 "INSERT INTO plan_payments (invoice_id, workspace, amount_micros, paid_at)
534 SELECT ?1, workspace, ?2, ?3 FROM subscriptions WHERE subscription_id = ?4
535 ON CONFLICT (invoice_id) DO NOTHING",
536 )
537 .bind(&[
538 invoice_id.into(),
539 ((amount_cents * 10_000) as f64).into(),
540 rfc3339(now_ms()).into(),
541 subscription_id.into(),
542 ])?
543 .run()
544 .await?;
545 Ok(())
546 }
547
548 /// A plan that changed at Stripe: renewed, failed, canceled.
549 pub(crate) async fn settle_subscription(&self, subscription_id: &str) -> Result<String> {
550 #[derive(Deserialize)]
551 struct Plan {
552 workspace: String,
553 feature: String,
554 started_by: String,
555 }
556 let Some(plan) = self
557 .db
558 .prepare("SELECT workspace, feature, started_by FROM subscriptions WHERE subscription_id = ?")
559 .bind(&[subscription_id.into()])?
560 .first::<Plan>(None)
561 .await?
562 else {
563 return Ok("ignored: not a g1t plan".to_owned());
564 };
565 let (Some(stripe), Some(feature)) = (&self.stripe, g1t_contracts::billing::Feature::parse(&plan.feature)) else {
566 return Ok("ignored".to_owned());
567 };
568 let subscription = match stripe.subscription(subscription_id).await {
569 Ok(subscription) => subscription,
570 // A plan Stripe no longer has (another account's, or deleted):
571 // it has ended here.
572 Err(error) if crate::stripe::is_missing(&error) => {
573 self.db
574 .prepare("UPDATE subscriptions SET status = 'canceled', updated_at = ? WHERE subscription_id = ?")
575 .bind(&[rfc3339(now_ms()).into(), subscription_id.into()])?
576 .run()
577 .await?;
578 return Ok(format!("{} plan for {} is gone at Stripe: canceled", feature.title(), plan.workspace));
579 }
580 Err(error) => return Err(error),
581 };
582 self.record(&plan.workspace, feature, &subscription, &plan.started_by).await?;
583 Ok(format!("{} plan for {} is {}", feature.title(), plan.workspace, subscription.status))
584 }
585
586 /// The workspace a Stripe customer belongs to.
587 pub(crate) async fn workspace_of_customer(&self, customer: &str) -> Result<Option<String>> {
588 #[derive(Deserialize)]
589 struct Row {
590 workspace: String,
591 }
592 Ok(self
593 .db
594 .prepare("SELECT workspace FROM accounts WHERE customer_id = ?")
595 .bind(&[customer.into()])?
596 .first::<Row>(None)
597 .await?
598 .map(|row| row.workspace))
599 }
600
601 /// The workspace a workspace invoice was sent to, under its slug now.
602 pub(crate) async fn workspace_of_invoice(&self, invoice_id: &str) -> Result<Option<String>> {
603 #[derive(Deserialize)]
604 struct Row {
605 workspace: String,
606 }
607 Ok(self
608 .db
609 .prepare("SELECT workspace FROM workspace_invoices WHERE invoice_id = ?")
610 .bind(&[invoice_id.into()])?
611 .first::<Row>(None)
612 .await?
613 .map(|row| row.workspace))
614 }
615
616 /// Money given back: what was paid is less, by the refund. A refund of
617 /// a plan's invoice takes back the plan's payment (`plan_payments`);
618 /// any other takes back credit on the balance. The charge is read again
619 /// at billing's API version, where it still carries `invoice`.
620 async fn refunded(&self, charge_id: &str) -> Result<String> {
621 let Some(stripe) = &self.stripe else { return Ok("ignored: payments off".to_owned()) };
622 let charge: Value = stripe.get(&format!("/charges/{charge_id}")).await?;
623 let charge = &charge;
624 let Some(customer) = charge["customer"].as_str() else { return Ok("ignored: no customer".to_owned()) };
625 let Some(workspace) = self.workspace_of_customer(customer).await? else {
626 return Ok("ignored: not a workspace's customer".to_owned());
627 };
628 let refunded = charge["amount_refunded"].as_i64().unwrap_or(0);
629 if refunded <= 0 {
630 return Ok("ignored: nothing refunded".to_owned());
631 }
632 let plan_invoice = match charge["invoice"].as_str() {
633 Some(invoice) => self.plan_payment_of(invoice).await?,
634 None => false,
635 };
636 // Each refund total once, so partial refunds add up correctly: what
637 // earlier refunds of the charge took back, wherever they were kept.
638 // A plan's refund recorded on the ledger (before plan refunds went
639 // to `plan_payments`) counts as much as one recorded there.
640 #[derive(Deserialize)]
641 struct Sum {
642 micros: Option<i64>,
643 }
644 let already = self
645 .db
646 .prepare(ALREADY_REFUNDED_SQL)
647 .bind(&[format!("refund/{charge_id}/%").into()])?
648 .first::<Sum>(None)
649 .await?
650 .and_then(|s| s.micros)
651 .unwrap_or(0);
652 // A refund gives back the payment's tax and card fee in proportion:
653 // only the rest comes off the balance, which never held them.
654 let (extras, transaction) = self.extras_of(charge["payment_intent"].as_str(), charge["invoice"].as_str()).await?;
655 let paid = charge["amount"].as_i64().unwrap_or(refunded);
656 let (balance_cents, tax_cents, fee_cents) = crate::tax::refund_split(refunded, paid, extras.tax_cents, extras.fee_cents);
657 let (given_tax, given_fee) = self.refunded_extras(charge_id).await?;
658 let new = balance_cents * 10_000 - already;
659 let new_extras = crate::tax::Extras { tax_cents: -(tax_cents - given_tax), fee_cents: -(fee_cents - given_fee) };
660 if new <= 0 && new_extras == crate::tax::Extras::default() {
661 return Ok("ignored: refund already recorded".to_owned());
662 }
663 let reference = format!("refund/{charge_id}/{refunded}");
664 if new > 0 && plan_invoice {
665 // The plan's revenue is less; nothing was ever on the balance.
666 self.db
667 .prepare(
668 "INSERT INTO plan_payments (invoice_id, workspace, amount_micros, paid_at) VALUES (?, ?, ?, ?)
669 ON CONFLICT (invoice_id) DO NOTHING",
670 )
671 .bind(&[reference.as_str().into(), workspace.as_str().into(), ((-new) as f64).into(), rfc3339(now_ms()).into()])?
672 .run()
673 .await?;
674 } else if new > 0 {
675 self.enter(&workspace, EntryKind::TopUp, -new, "Refunded to the card", &reference, None, None, None).await?;
676 }
677 self.record_extras(&workspace, &reference, charge["payment_intent"].as_str(), new_extras, None).await?;
678 // An off-session charge's tax was recorded by g1t (auto-reload):
679 // reverse what this refund gave back. Checkout's and invoices' tax
680 // Stripe Tax keeps itself.
681 if let (Some(stripe), Some(transaction)) = (&self.stripe, transaction.as_deref()) {
682 let given_back = new.max(0) / 10_000 - new_extras.tax_cents - new_extras.fee_cents;
683 if given_back > 0
684 && let Err(error) = stripe.reverse_tax(transaction, &reference, given_back).await
685 {
686 worker::console_error!("{workspace}: the tax on refund {reference} was not reversed: {error}");
687 }
688 }
689 Ok(format!("refund of {} recorded for {workspace}", crate::features::dollars(new.max(0))))
690 }
691
692 /// What earlier refunds of a charge gave back of its tax and card fee,
693 /// in cents, as positive amounts.
694 async fn refunded_extras(&self, charge_id: &str) -> Result<(i64, i64)> {
695 #[derive(Deserialize)]
696 struct Row {
697 kind: String,
698 micros: Option<i64>,
699 }
700 let rows = self
701 .db
702 .prepare("SELECT kind, SUM(amount_micros) AS micros FROM tax_and_fees WHERE reference LIKE ? GROUP BY kind")
703 .bind(&[format!("refund/{charge_id}/%").into()])?
704 .all()
705 .await?
706 .results::<Row>()?;
707 let of = |kind: &str| rows.iter().filter(|r| r.kind == kind).map(|r| -r.micros.unwrap_or(0) / 10_000).sum::<i64>();
708 Ok((of("tax"), of("card_fee")))
709 }
710
711 /// Whether a Stripe invoice is a plan's, its payment recorded in
712 /// `plan_payments`.
713 async fn plan_payment_of(&self, invoice_id: &str) -> Result<bool> {
714 Ok(self
715 .db
716 .prepare("SELECT invoice_id FROM plan_payments WHERE invoice_id = ?")
717 .bind(&[invoice_id.into()])?
718 .first::<Value>(None)
719 .await?
720 .is_some())
721 }
722
723 /// A disputed payment, acted on as Stripe says it stands now (so an
724 /// event late, twice or out of order does the right thing):
725 ///
726 /// - Open: the payment never counts toward trust, and work stops.
727 /// - Won: it counts again, and the stop lifts unless another dispute is
728 /// open or what a lost one took is unpaid. Won after it was lost (the
729 /// bank reversed itself), what was taken back is given back once
730 /// (`dispute/<id>/won`, where the loss was entered) and no longer
731 /// owed.
732 /// - Lost: its credit is taken back once (`dispute/<id>` on the ledger,
733 /// or on `plan_payments` for a plan's invoice), in the share of the
734 /// payment that was not tax or card fee, and work stays stopped until
735 /// that much is paid again (`dispute_owed_micros`). A smaller payment
736 /// in the meantime is credit like any other, but does not lift it.
737 async fn disputed(&self, dispute_id: &str) -> Result<String> {
738 let Some(stripe) = &self.stripe else { return Ok("ignored".to_owned()) };
739 let dispute: Value = stripe.get(&format!("/disputes/{dispute_id}")).await?;
740 let state = dispute_state(dispute["status"].as_str().unwrap_or_default());
741 let Some(charge_id) = dispute["charge"].as_str().or_else(|| dispute["charge"]["id"].as_str()) else {
742 return Ok("ignored: no charge".to_owned());
743 };
744 let charge: Value = stripe.get(&format!("/charges/{charge_id}")).await?;
745 let Some(workspace) = (match charge["customer"].as_str() {
746 Some(customer) => self.workspace_of_customer(customer).await?,
747 None => None,
748 }) else {
749 return Ok("ignored: not a workspace's customer".to_owned());
750 };
751 let now = rfc3339(now_ms());
752 let payment_intent = charge["payment_intent"].as_str();
753 let invoice = charge["invoice"].as_str();
754 // The payment as the ledger knows it: its PaymentIntent, its
755 // invoice, or the prepayment page that took it.
756 let mut references: Vec<String> = [payment_intent, invoice].into_iter().flatten().map(str::to_owned).collect();
757 if let Some(intent) = payment_intent {
758 let pages: Value = stripe.get(&format!("/checkout/sessions?payment_intent={intent}&limit=10")).await?;
759 references.extend(pages["data"].as_array().into_iter().flatten().filter_map(|page| page["id"].as_str().map(str::to_owned)));
760 }
761 // It counts toward trust again only if won.
762 for reference in &references {
763 self.db
764 .prepare("UPDATE ledger SET disputed = ? WHERE workspace = ? AND reference = ?")
765 .bind(&[(if state == "won" { 0 } else { 1 }).into(), workspace.as_str().into(), reference.as_str().into()])?
766 .run()
767 .await?;
768 }
769 #[derive(Deserialize)]
770 struct Known {
771 status: String,
772 reversed_micros: Option<i64>,
773 }
774 let known_row = self
775 .db
776 .prepare("SELECT status, reversed_micros FROM disputes WHERE dispute_id = ?")
777 .bind(&[dispute_id.into()])?
778 .first::<Known>(None)
779 .await?;
780 let taken = known_row.as_ref().and_then(|row| row.reversed_micros).unwrap_or(0);
781 let known = known_row.map(|row| row.status);
782 // Where it stood already: an event twice, or late, changes nothing.
783 // Work is not stopped again (an owner may have paid since), and
784 // nothing is taken back or owed twice.
785 if !dispute_moves(known.as_deref(), state) {
786 return Ok(format!("ignored: dispute {dispute_id} is {state} already"));
787 }
788 let reference = format!("dispute/{dispute_id}");
789 let mut writes = vec![];
790 // A lost dispute's credit, taken back once: the reversal, the
791 // dispute's row and what is owed go in one batch, so a delivery
792 // alongside (whose reversal fails on the same reference, undoing
793 // its whole batch) never adds what is owed a second time.
794 let mut reversed = 0;
795 if state == "lost" {
796 let (extras, _) = self.extras_of(payment_intent, invoice).await?;
797 let amount = dispute["amount"].as_i64().unwrap_or(0);
798 let paid = charge["amount"].as_i64().unwrap_or(amount);
799 reversed = crate::tax::refund_split(amount, paid, extras.tax_cents, extras.fee_cents).0 * 10_000;
800 let plan = match invoice {
801 Some(invoice) => self.plan_payment_of(invoice).await?,
802 None => false,
803 };
804 if reversed > 0 && plan {
805 // No `ON CONFLICT`: a second insert fails the batch.
806 writes.push(
807 self.db
808 .prepare("INSERT INTO plan_payments (invoice_id, workspace, amount_micros, paid_at) VALUES (?, ?, ?, ?)")
809 .bind(&[reference.as_str().into(), workspace.as_str().into(), ((-reversed) as f64).into(), now.as_str().into()])?,
810 );
811 } else if reversed > 0 {
812 writes.extend(self.entry_statements(
813 &workspace,
814 EntryKind::TopUp,
815 -reversed,
816 "Taken back by the card's bank: a dispute was lost",
817 &reference,
818 None,
819 None,
820 None,
821 )?);
822 }
823 }
824 // Lost, then won: what the loss took back is given back, where it
825 // was taken (the plan's payments or the ledger), in the same batch
826 // as the dispute's row, so a delivery alongside fails on the same
827 // reference and gives nothing twice.
828 let restored = restore_on_win(known.as_deref(), state, taken);
829 if restored > 0 {
830 let back = won_reference(dispute_id);
831 if self.plan_payment_of(&reference).await? {
832 // No `ON CONFLICT`: a second insert fails the batch.
833 writes.push(
834 self.db
835 .prepare("INSERT INTO plan_payments (invoice_id, workspace, amount_micros, paid_at) VALUES (?, ?, ?, ?)")
836 .bind(&[back.as_str().into(), workspace.as_str().into(), (restored as f64).into(), now.as_str().into()])?,
837 );
838 } else {
839 writes.extend(self.entry_statements(
840 &workspace,
841 EntryKind::TopUp,
842 restored,
843 "Given back by the card's bank: a lost dispute was reversed",
844 &back,
845 None,
846 None,
847 None,
848 )?);
849 }
850 }
851 let reversing = !writes.is_empty();
852 writes.push(
853 self.db
854 .prepare(
855 "INSERT INTO disputes (dispute_id, workspace, charge_id, status, reversed_micros, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
856 ON CONFLICT (dispute_id) DO UPDATE SET status = ?4, reversed_micros = reversed_micros + ?5, updated_at = ?6",
857 )
858 .bind(&[dispute_id.into(), workspace.as_str().into(), charge_id.into(), state.into(), (reversed as f64).into(), now.as_str().into()])?,
859 );
860 if state == "won" {
861 if restored > 0 {
862 writes.push(self.db.prepare(LOWER_DISPUTE_OWED).bind(&[workspace.as_str().into(), (restored as f64).into()])?);
863 }
864 // Only the stop a dispute made: a declined card stays stopped.
865 writes.push(
866 self.db
867 .prepare(format!("{} AND autopay_error = ?2", crate::CLEAR_STOP))
868 .bind(&[workspace.as_str().into(), DISPUTE_STOP.into()])?,
869 );
870 } else {
871 writes.push(
872 self.db
873 .prepare(
874 "INSERT INTO limits (workspace, autopay_failed_at, autopay_error, dispute_owed_micros, updated_at) VALUES (?1, ?2, ?3, ?4, ?2)
875 ON CONFLICT (workspace) DO UPDATE SET autopay_failed_at = ?2, autopay_error = ?3,
876 dispute_owed_micros = dispute_owed_micros + ?4, updated_at = ?2",
877 )
878 .bind(&[workspace.as_str().into(), now.as_str().into(), DISPUTE_STOP.into(), (reversed as f64).into()])?,
879 );
880 }
881 match self.db.batch(writes).await {
882 Ok(_) => {}
883 // The reversal was entered already: by a delivery alongside,
884 // whose batch recorded the dispute and what is owed with it, or
885 // (before these went in one batch) on its own, its dispute
886 // left unrecorded. Only then is the dispute recorded now, at
887 // the amount entered, once.
888 // Given back already, by a delivery alongside.
889 Err(error) if restored > 0 && crate::is_duplicate(&error) => {
890 return Ok(format!("ignored: dispute {dispute_id} is won already"));
891 }
892 Err(error) if reversing && crate::is_duplicate(&error) => {
893 return self.dispute_reversed_before(&workspace, dispute_id, charge_id, &reference, &now).await;
894 }
895 Err(error) => return Err(error),
896 }
897 let account = self.account_of(&workspace).await?;
898 let what = match state {
899 "won" if restored > 0 => {
900 format!("dispute reversed in the workspace's favour: {} given back", crate::features::dollars(restored))
901 }
902 "won" => "dispute closed in the workspace's favour".to_owned(),
903 "lost" => format!("dispute lost: {} taken back; work stopped until it is paid", crate::features::dollars(reversed)),
904 _ => "dispute: work stopped".to_owned(),
905 };
906 self.audit(&account.id, "dispute", &format!("{workspace}: {what}"), "stripe").await?;
907 Ok(format!("{workspace}: {what}"))
908 }
909
910 /// A lost dispute whose reversal is on the ledger (or the plan's
911 /// payments) already. Recorded as lost, and what it took back owed,
912 /// only by the one delivery that moves the dispute's row to lost.
913 async fn dispute_reversed_before(&self, workspace: &str, dispute_id: &str, charge_id: &str, reference: &str, now: &str) -> Result<String> {
914 #[derive(Deserialize)]
915 struct Sum {
916 micros: Option<i64>,
917 }
918 let reversed = self
919 .db
920 .prepare(
921 "SELECT -(COALESCE((SELECT SUM(amount_micros) FROM ledger WHERE reference = ?1), 0)
922 + COALESCE((SELECT SUM(amount_micros) FROM plan_payments WHERE invoice_id = ?1), 0)) AS micros",
923 )
924 .bind(&[reference.into()])?
925 .first::<Sum>(None)
926 .await?
927 .and_then(|s| s.micros)
928 .unwrap_or(0)
929 .max(0);
930 let moved = self
931 .db
932 .prepare(
933 "INSERT INTO disputes (dispute_id, workspace, charge_id, status, reversed_micros, updated_at) VALUES (?1, ?2, ?3, 'lost', ?4, ?5)
934 ON CONFLICT (dispute_id) DO UPDATE SET status = 'lost', reversed_micros = ?4, updated_at = ?5 WHERE disputes.status <> 'lost'
935 RETURNING dispute_id",
936 )
937 .bind(&[dispute_id.into(), workspace.into(), charge_id.into(), (reversed as f64).into(), now.into()])?
938 .first::<Value>(None)
939 .await?
940 .is_some();
941 if !moved {
942 return Ok(format!("ignored: dispute {dispute_id} is lost already"));
943 }
944 self.db
945 .prepare(
946 "INSERT INTO limits (workspace, autopay_failed_at, autopay_error, dispute_owed_micros, updated_at) VALUES (?1, ?2, ?3, ?4, ?2)
947 ON CONFLICT (workspace) DO UPDATE SET autopay_failed_at = ?2, autopay_error = ?3,
948 dispute_owed_micros = dispute_owed_micros + ?4, updated_at = ?2",
949 )
950 .bind(&[workspace.into(), now.into(), DISPUTE_STOP.into(), (reversed as f64).into()])?
951 .run()
952 .await?;
953 Ok(format!("{workspace}: dispute lost: {} taken back earlier; work stopped until it is paid", crate::features::dollars(reversed)))
954 }
955
956 // --- Enterprise invoices ------------------------------------------------
957
958 pub(crate) async fn admin_enterprise_billing(&self, a: AdminEnterpriseBillingArgs) -> Result<Outcome<BillingAccount>> {
959 let email = a.email.trim().to_lowercase();
960 if !email.contains('@') || email.contains(char::is_whitespace) || a.by.trim().is_empty() {
961 return Ok(Outcome::fail(FailureCode::Invalid, "Give the email the enterprise's invoices go to."));
962 }
963 let Some(stripe) = &self.stripe else {
964 return Ok(Outcome::fail(FailureCode::Conflict, "Payments are not set up on this g1t."));
965 };
966 #[derive(Deserialize)]
967 struct Row {
968 name: String,
969 kind: String,
970 customer_id: Option<String>,
971 }
972 let Some(row) = self
973 .db
974 .prepare("SELECT name, kind, customer_id FROM billing_accounts WHERE id = ?")
975 .bind(&[a.id.as_str().into()])?
976 .first::<Row>(None)
977 .await?
978 .filter(|row| row.kind == "enterprise")
979 else {
980 return Ok(Outcome::fail(FailureCode::NotFound, "No such enterprise."));
981 };
982 #[derive(Deserialize)]
983 struct Customer {
984 id: String,
985 }
986 let fields = [
987 ("name", row.name.clone()),
988 ("email", email.clone()),
989 ("metadata[g1t_enterprise]", a.id.clone()),
990 ];
991 let customer: Customer = match &row.customer_id {
992 Some(id) => stripe.post(&format!("/customers/{id}"), &fields).await?,
993 None => stripe.post("/customers", &fields).await?,
994 };
995 self.db
996 .prepare("UPDATE billing_accounts SET billing_email = ?, customer_id = ? WHERE id = ?")
997 .bind(&[email.as_str().into(), customer.id.as_str().into(), a.id.as_str().into()])?
998 .run()
999 .await?;
1000 self.audit(&a.id, "billing_email", &format!("Invoices go to {email}"), &a.by).await?;
1001 Ok(match self.enterprise(&a.id).await? {
1002 Some(account) => Outcome::Ok(account),
1003 None => Outcome::fail(FailureCode::NotFound, "No such enterprise."),
1004 })
1005 }
1006
1007 /// `admin_enterprise_address`: the address Stripe Tax works the
1008 /// enterprise's invoices out from, and its tax ID, on its customer.
1009 pub(crate) async fn admin_enterprise_address(&self, a: AdminEnterpriseAddressArgs) -> Result<Outcome<bool>> {
1010 if a.by.trim().is_empty() {
1011 return Ok(Outcome::fail(FailureCode::Invalid, "Say who is changing it."));
1012 }
1013 if let Some(why) = crate::details::address_invalid(&a.address).or_else(|| crate::details::tax_id_invalid(a.tax_id_type.as_deref(), a.tax_id.as_deref())) {
1014 return Ok(Outcome::fail(FailureCode::Invalid, why));
1015 }
1016 let place = serde_json::json!({ "country": a.address.country.trim(), "postal_code": a.address.postal_code.trim(), "state": a.address.state.trim() });
1017 if !crate::stripe::address_places_customer(&place) {
1018 return Ok(Outcome::fail(FailureCode::Invalid, "Give at least the country, and in the US the ZIP code: Stripe Tax needs them."));
1019 }
1020 let Some(stripe) = &self.stripe else {
1021 return Ok(Outcome::fail(FailureCode::Conflict, "Payments are not set up on this g1t."));
1022 };
1023 #[derive(Deserialize)]
1024 struct Row {
1025 kind: String,
1026 customer_id: Option<String>,
1027 }
1028 let Some(row) = self
1029 .db
1030 .prepare("SELECT kind, customer_id FROM billing_accounts WHERE id = ?")
1031 .bind(&[a.id.as_str().into()])?
1032 .first::<Row>(None)
1033 .await?
1034 .filter(|row| row.kind == "enterprise")
1035 else {
1036 return Ok(Outcome::fail(FailureCode::NotFound, "No such enterprise."));
1037 };
1038 let Some(customer) = row.customer_id else {
1039 return Ok(Outcome::fail(FailureCode::Conflict, "Set where its invoices go first: that makes its Stripe customer."));
1040 };
1041 let fields: Vec<(&str, String)> = vec![
1042 ("address[line1]", a.address.line1.trim().to_owned()),
1043 ("address[line2]", a.address.line2.trim().to_owned()),
1044 ("address[city]", a.address.city.trim().to_owned()),
1045 ("address[state]", a.address.state.trim().to_owned()),
1046 ("address[postal_code]", a.address.postal_code.trim().to_owned()),
1047 ("address[country]", a.address.country.trim().to_uppercase()),
1048 ];
1049 if let Err(error) = stripe.post::<Value>(&format!("/customers/{customer}"), &fields).await {
1050 return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error)));
1051 }
1052 if let (Some(kind), Some(value)) = (a.tax_id_type.as_deref(), a.tax_id.as_deref())
1053 && let Err(why) = crate::details::replace_tax_id(stripe, &customer, &a.id, kind, value).await
1054 {
1055 return Ok(Outcome::fail(FailureCode::Invalid, why));
1056 }
1057 self.audit(&a.id, "billing_address", &format!("Billing address set ({})", a.address.country.trim().to_uppercase()), &a.by).await?;
1058 Ok(Outcome::Ok(true))
1059 }
1060
1061 pub(crate) async fn admin_invoice_enterprise(&self, a: AdminInvoiceEnterpriseArgs) -> Result<Outcome<EnterpriseInvoice>> {
1062 if a.by.trim().is_empty() {
1063 return Ok(Outcome::fail(FailureCode::Invalid, "Say who is sending it."));
1064 }
1065 match self.invoice_enterprise(&a.id, "now", &a.by).await? {
1066 Ok(invoice) => Ok(Outcome::Ok(invoice)),
1067 Err(why) => Ok(Outcome::fail(FailureCode::Conflict, why)),
1068 }
1069 }
1070
1071 /// Invoices each enterprise for the month that closed. Live payments
1072 /// only; sudo can send one sooner in test mode.
1073 pub(crate) async fn invoice_enterprises(&self) -> Result<()> {
1074 if !self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live) {
1075 return Ok(());
1076 }
1077 let closing = crate::limits::previous_month(&rfc3339(now_ms())[..7]);
1078 #[derive(Deserialize)]
1079 struct Id {
1080 id: String,
1081 }
1082 let due = self
1083 .db
1084 .prepare(format!(
1085 "SELECT id FROM billing_accounts WHERE kind = 'enterprise' AND customer_id IS NOT NULL
1086 AND NOT {full}
1087 AND NOT EXISTS (SELECT 1 FROM enterprise_invoices i WHERE i.account_id = billing_accounts.id AND i.period = ?)",
1088 full = crate::sales::FULL_DISCOUNT_SQL
1089 ))
1090 .bind(&[closing.as_str().into()])?
1091 .all()
1092 .await?
1093 .results::<Id>()?;
1094 for Id { id } in due {
1095 if let Err(why) = self.invoice_enterprise(&id, &closing, "month close").await? {
1096 worker::console_log!("enterprise {id} not invoiced for {closing}: {why}");
1097 }
1098 }
1099 Ok(())
1100 }
1101
1102 /// One invoice for what each of the enterprise's workspaces owes now.
1103 async fn invoice_enterprise(&self, id: &str, period: &str, by: &str) -> Result<std::result::Result<EnterpriseInvoice, String>> {
1104 let Some(stripe) = &self.stripe else { return Ok(Err("Payments are not set up.".into())) };
1105 let Some(account) = self.enterprise(id).await? else { return Ok(Err("No such enterprise.".into())) };
1106 #[derive(Deserialize)]
1107 struct Customer {
1108 customer_id: Option<String>,
1109 }
1110 let Some(customer) = self
1111 .db
1112 .prepare("SELECT customer_id FROM billing_accounts WHERE id = ?")
1113 .bind(&[id.into()])?
1114 .first::<Customer>(None)
1115 .await?
1116 .and_then(|row| row.customer_id)
1117 else {
1118 return Ok(Err("Set where the enterprise's invoices go first.".into()));
1119 };
1120 // What each workspace owes: its charges less what it has paid, and
1121 // less what is on invoices still open.
1122 let mut lines = vec![];
1123 for workspace in &account.workspaces {
1124 let balance = self.row(workspace).await?.map_or(0, |row| row.balance_micros);
1125 #[derive(Deserialize)]
1126 struct Sum {
1127 micros: Option<i64>,
1128 }
1129 let invoiced = self
1130 .db
1131 .prepare(
1132 "SELECT SUM(l.amount_micros) AS micros FROM enterprise_invoice_lines l
1133 JOIN enterprise_invoices i ON i.invoice_id = l.invoice_id
1134 WHERE l.workspace = ? AND i.status IN ('open', 'overdue')",
1135 )
1136 .bind(&[workspace.as_str().into()])?
1137 .first::<Sum>(None)
1138 .await?
1139 .and_then(|s| s.micros)
1140 .unwrap_or(0);
1141 let owed = (-balance).max(0) - invoiced;
1142 if owed >= 10_000 {
1143 lines.push(InvoiceLine { workspace: workspace.clone(), amount_micros: owed });
1144 }
1145 }
1146 if lines.is_empty() {
1147 return Ok(Err("Its workspaces owe nothing to invoice.".into()));
1148 }
1149 // Stripe Tax places the customer by its address; without one the
1150 // invoice could not be finalized, so it is not started.
1151 match stripe.customer_placed(&customer).await {
1152 Ok(true) => {}
1153 Ok(false) => {
1154 return Ok(Err(format!(
1155 "Add {}'s billing address first (Invoices → Billing address): Stripe needs it to work out tax.",
1156 account.name
1157 )));
1158 }
1159 Err(error) => return Ok(Err(crate::stripe::friendly(&error))),
1160 }
1161 // Invoiced, never by card: no card fee. Tax on top, by Stripe Tax.
1162 let mut fields = vec![
1163 ("customer", customer.clone()),
1164 ("collection_method", "send_invoice".to_owned()),
1165 ("days_until_due", "30".to_owned()),
1166 ("pending_invoice_items_behavior", "exclude".to_owned()),
1167 ("description", format!("g1t usage for the {} enterprise", account.name)),
1168 ("metadata[g1t_enterprise]", id.to_owned()),
1169 ("metadata[period]", period.to_owned()),
1170 ];
1171 fields.extend(crate::stripe::invoice_tax_fields());
1172 #[derive(Deserialize)]
1173 struct Invoice {
1174 id: String,
1175 #[serde(default)]
1176 hosted_invoice_url: Option<String>,
1177 #[serde(default)]
1178 amount_due: i64,
1179 /// The tax Stripe added, in cents (`tax`, in this API version).
1180 #[serde(default)]
1181 tax: Option<i64>,
1182 }
1183 // The draft first, keyed on what it bills, and its lines put on it:
1184 // an attempt that failed half way is found again, never billed again
1185 // by an invoice sweeping its pending lines in.
1186 let cents: Vec<i64> = lines.iter().map(|line| (line.amount_micros + 9_999) / 10_000).collect();
1187 let keyed: Vec<(&str, i64)> = lines.iter().zip(&cents).map(|(line, cents)| (line.workspace.as_str(), *cents)).collect();
1188 let key = enterprise_invoice_key(id, period, &keyed);
1189 let draft: Invoice = stripe.post_idempotent("/invoices", &fields, &key).await?;
1190 for (line, cents) in lines.iter().zip(&cents) {
1191 let mut fields = vec![
1192 ("customer", customer.clone()),
1193 ("invoice", draft.id.clone()),
1194 ("amount", cents.to_string()),
1195 ("currency", "usd".to_owned()),
1196 ("description", format!("{}: g1t usage", line.workspace)),
1197 ("metadata[workspace]", line.workspace.clone()),
1198 ];
1199 fields.extend(crate::stripe::item_tax_fields());
1200 let _: Value = stripe.post_idempotent("/invoiceitems", &fields, &format!("{key}/item/{}", line.workspace)).await?;
1201 }
1202 // A retry finds it finalized already; that is fine.
1203 let _ = stripe.post::<Value>(&format!("/invoices/{}/finalize", draft.id), &[]).await;
1204 let sent: Invoice = match stripe.post(&format!("/invoices/{}/send", draft.id), &[]).await {
1205 Ok(sent) => sent,
1206 Err(error) if crate::stripe::is_tax_location_error(&error) => {
1207 return Ok(Err(format!("Stripe could not work out tax for {}: add its billing address, then send it again.", account.name)));
1208 }
1209 Err(error) => return Err(error),
1210 };
1211 let now = rfc3339(now_ms());
1212 // What the workspaces owe, before tax: the tax is the enterprise's
1213 // to pay on top, never their usage, and is kept apart.
1214 let tax_cents = sent.tax.unwrap_or(0).max(0);
1215 let total = lines.iter().map(|l| l.amount_micros).sum::<i64>().max((sent.amount_due - tax_cents) * 10_000);
1216 let mut writes = vec![self
1217 .db
1218 .prepare(
1219 "INSERT INTO enterprise_invoices (invoice_id, account_id, period, amount_micros, status, hosted_url, created_by, created_at)
1220 VALUES (?, ?, ?, ?, 'open', ?, ?, ?)",
1221 )
1222 .bind(&[
1223 sent.id.as_str().into(),
1224 id.into(),
1225 period.into(),
1226 (total as f64).into(),
1227 crate::optional(sent.hosted_invoice_url.as_deref()),
1228 by.into(),
1229 now.as_str().into(),
1230 ])?];
1231 for line in &lines {
1232 writes.push(
1233 self.db
1234 .prepare("INSERT INTO enterprise_invoice_lines (invoice_id, workspace, amount_micros) VALUES (?, ?, ?)")
1235 .bind(&[sent.id.as_str().into(), line.workspace.as_str().into(), (line.amount_micros as f64).into()])?,
1236 );
1237 }
1238 self.db.batch(writes).await?;
1239 self.audit(id, "invoice", &format!("Invoice {} for {} sent ({period})", sent.id, crate::features::dollars(total)), by)
1240 .await?;
1241 Ok(Ok(EnterpriseInvoice {
1242 invoice_id: sent.id,
1243 hosted_url: sent.hosted_invoice_url,
1244 amount_micros: total,
1245 status: "open".to_owned(),
1246 period: period.to_owned(),
1247 lines,
1248 created_at: now,
1249 }))
1250 }
1251
1252 /// An enterprise invoice paid: each workspace is credited its line, and
1253 /// any stop for the invoice is lifted.
1254 ///
1255 /// Each line is credited before the invoice is marked paid, once by its
1256 /// ledger reference: a failure part way leaves it open, and the event
1257 /// again credits only the lines not yet credited.
1258 async fn enterprise_invoice_paid(&self, invoice_id: &str) -> Result<String> {
1259 let open = self
1260 .db
1261 .prepare("SELECT invoice_id FROM enterprise_invoices WHERE invoice_id = ? AND status <> 'paid'")
1262 .bind(&[invoice_id.into()])?
1263 .first::<Value>(None)
1264 .await?;
1265 if open.is_none() {
1266 return Ok("ignored: not an open enterprise invoice".to_owned());
1267 }
1268 // Read at billing's API version, before anything is written: the
1269 // event's copy may lack `tax` and `payment_intent`.
1270 let fetched: Option<Value> = match &self.stripe {
1271 Some(stripe) => Some(stripe.get(&format!("/invoices/{invoice_id}")).await?),
1272 None => None,
1273 };
1274 let lines = self.invoice_lines(invoice_id).await?;
1275 for line in &lines {
1276 let reference = format!("inv/{invoice_id}/{}", line.workspace);
1277 let seen = self.db.prepare("SELECT id FROM ledger WHERE reference = ?").bind(&[reference.as_str().into()])?.first::<Value>(None).await?;
1278 if seen.is_some() {
1279 continue;
1280 }
1281 let entered = self
1282 .enter(
1283 &line.workspace,
1284 EntryKind::TopUp,
1285 line.amount_micros,
1286 &format!("Paid on the enterprise's invoice {invoice_id}"),
1287 &reference,
1288 None,
1289 None,
1290 None,
1291 )
1292 .await;
1293 match entered {
1294 Ok(()) => {}
1295 Err(error) if crate::is_duplicate(&error) => {}
1296 Err(error) => return Err(error),
1297 }
1298 }
1299 self.db
1300 .prepare("UPDATE enterprise_invoices SET status = 'paid', paid_at = ? WHERE invoice_id = ? AND status <> 'paid'")
1301 .bind(&[rfc3339(now_ms()).into(), invoice_id.into()])?
1302 .run()
1303 .await?;
1304 // Its tax is the enterprise's, paid on top of what its workspaces
1305 // owed, and never credited to them.
1306 #[derive(Deserialize)]
1307 struct Account {
1308 account_id: String,
1309 }
1310 if let Some(account) = self
1311 .db
1312 .prepare("SELECT account_id FROM enterprise_invoices WHERE invoice_id = ?")
1313 .bind(&[invoice_id.into()])?
1314 .first::<Account>(None)
1315 .await?
1316 && let Some(invoice) = &fetched
1317 {
1318 let extras = crate::tax::Extras { tax_cents: invoice["tax"].as_i64().unwrap_or(0).max(0), fee_cents: 0 };
1319 self.record_extras(&account.account_id, invoice_id, invoice["payment_intent"].as_str(), extras, None).await?;
1320 }
1321 Ok(format!("invoice {invoice_id} paid; {} workspaces credited", lines.len()))
1322 }
1323
1324 /// An enterprise invoice that went overdue stops its workspaces' work;
1325 /// one voided is simply closed.
1326 async fn enterprise_invoice_status(&self, invoice_id: &str, status: &str) -> Result<String> {
1327 let updated = self
1328 .db
1329 .prepare("UPDATE enterprise_invoices SET status = ? WHERE invoice_id = ? AND status <> 'paid' RETURNING invoice_id")
1330 .bind(&[status.into(), invoice_id.into()])?
1331 .first::<Value>(None)
1332 .await?;
1333 if updated.is_none() {
1334 return Ok("ignored: not an open enterprise invoice".to_owned());
1335 }
1336 if status == "overdue" {
1337 let now = rfc3339(now_ms());
1338 for line in self.invoice_lines(invoice_id).await? {
1339 self.db
1340 .prepare(
1341 "INSERT INTO limits (workspace, autopay_failed_at, autopay_error, updated_at) VALUES (?1, ?2, ?3, ?2)
1342 ON CONFLICT (workspace) DO UPDATE SET autopay_failed_at = ?2, autopay_error = ?3, updated_at = ?2",
1343 )
1344 .bind(&[line.workspace.as_str().into(), now.as_str().into(), format!("the enterprise's invoice {invoice_id} is overdue").into()])?
1345 .run()
1346 .await?;
1347 }
1348 }
1349 Ok(format!("invoice {invoice_id} is {status}"))
1350 }
1351
1352 async fn invoice_lines(&self, invoice_id: &str) -> Result<Vec<InvoiceLine>> {
1353 Ok(self
1354 .db
1355 .prepare("SELECT workspace, amount_micros FROM enterprise_invoice_lines WHERE invoice_id = ?")
1356 .bind(&[invoice_id.into()])?
1357 .all()
1358 .await?
1359 .results::<LineRow>()?
1360 .into_iter()
1361 .map(|row| InvoiceLine { workspace: row.workspace, amount_micros: row.amount_micros })
1362 .collect())
1363 }
1364
1365 /// An enterprise's invoices, newest first.
1366 pub(crate) async fn enterprise_invoices(&self, id: &str) -> Result<Vec<EnterpriseInvoice>> {
1367 let rows = self
1368 .db
1369 .prepare("SELECT * FROM enterprise_invoices WHERE account_id = ? ORDER BY created_at DESC LIMIT 24")
1370 .bind(&[JsValue::from(id)])?
1371 .all()
1372 .await?
1373 .results::<InvoiceRow>()?;
1374 let mut invoices = vec![];
1375 for row in rows {
1376 invoices.push(EnterpriseInvoice {
1377 lines: self.invoice_lines(&row.invoice_id).await?,
1378 invoice_id: row.invoice_id,
1379 hosted_url: row.hosted_url,
1380 amount_micros: row.amount_micros,
1381 status: row.status,
1382 period: row.period,
1383 created_at: row.created_at,
1384 });
1385 }
1386 Ok(invoices)
1387 }
1388}
1389
1390#[cfg(test)]
1391mod tests {
1392 use super::*;
1393
1394 #[test]
1395 fn an_enterprise_invoice_attempted_again_is_the_same_invoice() {
1396 let lines = [("acme", 1_250), ("acme-labs", 99)];
1397 let key = enterprise_invoice_key("ent_1", "2026-09", &lines);
1398 assert_eq!(key, enterprise_invoice_key("ent_1", "2026-09", &lines));
1399 assert!(key.len() < 255 && key.starts_with("ent-invoice/ent_1/2026-09/"));
1400 // Different amounts, even with the same total, are a different invoice.
1401 assert_ne!(key, enterprise_invoice_key("ent_1", "2026-09", &[("acme", 1_300), ("acme-labs", 49)]));
1402 assert_ne!(key, enterprise_invoice_key("ent_1", "2026-10", &lines));
1403 }
1404
1405 #[test]
1406 fn an_event_from_the_other_mode_is_ignored() {
1407 assert!(wrong_mode(Some(false), "live").is_some());
1408 assert!(wrong_mode(Some(true), "test").is_some());
1409 assert!(wrong_mode(Some(true), "live").is_none());
1410 assert!(wrong_mode(Some(false), "test").is_none());
1411 assert!(wrong_mode(None, "live").is_none());
1412 }
1413
1414 #[test]
1415 fn a_dispute_won_after_it_was_lost_gives_back_what_it_took_once() {
1416 // Lost then won: the $46.40 taken back is given back.
1417 assert_eq!(restore_on_win(Some("lost"), "won", 46_400_000), 46_400_000);
1418 // Won from open, or won again (a redelivery): nothing.
1419 assert_eq!(restore_on_win(Some("open"), "won", 0), 0);
1420 assert_eq!(restore_on_win(None, "won", 0), 0);
1421 assert_eq!(restore_on_win(Some("won"), "won", 46_400_000), 0);
1422 assert!(!dispute_moves(Some("won"), "won"));
1423 assert_eq!(restore_on_win(Some("lost"), "lost", 46_400_000), 0);
1424 assert_eq!(restore_on_win(Some("lost"), "won", -5), 0);
1425 // Its own reference: unique, so a delivery alongside fails the
1426 // batch instead of giving it back twice; apart from the loss's.
1427 assert_eq!(won_reference("dp_1"), "dispute/dp_1/won");
1428 assert_ne!(won_reference("dp_1"), "dispute/dp_1");
1429 // What is owed comes down by it, never below nothing, before the
1430 // stop is cleared (which needs nothing owed).
1431 assert!(LOWER_DISPUTE_OWED.contains("MAX(0, dispute_owed_micros - ?2)"));
1432 assert!(crate::CLEAR_STOP.contains("dispute_owed_micros = 0"));
1433 }
1434
1435 #[test]
1436 fn a_dispute_event_twice_or_late_changes_nothing() {
1437 assert!(dispute_moves(None, "open"));
1438 assert!(dispute_moves(None, "lost"));
1439 assert!(dispute_moves(Some("open"), "lost"));
1440 assert!(dispute_moves(Some("open"), "won"));
1441 assert!(dispute_moves(Some("lost"), "won"));
1442 // The same state again: nothing stopped, taken back or owed twice.
1443 for state in ["open", "won", "lost"] {
1444 assert!(!dispute_moves(Some(state), state), "{state}");
1445 }
1446 // A late `created` after the dispute was lost never re-opens it.
1447 assert!(!dispute_moves(Some("lost"), "open"));
1448 }
1449
1450 #[test]
1451 fn a_refund_counts_what_earlier_refunds_took_back_wherever_they_were_kept() {
1452 assert!(ALREADY_REFUNDED_SQL.contains("FROM plan_payments WHERE invoice_id LIKE ?1"));
1453 assert!(ALREADY_REFUNDED_SQL.contains("FROM ledger WHERE reference LIKE ?1"));
1454 assert_eq!(crate::rename::parameters(ALREADY_REFUNDED_SQL), 1);
1455 }
1456
1457 #[test]
1458 fn a_dispute_is_acted_on_as_it_stands_now() {
1459 for open in ["warning_needs_response", "warning_under_review", "needs_response", "under_review"] {
1460 assert_eq!(dispute_state(open), "open");
1461 }
1462 assert_eq!(dispute_state("won"), "won");
1463 assert_eq!(dispute_state("warning_closed"), "won");
1464 assert_eq!(dispute_state("lost"), "lost");
1465 // A lost dispute takes back only the credit: $50 paid as $46.40
1466 // credit, $1.60 card fee and $2 tax.
1467 assert_eq!(crate::tax::refund_split(5_000, 5_000, 200, 160).0, 4_640);
1468 // Lifting a stop for a won dispute leaves a declined card's.
1469 assert!(crate::CLEAR_STOP.contains("dispute_owed_micros = 0") && crate::CLEAR_STOP.contains("status = 'open'"));
1470 assert!(!DISPUTE_STOP.is_empty());
1471 }
1472
1473 fn sign(payload: &str, secret: &str, t: i64) -> String {
1474 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
1475 mac.update(format!("{t}.{payload}").as_bytes());
1476 format!("t={t},v1={}", hex::encode(mac.finalize().into_bytes()))
1477 }
1478
1479 #[test]
1480 fn a_destination_misses_the_events_billing_handles_that_it_does_not_send() {
1481 let has: Vec<String> = EVENTS.iter().take(11).map(|e| (*e).to_owned()).collect();
1482 assert_eq!(missing_events(&has), EVENTS[11..].iter().map(|e| (*e).to_owned()).collect::<Vec<_>>());
1483 let all: Vec<String> = EVENTS.iter().map(|e| (*e).to_owned()).collect();
1484 assert!(missing_events(&all).is_empty());
1485 assert!(missing_events(&["*".to_owned()]).is_empty());
1486 }
1487
1488 #[test]
1489 fn a_signed_event_is_believed_only_as_signed_and_only_fresh() {
1490 let payload = r#"{"id":"evt_1","type":"invoice.paid"}"#;
1491 let header = sign(payload, "whsec_test", 1_000_000);
1492 assert!(verify(payload, &header, "whsec_test", 1_000_010));
1493 assert!(!verify(payload, &header, "whsec_other", 1_000_010));
1494 assert!(!verify(&payload.replace("paid", "voided"), &header, "whsec_test", 1_000_010));
1495 assert!(!verify(payload, &header, "whsec_test", 1_000_000 + 301));
1496 assert!(!verify(payload, "v1=abc", "whsec_test", 1_000_000));
1497 // Stripe may sign with more than one secret while one is rolled.
1498 let both = format!("{},v1=00ff", sign(payload, "whsec_test", 1_000_000));
1499 assert!(verify(payload, &both, "whsec_test", 1_000_000));
1500 }
1501}