Skip to content

feat(squads): AssignCaptain + invariante de capitão único (#356) - #469

Open
sirelves wants to merge 1 commit into
feat/squadsfrom
story/356-assign-captain
Open

feat(squads): AssignCaptain + invariante de capitão único (#356)#469
sirelves wants to merge 1 commit into
feat/squadsfrom
story/356-assign-captain

Conversation

@sirelves

@sirelves sirelves commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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

Critério de aceite (#356) Onde
Partial unique impede dois capitães ativos 2026_07_30_113028_add_single_captain_unique_to_squad_members.php
AssignCaptain define o capitão e grava captain_assigned src/Actions/AssignCaptain.php
Trocar o capitão rebaixa o anterior (com evento) AssignCaptain::demote()
Squad::captain() deriva do pivot src/Models/Squad.php
Teste (inclui violação da unicidade) AssignCaptainTest.php + SquadMemberTest.php

Decisões que valem review

  • A invariante mora no Postgres. 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, sem left_at): uma definição só de "capitão", app e banco não podem divergir. Um capitão que sai vira ExMember e 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 gravariam captain_assigned + demote em 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.
  • NotAnActiveSquadMember em vez de firstOrFail(). 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 de InvalidSquadStatusTransition.
  • Reatribuir o capitão atual é no-op. Sem a guarda, o próprio sujeito seria rebaixado e repromovido, sujando a trilha com dois eventos falsos.

Ponto em aberto para a #358

CaptainAssigned é um promote. Depois que a #358 (PromoteToSubCaptain / demote) entrar, a taxonomia fica assimétrica: promote genérico para member→sub_captain, captain_assigned específico para →captain, e demote genérico na volta. A #356 pediu captain_assigned explicitamente 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 dois action diferentes. @danielhe4rt

Verificação

  • pest — 939 passed, 3012 assertions (suíte completa); 28 no módulo squads
  • pint --test · rector --dry-run · phpstan — todos limpos

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
@sirelves
sirelves requested a review from a team August 3, 2026 14:20
@danielmendss

Copy link
Copy Markdown

Não tenho acompanhado as reuniões e debate do squad, mas há constrains ou safety check para os seguintes casos:

  • um capitão seja excluído ou removido do squad, o sub-captain assume?
  • ou caso não haja sub, qual o comportamento ?

Caso as ideias aqui acima seja loucura ou fora do escopo, segue o meu LGTM.

@YuriSouzaDev

Copy link
Copy Markdown
Contributor

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

@stherzada

Copy link
Copy Markdown
Collaborator

Opa, pra responder vocês acho que eu sou a melhor pessoa kkkk

um capitão seja excluído ou removido do squad, o sub-captain assume?

Sim, a tendência é que quando um capitão seja removido automaticamente o sub fique no cargo.

ou caso não haja sub, qual o comportamento ?

A tendência é ser eleito um, ou caso seja necessário, iremos nomear.

Vocês encontram desse PRD que fiz:

https://app.notion.com/p/Governan-a-de-Squads-Nomea-o-e-Gest-o-de-Capit-es-3798545b81d08072891de3efbc0cb37e?source=copy_link

@YuriSouzaDev @danielmendss

@stherzada stherzada added this to the Squads milestone Aug 3, 2026
@stherzada stherzada linked an issue Aug 3, 2026 that may be closed by this pull request
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(squads): AssignCaptain + invariante de capitão único

4 participants