update review entity
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-15 12:07:58 +03:30
parent a05643daf5
commit 204b4f2007
7 changed files with 266 additions and 296 deletions
+222 -245
View File
@@ -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": {
@@ -0,0 +1,24 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260715083002_task extends Migration {
override async up(): Promise<void> {
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<void> {
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");`);
}
}
@@ -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)
+3 -4
View File
@@ -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;
@@ -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,
) {}
}
@@ -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: {
+16 -38
View File
@@ -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<Review> {
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<Review> = {
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<Review> {
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<Review> {
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<Review> {
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);
}