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 { 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user