mirror of
https://github.com/vernu/textbee.git
synced 2026-02-19 23:26:14 -05:00
chore(api): improve billing notifications
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter'
|
||||
import { InjectQueue } from '@nestjs/bull'
|
||||
import { Queue } from 'bull'
|
||||
import { Model, Types } from 'mongoose'
|
||||
import {
|
||||
BillingNotification,
|
||||
@@ -23,7 +24,7 @@ export class BillingNotificationsService {
|
||||
constructor(
|
||||
@InjectModel(BillingNotification.name)
|
||||
private readonly notificationModel: Model<BillingNotificationDocument>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
@InjectQueue('billing-notifications') private readonly billingQueue: Queue,
|
||||
) {}
|
||||
|
||||
async notifyOnce({ userId, type, title, message, meta = {}, sendEmail = true }: NotifyOnceInput) {
|
||||
@@ -34,7 +35,25 @@ export class BillingNotificationsService {
|
||||
|
||||
const created = await this.createNotification(userId, type, title, message, meta)
|
||||
|
||||
this.emitCreatedEvent(created, { sendEmail })
|
||||
await this.billingQueue.add(
|
||||
'send',
|
||||
{
|
||||
notificationId: created._id,
|
||||
userId: created.user,
|
||||
type: created.type,
|
||||
title: created.title,
|
||||
message: created.message,
|
||||
meta: created.meta,
|
||||
createdAt: created.createdAt,
|
||||
sendEmail,
|
||||
},
|
||||
{
|
||||
delay: 30000,
|
||||
attempts: 3,
|
||||
removeOnComplete: true,
|
||||
backoff: { type: 'exponential', delay: 2000 },
|
||||
},
|
||||
)
|
||||
|
||||
return created
|
||||
}
|
||||
@@ -84,18 +103,6 @@ export class BillingNotificationsService {
|
||||
})
|
||||
}
|
||||
|
||||
private emitCreatedEvent(notification: BillingNotificationDocument, options: { sendEmail: boolean }) {
|
||||
this.eventEmitter.emit('billing.notification.created', {
|
||||
notificationId: notification._id,
|
||||
userId: notification.user,
|
||||
type: notification.type,
|
||||
title: notification.title,
|
||||
message: notification.message,
|
||||
meta: notification.meta,
|
||||
createdAt: notification.createdAt,
|
||||
sendEmail: options.sendEmail,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { BillingNotificationType }
|
||||
|
||||
@@ -16,10 +16,20 @@ import { Device, DeviceSchema } from '../gateway/schemas/device.schema'
|
||||
import { CheckoutSession, CheckoutSessionSchema } from './schemas/checkout-session.schema'
|
||||
import { BillingNotification, BillingNotificationSchema } from './schemas/billing-notification.schema'
|
||||
import { BillingNotificationsService } from './billing-notifications.service'
|
||||
import { BillingNotificationsListener } from './billing-notifications.listener'
|
||||
// import { BillingNotificationsListener } from './billing-notifications.listener'
|
||||
import { BullModule } from '@nestjs/bull'
|
||||
import { BillingNotificationsProcessor } from 'src/billing/queue/billing-notifications.processor'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BullModule.registerQueue({
|
||||
name: 'billing-notifications',
|
||||
defaultJobOptions: {
|
||||
attempts: 2,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
},
|
||||
}),
|
||||
MongooseModule.forFeature([
|
||||
{ name: Plan.name, schema: PlanSchema },
|
||||
{ name: Subscription.name, schema: SubscriptionSchema },
|
||||
@@ -34,7 +44,7 @@ import { BillingNotificationsListener } from './billing-notifications.listener'
|
||||
MailModule,
|
||||
],
|
||||
controllers: [BillingController],
|
||||
providers: [BillingService, AbandonedCheckoutService, BillingNotificationsService, BillingNotificationsListener],
|
||||
providers: [BillingService, AbandonedCheckoutService, BillingNotificationsService, BillingNotificationsProcessor],
|
||||
exports: [BillingService, AbandonedCheckoutService, BillingNotificationsService],
|
||||
})
|
||||
export class BillingModule {}
|
||||
|
||||
92
api/src/billing/queue/billing-notifications.processor.ts
Normal file
92
api/src/billing/queue/billing-notifications.processor.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Process, Processor } from '@nestjs/bull'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Job } from 'bull'
|
||||
import { Model, Types } from 'mongoose'
|
||||
import { MailService } from '../../mail/mail.service'
|
||||
import { User, UserDocument } from '../../users/schemas/user.schema'
|
||||
import {
|
||||
BillingNotification,
|
||||
BillingNotificationDocument,
|
||||
} from '../schemas/billing-notification.schema'
|
||||
|
||||
@Processor('billing-notifications')
|
||||
export class BillingNotificationsProcessor {
|
||||
constructor(
|
||||
private readonly mailService: MailService,
|
||||
@InjectModel(BillingNotification.name)
|
||||
private readonly notificationModel: Model<BillingNotificationDocument>,
|
||||
@InjectModel(User.name)
|
||||
private readonly userModel: Model<UserDocument>,
|
||||
) {}
|
||||
|
||||
@Process({ name: 'send', concurrency: 1 })
|
||||
async handleSend(job: Job<{
|
||||
notificationId: Types.ObjectId
|
||||
userId: Types.ObjectId
|
||||
type: string
|
||||
title: string
|
||||
message: string
|
||||
meta: Record<string, any>
|
||||
createdAt: Date
|
||||
sendEmail?: boolean
|
||||
}>) {
|
||||
const payload = job.data
|
||||
if (!payload?.sendEmail) {
|
||||
return
|
||||
}
|
||||
|
||||
const user = await this.userModel.findById(payload.userId)
|
||||
if (!user?.email) {
|
||||
return
|
||||
}
|
||||
|
||||
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.sendEmailFromTemplate({
|
||||
to: user.email,
|
||||
subject,
|
||||
template: 'billing-notification',
|
||||
context: {
|
||||
name: user.name?.split(' ')?.[0] || 'there',
|
||||
title: payload.title,
|
||||
message: payload.message,
|
||||
ctaLabel,
|
||||
ctaUrl,
|
||||
brandName: 'textbee.dev',
|
||||
},
|
||||
from: undefined,
|
||||
})
|
||||
|
||||
await this.notificationModel.updateOne(
|
||||
{ _id: payload.notificationId },
|
||||
{ $inc: { sentEmailCount: 1 }, $set: { lastEmailSentAt: new Date() } },
|
||||
)
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user