Massive user-story-oriented refactor
This commit is contained in:
@@ -6,26 +6,6 @@ use crate::api::{Api, AuthedPubkey};
|
||||
use crate::query;
|
||||
use crate::web::{ApiResult, internal, not_found, ok};
|
||||
|
||||
/// 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
|
||||
/// drives it via the bolt11/Stripe endpoints. `null` when the tenant has no
|
||||
/// invoices and nothing is outstanding.
|
||||
pub async fn get_tenant_latest_invoice(
|
||||
State(api): State<Arc<Api>>,
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
Path(pubkey): Path<String>,
|
||||
) -> ApiResult {
|
||||
api.require_admin_or_tenant(&auth, &pubkey)?;
|
||||
let tenant = api.get_tenant_or_404(&pubkey).await?;
|
||||
|
||||
api.billing.reconcile_subscription(&tenant).await.map_err(internal)?;
|
||||
|
||||
let invoice = query::get_latest_invoice(&pubkey).await.map_err(internal)?;
|
||||
|
||||
ok(invoice)
|
||||
}
|
||||
|
||||
pub async fn get_invoice(
|
||||
State(api): State<Arc<Api>>,
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
@@ -63,3 +43,24 @@ pub async fn get_invoice_bolt11(
|
||||
|
||||
ok(serde_json::json!(bolt11))
|
||||
}
|
||||
|
||||
/// The line items billed on an invoice, for rendering its contents and a
|
||||
/// downloadable copy.
|
||||
pub async fn list_invoice_items(
|
||||
State(api): State<Arc<Api>>,
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
Path(invoice_id): Path<String>,
|
||||
) -> ApiResult {
|
||||
let invoice = query::get_invoice(&invoice_id)
|
||||
.await
|
||||
.map_err(internal)?
|
||||
.ok_or_else(|| not_found("invoice not found"))?;
|
||||
|
||||
api.require_admin_or_tenant(&auth, &invoice.tenant_pubkey)?;
|
||||
|
||||
let items = query::list_invoice_items_for_invoice(&invoice_id)
|
||||
.await
|
||||
.map_err(internal)?;
|
||||
|
||||
ok(items)
|
||||
}
|
||||
|
||||
@@ -9,14 +9,12 @@ use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::{Api, AuthedPubkey};
|
||||
use crate::{command, infra, query};
|
||||
use crate::models::{
|
||||
RELAY_STATUS_ACTIVE, RELAY_STATUS_DELINQUENT, RELAY_STATUS_INACTIVE, Relay,
|
||||
};
|
||||
use crate::models::{RELAY_STATUS_ACTIVE, RELAY_STATUS_DELINQUENT, RELAY_STATUS_INACTIVE, Relay};
|
||||
use crate::web::{
|
||||
ApiError, ApiResult, bad_request, created, internal, map_unique_error, ok,
|
||||
parse_bool_default, unprocessable,
|
||||
ApiError, ApiResult, bad_request, created, internal, map_unique_error, ok, parse_bool_default,
|
||||
unprocessable,
|
||||
};
|
||||
use crate::{command, infra, query};
|
||||
|
||||
pub async fn list_relays(
|
||||
State(api): State<Arc<Api>>,
|
||||
@@ -196,10 +194,7 @@ pub async fn update_relay(
|
||||
if plan_changed {
|
||||
let selected_plan = query::get_plan(&relay.plan_id).map_err(internal)?;
|
||||
if let Some(limit) = selected_plan.members {
|
||||
let current_members = fetch_relay_members(&relay)
|
||||
.await
|
||||
.map_err(internal)?
|
||||
.len() as i64;
|
||||
let current_members = fetch_relay_members(&relay).await.map_err(internal)?.len() as i64;
|
||||
|
||||
if current_members > limit {
|
||||
let message = format!(
|
||||
@@ -231,12 +226,13 @@ pub async fn deactivate_relay(
|
||||
}
|
||||
|
||||
if relay.status == RELAY_STATUS_INACTIVE {
|
||||
return Err(bad_request("relay-is-inactive", "relay is already inactive"));
|
||||
return Err(bad_request(
|
||||
"relay-is-inactive",
|
||||
"relay is already inactive",
|
||||
));
|
||||
}
|
||||
|
||||
command::deactivate_relay(&relay)
|
||||
.await
|
||||
.map_err(internal)?;
|
||||
command::deactivate_relay(&relay).await.map_err(internal)?;
|
||||
|
||||
ok(())
|
||||
}
|
||||
@@ -282,15 +278,21 @@ static SUBDOMAIN_RE: LazyLock<Regex> =
|
||||
/// premium features, and coerce the boolean columns to 0/1.
|
||||
fn prepare_relay(mut relay: Relay) -> Result<Relay, ApiError> {
|
||||
if !SUBDOMAIN_RE.is_match(&relay.subdomain)
|
||||
|| RESERVED_SUBDOMAINS.contains(&relay.subdomain.as_str()) {
|
||||
|| RESERVED_SUBDOMAINS.contains(&relay.subdomain.as_str())
|
||||
{
|
||||
return Err(unprocessable("invalid-subdomain", "subdomain is invalid"));
|
||||
}
|
||||
|
||||
let plan = query::get_plan(&relay.plan_id)
|
||||
.map_err(|_| unprocessable("invalid-plan", "plan not found"))?;
|
||||
|
||||
if (!plan.blossom && relay.blossom_enabled == 1) || (!plan.livekit && relay.livekit_enabled == 1) {
|
||||
return Err(unprocessable("premium-feature", "feature requires a paid plan"));
|
||||
if (!plan.blossom && relay.blossom_enabled == 1)
|
||||
|| (!plan.livekit && relay.livekit_enabled == 1)
|
||||
{
|
||||
return Err(unprocessable(
|
||||
"premium-feature",
|
||||
"feature requires a paid plan",
|
||||
));
|
||||
}
|
||||
|
||||
relay.policy_public_join = parse_bool_default(relay.policy_public_join, 0);
|
||||
|
||||
@@ -56,6 +56,21 @@ pub async fn list_tenants(
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Fetch a tenant by pubkey. Automatically refreshes the tenant's stripe payment data.
|
||||
pub async fn get_tenant(
|
||||
State(api): State<Arc<Api>>,
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
Path(pubkey): Path<String>,
|
||||
) -> ApiResult {
|
||||
api.require_admin_or_tenant(&auth, &pubkey)?;
|
||||
|
||||
let mut tenant = api.get_tenant_or_404(&pubkey).await?;
|
||||
|
||||
api.billing.sync_stripe_customer(&mut tenant).await.map_err(internal)?;
|
||||
|
||||
ok(TenantResponse::from(tenant))
|
||||
}
|
||||
|
||||
/// Create the tenant row for the calling pubkey and provision its Stripe
|
||||
/// customer. Idempotent: an existing tenant (including one created by a
|
||||
/// concurrent unique-constraint race) is returned as-is.
|
||||
@@ -99,16 +114,6 @@ pub async fn create_tenant(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tenant(
|
||||
State(api): State<Arc<Api>>,
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
Path(pubkey): Path<String>,
|
||||
) -> ApiResult {
|
||||
api.require_admin_or_tenant(&auth, &pubkey)?;
|
||||
let tenant = api.get_tenant_or_404(&pubkey).await?;
|
||||
ok(TenantResponse::from(tenant))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateTenantRequest {
|
||||
pub nwc_url: Option<String>,
|
||||
@@ -136,6 +141,7 @@ pub async fn update_tenant(
|
||||
ok(TenantResponse::from(tenant))
|
||||
}
|
||||
|
||||
/// List a tenant's relays.
|
||||
pub async fn list_tenant_relays(
|
||||
State(api): State<Arc<Api>>,
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
@@ -149,7 +155,7 @@ pub async fn list_tenant_relays(
|
||||
ok(relays)
|
||||
}
|
||||
|
||||
/// List a tenant's invoices, most recent first.
|
||||
/// List a tenant's invoices after reconciling the tenant's billing state.
|
||||
pub async fn list_tenant_invoices(
|
||||
State(api): State<Arc<Api>>,
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
@@ -157,10 +163,15 @@ pub async fn list_tenant_invoices(
|
||||
) -> ApiResult {
|
||||
api.require_admin_or_tenant(&auth, &pubkey)?;
|
||||
|
||||
let invoices = query::list_invoices(&pubkey)
|
||||
let tenant = api.get_tenant_or_404(&pubkey).await?;
|
||||
|
||||
api.billing
|
||||
.reconcile_subscription(&tenant, false)
|
||||
.await
|
||||
.map_err(internal)?;
|
||||
|
||||
let invoices = query::list_invoices(&pubkey).await.map_err(internal)?;
|
||||
|
||||
ok(invoices)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user