# Matriz de iniciação do PIX-out: front (IB + merchant) → Core → cabine PIX

Data: 2026-07-18. Frente de verificação do mandato do dono: "o front tem que
fazer distinção completa dos tipos de iniciação do PIX OUT sendo MANU para
dados por agência e conta, DICT para chaves e QRDN ou QRES para qrcode, de
forma que todos eles sejam populados corretamente na integração com a nossa
cabine PIX sem nenhuma margem para erro".

Commits desta frente (locais, worktree, SEM push):

- `5d8034b5` fix(core): fronteira de iniciação do funil (Outbound.Pix)
- `28b56f9d` fix(core): trilho V2 send_pix_external com a mesma disciplina
- `32631665` fix(ib): trilhos MANU e QRES/QRDN
- `3420ef4b` fix(merchant): trilhos MANU e QRES/QRDN
- `dbf185d4` test(pix): contrato de iniciação caracterizado na cabine (regra INALTERADA)

## 1. A autoridade (cabine) — regra NÃO alterada (item C de 17/07)

Elo final que decide a `formaDeIniciacao` (LclInstrm/Prtry) da pacs.008:

| Peça | Arquivo:linha | Regra |
|---|---|---|
| Extração da chave | `pix/backend/apps/settlement_service/lib/settlement_service/workers/core_event_processor.ex:233` | `pix_key = message["pix_key"] \|\| message["creditor_proxy"]` — presença de chave = pagamento POR CHAVE |
| E2E por chave | `core_event_processor.ex:432-449` | chave presente ⇒ E2E OBRIGATORIAMENTE da consulta DICT cacheada (`Shared.E2eCache.get_cached`); ausente ⇒ rejeita `DICT_CONSULT_REQUIRED` (`:269-291`). Sem chave ⇒ usa `end_to_end_id` do Core (MANU nunca reusa E2E de consulta — fix 15/07 preservado) |
| LocalInstrument | `core_event_processor.ex:452-457` | honra `message["local_instrument"]` ∈ {DICT, MANU, QRES, QRDN, INIC}; na ausência infere DICT se há chave, MANU se não |
| Validação semântica | `pix/backend/apps/settlement_service/lib/settlement_service/spi/pacs008_send_validator.ex` (chamado em `core_event_processor.ex:654`) | MANU: sem chave e sem TxId; DICT: chave, sem TxId; QRES: chave, TxId ≤ 25 opcional; QRDN: chave + TxId 26-35 |
| Recebedor da pacs.008 | `core_event_processor.ex:485-524` (`creditor_params/2`) | consulta DICT cacheada primeiro; fallback = chaves `creditor_name/document/branch/account/account_type` do payload do Core |

Caracterização nova (sem mudar regra): `pix/backend/apps/settlement_service/test/settlement_service/workers/core_event_processor_initiation_contract_test.exs` (8/0).

## 2. O funil do Core

| Elo | Arquivo:linha |
|---|---|
| Rota IB/merchant | `POST /api/v2/accounts/:account_id/pix/send` → `core/backend/lib/monetarie_web/router.ex:3302` |
| Controller | `V2.TransferController.create_pix_account` — `core/backend/lib/monetarie_web/controllers/v2/transfer_controller.ex:338-390`; whitelist `manual\|dict\|qr_static\|qr_dynamic` em `:704-706`; `sanitize_tx_id` (≤35) em `:708-713` |
| Params tipados | `Monetarie.UseCases.Payments.OutboundPaymentParams.Pix` (`outbound_payment_params.ex:27-67`): `pix_key`, `initiation_type`, `tx_id`, `recipient_*` |
| Normalização | `Outbound.Pix.validate_and_normalize/3` → `prepare_request/1` (metadata `initiation_type`/`tx_id`/`recipient_*`) — `core/backend/lib/monetarie/use_cases/payments/outbound/pix.ex:44-128` |
| **Fronteira (CORRIGIDA `5d8034b5`)** | `Outbound.Pix.build_pix_params/1` (`outbound/pix.ex`): mapeia `initiation_type` → `local_instrument` (manual→MANU, qr_static→QRES, qr_dynamic→QRDN, dict/ausente→DICT); **MANU nunca leva `pix_key`/`creditor_proxy` nem `tx_id`; `creditor_name/document/branch/account/account_type` fluem sempre** |
| Evento | `Provider.send_pix` → NATS `monetarie.core.pix.payment_request` → `CoreEventProcessor.handle_payment_request` |

Trilho secundário (mesma rota de negócio, outro endpoint): `POST /api/v2/merchants/:id/pix/send` → `V2.PixController.send_pix` → `send_pix_external` → **`build_cabin_payment_payload/2` (CORRIGIDO `28b56f9d`)** — antes descartava `txId` (QRDN morreria no validator DEPOIS do hold; QRES perdia a conciliação) e não levava `creditor_branch`. Nenhum front chama esse endpoint hoje (ambos usam `/accounts/:id/pix/send`), mas a rota é viva para clientes de API.

## 3. Matriz trilho a trilho

Unidades: fronts enviam CENTAVOS; controller converte para base_units (`MoneyUnit.from_cents`); cabine recebe reais decimais pelo MoneyBoundary (inalterado nesta frente).

| # | Trilho (front) | Entrada | initiationType no fio | local_instrument na cabine | E2E | tx_id | Recebedor da pacs.008 | Status |
|---|---|---|---|---|---|---|---|---|
| 1 | IB chave digitada | `PixSendView.vue` → `dictLookup` (GET `/pix/dict`, cacheia E2E na cabine) → sessão (`endToEndId`/`reservationId`) → `PixSendConfirmView.vue` (omite initiationType) | ausente → default `dict` | **DICT** (honrado) | da consulta DICT (cabine consome o cache; regra do balde preservada) | nil | consulta DICT cacheada | **OK** (já correto) |
| 2 | IB manual agência+conta | `PixManualView.vue` → sessão estruturada → confirm `initiationType=manual`, `pixKey=""` | `manual` | **MANU** (honrado; sem proxy) | fresco do Core (`Outbound.Pix.generate_e2e`, 32 chars) — MANU nunca reusa consulta | nil (fronteira zera) | `creditor_*` do payload (agência, conta dígitos, documento CRU, nome digitado, ISPB do banco selecionado) | **CORRIGIDO** `32631665` + `5d8034b5` |
| 3 | IB copia-e-cola QR estático | `PixCopyPasteView.vue` → POST `/pix/qrcode/parse` (`qr_type=qr_static` por PoIM 11, `v2/pix_controller.ex` parse_qrcode) → **dictLookup no continuar** → confirm `initiationType=qr_static` + `txId` (≤25) | `qr_static` | **QRES** (honrado) | da consulta DICT feita no continuar | txid do EMV (tag 62-05), flui até a pacs.008 | consulta DICT cacheada | **CORRIGIDO** `32631665` |
| 4 | IB copia-e-cola QR dinâmico (EMV com chave) | idem, `qr_type=qr_dynamic` por PoIM 12 | `qr_dynamic` | **QRDN** (honrado) | idem | txid 26-35 OBRIGATÓRIO (validator) — agora flui | consulta DICT cacheada | **CORRIGIDO** `32631665` |
| 5 | IB favoritos | prefill do `PixSendView` (trilho 1) | ausente | DICT | consulta | nil | consulta | **OK** |
| 6 | Merchant chave digitada | `merchant PixSendView.vue` (dictLookup + sessão com recipient completos) → confirm | ausente | **DICT** | consulta | nil | consulta (+ agora recipient* também fluem no payload como fallback) | **OK** (payload enriquecido em `3420ef4b`) |
| 7 | Merchant manual agência+conta | `merchant PixManualView.vue` → confirm | `manual` | **MANU** | fresco do Core | nil | `creditor_*` do payload | **CORRIGIDO** `3420ef4b` + `5d8034b5` |
| 8 | Merchant copia-e-cola (QRES/QRDN) | `merchant PixCopyPasteView.vue` → parse REAL + dictLookup no continuar → confirm | `qr_static`/`qr_dynamic` | **QRES/QRDN** | consulta do continuar | txid do EMV | consulta DICT | **CORRIGIDO** `3420ef4b` |
| 9 | Partner API (`POST /pix/payments`, router.ex:169) | por chave; mesmo funil `OutboundOrchestrator` | ausente | DICT | consulta (partner faz o GET dict antes — validado vivo 16/07) | nil | consulta | **OK** (fora do escopo front; herda a fronteira nova) |
| 10 | IB PIX agendado (one-off) | `ScheduledPixController` → `UseCases.Pix.ScheduledPix.default_dispatch` | `initiationType` do request original (default dict) | DICT (inferido pela chave) | **LACUNA P1 (pré-existente, NÃO corrigida aqui)**: o executor NÃO consulta DICT antes de despachar (`scheduled_pix.ex:200-250`) — com chave presente a cabine exige E2E de consulta em cache e rejeita `DICT_CONSULT_REQUIRED` na data de execução (fail-closed, hold liberado no rollback) | n/a | payload próprio (leva creditor_*) | **PENDENTE** (ver §5) |

## 4. Defeitos provados e corrigidos (RED→GREEN)

1. **Fronteira do funil vazava chave no MANU e descartava o recebedor** (`Outbound.Pix.build_pix_params`): `pix_key`/`creditor_proxy` iam SEMPRE; a pseudo-chave "agência/conta" do front fazia a cabine tratar o manual como pagamento por chave → `DICT_CONSULT_REQUIRED` em 100% dos envios manuais; e `recipient_name/document/branch/account` ficavam para trás → pacs.008 MANU sairia "Destinatario"/"00000000" (classe AC03). Fix `5d8034b5`, teste `pix_initiation_boundary_test.exs` 7/0.
2. **Trilho V2 `send_pix_external` descartava `txId` e a agência** — QRDN morreria no `Pacs008SendValidator` DEPOIS do bloqueio de saldo; QRES perdia o id de conciliação; `creditor_branch` (Issr) ausente = classe real dos AC03/AB09 de 14/07. Fix `28b56f9d`, teste `pix_send_external_payload_test.exs` 4/0.
3. **IB manual**: pseudo-chave fabricada + `dictLookup` do CPF/CNPJ do favorecido como se fosse chave PIX (falso requisito: destinatário sem chave não podia receber manual; gastava ficha do balde DICT e cacheava E2E espúrio na cabine) + documento MASCARADO na sessão + agência/conta nunca estruturadas. Fix `32631665` (campo nome do favorecido novo, obrigatório), testes `PixManualView.test.ts` 2/0 + confirm 3 casos.
4. **IB copia-e-cola**: (a) o fio real é camelizado (plug `KeyCase`; cliente manda `X-Key-Case: camelCase`) e o parse lia só `key`/`pix_key` → a CHAVE do QR era perdida; (b) nenhuma consulta DICT antes de pagar → todo copia-e-cola morreria `DICT_CONSULT_REQUIRED`. Fix `32631665`, testes `PixCopyPasteView.test.ts` 3/0.
5. **Merchant manual**: "consulta" do favorecido era FIXTURE (`ownerName: 'Maria Silva Santos'` hardcoded — exatamente a classe proibida pela regra do dono de validação sem fixture) + os mesmos defeitos do IB + o payload do confirm não levava NENHUM dado do recebedor além do nome (nem `recipientIspb` → a cabine daria raise "payment_request sem creditor_ispb"). Fix `3420ef4b`.
6. **Merchant copia-e-cola**: chamava `POST /pix/qrcode/consult` → `Provider.consult_qrcode` → `dict.api.request action=consult_qrcode` que NÃO EXISTE no `DictApiResponder` da cabine (nenhuma cláusula `dispatch("consult_qrcode", _)` — `pix/backend/apps/dict_service/lib/dict_service/nats/dict_api_responder.ex:96-586`) → todo copia-e-cola do lojista morria como "QR inválido" na tela. Fix `3420ef4b` (parse real + dictLookup no continuar).

## 5. Lacunas mapeadas NÃO corrigidas nesta frente (follow-ups)

- **P1 — PIX agendado sem consulta DICT na execução** (`core/backend/lib/monetarie/use_cases/pix/scheduled_pix.ex:200-250`): o `default_dispatch` precisa consultar a chave (`Provider.lookup_key` com PI-PayerId do titular) imediatamente antes do despacho, para cachear o E2E na cabine. Hoje a execução agendada rejeita `DICT_CONSULT_REQUIRED` (fail-closed; hold liberado). Mexe em money-path do executor — fazer como incremento próprio com TDD.
- **P1 — lista de bancos do manual é hardcode de 6 instituições** (IB `PixManualView.vue:47-54`, merchant idem): impossível pagar manual para a maioria dos PSPs. O Core tem o diretório BACEN (`institution_directory`, 901 participantes) — falta endpoint/consumo no IB/merchant.
- **P2 — tipo de conta do manual fixo em CACC**: sem seletor corrente/poupança/pagamento; recebedor poupança pode rejeitar no PSP dele.
- **P2 — `reservation_id` morto no funil**: `OutboundPaymentParams.Pix.reservation_id` é carregado e não consumido (`outbound_payment_params.ex:36`); a reserva durável (E2eReservation) resolve pela chave na cabine. Limpar ou ligar.
- **P3 — favorito salvo do fluxo manual** grava `pix_key` com a pseudo-chave/vazio (`PixSendConfirmView.vue` saveFavorite); favoritos manuais mereceriam tipo próprio.
- **Observação**: `consult_qrcode` segue sem handler na cabine; o endpoint `/pix/qrcode/consult` do Core continua vivo e devolve erro do provider. Ou implementa-se a ação na cabine (parse+lookup em um passo) ou aposenta-se a rota.

## 6. Suítes rodadas (todas com `MIX_TEST_PARTITION=fx` no mix)

| Suíte | Resultado |
|---|---|
| core `test/monetarie/use_cases/payments/outbound/` (inclui boundary novo) | 48/0 |
| core `test/monetarie/use_cases/payments/` (dir completo) | 172/0 |
| core `test/monetarie_web/controllers/v2/` (inclui payload V2 novo) | 75/0 |
| cabine settlement: contrato iniciação novo + Pacs008SendValidator | 47/0 (8 novos) |
| cabine settlement: creditor_branch (regressão) | 3/0 |
| IB vitest (suíte completa) | 184/184 (8 novos) |
| merchant vitest (suíte completa) | 9/9 (5 novos; dep de teste nova `@vue/test-utils`) |

## 7. O que fica para a validação viva (janela do orquestrador)

Zero deploy/escrita em ambiente vivo nesta frente. Provas de tela/fio recomendadas em HML:

1. **MANU**: IB → PIX → Dados bancários → banco+agência+conta+documento+nome → enviar. Provar no banco da cabine (SELECT read-only em `monetarie_spi.messages`/payments): `json_input->>'local_instrument' = 'MANU'`, `json_input->>'pix_key'` NULO, XML da pacs.008 com `<LclInstrm><Prtry>MANU`, `CdtrAcct` = conta digitada e `<Issr>` = agência. E2E gerado pelo Core (não igual a nenhum E2E de consulta).
2. **DICT**: envio por chave (trilho já validado vivo em 16/07 pela Partner API); conferir que o E2E da pacs.008 == E2E devolvido no GET `/pix/dict` (balde).
3. **QRES**: gerar QR estático (Partner `POST /pix/qrcodes/static`), copia-e-cola no IB → conferir consulta DICT disparada no continuar (log/telemetria da cabine), pacs.008 com `QRES` e TxId do EMV.
4. **QRDN**: QR dinâmico com chave embutida (ou aceitar que o dinâmico por URL segue 422 `url_only_qr_unsupported` no copia-e-cola — comportamento por design); pacs.008 `QRDN` com TxId 26-35.
5. **Merchant**: repetir 2-4 no portal do lojista (o copia-e-cola do merchant estava 100% morto; primeira validação viva pós-fix).
6. **Regressão de rejeição**: manual para conta inexistente deve voltar rejeição do PSP recebedor (AC03) com hold devolvido — não mais `DICT_CONSULT_REQUIRED`.
