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 { 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<PublicFoodListItem[]> {
return this.foodsService.findByRestuarantSlug(slug);
}
@@ -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;
}
+103 -31
View File
@@ -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<PublicFoodListItem[]> {
const restaurant = await this.restService.findBySlug(slug);
const queryFilter: FilterQuery<Food> = {
@@ -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<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> = {
// restaurant: { slug },
// isActive: true,
// weekDays: { $contains: iranWeekDay },
// ...(mealType ? { mealTypes: { $contains: mealType } } : {}),
// } as unknown as FilterQuery<Food>;
if ((food.inventory.availableStock ?? 0) <= 0) {
return {
isAvailable: false,
unavailabilityMessage: CartMessage.INSUFFICIENT_STOCK,
};
}
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).
* @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.