review
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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'] },
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user