# Dossie de paridade profunda — pacs.004 Devolucao (in + out)

Auditoria senior PIX, read-only, zero inferencia. Cada afirmacao tem prova
arquivo:linha ou tabela.coluna. Sem prova, o veredito e INCONCLUSIVO.

- Legado decompilado: `/Users/luizpenha/monetarie/.scratch/legado-pix-decompiled/*.decompiled.cs`
- Legado DB vivo (SQL Server): DBs `crk_spi`, `CRK_SPIDOMINIO`, `CRK_SPIMENSAGERIA`, `gid`, etc.
- Legado Angular: `/Users/luizpenha/monetarie/LegadoPIX/Pix/SPI/angular/src/`
- Nosso backend: `/Users/luizpenha/monetarie/pix/backend/apps/{spi_service,settlement_service,shared,dict_service}`

---

## 1. Escopo e taxonomia dos fluxos

A devolucao pacs.004 tem tres sub-fluxos no PIX/SPI. Este dossie cobre os tres,
com foco no que a PISTA pediu (ValidaAntiFraudeDevolucao, GeraRtrId, cadeia
original<->devolucao, reason codes; ReturnProcessor, ReturnIdBurn, CumulativeCap):

- **OUT-manual/cliente**: o recebedor de um PIX decide devolver (parcial ou
  total). Gera pacs.004, debita a Conta PI, envia ao BACEN.
- **OUT-automatica**: um credito ja liquidado pelo BACEN nao pode ser aplicado
  ao cliente (conta invalida/bloqueada/encerrada). O valor liquidou na Conta PI
  e precisa voltar ao pagador via pacs.004.
- **IN (devolucao recebida)**: um PIX que NOS enviamos e devolvido pelo PSP do
  recebedor. Chega uma pacs.004; aceitamos (pacs.002) e, na liquidacao
  confirmada (camt.054), creditamos o cliente pagador de volta.

Fora de escopo estrito (mecanismo adjacente, citado onde toca): MED / Mecanismo
Especial de Devolucao via DICT (`DICT.Core.Worker.Devolucao`, enum
`enumMotivoDevolucao` FRAUD/OPERATIONAL_FLAW em `DICT.Core.General.decompiled.cs:12953`),
que resulta num pacs.004 com BE08/FR01 executado pelo PSP pagador.

---

## 2. Sumario executivo

Paridade nuclear FORTE: geracao do RtrId (formato identico), cadeia
original<->devolucao, restricao "so devolve pagamento RECEBIDO e LIQUIDADO",
teto cumulativo por E2E, direcao IN (aceite + credito na confirmacao). Em
uniformidade estrutural NOSSA barreira e mais forte que a legada (RtrId queimado
em tabela + unique de particao + advisory lock; o legado so tinha um seed
sequence com try/catch silencioso).

Divergencias reais encontradas:

- **G1 (ALTO):** janela de devolucao de Pix Saque/Troco. O legado aplica 1 HORA
  para tpFinalidade OTHR/GSCB e 90 dias so para IPAY; nosso `SpiValidator` usa 90
  dias FIXO para tudo. Devolucao de Saque/Troco fora de 1h passa no nosso gate
  (o BACEN rejeitaria por DT05, mas nao barramos localmente).
- **G2 (MEDIO):** whitelist de reason codes diverge. Legado enum tem 15 codigos;
  nossa `@valid_reason_codes` tem 13 DIFERENTES. Os 4 manuais (BE08/FR01/MD06/SL02)
  coincidem, mas ha codigos que o legado aceita e nos rejeitamos (AM05, DS28,
  FOCR, NARR, RUTA, SL11, SL12, UPAY, AM23, RR06) e vice-versa.
- **G3 (MEDIO):** motor anti-fraude na devolucao. O legado tem gate opcional
  `ValidaAntiFraudeDevolucao` (pacs.004 TX_TYPE) chamando motor externo; nosso
  ReturnProcessor NAO tem nenhum gate anti-fraude na saida.
- **G4 (BAIXO/INFO):** `SpiValidator.validate_return_amount` (P6) esta MORTO
  (JOIN em `monetarie_spi.payments`, que nunca recebe linha, le 0 sempre) — mas
  o teto REAL e coberto pelo `CumulativeCap` no ReturnProcessor. Codigo morto
  que confunde, sem impacto de dinheiro.
- **G5 (BAIXO):** devolucao recebida (IN) nao valida que o OrgnlEndToEndId casa
  um pagamento que NOS enviamos, antes de aceitar; o legado casa por
  EndToEndIdOrig + RtrId contra a operacao original.
- **G6 (BAIXO/INFO):** o legado suporta LISTA de razoes por devolucao
  (`LstRazaoDevolucao`); nosso builder pacs.004 leva uma unica `return_reason`.

Itens cobertos com confianca: C1 RtrId, C2 cadeia, C3 "so recebido/liquidado",
C4 teto cumulativo, C5 unicidade do RtrId (nossa mais forte), C6 direcao IN
aceite+credito, C7 parcial, C8 auto-return em conta invalida.

---

## 3. Fluxo do LEGADO (com prova)

### 3.1 Geracao do RtrId

`SPI.Core.General.decompiled.cs:375`
```
public static string GetRtrId(string ispbOrigem, DateTime dtOperacao, int totalCaracteresNaSequencia = 11)
  => $"D{ispbOrigem}{dtOperacao.ToUniversalTime():yyyyMMddHHmm}{GeraRandomString(11)}";
```
Formato: `D` + ISPB(8) + `yyyyMMddHHmm`(12, UTC) + 11 alfanumericos = **32 chars**.
Identico em `Multiliquidacao.Core.General.decompiled.cs:235`. Persistencia com
"reserva" em `SPI.Core.Seeds.Infrastructure.decompiled.cs:373-393`
(`GeraRtrId` insere `SeedRtrId{Ispb,RtrId,DtMovto}` num try/catch que retorna
`null` em falha — NAO ha unique constraint provado, so a sequencia).

Na devolucao real o RtrId nasce como o proprio IdFimAFim da operacao de
devolucao: `Multiliquidacao.Core.Application.decompiled.cs:4738`
`informacaoPixDto.IdFimAFim = GeradorMsgId.GetRtrId(text, ...)`.

Armazenamento: `crk_spi.dbo.SpiMessage.RtrId varchar(35)`; em MessageFull
`RtrId char(32)` (`SPI.Core.Worker.FilaEntrada.Infrastructure.decompiled.cs:7320`).

### 3.2 Elegibilidade — OUT manual (DevolvePix)

Entrada pela tela `monitor-operacao-devolucao.component.ts:51` (DevolveOperacao)
-> `POST /api/Entrada/DevolvePix` (`monitor-operacao.service.ts:160`).

`Multiliquidacao.Core.Application.decompiled.cs:4069` DevolvePix:
- `4076` acha a operacao original por `IdOperacao`; nula => "Pagamento nao encontrado".
- `4081-4084` **so devolve RECEBIDO**: `_partControlado.FindByNrSpb(operacao.NrSpbRecebedor) == null`
  => erro "Pagamento original foi enviado. So e possivel devolver pagamentos recebidos."
- `4093-4129` monta a devolucao SWAPANDO pagador<->recebedor, `Devolucao=true`,
  `IdFimAFimOriginal = operacao.IdFimAFim`, `VlPagto = dtoDevSpi.Vlr` (parcial),
  `LstRazaoDevolucao=[{ReturnReasonCode=CodDevolucao, AddInfo=InfoAdicional}]`,
  `tpFinalidade="IPAY"`.

`Multiliquidacao.Core.Application.decompiled.cs:4715` Devolucao (validacoes duras):
- `4781` **janela IPAY**: `operOriginal.tpFinalidade=="IPAY" && (Now-DtPagto).TotalDays>90`
  => "Nao e possivel devolver pagamento apos 90 dias".
- `4786` **janela Saque/Troco**: `(tpFinalidade=="OTHR"||"GSCB") && (Now-DhOperacao).TotalHours>1`
  => "Nao e possivel devolver PIX Saque e Troco apos 1 hora".
- `4792` original sem IdFimAFim => erro.
- `4797` **valor unico**: `operOriginal.VlOperacao < dtoPagto.VlPagto` => "Valor devolvido e maior que o valor da operacao".
- `4802-4817` **teto cumulativo**: soma as devolucoes com
  `IdTipoSituacaoAtual == ConfPagtoRecebto_Ok` (`4806`) + a atual; se `> VlOperacao`
  => "Valor devolvido atual mais o anterior e maior que o valor da operacao".
- `4933` `retorno.RtrId = informacaoPixDto.IdFimAFim` (RtrId da devolucao).

### 3.3 Reason codes (LEGADO)

- Tela operador oferece **4**: `monitor-operacao-devolucao.component.html:23-26`
  BE08, FR01, MD06, SL02. Mesmos 4 no doc da API `Multiliquidacao.Web.Api.decompiled.cs:197`.
- Enum amplo (recebidas/geracao) **15**: `Multiliquidacao.Core.General.decompiled.cs:6283`
  enumReturnReasonCode = AM05, AM09, BE08, DS28, FOCR, FR01, MD06, NARR, RUTA,
  SL11, SL12, UPAY, SL02, AM23, RR06.
- Catalogo BACEN de codigos em `CRK_SPIDOMINIO.dbo.SpiReason` (grupo 5) inclui os
  relevantes a devolucao: AG13 "ForbiddenReturnPayment - Nao e permitido devolver
  a devolucao"; AM09 "Devolucao com valor diferente"; DT05 "extrapola prazo maximo
  para devolucao"; DS0H "ISPB do participante que submeteu resposta diferente do
  creditado". NAO ha whitelist server-side de codDevolucao no DevolvePix (a UI
  restringe; o BACEN e a autoridade final).
- MED (DICT): `enumMotivoDevolucao` FRAUD/OPERATIONAL_FLAW/REFUND_CANCELLED/PIX_AUTOMATICO
  (`DICT.Core.General.decompiled.cs:12953`); rejeicao `enumMotivoRejeicaoDevolucao`
  NO_BALANCE/ACCOUNT_CLOSURE/CANNOT_REFUND/OTHER/INVALID_REQUEST (`:12980`).

### 3.4 Anti-fraude na devolucao (LEGADO)

`SPI.Web.Api.decompiled.cs:953` `[HttpPost("validaAntiFraudeDevolucao")]` ->
`SPI.Core.Application.decompiled.cs:6717` `ValidaDevolucaoMotorAntifraude`:
- `6720` marca `CdMsg="PACS.004"`.
- `6721` `FindFullByEndToEndId(EndToEndIdMsgDev)` acha o pagamento ORIGINAL; se
  nao acha => `Reprovado, "Pagamento nao encontrado."` (`6722-6730`).
- `6733` gera o RtrId (`GeraRtrId(result.IspbDebtor, DtHrOperacao)`).
- `6737-6738` monta DTO TX_TYPE=PACS.004 e chama `AntiFraudeValidacao(..., ehPacs008:false)`.
- `AntiFraudeValidacao` (`6795`): so atua se `AtivaMotorAntiFraude` (flag,
  default false => passthrough `MotorHabilitado=false`); com motor ON, POST ao
  motor externo; reprovado grava `AdicionaMsgDev(..., ReprovadaPeloMotorAntiFraude)`.
  Gate OPCIONAL e configuravel.

### 3.5 Cadeia original<->devolucao (LEGADO)

`crk_spi.dbo.SpiMessage`: `EndToEndId char(32)`, `EndToEndIdOrig char(32)`,
`RtrId varchar(35)`, `IdOperacaoOrig bigint`, `MessageIdOrig varchar(32)`. A
devolucao referencia o pagamento pelo EndToEndIdOrig + IdOperacaoOrig + RtrId. A
correlacao de respostas usa `(OrgnlInstrId, OrgnlEndToEndId)`
(`SPI.Core.Mensageria.Geral.Application.decompiled.cs:1351`,`938`,`945`).

### 3.6 Devolucao RECEBIDA (IN, LEGADO)

`SPI.Core.Worker.FilaEntrada.Infrastructure.decompiled.cs:5201`
`HandlerDevolucaoPaymentFullDTO` processa a pacs.004 entrante; correlaciona por
`EndToEndIdMsgDev == EndToEndIdOrig && RtrId` (`:1167`). O credito da devolucao
recebida usa `tpServico = RecebimentoExternoDevolucao`
(`Multiliquidacao.ValidacaoCredito.Application.decompiled.cs:601`), com historico
"Devolucao SPI" (`:619`).

---

## 4. Fluxo NOSSO (com prova)

### 4.1 OUT — ReturnProcessor (constroi/envia pacs.004)

`apps/spi_service/lib/spi_service/workers/return_processor.ex`:
- `:37` `@rtr_id_re ~r/^D[0-9A-Z]{8}[0-9]{12}[a-zA-Z0-9]{11}$/` (32 chars, formato identico ao legado).
- `:40` `process_message(RETURN_CREATED)` -> `validate_return_eligibility`
  (`:184` amount>0 e original_end_to_end_id presente) -> `validate_spi_return_rules`
  (`:174` -> `SpiValidator.validate_outbound_return`) -> `claim_block_and_queue`.
- `:283-314` candidato de RtrId: usa o `return_id` do evento se casar o regex;
  senao deriva DETERMINISTICO da identidade do evento; colisao regenera via
  `MessageBuilder.generate_rtr_id`.
- `:401-505` `queue_outbound_return`: dentro de `Repo.transaction`,
  (a) `Shared.ReturnIdBurn.burn` (`:441`), (b) `Shared.Returns.CumulativeCap.guard`
  (`:465`), (c) `upsert_return_tracking` (linha propria pacs.004 OUTBOUND), (d)
  outbox `monetarie.spi.outbound.return` com o XML da pacs.004.
- `:367-399` `check_and_block_balance`: debita/bloqueia a Conta PI antes de enviar
  (DEBIT-THEN-SEND).
- `:718` `return_wire_agents`: DbtrAgt=nos (emissor), CdtrAgt=PSP do pagador
  original (convencao da devolucao provada por gabarito + RC09 vivo 2026-07-17).

### 4.2 ReturnIdBurn (unicidade estrutural do RtrId)

`apps/shared/lib/shared/return_id_burn.ex:45-63`: INSERT em
`monetarie_spi.return_id_registry (return_id, ispb, original_endtoend_id)` com
`ON CONFLICT (return_id) DO NOTHING`; `num_rows==1 -> :ok`, `0 -> {:error,:already_burned}`.
Roda no MESMO repo/transacao da linha (participa do rollback). Mais forte que o
seed sequence legado.

### 4.3 CumulativeCap (teto por E2E)

`apps/shared/lib/shared/returns/cumulative_cap.ex:44-65`: sob
`pg_advisory_xact_lock(hashtext(e2e))`, soma o valor (centavos) das pacs.004
OUTBOUND NAO rejeitadas do E2E (`status_id NOT IN (8,9)`, `:29`,`:101`); se
`previous + nova > original` => `{:error,{:exceeds_original,...}}`; sem E2E ou
sem valor original positivo => `:ok` (nao barra). Fail-closed, serializa
concorrentes. Cobre o caso real 0,50+0,25+0,25 de R$1,00.

### 4.4 SpiValidator (regras de saida)

`apps/spi_service/lib/spi_service/validators/spi_validator.ex`:
- `:21` `@valid_reason_codes ~w(MD06 SL02 BE08 FR01 AC03 AC06 AC07 AC14 AG03 AG13 AM09 AM18 RR04)` (13).
- `:30` `@max_return_days 90`; `:41` `validate_return_window` (FIXO 90 dias, sem
  distincao por tpFinalidade).
- `:62` `validate_return_amount` faz `get_previous_return_total` via JOIN em
  `monetarie_spi.payments` (nunca escrito na criacao da devolucao => le 0 sempre).
- `:216` `validate_outbound_return` = reason_code + janela (so se `original_date`
  presente) + amount.

### 4.5 OUT — cliente/IB via CoreEventProcessor

`apps/settlement_service/lib/settlement_service/workers/core_event_processor.ex:859`
`handle_return_request`:
- `:866` `fetch_original_inbound_credit` (`:939`): original ausente => `RETURN_ORIGINAL_NOT_FOUND`;
  OUTBOUND => `RETURN_ORIGINAL_NOT_INBOUND` "a devolucao via pacs.004 so vale para
  PIX recebido" (`:904-909`); nao liquidado => `RETURN_ORIGINAL_NOT_SETTLED`
  "exige liquidacao confirmada" (`:897-902`).
- `:1004` `build_return_created_event`: deriva valor/data/partes da pacs.008
  INBOUND real (`original_date = movement_date`, `:1012`), reason default MD06
  (`:1013`), publica `monetarie.spi.return.created` origin=client.

### 4.6 OUT — automatica (conta invalida)

`apps/spi_service/lib/spi_service/workers/inbound_processor.ex`:
- `:839` `@auto_return_reason_codes ~w(AC03 AC06 AC07 AC14 AG03)`.
- `:619` se `account_reject?` e `auto_return_reason?(code)` => `process_credit_returned`.
- `:866` credita a Conta PI (o valor liquidou), `send_pacs002_response("ACSP")`,
  publica RETURN_CREATED (`build_auto_return_event`, `:938`, com `original_date=hoje`).

### 4.7 IN — devolucao recebida

`apps/spi_service/lib/spi_service/workers/inbound_processor.ex`:
- `:202` roteia `pacs.004 -> process_incoming_return` (`:1288`).
- `:1324` `do_process_incoming_return`: extrai RtrId/OrgnlEndToEndId/valor do XML,
  cria linha INBOUND pacs.004 **PDNG** (nao credita), `send_return_acceptance(tx)`
  (pacs.002 ACSP, `:1419`).
- `:1642` `resolve_return_entry` (na camt.054): `Sts=BOOK` => STLD, espelha
  credito na Conta PI, publica `transaction.returned` ao Core (que credita o
  cliente); `Sts=INFO` + codigo => RJCT, nunca credita (caso real 14/07 AB03).

---

## 5. Comparacao dimensao a dimensao

| Dimensao | Legado (prova) | Nosso (prova) | Veredito |
|---|---|---|---|
| Formato RtrId | `D`+ISPB+yyyyMMddHHmm+11 =32 (`SPI.Core.General:375`) | idem `@rtr_id_re` 32 (`return_processor.ex:37`) | COBERTO |
| Unicidade RtrId | seed sequence, try/catch nulo (`Seeds.Infra:373`) | tabela burn + ON CONFLICT + unique particao (`return_id_burn.ex:45`) | COBERTO (nosso mais forte) |
| So devolve RECEBIDO | `FindByNrSpb(Recebedor)==null` erro (`Multiliq.App:4081`) | `NOT_INBOUND` (`core_event_processor.ex:904`) | COBERTO |
| Exige LIQUIDADO | fluxo confirma recebimento antes | `NOT_SETTLED` (`core_event_processor.ex:897`) | COBERTO |
| Janela IPAY 90d | `TotalDays>90` (`Multiliq.App:4781`) | 90d fixo (`spi_validator.ex:30,41`) | COBERTO |
| Janela Saque/Troco 1h | `OTHR/GSCB TotalHours>1` (`Multiliq.App:4786`) | AUSENTE (90d fixo p/ tudo) | **G1 DIVERGENCIA** |
| Valor <= original (unico) | `VlOperacao<VlPagto` (`Multiliq.App:4797`) | CumulativeCap (`cumulative_cap.ex:44`) | COBERTO |
| Teto cumulativo | soma ConfPagtoRecebto_Ok (`Multiliq.App:4802`) | soma NAO-rejeitadas sob advisory lock (`cumulative_cap.ex`) | COBERTO |
| Whitelist reason | UI 4 / enum 15 (`html:23`,`Multiliq.General:6283`) | 13 diferentes (`spi_validator.ex:21`) | **G2 DIVERGENCIA** |
| Anti-fraude devolucao | gate opcional PACS.004 (`SPI.App:6717`) | AUSENTE no ReturnProcessor | **G3 DIVERGENCIA** |
| Cadeia orig<->dev | EndToEndIdOrig+RtrId+IdOperacaoOrig | original_end_to_end_id+return_id | COBERTO |
| IN aceite pacs.002 | HandlerDevolucao (`FilaEntrada:5201`) | ACSP `send_return_acceptance` (`inbound:1419`) | COBERTO |
| IN credito na confirmacao | RecebimentoExternoDevolucao (`ValidacaoCredito:601`) | camt.054 BOOK -> STLD+credito+returned (`inbound:1655`) | COBERTO |
| IN valida original nossa | casa EndToEndIdOrig+RtrId (`FilaEntrada:1167`) | aceita sem casar outbound (`inbound:1324`) | **G5 PARCIAL** |
| Multiplas razoes | `LstRazaoDevolucao` lista (`Multiliq.App:4088`) | 1 `return_reason` (`return_processor.ex:421`) | **G6 PARCIAL** |
| P6 amount (SpiValidator) | teto na Devolucao (real) | JOIN payments le 0 (morto) (`spi_validator.ex:62`) | **G4 INFO** |
| Auto-return conta invalida | codigos AC03 etc. na validacao credito | AC03/06/07/14/AG03 -> pacs.004 (`inbound:839`) | COBERTO (INCONCLUSIVO se legado dispara automatico) |

---

## 6. Gaps detalhados

### G1 (ALTO) — Janela de 1 hora para Pix Saque/Troco ausente
- Legado: `Multiliquidacao.Core.Application.decompiled.cs:4786` barra devolucao de
  OTHR/GSCB apos 1 hora (alem dos 90 dias de IPAY em `:4781`).
- Nosso: `spi_validator.ex:30,41` aplica 90 dias FIXO a qualquer devolucao; nao ha
  ramo por tpFinalidade/finalidade.
- Risco: devolucao de Saque/Troco fora da janela de 1h passa no gate local; o
  BACEN rejeita (DT05 em `CRK_SPIDOMINIO.dbo.SpiReason`), mas perdemos a barreira
  preventiva e a mensagem legivel ao operador/cliente.

### G2 (MEDIO) — Whitelist de reason codes divergente
- Legado: UI operador 4 (`html:23-26` BE08/FR01/MD06/SL02); enum 15
  (`Multiliquidacao.Core.General.decompiled.cs:6283`: AM05 AM09 BE08 DS28 FOCR
  FR01 MD06 NARR RUTA SL11 SL12 UPAY SL02 AM23 RR06); sem whitelist server-side no
  DevolvePix.
- Nosso: `spi_validator.ex:21` 13 codigos (MD06 SL02 BE08 FR01 AC03 AC06 AC07 AC14
  AG03 AG13 AM09 AM18 RR04). Ha ainda uma lista ISO enorme e SEPARADA no schema
  `transaction_returns` (`transactions/return.ex:17`), tabela hoje pouco usada.
- Delta: nos rejeitamos AM05/DS28/FOCR/NARR/RUTA/SL11/SL12/UPAY/AM23/RR06 (legado
  aceita); aceitamos AC03/AC06/AC07/AC14/AG03/AG13/AM18/RR04 (nao no enum legado).
  Os 4 manuais coincidem, entao o caminho manual tem paridade. AG13
  (ForbiddenReturnPayment) e RR04 (sancao) como razao de INICIACAO sao
  semanticamente estranhos (sao motivos de REJEICAO). Requer confronto com o
  catalogo BACEN pacs.004.spi vigente para decidir a lista canonica.

### G3 (MEDIO) — Sem motor anti-fraude na devolucao de saida
- Legado: `SPI.Web.Api.decompiled.cs:953` + `SPI.Core.Application.decompiled.cs:6717`
  gate `ValidaDevolucaoMotorAntifraude` (TX_TYPE PACS.004), opcional por flag
  `AtivaMotorAntiFraude`; reprovado grava `ReprovadaPeloMotorAntiFraude`.
- Nosso: `return_processor.ex` e `core_event_processor.ex` (busca "antifraude"/
  "MotorAntiFraude" = 0 hits) nao chamam motor anti-fraude na saida. O
  `SanctionsCheck` existe so no credito ENTRANTE (pacs.008), nao na pacs.004.
- Risco: sem barreira anti-fraude configuravel para devolucoes; depende de
  controles externos. Baixo-medio porque o legado tambem tem o gate OFF por default.

### G4 (BAIXO/INFO) — validate_return_amount (P6) morto no SpiValidator
- Nosso: `spi_validator.ex:62-72` soma devolucoes via `monetarie_spi.payments`,
  tabela que NAO recebe linha na criacao da devolucao (a linha vai em
  `monetarie_spi.messages`) => le 0 sempre. O teto REAL e o `CumulativeCap` no
  ReturnProcessor (`return_processor.ex:465`). Sem impacto de dinheiro (coberto por
  outro caminho), mas codigo morto que induz a erro em manutencao.

### G5 (BAIXO) — IN nao amarra a devolucao a um pagamento nosso
- Legado: correlaciona a pacs.004 recebida por `EndToEndIdMsgDev==EndToEndIdOrig
  && RtrId` contra a operacao original (`FilaEntrada:1167`).
- Nosso: `inbound_processor.ex:1324` registra e aceita a pacs.004 (ACSP) sem
  verificar que o OrgnlEndToEndId casa um OUTBOUND nosso. Risco baixo (o BACEN nao
  roteia devolucao de pagamento que nao enviamos), mas nao ha guarda local; o
  credito so ocorre na camt.054 BOOK, o que limita o dano.

### G6 (BAIXO/INFO) — Multiplas razoes por devolucao
- Legado: `LstRazaoDevolucao` e uma LISTA (`Multiliquidacao.Core.Application.decompiled.cs:4087`).
- Nosso: `return_processor.ex:421` passa uma unica `return_reason` ao builder da
  pacs.004. Uso pratico e uma razao; divergencia estrutural menor.

---

## 7. Itens cobertos (confianca)

- **C1 RtrId** — formato `D`+ISPB(8)+yyyyMMddHHmm(12)+11 = 32, identico
  (`SPI.Core.General:375` x `return_processor.ex:37`).
- **C2 Cadeia original<->devolucao** — EndToEndIdOrig/RtrId x
  original_end_to_end_id/return_id (`SpiMessage.EndToEndIdOrig` x `messages.original_end_to_end_id`).
- **C3 So devolve RECEBIDO e LIQUIDADO** — `Multiliq.App:4081` x
  `core_event_processor.ex:904,897`.
- **C4 Teto cumulativo por E2E** — `Multiliq.App:4802` (soma confirmadas) x
  `cumulative_cap.ex:44` (soma nao-rejeitadas sob advisory lock).
- **C5 Unicidade do RtrId** — nossa e ESTRUTURAL (burn + unique de particao;
  `return_id_burn.ex:45`), superior ao seed sequence legado (`Seeds.Infra:373`).
- **C6 IN aceite + credito na confirmacao** — pacs.002 ACSP + credito so na
  camt.054 BOOK (`inbound_processor.ex:1419,1655`), espelha o
  HandlerDevolucao/RecebimentoExternoDevolucao legado.
- **C7 Devolucao parcial** — legado `VlPagto` parcial (`Multiliq.App:4099`) x
  nosso valor da devolucao em centavos (`return_processor.ex:681`).
- **C8 Auto-return de credito inaplicavel** — AC03/AC06/AC07/AC14/AG03 ->
  pacs.004 (`inbound_processor.ex:839`), alinhado aos codigos de erro de conta do
  legado (`Multiliquidacao.ValidacaoCredito.Infrastructure:1331` AC03). Se o
  legado disparava a pacs.004 AUTOMATICAMENTE ou por operador nao ficou provado no
  decompilado (INCONCLUSIVO), mas o comportamento nosso e o exigido pelo BACEN.

---

## 8. Notas de metodo e limites de prova

- `crk_spi.dbo.SpiMessage` tem 0 pacs.004 e 44 camt.060 no snapshot vivo (dados
  esparsos); a autoridade de comportamento e o codigo decompilado, nao o volume
  de dados.
- O gate anti-fraude legado depende de parametro `MotorAntiFraude` e URL externa;
  por default OFF (`SPI.Core.Application.decompiled.cs:6810` `AtivaMotorAntiFraude ?? false`).
- MED/DICT devolucao (FRAUD/OPERATIONAL_FLAW) e mecanismo adjacente que termina em
  pacs.004 com BE08/FR01; nao foi auditado a fundo aqui (fora do escopo pacs.004
  direto), apenas referenciado.
