From 6ac36e3de4aade3c71af5b30dad4108699170038 Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Thu, 21 May 2026 12:02:02 +0200 Subject: [PATCH] feat: add user fields --- app/Enums/PacePreference.php | 10 +++ app/Enums/PhysicalActivityLevel.php | 12 ++++ app/Enums/UserSex.php | 11 +++ app/Enums/WeightGoal.php | 10 +++ app/Http/Controllers/AuthController.php | 59 ++++++++++++---- app/Http/Requests/RegisterRequest.php | 11 +++ app/Http/Requests/UpdateUserRequest.php | 11 +++ app/Http/Resources/UserResource.php | 5 ++ app/Models/User.php | 14 ++++ ...05_21_091112_add_fields_to_users_table.php | 38 ++++++++++ tests/Feature/EmailVerificationTest.php | 26 ++++++- tests/Feature/UserNutritionGoalsTest.php | 70 +++++++++++++++++++ 12 files changed, 262 insertions(+), 15 deletions(-) create mode 100644 app/Enums/PacePreference.php create mode 100644 app/Enums/PhysicalActivityLevel.php create mode 100644 app/Enums/UserSex.php create mode 100644 app/Enums/WeightGoal.php create mode 100644 database/migrations/2026_05_21_091112_add_fields_to_users_table.php diff --git a/app/Enums/PacePreference.php b/app/Enums/PacePreference.php new file mode 100644 index 0000000..61da46a --- /dev/null +++ b/app/Enums/PacePreference.php @@ -0,0 +1,10 @@ +file('avatar')->store('avatars', 'public'); } - $user = User::create([ - 'name' => $data['name'], - 'email' => strtolower($data['email']), - 'password' => Hash::make($data['password']), - 'avatar_url' => $avatarPath, - ]); + $userAttributes = $this->userAttributesFromRequestData($data); + $userAttributes['email'] = strtolower($data['email']); + $userAttributes['password'] = Hash::make($data['password']); + $userAttributes['avatar_url'] = $avatarPath; + + $user = User::create($userAttributes); $user->sendEmailVerificationNotification(); @@ -158,18 +159,17 @@ class AuthController extends Controller $data['avatar_url'] = $path; } - unset($data['avatar']); - unset($data['nutritionGoals']); + $userAttributes = $this->userAttributesFromRequestData($data); if (is_array($nutritionGoals)) { - $data['daily_calorie_goal'] = $nutritionGoals['calories']; - $data['daily_protein_goal'] = $nutritionGoals['proteins']; - $data['daily_carbs_goal'] = $nutritionGoals['carbs']; - $data['daily_fats_goal'] = $nutritionGoals['fats']; + $userAttributes['daily_calorie_goal'] = $nutritionGoals['calories']; + $userAttributes['daily_protein_goal'] = $nutritionGoals['proteins']; + $userAttributes['daily_carbs_goal'] = $nutritionGoals['carbs']; + $userAttributes['daily_fats_goal'] = $nutritionGoals['fats']; } - if (! empty($data)) { - $user->update($data); + if (! empty($userAttributes)) { + $user->update($userAttributes); } return new UserResource($user->fresh()); @@ -212,4 +212,35 @@ class AuthController extends Controller { return filled($path) && ! Str::startsWith($path, ['http://', 'https://']); } + + /** + * @param array $data + * @return array + */ + private function userAttributesFromRequestData(array $data): array + { + $attributes = Arr::only($data, [ + 'name', + 'avatar_url', + 'height', + 'bio', + 'sex', + 'date_of_birth', + ]); + + $attributeMap = [ + 'physicalActivityLevel' => 'physical_activity_level', + 'weightGoal' => 'weight_goal', + 'pacePreference' => 'pace_preference', + 'dateOfBirth' => 'date_of_birth', + ]; + + foreach ($attributeMap as $requestKey => $attribute) { + if (array_key_exists($requestKey, $data)) { + $attributes[$attribute] = $data[$requestKey]; + } + } + + return $attributes; + } } diff --git a/app/Http/Requests/RegisterRequest.php b/app/Http/Requests/RegisterRequest.php index be0867e..0bc1a92 100644 --- a/app/Http/Requests/RegisterRequest.php +++ b/app/Http/Requests/RegisterRequest.php @@ -2,7 +2,12 @@ namespace App\Http\Requests; +use App\Enums\PacePreference; +use App\Enums\PhysicalActivityLevel; +use App\Enums\UserSex; +use App\Enums\WeightGoal; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class RegisterRequest extends FormRequest { @@ -28,6 +33,12 @@ class RegisterRequest extends FormRequest 'password' => ['required', 'string', 'min:8'], 'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'], 'bio' => ['nullable', 'string'], + 'physicalActivityLevel' => ['sometimes', Rule::enum(PhysicalActivityLevel::class)], + 'weightGoal' => ['sometimes', Rule::enum(WeightGoal::class)], + 'pacePreference' => ['sometimes', Rule::enum(PacePreference::class)], + 'sex' => ['sometimes', Rule::enum(UserSex::class)], + 'dateOfBirth' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'before:today', 'after:1900-01-01'], + 'date_of_birth' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'before:today', 'after:1900-01-01'], ]; } diff --git a/app/Http/Requests/UpdateUserRequest.php b/app/Http/Requests/UpdateUserRequest.php index b53b6de..34ae76e 100644 --- a/app/Http/Requests/UpdateUserRequest.php +++ b/app/Http/Requests/UpdateUserRequest.php @@ -2,7 +2,12 @@ namespace App\Http\Requests; +use App\Enums\PacePreference; +use App\Enums\PhysicalActivityLevel; +use App\Enums\UserSex; +use App\Enums\WeightGoal; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class UpdateUserRequest extends FormRequest { @@ -23,6 +28,12 @@ class UpdateUserRequest extends FormRequest 'nutritionGoals.proteins' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'], 'nutritionGoals.carbs' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'], 'nutritionGoals.fats' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'], + 'physicalActivityLevel' => ['sometimes', Rule::enum(PhysicalActivityLevel::class)], + 'weightGoal' => ['sometimes', Rule::enum(WeightGoal::class)], + 'pacePreference' => ['sometimes', Rule::enum(PacePreference::class)], + 'sex' => ['sometimes', Rule::enum(UserSex::class)], + 'dateOfBirth' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'before:today', 'after:1900-01-01'], + 'date_of_birth' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'before:today', 'after:1900-01-01'], ]; } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 80b1414..c628933 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -23,6 +23,11 @@ class UserResource extends JsonResource 'height' => $this->height, 'avatarUrl' => $this->avatar_url ? asset(Storage::url($this->avatar_url)) : null, 'bio' => $this->bio, + 'physicalActivityLevel' => $this->physical_activity_level, + 'weightGoal' => $this->weight_goal, + 'pacePreference' => $this->pace_preference, + 'sex' => $this->sex, + 'dateOfBirth' => $this->date_of_birth?->toDateString(), 'nutritionGoals' => [ 'calories' => (int) $this->daily_calorie_goal, 'proteins' => (float) $this->daily_protein_goal, diff --git a/app/Models/User.php b/app/Models/User.php index 162ef86..cc23e04 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,6 +2,10 @@ namespace App\Models; +use App\Enums\PacePreference; +use App\Enums\PhysicalActivityLevel; +use App\Enums\UserSex; +use App\Enums\WeightGoal; use Filament\Models\Contracts\FilamentUser; use Filament\Panel; use Illuminate\Contracts\Auth\MustVerifyEmail; @@ -35,6 +39,11 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail 'daily_protein_goal', 'daily_carbs_goal', 'daily_fats_goal', + 'physical_activity_level', + 'weight_goal', + 'pace_preference', + 'sex', + 'date_of_birth', ]; /** @@ -61,6 +70,11 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail 'daily_protein_goal' => 'float', 'daily_carbs_goal' => 'float', 'daily_fats_goal' => 'float', + 'physical_activity_level' => PhysicalActivityLevel::class, + 'weight_goal' => WeightGoal::class, + 'pace_preference' => PacePreference::class, + 'sex' => UserSex::class, + 'date_of_birth' => 'immutable_date', ]; } diff --git a/database/migrations/2026_05_21_091112_add_fields_to_users_table.php b/database/migrations/2026_05_21_091112_add_fields_to_users_table.php new file mode 100644 index 0000000..7f7818d --- /dev/null +++ b/database/migrations/2026_05_21_091112_add_fields_to_users_table.php @@ -0,0 +1,38 @@ +date('date_of_birth')->nullable(); + $table->string('sex')->default('unknown'); + $table->string('physical_activity_level')->default('sedentary'); + $table->string('weight_goal')->default('lose_weight'); + $table->string('pace_preference')->default('slow'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'date_of_birth', + 'sex', + 'physical_activity_level', + 'weight_goal', + 'pace_preference', + ]); + }); + } +}; diff --git a/tests/Feature/EmailVerificationTest.php b/tests/Feature/EmailVerificationTest.php index fd9244e..999ab2a 100644 --- a/tests/Feature/EmailVerificationTest.php +++ b/tests/Feature/EmailVerificationTest.php @@ -1,5 +1,9 @@ 'Léon', 'email' => 'leon@example.com', 'password' => 'password', + 'physicalActivityLevel' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value, + 'weightGoal' => WeightGoal::MAINTAIN_WEIGHT->value, + 'pacePreference' => PacePreference::NORMAL->value, + 'sex' => UserSex::WOMAN->value, + 'dateOfBirth' => '2003-04-24', ]) ->assertCreated() ->assertJsonPath('user.email', 'leon@example.com') - ->assertJsonPath('user.emailVerified', false); + ->assertJsonPath('user.emailVerified', false) + ->assertJsonPath('user.physicalActivityLevel', PhysicalActivityLevel::LIGHTLY_ACTIVE->value) + ->assertJsonPath('user.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value) + ->assertJsonPath('user.pacePreference', PacePreference::NORMAL->value) + ->assertJsonPath('user.sex', UserSex::WOMAN->value) + ->assertJsonPath('user.dateOfBirth', '2003-04-24'); $user = User::where('email', 'leon@example.com')->firstOrFail(); expect($user->hasVerifiedEmail())->toBeFalse(); + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'physical_activity_level' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value, + 'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value, + 'pace_preference' => PacePreference::NORMAL->value, + 'sex' => UserSex::WOMAN->value, + ]); + + expect($user->date_of_birth?->toDateString())->toBe('2003-04-24'); + Notification::assertSentTo($user, VerifyEmail::class); }); diff --git a/tests/Feature/UserNutritionGoalsTest.php b/tests/Feature/UserNutritionGoalsTest.php index d92ca19..8c5b64f 100644 --- a/tests/Feature/UserNutritionGoalsTest.php +++ b/tests/Feature/UserNutritionGoalsTest.php @@ -1,5 +1,9 @@ create([ + 'physical_activity_level' => PhysicalActivityLevel::VERY_ACTIVE->value, + 'weight_goal' => WeightGoal::GAIN_WEIGHT->value, + 'pace_preference' => PacePreference::FAST->value, + ]); + + Sanctum::actingAs($user); + + $this->getJson('/api/auth/me') + ->assertOk() + ->assertJsonPath('data.physicalActivityLevel', PhysicalActivityLevel::VERY_ACTIVE->value) + ->assertJsonPath('data.weightGoal', WeightGoal::GAIN_WEIGHT->value) + ->assertJsonPath('data.pacePreference', PacePreference::FAST->value); +}); + +it('updates profile preferences for the authenticated user', function () { + $user = User::factory()->create(); + + Sanctum::actingAs($user); + + $this->patchJson('/api/auth/me', [ + 'physicalActivityLevel' => PhysicalActivityLevel::MODERATELY_ACTIVE->value, + 'weightGoal' => WeightGoal::MAINTAIN_WEIGHT->value, + 'pacePreference' => PacePreference::NORMAL->value, + 'sex' => UserSex::MAN->value, + 'dateOfBirth' => '2003-04-24', + ]) + ->assertOk() + ->assertJsonPath('data.physicalActivityLevel', PhysicalActivityLevel::MODERATELY_ACTIVE->value) + ->assertJsonPath('data.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value) + ->assertJsonPath('data.pacePreference', PacePreference::NORMAL->value) + ->assertJsonPath('data.sex', UserSex::MAN->value) + ->assertJsonPath('data.dateOfBirth', '2003-04-24'); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'physical_activity_level' => PhysicalActivityLevel::MODERATELY_ACTIVE->value, + 'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value, + 'pace_preference' => PacePreference::NORMAL->value, + 'sex' => UserSex::MAN->value, + ]); + + expect($user->fresh()->date_of_birth?->toDateString())->toBe('2003-04-24'); +}); + +it('validates profile preferences', function () { + Sanctum::actingAs(User::factory()->create()); + + $this->patchJson('/api/auth/me', [ + 'physicalActivityLevel' => 'daily', + 'weightGoal' => 'bulk', + 'pacePreference' => 'urgent', + 'sex' => 'x', + 'dateOfBirth' => 'tomorrow', + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors([ + 'physicalActivityLevel', + 'weightGoal', + 'pacePreference', + 'sex', + 'dateOfBirth', + ]); +});