feat(squads): AssignCaptain + invariante de capitão único (#356) - #469
feat(squads): AssignCaptain + invariante de capitão único (#356)#469sirelves wants to merge 1 commit into
Conversation
Registra o desfecho de uma eleição de capitão que aconteceu off-system (ADR-0001: o módulo é livro-razão, não motor de processo). - Partial unique `UNIQUE (squad_id) WHERE role = 'captain'` garante a invariante no Postgres, não só na Action (ADR-0002, defense-in-depth). - `AssignCaptain` (super-admin) define/troca o capitão numa transação: rebaixa o anterior a `Member` e grava `demote` + `captain_assigned` na trilha append-only. Reatribuir o capitão atual é no-op. - `lockForUpdate` na linha do sujeito serializa atribuições concorrentes da mesma pessoa, para a trilha não receber eventos fantasma. - `NotAnActiveSquadMember` em vez de `ModelNotFoundException`, que o handler do Laravel converteria num 404 para quem chamar a Action. - `Squad::captain()` deriva do pivot pelo mesmo predicado do índice — uma definição só de "capitão", sem `captain_id` denormalizado. Closes #356
|
Não tenho acompanhado as reuniões e debate do squad, mas há constrains ou safety check para os seguintes casos:
Caso as ideias aqui acima seja loucura ou fora do escopo, segue o meu LGTM. |
|
Aproveitando um ponto que o @danielmendss levantou, pode haver trocas de sub e csptain? Ou vai precisar do capitão virar member e depois subcapitao? Não sei se já foi discutindo isso, me corrija se já foi e deaconsidere |
|
Opa, pra responder vocês acho que eu sou a melhor pessoa kkkk
Sim, a tendência é que quando um capitão seja removido automaticamente o sub fique no cargo.
A tendência é ser eleito um, ou caso seja necessário, iremos nomear. Vocês encontram desse PRD que fiz: |
Closes #356
O que entra
Registro do desfecho de uma eleição de capitão que aconteceu off-system — o módulo é livro-razão, não motor de processo (ADR-0001).
2026_07_30_113028_add_single_captain_unique_to_squad_members.phpAssignCaptaindefine o capitão e gravacaptain_assignedsrc/Actions/AssignCaptain.phpAssignCaptain::demote()Squad::captain()deriva do pivotsrc/Models/Squad.phpAssignCaptainTest.php+SquadMemberTest.phpDecisões que valem review
UNIQUE (squad_id) WHERE role = 'captain'é o defense-in-depth que o ADR-0002 pediu.Squad::captain()usa o mesmo predicado do índice (sórole, semleft_at): uma definição só de "capitão", app e banco não podem divergir. Um capitão que sai viraExMembere libera a vaga por consequência, não por regra extra.lockForUpdate()na linha do sujeito. O índice garante o estado, mas não a trilha: dois super-admins atribuindo a mesma pessoa ao mesmo tempo leriam o mesmo incumbente e gravariamcaptain_assigned+demoteem duplicata (ambos os UPDATEs caem na mesma linha, o índice não acusa). Com o lock, o perdedor relê o incumbente já atualizado e cai no no-op. Cenário improvável, custo zero, e a trilha é o produto que o ADR-0001 vende.NotAnActiveSquadMemberem vez defirstOrFail().ModelNotFoundExceptioné convertida pelo handler do Laravel em 404 — "essa pessoa não é membro ativo" renderizaria como página não encontrada quando isso for chamado do Filament ou do bot. Segue o precedente deInvalidSquadStatusTransition.Ponto em aberto para a #358
CaptainAssignedé um promote. Depois que a #358 (PromoteToSubCaptain / demote) entrar, a taxonomia fica assimétrica:promotegenérico para member→sub_captain,captain_assignedespecífico para →captain, edemotegenérico na volta. A #356 pediucaptain_assignedexplicitamente e é o que entrego aqui, mas vale fechar a decisão antes da #358 — senão um relatório de histórico de capitania precisa consultar doisactiondiferentes. @danielhe4rtVerificação
pest— 939 passed, 3012 assertions (suíte completa); 28 no módulosquadspint --test·rector --dry-run·phpstan— todos limpos