forked from coracle/caravel
Clear billing logic, do some cleanup
This commit is contained in:
+19
-31
@@ -21,11 +21,9 @@ Notes:
|
||||
|
||||
- Reads environment and populates members
|
||||
|
||||
## `pub fn serve(&self) -> Result<()>`
|
||||
## `pub fn router(&self) -> Result<()>`
|
||||
|
||||
- Initializes an `axum::Router`
|
||||
- Adds CORS middleware based on `origins`
|
||||
- Calls `axum::serve` with a listener
|
||||
- Returns an `axum::Router`
|
||||
|
||||
--- Plan routes
|
||||
|
||||
@@ -65,25 +63,19 @@ Notes:
|
||||
- Authorizes admin or matching tenant
|
||||
- Return `data` is a single tenant struct from `repo.get_tenant`
|
||||
|
||||
## `async fn update_tenant(...) -> Response`
|
||||
|
||||
- Serves `PUT /tenants/:pubkey`
|
||||
- Authorizes admin or matching tenant
|
||||
- Updates tenant using `repo.update_tenant`
|
||||
- Return `data` is the updated tenant struct
|
||||
|
||||
## `async fn list_tenant_relays(...) -> Response`
|
||||
|
||||
- Serves `GET /tenants/:pubkey/relays`
|
||||
- Authorizes admin or matching tenant
|
||||
- Return `data` is a list of relay structs from `repo.list_relays_for_tenant`
|
||||
|
||||
## `async fn list_tenant_invoices(...) -> Response`
|
||||
|
||||
- Serves `GET /tenants/:pubkey/invoices`
|
||||
- Authorizes admin or matching tenant
|
||||
- Return `data` is a list of invoice structs from `repo.list_invoices_for_tenant`
|
||||
|
||||
## `async fn update_tenant_billing(...) -> Response`
|
||||
|
||||
- Serves `PUT /tenants/:pubkey/billing`
|
||||
- Authorizes admin or matching tenant
|
||||
- Updates tenant billing NWC URL using `repo.update_tenant_nwc_url`
|
||||
- Return `data` is the submitted billing payload
|
||||
|
||||
--- Relay routes
|
||||
|
||||
## `async fn list_relays(...) -> Response`
|
||||
@@ -120,28 +112,24 @@ Notes:
|
||||
|
||||
- Serves `GET /relays/:id/activity`
|
||||
- Authorizes admin or relay owner
|
||||
- Return `data` is a list of activity structs from `repo.list_activity_for_relay`
|
||||
- Get activity from `repo.list_activity_for_relay`
|
||||
- Return `data` is `{activity}`
|
||||
|
||||
## `async fn deactivate_relay(...) -> Response`
|
||||
|
||||
- Serves `POST /relays/:id/deactivate`
|
||||
- Authorizes admin or relay owner
|
||||
- Deactivates relay using `repo.deactivate_relay`
|
||||
- If relay is already active, return a `400` with `code=relay-is-inactive`
|
||||
- Call `billing.deactivate_relay`
|
||||
- Return `data` is empty
|
||||
|
||||
--- Invoice routes
|
||||
## `async fn reactivate_relay(...) -> Response`
|
||||
|
||||
## `async fn list_invoices(...) -> Response`
|
||||
|
||||
- Serves `GET /invoices`
|
||||
- Authorizes admin only
|
||||
- Return `data` is a list of invoice structs from `repo.list_invoices`
|
||||
|
||||
## `async fn get_invoice(...) -> Response`
|
||||
|
||||
- Serves `GET /invoices/:id`
|
||||
- Authorizes admin or invoice owner
|
||||
- Return `data` is a single invoice struct from `repo.get_invoice`
|
||||
- Serves `POST /relays/:id/reactivate`
|
||||
- Authorizes admin or relay owner
|
||||
- If relay is already active, return a `400` with `code=relay-is-active`
|
||||
- Call `billing.reactivate_relay`
|
||||
- Return `data` is empty
|
||||
|
||||
--- Utilities
|
||||
|
||||
|
||||
+2
-39
@@ -1,50 +1,13 @@
|
||||
# `pub struct Billing`
|
||||
|
||||
Billing is a service which polls the database, creates invoices, and attempts to collect payment for invoices.
|
||||
Billing encapsulates logic related to synchronizing state with Stripe.
|
||||
|
||||
Members:
|
||||
|
||||
- `nwc_url: String` - a nostr wallet connect URL used to **create** invoices
|
||||
- `nwc_url: String` - a nostr wallet connect URL used to **create** bolt11 invoices
|
||||
- `repo: Repo`
|
||||
- `robot: Robot`
|
||||
|
||||
## `pub fn new(repo: Repo, robot: Robot) -> Self`
|
||||
|
||||
- Reads environment and populates members
|
||||
|
||||
## `pub async fn start(self)`
|
||||
|
||||
Calls `self.tick` in a loop every hour.
|
||||
|
||||
## `pub async fn tick(self)`
|
||||
|
||||
Iterates over `repo.list_activity` since last run and does the following:
|
||||
|
||||
- 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)`.
|
||||
|
||||
## `async fn generate_invoice_if_due(&self, tenant: &Tenant)`
|
||||
|
||||
- Skip tenants that have a `pending` invoice or have no active non-free relays.
|
||||
- Compute current billing period from `tenant.billing_anchor` as rolling monthly windows: `[period_start, period_end)`.
|
||||
- Only generate an invoice once the period has closed (`now >= period_end`).
|
||||
- Load activity needed to compute usage for the tenant using `repo.list_activity`.
|
||||
- Calculate how many hours (rounded up) each relay was active during the window per paid plan.
|
||||
- If total invoice amount is 0, return.
|
||||
- Generate a `bolt11` invoice. If this fails, log the error and return.
|
||||
- Generate an invoice for the tenant and an invoice item for each relay.
|
||||
- Persist invoice + items atomically using `repo.create_invoice`.
|
||||
|
||||
## `async fn collect_outstanding(&self, tenant: &Tenant)`
|
||||
|
||||
- Load `pending` tenant invoices and attempt to collect each one.
|
||||
- If `attempted_at` is less than 24 hours ago, skip it.
|
||||
- If the `bolt11` invoice has been paid out of band, call `repo.mark_invoice_paid` and return.
|
||||
- If the tenant has a `nwc_url`, attempt to pay the invoice with nwc.
|
||||
- If collection succeeds, call `repo.mark_invoice_paid`.
|
||||
- If collection fails, populate `repo.mark_invoice_attempted`.
|
||||
- If nwc isn't set up or fails and `sent_at` is not set:
|
||||
- Send a NIP 17 DM to the user with the invoice included.
|
||||
- Call `repo.mark_invoice_sent`.
|
||||
- If the invoice is 7 days past `created_at`, call `repo.mark_invoice_closed`.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
- Configures logging
|
||||
- Creates instances of `Repo`, `Robot`, `Billing`, `Api`, and `Infra`
|
||||
- Spawns `billing.start`
|
||||
- Spawns `infra.start`
|
||||
- Calls `api.serve`
|
||||
- Get an axum router from `api.router`
|
||||
- Adds CORS middleware based on `origins`
|
||||
- Calls `axum::serve` with a listener
|
||||
|
||||
+4
-36
@@ -19,18 +19,13 @@ Activity is an audit log of all actions performed by a user or a worker process.
|
||||
- `created_at` - unix timestamp when the activity was created
|
||||
- `activity_type` is one of:
|
||||
- `create_tenant`
|
||||
- `update_tenant_billing_anchor`
|
||||
- `update_tenant_nwc_url`
|
||||
- `update_tenant`
|
||||
- `create_relay`
|
||||
- `update_relay`
|
||||
- `update_relay_plan`
|
||||
- `activate_relay`
|
||||
- `deactivate_relay`
|
||||
- `fail_relay_sync`
|
||||
- `create_invoice`
|
||||
- `mark_invoice_paid`
|
||||
- `mark_invoice_attempted`
|
||||
- `mark_invoice_sent`
|
||||
- `mark_invoice_closed`
|
||||
- `resource_type` is a string identifying the resource type being modified.
|
||||
- `resource_id` is a string identifying the resource id being modified.
|
||||
|
||||
@@ -58,7 +53,6 @@ Tenants are customers of the service, identified by a nostr `pubkey`. Public met
|
||||
- `pubkey` is the nostr public key identifying the tenant
|
||||
- `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.
|
||||
|
||||
# Relay
|
||||
|
||||
@@ -69,7 +63,8 @@ A relay is a nostr relay owned by a `tenant` and hosted by the attached zooid in
|
||||
- `schema` - the relay's db schema (read_only, calculated based on `subdomain` + `id`)
|
||||
- `subdomain` - the relay's subdomain
|
||||
- `plan` - the relay's plan
|
||||
- `status` - `new|active|inactive`. Only `active` relays count toward billing.
|
||||
- `status` - `active|inactive`. Only `active` relays count toward billing.
|
||||
- `synced` - whether the relay has been successfully synced to zooid at least once.
|
||||
- `sync_error` - a string indicating any errors encountered when synchronizing.
|
||||
- `info_name` - the relay's name
|
||||
- `info_icon` - the relay's icon image URL
|
||||
@@ -81,7 +76,6 @@ A relay is a nostr relay owned by a `tenant` and hosted by the attached zooid in
|
||||
- `blossom_enabled` - whether blossom file storage is enabled
|
||||
- `livekit_enabled` - whether livekit calls are enabled
|
||||
- `push_enabled` - whether relay push is enabled
|
||||
- `synced` (private) - whether the relay has been successfully synced to zooid at least once. Used by infra to decide POST vs PUT.
|
||||
|
||||
Some attributes persisted to zooid via API have special handling:
|
||||
|
||||
@@ -91,29 +85,3 @@ Some attributes persisted to zooid via API have special handling:
|
||||
- The relay's `livekit_*` configuration is inferred based on environment variables and `livekit_enabled`.
|
||||
- The relay's `roles` are hard-coded for now.
|
||||
|
||||
# Invoice
|
||||
|
||||
Invoices are generated at the end of a tenant's monthly billing period. The billing module is responsible for creating them, collecting them, and dunning them.
|
||||
|
||||
- `id` - random invoice ID
|
||||
- `tenant` - tenant pubkey
|
||||
- `status` - `pending|paid|closed`
|
||||
- `amount` is derived as the sum of associated invoice item `sats` values (not stored as a separate source of truth)
|
||||
- `created_at` - unix timestamp for when the invoice was created
|
||||
- `attempted_at` - nullable unix timestamp for when collection was last attempted
|
||||
- `error` - optional human-readable error from the last failed collection attempt
|
||||
- `closed_at` - nullable unix timestamp for when the invoice was closed
|
||||
- `sent_at` - nullable unix timestamp for when the invoice was sent via DM
|
||||
- `paid_at` - nullable unix timestamp for when the invoice was paid
|
||||
- `bolt11` - a BOLT 11 lightning invoice that can be used to pay the invoice
|
||||
- `period_start` - unix timestamp for period start
|
||||
- `period_end` - unix timestamp for period end
|
||||
|
||||
# Invoice Item
|
||||
|
||||
Invoice items are attached to an invoice and represent charges for a given relay.
|
||||
|
||||
- `id` - random invoice item ID
|
||||
- `invoice` - invoice ID
|
||||
- `relay` - relay ID
|
||||
- `sats` - amount in satoshis
|
||||
|
||||
+4
-56
@@ -10,7 +10,7 @@ Notes:
|
||||
|
||||
- All public write methods should be run in a transaction so they're atomic
|
||||
- All writes should be accompanied by an activity log entry of `(tenant, activity_type, resource_type, resource_id)`
|
||||
- Database table names are singular: `activity`, `tenant`, `relay`, `invoice`, `invoice_item`
|
||||
- Database table names are singular: `activity`, `tenant`, `relay`
|
||||
|
||||
## `pub fn new() -> Self`
|
||||
|
||||
@@ -38,15 +38,10 @@ Notes:
|
||||
- Creates tenant, may throw sqlite uniqueness error on pubkey
|
||||
- Logs activity as `(create_tenant, tenant_id)`
|
||||
|
||||
## `pub fn update_tenant_billing_anchor(&self, pubkey: &str, billing_anchor: i64) -> Result<()>`
|
||||
## `pub fn update_tenant(&self, tenant: &Tenant) -> Result<()>`
|
||||
|
||||
- Updates the tenant's `billing_anchor`
|
||||
- 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)`
|
||||
- Updates tenant
|
||||
- Logs activity as `(update_tenant, tenant_id)`
|
||||
|
||||
## `pub fn list_plans() -> Vec<Plan>`
|
||||
|
||||
@@ -101,59 +96,12 @@ Notes:
|
||||
- Returns the maximum `created_at` value from the activity table, or 0 if empty
|
||||
- Used by infra to initialize the since guard on startup
|
||||
|
||||
## `pub fn create_invoice(&self, invoice: &Invoice, invoice_items: [&InvoiceItem]) -> Result<()>`
|
||||
|
||||
- Saves an `invoice` row and related `invoice_item` rows
|
||||
- Logs activity as `(create_invoice, invoice_id)`
|
||||
|
||||
## `pub fn list_invoices() -> Result<Vec<Invoice>>`
|
||||
|
||||
- Returns all invoices
|
||||
|
||||
## `pub fn list_invoices_for_tenant(tenant_id: &str) -> Result<Vec<Invoice>>`
|
||||
|
||||
- Returns all matching invoices
|
||||
|
||||
## `pub fn mark_invoice_paid(&self, invoice_id: &str) -> Result<()>`
|
||||
|
||||
- Sets invoice status to `paid`
|
||||
- Sets `paid_at` to now
|
||||
- Clears `error` if set
|
||||
- Logs activity as `(mark_invoice_paid, invoice_id)`
|
||||
|
||||
## `pub fn mark_invoice_attempted(&self, invoice_id: &str, error: &str) -> Result<()>`
|
||||
|
||||
- Sets `attempted_at` to now
|
||||
- Updates `error` if provided
|
||||
- Leaves status as `pending`
|
||||
- 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 `(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 `(mark_invoice_closed, invoice_id)`
|
||||
|
||||
## `pub fn list_activity(&self, since: &i64) -> Result<Vec<Activity>>`
|
||||
|
||||
- Returns all activity occuring after `since`
|
||||
|
||||
## `pub fn list_activity_for_tenant(&self, tenant: &str, since: &i64) -> Result<Vec<Activity>>`
|
||||
|
||||
- Returns all activity occuring after `since` matching `tenant`
|
||||
|
||||
## `pub fn list_activity_for_relay(&self, relay_id: &str) -> Result<Vec<Activity>>`
|
||||
|
||||
- Returns all activity where `resource_type = 'relay'` and `resource_id = relay_id`
|
||||
- Ordered newest-first
|
||||
|
||||
## `pub fn get_relay_plan_sats(&self, plan: &str) -> Result<i64>`
|
||||
|
||||
- Returns the monthly sats amount for a given plan id
|
||||
- Uses `list_plans()` data for consistent pricing logic across API and billing
|
||||
|
||||
Reference in New Issue
Block a user