# PIX-out: 3 rejeições reais — defeito NOSSO na pacs.008 (agência ausente) — 2026-07-14

## Resumo

O dono afirmou que estávamos montando a pacs.008 errada e causando as rejeições
dos bancos destinatários. **A afirmação estava correta.** Três transações
manuais de 14/07 falharam; a causa raiz comum é a nossa pacs.008 sair **sem a
agência do recebedor** (`CdtrAcct/Id/Othr/Issr`), mesmo com o DICT tendo nos
entregue a agência na consulta. Dois defeitos acessórios: valor sem as 2 casas
decimais canônicas e o registro da forma de iniciação como MANU em pagamento
por chave.

## As 3 transações e a verdade do BACEN

| E2E (sufixo) | Valor | Destino | Desfecho BACEN | Como foi provado |
|---|---|---|---|---|
| `...1827ajlsslokwg5` | R$ 2,00 | BB (00000000), conta 226610 vinda do DICT | **RJCT AC03** (conta inexistente) | pacs.002 recebida |
| `...1911bz3e4su36au` | R$ 1,00 | Bradesco (60701190) | **RJCT AB09** (erro no PSP do recebedor) | pacs.002 recebida |
| `...19056h7txkot2yx` | R$ 2,00 | BB (00000000), mesma conta DICT | **RJCT AC03** — a pacs.002 NUNCA chegou até nós; ficou presa em ACSP | **camt.060 consulta de operação** (EvtId = E2E) → camt.054 com `Sts INFO` + `AddtlNtryInf AC03` + "Pagamento rejeitado pelo PSP do recebedor" |

O AC03 numa conta que o PRÓPRIO DICT do BB informou é a prova empírica: a única
diferença entre o que o DICT devolveu (agência **5087**, conta 226610) e o que
mandamos no fio é a agência ausente.

## Gabarito do fio (BACEN é a verdade)

pacs.008 REAL recebida do PSP 90400888 (R$ 20.000, liquidada pelo BACEN em
14/07): `<Issr>` presente **nos dois lados** —
`DbtrAcct .. <Othr><Id>130013453</Id><Issr>2307</Issr></Othr>` e
`CdtrAcct .. <Othr><Id>526012720</Id><Issr>1</Issr></Othr>`.

Nossas 3 pacs.008 enviadas (extraídas de `bacen_outbound` em produção):
`lcl=DICT` (o XML estava certo na forma de iniciação), `amt=2 / 1 / 2`
(inteiro cru) e **SEM_ISSR** nas três.

## Defeitos e correções (TDD, commit `191f40d8`)

1. **Agência (Issr) descartada em TODOS os caminhos.** O builder
   (`message_builder.ex pacs008_account/5`) já suportava `creditor_branch`,
   mas nem o caminho manual (`payment_controller`) nem o core-originado
   (`core_event_processor`) passavam o campo, e a tela não tinha o campo.
   - `CoreEventProcessor.creditor_params/2`: projeção única dos campos do
     recebedor (consulta DICT cacheada primeiro, payload do Core como
     fallback) agora com `creditor_branch` (aceita `branch` e `branch_code`).
   - `PaymentController.merge_consult_defaults/2`: o envio manual completa os
     campos do recebedor ausentes a partir de
     `Shared.E2eCache.peek_consult_entry/1` (a MESMA fonte do caminho core);
     o que a tela mandou explicitamente nunca é sobrescrito.
   - Tela Nova Transação: campos "Agência" (pagador e recebedor); o do
     recebedor é preenchido pela consulta DICT (readonly via chave).
2. **Valor sem casas decimais.** `fmt_amount` com Decimal/string inteiro
   emitia `"2"`. Agora BRL canônico sempre com 2 casas (`Decimal.round(2)`;
   strings passam por parse). Vale para pacs.008/004, camt e Tax.
3. **MANU em pagamento por chave (registro).** A tela mandava o default
   `instrument_type: "MANU"` e o backend deixava o explícito vencer.
   `normalize_initiation_form/2`: chave presente + MANU/vazio → DICT; QR
   explícito respeitado; DICT sem chave é impossível → MANU. A tela também
   troca para DICT ao resolver a chave.

Testes novos: `message_builder_pacs008_wire_defects_test.exs` (6),
`payment_controller_consult_backfill_test.exs` (9, inclui initiation form),
`core_event_processor_creditor_branch_test.exs` (3) — fixtures = dados REAIS
do incidente. Suíte PIX completa: **3.817 testes, 0 falhas**.

## Reparos executados em produção (sem apagar nada)

- `lokwg5`: reserva manual presa em stage 2 (crash P19 `Ecto.UUID.dump!(nil)`,
  fix já em `core-api:39`) finalizada via `move_to_failed` com AC03 —
  `failed_transactions` = 1, TB já estava voided.
- `su36au`: mesmo padrão; reparada pelo caminho REAL
  `AtomicPaymentHandler.void_and_fail/3` (`:ok`, TB idempotente) com AB09.
- `kot2yx`: cabine atualizada para RJCT com histórico citando a camt.054
  (`status_origin=CAMT054`, `return_data` com AC03 e o MsgId da consulta) e
  evento canônico `monetarie.spi.transaction.rejected` publicado via outbox →
  o Core processou pelo pipeline real (void + `failed_transactions` = 1).
- Aprendizado ICOM: pacs.028 é fictícia no SPI v5.12; o instrumento certo para
  status de pagamento com resposta perdida é **camt.060 consulta de operação**
  (`event_id` = E2E, `ReqdMsgNmId=camt.054`, SEM `RptgPrd`); com período dá
  admi.002, e camt.052 por período exige `balance_type=REL`.

## Deploys

| Serviço | HML | PROD | Rollback |
|---|---|---|---|
| pix-api | `monetarie-pix-api-homolog:158` (tag `homolog-191f40d8-pacs008fix-20260714`) | **`monetarie-pix-api-prod:39`** (mesmo digest, sidecar mTLS preservado) | `:38` |
| pix-admin-ui | `:44` | **`:20`** | `:19` |

Validação viva pós-deploy (rpc no container de PROD): builder emite
`<IntrBkSttlmAmt Ccy="BRL">2.00</IntrBkSttlmAmt>` e
`<CdtrAcct><Id><Othr><Id>226610</Id><Issr>5087</Issr></Othr></Id>...`, e
`normalize_initiation_form("MANU", chave) == "DICT"`.

**Validação definitiva** (regra do dono: em produção não existe teste): o
próximo PIX-out orgânico por chave para o BB deve liquidar; conferir o fio em
`bacen_outbound` (Issr presente) e a pacs.002 ACSP→ACSC.

## Também nesta frente (P18, mesmo deploy)

Detalhe da transação com motivo de rejeição COM código ISO (fonte única
`RejectCodes`, catálogo deriva dela) e `GET /transactions/:id/xml` com as
mensagens BACEN cruas correlacionadas; chip de rejeição + seção XML na tela.
