From a9f66dc3e5d26bcff6f6eb1db5543d4af617c32c Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 3 Jun 2026 11:29:24 -0700 Subject: [PATCH] Fix not charging existing relays on reactivation --- backend/src/billing.rs | 10 ++++++---- backend/src/command.rs | 12 +++++++----- backend/src/query.rs | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/backend/src/billing.rs b/backend/src/billing.rs index 4c719c8..34f3bec 100644 --- a/backend/src/billing.rs +++ b/backend/src/billing.rs @@ -89,13 +89,15 @@ impl Billing { ) -> Result<()> { let mut tenant = tenant.clone(); - let activities = query::list_billable_activity(&tenant.pubkey).await?; + let mut activities = query::list_billable_activity(&tenant.pubkey).await?; // A churned tenant with fresh billable activity is using the service // again: re-activate billing (and restore their relays) before billing it. + // Reactivation records a billable activity for each restored relay; fold + // those into this pass so their prorated charges land on the same invoice. if tenant.churned_at.is_some() && !activities.is_empty() { let relays = query::list_relays_for_tenant(&tenant.pubkey).await?; - command::reactivate_tenant(&tenant.pubkey, &relays).await?; + activities.extend(command::reactivate_tenant(&tenant.pubkey, &relays).await?); tenant.churned_at = None; } @@ -154,12 +156,12 @@ impl Billing { self.make_prorated_item(tenant, activity, 1, "New relay created") .await? } - "activate_relay" => { + "activate_relay" | "unmark_relay_delinquent" => { self.make_prorated_item(tenant, activity, 1, "Relay reactivated") .await? } "deactivate_relay" => { - self.make_prorated_item(tenant, activity, -1, "Relay deactivated (prorated credit)") + self.make_prorated_item(tenant, activity, -1, "Relay deactivated") .await? } "update_relay" => self.make_plan_change_item(tenant, activity).await?, diff --git a/backend/src/command.rs b/backend/src/command.rs index 16366e1..1212443 100644 --- a/backend/src/command.rs +++ b/backend/src/command.rs @@ -114,8 +114,10 @@ pub async fn churn_tenant(tenant_pubkey: &str, now: i64, relays: &[Relay]) -> Re } /// Atomically re-activate a churned tenant: clear the churn marker, restore every -/// delinquent relay to active, and void any still-open invoices. -pub async fn reactivate_tenant(tenant_pubkey: &str, relays: &[Relay]) -> Result<()> { +/// delinquent relay to active, and void any still-open invoices. Returns the +/// `unmark_relay_delinquent` activities recorded for the restored relays, so the +/// caller can fold their prorated charges into the same reconcile pass. +pub async fn reactivate_tenant(tenant_pubkey: &str, relays: &[Relay]) -> Result> { let activities = with_tx(async |tx| { set_tenant_churned_at_tx(tx, tenant_pubkey, None).await?; @@ -135,10 +137,10 @@ pub async fn reactivate_tenant(tenant_pubkey: &str, relays: &[Relay]) -> Result< }) .await?; - for activity in activities { - publish(activity); + for activity in &activities { + publish(activity.clone()); } - Ok(()) + Ok(activities) } // --- Relays --- diff --git a/backend/src/query.rs b/backend/src/query.rs index f063534..9ab1955 100644 --- a/backend/src/query.rs +++ b/backend/src/query.rs @@ -233,7 +233,8 @@ pub async fn list_billable_activity(tenant_pubkey: &str) -> Result "WHERE tenant_pubkey = ? AND billed_at IS NULL AND activity_type IN ( - 'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay' + 'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay', + 'unmark_relay_delinquent' ) ORDER BY created_at ASC", ))