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.

59 linesCodeBlameRaw
1use g1t_contracts::billing::InvoiceLine;
2use serde::{Deserialize, Serialize};
3use worker::Result;
4use crate::Billing;
5
6pub(crate) const RETIRE: &str = "DELETE FROM enterprise_invoice_attempts WHERE account_id=?1 AND completed=1";
7// The snapshots used to calculate the invoice must still match at acquisition.
8// In particular, a caller that read before another invoice committed must not
9// invoice the same balance again after that attempt was retired.
10pub(crate) const INSERT: &str = "INSERT OR IGNORE INTO enterprise_invoice_attempts (account_id,id,payload,created_ms)
11 SELECT ?1,?2,?3,?4 WHERE NOT EXISTS (
12 SELECT 1 FROM json_each(?5) s
13 WHERE COALESCE((SELECT balance_micros FROM accounts WHERE workspace=json_extract(s.value,'$.workspace')),0) <> json_extract(s.value,'$.balance')
14 OR COALESCE((SELECT SUM(l.amount_micros) FROM enterprise_invoice_lines l JOIN enterprise_invoices i ON i.invoice_id=l.invoice_id
15 WHERE l.workspace=json_extract(s.value,'$.workspace') AND i.status IN ('open','overdue')),0) <> json_extract(s.value,'$.invoiced')
16 OR NOT EXISTS (SELECT 1 FROM account_members WHERE account_id=?1 AND workspace=json_extract(s.value,'$.workspace'))
17 )";
18
19#[derive(Serialize, Deserialize)]
20pub(crate) struct Request {
21 pub customer: String,
22 pub name: String,
23 pub period: String,
24 pub by: String,
25 pub lines: Vec<InvoiceLine>,
26}
27
28#[derive(Serialize)]
29pub(crate) struct Snapshot {
30 pub workspace: String,
31 pub balance: i64,
32 pub invoiced: i64,
33}
34
35#[derive(Deserialize)]
36pub(crate) struct Attempt {
37 pub id: String,
38 pub payload: String,
39 pub created_ms: u64,
40 pub invoice_id: Option<String>,
41}
42
43impl Billing {
44 pub(crate) async fn enterprise_attempt(&self, account: &str) -> Result<Option<Attempt>> {
45 self.db.prepare("SELECT id,payload,created_ms,invoice_id FROM enterprise_invoice_attempts WHERE account_id=?")
46 .bind(&[account.into()])?.first::<Attempt>(None).await
47 }
48
49 pub(crate) async fn begin_enterprise_attempt(&self, account: &str, request: Request, snapshots: Vec<Snapshot>) -> Result<Attempt> {
50 let now = g1t_kit::now_ms();
51 let json = |value| serde_json::to_string(value).map_err(|e| worker::Error::RustError(e.to_string()));
52 let payload = json(&request)?;
53 let snapshots = serde_json::to_string(&snapshots).map_err(|e| worker::Error::RustError(e.to_string()))?;
54 self.db.prepare(INSERT).bind(&[
55 account.into(), g1t_contracts::new_id("einv", now).into(), payload.into(), (now as f64).into(), snapshots.into(),
56 ])?.run().await?;
57 self.enterprise_attempt(account).await?.ok_or_else(|| worker::Error::RustError("Enterprise invoice balances changed; retry".into()))
58 }
59}