# Matriz de paridade DICT v2.11.0 — Spec × LegadoPIX × Monetarie

Data: 2026-07-07. Auditoria de paridade endpoint × campo × fluxo da API DICT do BACEN em três colunas de verdade, com evidência arquivo:linha. Fontes: spec oficial (extraída de `/Users/luizpenha/mwbank/md` e docs internos que citam o portal v2.11.0), LegadoPIX `.NET` (`/Users/luizpenha/mwbank/LegadoPIX/DICT`) e nossa cabine (`pix/backend/apps/dict_service` + `apps/shared`).

## Nota de cobertura de fonte (honestidade metodológica)

Nenhuma fonte local contém o texto integral da API-DICT v2.11.0 (o manual DICT específico do BACEN não está em `/md`; as tabelas de domínios/erros existem só como `.xls` binário). A coluna "Spec v2.11.0" foi reconstruída de: (a) `Manual_de_Seguranca_PIX.md` e `Manual das Interfaces de Comunicacao-1.12.md` (fonte primária BACEN, cobrem assinatura/mTLS); (b) docs internos que afirmam citar o portal (muitos rotulados v2.10, com deltas v2.11 no master-plan); (c) JDPI (wrapper, pt-BR). Itens sem fonte autorizada estão marcados SEM-FONTE e não foram inventados.

## Legenda de veredito
- **OK** = implementado e em paridade
- **GAP** = ausente ou incompleto do nosso lado
- **DIVERGE** = implementado mas diferente da spec/legado
- **SEM-FONTE** = não há fonte oficial local para cravar o item

---

## A) Entries (chaves)

| Item | Spec v2.11 | LegadoPIX | Monetarie | Veredito |
|---|---|---|---|---|
| Create (`POST /entries/`, CreateEntryRequest assinado, Reason, RequestId) | D3:52 | `UnitOfWorkInclusaoChave.cs:141,275` | `dict_client.ex:81-107`, rota `router.ex:184` | OK |
| GetEntry (`GET /entries/{Key}`, headers PI-RequestingParticipant/PI-PayerId/PI-EndToEndId) | D3:91-96 | `ReceiveService.cs:52-70`; E2E auto `Validacoes.cs:470` | `dict_client.ex:124-146`, headers `client.ex:656-687`, E2E auto `:128-137` | OK |
| PI-EndToEndId 32 chars, reusado na pacs.008 | master-plan:51,268 | `BuscaChaveUseCase.cs:182` | `dict_lookup_responder.ex:170-199` (E2eCache) | OK |
| Update (`PUT /entries/{Key}`, sem RequestId) | D3:125 | `UnitOfWorkAlteracaoChave.cs:110-113` | `dict_client.ex:196-221` | OK |
| Delete (`POST /entries/{Key}/delete` assinado, 10 reasons) | D3:149 | `UnitOfWorkExclusaoChave.cs:110-116` | `dict_client.ex:158-186`, rota `router.ex:187,191` | OK |
| AccountType CACC/SVGS/TRAN/SLRY + OTHR (v2.10.1) | D3:497,513 | `enumAccountType` | catálogo + builder | OK |
| CheckKeys (`POST /keys/check`, 1-200, não assinado, API-NP) | D3:21; D4:63 | `VerificaExistenciaChaveUseCase.cs:59` | `dict_client.ex:1310-1331`, NP `client.ex:139-150` | OK |
| sync-verification (`POST /sync-verifications/` assinado) | D4:83 | `ValidaSincronismoVSyncUseCase.cs:101-110` | `dict_client.ex:1343-1371` | OK |
| block/unblock de chave | (local — não há op BACEN) | bloqueio local | `entry_controller.ex:212-254` (local) | OK |
| Buckets/fichas de leitura (3 políticas antiscan, categorias A-H, contagem de tokens) | master-plan:62-91 | cache TTL + intervalos | `DictBudget` (`dict_budget.ex:110-300`) + `DictRateLimit` (`dict_rate_limit.ex:31-52`) | OK |
| Limite de chaves por titular (PF/PJ) | SEM-FONTE (valor) | — | — | SEM-FONTE |

**Entries: sem GAP funcional.** Cobertura forte, incluindo o orçamento de fichas espelhando o BACEN.

---

## B) Claims / Portabilidade

| Item | Spec v2.11 | LegadoPIX | Monetarie | Veredito |
|---|---|---|---|---|
| Create (`POST /claims/`, OWNERSHIP/PORTABILITY) | D3:242 | `UnitOfWorkInclusaoReivindicacao.cs:103-106` | `dict_client.ex:369-395` | **GAP** |
| — campo `claimer_opening_date` no payload | exigido | presente | **AUSENTE → falha-fecha `{:missing_field, :claimer_opening_date}`** (`dict_client.ex:1642-1645`) | GAP (alto) |
| Get (`GET /claims/{ClaimId}` read-only) | D3:321 | `BuscarReivindicacaoUseCase` | `dict_client.ex:408-411` | OK |
| Lifecycle acknowledge/confirm/cancel/complete (cada um assinado) | D3:261-306 | `UnitOfWorkAlteracaoReivindicacao.cs:128,311,428` | `dict_client.ex:424-485`, rotas `router.ex:208-211` | OK |
| List (`GET /claims/?Participant=`, filtros IsDonor/IsClaimer/Status/Type) | D3:333 | `ReceiveService.cs:175-199` | `dict_client.ex:494-515` (fix 2026-07-06) | OK |
| Polling dos DOIS lados (donor + claimer) | — | workers de quarentena/serviço | `inbound_sync.ex:204-221`, worker `dict_inbound_poll_worker.ex:39` | OK |
| Prazos D+7/D+14/D+30 (notificação/confirm/complete) | D3:186-191 | quarentena automática | `claim_deadline_consumer.ex:10-113` (NATS deliver-after), auto-confirm/auto-cancel | OK |
| State machine OPEN→WAITING_RESOLUTION→CONFIRMED→COMPLETED/CANCELLED | D3:179-184 | `enumTipoSituacao` | `dict_external_reconciler.ex:1496-1506` | OK |
| Resolução automática por timeout (conclui posse / cancela) | D3 | `ConcluiReivindicacaoPosseAutomaticoUseCase.cs:32` | `claims.ex:710,751` (auto_confirm/auto_cancel) | OK |
| Enum de motivos de cancel oficiais | SEM-FONTE | `enumTipoMotivoReivindicacao` | default USER_REQUESTED | SEM-FONTE |

**Claims: 1 GAP alto** — `claimer_opening_date` falta no payload de criação (a claim falha-fecha antes de ir ao BACEN). O legado inclui esse campo.

---

## C) Infraction Reports

| Item | Spec v2.11 | LegadoPIX | Monetarie | Veredito |
|---|---|---|---|---|
| Create (`POST /infraction-reports/` assinado) | D4:66 | `UnitOfWorkInclusaoInfracao.cs:89-92,160` | `dict_client.ex:673-695` | **GAP** |
| — campos `reason`/`situation_type` | exigidos | `Reason=IdTipoInfracao` | **AUSENTES → falha-fecha** (`dict_client.ex:701-706`) | GAP (alto) |
| Get/List (participant obrigatório na list) | D4:67 | `ReceiveService.cs:314-343` | `dict_client.ex:729-772` (fix 2026-07-06) | OK |
| acknowledge/close(AGREED/DISAGREED)/cancel | D4:68-70 | `UnitOfWorkAlteracaoInfracao.cs:88,150,264` | `dict_client.ex:786-914` | OK |
| Polling de infrações recebidas + roteamento MED | D4 | `BuscaRelatoInfracaoUseCase` | `inbound_sync.ex:202`, `infractions.ex:670` | OK |
| State machine OPEN→ACKNOWLEDGED→CLOSED/CANCELLED | D4:150-156 | `enumInfractionReportStatus` | `dict_external_reconciler.ex:1508-1518` | OK (nota: temos AWAITING_ANALYSIS/ANALYSED extras, extensão MED interna) |
| ContactInformation obrigatório (v2.4.0) | master-plan:257 | presente | verificar no builder | SEM-FONTE (a validar) |
| Enum oficial InfractionType/Reason | SEM-FONTE | `enumInfractionType` (FRAUD/AML_CTF/REFUND_REQUEST/REFUND_CANCELLED) | mapeado | SEM-FONTE |

**Infractions: 1 GAP alto** — `reason`/`situation_type` faltam no create.

---

## D) Refunds / MED (funds-recovery)

| Item | Spec v2.11 | LegadoPIX | Monetarie | Veredito |
|---|---|---|---|---|
| funds-recovery create/get/cancel/refund/tracking-graph (10 endpoints) | D4:527-540 | — (legado usa /refunds direto) | `dict_client.ex:534-655,929-955`, rotas `router.ex:243-260` | OK |
| tracking-graph GET não assinado (block DEPRECATED) | D4:97,537 | — | `dict_client.ex:605-614` | OK |
| Refunds `/refunds/` create/get/list/cancel/close | D4:72-76 | `FactoryUnitOfWorkDevolucao.cs:84,281,388` | `dict_client.ex:965-1132` | OK |
| close_refund com analysis_result | D4:206 | `RefundAnalysisResult`/`RejectionReason` | `dict_client.ex:1095-1132` | OK |
| Polling de refunds recebidos + paginação por cursor (novo v2.11) | master-plan:250 | `BuscaSolicitacaoDevolucaoUseCase` | `inbound_sync.ex:201`, `funds_recovery.ex:465` (fix 2026-07-06) | OK |
| State machine refund | D4:206-213 (OPEN→ACKNOWLEDGED→CLOSED, analysis AGREED/REJECTED) | `enumRefundsStatus` (OPEN/CLOSED/CANCELLED) + `enumRefundsAnalysisResult` (TOTALLY/PARTIALLY/REJECTED) | `dict_external_reconciler.ex:1520-1530` (CREATED/TRACKED/AWAITING_ANALYSIS/ANALYSED/REFUNDING/COMPLETED/CANCELLED) | DIVERGE (médio) |
| Enum RefundReason/RejectionReason oficial | SEM-FONTE | `enumRefundsReason` (FRAUD/OPERATIONAL_FLAW/REFUND_CANCELLED), `enumRefundsRejectionReason` (NO_BALANCE/ACCOUNT_CLOSURE/OTHER) | mapeado | SEM-FONTE |
| Prazos MED (janelas oficiais) | SEM-FONTE | — | — | SEM-FONTE |

**Refunds/MED: 1 DIVERGE médio** — nossos estados de refund/recovery não batem 1:1 com o enum do legado (`TOTALLY_ACCEPTED/PARTIALLY_ACCEPTED/REJECTED`). A listagem e persistência funcionam (validado vivo 07-06: 67 refunds), mas a semântica de análise parcial pode não estar mapeada.

---

## E) CID / Sincronismo do diretório

| Item | Spec v2.11 | LegadoPIX | Monetarie | Veredito |
|---|---|---|---|---|
| cids/files create (assinado, 40/dia), get, entries/{cid}, events | D4:83-87 | `ValidaSincronismoBaseUseCase.cs:90-209`, `ValidaSincronismoLogUseCase.cs:102-205` | `dict_client.ex:1499-1628`, rotas `router.ex:230-232` | OK |
| 1 arquivo por KeyType (sem "FULL" agregado) | D4:308-313 | por KeyType | `cid_sync_service.ex` | OK |
| limit events ≤ 200 | D3:431 | — | `@max_events_per_poll 200` | OK |
| Full sync periódico + event poll incremental (VSync/ParticipantSyncVerifier) | D3:407-438 | `SincronismoUseCase.cs:37-64` (worker com intervalo parametrizado) | `cid_sync_service.ex:2-17` | **GAP** |
| — habilitação por default | worker roda | worker roda (intervalo em banco) | default do CÓDIGO false, mas **HML tem env `CID_FULL_SYNC_ENABLED=true`/`CID_EVENT_POLL_ENABLED=true`** (task-def :120) e RODA (log vivo 07-07: "Processed 5 CID events" a cada 5min, full-sync 6h) | OK (revisado 07-07) |
| sync-verification confirmação | D4:83 | `ValidaSincronismoVSyncUseCase` | `cid_sync_service.ex:534-543` | OK |

**CID: 1 GAP alto operacional** — o serviço está implementado e validado vivo (GetEntry/CAMT/CID já provados em sessões anteriores), mas o full-sync e o event-poll estão DESLIGADOS por default. O legado roda o worker de sincronismo continuamente. Sem isso, nossa base de chaves não se reconcilia sozinha com o DICT — risco de divergência de diretório ao longo do tempo.

---

## F) Fraud Markers + Estatísticas

| Item | Spec v2.11 | LegadoPIX | Monetarie | Veredito |
|---|---|---|---|---|
| fraud-markers create/get/list/cancel (assinados) | D4:77-80 | `FraudeUseCaseDTO.cs:107,157` | `dict_client.ex:1168-1281`, rotas `router.ex:276-279` | OK |
| FraudType oficial (5 valores: APPLICATION_FRAUDS/MULE_ACCOUNTS/SCAMMER_ACCOUNTS/OTHER_FRAUDS/UNKNOWN_FRAUDS) | D4:35,392 | `enumTipoFraude` (5: APPLICATION_FRAUD/MULE_ACCOUNT/SCAMMER_ACCOUNT/OTHER/UNKNOWN) | `FraudMarkerEnumMapper.to_bacen/1` (nosso enum tem 6 valores) | DIVERGE (médio) |
| person-statistics (`GET /persons/{TaxId}/statistics`) | D4:81 | `ReceiveStatistics` | `dict_client.ex:1435` já usa o path oficial v2.11 (só em `:bacen`); `/entries/statistics/person` é só a superfície local do admin | OK (verificado + teste de contrato) |
| key-statistics (`GET /entries/{Key}?IncludeStatistics=true`) | D4:82 | `ReceiveStatisticsKey` | `dict_client.ex:1453` | OK |
| Statistics worker periódico | — | `StatisticsUseCase.cs:39` (intervalo em banco) | híbrido local+BACEN (`statistics.ex:52`) | OK |
| Campos antifraude (janelas, contadores oficiais EN) | SEM-FONTE | via JDPI pt-BR | local + BACEN | SEM-FONTE |

**Fraud/Statistics: 1 DIVERGE médio** — nosso enum de FraudType tem 6 valores; o oficial v2.11 tem 5. O mapper `to_bacen/1` traduz, mas há risco de um valor nosso sem correspondência oficial. Verificar o mapa.

---

## G) Transversais

| Item | Spec v2.11 | LegadoPIX | Monetarie | Veredito |
|---|---|---|---|---|
| XMLDSig: GET não assina; escrita assina; resposta sempre assinada; Signature na raiz; 2 References; RSA-SHA256/exc-c14n/SHA256 | Manual Seg §3 (`Manual_de_Seguranca_PIX.md:348-542`) | `SignXmlUseCase.cs:336-388` | `dict_client.ex:275-289`, `xml_signer.ex` | OK |
| Verificação da assinatura da RESPOSTA | resposta sempre assinada | pin cert SPICN | `verify_dict_response` advisory (enforced via `DICT_VERIFY_ENFORCED`) `dict_client.ex:295-331` | OK (advisory por default) |
| mTLS TLS1.2 + ICP-Brasil | Manual ICOM:49 | `ConfiguraHttpClientUseCase.cs:38-205` | `client.ex` conn_opts | OK |
| problem+xml / RFC 7807 | D3:454 | mapeamento de erro | `ResponseParser.parse_problem_xml` `dict_client.ex:333-337` | OK |
| Paginação ModifiedAfter/Limit/HasMoreElements | D3:333 | `ReceiveService.cs:180` (só Limit+ModifiedAfter, sem HasMoreElements) | `inbound_sync.ex:39-231` (ModifiedAfter incremental + cursor persistido) | OK (nossa impl é superior ao legado) |
| RequestId em Create Entry e Complete Claim | D3:52,299 | presente | presente | OK |
| Rate limit / DictBudget (fichas espelhadas, Retry-After) | master-plan:85-91 | cache TTL + intervalos (sem Polly/backoff) | `dict_budget.ex` + `dict_rate_limit.ex` | OK (nossa impl é superior) |
| event-notifications (`GET /event-notifications/`, IncludeIndirectParticipants v2.11) | D4:88 | — | rotas `router.ex:282-283` (superfície local) | GAP (baixo — sem chamada BACEN) |

---

## Resumo executivo

**Contagem:** ~48 itens auditados. **OK: 38. GAP: 5. DIVERGE: 3. SEM-FONTE: ~9** (não classificáveis por falta de fonte oficial local, majoritariamente enums de motivo e limites numéricos).

Veredito geral: **a cabine DICT está em paridade estrutural forte com a v2.11.0 e supera o legado em vários transversais** (cursor persistido, DictBudget espelhando fichas, verificação de assinatura de resposta). Os gaps são pontuais e concentrados em (a) dois payloads de criação que falham-fecham por campo faltante, (b) CID sync desligado por default, (c) três divergências de enum/estado.

### Top gaps priorizados (para correção)

| # | Severidade | Gap | Correção |
|---|---|---|---|
| 1 | ~~Alto~~ RESOLVIDO | ~~CID sync desligado~~ — FALSO: HML já tem env `=true` e roda (log vivo 07-07). Default do código false é só a base segura. | Nenhuma (já ativo e validado) |
| 2 | **Alto** | `claimer_opening_date` ausente no create_claim → claim falha-fecha | Threading do campo desde o IB/Core até o payload DICT |
| 3 | **Alto** | `reason`/`situation_type` ausentes no create_infraction → falha-fecha | Idem, propagar os campos até o builder |
| 4 | Médio | FraudType: nosso enum tem 6 valores, oficial v2.11 tem 5 | Auditar `FraudMarkerEnumMapper.to_bacen/1`, garantir que todo valor mapeia p/ um dos 5 oficiais |
| 5 | Médio | Estados de refund/recovery não batem com enum legado (TOTALLY/PARTIALLY/REJECTED) | Mapear analysis_result parcial; verificar close_refund |
| 6 | ~~Baixo~~ RESOLVIDO | ~~person-statistics usa rota local~~ — FALSO: `DictClient.get_person_statistics/2` já chama o path oficial v2.11 `GET /persons/{TaxIdNumber}/statistics` (não o pré-2.11 `/entries/statistics/person`), invocado só em `external_mode=:bacen` pelo `BacenAdapter`. A rota local `/entries/statistics/person` é só a superfície do admin, que sob o capô agrega dados locais + o payload cru do BACEN. Contrato travado por teste, incluindo a paridade de header com o legado `ReceiveStatistics` (envia `PI-RequestingParticipant`, NÃO envia `PI-EndToEndId` — divergente do key-statistics, que reaproveita `GetEntry`). | Nenhuma (já alinhado; regressão guardada em `dict_client_reads_test.exs`) |
| 7 | Baixo (documentado) | event-notifications sem chamada BACEN (superfície local) | Mantida como superfície local por decisão: a reconciliação de eventos de posse/vínculo do DICT já é feita pelo CID event-poll (`CidSyncService`, 5 min), que consome `/cids/events` do BACEN. Um poll separado de `/event-notifications/` seria redundante. Reavaliar só se o BACEN passar a publicar por ali eventos não cobertos pelo CID. |

### Itens SEM-FONTE (bloqueiam veredito, não são gaps)
Enums oficiais de motivo (claim cancel, infraction type/reason, refund reason/rejection), limites numéricos de chaves por titular, janelas de tempo MED, nomes EN dos contadores de estatística. **Ação recomendada:** obter o manual DICT v2.11.0 oficial (PDF) e as planilhas `Dom20260327.xls`/`Erros20260327.xls` em formato legível para fechar esses itens contra o BACEN, não contra o legado.
