food avaiability
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-06-15 16:50:51 +03:30
parent 744c93a7ca
commit fae9d1420c
3 changed files with 128 additions and 32 deletions
@@ -21,6 +21,7 @@ import { Permission } from 'src/common/enums/permission.enum';
import { Permissions } from 'src/common/decorators/permissions.decorator'; import { Permissions } from 'src/common/decorators/permissions.decorator';
import { CacheResponse } from 'src/common/decorators/cache-response.decorator'; import { CacheResponse } from 'src/common/decorators/cache-response.decorator';
import { CacheKeyPrefixes } from 'src/common/constants/cache-keys.constant'; import { CacheKeyPrefixes } from 'src/common/constants/cache-keys.constant';
import { PublicFoodListItem } from '../interface/food.interface';
@ApiTags('foods') @ApiTags('foods')
@Controller() @Controller()
@@ -32,7 +33,7 @@ export class FoodController {
@ApiOperation({ summary: 'Get all foods by restaurant slug' }) @ApiOperation({ summary: 'Get all foods by restaurant slug' })
@ApiHeader(API_HEADER_SLUG) @ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' }) @ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
findAllByRestaurant(@Param('slug') slug: string) { findAllByRestaurant(@Param('slug') slug: string): Promise<PublicFoodListItem[]> {
return this.foodsService.findByRestuarantSlug(slug); return this.foodsService.findByRestuarantSlug(slug);
} }
@@ -4,3 +4,26 @@ export enum MealType {
DINNER = 'dinner', DINNER = 'dinner',
SNACK = 'snack', 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;
}
+103 -31
View File
@@ -6,12 +6,11 @@ import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData, FilterQuery } from '@mikro-orm/core'; import { RequiredEntityData, FilterQuery } from '@mikro-orm/core';
import { Food } from '../entities/food.entity'; 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 { 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 { Inventory } from 'src/modules/inventory/entities/inventory.entity';
import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service'; 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 { CacheService } from 'src/modules/utils/cache.service';
import { CacheKeys } from 'src/common/constants/cache-keys.constant'; import { CacheKeys } from 'src/common/constants/cache-keys.constant';
@@ -116,7 +115,7 @@ export class FoodService {
* @param slug - Restaurant slug identifier * @param slug - Restaurant slug identifier
* @returns Array of active foods matching current day and meal time * @returns Array of active foods matching current day and meal time
*/ */
async findByRestuarantSlug(slug: string) { async findByRestuarantSlug(slug: string): Promise<PublicFoodListItem[]> {
const restaurant = await this.restService.findBySlug(slug); const restaurant = await this.restService.findBySlug(slug);
const queryFilter: FilterQuery<Food> = { const queryFilter: FilterQuery<Food> = {
@@ -124,46 +123,119 @@ export class FoodService {
isActive: true, isActive: true,
}; };
return this.foodRepository.find(queryFilter, { const foods = await this.foodRepository.find(queryFilter, {
fields: ['id', 'title', 'price', 'content', 'desc', 'images', 'category', fields: ['id', 'title', 'price', 'content', 'desc', 'images', 'category',
'inPlaceServe', 'pickupServe', 'discount', 'isSpecialOffer'], 'inPlaceServe', 'pickupServe', 'discount', 'isSpecialOffer', 'mealTypes', 'weekDays',
orderBy: { order: 'asc' } '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<Food[]> { /**
// 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<Food> = { if ((food.inventory.availableStock ?? 0) <= 0) {
// restaurant: { slug }, return {
// isActive: true, isAvailable: false,
// weekDays: { $contains: iranWeekDay }, unavailabilityMessage: CartMessage.INSUFFICIENT_STOCK,
// ...(mealType ? { mealTypes: { $contains: mealType } } : {}), };
// } as unknown as FilterQuery<Food>; }
const { iranWeekDay, mealType } = this.getCurrentIranTimeContext();
const dayNames = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه'];
const mealTypeMap: Record<MealType, string> = {
[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). * Get current Iran timezone context (weekday and meal type).
* @returns Object containing Iran weekday (0-6, where 0=Saturday) and current meal type * @returns Object containing Iran weekday (0-6, where 0=Saturday) and current meal type
*/ */
// private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } { private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } {
// const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' })); const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' }));
// const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday
// const currentHour = nowInIran.getHours(); const currentHour = nowInIran.getHours();
// // Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6 // Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6
// // JavaScript: Sunday=0, Monday=1, ..., Saturday=6 const iranWeekDay = (weekDay + 1) % 7;
// // Iran week: 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. * Determine meal type based on current hour in Iran timezone.