chore: add notification service for all user action

This commit is contained in:
mahyargdz
2025-02-28 19:43:53 +03:30
parent 5b1b574d41
commit e65334150c
19 changed files with 709 additions and 74 deletions
BIN
View File
Binary file not shown.
+23 -1
View File
@@ -323,8 +323,30 @@ export const enum NotificationMessage {
USERID_IS_UUID = "شناسه کاربر باید یک UUID معتبر باشد", USERID_IS_UUID = "شناسه کاربر باید یک UUID معتبر باشد",
USERID_IS_STRING = "شناسه کاربر باید یک رشته متنی باشد", USERID_IS_STRING = "شناسه کاربر باید یک رشته متنی باشد",
LOGIN = "لاگین", LOGIN = "لاگین",
LOGIN_MESSAGE = "یک لاگین به حساب کاربری شما صورت گرفت", LOGIN_MESSAGE = "یک لاگین در تاریخ [loginDate] به حساب کاربری شما صورت گرفت",
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی غیرمجاز است", NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی غیرمجاز است",
INVOICE_CREATION = "صدور فاکتور",
INVOICE_CREATION_MESSAGE = "یک فاکتور به شماره [id] جدید برای شما صادر شده است",
ANNOUNCEMENT = "اطلاعیه",
ANNOUNCEMENT_MESSAGE = "یک اطلاعیه جدید برای شما ارسال شده است",
WALLET_CHARGE = "شارژ کیف پول",
WALLET_CHARGE_MESSAGE = "کیف پول شما به مبلغ [amount] تومان شارژ شد",
WALLET_DEDUCTION = "کسر از کیف پول",
WALLET_DEDUCTION_MESSAGE = "مبلغ [amount] تومان از کیف پول شما کسر شد",
BILL_INVOICE_REMINDER = "یادآوری پرداخت",
BILL_INVOICE_REMINDER_MESSAGE = "فاکتور شماره [invoiceNumber] هنوز پرداخت نشده است",
BILL_INVOICE = "فاکتور پرداختی",
BILL_INVOICE_MESSAGE = "فاکتور شماره [invoiceNumber] با موفقیت پرداخت شد",
CREATE_SERVICE = "ایجاد سرویس",
CREATE_SERVICE_MESSAGE = "سرویس جدیدی با عنوان [serviceName] ایجاد شد",
UNBLOCK_SERVICE = "فعال‌سازی سرویس",
UNBLOCK_SERVICE_MESSAGE = "سرویس [serviceName] مجددا فعال شده است",
BLOCK_SERVICE = "غیرفعال‌سازی سرویس",
BLOCK_SERVICE_MESSAGE = "سرویس [serviceName] غیرفعال شده است",
ANSWER_TICKET = "پاسخ تیکت",
ANSWER_TICKET_MESSAGE = "پاسخ جدیدی برای تیکت [ticketId] دریافت کردید",
CREATE_TICKET = "ایجاد تیکت",
CREATE_TICKET_MESSAGE = "تیکت شما با شماره [ticketNumber] ثبت شد",
} }
export const enum SubscriptionMessage { export const enum SubscriptionMessage {
+14
View File
@@ -9,6 +9,13 @@ export function smsConfigs() {
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"), API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"), SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"),
SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"), SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"),
SMS_PATTERN_LOGIN: configService.getOrThrow<string>("SMS_PATTERN_LOGIN"),
SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_CREATED"),
SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow<string>("SMS_PATTERN_ANNOUNCEMENT"),
SMS_PATTERN_WALLET_CHARGE: configService.getOrThrow<string>("SMS_PATTERN_WALLET_CHARGE"),
SMS_PATTERN_WALLET_DEDUCTION: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
SMS_PATTERN_TICKET_CREATED: configService.getOrThrow<string>("SMS_PATTERN_TICKET_CREATED"),
SMS_PATTERN_TICKET_ANSWERED: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
}; };
}, },
}; };
@@ -19,4 +26,11 @@ export interface ISmsConfigs {
API_KEY: string; API_KEY: string;
SMS_PATTERN_OTP: string; SMS_PATTERN_OTP: string;
SMS_PATTERN_INVOICE: string; SMS_PATTERN_INVOICE: string;
SMS_PATTERN_LOGIN: string;
SMS_PATTERN_INVOICE_CREATED: string;
SMS_PATTERN_ANNOUNCEMENT: string;
SMS_PATTERN_WALLET_CHARGE: string;
SMS_PATTERN_WALLET_DEDUCTION: string;
SMS_PATTERN_TICKET_CREATED: string;
SMS_PATTERN_TICKET_ANSWERED: string;
} }
+2 -2
View File
@@ -95,7 +95,7 @@ export class AuthService {
const tokens = this.generateAccessAndRefreshToken(user); const tokens = this.generateAccessAndRefreshToken(user);
await this.notificationService.createLoginNotification(user.id); await this.notificationService.createLoginNotification(user.id, { userPhone: user.phone });
return { return {
message: AuthMessage.PASSWORD_LOGIN_SUCCESS, message: AuthMessage.PASSWORD_LOGIN_SUCCESS,
@@ -176,7 +176,7 @@ export class AuthService {
const tokens = this.generateAccessAndRefreshToken(user); const tokens = this.generateAccessAndRefreshToken(user);
await this.notificationService.createLoginNotification(user.id); await this.notificationService.createLoginNotification(user.id, { userPhone: user.phone });
return { return {
message: AuthMessage.LOGIN_SUCCESS, message: AuthMessage.LOGIN_SUCCESS,
...tokens, ...tokens,
+8 -1
View File
@@ -10,12 +10,19 @@ import { Discount } from "../discounts/entities/discount.entity";
import { UsageDiscount } from "../discounts/entities/usage-discount.entity"; import { UsageDiscount } from "../discounts/entities/usage-discount.entity";
import { DiscountRepository } from "../discounts/repositories/discount.repository"; import { DiscountRepository } from "../discounts/repositories/discount.repository";
import { UsageDiscountRepository } from "../discounts/repositories/usage-discount.repository"; import { UsageDiscountRepository } from "../discounts/repositories/usage-discount.repository";
import { NotificationModule } from "../notifications/notifications.module";
import { UsersModule } from "../users/users.module"; import { UsersModule } from "../users/users.module";
import { UtilsModule } from "../utils/utils.module"; import { UtilsModule } from "../utils/utils.module";
import { WalletsModule } from "../wallets/wallets.module"; import { WalletsModule } from "../wallets/wallets.module";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Invoice, InvoiceItem, Discount, UsageDiscount]), UsersModule, WalletsModule, UtilsModule], imports: [
TypeOrmModule.forFeature([Invoice, InvoiceItem, Discount, UsageDiscount]),
UsersModule,
WalletsModule,
UtilsModule,
NotificationModule,
],
providers: [InvoicesService, InvoicesRepository, DiscountRepository, UsageDiscountRepository], providers: [InvoicesService, InvoicesRepository, DiscountRepository, UsageDiscountRepository],
controllers: [InvoicesController], controllers: [InvoicesController],
exports: [InvoicesService], exports: [InvoicesService],
@@ -9,6 +9,7 @@ import { AuthMessage, DiscountMessage, InvoiceMessage, WalletMessage } from "../
import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto"; import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto";
import { DiscountRepository } from "../../discounts/repositories/discount.repository"; import { DiscountRepository } from "../../discounts/repositories/discount.repository";
import { UsageDiscountRepository } from "../../discounts/repositories/usage-discount.repository"; import { UsageDiscountRepository } from "../../discounts/repositories/usage-discount.repository";
import { NotificationsService } from "../../notifications/providers/notifications.service";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { UsersService } from "../../users/providers/users.service"; import { UsersService } from "../../users/providers/users.service";
import { OTPService } from "../../utils/providers/otp.service"; import { OTPService } from "../../utils/providers/otp.service";
@@ -28,6 +29,7 @@ export class InvoicesService {
private readonly logger = new Logger(InvoicesService.name); private readonly logger = new Logger(InvoicesService.name);
constructor( constructor(
private readonly notificationsService: NotificationsService,
private readonly invoiceRepository: InvoicesRepository, private readonly invoiceRepository: InvoicesRepository,
private readonly discountRepository: DiscountRepository, private readonly discountRepository: DiscountRepository,
private readonly usageDiscountRepository: UsageDiscountRepository, private readonly usageDiscountRepository: UsageDiscountRepository,
@@ -41,38 +43,63 @@ export class InvoicesService {
///********************************** */ ///********************************** */
async createInvoiceAdmin(createDto: CreateInvoiceDto) { async createInvoiceAdmin(createDto: CreateInvoiceDto) {
// const queryRunner = this.dataSource.createQueryRunner();
const invoiceItems = createDto.items.map((item) => {
return { await queryRunner.connect();
await queryRunner.startTransaction();
try {
const invoiceItems = createDto.items.map((item) => ({
name: item.name, name: item.name,
count: item.count, count: item.count,
unitPrice: item.unitPrice, unitPrice: item.unitPrice,
discount: item.discount || 0, discount: item.discount || 0,
totalPrice: item.unitPrice * item.count - (item.unitPrice * item.count * item.discount) / 100, totalPrice: item.unitPrice * item.count - (item.unitPrice * item.count * item.discount) / 100,
}; }));
});
//
const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0));
const tax = totalPrice.mul(0.1);
//
const dueDate = new Date();
dueDate.setDate(dueDate.getDate() + 5);
//
const invoice = this.invoiceRepository.create({
user: { id: createDto.userId },
totalPrice: totalPrice.toNumber(),
items: invoiceItems,
tax: tax.toNumber(),
dueDate,
});
//
await this.invoiceRepository.save(invoice);
//TODO: notify user
return { const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0));
message: InvoiceMessage.CREATED, const tax = totalPrice.mul(0.1);
invoice,
}; const dueDate = new Date();
dueDate.setDate(dueDate.getDate() + 5);
const invoice = queryRunner.manager.create(Invoice, {
user: { id: createDto.userId },
totalPrice: totalPrice.toNumber(),
items: invoiceItems,
tax: tax.toNumber(),
dueDate,
});
await queryRunner.manager.save(Invoice, invoice);
const user = await this.usersService.findOneByIdWithQueryRunner(createDto.userId, queryRunner);
await this.notificationsService.createInvoiceCreationNotification(
createDto.userId,
{
invoiceId: invoice.numericId.toString(),
dueDate: invoice.dueDate,
price: new Decimal(invoice.totalPrice).toNumber(),
userPhone: user.phone,
userEmail: user.email,
items: invoiceItems.map((item) => item.name).join(", "),
},
queryRunner,
);
await queryRunner.commitTransaction();
return {
message: InvoiceMessage.CREATED,
invoice,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
} }
//********************************** */ //********************************** */
@@ -1,12 +1,13 @@
import { ApiPropertyOptional } from "@nestjs/swagger"; import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer"; import { Type } from "class-transformer";
import { IsOptional } from "class-validator"; import { IsIn, IsOptional } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { PaginationDto } from "../../../common/DTO/pagination.dto";
export class SearchNotificationQueryDto extends PaginationDto { export class SearchNotificationQueryDto extends PaginationDto {
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsIn([0, 1])
@ApiPropertyOptional({ description: "Notification status", example: 1 }) @ApiPropertyOptional({ description: "Notification status", example: 1 })
isRead?: number; isRead?: number;
} }
@@ -1,5 +1,5 @@
export enum NotificationType { export enum NotificationType {
LOGIN = "LOGIN", LOGIN = "LOGIN",
PASSWORD_CAHNGED = "PASSWORD_CHANGED", PASSWORD_CHANGED = "PASSWORD_CHANGED",
WALLET_CHARGED = "WALLER_CHARGED", WALLET_CHARGED = "WALLER_CHARGED",
} }
@@ -0,0 +1,38 @@
export interface IBaseNotificationData {
userPhone: string;
userEmail?: string;
}
export interface IInvoiceNotificationData extends IBaseNotificationData {
price: number;
items: string;
dueDate: Date;
invoiceId: string;
}
export interface IServiceNotificationData extends IBaseNotificationData {
serviceName: string;
}
export interface ITicketNotificationData extends IBaseNotificationData {
ticketId: string;
subject: string;
date: Date;
}
export interface IWalletNotificationData extends IBaseNotificationData {
amount: number;
balance: number;
date: Date;
reason: string;
}
export interface IGenericNotificationData extends IBaseNotificationData {
message: string;
}
export interface IAnnouncementNotificationData extends IBaseNotificationData {
title: string;
description: string;
date: Date;
}
@@ -10,6 +10,7 @@ import { ParamDto } from "../../common/DTO/param.dto";
@Controller("notifications") @Controller("notifications")
@ApiTags("Notifications") @ApiTags("Notifications")
@AuthGuards()
export class NotificationController { export class NotificationController {
constructor(private readonly notificationService: NotificationsService) {} constructor(private readonly notificationService: NotificationsService) {}
@@ -17,7 +18,6 @@ export class NotificationController {
@ApiOperation({ summary: "all notifications by user" }) @ApiOperation({ summary: "all notifications by user" })
@Pagination() @Pagination()
@AuthGuards()
@Get() @Get()
getAllNotifications(@UserDec("id") userId: string, @Query() queryDto: SearchNotificationQueryDto) { getAllNotifications(@UserDec("id") userId: string, @Query() queryDto: SearchNotificationQueryDto) {
return this.notificationService.getAllNotifications(queryDto, userId); return this.notificationService.getAllNotifications(queryDto, userId);
@@ -26,7 +26,6 @@ export class NotificationController {
//********************* */ //********************* */
@ApiOperation({ summary: "mark user notification as read" }) @ApiOperation({ summary: "mark user notification as read" })
@AuthGuards()
@Patch(":id/read") @Patch(":id/read")
markAsRead(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { markAsRead(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
return this.notificationService.markAsRead(paramDto.id, userId); return this.notificationService.markAsRead(paramDto.id, userId);
@@ -35,7 +34,6 @@ export class NotificationController {
//********************* */ //********************* */
@ApiOperation({ summary: "mark user all notification as read" }) @ApiOperation({ summary: "mark user all notification as read" })
@AuthGuards()
@Patch("read-all") @Patch("read-all")
markAllAsRead(@UserDec("id") userId: string) { markAllAsRead(@UserDec("id") userId: string) {
return this.notificationService.markAllAsRead(userId); return this.notificationService.markAllAsRead(userId);
@@ -5,11 +5,11 @@ import { Notification } from "./entities/notification.entity";
import { NotificationController } from "./notifications.controller"; import { NotificationController } from "./notifications.controller";
import { NotificationsService } from "./providers/notifications.service"; import { NotificationsService } from "./providers/notifications.service";
import { NotificationRepository } from "./repositories/notifications.repository"; import { NotificationRepository } from "./repositories/notifications.repository";
import { User } from "../users/entities/user.entity"; import { SettingModule } from "../settings/settings.module";
import { UsersModule } from "../users/users.module"; import { UtilsModule } from "../utils/utils.module";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Notification, User]), UsersModule], imports: [TypeOrmModule.forFeature([Notification]), UtilsModule, SettingModule],
providers: [NotificationRepository, NotificationsService], providers: [NotificationRepository, NotificationsService],
controllers: [NotificationController], controllers: [NotificationController],
exports: [NotificationRepository, NotificationsService], exports: [NotificationRepository, NotificationsService],
@@ -1,30 +1,73 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource, QueryRunner } from "typeorm";
import { CommonMessage, NotificationMessage, UserMessage } from "../../../common/enums/message.enum"; import { CommonMessage, NotificationMessage } from "../../../common/enums/message.enum";
import { UsersService } from "../../users/providers/users.service"; import { NotifType } from "../../settings/enums/notif-settings.enum";
import { UserSettingsService } from "../../settings/providers/user-settings.service";
import { dateFormat, numberFormat } from "../../utils/providers/international.utils";
import { SmsService } from "../../utils/providers/sms.service";
import { CreateNotificationDto } from "../DTO/create-notification.dto"; import { CreateNotificationDto } from "../DTO/create-notification.dto";
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto"; import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
import { Notification } from "../entities/notification.entity";
import {
IAnnouncementNotificationData,
IBaseNotificationData,
IInvoiceNotificationData,
ITicketNotificationData,
IWalletNotificationData,
} from "../interfaces/ISendNotificationData";
import { NotificationRepository } from "../repositories/notifications.repository"; import { NotificationRepository } from "../repositories/notifications.repository";
@Injectable() @Injectable()
export class NotificationsService { export class NotificationsService {
private readonly logger = new Logger(NotificationsService.name);
constructor( constructor(
private readonly notificationRepository: NotificationRepository, private readonly notificationRepository: NotificationRepository,
private readonly usersService: UsersService, private readonly smsService: SmsService,
private readonly userSettingsService: UserSettingsService,
@InjectDataSource() private readonly dataSource: DataSource,
) {} ) {}
//************************ */ //************************ */
async createNotification(createNotificationDto: CreateNotificationDto) { async createNotification(createNotificationDto: CreateNotificationDto, queryRunner?: QueryRunner) {
const { user } = await this.usersService.findOneById(createNotificationDto.recipientId); if (!queryRunner) {
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); const localQueryRunner = this.dataSource.createQueryRunner();
const notification = this.notificationRepository.create({
...createNotificationDto,
recipient: user,
});
await this.notificationRepository.save(notification);
return { notification }; await localQueryRunner.connect();
await localQueryRunner.startTransaction();
try {
//
const notification = localQueryRunner.manager.create(Notification, {
...createNotificationDto,
recipient: { id: createNotificationDto.recipientId },
});
await localQueryRunner.manager.save(notification);
await localQueryRunner.commitTransaction();
return notification;
//
} catch (error) {
this.logger.error("error in createNotification", error);
await localQueryRunner.rollbackTransaction();
throw error;
//
} finally {
await localQueryRunner.release();
}
//
} else {
//
const notification = queryRunner.manager.create(Notification, {
...createNotificationDto,
recipient: { id: createNotificationDto.recipientId },
});
//
await queryRunner.manager.save(notification);
return notification;
}
} }
//************************ */ //************************ */
@@ -42,20 +85,13 @@ export class NotificationsService {
//************************ */ //************************ */
async createLoginNotification(recipientId: string) {
const message = NotificationMessage.LOGIN_MESSAGE;
return this.createNotification({ title: NotificationMessage.LOGIN, message, recipientId });
}
//************************ */
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) { async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
return await this.notificationRepository.getAllNotifications(queryDto, recipientId); return await this.notificationRepository.getAllNotifications(queryDto, recipientId);
} }
//************************ */ //************************ */
async getOneNotification(id: string) { async getNotificationById(id: string) {
const notification = await this.notificationRepository.findOneBy({ id }); const notification = await this.notificationRepository.findOneBy({ id });
if (!notification) throw new BadRequestException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED); if (!notification) throw new BadRequestException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
return { notification }; return { notification };
@@ -64,18 +100,199 @@ export class NotificationsService {
//************************ */ //************************ */
async markAllAsRead(userId: string) { async markAllAsRead(userId: string) {
await this.notificationRepository.update( await this.notificationRepository.update({ recipient: { id: userId } }, { isRead: true });
{
recipient: {
id: userId,
},
},
{
isRead: true,
},
);
return { return {
message: CommonMessage.UPDATE_SUCCESS, message: CommonMessage.UPDATE_SUCCESS,
}; };
} }
//************************ */
async createLoginNotification(recipientId: string, sendNotifData: IBaseNotificationData) {
const loginDate = new Date().toLocaleString("fa-IR");
const message = NotificationMessage.LOGIN_MESSAGE.replace("[loginDate]", loginDate);
//sent notification based on the userSettings
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.USER_LOGIN, recipientId);
if (isActive) {
await this.smsService.sendLoginSms(sendNotifData.userPhone, loginDate);
//send email too
}
return this.createNotification({ title: NotificationMessage.LOGIN, message, recipientId });
}
//************************ */
async createInvoiceCreationNotification(recipientId: string, data: IInvoiceNotificationData, queryRunner: QueryRunner) {
const message = NotificationMessage.INVOICE_CREATION_MESSAGE.replace("[id]", data.invoiceId);
//sent notification based on the userSettings
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(
NotifType.CREATE_INVOICE,
recipientId,
queryRunner,
);
if (isActive) {
await this.smsService.sendInvoiceCreationSms(data.userPhone, data.invoiceId, data.price, data.items, dateFormat(data.dueDate));
//send email too
}
return this.createNotification({ title: NotificationMessage.INVOICE_CREATION, message, recipientId }, queryRunner);
}
//************************ */
async createAnnouncementNotification(recipientId: string, data: IAnnouncementNotificationData, queryRunner: QueryRunner) {
const message = NotificationMessage.ANNOUNCEMENT_MESSAGE;
//sent notification based on the userSettings
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(
NotifType.ANNOUNCEMENT,
recipientId,
queryRunner,
);
if (isActive) {
await this.smsService.sendAnnouncementSms(data.userPhone, data.title, data.description, dateFormat(data.date));
//send email too
}
return this.createNotification({ title: NotificationMessage.ANNOUNCEMENT, message, recipientId });
}
//************************ */
async createWalletChargeNotification(recipientId: string, data: IWalletNotificationData, queryRunner: QueryRunner) {
const message = NotificationMessage.WALLET_CHARGE_MESSAGE.replace("[amount]", numberFormat(data.amount));
//sent notification based on the userSettings
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(
NotifType.WALLET_CHARGE,
recipientId,
queryRunner,
);
if (isActive) {
await this.smsService.sendWalletChargeSms(
data.userPhone,
numberFormat(data.amount),
numberFormat(data.balance),
dateFormat(data.date),
);
//send email too
}
return this.createNotification({ title: NotificationMessage.WALLET_CHARGE, message, recipientId });
}
//************************ */
async createWalletDeductionNotification(recipientId: string, data: IWalletNotificationData, queryRunner: QueryRunner) {
const message = NotificationMessage.WALLET_DEDUCTION_MESSAGE.replace("[amount]", numberFormat(data.amount));
//sent notification based on the userSettings
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(
NotifType.WALLET_DEDUCTION,
recipientId,
queryRunner,
);
if (isActive) {
await this.smsService.sendWalletDeductionSms(
data.userPhone,
numberFormat(data.amount),
numberFormat(data.balance),
data.reason,
dateFormat(data.date),
);
//send email too
}
return this.createNotification({ title: NotificationMessage.WALLET_DEDUCTION, message, recipientId });
}
// //************************ */
async createAnswerTicketNotification(recipientId: string, data: ITicketNotificationData, queryRunner: QueryRunner) {
const message = NotificationMessage.ANSWER_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
//sent notification based on the userSettings
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(
NotifType.ANSWER_TICKET,
recipientId,
queryRunner,
);
if (isActive) {
await this.smsService.sendAnswerTicketSms(data.userPhone, data.ticketId, data.subject);
//send email too
}
return this.createNotification({ title: NotificationMessage.ANSWER_TICKET, message, recipientId });
}
// //************************ */
async createTicketNotification(recipientId: string, data: ITicketNotificationData, queryRunner: QueryRunner) {
const message = NotificationMessage.CREATE_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
//sent notification based on the userSettings
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(
NotifType.CREATE_TICKET,
recipientId,
queryRunner,
);
if (isActive) {
await this.smsService.sendCreateTicketSms(data.userPhone, data.ticketId, data.subject, dateFormat(data.date));
//send email too
}
return this.createNotification({ title: NotificationMessage.CREATE_TICKET, message, recipientId });
}
//************************ */
// 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 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 createServiceNotification(recipientId: string, sendNotifData: ISendNotificationData) {
// const message = NotificationMessage.CREATE_SERVICE_MESSAGE.replace("[serviceName]", sendNotifData.serviceName);
// //sent notification based on the userSettings
// const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.CREATE_SERVICE, recipientId);
// if (isActive) {
// await this.smsService.sendCreateServiceSms(sendNotifData.userPhone, sendNotifData.serviceName);
// //send email too
// }
// return this.createNotification({ title: NotificationMessage.CREATE_SERVICE, message, recipientId });
// }
// //************************ */
// async createUnblockServiceNotification(recipientId: string, sendNotifData: ISendNotificationData) {
// const message = NotificationMessage.UNBLOCK_SERVICE_MESSAGE.replace("[serviceName]", sendNotifData.serviceName);
// //sent notification based on the userSettings
// const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.UNBLOCK_SERVICE, recipientId);
// if (isActive) {
// await this.smsService.sendUnblockServiceSms(sendNotifData.userPhone, sendNotifData.serviceName);
// //send email too
// }
// return this.createNotification({ title: NotificationMessage.UNBLOCK_SERVICE, message, recipientId });
// }
// //************************ */
// 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 });
// }
} }
+2
View File
@@ -18,12 +18,14 @@ import { DepositRequest } from "./entities/deposit-request.entity";
import { PaymentGateway } from "./entities/payment-gateway.entity"; import { PaymentGateway } from "./entities/payment-gateway.entity";
import { DepositRequestsRepository } from "./repositories/deposit-requests.repository"; import { DepositRequestsRepository } from "./repositories/deposit-requests.repository";
import { PaymentGatewaysRepository } from "./repositories/payment-gateway.repository"; import { PaymentGatewaysRepository } from "./repositories/payment-gateway.repository";
import { NotificationModule } from "../notifications/notifications.module";
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Payment, PaymentGateway, BankAccount, DepositRequest]), TypeOrmModule.forFeature([Payment, PaymentGateway, BankAccount, DepositRequest]),
BullModule.registerQueue({ name: PAYMENT.PAYMENT_QUEUE_NAME }), BullModule.registerQueue({ name: PAYMENT.PAYMENT_QUEUE_NAME }),
WalletsModule, WalletsModule,
NotificationModule,
], ],
controllers: [PaymentsController], controllers: [PaymentsController],
providers: [ providers: [
@@ -9,6 +9,7 @@ import { DataSource, Not, QueryRunner } from "typeorm";
import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { CommonMessage, PaymentMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum"; import { CommonMessage, PaymentMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum";
import { NotificationsService } from "../../notifications/providers/notifications.service";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { WalletsService } from "../../wallets/providers/wallets.service"; import { WalletsService } from "../../wallets/providers/wallets.service";
@@ -37,6 +38,7 @@ import { GatewayType } from "../types/gateway.type";
export class PaymentsService { export class PaymentsService {
constructor( constructor(
@InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue, @InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue,
private readonly notificationsService: NotificationsService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly gatewayFactory: PaymentGatewayFactory, private readonly gatewayFactory: PaymentGatewayFactory,
private readonly paymentGatewaysRepository: PaymentGatewaysRepository, private readonly paymentGatewaysRepository: PaymentGatewaysRepository,
@@ -159,12 +161,31 @@ export class PaymentsService {
const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount }); const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount });
if (verifyData.code === 100) { if (verifyData.code === 100) {
await this.handleSuccessfulPayment(payment.id, payment.user.id, payment.amount, `${verifyData.ref_id}`, queryRunner); const { wallet } = await this.handleSuccessfulPayment(
payment.id,
payment.user.id,
payment.amount,
`${verifyData.ref_id}`,
queryRunner,
);
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
frontUrl.searchParams.append("status", PaymentStatus.COMPLETED); frontUrl.searchParams.append("status", PaymentStatus.COMPLETED);
frontUrl.searchParams.append("id", payment.id); frontUrl.searchParams.append("id", payment.id);
frontUrl.searchParams.append("date", payment.createdAt.toISOString()); frontUrl.searchParams.append("date", payment.createdAt.toISOString());
frontUrl.searchParams.append("amount", payment.amount.toString()); frontUrl.searchParams.append("amount", payment.amount.toString());
//
await this.notificationsService.createWalletChargeNotification(
payment.user.id,
{
amount: new Decimal(payment.amount).toNumber(),
balance: new Decimal(wallet.balance).toNumber(),
date: payment.createdAt,
reason: WalletMessage.DEPOSIT_WALLET_IPG,
userPhone: payment.user.phone,
userEmail: payment.user.email,
},
queryRunner,
);
} else if (verifyData.code === 101) { } else if (verifyData.code === 101) {
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
frontUrl.searchParams.append("status", PaymentStatus.PENDING); frontUrl.searchParams.append("status", PaymentStatus.PENDING);
@@ -229,8 +250,7 @@ export class PaymentsService {
//*********************************** */ //*********************************** */
async handleSuccessfulPayment(paymentId: string, userId: string, amount: Decimal, transactionId: string, queryRunner: QueryRunner) { async handleSuccessfulPayment(paymentId: string, userId: string, amount: Decimal, transactionId: string, queryRunner: QueryRunner) {
await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.COMPLETED, transactionId }); await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.COMPLETED, transactionId });
const transaction = await this.walletsService.createPaymentTransaction(userId, amount, queryRunner); return this.walletsService.createPaymentTransaction(userId, amount, queryRunner);
return transaction;
} }
//*********************************** */ //*********************************** */
@@ -60,6 +60,23 @@ export class UserSettingsService {
} }
//*+******************************************* //*+*******************************************
async getUserNotificationSettingWithType(type: NotifType, userId: string, queryRunner?: QueryRunner) {
const repository = queryRunner ? queryRunner.manager.getRepository(UserSetting) : this.userSettingsRepository;
const setting = await repository.findOne({
where: { user: { id: userId }, notificationSetting: { type } },
relations: {
notificationSetting: true,
},
});
if (!setting) throw new BadRequestException(SettingMessageEnum.NOT_FOUND);
return setting;
}
//*+*******************************************
async toggleUserNotificationSetting(settingId: string) { async toggleUserNotificationSetting(settingId: string) {
const setting = await this.userSettingsRepository.findOneBy({ id: settingId }); const setting = await this.userSettingsRepository.findOneBy({ id: settingId });
+18 -1
View File
@@ -23,4 +23,21 @@ export interface ISmsVerifyBody {
TemplateId: string; TemplateId: string;
} }
export type TemplateParams = "VERIFICATIONCODE" | "code" | "invoiceId" | "price" | "items"; export type TemplateParams =
| "VERIFICATIONCODE"
| "code"
| "invoiceId"
| "price"
| "items"
| "loginDate"
| "invoiceDate"
| "title"
| "description"
| "ticketId"
| "serviceName"
| "date"
| "amount"
| "newBalance"
| "reason"
| "subject"
| "message";
@@ -0,0 +1,7 @@
export function numberFormat(number: number, locale: string = "fa-IR") {
return Intl.NumberFormat(locale).format(number);
}
export function dateFormat(date: Date, locale: string = "fa-IR") {
return new Intl.DateTimeFormat(locale).format(date);
}
+248
View File
@@ -84,6 +84,254 @@ export class SmsService {
throw new InternalServerErrorException("error in sending sms"); throw new InternalServerErrorException("error in sending sms");
} }
} }
//************************************************* */
async sendLoginSms(mobile: string, loginDate: string) {
const smsData: ISmsVerifyBody = {
Parameters: [{ name: "loginDate", value: loginDate }],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_LOGIN,
};
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 verify sms", err);
throw new InternalServerErrorException("error in sending verify sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
// throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendInvoiceCreationSms(mobile: string, invoiceId: string, price: number, items: string, invoiceDate: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "invoiceId", value: invoiceId },
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
{ name: "items", value: items },
{ name: "invoiceDate", value: invoiceDate },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_CREATED,
};
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 invoice created sms", err);
throw new InternalServerErrorException("error in sending invoice created sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendAnnouncementSms(mobile: string, title: string, description: string, date: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "title", value: title },
{ name: "description", value: description },
{ name: "date", value: date },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_ANNOUNCEMENT,
};
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 announcement sms", err);
throw new InternalServerErrorException("error in sending announcement sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendWalletChargeSms(mobile: string, amount: string, newBalance: string, date: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "amount", value: amount },
{ name: "newBalance", value: newBalance },
{ name: "date", value: date },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_WALLET_CHARGE,
};
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 wallet charge sms", err);
throw new InternalServerErrorException("error in sending wallet charge sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendWalletDeductionSms(mobile: string, amount: string, newBalance: string, reason: string, date: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "amount", value: amount },
{ name: "newBalance", value: newBalance },
{ name: "reason", value: reason },
{ name: "date", value: date },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_WALLET_DEDUCTION,
};
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 wallet deduction sms", err);
throw new InternalServerErrorException("error in sending wallet deduction sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendCreateTicketSms(mobile: string, ticketId: string, subject: string, date: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "ticketId", value: ticketId },
{ name: "subject", value: subject },
{ name: "date", value: date },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_CREATED,
};
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 ticket created sms", err);
throw new InternalServerErrorException("error in sending ticket created sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendAnswerTicketSms(mobile: string, ticketId: string, subject: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "ticketId", value: ticketId },
{ name: "subject", value: subject },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ANSWERED,
};
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 ticket answered sms", err);
throw new InternalServerErrorException("error in sending ticket answered 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.");
// }
// //************************************************* */
// sendBillInvoiceSms(userPhone: string, amount: any, invoiceId: any) {
// throw new Error("Method not implemented.");
// }
// //************************************************* */
// 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.");
// }
// //************************************************* */
//************************************************* */ //************************************************* */
@@ -56,7 +56,7 @@ export class WalletsService {
await queryRunner.manager.save(Wallet, wallet); await queryRunner.manager.save(Wallet, wallet);
await queryRunner.manager.save(WalletTransaction, transaction); await queryRunner.manager.save(WalletTransaction, transaction);
return transaction; return { transaction, wallet };
} }
//*********************************** */ //*********************************** */