From 6318c5b8ab6fccc276c2687c4fae3e80069ed75b Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 20 Dec 2025 20:09:41 +0330 Subject: [PATCH] validate food by week day and meal type --- .../cart/providers/cart-item.service.ts | 7 + .../cart/providers/cart-validation.service.ts | 122 ++++++++++++++++++ src/modules/cart/providers/cart.service.ts | 10 ++ 3 files changed, 139 insertions(+) diff --git a/src/modules/cart/providers/cart-item.service.ts b/src/modules/cart/providers/cart-item.service.ts index 6d2c710..f6963cc 100644 --- a/src/modules/cart/providers/cart-item.service.ts +++ b/src/modules/cart/providers/cart-item.service.ts @@ -57,6 +57,13 @@ export class CartItemService { */ async addOrIncrementItem(cart: Cart, foodId: string, restaurantId: string, quantityToAdd: number): Promise { 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) { diff --git a/src/modules/cart/providers/cart-validation.service.ts b/src/modules/cart/providers/cart-validation.service.ts index 4d819bc..d57d665 100644 --- a/src/modules/cart/providers/cart-validation.service.ts +++ b/src/modules/cart/providers/cart-validation.service.ts @@ -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 { + 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.`, + ); + } + } } diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts index 29791b8..d9f8a9c 100644 --- a/src/modules/cart/providers/cart.service.ts +++ b/src/modules/cart/providers/cart.service.ts @@ -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); }