subWeek()->startOfWeek(CarbonInterface::MONDAY); $periodStart = $periodStart->startOfDay(); $periodEnd = $periodStart->endOfWeek(CarbonInterface::SUNDAY); $existingReview = $user->weeklyReviews()->whereDate('period_start', $periodStart)->first(); if ($existingReview?->accepted_at !== null) { return $existingReview; } $currentPlan = $user->nutritionPlans()->where('is_active', true)->latest('effective_from')->firstOrFail(); $nutritionPlan = $user->nutritionPlans() ->where('effective_from', '<=', $periodEnd) ->latest('effective_from') ->first() ?? $currentPlan; $meals = MealPosts::query() ->where('user_id', $user->getKey()) ->whereBetween('eaten_at', [$periodStart, $periodEnd]) ->get(['calories', 'proteins', 'carbs', 'fats', 'eaten_at']); $loggedDaysCount = $meals->pluck('eaten_at') ->map(fn (CarbonInterface $date): string => $date->toDateString()) ->unique() ->count(); $averages = $this->nutritionAverages($meals, $loggedDaysCount); $weights = $user->weightEntries() ->whereBetween('measured_at', [$periodStart->subWeek(), $periodEnd]) ->oldest('measured_at') ->get(); $trend = $this->weightTrend($weights); $latestWeight = $weights->last()?->weight_kg ?? $user->latestWeightEntry()->value('weight_kg'); $targetTrend = $latestWeight === null ? null : $this->targetWeightTrend( (float) $latestWeight, $user->weight_goal, $user->pace_preference, ); [$status, $adjustment, $messageCode] = $this->recommendation( loggedDaysCount: $loggedDaysCount, weightCount: $weights->count(), trend: $trend, targetTrend: $targetTrend, averageCalories: $averages['calories'], planCalories: $nutritionPlan->calories, weightGoal: $user->weight_goal, ); if ($nutritionPlan->getKey() !== $currentPlan->getKey()) { $status = WeeklyReviewStatus::INSUFFICIENT_DATA; $adjustment = 0; $messageCode = 'WEEKLY_REVIEW_PLAN_CHANGED'; } return $user->weeklyReviews()->updateOrCreate( ['period_start' => $periodStart->toDateString()], [ 'nutrition_plan_id' => $nutritionPlan->getKey(), 'period_end' => $periodEnd->toDateString(), 'average_calories' => $averages['calories'], 'average_proteins' => $averages['proteins'], 'average_carbs' => $averages['carbs'], 'average_fats' => $averages['fats'], 'logged_days_count' => $loggedDaysCount, 'weight_entries_count' => $weights->count(), 'weight_trend_kg' => $trend, 'target_weight_trend_kg' => $targetTrend, 'status' => $status, 'recommended_calorie_adjustment' => $adjustment, 'message_code' => $messageCode, 'metrics' => [ 'analysisStart' => $periodStart->subWeek()->toDateString(), 'analysisEnd' => $periodEnd->toDateString(), 'firstWeightKg' => $weights->first()?->weight_kg, 'lastWeightKg' => $weights->last()?->weight_kg, 'nutritionPlanCalories' => $nutritionPlan->calories, ], 'generated_at' => now(), ], ); } /** * @param Collection $meals * @return array{calories: float, proteins: float, carbs: float, fats: float} */ private function nutritionAverages(Collection $meals, int $loggedDaysCount): array { $divisor = max(1, $loggedDaysCount); return [ 'calories' => round((float) $meals->sum('calories') / $divisor, 2), 'proteins' => round((float) $meals->sum('proteins') / $divisor, 2), 'carbs' => round((float) $meals->sum('carbs') / $divisor, 2), 'fats' => round((float) $meals->sum('fats') / $divisor, 2), ]; } private function weightTrend(Collection $weights): ?float { $first = $weights->first(); $last = $weights->last(); if ($weights->count() < 4 || $first === null || $last === null) { return null; } $days = max(1, $first->measured_at->diffInDays($last->measured_at)); if ($days < 7) { return null; } return round((($last->weight_kg - $first->weight_kg) / $days) * 7, 2); } private function targetWeightTrend( float $weightKg, WeightGoal $weightGoal, PacePreference $pacePreference, ): float { $percentage = match ($weightGoal) { WeightGoal::LOSE_WEIGHT => match ($pacePreference) { PacePreference::SLOW => -0.0025, PacePreference::NORMAL => -0.005, PacePreference::FAST => -0.0075, }, WeightGoal::GAIN_WEIGHT => match ($pacePreference) { PacePreference::SLOW => 0.00125, PacePreference::NORMAL => 0.0025, PacePreference::FAST => 0.00375, }, WeightGoal::MAINTAIN_WEIGHT => 0, }; return round($weightKg * $percentage, 2); } /** * @return array{WeeklyReviewStatus, int, string} */ private function recommendation( int $loggedDaysCount, int $weightCount, ?float $trend, ?float $targetTrend, float $averageCalories, int $planCalories, WeightGoal $weightGoal, ): array { if ($loggedDaysCount < 5 || $weightCount < 4 || $trend === null || $targetTrend === null) { return [ WeeklyReviewStatus::INSUFFICIENT_DATA, 0, 'WEEKLY_REVIEW_INSUFFICIENT_TRACKING', ]; } $calorieDifferenceRatio = abs($averageCalories - $planCalories) / max(1, $planCalories); if ($calorieDifferenceRatio > 0.15) { return [ WeeklyReviewStatus::INSUFFICIENT_DATA, 0, 'WEEKLY_REVIEW_INSUFFICIENT_ADHERENCE', ]; } $tolerance = $weightGoal === WeightGoal::MAINTAIN_WEIGHT ? 0.2 : max(0.1, abs($targetTrend) * 0.35); $difference = $trend - $targetTrend; if (abs($difference) <= $tolerance) { return [ WeeklyReviewStatus::ON_TRACK, 0, 'WEEKLY_REVIEW_ON_TRACK', ]; } $adjustment = match ($weightGoal) { WeightGoal::LOSE_WEIGHT => $difference > 0 ? -100 : 100, WeightGoal::GAIN_WEIGHT => $difference < 0 ? 100 : -100, WeightGoal::MAINTAIN_WEIGHT => $trend > 0 ? -100 : 100, }; return [ WeeklyReviewStatus::ADJUSTMENT_RECOMMENDED, $adjustment, $this->adjustmentMessage($weightGoal, $adjustment), ]; } private function adjustmentMessage(WeightGoal $weightGoal, int $adjustment): string { return match ($weightGoal) { WeightGoal::LOSE_WEIGHT => $adjustment < 0 ? 'WEEKLY_REVIEW_LOSS_SLOWER' : 'WEEKLY_REVIEW_LOSS_FASTER', WeightGoal::GAIN_WEIGHT => $adjustment > 0 ? 'WEEKLY_REVIEW_GAIN_SLOWER' : 'WEEKLY_REVIEW_GAIN_FASTER', WeightGoal::MAINTAIN_WEIGHT => $adjustment > 0 ? 'WEEKLY_REVIEW_MAINTENANCE_DOWN' : 'WEEKLY_REVIEW_MAINTENANCE_UP', }; } }