Avoid spammy payment DMs

This commit is contained in:
Jon Staab
2026-06-01 12:59:19 -07:00
parent f5403b6aef
commit 31c8e596a6
4 changed files with 31 additions and 2 deletions
+1
View File
@@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS invoice (
created_at INTEGER NOT NULL,
paid_at INTEGER,
voided_at INTEGER,
notified_at INTEGER,
FOREIGN KEY (tenant_pubkey) REFERENCES tenant(pubkey)
);
+14 -2
View File
@@ -18,6 +18,7 @@ const GRACE_PERIOD_SECS: i64 = 7 * 24 * 60 * 60;
/// issued invoice is surfaced to the tenant in-app first (e.g. right after they
/// create a relay), so we don't also nag by DM on the first dunning cycles.
const FRESH_INVOICE_DM_GRACE_SECS: i64 = 24 * 60 * 60;
const MANUAL_PAYMENT_DM_INTERVAL_SECS: i64 = 12 * 24 * 60 * 60;
const MANUAL_PAYMENT_DM: &str = "Payment is due for your relay subscription. Open the link below to review the invoice and pay by Lightning or card:";
const CHURN_DM: &str = "Your relay subscription is past due, so your relays have been paused. You can restore service any time by adding a payment method or paying an invoice from your dashboard:";
@@ -424,12 +425,19 @@ impl Billing {
invoice: &Invoice,
error: Option<String>,
) -> Result<()> {
// If the invoice was just generated, give the user a chance to check the dashboard before nagging them.
let now = chrono::Utc::now().timestamp();
// If the invoice was just generated, give the user a chance to check the dashboard before nagging them.
if now - invoice.created_at < FRESH_INVOICE_DM_GRACE_SECS {
return Ok(());
}
// The dunning poll runs hourly; avoid excessive reminder DMs.
if invoice.notified_at.is_some_and(|t| now - t < MANUAL_PAYMENT_DM_INTERVAL_SECS) {
return Ok(());
}
// Build the message
let invoice_id = &invoice.id;
let url_base = &env::get().app_url;
let payment_url = format!("{url_base}/account?invoice={invoice_id}");
@@ -443,7 +451,11 @@ impl Billing {
_ => base,
};
self.robot.send_dm(&tenant.pubkey, &dm_message).await
// Send via NIP 17
self.robot.send_dm(&tenant.pubkey, &dm_message).await?;
// Record the send to avoid spammy notifications.
command::mark_invoice_notified(invoice_id).await
}
// --- Bolt11 utils ---
+13
View File
@@ -422,6 +422,19 @@ pub async fn settle_invoice_via_stripe(
.await
}
/// Stamp an invoice with the time its manual-payment reminder DM was sent, so
/// dunning sends that DM once instead of on every hourly poll.
pub async fn mark_invoice_notified(invoice_id: &str) -> Result<()> {
let notified_at = chrono::Utc::now().timestamp();
sqlx::query("UPDATE invoice SET notified_at = ? WHERE id = ?")
.bind(notified_at)
.bind(invoice_id)
.execute(pool())
.await?;
Ok(())
}
// --- Bolt11 records ---
pub async fn insert_bolt11(
+3
View File
@@ -133,6 +133,9 @@ pub struct Invoice {
pub created_at: i64,
pub paid_at: Option<i64>,
pub voided_at: Option<i64>,
/// When the manual-payment reminder DM was sent for this invoice, or `None` if
/// it hasn't been sent in order to avoid duplicate reminders for the same invoice.
pub notified_at: Option<i64>,
/// How the invoice was paid — `nwc`, `stripe`, or `oob` (out-of-band
/// Lightning) — set when it is marked paid; `None` while open or void.
pub method: Option<String>,