Skip to content

prd(profile): module phase 1 — profile core #250

@danielhe4rt

Description

@danielhe4rt

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

  1. Separação de responsabilidades: Identity cuida de "quem você é" (auth). Profile cuida de "como você se apresenta".
  2. Escala futura: Skills, JobPreferences e outras features de perfil vão crescer. Um módulo dedicado evita poluir o Identity.
  3. Multi-tenancy: Perfil é tenant-scoped. Um dev pode ser "Backend PHP" na He4rt e "Estudante de Rust" em outra comunidade.
  4. 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

  1. 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.
  2. Como membro, quero definir um apelido diferente em cada comunidade, para que eu possa ser chamado de forma adequada em cada contexto.
  3. Como membro, quero adicionar meus links sociais manuais (Instagram, Twitter/X, site pessoal), para que visitantes possam me encontrar em outras plataformas.
  4. 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.
  5. Como membro, quero ligar/desligar minha disponibilidade para propostas, para sinalizar ao mercado que estou aberto a oportunidades.
  6. Como membro, quero informar meu prazo de disponibilidade para início (imediato, 1 semana, etc.), para que recrutadores saibam quando posso começar.
  7. Como membro, quero que meu perfil seja criado automaticamente quando entro em uma comunidade, sem precisar fazer nada extra.
  8. Como membro, quero ver minha data de nascimento no perfil, para que a comunidade possa me parabenizar.

Visitante / Recrutador

  1. 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.
  2. 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.
  3. Como visitante, quero ver os links sociais e provedores conectados do membro, para poder encontrá-lo em outras plataformas.

Admin

  1. 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

  1. 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.
  2. 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.
  3. Como sistema, quero que os endpoints REST legados de perfil (GET/PUT /api/users/profile/{value}) sejam removidos.
  4. 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

  1. 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
  2. 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
  3. Toggle de disponibilidade (Feature test)

    • Ativar/desativar available_for_proposals
    • start_availability só é relevante quando ativo
  4. 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
  5. Filament — Admin tab (Livewire test)

    • Tab existe no UserResource
    • Admin consegue editar profile de outro membro
  6. 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
  7. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    mod:profileUser profilestype:prdProduct Requirements Document

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions