# Dossiê de paridade profunda — pacs.008 ENVIO (PIX out)

Auditoria READ-ONLY, legado (.NET decompilado + DB SQL Server) x nossa cabine PIX
(Elixir umbrella). Zero inferência: cada afirmação cita prova arquivo:linha ou
tabela.coluna. Onde não houve prova suficiente, o veredito é INCONCLUSIVO.

Escopo: montagem, campos, validações, gatilho, transições de status e tratamento
de erro da mensagem pacs.008 (FIToFICstmrCdtTrf) ENVIADA ao BACEN/SPI.

---

## 1. Mapa do gatilho e do pipeline

### 1.1 Legado (.NET SPI.Core)

Fonte da pacs.008 de saída = tabela `crk_spi.dbo.SpiPayment` (dados do pagamento)
+ `crk_spi.dbo.SpiMessage` (índice da mensagem). O `MotorAntiFraudeUseCase`
gera o E2E e monta a mensagem via `PaymentDTO`/`PaymentFullDTO`
(SPI.Core.Application.decompiled.cs:6709-7077). O XML é serializado pelos
templates versionados `Book.v111` (v14/v15) em
SPI.Core.Mensageria.Application.Book.v111.decompiled.cs.

Pipeline interno (worker, `enumStatusOperacao` em
SPI.Core.General.decompiled.cs:7114-7216):
`EntradaDoRegistro` → `EntradaCriptografia`/`CriptografadaOk` (assinatura) →
`PendenteEnvio`/`EntradaEnvioApiBacen`/`Enviada` → `AguardandoRetorno`/
`EmProcessamento` → `Efetivada` | `Rejeitada` | `ErroValidacaoRetorno` (ADMI.002).
Gates opcionais no caminho: `MotorAntiFraude`
(`EnviadaMotorAntiFraude`→`Aprovada/ReprovadaPeloMotorAntiFraude/Timeout/Fallback`,
SPI.Core.Application.decompiled.cs:6795-6903) e alçada
(`PendenteAprovacao`→`Aprovada/Reprovada`).

Status de negócio da mensagem (`SpiMessage.IdStatus`) usa os `TxSts` do SPI
(`CRK_SPIDOMINIO.dbo.SpiStatus`): PDNG, ACSP, ACCC, ACSC, RJCT (com `IcSts`
P/F/R).

### 1.2 Nosso (Elixir)

Gatilho real (Core→PIX): evento NATS `payment_request` →
`SettlementService.Workers.CoreEventProcessor.handle_payment_request`
(core_event_processor.ex:226-267). Sequência:
`resolve_sender_ispb` (fail-closed, :249) → `resolve_e2e_id`
(reusa E2E da consulta DICT cacheada, :432-448) → `peek_consult_entry`
(recebedor da consulta, :462) → `check_and_block_balance` (debita PI antes de
enviar, :299) → `do_payment_request` (:530): cria a linha
`monetarie_spi.messages` em PDNG (status_id 1, :571), queima o E2E
(`Shared.E2eBurn`, :599), roda `Pacs008SendValidator.validate` (:653), monta o
XML via `Shared.Bacen.Iso20022.MessageBuilder.build("pacs.008", ...)` (:679) e
publica `monetarie.spi.outbound.send` (:733).

Envio: `SpiService.Workers.OutboundSender.do_dispatch_send`
(outbound_sender.ex:453) — send gate (alçada) → assinatura XMLDSig fail-closed →
gate XSD fail-closed → screening de sanções → claim → POST ICOM. Em ACK HTTP:
status 2/ACSP (:506). Desfecho terminal vem da pacs.002.

Mapa de status nosso (`monetarie_spi.messages.status_id`): 1=PDNG, 2=ACSP,
3=ACCC, 4=ACSC, 5=ACTC, 6=ACWC, 7=STLD, 8=RJCT, 9=CANC, 10=RTRN.

Caminho manual paralelo (Construir Mensagem):
`SettlementServiceWeb.MessageController.send_message` (message_controller.ex:125)
→ mesmo `Pacs008SendValidator` (:178) → mesmo `MessageBuilder.build` (:197),
porém com o mapa COMPLETO do formulário (tx_id/purpose/remittance wired,
:1074-1077).

---

## 2. Montagem da pacs.008 — comparação campo a campo

Template legado de referência: `v15.GeraNodeOperacao`
(Book.v111.decompiled.cs:4429) — o mais completo (Purp + Tax + Strd +
MndtRltdInf). Nosso: `MessageBuilder.render_cdt_trf_tx_inf`
(message_builder.ex:224-247) + `build_pacs008_envelope` (:204-219).

| Campo XML | Legado (prova) | Nosso (prova) | Veredito |
|---|---|---|---|
| GrpHdr/MsgId | `GeraGroupHeader` messageId (Book.v111:4560) | `p.msg_id` (message_builder.ex:209) | coberto |
| GrpHdr/CreDtTm | dtHrMsg (Book.v111:4562) | `fmt_dt(p.created_at)` (:210) | coberto |
| GrpHdr/NbOfTxs | count (Book.v111:4562) | `length(blocks)` (:211) | coberto |
| GrpHdr/SttlmInf/SttlmMtd | `dsMetodo` = IdMetodo.Descricao() (Book.v111:4562) | fixo `CLRG` (:212) | coberto (SPI = CLRG) |
| GrpHdr/PmtTpInf/InstrPrty | `dsPriority` = IdPriority.Descricao() HIGH/NORM (Book.v111:4562; enum default High, Domain:937) | `instruction_priority||priority||"HIGH"` (:213) | coberto |
| GrpHdr/PmtTpInf/SvcLvl/Prtry | `tpPrioridade` = TipoPrioridade PAGPRI/PAGFRD/PAGAGD (Book.v111:4562) | fixo `service_level||"PAGPRI"` (:213) | DIVERGÊNCIA (ver G5) |
| CdtTrfTxInf/PmtId/InstrId | `EndToEndId` (v11 templates Book.v111:3468) | ausente no v15/nosso | coberto (v15 não tem InstrId) |
| CdtTrfTxInf/PmtId/EndToEndId | `msg.EndToEndId` (Book.v111:4429) | `v[:end_to_end_id]` (:229) | coberto |
| CdtTrfTxInf/PmtId/TxId | `GeraTxId` condicional (Book.v111:4551) | `pacs008_txid` condicional (:290-292) | coberto (builder); ver G1 (auto flow) |
| IntrBkSttlmAmt Ccy | `Ccy` + NumberToStringComDecimais (Book.v111:4429) | `Ccy="BRL"` fixo + fmt_amount (:230) | coberto |
| AccptncDtTm | DtHrAcceptance ?? DtHrOperacao (Book.v111:4429) | `acceptance_datetime||created_at` (:231) | coberto |
| ChrgBr | `ChargeBearer.Descricao()`, default SLEV (Domain:945; EF default Angular.Infra:5998) | fixo `SLEV` (:232) | coberto |
| MndtRltdInf/Tp/LclInstrm/Prtry | `FormaIniciacao` (Book.v111:4429) | `pacs008_lcl_instrm` (:233,298-304) | coberto (builder); ver G1 |
| InitgPty | `GeraInitgPty` condicional (Book.v111:4542-4549) | `pacs008_initg_pty` condicional (:307-310) | coberto (builder); ver G1/G3 |
| Dbtr/Nm | `XmlEncode(NmDebtor)` (Book.v111:4429) | `debtor_name||""` (:235) | coberto |
| Dbtr/Id/PrvtId/Othr/Id | `CpfCnpjDebtor` sempre PrvtId (Book.v111:4429) | `debtor_document` sempre PrvtId (:235) | coberto (paridade; ver G10) |
| DbtrAcct/Id/Othr/Id[+Issr]/Tp/Cd | IdAccountDebtor + Issr + TypeAccountDebtor (Book.v111:4429) | `pacs008_account("DbtrAcct",...)` (:236,315-327) | coberto |
| DbtrAgt/.../MmbId | `IspbDebtor` (Book.v111:4429) | `v[:debtor_ispb]` (:237) | coberto |
| CdtrAgt/.../MmbId | `IspbCreditor` (Book.v111:4429) | `v[:creditor_ispb]` (:238) | coberto |
| Cdtr/Id/PrvtId/Othr/Id | `CpfCnpjCreditor` sempre PrvtId (Book.v111:4429) | `creditor_document` sempre PrvtId (:239) | coberto (paridade) |
| CdtrAcct + Prxy | IdAccountCreditor + Issr + Tp/Cd + `GeraProxyIdCreditor` (Book.v111:4515-4521) | `pacs008_account("CdtrAcct",...,proxy_key)` (:240,323) | coberto |
| Purp/Cd | `FinalidadeTransacao` (Book.v111:4429) | `pacs008_purpose`, default IPAY (:241,344-345) | coberto (builder); ver G1/G8 |
| Tax (RefNb + 2 Rcrd CBS/IBS) | `GeraTax` (Book.v111:4465-4513) | `pacs008_tax` version-gated 1.15 (:242,351-363) | coberto |
| RmtInf/Ustrd | `XmlEncode(Ustrd)` (Book.v111:4429) | `pacs008_rmt_inf` (:243,366-367) | coberto (builder); ver G1 |
| RmtInf/Strd (RfrdDocInf/RfrdDocAmt) | `GeraStrd`: ModalidadeAgente, PrestadorSaque, ValorTipo (Book.v111:4432-4463) | AUSENTE (só Ustrd, :366-367) | AUSENTE (ver G2) |

Envelope/AppHdr e assinatura: legado assina na etapa de criptografia
(`enumStatusOperacao.CriptografadaOk`); nosso assina em `sign_outbound_xml`
fail-closed antes do POST (outbound_sender.ex:471) e valida o XML JÁ ASSINADO
contra o XSD oficial (:472-473). Paridade estrutural coberta.

---

## 3. Validações de negócio no envio — legado x nosso

Legado: `ValidaPaymentUseCase.ValidaDados`
(SPI.Core.Application.decompiled.cs:4091-4271) + validadores em
`UtilsValidacao` (:2079-2270) + `ValidaInitiationParty` (:1650-1656).
Nosso: `SettlementService.Spi.Pacs008SendValidator` (pacs008_send_validator.ex)
+ checagens espalhadas no `CoreEventProcessor`/`OutboundSender` + gate XSD.

| Regra | Legado (prova) | Nosso (prova) | Veredito |
|---|---|---|---|
| Forma de iniciação MANU/DICT/QRES/QRDN/INIC/AUTO/APDN x TxId/chave | ValidaDados:4151-4220 | Pacs008SendValidator:65-166 | coberto |
| DICT exige chave; MANU sem chave/TxId | :4151-4172 | :80-109 | coberto |
| QRES TxId<=25; QRDN TxId 26-35 | :4173-4194 | :111-135 | coberto |
| INIC exige InitgPty + TxId<=25 + chave | :4195-4209 | :137-152 | coberto (presença) |
| AUTO/APDN TxId 26-35 | :4210-4216 | :154-160 | coberto |
| InitgPty deve ser CNPJ VÁLIDO | ValidaInitiationParty:1652 (CpfCnpj.ValidarCNPJ) | só presença p/ INIC (:142-144) | PARCIAL (ver G3) |
| Prioridade: TipoPrioridade PAGPRI/PAGFRD/PAGAGD + IdPriority 1/2 cruzados | ValidaDados:4221-4232 | só InstrPrty ISO HIGH/NORM (:174-186) | DIVERGÊNCIA (ver G5) |
| Finalidade domínio fechado {IPAY,GSCB,OTHR,REFU,LOAN,IPRT} | :4233-4266 | domínio aberto, defere ao XSD (:224-229) | DIVERGÊNCIA (ver G8) |
| Purpose x tipos de valor (GSCB 2, OTHR 1, instant 0, >0) | :4233-4262 | :190-239 | coberto |
| ValorTipo/Tributos: CBSSPLIT+IBSSPLIT (2), Ctgy INF, DocFiscal alfanum<=50 | ValidaTributos:4273-4310 | AUSENTE | AUSENTE (ver G6) |
| ModalidadeAgente AGFSS/AGTEC/AGTOT | ValidaDados:4143-4146 | AUSENTE | AUSENTE (ver G7) |
| PrestadorSaque = ISPB 8 dígitos | ValidaDados:4147-4150 | AUSENTE | AUSENTE (ver G7) |
| Valor > 0 | ValidaValor:2148-2154 | check_and_block_balance :1751 (`compare>0`) | coberto (funcional) |
| Documento pagador/recebedor: obrigatório + válido | ValidaDocto:2079-2110 (check-dígito) | require_debtor_document! (:415-430) + gate XSD pattern | PARCIAL (ver G9) |
| Currency BRL | ValidaCurrency:1638-1648 (default BRL) | fixo BRL (message_builder.ex:230) | coberto |
| Idempotência (UniqueId único, já recebido) | ValidaUniqueId:2218-2258 | E2eBurn estrutural (:599) + unique_id (:565) | coberto |
| ISPB debtor/creditor resolvíveis | ValidaIspbDebtor/Creditor:4126-4127 | resolve_sender_ispb fail-closed (:249) + creditor_ispb raise (:561) | coberto |
| ChargeBearer válido | ValidaChargeBearer:2112-2122 | fixo SLEV | coberto (funcional) |
| Método de liquidação válido | ValidaMetodo:2124-2134 | fixo CLRG | coberto (funcional) |
| Motor antifraude externo (URL config) antes do envio | MotorAntiFraudeUseCase:6709-6903 | screening de sanções OFAC/PLD (outbound_sender.ex:585); sem motor externo plugável | DIVERGÊNCIA (ver G4) |
| Alçada (4 olhos) | WorkerPosAlcada/ValidacaoAlcadaRepository | Shared.Alcada.SendGate (flag OFF, só manual) | coberto (ver nota G4/alçada) |

---

## 4. GAPS

### G1 — Fluxo automático Core→PIX valida mas NÃO emite TxId/Purpose/InitgPty/RmtInf/value_types (PARCIAL, alto)

O `CoreEventProcessor.do_payment_request` passa `tx_id`, `purpose`,
`initiating_party_cnpj`, `value_types` para o `Pacs008SendValidator`
(core_event_processor.ex:656-660), mas a chamada real do
`MessageBuilder.build("pacs.008", ...)` (:679-707) NÃO inclui esses campos nem
`remittance_info`/`tax`. O builder então emite TxId="", InitgPty="", Purp=IPAY,
sem RmtInf/Strd. Consequência: um pagamento por QR (local_instrument=QRDN/QRES)
originado pelo Core passa na validação (TxId presente) mas gera uma pacs.008 com
`LclInstrm/Prtry=QRDN` SEM `<TxId>` — inconsistente para o BACEN. Pagamentos INIC
perdem o InitgPty; a descrição (Ustrd) do Core não trafega. O caminho manual
(MessageController) NÃO tem essa lacuna (:1074-1077 wired).

### G2 — Bloco Strd (PIX Saque/Troco: ModalidadeAgente, PrestadorSaque, ValorTipo) ausente no builder (AUSENTE, médio)

Legado `GeraStrd` (Book.v111.decompiled.cs:4432-4463) emite
`RfrdDocInf` (ModalidadeAgente `CdOrPrtry/Prtry`, PrestadorSaque `Issr`) e
`RfrdDocAmt/AdjstmntAmtAndRsn` (ValorTipo VLCP/VLDN). Nosso `pacs008_rmt_inf`
(message_builder.ex:366-367) só emite `Ustrd`. Assim uma pacs.008 GSCB (Troco)
ou OTHR (Saque) — que o `Pacs008SendValidator` até valida quanto ao count de
valores (:211-222) — sairia sem os valores de saque/compra e sem a modalidade do
agente. Latente: SCD provavelmente não oferta Saque/Troco (nota no moduledoc do
validador, pacs008_send_validator.ex:41-43), mas a divergência é real.

### G3 — InitgPty sem validação de CNPJ e não emitido no fluxo automático (PARCIAL, médio)

Legado `ValidaInitiationParty` (SPI.Core.Application.decompiled.cs:1652) rejeita
InitgPty que não seja CNPJ válido (`CpfCnpj.ValidarCNPJ`). Nosso
`Pacs008SendValidator` só checa PRESENÇA de `initiating_party_cnpj` para a forma
INIC (:142-144), sem validar o CNPJ. Some-se a G1: o campo nem chega ao builder
no fluxo automático. SpiPayment.InitgPty é `char(14)` (tabela confirma o formato
CNPJ).

### G4 — Motor antifraude externo pluggável ausente (DIVERGÊNCIA, médio)

Legado chama um motor antifraude externo configurável (flag
`AtivaMotorAntiFraude`, `UrlMotorAntiFraude`, POST HTTPS com Origem/Destino
mascarados, políticas de fallback/timeout/circuit-breaker) ANTES de emitir a
pacs.008 (MotorAntiFraudeUseCase, SPI.Core.Application.decompiled.cs:6709-6903;
histórico em `enumStatusOperacao.EnviadaMotorAntiFraude` etc.). Nosso caminho tem
screening de SANÇÕES (OFAC/PLD, fail-closed, outbound_sender.ex:585-595) mas NÃO
um motor de fraude externo plugável com as mesmas políticas. Cobertura parcial
por natureza diferente (sanções ≠ score de fraude). A alçada existe nos dois,
porém a nossa está OFF por default e isenta o fluxo automático do Core por design
(send_gate.ex moduledoc) — legado aplica alçada também no caminho de pagamento.

### G5 — Prioridade: modelo ISO (HIGH/NORM) x códigos domésticos + cross-check (DIVERGÊNCIA, baixo)

Legado valida `TipoPrioridade` ∈ {PAGPRI,PAGFRD,PAGAGD} e cruza com `IdPriority`
(PAGPRI⇒1; demais⇒2) (ValidaDados:4221-4232), e o XML carrega
`SvcLvl/Prtry = TipoPrioridade` (Book.v111:4562). Nosso valida só o InstrPrty ISO
HIGH/NORM (pacs008_send_validator.ex:174-186) e emite `SvcLvl/Prtry` FIXO
`PAGPRI` (message_builder.ex:213). Consequência: pagamento por fraude (PAGFRD) ou
agendado (PAGAGD) sai rotulado PAGPRI; não há a checagem cruzada
prioridade↔InstrPrty do legado.

### G6 — Validação de Tributos ausente (PARCIAL, baixo)

Legado `ValidaTributos` (SPI.Core.Application.decompiled.cs:4273-4310): exige
exatamente 2 registros (CBSSPLIT + IBSSPLIT), `Categoria=INF`, `DocFiscal`
alfanumérico ≤50. Nosso `Pacs008SendValidator` não valida tributos; o builder até
monta o `<Tax>` (message_builder.ex:354-363) mas o fluxo automático não passa
`tax` (G1) e não há gate semântico — só o XSD 1.15 pega erro estrutural.

### G7 — ModalidadeAgente e PrestadorSaque sem validação (AUSENTE, baixo)

Legado valida ModalidadeAgente ∈ {AGFSS,AGTEC,AGTOT} (:4143-4146) e PrestadorSaque
= ISPB de 8 dígitos numéricos (:4147-4150). Nosso não valida nenhum (nem emite,
G2). Latente enquanto SCD não ofertar Saque/Troco.

### G8 — Finalidade: domínio fechado x aberto (DIVERGÊNCIA, baixo)

Legado rejeita `FinalidadeTransacao` fora de {IPAY,GSCB,OTHR,REFU,LOAN,IPRT}
(:4263-4266). Nosso `validate_purpose` trata IPAY/REFU/IPRT como instantâneo,
GSCB/OTHR com valores, e deixa QUALQUER outro passar (defere ao gate XSD)
(pacs008_send_validator.ex:224-229). Além disso o conjunto "instantâneo" nosso
não inclui LOAN (que o legado inclui em :4233); comentário nosso indica LOAN
removido do domínio 5.12 (message_builder.ex:341-343) — decisão consciente, não
defeito, mas é uma divergência de regra.

### G9 — Validação de CPF/CNPJ (dígito verificador) só via XSD pattern (DIVERGÊNCIA, baixo)

Legado `ValidaDocto` (:2079-2110, `Validacoes.ValidaDocto`) valida o dígito
verificador do CPF/CNPJ (salvo parâmetro `NaoValidarDocumentoRecebido=S`). Nosso
exige presença do documento do pagador (core_event_processor.ex:415-430) e conta
com o gate XSD, que enforce só o PADRÃO `[0-9]{11}|[0-9A-Z]{12}[0-9]{2}` (formato,
não dígito verificador). Documento com dígito inválido mas formato correto passa
no nosso e seria rejeitado pelo BACEN, não pela cabine.

---

## 5. COBERTOS relevantes (confiança)

- C1 Envelope/GrpHdr (MsgId, CreDtTm, NbOfTxs, SttlmMtd=CLRG, InstrPrty) —
  message_builder.ex:204-219 x Book.v111:4560-4562.
- C2 Núcleo CdtTrfTxInf (PmtId/EndToEndId, IntrBkSttlmAmt, AccptncDtTm, ChrgBr,
  Dbtr/DbtrAcct/DbtrAgt, CdtrAgt/Cdtr/CdtrAcct, Purp) — :224-247 x Book.v111:4429.
- C3 ChrgBr=SLEV (paridade: legado default SLEV, Domain:945; nosso fixo, :232).
- C4 Regras de forma de iniciação (MANU/DICT/QRES/QRDN/INIC/AUTO/APDN x
  TxId/chave/InitgPty) — pacs008_send_validator.ex:65-166 x ValidaDados:4151-4220.
- C5 TxId condicional (não preenchido em pagamento por chave/manual, guard CH16) —
  :290-292 x GeraTxId Book.v111:4551.
- C6 Purpose x tipos de valor (GSCB=2, OTHR=1, instantâneo=0, >0) — :190-239 x
  ValidaDados:4233-4262.
- C7 LclInstrm inferido (DICT se chave, MANU senão) — :298-304 x FormaIniciacao.
- C8 E2E reusado da consulta DICT (lookup-to-pay) — core_event_processor.ex:432-448
  x GeraEndToEndId (MotorAntiFraude:6711).
- C9 Valor > 0 — check_and_block_balance:1751 x ValidaValor:2148-2154.
- C10 Idempotência/dedup — E2eBurn:599 + unique_id:565 x ValidaUniqueId:2218-2258.
- C11 Documento do pagador obrigatório + pattern XSD — :415-430 + gate XSD x
  ValidaDocto (presença).
- C12 Currency BRL default — :230 x ValidaCurrency:1638-1648.
- C13 Bloco Tax CBS/IBS version-gated 1.15 — :354-363 x GeraTax:4465-4513.
- C14 Assinatura XMLDSig + gate XSD fail-closed antes do POST — outbound_sender.ex:471-473.
- C15 Separadores de conta/agência suprimidos — :320-335 (paridade com dado limpo
  em SpiPayment).
- C16 Status inicial PDNG→ACSP no ACK; terminal pela pacs.002 — :506,571 x
  SpiStatus (PDNG/ACSP/ACCC/ACSC/RJCT) + enumStatusOperacao pipeline.

## 6. Observação (info, sem risco)

- G10 Dbtr/Cdtr sempre em `<PrvtId>`: os DOIS lados emitem PrvtId para pagador e
  recebedor, inclusive quando o documento é CNPJ (message_builder.ex:235,239 x
  Book.v111:4429). É paridade estrita; se o BACEN aceita CNPJ em PrvtId no PIX,
  é coberto. Registrado como info por ser um ponto onde ISO 20022 "puro" usaria
  OrgId, mas ambos os sistemas convergem.
