diff --git a/app-modules/profile/routes/api-mobile-routes.php b/app-modules/profile/routes/api-mobile-routes.php new file mode 100644 index 00000000..2e7824d7 --- /dev/null +++ b/app-modules/profile/routes/api-mobile-routes.php @@ -0,0 +1,13 @@ +middleware(['api', 'auth:api']) + ->group(static function (): void { + Route::get('/profile', MobileProfileController::class) + ->name('mobile.profile.show'); + }); diff --git a/app-modules/profile/src/Http/Controllers/Mobile/MobileProfileController.php b/app-modules/profile/src/Http/Controllers/Mobile/MobileProfileController.php new file mode 100644 index 00000000..fad2fa02 --- /dev/null +++ b/app-modules/profile/src/Http/Controllers/Mobile/MobileProfileController.php @@ -0,0 +1,35 @@ +user()->id; + + $profile = Profile::ensureExists($userId) + ->load(['profileSkills.skill', 'workExperiences']); + + // ensureExists() pode ter acabado de criar o profile (firstOrCreate); + // ResourceResponse herdaria o 201 do model pra uma rota GET. + return new ProfileResource($profile) + ->response() + ->setStatusCode(Response::HTTP_OK); + } +} diff --git a/app-modules/profile/src/Http/Resources/ProfileResource.php b/app-modules/profile/src/Http/Resources/ProfileResource.php new file mode 100644 index 00000000..7e9c3c15 --- /dev/null +++ b/app-modules/profile/src/Http/Resources/ProfileResource.php @@ -0,0 +1,51 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'nickname' => $this->nickname, + 'headline' => $this->headline, + 'about' => $this->about, + 'seniority_level' => $this->seniority_level?->value, + 'years_experience' => $this->years_experience, + 'social_links' => $this->social_links ?? [], + 'available_for_proposals' => $this->available_for_proposals, + 'start_availability' => $this->start_availability?->value, + 'expected_salary_min' => $this->expected_salary_min, + 'expected_salary_max' => $this->expected_salary_max, + 'preferences' => $this->preferences->toArray(), + 'skills' => $this->profileSkills->map(static fn ($profileSkill): array => [ + 'id' => $profileSkill->skill->id, + 'name' => $profileSkill->skill->name, + 'category' => $profileSkill->skill->category->value, + 'proficiency' => $profileSkill->proficiency->value, + 'years_experience' => $profileSkill->years_experience, + ])->all(), + 'work_experiences' => $this->workExperiences->map(static fn ($experience): array => [ + 'id' => $experience->id, + 'company_name' => $experience->company_name, + 'position' => $experience->position, + 'description' => $experience->description, + 'start_date' => $experience->start_date->toDateString(), + 'end_date' => $experience->end_date?->toDateString(), + 'is_currently_working_here' => $experience->is_currently_working_here, + ])->all(), + ]; + } +} diff --git a/app-modules/profile/tests/Feature/Http/MobileProfileControllerTest.php b/app-modules/profile/tests/Feature/Http/MobileProfileControllerTest.php new file mode 100644 index 00000000..ffb200d0 --- /dev/null +++ b/app-modules/profile/tests/Feature/Http/MobileProfileControllerTest.php @@ -0,0 +1,63 @@ +getJson('/api/mobile/profile') + ->assertUnauthorized(); +}); + +it('creates the profile on first access when it does not exist yet', function (): void { + $user = User::factory()->create(); + Profile::query()->where('user_id', $user->id)->delete(); + + $token = Auth::guard('api')->login($user); + + $this->getJson('/api/mobile/profile', ['Authorization' => "Bearer {$token}"]) + ->assertOk() + ->assertJson([ + 'data' => [ + 'nickname' => null, + 'available_for_proposals' => false, + 'skills' => [], + 'work_experiences' => [], + ], + ]); + + expect(Profile::query()->where('user_id', $user->id)->exists())->toBeTrue(); +}); + +it('returns the full profile with skills and work experiences', function (): void { + $user = User::factory()->create(); + + $profile = Profile::factory() + ->for($user) + ->complete() + ->withSkills(2) + ->create(); + + $profile->workExperiences()->create([ + 'company_name' => 'He4rt Devs', + 'position' => 'Backend Engineer', + 'description' => 'Cuidando da API mobile.', + 'start_date' => '2023-01-01', + 'end_date' => null, + 'is_currently_working_here' => true, + ]); + + $token = Auth::guard('api')->login($user); + + $response = $this->getJson('/api/mobile/profile', ['Authorization' => "Bearer {$token}"]) + ->assertOk(); + + $response->assertJsonPath('data.nickname', $profile->nickname) + ->assertJsonPath('data.headline', $profile->headline) + ->assertJsonPath('data.seniority_level', $profile->seniority_level->value) + ->assertJsonCount(2, 'data.skills') + ->assertJsonPath('data.work_experiences.0.company_name', 'He4rt Devs') + ->assertJsonPath('data.work_experiences.0.is_currently_working_here', true); +}); diff --git a/docs/plans/2026-09-22-api-mobile-jwt.md b/docs/plans/2026-09-22-api-mobile-jwt.md index 75641e48..22d3226c 100644 --- a/docs/plans/2026-09-22-api-mobile-jwt.md +++ b/docs/plans/2026-09-22-api-mobile-jwt.md @@ -138,6 +138,8 @@ Domínio: `He4rt\Profile\*`. v1 é só leitura (o PRD explicitamente escopa "visualização do próprio perfil" — editar fica de fora). Um `ProfileResource` serializa os campos públicos do model (nickname, headline, seniority, social_links, etc.) — sem gap de domínio, `UpsertProfile`/`SyncProfileSkills` já existem se a edição entrar em escopo depois. +**Status: implementado** (`He4rt\Profile\Http\Controllers\Mobile\MobileProfileController` + `Http\Resources\ProfileResource`, também inclui `profileSkills.skill` e `workExperiences`). Rota registrada em `app-modules/profile/routes/api-mobile-routes.php`. + --- ## Fora de escopo (herdado do PRD, sem mudança)