This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user