# Causa-raiz: o lancamento de PIX recebido nasce sem o pagador

Data: 2026-07-26. Ambiente medido: PRODUCAO (`monetarie-greenfield-prod`).
Chamado de origem: "comprovante de PIX recebido nao mostra os dados do PAGADOR,
so DADOS DO RECEBEDOR (VOCE)".

Este documento prova a cadeia inteira com dado real, compara o modelo do Core
com o do `coreproviders` (que usa CloudPix OnZ como provider, enquanto aqui a
origem e a nossa propria cabine PIX via NATS, com a pacs.008 do BACEN), e
propoe as solucoes definitivas.

---

## 1. A cadeia do dado, elo por elo

O nome de quem pagou existe e esta correto em TODOS os elos, ate o ultimo.

| elo | fonte | cobertura medida |
|---|---|---|
| BACEN | `<Dbtr><Nm>` da pacs.008 em `icom_received` | presente em todas as amostras |
| cabine | `monetarie_spi.payments.debtor_name` | **8.812 de 8.812** |
| Core, transacao | `transactions.metadata.payer_name` | **37 de 37** |
| Core, lancamento | `account_entries.metadata.counterparty_name` | **5 de 34** |

Exemplo do fio (PIX-in de 25/07 16:57, ISPB 10573521):

```
<Dbtr><Nm>Luiz Marcelo Goncalves da Costa Penha</Nm>
  <Id><PrvtId><Othr><Id>32189410835</Id></Othr></PrvtId></Id></Dbtr>
```

O dado nunca se perde no BACEN nem na cabine. Ele se perde na escrita do
lancamento do Core.

## 2. Os 5 que "funcionaram" nao funcionaram

A impressao digital das chaves do metadata separa os grupos:

| grupo | chaves presentes | n | periodo |
|---|---|---|---|
| com nome | `...,counterparty_name,`**`counterparty_repair`**`,...` | 3 | 13 a 14/07 |
| com nome | **`backfill`**`,counterparty_name,...` | 2 | 17/07 |
| sem nome | `debtor_ispb,end_to_end_id,source,transaction_id` | **29** | 17 a 25/07 |

As chaves `counterparty_repair` e `backfill` sao marcas de rotinas de
remediacao posteriores. **O escritor vivo nunca gravou o nome, nenhuma vez.**
Nao e regressao de 17/07: nunca funcionou. Os 29 sem nome sao a saida crua do
escritor.

## 3. A causa-raiz, provada com os bytes

O `pix_in_credit_wal` guarda o payload que originou o credito. O da operacao
acima:

```json
{"account_id": 3236, "amount": 10400,
 "creditor_ispb": "46026562", "debtor_ispb": "10573521",
 "direction": "INBOUND", "end_to_end_id": "E10573521202607251657aCcsj6zsQoR",
 "event_type": "CREATED", "message_id": "UEkBn5o2KsjVRFDPsAhME6SqnX5FMTug",
 "message_type": "pacs.008", "source": "pix-umbrella",
 "transaction_id": 16142, "type": "pix_transaction"}
```

**So ISPB. Sem `debtor_name`, sem `debtor_document`, sem `creditor_name`.**

O `transaction_id: 16142` e o `debtor_ispb: 10573521` batem exatamente com o
metadata do lancamento gravado. Ou seja: o lancamento nasce do evento
`CREATED`, que carrega apenas identificadores de instituicao, nunca as partes.

`StatementEntries.record_inbound_settled/1` faz
`counterparty = payload["debtor_name"] || payload["sender_name"]`, e o
`drop_nils` do build do metadata remove a chave quando o valor e nil. Por isso
`counterparty_name` nao aparece: nao existia no payload.

A `transactions.metadata.payer_name`, por outro lado, e preenchida a partir de
um evento POSTERIOR e enriquecido (o mesmo que alimenta o webhook
`pix.received`, que carrega `debtor_name`, `debtor_cpf_cnpj`, `debtor_branch` e
`debtor_account`). Duas linhas do mesmo evento de negocio, alimentadas por dois
payloads de fidelidade diferente.

## 4. Como isso vira tela em branco

1. O extrato (`V2.AccountController.transactions`) deduplica transacao contra
   lancamento MANTENDO o lancamento e descartando a transacao espelhada.
2. Logo o `id` da linha clicada e o UUID do lancamento, e e ele que vai em
   `router.push('/receipts/' + tx.id)`.
3. O comprovante cai no ramo `find_account_entry` e le so `entry.metadata`.
4. `Receipts.Payload.metadata_party(metadata, "payer")` procura `payer_name` ou
   `sender_name`, nao acha nada, devolve nil.
5. `payer: nil` e a secao inteira do pagador some. O recebedor vem do cadastro
   da conta, entao sobra so "DADOS DO RECEBEDOR (VOCE)".

## 5. Um segundo defeito da mesma familia: vocabulario

O escritor grava `counterparty_name`. O leitor procura `payer_name` e
`recipient_name`. Sao nomes diferentes para a mesma coisa.

Consequencia comprovada em teste: no PIX **enviado**, o lancamento **tem** o
nome do recebedor gravado (`counterparty_name`) e o comprovante **ainda assim**
nao conseguia mostra-lo. Nao e falta de dado, e falta de contrato.

## 6. Comparacao com o `coreproviders` (CloudPix OnZ)

A diferenca nao esta no provider, esta no modelo de dados.

| aspecto | coreproviders | Core Monetarie |
|---|---|---|
| linhas por credito recebido | **uma** (`transactions`) | **duas** (`transactions` + `account_entries`) |
| origem do extrato | transferencias do **TigerBeetle**, com join opcional em `transactions` para enriquecer (`Statements.Queries`) | uniao de **tres** fontes (`internal_query`, `pt_query`, `entry_query`) |
| reconciliacao entre fontes | nao existe, nao ha o que reconciliar | dedup por casamento de string em `num_ctrl_str`, `end_to_end_id`, `transaction_id` |
| origem do comprovante | **sempre** a `transactions` (`Receipts.Builder.build/2` faz `find_transaction(id)` e ponto) | dois ramos: transacao **ou** lancamento |
| leitura do pagador | `meta["payer_name"] \|\| meta["debtor_name"]` (aceita os dois vocabularios) | so `payer_name`/`sender_name` |
| `Statements.Entry` | struct **em memoria**, projecao | tabela materializada |

O ponto central: no `coreproviders` o razao (TigerBeetle) e a fonte do
movimento e a `transactions` e a fonte do detalhe de negocio. O extrato e um
**join**, nao uma terceira copia. Nao existe um segundo escritor capaz de
gravar uma versao mais pobre do mesmo fato, entao esta classe de bug nao tem
onde nascer.

No Core, `account_entries` e uma terceira materializacao alimentada por um
evento mais pobre. Toda vez que um consumidor novo le a linha errada, o bug
reaparece. Ja aconteceu tres vezes, todas registradas em comentario no proprio
codigo: PIX duplicado no extrato (o dedup nao casava por e2e), TED duplicada e
com lancamento sem nome, e agora o comprovante sem pagador.

## 7. Solucoes definitivas

### S1 (na origem): JA ESCRITA E JA DEPLOYADA, mas NUNCA EXERCITADA

O evento `CREATED` publicado pela cabine levava so ISPB. A cabine **ja tinha**
o nome e o documento nesse instante (a linha `payments` cobre 8.812/8.812).

Isso ja foi corrigido: commit **`9e9acccc`** ("Extrato: o nome de quem pagou
volta a viajar da cabine para o Core", 25/07 14:05 BRT) acrescentou
`parties = payment_parties(tx)` em `publish_transaction_event/4`, e o
comentario do proprio commit descreve exatamente este defeito.

Estado verificado:

| fato | valor |
|---|---|
| commit | `9e9acccc`, 25/07 14:05 BRT |
| esta na imagem em PRD? | **sim** (`prod-9ba42932-ab03-20260726`, `9e9acccc` e ancestral) |
| task definition | `monetarie-pix-api-prod:90`, registrada **26/07 02:08 BRT** |
| PIX-in recebidos depois do deploy | **ZERO** |

O ultimo PIX-in de PRD foi em 25/07 16:57 UTC, ou seja, **antes do proprio
commit existir**. Logo a correcao nunca rodou em producao: nao ha uma unica
operacao que a exercite.

Acao: **nao reimplementar**. O que falta e confirmar no primeiro PIX-in real
que chegar, conferindo que o payload no `pix_in_credit_wal` passa a trazer
`debtor_name` e `debtor_document`, e que o `account_entries.metadata` nasce com
`counterparty_name`. Enquanto isso nao acontecer, S1 esta NAO VERIFICADA.

### S2 (contrato): vocabulario unico entre escritor e leitor

Hoje o escritor fala `counterparty_*` e o leitor procura `payer_*`/`recipient_*`.
Adotar a defesa do `coreproviders`: `metadata_party/2` aceita `counterparty_*`
como alias do lado da contraparte, e o escritor passa a gravar tambem o par
canonico (`payer_*` no credito, `recipient_*` no debito).

Risco: baixo, e leitura mais permissiva. So Core.

### S3 (leitura, JA ENTREGUE): recuperacao pela transacao espelhada

Implementado no commit `233125c0`. Quando o lancamento nao tem contraparte, o
comprovante a recupera da transacao espelhada por `end_to_end_id` (reserva:
`transaction_id`), com allowlist explicita de chaves de parte para nao arrastar
flags de liquidacao que decidiriam o status.

Cobertura verificada contra o PRD: **34 de 34** PIX-in resolvem o pagador.
Vale tambem para o PIX enviado. Isso resolve os 29 historicos sem backfill e
segue como rede de seguranca depois de S1 e S2.

### S4 (estrutural): parar de materializar uma terceira copia

Duas opcoes, em ordem de preferencia:

1. Seguir o `coreproviders`: extrato projetado do razao com join de
   enriquecimento, e comprovante **sempre** pela `transactions`. O
   `account_entries` fica restrito ao que ele existe para servir (COSIF,
   contabilidade, ETL legado), e some do caminho de leitura do cliente.
2. Se `account_entries` precisar continuar no caminho do cliente, dar a ele uma
   **FK real** para `transactions` em vez de casamento de string, e tratar a
   linha como projecao derivada (nunca fonte primaria de detalhe de negocio).

Risco: medio a alto, mexe no extrato. Exige desenho proprio e nao deve ser
feito no mesmo pacote de S1 a S3.

### S5 (guarda): o defeito nao pode voltar em silencio

1. Teste de contrato: nenhum lancamento de evento de dinheiro pode existir sem
   a contraparte quando a transacao espelhada a tem. Falha a suite, nao a tela.
2. Alarme operacional sobre a taxa de lancamentos de PIX-in sem contraparte.
   Os 29 casos passaram semanas sem ninguem ver porque nada media isso.

## 8. Achado colateral, relevante para o AB03 em aberto

Durante a medicao apareceu uma inconsistencia do Santander (ISPB 90400888) que
interessa a frente do AB03. Para a **mesma conta** do dono:

- o DICT devolve `AccountNumber = 00000000000010303760` e `Branch = 0079`;
- a pacs.008 que o **proprio Santander origina** (PIX-in de 25/07 16:02) traz
  `<DbtrAcct><Id><Othr><Id>10303760</Id><Issr>79</Issr>`.

Ou seja, a mesma conta tem duas representacoes: preenchida com zeros a esquerda
no cadastro do DICT, e sem preenchimento na mensagem que eles mesmos emitem.
Nos copiamos o DICT literalmente, que e o correto pela especificacao. Isso
reforca o chamado a ser aberto com o Santander e sugere um teste concreto:
pagar a mesma conta por dados manuais, com a conta na forma nao preenchida.

## 9. O que continua sem prova

Uma coisa so, e ela e verificavel na proxima operacao real:

**S1 nunca rodou.** A correcao da origem esta escrita (`9e9acccc`) e esta na
imagem que roda em PRD desde 26/07 02:08 BRT, mas nenhum PIX-in chegou depois
disso. Nao afirmo que funciona ate ver o primeiro payload com `debtor_name` no
`pix_in_credit_wal` e o primeiro lancamento nascendo com `counterparty_name`.

Consequencia pratica: os 29 lancamentos historicos continuam sem contraparte no
banco. Quem os conserta hoje e S3 (recuperacao na leitura, ja entregue), e por
isso S3 deve permanecer no codigo mesmo depois de S1 confirmada: ela e o que
mantem o comprovante correto para todo o acervo anterior a 26/07.
