# Dossie de paridade PROFUNDO: DICT infracoes + marcador de fraude

Auditoria READ-ONLY. Zero inferencia: cada afirmacao carrega prova (arquivo:linha ou tabela.coluna).
Onde falta prova, o veredito e INCONCLUSIVO. pt-br sem travessao.

- **Legado**: `.scratch/legado-pix-decompiled/*.decompiled.cs` (DICT.Core.*), DB vivo `des_4dict` (SQL Server), scripts `LegadoPIX/Pix/DICT`.
- **Nosso**: `pix/backend/apps/{dict_service,shared}` (Elixir) + `pix/frontend/admin/src/views/med` (Vue).

Escopo do legado: 3 Windows Services (`DICT.Core.Worker.Infracao`) + use cases `IncluirInfracao`,
`Fechar`, `Cancelar`, `AtualizarInfracao`, `ReceberReivindicaoInfracao`, `IncluirMarcacaoFraude`,
`CancelarMarcacaoFraude`, `CheckFraud`. Nosso escopo: `DictService.Infractions`,
`DictService.FraudMarkers`, `DictService.Statistics`, `Shared.Bacen.DictClient`, `DictService.Sync.InboundSync`.

---

## 0. Volumetria real do legado (DB `des_4dict`, prova viva)

| Tabela | Linhas | Distribuicao |
|---|---|---|
| `TB_INFRACAO` | 952 | tipo: 935 REFUND_REQUEST, 8 REFUND_CANCELLED, 7 FRAUD, 2 AML_CTF; status: 440 CLOSED, 512 CANCELLED; reportador: 477 reporter / 475 contestado; `ID_RECUPERACAOVALOR`: 952 NULL |
| `TB_FRAUDE` | 813 | status: 758 REGISTERED, 25 NEW, 30 CANCELLED; tipo: 574 APPLICATION_FRAUD, 195 MULE_ACCOUNT, 24 OTHER, 17 SCAMMER_ACCOUNT, 3 UNKNOWN; 255 com chave, 558 so por CPF/CNPJ |
| `TB_HISTSITUACAOINFRACAO` | 2349 | historico de situacao por infracao |

Leitura: o legado usa infracao majoritariamente como notificacao de REFUND_REQUEST (935/952) e
tem uso pesado de marcador de fraude (813 registros, 758 efetivamente registrados no BACEN).

---

## 1. LEGADO: comportamento com prova

### 1.1 Enums (fonte: `DICT.Core.General.decompiled.cs` + ref tables `des_4dict`)

- `enumInfractionType` (General:12084): `FRAUD=1, AML_CTF=2, REFUND_REQUEST=3, REFUND_CANCELLED=4`
  (ref `TB_TIPOINFRACAO`: 1 FRAUD, 2 AML_CTF PLD/FT, 3 REFUND_REQUEST, 4 REFUND_CANCELLED).
- `enumInfractionReportStatus` (General:12068): `OPEN=1, ACKNOWLEDGED, CLOSED, CANCELLED`
  (ref `TB_TIPOSITUACAORELINFRACAO`: 1 Aberto, 2 Reconhecido, 3 Fechado, 4 Cancelado).
- `enumAnalysisResult` (General:12012): `AGREED=1, DISAGREED`.
- `enumSituationType` (General:12175): `SCAM=1, ACCOUNT_TAKEOVER, COERCION, FRAUDULENT_ACCESS, OTHER, UNKNOWN`
  (ref `TB_SITUACAO_INFRACAO` 6 linhas).
- `enumPosicaoParticipante` (General:12100): `Reportador=1, Contestado`.
- `enumTipoFraude` (General:12253): `APPLICATION_FRAUD=1 (FALSIDADE IDEOLOGICA), MULE_ACCOUNT (CONTA LARANJA), SCAMMER_ACCOUNT (CONTA DE FRAUDADOR), OTHER, UNKNOWN` (ref `TB_TIPOFRAUDE` 5 linhas).
- `enumStatusFraude` (General:12208): `NEW=1 (NOVA/NAO ENVIADA), REGISTERED, CANCELLED`
  (ref `TB_STATUSFRAUDE`).
- `TB_TIPORELATADORINFRACAO`: `1 DEBITED_PARTICIPANT, 2 CREDITED_PARTICIPANT` (papel financeiro do relator).

### 1.2 Schema DB (`des_4dict`)

- `TB_INFRACAO`: `ID_TIPOINFRACAO`, `ID_TIPORELATADORINFRACAO`, `ID_TIPOSITUACAORELINFRACAO`,
  `ID_TIPORESULTADOANALISE`, `CD_IDTRANSACAO varchar(32)`, `CD_INFRACAO varchar(36)`,
  `NR_SPBPARTICIPANTE(_DEBITO/_CREDITO/_REPORTADOR/_CONTESTADO)`, `DS_DETALHE_INFRACAO varchar(2000)`,
  `DS_DETALHE_ANALISE varchar(2000)`, `DT_FECHAMENTO`, `DT_CANCELAMENTO`, `IC_REPORTADOR bit`,
  `ID_SITUACAO_INFRACAO`, `ID_FRAUDE uniqueidentifier`, `ID_TIPO_FRAUDE`, `TX_EMAIL`, `NR_TELEFONE`,
  `ID_RECUPERACAOVALOR uniqueidentifier`, `VL_INFRACAO decimal(9)`.
- `TB_FRAUDE`: `ID_FRAUDE uniqueidentifier PK`, `NR_SPBPARTICIPANTE`, `NR_CPFCNPJ varchar(14)`,
  `TX_CHAVE nvarchar(154)`, `ID_TIPOFRAUDE`, `DS_DETALHES varchar(2000)`, `ID_STATUSFRAUDE`,
  `ID_FRAUDE_BACEN varchar(36)`, `JS_INFRACAO varchar(max)`.
- `TB_HISTSITUACAOINFRACAO`: historico (`ID_INFRACAO`, `DH_ATUALIZACAO`, `ID_TIPOSITUACAORELINFRACAO`, `DS_MENSAGEM`).

### 1.3 Workers (`DICT.Core.Worker.Infracao.decompiled.cs`)

3 hosted services, cada um laco `while` + `Task.Delay(NR_QtSegundosIntervaloWorkerInfracao*1000)`:
- `WorkerBuscaInfracao` (linha 99) chama `IBuscaRelatoInfracaoUseCase.Processar(Interna: true, cursor)`.
- `WorkerBuscaInfracaoSolicitante` (linha 139) chama `Processar(Interna: false, cursor)`.
- `WorkerRecepcaoInfracao` (linha 178) chama `IBuscaSolicitacaoRelatorioInfracaoUseCase.Processar`.

Cursor incremental por participante: `_dataReferenciaPorParticipante = Dictionary<string,DateTime>`
(worker:107 e :147). `Interna=true` = infracoes onde SOMOS o contestado; `Interna=false` = onde somos
o reportador (`BuscaRelatoInfracaoUseCase.Processar`, Application:1475-1513 laco por
`ParticipantesControlados`, chama `AtualizarInfracao`, e `AddProcessoStatus(reportador: true|false)`).

### 1.4 Recepcao de infracao do BACEN + AUTO-ACKNOWLEDGE (Infrastructure:6287 `ReceberReivindicaoInfracao`)

- `BuscaSolicitacaoRelatorioInfracaoUseCase.Processar` (Application:1750) puxa
  `ListaInfracoes(..., icContestado:true, [OPEN], 100)` e chama `CarregarSolicitacao` por item.
- `CarregarSolicitacao` (Infrastructure:3569) delega a `ReceberReivindicaoInfracao`.
- Regra chave (Infrastructure:6365-6387): quando o registro esta `OPEN`, o legado dispara
  `NotificaRecebimento` AUTOMATICAMENTE (`POST /infraction-reports/{id}/acknowledge` assinado) e
  transiciona o espelho local para `ACKNOWLEDGED`. Persiste `Situacoes` (historico) a cada passo.

### 1.5 Inclusao (reportador) (Application:5601 `IncluirInfracaoDTOUseCase.IncluirInfracao` + Infrastructure:5735)

- Validacao (Application:5607-5626): tipo restrito a `REFUND_REQUEST`/`REFUND_CANCELLED`
  ("Apenas 'REFUND_REQUEST' E 'REFUND_CANCELLED' sao tipos de infracao validas!"); telefone
  OBRIGATORIO (`^\+\d{2,15}$`); email OBRIGATORIO (regex). Monta `InfracaoDTO` `Reportador=true`,
  `IdTipoSituacaoRelInfracao=OPEN`.
- `ValidaInfracaoUseCase.ValidaDados` (Application:935): `ValidarIdTransacao` (nao vazio + regex
  `\w{32}`, General:8823), `ValidarTipoInfracao` (!= 0, General:8807), `ValidarDetalheInfracao`
  (<=20000, General:8815), `ValidaSPB`.
- Envio (Infrastructure:5735): `Valida` -> `ValidarNrSPBParticipanteLogado` (autorizacao por claims)
  -> `ValidaHorario(ReportarInfracao)` -> monta `CreateInfractionReportRequest` (Infrastructure:5825:
  `TransactionId`, `Reason`, `SituationType`, `ReportDetails`, `ContactInformation{Email,Phone}`) ->
  `AssinaXml` -> `Send(GrupoMetodoInfractionReport)` -> parseia resposta, grava `CodigoInfracao`,
  `ReporterParticipant`, `CounterpartyParticipant`, historico. Rejeicao BACEN vira erro legivel
  (`problem.Detail` + `Violations`).

### 1.6 Fechar (analise) (Infrastructure:3921 `Fechar`)

- Regras: `ValidarResultadoAnalise` (AGREED/DISAGREED, General:8882), `ValidarDetalheAnalise` (<=2000,
  General:8891), `ValidaHorario(ConfirmarInfracao)`.
- **Regra de papel (Infrastructure:3950-3954)**: `if infracao.SPBParticipanteReportador == nrSpbParticipante`
  -> ReturnCode -108 "O participante que iniciou o relatorio de infracao nao pode fecha-lo".
  Fechar exige status `OPEN` ou `ACKNOWLEDGED` (Infrastructure:3955).
- `CloseInfractionReportRequest` (Infrastructure:4122): inclui `AnalysisDetails`, **`FraudType`**
  (`tipoFraude.Descricao()` quando presente) e `AnalysisResult`.
- Pos-BACEN (Infrastructure:3964-4003): status -> `CLOSED`, grava `IdTipoResultadoAnalise`,
  `IdTipoFraude`, e **captura `FraudMarkerId` da resposta** (`infracao.IdFraude = closeResponse...FraudMarkerId`).
  Se `IdFraude` presente, chama `_fraude.ConsultarMarcacaoFraude(IdFraude, ISPB)` (Infrastructure:4000-4003).
  Ou seja: fechar AGREED com FraudType faz o BACEN criar o marcador e o legado vincula/consulta.

### 1.7 Cancelar (Infrastructure:4027 `CancelarInfracao`)

- `ValidaHorario(CancelarInfracao)`. Regras (Infrastructure:4059-4068): CANCELLED ja -> -107 "ja
  cancelada"; CLOSED e participante != reportador -> -107 "Apenas o participante que abriu a infracao
  pode fecha-la"; senao `POST /infraction-reports/{id}/cancel` assinado -> `CANCELLED` + historico.

### 1.8 Marcador de fraude (Application:5298 `IncluirMarcacaoFraude`)

- Validacao (Application:5303-5330): `nrCpfCnpj` DEVE ter 14 digitos com `CpfCnpj.ValidarCNPJ` OU
  11 digitos com `ValidarCPF` (senao `CPFCNPJResponsavelInvalido`). Se `txChave` presente:
  `ConsultaChaveBaseLocal` -> chave inexistente = `ErroValidacaoChaves` "Chave nao encontrada"; chave
  cujo `CPFCNPJPessoa != nrCpfCnpj` = `ChaveAssociadaOutroCPFCNPJ` "Esta chave pertence a outro CPF/CNPJ".
- Fluxo (Application:5331-5387): grava `Fraude` local com `IdStatus=NEW` ANTES do envio ->
  `<CreateFraudMarkerRequest>` (Participant, TaxIdNumber, FraudType, opcional Key, RequestId) ->
  `AssinaXml` -> `Send("fraud-markers")` -> `TrataRetorno` grava `idRequisicaoBacen` e status ->
  `Atualizar` -> **`CheckFraudFull`** (recalcula estatistica local de fraude da chave/dono).
- Cancelar (Application:5403 `CancelarMarcacaoFraude`): `<CancelFraudMarkerRequest>` assinado ->
  `POST /fraud-markers/{id}/cancel` -> atualiza + `CheckFraudFull`.

### 1.9 CheckFraud (motor de decisao antifraude) (Application:3489 `CheckFraud`)

- `CheckFraud(FraudMarkers)` retorna `true` se qualquer janela temporal (`d90`/`m12`/`m60`) de
  `ApplicationFrauds`/`MuleAccounts`/`ScammerAccounts`/`OtherFrauds`/`UnknownFrauds`/
  `TotalFraudTransactionAmount`/`DistinctFraudReporters` for `> 0` (Application:3491-3508).
- `CheckFraudFull` (Application:3512): busca `RetornaKeyStatistic`/`RetornaOwnersStatistic` do BACEN,
  persiste em `KeyStatistics`/`OwnerStatistics` e devolve `Fraude` bool. Exposto por
  `POST statistics/CheckFraud` (DICT.Web.Api:1704) e `ConsultaEstatistica`/`ConsultaEstatisticaChave`.

---

## 2. NOSSO: comportamento com prova

### 2.1 Modelo (MED 2.0 / funds recovery, nao o endpoint DICT 2.10 morto)

- `DictService.Infractions.create_infraction_report/1` (`infractions.ex:103`): em modo `:bacen`, NAO
  usa `POST /infraction-reports/` (410 Gone); resolve/reusa uma Recuperacao de Valores e persiste o
  espelho local vinculado (`infractions.ex:140-221`). Publica `monetarie.dict.infractions.reported`.
- Fail-closed sem `situation_type`/`fraud_category` (`infractions.ex:141-143`); pre-valida contra o
  SituationType 2.12 de ESCRITA `SCAM/ACCOUNT_TAKEOVER/COERCION/FRAUDULENT_ACCESS/OTHER` (UNKNOWN so
  leitura) (`infractions.ex:40,151-160`).
- Retry idempotente por indice unico `uq_infraction_report_tx` -> `:already_reported`
  (`infractions.ex:279-336`), com warning se `situation_type` divergir (`infractions.ex:308-320`).

### 2.2 Enums / status (nosso)

- `@report_statuses ~w(OPEN ACKNOWLEDGED CLOSED CANCELLED)` (`infractions.ex:32`) = paridade
  `enumInfractionReportStatus`.
- `@analysis_results ~w(AGREED DISAGREED)` (`infractions.ex:33`) = paridade.
- Schema `Shared.Schemas.Dict.InfractionReport` (`infraction_report.ex`): tem `reason`,
  `situation_type`, `reporter_participant`, `counterparty_participant`, `fraud_marker_id`,
  `transaction_amount`, `funds_recovery_id`. Espelha a forma DICT 2.11 inbound (`infraction_report.ex:39-47`).
- Fraude: `FraudMarkerEnumMapper` (`fraud_marker_enum_mapper.ex:72`) = `APPLICATION_FRAUD/MULE_ACCOUNT/SCAMMER_ACCOUNT/OTHER/UNKNOWN` (5 singulares) = paridade `enumTipoFraude`.

### 2.3 Ciclo de vida (nosso)

- `acknowledge_report/2` (`infractions.ex:382`): OPEN + is_creditor + status OPEN -> `POST /acknowledge`
  assinado (`:bacen`) -> `ACKNOWLEDGED` + evento.
- `analyse_report/2` (`infractions.ex:486`): status ACKNOWLEDGED + AGREED/DISAGREED -> grava
  `analysis_result`/`analysed_at` MANTENDO status `ACKNOWLEDGED` (sub-estado interno) + evento.
- `close_report/2` (`infractions.ex:543`): status ACKNOWLEDGED -> `POST /close` assinado
  carregando `analysis_result`/`analysis_details` + `report_fraud_type(report)` -> `CLOSED`.
- `cancel_report/2` (`infractions.ex:667`): vinculo `funds_recovery_id` presente cancela a RECOVERY
  (`POST /funds-recoveries/{id}/cancel`), senao `POST /infraction-reports/{id}/cancel` (`infractions.ex:670-676`).

### 2.4 Inbound (nosso)

- `poll_and_route_inbound_from_bacen/2` (`infractions.ex:927`): puxa `GET /infraction-reports/`,
  upserta cada relato (`upsert_synced_infraction_report`, `infractions.ex:1270`) e ROTEIA os relatos
  onde somos o respondente (`counterpartyParticipant == own_ispb`) e status OPEN para o responder MED
  via `monetarie.dict.med.infraction_received` (fraud claim + bloqueio cautelar) (`infractions.ex:958-1009`).
- Poll automatico incremental por `DictInboundPollWorker` -> `InboundSync` com watermark em
  `dict_sync_cursors`, buffer de sobreposicao 300s (`inbound_sync.ex:63-74,239`). `:infractions` esta
  nos `@poll_types` automaticos.

### 2.5 Marcador de fraude (nosso)

- `Shared.Bacen.DictClient.create_fraud_marker/2` (`dict_client.ex:1139`) e `cancel_fraud_marker/2`
  (`dict_client.ex:1174`): XML assinado `<CreateFraudMarkerRequest>`/`<CancelFraudMarkerRequest>`.
  `get_fraud_marker/2` e `list_fraud_markers/2` (read-only).
- `DictService.FraudMarkers` (`fraud_markers.ex`): SO acervo local (mirror). `list_local/1`,
  `sync_inbound_fraud_markers_from_bacen/2` (5a perna, MANUAL only, `inbound_sync.ex:78`),
  `upsert_from_bacen_response/1` (create/cancel do admin lancam no acervo).
- Controllers: `FraudMarkerController` (`fraud_marker_controller.ex`, create/delete/index/show),
  `Admin.FraudController` (`admin/fraud_controller.ex`, mark_fraud/clear_fraud).
- Status persistido verbatim do BACEN (NEW/REGISTERED/CANCELLED), sem validate_inclusion
  (`fraud_marker.ex:11-13,43-48`).

### 2.6 UI (nosso, Vue)

- `views/med/InfractionListView.vue`, `InfractionDetailView.vue` (acknowledge/analyse/close),
  `FraudMarkerListView.vue`/`FraudMarkerDetailView.vue` (create/cancel), `MedHubView.vue`.

---

## 3. GAPS (legado x nosso)

### G1 [ausente/alto] Motor de decisao antifraude CheckFraud nao existe
- Legado (`Application:3489-3510`, `DICT.Web.Api:1704`): `CheckFraud`/`CheckFraudFull` decide fraude
  por janelas d90/m12/m60 de FraudMarkers e alimenta a inclusao de marcador + estatistica.
- Nosso (`statistics.ex:25-27`): "hoje nenhum decisor automatico consome estes numeros (o legado usava
  CheckFraud sobre os FraudMarkers do BACEN); o wiring da decisao antifraude fica registrado como
  follow-up". Existe agregacao de estatistica (`person_statistics`/`key_statistics`) mas nenhum decisor.

### G2 [ausente/medio] Auto-acknowledge de infracao recebida OPEN
- Legado (`Infrastructure:6365-6387`): ao receber infracao OPEN onde somos o contestado, dispara
  `POST /acknowledge` automaticamente e move para ACKNOWLEDGED.
- Nosso (`infractions.ex:958-1009`): o poll inbound ROTEIA para o responder MED (fraud claim +
  bloqueio cautelar), mas NAO envia acknowledge ao BACEN; acknowledge e acao manual separada
  (`acknowledge_report/2`, `infractions.ex:382`). Sem prova de caller automatico de acknowledge.

### G3 [divergencia/medio] FraudType nao e enviado no fechamento AGREED e FraudMarkerId nao e capturado
- Legado (`Infrastructure:4122-4131,3974-4003`): `CloseInfractionReportRequest` inclui `FraudType`;
  pos-close captura `FraudMarkerId` (`infracao.IdFraude`) e chama `ConsultarMarcacaoFraude`. Fechar
  AGREED com FraudType faz o BACEN criar o marcador de fraude e o legado o vincula.
- Nosso: `close_report_via_bacen` passa `report_fraud_type(report)` que le o campo `:fraud_type`
  INEXISTENTE no schema (`infractions.ex:1170-1172` -> sempre `nil`); o schema `InfractionReport`
  nao tem coluna `fraud_type` (`infraction_report.ex:23-47`). `close_report_local_after_bacen` grava
  so status/closed_at/last_updated, NAO captura `fraud_marker_id` da resposta (`infractions.ex:763-783`).
  Efeito: fechar AGREED nunca cria marcador de fraude no BACEN nem vincula.

### G4 [divergencia/medio] Regra "reportador nao pode fechar a propria infracao" ausente
- Legado (`Infrastructure:3950-3954`): fechar com `SPBParticipanteReportador == participante` = erro
  -108. So o contestado fecha; o reportador cancela.
- Nosso (`infractions.ex:543-551`): `close_report/2` valida apenas status ACKNOWLEDGED; nenhum check
  de papel reportador/contestado. Idem cancel (`infractions.ex:667-676`) so bloqueia terminal
  (`check_not_closed`), sem a regra "CLOSED so o reportador cancela" do legado (`Infrastructure:4064-4068`).

### G5 [parcial/medio] Validacao de inclusao de marcador de fraude (CPF/CNPJ + posse da chave) ausente
- Legado (`Application:5303-5329`): valida CPF (11, `ValidarCPF`) / CNPJ (14, `ValidarCNPJ`) e, se
  chave informada, exige que a chave exista e pertenca ao mesmo CPF/CNPJ
  (`ChaveAssociadaOutroCPFCNPJ`).
- Nosso (`fraud_marker_controller.ex:45-72`, `request_builder.ex:1486-1500`): valida apenas presenca
  de `participant_ispb`/`tax_id_number`/`fraud_type` e `fraud_type in bacen_enum`. Sem validacao de
  formato de CPF/CNPJ nem posse de chave (o BACEN eventualmente rejeita, mas nao ha pre-flight local).

### G6 [divergencia/baixo] Inclusao de infracao pelo reportador: contato (telefone+email) obrigatorio
- Legado (`Application:5611-5626`): telefone E email OBRIGATORIOS e validados por regex no
  `IncluirInfracao`; tipo restrito a REFUND_REQUEST/REFUND_CANCELLED.
- Nosso (`infractions.ex:204-224`): `contact_email`/`contact_phone` sao OPCIONAIS (`put_present`) e
  fluem para a Recuperacao de Valores; o fail-closed de contato fica no funil da recovery
  (`:med_contact_not_configured`), nao no create da infracao. `reason` (enumInfractionType) nunca e
  setado no create (so na sincronizacao inbound, `infraction_report.ex:41,90-94`). Divergencia de
  modelo esperada (MED 2.0 vs DICT 2.10), registrada como baixo.

### G7 [divergencia/baixo] Filtro de status do marcador na UI nao casa com o acervo
- Legado/acervo: status do marcador e NEW/REGISTERED/CANCELLED (`TB_STATUSFRAUDE`; `fraud_marker.ex:11-13`).
- Nosso (`FraudMarkerListView.vue:78-83`): filtros da tela sao `ACTIVE`/`CANCELLED`/`EXPIRED`. Como o
  acervo grava verbatim NEW/REGISTERED/CANCELLED e o filtro passa direto ao `where`
  (`fraud_markers.ex:105-108,257-260`), filtrar por ACTIVE/EXPIRED retorna 0 linhas.

### G8 [divergencia/baixo] Estados vestigiais AWAITING_ANALYSIS/ANALYSED no schema/UI
- Nosso: o schema `InfractionReport` mantem `@statuses` com `AWAITING_ANALYSIS`/`ANALYSED` e uma
  maquina de transicao que os usa (`infraction_report.ex:15,237-251`), e a UI mostra os botoes
  analyse/close condicionados a esses estados (`InfractionDetailView.vue:165,172`). Mas o contexto
  mantem o status em `ACKNOWLEDGED` durante a analise (`infractions.ex:492-503`) e nunca seta
  AWAITING_ANALYSIS/ANALYSED. Legado nao tem esses estados (`enumInfractionReportStatus` =
  OPEN/ACKNOWLEDGED/CLOSED/CANCELLED). Risco: os botoes analyse/close da tela podem nao aparecer
  (status fica ACKNOWLEDGED). Inconsistencia interna, nao regressao de dinheiro.

### G9 [info] Multi-participante (bureau) vs instituicao unica
- Legado: workers iteram `ParticipantesControlados` (`Application:1478,1586,1753`) servindo varios ISPB
  (modelo bureau CRK). Nosso: instituicao unica `own_ispb/0` (`dict_client.ex:1142`). Divergencia por
  desenho (Monetarie e uma SCD, ISPB 46026562). Info, nao gap.

### G10 [info] Trilhas reportador x contestado
- Legado: 2 workers separados (Interna=true contestado / Interna=false reportador) com cursor por
  participante (`worker:99,139`; `Application:1475-1513`). Nosso: um unico `InboundSync` incremental
  (watermark `dict_sync_cursors`) faz upsert dos relatos das duas direcoes no espelho e roteia so o
  lado respondente ao MED (`inbound_sync.ex:74,239`; `infractions.ex:934-943`). Cobre o espelho das 2
  direcoes; o comportamento de negocio distinto por lado (auto-ack no contestado) e o G2.

---

## 4. COBERTOS (paridade confirmada)

- **C1 [coberto]** Enum de status de infracao OPEN/ACKNOWLEDGED/CLOSED/CANCELLED
  (`infractions.ex:32` = `enumInfractionReportStatus` General:12068).
- **C2 [coberto]** Enum AnalysisResult AGREED/DISAGREED (`infractions.ex:33` = General:12012).
- **C3 [coberto]** Enum de tipo de fraude 5 valores singulares APPLICATION_FRAUD/MULE_ACCOUNT/
  SCAMMER_ACCOUNT/OTHER/UNKNOWN (`fraud_marker_enum_mapper.ex:72` = `enumTipoFraude` General:12253 e
  `TB_TIPOFRAUDE`).
- **C4 [coberto]** SituationType SCAM/ACCOUNT_TAKEOVER/COERCION/FRAUDULENT_ACCESS/OTHER(/UNKNOWN)
  espelhado (`infraction_report.ex:42`, `infractions.ex:40` = `enumSituationType` General:12175).
- **C5 [coberto]** Lifecycle acknowledge/close/cancel com XML assinado ao BACEN
  (`dict_client.ex:751,800,853`); cancel de infracao recovery-bound cancela a recovery
  (`infractions.ex:735-746`) alinhado ao BACEN 2.12.
- **C6 [coberto]** Create/cancel de marcador de fraude com XML assinado
  (`dict_client.ex:1139,1174`; `request_builder.ex:1446,1471`).
- **C7 [coberto]** Acervo local de marcadores + sync inbound por Id do BACEN, status verbatim
  (`fraud_markers.ex:127-183`, `fraud_marker.ex`).
- **C8 [coberto]** Sincronizacao inbound de infracoes com upsert idempotente por Id do BACEN e cursor
  incremental (`infractions.ex:846-896,1270-1298`; `inbound_sync.ex`).
- **C9 [coberto]** Roteamento do lado respondente para bloqueio cautelar MED
  (`infractions.ex:958-1009`) cobre o efeito de negocio que no legado vem da recepcao de infracao.
- **C10 [coberto]** Historico/eventos de transicao (publish reported/acknowledged/analysed/closed/
  cancelled, `infractions.ex:346-355,1203-1237`) analogo a `TB_HISTSITUACAOINFRACAO`.
