212 lines
7.2 KiB
TypeScript
212 lines
7.2 KiB
TypeScript
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
|
import { CreateReviewDto } from '../dto/create-review.dto';
|
|
import { UpdateReviewDto } from '../dto/update-review.dto';
|
|
import { FindRestuarantReviewsDto, FindReviewsDto } from '../dto/find-reviews.dto';
|
|
import { ReviewRepository } from '../repositories/review.repository';
|
|
import { FoodRepository } from '../../foods/repositories/food.repository';
|
|
import { UserRepository } from '../../users/repositories/user.repository';
|
|
import { EntityManager } from '@mikro-orm/postgresql';
|
|
import { RequiredEntityData } from '@mikro-orm/core';
|
|
import { Review } from '../entities/review.entity';
|
|
import { FoodMessage, UserMessage, ReviewMessage } from 'src/common/enums/message.enum';
|
|
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 {
|
|
constructor(
|
|
private readonly reviewRepository: ReviewRepository,
|
|
private readonly foodRepository: FoodRepository,
|
|
private readonly userRepository: UserRepository,
|
|
private readonly em: EntityManager,
|
|
private readonly eventEmitter: EventEmitter2,
|
|
) { }
|
|
|
|
async create(userId: string, createReviewDto: CreateReviewDto): Promise<Review> {
|
|
const { foodId, orderId, rating, comment, positivePoints, negativePoints } = createReviewDto;
|
|
|
|
const food = await this.foodRepository.findOne({ id: foodId });
|
|
if (!food) {
|
|
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
|
}
|
|
|
|
const user = await this.userRepository.findOne({ id: userId });
|
|
if (!user) {
|
|
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
|
|
// Verify the food is in the order
|
|
const orderItem = await this.em.findOne(OrderItem, {
|
|
order: { id: orderId },
|
|
food: { id: foodId },
|
|
});
|
|
if (!orderItem) {
|
|
throw new BadRequestException('Food is not in the specified order');
|
|
}
|
|
|
|
// Check if user already commented on this food for this order
|
|
const existingReview = await this.reviewRepository.findOne({
|
|
order: { id: orderId },
|
|
food: { id: foodId },
|
|
user: { id: userId },
|
|
});
|
|
|
|
if (existingReview) {
|
|
throw new BadRequestException(ReviewMessage.ALREADY_COMMENTED);
|
|
}
|
|
|
|
const data: RequiredEntityData<Review> = {
|
|
order,
|
|
food,
|
|
user,
|
|
rating,
|
|
comment: comment || undefined,
|
|
positivePoints: positivePoints || undefined,
|
|
negativePoints: negativePoints || undefined,
|
|
status: ReviewStatus.PENDING,
|
|
};
|
|
|
|
const review = this.reviewRepository.create(data);
|
|
if (!review) {
|
|
throw new BadRequestException('Failed to create review entity');
|
|
}
|
|
|
|
await this.em.persistAndFlush(review);
|
|
|
|
// 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;
|
|
}
|
|
|
|
async findAll(dto: FindReviewsDto) {
|
|
return this.reviewRepository.findAllPaginated(dto);
|
|
}
|
|
|
|
async findRestuarantAllReviews(dto: FindRestuarantReviewsDto) {
|
|
const { restuarantSlug, ...restDto } = dto;
|
|
const restaurant = await this.em.findOne(Restaurant, { slug: restuarantSlug });
|
|
if (!restaurant) {
|
|
throw new NotFoundException('RestaurantMessage.NOT_FOUND');
|
|
}
|
|
return this.reviewRepository.findAllPaginated({ ...restDto, restId: restaurant.id });
|
|
}
|
|
|
|
async findById(id: string, restaurantId: string): Promise<Review> {
|
|
const review = await this.reviewRepository.findOne(
|
|
{ id, order: { restaurant: { id: restaurantId } } },
|
|
{ populate: ['food', 'user', 'order'] },
|
|
);
|
|
if (!review) {
|
|
throw new NotFoundException(ReviewMessage.NOT_FOUND);
|
|
}
|
|
return review;
|
|
}
|
|
|
|
async update(id: string, userId: string, dto: UpdateReviewDto, isAdmin: boolean = false): Promise<Review> {
|
|
const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user', 'order'] });
|
|
if (!review) {
|
|
throw new NotFoundException(ReviewMessage.NOT_FOUND);
|
|
}
|
|
|
|
// Only allow user to update their own reviews (unless admin)
|
|
if (!isAdmin && review.user.id !== userId) {
|
|
throw new BadRequestException(ReviewMessage.CAN_ONLY_UPDATE_OWN);
|
|
}
|
|
|
|
// Users can only update rating and comment, not status
|
|
if (!isAdmin && dto.status !== undefined) {
|
|
throw new BadRequestException('Only admins can change review status');
|
|
}
|
|
|
|
const oldRating = review.rating;
|
|
this.em.assign(review, dto);
|
|
await this.em.persistAndFlush(review);
|
|
|
|
// Update food rating average if rating changed
|
|
if (dto.rating !== undefined && dto.rating !== oldRating) {
|
|
await this.updateFoodRating(review.food.id);
|
|
}
|
|
|
|
return review;
|
|
}
|
|
|
|
async changeStatus(reviewId: string, status: ReviewStatus, restaurantId: string): Promise<Review> {
|
|
const review = await this.reviewRepository.findOne({ id: reviewId, order: { restaurant: { id: restaurantId } } });
|
|
if (!review) {
|
|
throw new NotFoundException(ReviewMessage.NOT_FOUND);
|
|
}
|
|
review.status = status;
|
|
await this.em.persistAndFlush(review);
|
|
return review;
|
|
}
|
|
|
|
async remove(id: string, userId: string, isAdmin: boolean = false) {
|
|
const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user', 'order'] });
|
|
if (!review) {
|
|
throw new NotFoundException(ReviewMessage.NOT_FOUND);
|
|
}
|
|
|
|
// Only allow user to delete their own reviews (unless admin)
|
|
if (!isAdmin && review.user.id !== userId) {
|
|
throw new BadRequestException(ReviewMessage.CAN_ONLY_DELETE_OWN);
|
|
}
|
|
|
|
const foodId = review.food.id;
|
|
review.deletedAt = new Date();
|
|
await this.em.persistAndFlush(review);
|
|
|
|
// Update food rating average
|
|
await this.updateFoodRating(foodId);
|
|
}
|
|
|
|
private async updateFoodRating(foodId: string): Promise<void> {
|
|
const reviews = await this.reviewRepository.find(
|
|
{ food: { id: foodId }, status: ReviewStatus.APPROVED, deletedAt: null },
|
|
{ fields: ['rating'] },
|
|
);
|
|
|
|
if (reviews.length === 0) {
|
|
const food = await this.foodRepository.findOne({ id: foodId });
|
|
if (food) {
|
|
food.score = 0;
|
|
await this.em.persistAndFlush(food);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const averageRating = reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length;
|
|
|
|
const food = await this.foodRepository.findOne({ id: foodId });
|
|
if (food) {
|
|
food.score = parseFloat(averageRating.toFixed(2));
|
|
await this.em.persistAndFlush(food);
|
|
}
|
|
}
|
|
}
|