Collapse multiple invoice tables into one
This commit is contained in:
@@ -26,47 +26,30 @@ pub async fn get_invoice(
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
Path(id): Path<String>,
|
||||
) -> ApiResult {
|
||||
let Some(invoice) = self.stripe.get_invoice(id).await? else {
|
||||
return not_found("invoice not found")
|
||||
};
|
||||
|
||||
let tenant = api
|
||||
.query
|
||||
.get_tenant_by_stripe_customer_id(&invoice.customer)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("invoice not found"))?;
|
||||
|
||||
api.require_admin_or_tenant(&auth, &tenant.pubkey)?;
|
||||
let (invoice, _tenant) = load_authorized_invoice(&api, &auth, &id).await?;
|
||||
|
||||
let invoice = api
|
||||
.billing
|
||||
.reconcile_manual_lightning_invoice(&id, &invoice)
|
||||
.reconcile_invoice(&invoice)
|
||||
.await
|
||||
.map_err(internal)?;
|
||||
|
||||
ok(invoice)
|
||||
}
|
||||
|
||||
pub async fn get_invoice_bolt11(
|
||||
pub async fn get_lightning_invoice(
|
||||
State(api): State<Arc<Api>>,
|
||||
AuthedPubkey(auth): AuthedPubkey,
|
||||
Path(id): Path<String>,
|
||||
) -> ApiResult {
|
||||
let Some(invoice) = self.stripe.get_invoice(id).await? else {
|
||||
return not_found("invoice not found")
|
||||
};
|
||||
|
||||
let tenant = api
|
||||
.query
|
||||
.get_tenant_by_stripe_customer_id(&invoice.customer)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("invoice not found"))?;
|
||||
|
||||
api.require_admin_or_tenant(&auth, &tenant.pubkey)?;
|
||||
let (invoice, tenant) = load_authorized_invoice(&api, &auth, &id).await?;
|
||||
|
||||
// Settle first: this checks the currently-stored bolt11 against the wallet,
|
||||
// so a payment that landed before expiry is always caught before we'd
|
||||
// consider regenerating below.
|
||||
let invoice = api
|
||||
.billing
|
||||
.reconcile_manual_lightning_invoice(&id, &invoice)
|
||||
.reconcile_invoice(&invoice)
|
||||
.await
|
||||
.map_err(internal)?;
|
||||
|
||||
@@ -74,39 +57,37 @@ pub async fn get_invoice_bolt11(
|
||||
return Err(bad_request("invoice-not-open", "invoice is not open"));
|
||||
}
|
||||
|
||||
let bolt11 = if let Some(existing_bolt11) = api
|
||||
.query
|
||||
.get_invoice_manual_lightning_bolt11(&id)
|
||||
let bolt11 = api
|
||||
.billing
|
||||
.ensure_lightning_invoice(&invoice.id, &tenant.pubkey, invoice.amount_due, &invoice.currency)
|
||||
.await
|
||||
.map_err(internal)?
|
||||
{
|
||||
existing_bolt11
|
||||
} else {
|
||||
let bolt11 = api
|
||||
.billing
|
||||
.create_bolt11(invoice.amount_due, &invoice.currency)
|
||||
.await
|
||||
.map_err(internal)?;
|
||||
|
||||
if api
|
||||
.command
|
||||
.insert_manual_lightning_invoice_payment(&id, &tenant.pubkey, &bolt11)
|
||||
.await
|
||||
.map_err(internal)?
|
||||
{
|
||||
bolt11
|
||||
} else {
|
||||
api.query
|
||||
.get_invoice_manual_lightning_bolt11(&id)
|
||||
.await
|
||||
.map_err(internal)?
|
||||
.ok_or_else(|| {
|
||||
internal(format!(
|
||||
"manual lightning payment row missing after insert race for invoice {id}"
|
||||
))
|
||||
})?
|
||||
}
|
||||
};
|
||||
.map_err(internal)?;
|
||||
|
||||
ok(serde_json::json!({ "bolt11": bolt11 }))
|
||||
}
|
||||
|
||||
/// Fetch a Stripe invoice and the tenant that owns it, enforcing that the
|
||||
/// caller is that tenant (or an admin). Returns 404 if the invoice or its
|
||||
/// tenant can't be found.
|
||||
async fn load_authorized_invoice(
|
||||
api: &Api,
|
||||
auth: &str,
|
||||
stripe_invoice_id: &str,
|
||||
) -> Result<(crate::stripe::StripeInvoice, crate::models::Tenant), crate::web::ApiError> {
|
||||
let Some(invoice) = api.stripe.get_invoice(stripe_invoice_id).await.map_err(internal)? else {
|
||||
return Err(not_found("invoice not found"));
|
||||
};
|
||||
|
||||
let Some(tenant) = api
|
||||
.query
|
||||
.get_tenant_by_stripe_customer_id(&invoice.customer)
|
||||
.await
|
||||
.map_err(internal)?
|
||||
else {
|
||||
return Err(not_found("invoice not found"));
|
||||
};
|
||||
|
||||
api.require_admin_or_tenant(auth, &tenant.pubkey)?;
|
||||
|
||||
Ok((invoice, tenant))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::{Api, AuthedPubkey};
|
||||
use crate::billing::NwcInvoicePaymentOutcome;
|
||||
use crate::models::{RELAY_STATUS_ACTIVE, RELAY_STATUS_DELINQUENT};
|
||||
use crate::web::{ApiResult, bad_request, internal, ok};
|
||||
|
||||
@@ -75,8 +74,8 @@ async fn handle_webhook(api: &Api, payload: &str, signature: &str) -> Result<()>
|
||||
let customer = obj["customer"].as_str().unwrap_or_default();
|
||||
let amount_due = obj["amount_due"].as_i64().unwrap_or(0);
|
||||
let currency = obj["currency"].as_str().unwrap_or("usd");
|
||||
let invoice_id = obj["id"].as_str().unwrap_or_default();
|
||||
handle_invoice_created(api, customer, amount_due, currency, invoice_id).await?;
|
||||
let stripe_invoice_id = obj["id"].as_str().unwrap_or_default();
|
||||
handle_invoice_created(api, customer, amount_due, currency, stripe_invoice_id).await?;
|
||||
}
|
||||
"invoice.paid" => {
|
||||
let customer = obj["customer"].as_str().unwrap_or_default();
|
||||
@@ -114,7 +113,7 @@ async fn handle_invoice_created(
|
||||
stripe_customer_id: &str,
|
||||
amount_due: i64,
|
||||
currency: &str,
|
||||
invoice_id: &str,
|
||||
stripe_invoice_id: &str,
|
||||
) -> Result<()> {
|
||||
if amount_due == 0 {
|
||||
return Ok(());
|
||||
@@ -128,6 +127,12 @@ async fn handle_invoice_created(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Mint (or reuse) the single bolt11 that both NWC and manual payment settle.
|
||||
let bolt11 = api
|
||||
.billing
|
||||
.ensure_lightning_invoice(stripe_invoice_id, &tenant.pubkey, amount_due, currency)
|
||||
.await?;
|
||||
|
||||
let mut nwc_error_for_dm: Option<String> = None;
|
||||
|
||||
// 1. NWC auto-pay: if the tenant has a nwc_url
|
||||
@@ -135,22 +140,11 @@ async fn handle_invoice_created(
|
||||
let plain_nwc_url = api.env.decrypt(&tenant.nwc_url)?;
|
||||
match api
|
||||
.billing
|
||||
.nwc_pay_invoice(
|
||||
invoice_id,
|
||||
&tenant.pubkey,
|
||||
amount_due,
|
||||
currency,
|
||||
&plain_nwc_url,
|
||||
)
|
||||
.await?
|
||||
.nwc_pay_invoice(stripe_invoice_id, &tenant.pubkey, &bolt11, &plain_nwc_url)
|
||||
.await
|
||||
{
|
||||
NwcInvoicePaymentOutcome::Paid => {
|
||||
api.billing
|
||||
.mark_invoice_paid_out_of_band_after_nwc(invoice_id, &tenant.pubkey)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
NwcInvoicePaymentOutcome::Fallback(e) => {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
let error_msg = format!("{e}");
|
||||
api.command
|
||||
.set_tenant_nwc_error(&tenant.pubkey, &error_msg)
|
||||
@@ -159,25 +153,11 @@ async fn handle_invoice_created(
|
||||
error = %e,
|
||||
tenant_pubkey = %tenant.pubkey,
|
||||
stripe_customer_id,
|
||||
invoice_id,
|
||||
stripe_invoice_id,
|
||||
"nwc auto-payment failed for invoice.created"
|
||||
);
|
||||
nwc_error_for_dm = summarize_nwc_error_for_dm(&error_msg);
|
||||
// Fall through to next option
|
||||
}
|
||||
NwcInvoicePaymentOutcome::Pending(e) => {
|
||||
let error_msg = format!("{e}");
|
||||
api.command
|
||||
.set_tenant_nwc_error(&tenant.pubkey, &error_msg)
|
||||
.await?;
|
||||
tracing::error!(
|
||||
error = %e,
|
||||
tenant_pubkey = %tenant.pubkey,
|
||||
stripe_customer_id,
|
||||
invoice_id,
|
||||
"nwc auto-payment requires reconciliation before retry"
|
||||
);
|
||||
return Err(e);
|
||||
// Fall through to card / manual payment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user