feat: history and calculate needs for user
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\NutritionPlanSource;
|
||||
use App\Enums\WeeklyReviewStatus;
|
||||
use App\Models\NutritionPlan;
|
||||
use App\Models\WeeklyReview;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AcceptWeeklyReviewAdjustment
|
||||
{
|
||||
public function execute(WeeklyReview $weeklyReview): NutritionPlan
|
||||
{
|
||||
return DB::transaction(function () use ($weeklyReview): NutritionPlan {
|
||||
$lockedReview = WeeklyReview::query()->lockForUpdate()->findOrFail($weeklyReview->getKey());
|
||||
|
||||
abort_if($lockedReview->accepted_at !== null, 409, 'WEEKLY_ADJUSTMENT_ALREADY_ACCEPTED');
|
||||
abort_unless(
|
||||
$lockedReview->status === WeeklyReviewStatus::ADJUSTMENT_RECOMMENDED
|
||||
&& $lockedReview->recommended_calorie_adjustment !== 0,
|
||||
409,
|
||||
'WEEKLY_ADJUSTMENT_UNAVAILABLE',
|
||||
);
|
||||
|
||||
$user = $lockedReview->user()->lockForUpdate()->firstOrFail();
|
||||
$currentPlan = $user->nutritionPlans()->where('is_active', true)->latest('effective_from')->firstOrFail();
|
||||
|
||||
abort_unless(
|
||||
$lockedReview->nutrition_plan_id === $currentPlan->getKey(),
|
||||
409,
|
||||
'WEEKLY_ADJUSTMENT_STALE',
|
||||
);
|
||||
|
||||
$now = now();
|
||||
$targetCalories = max(1200, $currentPlan->calories + $lockedReview->recommended_calorie_adjustment);
|
||||
$proteins = (int) round($currentPlan->proteins);
|
||||
$fats = (int) round($currentPlan->fats);
|
||||
$carbs = max(0, (int) round(($targetCalories - ($proteins * 4) - ($fats * 9)) / 4));
|
||||
$calories = ($proteins * 4) + ($carbs * 4) + ($fats * 9);
|
||||
|
||||
abort_if(
|
||||
$calories === $currentPlan->calories,
|
||||
409,
|
||||
'WEEKLY_ADJUSTMENT_UNAVAILABLE',
|
||||
);
|
||||
|
||||
$currentPlan->update([
|
||||
'is_active' => false,
|
||||
'effective_until' => $now,
|
||||
]);
|
||||
|
||||
$nextPlan = $user->nutritionPlans()->create([
|
||||
'source' => NutritionPlanSource::WEEKLY_ADJUSTMENT,
|
||||
'formula_version' => $currentPlan->formula_version,
|
||||
'calories' => $calories,
|
||||
'proteins' => $proteins,
|
||||
'carbs' => $carbs,
|
||||
'fats' => $fats,
|
||||
'resting_metabolism' => $currentPlan->resting_metabolism,
|
||||
'maintenance_calories' => $currentPlan->maintenance_calories,
|
||||
'calorie_adjustment_percent' => ($calories - $currentPlan->maintenance_calories) / max(1, $currentPlan->maintenance_calories),
|
||||
'calculation_details' => [
|
||||
...$currentPlan->calculation_details,
|
||||
'weeklyReviewId' => $lockedReview->getKey(),
|
||||
'weeklyAdjustmentCalories' => $lockedReview->recommended_calorie_adjustment,
|
||||
],
|
||||
'effective_from' => $now,
|
||||
]);
|
||||
|
||||
$lockedReview->update([
|
||||
'accepted_at' => $now,
|
||||
'accepted_nutrition_plan_id' => $nextPlan->getKey(),
|
||||
]);
|
||||
|
||||
return $nextPlan;
|
||||
}, attempts: 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\NutritionPlanSource;
|
||||
use App\Models\User;
|
||||
|
||||
class CompleteNutritionOnboarding
|
||||
{
|
||||
public function __construct(private RecalculateNutritionPlan $recalculateNutritionPlan) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function execute(User $user, array $data): User
|
||||
{
|
||||
if ($user->hasCompletedNutritionOnboarding()) {
|
||||
return $user->loadMissing(['activeNutritionPlan', 'latestWeightEntry']);
|
||||
}
|
||||
|
||||
return $this->recalculateNutritionPlan->execute(
|
||||
user: $user,
|
||||
data: $data,
|
||||
source: NutritionPlanSource::ONBOARDING,
|
||||
completeOnboarding: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\NutritionPlanSource;
|
||||
use App\Enums\PacePreference;
|
||||
use App\Enums\PhysicalActivityLevel;
|
||||
use App\Enums\UserSex;
|
||||
use App\Enums\WeightGoal;
|
||||
use App\Models\User;
|
||||
use App\Services\NutritionPlanCalculator;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RecalculateNutritionPlan
|
||||
{
|
||||
public function __construct(private NutritionPlanCalculator $calculator) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function execute(
|
||||
User $user,
|
||||
array $data,
|
||||
NutritionPlanSource $source,
|
||||
bool $completeOnboarding = false,
|
||||
): User {
|
||||
return DB::transaction(function () use ($user, $data, $source, $completeOnboarding): User {
|
||||
$lockedUser = User::query()->lockForUpdate()->findOrFail($user->getKey());
|
||||
$now = now();
|
||||
|
||||
if ($completeOnboarding && $lockedUser->hasCompletedNutritionOnboarding()) {
|
||||
return $lockedUser->load(['activeNutritionPlan', 'latestWeightEntry']);
|
||||
}
|
||||
|
||||
$lockedUser->update([
|
||||
'height' => $data['heightCm'],
|
||||
'target_weight' => $data['targetWeightKg'] ?? null,
|
||||
'date_of_birth' => $data['dateOfBirth'],
|
||||
'sex' => $data['sex'],
|
||||
'physical_activity_level' => $data['physicalActivityLevel'],
|
||||
'weight_goal' => $data['weightGoal'],
|
||||
'pace_preference' => $data['pacePreference'],
|
||||
...($completeOnboarding ? [
|
||||
'onboarding_completed_at' => $now,
|
||||
'nutrition_estimate_accepted_at' => $now,
|
||||
] : []),
|
||||
]);
|
||||
|
||||
$weightKg = (float) $data['weightKg'];
|
||||
$latestWeight = $lockedUser->weightEntries()->latest('measured_at')->first();
|
||||
|
||||
if ($latestWeight === null || abs($latestWeight->weight_kg - $weightKg) >= 0.01) {
|
||||
$lockedUser->weightEntries()->create([
|
||||
'weight_kg' => $weightKg,
|
||||
'measured_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
$lockedUser->nutritionPlans()
|
||||
->where('is_active', true)
|
||||
->update([
|
||||
'is_active' => false,
|
||||
'effective_until' => $now,
|
||||
]);
|
||||
|
||||
$calculation = $this->calculator->calculate(
|
||||
weightKg: $weightKg,
|
||||
heightCm: (int) $data['heightCm'],
|
||||
dateOfBirth: CarbonImmutable::parse($data['dateOfBirth']),
|
||||
sex: UserSex::from($data['sex']),
|
||||
activityLevel: PhysicalActivityLevel::from($data['physicalActivityLevel']),
|
||||
weightGoal: WeightGoal::from($data['weightGoal']),
|
||||
pacePreference: PacePreference::from($data['pacePreference']),
|
||||
);
|
||||
|
||||
$lockedUser->nutritionPlans()->create([
|
||||
...$calculation,
|
||||
'source' => $source,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
]);
|
||||
|
||||
return $lockedUser->fresh()->load(['activeNutritionPlan', 'latestWeightEntry']);
|
||||
}, attempts: 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user