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