Move renewed_at to tenant
This commit is contained in:
+11
-7
@@ -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
@@ -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(())
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user