Parent
Parte da PRD #250 — Módulo Profile Fase 1.
Contexto
Com o módulo Profile scaffolded (#251) e o model/enums prontos, esta issue entrega a lógica de domínio — as actions que criam, atualizam e manipulam profiles.
Hoje o fluxo de edição de perfil vive no módulo Identity (UpdateProfile action + InformationUserAction + UpsertInformationDTO). Esse código será removido na issue de legado. Aqui construímos o substituto.
São duas actions distintas porque têm responsabilidades diferentes:
- UpsertProfile: dados pessoais e profissionais (headline, bio, senioridade, social links, etc.)
- ToggleAvailability: liga/desliga disponibilidade + prazo de início. Separada porque é uma operação atômica que pode ser chamada isoladamente (ex: bot Discord, toggle rápido na UI).
Decisões relevantes
social_links é JSONB validado contra o enum SocialPlatform. Chaves fora do enum são rejeitadas.
bio tem limite de 500 caracteres.
start_availability só é validado/relevante quando available_for_proposals = true.
- Links de OAuth (GitHub, Discord) não passam por aqui — vêm do
ExternalIdentity.
- Campos de conta (name, email, username) não passam por aqui — vivem em Settings/Identity.
O que construir
1. UpsertProfileDTO
DTO com os campos editáveis do profile:
nickname (string, nullable, max 100)
birthdate (Carbon/date, nullable)
about (string, nullable, max 500)
headline (string, nullable, max 100)
seniorityLevel (SeniorityLevel enum, nullable)
yearsExperience (int, nullable, 0-50)
socialLinks (array, nullable — validado contra SocialPlatform enum)
Factory method fromArray(array $data): self pra facilitar uso em Filament e testes.
2. UpsertProfile action
handle(Profile $profile, UpsertProfileDTO $dto): Profile
- Recebe um Profile existente (já criado eager no tenant join)
- Atualiza os campos com os valores do DTO
- Valida
socialLinks: só chaves presentes no enum SocialPlatform são permitidas. Lança exception se inválido.
- Retorna o Profile atualizado
Não faz updateOrCreate — o Profile já existe (decisão da issue #251). É um update puro.
3. ToggleAvailability action
handle(Profile $profile, bool $available, ?StartAvailability $startAvailability = null): Profile
- Se
$available = true, start_availability é obrigatório
- Se
$available = false, start_availability mantém o valor anterior (não apaga)
- Atualiza
available_for_proposals e start_availability
- Retorna o Profile atualizado
4. Validação de social_links
A validação de social_links acontece dentro da UpsertProfile action (ou em um value object/rule dedicado):
- Cada chave do array deve ser um caso válido do enum
SocialPlatform
- Cada valor deve ser string não-vazia
- Chaves desconhecidas são rejeitadas com exception
Cenários BDD
Funcionalidade: Atualizar perfil profissional
Cenário: Atualizar todos os campos do perfil
Dado que existe um Profile para "danielhe4rt" no tenant "He4rt"
Quando chamo UpsertProfile com headline "Backend Developer", seniority "pleno", years_experience 5, about "Dev PHP", nickname "Dan"
Então o Profile é atualizado com todos os campos
E o Profile retornado reflete as mudanças
Cenário: Atualizar parcialmente (só headline)
Dado que existe um Profile com nickname "Dan" e bio "Dev PHP"
Quando chamo UpsertProfile com apenas headline "Senior Dev"
Então headline muda para "Senior Dev"
E nickname e bio permanecem inalterados
Cenário: Bio acima de 500 caracteres é rejeitada
Dado que existe um Profile
Quando chamo UpsertProfile com about de 501 caracteres
Então uma exception de validação é lançada
E o Profile não é alterado
Cenário: Headline acima de 100 caracteres é rejeitada
Dado que existe um Profile
Quando chamo UpsertProfile com headline de 101 caracteres
Então uma exception de validação é lançada
Cenário: social_links com plataforma válida
Dado que existe um Profile
Quando chamo UpsertProfile com socialLinks {"instagram": "@dan", "website": "https://dan.dev"}
Então social_links é salvo como {"instagram": "@dan", "website": "https://dan.dev"}
Cenário: social_links com plataforma inválida é rejeitada
Dado que existe um Profile
Quando chamo UpsertProfile com socialLinks {"tiktok": "@dan"}
E "tiktok" não está no enum SocialPlatform
Então uma exception de validação é lançada
E o Profile não é alterado
Cenário: years_experience fora do range é rejeitado
Dado que existe um Profile
Quando chamo UpsertProfile com yearsExperience 51
Então uma exception de validação é lançada
Funcionalidade: Toggle de disponibilidade
Cenário: Ativar disponibilidade com prazo
Dado que "danielhe4rt" tem available_for_proposals false
Quando chamo ToggleAvailability com available true e startAvailability "immediate"
Então available_for_proposals é true
E start_availability é "immediate"
Cenário: Ativar sem prazo é rejeitado
Dado que "danielhe4rt" tem available_for_proposals false
Quando chamo ToggleAvailability com available true e sem startAvailability
Então uma exception de validação é lançada
E available_for_proposals permanece false
Cenário: Desativar disponibilidade mantém prazo anterior
Dado que "danielhe4rt" tem available_for_proposals true e start_availability "1_week"
Quando chamo ToggleAvailability com available false
Então available_for_proposals é false
E start_availability permanece "1_week"
Cenário: Alterar prazo sem mudar disponibilidade
Dado que "danielhe4rt" tem available_for_proposals true e start_availability "immediate"
Quando chamo ToggleAvailability com available true e startAvailability "2_weeks"
Então start_availability muda para "2_weeks"
E available_for_proposals continua true
Acceptance Criteria
Blocked by
Parent
Parte da PRD #250 — Módulo Profile Fase 1.
Contexto
Com o módulo Profile scaffolded (#251) e o model/enums prontos, esta issue entrega a lógica de domínio — as actions que criam, atualizam e manipulam profiles.
Hoje o fluxo de edição de perfil vive no módulo Identity (
UpdateProfileaction +InformationUserAction+UpsertInformationDTO). Esse código será removido na issue de legado. Aqui construímos o substituto.São duas actions distintas porque têm responsabilidades diferentes:
Decisões relevantes
social_linksé JSONB validado contra o enumSocialPlatform. Chaves fora do enum são rejeitadas.biotem limite de 500 caracteres.start_availabilitysó é validado/relevante quandoavailable_for_proposals = true.ExternalIdentity.O que construir
1.
UpsertProfileDTODTO com os campos editáveis do profile:
nickname(string, nullable, max 100)birthdate(Carbon/date, nullable)about(string, nullable, max 500)headline(string, nullable, max 100)seniorityLevel(SeniorityLevel enum, nullable)yearsExperience(int, nullable, 0-50)socialLinks(array, nullable — validado contra SocialPlatform enum)Factory method
fromArray(array $data): selfpra facilitar uso em Filament e testes.2.
UpsertProfileactionsocialLinks: só chaves presentes no enumSocialPlatformsão permitidas. Lança exception se inválido.Não faz
updateOrCreate— o Profile já existe (decisão da issue #251). É um update puro.3.
ToggleAvailabilityaction$available = true,start_availabilityé obrigatório$available = false,start_availabilitymantém o valor anterior (não apaga)available_for_proposalsestart_availability4. Validação de
social_linksA validação de social_links acontece dentro da
UpsertProfileaction (ou em um value object/rule dedicado):SocialPlatformCenários BDD
Acceptance Criteria
UpsertProfileDTOcom factory methodfromArray()UpsertProfileaction atualiza todos os campos do profileabout(max 500),headline(max 100),years_experience(0-50)social_linkscontra enumSocialPlatformToggleAvailabilityaction com toggle + prazo obrigatório ao ativarstart_availabilityanteriorvendor/bin/pint --dirty --format agentpassa sem errosBlocked by