This commit is contained in:
2025-11-15 08:41:35 +03:30
parent 15c0c508b9
commit 0f1407c748
23 changed files with 714 additions and 177 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ import { AdminModule } from './modules/admin/admin.module';
// import { CacheService } from './modules/utils/cache.service'; // import { CacheService } from './modules/utils/cache.service';
import { ThrottlerModule } from '@nestjs/throttler'; import { ThrottlerModule } from '@nestjs/throttler';
import { RestaurantsModule } from './modules/restaurants/restaurants.module'; import { RestaurantsModule } from './modules/restaurants/restaurants.module';
import { FoodsModule } from './modules/foods/foods.module'; import { FoodsModule } from './modules/foods/food.module';
@Module({ @Module({
imports: [ imports: [
+9 -1
View File
@@ -67,7 +67,15 @@ export const enum AuthMessage {
OLD_PASSWORD_REQUIRED = 'رمز عبور قبلی الزامی است', OLD_PASSWORD_REQUIRED = 'رمز عبور قبلی الزامی است',
PASSWORD_CHANGE_FAILED = 'تغییر رمز عبور با مشکل روبرد شد', PASSWORD_CHANGE_FAILED = 'تغییر رمز عبور با مشکل روبرد شد',
} }
export const enum FoodMessage {
NOT_FOUND = 'غذایی با این مشخصات یافت نشد',
}
export const enum CategoryMessage {
NOT_FOUND = 'دسته‌بندی مورد نظر یافت نشد',
NOT_CREATED = 'ایجاد دسته‌بندی با خطا مواجه شد',
NOT_UPDATED = 'به‌روزرسانی دسته‌بندی با خطا مواجه شد',
NOT_DELETED = 'حذف دسته‌بندی با خطا مواجه شد',
}
export const enum UserMessage { export const enum UserMessage {
USER_NOT_FOUND = 'کاربری با این مشخصات یافت نشد', USER_NOT_FOUND = 'کاربری با این مشخصات یافت نشد',
Rest_NOT_FOUND = 'رستورانی با این مشخصات یافت نشد', Rest_NOT_FOUND = 'رستورانی با این مشخصات یافت نشد',
@@ -0,0 +1,70 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { FoodService } from '../providers/food.service';
import { CreateFoodDto } from '../dto/create-food.dto';
import { UpdateFoodDto } from '../dto/update-food.dto';
import { FindFoodsDto } from '../dto/find-foods.dto';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiCreatedResponse,
ApiOkResponse,
ApiNotFoundResponse,
ApiQuery,
ApiBody,
ApiParam,
} from '@nestjs/swagger';
@ApiTags('foods')
@Controller('foods')
export class CategoryController {
constructor(private readonly foodService: FoodService) {}
@Post()
@ApiOperation({ summary: 'Create a new food' })
@ApiCreatedResponse({ description: 'The food has been successfully created.', type: CreateFoodDto })
@ApiBody({ type: CreateFoodDto })
create(@Body() createFoodDto: CreateFoodDto) {
return this.foodService.create(createFoodDto);
}
@Get()
@ApiOperation({ summary: 'Get a paginated list of foods' })
@ApiOkResponse({ description: 'Paginated list of foods' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
@ApiQuery({ name: 'categoryId', required: false, type: String })
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
findAll(@Query() dto: FindFoodsDto) {
return this.foodService.findAll(dto);
}
@Get(':id')
@ApiOperation({ summary: 'Get a food by id' })
@ApiParam({ name: 'id', required: true })
@ApiOkResponse({ description: 'The food', type: CreateFoodDto })
@ApiNotFoundResponse({ description: 'Food not found' })
findOne(@Param('id') id: string) {
return this.foodService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a food' })
@ApiParam({ name: 'id', required: true })
@ApiBody({ type: UpdateFoodDto })
@ApiOkResponse({ description: 'The updated food', type: UpdateFoodDto })
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
return this.foodService.update(id, updateFoodDto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete (soft) a food' })
@ApiParam({ name: 'id', required: true })
@ApiResponse({ status: 200, description: 'Food deleted' })
remove(@Param('id') id: string) {
return this.foodService.remove(id);
}
}
@@ -0,0 +1,65 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { CategoryService } from '../providers/category.service';
import { CreateCategoryDto } from '../dto/create-category.dto';
import { UpdateCategoryDto } from '../dto/update-category.dto';
import {
ApiTags,
ApiOperation,
ApiCreatedResponse,
ApiOkResponse,
ApiNotFoundResponse,
ApiParam,
ApiBody,
ApiQuery,
} from '@nestjs/swagger';
@ApiTags('categories')
@Controller('categories')
export class CategorysController {
constructor(private readonly categoryService: CategoryService) {}
@Post()
@ApiOperation({ summary: 'Create category' })
@ApiCreatedResponse({ description: 'Category created', type: CreateCategoryDto })
@ApiBody({ type: CreateCategoryDto })
create(@Body() dto: CreateCategoryDto) {
return this.categoryService.create(dto);
}
@Get()
@ApiOperation({ summary: 'List categories' })
@ApiOkResponse({ description: 'List of categories' })
@ApiQuery({ name: 'restId', required: false, type: String })
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
findAll(@Query('restId') restId?: string, @Query('isActive') isActive?: string) {
// convert isActive from query string to boolean when provided
const isActiveBool: boolean | undefined = typeof isActive !== 'undefined' ? isActive === 'true' : undefined;
return this.categoryService.findAll({ restId, isActive: isActiveBool });
}
@Get(':id')
@ApiOperation({ summary: 'Get category' })
@ApiParam({ name: 'id' })
@ApiOkResponse({ description: 'Category detail', type: CreateCategoryDto })
@ApiNotFoundResponse({ description: 'Category not found' })
findOne(@Param('id') id: string) {
return this.categoryService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update category' })
@ApiParam({ name: 'id' })
@ApiBody({ type: UpdateCategoryDto })
@ApiOkResponse({ description: 'Updated category', type: UpdateCategoryDto })
update(@Param('id') id: string, @Body() dto: UpdateCategoryDto) {
return this.categoryService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete category' })
@ApiParam({ name: 'id' })
@ApiOkResponse({ description: 'Deleted' })
remove(@Param('id') id: string) {
return this.categoryService.remove(id);
}
}
@@ -0,0 +1,70 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { FoodService } from '../providers/food.service';
import { CreateFoodDto } from '../dto/create-food.dto';
import { UpdateFoodDto } from '../dto/update-food.dto';
import { FindFoodsDto } from '../dto/find-foods.dto';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiCreatedResponse,
ApiOkResponse,
ApiNotFoundResponse,
ApiQuery,
ApiBody,
ApiParam,
} from '@nestjs/swagger';
@ApiTags('foods')
@Controller('foods')
export class FoodController {
constructor(private readonly foodsService: FoodService) {}
@Post()
@ApiOperation({ summary: 'Create a new food' })
@ApiCreatedResponse({ description: 'The food has been successfully created.', type: CreateFoodDto })
@ApiBody({ type: CreateFoodDto })
create(@Body() createFoodDto: CreateFoodDto) {
return this.foodsService.create(createFoodDto);
}
@Get()
@ApiOperation({ summary: 'Get a paginated list of foods' })
@ApiOkResponse({ description: 'Paginated list of foods' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
@ApiQuery({ name: 'categoryId', required: false, type: String })
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
findAll(@Query() dto: FindFoodsDto) {
return this.foodsService.findAll(dto);
}
@Get(':id')
@ApiOperation({ summary: 'Get a food by id' })
@ApiParam({ name: 'id', required: true })
@ApiOkResponse({ description: 'The food', type: CreateFoodDto })
@ApiNotFoundResponse({ description: 'Food not found' })
findOne(@Param('id') id: string) {
return this.foodsService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a food' })
@ApiParam({ name: 'id', required: true })
@ApiBody({ type: UpdateFoodDto })
@ApiOkResponse({ description: 'The updated food', type: UpdateFoodDto })
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
return this.foodsService.update(id, updateFoodDto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete (soft) a food' })
@ApiParam({ name: 'id', required: true })
@ApiResponse({ status: 200, description: 'Food deleted' })
remove(@Param('id') id: string) {
return this.foodsService.remove(id);
}
}
@@ -0,0 +1,26 @@
import { IsString, IsOptional, IsBoolean } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreateCategoryDto {
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'Beverages' })
title?: string;
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
@ApiPropertyOptional({ example: true })
isActive?: boolean;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'restaurant-ulid' })
restId?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'https://cdn.example.com/avatar.png' })
avatarUrl?: string;
}
+53 -8
View File
@@ -1,87 +1,132 @@
import { IsBoolean, IsOptional, IsString, IsNumber, IsArray } from 'class-validator'; import { IsBoolean, IsOptional, IsString, IsNumber, IsArray } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreateFoodDto { export class CreateFoodDto {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
breakfast?: boolean; breakfast?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
tue?: boolean; tue?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
wed?: boolean; wed?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
thu?: boolean; thu?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
fri?: boolean; fri?: boolean;
@IsOptional() @IsOptional()
@IsString() @IsString()
titleFa?: string; @ApiPropertyOptional({ example: 'قرمه سبزی' })
title?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
titleEn?: string; @ApiPropertyOptional({ example: 'توضیحات غذا' })
@IsOptional() content?: string;
@IsString()
contentFa?: string;
@IsOptional()
@IsString()
contentEn?: string;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 120000 })
price?: number; price?: number;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
points?: number; points?: number;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 15 })
prepareTime?: number; prepareTime?: number;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
sat?: boolean; sat?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
sun?: boolean; sun?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
mon?: boolean; mon?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
noon?: boolean; noon?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
dinner?: boolean; dinner?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
isPickup?: boolean; isPickup?: boolean;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
stock?: number; stock?: number;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
stockDefault?: number; stockDefault?: number;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
isActive?: boolean; isActive?: boolean;
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })
@ApiPropertyOptional({ type: [String] })
images?: string[]; images?: string[];
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
inPlaceServe?: boolean; inPlaceServe?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
pickupServe?: boolean; pickupServe?: boolean;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 4.5 })
rate?: number; rate?: number;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 0 })
discount?: number; discount?: number;
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })
@ApiPropertyOptional({ type: [String] })
categoryIds?: string[]; categoryIds?: string[];
} }
+43
View File
@@ -0,0 +1,43 @@
import { IsOptional, IsNumber, IsString, IsIn, IsBoolean } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class FindFoodsDto {
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 1 })
page?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
limit?: number;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'cheese' })
search?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'createdAt' })
orderBy?: string;
@IsOptional()
@IsIn(['asc', 'desc'])
@ApiPropertyOptional({ example: 'desc' })
order?: 'asc' | 'desc';
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'category-ulid' })
categoryId?: string;
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
@ApiPropertyOptional({ example: true })
isActive?: boolean;
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateCategoryDto } from './create-category.dto';
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
@@ -9,4 +9,13 @@ export class Category extends BaseEntity {
@ManyToMany(() => Foods, food => food.categories) @ManyToMany(() => Foods, food => food.categories)
foods = new Collection<Foods>(this); foods = new Collection<Foods>(this);
@Property({ default: true })
isActive: boolean = true;
@Property({ nullable: true })
restId?: string;
@Property({ nullable: true })
avatarUrl?: string;
} }
+6 -9
View File
@@ -1,5 +1,5 @@
import { Collection, Entity, ManyToMany, Property } from '@mikro-orm/core'; import { Collection, Entity, ManyToMany, Property } from '@mikro-orm/core';
import { Category } from './category'; import { Category } from './category.entity';
import { BaseEntity } from 'src/common/entities/base.entity'; import { BaseEntity } from 'src/common/entities/base.entity';
@Entity({ tableName: 'foods' }) @Entity({ tableName: 'foods' })
@@ -8,16 +8,10 @@ export class Foods extends BaseEntity {
categories = new Collection<Category>(this); categories = new Collection<Category>(this);
@Property({ nullable: true }) @Property({ nullable: true })
titleFa?: string; title?: string;
@Property({ nullable: true })
titleEn?: string;
@Property({ type: 'text', nullable: true }) @Property({ type: 'text', nullable: true })
contentFa?: string; content?: string;
@Property({ type: 'text', nullable: true })
contentEn?: string;
@Property({ type: 'decimal', precision: 10, scale: 2, nullable: true }) @Property({ type: 'decimal', precision: 10, scale: 2, nullable: true })
price?: number; price?: number;
@@ -25,6 +19,9 @@ export class Foods extends BaseEntity {
@Property({ type: 'int', nullable: true }) @Property({ type: 'int', nullable: true })
points?: number; points?: number;
@Property({ type: 'int', nullable: true })
order?: number;
@Property({ type: 'int', nullable: true }) @Property({ type: 'int', nullable: true })
prepareTime?: number; // in minutes prepareTime?: number; // in minutes
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { FoodService } from './providers/food.service';
import { FoodController } from './controllers/food.controller';
import { CategorysController } from './controllers/categorys.controller';
import { CategoryService } from './providers/category.service';
@Module({
controllers: [FoodController, CategorysController],
providers: [FoodService, CategoryService],
})
export class FoodModule {}
-34
View File
@@ -1,34 +0,0 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { FoodsService } from './providers/foods.service';
import { CreateFoodDto } from './dto/create-food.dto';
import { UpdateFoodDto } from './dto/update-food.dto';
@Controller('foods')
export class FoodsController {
constructor(private readonly foodsService: FoodsService) {}
@Post()
create(@Body() createFoodDto: CreateFoodDto) {
return this.foodsService.create(createFoodDto);
}
@Get()
findAll() {
return this.foodsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.foodsService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
return this.foodsService.update(+id, updateFoodDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.foodsService.remove(+id);
}
}
-9
View File
@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { FoodsService } from './providers/foods.service';
import { FoodsController } from './foods.controller';
@Module({
controllers: [FoodsController],
providers: [FoodsService],
})
export class FoodsModule {}
@@ -0,0 +1,57 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateCategoryDto } from '../dto/create-category.dto';
import { UpdateCategoryDto } from '../dto/update-category.dto';
import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData, FilterQuery } from '@mikro-orm/core';
import { Category } from '../entities/category.entity';
import { CategoryMessage } from 'src/common/enums/message.enum';
@Injectable()
export class CategoryService {
constructor(
private readonly categoryRepository: CategoryRepository,
private readonly em: EntityManager,
) {}
async create(dto: CreateCategoryDto): Promise<Category> {
const data = {
title: dto.title,
isActive: dto.isActive ?? true,
restId: dto.restId,
avatarUrl: dto.avatarUrl,
} as RequiredEntityData<Category>;
const category = this.categoryRepository.create(data);
await this.em.persistAndFlush(category);
return category;
}
async findAll(opts?: { restId?: string; isActive?: boolean }): Promise<Category[]> {
const where: FilterQuery<Category> = {};
if (opts?.restId) where.restId = opts.restId;
if (opts && typeof opts.isActive === 'boolean') where.isActive = opts.isActive;
return this.categoryRepository.find(where, { populate: ['foods'] });
}
async findOne(id: string): Promise<Category> {
const cat = await this.categoryRepository.findOne({ id }, { populate: ['foods'] });
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
return cat;
}
async update(id: string, dto: UpdateCategoryDto): Promise<Category> {
const cat = await this.categoryRepository.findOne({ id });
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
this.em.assign(cat, dto as Partial<Category>);
await this.em.persistAndFlush(cat);
return cat;
}
async remove(id: string): Promise<void> {
const cat = await this.categoryRepository.findOne({ id });
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
cat.deletedAt = new Date();
await this.em.persistAndFlush(cat);
}
}
+103
View File
@@ -0,0 +1,103 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateFoodDto } from '../dto/create-food.dto';
import { FindFoodsDto } from '../dto/find-foods.dto';
import { FoodRepository } from '../repositories/food.repository';
import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData } from '@mikro-orm/core';
import { Foods } from '../entities/food.entity';
import { FoodMessage } from 'src/common/enums/message.enum';
@Injectable()
export class FoodService {
constructor(
private readonly foodRepository: FoodRepository,
private readonly categoryRepository: CategoryRepository,
private readonly em: EntityManager,
) {}
async create(createFoodDto: CreateFoodDto): Promise<Foods> {
const { categoryIds, ...rest } = createFoodDto;
// prepare data with defaults for required fields so repository.create typing is satisfied
const data: RequiredEntityData<Foods> = {
// boolean/day flags
sat: rest.sat ?? false,
sun: rest.sun ?? false,
mon: rest.mon ?? false,
breakfast: rest.breakfast ?? false,
noon: rest.noon ?? false,
dinner: rest.dinner ?? false,
// pickup/stock
isPickup: rest.isPickup ?? false,
stock: rest.stock ?? 0,
stockDefault: rest.stockDefault ?? 0,
isActive: rest.isActive ?? true,
inPlaceServe: rest.inPlaceServe ?? false,
pickupServe: rest.pickupServe ?? false,
rate: rest.rate ?? 0,
discount: rest.discount ?? 0,
// map single-title/content DTO to localized fields
titleFa: rest.title,
contentFa: rest.content,
// numeric/array fields
price: rest.price ?? 0,
points: rest.points ?? 0,
prepareTime: rest.prepareTime ?? 0,
images: rest.images ?? undefined,
} as RequiredEntityData<Foods>;
const food = this.foodRepository.create(data);
// 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);
return food;
}
findAll(dto: FindFoodsDto) {
return this.foodRepository.findAllPaginated(dto);
}
findOne(id: string) {
return this.foodRepository.findOne({ id }, { populate: ['categories'] });
}
async update(id: string, dto: Partial<CreateFoodDto>): Promise<Foods> {
const food = await this.foodRepository.findOne({ id });
if (!food) {
throw new NotFoundException(FoodMessage.NOT_FOUND);
}
const { categoryIds, ...rest } = dto;
// assign scalar fields
this.em.assign(food, rest as Partial<Foods>);
// attach new categories if provided (adds, does not attempt to preserve/remove existing ones)
if (Array.isArray(categoryIds) && categoryIds.length) {
const categories = await this.categoryRepository.find({ id: { $in: categoryIds } });
categories.forEach(cat => {
// Collection.add will ignore duplicates
food.categories.add(cat);
});
}
await this.em.persistAndFlush(food);
return food;
}
async remove(id: string) {
const food = await this.foodRepository.findOne({ id });
if (!food) {
throw new NotFoundException(FoodMessage.NOT_FOUND);
}
food.deletedAt = new Date();
await this.em.persistAndFlush(food);
}
}
@@ -1,48 +0,0 @@
import { Injectable } from '@nestjs/common';
import { CreateFoodDto } from '../dto/create-food.dto';
import { FoodRepository } from '../repositories/food.repository';
import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { Foods } from '../entities/food.entity';
@Injectable()
export class FoodsService {
constructor(
private readonly foodRepository: FoodRepository,
private readonly categoryRepository: CategoryRepository,
private readonly em: EntityManager,
) {}
async create(createFoodDto: CreateFoodDto): Promise<Foods> {
const { categoryIds, ...rest } = createFoodDto;
// create an empty food entity and assign dto properties to avoid strict typing issues
const food = new Foods();
this.em.assign(food, rest as Partial<Foods>);
// 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);
return food;
}
findAll() {
return `This action returns all foods`;
}
findOne(id: number) {
return `This action returns a #${id} food`;
}
update(id: number) {
return `This action updates a #${id} food`;
}
remove(id: number) {
return `This action removes a #${id} food`;
}
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { Category } from '../entities/category'; import { Category } from '../entities/category.entity';
@Injectable() @Injectable()
export class CategoryRepository extends EntityRepository<Category> { export class CategoryRepository extends EntityRepository<Category> {
@@ -1,10 +1,66 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { FilterQuery } from '@mikro-orm/core';
import { Foods } from '../entities/food.entity'; import { Foods } from '../entities/food.entity';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
type FindFoodsOpts = {
page?: number;
limit?: number;
search?: string;
orderBy?: string;
order?: 'asc' | 'desc';
categoryId?: string;
isActive?: boolean;
};
@Injectable() @Injectable()
export class FoodRepository extends EntityRepository<Foods> { export class FoodRepository extends EntityRepository<Foods> {
constructor(readonly em: EntityManager) { constructor(readonly em: EntityManager) {
super(em, Foods); super(em, Foods);
} }
/**
* Find foods with pagination and optional filters.
* Supports: search (title/content), categoryId, isActive, ordering.
*/
async findAllPaginated(opts: FindFoodsOpts = {}): Promise<PaginatedResult<Foods>> {
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc', categoryId, isActive } = opts;
const offset = (page - 1) * limit;
const where: FilterQuery<Foods> = {};
if (typeof isActive === 'boolean') {
where.isActive = isActive;
}
if (search) {
const pattern = `%${search}%`;
where.$or = [{ title: { $ilike: pattern } }, { content: { $ilike: pattern } }];
}
if (categoryId) {
// filter by related categories (typed via FilterQuery)
Object.assign(where, { categories: { id: categoryId } } as unknown as FilterQuery<Foods>);
}
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['categories'],
});
const totalPages = Math.ceil(total / limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
},
};
}
} }
+41 -39
View File
@@ -1,53 +1,55 @@
import { IsString, IsNotEmpty, IsBoolean, IsNumber, MinLength } from 'class-validator'; import { IsString, IsOptional, IsBoolean, IsNumber } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
/**
* DTO for updating user profile.
* Mirrors fields defined on the User entity and keeps everything optional
* to allow partial updates.
*/
export class UpdateUserDto { export class UpdateUserDto {
@ApiProperty({ example: ' ', description: "User's first name" }) @ApiPropertyOptional({ description: "User's first name", example: 'John' })
@IsNotEmpty() @IsOptional()
@IsString() @IsString()
firstName: string; firstName?: string;
@ApiProperty({ example: ' ', description: "User's last name" }) @ApiPropertyOptional({ description: "User's last name", example: 'Doe' })
@IsNotEmpty() @IsOptional()
@IsString() @IsString()
lastName: string; lastName?: string;
@ApiProperty({ example: ' ', description: "User's father's name" }) @ApiPropertyOptional({ description: "User's birth date (ISO)", example: '1990-01-01' })
@IsNotEmpty() @IsOptional()
// keep as string here, caller should send ISO date; service will assign to Date
@IsString() @IsString()
fatherName: string; birthDate?: string;
// --- Personal Details --- @ApiPropertyOptional({ description: "User's marriage date (ISO)", example: '2015-06-01' })
@IsOptional()
@IsString()
marriageDate?: string;
@ApiPropertyOptional({ example: true, description: 'Gender: true for male, false for female' }) @ApiPropertyOptional({ description: "Referrer's identifier (optional)", example: 'abc123' })
@IsNotEmpty() @IsOptional()
@IsString()
referrer?: string;
@ApiPropertyOptional({ description: 'Is the user active?', example: true })
@IsOptional()
@IsBoolean() @IsBoolean()
isMale: boolean; isActive?: boolean;
@ApiPropertyOptional({ example: '1234567890', description: 'Unique national code' }) @ApiPropertyOptional({ description: 'Gender flag (boolean)', example: true })
@IsNotEmpty() @IsOptional()
@MinLength(10) @IsBoolean()
nationalCode: string; gender?: boolean;
@ApiPropertyOptional({ example: '1001', description: 'Employee personal code' }) @ApiPropertyOptional({ description: 'Wallet balance (integer)', example: 0 })
@IsNotEmpty() @IsOptional()
@IsString()
personalCode: string;
// --- Employment Details ---
@ApiPropertyOptional({ example: 'قراردادی', description: 'Type of hire (e.g., permanent, temporary)' })
@IsNotEmpty()
@IsString()
hireType: string;
@ApiPropertyOptional({ example: 'اراک', description: 'Primary work location' })
@IsNotEmpty()
@IsString()
workLocation: string;
@ApiPropertyOptional({ example: 5, description: 'Years of work experience' })
@IsNotEmpty()
@IsNumber() @IsNumber()
workExperienceYear: number; wallet?: number;
@ApiPropertyOptional({ description: 'Reward points (integer)', example: 0 })
@IsOptional()
@IsNumber()
points?: number;
} }
+21 -3
View File
@@ -4,8 +4,8 @@ import { BaseEntity } from '../../../common/entities/base.entity';
@Entity({ tableName: 'users' }) @Entity({ tableName: 'users' })
export class User extends BaseEntity { export class User extends BaseEntity {
@Property({ nullable: true }) @Property()
firstName?: string; firstName!: string;
@Property({ nullable: true }) @Property({ nullable: true })
lastName?: string; lastName?: string;
@@ -13,8 +13,26 @@ export class User extends BaseEntity {
@Property({ unique: true }) @Property({ unique: true })
phone!: string; phone!: string;
@Property({ default: null, type: 'date' })
birthDate!: Date;
@Property({ default: null, type: 'date' })
marriageDate!: Date;
@Property({ nullable: true })
referrer?: string;
@Property({ default: true }) @Property({ default: true })
isActive?: boolean; isActive?: boolean = true;
@Property({ default: true })
gender?: boolean;
@Property({ default: 0, type: 'int' })
wallet: number = 0;
@Property({ default: 0, type: 'int' })
points: number = 0;
@OneToMany(() => RefreshToken, token => token.user) @OneToMany(() => RefreshToken, token => token.user)
refreshTokens = new Collection<RefreshToken>(this); refreshTokens = new Collection<RefreshToken>(this);
+19 -18
View File
@@ -1,8 +1,8 @@
import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { Controller, Get, Req, UseGuards, Patch, Body, ValidationPipe } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { AuthGuard, type AuthRequest } from 'src/modules/auth/guards/auth.guard'; import { AuthGuard, type AuthRequest } from 'src/modules/auth/guards/auth.guard';
import { UserService } from './user.service'; import { UserService } from './user.service';
// import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
@ApiTags('User') @ApiTags('User')
@Controller('user') @Controller('user')
@@ -24,22 +24,23 @@ export class UserController {
restId, restId,
}; };
} }
// 2. Update User Specification (PATCH /user)
// 2. Update User Specification (PATCH /user/spec) @UseGuards(AuthGuard)
// @UseGuards(AuthGuard) @ApiBearerAuth()
// @ApiBearerAuth() @ApiOperation({ summary: 'Update the authenticated user profile' })
// @ApiOperation({ summary: 'Update the user specification' }) @Patch('/')
// @ApiBody({ type: UpdateUserDto }) async updateUser(
// @Patch('/') @Req() req: AuthRequest,
// async updateUserSpec(@Req() req: AuthRequest, @Body() dto: UpdateUserDto) { @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserDto,
// const userId = req.user.sub; ) {
// const user = await this.userService.updateUser(userId, dto); const userId = req.userId;
// return { const user = await this.userService.updateUser(userId, dto);
// message: `PATCH request: Updating specification for user ${userId} `, return {
// userId, message: `PATCH request: Updating profile for user ${userId}`,
// user, userId,
// }; user,
// } };
}
// @UseGuards(AdminAuthGuard) // @UseGuards(AdminAuthGuard)
// @ApiBearerAuth() // @ApiBearerAuth()
+49 -6
View File
@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { FilterQuery } from '@mikro-orm/core'; import { FilterQuery, RequiredEntityData } from '@mikro-orm/core';
import { User } from './entities/user.entity'; import { User } from './entities/user.entity';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
// import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { FindUsersDto } from './dto/find-user.dto'; import { FindUsersDto } from './dto/find-user.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { UserRepository } from './repositories/user.repository'; import { UserRepository } from './repositories/user.repository';
@@ -18,9 +18,20 @@ export class UserService {
let user = await this.userRepository.findOne({ phone }); let user = await this.userRepository.findOne({ phone });
if (!user) { if (!user) {
user = this.userRepository.create({ // firstName is required on the entity; use phone as a sensible default
const _raw = {
phone, phone,
}); firstName: phone,
// keep dates undefined (no value) — cast through unknown to satisfy TS typing
birthDate: undefined,
marriageDate: undefined,
wallet: 0,
points: 0,
};
const createData = _raw as unknown as RequiredEntityData<User>;
user = this.userRepository.create(createData);
await this.em.persistAndFlush(user); await this.em.persistAndFlush(user);
} }
@@ -48,11 +59,43 @@ export class UserService {
} }
async create(phone: string): Promise<User> { async create(phone: string): Promise<User> {
const user = this.userRepository.create({ phone }); const _raw = {
phone,
firstName: phone,
birthDate: undefined,
marriageDate: undefined,
wallet: 0,
points: 0,
};
const createData = _raw as unknown as RequiredEntityData<User>;
const user = this.userRepository.create(createData);
await this.em.persistAndFlush(user); await this.em.persistAndFlush(user);
return user; return user;
} }
async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
const user = await this.userRepository.findOne({ id: userId });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found.`);
}
// Normalize date strings into Date objects if provided
const assignData: Partial<User> = { ...dto } as Partial<User>;
if (dto.birthDate) {
assignData.birthDate = new Date(dto.birthDate);
}
if (dto.marriageDate) {
assignData.marriageDate = new Date(dto.marriageDate);
}
this.em.assign(user, assignData);
await this.em.flush();
return user;
}
async findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> { async findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> {
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto; const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;