# Design: contabilização instantânea do PIX no Core — migração ao padrão SPB

**Data:** 2026-07-13. **Status:** proposta para revisão (nenhum código escrito). **Base:** estudo `docs/reports/2026-07-10-estudo-contabilizacao-cabines-core.md`, balancete assinado `docs/reports/2026-07-10-balancete-oficial-vs-plano-vivo.md`, e levantamento de código (workflow `wf_a9bf8615-c2e`).

## 1. Objetivo e princípio

Toda movimentação da cabine PIX (entrada, saída, devolução, tarifa) deve produzir, **no mesmo instante e atomicamente** com o crédito/débito do cliente, o lançamento COSIF correto no razão do Core, usando as **contas segregadas** que o contador mantém, e isso deve ser **comprovável por reconciliação**. Meta: o razão nunca diverge do TigerBeetle e reflete a estrutura contábil real da SCD (não um mapeamento raso).

Hoje o dinheiro anda (TB + saldo do cliente), mas a contrapartida contábil é rasa e não-atômica. Este design fecha essa lacuna sem depender do contador para começar, deixando a parte que depende dele (as contas) como um seed encaixável.

## 2. Estado atual (após os fixes de 2026-07-10, commits `ada22a4c` + `2ba83756`)

Os 3 defeitos que faziam a contabilização **errada** já foram corrigidos: direção (`AccountResolver.inbound_direction?/1`), unidade (`MoneyUnit.to_cents`), e ordem/atomicidade parcial (TB-primeiro + `replay_pg_leg` do recovery recria o journal). Isso deixou o mapeamento **correto**, mas ainda **raso e não-atômico**. Gaps remanescentes:

| Gap | Detalhe | Evidência |
|---|---|---|
| **Não-atômico** | PIX-in escreve journal + transaction + extrato em 3 writes PG SEPARADOS, não um `Ecto.Multi`. Se um falha, os outros ficam. | `pix_handler.ex` `do_handle_transaction_ledger_instrumented` (journal em `journal_after_tb_credit`, transaction em `maybe_create_inbound_transaction`, extrato em `StatementEntries.record_inbound_settled` — três passos independentes) |
| **Sem desfecho auditado** | Não há `resolution["cosif_je"]` como no SPB; um JE que falha some no log. | vs `inbound_credits.ex:407-445` |
| **Contas rasas** | PIX-in e PIX-out batem ambos em `1.1.2.10.01.10.001` (liquidante) ⇄ `4.9.8.10.01.10.002` (cliente). Sem Conta PI / trânsito / compensação. | `mapper.ex` `@transfer_to_cosif` |
| **Mapeamento hardcoded** | O par D/C do PIX vem do `Cosif.Mapper` em CÓDIGO. Os templates `PIX_SEND`/`PIX_RECEIVE` existem em dados mas estão MORTOS (sem caller) e apontam contas **vedadas p/ SCD** (`2.1.1.10.00-9` = Depósitos; `1.1.1.10.00-6` = formato legado). | `030_posting_templates.exs:304-326` |
| **Sem reconciliação PIX** | O SPB tem `spb_cosif_mirror` (espelho de conciliação do roteiro da cabine). Não há reconciliação ativa journal × transactions × TB para o PIX. | `schemas/spb/cosif_mirror.ex` |

## 3. Estado-alvo: o padrão-ouro do SPB (`UseCases.Spb.InboundCredits`)

O crédito STR do SPB é o modelo a portar. Sequência (evidência `inbound_credits.ex:287-445`):

1. **TigerBeetle PRIMEIRO**, fora de transação PG, com id determinístico SHA-256 de 128 bits (`monetarie_core:spb_inbound_credit:<num_ctrl_str>`). Fail-CLOSED: TB falhou = zero escrita PG. Redelivery reproduz o id → `:exists` = no-op.
2. **Só após TB `{:ok}`**, um `Ecto.Multi` atômico com passos NOMEADOS em ordem:
   - `:account_entry` — insert do extrato (subcentavos)
   - `:transaction_record` — `transactions` (`SPBCR<num_ctrl_str>`)
   - `:cosif_je` — JE COSIF (CENTAVOS via `AccountingBridge` + `to_cents`)
   - `:mark` — UPDATE condicional `WHERE status = ^from_status`; 0 linhas = **rollback do Multi inteiro** (árbitro race-safe)
3. **JE COSIF fail-SOFT** deliberado: gap de mapeamento/persistência nunca segura o crédito; vira `{:ok, {:je_failed, reason}}` e é gravado em `resolution["cosif_je"]` ("created"/"skipped"/"failed: …") para repair do contador.
4. **Idempotência dupla**: TB determinístico + `Cosif.create_journal_entry` com `on_conflict: :nothing` no índice único parcial `(reference_type, reference_id, entry_date)` — desde que `reference_id` NÃO seja nulo (no PIX já cai até o E2E).

O PIX já tem os ingredientes (TB determinístico SHA-256, `to_cents`, `reference_id` até E2E). O que falta é **empacotar num Multi atômico com desfecho auditado**, e (idealmente) trocar o mapa hardcoded pelo motor de templates.

## 4. Camada A — código, sem depender do contador

### A0 (Fase 0) — Reconciliação PIX read-only (a PROVA, e o de menor risco)
Um reconciler (worker + relatório/endpoint) que, por dia/conta/E2E, compara: soma de `account_entries` (extrato) × `transactions` × `cosif_journal_entries` × saldo TB. Divergência = alerta. Entregável visível: uma seção de **reconciliação contábil do PIX** na Tesouraria (espelhando a ideia do `spb_cosif_mirror`, mas como reconciliação ATIVA). É o que responde diretamente à sua preocupação ("garantir que reflete na contabilização") e **expõe o estado atual sem tocar no código de escrita** — risco mínimo, evidência máxima. Roda primeiro para medir a saúde antes e depois das fases seguintes.

### A1 (Fase 1) — PIX-in num `Ecto.Multi` atômico
Reestruturar `do_handle_transaction_ledger` para, após o TB-primeiro (já é), envolver **journal + transaction + extrato** num único `Repo.transaction`/`Multi` com passos nomeados, espelhando `InboundCredits.persist_member_credit`. Desfecho COSIF fail-soft auditado (em `metadata`/`resolution` da transação ou numa coluna de desfecho). Integrar as marcas do WAL (`mark_pg_done`/`mark_completed`) ao fim do Multi. Idempotência preservada (TB determinístico + `reference_id`=E2E + `on_conflict :nothing`). `replay_pg_leg` do recovery passa a usar o MESMO Multi.

### A2 (Fase 2) — PIX-out com desfecho auditado
O `JournalEntryWorker` (Oban `:cosif`) roda async, enfileirado na transação de liquidação. **Recomendação: manter async** (não segurar a liquidação), MAS registrar o desfecho do JE (created/failed) e deixar a reconciliação (A0) pegar o que falhou — em vez do fail-silent atual após 5 tentativas.

### A3 (Fase 3) — Motor de templates data-driven (recomendado)
Trocar o `Cosif.Mapper` hardcoded pelo `Cosif.PostingEngine.post_event/5` no caminho PIX (mesmo mecanismo do SISBAJUD). Repontar `PIX_SEND`/`PIX_RECEIVE` (e criar `PIX_IN`/`PIX_OUT`/`PIX_RETURN`/`PIX_FEE`) para as contas SCD reais. Vantagem decisiva: **quando o contador validar as contas, é um seed, não código** — a Camada B encaixa sem re-escrita. Schema pronto: `cosif_posting_templates` (event_type, group, description, debit/credit_cosif_code, debit/credit_account_type, currency, requires_fx_conversion, is_active, metadata); `post_event` recebe amount em CENTAVOS e é fail-closed em conta inexistente (`resolve_account`).

## 5. Camada B — contas segregadas (gated no contador)

Do balancete assinado (Maio/2026), as folhas PIX-relevantes a criar (estrutura de linha do `scd_cosif_plan.exs`: `%{code, name, category, level, parent_code, bacen_account, description}`; conta nova sempre com `:id` explícito >= 50_300):

| Conceito (balancete) | Papel no fluxo PIX | Grupo COSIF sugerido |
|---|---|---|
| **CORNER SPI** (Conta PI) | Conta de trânsito do PIX no SPI (float na Conta PI no BACEN) | Reservas Livres `1.1.3` / `1.1.5` (a validar) |
| CONTA TRANSITÓRIA CCME, TRANSITÓRIA RECEBIMENTOS | Trânsito de liquidação | `1.1.x` / `1.8.x` |
| CONTA PAGTO PRE PAGA FECH STR, EMISSOR INSTR PAGTO PRE-PAGO, CONTA CORNER (3097x) | **Compensação** — espelho do volume transacionado ao centavo | `3.0.9x` / `9.0.9x` |
| TARIFAS PIX | Receita de tarifa | `7.1.7.x` |

**Desenho contábil correto do PIX-in** (a confirmar com o contador): credita a **Conta PI (trânsito)** → transfere para o **float do cliente `4.9.x`**, com o volume espelhado em **compensação `3097x`**. Isso é mais que as 2 pernas de hoje — precisa do desenho do contador. Quando validado: (1) adicionar as folhas ao `scd_cosif_plan`; (2) repontar os templates PIX; (3) **o código da Camada A não muda** (data-driven).

## 6. Plano de execução em fases (TDD, tudo local, push só com OK)

| Fase | Escopo | Depende de | Risco |
|---|---|---|---|
| 0 | Reconciliação PIX read-only + tela Tesouraria | — | Baixo (só leitura) |
| 1 | PIX-in em `Ecto.Multi` atômico + desfecho auditado | — | Médio (money path) |
| 2 | PIX-out desfecho auditado + reconciliação | Fase 1 | Médio |
| 3 | Motor de templates no PIX (Mapper→PostingEngine) | Fase 1 | Médio |
| 4 | Contas segregadas (seed + repontar templates) | **Contador** | Baixo (seed) |
| 5 | `validate_account` × stream JetStream | **HML/dono** | reservado (`docs/reports/2026-07-10-validate-account-stream-overlap-PENDENTE-HML.md`) |

Cada fase: TDD RED primeiro, validação viva com as cabines locais (PIX simulado → rastrear TB + extrato + journal + reconciliação), commit local.

## 7. Riscos e decisões

- **Money path em produção**: qualquer mudança no PIX-in toca o `core-api` de prod. A Fase 0 (reconciliação) é segura e deve rodar primeiro para medir. Fases 1-3 exigem deploy cuidadoso.
- **Fail-soft vs fail-closed**: manter **fail-soft** no COSIF (não segurar o crédito do cliente por gap contábil) — igual SPB. A reconciliação pega o que falhou. (Exceção: SISBAJUD é fail-closed por ser bloqueio judicial; PIX é crédito ao cliente.)
- **PIX-out async**: decisão de manter async (não segurar liquidação) com reconciliação, salvo o dono preferir atômico na liquidação.
- **Flag `two_phase_pix_in`**: manter OFF (PROIBIDO ligar — canônico do projeto).
- **Contador**: sem as contas, a Camada A já entrega **atomicidade + prova**; a fidelidade REGULATÓRIA (Conta PI, compensação) depende da Camada B.

## 8. Perguntas abertas (para o dono / contador)

1. Começar pela **Fase 0 (reconciliação read-only)**? É a de maior valor/menor risco e dá a base de evidência.
2. Confirmar com o **contador** o desenho contábil do PIX-in (Conta PI trânsito → float cliente `4.9.x` → compensação `3097x` espelho) — é o que define as contas segregadas.
3. **PIX-out**: async com reconciliação é aceitável, ou quer atômico na liquidação?
