# Dossie de paridade PROFUNDO — DICT Funds Recovery (MED 2.0) + Refunds

Auditor: revisao read-only, zero inferencia. Cada afirmacao com prova arquivo:linha ou tabela.coluna.
Data: 2026-07-23. Escopo: recuperacao de valores (funds-recoveries), tracking-graph, refunds, contato
institucional, ciclo de vida e eventos.

- LEGADO: GID/MED decompilado (`.scratch/legado-pix-decompiled/GID.*.decompiled.cs`) + DB `gid`
  (SQL Server, container `monetarie-bak-mssql`).
- NOSSO: cabine PIX Elixir `pix/backend/apps/{dict_service,shared}` + frontend `pix/frontend/admin`.

---

## 0. Sumario executivo

Cobertura funcional ALTA no caminho outbound (criar recovery, cancelar, pedir refund, ler tracking-graph)
com paridade de payload/assinatura contra o BACEN. As lacunas materiais estao no caminho INBOUND/eventos:

- O legado tem um worker que polla `/event-notifications` por cursor e mantem o status das recoveries que
  NOS criamos em sincronia com o BACEN (ANALYSED/COMPLETED/CANCELLED) + re-consulta em INFORMATION_UPDATED.
  NOSSO codigo NAO tem esse poll — a recovery so avanca por chamada MANUAL de `refresh_tracking_graph`.
- Nao ha PUT/Atualizar da recovery no BACEN (SituationType/Contato/Detalhes).
- Nao ha validacao local da transacao raiz (32 chars, inicia E/D, bloco de data, nao anterior a 80 dias).
- `refunded_amount` do refund e campo VIRTUAL: o valor efetivamente devolvido nao e persistido e o
  `total_refunded` do resumo fica sempre zero.

Observacao factual: a base `gid` do legado esta VAZIA de dados operacionais (TB_RECUPERACAOVALOR=0,
TB_EVENTO=0, TB_TRCK002=0, TB_CAMT025=0; parametros seed com ISPB 12345678 e URLs localhost) — o modulo
MED existia em codigo mas nunca rodou com dado real. Prova:
`gid.dbo.TB_PARAMETRO` (NR_ISPB=12345678, UrlBacen=https://localhost:5850/api/v2/) e contagens zeradas.

---

## 1. LEGADO — comportamento com prova

### 1.1 API e casos de uso (`GID.Core.Application.decompiled.cs`, `GID.Api.decompiled.cs`)

`RecuperacaoValorUseCase` (linha 363) implementa o contrato `IRecuperacaoValorUseCase` (linha 136):

| Operacao | Metodo BACEN | Guarda de status | Linha |
|---|---|---|---|
| Incluir | `POST /funds-recoveries/` (assinado) | validacao de transacao raiz | 390-434 |
| Consultar | `GET /funds-recoveries/{id}` | — | 460-483 |
| Atualizar | `PUT /funds-recoveries/{id}` (assinado) | so CREATED/TRACKED/AWAITING_ANALYSIS | 485-528 |
| Cancelar | `POST /funds-recoveries/{id}/cancel` (assinado) | nao em COMPLETED/CANCELLED | 530-572 |
| ConsultarGrafoRastreamento | `GET /funds-recoveries/{id}/tracking-graph` | — | 574-597 |
| ListarNotificacoesInfracao | `GET /funds-recoveries/{id}/infraction-reports` | — | 629-650 |
| ListarSolicitacaoDevolucao | `GET /funds-recoveries/{id}/refunds` | — | 652-673 |
| SolicitarDevolucao (refund) | `POST /funds-recoveries/{id}/refund` (assinado) | so ANALYSED | 675-714 |
| Listar / Detalhar | local | — | 599-627 |

Rotas em `GID.Api.decompiled.cs`: RecuperacaoValorController (linhas 441-617), NotificacoesEventoController
(400-407), MED20Controller (addTrck002 285, retCamt025 304, monitorList 320, getStatusOperacao 336).

### 1.2 Validacao da transacao raiz (`ValidaTransacao`, linha 436-457)

Fail-fast LOCAL antes de assinar/enviar:
- `id.Length != 32` -> "identificador deve conter exatamente 32 caracteres" (439-442).
- `id[0] != 'E' && id[0] != 'D'` -> "deve iniciar com 'E' ou 'D'" (443-447).
- `id.Substring(9,12)` parseado como `yyyyMMddHHmm` -> "Bloco de data invalido" (448-451).
- `DateTime.UtcNow.AddDays(-80)` -> "Transacao nao pode ser anterior a 80 dias" (452-456).

### 1.3 Regra do SituationType OTHER + ContactInformation (linha 396-399)

- `TipoSituacao == OTHER && string.IsNullOrEmpty(Detalhes)` -> erro "Detalhe e obrigatorio para
  tipo 'OTHER'" (396-399). Fora de OTHER, Detalhes NAO e obrigatorio (DTO `RecuperacaoValoresDTO.Detalhes`
  so tem MaxLength 2000, sem [Required] — linha 5165-5167).
- `InformacoesContatoDTO` (linha 4703-4714): Email [Required]+regex, Telefone [Required]+regex
  `^\+?[0-9]{10,15}$`. Contato SEMPRE obrigatorio (RecuperacaoValoresDTO.InformacoesContato [Required], 5169-5171).

### 1.4 Payload CreateFundsRecoveryRequest (`CriaRecuperacaoValorRequestDTO.ToXML`, linha 4554-4562)

Emite: `Participant` + `FundsRecovery{FlowType, RootTransactionId, SituationType, ReportDetails,
ContactInformation{Email,Phone}}` + `TrackingGraphParameters{MinTransactionAmount, MaxTransactions,
HopWindow, MaxHops}`. Nota: **`FlowType` default "AUTOMATED"** (linha 4552) SEMPRE emitido; e
`TrackingGraphParameters` SEMPRE emitido com defaults (`ParametrosGrafoRastreamentoDTO`, 5123-5152):
`ValorMinimoTransacao=200.00`, `MaximoTransacoes=500`, `JanelaHoras=24` (HopWindow PT24H),
`ProfundidadeGrafo=5` (range 1..10). ToXML usa fallback "0.01" para MinTransactionAmount se nulo (4556-4560).

### 1.5 Maquina de status e eventos

`enumFundsRecoveryStatus` (`GID.Core.General.decompiled.cs` 3526-3542):
CREATED(1) TRACKED(2) AWAITING_ANALYSIS(3) ANALYSED(4) REFUNDING(5) COMPLETED(6) CANCELLED(7).
Confirmado na tabela de referencia viva `gid.dbo.TB_RECUPERACAOVALOR_STATUS` (7 linhas, mesmos nomes).

`enumEventType` (3507-3517): FUNDS_RECOVERY_ANALYSED, FUNDS_RECOVERY_COMPLETED,
FUNDS_RECOVERY_INFORMATION_UPDATED, FUNDS_RECOVERY_CANCELLED. Confirmado em `gid.dbo.TB_TIPOEVENTO`.

`NotificacoesEventoUseCase.CarregarNotificacoesEventos` (linha 270-313): por participante, le a marca
`ObtemUltimoEvento` (275), pagina `GET /event-notifications?Participant={p}&Cursor={c}` (280) enquanto
`HasMoreElements` (303), filtra `x.Id > ultimoEvento` ordenado (292-295), e `ProcessaEvento` (315-342):
- ANALYSED -> `AtualizaStatus(..., ANALYSED)` (320).
- CANCELLED -> `AtualizaStatus(..., CANCELLED)` (324).
- COMPLETED -> `AtualizaStatus(..., COMPLETED)` (328).
- INFORMATION_UPDATED -> `Consultar` + `ConsultarGrafoRastreamento` (re-le RV + grafo, 330-336).
So grava a marca (`IncluirEvento`) se OK (337-340). Worker `GID.Worker.Eventos` roda por participante com
`IntervaloWorker` = 60000ms (prova viva `gid.dbo.TB_PARAMETRO`).

Nota importante: a maquina do legado NAO valida transicoes localmente — aplica o status que o BACEN mandar
via evento (AtualizaStatus direto).

### 1.6 Refund / devolucao — enums e modelo

- `enumRefundReason` (3595-3607): FRAUD, OPERATIONAL_FLAW, REFUND_CANCELLED, PIX_AUTOMATICO,
  FUNDS_RECOVERY_CANCELLED.
- `enumRefundStatus` (3621-3628): OPEN, CLOSED, CANCELLED.
- `enumRefundAnalysisResult` (3585-3592): TOTALLY_ACCEPTED, PARTIALLY_ACCEPTED, REJECTED.
- `enumRefundRejectionReason` (3609-3618): NO_BALANCE, ACCOUNT_CLOSURE, INVALID_REQUEST, OTHER.
- `SolicitacaoDevolucaoEstendidaDTO` (5199-5273) persiste: TransactionId, RefundReason, RefundAmount,
  RefundDetails, Status, Requesting/ContestedParticipant, InfractionReportId, FundsRecoveryId,
  AnalysisResult, AnalysisDetails, RefundRejectionReason, RefundTransactionId, RefundAccount, MonitorAccount,
  **EffectiveRefundedAmount (`ValorEfetivamenteDevolvido`, 5271-5272)**.
- Payload do refund (`SolicitarDevolucaoRecuperacaoValorRequestDTO.ToXML`, 5283-5286):
  `RefundFundsRecoveryRequest{Participant, FundsRecoveryId}` — o BACEN deriva o resto.

### 1.7 DB `gid` (SQL Server) — schema (prova viva)

`gid.dbo.TB_RECUPERACAOVALOR`: Id(uuid), DataHoraCriacao, DataHoraUltimaModificacao, IdStatus,
IdTransacaoRaiz char(32), IdTipoSituacao, ParticipanteReportador char(8), TempoRequisicao,
ContatoEmail varchar(255) NOTNULL, ContatoTelefone varchar(16) NOTNULL, Detalhes varchar(2000),
GrafoValorMinimo decimal, GrafoMaximoTransacoes int, GrafoJanelaHoras int, GrafoProfundidade int.
Ou seja: os parametros do grafo E o contato SAO persistidos na propria linha da recovery.
Tabelas de grafo: TB_GRAFO_PESSOA/CONTA/TRANSACAO. Tracking/cautelar: TB_TRCK002/TB_TRCK002_HIST,
TB_CAMT025. Todas com 0 linhas (modulo nunca operou com dado real).

---

## 2. NOSSO — comportamento com prova

### 2.1 Contexto e API (`apps/dict_service/lib/dict_service/funds_recovery.ex`, controller + router)

`DictService.FundsRecovery` implementa:
- `create_recovery/1` (87-115): valida report_details (124-138) e limites do grafo (147-164); em modo
  `:bacen` resolve contato via `MedContact.resolve` e chama `create_recovery_via_bacen` (105-113).
- `update_status/2` (201-237): maquina de transicao LOCAL + outbox `recovery.status_changed`.
- `refresh_tracking_graph/1` (272-296): em `:bacen` faz `GET /funds-recoveries/{id}/tracking-graph`,
  upserta o grafo e AVANCA o status para o automatico do BACEN (TRACKED/ANALYSED).
- `create_refund_request/2` (318-340): guarda ANALYSED/REFUNDING; payload so participant+fundsRecoveryId.
- `complete_refund/2` (355-401), `cancel_refund_request/3` (425-433), `cancel_recovery/3` (551-559).
- `sync_inbound_refunds_from_bacen/2` (708-750): poll `GET /refunds`, upsert idempotente por `refund_id`,
  roteia OPEN novos (`monetarie.dict.recovery.refund_received`).

Rotas (`apps/dict_service/lib/dict_service_web/router.ex` 243-276): GET/POST /funds-recoveries,
GET/:id, PUT /:id (=> update_status LOCAL), POST /:id/cancel, POST /:id/start-refund,
GET|POST /:id/tracking-graph, GET /:id/infraction-reports (LOCAL), GET /:id/refunds (LOCAL),
POST /:id/refund, GET /:id/refund-summary, /infraction-reports/*, /refund-requests/* (complete/cancel).

### 2.2 Cliente BACEN (`apps/shared/lib/shared/bacen/dict_client.ex`)

Metodos MED (assinam XMLDSig os POST): create_funds_recovery (557, `POST /funds-recoveries/`),
get_funds_recovery (612), get_tracking_graph (628, read-only), cancel_funds_recovery (652,
`/cancel`), create_refund_request (894, `POST /funds-recoveries/{id}/refund`), get_refund (964,
`GET /refunds/{id}`), list_refunds (990, `GET /refunds?query`), cancel_refund (1030, `/refunds/{id}/cancel`),
close_refund (`POST /refunds/{id}/close`, ~1087). **Nao existe** `PUT /funds-recoveries/{id}` (Atualizar)
nem chamada a `/event-notifications` (grep vazio em `apps/`).

### 2.3 Builder do create (`apps/shared/lib/shared/bacen/dict/request_builder.ex`)

`build_create_funds_recovery_xml/2` (456): campos obrigatorios participant, root_transaction_id,
situation_type, report_details, contact_email, contact_phone (428-435); so checa nao-blank
(`funds_recovery_field_missing?`, 475-477). `render_create_funds_recovery` (535-556) emite
`CreateFundsRecoveryRequest{Signature, Participant, FundsRecovery{RootTransactionId, SituationType,
ReportDetails, ContactInformation}}` + TrackingGraphParameters opcional. **NAO emite `<FlowType>`.**
Limites 2.12.1 fail-closed: MaxTransactions 1..5, MaxHops 1..2, HopWindow PT1H/PT2H, MinAmount>=0
(479-499). Sem parametro informado, o bloco inteiro e omitido (563-583) e o DICT aplica os defaults dele.
**NAO valida 32 chars / prefixo E-D / bloco de data / regra dos 80 dias** da transacao raiz.

### 2.4 Contato institucional (`apps/dict_service/lib/dict_service/med_contact.ex`)

`resolve/1` (28-37): precedencia attrs do caller > config `:dict_service,:med_contact`
(`DICT_MED_CONTACT_EMAIL/PHONE`). Vazio/whitespace conta como nao configurado; fail-closed
`{:error, :med_contact_not_configured}` (obrigatorio em modo `:bacen`). Nao valida formato de email/telefone
(o legado valida por regex).

### 2.5 Schemas

`Recovery` (`funds_recovery/recovery.ex`): status default CREATED, @statuses (12) e @fraud_types (15)
= 6 valores DICT 2.12.1 (FRAUDULENT_ACCESS, SCAM, ACCOUNT_TAKEOVER, COERCION, OTHER, UNKNOWN). Maquina de
transicao LOCAL (99-111). **Campos VIRTUAIS (nao persistidos)**: report_details, min_transaction_amount,
max_transactions, hop_window, max_hops, tracked_at, analysis_started_at, analysis_completed_at,
refund_started_at, cancelled_at, cancelled_by_ispb, correlation_id (36-47). Parametros do grafo vivem
na tabela do grafo (`TrackingGraph`), nao na linha da recovery.

`RefundRequest` (`funds_recovery/refund_request.ex`): @statuses local PENDING/PROCESSING/COMPLETED/
CANCELLED/FAILED (12); @bacen_statuses OPEN/CLOSED/CANCELLED (16). Espelha o `<Refund>` do BACEN
(refund_reason, refund_details, infraction_report_id, analysis_result, analysis_details,
refund_rejection_reason, refund_transaction_id, bacen_creation_time/last_modified, 38-46).
**`refunded_amount` e VIRTUAL (linha 31)** — nao persistido.

### 2.6 Poll inbound (`sync/inbound_sync.ex` + `workers/dict_inbound_poll_worker.ex`)

`@poll_types = refunds, infractions, claims_donor, claims_claimer` (inbound_sync.ex 74). Worker Oban
`DictInboundPollWorker.perform` (44-55) roda essas 4 pernas; agendado `* * * * *` (a cada minuto,
`config/config.exs:204`). Incremental por watermark `ModifiedAfter` em `dict_sync_cursors`, paginacao
por `LastModified` / HasMoreElements, teto 50 paginas (inbound_sync.ex 63-186). **Nao existe perna
`recoveries`/`event-notifications`** — o status das recoveries que criamos nao e re-sincronizado
automaticamente do BACEN.

### 2.7 InfractionResponseConsumer (`nats/infraction_response_consumer.ex`)

Consome `monetarie.dict.infractions.response` do Core e roteia por status (67-77): ACKNOWLEDGED ->
acknowledge no BACEN; DISAGREED -> analyse+close; AGREED -> analyse + `publish_med_return` que emite
`monetarie.spi.return.created` (RETURN_CREATED, reason_code FR01) para o ReturnProcessor construir/assinar/
enviar a pacs.004 (156-208). E o elo infracao->devolucao efetiva (pacs.004) — no legado esse pacs.004
vive no dominio SPI/liquidacao, nao no GID.

### 2.8 Frontend (`pix/frontend/admin/src/views/med/`)

Hub MED com FundsRecoveryListView, FundsRecoveryDetailView, RefundListView, InfractionList/Detail,
FraudMarkerList/Detail (`MedHubView.vue` agrega infractions/refunds/fraud-markers/funds-recoveries).

---

## 3. GAPS e itens cobertos (legado x nosso)

Ver objeto estruturado. Resumo:

GAPS materiais:
- G1 event-notifications poll ausente (status das recoveries nao auto-sincroniza) — ALTO/MEDIO.
- G2 FUNDS_RECOVERY_INFORMATION_UPDATED sem handler — MEDIO.
- G3 Atualizar (PUT /funds-recoveries/{id}) ausente — MEDIO/BAIXO.
- G4 validacao da transacao raiz (32/E-D/data/80 dias) ausente — MEDIO.
- G5 FlowType nao emitido no create — BAIXO.
- G6 refunded_amount virtual: valor efetivo devolvido nao persistido, total_refunded sempre 0 — MEDIO.
- G7 parametros do grafo + timestamps de ciclo virtuais na recovery — BAIXO/INFO.
- G8 TRCK002/camt.025 fora do dominio funds-recovery (vivem no SPI) — INFO.

COBERTOS relevantes: enum de status (7), SituationType (6), payload create/cancel/refund assinado,
tracking-graph read (grafo auto-gerado pelo BACEN), contato 2.12 fail-closed, sync inbound de refunds
por cursor/idempotente, espelho de campos do refund, UI MED, automacao infracao->pacs.004.
