From 4503721b906c54f0f7874934355abe4102a32005 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Tue, 11 Mar 2025 11:52:46 +0330 Subject: [PATCH] chore: add recurring invoice proccesor logic --- .gitignore | 1 + src/common/enums/message.enum.ts | 7 +- src/configs/sms.config.ts | 4 + src/modules/auth/guards/auth.guard.ts | 15 ++- src/modules/auth/strategies/jwt.strategy.ts | 1 - .../invoices/DTO/create-invoice.dto.ts | 20 ++- src/modules/invoices/constants/index.ts | 7 ++ .../enums/invoice-recurring-period.enum.ts | 7 ++ .../invoices/enums/invoice-status.enum.ts | 1 + .../providers/invoice-creation.service.ts | 0 .../invoices/providers/invoices.service.ts | 56 +++++++-- .../invoices/queue/invoice.processor.ts | 116 ++++++++++++++++-- .../interfaces/ISendNotificationData.ts | 5 + .../providers/notifications.service.ts | 40 ++++-- .../settings/enums/notif-settings.enum.ts | 2 + src/modules/utils/interfaces/ISms.ts | 1 + src/modules/utils/providers/sms.service.ts | 63 ++++++++++ 17 files changed, 310 insertions(+), 36 deletions(-) create mode 100644 src/modules/invoices/enums/invoice-recurring-period.enum.ts create mode 100644 src/modules/invoices/providers/invoice-creation.service.ts diff --git a/.gitignore b/.gitignore index 4b56acf..d3d969e 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /dist /node_modules /build +/keys # Logs logs diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index bbed418..7bb5c10 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -60,6 +60,7 @@ export const enum AuthMessage { REFRESH_TOKEN_EXPIRED = "توکن رفرش منقضی شده است", LOGOUT_SUCCESS = "خروج با موفقیت انجام شد", CURRENT_PASSWORD_INVALID = "رمز عبور فعلی نامعتبر است", + TOKEN_EXPIRED_OR_INVALID = "توکن منقضی شده یا نامعتبر است", } export const enum UserMessage { @@ -372,13 +373,15 @@ export const enum NotificationMessage { UNBLOCK_SERVICE = "فعال‌سازی سرویس", UNBLOCK_SERVICE_MESSAGE = "سرویس [serviceName] مجددا فعال شده است", BLOCK_SERVICE = "غیرفعال‌سازی سرویس", - BLOCK_SERVICE_MESSAGE = "سرویس [serviceName] غیرفعال شده است", + BLOCK_SERVICE_MESSAGE = "سرویس [serviceName] به دلیل عدم پرداخت صورتحساب غیرفعال شده است", ANSWER_TICKET = "پاسخ تیکت", ANSWER_TICKET_MESSAGE = "پاسخ جدیدی برای تیکت [ticketId] دریافت کردید", CREATE_TICKET = "ایجاد تیکت", CREATE_TICKET_MESSAGE = "تیکت شما با شماره [ticketId] ثبت شد", ASSIGN_TICKET_MESSAGE = "یک تیکت به شماره [ticketId] به شما اختصاص یافت", ASSIGN_TICKET = "اختصاص تیکت", + RECURRING_INVOICE_MESSAGE = "صورت حساب دوره‌ای شماره [invoiceNumber] صادر شده است، لطفا نسبت به تایید یا ویرایش آن اقدام کنید", + RECURRING_INVOICE = "صورت حساب دوره‌ای", } export const enum SubscriptionMessage { @@ -447,6 +450,8 @@ export const enum InvoiceMessage { INVOICE_IS_REJECTED = "صورت حساب قبلا رد شده است", INVOICE_CAN_NOT_PAID = "صورت حساب قابل پرداخت نیست", INVALID_DATE = "تاریخ وارد شده معتبر نیست", + IS_RECURRING_MUST_BE_A_BOOLEAN = "وضعیت تکرار باید یک بولین باشد", + RECURRING_PERIOD_REQUIRED = "دوره تکرار مورد نیاز است", } export const enum LearningMessage { diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index 3917153..b9cecb3 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_RECURRING_INVOICE_DRAFT: configService.getOrThrow("SMS_PATTERN_RECURRING_INVOICE_DRAFT"), + SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow("SMS_PATTERN_SUBSCRIPTION_CANCELLED"), }; }, }; @@ -41,4 +43,6 @@ export interface ISmsConfigs { SMS_PATTERN_INVOICE_APPROVED: string; SMS_PATTERN_INVOICE_PAID: string; SMS_PATTERN_INVOICE_PAYMENT_REMINDER: string; + SMS_PATTERN_RECURRING_INVOICE_DRAFT: string; + SMS_PATTERN_SUBSCRIPTION_CANCELLED: string; } diff --git a/src/modules/auth/guards/auth.guard.ts b/src/modules/auth/guards/auth.guard.ts index 908f55c..dfb6463 100755 --- a/src/modules/auth/guards/auth.guard.ts +++ b/src/modules/auth/guards/auth.guard.ts @@ -1,7 +1,18 @@ -import { Injectable } from "@nestjs/common"; +import { ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; +import { Observable } from "rxjs"; import { JWT_STRATEGY_NAME } from "../../../common/constants"; +import { AuthMessage } from "../../../common/enums/message.enum"; @Injectable() -export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {} +export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) { + canActivate(context: ExecutionContext): boolean | Promise | Observable { + return super.canActivate(context); + } + + handleRequest(err: unknown, user: any, _info: unknown) { + if (err || !user) throw new UnauthorizedException(AuthMessage.TOKEN_EXPIRED_OR_INVALID); + return user; + } +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index 7d6772b..4d80890 100755 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -18,6 +18,5 @@ export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) { async validate(payload: ITokenPayload) { return { id: payload.id, permissions: payload.permissions, isAdmin: payload.isAdmin }; - // return { id: payload.id, roles: payload.roles, permissions: payload.permissions }; } } diff --git a/src/modules/invoices/DTO/create-invoice.dto.ts b/src/modules/invoices/DTO/create-invoice.dto.ts index 1019c75..c3a3621 100755 --- a/src/modules/invoices/DTO/create-invoice.dto.ts +++ b/src/modules/invoices/DTO/create-invoice.dto.ts @@ -1,8 +1,9 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { ArrayMinSize, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Length, ValidateNested } from "class-validator"; +import { ArrayMinSize, IsBoolean, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Length, ValidateNested } from "class-validator"; import { InvoiceMessage } from "../../../common/enums/message.enum"; +import { RecurringPeriodEnum } from "../enums/invoice-recurring-period.enum"; export class InvoiceItemDto { @IsNotEmpty({ message: InvoiceMessage.NAME_REQUIRED }) @@ -43,4 +44,19 @@ export class CreateInvoiceDto { @ArrayMinSize(1) @Type(() => InvoiceItemDto) items: InvoiceItemDto[]; + + @IsOptional() + @IsBoolean({ message: InvoiceMessage.IS_RECURRING_MUST_BE_A_BOOLEAN }) + @ApiProperty({ description: "Is this a recurring invoice", example: false, default: false }) + isRecurring?: boolean; + + @IsOptional() + @IsNotEmpty({ message: InvoiceMessage.RECURRING_PERIOD_REQUIRED }) + @IsEnum(RecurringPeriodEnum) + @ApiPropertyOptional({ + description: "Recurring period (daily, weekly, monthly, quarterly, yearly)", + example: RecurringPeriodEnum.QUARTERLY, + enum: RecurringPeriodEnum, + }) + recurringPeriod?: RecurringPeriodEnum; } diff --git a/src/modules/invoices/constants/index.ts b/src/modules/invoices/constants/index.ts index 426be20..d1d6d68 100644 --- a/src/modules/invoices/constants/index.ts +++ b/src/modules/invoices/constants/index.ts @@ -1,6 +1,7 @@ export const INVOICE = Object.freeze({ INVOICE_QUEUE_NAME: "invoice", INVOICE_REMINDER_JOB_NAME: "reminderInvoice", + INVOICE_RECURRING_JOB_NAME: "recurringInvoice", // INVOICE_REMINDER_JOB_PRIORITY: 1, // High priority INVOICE_REMINDER_JOB_ATTEMPTS: 3, // Retry 3 times @@ -8,6 +9,12 @@ export const INVOICE = Object.freeze({ INVOICE_REMINDER_JOB_TIMEOUT: 10000, // Timeout after 10 seconds INVOICE_REMINDER_REPEAT_DELAY: 86400000, // 1 day delay in milliseconds // + + INVOICE_RECURRING_JOB_PRIORITY: 1, // High priority + INVOICE_RECURRING_JOB_ATTEMPTS: 3, // Retry 3 times + INVOICE_RECURRING_JOB_BACKOFF: 1000, // Retry after 1 second + INVOICE_RECURRING_JOB_TIMEOUT: 10000, // Timeout after 10 seconds + // FINE_PERCENTAGE: 0.01, // 1% of the total price MAX_DAYS_AFTER_OVERDUE: 10, DUEDATE: 7, diff --git a/src/modules/invoices/enums/invoice-recurring-period.enum.ts b/src/modules/invoices/enums/invoice-recurring-period.enum.ts new file mode 100644 index 0000000..40594e0 --- /dev/null +++ b/src/modules/invoices/enums/invoice-recurring-period.enum.ts @@ -0,0 +1,7 @@ +export enum RecurringPeriodEnum { + WEEKLY = 1, + MONTHLY, + QUARTERLY, + BIANNUALLY, + ANNUALLY, +} diff --git a/src/modules/invoices/enums/invoice-status.enum.ts b/src/modules/invoices/enums/invoice-status.enum.ts index 43e54c1..ac5f017 100755 --- a/src/modules/invoices/enums/invoice-status.enum.ts +++ b/src/modules/invoices/enums/invoice-status.enum.ts @@ -1,4 +1,5 @@ export enum InvoiceStatus { + DRAFT = "DRAFT", PENDING = "PENDING", WAIT_PAYMENT = "WAIT_PAYMENT", PAID = "PAID", diff --git a/src/modules/invoices/providers/invoice-creation.service.ts b/src/modules/invoices/providers/invoice-creation.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 9ac8029..df784d1 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -24,6 +24,7 @@ import { INVOICE } from "../constants"; import { CreateInvoiceDto } from "../DTO/create-invoice.dto"; import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "../DTO/invoices-search-query.dto"; import { Invoice } from "../entities/invoice.entity"; +import { RecurringPeriodEnum } from "../enums/invoice-recurring-period.enum"; import { InvoiceStatus } from "../enums/invoice-status.enum"; import { InvoicesRepository } from "../repositories/invoices.repository"; @@ -62,8 +63,7 @@ export class InvoicesService { const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0)); const tax = totalPrice.mul(0.1); - // const dueDate = dayjs().add(INVOICE.DUEDATE, "day").toDate(); - const dueDate = dayjs().add(1, "minute").toDate(); + const dueDate = dayjs().add(INVOICE.DUEDATE, "day").toDate(); const invoice = queryRunner.manager.create(Invoice, { user: { id: createDto.userId }, @@ -91,17 +91,34 @@ export class InvoicesService { }, queryRunner, ); + + //add this to queue for billing reminder await this.invoiceQueue.add( INVOICE.INVOICE_REMINDER_JOB_NAME, { invoiceId: invoice.id }, { delay: dayjs(invoice.dueDate).subtract(INVOICE.DAYS_BEFORE_OVERDUE, "day").diff(dayjs()), - // delay: 1.5 * 60 * 1000, // 5 minutes in milliseconds attempts: INVOICE.INVOICE_REMINDER_JOB_ATTEMPTS, - backoff: INVOICE.INVOICE_REMINDER_JOB_BACKOFF, + backoff: { type: "exponential", delay: INVOICE.INVOICE_REMINDER_JOB_BACKOFF }, }, ); + //add to queue for recurring invoice + if (createDto.isRecurring && createDto.recurringPeriod) { + await this.invoiceQueue.add( + INVOICE.INVOICE_RECURRING_JOB_NAME, + { + invoiceId: invoice.id, + adminCreated: true, + }, + { + delay: await this.calculateRecurringDelay(createDto.recurringPeriod), + attempts: INVOICE.INVOICE_RECURRING_JOB_ATTEMPTS, + backoff: { type: "exponential", delay: INVOICE.INVOICE_REMINDER_JOB_BACKOFF }, + }, + ); + this.logger.log(`Scheduled recurring invoice for user ${createDto.userId} with interval ${createDto.recurringPeriod}`); + } await queryRunner.commitTransaction(); return { @@ -264,8 +281,7 @@ export class InvoicesService { { delay: dayjs(invoice.dueDate).subtract(INVOICE.DAYS_BEFORE_OVERDUE, "day").diff(dayjs()), attempts: INVOICE.INVOICE_REMINDER_JOB_ATTEMPTS, - backoff: INVOICE.INVOICE_REMINDER_JOB_BACKOFF, - repeat: { every: 300000 }, + backoff: { type: "exponential", delay: INVOICE.INVOICE_REMINDER_JOB_BACKOFF }, }, ); @@ -453,7 +469,33 @@ export class InvoicesService { return invoiceCount; } - //*********************************** */ + //********************************** */ + + private async calculateRecurringDelay(recurringPeriod: RecurringPeriodEnum) { + let delayMs: number; + switch (recurringPeriod) { + case RecurringPeriodEnum.WEEKLY: + delayMs = dayjs().add(1, "week").subtract(7, "days").diff(dayjs()); + break; + case RecurringPeriodEnum.MONTHLY: + delayMs = dayjs().add(1, "month").subtract(7, "days").diff(dayjs()); + break; + case RecurringPeriodEnum.QUARTERLY: + delayMs = dayjs().add(3, "month").subtract(7, "days").diff(dayjs()); + break; + case RecurringPeriodEnum.BIANNUALLY: + delayMs = dayjs().add(6, "month").subtract(7, "days").diff(dayjs()); + break; + case RecurringPeriodEnum.ANNUALLY: + delayMs = dayjs().add(1, "year").subtract(7, "days").diff(dayjs()); + break; + default: + delayMs = dayjs().add(1, "month").subtract(7, "days").diff(dayjs()); + } + + this.logger.log(`Calculated recurring delay: ${delayMs}ms for period: ${recurringPeriod}`); + return delayMs; + } // async applyDiscount(invoiceId: string, applyDiscountDto: ApplyDiscountDto, userId: string) { // const invoice = await this.invoiceRepository.findOneBy({ id: invoiceId, status: InvoiceStatus.PENDING }); diff --git a/src/modules/invoices/queue/invoice.processor.ts b/src/modules/invoices/queue/invoice.processor.ts index 68ab03c..1ba7a86 100644 --- a/src/modules/invoices/queue/invoice.processor.ts +++ b/src/modules/invoices/queue/invoice.processor.ts @@ -9,9 +9,12 @@ import { DataSource, QueryRunner } from "typeorm"; import { WorkerProcessor } from "../../../common/queues/worker.processor"; import { NotificationsService } from "../../notifications/providers/notifications.service"; import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum"; +import { User } from "../../users/entities/user.entity"; +import { RoleEnum } from "../../users/enums/role.enum"; import { INVOICE } from "../constants"; import { Invoice } from "../entities/invoice.entity"; import { InvoiceStatus } from "../enums/invoice-status.enum"; +import { InvoicesService } from "../providers/invoices.service"; @Processor(INVOICE.INVOICE_QUEUE_NAME) export class InvoiceProcessor extends WorkerProcessor { @@ -19,6 +22,7 @@ export class InvoiceProcessor extends WorkerProcessor { constructor( @InjectQueue(INVOICE.INVOICE_QUEUE_NAME) private readonly invoiceQueue: Queue, + private readonly invoicesService: InvoicesService, private readonly dataSource: DataSource, private readonly notificationService: NotificationsService, ) { @@ -30,21 +34,84 @@ export class InvoiceProcessor extends WorkerProcessor { switch (job.name) { case INVOICE.INVOICE_REMINDER_JOB_NAME: return this.sendBillInvoiceReminder(job, token); + case INVOICE.INVOICE_RECURRING_JOB_NAME: + return this.createRecurringInvoice(job, token); default: this.logger.error(`Unknown job name: ${job.name}`); return; } } //********************************** */ + private async createRecurringInvoice(job: Job<{ invoiceId: string; adminCreated: boolean }>, token?: string): Promise { + const { invoiceId, adminCreated } = job.data; + this.logger.log(`Creating recurring invoice for original invoice: ${invoiceId} ${token ? `with token: ${token}` : ""}`); + + const queryRunner = this.dataSource.createQueryRunner(); + + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const invoice = await this.fetchInvoice(invoiceId, queryRunner); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + adminCreated + ? await this.handleAdminCreatedInvoice(invoice, queryRunner) + : await this.handleSubscriptionInvoice(invoice, queryRunner); + + await queryRunner.commitTransaction(); + this.logger.log(`Recurring invoice for ${invoiceId} created successfully`); + } catch (error) { + this.logger.error(`Failed to create recurring invoice for ${invoiceId}:`, error); + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + //********************************** */ + + private async handleAdminCreatedInvoice(invoice: Invoice, queryRunner: QueryRunner): Promise { + this.logger.verbose(`Creating admin-initiated recurring invoice in draft status`); + + const newInvoice = queryRunner.manager.create(Invoice, { + user: invoice.user, + status: InvoiceStatus.DRAFT, + dueDate: dayjs().add(INVOICE.DUEDATE, "day").toDate(), + totalPrice: invoice.totalPrice, + tax: invoice.tax, + items: invoice.items, + }); + + await this.sendNotificationToAdmins(invoice, queryRunner); + + return queryRunner.manager.save(Invoice, newInvoice); + } + //********************************** */ + + private async handleSubscriptionInvoice(invoice: Invoice, queryRunner: QueryRunner): Promise { + 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}`); + } + + const { plan } = userSubscriptionPlan; + const dueDate = dayjs().add(INVOICE.DUEDATE, "day").toDate(); + + return this.invoicesService.createInvoiceForSubscription(invoice.user, plan, userSubscriptionPlan, dueDate, queryRunner); + } + //********************************** */ private async sendBillInvoiceReminder(job: Job<{ invoiceId: string }>, token?: string) { this.logger.log(`Sending bill invoice reminder: ${job.data.invoiceId} to user. ${token}`); const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); try { + await queryRunner.connect(); + await queryRunner.startTransaction(); const invoice = await this.fetchInvoice(job.data.invoiceId, queryRunner); if (invoice.status === InvoiceStatus.PAID) { @@ -78,7 +145,7 @@ export class InvoiceProcessor extends WorkerProcessor { private async fetchInvoice(invoiceId: string, queryRunner: QueryRunner) { const invoice = await queryRunner.manager.findOne(Invoice, { where: { id: invoiceId }, - relations: { user: true, items: { subscriptionPlan: true } }, + relations: { user: true, items: { subscriptionPlan: { plan: true } } }, }); if (!invoice) throw new Error(`Invoice not found: ${invoiceId}`); @@ -104,15 +171,16 @@ export class InvoiceProcessor extends WorkerProcessor { await queryRunner.manager.save(userSubscription); - // await this.notificationService.createSubscriptionPlanCancelledNotification( - // invoice.user.id, - // { - // userPhone: invoice.user.phone, - // userEmail: invoice.user.email, - // subscriptionPlanId: invoice.items[0].subscriptionPlan.id, - // }, - // queryRunner, - // ); + await this.notificationService.createBlockServiceNotification( + invoice.user.id, + { + userPhone: invoice.user.phone, + userEmail: invoice.user.email, + planName: userSubscription.plan.name, + invoiceId: invoice.numericId.toString(), + }, + queryRunner, + ); } this.logger.log(`Invoice ${invoice.id} has been cancelled as it is more than 10 days overdue.`); } @@ -172,4 +240,28 @@ export class InvoiceProcessor extends WorkerProcessor { } //********************************** */ + + private async fetchAdmin(queryRunner: QueryRunner) { + return queryRunner.manager.find(User, { where: { roles: { name: RoleEnum.SUPER_ADMIN } }, relations: { roles: true } }); + } + //********************************** */ + private async sendNotificationToAdmins(invoice: Invoice, queryRunner: QueryRunner): Promise { + this.logger.log(`Sending notification to admins for invoice ${invoice.id}`); + + const admins = await this.fetchAdmin(queryRunner); + + const notificationPayload = { + userPhone: invoice.user.phone, + userEmail: invoice.user.email, + invoiceId: invoice.numericId.toString(), + price: new Decimal(invoice.totalPrice).toNumber(), + dueDate: invoice.dueDate, + createDate: invoice.createdAt, + items: invoice.items.join(", "), + }; + + await Promise.all( + admins.map((admin) => this.notificationService.createRucurringInvoiceNotification(admin.id, notificationPayload, queryRunner)), + ); + } } diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts index 25cf4fa..3f6f35a 100644 --- a/src/modules/notifications/interfaces/ISendNotificationData.ts +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -39,3 +39,8 @@ export interface IAnnouncementNotificationData extends IBaseNotificationData { description: string; date: Date; } + +export interface ISubscriptionNotificationData extends IBaseNotificationData { + planName: string; + invoiceId: string; +} diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index 08f899a..0b4b83c 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -14,6 +14,7 @@ import { IAnnouncementNotificationData, IBaseNotificationData, IInvoiceNotificationData, + ISubscriptionNotificationData, ITicketNotificationData, IWalletNotificationData, } from "../interfaces/ISendNotificationData"; @@ -316,6 +317,34 @@ export class NotificationsService { // //************************ */ + async createRucurringInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.RECURRING_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.RECURRING_INVOICE, recipientId); + if (isActive) { + await this.smsService.sendRecurringInvoiceDraftSms(data.userPhone, data.invoiceId, data.price); + //send email too + } + return this.createNotification( + { title: NotificationMessage.RECURRING_INVOICE, type: NotifType.RECURRING_INVOICE, message, recipientId }, + queryRunner, + ); + } + + async createBlockServiceNotification(recipientId: string, data: ISubscriptionNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.BLOCK_SERVICE_MESSAGE.replace("[serviceName]", data.planName); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BLOCK_SERVICE, recipientId); + if (isActive) { + await this.smsService.sendBlockServiceSms(data.userPhone, data.invoiceId, data.planName); + //send email too + } + return this.createNotification( + { title: NotificationMessage.BLOCK_SERVICE, type: NotifType.BLOCK_SERVICE, message, recipientId }, + queryRunner, + ); + } + // async createServiceNotification(recipientId: string, sendNotifData: ISendNotificationData) { // const message = NotificationMessage.CREATE_SERVICE_MESSAGE.replace("[serviceName]", sendNotifData.serviceName); // //sent notification based on the userSettings @@ -341,15 +370,4 @@ export class NotificationsService { // } // //************************ */ - - // async createBlockServiceNotification(recipientId: string, sendNotifData: ISendNotificationData) { - // const message = NotificationMessage.BLOCK_SERVICE_MESSAGE.replace("[serviceName]", sendNotifData.serviceName); - // //sent notification based on the userSettings - // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BLOCK_SERVICE, recipientId); - // if (isActive) { - // await this.smsService.sendBlockServiceSms(sendNotifData.userPhone, sendNotifData.serviceName); - // //send email too - // } - // return this.createNotification({ title: NotificationMessage.BLOCK_SERVICE, message, recipientId }); - // } } diff --git a/src/modules/settings/enums/notif-settings.enum.ts b/src/modules/settings/enums/notif-settings.enum.ts index cb462c1..de4b312 100755 --- a/src/modules/settings/enums/notif-settings.enum.ts +++ b/src/modules/settings/enums/notif-settings.enum.ts @@ -32,6 +32,7 @@ export enum NotifType { CREATE_TICKET = "CREATE_TICKET", // ASSIGN_TICKET = "ASSIGN_TICKET", + RECURRING_INVOICE = "RECURRING_INVOICE", } export const NotifDescriptions: Record = { @@ -48,6 +49,7 @@ export const NotifDescriptions: Record(`${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 recurring invoice draft sms", err); + throw new InternalServerErrorException("error in sending recurring invoice draft sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + throw new InternalServerErrorException("error in sending sms"); + } + } + + // //************************************************* */ + + async sendBlockServiceSms(mobile: string, invoiceId: string, planName: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "invoiceId", value: invoiceId }, + { name: "planName", value: planName }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_SUBSCRIPTION_CANCELLED, + }; + + 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 subscription cancelled sms", err); + throw new InternalServerErrorException("error in sending subscription cancelled sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + throw new InternalServerErrorException("error in sending sms"); + } + } // //************************************************* */