From a4cae0c273773691be0d51c82d54933c7cadf264 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Fri, 7 Mar 2025 16:09:41 +0330 Subject: [PATCH] chore: add new notification for invoice approve and paid invoice --- src/common/enums/message.enum.ts | 38 +++---- src/configs/sms.config.ts | 6 ++ .../invoices/providers/invoices.service.ts | 77 ++++++++++++--- .../interfaces/ISendNotificationData.ts | 1 + .../providers/notifications.service.ts | 61 ++++++++---- .../settings/enums/notif-settings.enum.ts | 2 + src/modules/utils/providers/sms.service.ts | 99 +++++++++++++++++-- 7 files changed, 223 insertions(+), 61 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index aa02638..46b5df1 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -348,8 +348,8 @@ export const enum NotificationMessage { LOGIN = "لاگین", LOGIN_MESSAGE = "یک لاگین در تاریخ [loginDate] به حساب کاربری شما صورت گرفت", NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی غیرمجاز است", - INVOICE_CREATION = "صدور فاکتور", - INVOICE_CREATION_MESSAGE = "یک فاکتور به شماره [id] جدید برای شما صادر شده است", + INVOICE_CREATION = "صدور صورت حساب", + INVOICE_CREATION_MESSAGE = "یک صورت حساب به شماره [id] جدید برای شما صادر شده است", ANNOUNCEMENT = "اطلاعیه", ANNOUNCEMENT_MESSAGE = "یک اطلاعیه جدید برای شما ارسال شده است", WALLET_CHARGE = "شارژ کیف پول", @@ -357,9 +357,11 @@ export const enum NotificationMessage { WALLET_DEDUCTION = "کسر از کیف پول", WALLET_DEDUCTION_MESSAGE = "مبلغ [amount] تومان از کیف پول شما کسر شد", BILL_INVOICE_REMINDER = "یادآوری پرداخت", - BILL_INVOICE_REMINDER_MESSAGE = "فاکتور شماره [invoiceNumber] هنوز پرداخت نشده است", - BILL_INVOICE = "فاکتور پرداختی", - BILL_INVOICE_MESSAGE = "فاکتور شماره [invoiceNumber] با موفقیت پرداخت شد", + BILL_INVOICE_REMINDER_MESSAGE = "صورت حساب شماره [invoiceNumber] هنوز پرداخت نشده است", + BILL_INVOICE = "صورت حساب پرداختی", + BILL_INVOICE_MESSAGE = "صورت حساب شماره [invoiceNumber] با موفقیت پرداخت شد", + APPROVED_INVOICE_MESSAGE = "صورت حساب شماره [invoiceNumber] تایید شد", + APPROVED_INVOICE = "تایید صورت حساب", CREATE_SERVICE = "ایجاد سرویس", CREATE_SERVICE_MESSAGE = "سرویس جدیدی با عنوان [serviceName] ایجاد شد", UNBLOCK_SERVICE = "فعال‌سازی سرویس", @@ -426,19 +428,19 @@ export const enum InvoiceMessage { NAME_LENGTH = "نام محصول باید بین ۳ تا ۱۰۰ کاراکتر باشد", USER_ID_REQUIRED = "شناسه کاربر مورد نیاز است", USER_ID_SHOULD_BE_A_UUID = "شناسه کاربر باید یک UUID معتبر باشد", - CREATED = "فاکتور با موفقیت ایجاد شد", + CREATED = "صورت حساب با موفقیت ایجاد شد", SEARCH_QUERY_MUST_BE_A_STRING = "رشته جستجو باید یک رشته باشد", - NOT_FOUND_BY_ID = "فاکتوری با این شناسه یافت نشد", - APPROVED = "فاکتور با موفقیت تایید شد", - ALREADY_APPROVED = "فاکتور قبلا تایید شده است", - INVOICE_CAN_NOT_APPROVED = "فاکتور قابل تایید نیست", - INVOICE_ALREADY_PAID = "فاکتور قبلا پرداخت شده است", - INVOICE_IS_OVERDUE = "فاکتور منقضی شده است", + NOT_FOUND_BY_ID = "صورت حسابی با این شناسه یافت نشد", + APPROVED = "صورت حساب با موفقیت تایید شد", + ALREADY_APPROVED = "صورت حساب قبلا تایید شده است", + INVOICE_CAN_NOT_APPROVED = "صورت حساب قابل تایید نیست", + INVOICE_ALREADY_PAID = "صورت حساب قبلا پرداخت شده است", + INVOICE_IS_OVERDUE = "صورت حساب منقضی شده است", INVOICE_PAID = "صورت حساب با موفقیت پرداخت شد", DISCOUNT_APPLIED = "تخفیف با موفقیت اعمال شد", DISCOUNT_CANCELLED = "تخفیف با موفقیت لغو شد", - INVOICE_IS_REJECTED = "فاکتور قبلا رد شده است", - INVOICE_CAN_NOT_PAID = "فاکتور قابل پرداخت نیست", + INVOICE_IS_REJECTED = "صورت حساب قبلا رد شده است", + INVOICE_CAN_NOT_PAID = "صورت حساب قابل پرداخت نیست", INVALID_DATE = "تاریخ وارد شده معتبر نیست", } @@ -459,7 +461,7 @@ export const enum LearningMessage { } export const enum DiscountMessage { - CAN_NOT_APPLICABLE = "این کد تخفیف برای این فاکتور قابل اعمال نیست", + CAN_NOT_APPLICABLE = "این کد تخفیف برای این صورت حساب قابل اعمال نیست", TYPE_REQUIRED = "وارد کردن نوع تخفیف الزامی است", TYPE_INVALID = "نوع تخفیف نامعتبر است", CALCULATION_TYPE_REQUIRED = "وارد کردن نوع محاسبه تخفیف الزامی است", @@ -482,12 +484,12 @@ export const enum DiscountMessage { USER_INVALID = "لیست کاربرا باید آرایه‌ای از ایدی باشد", NOT_FOUND = "کد تخفیف با این مشخصات یافت نشد", CODE_EXIST = "کد تخفیف قبلا ایجاد شده است", - INVOICE_ID_REQUIRED = "شناسه فاکتور نمی‌تواند خالی باشد", - INVOICE_ID_STRING = "شناسه فاکتور باید یک رشته باشد", + INVOICE_ID_REQUIRED = "شناسه صورت حساب نمی‌تواند خالی باشد", + INVOICE_ID_STRING = "شناسه صورت حساب باید یک رشته باشد", DISCOUNT_CODE_REQUIRED = "کد تخفیف نمی‌تواند خالی باشد", DISCOUNT_CODE_STRING = "کد تخفیف باید یک رشته باشد", DISCOUNT_NOT_FOUND = "کد تخفیف یافت نشد یا غیرفعال است", - DISCOUNT_NOT_APPLICABLE = "این کد تخفیف برای هیچ یک از پلن‌های اشتراک در فاکتور قابل اعمال نیست", + DISCOUNT_NOT_APPLICABLE = "این کد تخفیف برای هیچ یک از پلن‌های اشتراک در صورت حساب قابل اعمال نیست", ALREADY_USED = "این کد تخفیف قبلا استفاده شده است", } diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index 640111a..3917153 100755 --- a/src/configs/sms.config.ts +++ b/src/configs/sms.config.ts @@ -17,6 +17,9 @@ export function smsConfigs() { SMS_PATTERN_TICKET_CREATED: configService.getOrThrow("SMS_PATTERN_TICKET_CREATED"), SMS_PATTERN_TICKET_ANSWERED: configService.getOrThrow("SMS_PATTERN_WALLET_DEDUCTION"), SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"), + 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"), }; }, }; @@ -35,4 +38,7 @@ export interface ISmsConfigs { SMS_PATTERN_TICKET_CREATED: string; SMS_PATTERN_TICKET_ANSWERED: string; SMS_PATTERN_TICKET_ASSIGNED_ADMIN: string; + SMS_PATTERN_INVOICE_APPROVED: string; + SMS_PATTERN_INVOICE_PAID: string; + SMS_PATTERN_INVOICE_PAYMENT_REMINDER: string; } diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index ec1b377..3b54ecd 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -83,6 +83,7 @@ export class InvoicesService { userPhone: user.phone, userEmail: user.email, items: invoiceItems.map((item) => item.name).join(", "), + paidAt: invoice.paidAt, }, queryRunner, ); @@ -142,30 +143,58 @@ export class InvoicesService { //********************************** */ async approveInvoiceByUser(invoiceId: string, userId: string, verifyOtpDto: VerifyOtpWithUserId) { - const { code } = verifyOtpDto; + const queryRunner = this.dataSource.createQueryRunner(); - const invoice = await this.invoiceRepository.findOneBy({ id: invoiceId, user: { id: userId } }); - if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + const { code } = verifyOtpDto; - const { user } = await this.usersService.findOneById(userId); + const invoice = await queryRunner.manager.findOne(Invoice, { + where: { id: invoiceId, user: { id: userId } }, + relations: { items: true }, + }); + if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); - const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY"); - if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); + const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); - if (invoice.status === InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.ALREADY_APPROVED); - if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID); - if (dayjs().isAfter(invoice.dueDate)) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE); + const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY"); + if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); - if (invoice.status !== InvoiceStatus.PENDING) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_APPROVED); + if (invoice.status === InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.ALREADY_APPROVED); + if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID); + if (dayjs().isAfter(invoice.dueDate)) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE); + if (invoice.status !== InvoiceStatus.PENDING) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_APPROVED); - invoice.status = InvoiceStatus.WAIT_PAYMENT; + invoice.status = InvoiceStatus.WAIT_PAYMENT; + await queryRunner.manager.save(Invoice, invoice); - await this.invoiceRepository.save(invoice); + await this.notificationsService.createApprovedInvoiceNotification( + userId, + { + invoiceId: invoice.numericId.toString(), + dueDate: invoice.dueDate, + createDate: invoice.createdAt, + price: new Decimal(invoice.totalPrice).toNumber(), + userPhone: user.phone, + userEmail: user.email, + items: invoice.items.map((item) => item.name).join(", "), + }, + queryRunner, + ); - return { - message: InvoiceMessage.APPROVED, - invoice, - }; + await queryRunner.commitTransaction(); + + return { + message: InvoiceMessage.APPROVED, + invoice, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } ///********************************** */ async createInvoiceForSubscription(user: User, plan: SubscriptionPlan, userSub: UserSubscription, dueDate: Date, qryRnr: QueryRunner) { @@ -319,6 +348,22 @@ export class InvoicesService { invoice.paidAt = dayjs().toDate(); await queryRunner.manager.save(Invoice, invoice); + + await this.notificationsService.createBillInvoiceNotification( + userId, + { + invoiceId: invoice.numericId.toString(), + dueDate: invoice.dueDate, + createDate: invoice.createdAt, + price: new Decimal(invoice.totalPrice).toNumber(), + userPhone: user.phone, + userEmail: user.email, + items: invoice.items.map((item) => item.name).join(", "), + paidAt: invoice.paidAt, + }, + queryRunner, + ); + await queryRunner.commitTransaction(); return { message: InvoiceMessage.INVOICE_PAID, diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts index 832c7c2..25cf4fa 100644 --- a/src/modules/notifications/interfaces/ISendNotificationData.ts +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -9,6 +9,7 @@ export interface IInvoiceNotificationData extends IBaseNotificationData { dueDate: Date; createDate: Date; invoiceId: string; + paidAt?: Date; } export interface IServiceNotificationData extends IBaseNotificationData { diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index aff80d7..08f899a 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -269,29 +269,50 @@ export class NotificationsService { //************************ */ - // async createBillInvoiceReminderNotification(recipientId: string, sendNotifData: ISendNotificationData) { - // const message = NotificationMessage.BILL_INVOICE_REMINDER_MESSAGE; - // //sent notification based on the userSettings - // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BILL_INVOICE_REMINDER, recipientId); - // if (isActive) { - // await this.smsService.sendBillInvoiceReminderSms(sendNotifData.userPhone, sendNotifData.dueDate, sendNotifData.invoiceId); - // //send email too - // } - // return this.createNotification({ title: NotificationMessage.BILL_INVOICE_REMINDER, message, recipientId }); - // } + async createBillInvoiceReminderNotification(recipientId: string, data: IInvoiceNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.BILL_INVOICE_REMINDER_MESSAGE.replace("[invoiceNumber]", data.invoiceId); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BILL_INVOICE_REMINDER, recipientId); + if (isActive) { + await this.smsService.sendInvoicePaymentReminderSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate)); + //send email too + } + return this.createNotification( + { title: NotificationMessage.BILL_INVOICE_REMINDER, type: NotifType.BILL_INVOICE_REMINDER, message, recipientId }, + queryRunner, + ); + } //************************ */ - // async createBillInvoiceNotification(recipientId: string, sendNotifData: ISendNotificationData) { - // const message = NotificationMessage.BILL_INVOICE_MESSAGE; - // //sent notification based on the userSettings - // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BILL_INVOICE, recipientId); - // if (isActive) { - // await this.smsService.sendBillInvoiceSms(sendNotifData.userPhone, sendNotifData.amount, sendNotifData.invoiceId); - // //send email too - // } - // return this.createNotification({ title: NotificationMessage.BILL_INVOICE, message, recipientId }); - // } + async createBillInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.BILL_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BILL_INVOICE, recipientId); + if (isActive) { + await this.smsService.sendInvoicePaidSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.paidAt!)); + //send email too + } + return this.createNotification( + { title: NotificationMessage.BILL_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, + queryRunner, + ); + } + //************************ */ + + async createApprovedInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.APPROVED_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.APPROVE_INVOICE, recipientId); + if (isActive) { + await this.smsService.sendInvoiceApprovedSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate)); + //send email too + } + return this.createNotification( + { title: NotificationMessage.APPROVED_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, + queryRunner, + ); + } // //************************ */ diff --git a/src/modules/settings/enums/notif-settings.enum.ts b/src/modules/settings/enums/notif-settings.enum.ts index 5f5757b..cb462c1 100755 --- a/src/modules/settings/enums/notif-settings.enum.ts +++ b/src/modules/settings/enums/notif-settings.enum.ts @@ -19,6 +19,7 @@ export enum NotifType { //Invoice BILL_INVOICE_REMINDER = "BILL_INVOICE_REMINDER", BILL_INVOICE = "BILL_INVOICE", + APPROVE_INVOICE = "APPROVE_INVOICE", CREATE_INVOICE = "CREATE_INVOICE", // Service category @@ -46,6 +47,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 invoice approved sms", err); + throw new InternalServerErrorException("error in sending invoice approved sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + // throw new InternalServerErrorException("error in sending sms"); + } + } + //************************************************* */ + async sendInvoicePaidSms(mobile: string, invoiceId: string, price: number, date: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "invoiceId", value: invoiceId }, + { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, + { name: "date", value: date }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_PAID, + }; + + 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 paid sms", err); + throw new InternalServerErrorException("error in sending invoice paid sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + // throw new InternalServerErrorException("error in sending sms"); + } + } //************************************************* */ - // sendBillInvoiceReminderSms(userPhone: string, dueDate: any, invoiceId: any) { - // throw new Error("Method not implemented."); - // } - // //************************************************* */ + async sendInvoicePaymentReminderSms(mobile: string, invoiceId: string, price: number, dueDate: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "invoiceId", value: invoiceId }, + { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, + { name: "dueDate", value: dueDate }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_PAYMENT_REMINDER, + }; + + 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 payment reminder sms", err); + throw new InternalServerErrorException("error in sending invoice payment reminder sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + // throw new InternalServerErrorException("error in sending sms"); + } + } - // sendBillInvoiceSms(userPhone: string, amount: any, invoiceId: any) { - // throw new Error("Method not implemented."); - // } // //************************************************* */ // sendCreateServiceSms(userPhone: string, serviceName: any) {