## MG-5 — Apuração e classificação materializada em Cloud Functions

**PRD:** `docs/prd/migracao-firebase.md`

**Como** participante, **quero** que o ponto seja calculado pelo servidor e o
ranking apareça na hora, **para** ninguém precisar acreditar em ninguém.

**Prioridade:** Must — é o coração do produto e a regra que o cliente nunca pode
calcular.

**Critérios de aceite:**

- Dado que um jogo recebe placar final, então os palpites daquele jogo são apurados
  pela tabela **do bolão** (não por constantes), e a classificação é atualizada.
- Dado um bolão 20/10/5, então o placar exato vale 20 — no ranking **e** no extrato.
- Dado que palpitei `1x1` num jogo que terminou `4x4`, então valem **3** pontos: todo
  empate tem saldo zero e não existe faixa de saldo para empate.
- Dado que dois participantes empatam em pontos, então a ordem sai pela ordem de
  desempate **configurada no bolão**, e o critério aplicado fica visível para os
  dois lados.
- Dado que um placar é corrigido depois da apuração, então a pontuação é
  recalculada, a classificação acompanha e quem foi afetado é avisado — com **um**
  aviso consolidado quando vários jogos mudam de uma vez.
- Dado um bolão com 60 participantes, quando a apuração roda, então ela não estoura
  o limite de operações por transação.
- Dado que a apuração roda duas vezes para o mesmo jogo, então o ponto não dobra.

**Tasks:**

_Backend_
- [x] `BE-1` Portar `scoring.service` para Function pura, sem dependência de Prisma (depende de MG-2 BE-1)
- [x] `BE-2` Gatilho de escrita do jogo dispara a apuração (depende de BE-1)
- [x] `BE-3` Classificação materializada atualizada na apuração, com desempate configurável (depende de BE-1, MG-2 BE-4)
- [x] `BE-4` Extrato: composição em memória de palpite + jogo, sem `join` (depende de BE-1)
- [x] `BE-5` Correção manual de placar pelo admin, com trilha de auditoria (depende de BE-2)
- [x] `BE-6` Aviso consolidado por participante quando vários jogos são corrigidos juntos (depende de BE-5)
- [x] `BE-7` Escrita em lote para bolão grande, respeitando o limite de 500 operações (depende de BE-3)
- [x] `BE-8` Idempotência: apurar duas vezes não dobra ponto nem duplica registro (depende de BE-2)

_QA_
- [ ] `QA-1` Escrever o plano de casos de teste a partir desta story
- [ ] `QA-2` Executar reaproveitando `PR-1`, `PR-2`, `PR-5`, `PR-6`, `PR-7` e `PR-10`, mais o caso de bolão grande

**Riscos/Dependências:**

- Depende de `MG-2`. **Exige Blaze para publicar**, não para testar em emulador.
- **Limite de 500 operações por transação** é o risco técnico real: bolão de 60
  pessoas × jogos da rodada passa disso. Daí `BE-7`.
- A regra do empate (`1x1` num `4x4` = 3 pontos) foi **confirmada pelo dono em
  07/08**. Mudança aqui altera pontos já registrados.

- **07/08 — 5 de 8 tasks de backend feitas.** Execução em
  `docs/testes/MG-5-execucao.md`. Suíte total de **110** casos. `BE-4` (extrato),
  `BE-5` (correção manual) e `BE-6` (aviso consolidado) seguem abertas: dependem de
  rota chamável pelo app, que é assunto de `MG-7`.
- **Porte, não reescrita.** A pontuação e a classificação já eram funções puras no
  NestJS, com 33 casos cobrindo faixa de saldo, critério exibido e posição
  compartilhada. Vieram como cópia, com **os mesmos IDs** (`PR-1-*`, `PR-5-*`):
  se um caso mudar de resultado, mudou a regra, e isso é defeito de migração — não
  melhoria. Reescrever "melhor" na migração seria trocar regra verificada por regra
  nova, e a forma de descobrir seria o primeiro bolão real.
- **Um defeito no produto e um no teste, na mesma falha.** O gatilho
  (`onDocumentWritten`) dispara em toda escrita do jogo, inclusive o
  `sourceUpdatedAt` que a sincronização grava sem mudança — e reapurava o bolão
  inteiro a cada uma. Não errava o resultado; jogava leitura e escrita fora, no
  serviço em que **custo é por operação**. Agora compara resultado antes/depois e
  sai fora se nada mudou, o que também é a proteção de *at-least-once*.
  No teste, minha condição de espera ("existem 4 linhas") já era satisfeita pela
  passada anterior à apuração, então ele lia zero e acusava o produto. **Segunda vez
  nesta migração** que espera mal escolhida faz teste de integração parecer defeito
  de regra.
- **Decisão registrada:** a classificação é recalculada do zero, não por delta. É
  mais escrita e mantém o agregado derivável — delta acumulado erra em silêncio até
  alguém comparar com o extrato.

- **07/08 — `BE-4`, `BE-5` e `BE-6` fechadas junto com `MG-7`.** Extrato, correção
  manual com trilha e aviso consolidado foram escritos quando o app precisou
  deles, verificados nos casos `MG-7-55` a `MG-7-70`
  (`docs/testes/MG-7-execucao.md`). O aviso consolidado mudou de mecanismo: o
  coletor em memória virou fusão de aviso não lido, porque em Functions cada jogo
  corrigido é uma invocação separada.
