Fix bolt11 reconciliation

This commit is contained in:
Jon Staab
2026-06-01 15:09:03 -07:00
parent 76fbee6be1
commit fd38f9cbc0
4 changed files with 61 additions and 37 deletions
+22 -15
View File
@@ -372,7 +372,7 @@ impl Billing {
let result: Result<()> = async {
let nwc_url = env::get().decrypt(&tenant.nwc_url)?;
let tenant_wallet = Wallet::from_url(&nwc_url)?;
let bolt11 = self.ensure_bolt11(invoice).await?;
let bolt11 = self.ensure_bolt11_for_invoice(invoice).await?;
tenant_wallet.pay_invoice(bolt11.lnbc.clone()).await?;
@@ -433,7 +433,10 @@ impl Billing {
}
// The dunning poll runs hourly; avoid excessive reminder DMs.
if invoice.notified_at.is_some_and(|t| now - t < MANUAL_PAYMENT_DM_INTERVAL_SECS) {
if invoice
.notified_at
.is_some_and(|t| now - t < MANUAL_PAYMENT_DM_INTERVAL_SECS)
{
return Ok(());
}
@@ -445,7 +448,10 @@ impl Billing {
let dm_message = match error {
Some(error) if !error.is_empty() => {
let limit: usize = 240;
let summary = error.chars().take(limit.saturating_sub(3)).collect::<String>();
let summary = error
.chars()
.take(limit.saturating_sub(3))
.collect::<String>();
format!("{base}\n\nAuto-payment failed: {summary}")
}
_ => base,
@@ -460,7 +466,7 @@ impl Billing {
// --- Bolt11 utils ---
pub async fn ensure_bolt11(&self, invoice: &Invoice) -> Result<Bolt11> {
pub async fn ensure_bolt11_for_invoice(&self, invoice: &Invoice) -> Result<Bolt11> {
let now = chrono::Utc::now().timestamp();
if let Some(existing) = query::get_bolt11_for_invoice(&invoice.id).await?
@@ -479,22 +485,23 @@ impl Billing {
.ok_or_else(|| anyhow!("failed to insert bolt11"))
}
/// Catch an out-of-band payment we never recorded — e.g. the user paid the
/// invoice but the frontend failed to notify us. If the invoice's bolt11 has
/// settled on the robot wallet, mark it paid and return the refreshed record;
/// otherwise return it unchanged. Meant to run before presenting a payable
/// invoice so we never hand back one that's already been paid.
pub async fn ensure_and_reconcile_bolt11(&self, invoice: &Invoice) -> Result<Bolt11> {
let bolt11 = self.ensure_bolt11(invoice).await?;
pub async fn reconcile_bolt11_for_invoice(&self, invoice: &Invoice) -> Result<Option<Bolt11>> {
if let Some(bolt11) = query::get_bolt11_for_invoice(&invoice.id).await? {
return self.reconcile_bolt11(&bolt11).await;
};
Ok(None)
}
async fn reconcile_bolt11(&self, bolt11: &Bolt11) -> Result<Option<Bolt11>> {
if bolt11.settled_at.is_none() && self.wallet.is_settled(&bolt11.lnbc).await? {
command::settle_invoice_out_of_band(&bolt11.id, &invoice.id).await?;
command::settle_invoice_out_of_band(&bolt11.id, &bolt11.invoice_id).await?;
// Re-fetch so the caller sees that it's been settled.
Ok(query::get_bolt11(&bolt11.id).await?.unwrap_or(bolt11))
} else {
Ok(bolt11)
return query::get_bolt11(&bolt11.id).await;
}
Ok(Some(bolt11.clone()))
}
// --- Stripe utils ---
+14 -1
View File
@@ -18,6 +18,12 @@ pub async fn get_invoice(
api.require_admin_or_tenant(&auth, &invoice.tenant_pubkey)?;
// Implicitly reconcile an outstanding lightning invoice if we have one
api.billing
.reconcile_bolt11_for_invoice(&invoice)
.await
.map_err(internal)?;
ok(invoice)
}
@@ -35,9 +41,16 @@ pub async fn get_invoice_bolt11(
api.require_admin_or_tenant(&auth, &invoice.tenant_pubkey)?;
// Make sure we have a bolt11 for this invoice
api.billing
.ensure_bolt11_for_invoice(&invoice)
.await
.map_err(internal)?;
// Check to see whether it was reconciled out of band
let bolt11 = api
.billing
.ensure_and_reconcile_bolt11(&invoice)
.reconcile_bolt11_for_invoice(&invoice)
.await
.map_err(internal)?;
+4 -1
View File
@@ -66,7 +66,10 @@ pub async fn get_tenant(
let mut tenant = api.get_tenant_or_404(&pubkey).await?;
api.billing.sync_stripe_customer(&mut tenant).await.map_err(internal)?;
api.billing
.sync_stripe_customer(&mut tenant)
.await
.map_err(internal)?;
ok(TenantResponse::from(tenant))
}