forked from coracle/caravel
Avoid spammy payment DMs
This commit is contained in:
@@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS invoice (
|
|||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
paid_at INTEGER,
|
paid_at INTEGER,
|
||||||
voided_at INTEGER,
|
voided_at INTEGER,
|
||||||
|
notified_at INTEGER,
|
||||||
FOREIGN KEY (tenant_pubkey) REFERENCES tenant(pubkey)
|
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
|
/// 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.
|
/// 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 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 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:";
|
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,
|
invoice: &Invoice,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
) -> Result<()> {
|
) -> 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();
|
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 {
|
if now - invoice.created_at < FRESH_INVOICE_DM_GRACE_SECS {
|
||||||
return Ok(());
|
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 invoice_id = &invoice.id;
|
||||||
let url_base = &env::get().app_url;
|
let url_base = &env::get().app_url;
|
||||||
let payment_url = format!("{url_base}/account?invoice={invoice_id}");
|
let payment_url = format!("{url_base}/account?invoice={invoice_id}");
|
||||||
@@ -443,7 +451,11 @@ impl Billing {
|
|||||||
_ => base,
|
_ => 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 ---
|
// --- Bolt11 utils ---
|
||||||
|
|||||||
@@ -422,6 +422,19 @@ pub async fn settle_invoice_via_stripe(
|
|||||||
.await
|
.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 ---
|
// --- Bolt11 records ---
|
||||||
|
|
||||||
pub async fn insert_bolt11(
|
pub async fn insert_bolt11(
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ pub struct Invoice {
|
|||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub paid_at: Option<i64>,
|
pub paid_at: Option<i64>,
|
||||||
pub voided_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
|
/// How the invoice was paid — `nwc`, `stripe`, or `oob` (out-of-band
|
||||||
/// Lightning) — set when it is marked paid; `None` while open or void.
|
/// Lightning) — set when it is marked paid; `None` while open or void.
|
||||||
pub method: Option<String>,
|
pub method: Option<String>,
|
||||||
|
|||||||
Reference in New Issue
Block a user