## PP-10 — Job de sincronização com a fonte de dados

**PRD:** `docs/prd/palpite-e-prazo.md` (serve também `PR-1`, `PR-6` e `CC-1`)

> **Story criada em 03/08 ao auditar cobertura.** Três stories tocavam a
> integração com a fonte — `CC-1 BE-1` (calendário), `PR-1 BE-1` (placar) e
> `PP-6 BE-1` (mudança de horário) — mas **nenhuma era dona do processo
> recorrente** que busca esses dados: o intervalo, o tratamento de falha, e o
> requisito de continuar rodando. Sem dono, cada story assumiria que a outra
> resolve.

**Como** participante, **quero** que o app reflita o calendário e os resultados
oficiais sem ninguém rodar nada à mão, **para** o prazo fechar na hora certa e
o ponto aparecer sozinho.

**Prioridade:** Must — `PP-6`, `PR-1` e `PR-6` só funcionam se este job estiver
rodando. É infraestrutura, mas o produto para sem ela.

**Critérios de aceite:**

- Dado que existem jogos no bolão, quando o job roda, então ele busca da fonte
  o horário e o placar de cada jogo e atualiza o que mudou.
- Dado que o `utcDate` de um jogo mudou na fonte, quando o job roda, então a
  mudança é detectada e o gatilho de `PP-6` dispara.
- Dado que o placar de um jogo já apurado mudou na fonte, quando o job roda,
  então o gatilho de recálculo de `PR-6` dispara.
- Dado que a fonte está indisponível ou retorna erro, quando o job roda, então
  ele **não corrompe o dado existente**, registra a falha e tenta de novo no
  ciclo seguinte.
- Dado que a fonte impõe limite de requisições, quando o job roda, então ele
  respeita o limite — 10 req/min no tier gratuito.
- Dado que o job ficou parado por um período, quando volta a rodar, então ele
  recupera o atraso sem perder mudanças ocorridas no intervalo.

**Tasks:**

_Backend_
- [x] `BE-1` Cliente da `football-data.org` v4 com token em variável de ambiente
  - **Feito em 07/08**: `FootballDataAdapter`, token em `FOOTBALL_DATA_TOKEN`
    (`.env`, fora do versionamento). Toda falha HTTP — incluindo 400, 403, 404,
    429 e timeout de 10 s — é traduzida para `FonteIndisponivelError`, que o
    domínio já absorve sem sobrescrever dado bom.
- [x] `BE-2` Job recorrente que sincroniza jogos do bolão: `utcDate`, `status` e placar (depende de BE-1)
- [x] `BE-3` Detectar mudança comparando com o armazenado, usando `lastUpdated` para reduzir trabalho (depende de BE-2)
- [x] `BE-4` Disparar os gatilhos de `PP-6` (horário) e `PR-6` (placar) a partir da mudança detectada (depende de BE-3)
- [x] `BE-5` Tolerância a falha: erro da fonte não sobrescreve dado bom, e é registrado (depende de BE-2)
- [x] `BE-6` Respeitar o rate limit da fonte (depende de BE-1)
  - **Feito em 07/08.** O limite de 10 req/min **desenhou** o adaptador: uma
    requisição por temporada (não por rodada), cache de 5 min por competição, e a
    cota recusada **antes** de ser gasta — a chamada excedente falha localmente em
    vez de sair para tomar 429. O adaptador também obedece o que a própria fonte
    informa (`x-requests-available-minute`, `x-requestcounter-reset`), que é mais
    confiável que a nossa contagem porque considera outras instâncias.
- [x] `BE-7` Recuperar atraso após período parado, sem perder mudanças (depende de BE-2)

_DevOps_
- [ ] `OPS-1` 🔴 **Hospedagem que não hiberne o processo de polling.** Vários free tiers dormem por inatividade — e um job dormindo faz `PP-6` e `PR-1` pararem silenciosamente. Este é o requisito de infra mais fácil de errar de graça
- [x] `OPS-2` Token da fonte fora do repositório, e `.env.example` atualizado
- [x] `OPS-3` Log estruturado do job: o que sincronizou, o que mudou, o que falhou (depende de BE-5)

_QA_
- [x] `QA-1` Escrever o plano de casos de teste a partir desta story
- [x] `QA-2` Executar o plano com fonte indisponível, resposta malformada, rate limit excedido e job parado por horas

**Riscos/Dependências:**

- **Nada em `PP-6`, `PR-1` e `PR-6` funciona sem esta story.** Ela é o único
  ponto de entrada de dado externo do sistema.
- ⚠️ **O tier gratuito atrasa placar e schedule**, e o tamanho do atraso não é
  documentado (`docs/spikes/fonte-de-dados.md`). `QA-2` deve medir o atraso real
  observado — é o único jeito de saber se `PP-6` é confiável.
- **Hibernação de free tier é o risco silencioso:** o job para, nada dá erro, e
  o prazo fecha errado. `OPS-1` não é polimento.
- Rate limit de 10 req/min é folgado para este desenho (~24 req/dia pollando a
  rodada de hora em hora), **porque o app nunca chama a fonte direto** — só o
  backend chama, e o app consome o nosso banco.

- **06/08 — plano em `docs/testes/PP-10-plano.md`; execução: ⚠️ 9 de 11**
  (`docs/testes/PP-10-execucao.md`). `QA-2` **fica aberta**: `PP-10-06` (rate
  limit) e `PP-10-07` (resposta malformada) não têm o que executar enquanto o
  cliente real não existir. Marcar seria dizer que a integração está testada
  quando o que está testado é o adaptador falso.
- `BE-7` **fechada pela execução de `PP-10-05`**: montei duas mudanças acumuladas
  na fonte (um jogo adiado e outro com placar corrigido) e uma única volta aplicou
  as duas. O desenho é o que torna isso verdade — o job compara o estado atual do
  banco com o atual da fonte, sem depender de ter visto o intermediário. Um job
  baseado em cursor ("o que mudou desde") perderia a mudança da parada, e é o
  desenho mais natural de escrever.
- Casos novos sem cobertura anterior: `PP-10-08` (jogo que a fonte esquece
  permanece intacto — apagar levaria os palpites de todos), `PP-10-09` (jogo novo
  não entra em bolão já criado), `PP-10-11` (duas sincronizações concorrentes
  geram uma correção, não duas) e `PP-10-10b` (ausência da variável **não**
  desliga o job: deploy que a esqueça não pode fechar prazo na hora errada em
  silêncio).

- **07/08 — `BE-2` reaberta e refeita (falhou `PP-10-11`).** Duas sincronizações
  concorrentes gravavam **duas** correções para a mesma mudança de placar:
  histórico do jogo duplicado e aviso de recálculo saindo duas vezes para todos.
  O caso `PP-10-11` havia passado por sorte na execução de 06/08 e falhava em ~1
  de 4 execuções da suíte completa. Corrigido serializando a sincronização por
  campeonato (`ignorada: true` para o pedido concorrente, ciclo ignorado
  registrado no log do job) e reverificado 5× em série + suíte completa 3×.
  **Limite conhecido:** trava em processo, suficiente para a instância única
  desta versão; com mais de uma instância a trava tem de ir para o banco
  (`pg_try_advisory_lock`).

- **07/08 — `QA-2` fecha.** Os dois casos que estavam bloqueados (`PP-10-06` rate
  limit e `PP-10-07` resposta malformada) só existiam na fronteira HTTP, que não
  existia. Agora existe: 13 casos de unidade no adaptador, mais as falhas medidas
  contra a API real. Detalhes em `docs/testes/PP-10-execucao.md`.
- Sincronização executada contra a fonte real: 40 jogos verificados, nenhuma
  alteração falsa, e a trava de concorrência de 07/08 funcionando com dado real.
- `OPS-1` (hospedagem que não hiberne) **segue aberta**: é escolha de
  infraestrutura, não algo que o token resolva. Com o job parado, `PP-6` e `PR-1`
  param em silêncio — é o requisito mais fácil de errar de graça.
