Add dunning
This commit is contained in:
@@ -1,11 +1,53 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::api::{Api, AuthedPubkey};
|
||||
use crate::models::{Intent, Invoice};
|
||||
use crate::query;
|
||||
use crate::web::{ApiResult, internal, not_found, ok};
|
||||
|
||||
/// An invoice for the client, with its lifecycle flattened to a derived `status`
|
||||
/// ("open" | "paid" | "void") alongside the underlying timestamps, plus the
|
||||
/// Stripe PaymentIntents that settled it (empty unless requested).
|
||||
#[derive(Serialize)]
|
||||
pub struct InvoiceResponse {
|
||||
pub id: String,
|
||||
pub tenant_pubkey: String,
|
||||
pub status: String,
|
||||
pub period_start: i64,
|
||||
pub period_end: i64,
|
||||
pub created_at: i64,
|
||||
pub paid_at: Option<i64>,
|
||||
pub voided_at: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub intents: Vec<Intent>,
|
||||
}
|
||||
|
||||
impl From<Invoice> for InvoiceResponse {
|
||||
fn from(i: Invoice) -> Self {
|
||||
let status = if i.is_open() {
|
||||
"open"
|
||||
} else if i.paid_at.is_some() {
|
||||
"paid"
|
||||
} else {
|
||||
"void"
|
||||
};
|
||||
InvoiceResponse {
|
||||
status: status.to_string(),
|
||||
id: i.id,
|
||||
tenant_pubkey: i.tenant_pubkey,
|
||||
period_start: i.period_start,
|
||||
period_end: i.period_end,
|
||||
created_at: i.created_at,
|
||||
paid_at: i.paid_at,
|
||||
voided_at: i.voided_at,
|
||||
intents: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The tenant's most recent invoice, after first materializing any outstanding
|
||||
/// line items into a fresh one — so the frontend can collect payment right after
|
||||
/// a change (e.g. creating a relay). Payment isn't attempted here; the caller
|
||||
@@ -21,9 +63,9 @@ pub async fn get_tenant_latest_invoice(
|
||||
|
||||
api.billing.reconcile_subscription(&tenant).await.map_err(internal)?;
|
||||
|
||||
let invoice = query::get_latest_invoice_for_tenant(&pubkey).await.map_err(internal)?;
|
||||
let invoice = query::get_latest_invoice(&pubkey).await.map_err(internal)?;
|
||||
|
||||
ok(invoice)
|
||||
ok(invoice.map(InvoiceResponse::from))
|
||||
}
|
||||
|
||||
pub async fn get_invoice(
|
||||
@@ -38,7 +80,10 @@ pub async fn get_invoice(
|
||||
|
||||
api.require_admin_or_tenant(&auth, &invoice.tenant_pubkey)?;
|
||||
|
||||
ok(invoice)
|
||||
let mut response = InvoiceResponse::from(invoice);
|
||||
response.intents = query::list_intents_for_invoice(&id).await.map_err(internal)?;
|
||||
|
||||
ok(response)
|
||||
}
|
||||
|
||||
/// Return a payable Lightning invoice (bolt11) for an invoice, minting one if
|
||||
|
||||
@@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::{Api, AuthedPubkey};
|
||||
use crate::models::Tenant;
|
||||
use crate::routes::invoices::InvoiceResponse;
|
||||
use crate::web::{ApiResult, internal, map_unique_error, ok};
|
||||
use crate::{command, env, query};
|
||||
|
||||
@@ -17,9 +18,13 @@ pub struct TenantResponse {
|
||||
pub pubkey: String,
|
||||
pub nwc_is_set: bool,
|
||||
pub nwc_error: Option<String>,
|
||||
pub stripe_error: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub billing_anchor: Option<i64>,
|
||||
pub stripe_customer_id: String,
|
||||
/// Set when billing has churned the tenant; the UI uses it to warn that the
|
||||
/// account is delinquent until billing is re-activated.
|
||||
pub churned_at: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<Tenant> for TenantResponse {
|
||||
@@ -28,9 +33,11 @@ impl From<Tenant> for TenantResponse {
|
||||
nwc_is_set: !t.nwc_url.is_empty(),
|
||||
pubkey: t.pubkey,
|
||||
nwc_error: t.nwc_error,
|
||||
stripe_error: t.stripe_error,
|
||||
created_at: t.created_at,
|
||||
billing_anchor: t.billing_anchor,
|
||||
stripe_customer_id: t.stripe_customer_id,
|
||||
churned_at: t.churned_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +142,7 @@ pub async fn list_tenant_relays(
|
||||
) -> ApiResult {
|
||||
api.require_admin_or_tenant(&auth, &pubkey)?;
|
||||
|
||||
let relays = query::list_relays_for_tenant(&pubkey)
|
||||
let relays = query::list_relays(&pubkey)
|
||||
.await
|
||||
.map_err(internal)?;
|
||||
ok(relays)
|
||||
@@ -149,11 +156,14 @@ pub async fn list_tenant_invoices(
|
||||
) -> ApiResult {
|
||||
api.require_admin_or_tenant(&auth, &pubkey)?;
|
||||
|
||||
let invoices = query::list_invoices_for_tenant(&pubkey)
|
||||
let invoices = query::list_invoices(&pubkey)
|
||||
.await
|
||||
.map_err(internal)?;
|
||||
|
||||
ok(invoices)
|
||||
ok(invoices
|
||||
.into_iter()
|
||||
.map(InvoiceResponse::from)
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user