fix(email): calm, correctly-signed consent expiry notification (#1276)
* fix(email): calm, correctly-signed consent expiry notification The consent expiry email was signed with the recipient's own company name instead of the app, used red alarm chrome (header pill + button), and never said why the recipient got it. After the #1271 health probe drained a backlog of 25 dead sessions in one 05:00 cron run, that design read as phishing to a batch of users at once. - Sign off as the app; the company the connection belongs to moves into a details row and the why-did-I-get-this footer - Drop all red/orange chrome; neutral editorial layout, pill button - Explain that PSD2 consent expiry is routine and that no data is lost - Show the destination URL as plain text next to the button - Calmer subjects (renewal framing instead of 'synkronisering stoppad') - Reply-to support instead of dead-ending at noreply - Add template tests (was untested) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(enable-banking): pause consent expiry emails behind env flag Founder call 2026-07-29: the in-app surfaces already flag a dead connection, so the cron email adds noise. Status transitions keep running; set BANK_CONSENT_EXPIRY_EMAILS=true to resume sending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
80a14ddfd2
commit
0df05c83c6
@@ -428,6 +428,11 @@ export const GET = withCronContext('cron.bank_sync', async (_request, ctx) => {
|
||||
/**
|
||||
* Send consent expiry notification email.
|
||||
* Guards with last_expiry_notification_at to avoid spamming (2-day cooldown).
|
||||
*
|
||||
* Paused by default (founder call 2026-07-29, after the probe backlog drain
|
||||
* mass-emailed 24 users at once): the settings panel and attention surfaces
|
||||
* already flag a dead connection in-app. Set BANK_CONSENT_EXPIRY_EMAILS=true
|
||||
* to resume sending; the status transitions below run either way.
|
||||
*/
|
||||
async function sendConsentExpiryNotification(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -438,6 +443,8 @@ async function sendConsentExpiryNotification(
|
||||
baseUrl: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (process.env.BANK_CONSENT_EXPIRY_EMAILS !== 'true') return
|
||||
|
||||
// Check cooldown: skip if notified within last 2 days
|
||||
const lastNotified = connection.last_expiry_notification_at as string | null
|
||||
if (lastNotified) {
|
||||
@@ -465,7 +472,7 @@ async function sendConsentExpiryNotification(
|
||||
bankName: connection.bank_name as string,
|
||||
daysUntilExpiry: daysLeft,
|
||||
renewalUrl: `${baseUrl}/settings/banking`,
|
||||
companyName: companySettings?.company_name || getBranding().appName.toLowerCase(),
|
||||
companyName: companySettings?.company_name || '',
|
||||
isExpired,
|
||||
}
|
||||
|
||||
@@ -474,6 +481,7 @@ async function sendConsentExpiryNotification(
|
||||
subject: generateConsentExpiryEmailSubject(emailData),
|
||||
html: generateConsentExpiryEmailHtml(emailData),
|
||||
text: generateConsentExpiryEmailText(emailData),
|
||||
replyTo: getBranding().supportEmail,
|
||||
})
|
||||
|
||||
// Update last notification timestamp
|
||||
|
||||
Reference in New Issue
Block a user