import type { MealPlanRepository } from './meal-plans.repository.js'; import { NotFoundError, BadRequestError } from '../../common/errors.js'; import type { CreateMealPlanInput, UpdateMealPlanInput, MealPlanQueryInput, MealPlanStatus, NutritionInfo, MealPlanDaySchema, } from '@meshitrack/shared'; import { type z } from 'zod/v4'; type MealPlanDay = z.infer; interface Deps { mealPlanRepository: MealPlanRepository; } export class MealPlanService { private readonly mealPlanRepository: MealPlanRepository; public constructor({ mealPlanRepository }: Deps) { this.mealPlanRepository = mealPlanRepository; } public async list(householdId: string, query: MealPlanQueryInput) { return this.mealPlanRepository.findByHousehold(householdId, query); } public async getById(id: string, householdId: string) { const plan = await this.mealPlanRepository.findById(id, householdId); if (!plan) { throw new NotFoundError('Meal plan not found'); } return plan; } public async getByWeek(householdId: string, weekStartDate: string) { return this.mealPlanRepository.findByWeek(householdId, weekStartDate); } public async create(householdId: string, createdBy: string, input: CreateMealPlanInput) { // Prevent overlapping meal plans for same household/week const existing = await this.mealPlanRepository.findByWeek(householdId, input.weekStartDate); if (existing) { throw new BadRequestError( `A meal plan already exists for household ${householdId} starting ${input.weekStartDate}`, ); } // Force re-calculation of daily totals to ensure correctness const updatedDays = input.days.map((day) => this.computeDayTotals(day)); return this.mealPlanRepository.create({ ...input, days: updatedDays, householdId, createdBy, }); } public async update(id: string, householdId: string, input: UpdateMealPlanInput) { const existing = await this.getById(id, householdId); const data: Record = {}; if (input.status !== undefined) data.status = input.status; if (input.shoppingListId !== undefined) data.shoppingListId = input.shoppingListId; if (input.days !== undefined) { // Force re-calculation of daily totals data.days = input.days.map((day) => this.computeDayTotals(day)); } const updated = await this.mealPlanRepository.update(id, householdId, data); if (!updated) { throw new NotFoundError('Meal plan not found'); } return updated; } public async updateStatus(id: string, householdId: string, status: MealPlanStatus) { await this.getById(id, householdId); const updated = await this.mealPlanRepository.updateStatus(id, householdId, status); if (!updated) { throw new NotFoundError('Meal plan not found'); } return updated; } public async delete(id: string, householdId: string) { await this.getById(id, householdId); const deleted = await this.mealPlanRepository.delete(id, householdId); if (!deleted) { throw new NotFoundError('Meal plan not found'); } return deleted; } /** Calculates standard daily totals based on the component meals. */ private computeDayTotals(day: MealPlanDay): MealPlanDay { const total = { calories: 0, protein: 0, carbs: 0, fat: 0, fiber: 0, sugar: 0, sodium: 0, saturatedFat: 0, cholesterol: 0, }; for (const meal of day.meals) { const servings = meal.servings; // Use customNutrition if provided, otherwise scale perServingNutrition const source = meal.customNutrition ?? meal.perServingNutrition; total.calories += source.calories * servings; total.protein += source.protein * servings; total.carbs += source.carbs * servings; total.fat += source.fat * servings; if (source.fiber != null) total.fiber += source.fiber * servings; if (source.sugar != null) total.sugar += source.sugar * servings; if (source.sodium != null) total.sodium += source.sodium * servings; if (source.saturatedFat != null) { total.saturatedFat += source.saturatedFat * servings; } if (source.cholesterol != null) { total.cholesterol += source.cholesterol * servings; } } // Round final totals to 2 decimal places return { ...day, dailyNutritionTotal: { calories: Math.round(total.calories * 100) / 100, protein: Math.round(total.protein * 100) / 100, carbs: Math.round(total.carbs * 100) / 100, fat: Math.round(total.fat * 100) / 100, fiber: Math.round(total.fiber * 100) / 100, sugar: Math.round(total.sugar * 100) / 100, sodium: Math.round(total.sodium * 100) / 100, saturatedFat: Math.round(total.saturatedFat * 100) / 100, cholesterol: Math.round(total.cholesterol * 100) / 100, }, }; } }