chore: add new method for sms service and notification service

This commit is contained in:
mahyargdz
2025-05-04 12:27:21 +03:30
parent 8fe1d398a6
commit 3d24cee972
10 changed files with 451 additions and 8 deletions
+1 -3
View File
@@ -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()),
+13
View File
@@ -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 {
+14
View File
@@ -25,6 +25,13 @@ export function smsConfigs() {
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_CANCELLATION"),
//
SMS_PATTERN_BLOG_NEW_COMMENT: configService.getOrThrow<string>("SMS_PATTERN_BLOG_NEW_COMMENT"),
SMS_PATTERN_NEW_SERVICE_REVIEW: configService.getOrThrow<string>("SMS_PATTERN_NEW_SERVICE_REVIEW"),
SMS_PATTERN_NEW_CUSTOMER: configService.getOrThrow<string>("SMS_PATTERN_NEW_CUSTOMER"),
SMS_PATTERN_NEW_SUBSCRIPTION: configService.getOrThrow<string>("SMS_PATTERN_NEW_SUBSCRIPTION"),
SMS_PATTERN_NEW_TICKET_GLOBAL: configService.getOrThrow<string>("SMS_PATTERN_NEW_TICKET_GLOBAL"),
SMS_PATTERN_NEW_CRITICISM: configService.getOrThrow<string>("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;
}
@@ -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;
}
@@ -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,
);
}
}
@@ -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<NotifType, { fa: string; category: NotifCategory }> = {
@@ -65,6 +73,14 @@ export const NotifDescriptions: Record<NotifType, { fa: string; category: NotifC
// Support category
[NotifType.ANSWER_TICKET]: { fa: "پاسخ تیکت", category: NotifCategory.SUPPORT },
[NotifType.CREATE_TICKET]: { fa: "ثبت تیکت جدید", category: NotifCategory.SUPPORT },
//TODO: FIX THIS this is for admin
[NotifType.ASSIGN_TICKET]: { fa: "ارجاع تیکت", category: NotifCategory.SUPPORT },
// // Admin category
//this are not inserted in the database
[NotifType.NEW_BLOG_COMMENT]: { fa: "نظر جدید در بلاگ", category: NotifCategory.ADMIN },
[NotifType.NEW_SERVICE_REVIEW]: { fa: "نقد و بررسی جدید سرویس", category: NotifCategory.ADMIN },
[NotifType.NEW_CUSTOMER]: { fa: "مشتری جدید", category: NotifCategory.ADMIN },
[NotifType.NEW_SUBSCRIPTION]: { fa: "اشتراک جدید", category: NotifCategory.ADMIN },
[NotifType.NEW_TICKET]: { fa: "تیکت جدید", category: NotifCategory.ADMIN },
[NotifType.NEW_CRITICISM]: { fa: "انتقاد جدید", category: NotifCategory.ADMIN },
};
+4 -1
View File
@@ -45,4 +45,7 @@ export type TemplateParams =
| "user"
| "subject"
| "planName"
| "message";
| "message"
| "blogTitle"
| "fullName"
| "mobile";
@@ -153,4 +153,23 @@ export class EmailService {
this.logger.error("error in sending payment cancellation email", error);
}
}
async sendAdminNotificationEmail(data: { userEmail: string; title: string; message: string }) {
const emailData = {
to: data.userEmail,
subject: data.title,
template: "admin-notification",
context: {
title: data.title,
message: data.message,
},
};
try {
await this.mailerService.sendMail(emailData);
} catch (error) {
this.logger.error("error in sending admin notification email", error);
throw new InternalServerErrorException("error in sending admin notification email");
}
}
}
+186 -2
View File
@@ -490,7 +490,7 @@ export class SmsService {
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException("error in sending sms");
// throw new InternalServerErrorException("error in sending sms");
}
}
@@ -522,7 +522,7 @@ export class SmsService {
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException("error in sending sms");
// throw new InternalServerErrorException("error in sending sms");
}
}
@@ -604,4 +604,188 @@ export class SmsService {
}
//************************************************* */
async sendBlogNewCommentSms(mobile: string, blogTitle: string, fullName: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "blogTitle", value: blogTitle },
{ name: "fullName", value: fullName },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_BLOG_NEW_COMMENT,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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);
}
}
//************************************************* */
}
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #f8f9fa;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
.content {
background-color: #ffffff;
padding: 20px;
border-radius: 5px;
border: 1px solid #dee2e6;
}
.footer {
text-align: center;
margin-top: 20px;
color: #6c757d;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="header">
<h2>{{title}}</h2>
</div>
<div class="content">
<p>{{message}}</p>
</div>
<div class="footer">
<p>This is an automated message from DanakCo. Please do not reply to this email.</p>
</div>
</body>
</html>