From fae9d1420cc46718f40937737ce37896d065d74a Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 15 Jun 2026 16:50:51 +0330 Subject: [PATCH] food avaiability --- .../foods/controllers/food.controller.ts | 3 +- src/modules/foods/interface/food.interface.ts | 23 +++ src/modules/foods/providers/food.service.ts | 134 ++++++++++++++---- 3 files changed, 128 insertions(+), 32 deletions(-) diff --git a/src/modules/foods/controllers/food.controller.ts b/src/modules/foods/controllers/food.controller.ts index 2e38c6b..e94e966 100644 --- a/src/modules/foods/controllers/food.controller.ts +++ b/src/modules/foods/controllers/food.controller.ts @@ -21,6 +21,7 @@ import { Permission } from 'src/common/enums/permission.enum'; import { Permissions } from 'src/common/decorators/permissions.decorator'; import { CacheResponse } from 'src/common/decorators/cache-response.decorator'; import { CacheKeyPrefixes } from 'src/common/constants/cache-keys.constant'; +import { PublicFoodListItem } from '../interface/food.interface'; @ApiTags('foods') @Controller() @@ -32,7 +33,7 @@ export class FoodController { @ApiOperation({ summary: 'Get all foods by restaurant slug' }) @ApiHeader(API_HEADER_SLUG) @ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' }) - findAllByRestaurant(@Param('slug') slug: string) { + findAllByRestaurant(@Param('slug') slug: string): Promise { return this.foodsService.findByRestuarantSlug(slug); } diff --git a/src/modules/foods/interface/food.interface.ts b/src/modules/foods/interface/food.interface.ts index 033e3ca..c7e1063 100644 --- a/src/modules/foods/interface/food.interface.ts +++ b/src/modules/foods/interface/food.interface.ts @@ -4,3 +4,26 @@ export enum MealType { DINNER = 'dinner', SNACK = 'snack', } + +export interface FoodAvailabilityInfo { + isAvailable: boolean; + unavailabilityMessage: string | null; +} + +export interface PublicFoodListItem { + id: string; + title?: string; + desc?: string; + content?: string[]; + price?: number; + images?: string[]; + category: string; + inPlaceServe: boolean; + pickupServe: boolean; + discount: number; + isSpecialOffer: boolean; + // mealTypes: MealType[]; + // weekDays: number[]; + isAvailable: boolean; + unavailabilityMessage: string | null; +} diff --git a/src/modules/foods/providers/food.service.ts b/src/modules/foods/providers/food.service.ts index c08e594..dc6f53d 100644 --- a/src/modules/foods/providers/food.service.ts +++ b/src/modules/foods/providers/food.service.ts @@ -6,12 +6,11 @@ import { CategoryRepository } from '../repositories/category.repository'; import { EntityManager } from '@mikro-orm/postgresql'; import { RequiredEntityData, FilterQuery } from '@mikro-orm/core'; import { Food } from '../entities/food.entity'; -import { CategoryMessage, FoodMessage, RestMessage } from 'src/common/enums/message.enum'; +import { CategoryMessage, FoodMessage, RestMessage, CartMessage } from 'src/common/enums/message.enum'; import { Favorite } from '../entities/favorite.entity'; -import { MealType } from '../interface/food.interface'; +import { MealType, PublicFoodListItem, FoodAvailabilityInfo } from '../interface/food.interface'; import { Inventory } from 'src/modules/inventory/entities/inventory.entity'; import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service'; -import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; import { CacheService } from 'src/modules/utils/cache.service'; import { CacheKeys } from 'src/common/constants/cache-keys.constant'; @@ -116,7 +115,7 @@ export class FoodService { * @param slug - Restaurant slug identifier * @returns Array of active foods matching current day and meal time */ - async findByRestuarantSlug(slug: string) { + async findByRestuarantSlug(slug: string): Promise { const restaurant = await this.restService.findBySlug(slug); const queryFilter: FilterQuery = { @@ -124,46 +123,119 @@ export class FoodService { isActive: true, }; - return this.foodRepository.find(queryFilter, { + const foods = await this.foodRepository.find(queryFilter, { fields: ['id', 'title', 'price', 'content', 'desc', 'images', 'category', - 'inPlaceServe', 'pickupServe', 'discount', 'isSpecialOffer'], - orderBy: { order: 'asc' } + 'inPlaceServe', 'pickupServe', 'discount', 'isSpecialOffer', 'mealTypes', 'weekDays', + 'inventory.availableStock'], + populate: ['inventory'], + orderBy: { order: 'asc' }, + }); + + return foods.map((food): PublicFoodListItem => { + const availability = this.getFoodAvailability(food); + + return { + id: food.id, + title: food.title, + desc: food.desc, + content: food.content, + price: food.price, + images: food.images, + category: typeof food.category === 'object' ? food.category.id : food.category, + inPlaceServe: food.inPlaceServe, + pickupServe: food.pickupServe, + discount: food.discount, + isSpecialOffer: food.isSpecialOffer, + // mealTypes: food.mealTypes, + // weekDays: food.weekDays, + isAvailable: availability.isAvailable, + unavailabilityMessage: availability.unavailabilityMessage, + }; }); } - // async findByWeekDateAndMealType(slug: string): Promise { - // const { iranWeekDay, mealType } = this.getCurrentIranTimeContext(); + /** + * Whether a food can be ordered right now based on inventory, weekDays and mealTypes (Iran timezone). + */ + private getFoodAvailability(food: { + weekDays: Food['weekDays']; + mealTypes: Food['mealTypes']; + inventory?: { availableStock?: number } | null; + }): FoodAvailabilityInfo { + if (!food.inventory) { + return { + isAvailable: false, + unavailabilityMessage: CartMessage.FOOD_NO_INVENTORY, + }; + } - // const queryFilter: FilterQuery = { - // restaurant: { slug }, - // isActive: true, - // weekDays: { $contains: iranWeekDay }, - // ...(mealType ? { mealTypes: { $contains: mealType } } : {}), - // } as unknown as FilterQuery; + if ((food.inventory.availableStock ?? 0) <= 0) { + return { + isAvailable: false, + unavailabilityMessage: CartMessage.INSUFFICIENT_STOCK, + }; + } + + const { iranWeekDay, mealType } = this.getCurrentIranTimeContext(); + const dayNames = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه']; + const mealTypeMap: Record = { + [MealType.BREAKFAST]: 'صبحانه', + [MealType.LUNCH]: 'ناهار', + [MealType.SNACK]: 'عصرانه', + [MealType.DINNER]: 'شام', + }; + + if (food.weekDays?.length > 0 && food.weekDays.length < 7 && !food.weekDays.includes(iranWeekDay)) { + const allowedDays = food.weekDays.map(day => dayNames[day]).join(', '); + const currentDayName = dayNames[iranWeekDay]; + + return { + isAvailable: false, + unavailabilityMessage: `فقط در روزهای ${allowedDays} در دسترس است. امروز ${currentDayName} است.`, + }; + } + + if (food.mealTypes?.length > 0) { + if (mealType === null) { + return { + isAvailable: false, + unavailabilityMessage: 'فقط در ساعات وعده‌های غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است.', + }; + } + + if (!food.mealTypes.includes(mealType)) { + const allowedMealTypes = food.mealTypes.map(mt => mealTypeMap[mt]).join(', '); + const currentMealType = mealTypeMap[mealType]; + + return { + isAvailable: false, + unavailabilityMessage: `فقط برای ${allowedMealTypes} در دسترس است. زمان فعلی ${currentMealType} است.`, + }; + } + } + + return { + isAvailable: true, + unavailabilityMessage: null, + }; + } - // return this.foodRepository.find(queryFilter, { - // populate: ['category'], - // orderBy: { order: 'asc' } - // }); - // } /** * 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(); + 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; + // Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6 + const iranWeekDay = (weekDay + 1) % 7; - // const mealType = this.getMealTypeByHour(currentHour); + const mealType = this.getMealTypeByHour(currentHour); - // return { iranWeekDay, mealType }; - // } + return { iranWeekDay, mealType }; + } /** * Determine meal type based on current hour in Iran timezone.