This commit is contained in:
2025-12-09 11:18:56 +03:30
parent aa642bf5b4
commit 53eb7f3ace
9 changed files with 79 additions and 27 deletions
@@ -3,6 +3,7 @@ import { ReviewService } from '../providers/review.service';
import { CreateReviewDto } from '../dto/create-review.dto';
import { UpdateReviewDto } from '../dto/update-review.dto';
import { FindReviewsDto, FindRestuarantReviewsDto } from '../dto/find-reviews.dto';
import { ChangeStatusDto } from '../dto/change-status.dto';
import {
ApiTags,
ApiOperation,
@@ -18,6 +19,7 @@ import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { UserId } from 'src/common/decorators/user-id.decorator';
import { RestId } from 'src/common/decorators/rest-id.decorator';
import { ReviewStatus } from '../enums/review-status.enum';
@ApiTags('review')
@Controller()
@@ -44,7 +46,7 @@ export class ReviewController {
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
findAll(@Query() dto: FindReviewsDto, @Param('foodId') foodId: string) {
// Only show approved reviews for public endpoint
return this.reviewService.findAll({ ...dto, isApproved: true, foodId });
return this.reviewService.findAll({ ...dto, status: ReviewStatus.APPROVED, foodId });
}
@Get('public/reviews/restuarant/:restuarantSlug')
@@ -57,7 +59,7 @@ export class ReviewController {
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
findRestuarantAllReviews(@Query() dto: FindRestuarantReviewsDto, @Param('restuarantSlug') restuarantSlug: string) {
// Only show approved reviews for public endpoint
return this.reviewService.findRestuarantAllReviews({ ...dto, isApproved: true, restuarantSlug });
return this.reviewService.findRestuarantAllReviews({ ...dto, status: ReviewStatus.APPROVED, restuarantSlug });
}
// @Get('public/reviews/:id')
@@ -99,7 +101,7 @@ export class ReviewController {
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'foodId', required: false, type: String })
@ApiQuery({ name: 'userId', required: false, type: String })
@ApiQuery({ name: 'isApproved', required: false, type: Boolean })
@ApiQuery({ name: 'status', required: false, enum: ReviewStatus })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
findAllAdmin(@Query() dto: FindReviewsDto, @RestId() restId: string) {
@@ -116,6 +118,21 @@ export class ReviewController {
return this.reviewService.update(id, userId, updateReviewDto, true);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Patch('admin/reviews/:id/status')
@ApiOperation({ summary: 'Change review status (pending, approved, rejected)' })
@ApiParam({ name: 'id', required: true })
@ApiBody({ type: ChangeStatusDto })
@ApiOkResponse({ description: 'Review status updated successfully' })
changeStatus(
@Param('id') reviewId: string,
@Body() changeStatusDto: ChangeStatusDto,
@RestId() restaurantId: string,
) {
return this.reviewService.changeStatus(reviewId, changeStatusDto.status, restaurantId);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Delete('admin/reviews/:id')
@@ -0,0 +1,14 @@
import { IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { ReviewStatus } from '../enums/review-status.enum';
export class ChangeStatusDto {
@IsEnum(ReviewStatus)
@ApiProperty({
enum: ReviewStatus,
description: 'Review status: pending, approved, or rejected',
example: ReviewStatus.APPROVED,
})
status: ReviewStatus;
}
+5 -5
View File
@@ -1,6 +1,7 @@
import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';
import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ReviewStatus } from '../enums/review-status.enum';
export class FindReviewsDto {
@IsOptional()
@@ -31,10 +32,9 @@ export class FindReviewsDto {
userId?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ description: 'Filter by approval status' })
@Type(() => Boolean)
isApproved?: boolean;
@IsEnum(ReviewStatus)
@ApiPropertyOptional({ description: 'Filter by review status', enum: ReviewStatus })
status?: ReviewStatus;
@IsOptional()
@IsString()
+5 -5
View File
@@ -1,7 +1,8 @@
import { IsBoolean, IsNumber, IsOptional, IsString, Max, Min, IsArray, IsEnum } from 'class-validator';
import { IsNumber, IsOptional, IsString, Max, Min, IsArray, IsEnum } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { PositivePoint, NegativePoint } from '../enums/review-point.enum';
import { ReviewStatus } from '../enums/review-status.enum';
export class UpdateReviewDto {
@IsOptional()
@@ -38,9 +39,8 @@ export class UpdateReviewDto {
negativePoints?: NegativePoint[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ description: 'Approval status (admin only)' })
@Type(() => Boolean)
isApproved?: boolean;
@IsEnum(ReviewStatus)
@ApiPropertyOptional({ description: 'Review status (admin only)', enum: ReviewStatus })
status?: ReviewStatus;
}
+5 -3
View File
@@ -1,9 +1,10 @@
import { Entity, ManyToOne, Property, Unique } from '@mikro-orm/core';
import { Entity, ManyToOne, Property, Unique, Enum } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Food } from '../../foods/entities/food.entity';
import { User } from '../../users/entities/user.entity';
import { Order } from 'src/modules/orders/entities/order.entity';
import { PositivePoint, NegativePoint } from '../enums/review-point.enum';
import { ReviewStatus } from '../enums/review-status.enum';
@Entity({ tableName: 'reviews' })
@Unique({ properties: ['order', 'food', 'user'] })
@@ -29,6 +30,7 @@ export class Review extends BaseEntity {
@Property({ type: 'json', nullable: true })
negativePoints?: NegativePoint[];
@Property({ type: 'boolean', default: false })
isApproved: boolean = false;
@Enum(() => ReviewStatus)
@Property({ type: 'string', default: ReviewStatus.PENDING })
status: ReviewStatus = ReviewStatus.PENDING;
}
@@ -0,0 +1,6 @@
export enum ReviewStatus {
PENDING = 'pending',
REJECTED = 'rejected',
APPROVED = 'approved',
}
@@ -3,6 +3,7 @@ import { Cron } from '@nestjs/schedule';
import { EntityManager } from '@mikro-orm/postgresql';
import { FoodRepository } from '../../foods/repositories/food.repository';
import { ReviewRepository } from '../repositories/review.repository';
import { ReviewStatus } from '../enums/review-status.enum';
@Injectable()
export class FoodRatingCronService {
@@ -41,7 +42,7 @@ export class FoodRatingCronService {
const reviews = await this.reviewRepository.find(
{
food: { id: food.id },
isApproved: true,
status: ReviewStatus.APPROVED,
deletedAt: null,
},
{ fields: ['rating'] },
+16 -5
View File
@@ -12,6 +12,7 @@ import { FoodMessage, UserMessage, ReviewMessage } from 'src/common/enums/messag
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';
@Injectable()
export class ReviewService {
@@ -69,7 +70,7 @@ export class ReviewService {
comment: comment || undefined,
positivePoints: positivePoints || undefined,
negativePoints: negativePoints || undefined,
isApproved: false,
status: ReviewStatus.PENDING,
};
const review = this.reviewRepository.create(data);
@@ -117,9 +118,9 @@ export class ReviewService {
throw new BadRequestException(ReviewMessage.CAN_ONLY_UPDATE_OWN);
}
// Users can only update rating and comment, not isApproved
if (!isAdmin && dto.isApproved !== undefined) {
throw new BadRequestException('Only admins can change approval status');
// 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;
@@ -134,6 +135,16 @@ export class ReviewService {
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) {
@@ -155,7 +166,7 @@ export class ReviewService {
private async updateFoodRating(foodId: string): Promise<void> {
const reviews = await this.reviewRepository.find(
{ food: { id: foodId }, isApproved: true, deletedAt: null },
{ food: { id: foodId }, status: ReviewStatus.APPROVED, deletedAt: null },
{ fields: ['rating'] },
);
@@ -3,13 +3,14 @@ import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { FilterQuery } from '@mikro-orm/core';
import { Review } from '../entities/review.entity';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { ReviewStatus } from '../enums/review-status.enum';
type FindReviewsOpts = {
page?: number;
limit?: number;
foodId?: string;
userId?: string;
isApproved?: boolean;
status?: ReviewStatus;
orderBy?: string;
restId?: string;
order?: 'asc' | 'desc';
@@ -23,10 +24,10 @@ export class ReviewRepository extends EntityRepository<Review> {
/**
* Find reviews with pagination and optional filters.
* Supports: foodId, userId, isApproved, ordering.
* Supports: foodId, userId, status, ordering.
*/
async findAllPaginated(opts: FindReviewsOpts = {}): Promise<PaginatedResult<Review>> {
const { page = 1, limit = 10, foodId, restId, userId, isApproved, orderBy = 'createdAt', order = 'desc' } = opts;
const { page = 1, limit = 10, foodId, restId, userId, status, orderBy = 'createdAt', order = 'desc' } = opts;
const offset = (page - 1) * limit;
@@ -44,8 +45,8 @@ export class ReviewRepository extends EntityRepository<Review> {
where.user = { id: userId };
}
if (typeof isApproved === 'boolean') {
where.isApproved = isApproved;
if (status) {
where.status = status;
}
const [data, total] = await this.findAndCount(where, {