From 53eb7f3ace3b91979a7d0f02a1a1d33d2a6cdd31 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 9 Dec 2025 11:18:56 +0330 Subject: [PATCH] review --- .../review/controllers/review.controller.ts | 23 ++++++++++++++++--- src/modules/review/dto/change-status.dto.ts | 14 +++++++++++ src/modules/review/dto/find-reviews.dto.ts | 10 ++++---- src/modules/review/dto/update-review.dto.ts | 10 ++++---- src/modules/review/entities/review.entity.ts | 8 ++++--- .../review/enums/review-status.enum.ts | 6 +++++ .../providers/food-rating-cron.service.ts | 3 ++- .../review/providers/review.service.ts | 21 +++++++++++++---- .../review/repositories/review.repository.ts | 11 +++++---- 9 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 src/modules/review/dto/change-status.dto.ts create mode 100644 src/modules/review/enums/review-status.enum.ts diff --git a/src/modules/review/controllers/review.controller.ts b/src/modules/review/controllers/review.controller.ts index ab7168e..b4dccf2 100644 --- a/src/modules/review/controllers/review.controller.ts +++ b/src/modules/review/controllers/review.controller.ts @@ -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') diff --git a/src/modules/review/dto/change-status.dto.ts b/src/modules/review/dto/change-status.dto.ts new file mode 100644 index 0000000..f5c0198 --- /dev/null +++ b/src/modules/review/dto/change-status.dto.ts @@ -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; +} + diff --git a/src/modules/review/dto/find-reviews.dto.ts b/src/modules/review/dto/find-reviews.dto.ts index 202a3a0..b4a0377 100644 --- a/src/modules/review/dto/find-reviews.dto.ts +++ b/src/modules/review/dto/find-reviews.dto.ts @@ -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() diff --git a/src/modules/review/dto/update-review.dto.ts b/src/modules/review/dto/update-review.dto.ts index 206b88e..b815f5b 100644 --- a/src/modules/review/dto/update-review.dto.ts +++ b/src/modules/review/dto/update-review.dto.ts @@ -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; } diff --git a/src/modules/review/entities/review.entity.ts b/src/modules/review/entities/review.entity.ts index b5c4a6a..228e0b2 100644 --- a/src/modules/review/entities/review.entity.ts +++ b/src/modules/review/entities/review.entity.ts @@ -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; } diff --git a/src/modules/review/enums/review-status.enum.ts b/src/modules/review/enums/review-status.enum.ts new file mode 100644 index 0000000..7349b39 --- /dev/null +++ b/src/modules/review/enums/review-status.enum.ts @@ -0,0 +1,6 @@ +export enum ReviewStatus { + PENDING = 'pending', + REJECTED = 'rejected', + APPROVED = 'approved', +} + diff --git a/src/modules/review/providers/food-rating-cron.service.ts b/src/modules/review/providers/food-rating-cron.service.ts index 27a9515..feacede 100644 --- a/src/modules/review/providers/food-rating-cron.service.ts +++ b/src/modules/review/providers/food-rating-cron.service.ts @@ -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'] }, diff --git a/src/modules/review/providers/review.service.ts b/src/modules/review/providers/review.service.ts index 0f7aa4b..c039945 100644 --- a/src/modules/review/providers/review.service.ts +++ b/src/modules/review/providers/review.service.ts @@ -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 { + 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 { const reviews = await this.reviewRepository.find( - { food: { id: foodId }, isApproved: true, deletedAt: null }, + { food: { id: foodId }, status: ReviewStatus.APPROVED, deletedAt: null }, { fields: ['rating'] }, ); diff --git a/src/modules/review/repositories/review.repository.ts b/src/modules/review/repositories/review.repository.ts index 0c6e61c..9b3abaf 100644 --- a/src/modules/review/repositories/review.repository.ts +++ b/src/modules/review/repositories/review.repository.ts @@ -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 { /** * Find reviews with pagination and optional filters. - * Supports: foodId, userId, isApproved, ordering. + * Supports: foodId, userId, status, ordering. */ async findAllPaginated(opts: FindReviewsOpts = {}): Promise> { - 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 { where.user = { id: userId }; } - if (typeof isApproved === 'boolean') { - where.isApproved = isApproved; + if (status) { + where.status = status; } const [data, total] = await this.findAndCount(where, {