mirror of
https://github.com/vernu/textbee.git
synced 2026-02-20 07:34:00 -05:00
improve billing notification emails
This commit is contained in:
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 'You’re nearing today’s SMS limit',
|
||||
message: `You’ve used ${Math.round(dailyPct * 100)}% of today’s 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: 'You’re nearing this month’s SMS limit',
|
||||
message: `You’ve used ${Math.round(monthlyPct * 100)}% of this month’s 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: 'You’re nearing today’s SMS limit',
|
||||
message: `You’ve used ${Math.round(dailyPct * 100)}% of today’s 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: 'You’re nearing this month’s SMS limit',
|
||||
message: `You’ve used ${Math.round(monthlyPct * 100)}% of this month’s 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 — you’ve 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 — you’ve used this billing period’s 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,
|
||||
|
||||
87
api/src/mail/templates/billing-notification.hbs
Normal file
87
api/src/mail/templates/billing-notification.hbs
Normal 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>
|
||||
|
||||
Reference in New Issue
Block a user