# Auditoria campo a campo dos webhooks: os 20 eventos e os 28 produtores

**Data:** 2026-07-25 (segunda rodada)
**Escopo:** os 20 eventos com payload documentado em `docs/docs-site/webhooks-payloads.md`
confrontados com **todos** os pontos de emissao do Core (28 chamadas de
`dispatch_event/3` e `dispatch_event_to/4`).
**Metodo:** manual, como a primeira auditoria exigiu. A varredura automatica de
existencia dava falso positivo (`pix.payout.failed` aparecia como ausente e
existe). O contrato documentado foi extraido por parse dos proprios exemplos
JSON da doc — nao redigitado. Os produtores foram lidos um a um.

---

## 1. Retratacao da primeira auditoria

O relatorio anterior afirmou:

> `pix.charge.paid` (`use_cases/pix/charge_paid.ex:109`) [...] O produtor monta 9.

**Estava incompleto, e a conclusao que se tirava dele era errada.** `pix.charge.paid`
tem **QUATRO** produtores, nao um:

| produtor | campos | papel |
|---|---|---|
| `pix/tb_first/pg_writer.ex:237` | **17** | money path TB-first — **ja era completo** |
| `workers/pix_in_orphan_reconciliation.ex:445` | 13 | reconciliacao de orfaos |
| `use_cases/pix/charge_paid.ex:109` | 9 | espelho do QR (o que foi medido) |
| `workers/post_deploy_reconciliation.ex:238` | 9 | reconciliacao pos-deploy |

O caminho que mais dispara em producao **ja entregava tudo**. O que existia de
verdade nao era "payload incompleto", e sim **o mesmo evento com quatro formas
diferentes**, conforme qual caminho interno disparou. Para o parceiro isso e pior
do que faltar campo: ele nao consegue programar contra uma forma que muda.

O mesmo padrao vale para `pix.received` (3 produtores) e `pix.payout.returned` (3).

---

## 2. Achados

| # | achado | alcance | gravidade |
|---|---|---|---|
| 1 | `eventType` NAO ia no corpo pelo trilho dirigido | `webhook.test` (unico evento desse trilho) | **Critica** |
| 2 | Doc promete `sender`, produtor emite `payer` | 3 eventos `pix.payout.*` | **Critica** |
| 3 | Doc promete `e2eId`, produtor emite `endToEndId` | 5 eventos `pix.infraction.*` / `pix.refund.*` | **Alta** |
| 4 | Produtor le chave de metadata que NAO existe | `pix.charge.paid` da reconciliacao de orfaos | **Alta** |
| 5 | Mesmo evento com formas diferentes por caminho | `pix.charge.paid`, `pix.received`, `pix.payout.returned` | **Alta** |
| 6 | Campo documentado que nenhum produtor monta | ver §3 | Media |
| 7 | Campo emitido sem estar documentado | ver §4 | Baixa |

### Achado 1 — `eventType` sumia no trilho dirigido (CRITICO)

`dispatch_event/3` montava o corpo com `Map.put("eventType", event_type)`.
`dispatch_event_to/4` canonicalizava e convertia moeda, **mas nao punha o tipo**.

O detalhe que torna isso grave: **`webhook.test` so existe no trilho dirigido**
(4 pontos de chamada). Ou seja, o evento que o parceiro usa para validar a
integracao dele era exatamente o que chegava sem o campo — e quem roteia por
`body.eventType`, que e o padrao do ecossistema, falhava no teste de fumaca e
concluiria que a integracao estava quebrada.

O fix de 24/07 acrescentou o `eventType` a UM dos dois trilhos. Este e o custo de
nao ter um ponto unico: a correcao pegou metade.

### Achado 2 — `sender` x `payer` (CRITICO)

`infra/nats/handlers/pix_handler.ex:640` (`payout_webhook_payload/4`) monta:

```elixir
"payer" => webhook_payer(tx, account_id),
"recipient" => webhook_recipient(tx)
```

A doc de `pix.payout.confirmed`, `.processing` e `.failed` documenta
`sender` + `recipient`, com 5 campos cada.

E a mesma classe do `debtor_document` x `debtor_cpf_cnpj` de 24/07: nao quebra,
nao alarma, so entrega `undefined` para sempre. Note tambem a incoerencia
interna — `payer` pareado com `recipient`, em vez de `sender`/`recipient`.

### Achado 3 — `e2eId` x `endToEndId`

Doc usa `e2eId` em `pix.infraction.created`, `.resolved`, `.defense_submitted`,
`pix.refund.requested` e `.completed`. Os produtores (`use_cases/med/processor.ex`,
`external/med_controller.ex`, `partner_v1/pix_controller.ex`) emitem `endToEndId`.

`pix.refund.failed` e `pix.payout.*` usam `endToEndId` na doc — ali bate.

### Achado 4 — chave de metadata inexistente (defeito, com prova)

`workers/pix_in_orphan_reconciliation.ex:445` montava:

```elixir
payer_name: meta["debtor_name"],
payer_document: meta["debtor_document"],
payer_ispb: meta["debtor_ispb"],
```

Medido em PRD, nas 31 transacoes PIX-in de 10 dias, as chaves do metadata sao:

```
description, source, payer_document, payer_ispb, payer_name,
recipient_institution, institution_name, recipient_ispb
```

**`debtor_*` nao existe em nenhuma.** Esse produtor mandava quem pagou sempre
nulo, com o dado gravado na coluna ao lado. `debtor_*` sao os nomes do fio do
BACEN (a cabine), nao do metadata do Core — a confusao entre os dois vocabularios
ja tinha causado o incidente dos 138 `pix.received` sem pagador.

### Achado 5 — mesma forma para o mesmo evento

Alem do `pix.charge.paid` (§1):

- **`pix.received`** (3 produtores): `pg_writer.ex:339` monta 15 campos
  (com `receiver`, `payerBankName`, `recipientKey`); `pix_handler.ex:972` monta 8
  (com `payerBranch`/`payerAccount`, sem `receiver`); `partner_v1/pix_controller.ex:1491`
  monta 8 (PIX interno). E ainda **nao tem payload documentado** — so consta na
  lista de "eventos que faltavam no catalogo".
- **`pix.payout.returned`** (3 produtores): a doc lista 23 campos
  (`isPartial`, `netAmount`, `totalRefunded`, `remainingRefundable`,
  `counterparty*`); `pix_handler.ex:2180` monta 8.

---

## 3. Campos documentados que nenhum produtor monta

Continuam chegando nulos (agora **presentes e nulos**, ver §5):

| evento | campos sem produtor |
|---|---|
| `pix.charge.paid` | `feeAmount`, `payerBankName`, `receiver` (no trilho do espelho de QR) |
| `pix.payout.confirmed` / `.processing` / `.failed` | `description`, `entityId`, `externalId`, `feeAmount`, `initiatedAt`, `pixKey`, `pixKeyType`; e `account`/`agency` dentro de `payer` e `recipient`; `institutionName` no `recipient` |
| `pix.payout.returned` | `isPartial`, `netAmount`, `originalAmount`, `refundedAmount`, `totalRefunded`, `remainingRefundable`, `counterparty*`, `returnE2eId`, `returnedAt`, `feeAmount`, `entityId`, `externalId` (no trilho do `pix_handler`) |
| `pix.payout.queued` | `reason`, `reasonCode`, `reasonDescription`, `queuedAt`, `queueTtlSeconds`, `estimatedRetrySeconds`, `merchantId`, `externalId` |
| `pix.infraction.created` / `.resolved` | `infractionType`, `situation`, `creationTime`, `defenseDeadline`, `analysisResult`, `analysisDetails`, `counterpartIspb`, `merchantId`, `entityId` |
| `webhook.test` | `accountId`, `entityId`, `status` |

**Isto e uma lista de trabalho, nao um contrato cumprido.** A forma estavel (§5)
garante que o campo existe; ela nao inventa conteudo.

## 4. Campos emitidos sem estar documentados

Nao saem — sao aditivos e uteis:

| evento | campos extras |
|---|---|
| `pix.charge.paid` | `description` (todos os trilhos), `transactionId` e `settledAt` (orfaos) |
| `pix.payout.*` | `errorReason` |
| `pix.payout.confirmed` (interno) | `internal` |
| `pix.charge.created` | `brcode`, `expiresAt` |
| `pix.infraction.*` | `fraudCategory`, `blockId` |
| `webhook.test` | `timestamp`, `webhookId`, `merchantId` |

## 5. O que foi corrigido

`Monetarie.UseCases.Webhooks.Contract`, aplicado no **ponto unico** de montagem
do corpo — os dois trilhos de despacho passam por ele:

1. **`eventType` nos dois trilhos** (achado 1). `delivery_body/2` privado, do qual
   `delivery_body_for_test/2` passou a ser apenas a porta publica: nao ha mais
   como um trilho divergir do outro.
2. **Apelidos documentados** (achados 2 e 3): `sender` acompanha `payer`, `e2eId`
   acompanha `endToEndId`. Aditivo — os dois nomes viajam com o mesmo valor, e o
   apelido **nunca** sobrescreve o que o produtor mandou.
3. **Forma estavel** (achado 5): toda chave documentada existe no corpo, **nula
   quando nao temos o dado, nunca ausente**. `body.feeAmount` vira `null` em vez
   de `undefined` — a diferenca importa para destructuring e para distinguir
   "nao temos" de "campo inexistente".
4. **Chave de metadata corrigida** (achado 4): `payer_*` com fallback para
   `debtor_*` (acervo antigo).
5. **`charge_paid.ex` completado**: passou de 9 para 16 campos, lendo o pagador
   da transacao liquidada que ele **ja consultava** para o guard de liquidacao.

Documentacao alinhada nos 3 idiomas: forma estavel e tabela de apelidos.

**Cobertura:** 12 testes de contrato novos + 1 de regressao no trilho dirigido +
1 no `charge_paid`. Suite do Core: **8935 testes, 0 falhas**.

## 6. O que esta provado e o que nao esta

**Provado por leitura direta do codigo e medicao:**
- os 4 produtores de `pix.charge.paid`, os 3 de `pix.received` e os 3 de
  `pix.payout.returned`, com contagem de campos por produtor;
- o `eventType` ausente no trilho dirigido (teste de regressao DB-backed que
  falhava antes do fix e passa depois);
- as chaves reais do metadata do PIX-in em PRD (31 transacoes, 10 dias);
- o casing e os nomes divergentes (parse da doc x leitura dos produtores).

**NAO provado:** que a entrega real bate com o corpo montado, para os eventos
sem trafego. `webhook_deliveries` em PRD segue **vazia** — nenhum parceiro
recebeu webhook ainda. A confirmacao definitiva vem do primeiro disparo real
contra um endpoint de teste, evento a evento.

**Fora de escopo, segue aberto:** os 6 eventos documentados que nunca sao
emitidos (`pix.charge.cancelled`, `pix.charge.expired`, `pix.payout.held`, os
tres `tef.transfer.*`) — implementar ou remover da doc; e o payload documentado
de `pix.received`, que e o aviso de dinheiro entrando e ainda so consta na lista
de eventos faltantes.
