update food entity

This commit is contained in:
2026-02-08 09:09:21 +03:30
parent 8af4936ed0
commit 6be6a66079
8 changed files with 2 additions and 270 deletions
@@ -59,12 +59,6 @@ export class CartItemService {
async addOrIncrementItem(cart: Cart, foodId: string, restaurantId: string, quantityToAdd: number): Promise<void> {
const food = await this.validationService.validateAndGetFood(foodId, restaurantId, quantityToAdd);
// Validate meal type compatibility
this.validationService.assertMealTypeCompatibility(food);
// Validate weekday compatibility
this.validationService.assertWeekdayCompatibility(food);
const index = this.getItemIndex(cart, food.id);
if (index < 0) {
@@ -12,7 +12,6 @@ import { Order } from '../../orders/entities/order.entity';
import { OrderStatus } from '../../orders/interface/order.interface';
import { Cart } from '../interfaces/cart.interface';
import { GeographicUtils } from '../utils/geographic.utils';
import { MealType } from '../../foods/interface/food.interface';
import { CartMessage } from 'src/common/enums/message.enum';
import { WalletTransactionRepository } from 'src/modules/users/repositories/wallet-transaction.repository';
@@ -284,139 +283,5 @@ export class CartValidationService {
return new Map(foods.map(f => [f.id, f]));
}
/**
* Assert all foods in cart have pickupServe true when delivery method is courier
*/
async assertAllFoodsHavePickupServeForCourier(cart: Cart, deliveryMethod: DeliveryMethodEnum): Promise<void> {
if (deliveryMethod !== DeliveryMethodEnum.DeliveryCourier) {
return;
}
if (cart.items.length === 0) {
return;
}
const foodIds = cart.items.map(item => item.foodId);
const foods = await this.em.find(Food, { id: { $in: foodIds } });
const foodsWithoutPickupServe = foods.filter(food => !food.pickupServe);
if (foodsWithoutPickupServe.length > 0) {
const foodTitles = foodsWithoutPickupServe.map(f => f.title || f.id).join(', ');
throw new BadRequestException(
CartMessage.FOODS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER.replace('[foodTitles]', foodTitles),
);
}
}
/**
* Get current Iran time context (weekday and meal type)
*/
private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } {
const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' }));
const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday
const currentHour = nowInIran.getHours();
// Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6
// JavaScript: Sunday=0, Monday=1, ..., Saturday=6
// Iran week: Saturday=0, Sunday=1, ..., Friday=6
const iranWeekDay = (weekDay + 1) % 7;
const mealType = this.getMealTypeByHour(currentHour);
return { iranWeekDay, mealType };
}
/**
* Determine meal type based on current hour in Iran timezone.
* @param hour - Current hour (0-23)
* @returns Meal type or null if outside meal hours
*/
private getMealTypeByHour(hour: number): MealType | null {
const MEAL_TIME_RANGES = {
BREAKFAST: { start: 6, end: 11 },
LUNCH: { start: 11, end: 15 },
SNACK: { start: 15, end: 19 },
DINNER: { start: 19, end: 24 },
} as const;
if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) {
return MealType.BREAKFAST;
}
if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) {
return MealType.LUNCH;
}
if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) {
return MealType.SNACK;
}
if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) {
return MealType.DINNER;
}
return null;
}
/**
* Assert meal type compatibility - if food has mealTypes, current meal time must be in that array
*/
assertMealTypeCompatibility(food: Food): void {
// If food has no meal type restrictions, allow it
if (!food.mealTypes || food.mealTypes.length === 0) {
return;
}
const { mealType } = this.getCurrentIranTimeContext();
// If current time is outside meal hours, throw error
if (mealType === null) {
const foodTitle = food.title || food.id;
throw new BadRequestException(
CartMessage.FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES.replace('[foodTitle]', foodTitle),
);
}
// Check if current meal type is in food's allowed meal types
if (!food.mealTypes.includes(mealType)) {
const foodTitle = food.title || food.id;
const mealTypeMap: Record<MealType, string> = {
[MealType.BREAKFAST]: 'صبحانه',
[MealType.LUNCH]: 'ناهار',
[MealType.SNACK]: 'عصرانه',
[MealType.DINNER]: 'شام',
};
const allowedMealTypes = food.mealTypes.map(mt => mealTypeMap[mt]).join(', ');
const currentMealType = mealType ? mealTypeMap[mealType] : mealType;
throw new BadRequestException(
CartMessage.FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES.replace('[foodTitle]', foodTitle)
.replace('[allowedMealTypes]', allowedMealTypes)
.replace('[mealType]', currentMealType),
);
}
}
/**
* Assert weekday compatibility - current weekday must be in food's weekDays array
*/
assertWeekdayCompatibility(food: Food): void {
// If food has no weekday restrictions (empty array or all days), allow it
if (!food.weekDays || food.weekDays.length === 0 || food.weekDays.length === 7) {
return;
}
const { iranWeekDay } = this.getCurrentIranTimeContext();
// Check if current weekday is in food's allowed weekdays
if (!food.weekDays.includes(iranWeekDay)) {
const dayNames = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه'];
const currentDayName = dayNames[iranWeekDay];
const allowedDays = food.weekDays.map(day => dayNames[day]).join(', ');
const foodTitle = food.title || food.id;
throw new BadRequestException(
CartMessage.FOOD_ONLY_AVAILABLE_ON_DAYS.replace('[foodTitle]', foodTitle)
.replace('[allowedDays]', allowedDays)
.replace('[currentDay]', currentDayName),
);
}
}
}
@@ -120,9 +120,6 @@ export class CartService {
cart.description = description;
}
// Final validation: if effective delivery method is courier, ensure all foods have pickupServe
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);
// Final recalculation + save and return cart
return this.recalculateAndSaveCart(cart);
}
-37
View File
@@ -13,7 +13,6 @@ import {
Min,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { MealType } from '../interface/food.interface';
export class CreateFoodDto {
@IsNotEmpty()
@@ -37,23 +36,6 @@ export class CreateFoodDto {
@ApiPropertyOptional({ type: [String] })
content?: string[];
@IsOptional()
@IsArray()
@ArrayUnique()
@IsInt({ each: true })
@Min(0, { each: true })
@Max(6, { each: true })
@Type(() => Number)
@ApiPropertyOptional({ type: [Number], example: [0, 1, 2, 3, 4, 5, 6] })
weekDays?: number[];
@IsOptional()
@IsArray()
@ArrayUnique()
@IsEnum(MealType, { each: true })
@ApiPropertyOptional({ enum: MealType, isArray: true })
mealTypes?: MealType[];
@IsOptional()
@IsNumber()
@Min(0)
@@ -61,13 +43,6 @@ export class CreateFoodDto {
@ApiPropertyOptional({ example: 120000 })
price?: number;
@IsOptional()
@IsInt()
@Min(0)
@Type(() => Number)
@ApiPropertyOptional({ example: 15 })
prepareTime?: number;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: true })
@@ -80,18 +55,6 @@ export class CreateFoodDto {
@ApiPropertyOptional({ type: [String] })
images?: string[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
inPlaceServe?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
pickupServe?: boolean;
@IsOptional()
@IsNumber()
@Min(0)
-16
View File
@@ -4,7 +4,6 @@ import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
import { Review } from 'src/modules/review/entities/review.entity';
import { Inventory } from 'src/modules/inventory/entities/inventory.entity';
import { MealType } from '../interface/food.interface';
import { Favorite } from './favorite.entity';
@Entity({ tableName: 'foods' })
@@ -45,27 +44,12 @@ export class Food extends BaseEntity {
@Property({ type: 'int', nullable: true })
order?: number;
@Property({ type: 'int', nullable: true })
prepareTime?: number; // in minutes
@Property({ type: 'jsonb', default: [] })
weekDays: number[] = [0, 1, 2, 3, 4, 5, 6];
@Property({ type: 'jsonb', default: [] })
mealTypes: MealType[] = [];
@Property({ type: 'boolean', default: true })
isActive: boolean = true;
@Property({ type: 'json', nullable: true })
images?: string[];
@Property({ type: 'boolean', default: false })
inPlaceServe: boolean = false;
@Property({ type: 'boolean', default: false })
pickupServe: boolean = false;
@Property({ type: 'float', default: null })
score?: number | null = null;
+2 -61
View File
@@ -10,7 +10,6 @@ import { CategoryMessage, FoodMessage, RestMessage } from 'src/common/enums/mess
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
import { CacheService } from '../../utils/cache.service';
import { Favorite } from '../entities/favorite.entity';
import { MealType } from '../interface/food.interface';
import { Inventory } from 'src/modules/inventory/entities/inventory.entity';
@Injectable()
@@ -38,8 +37,6 @@ export class FoodService {
const data: RequiredEntityData<Food> = {
desc: rest.desc,
isActive: rest.isActive ?? true,
inPlaceServe: rest.inPlaceServe ?? false,
pickupServe: rest.pickupServe ?? false,
discount: rest.discount ?? 0,
isSpecialOffer: rest.isSpecialOffer ?? false,
order: rest.order ?? null,
@@ -48,11 +45,7 @@ export class FoodService {
content: rest.content,
// numeric/array fields
price: rest.price ?? 0,
prepareTime: rest.prepareTime ?? 0,
images: rest.images ?? undefined,
// new fields
weekDays: rest.weekDays ?? [0, 1, 2, 3, 4, 5, 6],
mealTypes: rest.mealTypes ?? [],
restaurant: restaurant,
category: category,
};
@@ -108,9 +101,9 @@ export class FoodService {
}
/**
* Find active foods for a restaurant based on current week day and meal type in Iran timezone.
* Find active foods for a restaurant.
* @param slug - Restaurant slug identifier
* @returns Array of active foods matching current day and meal time
* @returns Array of active foods
*/
async findByWeekDateAndMealType(slug: string): Promise<Food[]> {
const restaurant = await this.restRepository.findOne({ slug });
@@ -118,13 +111,9 @@ export class FoodService {
throw new NotFoundException(RestMessage.NOT_FOUND);
}
const { iranWeekDay, mealType } = this.getCurrentIranTimeContext();
const queryFilter: FilterQuery<Food> = {
restaurant: { slug },
isActive: true,
weekDays: { $contains: iranWeekDay },
...(mealType ? { mealTypes: { $contains: mealType } } : {}),
} as unknown as FilterQuery<Food>;
return this.foodRepository.find(queryFilter, {
@@ -133,54 +122,6 @@ export class FoodService {
});
}
/**
* Get current Iran timezone context (weekday and meal type).
* @returns Object containing Iran weekday (0-6, where 0=Saturday) and current meal type
*/
private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } {
const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' }));
const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday
const currentHour = nowInIran.getHours();
// Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6
// JavaScript: Sunday=0, Monday=1, ..., Saturday=6
// Iran week: Saturday=0, Sunday=1, ..., Friday=6
const iranWeekDay = (weekDay + 1) % 7;
const mealType = this.getMealTypeByHour(currentHour);
return { iranWeekDay, mealType };
}
/**
* Determine meal type based on current hour in Iran timezone.
* @param hour - Current hour (0-23)
* @returns Meal type or null if outside meal hours
*/
private getMealTypeByHour(hour: number): MealType | null {
const MEAL_TIME_RANGES = {
BREAKFAST: { start: 6, end: 11 },
LUNCH: { start: 11, end: 15 },
SNACK: { start: 15, end: 19 },
DINNER: { start: 19, end: 24 },
} as const;
if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) {
return MealType.BREAKFAST;
}
if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) {
return MealType.LUNCH;
}
if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) {
return MealType.SNACK;
}
if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) {
return MealType.DINNER;
}
return null;
}
async update(restId: string, id: string, dto: Partial<CreateFoodDto>): Promise<Food> {
const { categoryId, dailyStock, ...rest } = dto;
const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['restaurant'] });
-6
View File
@@ -1,5 +1,3 @@
import { MealType } from '../../modules/foods/interface/food.interface';
export interface FoodData {
title: string;
desc: string;
@@ -11,12 +9,8 @@ export interface FoodData {
stockDefault: number;
images: string[];
content?: string[];
weekDays?: number[];
mealTypes?: MealType[];
discount?: number;
score?: number;
inPlaceServe?: boolean;
pickupServe?: boolean;
isSpecialOffer?: boolean;
}
-6
View File
@@ -2,7 +2,6 @@ import type { EntityManager } from '@mikro-orm/core';
import { Food } from '../modules/foods/entities/food.entity';
import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
import type { Category } from '../modules/foods/entities/category.entity';
import { MealType } from '../modules/foods/interface/food.interface';
import { foodsData } from './data/foods.data';
export class FoodsSeeder {
@@ -53,13 +52,8 @@ export class FoodsSeeder {
restaurant,
category,
isActive: foodData.isActive,
// new fields on Food entity
weekDays: foodData.weekDays ?? [0, 1, 2, 3, 4, 5, 6],
mealTypes: foodData.mealTypes ?? [MealType.BREAKFAST, MealType.LUNCH, MealType.DINNER, MealType.SNACK],
discount: foodData.discount ?? 0,
score: foodData.score ?? 0,
inPlaceServe: true,
pickupServe: true,
images: foodData.images,
isSpecialOffer: foodData.isSpecialOffer ?? false,
});