From 204b4f2007d9dfcae2c52c036f18d2a723b9439e Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 15 Jul 2026 12:07:58 +0330 Subject: [PATCH] update review entity --- database/migrations/.snapshot-dmenu.json | 467 +++++++++--------- .../Migration20260715083002_task.ts | 24 + src/modules/review/dto/create-review.dto.ts | 5 - src/modules/review/entities/review.entity.ts | 7 +- src/modules/review/events/review.events.ts | 2 - .../review/listeners/review.listeners.ts | 3 +- .../review/providers/review.service.ts | 54 +- 7 files changed, 266 insertions(+), 296 deletions(-) create mode 100644 database/migrations/Migration20260715083002_task.ts diff --git a/database/migrations/.snapshot-dmenu.json b/database/migrations/.snapshot-dmenu.json index 9bdaade..84036b2 100644 --- a/database/migrations/.snapshot-dmenu.json +++ b/database/migrations/.snapshot-dmenu.json @@ -4666,6 +4666,228 @@ "foreignKeys": {}, "nativeEnums": {} }, + { + "columns": { + "id": { + "name": "id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "is_buyer": { + "name": "is_buyer", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "food_id": { + "name": "food_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "user_id": { + "name": "user_id", + "type": "char(26)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 26, + "mappedType": "character" + }, + "comment": { + "name": "comment", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "rating": { + "name": "rating", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" + }, + "positive_points": { + "name": "positive_points", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "negative_points": { + "name": "negative_points", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "status": { + "name": "status", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "'pending'", + "enumItems": [ + "pending", + "rejected", + "approved" + ], + "mappedType": "enum" + } + }, + "name": "reviews", + "schema": "public", + "indexes": [ + { + "keyName": "reviews_created_at_index", + "columnNames": [ + "created_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "reviews_deleted_at_index", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "reviews_user_id_index", + "columnNames": [ + "user_id" + ], + "composite": false, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "reviews_food_id_status_index", + "columnNames": [ + "food_id", + "status" + ], + "composite": true, + "constraint": false, + "primary": false, + "unique": false + }, + { + "keyName": "reviews_food_id_user_id_unique", + "columnNames": [ + "food_id", + "user_id" + ], + "composite": true, + "constraint": true, + "primary": false, + "unique": true + }, + { + "keyName": "reviews_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "reviews_food_id_foreign": { + "constraintName": "reviews_food_id_foreign", + "columnNames": [ + "food_id" + ], + "localTableName": "public.reviews", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.foods", + "updateRule": "cascade" + }, + "reviews_user_id_foreign": { + "constraintName": "reviews_user_id_foreign", + "columnNames": [ + "user_id" + ], + "localTableName": "public.reviews", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "updateRule": "cascade" + } + }, + "nativeEnums": {} + }, { "columns": { "id": { @@ -5501,251 +5723,6 @@ }, "nativeEnums": {} }, - { - "columns": { - "id": { - "name": "id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 26, - "mappedType": "character" - }, - "created_at": { - "name": "created_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 6, - "mappedType": "datetime" - }, - "order_id": { - "name": "order_id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 26, - "mappedType": "character" - }, - "food_id": { - "name": "food_id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 26, - "mappedType": "character" - }, - "user_id": { - "name": "user_id", - "type": "char(26)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 26, - "mappedType": "character" - }, - "comment": { - "name": "comment", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, - "rating": { - "name": "rating", - "type": "int", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "default": "0", - "mappedType": "integer" - }, - "positive_points": { - "name": "positive_points", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" - }, - "negative_points": { - "name": "negative_points", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" - }, - "status": { - "name": "status", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "default": "'pending'", - "enumItems": [ - "pending", - "rejected", - "approved" - ], - "mappedType": "enum" - } - }, - "name": "reviews", - "schema": "public", - "indexes": [ - { - "keyName": "reviews_created_at_index", - "columnNames": [ - "created_at" - ], - "composite": false, - "constraint": false, - "primary": false, - "unique": false - }, - { - "keyName": "reviews_deleted_at_index", - "columnNames": [ - "deleted_at" - ], - "composite": false, - "constraint": false, - "primary": false, - "unique": false - }, - { - "keyName": "reviews_order_id_index", - "columnNames": [ - "order_id" - ], - "composite": false, - "constraint": false, - "primary": false, - "unique": false - }, - { - "keyName": "reviews_user_id_index", - "columnNames": [ - "user_id" - ], - "composite": false, - "constraint": false, - "primary": false, - "unique": false - }, - { - "keyName": "reviews_food_id_status_index", - "columnNames": [ - "food_id", - "status" - ], - "composite": true, - "constraint": false, - "primary": false, - "unique": false - }, - { - "keyName": "reviews_order_id_food_id_user_id_unique", - "columnNames": [ - "order_id", - "food_id", - "user_id" - ], - "composite": true, - "constraint": true, - "primary": false, - "unique": true - }, - { - "keyName": "reviews_pkey", - "columnNames": [ - "id" - ], - "composite": false, - "constraint": true, - "primary": true, - "unique": true - } - ], - "checks": [], - "foreignKeys": { - "reviews_order_id_foreign": { - "constraintName": "reviews_order_id_foreign", - "columnNames": [ - "order_id" - ], - "localTableName": "public.reviews", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.orders", - "updateRule": "cascade" - }, - "reviews_food_id_foreign": { - "constraintName": "reviews_food_id_foreign", - "columnNames": [ - "food_id" - ], - "localTableName": "public.reviews", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.foods", - "updateRule": "cascade" - }, - "reviews_user_id_foreign": { - "constraintName": "reviews_user_id_foreign", - "columnNames": [ - "user_id" - ], - "localTableName": "public.reviews", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.users", - "updateRule": "cascade" - } - }, - "nativeEnums": {} - }, { "columns": { "id": { diff --git a/database/migrations/Migration20260715083002_task.ts b/database/migrations/Migration20260715083002_task.ts new file mode 100644 index 0000000..d55c261 --- /dev/null +++ b/database/migrations/Migration20260715083002_task.ts @@ -0,0 +1,24 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260715083002_task extends Migration { + + override async up(): Promise { + this.addSql(`alter table "reviews" drop constraint "reviews_order_id_foreign";`); + + this.addSql(`drop index "reviews_order_id_index";`); + this.addSql(`alter table "reviews" drop constraint "reviews_order_id_food_id_user_id_unique";`); + + this.addSql(`alter table "reviews" add column "is_buyer" boolean not null default false;`); + this.addSql(`alter table "reviews" add constraint "reviews_food_id_user_id_unique" unique ("food_id", "user_id");`); + } + + override async down(): Promise { + this.addSql(`alter table "reviews" drop constraint "reviews_food_id_user_id_unique";`); + + this.addSql(`alter table "reviews" add column "order_id" char(26) not null;`); + this.addSql(`alter table "reviews" add constraint "reviews_order_id_foreign" foreign key ("order_id") references "orders" ("id") on update cascade;`); + this.addSql(`create index "reviews_order_id_index" on "reviews" ("order_id");`); + this.addSql(`alter table "reviews" add constraint "reviews_order_id_food_id_user_id_unique" unique ("order_id", "food_id", "user_id");`); + } + +} diff --git a/src/modules/review/dto/create-review.dto.ts b/src/modules/review/dto/create-review.dto.ts index 0304d0b..33b07ca 100644 --- a/src/modules/review/dto/create-review.dto.ts +++ b/src/modules/review/dto/create-review.dto.ts @@ -9,11 +9,6 @@ export class CreateReviewDto { @ApiProperty({ description: 'Food ID' }) foodId: string; - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Order ID' }) - orderId: string; - @IsNotEmpty() @IsNumber() @Min(1) diff --git a/src/modules/review/entities/review.entity.ts b/src/modules/review/entities/review.entity.ts index aec0853..1ec6e69 100644 --- a/src/modules/review/entities/review.entity.ts +++ b/src/modules/review/entities/review.entity.ts @@ -7,13 +7,12 @@ import { PositivePoint, NegativePoint } from '../enums/review-point.enum'; import { ReviewStatus } from '../enums/review-status.enum'; @Entity({ tableName: 'reviews' }) -@Unique({ properties: ['order', 'food', 'user'] }) +@Unique({ properties: ['food', 'user'] }) @Index({ properties: ['food', 'status'] }) @Index({ properties: ['user'] }) -@Index({ properties: ['order'] }) export class Review extends BaseEntity { - @ManyToOne(() => Order) - order: Order; + @Property({ type: 'boolean' }) + isBuyer: boolean = false @ManyToOne(() => Food) food: Food; diff --git a/src/modules/review/events/review.events.ts b/src/modules/review/events/review.events.ts index 4d6f08d..53d2ee8 100644 --- a/src/modules/review/events/review.events.ts +++ b/src/modules/review/events/review.events.ts @@ -5,8 +5,6 @@ export class ReviewCreatedEvent { public readonly userId: string, public readonly foodId: string, public readonly foodName: string, - public readonly orderId: string, - public readonly orderNumber: string | number, public readonly rating: number, ) {} } diff --git a/src/modules/review/listeners/review.listeners.ts b/src/modules/review/listeners/review.listeners.ts index 99f374f..200450b 100644 --- a/src/modules/review/listeners/review.listeners.ts +++ b/src/modules/review/listeners/review.listeners.ts @@ -46,13 +46,12 @@ export class ReviewListeners { restaurantId: event.restaurantId, message: { title: NotifTitleEnum.REVIEW_CREATED, - content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} از 5 برای سفارش شماره ${event.orderNumber} ثبت شد`, + content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} از 5 برای سفارش شماره ثبت شد`, sms: { templateId: this.reviewCreatedSmsTemplateId, parameters: { foodName: event.foodName, rating: event.rating.toString(), - orderNumber: String(event.orderNumber), }, }, pushNotif: { diff --git a/src/modules/review/providers/review.service.ts b/src/modules/review/providers/review.service.ts index f498176..724efec 100644 --- a/src/modules/review/providers/review.service.ts +++ b/src/modules/review/providers/review.service.ts @@ -10,12 +10,12 @@ 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'; import { sanitizeText } from '../../utils/sanitize.util'; +import { OrderStatus } from 'src/modules/orders/interface/order.interface'; @Injectable() export class ReviewService { @@ -28,9 +28,9 @@ export class ReviewService { ) { } async create(userId: string, createReviewDto: CreateReviewDto): Promise { - const { foodId, orderId, rating, comment, positivePoints, negativePoints } = createReviewDto; + const { foodId, rating, comment, positivePoints, negativePoints } = createReviewDto; - const food = await this.foodRepository.findOne({ id: foodId }); + const food = await this.foodRepository.findOne({ id: foodId }, { populate: ['restaurant'] }); if (!food) { throw new NotFoundException(FoodMessage.NOT_FOUND); } @@ -40,34 +40,14 @@ export class ReviewService { 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 isBuyer = await this.em.count(Order, { + status: OrderStatus.COMPLETED, + items: { food: { id: foodId } }, + user: { id: userId } + }) > 0; const data: RequiredEntityData = { - order, + isBuyer: true, food, user, rating, @@ -92,12 +72,10 @@ export class ReviewService { ReviewCreatedEvent.name, new ReviewCreatedEvent( review.id, - order.restaurant.id, + food.restaurant.id, userId, foodId, - food.title || 'Unknown Food', - orderId, - order.orderNumber || '', + food.title || '', rating, ), ); @@ -120,8 +98,8 @@ export class ReviewService { async findById(id: string, restaurantId: string): Promise { const review = await this.reviewRepository.findOne( - { id, order: { restaurant: { id: restaurantId } } }, - { populate: ['food', 'user', 'order'] }, + { id, food: { restaurant: { id: restaurantId } } }, + { populate: ['food', 'user'] }, ); if (!review) { throw new NotFoundException(ReviewMessage.NOT_FOUND); @@ -130,7 +108,7 @@ export class ReviewService { } async update(id: string, userId: string, dto: UpdateReviewDto, isAdmin: boolean = false): Promise { - const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user', 'order'] }); + const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user',] }); if (!review) { throw new NotFoundException(ReviewMessage.NOT_FOUND); } @@ -165,7 +143,7 @@ export class ReviewService { } async changeStatus(reviewId: string, status: ReviewStatus, restaurantId: string): Promise { - const review = await this.reviewRepository.findOne({ id: reviewId, order: { restaurant: { id: restaurantId } } }); + const review = await this.reviewRepository.findOne({ id: reviewId, food: { restaurant: { id: restaurantId } } }); if (!review) { throw new NotFoundException(ReviewMessage.NOT_FOUND); } @@ -175,7 +153,7 @@ export class ReviewService { } 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'] }); if (!review) { throw new NotFoundException(ReviewMessage.NOT_FOUND); }