# Port best-of-breed AvivPay -> Monetarie (execução 04/07)

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development para implementar este plano task a task nesta sessão, com revisão entre tasks.

**Goal:** Absorver no Monetarie os padrões de escala e produto do coreproviders comprovados por 812k PIX/dia na produção AVIV, sem regredir nada do que é nosso diferencial (cabine própria, NATS, centavos na borda).

**Architecture:** Ports dirigidos por fonte: cada task lê o módulo original em `/Users/luizpenha/coreproviders` (âncoras abaixo), adapta pelas regras de port da seção 0 e entrega com teste + commit. Nada é escrito de memória: se a âncora não bater com o código real, o subagente PARA e reporta em vez de inferir.

**Tech Stack:** Elixir/Phoenix (core `Monetarie.*` em `/Users/luizpenha/monetarie/core/backend`, cabine PIX umbrella em `/Users/luizpenha/monetarie/pix/backend`), Oban, ETS, Aurora PG 16, TigerBeetle 0.17.3, Vue 3 (IB em `core/apps/banking`).

**Base de evidência:** `docs/reports/2026-07-04-comparativo-aviv-coreproviders-monetarie.md/pdf` + matriz `docs/reports/2026-07-02-paridade-core-coreproviders.md`.

---

## 0. Regras de port (valem para TODAS as tasks; violação = task reprovada)

1. **Unidade monetária:** coreproviders usa subcentavos (1 BRL = 10.000) no fio e em vários módulos; contrato Monetarie é CENTAVOS na borda (commit `f79e17fa`). Todo valor copiado passa por revisão de unidade. Grep obrigatório no diff: `10_000`, `10000`, `div(`, `* 100`.
2. **Nunca inferir:** cada task começa lendo a fonte cp E o alvo mon nos caminhos exatos. Se um caminho/linha não existir como descrito, o subagente reporta a divergência e para; não improvisa.
3. **Namespaces:** `Fluxiq.*` vira `Monetarie.*` (core) ou `Shared.*`/`SpiService.*`/`SettlementService.*` (cabine). Repos: cp `Fluxiq.Repo/BaseRepo/BatchRepo` -> mon `Monetarie.Infra.Repo.Base` é o Repo do Core (gotcha do handoff 07/02: Core Repo = `Monetarie.Infra.Repo.Base`); cabine usa `Shared.Repo`.
4. **Feature flags default OFF** para tudo que toca caminho de dinheiro (checkpoint, caches). Ligamos por env depois de validar em HML.
5. **TDD:** teste primeiro quando houver lógica; para workers de infra, teste de unidade do cálculo + smoke de enfileiramento.
6. **Commits pequenos** por task, mensagem `feat(core|pix): ...` em pt-br, sem travessão.
7. **NATS permanece; OnZ não existe aqui.** Qualquer conceito OnZ (long-poll, cursor, slots) é adaptado para o equivalente NATS/ICOM ou descartado com nota.
8. **TigerBeetle:** nunca resetar; flags de conta imutáveis; qualquer código TB copiado respeita nosso client (`tigerbeetlex`) e o TB 0.17.3.

## Decisões fora do escopo de hoje (gate do dono / infra)

- TigerBeetle 3 réplicas em produção (terraform, dinheiro parado: proposta separada).
- Aurora reader + ReadRepo (exige instância nova; proposta separada).
- OpenTelemetry completo (hoje entra PromEx; OTel depois).
- Dev portal completo no IB (hoje entra só fee preview; portal é épico próprio).

---

## Onda A: Core (tasks A1-A5, paralelizáveis entre si)

### Task A1 [P0]: Export de tarifas real (fim do stub "ready" falso)

**Fonte (ler primeiro):**
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/use_cases/admin/fees/export_commands.ex` (pipeline: criar registro, enfileirar worker, status)
- worker S3 correspondente (localizar via `grep -rn "export" /Users/luizpenha/coreproviders/backend/lib/fluxiq/workers/ | grep -i fee`)
- Schema/migration do registro de export (grep `fee_export` em `cp priv/repo/migrations`)

**Alvo:**
- Modify: `core/backend/lib/monetarie_web/controllers/admin/coreproviders_parity_controller.ex:1160-1315` (região do stub; as linhas 1183 e 1309 devolvem `status: "ready"` falso)
- Create: `core/backend/lib/monetarie/use_cases/fees/export.ex`, `core/backend/lib/monetarie/workers/fees/fee_export_worker.ex`, migration `create_fee_exports`
- Test: `core/backend/test/monetarie/use_cases/fees/export_test.exs`

**Steps:**
1. Ler fonte e alvo; mapear campos do registro de export (id, filtros, status pending|processing|ready|failed, file_url, requested_by).
2. Teste falhando: `create_export/2` persiste registro `pending` e enfileira worker; `get_status/1` devolve o registro real; worker gera CSV (mesmo formatter já usado no detalhe de tarifas) e marca `ready` com caminho.
3. S3: usar o bucket/prefixo que o core já usa para relatórios (grep `ExAws.S3` em `core/backend/lib/monetarie` e reutilizar; se o core não tiver S3 configurado, gravar em disco EFS/tmp com download via endpoint autenticado e registrar nota; NÃO inventar bucket).
4. Substituir o stub do controller pelas chamadas reais; remover o UUID fake.
5. `mix test test/monetarie/use_cases/fees/export_test.exs` verde; `mix compile --warnings-as-errors`.
6. Commit: `feat(core): export de tarifas real (Oban + arquivo) no lugar do stub ready falso`.

**Aceite:** POST de export cria linha real; GET status reflete o worker; zero `status: "ready"` hardcoded no controller (grep no diff).

### Task A2 [P1]: Particionar `mon_core.transactions` (shadow + cutover gated) + PartitionMaintenance

**Fonte:**
- `/Users/luizpenha/coreproviders/backend/priv/repo/migrations/20260621000000_partition_audit_logs_shadow.exs` (padrão shadow: cria `_new` particionada, PK composta, cutover é eval separado)
- `/Users/luizpenha/coreproviders/backend/priv/repo/migrations/20260301000003_create_transactions.exs:12-93` (DDL particionada de referência: RANGE `started_at`, PK `(id, started_at)`, índices por partição)
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/workers/partition_maintenance.ex` (worker: 3 meses à frente, `partitioned?/1` via relkind, `ensure_partition_indexes/2`, TTL cleanup)

**Alvo:**
- Ler primeiro: DDL real da nossa `transactions` (`psql`-less: extrair de `core/backend/priv/repo/migrations/20260101000000_create_schema.exs` + migrations posteriores que alteram transactions, grep `:transactions`); coluna de data de referência nossa é a que o código consulta (verificar `started_at` vs `inserted_at` em `lib/monetarie` antes de escolher a chave de partição; NÃO assumir).
- Create: migration `create_transactions_partitioned_shadow` (cria `transactions_new` particionada espelhando TODAS as colunas/índices reais + partições 2026-01..2028-12 + `_default`), release task `Monetarie.Release.cutover_transactions_partition/0` (copia dados, troca nomes em transação, valida contagem), worker `core/backend/lib/monetarie/workers/partition_maintenance.ex`
- Test: `core/backend/test/monetarie/workers/partition_maintenance_test.exs`

**Steps:**
1. Mapear DDL vivo da transactions (todas as migrations que a tocam) e listar índices/constraints; anexar o mapeamento como comentário da migration.
2. Migration shadow (não destrutiva, não faz cutover). PK composta `(id, <col_data>)`; uniques ganham a coluna de data como no cp (`:73-93`).
3. Worker PartitionMaintenance port: tabelas nossas particionadas (`transactions_new` pós-cutover, `account_entries`, `cosif_journal_entries`, `message_timeline`, `fee_transactions`, `fee_split_transactions`), 3 meses à frente, cron diário 01:00 em `config/runtime.exs` (bloco Oban existente, `runtime.exs:425`).
4. Release task de cutover COM GATE: só roda por eval manual; loga contagens antes/depois; **não executar em HML nesta task** (o cutover em HML é passo operacional separado com o dono, pois a tabela tem dados do ETL).
5. Testes: worker cria partição futura idempotente em tabela de teste particionada; `partitioned?/1` false-safe.
6. Commit: `feat(core): particionamento shadow de transactions + worker de manutencao de particoes`.

**Aceite:** migration aplica limpa em dev; cutover NÃO executado automaticamente; worker agendado.

### Task A3 [P2]: BalanceCheckpoint + prefold noturno (flag OFF)

**Fonte:**
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/use_cases/payments/pix_out/balance_checkpoint.ex` (tabela `account_balance_checkpoints`, `net_available/3` = checkpoint + delta, `advance/3`, boundary dia BRT em UTC `:277-281`)
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/workers/balance_checkpoint_prefold.ex` (cron 03:10 UTC, gate `balance_checkpoint_enabled`)

**Alvo:**
- Ler primeiro: nosso `pg_net_available` (grep `pg_net_available` em `core/backend/lib/monetarie`; a matriz aponta soma de summaries em `balance_guard.ex`) e a(s) tabela(s) que ele soma.
- Create: `core/backend/lib/monetarie/use_cases/payments/balance_checkpoint.ex`, migration `create_account_balance_checkpoints`, `core/backend/lib/monetarie/workers/balance_checkpoint_prefold.ex`
- Modify: ponto único onde `pg_net_available` calcula, com branch por flag `BALANCE_CHECKPOINT_ENABLED` (default false)
- Test: `core/backend/test/monetarie/use_cases/payments/balance_checkpoint_test.exs`

**Steps:** teste do fold incremental (2 dias de lançamentos, checkpoint avança e `net_available` = checkpoint + delta do dia, idempotente); adaptar nomes de colunas às NOSSAS tabelas de summary (não copiar nomes cp às cegas); prefold cron 03:10 UTC; flag OFF. Commit: `feat(core): BalanceCheckpoint com prefold noturno atras de flag (default off)`.

**Aceite:** com flag off, comportamento byte-idêntico ao atual (teste de paridade das duas vias no mesmo dataset).

### Task A4 [P2]: Caches opt-in UsageCache + TxStatusCache + BalanceCache (flags OFF)

**Fonte:**
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/use_cases/limits/usage_cache.ex` (TTL 15min, chave com token de período BRT `:106-109`, lazy, sem write-through)
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/use_cases/external/pix/tx_status_cache.ex` (TTL por shape `:65-68`, invalidação em insert/update + PubSub)
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/use_cases/payments/pix_out/balance_cache.ex` (TTL 10s, só display, PROIBIDO em verify, invalidação PubSub `:106`)

**Alvo:**
- Create: 3 módulos em `core/backend/lib/monetarie/use_cases/{limits/usage_cache.ex,external/tx_status_cache.ex,payments/balance_cache.ex}` + supervisão em `application.ex`
- Modify: `pix_in/limit_check.ex` (hoje SUM no PG a cada verificação, matriz dominio 1) lê via UsageCache sob flag; endpoint de status de transação da Partner API lê via TxStatusCache sob flag; endpoint de saldo display sob flag
- Test: 1 arquivo por cache (hit/miss/TTL/invalidação/flag off = passthrough)

**Regra dura (copiar do cp):** BalanceCache NUNCA entra em `BalanceGuard.verify`/caminho de débito; comentário-guarda no módulo. Commit: `feat(core): caches opt-in de uso de limites, status de tx e saldo display (flags off)`.

### Task A5 [P1]: Breakdown de saldo bloqueado cruzando TB + reconciliador de holds MED órfãos

**Fonte:**
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/use_cases/admin/accounts/blocked_breakdown.ex:12-46` (breakdown = TB pending por categoria: MED + in-transit + órfãos + judicial)
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/use_cases/pix_compliance/med/orphan_hold_reconciler.ex` (void por `user_data_128` real do TB)

**Alvo:**
- Modify: `core/backend/lib/monetarie_web/controllers/admin/coreproviders_parity_controller.ex:577-610` (hoje só MED por categoria, não cruza TB)
- Create: `core/backend/lib/monetarie/use_cases/accounts/blocked_breakdown.ex`, `core/backend/lib/monetarie/workers/med/orphan_hold_reconciler.ex` (cron, dry-run por flag primeiro: loga o que voidaria sem voidar)
- Test: breakdown com holds sintéticos; reconciler em modo dry-run.

**Regra dura:** reconciler NASCE em dry-run (`MED_ORPHAN_RECONCILER_MODE=log`), void real só após validação com o dono (mexe em TB). Commit: `feat(core): breakdown de bloqueado cruzando TB + reconciliador de holds MED orfaos em dry-run`.

---

## Onda B: Cabine PIX (tasks B1-B3, paralelizáveis entre si; B independe de A)

### Task B1 [P1]: Logs PERF por fase no padrão AVIV (auditabilidade de produção)

**Fonte (padrão de linhas, ver relatório seção 5):**
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/services/pix_providers/onz/poller.ex:919-1012` (MSG_RECV `end2end_ms`, DISPATCH_START `since_msg_recv_ms`, MSG_OK `duration_ms handler_ms`), `:1759-1838` (end2end prioriza `FctvIntrBkSttlmDt.DtTm` sobre `CreDtTm`)

**Alvo:**
- Modify: `pix/backend/apps/spi_service/lib/spi_service/workers/inbound_processor.ex`, `outbound_sender.ex`, `status_updater.ex` (pontos de entrada/dispatch/conclusão de cada mensagem)
- Test: asserts de formato de log via `ExUnit.CaptureLog` em 1 fluxo inbound e 1 outbound

**Steps:** definir o vocabulário nosso: `[ICOM] MSG_RECV rid=<x> e2e=<e> end2end_ms=<n>` (CreDtTm/FctvIntrBkSttlmDt -> received_at), `[ICOM] MSG_OK ... duration_ms=<n> handler_ms=<n>`, `[NATS] PUB_OK subject=... duration_ms=`; 1 linha por mensagem, chave=valor, sem PII (PiiMask onde houver nome/doc). Commit: `feat(pix): logs PERF por fase (end2end_ms, handler_ms) no padrao auditavel`.

**Aceite:** com 1 PIX simulado, `grep MSG_OK` devolve linha parseável; Logs Insights conseguirá agregar percentis como fizemos na AVIV.

### Task B2 [P1]: Malha de watchdogs do canal (adaptada OnZ -> ICOM/NATS)

**Fonte (conceitos; adaptar, não copiar transporte):**
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/workers/{onz_lag_monitor.ex,onz_poller_liveness_monitor.ex,onz_dlq_depth_monitor.ex,onz_lp_inbox_depth_monitor.ex,pix_in_okrate_monitor.ex}` + poison policy `poller.ex:1087-1216`
- `lag_window.ex` (janela rolling ETS 180s de end2end_ms)

**Alvo:**
- Create em `pix/backend/apps/settlement_service/lib/settlement_service/monitoring/`: `icom_lag_monitor.ex` (janela rolling de end2end_ms alimentada pelos logs/telemetry da B1; alerta se p95 estourar teto), `consumer_liveness_monitor.ex` (NATS consumers com pending crescendo e sem deliver = travado), `dlq_depth_monitor.ex` (stream `MONETARIE_DLQ` depth por subject), `pix_in_okrate_monitor.ex` (razão liquidado/iniciado na janela; queda = alerta)
- Poison policy: no consumer NATS dos workers SPI (onde há NAK/redelivery hoje), contador Redis por msg_id; após N (default 5) falhas consecutivas, publica em `monetarie.dlq.poison.<subject>` + ack + log CRÍTICO (bounded, não drop silencioso). Ler antes o consumer real em `spi_service/workers/` para acoplar no ponto de NAK existente.
- Test: cada monitor com dados sintéticos; poison policy com handler que sempre falha.

**Aceite:** 4 monitores agendados (cron `*/1` a `*/15` como no cp `runtime.exs:559-567`), poison policy com teste provando que a 6a falha vai a DLQ e o consumer avança. Commit: `feat(pix): watchdogs de canal (lag, liveness, dlq, ok-rate) + poison policy no consumer NATS`.

### Task B3 [P1]: PromEx nos dois backends (`/metrics`)

**Fonte:**
- `/Users/luizpenha/coreproviders/backend/lib/fluxiq/prom_ex.ex` + `lib/fluxiq/plugins/prom_ex.ex` + config `runtime.exs:958-966`

**Alvo:**
- Create: `core/backend/lib/monetarie/prom_ex.ex` + plug de métricas custom (money-path: contagem/latência de pix in/out materializados, webhooks, oban); `pix/backend/apps/shared/lib/shared/prom_ex.ex` (ou settlement_service) com métricas de mensagens ICOM por tipo/status e lag NATS
- Modify: `mix.exs` dos dois (dep `prom_ex`), endpoints (rota `/metrics` SÓ interna, sem auth pública mas atrás do ALB interno; conferir como o cp expõe e replicar a proteção), supervisão
- Test: smoke `GET /metrics` responde texto Prometheus com pelo menos 1 métrica custom.

Commit: `feat(core,pix): PromEx com metricas de money-path e canal ICOM`.

---

## Onda C: Deploy/UX (depois de A e B compilarem verde)

### Task C1 [P2]: Graceful drain (zero-5xx) no core-api e pix-api

**Fonte:** `Fluxiq.Infra.Drainer` + config `runtime.exs:400-418` (lameduck 5s segurando o listener no deregister do ALB, shutdown drain 90s sobre o default 15s do ThousandIsland). Localizar o módulo: `grep -rn "Drainer" /Users/luizpenha/coreproviders/backend/lib`.

**Alvo:** Create `core/backend/lib/monetarie/infra/drainer.ex` e equivalente na cabine (apps usam Bandit, mesmo do cp); envs `DRAIN_LAMEDUCK_MS=5000`, `DRAIN_SHUTDOWN_TIMEOUT_MS=90000`; registrar no ciclo de shutdown das Applications. Test: unit do drainer (recebe sinal, segura, libera). Commit: `feat(core,pix): graceful drain nos deploys (lameduck + drain 90s)`.

### Task C2 [P2]: Fee preview no IB (backend + Vue)

**Fonte:**
- Backend: `/Users/luizpenha/coreproviders/backend/lib/fluxiq_web/controllers/merchant/transfers/pix_fee_preview_controller.ex` + rota no banking_router (MELHORIAS.md item 1)
- Front: `/Users/luizpenha/coreproviders/frontends/banking/src/views/pix/PixSendView.vue:171-186` (debounce 350ms), `:542-562` (breakdown e bloqueio valor+tarifa>saldo)

**Alvo:**
- Create: `core/backend/lib/monetarie_web/controllers/v2/pix_fee_preview_controller.ex` (usa NOSSO `FeeCalculator`, que já tem vedações BACEN; resposta em CENTAVOS `{amount, fee, total}`), rota no router v2 do IB
- Modify: view de envio PIX do IB (`core/apps/banking/src/views/` localizar a PixSend nossa via grep) com debounce + breakdown + bloqueio
- Test: controller test com tarifa configurada e com isenção por pacote/franquia (casos do nosso FeeCalculator).

**Regra dura:** resposta e exibição em CENTAVOS (regra 0.1); o front do cp trabalha com subcentavos (`feeSub` `:98,:160`), NÃO copiar essas variáveis sem converter. Commit: `feat(core): fee preview no envio PIX do IB com breakdown e bloqueio por saldo`.

### Task C3 [P2, se sobrar dia]: Suíte de benchmark com gate de regressão

**Fonte:** `/Users/luizpenha/coreproviders/benchmark/` (harness telemetry->histograma ETS, cenários Direct/Contention, gate ±10% do bench-014).
**Alvo:** `pix/backend/benchmark/` adaptado ao nosso pipeline (NATS in-loco, simulador BACEN `SIMULATOR_ENABLED=true`); primeiro cenário: PIX IN direto 50 wallets x 1k, report em `benchmark/docs/bench-result-001.md` (nosso baseline). Commit: `feat(pix): suite de benchmark com baseline e gate de regressao`.

---

## Verificação final do dia (obrigatória antes de declarar entregue)

1. `mix test` verde nos dois backends; `mix compile --warnings-as-errors`.
2. Grep de regressão de unidade nos diffs (regra 0.1).
3. Subir localmente o que for de UI e validar por screenshot (regra 11 do CLAUDE.md).
4. Deploy HML apenas do que estiver verde, pipeline padrão (buildx arm64 -> ECR -> task-def nova -> rollout COMPLETED), com validação viva por item.
5. Atualizar `CLAUDE.md` (estado canônico) + handoff `docs/handoff/2026-07-04-port-aviv-best-of-breed-handoff.md` com o que entrou, flags e pendências.
