Skip to content

feat(squads): squad_applications + ApplyToSquad gated no APTO (#360) - #533

Open
sirelves wants to merge 2 commits into
feat/squadsfrom
story/360-squad-applications
Open

sirelves wants to merge 2 commits into
feat/squadsfrom
story/360-squad-applications

Conversation

@sirelves

Copy link
Copy Markdown
Contributor

Materializa a candidatura a um squad, o único fluxo que o módulo conduz, gated no APTO do onboarding.

  • Migration squad_applications: status (default pending), message, decided_by, decided_at, INDEX (squad_id, status) e o partial UNIQUE (squad_id, user_id) WHERE status = 'pending'
  • Model SquadApplication (PHPDoc @property, #[Table], #[UseFactory], casts) + SquadApplicationFactory + enum ApplicationStatus (pending/approved/rejected)
  • Action ApplyToSquad: lê OnboardingCompletionGate::isCompleted($user, OnboardingType::Squads) e barra quem não é APTO com NotAptForSquads, cuja mensagem já nomeia o onboarding exigido para a UI direcionar
  • Candidatura pendente duplicada cai no índice parcial e vira ApplicationAlreadyPending

Sobre a duplicata pendente

Não tem SELECT antes do insert: duas candidaturas simultâneas leriam "nenhuma pendente" e inseririam as duas. Quem decide é o índice parcial, e a Action traduz a violação para a exception de domínio. Como candidatura recusada sai do índice, candidatar-se de novo depois continua valendo (tem teste pra isso).

Fora de escopo (por dependência)

Plano de testes

  • ApplyToSquadTest.php: APTO abre pending com mensagem, não-APTO barrado (sem onboarding, onboarding em progresso, só o Welcome concluído), duplicata pendente barrada, candidatura recusada não bloqueia nova, pendente em outro squad não bloqueia. 7/7 verdes
  • Suite de app-modules/squads: 47/47 verdes
  • make check (rector + pint + phpstan) limpo

Closes #360

Abre o único fluxo que o módulo conduz: a candidatura. Entrar exige o gate
APTO do onboarding, então quem não concluiu o onboarding Squads é barrado e
direcionado para lá.

O limite de uma candidatura pendente por squad é o índice parcial
UNIQUE (squad_id, user_id) WHERE status = 'pending', não um SELECT prévio:
duas candidaturas simultâneas leriam "nenhuma pendente" e inseririam as
duas. O insert decide. Uma candidatura recusada sai do índice, então
candidatar-se de novo depois continua valendo.

A decisão do capitão (aprovar/recusar) fica na #361, e a exclusividade de
um squad ativo por pessoa na #362.
@sirelves
sirelves requested a review from a team August 31, 2026 14:44
@stherzada stherzada added this to the Squads milestone Aug 31, 2026
@stherzada stherzada linked an issue Aug 31, 2026 that may be closed by this pull request
5 tasks
BrunaDomingues
BrunaDomingues previously approved these changes Sep 8, 2026

@BrunaDomingues BrunaDomingues left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Comment on lines +52 to +54
} catch (UniqueConstraintViolationException) {
throw ApplicationAlreadyPending::for($squad, $applicant);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este catch não está genérico demais? Hoje só tem um campo unique nesta tabela, mas amanhã se tiver um outro unique irá mascará qualquer erro como somente no unique atual.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faz sentido, boa pegada. Hoje só existe um unique na tabela então não quebra nada agora, mas o catch pega qualquer UniqueConstraintViolationException sem checar qual índice foi. Vou trocar pra reconferir se existe mesmo uma candidatura pendente pra esse squad+usuário antes de mapear pra ApplicationAlreadyPending, e relançar a exceção original se não existir. Assim não fica preso ao nome físico do índice, se um dia surgir outro unique na tabela o catch não mascara ele.

@sirelves sirelves Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigido no commit 0f0c26d. Agora reconfere se existe candidatura pendente pra esse squad e usuário antes de mapear pra ApplicationAlreadyPending, e relança a exceção original se não for esse o caso.
@hefeus

GabrielFVDev
GabrielFVDev previously approved these changes Sep 14, 2026

@GabrielFVDev GabrielFVDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

O catch pegava qualquer UniqueConstraintViolationException e assumia
que era sempre candidatura pendente. Agora reconfere se existe mesmo
uma candidatura pendente pra esse squad e usuário antes de mapear pra
ApplicationAlreadyPending, e relança a exceção original se não existir.

O insert precisou ir pra dentro de um DB::transaction porque, sem
isolar numa savepoint, o insert que falha aborta a transação inteira
no Postgres e a query de reconferência quebra junto.

@davicbtoliveira davicbtoliveira left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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): squad_applications + ApplyToSquad (gated em APTO)

7 participants