# Deep audit — pacs.008 recepção (PIX in): validação de crédito e resposta

Auditor senior. READ-ONLY. Zero inferência: cada afirmação tem prova `arquivo:linha`
ou `tabela.coluna`. Onde não houve prova, o veredito é INCONCLUSIVO.

Escopo (pista): **Legado** `FilaEntrada.ValidaCredito` (componentes por IdSystem),
resposta ACCC/RJCT. **Nosso** `SpiService.Workers.InboundProcessor` (validação da conta
no Core via RPC, XSD, sanções, dedup). Comparar **validação de crédito** e **resposta**.

Convenção de caminhos:
- Legado decompilado: `.scratch/legado-pix-decompiled/*.decompiled.cs`
- Legado SQL: `LegadoPIX/Pix/...`
- Nosso: `pix/backend/apps/...`

---

## 0. Resumo executivo

Ambos os lados implementam a MESMA espinha do PIX in: puxa a pacs.008 do ICOM, valida
XSD + assinatura do XML recebido, deduplica, valida o crédito ao recebedor e responde ao
BACEN uma pacs.002 (ACSP no aceite / RJCT com reason code na recusa). A tabela de-para de
motivo de recusa → código ISO é a MESMA (nosso `CreditErrorCodes` cita explicitamente o
`ConversaoValidationError` do legado).

Os pontos que NÃO batem são poucos e todos rastreáveis:

1. **Mecanismo de validação de crédito**: legado usa **componentes plugáveis por IdSystem**
   (um por banco cliente: Agibank, BNP, Cobis, Crefisa, Sinqia, Topázio — arquitetura de
   multiliquidante). Nosso usa **um único RPC ao Core** (Monetarie é SCD única). Divergência
   estrutural justificada pelo produto, mas o nosso caminho NÃO tem roteamento multi-IdSystem.
2. **Resposta ao crédito inaplicável (conta destino inválida/bloqueada/encerrada)**: legado
   responde **pacs.002 RJCT** (a operação nunca liquida). Nosso responde **pacs.002 ACSP +
   devolução automática pacs.004** para AC03/AC06/AC07/AC14/AG03. Modelos de liquidação
   diferentes (o nosso parte do achado canônico "o BACEN liquida ANTES de entregar a
   pacs.008").
3. **Momento do crédito ao cliente**: legado credita o cliente só APÓS a confirmação de
   liquidação (camt.054 BOOK), via `retornolegado`. Nosso credita no ACSP (flag
   `two_phase_pix_in` OFF por padrão), coerente com o mesmo achado canônico.
4. Detalhes menores: `DT05` (devolução fora do prazo) não está na nossa de-para; sanções no
   inbound (nosso adiciona, legado não tem); participante indireto (LPI/IspbIF, legado
   completo, nosso assume ISPB único).

Limitação honesta de grounding: o backup operacional legado (`crk_spi.dbo.SpiMessage`) tem
44 linhas, TODAS `CAMT.060` (consulta de saldo), `FlSentido=1` (envio). **Não há pacs.008
recebida real no dump** para confrontar dados; o comportamento é provado pelo código
decompilado (autoritativo) e pelos scripts SQL.

---

## 1. LEGADO — comportamento provado

### 1.1 Onde a pacs.008 recebida entra e é validada (XSD + assinatura)

Toda mensagem puxada do BACEN passa por `ValidarXmlRecebido`
(`SPI.Core.Mensageria.Application.decompiled.cs:76`):

- `_headerReader.Read(retorno.XmlMsg)` lê o AppHdr (linha 82).
- **XSD**: `_validaXsd.Validate2(retorno.XmlMsg, Header)` (linha 86).
- **Assinatura**: `_signXml.VerificaAssinaturaXml(retorno.XmlMsg)` (linha 87) — a verificação
  em si é gated por `_config.ValidarAssinatura`
  (`SPI.Core.General.SignXml.Application.decompiled.cs:175,347`; `VerifyHash` →
  `AssinaturaInvalida` linha 352-354).
- XSD/assinatura reprovada → erros vão para `ErrosValidacao` e a resposta vira erro
  (`CdMsgErro`/`ADMI002`) (linhas 88-96, 127).
- Header inválido → gera `ADMI002` "Xml inválido" (linhas 132-142).
- Classifica a mensagem: `MsgCredito = Caracteristica ∈ {Pagamento, Devolucao}` (linha 98) —
  é o que roteia a pacs.008 (pagamento) e a pacs.004 (devolução) para a validação de crédito.

### 1.2 Persistência + dedup de inbound (autoridade de recepção)

`TratamentoCreditoRecebidoBase.ValidaOperacaoACreditoRecebida`
(`SPI.Core.Worker.TratamentoRetornoApiBacen.Infrastructure.decompiled.cs:2108`):

- Só processa se `MsgCredito` (linha 2110).
- **DEDUP**: `if (!await _valCredito.ExisteMsgRecebida(item.ObjEnviar.IdSystem,
  item.ObjEnviar.UniqueId, item.ObjEnviar.DtMovto))` — se já recebida, IGNORA
  ("Mensagem ... ignorada por estar repetida", linhas 2138-2145). `UniqueId` de uma
  pacs.008 = o EndToEndId (`ConvertToPACS002FullDTO`/`MontaDetalhePACS002` usam
  `UniqueId ?? EndToEndId`, `SPI.Core.Mensageria.Geral.Application.decompiled.cs:919,942`).
- Bulk-insert das mensagens + payments (linhas 2166-2181) e enfileira para o
  `FilaEntrada` (`MsgsDespachoFilas`, linhas 2183-2201).

Registry durável de identificadores recebidos por ISPB (proc `SPI_UPDDADOSRECEPMSG2`,
`LegadoPIX/Pix/SPI/Database-Inicial/Base CRK_SPISEEDS/Procedures/SPI_UPDDADOSRECEPMSG2.sql`):
- `SpiEndToEndIds` marca `FlUtilizado='S'` (E2E consumido) por `(Ispb, EndToEndId)` (linhas 57-74).
- `SpiReturnIdentification` grava `RtrId` por `(Ispb, RtrId)` (linhas 76-84).
- `SpiMsgIds` grava `MsgId` por `(Ispb, MsgId)` (linhas 86-97).
- `SpiRecMsg` grava `UniqueId` por `(UniqueId, IdSystem)` (linhas 99-107).
- PKs em `01-schema-indices.md:178-180`. É a dedup de reentrega do BACEN.

### 1.3 Validação de crédito (componentes por IdSystem) — o coração

`ValidaCredito.EfetuaValidacaoCredito`
(`SPI.Core.Worker.FilaEntrada.Infrastructure.decompiled.cs:854`):

- Só roda quando o `Sentido == enumSentidoMsg.Retorno` (entrada de crédito), não em lote de
  saída (linha 862).
- `ValidaMsgCredito → TratamentoRetornoApi` (linhas 945, 984):
  - `_validacoes.List(null, [enumTipoComponenteLegado.ValidacaoCredito])` — busca os
    componentes de validação cadastrados (linha 992).
  - `InstanciaComponentesParaLegado` — para cada `ParametroClasseRetornoLegado.IdSystem`,
    instancia dinamicamente um `IValidacaoCreditoUseCase` (cache `_colTratamento.GetOrAdd`,
    linhas 1130-1143). **Cada IdSystem = uma integração com um core legado cliente.** No
    produto Multiliquidação os componentes concretos são `Services.CC.{Agibank, BNP, Cobis,
    Crefisa, Sinqia, Topazio}.SendService`
    (`Multiliquidacao.Core.Worker.ValidacaoCredito.decompiled.cs:260-265`).
  - `TrataPayment` (linha 1059): para cada `PaymentFullDTO` do lote, casa por `EndToEndId`
    (linha 1081) e, se `paymentReturnValue.OK`, roda `AplicaValidacoes` → `ValidaInstancia`:
    `validacao.Instancia.Validacao(item, validacao.IdSystem)` (linha 1122). O componente
    responde `ValidacaoCreditoReturnValue { OK, Erros }` — é onde se verifica: conta existe,
    não bloqueada, não fechada, titular do documento, tipo de conta aceita, etc.
  - `TrataDevolucao` (linha 1145): pacs.004 in casa por `EndToEndIdMsgDev + RtrId`
    (linha 1167) e roda as MESMAS validações.

### 1.4 Resposta ACSP/RJCT + reason code

`ConversaoObjetos.ConvertToPACS002FullDTO` + `MontaDetalhePACS002`
(`SPI.Core.Mensageria.Geral.Application.decompiled.cs:859,886,913`):
- `Status = item.OK ? enumTransactionStatus.ValidadoCreditor : enumTransactionStatus.Rejeitado`
  (linhas 923, 946). `enumTransactionStatus` (`SPI.Core.General.decompiled.cs:8246`):
  ACCC=ConcluidoCreditor(1), ACSC=ConcluidoDebtor(2), **ACSP=ValidadoCreditor(3)**,
  **RJCT=Rejeitado(4)**. Ou seja, **o aceite de uma pacs.008 recebida sai ACSP, não ACCC**.
- Na recusa, cada `ValidationError` vira um `ReasonCode` via
  `ConversaoValidationError.TraduzErro` (linhas 929-932).
- MessageId do pacs.002 mintado por `GeradorMsgId.GetMsgId(IspbCreditor, DtHrOperacao)`
  (linha 895). O pacs.002 é assinado no `TratamentoMsgRetornoUseCase._signer.AssinaXml`
  (`SPI.Core.Worker.TratamentoRetornoApiBacen.Application.decompiled.cs:197`) e enviado.

De-para de recusa de crédito → ISO (`ConversaoValidationError`,
`SPI.Core.General.decompiled.cs:218-286`):

| enumValidationError | ISO | Semântica |
|---|---|---|
| ErroCreditoGenerico | AB09 | erro genérico |
| ContaCreditoNaoEncontrada | AC03 | conta inexistente |
| ContaCreditorBloqueada | AC06 | conta bloqueada |
| ContaCreditorFechada | AC07 | conta encerrada |
| TipoContaCreditorInvalida / Obrigatorio | AC14 | tipo de conta |
| TransacaoNaoSuportadaContaCreditor | AG03 | operação não suportada |
| ContaCreditorNaoPertenceAoDocumento | BE01 | titular divergente |
| DocumentCreditorInvalido | CH11 | doc do credor inválido |
| OrdemCreditoRejeitada | DS04 | + AddData=erro.Msg |
| OperacaoCreditoFalhou | ED05 | + AddData=erro.Msg (fallback default) |
| DevolucaoExtrapolaOPrazoMaximo | DT05 | devolução fora do prazo |
| ErroValidacaoCredito | ED05 | fallback |

`TraduzErro`: código não mapeado cai em `OperacaoCreditoFalhou → ED05`; DS04/ED05 anexam
`AddData = erro.Msg` (linha 281-284).

### 1.5 Crédito ao cliente (timing) — só após liquidação

O crédito efetivo à conta do cliente é confirmado ao core legado pelo worker
`retornolegado` (via MQ) e só para status FINAL; e o camt.054 BOOK
(`TratamentoRetorno...:1068`, `ImportaCAMT0054`) é a verdade de liquidação
(`07-spi-moneypath.md:118-139,258-260`). Ou seja: o legado responde ACSP e credita o
cliente DEPOIS da confirmação de liquidação. Na recusa de crédito ele responde RJCT e a
operação não liquida (não há devolução pacs.004 gerada por recusa de crédito — a pacs.004
in é só a devolução RECEBIDA).

---

## 2. NOSSO — comportamento provado

Arquivo central: `pix/backend/apps/spi_service/lib/spi_service/workers/inbound_processor.ex`.

### 2.1 Dispatch: assinatura + XSD antes de tudo (fail-closed)

`dispatch_message/2` (linha 185):
```
with :ok <- maybe_signature_check(msg_type, message),        # DS01 (XMLDSig)
     :ok <- maybe_validation_check_pacs008(msg_type, message) do   # XSD strict inbound
  "pacs.008" -> process_incoming_payment(...)
```
- Assinatura: `maybe_signature_check` (linha 322) verifica `<Sgntr>`; falha → `record_rejected_inbound(... "DS01" ...)` (linhas 347-362). Gated por `signature_enforcement_enabled?` = `:shared, :bacen, :enabled` (linhas 385-388) → em PROD (BACEN_ENABLED=true) é ENFORCED.
- XSD: `validate_message_or_reject` → `MessageValidator.validate(xml, direction: :inbound, strict: true)` (linha 275); reject → grava rejeição + `:rejected` (linhas 285-309). Fail-closed no pacs.008 (money path).

### 2.2 Pipeline de decisão do crédito (puro, short-circuit)

`process_single_incoming_payment` (linha 538) resolve o ISPB credor
(`message["creditor_ispb"] || parsed[:to_ispb] || Shared.Bacen.Client.ispb()`, linhas 548-551)
e roda, em ordem, com curto-circuito:
1. `check_spi_inbound` → `SpiValidator.validate_inbound_pacs008` (linha 572) — valida
   FormaIniciacao, TipoPrioridade e formato dos documentos debtor/creditor
   (`validators/spi_validator.ex:200-209`). Recusa → `{:reject, "CH16", ...}`.
2. `check_sanctions_inbound` (linha 574) → `SanctionsCheck.run` (adapter default Noop =
   `:proceed`; `Shared.Compliance.NoopSanctionsAdapter`).
3. `validate_creditor_account` (linha 577) — **RPC ao Core** (equivalente ao componente por
   IdSystem do legado).
4. `qr_check` (linha 589) — gate de QR cobrança por txid (só se conta OK).

Decisão pura `inbound_outcome/4` (linhas 595, 706): `{:credit, account_id}` |
`{:reject, code, desc, level, telemetry?}` | `{:retry, reason}`.

### 2.3 Validação da conta no Core (RPC) + de-para de motivo

`validate_creditor_account` (linha 1169): `Gnat.request(@nats_conn,
"pix.core.validate_account.request", payload, receive_timeout: 200)` (linhas 1173-1174).
- `build_account_validation_payload` (linha 1147) manda `creditor_cpf_cnpj/document/pix_key/
  account/issuer/account_type/ispb/end_to_end_id`.
- `credit_validation_result` (linha 1209):
  - `%{"valid" => true, "account_id" => id}` → `{:ok, id}`.
  - `%{"valid" => false}` → `{:reject, CreditErrorCodes.to_iso(reason), ...}` (linhas 1212-1216).
  - Timeout/erro/exceção/decode_error/resposta inesperada → `{:skip_validation, ...}` →
    `inbound_outcome` devolve `{:retry, reason}` → NAK/reentrega (linhas 679-680, 1182-1197).
    **Indisponibilidade nossa NUNCA vira rejeição** (evita AB03 indevido; incidente
    2026-07-09).

De-para `SpiService.Spi.CreditErrorCodes` (`spi/credit_error_codes.ex`) — cita
`ConversaoValidationError.cs:12-70` no moduledoc; cobre AC03/AC06/AC07/AC14/AG03/BE01/CH11/
DS04/AB09/ED05, fallback `ED05`, e passthrough de código ISO já-canônico (`@known_iso`,
linha 47). **DT05 NÃO está na tabela** (`@known_iso` não inclui DT05).

### 2.4 Crédito + resposta ACSP

`process_validated_payment` → `do_process_validated_payment` (linhas 985, 1011):
- Dedup Redis SET-NX por `resource_id = message_id` (`check_message_dedup`, linhas 138-171,
  1000).
- `Repo.transaction`: `upsert_inbound_in_tx` (PK `bacen_inbound (ispb, resource_id)`,
  migração `20260308500001_fix_bacen_inbound_pk.exs:44`) + `create_transaction_in_tx`
  (unique `messages (end_to_end_id, operation_time)`, migração
  `20260716160000_create_e2e_outbound_burns.exs:11-12`) + `create_payment_in_tx`
  (linhas 1019-1026). Duplicata → rollback limpo (linhas 1044-1058).
- `update_pi_balance(:credit, ...)` credita a Conta PI (linha 1074).
- `publish_transaction_event(pacs008_event_type(), ...)` — evento de crédito ao Core via
  outbox durável (linha 1100). `pacs008_event_type` (linhas 3974-3985): flag
  `:two_phase_pix_in_enabled` (default **false**) → publica `transaction.created` (Core
  credita já); true → `transaction.received` (Core espera ACCC).
- **`send_pacs002_response(message, "ACSP")`** (linha 1107). Comentário 1102-1106: "Antes
  mandava ACCC ... um aceite fantasma ... agora ACSP". Paridade explícita com o legado.
- `maybe_link_qr_payment` (linha 1114, fail-soft).
- `advance_inbound_credit_to_accc(tx)` (linha 1121): como a entrada NÃO recebe pacs.002 de
  volta, avança a linha PDNG→ACCC(3) para refletir a realidade; fail-soft (linhas 3358-3380).

`send_pacs002_response` (linha 3488): recupera o E2E original do XML se o envelope não traz
(linha 3493), `sanitize_reason_code` garante `Cd` dentro do enum `ExternalStatusReason1Code`
(inválido → ED05 + log CRÍTICO, linhas 3566-3580), `additional_info` → `<AddtlInf>`
(linhas 3585-3596). Sai por outbox `monetarie.spi.outbound.pacs002` (linhas 3536-3538).

### 2.5 Recusa e devolução automática

`process_single_incoming_payment` (linhas 600-642):
- Reject de conta (AC03/AC06/AC07/AC14/AG03) → `process_credit_returned`
  (linhas 619-620, 866): grava inbound, publica `RETURN_CREATED` (→ ReturnProcessor →
  pacs.004) e **responde `send_pacs002_response(message, "ACSP")`** (linha 908) — aceita para
  o BACEN e devolve por pacs.004. `@auto_return_reason_codes` (linha 839).
- Reject de SPI/sanção/QR (ou conta fora da whitelist) → `record_rejected_inbound`
  (linha 622) → `send_pacs002_response(message, "RJCT", reason_code, errors_detail)`
  (linha 1275).

---

## 3. GAPS (legado x nosso)

### G1 — Resposta a crédito inaplicável: RJCT (legado) x ACSP + pacs.004 (nosso). DIVERGÊNCIA. Risco MEDIO
- Legado: conta inválida/bloqueada/encerrada → **pacs.002 RJCT** com AC03/AC06/AC07/AC14/AG03
  (`MontaDetalhePACS002` Status=Rejeitado, `...Geral.Application.decompiled.cs:923,946`;
  de-para em `General.decompiled.cs:225-247`). A operação não liquida; não há pacs.004
  gerada por recusa.
- Nosso: mesmos códigos → **pacs.002 ACSP + devolução automática pacs.004**
  (`inbound_processor.ex:619-620,866-908`, `@auto_return_reason_codes` linha 839).
- Origem da divergência: achado canônico "o BACEN liquida ANTES de entregar a pacs.008"
  (memória `monetarie-pix-in-credit-path-canonical`), então não daria para RJCT um crédito já
  liquidado — aceita e devolve. Comportamentos genuinamente diferentes; não há prova no dump
  de qual o BACEN atual exige. Verificar contra o Manual/Catálogo SPI vigente antes de tratar
  como defeito.

### G2 — Validação de crédito: plugins por IdSystem (legado) x RPC único ao Core (nosso). DIVERGÊNCIA. Risco INFO
- Legado: `IValidacaoCreditoUseCase.Validacao(item, IdSystem)` instanciado por IdSystem
  (`FilaEntrada.Infrastructure.decompiled.cs:1122,1130-1143`); componentes concretos por
  banco cliente (`Multiliquidacao.Core.Worker.ValidacaoCredito.decompiled.cs:260-265`).
- Nosso: um `Gnat.request` ao Core (`inbound_processor.ex:1173`). Monetarie é SCD única, então
  1 integração é suficiente; mas não há roteamento multi-IdSystem/multi-core. Só relevante se
  a Monetarie passar a liquidar indiretos com cores distintos.

### G3 — DT05 (devolução fora do prazo) não mapeada. AUSENTE. Risco BAIXO
- Legado: `DevolucaoExtrapolaOPrazoMaximo → DT05` (`General.decompiled.cs:265-266`).
- Nosso: `CreditErrorCodes` não inclui DT05; `@known_iso` sem DT05
  (`credit_error_codes.ex:47`). Um motivo interno "prazo de devolução" cairia em ED05. Impacto
  restrito à validação de crédito de pacs.004 in (devolução recebida), não ao pacs.008 puro.

### G4 — Momento do crédito: pós-liquidação (legado) x no ACSP (nosso, flag OFF). DIVERGÊNCIA. Risco INFO
- Legado: crédito ao cliente confirmado só após camt.054 BOOK, via `retornolegado`
  (`07-spi-moneypath.md:118-139,258-260`).
- Nosso: com `two_phase_pix_in_enabled=false` (default), `transaction.created` credita já no
  ACSP (`inbound_processor.ex:1093-1100,3974-3985`). Flag existe para o modo two-phase mas
  está OFF. Coerente com o mesmo achado canônico do G1.

### G5 — Sanções/PLD no inbound: nosso adiciona; legado não tem. DIVERGÊNCIA. Risco INFO
- Legado: antifraude é só PRÉ-transação no PIX OUT (`07-spi-moneypath.md:53-68,291-301`); não
  há screening de sanções no crédito recebido.
- Nosso: `check_sanctions_inbound` no pipeline (`inbound_processor.ex:574,728-734`), adapter
  default Noop (`:proceed`) — inócuo até configurar o `NatsSanctionsAdapter`. Recusa por
  sanção → RJCT (não devolve). Enriquecimento nosso, não gap contra o legado.

### G6 — Participante indireto (IspbIF/LPI): legado completo; nosso assume ISPB único. DIVERGÊNCIA. Risco BAIXO
- Legado: separa `IspbIF` (liquidante direto) de `IspbDebtor/IspbCreditor` e movimenta a
  Conta PI por LPI0001-0004 (Propria/Liquidante/SME)
  (`20201106_TratamentoPartIndiretoCreditoEDebito.sql`; `07-spi-moneypath.md:214-231`).
- Nosso: no pacs.008-in o `creditor_ispb` resolve para o NOSSO ISPB
  (`inbound_processor.ex:548-551`) e `update_pi_balance` credita a PI genericamente. Sem
  distinção liquidante/indireto. Gap geral já mapeado; para pacs.008-in é latente enquanto a
  Monetarie não liquidar indiretos.

---

## 4. COBERTO (paridade confirmada — para dar confiança)

- **C1 — Validação de crédito no inbound + resposta ACSP/RJCT**: ambos validam o crédito ao
  recebedor e respondem pacs.002 (aceite/recusa). Legado
  `EfetuaValidacaoCredito`+`MontaDetalhePACS002`; nosso `process_single_incoming_payment`+
  `send_pacs002_response`.
- **C2 — De-para motivo→ISO idêntica** (AC03/AC06/AC07/AC14/AG03/BE01/CH11/DS04/AB09/ED05,
  fallback ED05): `ConversaoValidationError` (`General.decompiled.cs:218-286`) x
  `CreditErrorCodes` (`credit_error_codes.ex`, cita a fonte legada).
- **C3 — Dedup de inbound**: legado `ExisteMsgRecebida(IdSystem, UniqueId, DtMovto)` +
  registry `SPI_UPDDADOSRECEPMSG2` (E2E `FlUtilizado='S'`, RtrId, MsgId, UniqueId); nosso
  Redis SET-NX(message_id) + PK `bacen_inbound(ispb, resource_id)` + unique
  `messages(end_to_end_id, operation_time)`. Ambos impedem duplo-crédito por reentrega.
- **C4 — XSD do XML recebido**: legado `_validaXsd.Validate2`
  (`Mensageria.Application.decompiled.cs:86`); nosso `MessageValidator.validate(direction:
  :inbound, strict: true)` fail-closed no pacs.008 (`inbound_processor.ex:194,275`).
- **C5 — Verificação de assinatura do XML recebido**: legado `VerificaAssinaturaXml` gated
  `ValidarAssinatura` (`SignXml.Application.decompiled.cs:175,347-354`;
  `Mensageria.Application.decompiled.cs:87`); nosso `maybe_signature_check` (DS01) gated
  `bacen.enabled` (`inbound_processor.ex:322-388`).
- **C6 — Aceite sai ACSP (não ACCC)**: legado `ValidadoCreditor=ACSP`
  (`General.decompiled.cs:8246-8253`; `...Geral.Application.decompiled.cs:923`); nosso
  `send_pacs002_response("ACSP")` (`inbound_processor.ex:1107`, mudança explícita ACCC→ACSP).
- **C7 — AddtlInf no RJCT**: legado `AddData=erro.Msg` para DS04/ED05
  (`General.decompiled.cs:281-284`); nosso `format_errors_detail_for_addtl_inf` → `<AddtlInf>`
  (`inbound_processor.ex:3585-3596`).
- **C8 — Falha transitória não rejeita**: legado terminal-safe `SPI_SPUPDSTATUSMESSAGE`
  (`07-spi-moneypath.md:246-256`) nunca reverte terminal; nosso `{:retry}` → NAK/reentrega em
  indisponibilidade do Core, sem rejeição indevida (`inbound_processor.ex:625-641,679-680`).
- **C9 — Guarda de código de rejeição XSD-válido**: nosso `sanitize_reason_code` força `Cd` no
  enum `ExternalStatusReason1Code` (inválido→ED05+log crítico,
  `inbound_processor.ex:3566-3580`); legado usa o enum fixo `enumReasonCode` na origem.
- **C10 — Casamento por E2E (pagamento) e E2E+RtrId (devolução)**: legado `TrataPayment` por
  `EndToEndId` e `TrataDevolucao` por `EndToEndIdMsgDev+RtrId`
  (`FilaEntrada.Infrastructure.decompiled.cs:1081,1167`); nosso resolve/propaga E2E original
  na pacs.002 (`inbound_processor.ex:3493,3543-3561`) e trata pacs.004 in em caminho próprio.

---

## 5. Limitações de grounding (honestas)

- Sem pacs.008 recebida real no dump legado (`crk_spi.dbo.SpiMessage` = 44 linhas, todas
  CAMT.060/envio). Comportamento provado pelo decompilado + scripts SQL, não por dados vivos.
- O comportamento "correto" do BACEN atual (RJCT x ACSP+pacs.004 no crédito inaplicável)
  não foi confrontado com o Manual/Catálogo SPI vigente nesta auditoria — G1 fica como
  divergência a validar, não como defeito confirmado.
- O que o componente por IdSystem valida internamente (regras exatas de conta) não está no
  repo decompilado (é integração externa por banco cliente); inferido pela de-para de erros.
