PRD: Módulo Profile — Fase 1 (Profile Core)
Contexto
A He4rt Developers é uma comunidade brasileira de programadores que conecta membros entre si e com o mercado. Hoje, os dados de "apresentação" de um membro (apelido, bio, LinkedIn, GitHub) vivem dentro do módulo Identity na tabela user_information, junto com autenticação, OAuth e gerenciamento de tenants.
Estamos construindo um sistema de perfil profissional inspirado no LinkedIn: headline, senioridade, disponibilidade para propostas, links sociais. Isso vai crescer (skills, preferências de contratação), e manter dentro do Identity criaria um módulo com duas responsabilidades distintas.
A plataforma suporta múltiplas comunidades (tenants). Cada tenant é uma comunidade independente com domínio próprio, e um membro pode ter perfis diferentes em cada uma — apresentar-se de forma diferente, ter apelido diferente, bio diferente.
Motivação
- Separação de responsabilidades: Identity cuida de "quem você é" (auth). Profile cuida de "como você se apresenta".
- Escala futura: Skills, JobPreferences e outras features de perfil vão crescer. Um módulo dedicado evita poluir o Identity.
- Multi-tenancy: Perfil é tenant-scoped. Um dev pode ser "Backend PHP" na He4rt e "Estudante de Rust" em outra comunidade.
- Visibilidade pública: O perfil é a vitrine do membro para outros membros e recrutadores — precisa ser acessível sem login.
Problema
Não existe um lugar dedicado para o membro construir sua identidade profissional na plataforma. Os dados atuais são limitados (nome, apelido, bio, dois links), vivem dentro de um módulo de autenticação, e não suportam informações profissionais (cargo, senioridade, anos de experiência, disponibilidade para o mercado).
Recrutadores e outros membros não têm como descobrir as competências e a disponibilidade de alguém de forma rápida. Não existe página pública de perfil.
Solução
Criar o módulo app-modules/profile/ que é dono de toda a camada de apresentação do membro. Na Fase 1:
- Tabela
user_profiles com dados pessoais, profissionais, links sociais manuais (JSONB) e disponibilidade.
- Perfil criado automaticamente quando o membro entra em um tenant.
- Página pública acessível sem login, resolvida pelo domínio do tenant.
- Edição do perfil no painel do membro (User panel).
- Visualização/edição pelo admin como tab no UserResource.
- Address extraído para model polimórfico compartilhado.
- Remoção completa do código legado do
Information e da API REST de perfil.
User Stories
Membro
- Como membro, quero editar meu perfil profissional (headline, senioridade, anos de experiência, bio), para que outros membros e recrutadores saibam o que eu faço.
- Como membro, quero definir um apelido diferente em cada comunidade, para que eu possa ser chamado de forma adequada em cada contexto.
- Como membro, quero adicionar meus links sociais manuais (Instagram, Twitter/X, site pessoal), para que visitantes possam me encontrar em outras plataformas.
- Como membro, quero que meus links de GitHub, Discord e outros provedores OAuth apareçam automaticamente no perfil (derivados da conexão), sem precisar digitar URLs.
- Como membro, quero ligar/desligar minha disponibilidade para propostas, para sinalizar ao mercado que estou aberto a oportunidades.
- Como membro, quero informar meu prazo de disponibilidade para início (imediato, 1 semana, etc.), para que recrutadores saibam quando posso começar.
- Como membro, quero que meu perfil seja criado automaticamente quando entro em uma comunidade, sem precisar fazer nada extra.
- Como membro, quero ver minha data de nascimento no perfil, para que a comunidade possa me parabenizar.
Visitante / Recrutador
- Como visitante, quero acessar o perfil público de um membro pela URL do domínio do tenant (ex:
he4rtdevs.com/@danielhe4rt), sem precisar criar conta.
- Como visitante, quero ver os dados profissionais, bio, badges, level e disponibilidade do membro na página pública, para avaliar se quero entrar em contato.
- Como visitante, quero ver os links sociais e provedores conectados do membro, para poder encontrá-lo em outras plataformas.
Admin
- Como admin, quero ver e editar o perfil de qualquer membro como uma tab dentro do UserResource, para moderar conteúdo ofensivo ou corrigir dados.
Sistema
- Como sistema, quero que ao adicionar um User a um Tenant (via pivot
tenant_users), um Profile seja criado automaticamente para aquele par user+tenant.
- Como sistema, quero que a tabela
user_information pare de ser referenciada no código, mas continue existindo no banco de dados como safety net.
- Como sistema, quero que os endpoints REST legados de perfil (
GET/PUT /api/users/profile/{value}) sejam removidos.
- Como sistema, quero que o model
Address seja polimórfico e compartilhado, para que qualquer entidade (User, Event, etc.) possa ter um endereço.
Cenários BDD — Fase 1
Criar Profile automaticamente
Funcionalidade: Criação automática de Profile
Cenário: Membro é adicionado a um tenant
Dado que existe um User "danielhe4rt"
E existe um Tenant "He4rt Developers"
Quando o User é adicionado como membro do Tenant
Então um Profile deve existir para esse User nesse Tenant
E todos os campos opcionais do Profile devem ser nulos
E available_for_proposals deve ser false
Cenário: Membro já tem profile no tenant
Dado que "danielhe4rt" já é membro do Tenant "He4rt Developers"
E já possui um Profile nesse tenant
Quando o sistema tenta criar um Profile duplicado
Então nenhum Profile adicional é criado
E o Profile existente permanece inalterado
Cenário: Membro participa de múltiplos tenants
Dado que "danielhe4rt" é membro de "He4rt Developers" e "Outro Tenant"
Então devem existir dois Profiles distintos para esse User
E cada Profile pode ter dados diferentes
Editar Profile (User Panel)
Funcionalidade: Edição de perfil no User Panel
Cenário: Membro preenche todos os campos do perfil
Dado que estou logado como membro no User panel
E estou no tenant "He4rt Developers"
Quando acesso a página de edição do meu perfil
E preencho headline com "Backend Developer"
E seleciono senioridade "Pleno"
E preencho anos de experiência com 5
E preencho apelido com "Dan"
E preencho bio com "Desenvolvedor PHP apaixonado por Laravel"
E preencho data de nascimento com "1995-03-15"
E salvo o formulário
Então o Profile deve ser atualizado com todos os campos
E uma notificação de sucesso deve aparecer
Cenário: Membro atualiza links sociais manuais
Dado que estou logado como membro no User panel
Quando preencho o campo Instagram com "@danielhe4rt"
E preencho o campo Website com "https://danielheart.dev"
E salvo o formulário
Então social_links deve conter {"instagram": "@danielhe4rt", "website": "https://danielheart.dev"}
Cenário: Membro envia plataforma social inválida
Dado que estou logado como membro no User panel
Quando tento salvar social_links com uma plataforma não listada no enum
Então a validação deve rejeitar o campo
E o formulário deve exibir erro de validação
Cenário: Bio com limite de caracteres
Dado que estou logado como membro no User panel
Quando preencho a bio com um texto de 501 caracteres
E salvo o formulário
Então a validação deve rejeitar o campo
E o formulário deve exibir erro "máximo 500 caracteres"
Cenário: Membro edita perfil — campos de conta não aparecem
Dado que estou logado como membro no User panel
Quando acesso a página de edição do perfil
Então não devo ver campos de name, email, username ou password
E esses campos vivem na página de Settings
Disponibilidade
Funcionalidade: Toggle de disponibilidade para propostas
Cenário: Membro ativa disponibilidade
Dado que estou logado e meu available_for_proposals é false
Quando ativo o toggle "Disponível para propostas"
E seleciono start_availability "immediate"
E salvo
Então available_for_proposals deve ser true
E start_availability deve ser "immediate"
Cenário: Membro desativa disponibilidade
Dado que estou logado e meu available_for_proposals é true
Quando desativo o toggle "Disponível para propostas"
E salvo
Então available_for_proposals deve ser false
E start_availability deve permanecer com o valor anterior (não é apagado)
Cenário: start_availability só é editável quando disponível
Dado que estou logado e available_for_proposals é false
Quando vejo o formulário
Então o campo start_availability não deve estar visível
Perfil Público
Funcionalidade: Página pública de perfil
Cenário: Visitante acessa perfil existente
Dado que "danielhe4rt" tem um Profile no tenant com domínio "he4rtdevs.com"
E o Profile tem headline "Backend Developer" e available_for_proposals true
Quando um visitante anônimo acessa "he4rtdevs.com/@danielhe4rt"
Então a página exibe: nome, headline, senioridade, bio, badges, level
E a página exibe o badge de "Disponível"
E a página exibe links sociais manuais e provedores OAuth conectados
Cenário: Visitante acessa perfil inexistente
Dado que "fantasma" não existe no tenant com domínio "he4rtdevs.com"
Quando um visitante acessa "he4rtdevs.com/@fantasma"
Então a página retorna 404
Cenário: Perfil com campos opcionais vazios
Dado que "novato" tem um Profile mas nunca preencheu nada
Quando um visitante acessa o perfil de "novato"
Então a página exibe o nome do User (da tabela users)
E campos vazios não aparecem (sem "null" ou placeholders quebrados)
Cenário: Dados de gamificação aparecem no perfil
Dado que "danielhe4rt" tem um Character com level 15 e 3 badges no tenant
Quando um visitante acessa o perfil
Então o level e os badges são exibidos na página
E esses dados vêm do módulo Gamification (não são duplicados no Profile)
Admin — Tab no UserResource
Funcionalidade: Admin edita perfil de membro
Cenário: Admin visualiza profile como tab
Dado que estou logado como admin no painel Admin
Quando acesso o UserResource de "danielhe4rt"
Então vejo uma tab "Profile" com os dados do perfil daquele User no tenant atual
Cenário: Admin edita bio de um membro
Dado que estou logado como admin
Quando edito a bio do perfil de "danielhe4rt" para "Bio moderada"
E salvo
Então o Profile é atualizado no banco
E uma notificação de sucesso aparece
Address polimórfico
Funcionalidade: Address como model polimórfico compartilhado
Cenário: User tem um endereço
Dado que existe um User "danielhe4rt"
Quando atribuo um Address com country "BR", state "SP", city "São Paulo"
Então o Address é salvo com addressable_type "user" e addressable_id do User
Cenário: Múltiplas entidades com address
Dado que existe um User e um Event
Quando ambos têm um Address associado
Então cada Address tem seu próprio registro com o morphable correto
Remoção de código legado
Funcionalidade: Limpeza do código legado
Cenário: Information não é mais referenciado
Dado que o módulo Profile foi implementado
Então nenhum arquivo PHP deve importar ou referenciar o model Information
E a tabela user_information continua existindo no banco (safety net)
Cenário: API REST de perfil não existe mais
Quando faço GET /api/users/profile/danielhe4rt
Então recebo 404
E as rotas users.profile e users.profile.update não existem
Decisões de Implementação
1. Novo módulo app-modules/profile/
Segue o padrão do monorepo modular (internachi/modular). Namespace He4rt\Profile. Estrutura:
ProfileServiceProvider — registra migrations, observers, morph map
Models/Profile — model Eloquent para user_profiles
Actions/UpsertProfile — cria ou atualiza profile
Actions/ToggleAvailability — liga/desliga disponibilidade
Enums/SeniorityLevel — junior, pleno, senior, specialist, lead
Enums/StartAvailability — immediate, 1_week, 2_weeks, etc.
Enums/SocialPlatform — enum de plataformas permitidas no JSONB social_links
2. Schema user_profiles
- UUID primary key
user_id FK para users
tenant_id FK para tenants
- Unique composite em
(user_id, tenant_id)
- Campos:
nickname, birthdate, about, headline, seniority_level, years_experience, social_links (jsonb), available_for_proposals (bool, default false), start_availability
- Partial index no Postgres:
WHERE available_for_proposals = true
3. Criação eager via Observer/Event
Quando um registro é inserido na pivot tenant_users, o sistema cria automaticamente um Profile com campos nulos e available_for_proposals = false. A implementação pode ser via Model Observer no Tenant (no members()->attach()) ou listener de evento.
4. Address polimórfico
Nova tabela addresses com campos addressable_type, addressable_id, country, state, city, zip_code. Model compartilhado em app/Models/Address.php (fora de módulo). Qualquer model pode usar morphOne(Address::class, 'addressable').
A migração antiga user_address fica no banco (safety net), código do model antigo He4rt\Identity\User\Models\Address é removido.
5. Social links como JSONB
Campo social_links armazena um objeto JSON com chave = plataforma (do enum SocialPlatform) e valor = handle/URL. Validação no PHP garante que só plataformas do enum são aceitas. Exemplo: {"instagram": "@danielhe4rt", "twitter": "@danielhe4rt", "website": "https://danielheart.dev"}.
Links de provedores OAuth (GitHub, Discord, LinkedIn) não entram aqui — são derivados automaticamente do ExternalIdentity quando o perfil público é montado.
6. Filament — User Panel
Página dedicada de Profile no User panel (/app). Usa Filament form components. O form edita apenas dados de user_profiles — dados de conta (name, email, password) ficam em página separada de Settings.
7. Filament — Admin Panel
Tab "Profile" no UserResource existente, usando RelationManager ou tab com form inline. Admin pode ver e editar o profile do membro no tenant atual.
8. Perfil público via domain routing
Rota /@{username} registrada no módulo Profile com middleware de resolução de tenant por domínio. Sem autenticação. A view compõe dados de Profile + User (nome, avatar) + Character (level, badges) + ExternalIdentity (provedores conectados) + Address.
9. Remoção de legado
Remover completamente do código:
- Model
Information e factory InformationFactory
InformationUserAction, UpsertInformationDTO
UserInformationForm (Filament schema)
UpdateProfile action, UpdateProfileDTO, UpdateProfileRequest
UsersController::getProfile(), UsersController::putProfile()
- Rotas
users.profile e users.profile.update
ProfileException
- Relação
information() no User model
- Testes
FindProfileTest, UpdateProfileTest
- Model
Address antigo (He4rt\Identity\User\Models\Address) e factory
- Relação
address() apontando para o model antigo
Tabelas user_information e user_address permanecem no banco.
Decisões de Teste
Filosofia
Testar comportamento externo, não implementação. Um bom teste responde "o que o sistema faz" e não "como o sistema faz". Se a implementação interna mudar mas o comportamento continuar o mesmo, o teste não deve quebrar.
O que testar
-
Criação automática de Profile (Feature test)
- Attach de User em Tenant cria Profile
- Não duplica se já existe
- Profile de tenants diferentes são independentes
-
UpsertProfile action (Feature test)
- Atualiza todos os campos
- Validação de social_links contra o enum
- Validação de bio (max 500)
- Validação de seniority_level contra enum
-
Toggle de disponibilidade (Feature test)
- Ativar/desativar available_for_proposals
- start_availability só é relevante quando ativo
-
Filament — Profile page no User Panel (Livewire test)
- Form carrega dados do profile
- Form salva alterações
- Validação de campos obrigatórios e limites
- Campos de conta (name, email) não aparecem
-
Filament — Admin tab (Livewire test)
- Tab existe no UserResource
- Admin consegue editar profile de outro membro
-
Perfil público (Feature test)
- Rota
/@{username} retorna 200 com dados corretos
- Username inexistente retorna 404
- Dados de gamificação aparecem
- Não requer autenticação
-
Address polimórfico (Unit test)
- User morphOne Address funciona
- Múltiplas entidades com address
Padrão de teste existente
Usar Pest 4 com livewire() helper pra testes Filament. Setup com beforeEach criando User, Tenant, e setando Filament::setCurrentPanel() e Filament::setTenant(). Factories para todos os models novos.
Onde vivem os testes
app-modules/profile/tests/Feature/ e app-modules/profile/tests/Unit/, seguindo a convenção dos outros módulos.
Fora de Escopo
- Skills e SkillCategories — catálogo de habilidades, sync many-to-many, featured skills. Fase futura.
- JobPreferences — modelo de trabalho, tipo de contrato, faixa salarial. Fase futura.
- Busca/filtro de perfis — listagem de membros com filtros (por senioridade, disponibilidade, etc.). Fase futura.
- Notificações — avisar quando alguém visita seu perfil. Fora de escopo.
- Profile completeness score — porcentagem de preenchimento. Pode ser um computed attribute simples, mas não é feature priorizada.
- Migração de dados — copiar dados existentes de
user_information pra user_profiles. Pode ser feita via migration ou tinker, mas o escopo aqui é código novo.
Notas
- O ADR-0001 (
docs/adr/0001-extract-profile-module-from-identity.md) documenta a decisão arquitetural e as alternativas rejeitadas.
- O CONTEXT.md do módulo (
app-modules/profile/CONTEXT.md) define o glossário e os invariantes.
- A tabela
user_information e user_address continuam no banco como safety net. Podem ser removidas em uma migration futura quando houver confiança de que nenhum dado foi perdido.
PRD: Módulo Profile — Fase 1 (Profile Core)
Contexto
A He4rt Developers é uma comunidade brasileira de programadores que conecta membros entre si e com o mercado. Hoje, os dados de "apresentação" de um membro (apelido, bio, LinkedIn, GitHub) vivem dentro do módulo Identity na tabela
user_information, junto com autenticação, OAuth e gerenciamento de tenants.Estamos construindo um sistema de perfil profissional inspirado no LinkedIn: headline, senioridade, disponibilidade para propostas, links sociais. Isso vai crescer (skills, preferências de contratação), e manter dentro do Identity criaria um módulo com duas responsabilidades distintas.
A plataforma suporta múltiplas comunidades (tenants). Cada tenant é uma comunidade independente com domínio próprio, e um membro pode ter perfis diferentes em cada uma — apresentar-se de forma diferente, ter apelido diferente, bio diferente.
Motivação
Problema
Não existe um lugar dedicado para o membro construir sua identidade profissional na plataforma. Os dados atuais são limitados (nome, apelido, bio, dois links), vivem dentro de um módulo de autenticação, e não suportam informações profissionais (cargo, senioridade, anos de experiência, disponibilidade para o mercado).
Recrutadores e outros membros não têm como descobrir as competências e a disponibilidade de alguém de forma rápida. Não existe página pública de perfil.
Solução
Criar o módulo
app-modules/profile/que é dono de toda a camada de apresentação do membro. Na Fase 1:user_profilescom dados pessoais, profissionais, links sociais manuais (JSONB) e disponibilidade.Informatione da API REST de perfil.User Stories
Membro
Visitante / Recrutador
he4rtdevs.com/@danielhe4rt), sem precisar criar conta.Admin
Sistema
tenant_users), um Profile seja criado automaticamente para aquele par user+tenant.user_informationpare de ser referenciada no código, mas continue existindo no banco de dados como safety net.GET/PUT /api/users/profile/{value}) sejam removidos.Addressseja polimórfico e compartilhado, para que qualquer entidade (User, Event, etc.) possa ter um endereço.Cenários BDD — Fase 1
Criar Profile automaticamente
Editar Profile (User Panel)
Disponibilidade
Perfil Público
Admin — Tab no UserResource
Address polimórfico
Remoção de código legado
Decisões de Implementação
1. Novo módulo
app-modules/profile/Segue o padrão do monorepo modular (
internachi/modular). NamespaceHe4rt\Profile. Estrutura:ProfileServiceProvider— registra migrations, observers, morph mapModels/Profile— model Eloquent parauser_profilesActions/UpsertProfile— cria ou atualiza profileActions/ToggleAvailability— liga/desliga disponibilidadeEnums/SeniorityLevel— junior, pleno, senior, specialist, leadEnums/StartAvailability— immediate, 1_week, 2_weeks, etc.Enums/SocialPlatform— enum de plataformas permitidas no JSONB social_links2. Schema
user_profilesuser_idFK parauserstenant_idFK paratenants(user_id, tenant_id)nickname,birthdate,about,headline,seniority_level,years_experience,social_links(jsonb),available_for_proposals(bool, default false),start_availabilityWHERE available_for_proposals = true3. Criação eager via Observer/Event
Quando um registro é inserido na pivot
tenant_users, o sistema cria automaticamente um Profile com campos nulos eavailable_for_proposals = false. A implementação pode ser via Model Observer no Tenant (nomembers()->attach()) ou listener de evento.4. Address polimórfico
Nova tabela
addressescom camposaddressable_type,addressable_id,country,state,city,zip_code. Model compartilhado emapp/Models/Address.php(fora de módulo). Qualquer model pode usarmorphOne(Address::class, 'addressable').A migração antiga
user_addressfica no banco (safety net), código do model antigoHe4rt\Identity\User\Models\Addressé removido.5. Social links como JSONB
Campo
social_linksarmazena um objeto JSON com chave = plataforma (do enumSocialPlatform) e valor = handle/URL. Validação no PHP garante que só plataformas do enum são aceitas. Exemplo:{"instagram": "@danielhe4rt", "twitter": "@danielhe4rt", "website": "https://danielheart.dev"}.Links de provedores OAuth (GitHub, Discord, LinkedIn) não entram aqui — são derivados automaticamente do
ExternalIdentityquando o perfil público é montado.6. Filament — User Panel
Página dedicada de Profile no User panel (
/app). Usa Filament form components. O form edita apenas dados deuser_profiles— dados de conta (name, email, password) ficam em página separada de Settings.7. Filament — Admin Panel
Tab "Profile" no
UserResourceexistente, usando RelationManager ou tab com form inline. Admin pode ver e editar o profile do membro no tenant atual.8. Perfil público via domain routing
Rota
/@{username}registrada no módulo Profile com middleware de resolução de tenant por domínio. Sem autenticação. A view compõe dados de Profile + User (nome, avatar) + Character (level, badges) + ExternalIdentity (provedores conectados) + Address.9. Remoção de legado
Remover completamente do código:
Informatione factoryInformationFactoryInformationUserAction,UpsertInformationDTOUserInformationForm(Filament schema)UpdateProfileaction,UpdateProfileDTO,UpdateProfileRequestUsersController::getProfile(),UsersController::putProfile()users.profileeusers.profile.updateProfileExceptioninformation()no User modelFindProfileTest,UpdateProfileTestAddressantigo (He4rt\Identity\User\Models\Address) e factoryaddress()apontando para o model antigoTabelas
user_informationeuser_addresspermanecem no banco.Decisões de Teste
Filosofia
Testar comportamento externo, não implementação. Um bom teste responde "o que o sistema faz" e não "como o sistema faz". Se a implementação interna mudar mas o comportamento continuar o mesmo, o teste não deve quebrar.
O que testar
Criação automática de Profile (Feature test)
UpsertProfile action (Feature test)
Toggle de disponibilidade (Feature test)
Filament — Profile page no User Panel (Livewire test)
Filament — Admin tab (Livewire test)
Perfil público (Feature test)
/@{username}retorna 200 com dados corretosAddress polimórfico (Unit test)
Padrão de teste existente
Usar Pest 4 com
livewire()helper pra testes Filament. Setup combeforeEachcriando User, Tenant, e setandoFilament::setCurrentPanel()eFilament::setTenant(). Factories para todos os models novos.Onde vivem os testes
app-modules/profile/tests/Feature/eapp-modules/profile/tests/Unit/, seguindo a convenção dos outros módulos.Fora de Escopo
user_informationprauser_profiles. Pode ser feita via migration ou tinker, mas o escopo aqui é código novo.Notas
docs/adr/0001-extract-profile-module-from-identity.md) documenta a decisão arquitetural e as alternativas rejeitadas.app-modules/profile/CONTEXT.md) define o glossário e os invariantes.user_informationeuser_addresscontinuam no banco como safety net. Podem ser removidas em uma migration futura quando houver confiança de que nenhum dado foi perdido.