# Handoff 2026-07-25: split-brain ICOM, chamados do IB, orfaos e contratos de webhook

Sessao longa, toda em cima de defeito PROVADO contra a fonte da verdade. Tudo o
que esta aqui foi medido; onde nao houve prova, esta escrito que nao houve.

Regra que guiou: **BACEN e a verdade, ache o NOSSO defeito e PROVE, zero
inferencia.** As retratacoes estao na secao 8 — houve varias, e uma delas foi
cobrada pelo dono com razao.

---

## 1. Estado final

`main` == `origin/main` == **`d06e9a4f`**, working tree limpo (so os untracked
pre-existentes: `LegadoPIX/`, `md/`, `monetarie-*`).

| servico | HML | PRD | digest |
|---|---|---|---|
| core-api | td **220** | td **109** | `sha256:cb872517` |
| pix-api | td **217** | td **87** | `sha256:d4175bfa` |
| core-banking-ui | td 54 | td **17** | `sha256:e532a97d` |

Guard de digest MATCH em todos os retags. Aurora PRD **min 2 / max 32 ACU**
(era 0.5/16); HML **max 8** (era 4).

---

## 2. Split-brain da lideranca ICOM (a maior frente)

Relatorio: `docs/reports/2026-07-25-auditoria-conn-limited-split-brain-icom.md`.

**O `conn_limited` nunca foi o BACEN.** Nasce no NOSSO semaforo Redis
(`client.ex:512`): sem permit em 3s, vira `:conn_limited`. Nenhuma request chega
a sair.

**Causa-raiz:** o `Coordinator` tomava `pg_try_advisory_lock` numa conexao do
pool Ecto compartilhado e **nunca reverificava**. Quando o Postgrex derrubava
aquela conexao (~10x/hora), o Postgres soltava o lock e o Coordinator seguia
liderando. Outro pod assumia junto. **3 lideres = 18 workers para 6 permits.**

Cronico: 5 episodios em 48h, um de ~9h. Dose x efeito: 2 lideres = 2.622/h,
3 lideres = 8.157/h.

**Corrigido:** conexao Postgrex DEDICADA para o lock (`Shared.Postgres.AdvisoryLock`),
reverificacao a cada 5s com step-down (derruba os workers ANTES de tudo),
fail-open degradado com teto local por pod, e `SlotMetrics`.
**Resultado medido: `IcomConnLimited` = ZERO em todas as horas desde o deploy.**

**Retratacao dentro da propria correcao:** emiti um flag `saturado` em
`in_use >= max`; o dado vivo mostrou que os pools de LEITURA vivem no teto por
desenho (6 workers, 6 permits). Virou `no_teto` (factual) e o alarme passou a ser
a RECUSA.

---

## 3. Chamados do IB (prints do dono)

Detalhe: [[monetarie-ib-telefone-e164-qr-envelope-0725]].

1. **Chave por telefone**: `11942503269` ia CRUA ao DICT
   (`GET /api/v2/entries/11942503269 -> 404`). Armadilha: celular tem 11 digitos
   e **CPF tambem**. `Monetarie.UseCases.Pix.PixKey` desambigua pelo **DV do CPF**.
   Duas rotas de lookup (IB e a propria do merchant) foram corrigidas.
2. **QR em branco** (`status.undefined`): o backend estava certo; a tela lia
   `response.data`, que e o ENVELOPE. A lista irma ja desembrulhava. **O merchant
   ja tinha esse fix desde 20/07** — o IB ficou para tras.
3. **404 do DICT** virava "servico indisponivel". Agora vira 404 com "nao
   encontrada", que o front ja traduz para `[DICT_NAO_ENCONTRADA]`. O merchant
   NAO mudou: tem contrato proprio (200 com status no corpo).
4. **QR vencido aparecia Ativo**: `qrcode_status` nunca olhava `expires_at`, e
   nada no fluxo marca vencimento. Agora e derivado na leitura. `paid`/`used` nao
   viram expirado.
5. **Imagem do QR quebrada**: o serializer devolvia so `imageUrl: location_url`
   (URL de location, nao imagem). Agora sai do BRCode.
6. **Expiracao fixa em 1h**: `create_pix_charge` ignorava `expires_in`. Agora
   aceita `expiresIn`, teto de 24h.

---

## 4. Nome do pagador no extrato

O BACEN **manda** o nome em toda pacs.008 (`<Dbtr><Nm>`), a cabine guarda em
`payments` (**8812 de 8812**) e o Core **ja sabia ler**. O dado se perdia num
unico ponto: `transaction.created` levava so os ISPBs. Mesmo padrao do incidente
D8 ("o envelope nao carrega amount") — enriqueceram o amount e nao as partes.

**PENDENTE:** o acervo ja gravado segue sem nome (16 de 37 PIX-in). O
`Monetarie.Release.PixPartyBackfill` (de 22/07) existe para isso e **nao foi
reexecutado**.

---

## 5. Orfaos de PIX-in: 14 -> ZERO

Os 14 `open` (R$ 55.602,92) foram resolvidos **com prova**, nenhum era dinheiro
perdido:

- 7 `message_not_credited` RJCT = os 7 AB03, ja estornados em 24/07;
- 1 de R$ 600 com RTRN = recebemos e devolvemos (pacs.004 no mesmo segundo);
- 4 `no_outbox_event` = **falso positivo por poda do Oban** (os 4 tem transacao
  no Core, logo o evento chegou e o job foi podado);
- R$ 4,54 = incidente de 09/07, **dinheiro ja creditado** na epoca;
- R$ 0,25 = **nao era PIX-in**: pacs.008 OUTBOUND nossa com 4 pacs.004 de
  devolucao.

**Tres tipos de falso positivo corrigidos no worker:** job podado (guard pela
janela real do Pruner, lida da config), devolucao de saida (E2E com pacs.008
OUTBOUND nossa nao e orfao), e o acervo ja tratado.

---

## 6. Contratos de webhook da Partner API

Relatorio: `docs/reports/2026-07-25-auditoria-contratos-webhook-partner-api.md`.

Auditado contra o **corpo entregue de verdade em HML**:

- **camelCase** na entrega x **snake_case** em toda a doc -> integracao escrita
  pela doc nao encontrava campo nenhum;
- **`event_type` removido** do corpo (ia so no header);
- **`pix.received` sem identificar quem pagou**: `payerDocument` null em **138**
  entregas, `payerName` em 135. Causa: o Core le `debtor_cpf_cnpj`/`debtor_branch`/
  `debtor_account` e a cabine nao enviava nenhum — o fix da secao 4 mandava
  `debtor_document`, nome DIFERENTE, e o campo continuaria nulo;
- **6 eventos documentados que NAO existem** (`pix.charge.cancelled`,
  `pix.charge.expired`, `pix.payout.held`, os tres `tef.transfer.*`);
- **10 eventos emitidos sem documentacao**, incluindo `pix.received`.

**Padronizado (decisao do dono: doc segue o codigo):** `eventType` no corpo
(aditivo, header mantido), `delivery_body_for_test/2` como ponto unico
verificavel, 58 payloads + 440 campos de tabela convertidos em 3 idiomas, aviso
explicito dos 6 nao emitidos e catalogo dos 10 faltantes.

**Cuidado registrado:** a API HTTP de gestao de webhooks responde em
**snake_case** (`is_active`, `created_at`); so o CORPO ENTREGUE e camelCase. Na
primeira conversao eu quebrei os exemplos de erro (`bad_request` -> `badRequest`)
e revertei.

**PENDENTE (precisa de aval, muda o que o parceiro recebe):** payloads
incompletos. `pix.charge.paid` entrega 9 de 18 campos documentados;
`pix.payout.confirmed` entrega 6 de 15+2 objetos. Os outros 19 eventos **nao
foram auditados campo a campo** — a varredura automatica dava falso positivo
(o proprio `pix.payout.failed` apareceu como ausente e existe).

---

## 7. Outras frentes fechadas

- **DLQ**: PRD zerada (20 purgadas, apos o fix que as gerava). HML 21 purgadas e
  **16 PRESERVADAS de proposito** — defeitos distintos, nao investigados:
  `poison.settlement-scheduler` (`not_null_violation` x7, `undefined_column` x3)
  e `spb.failed` (`string_data_right_truncation` x2, vazio x4).
- **Seq scans**: Oban Pruner 7 -> 2 dias (`oban_jobs` lia 7,36 bilhoes de tuplas
  guardando 29 mil jobs `completed`); indice `camt060_requests.sent_at`
  CONCURRENTLY (`list_recent/1` fazia scan+sort de 45 mil linhas para devolver 50).
  `icom_sessions` NAO foi tocada: 2,2 milhoes de scans mas 40 tuplas cada em
  tabela de 44 linhas — ali o planner acerta.
- **Pool do banco**: 28 -> 72 conexoes/task (medido: 2000 disponiveis, 165 em uso).
- **Alarmes**: SNS `monetarie-pix-alarms` + 3 alarmes CloudWatch.
  **luiz@vulci.com.br CONFIRMOU; gabriel@monetarie.com e everton@vulci.com.br
  seguem `PendingConfirmation` — sem clicar no link, o alarme dispara para o vazio.**
- **CCS**: 3 testes dependentes de data corrigidos (o prefixo do `num_remessa` e o
  DIA DE MOVIMENTO, nao a data crua; e o job diario so roda em dia util).

---

## 8. Retratacoes desta sessao

1. "O lag ICOM NAO e do split-brain" -> **ERRADO**. Olhei uma amostra; a serie por
   hora mostra 5-20/h de baseline indo a **100/h** no pico e voltando a ~0,3/h
   depois do fix.
2. Emiti alarme de `saturado` em `in_use >= max`, que e o estado NORMAL.
3. **Deixei 2 orfaos "sem prova" sem consultar o BACEN** — o dono cobrou com
   razao. A prova estava nas mensagens que o BACEN ja tinha entregue, e uma delas
   ja estava documentada na propria memoria do incidente de 09/07.
4. Procurei o "CPF no lugar do nome" no Core quando o dono disse **cabine**; e
   depois olhei `json_input`, o campo errado para mensagem RECEBIDA. So achei
   varrendo as 93 colunas de nome da cabine.
5. Na conversao da doc, converti TODOS os blocos e quebrei os exemplos de erro da
   API HTTP. Revertido e refeito com filtro.

---

## 9. Fila da proxima sessao

1. **Confirmar as 2 inscricoes SNS pendentes** (gabriel, everton) — sem isso os
   alarmes nao chegam a ninguem.
2. **Completar os payloads dos webhooks** (secao 6) — precisa de aval, muda o que
   o parceiro recebe. Comecar por `pix.charge.paid` e `pix.payout.confirmed`.
3. **Auditar os outros 19 eventos** campo a campo, pelo metodo manual do relatorio.
4. **Reexecutar o `PixPartyBackfill`** para o acervo sem nome (16 PIX-in).
5. **CPF concatenado ao nome** (206 registros legados, dez/2025 a mar/2026, zero
   no fluxo atual): o numero embutido NAO bate com a coluna de documento, entao
   limpar automaticamente seria adivinhar. Decisao do dono.
6. **Validar em PRD com transacao real**: PIX-out com IPAY, AB03 com volume, QR
   pelo IB.
7. **P2 do split-brain**: fencing por epoca (recomendei ADIAR — retorno marginal
   agora) e rever `queue_target` (recomendei NAO mexer: a lacuna sumiu com a
   conexao dedicada).
8. **16 mensagens preservadas na DLQ de HML** (secao 7): 3 defeitos distintos.
9. **Lag ICOM residual** p95/p99 de 10-25s: frente separada, precede o split-brain.
10. **Portar fluxo de credito coreproviders** + fix `reverse_entry`; conciliacao
    SPB x Core contra BACEN real; E2E divergente; metade 2 do port PIX-in.

---

## 10. Ferramentas

Os helpers ficaram FORA do repo (em `scratchpad/` de sessao), porque
`scratchpad/` nao esta no `.gitignore` e sujaria o working tree que o codex divide:

- `rpc.sh <cluster> <servico> <container> "<bin> rpc" <arquivo.exs>` (base64 via
  ECS Exec). Clusters: `monetarie-greenfield-{prod,homolog}`. Bins:
  `bin/monetarie rpc` (Core), `bin/monetarie_pix rpc` (cabine).
- Log groups: `/ecs/monetarie/{prod,homolog}/{core-api,pix-api}` — o ambiente vai
  NO MEIO do caminho.
- Fronts sao servicos ECS proprios (`core-banking-ui`, `core-merchant-ui`), build
  com contexto `core/` e `-f core/apps/<app>/Dockerfile`.
- **GOTCHA**: varredura de XML em `icom_received`/`bacen_inbound` sem filtro de
  data estoura a sessao do ECS Exec. Sempre limitar por `received_at`/`receive_date`.
