# Execução — plano `PP-10` (06/08)

**Veredito: ⚠️ Parcial** — 9 de 11 casos passaram; **2 bloqueados** pela ausência
do cliente real e do token.

| Caso | Resultado | Evidência |
| ---- | --------- | --------- |
| PP-10-01 | passou | `test/pp6-reabertura.e2e-spec.ts` |
| PP-10-02 | passou | idem (detalhado no plano `PP-6`) |
| PP-10-03 | passou | idem — caminho de correção com trilha e aviso |
| PP-10-04 | passou | `test/sigilo-pontuacao.e2e-spec.ts` — fonte fora do ar |
| PP-10-05 | passou | `test/pp6-reabertura.e2e-spec.ts` — `PP-10-05` (novo) |
| PP-10-06 | ✅ **desbloqueado em 07/08** | `src/fonte-dados/football-data.adapter.spec.ts` + cabeçalhos reais |
| PP-10-07 | ✅ **desbloqueado em 07/08** | idem — payload sem `matches`, jogo sem rodada, sem data, sem time |
| PP-10-08 | passou | `test/pp6-reabertura.e2e-spec.ts` — `PP-10-08` (novo) |
| PP-10-09 | passou | idem — `PP-10-09` (novo) |
| PP-10-10 | passou | `src/jobs/pp-10-plano.spec.ts` — `PP-10-10` e `PP-10-10b` |
| PP-10-11 | ⚠️ **reprovou depois** → corrigido em 07/08 | `test/pp6-reabertura.e2e-spec.ts` — `PP-10-11` |

`QA-2` ficou **aberta em 06/08** porque dois casos não tinham o que executar sem o
cliente real. **Com o token recebido em 07/08 os dois foram executados** e `QA-2`
fecha — ver a seção "Fonte real" no fim deste documento.

## O que a execução confirmou (`BE-7`)

`PP-10-05` é a verificação da task `BE-7` ("recuperar atraso após período parado,
sem perder mudanças"), que estava **aberta**. Montei duas mudanças acumuladas na
fonte — um jogo adiado e outro com placar corrigido — e uma única execução aplicou
as duas.

O desenho é o que torna isso verdade: o job compara o estado **atual** do banco
com o estado **atual** da fonte, sem depender de ter visto o intermediário. Um job
baseado em "o que mudou desde o último cursor" perderia a mudança ocorrida
durante a parada — e é o desenho mais natural de escrever.

## Casos novos que não tinham cobertura

- **`PP-10-08`** — jogo que a fonte deixa de conhecer **permanece intacto**.
  Apagar levaria embora os palpites de todos por causa de uma falha da fonte.
- **`PP-10-09`** — jogo novo na fonte **não** entra num bolão já criado. Os jogos
  são fixados na criação; acrescentar depois mudaria a competição no meio.
- **`PP-10-11`** — duas sincronizações concorrentes geram **uma** correção, não
  duas. **Passou por sorte na primeira execução, e isso foi um erro meu de
  leitura:** eu concluí que a correção sem mudança é no-op porque "quando a
  segunda volta compara, o placar novo já está aplicado" — o que só é verdade se
  as duas não se cruzarem. Rodando a suíte completa várias vezes em 07/08, o caso
  falhou em **~1 de 4** execuções, com 2 correções.

  A corrida é do produto, não do teste: as duas execuções liam o placar antigo
  ao mesmo tempo e as duas gravavam correção — o histórico do jogo mostrava a
  mesma alteração duplicada e o aviso de recálculo saía duas vezes para todo
  mundo. É alcançável de verdade: cron horário que demore mais de uma hora, ou
  cron coincidindo com sincronização disparada à mão.

  Corrigido em `src/sync/sync.service.ts`: sincronização por campeonato passa a
  ser serializada, e o pedido concorrente volta com `ignorada: true` em vez de
  devolver os números da outra execução (somar duas vezes mentiria no relatório
  do job). O job registra o ciclo ignorado — se aparecer sempre, o intervalo do
  cron está curto demais para o tempo da sincronização.

  **Limite conhecido:** a trava é **em processo**, o que cobre a instância única
  desta versão. Com mais de uma instância o lugar da trava passa a ser o banco
  (`pg_try_advisory_lock`).

  Lição registrada: caso concorrente que passa **uma vez** não está verificado.
  Passou a ser rodado em série (5×) além da suíte completa.
- **`PP-10-10b`** — ausência da variável `JOBS_HABILITADOS` **não** desliga o job.
  Padrão ligado importa: um deploy que esqueça a variável não pode ficar com o
  prazo fechando na hora errada, em silêncio.

## Aviso que não bloqueia

⚠️ O requisito de infra da story (`OPS-1`) continua sendo o mais fácil de errar:
hospedagem que hiberna por inatividade **para o job sem gerar erro nenhum**. Não
há teste que pegue isso — só monitoração de execução em produção. Segue
registrado nos impedimentos do `CONTROLE.md`.

---

## Fonte real, 07/08 — os dois casos bloqueados

O token da `football-data.org` chegou, `BE-1` e `BE-6` foram implementados
(`src/fonte-dados/football-data.adapter.ts`) e os dois casos que estavam sem o que
executar foram executados.

### `PP-10-06` — rate limit de 10 req/min

O limite do plano gratuito não é detalhe de implementação: ele **desenhou** o
adaptador.

- **Uma requisição por temporada, não por rodada.** A v4 aceita `?matchday=N`, mas
  um bolão de 16 rodadas gastaria 16 chamadas — o minuto inteiro, por uma criação
  de bolão. O adaptador busca a competição toda (380 jogos, ~410 KB, 786 ms
  medidos) e filtra o intervalo em memória.
- **Cache de 5 min por competição.** Criar bolão é rajada: todo mundo cria no mesmo
  dia. Medido: segunda leitura em **0 ms**, sem chamada.
- **A cota é recusada antes de ser gasta.** Ao chegar em 9 chamadas na janela, a
  décima falha localmente com `FonteIndisponivelError` — não sai para a rede.
  Gastar e tomar 429 queimaria a cota **e** deixaria o rastro de abuso.
- **A fonte manda mais que a nossa contagem.** O adaptador lê
  `x-requests-available-minute` e `x-requestcounter-reset` (confirmados na resposta
  real) e se pausa sozinho quando a fonte diz que zerou — ela sabe o que outra
  instância já gastou com o mesmo token.

Verificado em teste de unidade com `fetch` falso (13 casos) porque martelar a API
real para provar o limite seria justamente o abuso que o limite existe para evitar.

### `PP-10-07` — resposta malformada

Agora existe fronteira HTTP para malformar. Coberto: payload sem `matches`, jogo
sem rodada, jogo com data inválida, jogo sem time, e o caso em que **todos** os
registros são inutilizáveis.

A decisão que importa: registro ruim é **descartado** (um jogo sem rodada não entra
num bolão organizado por rodada; sem data não tem prazo), mas payload inteiro
inútil é **indisponibilidade**, não lista vazia. Lista vazia o domínio leria como
"não há jogos nessas rodadas" e o admin veria erro de validação em vez de "tente de
novo em alguns minutos".

Falhas reais medidas contra a API: token inválido → `400`, competição inexistente
→ `404`, ambas traduzidas para `FonteIndisponivelError` — que é o que a criação de
bolão já sabe absorver sem criar bolão pela metade.
