fix: silent NWC auto-payment failure messaging in invoice.created fallback #46

Merged
hodlbod merged 1 commits from userAdityaa/caravel:silent-failure into master 2026-04-23 16:09:51 +00:00
+41 -11
View File
@@ -17,6 +17,9 @@ type HmacSha256 = Hmac<Sha256>;
const STRIPE_API: &str = "https://api.stripe.com/v1";
const COINBASE_SPOT_API: &str = "https://api.coinbase.com/v2/prices";
const WEBHOOK_TOLERANCE_SECS: i64 = 300;
const MANUAL_LIGHTNING_PAYMENT_DM: &str = "Payment is due for your relay subscription. Please visit the application to complete a manual Lightning payment.";
const NWC_ERROR_DM_PREFIX: &str = "NWC auto-payment failed:";
const NWC_ERROR_DM_MAX_CHARS: usize = 240;
#[derive(Debug)]
pub enum InvoiceLookupError {
@@ -80,7 +83,6 @@ pub struct Billing {
nwc_url: String,
stripe_secret_key: String,
stripe_webhook_secret: String,
btc_quote_api_base: String,
http: reqwest::Client,
query: Query,
command: Command,
@@ -98,13 +100,10 @@ impl Billing {
if stripe_webhook_secret.trim().is_empty() {
panic!("missing STRIPE_WEBHOOK_SECRET environment variable");
}
let btc_quote_api_base =
std::env::var("BTC_PRICE_API_BASE").unwrap_or_else(|_| COINBASE_SPOT_API.to_string());
Self {
nwc_url,
stripe_secret_key,
stripe_webhook_secret,
btc_quote_api_base,
http: reqwest::Client::new(),
query,
command,
@@ -360,6 +359,8 @@ impl Billing {
return Ok(());
};
let mut nwc_error_for_dm: Option<String> = None;
// 1. NWC auto-pay: if the tenant has a nwc_url
if !tenant.nwc_url.is_empty() {
match self
@@ -376,6 +377,14 @@ impl Billing {
self.command
.set_tenant_nwc_error(&tenant.pubkey, &error_msg)
.await?;
tracing::warn!(
error = %e,
tenant_pubkey = %tenant.pubkey,
stripe_customer_id,
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
}
}
@@ -390,12 +399,8 @@ impl Billing {
}
// 3. Manual payment: send a DM
self.robot
.send_dm(
&tenant.pubkey,
"Payment is due for your relay subscription. Please visit the application to complete a manual Lightning payment.",
)
.await?;
let dm_message = manual_lightning_payment_dm(nwc_error_for_dm.as_deref());
self.robot.send_dm(&tenant.pubkey, &dm_message).await?;
Ok(())
}
@@ -1057,7 +1062,7 @@ impl Billing {
}
async fn fetch_btc_spot_price(&self, currency: &str) -> Result<f64> {
fetch_btc_spot_price_from_base(&self.http, &self.btc_quote_api_base, currency).await
fetch_btc_spot_price_from_base(&self.http, COINBASE_SPOT_API, currency).await
}
fn currency_minor_exponent(currency: &str) -> Result<u8> {
@@ -1101,6 +1106,31 @@ pub async fn fetch_btc_spot_price_from_base(
Ok(amount)
}
fn summarize_nwc_error_for_dm(error: &str) -> Option<String> {
let normalized = error.split_whitespace().collect::<Vec<_>>().join(" ");
if normalized.is_empty() {
return None;
}
if normalized.chars().count() <= NWC_ERROR_DM_MAX_CHARS {
return Some(normalized);
}
let prefix_len = NWC_ERROR_DM_MAX_CHARS.saturating_sub(3);
let mut truncated = normalized.chars().take(prefix_len).collect::<String>();
truncated.push_str("...");
Some(truncated)
}
fn manual_lightning_payment_dm(nwc_error: Option<&str>) -> String {
match nwc_error {
Some(error) if !error.is_empty() => {
format!("{MANUAL_LIGHTNING_PAYMENT_DM}\n\n{NWC_ERROR_DM_PREFIX} {error}")
}
_ => MANUAL_LIGHTNING_PAYMENT_DM.to_string(),
}
}
pub fn fiat_minor_to_msats_from_quote(
amount_due_minor: i64,
currency: &str,