From e7d547f19b589994681a426bc683b91100ac3a67 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Mon, 21 Apr 2025 14:51:54 +0330 Subject: [PATCH] refactor: the invoice queue logic --- src/app.module.ts | 6 +- src/common/enums/message.enum.ts | 2 + src/configs/sms.config.ts | 4 ++ .../invoices/enums/invoice-status.enum.ts | 1 + .../invoices/queue/invoice.processor.ts | 49 +++++++++++---- .../interfaces/ISendNotificationData.ts | 1 + .../providers/notifications.service.ts | 22 ++++++- .../settings/enums/notif-settings.enum.ts | 2 + src/modules/utils/interfaces/ISms.ts | 2 + src/modules/utils/providers/sms.service.ts | 63 ++++++++++++++----- 10 files changed, 121 insertions(+), 31 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 1281a70..69c859e 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,13 +7,13 @@ import { ConfigModule } from "@nestjs/config"; import { ThrottlerModule } from "@nestjs/throttler"; import { TypeOrmModule } from "@nestjs/typeorm"; import { MailerModule } from "@nestjs-modules/mailer"; -// import { TelegrafModule } from "nestjs-telegraf"; +import { TelegrafModule } from "nestjs-telegraf"; import { bullMqConfig } from "./configs/bullmq.config"; import { cacheConfig } from "./configs/cache.config"; import { mailerConfig } from "./configs/mailer.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; -// import { telegrafConfig } from "./configs/telegraf.config"; +import { telegrafConfig } from "./configs/telegraf.config"; import { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AddressModule } from "./modules/address/address.module"; @@ -42,7 +42,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; @Module({ imports: [ - // TelegrafModule.forRootAsync(telegrafConfig()), + TelegrafModule.forRootAsync(telegrafConfig()), MailerModule.forRootAsync(mailerConfig()), BullModule.forRootAsync(bullMqConfig()), ThrottlerModule.forRootAsync(rateLimitConfig()), diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 2ff484d..1f03f70 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -382,6 +382,8 @@ export const enum NotificationMessage { BILL_INVOICE_MESSAGE = "صورت حساب شماره [invoiceNumber] با موفقیت پرداخت شد", APPROVED_INVOICE_MESSAGE = "صورت حساب شماره [invoiceNumber] تایید شد", APPROVED_INVOICE = "تایید صورت حساب", + INVOICE_OVERDUE = "صورت حساب معوق", + INVOICE_OVERDUE_MESSAGE = "صورت حساب شماره [invoiceNumber] معوق شده و دارای مبلغ [lateFee] جریمه تاخیر است", CREATE_SERVICE = "ایجاد سرویس", CREATE_SERVICE_MESSAGE = "سرویس جدیدی با عنوان [serviceName] ایجاد شد", UNBLOCK_SERVICE = "فعال‌سازی سرویس", diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index b9cecb3..ee9135a 100755 --- a/src/configs/sms.config.ts +++ b/src/configs/sms.config.ts @@ -20,6 +20,8 @@ export function smsConfigs() { SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow("SMS_PATTERN_INVOICE_APPROVED"), SMS_PATTERN_INVOICE_PAID: configService.getOrThrow("SMS_PATTERN_INVOICE_PAID"), SMS_PATTERN_INVOICE_PAYMENT_REMINDER: configService.getOrThrow("SMS_PATTERN_INVOICE_PAYMENT_REMINDER"), + SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow("SMS_PATTERN_INVOICE_REMINDER"), + SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow("SMS_PATTERN_INVOICE_OVERDUE"), SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow("SMS_PATTERN_RECURRING_INVOICE_DRAFT"), SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow("SMS_PATTERN_SUBSCRIPTION_CANCELLED"), }; @@ -43,6 +45,8 @@ export interface ISmsConfigs { SMS_PATTERN_INVOICE_APPROVED: string; SMS_PATTERN_INVOICE_PAID: string; SMS_PATTERN_INVOICE_PAYMENT_REMINDER: string; + SMS_PATTERN_INVOICE_REMINDER: string; + SMS_PATTERN_INVOICE_OVERDUE: string; SMS_PATTERN_RECURRING_INVOICE_DRAFT: string; SMS_PATTERN_SUBSCRIPTION_CANCELLED: string; } diff --git a/src/modules/invoices/enums/invoice-status.enum.ts b/src/modules/invoices/enums/invoice-status.enum.ts index ac5f017..8464722 100755 --- a/src/modules/invoices/enums/invoice-status.enum.ts +++ b/src/modules/invoices/enums/invoice-status.enum.ts @@ -3,6 +3,7 @@ export enum InvoiceStatus { PENDING = "PENDING", WAIT_PAYMENT = "WAIT_PAYMENT", PAID = "PAID", + OVERDUE = "OVERDUE", EXPIRED = "EXPIRED", CANCELLED = "CANCELLED", } diff --git a/src/modules/invoices/queue/invoice.processor.ts b/src/modules/invoices/queue/invoice.processor.ts index d779374..380d4d5 100755 --- a/src/modules/invoices/queue/invoice.processor.ts +++ b/src/modules/invoices/queue/invoice.processor.ts @@ -18,8 +18,6 @@ import { InvoicesService } from "../providers/invoices.service"; @Processor(INVOICE.INVOICE_QUEUE_NAME, { concurrency: 5 }) export class InvoiceProcessor extends WorkerProcessor { - // protected readonly logger = new Logger(InvoiceProcessor.name); - constructor( @InjectQueue(INVOICE.INVOICE_QUEUE_NAME) private readonly invoiceQueue: Queue, private readonly invoicesService: InvoicesService, @@ -94,9 +92,7 @@ export class InvoiceProcessor extends WorkerProcessor { this.logger.verbose(`Creating subscription-based recurring invoice`); const userSubscriptionPlan = invoice.items[0]?.subscriptionPlan; - if (!userSubscriptionPlan) { - throw new Error(`No subscription plan found for invoice ${invoice.id}`); - } + if (!userSubscriptionPlan) throw new Error(`No subscription plan found for invoice ${invoice.id}`); const { plan } = userSubscriptionPlan; const dueDate = dayjs().add(INVOICE.DUEDATE, "day").toDate(); @@ -192,18 +188,49 @@ export class InvoiceProcessor extends WorkerProcessor { const dueDate = dayjs(invoice.dueDate); if (today.isAfter(dueDate)) { + // Mark invoice as overdue in status if not already paid + if (invoice.status === InvoiceStatus.PENDING || invoice.status === InvoiceStatus.WAIT_PAYMENT) { + this.logger.log(`Marking invoice ${invoice.id} as overdue`); + invoice.status = InvoiceStatus.OVERDUE; + } + const daysOverdue = Math.min(today.diff(dueDate, "day"), INVOICE.MAX_DAYS_AFTER_OVERDUE); + // Get the original price without late fees to calculate the fine correctly + const originalPrice = invoice.originalPrice || invoice.totalPrice; const currentFine = invoice.lateFee || new Decimal(0); - const totalDaysFineShouldBeApplied = daysOverdue; - const finePerDay = new Decimal(invoice.totalPrice).mul(INVOICE.FINE_PERCENTAGE); - const newTotalFine = finePerDay.mul(totalDaysFineShouldBeApplied); + const finePerDay = new Decimal(originalPrice).mul(INVOICE.FINE_PERCENTAGE); + const newTotalFine = finePerDay.mul(daysOverdue); if (newTotalFine.gt(currentFine)) { this.logger.log(`Applying late fee for invoice ${invoice.id}: ${newTotalFine} (${daysOverdue} days overdue)`); + + // Calculate the difference between new and current fine + const additionalFine = newTotalFine.minus(currentFine); + + // Update the late fee invoice.lateFee = newTotalFine; - invoice.totalPrice = new Decimal(invoice.totalPrice).plus(newTotalFine.minus(currentFine)); + + // Add only the additional fine to total price to avoid compounding + invoice.totalPrice = new Decimal(invoice.totalPrice).plus(additionalFine); + await queryRunner.manager.save(Invoice, invoice); + + // Send overdue notification to the user + await this.notificationService.createInvoiceOverdueNotification( + invoice.user.id, + { + userPhone: invoice.user.phone, + userEmail: invoice.user.email, + invoiceId: invoice.numericId.toString(), + price: new Decimal(invoice.totalPrice).toNumber(), + lateFee: new Decimal(newTotalFine).toNumber(), + dueDate: invoice.dueDate, + createDate: invoice.createdAt, + items: invoice.items.map((item) => item.name).join(", "), + }, + queryRunner, + ); } } } @@ -220,7 +247,7 @@ export class InvoiceProcessor extends WorkerProcessor { price: new Decimal(invoice.totalPrice).toNumber(), dueDate: invoice.dueDate, createDate: invoice.createdAt, - items: invoice.items.join(", "), + items: invoice.items.map((item) => item.name).join(", "), }, queryRunner, ); @@ -258,7 +285,7 @@ export class InvoiceProcessor extends WorkerProcessor { price: new Decimal(invoice.totalPrice).toNumber(), dueDate: invoice.dueDate, createDate: invoice.createdAt, - items: invoice.items.join(", "), + items: invoice.items.map((item) => item.name).join(", "), }; await Promise.all( diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts index 3f6f35a..43c02d9 100755 --- a/src/modules/notifications/interfaces/ISendNotificationData.ts +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -10,6 +10,7 @@ export interface IInvoiceNotificationData extends IBaseNotificationData { createDate: Date; invoiceId: string; paidAt?: Date; + lateFee?: number; } export interface IServiceNotificationData extends IBaseNotificationData { diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index c1562e4..cdaa96e 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -315,7 +315,27 @@ export class NotificationsService { ); } - // //************************ */ + //************************ */ + + async createInvoiceOverdueNotification(recipientId: string, data: IInvoiceNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.INVOICE_OVERDUE_MESSAGE.replace("[invoiceNumber]", data.invoiceId).replace( + "[lateFee]", + numberFormat(data.lateFee || 0), + ); + + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.INVOICE_OVERDUE, recipientId); + if (isActive) { + await this.smsService.sendInvoiceOverdueSms(data.userPhone, data.invoiceId, data.price, data.lateFee || 0, dateFormat(data.dueDate)); + //send email too + } + return this.createNotification( + { title: NotificationMessage.INVOICE_OVERDUE, type: NotifType.INVOICE_OVERDUE, message, recipientId }, + queryRunner, + ); + } + + //************************ */ async createRecurringInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, queryRunner: QueryRunner) { const message = NotificationMessage.RECURRING_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId); diff --git a/src/modules/settings/enums/notif-settings.enum.ts b/src/modules/settings/enums/notif-settings.enum.ts index de4b312..4fbf77b 100755 --- a/src/modules/settings/enums/notif-settings.enum.ts +++ b/src/modules/settings/enums/notif-settings.enum.ts @@ -21,6 +21,7 @@ export enum NotifType { BILL_INVOICE = "BILL_INVOICE", APPROVE_INVOICE = "APPROVE_INVOICE", CREATE_INVOICE = "CREATE_INVOICE", + INVOICE_OVERDUE = "INVOICE_OVERDUE", // Service category CREATE_SERVICE = "CREATE_SERVICE", @@ -50,6 +51,7 @@ export const NotifDescriptions: Record { - this.logger.error("error in sending invoice payment reminder sms", err); - throw new InternalServerErrorException("error in sending invoice payment reminder sms"); + this.logger.error("error in sending invoice reminder sms", err); + throw new InternalServerErrorException("error in sending invoice reminder sms"); }), ), ); return data; } catch (error) { this.logger.error("error in sending sms", error); - // throw new InternalServerErrorException("error in sending sms"); } } - // //************************************************* */ + + //************************************************* */ + + async sendInvoiceOverdueSms(mobile: string, invoiceId: string, amount: number, lateFee: number, dueDate: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "invoiceId", value: invoiceId }, + { name: "amount", value: amount.toString() }, + { name: "lateFee", value: lateFee.toString() }, + { name: "dueDate", value: dueDate }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_OVERDUE, + }; + + try { + const { data } = await firstValueFrom( + this.httpService + .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { + headers: { "X-API-KEY": this.smsConfigs.API_KEY }, + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error("error in sending invoice overdue sms", err); + throw new InternalServerErrorException("error in sending invoice overdue sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } + + //************************************************* */ async sendRecurringInvoiceDraftSms(adminMobile: string, invoiceId: string, price: number) { const smsData: ISmsVerifyBody = { @@ -461,7 +494,7 @@ export class SmsService { } } - // //************************************************* */ + //************************************************* */ async sendBlockServiceSms(mobile: string, invoiceId: string, planName: string) { const smsData: ISmsVerifyBody = { @@ -493,23 +526,21 @@ export class SmsService { } } - // //************************************************* */ + //************************************************* */ // sendCreateServiceSms(userPhone: string, serviceName: any) { // throw new Error("Method not implemented."); // } - // //************************************************* */ + //************************************************* */ // sendUnblockServiceSms(userPhone: string, serviceName: any) { // throw new Error("Method not implemented."); // } - // //************************************************* */ + //************************************************* */ // sendBlockServiceSms(userPhone: string, serviceName: any) { // throw new Error("Method not implemented."); // } - // //************************************************* */ - //************************************************* */ async getSmsLines() {