review notification
This commit is contained in:
@@ -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,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<string>('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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ import { Order } from '../../orders/entities/order.entity';
|
|||||||
import { OrderItem } from '../../orders/entities/order-item.entity';
|
import { OrderItem } from '../../orders/entities/order-item.entity';
|
||||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||||
import { ReviewStatus } from '../enums/review-status.enum';
|
import { ReviewStatus } from '../enums/review-status.enum';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { ReviewCreatedEvent } from '../events/review.events';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReviewService {
|
export class ReviewService {
|
||||||
@@ -21,6 +23,7 @@ export class ReviewService {
|
|||||||
private readonly foodRepository: FoodRepository,
|
private readonly foodRepository: FoodRepository,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async create(userId: string, createReviewDto: CreateReviewDto): Promise<Review> {
|
async create(userId: string, createReviewDto: CreateReviewDto): Promise<Review> {
|
||||||
@@ -36,8 +39,8 @@ export class ReviewService {
|
|||||||
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find order and verify it belongs to the user
|
// 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 } });
|
const order = await this.em.findOne(Order, { id: orderId, user: { id: userId } }, { populate: ['restaurant'] });
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new NotFoundException('Order not found or does not belong to the current user');
|
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
|
// Update food rating average
|
||||||
await this.updateFoodRating(foodId);
|
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;
|
return review;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ import { FoodModule } from '../foods/food.module';
|
|||||||
import { UserModule } from '../users/user.module';
|
import { UserModule } from '../users/user.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { ReviewListeners } from './listeners/review.listeners';
|
||||||
|
import { AdminModule } from '../admin/admin.module';
|
||||||
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule],
|
imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule, AdminModule, NotificationsModule],
|
||||||
controllers: [ReviewController],
|
controllers: [ReviewController],
|
||||||
providers: [ReviewService, ReviewRepository, FoodRatingCronService],
|
providers: [ReviewService, ReviewRepository, FoodRatingCronService, ReviewListeners],
|
||||||
exports: [ReviewRepository],
|
exports: [ReviewRepository],
|
||||||
})
|
})
|
||||||
export class ReviewModule {}
|
export class ReviewModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user