feat(profile): phase 1 — domain actions, legacy cleanup, and profile edit page#282
Conversation
Domain actions for profile management: - UpsertProfileDTO with fromArray() factory for Filament/test compatibility - UpsertProfile action with validation (about max 500, headline max 100, social_links enum check) - ToggleAvailability action with required start_availability when activating - Feature tests covering all BDD scenarios from the issue
…254) - Delete Information and Address models, factories, actions, DTOs, exceptions - Remove UserInformationForm Filament schema - Remove information() relationship from User model - Update EditProfileCommand and IntroductionCommand to use Profile module - GitHub/LinkedIn fields removed from bot modals (now from OAuth/ExternalIdentity) - Tables user_information and user_address preserved in database as safety net
📝 WalkthroughWalkthroughThis PR migrates profile persistence from the Identity information flow to the new Profile module: it adds UpsertProfileDTO, UpsertProfile, and ToggleAvailability with validation and tests; updates Discord EditProfile/Introduction commands and adds AddressCommand; removes the User->information relationship and stops creating legacy information/address on external account creation; and introduces a Filament ProfilePage, Blade components, translations, provider registration, and accompanying tests. Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
app-modules/profile/tests/Feature/UpsertProfileTest.php (1)
83-90: ⚡ Quick winAdd a lower-bound validation test for
years_experience(-1).The range contract is
0–50, but this suite only checks51. Adding a-1case closes a regression gap.Suggested additional test
+test('upsert profile rejects years_experience below 0', function (): void { + $profile = Profile::factory()->create(); + $dto = UpsertProfileDTO::fromArray([ + 'years_experience' => -1, + ]); + + resolve(UpsertProfile::class)->handle($profile, $dto); +})->throws(ValidationException::class);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/profile/tests/Feature/UpsertProfileTest.php` around lines 83 - 90, Add a new test in UpsertProfileTest that mirrors the existing "upsert profile rejects years_experience outside 0-50 range" case but passes years_experience = -1 to UpsertProfileDTO::fromArray and asserts that resolve(UpsertProfile::class)->handle($profile, $dto) throws ValidationException; this ensures the lower bound is validated the same as the existing upper-bound (51) test.app-modules/profile/tests/Feature/ToggleAvailabilityTest.php (1)
53-62: ⚡ Quick winAssert
start_availabilityremainsnullin the idempotent deactivation case.This test title says idempotent behavior, but it only asserts
available_for_proposals. Add astart_availabilityassertion to lock the full contract.Suggested test assertion
test('deactivate already inactive availability is idempotent', function (): void { $profile = Profile::factory()->create([ 'available_for_proposals' => false, 'start_availability' => null, ]); $result = resolve(ToggleAvailability::class)->handle($profile, false); - expect($result->available_for_proposals)->toBeFalse(); + expect($result->available_for_proposals)->toBeFalse() + ->and($result->start_availability)->toBeNull(); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/profile/tests/Feature/ToggleAvailabilityTest.php` around lines 53 - 62, The test "deactivate already inactive availability is idempotent" currently only asserts available_for_proposals; add an assertion that start_availability stays null to enforce the idempotent contract. After calling resolve(ToggleAvailability::class)->handle($profile, false) (the $result variable), add an assertion like expect($result->start_availability)->toBeNull() so the test verifies both availability and start_availability remain unchanged.app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php (1)
125-127: 💤 Low valueConsider adding null-safe operator for consistency with EditProfileCommand.
EditProfileCommanduses$components->get(...)?->valuewith null-safe operator, while this file accesses.valuedirectly. Although these are required fields (unlikely to be null), the inconsistency could lead to confusion or issues if the modal submission behavior changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php` around lines 125 - 127, The three lines in IntroductionCommand that access $components->get('custom_id', ...)->value should use the null-safe operator like EditProfileCommand to avoid potential null access; update the calls that set $name, $nickname, and $about to use $components->get(...)?->value so they gracefully handle a missing component value while keeping behavior consistent with EditProfileCommand.app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php (1)
127-130: 💤 Low valueConsider passing the already-fetched profile to avoid duplicate query.
The profile is queried in
handle()(lines 65-68) and again here inpersistData(). While functional, this duplicates the database call. You could refactorpersistData()to accept the profile as a parameter.Note: There's also a theoretical race condition where the profile could be deleted between the two queries, causing
firstOrFail()to throw an unexpected exception.♻️ Suggested refactor to avoid duplicate query
- private function persistData( + private function persistData( Interaction $interaction, - Collection $components + Collection $components, + Profile $profile ): void { try { $name = $components->get('custom_id', 'name')?->value; $nickname = $components->get('custom_id', 'nickname')?->value; $about = $components->get('custom_id', 'about')?->value; $this->memberProvider->user->update(['name' => $name]); - $profile = Profile::query() - ->where('user_id', $this->memberProvider->user->id) - ->where('tenant_id', $this->memberProvider->tenant_id) - ->firstOrFail(); - $dto = UpsertProfileDTO::fromArray([Then update the caller at line 106-109 to pass
$profile.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php` around lines 127 - 130, Change persistData() to accept the already-fetched Profile instance (e.g., function persistData(Profile $profile, ...)) and remove the duplicate Profile::query()->where(...)->firstOrFail() call inside persistData; update the code path that currently invokes persistData() to pass the $profile returned in handle() instead of letting persistData re-query, and add a concise guard in persistData (or the caller) to handle the case the profile is null/deleted before use.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php`:
- Around line 127-130: Change persistData() to accept the already-fetched
Profile instance (e.g., function persistData(Profile $profile, ...)) and remove
the duplicate Profile::query()->where(...)->firstOrFail() call inside
persistData; update the code path that currently invokes persistData() to pass
the $profile returned in handle() instead of letting persistData re-query, and
add a concise guard in persistData (or the caller) to handle the case the
profile is null/deleted before use.
In `@app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php`:
- Around line 125-127: The three lines in IntroductionCommand that access
$components->get('custom_id', ...)->value should use the null-safe operator like
EditProfileCommand to avoid potential null access; update the calls that set
$name, $nickname, and $about to use $components->get(...)?->value so they
gracefully handle a missing component value while keeping behavior consistent
with EditProfileCommand.
In `@app-modules/profile/tests/Feature/ToggleAvailabilityTest.php`:
- Around line 53-62: The test "deactivate already inactive availability is
idempotent" currently only asserts available_for_proposals; add an assertion
that start_availability stays null to enforce the idempotent contract. After
calling resolve(ToggleAvailability::class)->handle($profile, false) (the $result
variable), add an assertion like expect($result->start_availability)->toBeNull()
so the test verifies both availability and start_availability remain unchanged.
In `@app-modules/profile/tests/Feature/UpsertProfileTest.php`:
- Around line 83-90: Add a new test in UpsertProfileTest that mirrors the
existing "upsert profile rejects years_experience outside 0-50 range" case but
passes years_experience = -1 to UpsertProfileDTO::fromArray and asserts that
resolve(UpsertProfile::class)->handle($profile, $dto) throws
ValidationException; this ensures the lower bound is validated the same as the
existing upper-bound (51) test.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 7a6840a6-3a01-4e55-9e7e-41738e5430f1
📒 Files selected for processing (18)
app-modules/bot-discord/src/SlashCommands/EditProfileCommand.phpapp-modules/bot-discord/src/SlashCommands/IntroductionCommand.phpapp-modules/identity/database/factories/AddressFactory.phpapp-modules/identity/database/factories/InformationFactory.phpapp-modules/identity/src/Filament/Shared/Schemas/UserInformationForm.phpapp-modules/identity/src/User/Actions/InformationUserAction.phpapp-modules/identity/src/User/Actions/UpdateProfile.phpapp-modules/identity/src/User/DTOs/UpdateProfileDTO.phpapp-modules/identity/src/User/DTOs/UpsertInformationDTO.phpapp-modules/identity/src/User/Exceptions/ProfileException.phpapp-modules/identity/src/User/Models/Address.phpapp-modules/identity/src/User/Models/Information.phpapp-modules/identity/src/User/Models/User.phpapp-modules/profile/src/Actions/ToggleAvailability.phpapp-modules/profile/src/Actions/UpsertProfile.phpapp-modules/profile/src/DTOs/UpsertProfileDTO.phpapp-modules/profile/tests/Feature/ToggleAvailabilityTest.phpapp-modules/profile/tests/Feature/UpsertProfileTest.php
💤 Files with no reviewable changes (11)
- app-modules/identity/database/factories/InformationFactory.php
- app-modules/identity/src/Filament/Shared/Schemas/UserInformationForm.php
- app-modules/identity/src/User/Actions/UpdateProfile.php
- app-modules/identity/src/User/Models/Information.php
- app-modules/identity/src/User/Exceptions/ProfileException.php
- app-modules/identity/src/User/DTOs/UpdateProfileDTO.php
- app-modules/identity/src/User/DTOs/UpsertInformationDTO.php
- app-modules/identity/database/factories/AddressFactory.php
- app-modules/identity/src/User/Actions/InformationUserAction.php
- app-modules/identity/src/User/Models/Address.php
- app-modules/identity/src/User/Models/User.php
- Add $fillable to Address model for mass assignment - New /endereco command with autocomplete for Brazilian states (27 UFs) - Persists country (BRA default), state (UF), and city via User's polymorphic address - Add updateOrCreate test for address relationship
- Custom ProfilePage in User panel with cover/avatar media header - Cover as full-width banner with hover overlay upload, avatar overlapping - Live preview card (sticky sidebar) updates as fields change - Gamification data: level, XP progress bar, badges via computed property - Social links via Repeater with SocialPlatform enum - Availability toggle with conditional start_availability - Manual media uploads via WithFileUploads + Spatie Media Library - Added 'cover' media collection to User model - Translations in en + pt_BR - 8 Livewire tests covering form load, save, validation, social links
…ew, media fixes
- Address section using Filament relationship('address') for auto load/save
- Country select with 18 countries (default BRA), state select with 27 UFs for BRA
- State field adapts: shows Brazilian states when BRA, empty select for other countries
- Location shown in preview card with map pin icon
- Hide preview card on mobile (hidden xl:block)
- Fix cover image using bg-cover instead of img tag
- Fix birthdate format (Carbon to Y-m-d for date input)
- Merge media header with personal section fields
…rors - ProfileCommand: use Profile model instead of information relationship - CreateAccountByExternalIdentity: remove information()->create() call - ImportDiscordMembers: remove github_url import via legacy information model - Profile model: add complete PHPDoc @Property block for all columns - ProfilePage: fix socialLinks PHPDoc types and redundant operations
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app-modules/identity/src/User/Models/User.php (1)
56-62:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBreaking API removal:
information()is still called downstream.
CreateAccountByExternalIdentitystill invokes$user->information()->create()(inapp-modules/identity/src/ExternalIdentity/Actions/CreateAccountByExternalIdentity.php, context snippet). Removing this relationship here introduces a runtime fatal on account creation. Please migrate that caller in the same PR (or keep a temporary compatibility shim until migration is complete).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/identity/src/User/Models/User.php` around lines 56 - 62, You removed the User::information() relationship but CreateAccountByExternalIdentity (in CreateAccountByExternalIdentity.php) still calls $user->information()->create(), causing a runtime error; either restore a compatibility shim by re-adding the information() relation on the User model that proxies to the new providers() morphMany, or update CreateAccountByExternalIdentity to use the new providers() relationship and its expected payload (replace $user->information()->create(...) with $user->providers()->create(...) and adjust attributes accordingly). Ensure both symbols are updated consistently so CreateAccountByExternalIdentity and User::providers() remain in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@app-modules/panel-app/resources/views/components/profile-preview-card.blade.php`:
- Around line 99-100: The template profile-preview-card.blade.php contains
hardcoded Portuguese strings like "Disponível" (and similar labels at the other
locations) that bypass localization; replace each hardcoded label with a
translation helper call (e.g. __('panel-app::profile.some_key')) and add
corresponding keys to the panel-app::profile translation file, ensuring you use
descriptive keys (e.g. availability_label, status_online, etc.), then update the
Blade view instances (the span and the other occurrences referenced) to call
__() with those keys so locale switching works correctly.
- Around line 118-119: The Blade conditional is using truthiness so a valid
numeric 0 for $yearsExperience gets hidden; update the conditionals that
currently read things like "`@if` ($seniority || $yearsExperience)" and the
similar block around lines 139-143 to explicitly check for null (e.g., "`@if`
($seniority !== null || $yearsExperience !== null)") and any inner checks that
rely on truthiness to use !== null for $yearsExperience and $seniority so 0
renders correctly.
In `@app-modules/panel-app/src/Pages/ProfilePage.php`:
- Around line 70-78: The Profile properties are accessed dynamically
($profile->nickname, ->birthdate, ->headline, ->seniority_level,
->years_experience, ->about, ->social_links, ->available_for_proposals,
->start_availability) which PHPStan flags as undefined on
He4rt\Profile\Models\Profile; fix by giving the analyzer a concrete type: either
add PHPDoc `@property` annotations with correct types to the
He4rt\Profile\Models\Profile class, or add a local docblock/typed var before use
(/** `@var` \He4rt\Profile\Models\Profile $profile */), or replace dynamic access
with typed getters/getAttribute calls on Profile; ensure birthdate is typed as
?\DateTimeInterface if used with ->format and keep reference to
socialLinksToRepeater when preserving the social_links access.
- Around line 251-254: The match expression in ProfilePage:: (creating
$startAvailability) uses StartAvailability::from(...) which will throw on
invalid strings and cause 500s; change it to
StartAvailability::tryFrom($rawStartAvailability) so invalid input yields null
instead of an exception, keeping the existing match branches and ensuring
ToggleAvailability::handle still receives either a StartAvailability instance or
null/validation flow; update the branch that handles
is_string($rawStartAvailability) to call tryFrom rather than from.
---
Outside diff comments:
In `@app-modules/identity/src/User/Models/User.php`:
- Around line 56-62: You removed the User::information() relationship but
CreateAccountByExternalIdentity (in CreateAccountByExternalIdentity.php) still
calls $user->information()->create(), causing a runtime error; either restore a
compatibility shim by re-adding the information() relation on the User model
that proxies to the new providers() morphMany, or update
CreateAccountByExternalIdentity to use the new providers() relationship and its
expected payload (replace $user->information()->create(...) with
$user->providers()->create(...) and adjust attributes accordingly). Ensure both
symbols are updated consistently so CreateAccountByExternalIdentity and
User::providers() remain in sync.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Pro
Run ID: f62c623b-c8bb-4172-bf6a-6d7d0398a7ed
📒 Files selected for processing (9)
app-modules/identity/src/User/Models/User.phpapp-modules/panel-app/lang/en/profile.phpapp-modules/panel-app/lang/pt_BR/profile.phpapp-modules/panel-app/resources/views/components/profile-media-header.blade.phpapp-modules/panel-app/resources/views/components/profile-preview-card.blade.phpapp-modules/panel-app/resources/views/pages/profile.blade.phpapp-modules/panel-app/src/Pages/ProfilePage.phpapp-modules/panel-app/tests/Feature/ProfilePageTest.phpapp/Providers/Filament/AppPanelProvider.php
✅ Files skipped from review due to trivial changes (1)
- app-modules/panel-app/resources/views/components/profile-media-header.blade.php
- Validate file uploads (image, mimes, max size) via Livewire #[Validate] - Use UUID filenames instead of client-provided names for media uploads - Guard against null tenant in ProfilePage::getRecord() - Null-check on $profile in ProfileCommand before accessing properties - Fix preview card reading address from nested relationship path - Replace resolve() with app() for container resolution - Complete User model PHPDoc with suspended_until, banned_at, nullable timestamps
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Console/Commands/ImportDiscordMembers.php`:
- Line 76: Remove the dead --force code path and any stale GitHub overwrite
metrics: delete the call to option('force') in the command execution (search for
option('force') in ImportDiscordMembers class, likely inside handle()) and
remove the related log/output that reports GitHub overwrite/write counters (the
messages and variables that mention GitHub writes/overwrite counts). Also update
the command signature/help/description in the ImportDiscordMembers class to stop
advertising a --force flag so help/output no longer references that option or
GitHub overwrite metrics.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 03575527-810d-4b2e-8f49-ec5f15d2283c
📒 Files selected for processing (7)
app-modules/bot-discord/src/SlashCommands/ProfileCommand.phpapp-modules/identity/src/ExternalIdentity/Actions/CreateAccountByExternalIdentity.phpapp-modules/identity/src/User/Models/User.phpapp-modules/panel-app/resources/views/components/profile-preview-card.blade.phpapp-modules/panel-app/src/Pages/ProfilePage.phpapp-modules/profile/src/Models/Profile.phpapp/Console/Commands/ImportDiscordMembers.php
💤 Files with no reviewable changes (1)
- app-modules/identity/src/ExternalIdentity/Actions/CreateAccountByExternalIdentity.php
✅ Files skipped from review due to trivial changes (1)
- app-modules/profile/src/Models/Profile.php
Summary
Profile module phase 1 implementation (PRD #250). Delivers the core domain layer, legacy cleanup, and the full profile edit experience in the User panel.
Issues completed
UpsertProfileandToggleAvailabilitydomain actions with DTO, validation, and testsInformation,Addressmodels, actions, DTOs, and REST API from Identity moduleHighlights
UpsertProfilevalidates about (max 500), headline (max 100), social_links againstSocialPlatformenum, years_experience (0-50).ToggleAvailabilityrequiresstart_availabilitywhen activating.EditProfileCommand,IntroductionCommand,ProfileCommand) to use Profile module. Tables preserved as safety net.relationship('address'), availability toggle with conditional start_availability.getRecord(), null checks on bot command profile access.Addressmodel with country select (18 countries, default BRA), Brazilian state select (27 UFs), city text input. Filament auto-loads/saves viarelationship().What's remaining (future PRs)
/@usernamewith domain routingTest plan
Description
References
#253— Domain actions, DTO, validation, tests#254— Legacy Identity cleanup and bot updates#250— Profile module specification#255(panel edit),#256(admin profile tab),#257(public profile routing)Dependencies & Requirements
Contributor Summary
Changes Summary