From 28e564e79519e3f24bec5bb653b70621fd6d9dcb Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 25 Mar 2026 17:01:52 -0700 Subject: [PATCH] Review pass --- backend/spec/billing.md | 2 +- backend/spec/infra.md | 4 +- backend/spec/main.md | 1 - backend/spec/models.md | 30 +++++---- backend/spec/repo.md | 37 ++++++----- backend/src/billing.rs | 6 +- backend/src/infra.rs | 2 +- backend/src/main.rs | 1 - backend/src/models.rs | 1 + backend/src/repo.rs | 140 +++++++--------------------------------- 10 files changed, 70 insertions(+), 154 deletions(-) diff --git a/backend/spec/billing.md b/backend/spec/billing.md index d1e0739..fe28c18 100644 --- a/backend/spec/billing.md +++ b/backend/spec/billing.md @@ -20,7 +20,7 @@ Calls `self.tick` in a loop every hour. Iterates over `repo.list_activity` since last run and does the following: -- For any `relay_created|relay_updated|relay_activated` activity if this is the first non-free relay for the tenant, update tenant's billing anchor to the time the relay was created. +- For any `create_relay|update_relay|activate_relay` activity if this is the first non-free relay for the tenant, update tenant's billing anchor to the time the relay was created. Also iterates over `repo.list_tenants()` and for each tenant calls `self.generate_invoice_if_due(tenant)` and `self.collect_outstanding(tenant)`. diff --git a/backend/spec/infra.md b/backend/spec/infra.md index d65d70f..b89ef2a 100644 --- a/backend/spec/infra.md +++ b/backend/spec/infra.md @@ -19,6 +19,6 @@ Calls `self.tick` in a loop every 10 seconds. Iterates over `repo.list_activity` since last run and does the following: -- For any `relay_created|relay_updated` activity, sync relay config to zooid. -- For any `relay_deactivated` activity, sync relay config to zooid. +- For any `create_relay|update_relay` activity, sync relay config to zooid. +- For any `deactivate_relay` activity, sync relay config to zooid. - If unsuccessful, call `repo.fail_relay_sync`. diff --git a/backend/spec/main.md b/backend/spec/main.md index d55009e..1740e24 100644 --- a/backend/spec/main.md +++ b/backend/spec/main.md @@ -2,7 +2,6 @@ - Configures logging - Creates instances of `Repo`, `Robot`, `Billing`, `Api`, and `Infra` -- Calls `repo.migrate` - Spawns `billing.start` - Spawns `infra.start` - Calls `api.serve` diff --git a/backend/spec/models.md b/backend/spec/models.md index fcb86dd..e4e7758 100644 --- a/backend/spec/models.md +++ b/backend/spec/models.md @@ -1,5 +1,8 @@ This file describes the domain model. This description should be translated into standard structs and sqlite schemas in a way that makes sense. +- Fields marked as private should use `#[serde(skip_serializing)]` in their definition. +- Fields marked as readonly should use `#[serde(skip_deserializing)]` in their definition. + # Activity Activity is an audit log of all actions performed by a user or a worker process. This allows us to trace history to create invoices, synchronize actions to external services, and debug system behavior. @@ -7,18 +10,19 @@ Activity is an audit log of all actions performed by a user or a worker process. - `id` - a random activity ID - `created_at` - unix timestamp when the activity was created - `activity_type` is one of: - - `tenant_created` - - `tenant_billing_anchor_updated` - - `relay_created` - - `relay_updated` - - `relay_activated` - - `relay_deactivated` - - `relay_sync_failed` - - `invoice_created` - - `invoice_paid` - - `invoice_attempted` - - `invoice_sent` - - `invoice_closed` + - `create_tenant` + - `update_tenant_billing_anchor` + - `update_tenant_nwc_url` + - `create_relay` + - `update_relay` + - `activate_relay` + - `deactivate_relay` + - `fail_relay_sync` + - `create_invoice` + - `mark_invoice_paid` + - `mark_invoice_attempted` + - `mark_invoice_sent` + - `mark_invoice_closed` - `identifier` is a string identifying the resource being modified. This id in interpreted depending on what the `activity_type` is. # Tenant @@ -26,7 +30,7 @@ Activity is an audit log of all actions performed by a user or a worker process. Tenants are customers of the service, identified by a nostr `pubkey`. Public metadata like name etc are pulled from the nostr network. They also have associated billing information. - `pubkey` is the nostr public key identifying the tenant -- `nwc_url` a nostr wallet connect URL used for **paying** invoices generated by the system +- `nwc_url` (private) a nostr wallet connect URL used for **paying** invoices generated by the system - `created_at` unix timestamp identifying tenant creation time - `billing_anchor` unix timestamp identifying billing cycle anchor. This gets reset when the tenant has no paid relays and adds (or reactivates) one. diff --git a/backend/spec/repo.md b/backend/spec/repo.md index e605048..6484c2b 100644 --- a/backend/spec/repo.md +++ b/backend/spec/repo.md @@ -17,11 +17,13 @@ Notes: - Reads `DATABASE_URL` from environment - Ensures that any directories referred to in `DATABASE_URL` exist - Initializes its sqlx `pool` - -## `pub fn migrate(&self) -> Result<()>` - - Runs migrations found in the `migrations` directory. +## `fn insert_activity(activity_type, identifier) -> Result<()>` + +- Private helper that inserts one row into `activity` +- Used by write methods to avoid repeating audit-log SQL + ## `pub fn list_tenants(&self) -> Result>` - Returns all tenants @@ -33,12 +35,17 @@ Notes: ## `pub fn create_tenant(&self, tenant: &Tenant) -> Result<()>` - Creates tenant, may throw sqlite uniqueness error on pubkey -- Logs activity as `(tenant_created, tenant_id)` +- Logs activity as `(create_tenant, tenant_id)` ## `pub fn update_tenant_billing_anchor(&self, pubkey: &str, billing_anchor: i64) -> Result<()>` - Updates the tenant's `billing_anchor` -- Logs activity as `(tenant_billing_anchor_updated, tenant_id)` +- Logs activity as `(update_tenant_billing_anchor, tenant_id)` + +## `pub fn update_tenant_nwc_url(&self, pubkey: &str, nwc_url: &str) -> Result<()>` + +- Updates tenant `nwc_url` +- Logs activity as `(update_tenant_nwc_url, tenant_id)` ## `pub fn list_relays(&self, tenant_id: Option<&str>) -> Result>` @@ -52,32 +59,32 @@ Notes: - Creates relay, may throw sqlite uniqueness error on subdomain - Sets relay status to `new` -- Logs activity as `(relay_created, relay_id)` +- Logs activity as `(create_relay, relay_id)` ## `pub fn update_relay(&self, relay: &Relay) -> Result<()>` - Updates relay, may throw sqlite uniqueness error on subdomain -- Logs activity as `(relay_updated, relay_id)` +- Logs activity as `(update_relay, relay_id)` ## `pub fn deactivate_relay(&self, relay: &Relay) -> Result<()>` - Sets relay status to `inactive` -- Logs activity as `(relay_deactivated, relay_id)` +- Logs activity as `(deactivate_relay, relay_id)` ## `pub fn activate_relay(&self, relay: &Relay) -> Result<()>` - Sets relay status to `active` -- Logs activity as `(relay_activated, relay_id)` +- Logs activity as `(activate_relay, relay_id)` ## `pub fn fail_relay_sync(&self, relay: &Relay, sync_error: String) -> Result<()>` - Sets relay status to `inactive`, sets `sync_error` -- Logs activity as `(relay_sync_failed, relay_id)` +- Logs activity as `(fail_relay_sync, relay_id)` ## `pub fn create_invoice(&self, invoice: &Invoice, invoice_items: [&InvoiceItem]) -> Result<()>` - Saves an `invoice` row and related `invoice_item` rows -- Logs activity as `(invoice_created, invoice_id)` +- Logs activity as `(create_invoice, invoice_id)` ## `pub fn list_invoices(tenant_id: Option<&str>) -> Result>` @@ -88,26 +95,26 @@ Notes: - Sets invoice status to `paid` - Sets `paid_at` to now - Clears `error` if set -- Logs activity as `(invoice_paid, invoice_id)` +- Logs activity as `(mark_invoice_paid, invoice_id)` ## `pub fn mark_invoice_attempted(&self, invoice_id: &str, error: Option<&str>) -> Result<()>` - Sets `attempted_at` to now - Updates `error` if provided - Leaves status as `pending` -- Logs activity as `(invoice_attempted, invoice_id)` +- Logs activity as `(mark_invoice_attempted, invoice_id)` ## `pub fn mark_invoice_sent(&self, invoice_id: &str) -> Result<()>` - Sets `sent_at` to now - Leaves status as `pending` -- Logs activity as `(invoice_sent, invoice_id)` +- Logs activity as `(mark_invoice_sent, invoice_id)` ## `pub fn mark_invoice_closed(&self, invoice_id: &str) -> Result<()>` - Sets invoice status to `closed` - Sets `closed_at` to now -- Logs activity as `(invoice_closed, invoice_id)` +- Logs activity as `(mark_invoice_closed, invoice_id)` ## `pub fn list_activity(&self, since: &i64, tenant: Option<&str>) -> Result>` diff --git a/backend/src/billing.rs b/backend/src/billing.rs index 95dbd9d..d7ff3b8 100644 --- a/backend/src/billing.rs +++ b/backend/src/billing.rs @@ -42,7 +42,7 @@ impl Billing { let since = *since_guard; let activity = self.repo.list_activity(&since, None).await?; for a in &activity { - if matches!(a.activity_type.as_str(), "relay_created" | "relay_updated" | "relay_activated") { + if matches!(a.activity_type.as_str(), "create_relay" | "update_relay" | "activate_relay") { self.maybe_reset_anchor_for_first_paid_relay(a).await?; } *since_guard = (*since_guard).max(a.created_at); @@ -345,13 +345,13 @@ fn relay_active_hours_in_window( } match event.activity_type.as_str() { - "relay_created" | "relay_activated" => { + "create_relay" | "activate_relay" => { if !active { active = true; cursor = ts; } } - "relay_deactivated" | "relay_sync_failed" => { + "deactivate_relay" | "fail_relay_sync" => { if active { active = false; secs += (ts - cursor).num_seconds().max(0); diff --git a/backend/src/infra.rs b/backend/src/infra.rs index 8483943..58d4c89 100644 --- a/backend/src/infra.rs +++ b/backend/src/infra.rs @@ -50,7 +50,7 @@ impl Infra { for a in activity { if matches!( a.activity_type.as_str(), - "relay_created" | "relay_updated" | "relay_deactivated" + "create_relay" | "update_relay" | "deactivate_relay" ) { let Some(relay) = self.repo.get_relay(&a.identifier).await? else { continue; diff --git a/backend/src/main.rs b/backend/src/main.rs index 48d1264..356907a 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -24,7 +24,6 @@ async fn main() -> Result<()> { .init(); let repo = Repo::new().await?; - repo.migrate().await?; let robot = Robot::new().await?; let billing = Billing::new(repo.clone(), robot.clone()); let infra = Infra::new(repo.clone()); diff --git a/backend/src/models.rs b/backend/src/models.rs index e8ef893..a47ef9c 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -11,6 +11,7 @@ pub struct Activity { #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct Tenant { pub pubkey: String, + #[serde(skip_serializing)] pub nwc_url: String, pub created_at: i64, pub billing_anchor: i64, diff --git a/backend/src/repo.rs b/backend/src/repo.rs index 18cb688..c2ad246 100644 --- a/backend/src/repo.rs +++ b/backend/src/repo.rs @@ -1,7 +1,7 @@ use std::path::Path; use anyhow::Result; -use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; +use sqlx::{Sqlite, SqlitePool, Transaction, sqlite::SqlitePoolOptions}; use crate::models::{Activity, Invoice, InvoiceItem, Relay, Tenant}; @@ -33,15 +33,16 @@ impl Repo { .execute(&pool) .await?; + sqlx::migrate!("./migrations").run(&pool).await?; + Ok(Self { pool }) } - pub async fn migrate(&self) -> Result<()> { - sqlx::migrate!("./migrations").run(&self.pool).await?; - Ok(()) - } - - async fn log_activity(&self, activity_type: &str, identifier: &str) -> Result<()> { + async fn insert_activity( + tx: &mut Transaction<'_, Sqlite>, + activity_type: &str, + identifier: &str, + ) -> Result<()> { sqlx::query( "INSERT INTO activity (id, created_at, activity_type, identifier) VALUES (?, strftime('%s','now'), ?, ?)", @@ -49,7 +50,7 @@ impl Repo { .bind(uuid::Uuid::new_v4().to_string()) .bind(activity_type) .bind(identifier) - .execute(&self.pool) + .execute(&mut **tx) .await?; Ok(()) } @@ -91,14 +92,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'tenant_created', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(&tenant.pubkey) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "create_tenant", &tenant.pubkey).await?; tx.commit().await?; Ok(()) @@ -113,14 +107,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'tenant_billing_anchor_updated', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(pubkey) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "update_tenant_billing_anchor", pubkey).await?; tx.commit().await?; Ok(()) @@ -135,14 +122,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'tenant_billing_updated', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(pubkey) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "update_tenant_nwc_url", pubkey).await?; tx.commit().await?; Ok(()) @@ -226,14 +206,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'relay_created', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(&relay.id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "create_relay", &relay.id).await?; tx.commit().await?; Ok(()) @@ -271,14 +244,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'relay_updated', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(&relay.id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "update_relay", &relay.id).await?; tx.commit().await?; Ok(()) @@ -292,14 +258,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'relay_deactivated', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(&relay.id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "deactivate_relay", &relay.id).await?; tx.commit().await?; Ok(()) @@ -313,14 +272,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'relay_activated', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(&relay.id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "activate_relay", &relay.id).await?; tx.commit().await?; Ok(()) @@ -335,14 +287,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'relay_sync_failed', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(&relay.id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "fail_relay_sync", &relay.id).await?; tx.commit().await?; Ok(()) @@ -382,14 +327,7 @@ impl Repo { .await?; } - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'invoice_created', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(&invoice.id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "create_invoice", &invoice.id).await?; tx.commit().await?; Ok(()) @@ -432,14 +370,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'invoice_paid', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(invoice_id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "mark_invoice_paid", invoice_id).await?; tx.commit().await?; Ok(()) @@ -458,14 +389,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'invoice_attempted', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(invoice_id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "mark_invoice_attempted", invoice_id).await?; tx.commit().await?; Ok(()) @@ -479,14 +403,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'invoice_sent', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(invoice_id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "mark_invoice_sent", invoice_id).await?; tx.commit().await?; Ok(()) @@ -504,14 +421,7 @@ impl Repo { .execute(&mut *tx) .await?; - sqlx::query( - "INSERT INTO activity (id, created_at, activity_type, identifier) - VALUES (?, strftime('%s','now'), 'invoice_closed', ?)", - ) - .bind(uuid::Uuid::new_v4().to_string()) - .bind(invoice_id) - .execute(&mut *tx) - .await?; + Self::insert_activity(&mut tx, "mark_invoice_closed", invoice_id).await?; tx.commit().await?; Ok(()) @@ -524,7 +434,7 @@ impl Repo { FROM activity a WHERE a.created_at > ? AND ( - a.activity_type IN ('tenant_created', 'tenant_billing_anchor_updated') + a.activity_type IN ('create_tenant', 'update_tenant_billing_anchor') AND a.identifier = ? OR EXISTS ( SELECT 1 FROM relay r @@ -601,8 +511,4 @@ impl Repo { Ok(sats) } - #[allow(dead_code)] - async fn _log_activity_public(&self, activity_type: &str, identifier: &str) -> Result<()> { - self.log_activity(activity_type, identifier).await - } }