# Cabine SPI legada (CRK/Corner) — money-path ponta a ponta

Fonte: código decompilado em `.scratch/legado-pix-decompiled/` + scripts SQL em
`LegadoPIX/Pix/SPI/Database-Atualizacao/Evolucao`. Read-only. Objetivo: confrontar
com a nossa cabine Elixir (pix/backend) e achar defeitos/omissões.

## 0. Arquitetura de workers (o desenho que importa)

O SPI legado NÃO é um serviço só. É uma malha de ~20 workers `BackgroundService`
(.NET) que se comunicam por FILAS (SQL Server Service Broker por padrão; também há
`RabbitMQPoolFactory` e `MQQueuePoolFactory` para IBM MQ). Cada etapa do money-path
é um worker separado, e vários são replicados (`FIN`, `GEN`, `RSP`, `sec`) — ver as
pastas `worker.*` em `LegadoPIX/Pix/SPI/`.

Pipeline de uma operação (PIX out e PIX in compartilham a espinha):

```
[Web.Api / EntradaController]  → (antifraude externo) → grava operação + enfileira
        ↓ fila EntradaAPI
[worker.filaentrada]  (WorkerEntradaAPI)   → valida crédito (PIX-in) / monta msg saída
        ↓
[worker.filaentrada]  → alçada (limites/maker-checker) → assina (cripto/HSM) 
        ↓ fila envio
[worker.filaenvioapibacen]  (WorkerEnvioApi/FIN)  → POST gzip XML assinado à API BACEN (ICOM)
        ↓ (resposta 201 + PI-ResourceId)                     
[worker.filaretornoapibacen] (Worker)  → long-poll PULL por ISPB do ICOM (pacs.002, pacs.008-in, camt.054, admi.002...)
        ↓ fila retorno
[worker.tratamentoretornoapibacen] (FIN/RSP/sec) → interpreta pacs.002/camt.054/admi.002 → decide status
        ↓ fila status
[worker.filastatus] (FIN/RSP) → SPI_SPUPDSTATUSMESSAGE (máquina de status, terminal-safe) + alertas
        ↓ fila retorno legado
[worker.retornolegado] (sec) → confirma crédito/débito ao CORE LEGADO por MQ (LPI/transação)
```

Workers de suporte: `envioconsultaoperacao` (camt.060 p/ presos), `envioautomacaomensagem`,
`envioalerta`/`envioalertaalcada`, `conciliacao`, `downloadautomatico`, `caching`, `expurgo`,
`tarefasautomaticas`, e a família `atualizacaolog*` (persistência assíncrona de logs).

Separação `FIN` (financeiro/money-path) x `GEN` (geral) x `RSP` x `sec` (site secundário/DR):
filas dedicadas para o money-path não competirem com mensagens administrativas. Fonte:
`enumFinalidadeFila.{EntradaAPI, EntradaApiRETFinanceiras, EntradaApiRETGEN, MED20TRCK002, ...}`.

**Threading:** cada worker roda N threads (`_config.NumeroThreads`), cada uma abre um
`LifetimeScope`, resolve um consumer de fila e chama `LerMensagensLoopPadrao` num loop
com pausa configurável (`SpiWorker.TempoPausa`). Falha de MQ (`ErroMQProcessoReiniciado`)
=> `StopApplication()` (deixa o orquestrador reiniciar o processo). Ver
`SPI.Core.Worker.FilaEntrada.decompiled.cs:178-295`.

---

## 1. PIX OUT (envio de pagamento)

### 1.1 Entrada e antifraude
- `SPI.Web.Api EntradaController` recebe `PaymentAPIDTO`. Antes de tudo chama o
  **motor antifraude EXTERNO** (`POST /validaAntiFraudePagamento`,
  `SPI.Web.Api.decompiled.cs:934`).
- `MotorAntiFraudeUseCase.ValidaPagamentoMotorAntifraude`
  (`SPI.Core.Application.decompiled.cs:6709`):
  - **Gera o EndToEndId AQUI** (`GeraEndToEndId(..., bUtilizado:false)`) e o carimba no
    payload. Ou seja: o E2E nasce na fronteira de entrada, reservado, e é reusado até o
    fim. Nunca é re-mintado depois.
  - Monta `MotorAntiFraudeDTO` (E2E, valor, timestamp, Canal="front-spi", USER_ID,
    USER_AGENT, IP, origem CPF/nome/banco, destino CNPJ/ISPB) e faz `POST` HTTPS ao motor.
  - Config: `AtivaMotorAntiFraude` (liga/desliga), `UrlMotorAntiFraude` (obriga `https://`),
    `FallbackPermitirRejeitar` (fail-closed vs fail-open quando o motor cai).
  - Resultado != "Aprovado" → status `ReprovadaPeloMotorAntiFraude` e injeta msg de
    rejeição. Qualquer exceção/timeout/erro do motor → "Reprovado" com o flag de fallback
    decidindo se de fato barra. Todo request/resposta é auditado em `HistMotorAntiFraude`.

### 1.2 Alçada / limites (maker-checker) — `worker.filaentrada`
- `ValidacaoAlcadaUseCase.ValidaAlcada` (`FilaEntrada.Application:72`) + a engine em
  `FilaEntrada.Shared` (`ValidacaoAlcadaRepository`):
  - Busca `ParametroAlcada` aplicável por InstFinanc + CdMsg + usuário/grupo/sistema +
    janela de horário (`ChecaHorario`), com períodos `Diariamente` / `DiasUteis` /
    `DiasNaoUteis`, tratamento de **feriado**, **overnight** (`EstaEmMadrugadaDeJanela...`)
    e `EstendeLimites` (limite estendido em dia útil). É o equivalente do limite noturno
    Res. 142.
  - `ConsistirValorAcumulado`: lê `RegistroLimiteOperacao` (registro DIÁRIO por
    `DtBase + Cliente(CPF/CNPJ) + ISPB + Agencia + Conta + IdAlcada`) com o
    `ValorDisponivel` remanescente do dia. Limite CUMULATIVO por conta/dia consumido a
    cada operação.
  - Decisão: valor em `[VlrMinimo, VlrMaximo]` e acumulado OK → `Aprovada`. Fora, e
    `NrQtdeVistos == 0` → `Rejeitada`. Senão → `PendenteAprovacao` (precisa de N "vistos"
    de aprovadores). `WorkerPosAlcada` retoma o que foi aprovado e segue o fluxo.
- Só depois de aprovado (ou sem alçada) a mensagem é convertida em DTO de saída, montada
  como pacs.008, assinada (cripto/HSM) e enfileirada para o `filaenvioapibacen`. A
  montagem/assinatura do XML acontece ANTES do worker de envio; o `SpiMessage.IspbIF` é
  gravado nesse ponto (participante direto liquidante — ver §5).

### 1.3 Envio à API BACEN — `worker.filaenvioapibacen`
- `UnitOfWorkEnvioApiBase.ProcessaMsg` (`EnvioApiBacen.Infrastructure:480`): pega o
  `MensagemEnvioDTO` já com `XmlAssinado64` + `MessageId` + `EndToEndId`, e chama
  `SendService.Send(IspbIF, xml)`.
- `SendService.Send` (`Mensageria.Geral.Infrastructure:734`): comprime o XML em **gzip**
  (`application/xml`, `Content-Encoding: gzip`) e faz
  `POST {URL}/{MetodoEnvio}/{ispbIF}/msgs`. Cliente HttpClient nomeado `spi-envio`.
- Resposta: `LerRetornoEnvio` lê `HttpStatusCode`. **201 Created = OK** (`EnvioApiReturnValue.OK => ReturnCode == 201`),
  captura o header **`PI-ResourceId`** (só quando 201). `400-499 = ErroPSP`, `503 = ErroConexao`,
  `-1 = exceção de rede`. Status da operação vira `Enviada` (sucesso) ou `ErroEnvio`.
- **Controle de TPS**: `LimitaTransacao` (`EnvioApiBacen.Infrastructure:435`) — janela de
  1 s por `LimiteTransacoesHorario.TransacoesSegundo` (por faixa de horário). `Thread.Sleep(1000)`
  quando estoura. Rate-limit client-side para não estourar o SPI.
- **Multipart/batch**: `SendServiceMultipart` agrupa por ISPB e manda várias mensagens num
  `multipart/mixed` gzip (`EnviaMultipart` + `TrataRetornoMultipart`), casando a resposta por
  `MessageId`. Ligado por `_config.Multipart`.

### 1.4 Recepção da resposta (pacs.002) — `worker.filaretornoapibacen`
- **Não é a mesma conexão do envio.** O SPI recebe o pacs.002 puxando o ICOM.
- `ThreadConsumoApiBacen.Processamento(ispbIF, ...)` (`RetornoApiBacen.Infrastructure:1337`):
  loop long-poll por ISPB (`_receiveService.Receive(ispbIF, PIResourceIdNext)`):
  - Cursor **`PI-ResourceId` / `PI-Pull-Next`** — pull incremental (mesma máquina ICOM).
  - `204` = fim do slot atual → marca para `Delete`. XML presente → `ProcessaMsg` (enfileira
    para tratamento) e só faz `Delete(ispbIF, resourceId)` DEPOIS de tratado com sucesso
    (ack explícito). `410 Gone` é tolerado no delete. Erro no meio → `RollbackAsync` (não
    avança cursor, re-lê). Isto é at-least-once com ack pós-processamento.
- Um worker por ISPB (`_repoIF.List(_config.ISPBSIFS)`), então a leitura é multi-participante.

### 1.5 Tratamento do retorno (define efetivada/rejeitada) — `worker.tratamentoretornoapibacen`
- `TratamentoRetornoApiBacen.Infrastructure`: interpreta a mensagem lida.
  - **pacs.002**: mapeia para status final (`Efetivada` / `Rejeitada`) da operação original
    (casada por E2E).
  - **camt.054** (`ImportaCAMT0054`, linha 1068): `Sts/Cd == "BOOK"` → `Efetivada`;
    `AddtlNtryInf` presente → `Rejeitada`. Casa pacs.008 por `Refs/EndToEndId` e pacs.004
    por `Refs/InstrId`. É a CONFIRMAÇÃO DE LIQUIDAÇÃO no ledger — o camt.054 é a verdade
    final, não só o pacs.002. `AjustaStatusCAMT054` grava `DtHrLiquidacao`/`DtContabil` e,
    se a operação original não existir, CRIA um `MessageFull` a partir do camt.054 (backfill
    de operações órfãs — retorno recebido sem envio conhecido).
  - **admi.002** (`ImportaADMI002`, linha 1115): se veio como resposta de erro de uma
    `CAMT.060` com `RsnDesc == "Não existe lançamento com o identificador da operação
    solicitado"` → `RejeitaPgtoPorADMI002` marca o pagamento como `Rejeitada`. É o fecho
    do fallback camt.060 (operação nunca liquidou).

### 1.6 Confirmação ao core legado — `worker.retornolegado`
- `worker.filastatus` roda `SPI_SPUPDSTATUSMESSAGE` (§6) e, para status FINAL, gera alerta
  (`MensagemEfetivada`/`MensagemRejeitada`) e enfileira o retorno ao legado.
- `worker.retornolegado` (`FilaRetornoLegado.Infrastructure`) lê essa fila e devolve o
  resultado ao CORE LEGADO por MQ. É onde o legado credita/debita a conta do cliente e
  gera a contabilização. (Na nossa arquitetura isso é o evento NATS que a cabine publica
  para o Core.)

---

## 2. PIX IN (recepção de pacs.008)

- A pacs.008 recebida é puxada pelo `filaretornoapibacen` e roteada como ENTRADA de crédito
  para o `worker.filaentrada` (`Sentido = Retorno`, não é lote de saída).
- `ValidaCredito.EfetuaValidacaoCredito` (`FilaEntrada.Infrastructure:854`):
  - Converte a msg, roda **componentes de validação de crédito plugáveis por IdSystem**
    (`IValidacaoCreditoUseCase.Validacao(item, idSystem)` — `AplicaValidacoes`/`ValidaInstancia`,
    linha 1112). Cada componente representa a integração com um sistema legado (checar se a
    conta existe, não está bloqueada, aceita o crédito). É o equivalente ao nosso "consultar
    o Core para validar a conta de recebimento".
  - `ConverteMsg` → `ConversaoObjetos.ConvertToPACS002FullDTO` monta o **pacs.002 de
    resposta** (ACCC/RJCT). O `MessageId` do pacs.002 é mintado com
    `GeradorMsgId.GetMsgId(IspbCreditor, DtHrOperacao)`.
  - `TrataPayment` casa cada pagamento do lote por `EndToEndId`.
- O pacs.002 é assinado e enviado de volta pelo `filaenvioapibacen`. O crédito ao cliente é
  confirmado ao legado pelo `retornolegado` quando a liquidação for confirmada (camt.054 BOOK).

### 2.1 Devolução recebida (pacs.004 in)
- `HandlerDevolucaoPaymentFullDTO` / `TrataDevolucao` (`FilaEntrada.Infrastructure:1145`):
  casa a devolução por `EndToEndIdOrig` + `RtrId` contra o pagamento original e roda os
  mesmos componentes de validação de crédito antes de aceitar. O `RtrId` (`D` + ISPB + tempo +
  random) identifica a devolução. Return reason codes lidos de `RtrInf/Cd`.

---

## 3. DEVOLUÇÃO (pacs.004 out) e MED

- **pacs.004 out**: iniciada pela API (`ValidaAntiFraudeDevolucao` →
  `Application.decompiled.cs:6717`). Exige achar o pagamento original por
  `EndToEndIdMsgDev` (`FindFullByEndToEndId`); se não achar → "Pagamento não encontrado"
  (não devolve às cegas). **Gera o RtrId** (`GeraRtrId(IspbDebtor, DtHrOperacao)`) e passa
  pelo antifraude também. Monta pacs.004 com `ReturnReasonCode` (enum `enumReturnReasonCode`).
- No tratamento do retorno, a pacs.004 é reconstruída referenciando a pacs.008 original
  (`TratamentoRetorno...Infrastructure:1981`: busca `MessageFull` com `CdMsg=="PACS.008" &&
  EndToEndId == <do XML>`) e liga `MessageIdOrig/IdOperacaoOrig/EndToEndIdOrig` — mantém a
  cadeia original↔devolução.
- **MED 2.0 / TRCK002**: worker/handler dedicado `WorkerMED20TRCK002` +
  `HandlerMED20TRCK002` (`FilaEntrada.Infrastructure:5352`), fila `MED20TRCK002` própria.
  O DICT legado (`DICT.Core.Worker.{Infracao,Devolucao,Claims,Protecao}`) trata infrações,
  claims e proteção separadamente da cabine SPI.

---

## 4. Filas: prioridades, Service Broker, camt.060, timeout

### 4.1 Service Broker (transporte padrão)
- `CriaFilasSQL.decompiled.cs`: cada fila = QUEUE + MESSAGE TYPE (`VALIDATION=NONE`) +
  CONTRACT + SERVICE + BROKER PRIORITY.
  - **Prioridade**: filas com "LOG" no nome = `PRIORITY_LEVEL 5`; todo o resto (money-path) =
    `PRIORITY_LEVEL 10`. Money-path tem prioridade sobre log.
  - `POISON_MESSAGE_HANDLING(STATUS=OFF)` — não deixa o SB desabilitar a fila; o veneno é
    tratado pela via de REPROCESSAMENTO própria (filas/consumers `Reprocessamento*`).
- Também há `RabbitMQPoolFactory` e `MQQueuePoolFactory` (IBM MQ) — transporte configurável.

### 4.2 camt.060 (consulta de operação) — `worker.envioconsultaoperacao`
- `PagamentosPendentes(ISPB)` (`EnvioConsultaOperacao.Infrastructure:116`): seleciona
  pacs.008/pacs.004 com `IdStatus IN (0,1,2,4,5,7,13)` (todos NÃO-terminais: EntradaRegistro,
  PendenteCripto, CriptografadaOk, PendenteEnvio, Enviada, AguardandoRetorno,
  EntradaRetornoApiBacen), `DtHrOperacao < now-50min`, `TipoPrioridade == "PAGPRI"`.
- Ou seja: **50 minutos sem status definitivo → dispara camt.060** para reconciliar contra o
  SPI. A resposta (camt.052 balanço/extrato, ou admi.002 "não existe lançamento") fecha o
  status. É o mesmo papel do nosso StuckOutboundChecker + OperationQuery por E2E.
- Catálogo (`20200624_CAMT.sql`): `CAMT.060` respondida por `CAMT.052/053/054`, erro por
  `ADMI.002`. `SpiSaldoCAMT` guarda saldo disponível/bloqueado por ISPB (camt.052/053).

### 4.3 Timeout de data de aceite / QRDN
- Existe `enumStatusOperacao.TimeoutDataAceite` tratado no `filastatus` (força status final
  em operações com data de aceite vencida).

---

## 5. Participante indireto (crédito/débito) — script 20201106 + IspbIF

- `20201106_TratamentoPartIndiretoCreditoEDebito.sql`: adiciona a coluna
  **`SpiMessage.IspbIF`** (ISPB do participante DIRETO liquidante). O modelo separa três
  ISPBs por operação:
  - `IspbIF` = participante direto que liquida no SPI (o que o BACEN enxerga; chave do
    long-poll ICOM, do envio e do camt.054).
  - `IspbDebtor` / `IspbCreditor` = pagador/recebedor reais (podem ser o participante
    INDIRETO).
- Todo o pipeline é multi-ISPB por causa disso: `filaretornoapibacen` puxa por `IspbIF`,
  `SendService.Send(IspbIF, ...)`, camt.054 casa por `ISPB=IspbIF`.
- Movimentação da Conta PI (saldo SPI) com o SPB via mensagens LPI
  (`SPI.Core.SPB.Application:46`), distinguindo `Propria` / `Liquidante` / `SME` (indireto):
  - `LPI0001` = crédito Própria/Liquidante · `LPI0002` = crédito SME (indireto) ·
    `LPI0003` = débito Própria/Liquidante · `LPI0004` = débito SME.
- O proc `SPI_UPDDADOSRECEPMSG2` (mesmo script) registra por ISPB os IDs recebidos
  (`E2E` com flag 'S', `RtrId`, `MessageId`) — **registry de dedup de INBOUND** por
  participante (detecta reentrega duplicada do BACEN).

---

## 6. Máquina de status (idempotência terminal-safe)

`enumStatusOperacao` (0-based, `General.decompiled.cs:7114`): 0 EntradaDoRegistro,
1 PendenteCriptografia, 2 CriptografadaOk, 3 ErroCriptografia, 4 PendenteEnvio, 5 Enviada,
6 ErroEnvio, 7 AguardandoRetorno, 8 EmProcessamento, **9 Efetivada**, **10 Rejeitada**,
11 PendenteVerificacaoCredito, ... `PendenteAprovacao`, `ReprovadaPeloMotorAntiFraude`,
`TimeoutDataAceite`, `OperacaoCancelada`, etc.

Cada status mapeia para `enumClassificacaoStatus` {Pendente, ErroIntermediario, TerminoOK,
TerminoErro} (`General.decompiled.cs:12800+`). **Terminal = TerminoOK ou TerminoErro.**

**Guardião central**: proc `DBO.SPI_SPUPDSTATUSMESSAGE`
(`20200605_ControleMessageIdsGeradosEnvio.sql`):
- `UPDATE ... WITH (UPDLOCK)` (concorrência).
- Só troca o status se `CLASSIF.EhStatusFinal <> 'S'`. **Uma vez terminal (Efetivada/
  Rejeitada), NENHUM update posterior reverte** — pacs.002 tardio, camt fora de ordem,
  reentrega: tudo é no-op. Idempotência de status por construção.
- `ResourceId = COALESCE(existente, novo)`, datas (`DtHrMsg/Termino/Liquidacao/Contabil`)
  todas `COALESCE` — first-write-wins, nunca sobrescreve.
- `DtHrUltimaAlteracao` só avança (monotônico).
- Insere em `SpiMessageIds` cada `MessageId` gerado para a operação (se ainda não existe) —
  ver §7.

`filastatus` só gera alerta e enfileira retorno-ao-legado quando o status é FINAL
(`MontaAlerta` retorna null se `!EhStatusFinal()`), e trata crédito separado de débito.

---

## 7. Idempotência / MessageId / EndToEndId

Geradores (`GeradorMsgId`, `General.decompiled.cs:288`), todos `prefixo + ISPB(8) +
yyyyMMddHHmm(UTC) + randomBase62`:
- **MessageId**: `M` + ISPB + tempo + rnd(11) = 32 chars. Um POR mensagem enviada.
- **EndToEndId**: `E` + ISPB + tempo + rnd(11) = 32 chars. Um POR operação (gerado na
  entrada/antifraude, `bUtilizado:false`, persistido via `GravaEndToEndId`).
- **RtrId** (devolução): `D` + ISPB + tempo + rnd(11).
- IdCancelamento `CA`, IdRecorrencia `C/R`+`R/N`, SolicRecorrência `SC`, CancRecorrência `IC`,
  AutorizaRecorrência `IS`. `GetLabelMsg` = ISPB + ddMMyyyy + rnd(8) (rótulo de fila).

**Controle de MessageId gerado** (`SpiMessageIds`, PK `(MessageId, IdOperacao)`, FK cascade
para `SpiMessage`): toda geração de MessageId (envio + consulta + status) é registrada. É o
LEDGER de MessageIds de uma operação.

**Idempotência de reenvio (crítico)**: o `worker.filaenvioapibacen` de REPROCESSAMENTO
(`WorkerReprocessamentoApi`, `EnvioApiBacen.decompiled.cs:360`) re-lê a MESMA
`MensagemEnvioDTO` (mesmo `XmlAssinado64`, **mesmo MessageId, mesmo E2E**) da fila de
reprocessamento e faz `POST` idêntico. O SPI do BACEN deduplica por MessageId (por ISPB),
então re-postar o mesmo MessageId é idempotente na ponta do BACEN. **A cabine NUNCA
re-minta MessageId/E2E numa retentativa** — é o que impede duplo-pagamento em timeout de
rede.

Inbound: `SPI_UPDDADOSRECEPMSG2` registra E2E/RtrId/MessageId recebidos por ISPB → dedup de
reentrega (§5).

---

## 8. Motor antifraude (HistMotorAntiFraude)

- Serviço HTTP EXTERNO (`MotorAntiFraudeClient` com Polly policy), chamado na FRONTEIRA de
  entrada (pagamento e devolução), ANTES de montar/assinar/enviar. Só pré-transação.
- Regras: exige `https://`; envia E2E, valor, timestamp, canal, user/agent/IP, origem
  (nome/CPF/banco) e destino (nome/CNPJ/ISPB). Resposta `{status, codigo, mensagem}`.
- `AtivaMotorAntiFraude` liga/desliga. `FallbackPermitirRejeitar` decide fail-closed x
  fail-open quando o motor falha/timeout. Todo request+resposta persiste em
  `HistMotorAntiFraude` (auditoria). Não-aprovado → operação `ReprovadaPeloMotorAntiFraude`
  e não sai. As REGRAS de fraude ficam no serviço externo, não no SPI (o SPI só orquestra e
  audita). Marcador de fraude/decisão fica fora do repo decompilado.

---

## Comportamentos críticos que a NOSSA cabine tem que cobrir

1. **E2E/MessageId reusados na retentativa; nunca re-mintar.** Reprocessamento re-posta o
   MESMO XML/MessageId. BACEN deduplica por MessageId. Re-mintar em timeout = duplo-pagamento.
2. **Status terminal é imutável.** `SPI_SPUPDSTATUSMESSAGE` bloqueia qualquer regressão de
   Efetivada/Rejeitada. Late pacs.002 / camt fora de ordem / reentrega = no-op. Confere se o
   nosso funil de status tem CAS terminal-safe (a auditoria R1-R10 já cobriu parte disso).
3. **camt.054 BOOK é a verdade final de liquidação**, não o pacs.002. `AddtlNtryInf` presente
   = rejeição. Gravar `DtHrLiquidacao/DtContabil` só no BOOK. Backfill de operação órfã a
   partir do camt.054 (retorno sem envio conhecido).
4. **camt.060 aos 50 min para toda pacs.008/004 presa** (status não-terminal, PAGPRI), e
   admi.002 "não existe lançamento" fecha como Rejeitada. É o nosso StuckOutboundChecker +
   OperationQuery — confirmar limiar (~50 min) e o fecho por admi.002.
5. **ICOM é PULL com cursor `PI-ResourceId`/`PI-Pull-Next`, ack pós-processamento, delete só
   após sucesso, rollback (não avança cursor) em erro.** 204 = fim de slot; 410 tolerado.
   At-least-once por ISPB. Confirmar que a nossa leitura ICOM só faz ack/advance depois de
   persistir e é idempotente contra reentrega.
6. **Multi-participante (participante indireto): IspbIF (liquidante direto) ≠
   IspbDebtor/IspbCreditor.** Long-poll, envio, camt.054 e crédito/débito por IspbIF; conta
   PI via LPI0001-0004 distinguindo Propria/Liquidante/SME. Se a nossa cabine assume ISPB
   único, quebra em cenário de liquidante/indireto.
7. **Alçada/limites com maker-checker**: limite CUMULATIVO diário por conta
   (`RegistroLimiteOperacao.ValorDisponivel`), janela noturna/overnight/feriado, e
   `PendenteAprovacao` com N vistos para PIX-out acima do limite. Fraude/operacional.
8. **Antifraude pré-transação fail-closed configurável** (E2E gerado antes, HTTPS
   obrigatório, tudo auditado, fallback rejeita por padrão sensato). Nossa cabine tem hook
   equivalente? (hoje o marcador de fraude é do lado DICT/partner.)
9. **Rate-limit client-side de TPS por faixa de horário** no envio (`LimitaTransacao`) +
   envio multipart/batch por ISPB. Necessário para não estourar o SPI em rajada (a nossa
   frente de capacidade extrema + cap 6+6 Redis endereça isso, mas o TPS/segundo por janela
   é uma peça a mais).
10. **Dedup de inbound por (ISPB, E2E/RtrId/MessageId)** registrado antes de creditar
    (`SPI_UPDDADOSRECEPMSG2`). Impede duplo-crédito por reentrega do BACEN. Confirmar o
    nosso guard equivalente no PIX-in.
11. **Devolução (pacs.004) só com pagamento original localizado** (por EndToEndIdMsgDev),
    RtrId gerado e queimado, cadeia original↔devolução ligada; nunca devolver às cegas.
12. **Reprocessamento/poison é via fila dedicada com retry explícito** (POISON_HANDLING=OFF),
    não descarte silencioso. Falha de MQ derruba o worker de propósito (reinício limpo).
