115 lines
6.5 KiB
Markdown
115 lines
6.5 KiB
Markdown
# `pub struct Billing`
|
|
|
|
Billing encapsulates logic related to synchronizing state with Stripe, processing payments via NWC, and managing subscription lifecycle.
|
|
|
|
Members:
|
|
|
|
- `nwc_url: String` - a nostr wallet connect URL used to **create** bolt11 invoices (i.e. receive payments), from `NWC_URL`
|
|
- `stripe_secret_key: String` - Stripe API key used for billing API operations, from `STRIPE_SECRET_KEY`
|
|
- `stripe_webhook_secret: String` - secret for verifying Stripe webhook signatures, from `STRIPE_WEBHOOK_SECRET`
|
|
- `query: Query`
|
|
- `command: Command`
|
|
- `robot: Robot`
|
|
|
|
## `pub fn new(query: Query, command: Command, robot: Robot) -> Self`
|
|
|
|
- Reads environment and populates members
|
|
- Panics if `STRIPE_SECRET_KEY` is missing/empty
|
|
- Panics if `STRIPE_WEBHOOK_SECRET` is missing/empty
|
|
|
|
## `pub fn start(&self)`
|
|
|
|
- Subscribes to `command.notify.subscribe()`
|
|
- On `create_relay`, `update_relay`, `activate_relay`, `deactivate_relay`, `fail_relay_sync`, and `complete_relay_sync`, call `self.sync_relay_subscription`.
|
|
|
|
## `pub fn sync_relay_subscription(&self, activity: &Activity)`
|
|
|
|
Manages the Stripe subscription and subscription items for a relay's tenant. Only paid (non-free) relays interact with Stripe. Free-only tenants have no subscription. Must be idempotent.
|
|
|
|
- Fetch the relay and tenant associated with the `activity`
|
|
- **If relay plan is `free`**: if the relay has a `stripe_subscription_item_id`, delete it via the Stripe API and call `command.delete_relay_subscription_item`. Then check cleanup (below). Return early.
|
|
- **If relay is `inactive` or `delinquent`**: if the relay has a `stripe_subscription_item_id`, delete it via the Stripe API and call `command.delete_relay_subscription_item`. Then check cleanup (below). Return early.
|
|
- **If relay is `active` and on a paid plan**:
|
|
- **Ensure subscription exists**: If the tenant has no `stripe_subscription_id`, create a Stripe subscription for the customer with `collection_method: "charge_automatically"` and the relay's price as the first item. Save the subscription ID via `command.set_tenant_subscription` and the item ID via `command.set_relay_subscription_item`. Return early.
|
|
- **Sync the subscription item**: If the tenant already has a subscription, create or update the relay's Stripe subscription item to the plan's `stripe_price_id` via the Stripe API, then call `command.set_relay_subscription_item`.
|
|
- **Clean up empty subscription**: After any item deletion, check if the tenant has any remaining active paid relays. If none and the tenant has a `stripe_subscription_id`, cancel the Stripe subscription immediately and call `command.clear_tenant_subscription`.
|
|
|
|
## `pub fn handle_webhook(&self, payload: &str, signature: &str) -> Result<()>`
|
|
|
|
- Verify the webhook signature using `self.stripe_webhook_secret`
|
|
- Parse the event and dispatch by type:
|
|
- `invoice.created` -> `self.handle_invoice_created`
|
|
- `invoice.paid` -> `self.handle_invoice_paid`
|
|
- `invoice.payment_failed` -> `self.handle_invoice_payment_failed`
|
|
- `invoice.overdue` -> `self.handle_invoice_overdue`
|
|
- `customer.subscription.updated` -> `self.handle_subscription_updated`
|
|
- `customer.subscription.deleted` -> `self.handle_subscription_deleted`
|
|
- Unknown event types are ignored (return Ok)
|
|
|
|
## `pub async fn stripe_list_invoices(&self, customer_id: &str) -> Result<Value>`
|
|
|
|
- Fetches invoices from Stripe API for the given customer
|
|
- Returns the `data` array from the Stripe response
|
|
|
|
## `pub async fn stripe_get_invoice(&self, invoice_id: &str) -> Result<Value>`
|
|
|
|
- Fetches a single invoice from Stripe API by ID
|
|
- Returns the full Stripe invoice object
|
|
|
|
## `pub async fn create_bolt11(&self, amount_due_cents: i64) -> Result<String>`
|
|
|
|
- Creates a bolt11 Lightning invoice for the given amount using the system NWC wallet (`self.nwc_url`)
|
|
- Returns the bolt11 invoice string
|
|
|
|
## `pub async fn stripe_create_portal_session(&self, customer_id: &str) -> Result<String>`
|
|
|
|
- Creates a Stripe Customer Portal session for the given customer
|
|
- Returns the portal session URL
|
|
|
|
## `fn handle_invoice_created(&self, invoice: &Invoice)`
|
|
|
|
Attempts to pay a new subscription invoice. Payment priority:
|
|
|
|
1. **NWC auto-pay**: If the tenant has a `nwc_url`:
|
|
- Create a bolt11 Lightning invoice for the invoice amount using `self.nwc_url` (the receiving/system wallet)
|
|
- Pay the bolt11 invoice using the tenant's `nwc_url` (the spending/tenant wallet)
|
|
- If payment succeeds: call Stripe `POST /v1/invoices/:id/pay` with `paid_out_of_band: true`. Clear `nwc_error` via `command.clear_tenant_nwc_error`.
|
|
- If payment fails: set `nwc_error` on tenant via `command.set_tenant_nwc_error`. Fall through to next option.
|
|
2. **Card on file**: If the tenant has a payment method on the Stripe customer, do nothing — Stripe will charge automatically.
|
|
3. **Manual payment**: If neither NWC nor card is available, send a DM via `robot.send_dm` notifying the tenant that payment is due with a link to the application for manual Lightning payment.
|
|
|
|
Skip invoices with `amount_due` of 0.
|
|
|
|
## `fn handle_invoice_paid(&self, invoice: &Invoice)`
|
|
|
|
- Look up tenant by `stripe_customer_id`
|
|
- If tenant has `past_due_at` set:
|
|
- Clear `past_due_at` via `command.clear_tenant_past_due`
|
|
- Find all `delinquent` relays on paid plans for the tenant (relays marked delinquent by the billing system due to non-payment)
|
|
- Reactivate each one via `command.activate_relay`
|
|
|
|
## `fn handle_invoice_payment_failed(&self, invoice: &Invoice)`
|
|
|
|
- Look up tenant by `stripe_customer_id`
|
|
- If tenant does not already have `past_due_at` set:
|
|
- Set `past_due_at` to now via `command.set_tenant_past_due`
|
|
- Send a DM via `robot.send_dm` notifying the tenant that their payment has failed and their relays may be deactivated if not resolved.
|
|
|
|
## `fn handle_invoice_overdue(&self, invoice: &Invoice)`
|
|
|
|
- Look up tenant by `stripe_customer_id`
|
|
- Mark all active relays on paid plans as delinquent via `command.mark_relay_delinquent` (sets status to `delinquent`, distinct from user-initiated `deactivate_relay`)
|
|
- Send a DM via `robot.send_dm` notifying the tenant that their paid relays have been deactivated due to non-payment
|
|
|
|
## `fn handle_subscription_updated(&self, subscription: &Subscription)`
|
|
|
|
- Look up tenant by `stripe_customer_id`
|
|
- If subscription status is `canceled` or `unpaid`:
|
|
- Clear `stripe_subscription_id` via `command.clear_tenant_subscription`
|
|
- Mark all active paid relays as delinquent via `command.mark_relay_delinquent`
|
|
|
|
## `fn handle_subscription_deleted(&self, subscription: &Subscription)`
|
|
|
|
- Look up tenant by `stripe_customer_id`
|
|
- Clear `stripe_subscription_id` via `command.clear_tenant_subscription`
|