# Handoff 2026-07-24 (noite): canal da resposta AB03, Purp/Cd, port PIX-in, E2E divergente e chave do QR

Sessao longa. Quatro defeitos de money-path PROVADOS em producao, dois ja
corrigidos e deployados, um corrigido e aguardando deploy, e dois mapeados com
desenho fechado mas nao implementados. Um trabalho em andamento ficou no working
tree, NAO commitado.

Regra que guiou tudo: **BACEN e a verdade, ache o NOSSO defeito e PROVE, zero
inferencia**. Onde eu inferi, o dono corrigiu, e as retratacoes estao aqui.

---

## 1. Estado do repositorio e dos ambientes

- `main` == `origin/main` == **`f498674b`** (inclui os meus 4 commits + o merge do
  codex `05084ca0`).
- Branch `deepaudit/paridade-legadopix-2026-07-23` == mesmo commit (equalizada).
- **Working tree tem 1 arquivo modificado NAO commitado**:
  `core/backend/lib/monetarie/use_cases/pix/qr_generation.ex` (secao 6).

| servico | HML | PRD | contem |
|---|---|---|---|
| pix-api | td **210** | td **81** | fix do canal da resposta + guard do Purp/Cd (digest `sha256:7324b32e`, guard MATCH) |
| core-api | nao deployado | **nao deployado** | pendente: fix do purpose, port PIX-in (flag OFF), webhook do time, merge do codex |

**pix-api esta pronto para novo deploy** (suites verdes) mas o merge do codex trouxe
a migration `20260724190000_create_dict_nats_request_deduplications` (em
`pix/backend/apps/shared/priv/repo/migrations/`). **Rodar a migration ANTES** do
deploy, senao o `DictService.Nats.RequestDedup` quebra em runtime.

**core-api NAO pode ser deployado ainda**: suite vermelha (secao 6).

---

## 2. Defeito 1 (RESOLVIDO E DEPLOYADO): AB03 era defeito NOSSO, canal da resposta

**Retrata a conclusao de 24/07 de manha que dizia "AB03 = abort do BACEN, nao e
defeito nosso, PROVADO". Estava errada.**

O BACEN escreveu o motivo por extenso na `admi.002`:

```
<RjctgPtyRsn>Erro no processamento do SPI</RjctgPtyRsn>
<RsnDesc>PACS.002 recebida em canal diferente da PACS.008 original</RsnDesc>
```

Nossa pacs.002 de resposta saia pelo canal SECUNDARIO enquanto a pacs.008 chegou
pelo PRIMARIO. Sem aceite valido o SPI aborta o clearing ~40s depois (RJCT AB03).

**Prova, 7 de 7, correspondencia 1:1, com log nosso <2s antes de cada rejeicao:**

| # | log em `/ecs/monetarie/prod/pix-api` | admi.002 | AB03 |
|---|---|---|---|
| 1 | 22/07 17:34:05 `Failover: retrying /api/v1/in/... on secondary channel` | 17:34:06 | 17:34:41 |
| 2 | 22/07 17:37:11 idem | 17:37:13 | 17:37:48 |
| 3 | 24/07 11:29:04 `[ChannelRouter] primary circuit OPEN, routing pacs.002 to secondary` | 11:29:05 | 11:29:41 |
| 4 | 24/07 13:23:36 idem | 13:23:37 | 13:24:17 |
| 5 | 24/07 13:26:09 idem | 13:26:10 | 13:26:49 |
| 6 | 24/07 16:05:16 idem | 16:05:17 | 16:05:56 |
| 7 | 24/07 16:27:34 idem | 16:27:35 | 16:28:14 |

Zero AB03 sem admi.002; zero admi.002 sem AB03; nenhum PIX-in com ACSP aceito
tomou AB03. `icom_received` nas 72h: **7543 de 7543 chegaram por CPM, zero CSM**.

**Impacto real:** os 7 PIX-in eram legitimos. Sem o nosso aceite o dinheiro voltou
ao pagador: **7 clientes nao receberam PIX**. Os 7 creditos fantasma de R$10.913 no
Core foram o efeito colateral, nao a doenca.

**Fix (commit `b73c9de3`, deployado):** `Client.request_channel/2` (precedencia
`:channel` > `:reply_channel` > roteamento por tipo) + `Client.failover_allowed?/1`
(resposta NUNCA troca de canal no retry) + `SpiService.Icom.OriginChannel` (le o
canal de chegada de `monetarie_spi.icom_received.canal` por `message_id`, que tem
unique_index) + `InboundProcessor.pacs002_outbound_msg/4` leva o canal no outbox +
`OutboundSender.send_opts/2` repassa. 18 testes, 4 ciclos RED->GREEN.

---

## 3. Defeito 2 (RESOLVIDO E DEPLOYADO): Purp/Cd bloqueando PIX de saida

**Regressao MINHA, introduzida em `71dc970b` (23/07, defeito #5 do deep audit).**

O Core publica `payment_request` com `"purpose": "pagamento"` (TEXTO LIVRE). O
commit passou a repassar isso ao builder como `purpose_code`, e o builder escrevia
cru em `<Purp><Cd>`. O XSD so aceita `GSCB|IPAY|IPRT|OTHR|REFU`, entao o nosso gate
fail-CLOSED barrava o envio: **8 PIX de saida do fluxo automatico nao sairam**
(20:36 a 21:26 UTC de 24/07).

**Por que so a partir das 17:36 BRT:** nao foi o tempo, foi o CAMINHO. Ate 17:03 os
PIX sairam pelo caminho MANUAL (MessageController), que nao passa por
`payment_request_initiation_params` — sairam com IPAY e liquidaram.

**Por que 22/07 funcionava com o mesmo "pagamento":** antes de `71dc970b` o builder
nao recebia purpose e caia no default. Prova: **73 de 73** pacs.008 enviadas desde
20/07 tem `<Purp><Cd>IPAY</Cd>`.

**Por que a suite nao pegou:** o teste existente
(`core_event_processor_initiation_contract_test.exs:112`) usa
`%{"purpose" => "OTHR"}`, um codigo VALIDO. Producao manda texto livre.

**Fix (commits `47e09fcd` cabine deployado + `3e776aa8` Core NAO deployado):**
whitelist do `ExternalPurpose1Code` no builder (fora do dominio -> IPAY + log alto)
e normalizacao na fronteira `build_pix_params/1` do Core.

---

## 4. Defeito 3 (MAPEADO, NAO IMPLEMENTADO): E2E divergente Core x cabine

Ver [[monetarie-e2e-divergencia-core-cabine-0724]] na memoria.

O Core reserva o dinheiro em `outbound_requests` com um E2E e a cabine paga com
OUTRO: **7 de 20 em 24/07 (35%)**, **15 de 54 em 22/07 (28%)**. Toda divergencia e
pagamento REPETIDO para a MESMA chave em sequencia rapida.

**Causa, 3 pontos:**
1. `core_event_processor.ex:434 resolve_e2e_id/3` — havendo `pix_key`, a cabine usa
   o E2E da consulta DICT em cache e **ignora** o do Core (do lado BACEN isso esta
   CERTO: rastreabilidade consulta -> pagamento).
2. `monetarie_spi.dict_e2e_reservations` e chaveada por **pix_key** = 1 linha por
   chave. Pagamentos sucessivos para a mesma chave DISPUTAM a reserva.
3. `core/.../outbound/pix.ex:395` — usa `pix.end_to_end_id` quando vem, senao
   **GERA o proprio**.

**Impacto:** `terminalize_outbound_request` casa a reserva por E2E; divergindo, a
reserva fica orfa e so sai no TTL de 2h do `PixOutRetryWorker` (confirmado: as 2 de
ontem drenaram sozinhas no TTL). Ocorre tambem em operacoes LIQUIDADAS, entao ha
dinheiro que saiu com E2E diferente do registrado no Core.

**DESENHO DEFINIDO PELO DONO (implementar assim):**
- **DICT:** o Core **nao gera** E2E. Chama o DICT da nossa cabine; a cabine gera e
  reserva e devolve via NATS (isso JA EXISTE: `dict_lookup_responder.ex:200,204,256`
  `found_reply` devolve `end_to_end_id`). O Core monta a pacs.008 com esse E2E; a
  cabine **VALIDA** se e o mesmo reservado e so entao sensibiliza, contabilizando a
  devolucao do token do orcamento DICT e o tipo de iniciacao DICT. Divergencia =
  rejeicao explicita, nunca troca silenciosa.
- **MANU:** o **Core** gera o E2E e ele e **persistido na cabine**, para a devolucao
  casar.
- **Reserva por OPERACAO**, nao por chave.

Ordem: validacao na cabine + fail-closed no Core primeiro (rede de seguranca, nao
mexe na regra do BACEN), reserva por operacao depois.

---

## 5. Defeito 4 (PARCIAL): QR gerado com chave que NAO e nossa

Ver [[monetarie-qrcode-chave-de-outra-instituicao-0724]] na memoria.

`qrcodes` do Core tinha **2 linhas no total** e nas **2** a `pix_key` era o
documento do titular, nao uma chave registrada da conta. **Nenhuma das duas esta em
`monetarie_dict.keys`** (nosso diretorio, 306 chaves ativas, todas ISPB 46026562).

A chave `09918358912` e respondida pelo DICT como **ISPB 60746948** (16 pagamentos
nossos como prova). O QR da conta 2031 pagava a outra instituicao, com
`qrcode.monetarie.com` na cara do pagador.

**RESOLUCAO DEFINITIVA DA PERGUNTA "por que o IB deixou passar":**
`resolve_active_qr_key` (a validacao de posse) nasceu **so** no commit `05084ca0`,
de **24/07 19:36:34**. Os 2 QR sao de ANTES (21/07 e 44 minutos antes do commit).
**Nao houve contorno: a validacao nao existia.** E como o **Core nao foi
deployado**, PRODUCAO AINDA RODA O CODIGO ANTIGO — **o buraco esta aberto agora**, e
o que o fecha e o deploy do Core.

**EXECUTADO (ordem do dono): os 2 QR REVOGADOS nos dois lados.** Cabine
`monetarie_settlement.qr_codes` -> `status='cancelled'`, `is_active=false`,
`cancelled_at`, `cancellation_reason`. Espelho Core `qrcodes` -> `status='cancelled'`.
2 linhas em cada, antes/depois conferidos.

**Chaves CORRETAS (vinculacao trivial: `account_number` do DICT == `account_id` do QR):**

| conta | titular | chave nossa (EVP, ACTIVE, ISPB 46026562) |
|---|---|---|
| 2031 | GABRIEL CARDOSO (09918358912) | `31eaebe4-ecab-4e14-a25e-2458f951a705`, `7d98c4c8-2c0b-4c42-84d7-6c91ad97a503` |
| 3236 | Luiz Marcelo Goncalves da Costa Penha (32189410835) | `82d58d65-3acf-489c-a306-0213ca048b85` |

**PADRAO DE OURO = coreproviders**, comentario literal em
`backend/lib/fluxiq_web/controllers/merchant/pix/generate_qrcode_controller.ex`:
> "If frontend sends pix_key param, validates it belongs to the account in pix_keys.
> If not sent, picks the first active key for the account. **NEVER falls back to
> entity CNPJ — that key may belong to another institution.**"

**Achado colateral:** o espelho do Core marcava `active` enquanto a cabine ja tinha
`expired`. O espelho NAO recebe a expiracao. Defeito por si, ainda nao tratado.

---

## 6. QR: regra portada e FECHADA (commit `80271cfa`, ja na main)

**CONCLUIDO.** O WIP foi finalizado, testado e commitado. Working tree LIMPO.

**O que foi feito:** portei a regra do coreproviders para `base_attrs/2`. Extrai
`resolve_pix_key/2` e `first_active_account_key/2`: chave enviada e usada; sem chave,
busca a **primeira chave ATIVA da conta** via `key_provider().list_keys(entity_id,
%{"account" => account.id})`; fail-CLOSED se o diretorio nao responder
(`:pix_key_lookup_failed`), nunca inventa chave, nunca usa documento. Provider
sobreponivel por `Application.get_env(:monetarie, :qr_key_provider, ...)` (seam de teste).

**Testes: 25/25 VERDES** (`qr_generation_test` + `merchant_portal_controller_test`).
3 casos novos: usa a primeira chave ATIVA da conta; chave INATIVA nao serve; sem
diretorio RECUSA e NAO usa o documento. Stub do diretorio nos dois arquivos via o
seam `:qr_key_provider`.

**ATENCAO — 2 falhas PRE-EXISTENTES na suite completa do Core**, em
`test/monetarie/use_cases/ccs/accs001_generation_test.exs`: dependem de DATA
(esperam prefixo `20260725`, recebem `20260727`). **Provado por A/B: falham igual
SEM este commit**, nao tem relacao com QR. Precisam de correcao propria antes do
deploy do Core, ou de confirmacao do dono de que sao aceitaveis.

**Regressao ja provada dessa area (A/B):** restaurando so o `qr_generation.ex` de
`ef97566e` a suite do merchant volta 18/0; com a versao do merge, 18/1. Ou seja o
codex acertou em matar o fallback ao documento e faltou a resolucao automatica —
que e exatamente o que este WIP entrega.

---

## 7. Port PIX-in fail-CLOSED (metade 1 FEITA, commit `ef97566e`, flag OFF)

Ver [[monetarie-pix-in-portar-coreproviders-concluida-0724]].

`created` -> WAL `acsp_sent` sem credito; `settled` -> credita **do payload do WAL**;
`rejected` -> WAL `rejected` terminal. Flag `:pix_in_fail_closed_enabled` **default
OFF** (com OFF o comportamento e identico ao atual, tem teste).

**Medicao que justifica (PRD, 30 PIX-in desde 13/07):** confirmacao do BACEN
(pacs.002 ACCC) chega em **0,4s a 3,6s** nos 17 liquidados; os 7 rejeitados vem em
~40s. Custo da espera ~2,5s, ganho = zero fantasma.

**FALTA A METADE 2 (nao ligar a flag em PRD sem ela):** recovery worker por PULL da
camt.060 via `SpiService.OperationQuery`. Precisa de subject NATS novo
(`monetarie.pix.operation.query`) no `CoreGatewayHandler`, que hoje serve
`monetarie.pix.*`. E o que cobre os 2 PIX-in que nunca receberam confirmacao
(14752 R$42.663 e 14521 R$50).

---

## 8. Fila da proxima sessao

1. **Fechar o WIP do QR** (secao 6): alinhar os 2 testes, suite do Core verde.
2. **Deploy do Core** -> fecha o buraco do QR em producao e leva o fix do `purpose`,
   o port desligado e o webhook do time.
3. **Deploy do pix-api** com a **migration antes**.
4. **Validar em PRD com transacao real**: (a) PIX de saida pelo fluxo automatico sai
   com `<Purp><Cd>IPAY</Cd>` e liquida; (b) refazer a correlacao `admi.002` "canal
   diferente" x AB03 — com o fix os dois tem de ir a ZERO (hoje foram 7 e 7).
5. **QR**: validar o fluxo do partner API (que ja funciona e vem da cabine), refletir
   no ib-front e no merchant-front, conferir `api.monetarie.com`.
6. **Pendencias do cliente em HML**: persistir `txid` do QR e o tipo de iniciacao do
   QR no PIX-out.
7. **E2E divergente** (secao 4), com o desenho ja fechado pelo dono.
8. **Metade 2 do port PIX-in** (secao 7).
9. Menor: espelho `qrcodes` do Core nao recebe expiracao da cabine.

---

## 9. Retratacoes desta sessao (para nao repetir)

1. "AB03 = abort do BACEN, nao e defeito nosso" -> **FALSO**, e nosso (secao 2).
2. "Dois creditos fantasma NOVOS hoje" -> **FALSO**, eram parte dos 7 ja estornados.
3. "Os 2 QR sairam do merchant" -> **FALSO e sem base**, inferi do caminho de codigo.
   `qrcodes` nao tem coluna de origem nem audit de criacao. Foram feitos no **IB**.
4. "Os 2 QR estao `active`" -> vinha do espelho do Core; na **cabine** estavam
   `expired`.
5. Rodei o `StuckOutboundChecker` em PRD afirmando "nada e mutado" **sem verificar a
   flag** — `stuck_outbound_auto_resolve_enabled` estava `true`. Conferi depois e de
   fato nada mutou (o worker cai em quarentena sem evidencia), mas a afirmacao foi
   irresponsavel.

## 10. Ferramentas

- Helper de RPC read/write em PRD e HML:
  `scratchpad/rpc.sh <cluster> <servico> <container> "<bin> rpc" <arquivo.exs>`
  (codigo por base64, via ECS Exec). Clusters: `monetarie-greenfield-prod` /
  `monetarie-greenfield-homolog`. Bins: `bin/monetarie rpc` (core),
  `bin/monetarie_pix rpc` (cabine).
- Logs: `/ecs/monetarie/prod/pix-api`, `/ecs/monetarie/prod/core-api` (note o `prod/`
  no meio, nao `/ecs/monetarie/pix-api`).
