diff --git a/backend/src/api.rs b/backend/src/api.rs index 03b6e01..52e5887 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -18,8 +18,7 @@ use std::sync::Arc; use anyhow::{Result, anyhow, ensure}; use axum::{ - Router, - async_trait, + Router, async_trait, extract::FromRequestParts, http::{HeaderMap, request::Parts}, routing::{get, post}, @@ -32,9 +31,8 @@ use crate::env; use crate::models::{Relay, Tenant}; use crate::query; use crate::robot::Robot; -use crate::stripe::Stripe; use crate::routes::identity::get_identity; -use crate::routes::invoices::{get_invoice, get_invoice_bolt11, get_tenant_latest_invoice}; +use crate::routes::invoices::{get_invoice, get_invoice_bolt11, list_invoice_items}; use crate::routes::plans::{get_plan, list_plans}; use crate::routes::relays::{ create_relay, deactivate_relay, get_relay, list_relay_activity, list_relay_members, @@ -44,6 +42,7 @@ use crate::routes::tenants::{ create_stripe_session, create_tenant, get_tenant, list_tenant_invoices, list_tenant_relays, list_tenants, update_tenant, }; +use crate::stripe::Stripe; use crate::web::{ApiError, forbidden, internal, not_found, unauthorized}; #[derive(Clone)] @@ -74,10 +73,9 @@ impl Api { .route("/tenants/:pubkey/relays", get(list_tenant_relays)) .route("/tenants/:pubkey/invoices", get(list_tenant_invoices)) .route( - "/tenants/:pubkey/invoices/latest", - get(get_tenant_latest_invoice), + "/tenants/:pubkey/stripe/session", + get(create_stripe_session), ) - .route("/tenants/:pubkey/stripe/session", get(create_stripe_session)) .route("/relays", get(list_relays).post(create_relay)) .route("/relays/:id", get(get_relay).put(update_relay)) .route("/relays/:id/members", get(list_relay_members)) @@ -86,6 +84,7 @@ impl Api { .route("/relays/:id/reactivate", post(reactivate_relay)) .route("/invoices/:id", get(get_invoice)) .route("/invoices/:id/bolt11", get(get_invoice_bolt11)) + .route("/invoices/:id/items", get(list_invoice_items)) .with_state(api) } @@ -185,7 +184,10 @@ impl Api { .last() .ok_or_else(|| anyhow!("missing u tag"))?; - ensure!(got_u == env::get().server_host, "authorization host mismatch"); + ensure!( + got_u == env::get().server_host, + "authorization host mismatch" + ); Ok(event.pubkey.to_hex()) } diff --git a/backend/src/billing.rs b/backend/src/billing.rs index 0de6541..71be8ef 100644 --- a/backend/src/billing.rs +++ b/backend/src/billing.rs @@ -14,9 +14,12 @@ use crate::wallet::Wallet; const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); const GRACE_PERIOD_SECS: i64 = 7 * 24 * 60 * 60; +/// Hold the manual-payment DM until an open invoice is at least this old. A freshly +/// issued invoice is surfaced to the tenant in-app first (e.g. right after they +/// create a relay), so we don't also nag by DM on the first dunning cycles. +const FRESH_INVOICE_DM_GRACE_SECS: i64 = 24 * 60 * 60; const MANUAL_PAYMENT_DM: &str = "Payment is due for your relay subscription. Open the link below to review the invoice and pay by Lightning or card:"; -const USER_ERROR_PREFIX: &str = "Auto-payment failed:"; -const USER_ERROR_MAX_CHARS: usize = 240; +const CHURN_DM: &str = "Your relay subscription is past due, so your relays have been paused. You can restore service any time by adding a payment method or paying an invoice from your dashboard:"; /// Owns subscription billing: it reconciles tenant activity into invoice items, /// renews subscriptions each period, and collects payment (Lightning, then a @@ -54,10 +57,13 @@ impl Billing { async fn reconcile_subscriptions(&self) -> Result<()> { let tenants = query::list_tenants().await?; - tracing::info!(tenant_count = tenants.len(), "reconciling all subscriptions"); + tracing::info!( + tenant_count = tenants.len(), + "reconciling all subscriptions" + ); for tenant in tenants { - if let Err(error) = self.reconcile_subscription(&tenant).await { + if let Err(error) = self.reconcile_subscription(&tenant, true).await { tracing::error!( tenant = %tenant.pubkey, error = ?error, @@ -73,10 +79,13 @@ impl Billing { /// Reconciles a tenant's billing: re-activates them if a churned tenant has /// re-engaged, folds billable activity into line items (setting the billing - /// anchor on the first), renews the current period if due, claims outstanding - /// items onto an invoice, and then collects every open invoice — churning the - /// tenant if one has gone unpaid past the grace period. - pub async fn reconcile_subscription(&self, tenant: &Tenant) -> Result<()> { + /// anchor based on the first one), renews the current period if due, and claims + /// outstanding items onto an invoice. + pub async fn reconcile_subscription( + &self, + tenant: &Tenant, + attempt_payment: bool, + ) -> Result<()> { let mut tenant = tenant.clone(); let activities = query::list_billable_activity(&tenant.pubkey).await?; @@ -110,9 +119,11 @@ impl Billing { command::create_invoice(&tenant, &period).await?; } - // Retry payment on every open invoice (this also pays one just created), - // churning the tenant if the oldest has aged past the grace period. - self.collect_open_invoices(&tenant).await?; + // Attempt payment on every open invoice after syncing with stripe. + if attempt_payment { + self.sync_stripe_customer(&mut tenant).await?; + self.collect_open_invoices(&tenant).await?; + } Ok(()) } @@ -140,7 +151,7 @@ impl Billing { }; match invoice_item { - Some(ref item) => command::insert_invoice_item_for_activity(&item, &activity.id).await, + Some(ref item) => command::insert_invoice_item_for_activity(item, &activity.id).await, None => command::mark_activity_billed(&activity.id).await, } } @@ -242,7 +253,11 @@ impl Billing { else { continue; }; - let Snapshot::Relay { plan: plan_id, status, .. } = &*activity.snapshot; + let Snapshot::Relay { + plan: plan_id, + status, + .. + } = &*activity.snapshot; if status != RELAY_STATUS_ACTIVE { continue; } @@ -270,6 +285,37 @@ impl Billing { // --- Payments --- + /// Dunning pass over a tenant's open invoices: if the oldest has been unpaid + /// past the grace period, churn the tenant; otherwise retry payment on each. + async fn collect_open_invoices(&self, tenant: &Tenant) -> Result<()> { + let open = query::list_open_invoices(&tenant.pubkey).await?; + let Some(oldest) = open.first() else { + return Ok(()); + }; + + let now = chrono::Utc::now().timestamp(); + if now - oldest.created_at >= GRACE_PERIOD_SECS { + if tenant.churned_at.is_none() { + let relays = query::list_relays_for_tenant(&tenant.pubkey).await?; + command::churn_tenant(&tenant.pubkey, now, &relays).await?; + + // Notify the tenant once, on the transition into churn (the guard + // above fires this a single time). Log-and-continue on failure. + let message = format!("{CHURN_DM}\n\n{}/account", env::get().app_url); + if let Err(e) = self.robot.send_dm(&tenant.pubkey, &message).await { + tracing::error!(tenant = %tenant.pubkey, error = %e, "failed to send churn DM"); + } + } + return Ok(()); + } + + for invoice in &open { + self.attempt_payment(tenant, invoice).await?; + } + + Ok(()) + } + /// Collect an invoice via NWC, then a saved card, then a manual DM. A failing /// method's error is stored on the tenant (to warn them in the UI) but never /// aborts the cascade or future retries; a method's error is cleared when it @@ -294,12 +340,11 @@ impl Billing { return Ok(()); } - // 3. Payment method on file: if the tenant has one saved, charge it via Stripe. - if let Some(payment_method) = - self.stripe.get_saved_payment_method(&tenant.stripe_customer_id).await? - { + // 3. Payment method on file: charge the tenant's cached Stripe payment + // method, kept fresh by sync_stripe_payment_method before collection. + if let Some(payment_method) = &tenant.stripe_payment_method_id { match self - .attempt_payment_using_stripe(tenant, invoice, &payment_method) + .attempt_payment_using_stripe(tenant, invoice, payment_method) .await { Ok(()) => return Ok(()), @@ -308,8 +353,10 @@ impl Billing { } // 4. Manual payment: DM a link to the in-app payment page for this invoice. - let summary = error_message.as_deref().and_then(summarize_error_message); - if let Err(e) = self.attempt_payment_using_dm(tenant, invoice, summary).await { + if let Err(e) = self + .attempt_payment_using_dm(tenant, invoice, error_message) + .await + { tracing::error!( tenant = %tenant.pubkey, error = %e, @@ -375,15 +422,23 @@ impl Billing { &self, tenant: &Tenant, invoice: &Invoice, - error_message: Option, + error: Option, ) -> Result<()> { + // If the invoice was just generated, give the user a chance to check the dashboard before nagging them. + let now = chrono::Utc::now().timestamp(); + if now - invoice.created_at < FRESH_INVOICE_DM_GRACE_SECS { + return Ok(()); + } + let invoice_id = &invoice.id; let url_base = &env::get().app_url; let payment_url = format!("{url_base}/account?invoice={invoice_id}"); let base = format!("{MANUAL_PAYMENT_DM}\n\n{payment_url}"); - let dm_message = match error_message { - Some(error_message) if !error_message.is_empty() => { - format!("{base}\n\n{USER_ERROR_PREFIX} {error_message}") + let dm_message = match error { + Some(error) if !error.is_empty() => { + let limit: usize = 240; + let summary = error.chars().take(limit.saturating_sub(3)).collect::(); + format!("{base}\n\nAuto-payment failed: {summary}") } _ => base, }; @@ -391,32 +446,6 @@ impl Billing { self.robot.send_dm(&tenant.pubkey, &dm_message).await } - // --- Dunning --- - - /// Dunning pass over a tenant's open invoices: if the oldest has been unpaid - /// past the grace period, churn the tenant; otherwise retry payment on each. - async fn collect_open_invoices(&self, tenant: &Tenant) -> Result<()> { - let open = query::list_open_invoices(&tenant.pubkey).await?; - let Some(oldest) = open.first() else { - return Ok(()); - }; - - let now = chrono::Utc::now().timestamp(); - if now - oldest.created_at >= GRACE_PERIOD_SECS { - if tenant.churned_at.is_none() { - let relays = query::list_relays_for_tenant(&tenant.pubkey).await?; - command::churn_tenant(&tenant.pubkey, now, &relays).await?; - } - return Ok(()); - } - - for invoice in &open { - self.attempt_payment(tenant, invoice).await?; - } - - Ok(()) - } - // --- Bolt11 utils --- pub async fn ensure_bolt11(&self, invoice: &Invoice) -> Result { @@ -455,6 +484,37 @@ impl Billing { Ok(bolt11) } } + + // --- Stripe utils --- + + /// Copy down any stripe-related stuff to our local tenant model. Fail gracefully. + pub async fn sync_stripe_customer(&self, tenant: &mut Tenant) -> Result<()> { + match self.sync_stripe_payment_method(tenant).await { + Ok(payment_method_id) => { + tenant.stripe_payment_method_id = payment_method_id; + } + Err(error) => { + tracing::error!(tenant = %tenant.pubkey, error = ?error, "failed to sync payment method"); + } + }; + + Ok(()) + } + + /// Refresh the cached Stripe payment method from Stripe so collection can charge + /// it directly and the UI reflects cards added via the portal. + async fn sync_stripe_payment_method(&self, tenant: &Tenant) -> Result> { + let payment_method_id = self + .stripe + .get_saved_payment_method(&tenant.stripe_customer_id) + .await?; + + if payment_method_id != tenant.stripe_payment_method_id { + command::set_tenant_stripe_payment_method(&tenant.pubkey, &payment_method_id).await?; + } + + Ok(payment_method_id) + } } /// One tenant's monthly billing period containing some timestamp, anchored at @@ -517,19 +577,3 @@ impl BillingPeriod { (amount as f64 * self.fraction_remaining(at)).round() as i64 } } - -fn summarize_error_message(error: &str) -> Option { - let normalized = error.split_whitespace().collect::>().join(" "); - if normalized.is_empty() { - return None; - } - - if normalized.chars().count() <= USER_ERROR_MAX_CHARS { - return Some(normalized); - } - - let prefix_len = USER_ERROR_MAX_CHARS.saturating_sub(3); - let mut truncated = normalized.chars().take(prefix_len).collect::(); - truncated.push_str("..."); - Some(truncated) -} diff --git a/backend/src/bitcoin.rs b/backend/src/bitcoin.rs index 58025ac..db1be9b 100644 --- a/backend/src/bitcoin.rs +++ b/backend/src/bitcoin.rs @@ -27,8 +27,7 @@ pub async fn get_bitcoin_price(currency: &str) -> Result { let resp = http.get(url).send().await?; let body: CoinbaseSpotPriceResponse = resp.error_for_status()?.json().await?; - body - .data + body.data .amount .parse::() .map_err(|e| anyhow!("invalid BTC spot quote for {currency}: {e}")) diff --git a/backend/src/command.rs b/backend/src/command.rs index 7939bf8..a7379d5 100644 --- a/backend/src/command.rs +++ b/backend/src/command.rs @@ -25,8 +25,10 @@ pub async fn create_tenant(tenant: &Tenant) -> Result<()> { Ok(()) } +/// Update a tenant's NWC credentials, clearing any stored NWC error so a fresh +/// wallet starts from a clean slate (it re-errors on the next charge if invalid). pub async fn update_tenant(tenant: &Tenant) -> Result<()> { - sqlx::query("UPDATE tenant SET nwc_url = ? WHERE pubkey = ?") + sqlx::query("UPDATE tenant SET nwc_url = ?, nwc_error = NULL WHERE pubkey = ?") .bind(&tenant.nwc_url) .bind(&tenant.pubkey) .execute(pool()) @@ -34,6 +36,24 @@ pub async fn update_tenant(tenant: &Tenant) -> Result<()> { Ok(()) } +/// Cache the tenant's Stripe payment method id (or clear it with `None`) and clear +/// any stored Stripe error. Called when a card is (re)attached via the portal or +/// detected during reconciliation, so collection can charge it directly and the UI +/// reflects the change. +pub async fn set_tenant_stripe_payment_method( + pubkey: &str, + payment_method_id: &Option, +) -> Result<()> { + sqlx::query( + "UPDATE tenant SET stripe_payment_method_id = ?, stripe_error = NULL WHERE pubkey = ?", + ) + .bind(payment_method_id) + .bind(pubkey) + .execute(pool()) + .await?; + Ok(()) +} + pub async fn set_tenant_billing_anchor(tenant: &Tenant) -> Result<()> { sqlx::query("UPDATE tenant SET billing_anchor = ? WHERE pubkey = ?") .bind(tenant.billing_anchor) @@ -70,9 +90,13 @@ pub async fn churn_tenant(tenant_pubkey: &str, now: i64, relays: &[Relay]) -> Re let mut activities = Vec::new(); for relay in relays { if relay.status == RELAY_STATUS_ACTIVE { - let activity = - set_relay_status_tx(tx, relay, RELAY_STATUS_DELINQUENT, "mark_relay_delinquent") - .await?; + let activity = set_relay_status_tx( + tx, + relay, + RELAY_STATUS_DELINQUENT, + "mark_relay_delinquent", + ) + .await?; activities.push(activity); } } @@ -252,7 +276,10 @@ pub async fn complete_relay_sync(relay: &Relay) -> Result<()> { /// Persist a reconciled activity's line item and mark the activity billed in one /// transaction, so a recovery pass never re-bills it. -pub async fn insert_invoice_item_for_activity(invoice_item: &InvoiceItem, activity_id: &str) -> Result<()> { +pub async fn insert_invoice_item_for_activity( + invoice_item: &InvoiceItem, + activity_id: &str, +) -> Result<()> { let now = chrono::Utc::now().timestamp(); with_tx(async |tx| { @@ -282,12 +309,11 @@ pub async fn insert_invoice_items_for_renewal( 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>( - "SELECT renewed_at FROM tenant WHERE pubkey = ?", - ) - .bind(tenant_pubkey) - .fetch_one(&mut **tx) - .await?; + let renewed_at = + sqlx::query_scalar::<_, Option>("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(()); @@ -339,7 +365,7 @@ pub async fn create_invoice(tenant: &Tenant, period: &BillingPeriod) -> Result, item: &InvoiceItem) -> Result<()> { +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_id, amount, description, created_at) @@ -520,11 +549,12 @@ async fn mark_activity_billed_tx( activity_id: &str, billed_at: i64, ) -> Result { - let result = sqlx::query("UPDATE activity SET billed_at = ? WHERE id = ? AND billed_at IS NULL") - .bind(billed_at) - .bind(activity_id) - .execute(&mut **tx) - .await?; + let result = + sqlx::query("UPDATE activity SET billed_at = ? WHERE id = ? AND billed_at IS NULL") + .bind(billed_at) + .bind(activity_id) + .execute(&mut **tx) + .await?; Ok(result.rows_affected() > 0) } @@ -578,7 +608,10 @@ async fn mark_invoice_paid_tx( /// Void all of a tenant's open invoices, forgiving the balance — used when a /// tenant churns or re-activates, so old debt never has to be collected. -async fn void_open_invoices_tx(tx: &mut Transaction<'_, Sqlite>, tenant_pubkey: &str) -> Result<()> { +async fn void_open_invoices_tx( + tx: &mut Transaction<'_, Sqlite>, + tenant_pubkey: &str, +) -> Result<()> { let voided_at = chrono::Utc::now().timestamp(); sqlx::query( @@ -615,7 +648,10 @@ async fn clear_tenant_nwc_error_tx(tx: &mut Transaction<'_, Sqlite>, pubkey: &st Ok(()) } -async fn clear_tenant_stripe_error_tx(tx: &mut Transaction<'_, Sqlite>, pubkey: &str) -> Result<()> { +async fn clear_tenant_stripe_error_tx( + tx: &mut Transaction<'_, Sqlite>, + pubkey: &str, +) -> Result<()> { sqlx::query("UPDATE tenant SET stripe_error = NULL WHERE pubkey = ?") .bind(pubkey) .execute(&mut **tx) diff --git a/backend/src/infra.rs b/backend/src/infra.rs index d534362..855a509 100644 --- a/backend/src/infra.rs +++ b/backend/src/infra.rs @@ -74,7 +74,11 @@ async fn reconcile_relay_state(source: &str) -> Result<()> { return Ok(()); } - tracing::info!(source, relay_count = relays.len(), "reconciling pending relay state"); + tracing::info!( + source, + relay_count = relays.len(), + "reconciling pending relay state" + ); for relay in relays { if relay.sync_error.trim().is_empty() { @@ -229,7 +233,11 @@ async fn try_sync_relay(relay: &Relay) -> Result<()> { ); } - let method = if is_new { HttpMethod::POST } else { HttpMethod::PATCH }; + let method = if is_new { + HttpMethod::POST + } else { + HttpMethod::PATCH + }; request(method, &format!("relay/{}", relay.id), Some(&body)).await?; Ok(()) } diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 16a0d8d..cde3fe9 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -2,10 +2,10 @@ pub mod api; pub mod billing; pub mod bitcoin; pub mod command; +pub mod db; pub mod env; pub mod infra; pub mod models; -pub mod db; pub mod query; pub mod robot; pub mod routes; diff --git a/backend/src/main.rs b/backend/src/main.rs index 9b52f2c..2188bcb 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,10 +2,10 @@ mod api; mod billing; mod bitcoin; mod command; +mod db; mod env; mod infra; mod models; -mod db; mod query; mod robot; mod routes; @@ -16,7 +16,7 @@ mod web; use anyhow::Result; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use tower_http::cors::{AllowOrigin, CorsLayer, Any}; +use tower_http::cors::{AllowOrigin, Any, CorsLayer}; use crate::api::Api; use crate::billing::Billing; @@ -61,8 +61,7 @@ async fn main() -> Result<()> { billing.start().await; }); - let listener = - tokio::net::TcpListener::bind(format!( + let listener = tokio::net::TcpListener::bind(format!( "{}:{}", env::get().server_host, env::get().server_port diff --git a/backend/src/models.rs b/backend/src/models.rs index 409047c..ccb6801 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -39,7 +39,8 @@ pub struct Tenant { pub nwc_url: String, /// Last NWC auto-payment error, or `None` when the wallet last paid (or has /// never been tried). Surfaced in the UI to warn the user; it never blocks a - /// retry — the next reconcile attempts payment again regardless. + /// retry — the next reconcile attempts payment again regardless. Also cleared + /// when the tenant updates their NWC credentials. pub nwc_error: Option, /// Last Stripe auto-payment error, with the same semantics as `nwc_error`. pub stripe_error: Option, @@ -122,40 +123,43 @@ impl Default for Relay { /// balance forgiven when the tenant churns). #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct Invoice { - pub id: String, - pub tenant_pubkey: String, - /// The total owed, fixed when the invoice is cut from its outstanding line - /// items, so collection never has to re-sum them. - pub amount: i64, - pub period_start: i64, - pub period_end: i64, - pub created_at: i64, - pub paid_at: Option, - pub voided_at: Option, + pub id: String, + pub tenant_pubkey: String, + /// The total owed, fixed when the invoice is cut from its outstanding line + /// items, so collection never has to re-sum them. + pub amount: i64, + pub period_start: i64, + pub period_end: i64, + pub created_at: i64, + pub paid_at: Option, + pub voided_at: Option, + /// How the invoice was paid — `nwc`, `stripe`, or `oob` (out-of-band + /// Lightning) — set when it is marked paid; `None` while open or void. + pub method: Option, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct InvoiceItem { - pub id: String, - /// `None` while outstanding; set once the item is claimed onto an invoice. - pub invoice_id: Option, - /// `None` for renewal items, which have no source activity. - pub activity_id: Option, - pub tenant_pubkey: String, - pub relay_id: String, - pub plan_id: String, - pub amount: i64, - pub description: String, - pub created_at: i64, + pub id: String, + /// `None` while outstanding; set once the item is claimed onto an invoice. + pub invoice_id: Option, + /// `None` for renewal items, which have no source activity. + pub activity_id: Option, + pub tenant_pubkey: String, + pub relay_id: String, + pub plan_id: String, + pub amount: i64, + pub description: String, + pub created_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct Bolt11 { - pub id: String, - pub invoice_id: String, - pub lnbc: String, - pub msats: i64, - pub created_at: i64, - pub expires_at: i64, - pub settled_at: Option, + pub id: String, + pub invoice_id: String, + pub lnbc: String, + pub msats: i64, + pub created_at: i64, + pub expires_at: i64, + pub settled_at: Option, } diff --git a/backend/src/query.rs b/backend/src/query.rs index c574986..2f00571 100644 --- a/backend/src/query.rs +++ b/backend/src/query.rs @@ -1,7 +1,7 @@ use anyhow::{Result, anyhow}; -use crate::models::{Activity, Bolt11, Invoice, Plan, Relay, Tenant}; use crate::db::pool; +use crate::models::{Activity, Bolt11, Invoice, InvoiceItem, Plan, Relay, Tenant}; fn select_tenant(tail: &str) -> String { format!("SELECT * FROM tenant {tail}") @@ -62,10 +62,12 @@ pub async fn list_tenants() -> Result> { } pub async fn get_tenant(pubkey: &str) -> Result> { - Ok(sqlx::query_as::<_, Tenant>(&select_tenant("WHERE pubkey = ?")) - .bind(pubkey) - .fetch_optional(pool()) - .await?) + Ok( + sqlx::query_as::<_, Tenant>(&select_tenant("WHERE pubkey = ?")) + .bind(pubkey) + .fetch_optional(pool()) + .await?, + ) } // --- Relays --- @@ -85,10 +87,12 @@ pub async fn list_relays_pending_sync() -> Result> { } pub async fn list_relays_for_tenant(tenant_pubkey: &str) -> Result> { - Ok(sqlx::query_as::<_, Relay>(&select_relay("WHERE tenant_pubkey = ?")) - .bind(tenant_pubkey) - .fetch_all(pool()) - .await?) + Ok( + sqlx::query_as::<_, Relay>(&select_relay("WHERE tenant_pubkey = ?")) + .bind(tenant_pubkey) + .fetch_all(pool()) + .await?, + ) } pub async fn get_relay(id: &str) -> Result> { @@ -119,10 +123,12 @@ pub async fn get_relay_plan_before(relay_id: &str, before: i64) -> Result