# Auditoria de fidelidade do simulador PIX vs BACEN real

Data: 2026-07-10. Escopo: cabine PIX (`pix/`), modo simulador (`SIMULATOR_ENABLED=true`, `BACEN_ENABLED=false`), comparado com o comportamento real do BACEN provado empiricamente em produção em 2026-07-09 e com o catálogo oficial v5.12.1 (XSDs em `pix/backend/apps/shared/priv/xsd/spi/v5.12.1/`, exemplos oficiais em `/Users/luizpenha/mwbank/md/v5.12.1/exemplos/`).

Método: leitura de código com evidência arquivo:linha, validação empírica dos XMLs gerados pelo simulador contra os XSDs oficiais com `xmllint` (saídas reais reproduzidas abaixo) e execução dos testes do simulador (somente eles). Nenhum código de produção foi modificado.

## 0. Atualização 2026-07-10 (noite): os 5 P0s foram CORRIGIDOS

Mandato do dono executado nesta data: os P0s da seção 6 foram corrigidos com TDD (testes escritos antes, fase vermelha registrada com 13 falhas), somente em código do simulador e testes — o caminho real de produção (InboundProcessor, RejectCodes, StatusUpdater, MessageBuilder) não foi tocado. As seções 1 a 7 abaixo são o retrato de ANTES e ficam como histórico; as anotações "CORRIGIDO" remetem a esta seção.

### 0.1 P0-1 CORRIGIDO — two-phase fora do fluxo normal

`ResponseGenerator.respond_to_pacs008` agora RECUSA fabricar pacs.002 para pacs.008 RECEBIDA (Fr != nosso ISPB): retorna `{:error, :pacs002_not_delivered_to_receiver}` (`response_generator.ex:181-193`). Isso neutraliza a fabricação via `/api/v1/simulator/process` e via `Simulator.process_message` — pacs.002 só existe para a pacs.008 que NÓS enviamos (Fr = nosso ISPB), como no BACEN real. A simulação de PIX-in entrega a pacs.008 como já liquidada (a entrega É a liquidação) e o crédito flui SÓ pelo caminho real (InboundProcessor -> `transaction.created` INBOUND).

O código de produção da flag `TWO_PHASE_PIX_IN` foi MANTIDO (OFF, proibido, como estava). O teste `two_phase_pix_in_test.exs` foi reescrito para deixar explícito no moduledoc e nos nomes dos describes/testes que ele é prova de comportamento de FLAG PROIBIDA e NÃO o fluxo real ("FLAG PROIBIDA (two_phase ON) — prova do gate, NAO e o fluxo real do BACEN"); o teste do caminho canônico flag OFF foi renomeado para "CAMINHO CANONICO (flag OFF) — pacs.008 entregue ja liquidada credita". O helper `send_pacs002_via_simulator` agora documenta que o simulador recusa essa fabricação e que a prova do gate exigiria injeção NATS direta de XML montado à mão. A prova executável do gate (flag OFF => evento "created") segue coberta por `inbound_processor_test.exs` ("pacs008_event_type/0"), que roda na suíte normal (43 testes / 0 falhas nesta sessão).

Prova viva do guard (script `gen_sim_xmls.exs` desta sessão): `P0-1 guard OK — pacs.008 recebida recusada: :pacs002_not_delivered_to_receiver`; controle: a pacs.008 NOSSA segue recebendo pacs.002 ACCC.

### 0.2 P0-2 CORRIGIDO — crédito duplo eliminado

Removidas do `scenario_runner.ex` TODAS as publicações sintéticas: o segundo publish em `monetarie.spi.transaction.created` das injeções (antes linhas 806-808 e 848-850), e as funções `publish_nats_event`/`publish_nats_response_event` inteiras (antes linhas 976-1051, com `TRANSACTION_CREATED/ACCEPTED/REJECTED` e `RETURN_CREATED/SETTLED` de contrato falso), junto com `default_account_id` (conta fixa "201"). A injeção agora publica UM único destino via a função de contrato `ScenarioRunner.inbound_injection_publish/3` (`scenario_runner.ex:884-899`), com o payload do NatsBridge real: `%{message_type, message_id, xml_content}` — nada de `direction: "inbound"` minúsculo, `e2e_id`, `account_id` "201" ou `amount` string. O subject passou a ser derivado do tipo (`monetarie.spi.inbound.pacs004` para :return etc.; antes tudo ia para `...pacs008`). O auto-refund do `dict_simulator.ex` também passou ao contrato do bridge (`dict_simulator.ex:1489-1497` — antes publicava `xml:` sem `xml_content`, que o dispatch do InboundProcessor nem lia). 1 injeção = 1 entrega de XML = 1 evento de crédito, o do InboundProcessor real.

Testes que travam o contrato: `scenario_runner_injection_test.exs` (payload com exatamente `message_id/message_type/xml_content` + tripwire estático que refuta `monetarie.spi.transaction.`/`monetarie.spi.return.`/`INBOUND_PAYMENT`/`direction: "inbound"`/`default_account_id` no fonte do runner).

### 0.3 P0-3 CORRIGIDO — pacs.008 do simulador XSD-VÁLIDA (provado com xmllint)

`generate_incoming_pacs008` (`response_generator.ex:322-371`) passou às chaves REAIS do builder: `debtor_document`/`creditor_document` (antes `*_cpf_cnpj`, descartadas => Id vazio), `local_instrument` (antes `initiation_form`, descartada => sempre MANU), conta sem hífen no pattern `[0-9A-Z]{1,20}` (`random_account`, `response_generator.ex:562-565`), DICT carrega `Prxy` com a chave do recebedor, QRES/QRDN só com `tx_id` explícito (QR real carrega TxId), e agora aceita `creditor_document`/`creditor_account` do chamador (permite creditar um cliente específico pelo caminho real — fecha a lacuna da seção 7). Mesma classe de defeito corrigida nos cenários do `scenario_runner.ex` (happy_path, rejected_payment, return_flow, full_cycle, stress, MED, pix_limit, automatico_execute, cnpj_alfa: documentos/contas/local_instrument/proxy_key em todos os :send de pacs.008) e no `capacity_test.ex:464-478`.

Prova xmllint (XMLs gerados pelo `ResponseGenerator` REAL pós-fix; baseline = exemplo oficial BACEN falha SÓ no `Sgntr` vazio):

```
$ xmllint --noout --schema pacs.008.spi.1.15.xsd sim_inbound_pacs008.xml
sim_inbound_pacs008.xml:9: element Sgntr: ... Missing child element(s) ...   (ÚNICO erro, igual ao baseline oficial)
```

Com assinatura sintética, `Shared.Validation.SpiXsd.validate/2` (o gate real de produção) retorna `:ok` — provado no teste `response_generator_fidelity_test.exs`. Fio conferido: `<LclInstrm><Prtry>DICT</Prtry>`, documentos do Dbtr E do Cdtr presentes, conta `3741430` (sem hífen), `<Prxy><Id>` com a chave, E2E 32 chars.

### 0.4 P0-4 CORRIGIDO — pacs.004 com motivo e valor reais (provado com xmllint)

`generate_incoming_pacs004` (`response_generator.ex:372-421`) passou às chaves REAIS do builder: `return_reason`/`return_amount`/`rtr_id` (antes `reason_code`/`amount`/`return_id`, descartadas => sempre MD06 com R$ 0,00), `debtor_ispb`/`creditor_ispb` para os MmbId do OrgnlTxRef (antes vazios, XSD-inválido), RtrId gerado por `MessageBuilder.generate_rtr_id` no pattern `D+ISPB+AAAAMMDDHHmm+11` com o ISPB do PSP devolvedor. `@return_reasons` agora é exatamente o enum real `BE08 FR01 MD06 SL02` (`response_generator.ex:52`); motivo inválido do chamador não vaza para o fio (fallback logado para MD06). Cenários MED do runner corrigidos: FR09 -> FR01 (fraude) e SL01 -> SL02 (falha operacional), com ISPBs no fio. Auto-refund do DictSimulator: FOCR -> FR01 com valor real (`dict_simulator.ex:1459-1499`).

Prova xmllint e fio:

```
$ xmllint --noout --schema pacs.004.spi.1.5.xsd sim_inbound_pacs004_FR01.xml
sim_inbound_pacs004_FR01.xml:9: element Sgntr: ... (ÚNICO erro, igual ao baseline)

<RtrId>D18236120202607110054gchqpmlwb2j</RtrId>
<RtrdIntrBkSttlmAmt Ccy="BRL">150.00</RtrdIntrBkSttlmAmt>
<Rsn><Cd>FR01</Cd></Rsn>
<MmbId>46026562</MmbId> / <MmbId>18236120</MmbId>
```

Assinada sinteticamente, `SpiXsd.validate == :ok` (teste).

### 0.5 P0-5 CORRIGIDO — @reject_reasons só com o enum oficial

`@reject_reasons` perdeu AB06/FF01/RC01 e ficou com 12 códigos, todos do enum oficial (`response_generator.ex:39`); um guard de COMPILAÇÃO recusa qualquer código fora de `RejectCodes.pacs002_codes()` (raise em build, `response_generator.ex:41-48`) e, em runtime, todo motivo de pacs.002 do simulador passa por `RejectCodes.sanitize_pacs002/1` com log CRÍTICO na substituição (`response_generator.ex:219-239`) — o mesmo guarda do caminho real. Prova xmllint: pacs.002 RJCT sorteada valida (só Sgntr); com assinatura sintética, `SpiXsd.validate == :ok` (teste).

### 0.6 Execução dos testes (somente simulador e afetados)

Fase vermelha (antes dos fixes): 15 testes novos, 13 falhas — exatamente os defeitos auditados. Fase verde (depois):

```
apps/shared simulator (fidelity + injection + alfa + dict sync):  21 testes, 0 falhas
settlement_service simulator_controller_test.exs:                  9 testes, 0 falhas
dict_service bacen_adapter_simulator_test.exs:                     7 testes, 0 falhas
spi_service inbound_processor_test.exs (gate da flag) + compilação
  do two_phase_pix_in_test.exs:                                   43 testes, 0 falhas (2 skipped pré-existentes; e2e excluídos)
```

Compilação dos 4 módulos do simulador sem warnings.

### 0.7 Arquivos alterados (nenhum do caminho de produção)

- `pix/backend/apps/shared/lib/shared/bacen/simulator/response_generator.ex` (P0-1/3/4/5)
- `pix/backend/apps/shared/lib/shared/bacen/simulator/scenario_runner.ex` (P0-2/3/4)
- `pix/backend/apps/shared/lib/shared/bacen/simulator/dict_simulator.ex` (P0-2/4)
- `pix/backend/apps/shared/lib/shared/bacen/simulator/capacity_test.ex` (P0-3)
- `pix/backend/apps/spi_service/test/spi_service/integration/two_phase_pix_in_test.exs` (P0-1, prova de flag proibida)
- `pix/backend/apps/shared/test/shared/bacen/simulator/response_generator_fidelity_test.exs` (novo)
- `pix/backend/apps/shared/test/shared/bacen/simulator/scenario_runner_injection_test.exs` (novo)

Fora do escopo desta correção (seguem abertos): P1 6-12 e P2 13-16 da seção 6 (AppHdr `To`, subject com ponto + headers PI-*, camt.060 assíncrona, admi.002 correlacionada, PI-EndToEndId no DictSimulator e seu `generate_e2e_id` local [CORRIGIDO na trilha DICT/MED/CID, seção 0.10], timeout sem PDNG, accept_rate opt-in, ack de tipo desconhecido, auth das rotas, perfil de latência). CobV/QR (item 19 da matriz / P1-11) foi CORRIGIDO em 2026-07-10 na trilha de cobrança, ver seção 0.8. Os fluxos SPI/ICOM (P1 6, 7, 8, 9 e 12; P2 13 e 14) foram CORRIGIDOS em 2026-07-10 na trilha de fluxos, ver seção 0.9. Os seeds de catálogo (`simulator_seed.exs`/`bacen_error_codes_seed.exs`) mantêm FR09/SL01/FOCR como dados de referência de tela, não de fio.

### 0.8 CobV/QR CORRIGIDO (2026-07-10, trilha de cobrança): simulação de Cob e CobV + QR, antes totalmente AUSENTE

O item 19 da matriz (AUSENTE) e o P1-11 foram atendidos com módulos novos do simulador, TDD (54 testes novos, 0 falhas), usando o código REAL da cabine como régua e sem tocar nenhum arquivo de outra trilha (response_generator.ex, scenario_runner.ex e dict_simulator.ex intactos) nem o caminho de produção.

#### 0.8.1 O que existe agora

Módulos novos em `pix/backend/apps/shared/lib/shared/bacen/simulator/`:

- `cobv_brcode.ex` (`CobvBrcode`): análise de BR Code sobre o EMV canônico da cabine (`Shared.Pix.BRCode.Decoder`/`Encoder`): CRC-16/CCITT-FALSE, tags obrigatórias (o mesmo conjunto do `SettlementService.QRCodes.validate_required_fields`), GUI `br.gov.bcb.pix`, moeda 986, país BR, classificação estático (chave na 26/01) vs dinâmico (URL na 26/25) e faixa do txid por tipo (estático 1-25; dinâmico/cobv 26-35).
- `cobv_qr_resolver.ex` (`CobvQrResolver`): lado pagador, GET na location do BR Code (cliente HTTP injetável; default Finch `Shared.Finch`), aceita o corpo da nossa cabine (JSON com campo `jws`, forma do PayloadController/LegadoPIX) e JWS compacto puro (forma de PSP externo), exige o cabeçalho completo do Manual de Segurança PIX 4.2 (`alg`, `jku`, `kid`, `x5t`) e verifica o JWS pelo caminho REAL da cabine (`Shared.Crypto.JwsSigner.verify/2`: allowlist de algoritmos e resolução da chave pelo `kid` contra os certificados CERTQRC de dev). 404 = `qr_not_found`; 503 = `qr_signing_unavailable`; 410 resolve e a visão marca não pagável (cobrança removida), como um app de banco real mostra.
- `cobv_validator.ex` (`CobvValidator`): a visão do PSP pagador sobre o payload resolvido, com as regras reais da cabine: txid na faixa 26-35, status ATIVA como único pagável, cob expira por `calendario.criacao + expiracao`, cobv usa a regra de DIA ÚTIL do vencimento pela mesma fonte da cabine (`Shared.Pix.BusinessDayCalendar.next_business_day`, o GetProxDataUtil do LegadoPIX, feriados nacionais computados + municipais do DB), janela de pagamento até `dataDeVencimento + validadeAposVencimento`, e consistência aritmética dos valores quando presentes no payload (`final = original - abatimento - desconto + juros + multa`, truncado em 2 casas como o FmtDec; desconto só até o vencimento efetivo em dia útil; juros/multa só depois do vencimento, os mesmos cortes do `Shared.Pix.CobvCalculator`).
- `cobv_simulator.ex` (`CobvSimulator`): a fachada dos dois fluxos. (1) Lado pagador: `resolve_payer/2` resolve um BR Code dinâmico da nossa cabine e `pay/2` dispara o pagamento simulado como pacs.008 de ENTRADA com os dados da cobrança (TxId = txid da cobrança, LclInstrm = QRDN, Prxy = chave, valor = final, devedor do payload como pagador) pelo caminho real de injeção pós-P0: `ResponseGenerator.generate_inbound/2` (XSD-válida) + o contrato único `ScenarioRunner.inbound_injection_publish/3` (payload do NatsBridge, no subject vigente do contrato da trilha 1); 1 pagamento = 1 entrega de XML = 1 evento de crédito, o do InboundProcessor real. Com isso o `maybe_link_qr_payment`/`pix.charge.paid` do caminho real passa a ser exercitável em dev (o item 19 apontava que o simulador nunca passava `tx_id`). (2) Lado recebedor externo: `decode_external/1` decodifica um BR Code de fora e `build_outbound_pacs008/2` monta a pacs.008 de SAÍDA correspondente para testar o nosso PIX-out de cobrança (estático: QRES, TxId do QR quando diferente de `***`, valor da tag 54; dinâmico externo: QRDN com amount/tx_id 26-35 informados pelo chamador, já que o JWS de PSP externo não verifica contra a nossa chave), com o mesmo `MessageBuilder` da produção.

API REST nova (settlement_service, mesmo gate `SIMULATOR_ENABLED` dos demais endpoints do simulador; controller `cobv_simulator_controller.ex` + rotas em escopo próprio no router): `POST /api/v1/simulator/cobv/{resolve,pay,decode-external,external-pacs008}`, erros com código estável + `detail` claro (`invalid_crc`, `not_dynamic`, `charge_not_payable`, `amount_required`, `external_payload_required`, `invalid_txid`, `invalid_signature` etc.).

#### 0.8.2 Prova ponta a ponta local (TDD verde)

O E2E `apps/settlement_service/test/settlement_service/cobv_simulator_e2e_test.exs` executa o circuito completo com os MÓDULOS REAIS da cabine em modo dev: `SettlementService.QRCodes.generate_cobv_qr` cria a CobV real (txid gerado pela CABINE na faixa 26-35, provado; EMV com URL na 26/25 e 62/05 = `***`; juros 1%/multa 2%/desconto 5% no jsonb) -> o simulador resolve o BR Code com o GET servido pelo `PayloadController.show/2` REAL (JWS assinado pela CERTQRC de dev, RS512, verificado pelo JwsSigner) -> visão do pagador com desconto de 5% aplicado pela matemática real (`final = 95.00`) -> `pay/2` injeta a pacs.008 de entrada (única publicação, no subject do contrato único de injeção) -> XSD oficial v5.12.1 validada com xmllint pelo gate real (`Shared.Validation.SpiXsd`, baseline Sgntr sintética, igual à seção 4). Segundo cenário E2E: CobV removida pelo usuário resolve com 410 + `REMOVIDA_PELO_USUARIO_RECEBEDOR` e o `pay/2` RECUSA publicar.

Execução (somente testes novos/afetados):

```
apps/shared cobv (brcode + validator + simulator):            42 testes, 0 falhas
settlement cobv (controller REST + e2e):                      12 testes, 0 falhas
vizinhos re-rodados sem regressão: response_generator_fidelity,
  scenario_runner_injection, scenario_runner_alfa,
  simulator_controller_test:                                  todos 0 falhas
```

(Na rodada conjunta, a única falha do diretório era `dict_simulator_sync_test.exs`, arquivo modificado em progresso pela trilha 2 do DICT, fora desta trilha.)

#### 0.8.3 Achados e pendências honestas da trilha CobV/QR

1. **Chave AES de teste inválida em `config/test.exs` (defeito de baseline, pré-existente)**: `:cert_encryption_key` decodifica em 34 bytes e o AES-256-GCM exige 32, então TODO teste que registra certificado com chave privada quebra na baseline (`payload_controller_test.exs` 8/8 falhas, `jws_signer_cert_lookup_test.exs` 4/5). Os testes desta trilha contornam com a env `CERT_ENCRYPTION_KEY` (que tem precedência no CertificateManager). Correção de 1 linha em `config/test.exs`, arquivo compartilhado, fica para quem for o dono do arquivo.
2. **`Encoder.tlv/2` de produção não valida o teto de 99 chars do TLV**: valor com 100+ chars emite comprimento de 3 dígitos silenciosamente e o EMV inteiro fica ilegível (provado nesta trilha: com o access_token de 48 hex, a tag 26 fecha em EXATAMENTE 99 chars usando o host real `qrcode-h.monetarie.com`; um host 1 char mais longo, por exemplo com `.br`, quebra o BR Code em silêncio). Não corrigido aqui (encoder é caminho de produção, fora do meu escopo); recomendação: guard fail-fast no encoder ou validação no `generate_*_qr`.
3. **Subject da injeção**: `pay/2` delega ao contrato pós-P0 do ScenarioRunner e herda automaticamente o subject vigente (provado nesta sessão: quando a trilha 1 aplicou o P1-7 e o subject passou a `monetarie.spi.inbound.pacs.008`, o fluxo de cobrança acompanhou sem mudança de código; os testes travam contra o próprio contrato, não contra a constante).
4. **BR Code dinâmico EXTERNO não tem verificação de JWS**: o certificado do PSP externo não está na nossa store CERTQRC (por design); o fluxo exige `amount`/`tx_id` resolvidos pelo chamador. Melhoria futura: verificação opcional via fetch do JWK Set público do `jku` externo.
5. **Percurso pós-injeção em dev**: o E2E local prova até a entrega do XML no contrato do fio + XSD; o percurso completo InboundProcessor -> crédito -> `link_payment`/`pix.charge.paid` exige o cluster de dev com NATS de pé (não roda na suíte), validar no ambiente de dev com o passo a passo da seção 7.
6. **UI**: as telas do simulador (pix-admin e simulator-frontend) ainda não têm aba de cobrança; a API REST está pronta para elas.

### 0.9 Fluxos SPI/ICOM CORRIGIDOS (2026-07-10, trilha de fluxos): P1 6-9/12 e P2 13-14

Mandato do dono, mesma data: os fluxos SPI/ICOM restantes desta trilha foram corrigidos com TDD (fase vermelha registrada: 44 testes, 25 falhas antes dos fixes) somente em `response_generator.ex`, `scenario_runner.ex` e testes. O caminho de produção (InboundProcessor, NatsBridge, MessageBuilder, StatusUpdater) não foi tocado. `capacity_test.ex` foi auditado e não precisou de mudança (consome `Simulator.process_message` com `scenario: :always_accept`, que segue devolvendo pacs.002 para a NOSSA pacs.008).

#### 0.9.1 ERRATA do item 9 da matriz + P1-6 CORRIGIDO: AppHdr da mensagem ENTREGUE

A afirmação original do item 9 ("Fr = ISPB do PSP remetente, To = 00038166, inclusive na mensagem entregue") estava ERRADA: releitura dos exemplos oficiais do catálogo v5.12.1 prova o padrão oposto para mensagens entregues. Evidência (Fr/To extraídos de cada exemplo em `/Users/luizpenha/mwbank/md/v5.12.1/exemplos/`):

```
pacs.008_COMP_TROCO_1 (participante ENVIA):  Fr=99999010  To=00038166  BizMsgIdr=M99999010...
pacs.008_CONTA_1      (SPI ENTREGA):         Fr=00038166  To=99999010  BizMsgIdr=M00038166...
pacs.008_END_1        (SPI ENTREGA):         Fr=00038166  To=99999010  BizMsgIdr=M00038166...
camt.052/053/054, admi.002 (SPI ENTREGA):    Fr=00038166  To=99999010
pacs.004, pibr.001, camt.060 (participante): Fr=participante  To=00038166
```

Ou seja: tudo que o participante envia vai com To=00038166; tudo que o SPI entrega chega com Fr=00038166, To=participante recebedor e BizMsgIdr NOVO cunhado pelo BACEN (M00038166...), preservando no CORPO os dados do remetente (E2E `E`+ISPB do PSP pagador, DbtrAgt/CdtrAgt). Corrobora: o LegadoPIX guarda o certificado do ISPB 00038166 (`GetX509Certificate2Sync("00038166", SPICN, ...)`) e monta todo AppHdr de saída com To=00038166 (`SPI.Core.Mensageria.Application.Book.v111...HEAD/v11.cs:27`); o parser real da cabine trata o `To` do pacs.008 de entrada como sempre NOSSO (`inbound_processor.ex:411-426`); e as admi.002 reais recebidas em produção têm BizMsgIdr M00038166... (comentário empírico em `maybe_correlate_admi002_camt060`).

Fix: `generate_incoming_pacs008` (`response_generator.ex:490-538`) e `generate_incoming_pacs004` (`response_generator.ex:540-590`) agora emitem Fr=00038166, To=nosso ISPB e `msg_id = generate_msg_id("00038166")`; o remetente segue no corpo (DbtrAgt=ISPB do PSP, E2E `E`+ISPB dele, RtrId `D`+ISPB do devolvedor). Fio provado:

```
<Fr><FIId><FinInstnId><Othr><Id>00038166</Id></Othr></FinInstnId></FIId></Fr>
<To><FIId><FinInstnId><Othr><Id>46026562</Id></Othr></FinInstnId></FIId></To>
<BizMsgIdr>M00038166ed005dd04aaac71c5fd3749</BizMsgIdr>
...DbtrAgt MmbId=18236120, E2E=E18236120..., RtrId=D18236120...
```

O guard do P0-1 continua valendo (Fr=00038166 != nosso ISPB, pacs.002 recusada) e foi ESTENDIDO à pacs.004 recebida (`respond_to_pacs004`, `response_generator.ex:294-300`): o recebedor de uma pacs.004 também nunca ganha pacs.002 dela.

#### 0.9.2 P1-7 CORRIGIDO: entrega ICOM com subject real (com ponto) e headers PI-*

Contrato único `ScenarioRunner.icom_delivery_contract/3` (`scenario_runner.ex:1195-1212`): subject `monetarie.spi.inbound.<msg_type COM ponto>` (ex.: `...inbound.pacs.008`), payload EXATO `%{message_type, message_id, xml_content}` e headers `[{"PI-Canal","CPM"}, {"PI-ResourceId",...}, {"Idempotency-Key", message_id}]`, espelhando `nats_bridge.ex:130-148`. `do_publish` (`scenario_runner.ex:1370`) parou de injetar `event_id` no payload e de pôr `Nats-Msg-Id` (o bridge real não põe). `inbound_injection_publish/3` mantém a 2-tupla por compatibilidade (o CobvSimulator chama), agora com o subject correto. Cenários `:inbound_nats_inject`/`:inbound_multi_ispb` passaram a `monetarie.spi.inbound.pacs.008`. `deliver_inbound/3` (`scenario_runner.ex:1231`) é o publicador usado pelas entregas assíncronas.

#### 0.9.3 P1-8 (camt.060) CORRIGIDO: resposta ASSINCRONA pelo fluxo real

`respond_to_camt060` (`response_generator.ex:327-373`) não devolve mais camt.052 síncrona com saldo fabricado e ISPB vazio. Comportamento real: o POST recebe só ack de transporte (`{:ok, nil, %{type: :camt060_accepted_async, ...}}`) e a resposta chega DEPOIS pelo fio de entrega (`deliver_inbound`, delay configurável `async_delay_ms`, default 400 ms), conforme o `ReqdMsgNmId`, com as MESMAS correlações que o InboundProcessor real usa:

- saldo (camt.053): `Stmt/Id` ecoa o `GrpHdr/MsgId` da camt.060 (correlação de `process_eod_statement`); saldos SADP/SABK vindos de `bacen_simulator.spi_balances` quando existir (`conta_pi_balances`, `response_generator.ex:436-455`), com fallback nos defaults históricos;
- arquivo/relatório (camt.052): `Rpt/Id` ecoa o MsgId (correlação de `process_account_report`);
- consulta de lançamento (camt.054): `EndToEndId` ecoa o `RptgReq/Id` (provado vivo em HML 2026-07-07).

No `OutboundSender`, o retorno `{:ok, nil, meta}` pula o publish síncrono (guarda `is_binary(response_xml)` pré-existente em `outbound_sender.ex:361`), então a camt.060 real enviada em dev agora percorre o ciclo assíncrono completo: cabine envia, simulador entrega camt.053 pelo NATS, InboundProcessor materializa a posição.

#### 0.9.4 P1-9 (admi.002) CORRIGIDO: Message Reject correlacionado

`generate_admi002` (`response_generator.ex:622-651`) parou de fabricar "evento de sistema" (SYSTEM_START etc., que não existe no fio): agora é Message Reject com `RltdRef/Ref` = MsgId da mensagem NOSSA (param `related_ref`; default no formato MsgIdType `M`+nosso ISPB+23, correlacionável pelo padrão que `maybe_correlate_admi002_camt060` usa). Tipo novo `:message_reject` em `generate_inbound`; `:system_event` mantido como alias legado (a rota REST `/inbound type=admi.002` continua funcionando). Cenário `:message_reject` (`scenario_runner.ex:421-439`): envia camt.060 e entrega a admi.002 com RltdRef = MsgId dela pelo fio real.

#### 0.9.5 P1-12 (timeout) + eco + camt.054 avulsa CORRIGIDOS

- Timeout: scenario `:timeout` devolve `{:ok, nil, %{type: :no_response}}` (`response_generator.ex:213-220`); nenhuma pacs.002 PDNG espontânea sobra no gerador (tripwire no teste). Cenário `:outbound_timeout` (`scenario_runner.ex:377-403`): pacs.008 enviada, prova de ausência de resposta no SLA (`{:expect_no_response}`, `scenario_runner.ex:1050`) e pacs.002 TARDIA entregue depois pelo fio real (`{:deliver_late_pacs002}`, `scenario_runner.ex:1096`, delay configurável `late_delay_ms`).
- Eco (item 21 da matriz, fidelidade fina): `respond_to_pibr001` (`response_generator.ex:458-475`) ecoa o `Data` da pibr.001 no `OrgnlData` da pibr.002 (antes ecoava o MsgId), a chave REAL de agrupamento provada em produção; o cenário `:echo_test` agora FALHA se `OrgnlData != Data` (`scenario_runner.ex:954-956`).
- camt.054 avulsa (item 20): `generate_camt054` (`response_generator.ex:591-620`) aceita `end_to_end_id`/`debtor_ispb`/`creditor_ispb` do chamador; cenário `:camt054_credit_notice` (`scenario_runner.ex:406-418`) entrega pelo caminho real. Bônus: `generate_camt053` (`response_generator.ex:653-679`) ficou XSD-válida (antes saía com Acct/Id vazio), com `Stmt/Id` = 32 zeros (EOD não solicitado, mesmo teste do InboundProcessor/LegadoPIX) e SADP/SABK do estado do simulador.

#### 0.9.6 P2-13 e P2-14 CORRIGIDOS; P2-15 e P2-16 registrados

- P2-13: o sorteio de rejeição com `accept_rate` virou OPT-IN via scenario `:random_reject` (`response_generator.ex:236`); o default `:normal` SEMPRE aceita (o BACEN real não rejeita aleatoriamente). O cenário `:stress_test` passou a usar `:random_reject` para manter resultados mistos.
- P2-14: tipo sem processamento modelado não ganha mais pacs.002 ACSP fabricada: `generate_ack` foi removido e o fallback devolve `{:ok, nil, %{type: :transport_ack_only}}` (`response_generator.ex:145`), fiel ao ack de transporte da ICOM. Cenários Pix Automático atualizados (`{:expect_transport_ack}`, `scenario_runner.ex:742` e `782`): a resposta de negócio de pain.009/012 é assíncrona, não uma pacs.002.
- P2-15 (auth das rotas) REGISTRADO: exige mudança em `settlement_service/.../router.ex:802-829`, arquivo fora desta trilha.
- P2-16 (perfil de latência) REGISTRADO: o delay vive em `simulator.ex:261-267` (fora desta trilha); operacionalmente já dá para aproximar o real via `POST /api/v1/simulator/config` com `min_delay_ms=25`/`max_delay_ms=25`.

#### 0.9.7 Cenários novos disparáveis pela API

`list_scenarios/0` ganhou `:outbound_timeout` (3 passos), `:camt054_credit_notice` (1), `:message_reject` (2), `:camt060_report_file` (2) e `:camt060_lancto_query` (2); `:balance_check` e `:full_cycle` migraram para o fluxo assíncrono real. Como `parse_scenario_name` do controller resolve contra `list_scenarios()`, todos já são disparáveis por `POST /api/v1/simulator/scenarios/run` sem mudança no controller.

#### 0.9.8 Prova xmllint (XSDs oficiais v5.12.1; baseline = só o Sgntr, igual ao exemplo oficial)

XMLs gerados pelo `ResponseGenerator` REAL (script `gen_sim_xmls_p1p2.exs` na scratchpad da sessão). Resultado: TODOS os 11 XMLs falham SOMENTE no `Sgntr` vazio, idêntico ao baseline do exemplo oficial `pacs.008_CONTA_1_msg.xml`:

```
sim_inbound_pacs008_delivered.xml   vs pacs.008.spi.1.15.xsd: so Sgntr
sim_inbound_pacs004_delivered.xml   vs pacs.004.spi.1.5.xsd:  so Sgntr
sim_camt054_avulsa.xml              vs camt.054.spi.1.15.xsd: so Sgntr
sim_camt053_avulsa.xml              vs camt.053.spi.1.4.xsd:  so Sgntr
sim_admi002_reject.xml              vs admi.002.spi.1.5.xsd:  so Sgntr
sim_camt060_resp_camt053_saldo.xml  vs camt.053.spi.1.4.xsd:  so Sgntr (Stmt/Id == MsgId da camt.060: true)
sim_camt060_resp_camt052_rel.xml    vs camt.052.spi.1.3.xsd:  so Sgntr (Rpt/Id correlacionado: true)
sim_camt060_resp_camt054_lancto.xml vs camt.054.spi.1.15.xsd: so Sgntr (EndToEndId ecoa RptgReq/Id: true)
sim_pibr001_request.xml             vs pibr.001.spi.1.3.xsd:  so Sgntr
sim_pibr002_echo.xml                vs pibr.002.spi.1.3.xsd:  so Sgntr (OrgnlData == Data: true)
sim_pacs002_tardia_ACCC.xml         vs pacs.002.spi.1.16.xsd: so Sgntr
```

Com assinatura sintética, `SpiXsd.validate == :ok` para pacs.008/004, camt.052/053/054, admi.002 e pibr.002 (provado nos testes de fidelidade). Guards provados no mesmo script: pacs.008 e pacs.004 recebidas recusam pacs.002 (`:pacs002_not_delivered_to_receiver`); scenario `:timeout` devolve `type=no_response`; admi.004 enviada devolve `type=transport_ack_only`.

#### 0.9.9 Execução dos testes (somente simulador e afetados)

Fase vermelha: 44 testes, 25 falhas (os comportamentos novos). Fase verde (após os fixes):

```
shared simulator (fidelity + injection + flows novo + alfa):  48 testes, 0 falhas
settlement_service simulator_controller_test.exs:              9 testes, 0 falhas
dict_service bacen_adapter_simulator_test.exs:                 7 testes, 0 falhas
spi_service inbound_processor_test.exs + two_phase (compila): 43 testes, 0 falhas (2 skipped pre-existentes)
```

`response_generator.ex` e `scenario_runner.ex` compilam sem warnings. Observação: `dict_simulator_sync_test.exs` está com 1 falha NO MOMENTO por causa do trabalho em voo da trilha DICT (o `dict_simulator.ex` da working tree referencia `Shared.Bacen.Simulator.ClaimSimulator`, módulo ainda não presente); não é efeito desta trilha.

#### 0.9.10 Arquivos alterados nesta trilha

- `pix/backend/apps/shared/lib/shared/bacen/simulator/response_generator.ex`
- `pix/backend/apps/shared/lib/shared/bacen/simulator/scenario_runner.ex`
- `pix/backend/apps/shared/test/shared/bacen/simulator/response_generator_fidelity_test.exs` (novos describes P1-6/8/9/12, eco, camt.053/054, P2-13/14)
- `pix/backend/apps/shared/test/shared/bacen/simulator/scenario_runner_injection_test.exs` (reescrito para o contrato com ponto + headers PI-*)
- `pix/backend/apps/shared/test/shared/bacen/simulator/scenario_runner_flows_test.exs` (novo, cenários ponta a ponta com DB)
- `pix/backend/apps/spi_service/test/spi_service/integration/two_phase_pix_in_test.exs` (só strings de mensagens de raise: subject com ponto)

#### 0.9.11 Registros para outras trilhas e achados de PRODUÇÃO (não corrigidos aqui)

1. PRODUÇÃO (P0 potencial, caminho real, fora desta trilha): `process_incoming_return` dá `raise` quando o envelope não traz `debtor_ispb` (`inbound_processor.ex:1106-1108`). O payload REAL do NatsBridge só tem `message_type/message_id/xml_content`, então uma pacs.004 REAL entregue pelo BACEN crasharia o worker (o mesmo defeito que o pacs.008 real tinha antes do fix de `inbound_processor.ex:411-426`). Nunca recebemos pacs.004 em produção até hoje; corrigir ANTES da primeira devolução orgânica.
2. PRODUÇÃO (atribuição do pagador): `payer_ispb`/`debtor_ispb` preferem `parsed[:from_ispb]` (AppHdr Fr) ao DbtrAgt do corpo (`inbound_processor.ex:806-807` e `2718-2720`). Como o fio real entregue tem Fr=00038166, a cabine registra o pagador como 00038166 em vez do PSP pagador. Com o AppHdr agora fiel, o simulador REPRODUZ esse comportamento em dev (fidelidade), o que expõe o defeito para correção pela trilha do caminho real (fonte correta: DbtrAgt do corpo).
3. Trilha CobV: `cobv_simulator_test.exs:238` afirma o subject antigo sem ponto (`monetarie.spi.inbound.pacs008`); atualizar para `monetarie.spi.inbound.pacs.008` (o código da trilha herda o fix automaticamente via `inbound_injection_publish/3`).
4. Trilha DICT: o auto-refund do `dict_simulator.ex` publica no subject sem ponto (`monetarie.spi.inbound.pacs004`) com publisher próprio e sem headers PI-*; migrar para `ScenarioRunner.deliver_inbound/3`. [RESOLVIDO na seção 0.10: as duas entregas de pacs.004 (auto-refund legado e refund MED aceito) usam `ScenarioRunner.deliver_inbound/3`.]
5. Controller (`simulator_controller.ex:18-32`): sugerido adicionar `"message_reject" => :message_reject` ao `@inbound_type_map` (hoje `admi.002`/`system_event` chegam ao mesmo gerador via alias, então nada quebra).

## 0.10 Atualização 2026-07-10 (trilha DICT/MED/CID): DICT COMPLETO + CLAIMS + MED 2.0 + CID no simulador

Mandato do dono executado nesta data (trilha dedicada, TDD com fase vermelha registrada: 58 testes novos, 54 falhas antes da implementação). A régua de fidelidade é o que a cabine REAL espera do DICT REAL v2.11.0: o cliente `Shared.Bacen.DictClient` + `Shared.Bacen.Dict.{RequestBuilder,ResponseParser}` (que já rodaram contra o BACEN homolog), os fixtures oficiais `apps/shared/test/fixtures/dict/v2.10/*` e as capturas reais do homolog `v2.11/*_hml.xml`, o CID sync real (`DictService.Sync.CidSyncService`) e a taxonomia de erros do DICT (espelhada pela JDPI Lista de APIs v5.4.3, módulo DICT). Nenhum arquivo do caminho de produção foi tocado; trilha restrita a `simulator/dict_simulator.ex` + módulos novos `claim_simulator.ex`/`med_simulator.ex` + testes.

### 0.10.1 DICT entries fiéis (GetEntry/CreateEntry/UpdateEntry/DeleteEntry)

`GET /entries/{Key}` agora EXIGE `PI-EndToEndId` no formato real `E+ISPB(8)+AAAAMMDDHHmm(12)+11 alfanuméricos` — sem o header responde 400 com detail "Missing request header 'PI-EndToEndId' ..." (o comportamento provado vivo do BACEN, dict_client.ex:128-133); formato inválido também é 400. Consulta de chave custodiada no próprio PSP responde `EntryCannotBeQueriedForBookTransfer` (400), como o DICT real. CreateEntry com as validações reais: posse (chave CPF/CNPJ deve ser o documento do dono, `EntryInvalid`), formato por tipo, limites REAIS por conta (5 PF / 20 PJ, os mesmos da cabine `DictService.Keys @key_limits`, `EntryLimitExceeded`) e a taxonomia real de duplicidade: mesma chave+dono+participante = `EntryAlreadyExists` (400, não mais o 409 fictício); mesmo dono em outro PSP = `EntryKeyInCustodyOfDifferentParticipant` (ensina o caminho da portabilidade); outra pessoa = `EntryKeyOwnedByDifferentPerson` (ensina a reivindicação). EVP: o simulador GERA o UUID da chave. Update/Delete com reivindicação aberta = `EntryLockedByClaim`; delete de quem não custodia = 403 `Forbidden`. `GetEntry?IncludeStatistics=true` devolve o bloco `<Statistics>` do fixture oficial.

### 0.10.2 CLAIMS com a máquina de estados real

Módulo novo `claim_simulator.ex` (rotas `POST /claims/`, `GET /claims/{Id}`, `GET /claims/?Participant&IsDonor&IsClaimer&Status&ModifiedAfter&Limit`, `POST /claims/{Id}/{acknowledge|confirm|cancel|complete}`):

- Máquina: `OPEN -> WAITING_RESOLUTION -> CONFIRMED -> COMPLETED` e `CANCELLED` (CancelledBy DONOR/CLAIMER + CancelReason), com papéis reais (acknowledge/confirm = doador; complete = reivindicador; participante errado = 403 `Forbidden`; transição inválida = `ClaimOperationInvalid`).
- Prazos reais: `ResolutionPeriodEnd` = abertura + 7 dias; `CompletionPeriodEnd` = abertura + 30 dias; aceleráveis por `config :shared, :dict_simulator_claim_periods, resolution_seconds:/completion_seconds:`. Regras de prazo do DICT real: em OWNERSHIP o doador NÃO confirma antes do fim da resolução (`ClaimResolutionPeriodNotEnded`); conclusão unilateral sem confirmação exige o fim do período (`ClaimCompletionPeriodNotEnded`/`ClaimResolutionPeriodNotEnded` conforme o tipo) — códigos e semântica da tabela JDPI.
- Validações de criação reais: `ClaimKeyNotFound`, `ClaimAlreadyExistsForKey`, `ClaimTypeInconsistent` (portabilidade = mesmo dono; posse = dono diferente), `ClaimResultingEntryAlreadyExists`.
- COMPLETED transfere a chave no diretório simulado (REMOVED + ADDED nos eventos CID) e o GetEntry seguinte devolve o vínculo no reivindicador (provado em teste).
- Contraparte simulada: `open_claim_against_us/1` (BACEN abre claim em que somos doador; a claim chega a `monetarie_dict.claims` SOMENTE pelo polling real `Claims.sync_inbound_claims_from_bacen` — o `generate_claim_as_donor` do controller foi re-apontado, o INSERT SQL direto e o evento NATS sintético `monetarie.dict.claims.incoming` foram removidos), `simulated_party_respond/3` (o lado simulado age com as MESMAS regras do fio) e `expire_overdue_claims/1`.

### 0.10.3 MED 2.0 completo (infrações + refunds + funds recoveries)

Módulo novo `med_simulator.ex`:

- Infrações (`POST /infraction-reports/`, `GET .../{Id}`, `GET .../?participant&Status&ModifiedAfter&Limit`, `POST .../{Id}/{acknowledge|close|cancel}`): máquina real `OPEN -> ACKNOWLEDGED -> CLOSED` (+ `CANCELLED` pelo reporter), papéis reais (só a CONTRAPARTE acata/fecha, só o REPORTER cancela; fechar sem acatar = `InfractionReportOperationInvalid`), enums reais validados (`Reason` FRAUD|AML_CTF|REFUND_REQUEST|REFUND_CANCELLED; `SituationType` SCAM|ACCOUNT_TAKEOVER|COERCION|FRAUDULENT_ACCESS|OTHER|UNKNOWN — a mesma fonte da cabine, infractions.ex:829-831), TransactionId no formato E2E real, duplicidade = `InfractionReportAlreadyBeingProcessedForTransaction`. Fechamento AGREED carrega `FraudMarkerId` (comportamento das capturas reais do homolog). Contraparte derivada do ISPB embutido no E2E.
- Refunds (`POST /refunds/`, `GET /refunds/{Id}`, `GET /refunds?participant`, `POST /refunds/{Id}/{close|cancel}`): `OPEN -> CLOSED` (TOTALLY_ACCEPTED|PARTIALLY_ACCEPTED|REJECTED; aceito exige `RefundTransactionId` no formato real `D+ISPB+AAAAMMDDHHmm+11`; rejeição com `RefundRejectionReason` do enum NO_BALANCE|ACCOUNT_CLOSURE|OTHER|PARTICIPANT_EXCLUSION) e `CANCELLED` (só o solicitante). Prazo de análise de 7 dias por refund, acelerável (`config :shared, :dict_simulator_med, refund_analysis_seconds:`), com `overdue_refunds/1`.
- Funds recoveries (`POST /funds-recoveries/`, `GET .../{Id}`, `GET .../{Id}/tracking-graph`, `POST .../{Id}/{refund|cancel}`): `CREATED -> REFUNDING` (o refund do recovery abre um `<Refund>` OPEN vinculado, visível no `/refunds` do poller) e `CANCELLED` (só o criador, nunca após REFUNDING = `FundsRecoveryOperationInvalid`); resposta de criação com o bloco `<TrackingGraph>` no shape do fixture oficial.
- Time de teste: `open_infraction_against_us/1` e `open_refund_against_us/1` (o BACEN simulado abre contra nós; chegam às tabelas de negócio pelos pollers REAIS `Infractions.poll_and_route_inbound_from_bacen` e `FundsRecovery.sync_inbound_refunds_from_bacen`), `respond_infraction_as_counterparty/3` e `respond_refund_as_contested/3` (a contraparte responde aos que NÓS criamos, com atraso configurável). Refund aceito com `deliver_pacs004: true` entrega a pacs.004 REAL (mesmo `MessageBuilder` da produção, RtrId = RefundTransactionId, motivo mapeado ao enum real: FRAUD -> FR01, OPERATIONAL_FLAW -> SL02, REFUND_CANCELLED -> MD06) pelo contrato ÚNICO de entrega ICOM do simulador (`ScenarioRunner.deliver_inbound/3`: subject com ponto + headers PI-*) — o dinheiro volta pelo InboundProcessor real, nunca por evento sintético. Prova XSD no gate real: `SpiXsd.validate(pacs.004, "pacs.004") == :ok` (teste).
- `generate_infraction_against_monetarie` e `auto_process_refund` do controller foram re-apontados para o estado do simulador (com fallback legado para refunds locais em DB), eliminando o insert direto em tabelas de negócio; o auto-refund legado também passou a entregar a pacs.004 via `ScenarioRunner.deliver_inbound/3` (fecha o item 4 do registro 0.9.11).

### 0.10.4 CID completo e fiel ao sync real

- **Verificador de sincronismo corrigido para XOR**: o verificador REAL é o XOR bit a bit dos 32 bytes de cada CID ativo, hex minúsculo, comutativo (LegadoPIX `Validacoes.CalcularXor`; cabine `cid_sync_service.ex sync_verifier_for_cids/1`; conjunto vazio = 64 zeros). O SHA-256 anterior do simulador faria o poll real acusar drift para sempre. `SyncVerifierStart/End` dos eventos e o `Result` do `/sync-verifications/` agora usam XOR, e o resultado divergente é `OUT_OF_SYNC` (enum real, response_parser.ex:760 — antes "DIVERGENT", que não existe).
- **CID = HMAC**: o CID de cada entry passou a ser `hmacSha256(request_id, atributos da entry)` (contrato documentado no parser real, response_parser.ex:2947-2949 — o `GetEntryByCidResponse` expõe o `RequestId` exatamente para essa verificação), com `DictSimulator.verify_entry_cid/3` espelhando a verificação sobre a entry parseada.
- **Arquivo CID servível**: `DictSimulator.cid_file_body/1` devolve o corpo no formato REAL do download do full sync (texto, uma CID por linha — o `parse_cid_lines/1` da cabine), com `Sha256`/`Bytes` do `GetCidSetFileResponse` calculados sobre esses bytes; a URL publicada preserva o path `/cid-set-files/...` para o rewrite de origem via `ARQ_BASE_URL` (o único seam do download real, Finch direto na URL).
- `generate_e2e_id` local do DictSimulator corrigido para o formato real de 32 chars via `MessageBuilder.generate_e2e_id/1` (fecha o P1-10 e a divergência do item 8 da matriz).
- Ingestão real provada: `CidEntryIngestor.ingest_cid/1` (o ingestor REAL) materializa uma chave em `monetarie_dict.keys` a partir do `GetEntryByCid` do simulador (teste de integração).

### 0.10.5 Erros RFC 7807 na taxonomia real + RateLimited

Todos os erros do simulador DICT saem como `application/problem+xml` com namespace `urn:ietf:rfc:7807` e `type` na base real `https://dict.pi.rsfn.net.br/api/v2/error/<Código>`, integralmente parseáveis por `ResponseParser.parse_problem_xml/1` e classificados por `classify_error/2` (404 -> `:rollback`, 400 -> `:keep_failed` etc.). Taxonomia ancorada na tabela de erros do DICT (JDPI v5.4.3): gerais `BadRequest`/`Forbidden`/`NotFound`/`RateLimited`/`ServiceUnavailable`; vínculos `EntryInvalid`/`EntryLimitExceeded`/`EntryAlreadyExists`/`EntryKeyOwnedByDifferentPerson`/`EntryKeyInCustodyOfDifferentParticipant`/`EntryLockedByClaim`/`EntryCannotBeQueriedForBookTransfer`; reivindicações `ClaimKeyNotFound`/`ClaimAlreadyExistsForKey`/`ClaimResultingEntryAlreadyExists`/`ClaimTypeInconsistent`/`ClaimOperationInvalid`/`ClaimResolutionPeriodNotEnded`/`ClaimCompletionPeriodNotEnded`; MED `InfractionReportInvalid`/`InfractionReportOperationInvalid`/`InfractionReportAlreadyBeingProcessedForTransaction`/`RefundInvalid`/`RefundOperationInvalid`/`RefundAlreadyBeingProcessedForTransaction`/`FundsRecoveryInvalid`/`FundsRecoveryOperationInvalid`. O 404 genérico deixou de ser o fictício "EntryNotFound" e virou o real `NotFound`. `DictSimulator.set_rate_limited/1` liga a limitação: TODA requisição responde 429 `RateLimited` com header `Retry-After` no mapa de erro (o shape que o client real preserva para o `DictBudget`, client.ex:113-116).

### 0.10.6 Integração ponta a ponta com o pipeline REAL da cabine

`apps/dict_service/test/dict_service/dict_simulator_inbound_poll_test.exs` (4 testes) exercita a cadeia REAL completa — `BacenAdapter.Real -> DictClient -> seam :dict_http_client -> simulador`, com `dict_external_mode: :bacen` — sem nenhum stub:

1. **Claim ponta a ponta acelerada**: BACEN simulado abre PORTABILITY contra nós -> `Claims.sync_inbound_claims_from_bacen(IsDonor)` materializa OPEN em `monetarie_dict.claims` -> respondemos como doador pelo client REAL (acknowledge -> confirm, com `resolution_seconds: 0`) -> o reivindicador simulado conclui -> o poll seguinte reflete COMPLETED no banco -> a chave aparece TRANSFERIDA no GetEntry.
2. **Infração MED ponta a ponta**: BACEN simulado abre infração contra nós -> `Infractions.poll_and_route_inbound_from_bacen` materializa OPEN (counterparty = nós) -> acatamos e fechamos DISAGREED pelo client REAL assinado -> o poll reflete CLOSED + analysis_result no banco.
3. **Refund contra nós**: poll real materializa OPEN em `monetarie_dict.refund_requests` (contested = nós) -> fechamos REJECTED/NO_BALANCE pelo client real -> o poll reflete CLOSED.
4. **CID**: `CidEntryIngestor.ingest_cid` materializa a chave em `monetarie_dict.keys` a partir do simulador.

### 0.10.7 Execução dos testes (fase vermelha e verde)

Fase vermelha (antes da implementação): 58 testes novos, 54 falhas. Fase verde (após, incluindo regressão das outras trilhas):

```
apps/shared .../simulator/ (todos, incl. trilhas 1/3):  151 testes, 0 falhas
  dict_simulator_entries_test.exs                         17
  claim_simulator_test.exs                                15
  med_simulator_test.exs                                  20 (inclui prova XSD da pacs.004)
  cid_simulator_fidelity_test.exs                          6
  dict_simulator_sync_test.exs (atualizado p/ XOR)         2
settlement_service simulator_controller_test.exs:          9 testes, 0 falhas
dict_service (adapter + poll integração + claims sync):   25 testes, 0 falhas
```

Compilação dos módulos da trilha sem warnings. Zero regressão nos testes das trilhas 1/3 e no controller do simulador.

### 0.10.8 Arquivos da trilha (nenhum do caminho de produção)

- `pix/backend/apps/shared/lib/shared/bacen/simulator/dict_simulator.ex` (roteador DICT 2.11.0 completo, validações de entries, taxonomia de erros, XOR, HMAC-CID, corpo de arquivo CID, RateLimited, delegates fiéis)
- `pix/backend/apps/shared/lib/shared/bacen/simulator/claim_simulator.ex` (novo)
- `pix/backend/apps/shared/lib/shared/bacen/simulator/med_simulator.ex` (novo)
- Testes novos: `apps/shared/test/shared/bacen/simulator/{dict_simulator_entries_test.exs,claim_simulator_test.exs,med_simulator_test.exs,cid_simulator_fidelity_test.exs}` e `apps/dict_service/test/dict_service/dict_simulator_inbound_poll_test.exs`; atualizado: `dict_simulator_sync_test.exs` (XOR + OUT_OF_SYNC)

### 0.10.9 Pendências honestas da trilha

- O download HTTP do arquivo CID pelo full sync real (Finch direto na URL) exige um servidor servindo `DictSimulator.cid_file_body/1` atrás de `ARQ_BASE_URL`; o simulador fornece corpo/sha/bytes coerentes e a URL com o path correto, mas não sobe listener HTTP (fora do escopo de módulos desta trilha).
- `GET /funds-recoveries/{Id}/tracking-graph` responde um `<TrackingGraph>` fiel ao fixture, mas o consumidor real (`tracking_graph_attrs_from_bacen`, funds_recovery.ex:1233) lê chaves de um mapa que o `dict_request` cru não fornece — comportamento idêntico contra o BACEN real (gap latente do caminho de produção, não do simulador; não tocado por regra da trilha).
- Fraud markers (`/fraud-markers/*`) não foram simulados (o poller real não os pagina por `ModifiedAfter` e o fluxo MED interno gera `FraudMarkerId` no fechamento AGREED); fica como extensão natural.
- Claims: o DICT real restringe reivindicação de posse para chaves CPF/CNPJ (o documento identifica o dono); sem evidência direta no código da cabine, essa regra específica não foi imposta além do `ClaimTypeInconsistent` por documento.
- Em dev, os pollers só rodam com `DICT_EXTERNAL_MODE=bacen` (e `dict_http_client` apontado ao simulador via `config :shared, :dict_http_client, Shared.Bacen.Simulator.DictHttpClient`); em modo `local` as claims/infrações/refunds do simulador não chegam ao banco — comportamento documentado, igual ao gate real.

## 1. Sumário executivo

O simulador cobre bem a mecânica de ida e volta (pacs.008 enviada recebe pacs.002; formato do E2E; enum de status ACSP/ACCC/RJCT; DICT key sync com CID/verificadores), mas divergiu do BACEN real em pontos que ensinam ao time comportamentos que NÃO existem no fio:

1. Ele sustenta o modelo two-phase de PIX-in (pacs.002 entregue ao RECEBEDOR de uma pacs.008), que é ficção: o BACEN liquida ANTES de entregar a pacs.008 e o recebedor nunca recebe pacs.002 dela (fato canônico provado em produção em 2026-07-09).
2. Ele publica eventos NATS sintéticos com contrato DIFERENTE do real diretamente em `monetarie.spi.transaction.created`, em paralelo ao evento real que o InboundProcessor também publica para a mesma injeção, criando risco de crédito duplo em dev e ensinando um payload que não existe.
3. Os XMLs de entrada que ele gera (pacs.008 e pacs.004 recebidas) são XSD-inválidos contra o catálogo oficial (provado com xmllint), e a devolução simulada sai sempre com motivo MD06 e valor R$ 0,00 por mismatch de chaves de parâmetro.
4. O sorteio de rejeição usa códigos que não existem no enum oficial da pacs.002 (AB06, FF01, RC01), a mesma classe de defeito do incidente de produção de 2026-07-09 (AC04).

## 2. Inventário: o que o simulador é e onde vive

### 2.1 Componentes de código

| Componente | Arquivo | Papel |
|---|---|---|
| GenServer principal | `pix/backend/apps/shared/lib/shared/bacen/simulator.ex` | Liga com `SIMULATOR_ENABLED=true` (linhas 106-108); config em DB (`bacen_simulator.simulator_config`); accept_rate default 0.9, delay 50-500 ms (linhas 46-52, 261-267) |
| Gerador de respostas/inbound | `.../shared/bacen/simulator/response_generator.ex` | pacs.008→pacs.002, pacs.004→pacs.002, camt.060→camt.052, pibr.001→pibr.002, ack genérico (linhas 101-123); inbound não solicitado: pacs.008, pacs.004, camt.054, camt.053, admi.002, pibr.001 (linhas 139-150) |
| Cenários | `.../shared/bacen/simulator/scenario_runner.ex` | 20 cenários (happy_path, rejected_payment, return_flow, incoming_payment, echo, balance, full_cycle, stress, 5 MED, limite noturno, 3 Pix Automático, inject NATS, multi-ISPB, CNPJ-alfa) (linhas 102-200); publica eventos NATS "Core Banking integration" (linhas 976-1051) |
| Teste de capacidade IN 508 | `.../shared/bacen/simulator/capacity_test.ex` | DICT GetEntry por intervalo e SPI tps (linhas 71, 234) |
| Simulador DICT (HTTP shape) | `.../shared/bacen/simulator/dict_simulator.ex` | Estado ETS; rotas `/keys/check`, `/entries/*`, `/cids/files`, `/cids/entries/{cid}`, `/cids/events` (querystring), `/sync-verifications/` (linhas 509-551); claims como doador, infração contra a Monetarie, auto-refund com pacs.004 FOCR (linhas 81, 205, 366, 1459-1503) |
| Adapter HTTP DICT plugável | `.../shared/bacen/simulator/dict_http_client.ex` | Só entra se configurado `config :shared, :dict_http_client` (moduledoc linhas 5-7); NÃO está configurado em nenhum config (`grep` em `pix/backend/config/*.exs` vazio) |
| Adapter DICT em DB | `pix/backend/apps/dict_service/lib/dict_service/bacen_adapter/simulator.ex` | Segundo simulador DICT, tabela `bacen_simulator.dict_entries`; usado por ex. pelo fraud marker quando `SIMULATOR_ENABLED=true` e modo != bacen (`fraud_marker_controller.ex:105-111`) |
| Schemas de DB | `.../shared/bacen/simulator/schemas.ex` | `bacen_simulator.{simulator_config, scenarios, scenario_steps, scenario_variables, test_runs, test_results, message_exchanges, message_validations, response_templates, async_responses, dict_entries, spi_balances}` |
| Controller REST | `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/simulator_controller.ex` | 20 endpoints sob `/api/v1/simulator` |
| Rotas | `pix/backend/apps/settlement_service/lib/settlement_service_web/router.ex:802-829` | `pipe_through [:gateway, :api]` (SEM pipeline autenticado; linha 803) |
| UI dedicada | `pix/simulator-frontend/` (Vue 3) | Views: Dashboard, Scenarios, Inbound (gera XML), Exchanges, TestRuns, DictTest, CapacityTest, Settings; `stores/simulator.ts:72-73` aponta `/api/v1/simulator` |
| UI no pix-admin | `pix/frontend/admin/src/views/simulator/{SimulatorDashboardView,ScenarioRunnerView,MessageExchangeView}.vue` + `services/simulator.ts:192-197` | roda cenários e gera inbound |

### 2.2 Acoplamento com o caminho real

- Saída: com `BACEN_ENABLED=false`, `Shared.Bacen.Client.spi_request/dict_request` devolvem `{:error, :bacen_disabled}` (`client.ex:104-107, 238-241`). O `OutboundSender` roteia então para `Shared.Bacen.Simulator.process_message` (`outbound_sender.ex:135-136, 325-379`) e publica a pacs.002 simulada como inbound em `monetarie.spi.inbound.received` (`outbound_sender.ex:381-403`), que o `InboundProcessor` consome (filtro `monetarie.spi.inbound.>`, `inbound_processor.ex:82`).
- Entrada: os cenários `:incoming_payment`/`:inbound_nats_inject`/`:inbound_multi_ispb` geram uma pacs.008 e publicam o XML em `monetarie.spi.inbound.pacs008` E TAMBÉM um evento sintético direto em `monetarie.spi.transaction.created` (`scenario_runner.ex:806-808` e `848-850`).
- Verificação de assinatura XMLDSig do inbound é bypassada automaticamente quando `BACEN_ENABLED=false` (`inbound_processor.ex:175-177`).
- DICT em dev: `DICT_EXTERNAL_MODE` default é `local` (`config/config.exs:13`, `config/runtime.exs:109-124`), ou seja, a consulta de chave em dev é servida pelo DB local do dict_service e NÃO passa pelo simulador DICT, a menos que se configure `:dict_http_client` ou `:bacen_adapter` manualmente.

### 2.3 Como o time dispara entrada de dinheiro (ver seção 7 para o passo a passo)

REST `POST /api/v1/simulator/inject-inbound` (`simulator_controller.ex:311-340`), REST `POST /api/v1/simulator/scenarios/run` com `incoming_payment`/`inbound_nats_inject`/`inbound_multi_ispb`, ou pelas telas (pix-admin Simulador / simulator-frontend). A tela "Inbound" do simulator-frontend apenas GERA o XML (`POST /api/v1/simulator/inbound`), não injeta.

### 2.4 XMLs que gera/consome

Gera (via `Shared.Bacen.Iso20022.MessageBuilder`, o mesmo builder da produção): pacs.008, pacs.004, pacs.002, camt.052, camt.053, camt.054, admi.002, pibr.001, pibr.002, e no DictSimulator os XMLs REST do DICT (CheckKeys/Entry/CIDs/SyncVerification, montados à mão em `dict_simulator.ex:592-1100`). Consome: pacs.008, pacs.004, camt.060, pibr.001 (respostas) e qualquer XML via `POST /api/v1/simulator/process`.

## 3. Matriz de fidelidade

Vereditos: FIEL (igual ao real provado), DIVERGE (existe mas diferente), AUSENTE (não coberto).

| # | Item | Comportamento real provado | Comportamento do simulador | Veredito | Evidência |
|---|---|---|---|---|---|
| 1 | Ordem liquidação/entrega do PIX-in | BACEN liquida a pacs.008 ANTES de entregá-la; recebedor credita no recebimento (evento `created` INBOUND) | O caminho legado flag-OFF injetado via `monetarie.spi.inbound.pacs008` reproduz a ordem (crédito no created); MAS o simulador também sustenta o modelo two-phase com pacs.002 ACCC/STLD entregue ao recebedor, que não existe no BACEN | DIVERGE (P0) — CORRIGIDO 2026-07-10 (seção 0.1) | fato canônico 2026-07-09; `two_phase_pix_in_test.exs:10, 73-79`; `inbound_processor.ex:2858-2864` |
| 2 | Recebedor recebe pacs.002 da pacs.008 recebida? | NÃO, nunca | pacs.002 é gerada só para NOSSAS mensagens enviadas (`respond_to_pacs008`); porém `/api/v1/simulator/process` e `/inbound type=pacs.002` permitem fabricar pacs.002 correlata a uma pacs.008 recebida (é assim que o teste two-phase "liquida") | DIVERGE (P0) — CORRIGIDO 2026-07-10 (seção 0.1: guard `:pacs002_not_delivered_to_receiver`) | `response_generator.ex:154-198`; `simulator_controller.ex:104-129, 131-167` |
| 3 | Evento NATS do PIX-in (contrato real) | `monetarie.spi.transaction.created` com `transaction_id, message_id, message_type, event_type:"CREATED", debtor_ispb, creditor_ispb, end_to_end_id, direction:"INBOUND", type:"pix_transaction", amount, account_id, source:"pix", published_at, trace_id` | Evento sintético paralelo: `event_type/type:"INBOUND_PAYMENT"`, `direction:"inbound"` (minúsculo), chave `e2e_id` (não `end_to_end_id`), `amount` string em reais, `account_id` fixo "201", `source:"pix_simulator"`, sem `transaction_id/creditor_ispb` | DIVERGE (P0) — CORRIGIDO 2026-07-10 (seção 0.2: evento sintético removido) | real: `inbound_processor.ex:2874-2892`; sim: `scenario_runner.ex:833-850, 788-808`; conta default: `scenario_runner.ex:43` |
| 4 | Unicidade do evento de crédito | 1 pacs.008 = 1 evento `created` (dedup Redis + advisory lock + transfer_id determinístico) | 1 injeção publica DOIS eventos que disparam crédito no Core: o sintético direto e o real do InboundProcessor (identidades diferentes: `e2e_id` vs `end_to_end_id`); o Core lê `payload["transaction_id"] \|\| payload["end_to_end_id"]` (nil no sintético) e aceita `"inbound"` minúsculo como inbound | DIVERGE (P0, risco de crédito duplo em dev) — CORRIGIDO 2026-07-10 (seção 0.2: 1 injeção = 1 entrega de XML) | `scenario_runner.ex:806-808, 848-850`; Core `pix_handler.ex:62`; Core `account_resolver.ex:154-176` |
| 5 | pacs.008 recebida XSD-válida | BACEN só entrega mensagem XSD-válida e assinada | XML gerado é XSD-INVÁLIDO: CPF/CNPJ do Dbtr/Cdtr vazios (simulador passa `debtor_cpf_cnpj`, builder lê `debtor_document`), conta `123456-7` viola pattern `[0-9A-Z]{1,20}` (hífen), `initiation_form` ignorado (builder lê `local_instrument`) e LclInstrm sai sempre MANU | DIVERGE (P0) — CORRIGIDO 2026-07-10 (seção 0.3: xmllint = só Sgntr, SpiXsd :ok) | xmllint na seção 4; `response_generator.ex:274-284, 469-471`; `message_builder.ex:183, 187, 211-217`; o fix análogo da produção está comentado em `payment_controller.ex:258-263` |
| 6 | pacs.004 recebida (devolução) | Enum XSD de motivo: BE08, FR01, MD06, SL02; valor devolvido real; RtrId `D+ISPB+YYYYMMDDHHmm+11` | Builder ignora `reason_code`, `amount` e `return_id` do simulador (lê `return_reason`, `return_amount`, `rtr_id`): TODA devolução simulada sai `MD06` com `RtrdIntrBkSttlmAmt=0.00` e MmbId vazio (XSD-inválida); os motivos pretendidos (`FR09 AC03 AM04 FOCR SL01` em `@return_reasons` e FOCR no auto-refund) nem existem no enum, e o FR01 real (fraude) falta | DIVERGE (P0) — CORRIGIDO 2026-07-10 (seção 0.4: motivo/valor reais, enum BE08/FR01/MD06/SL02) | xmllint na seção 4; `response_generator.ex:34, 298-329`; `message_builder.ex:334, 348, 351-352`; `dict_simulator.ex:1471`; enum extraído de `pacs.004.spi.1.5.xsd` |
| 7 | Enum de rejeição da pacs.002 | 43 códigos `ExternalStatusReason1Code` do XSD oficial, fonte única `Shared.Bacen.Iso20022.RejectCodes` (com guarda `sanitize_pacs002`); reais vistos: AB03, ED05 | `@reject_reasons` tem 15 códigos, dos quais AB06, FF01 e RC01 NÃO existem no enum; o sorteio pode emitir pacs.002 XSD-inválida (provado com xmllint) | DIVERGE (P0) — CORRIGIDO 2026-07-10 (seção 0.5: guard de compilação + sanitize) | `response_generator.ex:33`; `reject_codes.ex:26-32`; xmllint na seção 4 |
| 8 | Formato do E2E | `E + ISPB(8) + AAAAMMDDHHmm(12) + 11 alfanum` = 32 chars | `MessageBuilder.generate_e2e_id` gera exatamente isso (gerado nesta auditoria: `E18236120202607102301i63wxjowto3`, 32 chars); MAS o `DictSimulator.generate_e2e_id` local usa só a DATA (8 dígitos) + 15 alfanum, violando o pattern do fio nos E2E de infração | FIEL no ResponseGenerator / DIVERGE no DictSimulator — CORRIGIDO 2026-07-10 (seção 0.10.4: `generate_e2e_id` do DictSimulator delega ao MessageBuilder) | `message_builder.ex:1614, 1626-1632`; `dict_simulator.ex:1422-1426` |
| 9 | AppHdr da pacs.008 no fio | ERRATA 2026-07-10 (seção 0.9.1): a mensagem ENTREGUE pelo SPI tem `Fr` = `00038166`, `To` = participante recebedor e BizMsgIdr novo M00038166... (a afirmação anterior desta linha estava errada; os exemplos oficiais provam) | pacs.008 recebida simulada saia com `Fr` = remetente | DIVERGE (P1) — CORRIGIDO 2026-07-10 (seção 0.9.1) | exemplo oficial `/Users/luizpenha/mwbank/md/v5.12.1/exemplos/pacs008/pacs.008_CONTA_1_msg.xml` (`<To>...00038166`); `response_generator.ex:267-268`; `message_builder.ex:135-142` |
| 10 | AppHdr da pacs.002 do SPI | `Fr` = `00038166`, `To` = ISPB do participante | Igual (`from_ispb: @bacen_ispb`, `to_ispb` = participante) | FIEL | exemplo oficial `exemplos/pacs002/pacs.002_SPI_1_msg.xml`; `response_generator.ex:177-188` |
| 11 | Status da pacs.002 | ACSP = processando; ACCC/ACSC/STLD = liquidação concluída da NOSSA saída; RJCT | Mesmo enum; sorteio 90/10 ACCC/RJCT; cenário `:timeout` devolve "PDNG" em pacs.002 (o real fica sem resposta, não manda TxSts PDNG numa pacs.002 de status terminal) | FIEL em ACCC/RJCT; rejeição ALEATÓRIA e PDNG divergem — CORRIGIDOS 2026-07-10 (seções 0.9.5 e 0.9.6: :timeout = sem resposta; sorteio opt-in) | `response_generator.ex:158-175`; `inbound_processor.ex:1875-1881` |
| 12 | Transporte de recepção | ICOM long-poll multipart com ACK GET, leitura de pacs.002 medida ~25 ms; `NatsBridge` publica `monetarie.spi.inbound.<msg_type com ponto>` com headers `PI-Canal`, `PI-ResourceId`, `Idempotency-Key` | Injeção direta no NATS: subject sem ponto (`...inbound.pacs008`) ou `...inbound.received` (resposta do simulador), sem os headers PI-* (dedup por resource_id não exercitado); delay artificial de 50-500 ms | DIVERGE (P1) — CORRIGIDO 2026-07-10 (seção 0.9.2: subject com ponto + headers PI-*); delay segue registrado (P2-16) | `icom/cpm/coordinator.ex:3, 74`; `nats_bridge.ex:55, 130-148`; `scenario_runner.ex:1053-1079`; `outbound_sender.ex:393-396`; `simulator.ex:261-267` |
| 13 | Resposta do recebedor à pacs.008 recebida | O caminho real da cabine responde pacs.002 ACSP e avança a linha para ACCC | A injeção via `monetarie.spi.inbound.pacs008` percorre esse caminho real (fiel); o gate estrutural deixa passar o XML XSD-inválido do simulador porque valida campo pelo ÚLTIMO segmento do path (o `Cdtr.Nm` é satisfeito pelo `Nm` do Dbtr) | FIEL no fluxo, mas mascarando XML inválido | `inbound_processor.ex:938, 952`; `message_validator.ex:357-386`; `message_schemas.ex:84-99` |
| 14 | DICT GetEntry v2.11.0 | EXIGE header `PI-EndToEndId` (400 sem ele) | `DictSimulator.handle_dict_request` não valida nenhum header; GetEntry responde 200 sem PI-EndToEndId; e o adapter HTTP nem está ligado por default (dev usa modo `local`) | DIVERGE (P1) — CORRIGIDO 2026-07-10 (seção 0.10.1: 400 sem/with formato inválido do PI-EndToEndId, book-transfer, taxonomia real) | `dict_client.ex:125-130, 340`; `dict_simulator.ex:509-551, 625-630` |
| 15 | CID sync (arquivos/eventos/verificador) | ERRATA 2026-07-10 (seção 0.10.4): o verificador REAL é XOR dos 32 bytes de cada CID (LegadoPIX `CalcularXor`; cabine `cid_sync_service.ex`), não SHA-256; rotas `/cids/files`, `/cids/files/{id}`, `/cids/entries/{cid}`, `/cids/events?Participant&KeyType` | Rotas fiéis, mas verificador era SHA-256 (drift permanente contra o poll real), `Result` divergente inexistente ("DIVERGENT"), CID não era HMAC e o corpo do arquivo não era servível | DIVERGE — CORRIGIDO 2026-07-10 (seção 0.10.4: XOR + OUT_OF_SYNC + HMAC-CID + corpo do arquivo) | `dict_simulator.ex`; `cid_sync_service.ex:508-527`; `response_parser.ex:760, 2947-2949` |
| 16 | camt.060 (saldo/extrato Conta PI) | Resposta ASSÍNCRONA via ICOM; camt.053 com saldos proprietários SADP/SABK materializa a posição | Resposta SÍNCRONA camt.052 com saldo fictício fixo (R$ 1.500.000,00); camt.052 gerada é XSD-inválida (Id de ISPB vazio) | DIVERGE (P1) — CORRIGIDO 2026-07-10 (seção 0.9.3: resposta assíncrona camt.052/053/054 correlacionada, saldos de spi_balances) | `inbound_processor.ex:1933-1937`; `response_generator.ex:217-232`; xmllint na seção 4 |
| 17 | admi.002 | Message Reject: rejeição correlacionada a uma mensagem NOSSA (casada com `bacen_outbound`); o XSD tem RjctgPtyRsn/RjctnDtTm | Usado como "evento de sistema" (SYSTEM_START, MAINTENANCE...), sem correlação com mensagem alguma; o XML em si valida no XSD | DIVERGE (P1, semântica) — CORRIGIDO 2026-07-10 (seção 0.9.4: Message Reject correlacionado via RltdRef) | XSD `admi.002.spi.1.5.xsd` (RjctgPtyRsn); `response_generator.ex:352-374` |
| 18 | Mensagem desconhecida | BACEN rejeita (admi.002/HTTP), não responde pacs.002 | `generate_ack` responde pacs.002 ACSP para qualquer tipo não mapeado | DIVERGE (P2) — CORRIGIDO 2026-07-10 (seção 0.9.6: ack de transporte, sem pacs.002 fabricada) | `response_generator.ex:108, 247-256` |
| 19 | CobV/QR (txid faixa 26-35 gerado pela cabine; pacs.008 de cobrança carrega TxId) | Provado: cabine gera o txid; pagamento por chave NÃO leva TxId (CH16) | AUSENTE: nenhum cenário de cobrança; o simulador nunca passa `tx_id`, então `maybe_link_qr_payment` nunca é exercitado e não há simulação de pagamento de QR/CobV recebido | AUSENTE (P1) — CORRIGIDO 2026-07-10 (seção 0.8: módulos cobv_* + API REST; pacs.008 de cobrança com TxId/QRDN/Prxy pelo caminho real de injeção) | `qr_codes.ex:122-128, 183-189`; `message_builder.ex:198-205`; `inbound_processor.ex:940-945`; seção 0.8 |
| 20 | camt.054 (aviso de crédito/débito) | Usada em prod para reconciliar órfãos de PIX-in (alert-only) | Gera camt.054 CRDT/DBIT estruturalmente válida (xmllint: só o Sgntr vazio, igual aos exemplos oficiais) | FIEL (estrutura) — MELHORADO 2026-07-10 (seção 0.9.5: E2E/agentes do chamador + cenário :camt054_credit_notice pelo fio real) | `response_generator.ex:331-350`; xmllint na seção 4 |
| 21 | pibr.001/002 (echo) | Eco real validado vivo (ICOM) | pibr.001/002 estruturalmente válidos | FIEL — MELHORADO 2026-07-10 (seção 0.9.5: OrgnlData ecoa o Data, antes ecoava o MsgId) | `response_generator.ex:234-245, 399-410`; xmllint na seção 4 |
| 22 | Eventos de saída (pacs.008 enviada) | `payment_controller` publica `transaction.created` com envelope `{type:"payment.created", data:{...}}`; liquidação vem por `transaction.settled` do StatusUpdater com contrato fixo (centavos, status_id 4) | ScenarioRunner publica `TRANSACTION_CREATED/ACCEPTED/REJECTED` sintéticos com outro shape (sem `direction`, `account_id` "201"), e `RETURN_CREATED/RETURN_SETTLED` com `reason_code` que nem foi para o XML | DIVERGE (P1) | `payment_controller.ex:235-252`; `status_updater.ex:222-228, 253-259`; `scenario_runner.ex:976-1051` |
| 23 | MED 2.0 | Devolução especial real usa códigos do enum (FR01) e fluxo camt.055/camt.029 | Cenários MED mandam pacs.004 com `reason_code: FR09/SL01` que o builder descarta (sai MD06, R$ 0,00); "chain blocking", "partial block" e "timer" são apenas sequências de pacs.008/004 com metadados que não vão ao fio | DIVERGE (P1) — COBERTO 2026-07-10 (seção 0.10.3: infrações/refunds/funds recoveries com máquinas de estado, papéis, prazos e pacs.004 real no refund aceito) | `scenario_runner.ex:333-491`; seção 4 |
| 24 | Latência | Leitura de pacs.002 real medida ~25 ms | Delay artificial uniforme 50-500 ms (configurável) | DIVERGE (P2) | `simulator.ex:47-49, 261-267` |
| 25 | Segurança das rotas | n/a | `/api/v1/simulator/*` roda em pipeline SEM autenticação; endpoints de listagem (scenarios, test-runs, exchanges) respondem mesmo com simulador OFF | DIVERGE (P2, higiene) | `router.ex:802-829`; `simulator_controller.ex:207-234, 268-309` |

## 4. Validação empírica dos XMLs (xmllint vs XSDs oficiais v5.12.1)

Os XMLs foram gerados com o MESMO `MessageBuilder` e os MESMOS parâmetros que o simulador usa (script na scratchpad desta sessão, replicando `response_generator.ex` linha a linha). Baseline: os exemplos OFICIAIS do BACEN falham apenas no `<Sgntr/>` vazio (artefato de amostra não assinada), então qualquer erro além do Sgntr é divergência real:

```
$ xmllint --noout --schema pacs.008.spi.1.15.xsd exemplos/pacs008/pacs.008_CONTA_1_msg.xml
... element Sgntr: ... Missing child element(s) ...   (ÚNICO erro)
```

pacs.008 recebida do simulador (parâmetros de `generate_incoming_pacs008`):

```
sim_inbound_pacs008.xml:28: element Id: ... The value '' is not accepted by the
  pattern '[0-9]{11}|[0-9A-Z]{12}[0-9]{2}'.        (CPF/CNPJ do Dbtr VAZIO)
sim_inbound_pacs008.xml:29: element Id: ... The value '123456-7' is not accepted
  by the pattern '[0-9A-Z]{1,20}'.                 (conta com hífen)
sim_inbound_pacs008.xml:32/33: idem para o Cdtr (documento vazio, conta 654321-0)
```

pacs.002 RJCT com código sorteável do simulador (AB06 está em `@reject_reasons`):

```
sim_pacs002_rjct_AB06.xml:22: element Cd: ... The value 'AB06' is not an element
  of the set {'AB03','AB09','AB11','AC03','AC06','AC07','AC14','AG03','AG12',
  'AG13','AGNT','AM01','AM02','AM04','AM09','AM12','AM18','AM23','BE01','BE05',
  'BE15','BE17','CH11','CH16','CN01','DS04','DS0G','DS27','DT02','DT05','DUPL',
  'ED05','FF07','FF08','FRAD','INDT','MD01','RC09','RC10','RR04','RR06','SL02','UPAY'}.
```

Controles: pacs.002 com AB03 e com ACCC validam (só Sgntr). O enum oficial tem 43 códigos de motivo e é idêntico ao `Shared.Bacen.Iso20022.RejectCodes` (`reject_codes.ex:26-32`), confirmando a fonte única do caminho real.

pacs.004 recebida do simulador (parâmetros de `generate_incoming_pacs004` com `reason_code: "FR09"`, `amount: 150.00`):

```
<RtrdIntrBkSttlmAmt Ccy="BRL">0.00</RtrdIntrBkSttlmAmt>     (valor PERDIDO)
<RtrRsnInf><Rsn><Cd>MD06</Cd></Rsn></RtrRsnInf>              (FR09 IGNORADO)
sim_inbound_pacs004_FR09.xml:27: element MmbId: ... The value '' is not accepted
  by the pattern '[0-9A-Z]{8}'.  (DbtrAgt/CdtrAgt vazios, 2 erros)
```

Enum de devolução do XSD `pacs.004.spi.1.5.xsd`: `BE08, FR01, MD06, SL02` apenas. Cruzamento com o simulador: `@return_reasons = FR09 MD06 AC03 AM04 FOCR SL01 SL02 BE08` contém 5 códigos inexistentes (`FR09, AC03, AM04, FOCR, SL01`) e omite o `FR01` real.

camt.052 do simulador: além do Sgntr, `element Id: ... The value '' is not accepted by the pattern '[0-9A-Z]{8}'` (ISPB vazio, mismatch de chave no builder). camt.054, admi.002, pibr.001 e pacs.002 (códigos válidos): estruturalmente VÁLIDOS (somente o erro de Sgntr, igual ao baseline oficial).

## 5. Testes do simulador (executados nesta auditoria)

Comando: `mix test apps/shared/test/shared/bacen/simulator/dict_simulator_sync_test.exs apps/settlement_service/test/settlement_service_web/controllers/simulator_controller_test.exs apps/dict_service/test/dict_service/bacen_adapter_simulator_test.exs` (raiz `pix/backend`, Postgres/Redis locais de dev).

Resultado real: shared 2 testes / 0 falhas; settlement_service 9 testes / 0 falhas; dict_service 7 testes / 0 falhas. Total 18/0. Observação: `config/test.exs:74` liga `SIMULATOR_ENABLED=true` por default na suíte. O teste de integração two-phase (`two_phase_pix_in_test.exs`) não roda sem `E2E_ENABLED=true` e cluster completo; não foi executado, e é justamente o artefato que codifica o fluxo proibido (ver P0-1). [Atualização 2026-07-10: reescrito como prova explícita de FLAG PROIBIDA — seção 0.1; execução pós-fix na seção 0.6.]

## 6. Lista priorizada de correções

### P0 (ensina comportamento que NÃO existe no BACEN real, ou corrompe o money path de dev)

TODOS OS 5 P0s CORRIGIDOS EM 2026-07-10 — evidência na seção 0. O texto abaixo fica como histórico do que era antes.

1. **CORRIGIDO (seção 0.1). Matar o two-phase PIX-in do simulador.** Remover/neutralizar a capacidade de entregar pacs.002 correlata a uma pacs.008 RECEBIDA (via `/process`, `/inbound type=pacs.002` e o roteiro do `two_phase_pix_in_test.exs`), e marcar o teste como legado proibido. O fluxo real credita no `transaction.created` INBOUND; `TWO_PHASE_PIX_IN` segue PROIBIDA (`runtime.exs:22-24`). Evidência: fatos canônicos 2026-07-09; `two_phase_pix_in_test.exs:73-79`.
2. **CORRIGIDO (seção 0.2). Eliminar o evento sintético direto em `monetarie.spi.transaction.created`** (`scenario_runner.ex:806-808, 848-850` e `publish_nats_event/publish_nats_response_event`, linhas 976-1051). A injeção deve publicar SOMENTE o XML em `monetarie.spi.inbound.*` e deixar o InboundProcessor emitir o evento real (`inbound_processor.ex:2874-2892`). Hoje 1 injeção = 2 eventos de crédito com identidades diferentes (`e2e_id` vs `end_to_end_id`, `direction` minúsculo, conta fixa "201"), risco de crédito duplo no Core de dev e contrato falso ensinado ao time.
3. **CORRIGIDO (seção 0.3). Corrigir as chaves de parâmetro da pacs.008 gerada** em `generate_incoming_pacs008` (`response_generator.ex:260-296`): `debtor_cpf_cnpj`→`debtor_document`, `creditor_cpf_cnpj`→`creditor_document`, `initiation_form`→`local_instrument`, conta sem hífen (pattern `[0-9A-Z]{1,20}`). A produção já corrigiu esse mesmo mismatch no envio (`payment_controller.ex:258-263`); o simulador ficou para trás e gera XML que o BACEN jamais entregaria (xmllint, seção 4).
4. **CORRIGIDO (seção 0.4). Corrigir a pacs.004 gerada**: `reason_code`→`return_reason`, `amount`→`return_amount`, `return_id`→`rtr_id`, e passar `debtor_ispb`/`creditor_ispb` (MmbId). Restringir `@return_reasons` ao enum real `BE08 FR01 MD06 SL02` (hoje toda devolução simulada sai MD06 com R$ 0,00; os cenários MED nunca põem o motivo no fio). `response_generator.ex:34, 298-329`; `message_builder.ex:334-352`; `dict_simulator.ex:1471` (FOCR).
5. **CORRIGIDO (seção 0.5). Trocar `@reject_reasons` para subconjunto de `RejectCodes.pacs002_codes()`** (remover AB06, FF01, RC01) ou derivar direto da fonte única, e passar toda pacs.002 do simulador por `RejectCodes.sanitize_pacs002/1`. `response_generator.ex:33`; `reject_codes.ex:26-60`.

### P1 (fidelidade de fio e semântica)

6. **CORRIGIDO (seção 0.9.1), com ERRATA.** A hipótese original desta linha estava invertida: nos exemplos oficiais, a mensagem ENTREGUE pelo SPI tem `Fr` = `00038166` e `To` = participante recebedor (nosso ISPB), com BizMsgIdr novo M00038166.... O simulador agora emite exatamente isso; o remetente vive no corpo (DbtrAgt, E2E).
7. **CORRIGIDO (seção 0.9.2).** Injetar no subject real `monetarie.spi.inbound.pacs.008` (com ponto) e com headers `PI-Canal`/`PI-ResourceId`/`Idempotency-Key`, como o `NatsBridge` (`nats_bridge.ex:130-148`), para exercitar o dedup por resource_id.
8. **CORRIGIDO (seção 0.9.3).** camt.060: responder assíncrono com camt.052/053 carregando SADP/SABK derivado de `bacen_simulator.spi_balances`, não camt.052 síncrona com saldo fixo e ISPB vazio (`response_generator.ex:217-232`; `inbound_processor.ex:1933-1937`).
9. **CORRIGIDO (seção 0.9.4).** admi.002 como Message Reject correlacionado a uma mensagem enviada (casável com `bacen_outbound`), não "evento de sistema" (`response_generator.ex:352-374`).
10. **CORRIGIDO (seção 0.10).** DICT: exigir `PI-EndToEndId` no GetEntry do DictSimulator (espelho do 400 real, `dict_client.ex:125-130`) e corrigir `DictSimulator.generate_e2e_id` para `E+ISPB+AAAAMMDDHHmm+11` (`dict_simulator.ex:1422-1426`). Documentar que em dev o DICT roda em modo `local` e o simulador DICT só entra com configuração explícita.
11. **CORRIGIDO (seção 0.8).** Cobrir CobV/QR: cenário de pacs.008 recebida com `tx_id` na faixa txid da cabine (26-35), para exercitar `maybe_link_qr_payment` e `pix.charge.paid` (`inbound_processor.ex:940-945`; `qr_codes.ex:183-189`).
12. **CORRIGIDO (seção 0.9.5).** Cenário `:timeout` não deve responder pacs.002 "PDNG"; timeout real = ausência de resposta (deixar a mensagem sem pacs.002 e o `StuckOutboundChecker` agir); pacs.002 tardia coberta pelo cenário `:outbound_timeout`.

### P2 (higiene e realismo fino)

13. **CORRIGIDO (seção 0.9.6).** Rejeição aleatória (accept_rate) deveria ser opt-in por cenário; o BACEN não rejeita aleatoriamente (rejeição real vem de validação/recebedor: AB03, ED05). Agora só rejeita via scenario `:random_reject`.
14. **CORRIGIDO (seção 0.9.6).** `generate_ack` (pacs.002 ACSP para tipo desconhecido) removido; fallback devolve ack de transporte (`:transport_ack_only`), nunca pacs.002 fabricada.
15. REGISTRADO (arquivo de outra trilha, seção 0.9.6). Rotas `/api/v1/simulator` sem autenticação e com listagens ativas mesmo com simulador OFF (`router.ex:803`; garantir que NUNCA suba com `SIMULATOR_ENABLED=true` fora de dev, vide precedência declarada em `shared/application.ex:25-37`).
16. REGISTRADO (arquivo fora desta trilha, seção 0.9.6). Delay default 50-500 ms vs ~25 ms reais; oferecer perfil "realista" (paliativo operacional: `POST /config` com min/max = 25 ms).

## 7. Como o time simula dinheiro entrando (o que funciona HOJE)

Pré-condições (dev/local): `SIMULATOR_ENABLED=true`, `BACEN_ENABLED=false`, `NATS_ENABLED=true` na cabine PIX; NATS/Postgres/Redis de dev de pé (`cd pix/backend && docker compose up -d`); Core conectado ao MESMO NATS para o crédito aparecer no IB. O gateway HTTP é o settlement_service na porta 4003.

Passo a passo A (REST, caminho completo pela cabine):

```bash
# 1 PIX de entrada de R$ 150,00 vindo do ISPB 18236120 (Nubank)
curl -X POST http://localhost:4003/api/v1/simulator/inject-inbound \
  -H 'Content-Type: application/json' \
  -d '{"sender_ispb": "18236120", "amount": "150.00"}'

# N entradas de 6 ISPBs diferentes (count>1 muda para o cenário inbound_multi_ispb)
curl -X POST http://localhost:4003/api/v1/simulator/inject-inbound \
  -H 'Content-Type: application/json' -d '{"count": "6"}'
```

Passo a passo B (cenários):

```bash
curl -X POST http://localhost:4003/api/v1/simulator/scenarios/run \
  -H 'Content-Type: application/json' -d '{"scenario": "incoming_payment"}'
# outros: happy_path, return_flow, full_cycle, med_fraud_claim, inbound_nats_inject...
curl http://localhost:4003/api/v1/simulator/scenarios     # lista
curl http://localhost:4003/api/v1/simulator/test-runs     # resultados
curl http://localhost:4003/api/v1/simulator/exchanges     # XMLs trocados
```

Passo a passo C (telas): pix-admin, menu Simulador (`ScenarioRunnerView` roda cenários; `MessageExchangeView` mostra os XMLs), ou o `simulator-frontend` standalone (`npm run dev` em `pix/simulator-frontend`, proxy para `/api/v1/simulator`). A aba "Inbound" dessas telas apenas GERA o XML, não injeta dinheiro.

O que acontece por baixo (hoje): o cenário gera a pacs.008 e publica em `monetarie.spi.inbound.pacs008` (`scenario_runner.ex:848` ou `806`); o `InboundProcessor` consome (`inbound_processor.ex:82`), pula a verificação de assinatura (BACEN off, linhas 175-177), valida estruturalmente, valida a conta do favorecido via RPC NATS com o Core (`monetarie.core.pix.validate_account`, linhas 114 e 971-999), grava a linha, credita a Conta PI (linha 918), publica o evento REAL `monetarie.spi.transaction.created` com `direction: "INBOUND"` (linhas 2874-2892), responde pacs.002 ACSP (linha 938) e avança a linha para ACCC (linha 952); o Core credita o cliente pelo caminho canônico (created INBOUND não rastreado). ATENÇÃO (histórico): o mesmo cenário TAMBÉM publicava o evento sintético divergente direto em `monetarie.spi.transaction.created` (P0-2) — RESOLVIDO em 2026-07-10 (seção 0.2): a injeção agora publica só a entrega do XML e o único evento de crédito é o do InboundProcessor.

Conta creditada: o caminho real resolve pela validação com o Core usando o documento/conta do favorecido gerados aleatoriamente; o evento sintético usava `account_id` fixo "201" (`config :shared, :default_account_id`, `scenario_runner.ex:43`) — sintético REMOVIDO em 2026-07-10 (seção 0.2). Desde 2026-07-10 o gerador aceita `creditor_document`/`creditor_account` (`ResponseGenerator.generate_inbound(:incoming_payment, creditor_document: ..., creditor_account: ...)`), então dá para creditar um cliente específico pelo caminho real; o documento do favorecido agora chega ao XML (seção 0.3). O endpoint REST `inject-inbound` segue expondo só `sender_ispb`/`amount`/`count` (follow-up de UX, não P0).

## 8. Arquivos-fonte citados (âncoras principais)

- Simulador: `pix/backend/apps/shared/lib/shared/bacen/simulator.ex`, `simulator/response_generator.ex`, `simulator/scenario_runner.ex`, `simulator/dict_simulator.ex`, `simulator/claim_simulator.ex` (novo 2026-07-10), `simulator/med_simulator.ex` (novo 2026-07-10), `simulator/dict_http_client.ex`, `simulator/capacity_test.ex`, `simulator/schemas.ex`
- Controller/rotas: `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/simulator_controller.ex`, `.../router.ex:802-829`
- Caminho real: `pix/backend/apps/spi_service/lib/spi_service/workers/inbound_processor.ex`, `workers/outbound_sender.ex`, `workers/status_updater.ex`, `icom/nats_bridge.ex`, `icom/cpm/coordinator.ex`, `spi_service_web/controllers/payment_controller.ex`
- Fonte única de códigos: `pix/backend/apps/shared/lib/shared/bacen/iso20022/reject_codes.ex`; builder: `.../iso20022/message_builder.ex`
- XSDs: `pix/backend/apps/shared/priv/xsd/spi/v5.12.1/`; exemplos oficiais: `/Users/luizpenha/mwbank/md/v5.12.1/exemplos/`
- Core (consumo dos eventos): `core/backend/lib/monetarie/infra/nats/handlers/pix_handler.ex`, `core/backend/lib/monetarie/use_cases/banking/account_resolver.ex`
- UIs: `pix/simulator-frontend/`, `pix/frontend/admin/src/views/simulator/`, `pix/frontend/admin/src/services/simulator.ts`
- Teste proibido (two-phase): `pix/backend/apps/spi_service/test/spi_service/integration/two_phase_pix_in_test.exs` (desde 2026-07-10 reescrito como prova explícita de flag proibida — seção 0.1)
- Testes de fidelidade novos (2026-07-10): `pix/backend/apps/shared/test/shared/bacen/simulator/response_generator_fidelity_test.exs`, `.../scenario_runner_injection_test.exs`, `.../dict_simulator_entries_test.exs`, `.../claim_simulator_test.exs`, `.../med_simulator_test.exs`, `.../cid_simulator_fidelity_test.exs`, `pix/backend/apps/dict_service/test/dict_service/dict_simulator_inbound_poll_test.exs`
