# Design: Core PIX async materialization + reconciliation - 2026-06-29

Frente: divida estrutural da onda 06-28 (PIX IB/Core/cabine). Objetivo: o Core
materializa o status final do PIX (vindo da cabine por NATS) na propria base, de
forma que o comprovante, o extrato e o in-transit leiam SO o Core, sem consultar a
cabine por request. Calibrado pelo sistema de producao `coreproviders` (AvivPay,
provider OnZ), que ja roda saudavel; aqui melhoramos com NATS (a cabine ja emite
eventos durаveis) em vez do long-polling/webhook deles.

Regra do dono: nao inferir; BACEN/cabine e a verdade; pt-br sem travessao de IA.

## 1. Principio de producao (coreproviders)

- O **Core e a fonte unica de verdade** do status. O status e PUSHED do provider
  uma vez e materializado; **nunca se consulta o provider por request** (sem "checa
  de novo"). Evidencia: `coreproviders/backend/lib/fluxiq/use_cases/payments/atomic_payment_handler.ex`
  (on_reply materializa) + `.../use_cases/receipts/builder.ex` (comprovante le so o Core)
  + `.../use_cases/merchant/transactions/queries.ex` (lista le so o Core, indexada).
- Lacuna de status (evento perdido/sem evento) e coberta por **recuperacao por lote**
  (o `queryable?=true` deles, query_status para stale), nao por pull-por-request.
- Idempotencia em camadas + guard de ordenacao por estagio + poison/DLQ.
- Tipo de chave PIX e **detectado na leitura** (`fluxiq/use_cases/pix/key_type.ex`),
  nunca armazenado. Nome do banco resolvido por ISPB via cache de instituicoes.
- Valor sempre em base units/subcentavos, consistente.

## 2. Estado atual do Core Monetarie (empirico)

- Tabela unica `transactions` (Monetarie.Schemas.Relational.Transaction): particionada
  por mes em `started_at`, PK binaria; campos `payment_status` (processing/accepted/
  settled/confirmed/completed/rejected/timeout/cancelled/refunded), `status`,
  `end_to_end_id`, `amount` (base units), `account_id`/`from_account_id`/`to_account_id`,
  `metadata` (jsonb), `completed_at`, `started_at`, `finished_at`, `direction`, `type`.
- Consumo NATS JA existe: `Monetarie.Infra.Nats.Consumers.PixConsumer` (subjects
  `monetarie.spi.transaction.*`, stream `MONETARIE_SPI`, consumer duravel
  `core-pix-consumer`) -> `Monetarie.Infra.Nats.Handlers.PixHandler.handle_transaction/2`
  (created/rejected/settled/accepted -> `update_status/3` em payment_status/status/completed_at).
- Caminhos SINCRONOS a remover: `monetarie_web/controllers/v2/transaction_controller.ex:289`
  (receipt -> `CabinStatusLookup.by_end_to_end_id`) e
  `monetarie/services/pix_providers/in_house/adapter.ex:50` (query_transaction).
  Modulo `.../in_house/cabin_status_lookup.ex` (mantido SO p/ a reconciliacao + reprocesso).
- Reconciliacao: padrao pronto `Monetarie.Workers.PixInOrphanReconciliation` (Oban cron 15min).
- Diretorio de participantes BACEN p/ ISPB->nome: ja existe (bacen_pix_participants / Institutions cache).
- A cabine emite (StatusUpdater, durаvel/outbox): `monetarie.spi.transaction.settled`
  (status 4, payload rico) e `.rejected` (status 8, com reason_code/reason_description).
  Retorno (pacs.004, status 10) e SO broadcast, sem evento NATS. Fonte autoritativa de
  reconciliacao = `monetarie_spi.messages.status_id` + API admin da cabine.

## 3. Componentes

### 3.1 Materializar no evento (PixHandler = nosso on_reply)
Estender `PixHandler.handle_transaction/2` (preservar o comportamento atual):
- `settled` (status_id 4): `payment_status="settled"`, `completed_at=settled_at`, merge em
  `metadata`: `spi_status_id: 4`, `spi_settled_at`, `settlement_source: "spi"`,
  `debtor_ispb`, `creditor_ispb`, `spi_status: "settled"`, `spi_materialized_at`.
- `rejected` (status_id 8): `payment_status="rejected"`, merge em `metadata`:
  `spi_status_id: 8`, `reason_code`, `reason_description`, `error_reason`,
  `spi_materialized_at`. (A liberacao de fundos do tracked outbound ja existe; mantida.)
- Nome/documento/ISPB da contraparte: vem da iniciacao (o IB resolve a chave via DICT e
  ja grava em metadata), unido ao dado do evento. Sem tocar na cabine.

### 3.2 Idempotencia + ordenacao + poison/DLQ (espelha producao)
- Idempotente por `end_to_end_id`; guard anti-regressao por status: nao rebaixar um
  estado final (settled/rejected/refunded) por evento fora de ordem (espelha o guard de
  estagio do coreproviders + o guard do StatusUpdater da cabine).
- Dedup por `Nats-Msg-Id` (o PixConsumer e duravel/replayable). 
- Poison/DLQ: apos N falhas no mesmo evento, publicar em `monetarie.dlq.*` + alerta +
  ACK (sem wedge), espelhando a poison policy do coreproviders (poller.ex record_poisoned).

### 3.3 Leitura Core-only (comprovante + extrato + in-transit)
- Comprovante (`transaction_controller.ex` receipt) e `adapter.ex` query_transaction:
  **remover a chamada sincrona a cabine**; construir a resposta so do Core (transaction +
  metadata), espelhando `coreproviders .../receipts/builder.ex`. Campos: amount (base units),
  status normalizado, end_to_end_id, auth/completed_at, `counterparty_name`, `recipient_key`,
  `tipo_chave` (DETECTADO NA LEITURA via um `Monetarie...Pix.KeyType.detect/1`), ISPB->nome
  do banco via diretorio de participantes. Pendente (sem status final) = "em processamento"
  honesto (nao chamar a cabine).
- Extrato/in-transit: ler so o Core (filtro por payment_status), sem chamada por item.

### 3.4 Reconciliacao (Oban) + reprocesso pontual
- Oban cron (molde `PixInOrphanReconciliation`): seleciona transacoes presas em
  `processing` alem de um limite (ex.: > N min) + retornos (pacs.004, sem evento) + ACSP
  legados; consulta a cabine por E2E em LOTE (API admin, rate-limit + checkpoint) e
  materializa/transiciona (mesma logica do 3.1). One-off para o backlog legado.
- Reprocesso pontual: endpoint admin que materializa um E2E especifico via a cabine
  (o "lookup pontual" autorizado), reusando `cabin_status_lookup`.

### 3.5 Performance (indices, sem cache)
- Indices particao-aware na `transactions` (mirror coreproviders): `(account_id, started_at DESC)
  WHERE payment_status IN (settled/...)`, `(account_id, direction, started_at DESC)` INCLUDE
  (amount, fee, type), unico por `(end_to_end_id, started_at)`. Sem camada de cache (indices bastam).

## 4. Mapeamento de status (cabine -> Core)
- 4 ACSC / settled -> payment_status "settled"
- 8 RJCT / rejected -> "rejected"
- 10 RTRN / returned -> "refunded" (via reconciliacao; sem evento)
- 1/2/3 PDNG/ACSP/ACCC -> "processing" (intermediario; nao rebaixa final)

## 5. Testes
- PixHandler: settled materializa metadata completa; rejected materializa reason; evento
  fora de ordem NAO rebaixa final; idempotente por e2e; poison -> DLQ.
- Receipt builder: monta so do Core (sem chamar a cabine; mockar e provar ZERO chamada),
  tipo de chave detectado, ISPB->nome, pendente = processing.
- Reconciliacao: transacao processing antiga -> consulta cabine (stub) -> materializa;
  retorno -> refunded; idempotente.
- Indices: migracao aplica; query da lista usa o indice (explain opcional).

## 6. Criterios de sucesso
1. `mix compile --force` + ExUnit focado verdes (PixHandler, receipt, reconciliacao).
2. Comprovante/extrato NAO chamam a cabine por request (provado por teste + grep).
3. Deploy core-api (rollout COMPLETED, digest), migracao de indices aplicada.
4. (homolog) um PIX out settled aparece materializado no comprovante lido SO do Core.

## 7. Fora de escopo (YAGNI)
- Nao reescrever o modelo de 2 tabelas (outbound_requests/transactions) do coreproviders;
  a Monetarie usa tabela unica com payment_status e isso fica.
- Cache (indices bastam, como no prod). Materializacao de inbound PIX-IN (ja tem fluxo).
- SGCT/PIX Automatico (diferido).

## 8. Referencias
- coreproviders: `atomic_payment_handler.ex`, `use_cases/payments/outbound_payment/pix.ex`
  (on_reply), `receipts/builder.ex`, `merchant/transactions/queries.ex`, `pix/key_type.ex`,
  `onz/poller.ex` (poison/dedup), migration `20260614100100_*` (indices cobrindo).
- Monetarie Core: `infra/nats/handlers/pix_handler.ex`, `infra/nats/consumers/pix_consumer.ex`,
  `controllers/v2/transaction_controller.ex:289`, `services/pix_providers/in_house/{adapter,cabin_status_lookup}.ex`,
  `schemas/relational/transaction.ex`, `workers/pix_in_orphan_reconciliation.ex`.
- Cabine: `spi_service/workers/status_updater.ex` (emite settled/rejected).

## 9. Guard-rails
Nao tocar na cabine (ela ja emite). Nao rebaixar status final. Sem chamada a cabine por
request (so reconciliacao em lote + reprocesso pontual). Valor em base units consistente.
Materializacao idempotente + fail-soft (nunca quebrar o consumo NATS).
