Avoid spammy payment DMs
This commit is contained in:
@@ -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
@@ -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 ---
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user