# 12 - Verificação D4/D5/D8: antivarredura DICT, antifraude PIX-out, cobertura contábil COSIF

Auditoria adversarial READ-ONLY da cabine PIX Monetarie (`pix/backend/apps/`), confrontando o
legado CRK/Corner (relatórios 08-dict-med.md e 09-alcada-contabil-conciliacao.md). Regra: achar
o defeito e PROVAR com arquivo:linha, zero inferência.

---

## D4 — Proteção antivarredura DICT (correlaciona consulta × pagamento efetivo)

**VEREDITO: LACUNA DE FEATURE.** Existe proteção anti-enumeração PARCIAL (rate-limit por tier
BACEN + penalidade de 404 + consumo de E2E por pagamento), mas NÃO existe o motor de antivarredura
do legado que correlaciona consultas DICT com pagamentos EFETIVOS por participante/tipo de pessoa.

### O que a nossa cabine TEM
1. **Rate-limit por tier BACEN** (`DictServiceWeb.Plugs.DictRateLimit`):
   - Categorias A..H com capacidade + refill/min — `dict_rate_limit.ex:31-40`.
   - Bucket por instituição (`rate_limit:dict:ispb:<ispb>`) — `dict_rate_limit.ex:80-96`.
   - Bucket por pagador (PI-PayerId = 1/10 da cota da IF) — `dict_rate_limit.ex:99-125`.
2. **Penalidade de 404 (anti-enumeração)**: `penalize_not_found/2` consome 5 fichas extras num
   lookup que deu not_found, "to prevent key enumeration" — `dict_rate_limit.ex:154-177`. É uma
   defesa contra enumeração, mas é um custo fixo por 404, NÃO a correlação consulta×pagamento.
3. **Consumo de E2E por pagamento (uso único)**: a consulta DICT gera um E2E que precisa ser usado
   num pagamento dentro do TTL; o pagamento QUEIMA o E2E (`Shared.E2eBurn.burn`,
   `core_event_processor.ex:599`; reserva/reuso em `E2eReservation`/`E2eCache`). Isso impede reusar
   uma consulta para vários pagamentos, mas NÃO limita quem consulta muito sem pagar.

### O que FALTA (o legado tem, nós não)
- O legado `Worker.Protecao` / `ValidaProtecaoUseCase` / `RegraProtecao` cruza
  `OperacaoConsulta.IdFimAFim` (consultas DICT) com `ListarPagto` (pagamentos efetivos) por
  `SPBParticipante × TipoPessoa`, com `QtSegundosLimite`, `QtConsultasLimite`,
  `QtSegundosPeriodicidadeVerifPagto` — consulta que NÃO virou pagamento na janela = varredura, e o
  participante é limitado (relatório 08 §5).
- Na nossa cabine NÃO há: schema `RegraProtecao`/protection-rule (grep em `apps/shared/lib` e
  `apps/dict_service/lib` por `regra.?protec|protection.?rule|qtconsult|consultas.?limit` = 0
  resultados), NÃO há worker de proteção (os únicos workers do dict_service são
  `dict_external_reconcile_worker` e `dict_inbound_poll_worker`), e o motor de estatística existe
  mas NÃO alimenta nenhum decisor.
- **O próprio código admite o gap**: `DictService.Statistics` moduledoc — "Consumo antifraude: hoje
  nenhum decisor automático consome estes números (o legado usava CheckFraud sobre os FraudMarkers
  do BACEN); o wiring da decisão antifraude fica registrado como follow-up do P9a" —
  `statistics.ex:28-30`.
- **Falso amigo de nome**: `DictService.Ownership` cita "Paridade LegadoPIX (ValidaProtecaoUseCase)"
  em `ownership.ex:5`, mas reusou o NOME para validação de POSSE por OTP (PHONE/EMAIL), que é outra
  coisa — não é a antivarredura.

### Impacto real
Um participante indireto/parceiro pode varrer o DICT (enumerar chaves/donos) dentro da cota de tier
sem que a razão consulta-sem-pagamento seja detectada e estrangulada. A defesa hoje é só a cota
bruta + custo de 404 + uso único de E2E. É lacuna de feature regulatória (o BACEN exige limitação de
consulta desproporcional a pagamento), não um bug de comportamento errado.

---

## D5 — Antifraude pré-transação no PIX-out

**VEREDITO: LACUNA DE FEATURE (defeito de cobertura de controle).** NÃO existe nenhum hook de
antifraude pré-envio no money-path automático de PIX-out nem de devolução. O único controle de valor
pré-envio (alçada) está desligado por default e explicitamente NÃO cobre o fluxo automático do Core.

### Fronteira de entrada do PIX-out (fluxo real Core→PIX)
`CoreEventProcessor.handle_payment_request` — `core_event_processor.ex:227`. As validações ANTES de
montar/assinar/enviar são, em ordem:
1. `resolve_sender_ispb` fail-closed — `core_event_processor.ex:249-262`.
2. `resolve_e2e_id` (exige E2E de consulta DICT em cache) — `core_event_processor.ex:276-290`.
3. `check_and_block_balance` (saldo PI + bloqueio) — `core_event_processor.ex:299`.
4. `SettlementService.Spi.Pacs008SendValidator.validate` — `core_event_processor.ex:653-661`. É
   validação SEMÂNTICA/estrutural (forma de iniciação × TxId/chave, finalidade × tipos de valor,
   prioridade), espelho do `ValidaPaymentUseCase.cs:200-315`, NÃO antifraude —
   `pacs008_send_validator.ex:1-40`.

Nenhuma dessas chama motor antifraude. Grep por
`antifraud|validaAntiFraude|fraud.*engine|external.*fraud|risk_engine` em `apps/spi_service/lib` +
`apps/settlement_service/lib` = **0 resultados**.

### OutboundSender (envio ao BACEN)
`outbound_sender.ex` só tem: guarda de `alcada_status != "awaiting_approval"` (defesa em profundidade
do gate manual, `outbound_sender.ex:162,417`), validação XSD estrutural (`:454,473`) e confirmação do
bloqueio de saldo (`:511-518`). Nenhum antifraude.

### Alçada existe, mas não no money-path automático
`Shared.Alcada.SendGate` só é chamado no ENVIO MANUAL `POST /send` (`message_controller.ex:26-39`),
com flag `ALCADA_PIPELINE_ENABLED` default DESLIGADO, e o moduledoc afirma: "O fluxo automático do
Core não passa por aqui (ANS 1,6s; alçada do Core, Onda 1)" — `message_controller.ex:30-33`. Ou
seja, mesmo o gate de valor 4-olhos NÃO cobre o PIX-out real.

### O que o legado faz e nós não
Legado chama motor antifraude EXTERNO (`POST /validaAntiFraudePagamento`) na fronteira de entrada de
TODO PIX-out e devolução, ANTES de montar/assinar/enviar, fail-closed configurável, auditado
(relatório de contexto D5). Do nosso lado o controle de fraude é: (a) marcadores de fraude DICT
(reativos, registro/consulta — `fraud_markers.ex`), (b) partner/Core. Nada pré-envio na cabine.

### Impacto real
Um PIX-out (ou devolução) suspeito segue direto para assinatura/envio ao BACEN sem passar por
scoring/decisão antifraude na cabine. A proteção depende inteiramente de o Core/partner ter barrado
antes. Não há defesa-em-profundidade na cabine no ponto de não-retorno (pós-envio ao SPI o dinheiro
liquida). Lacuna de feature de controle, real vs o legado.

---

## D8 — Cobertura contábil COSIF (todo money-path liquidado gera lançamento? há detecção de
"não contabilizado"?)

**VEREDITO: PARCIAL / LACUNA DE FEATURE dentro da cabine.** A contabilidade da cabine gera evento
APENAS para PIX liquidado (in/out), com mapa GROSSO por direção (não por CdMsg+tag como o legado);
DEVOLUÇÕES e TARIFAS não geram lançamento na cabine; e NÃO há reconciliação de "não contabilizado".
Mitigação: o razão COSIF AUTORITATIVO do PIX é o Core (Fase 1/2), não a cabine.

### O que dispara contabilidade na cabine
Único gatilho: evento `monetarie.spi.transaction.settled` consumido pelo
`SettlementObligationWorker`, que chama `emit_accounting/2` →
`SettlementService.Accounting.create_accounting_event/1` —
`settlement_obligation_worker.ex:203,392-408`.

### Mapa contábil é GROSSO (por direção, não por CdMsg+tag)
`settlement_cosif/1` tem só DOIS casos por direção do dinheiro —
`settlement_obligation_worker.ex:177-179`:
```
def settlement_cosif("INBOUND"), do: {reserve_account(), payable_account()}
def settlement_cosif(_),          do: {payable_account(), reserve_account()}
```
Não há o modelo `EventoContabil` do legado (até 2 D + 2 C, centro de custo, histórico, casado por
CdMsg + tags com peso/especificidade — relatório 09 §3). É um par reserva↔payable por sentido.

### Tipos que NÃO geram lançamento na cabine
1. **Devoluções de saída (pacs.004)**: publicam `monetarie.spi.return.settled`, NÃO
   `transaction.settled` — `status_updater.ex:236-244`. O `SettlementObligationWorker` só consome
   `transaction.settled` (`filter_subject`, `settlement_obligation_worker.ex:48`), então a devolução
   NÃO passa por `emit_accounting`. O `ReturnProcessor` que consome `RETURN_SETTLED`
   (`return_processor.ex:61`) NÃO tem chamada de contabilidade (grep por
   `emit_accounting|create_accounting_event|create_journal|cosif` no return money-path = 0).
2. **Tarifas**: nenhum hook contábil (grep `fee.*accounting|fee.*journal|fee.*cosif` = 0).

### emit_accounting é best-effort / não-bloqueante
Falha é engolida com `Logger.warning` e o worker segue: `{:error, reason}` →
`settlement_obligation_worker.ex:412-416`; `rescue e ->` → `settlement_obligation_worker.ex:418-421`.
Não vira DLQ nem trava a liquidação (correto pelo princípio "COSIF nunca bloqueia o transacional",
mas significa que uma falha de contabilização pode passar sem retentativa).

### Detecção de "não contabilizado"
- A ÚNICA visibilidade de gap é por-evento: se o COSIF do débito/crédito não está semeado, o evento
  fica em status `"error"` (queryável em `list_accounting_events` status=error) — Task P11,
  `accounting.ex:182-259`. Mas isso só cobre eventos que FORAM criados; não detecta liquidações que
  NUNCA emitiram evento (devoluções, tarifas, ou o `emit_accounting` que caiu no rescue).
- NÃO há sweep/reconciliação de cobertura (liquidação sem journal). O
  `SettlementService.Reconciliation` é conciliação de LIQUIDAÇÃO BACEN×cabine (amount_mismatch,
  missing_transaction, etc. — `reconciliation.ex:1-30`), NÃO cobertura contábil. Grep por
  `accounting.*reconcil|settled.*without.*journal|unposted|sem.?lancamento` = só comentários, nenhum
  mecanismo. Não existe o bucket "NÃO CONTABILIZADO" do legado (`MonitorContabilUseCase`,
  relatório 09 §3.2).

### Contexto que atenua (não anula) o gap
Pelo CLAUDE.md, o COSIF autoritativo do PIX é o CORE (Fase 1 PIX-in + Fase 2 PIX-out, journal por
operação + "Reconciliação Contábil PIX" que grita `amount_mismatch` 100x). A contabilidade do
`settlement_service` é um espelho SECUNDÁRIO/standalone. Então o gap da cabine não implica
necessariamente PIX sem lastro contábil no ecossistema — implica que a trilha contábil DA CABINE é
incompleta (só PIX in/out liquidado, mapa grosso, sem devolução/tarifa, sem detecção de não
contabilizado).

### Impacto real
Se alguém usar a contabilidade da cabine como fonte (telas `/accounting` do settlement_service), ela
subnotifica: devoluções e tarifas liquidadas não aparecem, e um evento perdido no rescue some sem
alerta. A reconciliação de cobertura (o "não contabilizado" do legado) tem que ser feita no Core.
Dentro da cabine é lacuna de feature.

---

## Resumo executivo

| # | Tema | Veredito | Prova-chave |
|---|------|----------|-------------|
| D4 | Antivarredura DICT (consulta×pagamento) | **LACUNA DE FEATURE** | `statistics.ex:28-30` (código admite: nenhum decisor consome); só rate-limit `dict_rate_limit.ex:31-40` + 404 penalty `:154-177`; sem RegraProtecao/worker de proteção |
| D5 | Antifraude pré-envio PIX-out | **LACUNA DE FEATURE** | `core_event_processor.ex:227-266` só valida ispb/e2e/saldo/estrutura; grep antifraud = 0; alçada só no manual e off `message_controller.ex:30-33` |
| D8 | Cobertura contábil COSIF | **PARCIAL / LACUNA DE FEATURE** | mapa grosso por direção `settlement_obligation_worker.ex:177-179`; devolução bypassa (`status_updater.ex:236-244`); sem reconciliação de não-contabilizado; Core é o autoritativo |
