mirror of
https://github.com/vernu/textbee.git
synced 2026-05-19 05:46:23 -04:00
feat(api): billing notifications
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
"@nestjs/common": "^10.4.5",
|
||||
"@nestjs/config": "^4.0.1",
|
||||
"@nestjs/core": "^10.4.5",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/mongoose": "^10.0.10",
|
||||
|
||||
20
api/pnpm-lock.yaml
generated
20
api/pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
||||
'@nestjs/core':
|
||||
specifier: ^10.4.5
|
||||
version: 10.4.5(@nestjs/common@10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1)
|
||||
'@nestjs/event-emitter':
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(@nestjs/common@10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)
|
||||
'@nestjs/jwt':
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0(@nestjs/common@10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))
|
||||
@@ -851,6 +854,12 @@ packages:
|
||||
'@nestjs/websockets':
|
||||
optional: true
|
||||
|
||||
'@nestjs/event-emitter@3.0.1':
|
||||
resolution: {integrity: sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==}
|
||||
peerDependencies:
|
||||
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||
'@nestjs/core': ^10.0.0 || ^11.0.0
|
||||
|
||||
'@nestjs/jwt@10.2.0':
|
||||
resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==}
|
||||
peerDependencies:
|
||||
@@ -2258,6 +2267,9 @@ packages:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter2@6.4.9:
|
||||
resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==}
|
||||
|
||||
events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
@@ -5613,6 +5625,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)':
|
||||
dependencies:
|
||||
'@nestjs/common': 10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
|
||||
'@nestjs/core': 10.4.5(@nestjs/common@10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1)
|
||||
eventemitter2: 6.4.9
|
||||
|
||||
'@nestjs/jwt@10.2.0(@nestjs/common@10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))':
|
||||
dependencies:
|
||||
'@nestjs/common': 10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
|
||||
@@ -7315,6 +7333,8 @@ snapshots:
|
||||
event-target-shim@5.0.1:
|
||||
optional: true
|
||||
|
||||
eventemitter2@6.4.9: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
execa@5.1.1:
|
||||
|
||||
@@ -19,6 +19,7 @@ import { BillingModule } from './billing/billing.module'
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config'
|
||||
import { BullModule } from '@nestjs/bull'
|
||||
import { SupportModule } from './support/support.module'
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter'
|
||||
|
||||
@Injectable()
|
||||
export class LoggerMiddleware implements NestMiddleware {
|
||||
@@ -36,6 +37,7 @@ export class LoggerMiddleware implements NestMiddleware {
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
EventEmitterModule.forRoot(),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60000,
|
||||
|
||||
77
api/src/billing/billing-notifications.listener.ts
Normal file
77
api/src/billing/billing-notifications.listener.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { OnEvent } from '@nestjs/event-emitter'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Model, Types } from 'mongoose'
|
||||
import { MailService } from '../mail/mail.service'
|
||||
import {
|
||||
BillingNotification,
|
||||
BillingNotificationDocument,
|
||||
} from './schemas/billing-notification.schema'
|
||||
import { User, UserDocument } from '../users/schemas/user.schema'
|
||||
|
||||
@Injectable()
|
||||
export class BillingNotificationsListener {
|
||||
constructor(
|
||||
private readonly mailService: MailService,
|
||||
@InjectModel(BillingNotification.name)
|
||||
private readonly notificationModel: Model<BillingNotificationDocument>,
|
||||
@InjectModel(User.name)
|
||||
private readonly userModel: Model<UserDocument>,
|
||||
) {}
|
||||
|
||||
@OnEvent('billing.notification.created', { async: true })
|
||||
async handleCreatedEvent(payload: {
|
||||
notificationId: Types.ObjectId
|
||||
userId: Types.ObjectId
|
||||
type: string
|
||||
title: string
|
||||
message: string
|
||||
meta: Record<string, any>
|
||||
createdAt: Date
|
||||
sendEmail?: boolean
|
||||
}) {
|
||||
if (!payload?.sendEmail) {
|
||||
return
|
||||
}
|
||||
|
||||
const user = await this.userModel.findById(payload.userId)
|
||||
if (!user?.email) {
|
||||
return
|
||||
}
|
||||
|
||||
const html = this.buildEmailHtml({
|
||||
name: user.name?.split(' ')?.[0] || 'there',
|
||||
title: payload.title,
|
||||
message: payload.message,
|
||||
})
|
||||
|
||||
await this.mailService.sendEmail({
|
||||
to: user.email,
|
||||
subject: payload.title,
|
||||
html,
|
||||
from: undefined,
|
||||
})
|
||||
|
||||
await this.notificationModel.updateOne(
|
||||
{ _id: payload.notificationId },
|
||||
{ $inc: { sentEmailCount: 1 }, $set: { lastEmailSentAt: new Date() } },
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
`
|
||||
}
|
||||
}
|
||||
103
api/src/billing/billing-notifications.service.ts
Normal file
103
api/src/billing/billing-notifications.service.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter'
|
||||
import { Model, Types } from 'mongoose'
|
||||
import {
|
||||
BillingNotification,
|
||||
BillingNotificationDocument,
|
||||
BillingNotificationSchema,
|
||||
BillingNotificationType,
|
||||
} from './schemas/billing-notification.schema'
|
||||
|
||||
type NotifyOnceInput = {
|
||||
userId: Types.ObjectId | string
|
||||
type: BillingNotificationType
|
||||
title: string
|
||||
message: string
|
||||
meta?: Record<string, any>
|
||||
sendEmail?: boolean
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BillingNotificationsService {
|
||||
constructor(
|
||||
@InjectModel(BillingNotification.name)
|
||||
private readonly notificationModel: Model<BillingNotificationDocument>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
async notifyOnce({ userId, type, title, message, meta = {}, sendEmail = true }: NotifyOnceInput) {
|
||||
const recent = await this.findRecentSimilar(userId, type)
|
||||
if (recent) {
|
||||
return recent
|
||||
}
|
||||
|
||||
const created = await this.createNotification(userId, type, title, message, meta)
|
||||
|
||||
this.emitCreatedEvent(created, { sendEmail })
|
||||
|
||||
return created
|
||||
}
|
||||
|
||||
async listForUser(userId: Types.ObjectId | string, { limit = 50 } = {}) {
|
||||
return this.notificationModel
|
||||
.find({ user: new Types.ObjectId(userId) })
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
private getDedupeWindowMs(type: BillingNotificationType) {
|
||||
const hours = {
|
||||
[BillingNotificationType.EMAIL_VERIFICATION_REQUIRED]: 24,
|
||||
[BillingNotificationType.DAILY_LIMIT_REACHED]: 12,
|
||||
[BillingNotificationType.MONTHLY_LIMIT_REACHED]: 48,
|
||||
[BillingNotificationType.BULK_SMS_LIMIT_REACHED]: 12,
|
||||
[BillingNotificationType.DAILY_LIMIT_APPROACHING]: 24,
|
||||
[BillingNotificationType.MONTHLY_LIMIT_APPROACHING]: 48,
|
||||
}[type]
|
||||
|
||||
return hours * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
private async findRecentSimilar(userId: Types.ObjectId | string, type: BillingNotificationType) {
|
||||
const since = new Date(Date.now() - this.getDedupeWindowMs(type))
|
||||
return this.notificationModel.findOne({
|
||||
user: new Types.ObjectId(userId),
|
||||
type,
|
||||
createdAt: { $gte: since },
|
||||
})
|
||||
}
|
||||
|
||||
private async createNotification(
|
||||
userId: Types.ObjectId | string,
|
||||
type: BillingNotificationType,
|
||||
title: string,
|
||||
message: string,
|
||||
meta: Record<string, any>,
|
||||
) {
|
||||
return this.notificationModel.create({
|
||||
user: new Types.ObjectId(userId),
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
meta,
|
||||
})
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import { MailModule } from 'src/mail/mail.module'
|
||||
import { PolarWebhookPayload, PolarWebhookPayloadSchema } from './schemas/polar-webhook-payload.schema'
|
||||
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'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -23,6 +26,7 @@ import { CheckoutSession, CheckoutSessionSchema } from './schemas/checkout-sessi
|
||||
{ name: PolarWebhookPayload.name, schema: PolarWebhookPayloadSchema },
|
||||
{ name: Device.name, schema: DeviceSchema },
|
||||
{ name: CheckoutSession.name, schema: CheckoutSessionSchema },
|
||||
{ name: BillingNotification.name, schema: BillingNotificationSchema },
|
||||
]),
|
||||
forwardRef(() => AuthModule),
|
||||
forwardRef(() => UsersModule),
|
||||
@@ -30,7 +34,7 @@ import { CheckoutSession, CheckoutSessionSchema } from './schemas/checkout-sessi
|
||||
MailModule,
|
||||
],
|
||||
controllers: [BillingController],
|
||||
providers: [BillingService, AbandonedCheckoutService],
|
||||
exports: [BillingService, AbandonedCheckoutService],
|
||||
providers: [BillingService, AbandonedCheckoutService, BillingNotificationsService, BillingNotificationsListener],
|
||||
exports: [BillingService, AbandonedCheckoutService, BillingNotificationsService],
|
||||
})
|
||||
export class BillingModule {}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
PolarWebhookPayloadDocument,
|
||||
} from './schemas/polar-webhook-payload.schema'
|
||||
import { CheckoutSession, CheckoutSessionDocument } from './schemas/checkout-session.schema'
|
||||
import { BillingNotificationsService, BillingNotificationType } from './billing-notifications.service'
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
@@ -34,6 +35,7 @@ export class BillingService {
|
||||
private polarWebhookPayloadModel: Model<PolarWebhookPayloadDocument>,
|
||||
@InjectModel(CheckoutSession.name)
|
||||
private checkoutSessionModel: Model<CheckoutSessionDocument>,
|
||||
private readonly billingNotifications: BillingNotificationsService,
|
||||
) {
|
||||
this.polarApi = new Polar({
|
||||
accessToken: process.env.POLAR_ACCESS_TOKEN ?? '',
|
||||
@@ -74,6 +76,39 @@ export class BillingService {
|
||||
|
||||
if (subscription) {
|
||||
const plan = subscription.plan
|
||||
// fire-and-forget: approaching threshold notifications
|
||||
try {
|
||||
if (plan?.dailyLimit && plan.dailyLimit > 0) {
|
||||
const dailyPct = processedSmsToday / plan.dailyLimit
|
||||
if (dailyPct >= 0.8 && processedSmsToday < plan.dailyLimit) {
|
||||
this.billingNotifications
|
||||
.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.`,
|
||||
meta: { processedSmsToday, dailyLimit: plan.dailyLimit },
|
||||
sendEmail: true,
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
if (plan?.monthlyLimit && plan.monthlyLimit > 0) {
|
||||
const monthlyPct = processedSmsLastMonth / plan.monthlyLimit
|
||||
if (monthlyPct >= 0.8 && processedSmsLastMonth < plan.monthlyLimit) {
|
||||
this.billingNotifications
|
||||
.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.`,
|
||||
meta: { processedSmsLastMonth, monthlyLimit: plan.monthlyLimit },
|
||||
sendEmail: true,
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return {
|
||||
...subscription.toObject(),
|
||||
usage: {
|
||||
@@ -91,6 +126,40 @@ export class BillingService {
|
||||
|
||||
const plan = await this.planModel.findOne({ name: 'free' })
|
||||
|
||||
// fire-and-forget: approaching threshold notifications
|
||||
try {
|
||||
if (plan?.dailyLimit && plan.dailyLimit > 0) {
|
||||
const dailyPct = processedSmsToday / plan.dailyLimit
|
||||
if (dailyPct >= 0.8 && processedSmsToday < plan.dailyLimit) {
|
||||
this.billingNotifications
|
||||
.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.`,
|
||||
meta: { processedSmsToday, dailyLimit: plan.dailyLimit },
|
||||
sendEmail: true,
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
if (plan?.monthlyLimit && plan.monthlyLimit > 0) {
|
||||
const monthlyPct = processedSmsLastMonth / plan.monthlyLimit
|
||||
if (monthlyPct >= 0.8 && processedSmsLastMonth < plan.monthlyLimit) {
|
||||
this.billingNotifications
|
||||
.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.`,
|
||||
meta: { processedSmsLastMonth, monthlyLimit: plan.monthlyLimit },
|
||||
sendEmail: true,
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
plan,
|
||||
isActive: true,
|
||||
@@ -416,29 +485,32 @@ export class BillingService {
|
||||
},
|
||||
})
|
||||
|
||||
let dailyExceeded = false
|
||||
let monthlyExceeded = false
|
||||
let bulkExceeded = false
|
||||
|
||||
if (['send_sms', 'receive_sms', 'bulk_send_sms'].includes(action)) {
|
||||
// check daily limit
|
||||
if (
|
||||
plan.dailyLimit !== -1 &&
|
||||
processedSmsToday + value > plan.dailyLimit
|
||||
) {
|
||||
const dailyFinite = plan.dailyLimit !== -1
|
||||
const monthlyFinite = plan.monthlyLimit !== -1
|
||||
|
||||
// exceeded checks
|
||||
dailyExceeded = dailyFinite && processedSmsToday + value > plan.dailyLimit
|
||||
monthlyExceeded = monthlyFinite && processedSmsLastMonth + value > plan.monthlyLimit
|
||||
bulkExceeded = plan.bulkSendLimit !== -1 && value > plan.bulkSendLimit
|
||||
|
||||
if (dailyExceeded) {
|
||||
hasReachedLimit = true
|
||||
message = `You have reached your daily limit, you only have ${plan.dailyLimit - processedSmsToday} remaining`
|
||||
message = `Daily limit reached. ${Math.max(0, plan.dailyLimit - processedSmsToday)} remaining today.`
|
||||
}
|
||||
|
||||
// check monthly limit
|
||||
if (
|
||||
plan.monthlyLimit !== -1 &&
|
||||
processedSmsLastMonth + value > plan.monthlyLimit
|
||||
) {
|
||||
if (monthlyExceeded) {
|
||||
hasReachedLimit = true
|
||||
message = `You have reached your monthly limit, you only have ${plan.monthlyLimit - processedSmsLastMonth} remaining`
|
||||
message = `Monthly limit reached. ${Math.max(0, plan.monthlyLimit - processedSmsLastMonth)} remaining this month.`
|
||||
}
|
||||
|
||||
// check bulk send limit
|
||||
if (plan.bulkSendLimit !== -1 && value > plan.bulkSendLimit) {
|
||||
if (bulkExceeded) {
|
||||
hasReachedLimit = true
|
||||
message = `You can only send ${plan.bulkSendLimit} sms at a time`
|
||||
message = `Bulk send limit is ${plan.bulkSendLimit} messages per batch.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +532,32 @@ export class BillingService {
|
||||
monthlyLimit: plan.monthlyLimit,
|
||||
}),
|
||||
)
|
||||
|
||||
// send a deduped notification (single call here for minimal change)
|
||||
let type: BillingNotificationType
|
||||
if (dailyExceeded) {
|
||||
type = BillingNotificationType.DAILY_LIMIT_REACHED
|
||||
} else if (monthlyExceeded) {
|
||||
type = BillingNotificationType.MONTHLY_LIMIT_REACHED
|
||||
} else if (bulkExceeded) {
|
||||
type = BillingNotificationType.BULK_SMS_LIMIT_REACHED
|
||||
}
|
||||
if (type) {
|
||||
await this.billingNotifications.notifyOnce({
|
||||
userId: user._id,
|
||||
type,
|
||||
title: message.split('.')[0],
|
||||
message,
|
||||
meta: {
|
||||
processedSmsToday,
|
||||
processedSmsLastMonth,
|
||||
attempted: value,
|
||||
dailyLimit: plan.dailyLimit,
|
||||
monthlyLimit: plan.monthlyLimit,
|
||||
bulkSendLimit: plan.bulkSendLimit,
|
||||
},
|
||||
sendEmail: true,
|
||||
})
|
||||
}
|
||||
throw new HttpException(
|
||||
{
|
||||
message: message,
|
||||
|
||||
58
api/src/billing/schemas/billing-notification.schema.ts
Normal file
58
api/src/billing/schemas/billing-notification.schema.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
|
||||
import { Document, Types } from 'mongoose'
|
||||
import { User } from '../../users/schemas/user.schema'
|
||||
|
||||
export type BillingNotificationDocument = BillingNotification & Document
|
||||
|
||||
export enum BillingNotificationType {
|
||||
EMAIL_VERIFICATION_REQUIRED = 'email_verification_required',
|
||||
DAILY_LIMIT_REACHED = 'daily_limit_reached',
|
||||
MONTHLY_LIMIT_REACHED = 'monthly_limit_reached',
|
||||
BULK_SMS_LIMIT_REACHED = 'bulk_sms_limit_reached',
|
||||
DAILY_LIMIT_APPROACHING = 'daily_limit_approaching',
|
||||
MONTHLY_LIMIT_APPROACHING = 'monthly_limit_approaching',
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class BillingNotification {
|
||||
_id?: Types.ObjectId
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true })
|
||||
user: User
|
||||
|
||||
@Prop({ type: String, enum: Object.values(BillingNotificationType), required: true, index: true })
|
||||
type: BillingNotificationType
|
||||
|
||||
@Prop({ type: String, required: true })
|
||||
title: string
|
||||
|
||||
@Prop({ type: String, required: true })
|
||||
message: string
|
||||
|
||||
@Prop({ type: Object, default: {} })
|
||||
meta: Record<string, any>
|
||||
|
||||
@Prop({ type: Date })
|
||||
readAt?: Date
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
isDismissed: boolean
|
||||
|
||||
@Prop({ type: Number, default: 0 })
|
||||
sentEmailCount: number
|
||||
|
||||
@Prop({ type: Date })
|
||||
lastEmailSentAt?: Date
|
||||
|
||||
// present because of timestamps: true
|
||||
@Prop({ type: Date })
|
||||
createdAt?: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
updatedAt?: Date
|
||||
}
|
||||
|
||||
export const BillingNotificationSchema =
|
||||
SchemaFactory.createForClass(BillingNotification)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user