This commit is contained in:
2025-11-25 11:26:33 +03:30
parent d737b3dc24
commit e21abda0ce
7 changed files with 67 additions and 69 deletions
@@ -64,8 +64,8 @@ export class FoodController {
@ApiParam({ name: 'id', required: true }) @ApiParam({ name: 'id', required: true })
@ApiOkResponse({ description: 'The food', type: CreateFoodDto }) @ApiOkResponse({ description: 'The food', type: CreateFoodDto })
@ApiNotFoundResponse({ description: 'Food not found' }) @ApiNotFoundResponse({ description: 'Food not found' })
findOne(@Param('id') id: string) { findById(@Param('id') id: string) {
return this.foodsService.findOne(id); return this.foodsService.findById(id);
} }
@UseGuards(AdminAuthGuard) @UseGuards(AdminAuthGuard)
+9 -5
View File
@@ -1,8 +1,12 @@
import { IsBoolean, IsOptional, IsString, IsNumber, IsArray } from 'class-validator'; import { IsBoolean, IsOptional, IsString, IsNumber, IsArray, IsNotEmpty } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
export class CreateFoodDto { export class CreateFoodDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
categoryId: string;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false }) @ApiPropertyOptional({ example: false })
@@ -92,7 +96,7 @@ export class CreateFoodDto {
@ApiPropertyOptional({ example: true }) @ApiPropertyOptional({ example: true })
@Type(() => Boolean) @Type(() => Boolean)
dinner?: boolean; dinner?: boolean;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number) @Type(() => Number)
@@ -128,13 +132,13 @@ export class CreateFoodDto {
@ApiPropertyOptional({ example: false }) @ApiPropertyOptional({ example: false })
@Type(() => Boolean) @Type(() => Boolean)
pickupServe?: boolean; pickupServe?: boolean;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number) @Type(() => Number)
@ApiPropertyOptional({ example: 0 }) @ApiPropertyOptional({ example: 0 })
discount?: number; discount?: number;
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })
@@ -1,4 +1,4 @@
import { Entity, Property, ManyToMany, Collection } from '@mikro-orm/core'; import { Entity, Property, Collection, OneToMany } from '@mikro-orm/core';
import { Food } from './food.entity'; import { Food } from './food.entity';
import { BaseEntity } from '../../../common/entities/base.entity'; import { BaseEntity } from '../../../common/entities/base.entity';
@@ -7,7 +7,7 @@ export class Category extends BaseEntity {
@Property() @Property()
title!: string; title!: string;
@ManyToMany(() => Food, food => food.categories) @OneToMany(() => Food, food => food.category)
foods = new Collection<Food>(this); foods = new Collection<Food>(this);
@Property({ default: true }) @Property({ default: true })
+3 -3
View File
@@ -1,4 +1,4 @@
import { Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; import { Entity, ManyToOne, Property } from '@mikro-orm/core';
import { Category } from './category.entity'; import { Category } from './category.entity';
import { BaseEntity } from '../../../common/entities/base.entity'; import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
@@ -8,8 +8,8 @@ export class Food extends BaseEntity {
@ManyToOne(() => Restaurant) @ManyToOne(() => Restaurant)
restaurant: Restaurant; restaurant: Restaurant;
@ManyToMany(() => Category) @ManyToOne(() => Category)
categories = new Collection<Category>(this); category!: Category;
@Property({ nullable: true }) @Property({ nullable: true })
title?: string; title?: string;
+23 -27
View File
@@ -6,7 +6,7 @@ import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData } from '@mikro-orm/core'; import { RequiredEntityData } from '@mikro-orm/core';
import { Food } from '../entities/food.entity'; import { Food } from '../entities/food.entity';
import { FoodMessage, RestMessage } from 'src/common/enums/message.enum'; import { CategoryMessage, FoodMessage, RestMessage } from 'src/common/enums/message.enum';
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository'; import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
@Injectable() @Injectable()
@@ -19,11 +19,16 @@ export class FoodService {
) {} ) {}
async create(restId: string, createFoodDto: CreateFoodDto): Promise<Food> { async create(restId: string, createFoodDto: CreateFoodDto): Promise<Food> {
const { categoryIds, ...rest } = createFoodDto; const { categoryId, ...rest } = createFoodDto;
const restaurant = await this.restRepository.findOne({ id: restId }); const restaurant = await this.restRepository.findOne({ id: restId });
if (!restaurant) { if (!restaurant) {
throw new NotFoundException(RestMessage.NOT_FOUND); throw new NotFoundException(RestMessage.NOT_FOUND);
} }
const category = await this.categoryRepository.findOne({ id: categoryId });
if (!category) {
throw new NotFoundException(CategoryMessage.NOT_FOUND);
}
// prepare data with defaults for required fields so repository.create typing is satisfied // prepare data with defaults for required fields so repository.create typing is satisfied
const data: RequiredEntityData<Food> = { const data: RequiredEntityData<Food> = {
// boolean/day flags // boolean/day flags
@@ -49,6 +54,7 @@ export class FoodService {
prepareTime: rest.prepareTime ?? 0, prepareTime: rest.prepareTime ?? 0,
images: rest.images ?? undefined, images: rest.images ?? undefined,
restaurant: restaurant, restaurant: restaurant,
category: category
}; };
const food = this.foodRepository.create(data); const food = this.foodRepository.create(data);
@@ -56,12 +62,6 @@ export class FoodService {
throw new Error('Failed to create food entity'); throw new Error('Failed to create food entity');
} }
// attach categories if provided
if (categoryIds && categoryIds.length) {
const categories = await this.categoryRepository.find({ id: { $in: categoryIds } });
categories.forEach(cat => food.categories.add(cat));
}
await this.em.persistAndFlush(food); await this.em.persistAndFlush(food);
return food; return food;
} }
@@ -70,40 +70,36 @@ export class FoodService {
return this.foodRepository.findAllPaginated(restId, dto); return this.foodRepository.findAllPaginated(restId, dto);
} }
async findById(id: string): Promise<Food> {
const food = await this.foodRepository.findOne({ id }, { populate: ['category'] });
if (!food) {
throw new NotFoundException(FoodMessage.NOT_FOUND);
}
return food;
}
async findAllByRestaurant(slug: string): Promise<Food[]> { async findAllByRestaurant(slug: string): Promise<Food[]> {
const restaurant = await this.restRepository.findOne({ slug }); const restaurant = await this.restRepository.findOne({ slug });
if (!restaurant) { if (!restaurant) {
throw new NotFoundException(RestMessage.NOT_FOUND); throw new NotFoundException(RestMessage.NOT_FOUND);
} }
return this.foodRepository.find({ restaurant: { slug } }); return this.foodRepository.find({ restaurant: { slug }, isActive: true }, { populate: ['category'] });
}
findOne(id: string) {
return this.foodRepository.findOne({ id }, { populate: ['categories'] });
} }
async update(id: string, dto: Partial<CreateFoodDto>): Promise<Food> { async update(id: string, dto: Partial<CreateFoodDto>): Promise<Food> {
const food = await this.foodRepository.findOne({ id }); const food = await this.foodRepository.findOne({ id });
if (!food) { if (!food) {
throw new NotFoundException(FoodMessage.NOT_FOUND); throw new NotFoundException(FoodMessage.NOT_FOUND);
} }
const { categoryIds, ...rest } = dto;
// assign scalar fields
this.em.assign(food, rest as Partial<Food>);
// attach new categories if provided (adds, does not attempt to preserve/remove existing ones) // attach new categories if provided (adds, does not attempt to preserve/remove existing ones)
if (Array.isArray(categoryIds) && categoryIds.length) { if (dto.categoryId) {
const categories = await this.categoryRepository.find({ id: { $in: categoryIds } }); const category = await this.categoryRepository.findOne({ id: dto.categoryId });
food.categories.removeAll(); if (!category) {
throw new NotFoundException(CategoryMessage.NOT_FOUND);
}
categories.forEach(cat => { this.em.assign(food, { category: category });
// Collection.add will ignore duplicates
food.categories.add(cat);
});
} }
await this.em.persistAndFlush(food); await this.em.persistAndFlush(food);
@@ -48,7 +48,7 @@ export class FoodRepository extends EntityRepository<Food> {
limit, limit,
offset, offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['categories'], populate: ['category'],
}); });
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
+27 -29
View File
@@ -271,7 +271,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'کباب کوبیده خوشمزه با برنج و کره', desc: 'کباب کوبیده خوشمزه با برنج و کره',
price: 150000, price: 150000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[0]], // غذای اصلی category: createdCategories[0], // غذای اصلی
isActive: true, isActive: true,
stock: 50, stock: 50,
stockDefault: 50, stockDefault: 50,
@@ -281,7 +281,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'جوجه کباب تازه با برنج', desc: 'جوجه کباب تازه با برنج',
price: 120000, price: 120000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[0]], // غذای اصلی category: createdCategories[0], // غذای اصلی
isActive: true, isActive: true,
stock: 40, stock: 40,
stockDefault: 40, stockDefault: 40,
@@ -291,7 +291,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'کباب بختیاری با برنج و کره', desc: 'کباب بختیاری با برنج و کره',
price: 180000, price: 180000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[0]], // غذای اصلی category: createdCategories[0], // غذای اصلی
isActive: true, isActive: true,
stock: 35, stock: 35,
stockDefault: 35, stockDefault: 35,
@@ -301,7 +301,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'کباب برگ مرغوب با برنج', desc: 'کباب برگ مرغوب با برنج',
price: 160000, price: 160000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[0]], // غذای اصلی category: createdCategories[0], // غذای اصلی
isActive: true, isActive: true,
stock: 30, stock: 30,
stockDefault: 30, stockDefault: 30,
@@ -311,7 +311,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'سالاد تازه با سس مخصوص', desc: 'سالاد تازه با سس مخصوص',
price: 35000, price: 35000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[1]], // پیش غذا category: createdCategories[1], // پیش غذا
isActive: true, isActive: true,
stock: 30, stock: 30,
stockDefault: 30, stockDefault: 30,
@@ -321,7 +321,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'ماست و خیار تازه', desc: 'ماست و خیار تازه',
price: 25000, price: 25000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[1]], // پیش غذا category: createdCategories[1], // پیش غذا
isActive: true, isActive: true,
stock: 25, stock: 25,
stockDefault: 25, stockDefault: 25,
@@ -331,7 +331,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'ترشی مخلوط خانگی', desc: 'ترشی مخلوط خانگی',
price: 20000, price: 20000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[1]], // پیش غذا category: createdCategories[1], // پیش غذا
isActive: true, isActive: true,
stock: 20, stock: 20,
stockDefault: 20, stockDefault: 20,
@@ -341,7 +341,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'بستنی وانیلی خوشمزه', desc: 'بستنی وانیلی خوشمزه',
price: 25000, price: 25000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[2]], // دسر category: createdCategories[2], // دسر
isActive: true, isActive: true,
stock: 20, stock: 20,
stockDefault: 20, stockDefault: 20,
@@ -351,7 +351,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'شیرینی محلی', desc: 'شیرینی محلی',
price: 30000, price: 30000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[2]], // دسر category: createdCategories[2], // دسر
isActive: true, isActive: true,
stock: 15, stock: 15,
stockDefault: 15, stockDefault: 15,
@@ -361,7 +361,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'نوشابه گازدار', desc: 'نوشابه گازدار',
price: 15000, price: 15000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[3]], // نوشیدنی category: createdCategories[3], // نوشیدنی
isActive: true, isActive: true,
stock: 100, stock: 100,
stockDefault: 100, stockDefault: 100,
@@ -371,7 +371,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'دوغ محلی', desc: 'دوغ محلی',
price: 20000, price: 20000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[3]], // نوشیدنی category: createdCategories[3], // نوشیدنی
isActive: true, isActive: true,
stock: 50, stock: 50,
stockDefault: 50, stockDefault: 50,
@@ -381,7 +381,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'آب معدنی', desc: 'آب معدنی',
price: 10000, price: 10000,
restaurant: zhivan, restaurant: zhivan,
categories: [createdCategories[3]], // نوشیدنی category: createdCategories[3], // نوشیدنی
isActive: true, isActive: true,
stock: 80, stock: 80,
stockDefault: 80, stockDefault: 80,
@@ -392,7 +392,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'پیتزا با پنیر و قارچ', desc: 'پیتزا با پنیر و قارچ',
price: 180000, price: 180000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[4]], // غذای اصلی category: createdCategories[4], // غذای اصلی
isActive: true, isActive: true,
stock: 30, stock: 30,
stockDefault: 30, stockDefault: 30,
@@ -402,7 +402,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'پیتزا پپرونی تند', desc: 'پیتزا پپرونی تند',
price: 200000, price: 200000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[4]], // غذای اصلی category: createdCategories[4], // غذای اصلی
isActive: true, isActive: true,
stock: 25, stock: 25,
stockDefault: 25, stockDefault: 25,
@@ -412,7 +412,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'برگر با سیب زمینی سرخ کرده', desc: 'برگر با سیب زمینی سرخ کرده',
price: 140000, price: 140000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[4]], // غذای اصلی category: createdCategories[4], // غذای اصلی
isActive: true, isActive: true,
stock: 25, stock: 25,
stockDefault: 25, stockDefault: 25,
@@ -422,7 +422,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'برگر دوبل با پنیر', desc: 'برگر دوبل با پنیر',
price: 180000, price: 180000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[4]], // غذای اصلی category: createdCategories[4], // غذای اصلی
isActive: true, isActive: true,
stock: 20, stock: 20,
stockDefault: 20, stockDefault: 20,
@@ -432,7 +432,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'سوپ گرم و خوشمزه', desc: 'سوپ گرم و خوشمزه',
price: 45000, price: 45000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[5]], // پیش غذا category: createdCategories[5], // پیش غذا
isActive: true, isActive: true,
stock: 15, stock: 15,
stockDefault: 15, stockDefault: 15,
@@ -442,7 +442,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'سالاد سزار با سس مخصوص', desc: 'سالاد سزار با سس مخصوص',
price: 55000, price: 55000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[5]], // پیش غذا category: createdCategories[5], // پیش غذا
isActive: true, isActive: true,
stock: 20, stock: 20,
stockDefault: 20, stockDefault: 20,
@@ -452,7 +452,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'سیب زمینی سرخ کرده ترد', desc: 'سیب زمینی سرخ کرده ترد',
price: 40000, price: 40000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[5]], // پیش غذا category: createdCategories[5], // پیش غذا
isActive: true, isActive: true,
stock: 30, stock: 30,
stockDefault: 30, stockDefault: 30,
@@ -462,7 +462,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'کیک شکلاتی خامه‌ای', desc: 'کیک شکلاتی خامه‌ای',
price: 60000, price: 60000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[6]], // دسر category: createdCategories[6], // دسر
isActive: true, isActive: true,
stock: 10, stock: 10,
stockDefault: 10, stockDefault: 10,
@@ -472,7 +472,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'چیزکیک وانیلی', desc: 'چیزکیک وانیلی',
price: 65000, price: 65000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[6]], // دسر category: createdCategories[6], // دسر
isActive: true, isActive: true,
stock: 12, stock: 12,
stockDefault: 12, stockDefault: 12,
@@ -482,7 +482,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'نوشابه گازدار', desc: 'نوشابه گازدار',
price: 15000, price: 15000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[7]], // نوشیدنی category: createdCategories[7], // نوشیدنی
isActive: true, isActive: true,
stock: 100, stock: 100,
stockDefault: 100, stockDefault: 100,
@@ -492,7 +492,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'آبمیوه تازه', desc: 'آبمیوه تازه',
price: 35000, price: 35000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[7]], // نوشیدنی category: createdCategories[7], // نوشیدنی
isActive: true, isActive: true,
stock: 40, stock: 40,
stockDefault: 40, stockDefault: 40,
@@ -502,7 +502,7 @@ export class DatabaseSeeder extends Seeder {
desc: 'قهوه اسپرسو', desc: 'قهوه اسپرسو',
price: 45000, price: 45000,
restaurant: boote, restaurant: boote,
categories: [createdCategories[7]], // نوشیدنی category: createdCategories[7], // نوشیدنی
isActive: true, isActive: true,
stock: 50, stock: 50,
stockDefault: 50, stockDefault: 50,
@@ -510,7 +510,7 @@ export class DatabaseSeeder extends Seeder {
]; ];
for (const foodData of foods) { for (const foodData of foods) {
if (foodData.restaurant) { if (foodData.restaurant && foodData.category) {
const existing = await em.findOne(Food, { const existing = await em.findOne(Food, {
title: foodData.title, title: foodData.title,
restaurant: foodData.restaurant, restaurant: foodData.restaurant,
@@ -521,6 +521,7 @@ export class DatabaseSeeder extends Seeder {
desc: foodData.desc, desc: foodData.desc,
price: foodData.price, price: foodData.price,
restaurant: foodData.restaurant, restaurant: foodData.restaurant,
category: foodData.category,
isActive: foodData.isActive, isActive: foodData.isActive,
stock: foodData.stock, stock: foodData.stock,
stockDefault: foodData.stockDefault, stockDefault: foodData.stockDefault,
@@ -535,10 +536,7 @@ export class DatabaseSeeder extends Seeder {
inPlaceServe: false, inPlaceServe: false,
pickupServe: false, pickupServe: false,
}); });
// Add categories
if (foodData.categories) {
foodData.categories.forEach(cat => food.categories.add(cat));
}
em.persist(food); em.persist(food);
} }
} }