# Core PIX Materialization + Reconciliation Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (or superpowers:subagent-driven-development) to implement this task-by-task. Use superpowers:test-driven-development for every task (failing test first, see it fail, minimal impl, see it pass, commit).

**Goal:** Make the Monetarie Core the single source of truth for PIX status: materialize the cabine's final status (via the existing NATS events) into `transactions`, so the receipt/extrato/in-transit read ONLY the Core (no per-request cabine call); cover gaps with an Oban reconciliation worker + a pointwise reprocess. Calibrated to the production `coreproviders` (AvivPay/OnZ) patterns.

**Architecture:** Enrich `PixHandler` (the existing NATS consumer handler) to materialize the full settled/rejected payload into `transactions.metadata` with idempotency + an anti-regression status guard + poison/DLQ. Switch the receipt/adapter to read Core-only (key-type detected at read; ISPB->bank-name via the participants directory). Add an Oban reconciliation cron (mirror `PixInOrphanReconciliation`) + an admin reprocess endpoint. Add partition-aware covering indexes.

**Tech Stack:** Elixir/Phoenix (Monetarie.* core/backend), Ecto/Postgres (transactions partitioned monthly), NATS JetStream (PixConsumer durable), Oban, ExUnit.

**Workspace:** worktree `~/.config/superpowers/worktrees/monetarie/core-pix-materialization`, branch `feat/core-pix-materialization`. Backend = `core/backend`. Design: `docs/plans/2026-06-29-core-pix-materialization-reconciliation-design.md`.

**Guard-rails:** Do NOT touch the cabine (it already emits). Never downgrade a final status. No per-request cabine call (only the batch reconciliation + pointwise reprocess). Amount stays in base units. Materialization idempotent + fail-soft (never break the NATS consumer). pt-br, no AI em-dash. KEEP commit trailers:
```
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPy7XwjcPaKUNTAUGGb7UW
```

**Pre-flight (once):** `cd core/backend && mix deps.get && MIX_ENV=test mix compile`. Confirm the worktree test DB is reachable (mirror how core tests run; if blocked, write tests anyway and prove via compile + deploy).

---

## Phase A: Materialize on the NATS event

### Task 1: PIX key-type detector (detect-at-read)
**Files:** Read first: `coreproviders/backend/lib/fluxiq/use_cases/pix/key_type.ex` (the algorithm). Search Core for an existing detector: `grep -rn "key_type\|KeyType\|detect" core/backend/lib/monetarie | grep -i pix`. Create only if absent: `core/backend/lib/monetarie/use_cases/pix/key_type.ex`. Test: `core/backend/test/monetarie/use_cases/pix/key_type_test.exs`.
- Step 1 (TDD): failing test asserting `detect/1`: "12345678901"(valid CPF)->"cpf"; 14 digits->"cnpj"; "x@y.com"->"email"; "+5511999999999"->"phone"; UUID->"evp".
- Step 2 run -> fail. Step 3 implement mirroring coreproviders key_type.ex. Step 4 pass. Step 5 commit `feat(core-pix): pix key-type detector (detect-at-read)`.
- If Core ALREADY has an equivalent, skip creation and note it; later tasks use the existing one.

### Task 2: PixHandler materializes SETTLED into transactions.metadata
**Files:** Read first: `core/backend/lib/monetarie/infra/nats/handlers/pix_handler.ex` (handle_transaction/2 ~44-117, update_status/3 ~148-176) to mirror its exact pattern + Repo access; `core/backend/lib/monetarie/schemas/relational/transaction.ex` (metadata field + changeset). Modify: `pix_handler.ex`. Test: `core/backend/test/monetarie/infra/nats/handlers/pix_handler_materialization_test.exs`.
- The cabine `settled` event payload: `%{"end_to_end_id","status_id"=>4,"settled_at","debtor_ispb","creditor_ispb","amount","status"=>"settled",...}` (see cabine status_updater).
- Step 1 (TDD): seed a `transactions` row (type "pix", direction outbound, payment_status "processing", end_to_end_id "E..."); feed a settled event to the handler; assert the row becomes payment_status="settled", completed_at set, and metadata merged with `spi_status_id: 4`, `spi_settled_at`, `settlement_source: "spi"`, `debtor_ispb`, `creditor_ispb`, `spi_materialized_at`. Preserve existing metadata keys (recipient_name from initiation).
- Step 2 fail. Step 3: extend the settled branch of handle_transaction to merge the full metadata (use `Map.merge(existing_metadata, materialized)`), keeping update_status behavior. Step 4 pass. Step 5 commit `feat(core-pix): materialize settled status into transactions.metadata`.

### Task 3: PixHandler materializes REJECTED (with reason)
**Files:** Modify `pix_handler.ex` (rejected branch ~73). Test: same/sibling test file.
- The `rejected` payload: `%{"end_to_end_id","status_id"=>8,"reason_code","reason_description","error_reason","rejected_at",...}`.
- Step 1 (TDD): seed processing row; feed rejected event; assert payment_status="rejected", completed_at set, metadata has `spi_status_id: 8`, `reason_code`, `reason_description`, `error_reason`, `spi_materialized_at`. Existing fund-release behavior preserved.
- Steps 2-4 TDD. Step 5 commit `feat(core-pix): materialize rejected status + reason into metadata`.

### Task 4: Idempotency + anti-regression guard + poison/DLQ
**Files:** Read first: `coreproviders/backend/lib/fluxiq/use_cases/payments/atomic_payment_handler.ex:63-75` (stage guard) and `coreproviders/.../onz/poller.ex:1088-1199` (poison policy) for the pattern. Also read `core/backend/lib/monetarie/infra/nats/consumers/pix_consumer.ex` (how it acks/handles errors). Modify `pix_handler.ex` + possibly `pix_consumer.ex`. Test: sibling test file.
- Step 1 (TDD): (a) anti-regression: a settled row receiving a later "processing"/duplicate event stays settled (no downgrade); (b) idempotent: same settled event twice = single effect; (c) poison: a handler error N times -> publish to `monetarie.dlq.pix` + ack (no infinite wedge). Use Nats-Msg-Id for dedup if available.
- Steps 2-4 TDD. Step 5 commit `feat(core-pix): idempotent materialization + anti-regression guard + poison/DLQ`.

---

## Phase B: Read Core-only

### Task 5: Receipt reads Core-only (remove sync cabine call)
**Files:** Read first: `coreproviders/backend/lib/fluxiq/use_cases/receipts/builder.ex` (the Core-only build pattern) + the Core's `monetarie_web/controllers/v2/transaction_controller.ex` (receipt ~91, maybe_enrich_with_pix_cabin ~286-298, line 289) + `monetarie/services/pix_providers/in_house/adapter.ex:49-70` + the participants/Institutions cache for ISPB->name (`grep -rn "by_ispb\|bacen_pix_participants\|Institutions" core/backend/lib/monetarie`). Modify the controller + adapter. Test: `core/backend/test/monetarie_web/controllers/v2/transaction_receipt_core_only_test.exs`.
- Step 1 (TDD): seed a settled+materialized tx (metadata complete); call the receipt; assert the response has counterparty_name, tipo_chave (detect-at-read via Task 1), recipient_ispb->bank name, amount, status, e2e, completed_at; AND assert the cabine lookup is NOT called (stub `CabinStatusLookup` and assert zero invocations, or assert via a telemetry/log). For a processing tx (no materialized status), assert it shows "processing" WITHOUT calling the cabine.
- Step 2 fail (today it calls the cabine). Step 3: remove `maybe_enrich_with_pix_cabin` sync call at line 289 (and the adapter.ex:50 cabine call); build from Core transaction + metadata, mirroring coreproviders builder. Step 4 pass. Step 5 commit `feat(core-pix): receipt reads Core-only (drop per-request cabine call)`.

### Task 6: in-transit / extrato read Core-only
**Files:** Read first: the in-transit/extrato query path (from adapter.ex list ~79 + the controller). Verify it reads Core only; if it calls the cabine per item, remove it. Test: list returns from Core, no cabine call.
- TDD: a list with N processing + settled txs returns from Core with zero cabine calls. Step 5 commit `feat(core-pix): in-transit/extrato read Core-only`.

---

## Phase C: Reconciliation + reprocess

### Task 7: Reconciliation Oban worker (stale + returns + legacy)
**Files:** Read first: `core/backend/lib/monetarie/workers/pix_in_orphan_reconciliation.ex` (the Oban cron pattern + cutoff + telemetry) + `cabin_status_lookup.ex` (batch query by e2e). Create: `core/backend/lib/monetarie/workers/pix_status_reconciliation.ex`. Config: add Oban cron entry (mirror PixInOrphanReconciliation in config/runtime.exs). Test: `core/backend/test/monetarie/workers/pix_status_reconciliation_test.exs`.
- Step 1 (TDD): seed a tx stuck `processing` older than the threshold; stub `CabinStatusLookup` to return settled; run the worker; assert the tx is materialized to settled. Second: returned (status 10) -> "refunded". Idempotent (re-run = no-op). Rate-limit/checkpoint: process in bounded batches.
- Steps 2-4 TDD. Step 5 commit `feat(core-pix): Oban reconciliation worker (stale/returns/legacy)`.

### Task 8: Admin pointwise reprocess endpoint
**Files:** Read first: an existing admin controller in Core for the pattern. Create a controller action `POST /api/admin/pix/transactions/:e2e/reprocess` that calls `CabinStatusLookup.by_end_to_end_id` + materializes the one tx. Test: controller test (auth + materializes one e2e).
- TDD. Step 5 commit `feat(core-pix): admin pointwise PIX reprocess by e2e`.

---

## Phase D: Performance

### Task 9: Partition-aware covering indexes on transactions
**Files:** Read first: `coreproviders/backend/priv/repo/migrations/20260614100100_*` (the covering index pattern) + the Core transactions migration. Create: `core/backend/priv/repo/migrations/<ts>_add_pix_transaction_read_indexes.exs`.
- Indexes (adapt to Core's actual columns/partitioning): `(account_id, started_at DESC) WHERE payment_status IN (...)`, `(account_id, direction, started_at DESC)` INCLUDE (amount, type), unique `(end_to_end_id, started_at) WHERE end_to_end_id IS NOT NULL` if not present. Use `create index(..., concurrently: false)` appropriate to partitioned tables.
- Step: author migration; `mix ecto.migrate` (or defer to deploy if DB blocked). Commit `feat(core-pix): read-path indexes for transactions`.

---

## Phase E: Gate + deploy + verify

### Task 10: Backend gate
`cd core/backend && mix compile --force` + run all new focused ExUnit (key_type, pix_handler materialization, receipt core-only, reconciliation). Record any DB-blocked tests. Commit fixes only.

### Task 11: Deploy core-api
Build+push+deploy via `scripts/deploy_hml_arm64.sh homolog-<sha>-20260629 core-api`; run the index migration as a one-off (`bin/<core_release> eval "<Core>.Release.migrate()"` via ECS Exec - read how core migrates); wait rollout COMPLETED; confirm by digest + target group healthy (AWS_PROFILE=vulcimonetarie).

### Task 12: Homolog verify
- Confirm a settled PIX out: its receipt now reads Core-only and shows the materialized status/counterparty (ECS Exec rpc or via the IB). Confirm zero per-request cabine call (logs/telemetry). Run the reconciliation worker once and confirm it materializes a stale tx.

## Definition of done
Materialization on event (settled/rejected) idempotent + guarded; receipt/extrato/in-transit read Core-only (no per-request cabine call, proven by test); reconciliation worker + reprocess endpoint cover gaps; indexes applied; core-api deployed (rollout COMPLETED, digest, TG healthy); homolog spot-check green. Update memories (`monetarie-pix-ib-core-receipt` / a new `monetarie-core-pix-materialization` + MEMORY.md + `mem:monetarie/work-ahead`).

## Execution handoff
Use superpowers:subagent-driven-development (this session) or superpowers:executing-plans (separate session). Fresh subagent per task + review. TDD throughout.
