From 786ea465ff9730a79afd0716115c6a40a990c384 Mon Sep 17 00:00:00 2001 From: Matin Date: Sat, 8 Feb 2025 11:56:56 +0330 Subject: [PATCH] chore: implement notification module --- src/app.module.ts | 2 + src/common/enums/message.enum.ts | 14 ++++ src/modules/auth/auth.module.ts | 3 +- src/modules/auth/providers/auth.service.ts | 5 ++ .../DTO/create-notification.dto.ts | 18 +++++ .../entities/notification.entity.ts | 32 +++++++-- .../enums/notification-type.enum.ts | 5 ++ .../notifications/notifications.controller.ts | 34 ++++++++++ .../notifications/notifications.module.ts | 17 +++++ .../providers/notifications.service.ts | 65 +++++++++++++++++++ .../repositories/notifications.repository.ts | 12 ++++ src/modules/users/entities/user.entity.ts | 5 ++ 12 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 src/modules/notifications/DTO/create-notification.dto.ts create mode 100644 src/modules/notifications/enums/notification-type.enum.ts create mode 100644 src/modules/notifications/notifications.controller.ts create mode 100644 src/modules/notifications/notifications.module.ts create mode 100644 src/modules/notifications/providers/notifications.service.ts create mode 100644 src/modules/notifications/repositories/notifications.repository.ts diff --git a/src/app.module.ts b/src/app.module.ts index 8110764..f7e7dad 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,6 +17,7 @@ import { AuthModule } from "./modules/auth/auth.module"; import { ContactUsModule } from "./modules/contact-us/contact-us.module"; import { CriticismModule } from "./modules/criticisms/criticisms.module"; import { DanakServicesModule } from "./modules/danak-services/danak-services.module"; +import { NotificationModule } from "./modules/notifications/notifications.module"; import { PaymentsModule } from "./modules/payments/payments.module"; import { SettingModule } from "./modules/settings/settings.module"; import { TicketsModule } from "./modules/tickets/tickets.module"; @@ -44,6 +45,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; ContactUsModule, WalletsModule, PaymentsModule, + NotificationModule, ], controllers: [], providers: [], diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 5a793e2..a8f0bd1 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -245,3 +245,17 @@ export const enum ContactUsMessage { TITLE_STRING = "عنوان باید یک رشته باشد", NOT_FOUND = "پیامی با این ایدی یافت نشد", } + +export const enum NotificationMessage { + NOT_FOUNT = "اعلان مورد نظر یافت نشد", + TITLE_IS_REQUIRED = "عنوان الزامی است", + TITLE_STRING = "عنوان باید یک رشته متنی باشد", + MESSAGE_IS_REQUIRED = "پیام الزامی است", + MESSAGE_STRING = "پیام باید یک رشته متنی باشد", + USERID_IS_REQUIRED = "ایدی کاربر الزامی است", + USERID_IS_UUID = "شناسه کاربر باید یک UUID معتبر باشد", + USERID_IS_STRING = "شناسه کاربر باید یک رشته متنی باشد", + LOGIN = "لاگین", + LOGIN_MESSAGE = "در تاریخ [DATE] یک لاگین به حساب کاربری شما صورت گرفت", + NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی غیرمجاز است", +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 7e96ebf..4621873 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -9,9 +9,10 @@ import { UtilsModule } from "../utils/utils.module"; import { AuthService } from "./providers/auth.service"; import { TokensService } from "./providers/tokens.service"; import { JwtStrategy } from "./strategies/jwt.strategy"; +import { NotificationModule } from "../notifications/notifications.module"; @Module({ - imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig())], + imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), NotificationModule], controllers: [AuthController], providers: [AuthService, TokensService, JwtStrategy], exports: [AuthService], diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index 991c0f4..cf7bb31 100644 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -3,6 +3,7 @@ import { DataSource } from "typeorm"; import { TokensService } from "./tokens.service"; import { AuthMessage, UserMessage } from "../../../common/enums/message.enum"; +import { NotificationsService } from "../../notifications/providers/notifications.service"; import { RoleEnum } from "../../users/enums/role.enum"; import { UsersService } from "../../users/providers/users.service"; import { OTPService } from "../../utils/providers/otp.service"; @@ -22,6 +23,7 @@ export class AuthService { private readonly otpService: OTPService, private readonly tokensService: TokensService, private readonly dataSource: DataSource, + private readonly notificationService: NotificationsService, ) {} //****************** */ //****************** */ @@ -156,6 +158,9 @@ export class AuthService { const user = await this.checkUserLoginCredentialWithPhone(phone, code); const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name }); + + const { notification } = await this.notificationService.createLoginNotification(user.id); + console.log(notification); return { message: AuthMessage.LOGIN_SUCCESS, ...tokens, diff --git a/src/modules/notifications/DTO/create-notification.dto.ts b/src/modules/notifications/DTO/create-notification.dto.ts new file mode 100644 index 0000000..398e8ae --- /dev/null +++ b/src/modules/notifications/DTO/create-notification.dto.ts @@ -0,0 +1,18 @@ +import { IsNotEmpty, IsString, IsUUID } from "class-validator"; + +import { NotificationMessage } from "../../../common/enums/message.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("4", { message: NotificationMessage.USERID_IS_UUID }) + recipientId: string; +} diff --git a/src/modules/notifications/entities/notification.entity.ts b/src/modules/notifications/entities/notification.entity.ts index b636da0..13cab27 100644 --- a/src/modules/notifications/entities/notification.entity.ts +++ b/src/modules/notifications/entities/notification.entity.ts @@ -1,7 +1,27 @@ -// import { Column, Entity } from "typeorm"; -// import { BaseEntity } from "../../../common/entities/base.entity"; +import { Column, Entity, ManyToOne } from "typeorm"; -// @Entity() -// export class Notification extends BaseEntity { -// @Column({ type: "" }) -// } +import { BaseEntity } from "../../../common/entities/base.entity"; +import { User } from "../../users/entities/user.entity"; +import { NotificationType } from "../enums/notification-type.enum"; + +@Entity() +export class Notification extends BaseEntity { + @Column({ type: "varchar", length: 250 }) + title: string; + + @Column({ type: "text" }) + message: string; + + @Column({ + type: "enum", + enum: NotificationType, + default: NotificationType.LOGIN, + }) + type: NotificationType; + + @Column({ type: "boolean", default: false }) + isRead: boolean; + + @ManyToOne(() => User, (user) => user.notifications, { onDelete: "CASCADE", eager: true }) + recipient: User; +} diff --git a/src/modules/notifications/enums/notification-type.enum.ts b/src/modules/notifications/enums/notification-type.enum.ts new file mode 100644 index 0000000..a90fbd5 --- /dev/null +++ b/src/modules/notifications/enums/notification-type.enum.ts @@ -0,0 +1,5 @@ +export enum NotificationType { + LOGIN = "LOGIN", + PASSWORD_CAHNGED = "PASSWORD_CHANGED", + WALLET_CHARGED = "WALLER_CHARGED", +} diff --git a/src/modules/notifications/notifications.controller.ts b/src/modules/notifications/notifications.controller.ts new file mode 100644 index 0000000..d669cd7 --- /dev/null +++ b/src/modules/notifications/notifications.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get, Param, Patch, Query } from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; + +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 { PaginationDto } from "../../common/DTO/pagination.dto"; + +@Controller("notifications") +@ApiTags("Notifications") +export class NotificationController { + constructor(private readonly notificationService: NotificationsService) {} + + //********************* */ + + @ApiOperation({ summary: "all notifications by user" }) + @Pagination() + @AuthGuards() + @Get() + getAllNotifications(@UserDec("id") userId: string, @Query() paginationDto: PaginationDto) { + console.log(userId); + return this.notificationService.getAllNotifications(paginationDto, userId); + } + + //********************* */ + + @ApiOperation({ summary: "mark user notification as read" }) + @AuthGuards() + @Patch(":id/read") + markAsRead(@Param("id") id: string, @UserDec("id") userId: string) { + return this.notificationService.markAsRead(id, userId); + } +} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts new file mode 100644 index 0000000..2d5030d --- /dev/null +++ b/src/modules/notifications/notifications.module.ts @@ -0,0 +1,17 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { Notification } from "./entities/notification.entity"; +import { NotificationController } from "./notifications.controller"; +import { NotificationsService } from "./providers/notifications.service"; +import { NotificationRepository } from "./repositories/notifications.repository"; +import { User } from "../users/entities/user.entity"; +import { UsersModule } from "../users/users.module"; + +@Module({ + imports: [TypeOrmModule.forFeature([Notification, User]), UsersModule], + providers: [NotificationRepository, NotificationsService], + controllers: [NotificationController], + exports: [NotificationRepository, NotificationsService], +}) +export class NotificationModule {} diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts new file mode 100644 index 0000000..a5a193d --- /dev/null +++ b/src/modules/notifications/providers/notifications.service.ts @@ -0,0 +1,65 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; + +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { NotificationMessage, UserMessage } from "../../../common/enums/message.enum"; +import { UsersService } from "../../users/providers/users.service"; +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { CreateNotificationDto } from "../DTO/create-notification.dto"; +import { NotificationRepository } from "../repositories/notifications.repository"; + +@Injectable() +export class NotificationsService { + constructor( + private readonly notificationRepository: NotificationRepository, + private readonly usersService: UsersService, + ) {} + + //************************ */ + + async createNotification(createNotificationDto: CreateNotificationDto) { + const { user } = await this.usersService.findOneById(createNotificationDto.recipientId); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + const notification = this.notificationRepository.create({ + ...createNotificationDto, + recipient: user, + }); + await this.notificationRepository.save(notification); + + return { notification }; + } + + //************************ */ + + async markAsRead(notificationId: string, userId: string) { + const notification = await this.notificationRepository.findOne({ + where: { id: notificationId, recipient: { id: userId } }, + }); + + if (!notification) throw new NotFoundException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED); + + notification.isRead = true; + return this.notificationRepository.save(notification); + } + + //************************ */ + + async createLoginNotification(recipientId: string) { + const message = NotificationMessage.LOGIN_MESSAGE.replace("[DATE]", new Date().toLocaleString("fa-IR")); + return this.createNotification({ title: NotificationMessage.LOGIN, message, recipientId }); + } + + //************************ */ + + async getAllNotifications(paginationDto: PaginationDto, recipientId: string) { + const { skip, limit } = PaginationUtils(paginationDto); + const [notifications, count] = await this.notificationRepository + .createQueryBuilder("notification") + .where("notification.recipientId = :recipientId", { recipientId }) + .orderBy("notification.createdAt", "DESC") + .skip(skip) + .take(limit) + .getManyAndCount(); + + return { notifications, count, paginate: true }; + } +} diff --git a/src/modules/notifications/repositories/notifications.repository.ts b/src/modules/notifications/repositories/notifications.repository.ts new file mode 100644 index 0000000..eaef556 --- /dev/null +++ b/src/modules/notifications/repositories/notifications.repository.ts @@ -0,0 +1,12 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { Notification } from "../entities/notification.entity"; + +@Injectable() +export class NotificationRepository extends Repository { + constructor(@InjectRepository(Notification) notificationRepository: Repository) { + super(notificationRepository.target, notificationRepository.manager, notificationRepository.queryRunner); + } +} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index c069037..6a64771 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -7,6 +7,7 @@ import { BaseEntity } from "../../../common/entities/base.entity"; import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity"; import { Criticism } from "../../criticisms/entities/criticism.entity"; import { DanakService } from "../../danak-services/entities/danak-service.entity"; +import { Notification } from "../../notifications/entities/notification.entity"; import { Payment } from "../../payments/entities/payment.entity"; import { UserSetting } from "../../settings/entities/user-setting.entity"; import { TicketMessage } from "../../tickets/entities/ticket-message.entity"; @@ -64,9 +65,13 @@ export class User extends BaseEntity { @OneToMany(() => UserSetting, (settings) => settings.user, { nullable: false }) settings: UserSetting; + @OneToOne(() => Wallet, (wallet) => wallet.user) wallet: Wallet; @OneToMany(() => Payment, (payment) => payment.user) payments: Payment[]; + + @OneToMany(() => Notification, (notification) => notification.recipient) + notifications: Notification[]; }