# Execução parcial — `MG-5` Apuração e classificação materializada

**Story:** `docs/stories/MG-5-apuracao-functions.md` · **Data:** 07/08/2026
**Ambiente:** testes puros (sem emulador) + integração nos emuladores. Suíte total:
**110** casos, 9 arquivos.

**Resultado: ⚠️ parcial — 5 de 8 tasks de backend.** `BE-4` (extrato), `BE-5`
(correção manual pelo admin) e `BE-6` (aviso consolidado) seguem abertas: as três
dependem de rota chamável pelo app, que é assunto de `MG-7`, e o aviso consolidado
depende do gatilho de correção existir.

## O que foi portado, e por que porte e não reescrita

A regra de pontuação e a de classificação já eram **funções puras** no NestJS, com
33 casos de teste cobrindo empate, faixa de saldo, critério exibido e posição
compartilhada. Trouxe as duas como cópia, com **os mesmos IDs de caso**
(`PR-1-01`… , `PR-5-01`…).

Reescrever "melhor" durante a migração seria trocar regra verificada por regra nova
sem ninguém pedir — e a forma de descobrir o erro seria o primeiro bolão real. Com o
porte, se um caso mudar de resultado, mudou a regra, e isso é **defeito de
migração**, não melhoria.

Três decisões que vieram de defeito real e foram preservadas com o comentário que
explica por quê:

- **Empate não tem faixa de saldo** — `1x1` num jogo que termina `4x4` vale 3, não 5
  (confirmado pelo dono em 07/08).
- **O critério de desempate é olhado para os dois lados** — comparar só com o
  anterior deixava quem está **na frente** do empate sem explicação, e é justamente
  quem é acusado de estar na frente sem motivo (`PR-5 BE-4`).
- **Campeão precisa ter pontuado** — sem o `points > 0`, bolão em que ninguém acertou
  nada declarava todo mundo co-campeão (`CV-1-09`).

## Adaptação de compilador, não de significado

Os specs portados usavam `r[0].campo`, e este projeto compila com
`noUncheckedIndexedAccess`. Acrescentei `!` nos acessos por índice — nos testes e em
dois pontos do `classificar`. Muda o compilador, não o que cada caso afirma.

## Um defeito no produto e um no teste, na mesma falha

`MG-5-01` falhou lendo 0 onde esperava 10, e o log entregou os dois:

**No produto:** o gatilho é `onDocumentWritten`, que dispara em **toda** escrita do
jogo — inclusive na criação e no `sourceUpdatedAt` que a sincronização grava a cada
volta mesmo sem mudança. Cada uma dessas escritas reapurava o bolão inteiro e
regravava a classificação de todos. Não estava errado no resultado; estava jogando
leitura e escrita fora, no serviço em que **custo é cobrado por operação** e o free
tier é o requisito. Agora a Function compara `status`/`homeScore`/`awayScore` antes e
depois e sai fora se o resultado não mudou — o que também é a proteção de
*at-least-once*, já que o Firestore não garante *exactly-once*.

**No teste:** eu esperava por "existem 4 linhas de classificação", condição que a
passada **anterior** à apuração já satisfazia (classificação de zeros). O teste lia
zero e acusava o produto. Passou a esperar pelo estado de interesse (`pontos === 10`).

É a segunda vez nesta migração que uma **condição de espera mal escolhida** faz um
teste de integração parecer defeito de regra. A primeira foi o timeout do Jest em
`MG-3`.

## Os sete casos de integração

| ID | O que verifica |
| -- | -------------- |
| MG-5-01 | o gatilho apura e materializa a classificação (10/5/3/0) |
| MG-5-02 | a tabela lida é **a do bolão** — mesmo jogo, 20/10/5 em outro bolão |
| MG-5-03 | ordem, posição e critério (`PONTOS` sem empate) |
| MG-5-04 | apurar duas vezes não dobra ponto |
| MG-5-05 | correção de placar recalcula e troca a ordem |
| MG-5-06 | placar retirado **desfaz** a apuração (`PP-6-07`) |
| MG-5-07 | quem saiu fica fora do ranking, e o palpite dele permanece |

`MG-5-02` é o que impede a configuração de virar decorativa: se a apuração usasse
constante, os dois bolões sairiam iguais — exatamente o defeito de `CC-2 BE-3`.

## Decisão registrada: recalcular tudo em vez de somar delta

A classificação é recalculada do zero a cada apuração. É mais escrita, e é o que
mantém o agregado **derivável**: correção de placar, saída de participante e
recriação de temporada mexem na base, e delta acumulado erra em silêncio até alguém
comparar com o extrato. O pior caso aqui é gastar escrita; o pior caso do delta é
ranking errado que ninguém percebe.
