validate food by week day and meal type

This commit is contained in:
2025-12-20 20:09:41 +03:30
parent 0815fd9780
commit 6318c5b8ab
3 changed files with 139 additions and 0 deletions
@@ -57,6 +57,13 @@ export class CartItemService {
*/
async addOrIncrementItem(cart: Cart, foodId: string, restaurantId: string, quantityToAdd: number): Promise<void> {
const food = await this.validationService.validateAndGetFood(foodId, restaurantId, quantityToAdd);
// Validate meal type compatibility
this.validationService.assertMealTypeCompatibility(food);
// Validate weekday compatibility
this.validationService.assertWeekdayCompatibility(food);
const index = this.getItemIndex(cart, food.id);
if (index < 0) {
@@ -12,6 +12,7 @@ import { Order } from '../../orders/entities/order.entity';
import { OrderStatus } from '../../orders/interface/order.interface';
import { Cart } from '../interfaces/cart.interface';
import { GeographicUtils } from '../utils/geographic.utils';
import { MealType } from '../../foods/interface/food.interface';
@Injectable()
export class CartValidationService {
@@ -290,5 +291,126 @@ export class CartValidationService {
const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['category'] });
return new Map(foods.map(f => [f.id, f]));
}
/**
* Assert all foods in cart have pickupServe true when delivery method is courier
*/
async assertAllFoodsHavePickupServeForCourier(cart: Cart, deliveryMethod: DeliveryMethodEnum): Promise<void> {
if (deliveryMethod !== DeliveryMethodEnum.DeliveryCourier) {
return;
}
if (cart.items.length === 0) {
return;
}
const foodIds = cart.items.map(item => item.foodId);
const foods = await this.em.find(Food, { id: { $in: foodIds } });
const foodsWithoutPickupServe = foods.filter(food => !food.pickupServe);
if (foodsWithoutPickupServe.length > 0) {
const foodTitles = foodsWithoutPickupServe.map(f => f.title || f.id).join(', ');
throw new BadRequestException(
`All foods must have pickupServe enabled for courier delivery. The following foods do not: ${foodTitles}`,
);
}
}
/**
* Get current Iran time context (weekday and 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();
// 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;
const mealType = this.getMealTypeByHour(currentHour);
return { iranWeekDay, mealType };
}
/**
* Determine meal type based on current hour in Iran timezone.
* @param hour - Current hour (0-23)
* @returns Meal type or null if outside meal hours
*/
private getMealTypeByHour(hour: number): MealType | null {
const MEAL_TIME_RANGES = {
BREAKFAST: { start: 6, end: 11 },
LUNCH: { start: 11, end: 15 },
SNACK: { start: 15, end: 19 },
DINNER: { start: 19, end: 24 },
} as const;
if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) {
return MealType.BREAKFAST;
}
if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) {
return MealType.LUNCH;
}
if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) {
return MealType.SNACK;
}
if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) {
return MealType.DINNER;
}
return null;
}
/**
* Assert meal type compatibility - if food has mealTypes, current meal time must be in that array
*/
assertMealTypeCompatibility(food: Food): void {
// If food has no meal type restrictions, allow it
if (!food.mealTypes || food.mealTypes.length === 0) {
return;
}
const { mealType } = this.getCurrentIranTimeContext();
// If current time is outside meal hours, throw error
if (mealType === null) {
throw new BadRequestException(
`Food ${food.title || food.id} is only available during meal times (breakfast, lunch, snack, or dinner). Current time is outside meal hours.`,
);
}
// Check if current meal type is in food's allowed meal types
if (!food.mealTypes.includes(mealType)) {
const allowedMealTypes = food.mealTypes.join(', ');
throw new BadRequestException(
`Food ${food.title || food.id} is only available for ${allowedMealTypes}. Current meal time is ${mealType}, which is incompatible.`,
);
}
}
/**
* Assert weekday compatibility - current weekday must be in food's weekDays array
*/
assertWeekdayCompatibility(food: Food): void {
// If food has no weekday restrictions (empty array or all days), allow it
if (!food.weekDays || food.weekDays.length === 0 || food.weekDays.length === 7) {
return;
}
const { iranWeekDay } = this.getCurrentIranTimeContext();
// Check if current weekday is in food's allowed weekdays
if (!food.weekDays.includes(iranWeekDay)) {
const dayNames = ['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
const currentDayName = dayNames[iranWeekDay];
const allowedDays = food.weekDays.map(day => dayNames[day]).join(', ');
throw new BadRequestException(
`Food ${food.title || food.id} is only available on ${allowedDays}. Today is ${currentDayName}, which is not available.`,
);
}
}
}
@@ -138,6 +138,16 @@ export class CartService {
cart.description = description;
}
// Final validation: if effective delivery method is courier, ensure all foods have pickupServe
const effectiveDeliveryMethodId = deliveryMethodId ?? cart.deliveryMethodId;
if (effectiveDeliveryMethodId) {
const effectiveDeliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
restaurantId,
effectiveDeliveryMethodId,
);
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, effectiveDeliveryMethod.method);
}
// Final recalculation + save and return cart
return this.recalculateAndSaveCart(cart);
}