chore: first
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { IsEnum, IsNotEmpty, IsString, IsUUID } from "class-validator";
|
||||
|
||||
import { NotificationMessage } from "../../../common/enums/message.enum";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
|
||||
export class CreateNotificationDto {
|
||||
@IsNotEmpty({ message: NotificationMessage.TITLE_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.TITLE_STRING })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.MESSAGE_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.MESSAGE_STRING })
|
||||
message: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.USERID_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.USERID_IS_STRING })
|
||||
@IsUUID("7", { message: NotificationMessage.USERID_IS_UUID })
|
||||
recipientId: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEnum(NotifType)
|
||||
type: NotifType;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsIn, IsOptional } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
|
||||
export class SearchNotificationQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsIn([0, 1])
|
||||
@ApiPropertyOptional({ description: "Notification status", example: 1 })
|
||||
isRead?: number;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const NOTIFICATION = Object.freeze({
|
||||
QUEUE_NAME: "notification",
|
||||
SEND_NOTIFICATION_JOB_NAME: "sendNotification",
|
||||
|
||||
SEND_NOTIFICATION_JOB_DELAY: 1000, // delay after 1 second
|
||||
SEND_NOTIFICATION_JOB_PRIORITY: 1, // high priority
|
||||
SEND_NOTIFICATION_JOB_ATTEMPTS: 3, // retry 3 times
|
||||
SEND_NOTIFICATION_JOB_BACKOFF: 5 * 1000, // retry after 5 seconds
|
||||
SEND_NOTIFICATION_JOB_TIMEOUT: 10000, // timeout after 10 seconds
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Entity, EntityRepositoryType, Enum, ManyToOne, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||
|
||||
@Entity({ repository: () => NotificationRepository })
|
||||
export class Notification extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 250, nullable: false })
|
||||
title!: string;
|
||||
|
||||
@Property({ type: "text", nullable: false })
|
||||
message!: string;
|
||||
|
||||
@Enum({ items: () => NotifType, nativeEnumName: "notif_type", nullable: false })
|
||||
type: NotifType;
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
isRead: boolean & Opt;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade" })
|
||||
recipient!: User;
|
||||
|
||||
[EntityRepositoryType]?: NotificationRepository;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
export enum NotifType {
|
||||
// Account category
|
||||
OTP = "OTP",
|
||||
USER_LOGIN = "USER_LOGIN",
|
||||
ANNOUNCEMENT = "ANNOUNCEMENT",
|
||||
|
||||
// Finance category
|
||||
WALLET_CHARGE = "WALLET_CHARGE",
|
||||
WALLET_DEDUCTION = "WALLET_DEDUCTION",
|
||||
|
||||
//Invoice
|
||||
BILL_INVOICE_REMINDER = "BILL_INVOICE_REMINDER",
|
||||
BILL_INVOICE = "BILL_INVOICE",
|
||||
APPROVE_INVOICE = "APPROVE_INVOICE",
|
||||
CREATE_INVOICE = "CREATE_INVOICE",
|
||||
INVOICE_OVERDUE = "INVOICE_OVERDUE",
|
||||
|
||||
// Service category
|
||||
CREATE_SERVICE = "CREATE_SERVICE",
|
||||
UNBLOCK_SERVICE = "UNBLOCK_SERVICE",
|
||||
BLOCK_SERVICE = "BLOCK_SERVICE",
|
||||
|
||||
// Support category
|
||||
ANSWER_TICKET = "ANSWER_TICKET",
|
||||
CREATE_TICKET = "CREATE_TICKET",
|
||||
//
|
||||
ASSIGN_TICKET = "ASSIGN_TICKET",
|
||||
RECURRING_INVOICE = "RECURRING_INVOICE",
|
||||
PAYMENT_REMINDER = "PAYMENT_REMINDER",
|
||||
PAYMENT_CANCELLATION = "PAYMENT_CANCELLATION",
|
||||
|
||||
// // Admin category
|
||||
|
||||
NEW_CUSTOMER = "NEW_CUSTOMER",
|
||||
NEW_TICKET = "NEW_TICKET",
|
||||
NEW_CRITICISM = "NEW_CRITICISM",
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export interface IBaseNotificationData {
|
||||
userPhone: string;
|
||||
userEmail?: string;
|
||||
}
|
||||
|
||||
export interface IOtpNotificationData extends IBaseNotificationData {
|
||||
otp: string;
|
||||
}
|
||||
|
||||
export interface IInvoiceNotificationData extends IBaseNotificationData {
|
||||
price: number;
|
||||
items: string;
|
||||
dueDate: Date;
|
||||
createDate: Date;
|
||||
invoiceId: string;
|
||||
paidAt?: Date;
|
||||
lateFee?: number;
|
||||
}
|
||||
|
||||
export interface ITicketNotificationData extends IBaseNotificationData {
|
||||
ticketId: string;
|
||||
subject: string;
|
||||
date: Date;
|
||||
user?: string;
|
||||
}
|
||||
|
||||
export interface IGenericNotificationData extends IBaseNotificationData {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IAnnouncementNotificationData extends IBaseNotificationData {
|
||||
title: string;
|
||||
description: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface IPaymentNotificationData extends IBaseNotificationData {
|
||||
amount: number;
|
||||
paymentId: string;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Controller, Get, Param, Patch, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { SearchNotificationQueryDto } from "./DTO/search-notification-query.dto";
|
||||
import { NotificationsService } from "./providers/notifications.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { Pagination } from "../../common/decorators/pagination.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
|
||||
@Controller("notifications")
|
||||
@ApiTags("Notifications")
|
||||
@AuthGuards()
|
||||
export class NotificationController {
|
||||
constructor(private readonly notificationService: NotificationsService) {}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "all notifications by user" })
|
||||
@Pagination()
|
||||
@Get()
|
||||
getAllNotifications(@UserDec("id") userId: string, @Query() queryDto: SearchNotificationQueryDto) {
|
||||
return this.notificationService.getAllNotifications(queryDto, userId);
|
||||
}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "mark user notification as read" })
|
||||
@Patch(":id/read")
|
||||
markAsRead(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.notificationService.markAsRead(paramDto.id, userId);
|
||||
}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "mark user all notification as read" })
|
||||
@Patch("read-all")
|
||||
markAllAsRead(@UserDec("id") userId: string) {
|
||||
return this.notificationService.markAllAsRead(userId);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
import { NOTIFICATION } from "./constants";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { Notification } from "./entities/notification.entity";
|
||||
import { NotificationController } from "./notifications.controller";
|
||||
import { NotificationsService } from "./providers/notifications.service";
|
||||
import { NotificationProcessor } from "./queue/notification.processor";
|
||||
import { NotificationQueue } from "./queue/notification.queue";
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
MikroOrmModule.forFeature([Notification]),
|
||||
BullModule.registerQueue({
|
||||
name: NOTIFICATION.QUEUE_NAME,
|
||||
defaultJobOptions: {
|
||||
attempts: NOTIFICATION.SEND_NOTIFICATION_JOB_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_BACKOFF,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_DELAY,
|
||||
},
|
||||
}),
|
||||
UtilsModule,
|
||||
],
|
||||
providers: [NotificationsService, NotificationQueue, NotificationProcessor],
|
||||
controllers: [NotificationController],
|
||||
exports: [NotificationsService, NotificationQueue],
|
||||
})
|
||||
export class NotificationModule {}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { CommonMessage, NotificationMessage } from "../../../common/enums/message.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { EmailService } from "../../utils/providers/email.service";
|
||||
import { dateFormat, numberFormat } from "../../utils/providers/international.utils";
|
||||
import { SmsService } from "../../utils/providers/sms.service";
|
||||
import { CreateNotificationDto } from "../DTO/create-notification.dto";
|
||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||
import { Notification } from "../entities/notification.entity";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IBaseNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
private readonly logger = new Logger(NotificationsService.name);
|
||||
constructor(
|
||||
private readonly notificationRepository: NotificationRepository,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createNotification(createNotificationDto: CreateNotificationDto, em?: EntityManager) {
|
||||
if (!em) {
|
||||
const localEm = this.em.fork();
|
||||
|
||||
try {
|
||||
await localEm.begin();
|
||||
//
|
||||
const notification = localEm.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: localEm.getReference(User, createNotificationDto.recipientId),
|
||||
});
|
||||
await localEm.persistAndFlush(notification);
|
||||
|
||||
await localEm.commit();
|
||||
|
||||
return notification;
|
||||
//
|
||||
} catch (error) {
|
||||
this.logger.error("error in createNotification", error);
|
||||
await localEm.rollback();
|
||||
throw error;
|
||||
//
|
||||
}
|
||||
//
|
||||
} else {
|
||||
//
|
||||
const notification = em.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: em.getReference(User, createNotificationDto.recipientId),
|
||||
});
|
||||
//
|
||||
await em.persistAndFlush(notification);
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async markAsRead(notificationId: string, userId: string) {
|
||||
const notification = await this.notificationRepository.findOne({ id: notificationId, recipient: { id: userId } });
|
||||
|
||||
if (!notification) throw new NotFoundException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
|
||||
notification.isRead = true;
|
||||
await this.em.flush();
|
||||
return notification;
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||
return await this.notificationRepository.getAllNotifications(queryDto, recipientId);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async getNotificationById(id: string) {
|
||||
const notification = await this.notificationRepository.findOne({ id });
|
||||
if (!notification) throw new BadRequestException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
return { notification };
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async countUserNotifications(userId: string) {
|
||||
const notificationCount = await this.notificationRepository.count({ recipient: { id: userId }, isRead: false });
|
||||
return notificationCount;
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async markAllAsRead(userId: string) {
|
||||
await this.em.nativeUpdate(Notification, { recipient: { id: userId } }, { isRead: true });
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createOtpNotification(data: IOtpNotificationData) {
|
||||
//sent notification based on the userSettings
|
||||
return await this.smsService.sendSmsVerifyCode(data.userPhone, data.otp);
|
||||
}
|
||||
|
||||
async createLoginNotification(recipientId: string, data: IBaseNotificationData) {
|
||||
const loginDate = new Date().toLocaleString("fa-IR");
|
||||
const message = NotificationMessage.LOGIN_MESSAGE.replace("[loginDate]", loginDate);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendLoginSms(data.userPhone, loginDate);
|
||||
// if (data.userEmail) await this.emailService.sendLoginEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.LOGIN, type: NotifType.USER_LOGIN, message, recipientId });
|
||||
}
|
||||
|
||||
//************************ */
|
||||
async createInvoiceCreationNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.INVOICE_CREATION_MESSAGE.replace("[id]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoiceCreationSms(
|
||||
data.userPhone,
|
||||
data.invoiceId,
|
||||
data.price,
|
||||
data.items,
|
||||
dateFormat(data.createDate),
|
||||
dateFormat(data.dueDate),
|
||||
);
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.INVOICE_CREATION, type: NotifType.CREATE_INVOICE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
//************************ */
|
||||
|
||||
async createAnnouncementNotification(recipientId: string, data: IAnnouncementNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ANNOUNCEMENT_MESSAGE;
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendAnnouncementSms(data.userPhone, data.title, data.description, dateFormat(data.date));
|
||||
if (data.userEmail) await this.emailService.sendAnnouncementEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ANNOUNCEMENT, type: NotifType.ANNOUNCEMENT, message, recipientId }, em);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createAnswerTicketNotification(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ANSWER_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendAnswerTicketSms(data.userPhone, data.ticketId, data.subject);
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ANSWER_TICKET, type: NotifType.ANSWER_TICKET, message, recipientId }, em);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createTicketNotification(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.CREATE_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendCreateTicketSms(data.userPhone, data.ticketId, data.subject, dateFormat(data.date));
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.CREATE_TICKET, type: NotifType.CREATE_TICKET, message, recipientId }, em);
|
||||
}
|
||||
//************************ */
|
||||
async createAssignTicketNotificationForAdmin(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ASSIGN_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendTicketAssignedToAdminSms(data.userPhone, data.ticketId, data.subject, data.user!);
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ASSIGN_TICKET, type: NotifType.ASSIGN_TICKET, message, recipientId }, em);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createBillInvoiceReminderNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.BILL_INVOICE_REMINDER_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoicePaymentReminderSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.BILL_INVOICE_REMINDER, type: NotifType.BILL_INVOICE_REMINDER, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createBillInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.BILL_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoicePaidSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.paidAt!));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.BILL_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, em);
|
||||
}
|
||||
//************************ */
|
||||
|
||||
async createApprovedInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.APPROVED_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendInvoiceApprovedSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.APPROVED_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, em);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createInvoiceOverdueNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.INVOICE_OVERDUE_MESSAGE.replace("[invoiceNumber]", data.invoiceId).replace(
|
||||
"[lateFee]",
|
||||
numberFormat(data.lateFee || 0),
|
||||
);
|
||||
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoiceOverdueSms(data.userPhone, data.invoiceId, data.price, data.lateFee || 0, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.INVOICE_OVERDUE, type: NotifType.INVOICE_OVERDUE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createRecurringInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.RECURRING_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendRecurringInvoiceDraftSms(data.userPhone, data.invoiceId, data.price);
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.RECURRING_INVOICE, type: NotifType.RECURRING_INVOICE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createPaymentReminderNotification(recipientId: string, data: IPaymentNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.PAYMENT_REMINDER_MESSAGE.replace("[amount]", numberFormat(data.amount));
|
||||
|
||||
await this.smsService.sendPaymentReminderSms(data.userPhone, numberFormat(data.amount));
|
||||
if (data.userEmail) await this.emailService.sendPaymentReminderEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{
|
||||
title: NotificationMessage.PAYMENT_REMINDER,
|
||||
type: NotifType.PAYMENT_REMINDER,
|
||||
message,
|
||||
recipientId,
|
||||
},
|
||||
em,
|
||||
);
|
||||
}
|
||||
// //************************ */
|
||||
|
||||
async createPaymentCancellationNotification(recipientId: string, data: IPaymentNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.PAYMENT_CANCELLATION_MESSAGE.replace("[amount]", numberFormat(data.amount));
|
||||
|
||||
await this.smsService.sendPaymentCancellationSms(data.userPhone, numberFormat(data.amount));
|
||||
if (data.userEmail) await this.emailService.sendPaymentCancellationEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{
|
||||
title: NotificationMessage.PAYMENT_CANCELLATION,
|
||||
type: NotifType.PAYMENT_CANCELLATION,
|
||||
message,
|
||||
recipientId,
|
||||
},
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Processor } from "@nestjs/bullmq";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Job } from "bullmq";
|
||||
|
||||
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||
import { NOTIFICATION } from "../constants";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
import { NotificationsService } from "../providers/notifications.service";
|
||||
|
||||
type NotificationJobData = {
|
||||
type: NotifType;
|
||||
recipientId: string;
|
||||
data:
|
||||
| ITicketNotificationData
|
||||
| IInvoiceNotificationData
|
||||
| IAnnouncementNotificationData
|
||||
| IPaymentNotificationData
|
||||
| IOtpNotificationData;
|
||||
};
|
||||
|
||||
@Processor(NOTIFICATION.QUEUE_NAME, { concurrency: 5 })
|
||||
export class NotificationProcessor extends WorkerProcessor {
|
||||
protected readonly logger = new Logger(NotificationProcessor.name);
|
||||
|
||||
constructor(
|
||||
private readonly notificationsService: NotificationsService,
|
||||
private readonly em: EntityManager,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<NotificationJobData>, token?: string) {
|
||||
this.logger.log(`Processing notification job: ${job.id} ${token ? `with token: ${token}` : ""}`);
|
||||
|
||||
const entityManager = this.em.fork();
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
|
||||
const { type, recipientId, data } = job.data;
|
||||
|
||||
switch (type) {
|
||||
// User Notifications
|
||||
case NotifType.OTP:
|
||||
await this.notificationsService.createOtpNotification(data as IOtpNotificationData);
|
||||
break;
|
||||
|
||||
case NotifType.USER_LOGIN:
|
||||
await this.notificationsService.createLoginNotification(recipientId, data);
|
||||
break;
|
||||
case NotifType.ANNOUNCEMENT:
|
||||
await this.notificationsService.createAnnouncementNotification(recipientId, data as IAnnouncementNotificationData, entityManager);
|
||||
break;
|
||||
|
||||
// Ticket Notifications
|
||||
case NotifType.ANSWER_TICKET:
|
||||
await this.notificationsService.createAnswerTicketNotification(recipientId, data as ITicketNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.CREATE_TICKET:
|
||||
await this.notificationsService.createTicketNotification(recipientId, data as ITicketNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.ASSIGN_TICKET:
|
||||
await this.notificationsService.createAssignTicketNotificationForAdmin(
|
||||
recipientId,
|
||||
data as ITicketNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
|
||||
// Invoice Notifications
|
||||
case NotifType.CREATE_INVOICE:
|
||||
await this.notificationsService.createInvoiceCreationNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.BILL_INVOICE_REMINDER:
|
||||
await this.notificationsService.createBillInvoiceReminderNotification(
|
||||
recipientId,
|
||||
data as IInvoiceNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
case NotifType.BILL_INVOICE:
|
||||
await this.notificationsService.createBillInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.APPROVE_INVOICE:
|
||||
await this.notificationsService.createApprovedInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.INVOICE_OVERDUE:
|
||||
await this.notificationsService.createInvoiceOverdueNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.RECURRING_INVOICE:
|
||||
await this.notificationsService.createRecurringInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
|
||||
// Payment Notifications
|
||||
case NotifType.PAYMENT_REMINDER:
|
||||
await this.notificationsService.createPaymentReminderNotification(recipientId, data as IPaymentNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.PAYMENT_CANCELLATION:
|
||||
await this.notificationsService.createPaymentCancellationNotification(
|
||||
recipientId,
|
||||
data as IPaymentNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.warn(`Unknown notification type: ${type}`);
|
||||
}
|
||||
|
||||
await entityManager.commit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to process notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
await entityManager.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { InjectQueue } from "@nestjs/bullmq";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Queue } from "bullmq";
|
||||
|
||||
import { NOTIFICATION } from "../constants";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IBaseNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationQueue {
|
||||
constructor(@InjectQueue(NOTIFICATION.QUEUE_NAME) private readonly notificationsQueue: Queue) {}
|
||||
|
||||
async addNotificationJob(type: NotifType, recipientId: string, data: IBaseNotificationData) {
|
||||
await this.notificationsQueue.add(
|
||||
NOTIFICATION.SEND_NOTIFICATION_JOB_NAME,
|
||||
{
|
||||
type,
|
||||
recipientId,
|
||||
data,
|
||||
},
|
||||
{
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_DELAY,
|
||||
attempts: NOTIFICATION.SEND_NOTIFICATION_JOB_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_BACKOFF,
|
||||
},
|
||||
priority: NOTIFICATION.SEND_NOTIFICATION_JOB_PRIORITY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async addOtpNotification(recipientId: string, data: IOtpNotificationData) {
|
||||
await this.addNotificationJob(NotifType.OTP, recipientId, data);
|
||||
}
|
||||
|
||||
// User notification methods
|
||||
async addLoginNotification(recipientId: string, data: IBaseNotificationData) {
|
||||
await this.addNotificationJob(NotifType.USER_LOGIN, recipientId, data);
|
||||
}
|
||||
|
||||
async addInvoiceCreationNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.CREATE_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addAnnouncementNotification(recipientId: string, data: IAnnouncementNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ANNOUNCEMENT, recipientId, data);
|
||||
}
|
||||
|
||||
async addAnswerTicketNotification(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ANSWER_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addTicketNotification(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.CREATE_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addAssignTicketNotificationForAdmin(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ASSIGN_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addBillInvoiceReminderNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.BILL_INVOICE_REMINDER, recipientId, data);
|
||||
}
|
||||
|
||||
async addBillInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.BILL_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addApprovedInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.APPROVE_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addInvoiceOverdueNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.INVOICE_OVERDUE, recipientId, data);
|
||||
}
|
||||
|
||||
async addRecurringInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.RECURRING_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addPaymentReminderNotification(recipientId: string, data: IPaymentNotificationData) {
|
||||
await this.addNotificationJob(NotifType.PAYMENT_REMINDER, recipientId, data);
|
||||
}
|
||||
|
||||
async addPaymentCancellationNotification(recipientId: string, data: IPaymentNotificationData) {
|
||||
await this.addNotificationJob(NotifType.PAYMENT_CANCELLATION, recipientId, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||
import { Notification } from "../entities/notification.entity";
|
||||
|
||||
export class NotificationRepository extends EntityRepository<Notification> {
|
||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||
const { skip, limit } = PaginationUtils(queryDto);
|
||||
|
||||
const qb = this.qb("notification").where({ recipientId });
|
||||
|
||||
if (queryDto.isRead !== undefined) {
|
||||
qb.andWhere({ isRead: queryDto.isRead === 1 });
|
||||
}
|
||||
|
||||
return qb.orderBy({ createdAt: "DESC" }).offset(skip).limit(limit).getResultAndCount();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user