Files
dmenu-api/src/modules/review/providers/review.service.ts
T
2025-12-25 11:15:51 +03:30

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);
}
}
}