From 9f7c695ed1ae38eadddfe0a14964cf3e17d8f12f Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 25 Dec 2025 11:15:51 +0330 Subject: [PATCH] review notification --- src/modules/review/events/review.events.ts | 13 ++++ .../review/listeners/review.listeners.ts | 77 +++++++++++++++++++ .../review/providers/review.service.ts | 22 +++++- src/modules/review/review.module.ts | 7 +- 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/modules/review/events/review.events.ts create mode 100644 src/modules/review/listeners/review.listeners.ts diff --git a/src/modules/review/events/review.events.ts b/src/modules/review/events/review.events.ts new file mode 100644 index 0000000..4d6f08d --- /dev/null +++ b/src/modules/review/events/review.events.ts @@ -0,0 +1,13 @@ +export class ReviewCreatedEvent { + constructor( + public readonly reviewId: string, + public readonly restaurantId: string, + public readonly userId: string, + public readonly foodId: string, + public readonly foodName: string, + public readonly orderId: string, + public readonly orderNumber: string | number, + public readonly rating: number, + ) {} +} + diff --git a/src/modules/review/listeners/review.listeners.ts b/src/modules/review/listeners/review.listeners.ts new file mode 100644 index 0000000..69c6a40 --- /dev/null +++ b/src/modules/review/listeners/review.listeners.ts @@ -0,0 +1,77 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReviewCreatedEvent } from '../events/review.events'; +import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; +import { Permission } from 'src/common/enums/permission.enum'; +import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface'; +import { NotificationService } from 'src/modules/notifications/services/notification.service'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class ReviewListeners { + private readonly logger = new Logger(ReviewListeners.name); + private reviewCreatedSmsTemplateId: string; + + constructor( + private readonly adminService: AdminRepository, + private readonly notificationService: NotificationService, + private readonly configService: ConfigService, + ) { + this.reviewCreatedSmsTemplateId = this.configService.get('SMS_PATTERN_REVIEW_CREATED') ?? '123'; + } + + @OnEvent(ReviewCreatedEvent.name) + async handleReviewCreated(event: ReviewCreatedEvent) { + try { + this.logger.log( + `Review created event received: ${event.reviewId} for restaurant: ${event.restaurantId}, food: ${event.foodName}, rating: ${event.rating}`, + ); + + // Get admins of restaurant that have review management permissions + const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_REVIEWS); + const recipients = admins.map(admin => ({ + adminId: admin.id, + })); + + if (recipients.length === 0) { + this.logger.warn(`No admins found with MANAGE_REVIEWS permission for restaurant ${event.restaurantId}`); + return; + } + + await this.notificationService.sendNotification({ + restaurantId: event.restaurantId, + message: { + title: NotifTitleEnum.REVIEW_CREATED, + content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} از 5 برای سفارش شماره ${event.orderNumber} ثبت شد`, + sms: { + templateId: this.reviewCreatedSmsTemplateId, + parameters: { + foodName: event.foodName, + rating: event.rating.toString(), + orderNumber: String(event.orderNumber), + }, + }, + pushNotif: { + title: `نظر جدید`, + content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} ثبت شد`, + icon: `/`, + action: { + type: NotifTitleEnum.REVIEW_CREATED, + url: `/`, + }, + }, + }, + recipients, + metadata: { + priority: 1, + }, + }); + } catch (error) { + this.logger.error( + `Failed to send notification for review created event: ${event.reviewId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } +} + diff --git a/src/modules/review/providers/review.service.ts b/src/modules/review/providers/review.service.ts index cff7811..71f97cb 100644 --- a/src/modules/review/providers/review.service.ts +++ b/src/modules/review/providers/review.service.ts @@ -13,6 +13,8 @@ import { Order } from '../../orders/entities/order.entity'; import { OrderItem } from '../../orders/entities/order-item.entity'; import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; import { ReviewStatus } from '../enums/review-status.enum'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ReviewCreatedEvent } from '../events/review.events'; @Injectable() export class ReviewService { @@ -21,6 +23,7 @@ export class ReviewService { private readonly foodRepository: FoodRepository, private readonly userRepository: UserRepository, private readonly em: EntityManager, + private readonly eventEmitter: EventEmitter2, ) { } async create(userId: string, createReviewDto: CreateReviewDto): Promise { @@ -36,8 +39,8 @@ export class ReviewService { throw new NotFoundException(UserMessage.USER_NOT_FOUND); } - // Find order and verify it belongs to the user - const order = await this.em.findOne(Order, { id: orderId, user: { id: userId } }); + // Find order and verify it belongs to the user, populate restaurant to get restaurantId + const order = await this.em.findOne(Order, { id: orderId, user: { id: userId } }, { populate: ['restaurant'] }); if (!order) { throw new NotFoundException('Order not found or does not belong to the current user'); } @@ -83,6 +86,21 @@ export class ReviewService { // Update food rating average await this.updateFoodRating(foodId); + // Emit ReviewCreatedEvent + this.eventEmitter.emit( + ReviewCreatedEvent.name, + new ReviewCreatedEvent( + review.id, + order.restaurant.id, + userId, + foodId, + food.title || 'Unknown Food', + orderId, + order.orderNumber || '', + rating, + ), + ); + return review; } diff --git a/src/modules/review/review.module.ts b/src/modules/review/review.module.ts index 7596d6e..c76b69d 100644 --- a/src/modules/review/review.module.ts +++ b/src/modules/review/review.module.ts @@ -9,11 +9,14 @@ import { FoodModule } from '../foods/food.module'; import { UserModule } from '../users/user.module'; import { AuthModule } from '../auth/auth.module'; import { JwtModule } from '@nestjs/jwt'; +import { ReviewListeners } from './listeners/review.listeners'; +import { AdminModule } from '../admin/admin.module'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule], + imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule, AdminModule, NotificationsModule], controllers: [ReviewController], - providers: [ReviewService, ReviewRepository, FoodRatingCronService], + providers: [ReviewService, ReviewRepository, FoodRatingCronService, ReviewListeners], exports: [ReviewRepository], }) export class ReviewModule {}