Add BillingPeriod helper

This commit is contained in:
Jon Staab
2026-05-28 13:17:06 -07:00
parent b11fb5dc25
commit 72b30489b9
2 changed files with 133 additions and 132 deletions
+42 -36
View File
@@ -1,6 +1,7 @@
use anyhow::Result;
use sqlx::{Sqlite, Transaction};
use crate::billing::BillingPeriod;
use crate::db::{pool, publish, with_tx};
use crate::models::{
Activity, Bolt11, Invoice, InvoiceItem, RELAY_STATUS_ACTIVE, RELAY_STATUS_DELINQUENT,
@@ -59,42 +60,6 @@ pub async fn clear_tenant_nwc_error(pubkey: &str) -> Result<()> {
Ok(())
}
/// Insert this period's renewal items and advance the tenant's `renewed_at`
/// marker to `period_start`, atomically and idempotently.
pub async fn renew_tenant(
tenant_pubkey: &str,
period_start: i64,
items: &[InvoiceItem],
) -> Result<()> {
with_tx(async |tx| {
// Re-read the marker inside the transaction so the guard and the writes
// commit together — this ensures idempotency so we don't double-invoice.
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 {
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(())
})
.await
}
// --- Relays ---
pub async fn create_relay(relay: &Relay) -> Result<()> {
@@ -241,6 +206,47 @@ pub async fn insert_invoice_item_for_activity(invoice_item: &InvoiceItem, activi
.await
}
/// Insert this period's renewal items and advance the tenant's `renewed_at`
/// marker to `period.start`, atomically and idempotently. Empty `items` is a
/// no-op — a tenant with no active paid relays has nothing to renew.
pub async fn insert_invoice_items_for_renewal(
items: &[InvoiceItem],
period: &BillingPeriod,
) -> Result<()> {
let Some(first) = items.first() else {
return Ok(());
};
let tenant_pubkey = &first.tenant_pubkey;
with_tx(async |tx| {
// Re-read the marker inside the transaction so the guard and the writes
// commit together — this ensures idempotency so we don't double-invoice.
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 {
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(())
})
.await
}
/// Mark an activity billed without a line item — for activities that produce no
/// charge (e.g. free-plan changes), so a recovery pass doesn't re-scan them.
pub async fn mark_activity_billed(activity_id: &str) -> Result<()> {