# Trilha 04: MED, infracoes, reivindicacoes e devolucoes (AVIV vs Monetarie)

Data: 2026-07-09. Trilha READ-ONLY do mandato de mapeamento exaustivo AVIV/coreproviders vs Monetarie.
Toda afirmacao tecnica cita arquivo:linha. Onde nao foi possivel provar, esta marcado NAO VERIFICADO.
Convencoes: AVIV = `/Users/luizpenha/coreproviders` (OTP `:fluxiq`, participante INDIRETO via OnZ CloudPIX, subcentavos 1 BRL = 10.000); Monetarie = `/Users/luizpenha/monetarie` (participante DIRETO, cabine PIX umbrella + Core `Monetarie.*` com TigerBeetle).

---

## 1. Como a AVIV faz

### 1.1 Entrada de infracoes e MED (polling do provider)

A AVIV nao recebe push: o `ComplianceSyncWorker` (Oban cron `11-59/15 * * * *` em prod, `backend/config/runtime.exs:689`; a cada 15 min em dev, `backend/config/config.exs`) sincroniza por entidade tres coisas (`backend/lib/fluxiq/workers/compliance_sync_worker.ex:37-66`):

1. `PixCompliance.sync_infractions/1`: paginado via `Provider.list_infractions` (OnZ `POST /v3/dict/infracao/list`, `backend/lib/fluxiq/services/pix_providers/onz/endpoints/infractions.ex:80-82`), 100 itens/pagina ate 100 paginas (`backend/lib/fluxiq/use_cases/pix_compliance/pix_compliance.ex:438-439`), upsert em `pix_infractions`.
2. `PixCompliance.sync_recoveries/1`: espelha `pix_med_recoveries` via `POST /v4/dict/funds-recovery/list` (`backend/lib/fluxiq/services/pix_providers/onz/endpoints/med.ex:26-29`).
3. `PixCompliance.sync_med_event_notifications/1`: consome a fila `/v4/dict/event-notifications/pending`, filtra `EntityType == "FUNDS_RECOVERY"`, ingere o recovery e marca a notificacao como processada para drenar a fila (`pix_compliance.ex:561-633`).

O moduledoc do worker registra o motivo historico: a sincronizacao sincrona no endpoint de listagem travava o admin (8K+ itens = 30-120s por request), por isso virou cron assincrono (`compliance_sync_worker.ex:3-8`).

Anti-ruido de auditoria: o upsert de infracao so gera linha de audit quando muda campo "material" (status, analysis_result, resolved_at, deadlines, valores); re-sync que so atualiza `provider_data`/`last_modified` persiste via `Repo.update_without_audit` (`backend/lib/fluxiq/schemas/pix_compliance/pix_infraction.ex:62-79` e `pix_compliance.ex:700-714`). O comentario registra que esse re-upsert era ~99% do audit em PRD.

### 1.2 Auto-processamento de infracoes REFUND_REQUEST

Gatilho no proprio upsert: infracao nova OU ainda em status aberto, sem bloqueio cautelar ja existente para o e2e (guard `cautelar_block_exists_for_infraction?`, `pix_compliance.ex:723-727` e `1033-1041`). Gated pela flag `infraction_auto_processing_enabled`, default ON em prod (`backend/config/runtime.exs:165-167`, "decisao Luiz 2026-06-15").

Papel do participante primeiro (`avivpay_infraction_role/1`, `pix_compliance.ex:785-796`): so o participante CREDITADO pode fechar a infracao no OnZ; quando a AVIV e a debitada, marca `PROCESSING` e nao chama a API (evita o loop infinito do erro `ONZ_0400`, `pix_compliance.ex:752-777` e guarda de rejeicao permanente em `1095-1107`).

Decisao pura `infraction_decision/3` (`pix_compliance.ex:864-875`):

- config desabilitada: `:skip`;
- e2e NAO existe nas transactions: `:auto_deny` com texto padrao pedindo arquivamento citando a Resolucao BCB 103/2021 (`@no_e2e_match_deny_reason`, `pix_compliance.ex:852`);
- valor <= threshold (default R$ 1.000 = 10.000.000 subcentavos, `pix_compliance.ex:744`): `:auto_deny` com a justificativa da config;
- valor acima do threshold com toggle cautelar ligado: `:create_hold` (vai ao MED Processor);
- toggle desligado: `:manual_review` (fica para hold manual).

A config de infracao e independente da config MED e resolve em 4 camadas: por conta, por merchant, institucional (linha entity com merchant NULL) e default hardcoded (`resolve_infraction_config/2`, `pix_compliance.ex:886-898`; schema `backend/lib/fluxiq/schemas/pix_compliance/infraction/infraction_configuration.ex`).

O auto-deny chama `Provider.close_infraction` com `AnalysisResult=DISAGREED` (`pix_compliance.ex:1046-1049`, OnZ `POST /v3/dict/infracao/{id}/fechar`, `endpoints/infractions.ex:85-88`), marca a infracao `CLOSED/DISAGREED` local, libera cautelares do e2e via `BlockRelease.release_for_e2e` (`pix_compliance.ex:1062` e `backend/lib/fluxiq/use_cases/pix_compliance/med/block_release.ex`) e dispara webhook `pix.infraction.resolved`.

### 1.3 Bloqueio cautelar MED (modelo WAL com dinheiro de verdade)

`Fluxiq.UseCases.PixCompliance.Med.Processor.process_inbound_med/1` (`backend/lib/fluxiq/use_cases/pix_compliance/med/processor.ex:68-118`):

1. Ignora infracao que nao seja para o nosso ISPB (`processor.ex:70-75`).
2. Resolve merchant/conta pelo e2e com lookup O(1) por PK reversa (TB id derivado do E2E via `Fluxiq.ID.tb_id_from_e2e`, `processor.ex:896-915`) e retry com sleep para a corrida com PIX-IN (5 tentativas de 1s, `processor.ex:869-893`). E2E inexistente = auto-DISAGREED (`processor.ex:92-108`).
3. Config MED em 4 camadas (conta, merchant, institucional, default 72h/R$ 1.000, `processor.ex:275-342`).
4. Decisao `cautelar_decision/2` (`processor.ex:297-305`): disabled, auto_reject (<= threshold), create_hold, manual_review.
5. `create_cautelar_block`: `Repo.transaction` + `pg_advisory_xact_lock` por conta (serializa todas as operacoes MED da conta, `processor.ex:400-403`), idempotencia por (infraction_report_id, e2e) (`processor.ex:421-433`).
6. Modelo WAL (`processor.ex:9-17` e `435-553`): a linha PG `med_cautelar_blocks` nasce ANTES dos transfers TB, com IDs pre-computados deterministicamente do E2E (`BatchChain.root_id` com `Disc.med_hold()`, `processor.ex:451-468`). Tres cenarios em `determine_hold_scenario/3` (`processor.ex:555-573`):
   - full: saldo cobre valor + fee, hold TB pending do valor todo, `blocked_amount = 0`;
   - partial: hold do que da, `blocked_amount` = o que faltou;
   - zero: nada no TB, `blocked_amount = valor + fee`.
   O saldo disponivel ja desconta obrigacoes nao cobertas anteriores (`ObligationCheck.unfunded_amount`, `processor.ex:439-441`).
7. Transfers TB pending: `med_hold_covered` (sharp_code 1022) + fee `med_hold_fee` (1031) linkados, ambos com `user_data_128 = block.id` para reconstruir a historia por bloco (`processor.ex:498-515` e `590-634`; codes em `backend/lib/fluxiq/util/codes/transfer_code.ex:228-283`). Fee via `FeeCalculator` tipo `pix_med_hold` (`processor.ex:679-701`).
8. Finalizacao (`processor.ex:703-763`): invalida o cache de saldo de display (`BalanceCache.invalidate`), audita `MED_CAUTELAR_BLOCKED` com o cenario, notifica cliente e compliance (e-mail so para compliance, `processor.ex:1004-1006`), dispara webhook `pix.refund.requested` e responde `ACKNOWLEDGED` ao provider.

IMPORTANTE: `blocked_amount` na AVIV significa a porcao NAO coberta (unfunded). E essa porcao entra no calculo do saldo disponivel do PIX-out: `BalanceGuard`/`BalanceCheck` subtraem `ObligationCheck.unfunded_amount` (`backend/lib/fluxiq/use_cases/payments/pix_out/balance_guard.ex:53,114,307` e `balance_check.ex:73`). Ou seja, mesmo o valor que nao coube no hold TB bloqueia saidas.

### 1.4 UnfundedDrainer (dreno da porcao nao coberta)

`backend/lib/fluxiq/use_cases/pix_compliance/med/unfunded_drainer.ex`: dreno atomico por bloco, dois modos de lock (`:sync` com `pg_advisory_xact_lock` no hot path de saida; `:async` com try-lock no cron, `unfunded_drainer.ex:1-30`). O hot path e ligado por `use DrainHelper` nos handlers de saida `pix.ex:5`, `ted.ex:5`, `tef.ex:8`, `return.ex:3` (o handler Med implementa override `:ok` para nao recursar, `med.ex:544-546`). O cron `UnfundedDrainerPeriodic` roda de hora em hora em prod (`runtime.exs:707`). Self-heal: SET absoluto de `blocked_amount` a partir da verdade do TB.

### 1.5 Analise, defesa e devolucao MED (pacs.004)

- Defesa do merchant: `Med.submit_defense` + fila de analistas `list_defenses` (default `defense_submitted`), com escopo por conta para isolamento entre merchants (`backend/lib/fluxiq/use_cases/pix_compliance/med/med.ex:263-326`).
- Veredito (`Processor.resolve_analysis/1`, `processor.ex:120-186`):
  - `founded`: mantem o hold, publica `AGREED` ao provider; a devolucao e acao explicita separada;
  - `unfounded`: voida todos os legs pending (hold + fee + drainer) num unico batch linkado (`HoldLegs.build_void_batch`, `processor.ex:788-851`), publica `DISAGREED`, libera o bloco (CHECK constraint exige `analysis_status` terminal para sair de `active`, `med_cautelar_block.ex:114-134`).
  - `manual_release` (admin cancela sem julgar merito): `analysis_status=cancelled` (`processor.ex:188-220`).
- Resposta ao BACEN sempre via provider: `close_infraction` com `AnalysisResult`/`AnalysisDetails` (NATS foi descomissionado; o comentario registra que o branch NATS era fan-out interno que nunca chegava ao BACEN, `processor.ex:1044-1112`).
- Devolucao MED (`Med.execute_return/1`, `med.ex:449-488`): passa pelo `Orchestrator` com o handler `Med` implementando `OutboundPayment.Behaviour`:
  - `validate_and_normalize`: bloco `active` + `founded` + hold consistente; `return_amount = requested_amount - blocked_amount` (so devolve o que esta coberto), gera E2E de devolucao proprio (`med.ex:549-631`);
  - `prepare_request`: re-verifica o estado sob transacao, cria `OutboundRequest` tipo "med" com prefixo `MEDRET`, grava `return_e2e_id` em `evidence_metadata` e avanca `return_stage=0` (`med.ex:641-689`);
  - `send_request`: pacs.004 com `return_code: "FR01"` via `Pacs004.OnzAdapter` (ICOM path `POST /v3/icom/pacs/004`, `backend/lib/fluxiq/use_cases/pix/pacs004/onz_adapter.ex:5`) (`med.ex:692-710`);
  - `finish`: aceito = stage 2 (aguarda callback pacs.002); erro permanente = callback sintetico RJCT; erro transitorio = fica em stage 1 para retry (`med.ex:713-797`);
  - `on_reply` (callback pacs.002): settled = post dos legs pending + legs de retorno (mirror -> transit -> SPI -> reserve, codes 1024/1025/1026) via `HoldLegs.build_post_batch`, bloco vira `transferred`/`return_stage=3`; rejected = void batch, bloco `released` + `FailedTransaction`; conflitos TB vs pacs.002 viram overrides auditados (`tb_override_voided`/`tb_override_posted`) (`med.ex:808-1239`).
- `handle_recovery_update`: callback de recovery `REFUND_COMPLETED`/`COMPLETED` marca `transferred` (auto-founded se preciso) (`processor.ex:222-269`).

### 1.6 Watchdogs e prazos (principio: prazo nao decide sozinho)

- `MedDefenseExpiration` (cron 5 min, `runtime.exs:696` e `config.exs:83`): blocos ativos `pending`/`defense_submitted` com deadline a <= 30 min criam UM alerta critico dedup por bloco/razao; explicitamente NAO auto-aceita fraude nem dispara pacs.004 (`backend/lib/fluxiq/workers/med_defense_expiration.ex:1-54`).
- `MedReturnRecovery` (cron 5 min, `runtime.exs:685`): devolucao presa (`return_stage` 0/1/2 ha > 300s) gera alerta critico dedup, visibility-only (`backend/lib/fluxiq/workers/med_return_recovery.ex:1-42`).
- `MedOrphanHoldReconciler` (cron `5-59/15`, `runtime.exs:650`): voida "phantom holds" (bloco released/transferred no PG com pending vivo no TB), resolvendo os ids REAIS no TB por `user_data_128 = block.id`, independente das colunas PG NULL-prone (`backend/lib/fluxiq/use_cases/pix_compliance/med/orphan_hold_reconciler.ex:1-25`).
- `Rc6SlaChecker` (cron 15 min, `runtime.exs:698`): alerta de SLA de 24h de compartilhamento de indicios de fraude RC6, tambem visibility-only (`backend/lib/fluxiq/workers/rc6_sla_checker.ex:1-8`).

### 1.7 Bloqueio cautelar manual

`ManualCautelarBlock.create/4` (`backend/lib/fluxiq/use_cases/pix_compliance/med/manual_cautelar_block.ex:1-90`): admin compliance cria hold com cap servidor de R$ 50.000 (500.000.000 BU), justificativa >= 20 chars, `infraction_report_id = "MANUAL-<uuid>"`, mesmo padrao WAL, e-mail assincrono. Rota `POST /med/cautelar/manual` (`backend/lib/fluxiq_web/routers/admin_router.ex:341`).

### 1.8 Devolucao comum (nao MED) iniciada pelo recebedor

Handler `Return` (`backend/lib/fluxiq/use_cases/payments/outbound_payment/return.ex`):

- validacoes: transacao original inbound PIX (`return.ex:377-390`), valor <= original (`392-395`), janela de 90 dias (`397-411`), fee `pix_devolucao` (`420-447`);
- TETO CUMULATIVO por transacao original: `cumulative_returned/1` soma `ReturnRequest` pending+posted e o `prepare_request` faz rollback `:ceiling_exceeded` dentro da transacao (`return.ex:94-99` e `353-370`);
- envio pacs.004 via OnZ e callback pacs.002 com post/void + splits de fee + espelho caixa (`return.ex:128-348`);
- webhooks `pix.refund.completed`/`pix.refund.failed` (`return.ex:454-516`).

Codigos de devolucao: `PixReturnCode` valida whitelist MD06/BE08/FR01/SL02 e normaliza texto livre para o campo motivo, com prova de PRD 2026-07-08 (todas as devolucoes com texto livre no campo de codigo falharam com AB03; MD06/FR01 liquidaram) (`backend/lib/fluxiq/util/codes/pix_return_code.ex:1-50`). O controller externo aplica a normalizacao (`backend/lib/fluxiq_web/controllers/external/pix/refund_controller.ex:22-25`) com validacao de propriedade da transacao.

### 1.9 Reivindicacoes (claims)

Somente espelho + proxy: endpoints OnZ `POST /v3/dict/reivindicacao` (create/confirm/cancel/list, `backend/lib/fluxiq/services/pix_providers/onz/endpoints/claims.ex`), sync em `pix_claims` com `deadline` vindo de `ResolutionPeriodEnd` do provider (`pix_compliance.ex:1195-1252`). NAO ha worker de prazo de claim: `PixClaim` so e referenciado em `pix_compliance.ex` (verificado por grep em `workers/` e `use_cases/`), e nenhuma entrada de cron trata claims (`runtime.exs` crontab). A gestao de prazos de reivindicacao fica no provider OnZ.

### 1.10 Telas, RBAC e relatorios

- Rotas admin: infracoes (list/pending-defenses/export/defense/resolve), configs MED e Infraction (globais + por merchant), blocos cautelares (defenses/evidence/analyze/release/execute-return/infraction-reports/refunds), manual block, drain manual por conta, tokens da API interna de integracao MED (`backend/lib/fluxiq_web/routers/admin_router.ex:301-350`).
- Merchant: infracoes + defesa, blocos cautelares proprios com escopo por conta (`backend/lib/fluxiq_web/routers/merchant_router.ex:279-284,587`; isolamento `get_block_scoped`, `med.ex:263-279`).
- API externa: `POST /med/:id/defense`, `GET /med` (`backend/lib/fluxiq_web/routers/external_router.ex:27,51-52`); API de integracao com escopos (`RequireIntegrationScope`, ex. `med:read`, `backend/lib/fluxiq_web/plugs/require_integration_scope.ex:6`) e rotas de config/hold/respond (`backend/lib/fluxiq_web/routers/integration_router.ex:23-37`).
- RBAC de perfis admin: `Admin.Rbac.PermissionsController` + admin profiles (`admin_router.ex:547,732`). O screenshot `med-rbac-gestao-meds-compliance-officer.png` na raiz do repo mostra a tela "Gestao de MEDs" sob perfil compliance officer (evidencia visual, nao codigo).
- Relatorios: `PixCompliance.Report` monta export CSV/XLSX de infracoes + MEDs por conta (50k linhas) e lista filtrada (10k) com atribuicao de recovery via `funds_recovery_id` (`backend/lib/fluxiq/use_cases/pix_compliance/report.ex:1-28`); ha ainda export de ranking de compliance (`backend/lib/fluxiq_web/controllers/admin/analytics/compliance_ranking_export_controller.ex`) e buckets de status para analytics (`backend/lib/fluxiq/use_cases/admin/analytics/infraction_status_buckets.ex`).

---

## 2. Como a Monetarie faz

A Monetarie tem DUAS camadas: a cabine PIX (participante direto, DICT v2.11.0 REST + SPI/ICOM) e o Core (ledger TigerBeetle + espelho PG), ligadas por NATS.

### 2.1 Cabine: polling inbound do DICT (Lane 3)

`DictService.Workers.DictInboundPollWorker` roda A CADA MINUTO (`pix/backend/config/config.exs:176`) e sincroniza 4 pernas: `claims_donor`, `claims_claimer`, `infractions`, `refunds` (`pix/backend/apps/dict_service/lib/dict_service/workers/dict_inbound_poll_worker.ex:44-55`). O sync e INCREMENTAL com cursor persistente por `LastModified` em `monetarie_dict.dict_sync_cursors`, usando `ModifiedAfter` e paginando so o delta (moduledoc `dict_inbound_poll_worker.ex:23-28`; pernas em `dict_service/sync/inbound_sync.ex:74,244-247`). Fraude (marcadores) fica fora do ciclo automatico porque o BACEN rejeita `GET /fraud-markers/?ModifiedAfter` com 400 (`dict_inbound_poll_worker.ex:13-18`).

### 2.2 Cabine: roteamento de infracao recebida contra nos

`DictService.Infractions.poll_and_route_inbound_from_bacen/2` (`pix/backend/apps/dict_service/lib/dict_service/infractions.ex:653-698`): espelha cada relato e, quando somos o respondente (creditor == nosso ISPB) e o relato esta OPEN, publica `monetarie.dict.med.infraction_received` com dedup `msg_id "med-infraction-#{e2e}"` (`infractions.ex:700-751`). Como a listagem 2.11 nao devolve o valor da transacao, busca o valor na transacao local pelo E2E (`infractions.ex:718-729` e `757-766`).

O consumidor `SpiService.Med.InfractionResponder` (push consumer NATS, `pix/backend/apps/spi_service/lib/spi_service/med/infraction_responder.ex:16-21`) chama `SpiService.Med.open_inbound_fraud_claim/1`: cria o fraud claim (deadline de 90 dias, `opening_reported_at`) e aplica o "bloqueio cautelar" de 72h + prazo de resposta de 7 dias (`pix/backend/apps/spi_service/lib/spi_service/med.ex:69-99` e `128-146`), idempotente por e2e.

LIMITACAO CENTRAL: o `apply_cautelar_block` da cabine so muda o STATUS do claim (`status: "cautelar_blocked"`, `med.ex:138-146`). Nao ha hold em TigerBeetle, nao ha escrita em `Balance.blocked_amount`, e nenhum evento chega ao Core para segurar o dinheiro do cliente. Verificado por grep: nenhum consumidor de `ChainBlock`/`Claim` toca saldo ou caminho de saida (unico uso de `blocked_amount` fora do MED e a conta PI/judicial; `shared/schemas/spi/balance.ex:15` nao e escrito pelo MED).

### 2.3 Cabine: chain blocking (5 hops)

`SpiService.Med.ChainBlocker` (`pix/backend/apps/spi_service/lib/spi_service/med/chain_blocker.ex`): rastreia ate 5 hops, 500 tx/hop, janela de 24h (`chain_blocker.ex:14-16`), aplica bloqueio parcial pelo saldo disponivel e grava `ChainBlock` rows. Problemas de aderencia ao modelo canonico:

- consulta `from(t in "transactions", where: t.status == "COMPLETED" ...)` com colunas `creditor_account`/`debtor_account`/`completed_at` (`chain_blocker.ex:189-256`), mas a tabela canonica de producao e `monetarie_spi.messages` e `monetarie_spi.transactions` e "mostly empty" (gotcha 10 do `pix/CLAUDE.md`); em producao o trace tenderia a voltar vazio. NAO VERIFICADO em runtime.
- o saldo vem de `accounts.available_balance_cents` na cabine (`chain_blocker.ex:258-291`), nao do Core (dono do dinheiro); em erro de infra faz fallback `:max` (bloqueia o valor do claim, citando BCB 493/2025 Art. 41).
- os `ChainBlock` rows tambem nao seguram dinheiro em lugar nenhum (mesma limitacao do 2.2).

### 2.4 Cabine: timers e prazos

- `SpiService.Med.TimerEnforcer` (cron 5 min, `pix/backend/config/config.exs:79`): 30 min para abertura da notificacao, 7 dias de resposta do respondente, 90 dias de janela total; escala (`escalated=true`) e expira claims (`pix/backend/apps/spi_service/lib/spi_service/med/timer_enforcer.ex:20-152`). A escalada e so marcacao + log; o proprio codigo admite que notificar compliance/BACEN esta pendente (`timer_enforcer.ex:210-214`).
- `SpiService.Med.CautelarWorker` (cron 15 min, `config.exs:85`): libera cautelares vencidas (72h -> `analysis`), expira claims de 90 dias e roda o TimerEnforcer (`med/cautelar_worker.ex:21-46`).
- Reivindicacoes DICT com timers DURAVEIS: `DictService.Nats.ClaimDeadlineConsumer` consome `monetarie.dict.deadlines.>` agendados via JetStream `Nats-Msg-Deliver-After` (nunca `Process.send_after`): `CLAIM_DONOR_NOTIFICATION` em D+7, `CLAIM_DONOR_TIMEOUT` em D+14 (OWNERSHIP confirma, PORTABILITY cancela, paridade LegadoPIX com cita de arquivo .cs), `CLAIM_CLAIMER_TIMEOUT` em D+30 (`pix/backend/apps/dict_service/lib/dict_service/nats/claim_deadline_consumer.ex:1-63`). Cobre os DOIS papeis (doador e reivindicador), coisa que a AVIV nao tem.

### 2.5 Cabine: resolucao MED via SPI (camt.029)

`SpiService.Med.approve_claim/deny_claim` emitem camt.029 (`RsltnOfInvstgtn`) com `ACCR`/`RJCR` atomicamente com a transicao de status, via outbox + OutboundSender (assinatura XMLDSig + XSD + envio) (`med.ex:293-354` e `emit_med_message` em `503-552`). O caminho contrario existe: o InboundProcessor publica `monetarie.spi.med.resolution_received` ao receber camt.029 e o `ResolutionConsumer` chama `release_cautelar_from_resolution/1`, correlacionando por `OrgnlEndToEndId` e `cancellation_id`, liberando chain blocks e fechando o claim como settled/denied (`med.ex:368-446`; `med/resolution_consumer.ex:1-25`). A abertura de infracao e a recuperacao de fundos MED 2.0 vao pela API REST do DICT (o codigo removeu emissoes SPI camt.035/camt.056 alucinadas, `med.ex:19-24` e `494-500`).

### 2.6 Cabine: devolucao automatica (pacs.004) e devolucao manual

- Automatica (Lane 2): quando o credito ja liquidado pelo BACEN nao pode ser aplicado (conta invalida/bloqueada/encerrada: AC03/AC06/AC07/AC14/AG03), o InboundProcessor NAO credita o cliente, publica `RETURN_CREATED` com dedup, responde pacs.002 `ACSP` e credita a conta PI ate a devolucao sair (`pix/backend/apps/spi_service/lib/spi_service/workers/inbound_processor.ex:480-492` e `740-830`). Sancao e rejeicao SPI seguem em pacs.002 RJCT, nao em devolucao.
- `SpiService.Workers.ReturnProcessor` (consumer de `monetarie.spi.return.>`, `return_processor.ex:14-20`): valida elegibilidade + regras SPI (`SpiValidator.validate_outbound_return`, `return_processor.ex:90-98`), garante `RtrId` no formato de 32 chars (regex `return_processor.ex:37`), faz DEBIT-THEN-SEND bloqueando o saldo PI antes de enviar (`Balances.create_block`, `return_processor.ex:136-168`), monta a pacs.004 assinavel e cria linha PROPRIA da devolucao em `monetarie_spi.messages` com idempotencia real por indice unico parcial (`operation_time` deterministico derivado do RtrId, particao RANGE) (`return_processor.ex:170-363`). O StatusUpdater publica `monetarie.spi.return.rejected` num RJCT para o Core estornar a materializacao (`workers/status_updater.ex:344-359`).
- Manual: `POST /transactions/:transaction_id/return` no gateway (`settlement_service_web/router.ex:538`) chega em `SpiService.Transactions.create_return` que checa `return_amount > available_amount` (devolucoes parciais acumuladas) antes de publicar `RETURN_CREATED` (`spi_service/transactions.ex:296-321`).

### 2.7 Cabine: acervo MED 2.0 outbound e refund_requests

- `DictService.FundsRecovery`: cria recovery (fraud_category, defaults iguais aos do BACEN: min R$ 100, 500 tx, 24h/hop, 5 hops), tracking graph, refund requests contra outros PSPs (`pix/backend/apps/dict_service/lib/dict_service/funds_recovery.ex:1-60`).
- `monetarie_dict.refund_requests` e o consolidado: espelho 1:1 do `<Refund>` do BACEN (status BACEN OPEN/CLOSED/CANCELLED distintos do workflow local PENDING/PROCESSING/COMPLETED/CANCELLED/FAILED) + cancelamento com motivo (`dict_service/funds_recovery/refund_request.ex:1-55`), alimentado pela perna `:refunds` do poll (2.1).
- Infracoes: fluxo completo acknowledge/analyse/close/cancel exposto no controller (`dict_service_web/controllers/infraction_report_controller.ex:63-134`) e eventos publicados (`infractions.ex:148,312,998,1010`).
- Telas admin (mandato 07-07): `MedHubView`, `FundsRecoveryList/Detail`, `InfractionList/Detail`, `RefundListView`, `FraudMarkerList/Detail`, `ClaimList/Detail` (`pix/frontend/admin/src/router/index.ts:144-156,268-279`).

### 2.8 Core: MED com hold REAL no TigerBeetle (mas com a ponte quebrada)

O Core tem um processor proprio: `Monetarie.UseCases.Med.Processor` consome eventos `monetarie.dict.infractions.*`/`monetarie.dict.recovery.*` via `DictConsumer` -> `PixHandler` (`core/backend/lib/monetarie/infra/nats/consumers/dict_consumer.ex:24,143-150`; `handlers/pix_handler.ex:1198-1227`). Threshold por merchant, auto-DISAGREED se e2e nao existe (espelho do coreproviders, comentario em `use_cases/med/processor.ex:66-69`), e cria bloco com hold DE VERDADE no TB via `Monetarie.UseCases.Transaction.hold_funds` com pool MED separado do judicial (`@med_pool_base 850_000_000`, `processor.ex:33-35,304-306`).

Defeitos e lacunas encontrados (nivel codigo, sem prova de runtime nesta trilha):

1. DEFEITO CONFIRMADO (entrada morta): a cabine publica no subject `monetarie.dict.infractions.reported` com `event_type: "INFRACTION_REPORTED"` no PAYLOAD (`dict_service/infractions.ex:148-149`; `dict_external_reconciler.ex:1062-1063`; mapeamento de subjects em `dict_service/events/nats_publisher.ex:305`). O `DictConsumer` do Core deriva o event_type do SUFIXO DO SUBJECT (`"monetarie.dict.infractions." <> event_type`, `dict_consumer.ex:143`, subject original sem transformacao, `dict_consumer.ex:235-243`) e chama `PixHandler.handle_med_infraction("reported", payload)`. A clausula que processa exige exatamente `"INFRACTION_REPORTED"` (`pix_handler.ex:1198`); `"reported"` cai no catch-all que apenas audita (`pix_handler.ex:1210-1217`). Consequencia: o bloqueio cautelar do Core nunca e acionado pelo evento da cabine.
2. DEFEITO CONFIRMADO (saida morta): as respostas do Core (ACKNOWLEDGED, AGREED, DISAGREED) vao para `monetarie.dict.infractions.response` (`use_cases/med/processor.ex:467-477`) e NAO existe nenhum consumidor desse subject no monorepo (grep: unico hit e o proprio publisher). O comentario do codigo diz "Publish AGREED to PIX Cabin, it will initiate pacs.004 return" (`processor.ex:109-113`), mas a cabine nao escuta; o AGREED nunca vira `RETURN_CREATED`/pacs.004 e o DISAGREED nunca vira `analyse/close` no DICT.
3. RISCO REGULATORIO CONFIRMADO (all-or-nothing): o hold do Core e tudo-ou-nada; saldo insuficiente = `auto_reject(params, "Saldo insuficiente para bloqueio cautelar")`, ou seja, responde DISAGREED justamente no cenario tipico de fraude (conta ja drenada) (`processor.ex:364-366`). A AVIV cobre com cenarios partial/zero + drainer (1.3/1.4).
4. COLISAO DE SEMANTICA PLAUSIVEL (dois codigos MED convivendo): o port Owem/AVIV de 07-04 (`use_cases/med/{unfunded_drainer,hold_legs,obligation_check,block_release}.ex` + workers) trata `blocked_amount` como porcao NAO coberta e o `self_heal` do drainer SETa `blocked_amount = current_overdraft` calculado a partir de `requested_amount` (`unfunded_drainer.ex:338-348`; `hold_legs.ex:102-103`). Os blocos criados pelo Processor legado tem `requested_amount` nil e `blocked_amount` = valor TOTAL efetivamente bloqueado (`processor.ex:306-325`), entao `current_overdraft = 0` e o self_heal ZERA o `blocked_amount` desses blocos. O cron roda de hora em hora em prod (`core/backend/config/runtime.exs:455`) e o scan pega exatamente esses blocos (`status active/transferred AND blocked_amount > 0`, `workers/med/unfunded_drainer_periodic.ex:55-64`). Alem de corromper o registro do valor bloqueado, o release depois passaria `amount = 0` para `Transaction.release_funds/2`, cujo unico head exige `release_amount > 0` (`use_cases/transaction.ex:82`), o que estoura FunctionClauseError no caminho de liberacao. Impacto vivo NAO VERIFICADO (exige query no banco, fora do escopo desta trilha).
5. GAP CONFIRMADO (enforcement no PIX-out): `ObligationCheck.unfunded_amount` nao tem NENHUM chamador de producao no Core (grep: so moduledocs de `drain_helper.ex:49` e `limit_check_context.ex:24`), e nenhum handler faz `use DrainHelper` (grep sem hits fora do proprio arquivo). Ou seja, diferente da AVIV (1.3/1.4), o disponivel do PIX-out do Core nao desconta obrigacoes MED nao cobertas e o hot path nao drena.
6. Reconciler MED portado (o que ligamos em dry-run): `Monetarie.Workers.Med.OrphanHoldReconciler`, cron diario 05:20 BRT (`runtime.exs:463`), modo default `"log"` (dry-run) via env `MED_ORPHAN_RECONCILER_MODE`, void SO por verdade do TB (`user_data_128`), com regras de duvida (pagina de legs cheia pula; bloco transferred com legs abertos pula e loga critico) e distincao explicita do `StaleHoldChecker` (`workers/med/orphan_hold_reconciler.ex:1-58`).
7. Admin Core: analyze/release com RBAC (feature `operacoes.pix` edit ou permissao `regulatory`, `monetarie_web/controllers/admin/med_cautelar_controller.ex:20-26`), configs MED por merchant (`admin/med_config_controller.ex`), API v2 para blocos (`v2/med_cautelar_blocks_controller.ex`). Nao ha bloqueio manual com cap (equivalente ao ManualCautelarBlock da AVIV) nem `execute_return` no Core (`use_cases/med/med.ex` nao tem; grep).

---

## 3. Diferencas estruturais e por que existem

1. Direto vs indireto: a AVIV so enxerga o que o OnZ expoe (endpoints singulares `/v3/dict/{infracao,reivindicacao,devolucao}` + `/v4/dict/funds-recovery`), entao o desenho e "espelho local + política de auto-processamento + resposta via provider". A Monetarie fala com o DICT/SPI reais: precisa (e tem) acervo proprio com paginacao incremental, camt.029 assinada, pacs.004 propria com RtrId, XSD e conta PI.
2. Um app vs duas camadas: na AVIV ledger e compliance vivem no mesmo OTP app, entao o bloqueio cautelar e nativamente dinheiro (TB pending) e a politica alimenta o saldo. Na Monetarie o dinheiro esta no Core e o conhecimento regulatorio esta na cabine; o MED depende de uma ponte NATS que hoje esta quebrada nos dois sentidos (2.8 itens 1 e 2), deixando DOIS cautelares desconexos: o claim de status na cabine (sem dinheiro) e o hold TB no Core (sem gatilho).
3. Broker vs HTTP: a AVIV matou o NATS e responde infracao inline no provider (com guarda de erro permanente); a Monetarie usa outbox+JetStream com dedup e timers duraveis (Deliver-After), o que da a ela timers de claim de qualidade superior, mas cria os pontos de quebra de contrato subject/payload observados.
4. Historia dos ports: o Core da Monetarie carrega DUAS geracoes de MED (processor proprio all-or-nothing + port Owem/AVIV do drainer/hold_legs) com semanticas conflitantes de `blocked_amount`, coisa que a AVIV nao tem (la o WAL model e unico).

## 4. O que a AVIV tem de melhor (candidatos a port, com esforco)

1. Cadeia MED fechada com dinheiro: WAL partial/zero + unfunded + drainer sync/async + devolucao orquestrada com callback pacs.002 idempotente e overrides auditados (1.3, 1.4, 1.5). Esforco: ALTO (mas o Core ja tem hold_legs/drainer/obligation_check portados; o que falta e migrar o Processor para o WAL e ligar os consumidores, ver P0-3/P0-4).
2. `ObligationCheck` no saldo disponivel do PIX-out + `use DrainHelper` nos handlers de saida (`balance_guard.ex:53`, `pix.ex:5`). Esforco: MEDIO (modulos ja existem no Core, e wiring).
3. Politica de auto-processamento de infracoes com 4 camadas de config, papel do participante, textos regulatorios prontos (BCB 103/2021) e guarda de rejeicao permanente (1.2). Esforco: MEDIO.
4. Watchdogs deduplicados visibility-only com o principio "prazo nao decide sozinho" (MedDefenseExpiration, MedReturnRecovery) (1.6). Esforco: BAIXO.
5. Auditoria material-only nos upserts de espelho (`material_change?`) para nao inundar audit_logs (prova de PRD: ~99% do audit era re-sync) (1.1). Esforco: BAIXO.
6. Whitelist de codigos de devolucao `PixReturnCode` com prova empirica de PRD (texto livre no campo de codigo = AB03) (1.8). A Monetarie tem default "MD06" no ReturnProcessor (`return_processor.ex:190`) e validacao no SpiValidator cujo conteudo NAO VERIFIQUEI; portar a whitelist e barato. Esforco: BAIXO.
7. Teto cumulativo persistido de devolucoes por transacao original (`ReturnRequest` + rollback `:ceiling_exceeded` sob transacao) (1.8). A checagem da Monetarie em `transactions.ex:296` compara com `available_amount` da transacao, mas o caminho por evento (auto-return) nao passa por ela. Esforco: BAIXO-MEDIO.
8. Bloqueio cautelar manual com cap R$ 50k e validacoes (1.7). Esforco: BAIXO.
9. Relatorios/export de infracoes+MED (CSV/XLSX) e buckets analytics (1.10). Esforco: BAIXO-MEDIO.
10. Resolucao de merchant por PK reversa O(1) (tb_id derivado do E2E) com retry anti-corrida PIX-IN (`processor.ex:869-915`): padrao bom para volume (evita scan por e2e). Esforco: ja parcialmente coberto pelo indice por E2E; avaliar.

## 5. O que a Monetarie tem de melhor (nao regredir)

1. Acervo regulatorio em casa por ser participante direto: infracoes com fluxo acknowledge/analyse/close/cancel proprio, claims nos DOIS papeis, `refund_requests` espelho 1:1 do BACEN, funds recovery com tracking graph, fraud markers (2.1, 2.2, 2.7). A AVIV depende do formato OnZ.
2. Timers de claim DURAVEIS via JetStream `Nats-Msg-Deliver-After` com auto-resolucao D+7/D+14/D+30 e paridade provada com o LegadoPIX (2.4). A AVIV nao gerencia prazo de claim nenhum (1.9).
3. Devolucao automatica de credito inaplicavel (Lane 2) com pacs.002 ACSP + pacs.004 e linha propria na timeline, com DEBIT-THEN-SEND na conta PI (2.6). Nao encontrei analogo na AVIV (grep por auto-return inbound vazio; provavelmente fica no OnZ, NAO VERIFICADO).
4. camt.029 de resolucao MED emitida e consumida no SPI com outbox atomico (2.5).
5. Poll incremental com cursor persistente (`dict_sync_cursors` + `ModifiedAfter`), muito mais barato em volume que o full-list paginado da AVIV (ate 100 paginas x 100 itens por ciclo de 15 min) (2.1 vs 1.1).
6. Idempotencia de devolucao por indice unico parcial com `operation_time` deterministico derivado do RtrId em tabela particionada (2.6).
7. Reconciler MED portado com modos log/void e regras de duvida mais conservadoras que o original (pagina cheia pula, transferred com legs abertos pula) (2.8 item 6).

## 6. Recomendacoes priorizadas (meta: mais volume, menos latencia, sem risco regulatorio)

P0 (defeitos que deixam o MED da Monetarie inoperante ou perigoso):

- P0-1. Corrigir o dispatch do `DictConsumer` do Core: rotear pelo `payload["event_type"]` (ou casar a clausula com o sufixo "reported"). Hoje `PixHandler.handle_med_infraction/2` nunca recebe "INFRACTION_REPORTED" (evidencia: `dict_consumer.ex:143` vs `infractions.ex:148-149` vs `pix_handler.ex:1198`).
- P0-2. Dar consumidor ao `monetarie.dict.infractions.response` na cabine: DISAGREED -> `Infractions.analyse_report/close_report` no DICT; AGREED -> publicar `RETURN_CREATED` (pacs.004 FR01) reutilizando o ReturnProcessor; ACKNOWLEDGED -> `acknowledge_report`. Sem isso a resposta do Core morre no broker (`processor.ex:476`, zero consumidores).
- P0-3. Resolver a colisao de semantica de `blocked_amount` no Core antes de qualquer volume: ou migrar o Processor legado para o modelo WAL do port (gravar `requested_amount`, `blocked_amount` = unfunded, `hold_transfer_id` com `user_data_128`), ou excluir blocos legados (requested_amount nil) do scan do drainer. Hoje o cron horario pode zerar `blocked_amount` de blocos validos e quebrar o release (`unfunded_drainer.ex:338-348` + `processor.ex:306-325` + `transaction.ex:82`).
- P0-4. Eliminar o auto-DISAGREED por saldo insuficiente (`processor.ex:364-366`): portar os cenarios partial/zero da AVIV. Responder "discordo" ao BACEN porque o fraudador ja sacou e o pior desfecho regulatorio possivel (BCB 493/2025).

P1 (fechar o ciclo e enforcement):

- P1-1. Ligar `ObligationCheck.unfunded_amount` e o `DrainHelper` no caminho de saida do Core (PIX-out/TED), como a AVIV faz em `balance_guard.ex:53` e `pix.ex:5`; e o que transforma bloqueio parcial em bloqueio de verdade sob volume.
- P1-2. Unificar os dois cautelares: o claim da cabine (`SpiService.Med.apply_cautelar_block`) deve disparar o hold no Core (evento com contrato explicito), e a liberacao (camt.029, 72h, verdito) deve liberar o hold. Hoje cabine marca status sem dinheiro e o Core segura dinheiro sem gatilho.
- P1-3. Corrigir o `ChainBlocker` para as fontes canonicas (`monetarie_spi.messages` para o trace, saldo via Core) ou retirar a funcionalidade da UI ate la; um rastreio que sempre volta vazio da falsa sensacao de conformidade com o Art. 41 (`chain_blocker.ex:189-256`).
- P1-4. Portar os watchdogs dedup da AVIV: alerta de deadline de defesa (30 min antes) e de devolucao MED presa, em cima de `med_cautelar_blocks` (Core) e `med_claims` (cabine), sempre visibility-only.
- P1-5. Definir o dono unico da resposta de infracao: hoje ha tres maquinas concorrentes (auto-claim da cabine, MED do Core, analyse/close manual no admin). Documentar e travar a matriz (quem responde, quem bloqueia, quem devolve) evita respostas duplicadas ao BACEN quando P0-1/P0-2 forem corrigidos.

P2 (paridade funcional e higiene):

- P2-1. Auditoria material-only nos upserts dos espelhos DICT (claims/infractions/refunds) da cabine, espelhando `material_change?` (previne o problema de audit que a AVIV mediu em PRD).
- P2-2. Bloqueio cautelar manual com cap e validacoes (port do `ManualCautelarBlock`).
- P2-3. Whitelist `PixReturnCode` (MD06/BE08/FR01/SL02) na entrada das devolucoes (gateway e ReturnProcessor), com a prova de PRD da AVIV como justificativa.
- P2-4. Teto cumulativo persistido de devolucoes por transacao original tambem no caminho por evento (hoje so o `create_return` manual checa, `transactions.ex:296`).
- P2-5. Relatorio/export CSV-XLSX de infracoes + MED e buckets de status para o admin (paridade com `PixCompliance.Report`).
- P2-6. Escalada do TimerEnforcer da cabine hoje so marca flag e loga (`timer_enforcer.ex:200-214`); ligar em notificacao real de compliance (o TODO esta no codigo).

## 7. Perguntas abertas / NAO VERIFICADO

1. Impacto vivo dos defeitos P0: prod tem 1 pacs.008 recebida e 0 enviadas ate 09/07, entao e provavel que nenhuma infracao MED real tenha exercitado essas pontes; confirmar em HML/prod com queries em `med_cautelar_blocks` (Core) e `med_claims` (cabine) esta fora do escopo read-only desta trilha.
2. `SpiValidator.validate_outbound_return` (P5/P6/P7): nao li o conteudo; verificar se ja valida o codigo de devolucao contra catalogo antes de portar P2-3.
3. `ChainBlocker` em runtime: qual schema o `SpiService.Repo` resolve para `"transactions"`/`"accounts"` sem prefix e se ha dados; a conclusao "trace vazio" e inferida do modelo canonico (gotcha 10 do `pix/CLAUDE.md`), nao provada em banco.
4. AVIV inbound auto-return: nao encontrei codigo de devolucao automatica de PIX-in na AVIV (grep vazio em `backend/lib`); presumo que rejeicao/devolucao a montante fica no OnZ, sem prova.
5. Se o `UnfundedDrainerPeriodic` do Core ja zerou `blocked_amount` de blocos legados em HML (o cron esta ativo desde o deploy do port de 07-04): exige query no banco.
6. AVIV `list_stuck_founded_blocks` (`med.ex:237-250`) e consumido por qual worker alem de visibilidade? Nao rastreei todos os call sites.
7. O modo do reconciler MED em HML: o codigo default e `"log"` (dry-run); a memoria do projeto registra que em HML foi ligado em VOID via task-def (`core-api:83`), que e configuracao de ambiente, nao codigo; confirmar o valor vivo da env `MED_ORPHAN_RECONCILER_MODE` por ambiente antes de confiar no void.
