Move renewed_at to tenant

This commit is contained in:
Jon Staab
2026-05-27 15:35:02 -07:00
parent f37bb55286
commit cd70ca6654
4 changed files with 45 additions and 37 deletions
+11 -7
View File
@@ -213,7 +213,7 @@ impl Billing {
let fraction = period_fraction_remaining(anchor, activity.created_at);
let amount = sign * prorate(plan.amount, fraction);
Ok(Some(line_item(activity, &relay.id, plan.id, amount, description, None)))
Ok(Some(line_item(activity, &relay.id, plan.id, amount, description)))
}
/// The prorated delta for a plan change, read straight from the activity log:
@@ -259,7 +259,6 @@ impl Billing {
new_plan.id,
amount,
&description,
None,
)))
}
@@ -283,7 +282,13 @@ impl Billing {
let period_start = period_start_at(anchor, now);
let period_end = add_one_month(period_start);
self.renew_period(&tenant, period_start).await?;
// Short-circuit the renewal scan once this period is already renewed — the
// common case on all but the first poll of a period (saving ~720 scans a
// month per tenant). renew_tenant re-checks this in-tx as the real guard.
if tenant.renewed_at.is_none_or(|at| at < period_start) {
self.renew_period(&tenant, period_start).await?;
}
self.claim_outstanding(&tenant, period_start, period_end).await
}
@@ -317,11 +322,12 @@ impl Billing {
amount: plan.amount,
description: "Subscription renewal".to_string(),
created_at: period_start,
period_start: Some(period_start),
});
}
command::create_renewal_items(&renewal_items).await
// Inserts the items and advances `renewed_at` to `period_start` in one
// transaction (idempotent via an in-tx guard), so a re-tick is a no-op.
command::renew_tenant(&tenant.pubkey, period_start, &renewal_items).await
}
/// Claim the tenant's outstanding items onto a fresh invoice if they net
@@ -562,7 +568,6 @@ fn line_item(
plan: String,
amount: i64,
description: &str,
period_start: Option<i64>,
) -> InvoiceItem {
InvoiceItem {
id: uuid::Uuid::new_v4().to_string(),
@@ -574,7 +579,6 @@ fn line_item(
amount,
description: description.to_string(),
created_at: activity.created_at,
period_start,
}
}
+29 -23
View File
@@ -212,30 +212,37 @@ pub async fn mark_activity_billed(activity_id: &str) -> Result<()> {
/// `period_start`. The per-relay existence check and insert are a single
/// statement, so neither a re-tick nor a relay's own creation/activation charge
/// (which also stamps `period_start`) can bill the same relay-period twice.
pub async fn create_renewal_items(items: &[InvoiceItem]) -> Result<()> {
pub async fn renew_tenant(
tenant_pubkey: &str,
period_start: i64,
items: &[InvoiceItem],
) -> Result<()> {
with_tx(async |tx| {
// In-tx guard: bail if this tenant has already been renewed for this
// period (or later). This is the correctness backstop — it keeps renewal
// idempotent under a crash mid-renewal or a poll racing the eager
// endpoint, since the item inserts and the `renewed_at` write commit
// together.
let renewed_at = sqlx::query_scalar::<_, Option<i64>>(
"SELECT renewed_at FROM tenant WHERE pubkey = ?",
)
.bind(tenant_pubkey)
.fetch_one(&mut **tx)
.await?;
if renewed_at.is_some_and(|at| at >= period_start) {
return Ok(());
}
for item in items {
sqlx::query(
"INSERT INTO invoice_item
(id, invoice_id, activity_id, tenant_pubkey, relay_id, plan, amount, description, created_at, period_start)
SELECT ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM invoice_item WHERE relay_id = ? AND period_start = ?
)",
)
.bind(&item.id)
.bind(&item.tenant_pubkey)
.bind(&item.relay_id)
.bind(&item.plan)
.bind(item.amount)
.bind(&item.description)
.bind(item.created_at)
.bind(item.period_start)
.bind(&item.relay_id)
.bind(item.period_start)
insert_invoice_item_tx(tx, item).await?;
}
sqlx::query("UPDATE tenant SET renewed_at = ? WHERE pubkey = ?")
.bind(period_start)
.bind(tenant_pubkey)
.execute(&mut **tx)
.await?;
}
Ok(())
})
@@ -433,8 +440,8 @@ async fn insert_invoice_tx(
async fn insert_invoice_item_tx(tx: &mut Transaction<'_, Sqlite>, item: &InvoiceItem) -> Result<()> {
sqlx::query(
"INSERT INTO invoice_item
(id, invoice_id, activity_id, tenant_pubkey, relay_id, plan, amount, description, created_at, period_start)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(id, invoice_id, activity_id, tenant_pubkey, relay_id, plan, amount, description, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(&item.id)
.bind(&item.invoice_id)
@@ -445,7 +452,6 @@ async fn insert_invoice_item_tx(tx: &mut Transaction<'_, Sqlite>, item: &Invoice
.bind(item.amount)
.bind(&item.description)
.bind(item.created_at)
.bind(item.period_start)
.execute(&mut **tx)
.await?;
Ok(())
+3 -3
View File
@@ -36,6 +36,9 @@ pub struct Tenant {
pub created_at: i64,
pub billing_anchor: Option<i64>,
pub stripe_customer_id: String,
/// `period_start` of the most recent period this tenant was renewed for, or
/// `None` if never renewed. The per-period renewal idempotency marker.
pub renewed_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
@@ -107,9 +110,6 @@ pub struct InvoiceItem {
pub amount: i64,
pub description: String,
pub created_at: i64,
/// Set only on renewal items: the billing period this item renews. Doubles as
/// the marker that prevents a period from being renewed twice.
pub period_start: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]