From 3d24cee972344b0537f1a188f33606ef5cba3cb1 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sun, 4 May 2025 12:27:21 +0330 Subject: [PATCH] chore: add new method for sms service and notification service --- src/app.module.ts | 4 +- src/common/enums/message.enum.ts | 13 ++ src/configs/sms.config.ts | 14 ++ .../interfaces/ISendNotificationData.ts | 32 +++ .../providers/notifications.service.ts | 118 +++++++++++ .../settings/enums/notif-settings.enum.ts | 20 +- src/modules/utils/interfaces/ISms.ts | 5 +- src/modules/utils/providers/email.service.ts | 19 ++ src/modules/utils/providers/sms.service.ts | 188 +++++++++++++++++- src/templates/email/admin-notification.hbs | 46 +++++ 10 files changed, 451 insertions(+), 8 deletions(-) create mode 100644 src/templates/email/admin-notification.hbs diff --git a/src/app.module.ts b/src/app.module.ts index 443884e..bb745ff 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,13 +7,11 @@ 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 { 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 { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AddressModule } from "./modules/address/address.module"; @@ -43,7 +41,7 @@ import { MonitoringModule } from "./monitoring/monitoring.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 dd63f8a..ff98a10 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -414,6 +414,19 @@ export const enum NotificationMessage { PAYMENT_REMINDER_MESSAGE = "لطفا مبلغ [amount] تومان را پرداخت کنید", PAYMENT_CANCELLATION = "لغو پرداخت", PAYMENT_CANCELLATION_MESSAGE = "پرداخت مبلغ [amount] تومان لغو شد", + // Admin notification messages + NEW_BLOG_COMMENT = "نظر جدید در بلاگ", + NEW_BLOG_COMMENT_MESSAGE = "یک نظر جدید در بلاگ [blogTitle] ثبت شده است", + NEW_SERVICE_REVIEW = "نقد و بررسی جدید سرویس", + NEW_SERVICE_REVIEW_MESSAGE = "یک نقد و بررسی جدید برای سرویس [serviceName] ثبت شده است", + NEW_CUSTOMER = "مشتری جدید", + NEW_CUSTOMER_MESSAGE = "یک مشتری جدید با نام [customerName] ثبت نام کرده است", + NEW_SUBSCRIPTION = "اشتراک جدید", + NEW_SUBSCRIPTION_MESSAGE = "یک اشتراک جدید برای پلن [serviceName] برای [fullName] ثبت شد", + NEW_TICKET = "تیکت جدید", + NEW_TICKET_MESSAGE = "یک تیکت جدید با موضوع [ticketSubject] ثبت شده است", + NEW_CRITICISM = "انتقاد جدید", + NEW_CRITICISM_MESSAGE = "یک انتقاد جدید با موضوع [title] ثبت شده است", } export const enum SubscriptionMessage { diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index d96ab42..802ba0f 100755 --- a/src/configs/sms.config.ts +++ b/src/configs/sms.config.ts @@ -25,6 +25,13 @@ export function smsConfigs() { SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow("SMS_PATTERN_INVOICE_OVERDUE"), SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow("SMS_PATTERN_PAYMENT_REMINDER"), SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow("SMS_PATTERN_PAYMENT_CANCELLATION"), + // + SMS_PATTERN_BLOG_NEW_COMMENT: configService.getOrThrow("SMS_PATTERN_BLOG_NEW_COMMENT"), + SMS_PATTERN_NEW_SERVICE_REVIEW: configService.getOrThrow("SMS_PATTERN_NEW_SERVICE_REVIEW"), + SMS_PATTERN_NEW_CUSTOMER: configService.getOrThrow("SMS_PATTERN_NEW_CUSTOMER"), + SMS_PATTERN_NEW_SUBSCRIPTION: configService.getOrThrow("SMS_PATTERN_NEW_SUBSCRIPTION"), + SMS_PATTERN_NEW_TICKET_GLOBAL: configService.getOrThrow("SMS_PATTERN_NEW_TICKET_GLOBAL"), + SMS_PATTERN_NEW_CRITICISM: configService.getOrThrow("SMS_PATTERN_NEW_CRITICISM"), }; }, }; @@ -51,4 +58,11 @@ export interface ISmsConfigs { SMS_PATTERN_INVOICE_OVERDUE: string; SMS_PATTERN_PAYMENT_REMINDER: string; SMS_PATTERN_PAYMENT_CANCELLATION: string; + // + SMS_PATTERN_BLOG_NEW_COMMENT: string; + SMS_PATTERN_NEW_SERVICE_REVIEW: string; + SMS_PATTERN_NEW_CUSTOMER: string; + SMS_PATTERN_NEW_SUBSCRIPTION: string; + SMS_PATTERN_NEW_TICKET_GLOBAL: string; + SMS_PATTERN_NEW_CRITICISM: string; } diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts index ebce510..26ce2ec 100755 --- a/src/modules/notifications/interfaces/ISendNotificationData.ts +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -50,3 +50,35 @@ export interface IPaymentNotificationData extends IBaseNotificationData { amount: number; paymentId: string; } + +export interface IBlogCommentNotificationData extends IBaseNotificationData { + blogTitle: string; + fullName: string; +} + +export interface IServiceReviewNotificationData extends IBaseNotificationData { + serviceName: string; + fullName: string; +} + +export interface INewCustomerNotificationData extends IBaseNotificationData { + fullName: string; + mobile: string; +} + +export interface INewSubscriptionNotificationData extends IBaseNotificationData { + serviceName: string; + fullName: string; + date: Date; +} + +export interface INewTicketNotificationData extends IBaseNotificationData { + ticketId: string; + ticketSubject: string; + fullName: string; +} + +export interface INewCriticismNotificationData extends IBaseNotificationData { + title: string; + fullName: string; +} diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index eb12a7d..a56e2f9 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -13,8 +13,14 @@ import { Notification } from "../entities/notification.entity"; import { IAnnouncementNotificationData, IBaseNotificationData, + IBlogCommentNotificationData, IInvoiceNotificationData, + INewCriticismNotificationData, + INewCustomerNotificationData, + INewSubscriptionNotificationData, + INewTicketNotificationData, IPaymentNotificationData, + IServiceReviewNotificationData, ISubscriptionNotificationData, ITicketNotificationData, IWalletNotificationData, @@ -441,4 +447,116 @@ export class NotificationsService { queryRunner, ); } + + //-------------------------------------- + + // Admin notification methods + + //-------------------------------------- + async createNewBlogCommentNotification(recipientId: string, data: IBlogCommentNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.NEW_BLOG_COMMENT_MESSAGE.replace("[blogTitle]", data.blogTitle); + + await this.smsService.sendBlogNewCommentSms(data.userPhone, data.blogTitle, data.fullName); + if (data.userEmail) { + await this.emailService.sendAdminNotificationEmail({ + userEmail: data.userEmail, + title: NotificationMessage.NEW_BLOG_COMMENT, + message, + }); + } + return this.createNotification( + { title: NotificationMessage.NEW_BLOG_COMMENT, type: NotifType.NEW_BLOG_COMMENT, message, recipientId }, + queryRunner, + ); + } + //************************************************* */ + async createNewServiceReviewNotification(recipientId: string, data: IServiceReviewNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.NEW_SERVICE_REVIEW_MESSAGE.replace("[serviceName]", data.serviceName); + + await this.smsService.sendNewServiceReviewSms(data.userPhone, data.serviceName, data.fullName); + if (data.userEmail) { + await this.emailService.sendAdminNotificationEmail({ + userEmail: data.userEmail, + title: NotificationMessage.NEW_SERVICE_REVIEW, + message, + }); + } + + return this.createNotification( + { title: NotificationMessage.NEW_SERVICE_REVIEW, type: NotifType.NEW_SERVICE_REVIEW, message, recipientId }, + queryRunner, + ); + } + //************************************************* */ + + async createNewCustomerNotification(recipientId: string, data: INewCustomerNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.NEW_CUSTOMER_MESSAGE.replace("[fullName]", data.fullName); + + await this.smsService.sendNewCustomerSms(data.userPhone, data.fullName); + if (data.userEmail) { + await this.emailService.sendAdminNotificationEmail({ + userEmail: data.userEmail, + title: NotificationMessage.NEW_CUSTOMER, + message, + }); + } + return this.createNotification( + { title: NotificationMessage.NEW_CUSTOMER, type: NotifType.NEW_CUSTOMER, message, recipientId }, + queryRunner, + ); + } + //************************************************* */ + + async createNewSubscriptionNotification(recipientId: string, data: INewSubscriptionNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.NEW_SUBSCRIPTION_MESSAGE.replace("[serviceName]", data.serviceName); + + await this.smsService.sendNewSubscriptionSms(data.userPhone, data.serviceName, data.fullName, dateFormat(data.date)); + if (data.userEmail) { + await this.emailService.sendAdminNotificationEmail({ + userEmail: data.userEmail, + title: NotificationMessage.NEW_SUBSCRIPTION, + message, + }); + } + return this.createNotification( + { title: NotificationMessage.NEW_SUBSCRIPTION, type: NotifType.NEW_SUBSCRIPTION, message, recipientId }, + queryRunner, + ); + } + //************************************************* */ + + async createNewTicketNotification(recipientId: string, data: INewTicketNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.NEW_TICKET_MESSAGE.replace("[ticketSubject]", data.ticketSubject); + + await this.smsService.sendGlobalNewTicketSms(data.userPhone, data.ticketId, data.ticketSubject, data.fullName); + if (data.userEmail) { + await this.emailService.sendAdminNotificationEmail({ + userEmail: data.userEmail, + title: NotificationMessage.NEW_TICKET, + message, + }); + } + return this.createNotification( + { title: NotificationMessage.NEW_TICKET, type: NotifType.NEW_TICKET, message, recipientId }, + queryRunner, + ); + } + //************************************************* */ + + async createNewCriticismNotification(recipientId: string, data: INewCriticismNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.NEW_CRITICISM_MESSAGE.replace("[title]", data.title); + + await this.smsService.sendNewCriticismSms(data.userPhone, data.title, data.fullName); + if (data.userEmail) { + await this.emailService.sendAdminNotificationEmail({ + userEmail: data.userEmail, + title: NotificationMessage.NEW_CRITICISM, + message, + }); + } + return this.createNotification( + { title: NotificationMessage.NEW_CRITICISM, type: NotifType.NEW_CRITICISM, message, recipientId }, + queryRunner, + ); + } } diff --git a/src/modules/settings/enums/notif-settings.enum.ts b/src/modules/settings/enums/notif-settings.enum.ts index d9b3433..6ed9856 100755 --- a/src/modules/settings/enums/notif-settings.enum.ts +++ b/src/modules/settings/enums/notif-settings.enum.ts @@ -4,7 +4,7 @@ export enum NotifCategory { INVOICE = "INVOICE", ACCOUNT = "ACCOUNT", FINANCE = "FINANCE", //WALLET AND ... - // ANNOUNCEMENT = "ANNOUNCEMENT", + ADMIN = "ADMIN", // New category for admin notifications } export enum NotifType { @@ -36,6 +36,14 @@ export enum NotifType { RECURRING_INVOICE = "RECURRING_INVOICE", PAYMENT_REMINDER = "PAYMENT_REMINDER", PAYMENT_CANCELLATION = "PAYMENT_CANCELLATION", + + // // Admin category + NEW_BLOG_COMMENT = "NEW_BLOG_COMMENT", + NEW_SERVICE_REVIEW = "NEW_SERVICE_REVIEW", + NEW_CUSTOMER = "NEW_CUSTOMER", + NEW_SUBSCRIPTION = "NEW_SUBSCRIPTION", + NEW_TICKET = "NEW_TICKET", + NEW_CRITICISM = "NEW_CRITICISM", } export const NotifDescriptions: Record = { @@ -65,6 +73,14 @@ 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 blog new comment sms", err); + throw new InternalServerErrorException("error in sending blog new comment sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + // throw new InternalServerErrorException("error in sending sms"); + } + } + //************************************************* */ + + async sendNewServiceReviewSms(mobile: string, serviceName: string, fullName: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "serviceName", value: serviceName }, + { name: "fullName", value: fullName }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_NEW_SERVICE_REVIEW, + }; + + 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 new service review sms", err); + throw new InternalServerErrorException("error in sending new service review sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } + //************************************************* */ + + async sendNewCustomerSms(mobile: string, fullName: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "fullName", value: fullName }, + { name: "mobile", value: mobile }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_NEW_CUSTOMER, + }; + + 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 new customer sms", err); + throw new InternalServerErrorException("error in sending new customer sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } + //************************************************* */ + + async sendNewSubscriptionSms(mobile: string, serviceName: string, fullName: string, date: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "serviceName", value: serviceName }, + { name: "fullName", value: fullName }, + { name: "date", value: date }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_NEW_SUBSCRIPTION, + }; + + 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 new subscription sms", err); + throw new InternalServerErrorException("error in sending new subscription sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } + //************************************************* */ + + async sendGlobalNewTicketSms(mobile: string, ticketId: string, subject: string, fullName: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "ticketId", value: ticketId }, + { name: "subject", value: subject }, + { name: "fullName", value: fullName }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_NEW_TICKET_GLOBAL, + }; + + 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 global new ticket sms", err); + throw new InternalServerErrorException("error in sending global new ticket sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } + //************************************************* */ + + async sendNewCriticismSms(mobile: string, title: string, fullName: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "title", value: title }, + { name: "fullName", value: fullName }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_NEW_CRITICISM, + }; + + 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 new criticism sms", err); + throw new InternalServerErrorException("error in sending new criticism sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } + + //************************************************* */ } diff --git a/src/templates/email/admin-notification.hbs b/src/templates/email/admin-notification.hbs new file mode 100644 index 0000000..45afd47 --- /dev/null +++ b/src/templates/email/admin-notification.hbs @@ -0,0 +1,46 @@ + + + + + {{title}} + + + +
+

{{title}}

+
+
+

{{message}}

+
+ + + \ No newline at end of file