improve billing notification emails

This commit is contained in:
isra el
2025-10-29 06:43:51 +03:00
parent 455696275b
commit e2246cf8e8
3 changed files with 138 additions and 35 deletions

View File

@@ -39,16 +39,26 @@ export class BillingNotificationsListener {
return
}
const html = this.buildEmailHtml({
name: user.name?.split(' ')?.[0] || 'there',
title: payload.title,
message: payload.message,
})
const subject = this.subjectForType(payload.type, payload.title)
const ctaUrlBase = process.env.FRONTEND_URL || 'https://app.textbee.dev'
const isEmailVerification = payload.type === 'email_verification_required'
const ctaUrl = isEmailVerification
? `${ctaUrlBase}/dashboard/account`
: 'https://textbee.dev/#pricing'
const ctaLabel = isEmailVerification ? 'Verify your email' : 'View plans & pricing'
await this.mailService.sendEmail({
await this.mailService.sendEmailFromTemplate({
to: user.email,
subject: payload.title,
html,
subject,
template: 'billing-notification',
context: {
name: user.name?.split(' ')?.[0] || 'there',
title: payload.title,
message: payload.message,
ctaLabel,
ctaUrl,
brandName: 'textbee.dev',
},
from: undefined,
})
@@ -58,20 +68,22 @@ export class BillingNotificationsListener {
)
}
private buildEmailHtml({
name,
title,
message,
}: {
name: string
title: string
message: string
}) {
return `
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height:1.6;">
<h2 style="margin:0 0 8px 0;">${title}</h2>
<p style="margin:0;">Hi ${name}, ${message}</p>
</div>
`
private subjectForType(type: string, fallback: string) {
switch (type) {
case 'daily_limit_reached':
return 'Daily SMS limit reached — action required'
case 'monthly_limit_reached':
return 'Monthly SMS limit reached — action required'
case 'bulk_sms_limit_reached':
return 'Bulk send limit exceeded'
case 'daily_limit_approaching':
return 'Heads up: daily usage nearing your limit'
case 'monthly_limit_approaching':
return 'Heads up: monthly usage nearing your limit'
case 'email_verification_required':
return 'Verify your email to keep using textbee'
default:
return fallback || 'Account notification'
}
}
}

View File

@@ -85,8 +85,8 @@ export class BillingService {
.notifyOnce({
userId: user._id,
type: BillingNotificationType.DAILY_LIMIT_APPROACHING,
title: 'Daily limit approaching',
message: `You have used ${Math.round(dailyPct * 100)}% of your daily limit. ${plan.dailyLimit - processedSmsToday} messages remaining today.`,
title: 'Youre nearing todays SMS limit',
message: `Youve used ${Math.round(dailyPct * 100)}% of todays SMS allocation. ${plan.dailyLimit - processedSmsToday} messages remain for today. Consider upgrading your plan or scheduling sends for later.`,
meta: { processedSmsToday, dailyLimit: plan.dailyLimit },
sendEmail: true,
})
@@ -100,8 +100,8 @@ export class BillingService {
.notifyOnce({
userId: user._id,
type: BillingNotificationType.MONTHLY_LIMIT_APPROACHING,
title: 'Monthly limit approaching',
message: `You have used ${Math.round(monthlyPct * 100)}% of your monthly limit. ${plan.monthlyLimit - processedSmsLastMonth} messages remaining this month.`,
title: 'Youre nearing this months SMS limit',
message: `Youve used ${Math.round(monthlyPct * 100)}% of this months SMS allocation. ${plan.monthlyLimit - processedSmsLastMonth} messages remain this billing period. Upgrade to increase your monthly capacity.`,
meta: { processedSmsLastMonth, monthlyLimit: plan.monthlyLimit },
sendEmail: true,
})
@@ -135,8 +135,8 @@ export class BillingService {
.notifyOnce({
userId: user._id,
type: BillingNotificationType.DAILY_LIMIT_APPROACHING,
title: 'Daily limit approaching',
message: `You have used ${Math.round(dailyPct * 100)}% of your daily limit. ${plan.dailyLimit - processedSmsToday} messages remaining today.`,
title: 'Youre nearing todays SMS limit',
message: `Youve used ${Math.round(dailyPct * 100)}% of todays SMS allocation. ${plan.dailyLimit - processedSmsToday} messages remain for today. Consider upgrading your plan or scheduling sends for later.`,
meta: { processedSmsToday, dailyLimit: plan.dailyLimit },
sendEmail: true,
})
@@ -150,8 +150,8 @@ export class BillingService {
.notifyOnce({
userId: user._id,
type: BillingNotificationType.MONTHLY_LIMIT_APPROACHING,
title: 'Monthly limit approaching',
message: `You have used ${Math.round(monthlyPct * 100)}% of your monthly limit. ${plan.monthlyLimit - processedSmsLastMonth} messages remaining this month.`,
title: 'Youre nearing this months SMS limit',
message: `Youve used ${Math.round(monthlyPct * 100)}% of this months SMS allocation. ${plan.monthlyLimit - processedSmsLastMonth} messages remain this billing period. Upgrade to increase your monthly capacity.`,
meta: { processedSmsLastMonth, monthlyLimit: plan.monthlyLimit },
sendEmail: true,
})
@@ -500,17 +500,17 @@ export class BillingService {
if (dailyExceeded) {
hasReachedLimit = true
message = `Daily limit reached. ${Math.max(0, plan.dailyLimit - processedSmsToday)} remaining today.`
message = `Daily SMS limit reached — youve used your full daily allocation. ${Math.max(0, plan.dailyLimit - processedSmsToday)} messages remain for today. Upgrade to increase your daily capacity or try again tomorrow.`
}
if (monthlyExceeded) {
hasReachedLimit = true
message = `Monthly limit reached. ${Math.max(0, plan.monthlyLimit - processedSmsLastMonth)} remaining this month.`
message = `Monthly SMS limit reached — youve used this billing periods allocation. Upgrade to continue sending immediately, or wait for the next billing period.`
}
if (bulkExceeded) {
hasReachedLimit = true
message = `Bulk send limit is ${plan.bulkSendLimit} messages per batch.`
message = `Bulk send limit exceeded — your plan allows up to ${plan.bulkSendLimit} messages per batch. Split your send into smaller batches or upgrade your plan.`
}
}
@@ -534,18 +534,22 @@ export class BillingService {
)
// send a deduped notification (single call here for minimal change)
let type: BillingNotificationType
let titleForEmail = ''
if (dailyExceeded) {
type = BillingNotificationType.DAILY_LIMIT_REACHED
titleForEmail = 'Daily SMS limit reached'
} else if (monthlyExceeded) {
type = BillingNotificationType.MONTHLY_LIMIT_REACHED
titleForEmail = 'Monthly SMS limit reached'
} else if (bulkExceeded) {
type = BillingNotificationType.BULK_SMS_LIMIT_REACHED
titleForEmail = 'Bulk send limit exceeded'
}
if (type) {
await this.billingNotifications.notifyOnce({
userId: user._id,
type,
title: message.split('.')[0],
title: titleForEmail || 'Usage limit notice',
message,
meta: {
processedSmsToday,

View File

@@ -0,0 +1,87 @@
<html lang='en' xmlns:v='urn:schemas-microsoft-com:vml' xmlns:o='urn:schemas-microsoft-com:office:office'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta http-equiv='x-ua-compatible' content='ie=edge' />
<title>{{title}}{{brandName}}</title>
<style>
.preheader { display:none !important; visibility:hidden; opacity:0; color:transparent; height:0; width:0; overflow:hidden; mso-hide:all; }
@media screen and (max-width: 600px) { .container { width:100% !important; } .stack { display:block !important; width:100% !important; } .p-sm { padding:16px !important; } .text-center-sm { text-align:center !important; } .hide-sm { display:none !important; } }
</style>
<!--[if mso]>
<style type="text/css"> body, table, td, a { font-family: Helvetica, Arial, sans-serif !important; } </style>
<![endif]-->
</head>
<body style='margin:0; padding:0; background:#f7f9fc;'>
<div class='preheader'>Important account update from {{brandName}}</div>
<table role='presentation' cellpadding='0' cellspacing='0' border='0' width='100%' class='email-bg' style='background:#f7f9fc;'>
<tr>
<td align='center' style='padding:24px;'>
<table role='presentation' cellpadding='0' cellspacing='0' border='0' width='600' class='container' style='width:600px; max-width:600px;'>
<tr>
<td style='padding:12px 16px 0 16px;'>
<table role='presentation' width='100%' cellspacing='0' cellpadding='0' border='0'>
<tr>
<td class='stack' valign='middle' style='padding:8px 0;'>
<table role='presentation' cellspacing='0' cellpadding='0' border='0'>
<tr>
<td valign='middle' style='padding-right:10px;'>
<img src='https://textbee.dev/images/logo.png' alt='{{brandName}}' width='36' height='36' style='display:block; border:0; outline:none; text-decoration:none;' />
</td>
<td valign='middle' style='font:600 18px Arial, Helvetica, sans-serif; color:#EA580C;'>{{brandName}}</td>
</tr>
</table>
</td>
<td class='stack text-center-sm' valign='middle' align='right' style='padding:8px 0;'>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align='center' style='padding:16px 16px 0 16px;'>
<table role='presentation' width='100%' cellspacing='0' cellpadding='0' border='0' class='card' style='background:#ffffff; border-radius:10px;'>
<tr>
<td align='center' style='background:#F97316; border-radius:10px 10px 0 0; padding:28px 20px;'>
<div style='font:700 24px Arial, Helvetica, sans-serif; color:#ffffff;'>{{title}}</div>
</td>
</tr>
<tr>
<td class='p-sm' style='padding:28px; font:16px/1.6 Arial, Helvetica, sans-serif; color:#111827;'>
<div style='font:700 18px Arial, Helvetica, sans-serif; color:#111827; margin-bottom:8px;'>Hi {{name}},</div>
<p style='margin:0 0 16px 0;'>{{message}}</p>
<div style='text-align:center; padding:8px 0 2px;'>
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ctaUrl}}" style="height:48px;v-text-anchor:middle;width:260px;" arcsize="10%" strokecolor="#EA580C" fillcolor="#F97316">
<w:anchorlock/>
<center style="color:#ffffff;font-family:Arial, Helvetica, sans-serif;font-size:16px;font-weight:bold;">{{ctaLabel}}</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-- -->
<a href='{{ctaUrl}}' style='background:#F97316; border:1px solid #EA580C; border-radius:6px; color:#ffffff; display:inline-block; font:700 16px Arial, Helvetica, sans-serif; line-height:48px; text-align:center; text-decoration:none; width:260px;'>{{ctaLabel}}</a>
<!--<![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align='center' style='padding:16px;'>
<table role='presentation' width='100%' cellspacing='0' cellpadding='0' border='0'>
<tr>
<td align='center' style='font:12px/1.6 Arial, Helvetica, sans-serif; color:#6b7280;'>
<div>© 2025 {{brandName}}. All rights reserved.</div>
<div class='muted' style='margin-top:4px;'>Manage notifications in <a href='https://app.textbee.dev/dashboard/account/' style='color:#6b7280; text-decoration:underline;'>Account settings</a>.</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>