# Handoff — 🔴 BUG CRÍTICO de unidade (100x) no caminho do dinheiro do SISBAJUD

**Data:** 2026-07-08
**Status:** CONFIRMADO (verificação adversarial: 2 agentes, incluindo o que tentou refutar) + raio de alcance mapeado (4 superfícies). **NÃO corrigido** — decisão do dono de tratar numa frente dedicada.
**Branch:** `feat/monetarie_improviments` (commits SISBAJUD desta sessão já pushados; este handoff é só documentação).

---

## 1. Resumo executivo

O caminho judicial do SISBAJUD (bloqueio/desbloqueio/transferência) **mistura duas unidades monetárias sem converter**:

- **CENTAVOS** — vindo do campo V do arquivo 5301 (`F.parse_v` retorna o inteiro cru, 2 decimais implícitos = centavos).
- **SUBCENTAVOS (base_units)** — a unidade do TigerBeetle (BRL × 10.000; 1 centavo = 100 base_units), usada por `Wallet.get_balance` e `Transaction.hold_funds`.

O conversor canônico `Monetarie.Util.MoneyUnit.from_cents/1` (`@scale=100`) **não é chamado em nenhum ponto** sob `use_cases/judicial/` ou `sisbajud/` (grep vazio). Onde as duas unidades se cruzam, há erro de **100x**.

**Impacto:** uma ordem judicial de bloqueio de R$50 congela **R$0,50** no TigerBeetle (100x menos) e ainda reporta **"executado" (código 01)** ao BCB; um desbloqueio parcial **não libera nada** (dinheiro fica preso); a guarda de valor da transferência (código 11) **nunca dispara**; e os valores no arquivo-resposta 5302 saem **100x inflados**. Risco legal sério: descumprimento de ordem judicial mascarado como cumprimento.

---

## 2. Fato-âncora (as unidades)

| Origem | Unidade | Evidência |
|---|---|---|
| `parse_v` (campo V do 5301) | **centavos** | `formatter.ex:109-112` (doc: "centavos") |
| `Wallet.get_balance` (saldo TB) | **subcentavos** | `wallet.ex:124-131` (credits-debits crus); confirmado em `merchants_controller.ex:1196-1198` (`div(_,100)` p/ virar centavos) |
| `Transaction.hold_funds` (congela no TB) | **subcentavos** | `transaction.ex:50-63` (amount cru → build_transfer) |
| `pad_v` (campo V da resposta 5302) | **centavos** | `formatter.ex:33-35` (inteiro emitido verbatim) |
| `post_cosif_entry` / PostingEngine | **centavos** | `posting_engine.ex:72` |
| UI `amountToReais` | **centavos** (`/100`) | `SisbajudView.vue:439` |

**Padrão canônico correto (a replicar):** `MoneyUnit.from_cents/1` na ENTRADA (centavos→base_units) e `MoneyUnit.to_cents/1` na SAÍDA (base_units→centavos). Já usado em:
- SPB crédito de entrada: `spb/inbound_credits.ex:285,308` (`deposit(..., MoneyUnit.from_cents(row.amount_cents))`).
- PIX/TED: `pix_controller.ex:282`, `transfer_controller.ex:265` (from_cents na entrada); `accounting_bridge.ex:159,257` (to_cents antes do COSIF).

---

## 3. O nó-raiz do bug

`do_block_account/3` — `processor.ex:1536-1544`:

```elixir
case Wallet.get_balance(account.id) do   # balance em SUBCENTAVOS
  {:ok, balance} when balance > 0 ->
    amount_to_block =
      cond do
        remaining == nil     -> balance    # subcentavos (bloqueia tudo) — correto por acaso
        remaining <= 0       -> 0
        balance >= remaining -> remaining  # CENTAVOS (requested_amount cru) — 100x MENOS
        true                 -> balance    # subcentavos (parcial)
      end
    ... Transaction.hold_funds(account.id, pool_id, amount_to_block)  # TB espera subcentavos
```

`remaining` = `order.requested_amount` (`processor.ex:1817`) = `record[:valor_bloqueio]` (centavos, `file_processor.ex:390`). Logo:
- `balance (subcentavos) >= remaining (centavos)` compara unidades diferentes (balance ~100x inflado) → o ramo "saldo insuficiente" quase nunca dispara.
- No ramo `balance >= remaining`, `amount_to_block = remaining` (centavos) → `hold_funds` congela 100x menos.
- **`blocked_amount` é gravado em unidade INCONSISTENTE** (`processor.ex:1554`): centavos no ramo `balance>=remaining`, subcentavos nos ramos `remaining==nil`/parcial. Isso contamina o storage, o COSIF, o wire e a UI.

---

## 4. Raio de alcance (4 superfícies mapeadas)

### 4.1 Propagação BLOCK / UNBLOCK / TRANSFER / NOTIFICATION / CANCEL
- **BLOCK** — bug confirmado (R$50 → R$0,50).
- **UNBLOCK** — entrada `file_processor.ex:615`; seleção total-vs-parcial `processor.ex:1668` e acumulação parcial `processor.ex:1676` comparam centavos com subcentavos → **desbloqueio parcial não libera nada** (dinheiro do réu preso).
- **TRANSFER** — entrada `file_processor.ex:705`; guarda `amount_exceeds_blocked?` `processor.ex:617,971-973` fica **fail-open** (código 11 nunca dispara; viola fail-CLOSED); o settle liquida todos os blocks via `collect_funds` sem respeitar o valor pedido.
- **NOTIFICATION (08)** — SEM mismatch (não carrega valor nesta camada; `requested_amount` nil, `blocked_amount` 0). `file_processor.ex:822`.
- **CANCEL** — seleciona/libera certo (usa `block.blocked_amount` subcentavos), mas emite valor **100x** no wire. `processor.ex:1111-1127`, `file_processor.ex:991`.
- **Nuance importante:** o `release_funds`/`collect_funds` REAL no TB está **correto** (usa `block.blocked_amount` já em subcentavos). O dinheiro efetivamente movido no desbloqueio/transferência acerta a unidade — o mismatch morde na SELEÇÃO, na GUARDA e na SAÍDA.

### 4.2 Valores emitidos ao BCB no 5302 (todos passam por `pad_v`, que assume centavos)
- `VALOR_RESPOSTA_TOTAL` (03) — `response_builder.ex:132` (fonte `file_processor.ex:501` + `processor.ex:1087-1095`).
- `VALOR_RESPOSTA_CONTA_SALARIO` (03) — `response_builder.ex:138` (fonte `file_processor.ex:503`).
- `VALOR_DESBLOQUEADO` + `SALDO_REMANESCENTE_TOTAL` (05) — `response_builder.ex:165,169` (fonte `file_processor.ex:639` + `processor.ex:1666-1683,1707`).
- `VALOR_A_TRANSFERIR` (07) — `response_builder.ex:205` (fonte `file_processor.ex:1043` + `processor.ex:662`).
- CANCEL — `file_processor.ex:991`.
- **Decisão do código 01/02/03** — `processor.ex:76-82` compara `total_blocked` (subcentavos) com `requested_amount` (centavos).

### 4.3 COSIF + storage + UI
- COSIF `SBJ_BLOCK` recebe `amount_to_block` cru — `processor.ex:1567` (def 2037-2076); `SBJ_UNBLOCK` recebe `block.blocked_amount` — `processor.ex:2085` (def 2080-2111). PostingEngine espera centavos (`posting_engine.ex:72`).
- Storage: `JudicialBlock.blocked_amount` (`processor.ex:1549,1554`), `total_blocked` (`processor.ex:1902,593`), `JudicialOrder.blocked_amount` (`processor.ex:101,675`) — unidades mistas/somadas incoerentes.
- Decremento do iterador `new_remaining = remaining - amount` (`processor.ex:1894`) mistura unidades.
- **UI (`SisbajudView.vue`):** "Saldo Disponível" `:777` está **100x errado sempre** (subcentavos exibidos como centavos; fonte `file_processor.ex:289`); "Valor Bloqueado" `:637` certo/errado por ramo; "Valor Solicitado" `:631` é o **único sempre certo** (centavos ÷100).

### 4.4 Preview de upload (enrich_record)
- `file_processor.ex:289-292` — `total_balance`/`has_balance` (subcentavos, de `Wallet.get_balance`) vs `valor_solicitado` (centavos, de `record[:valor_*]`) exibidos lado a lado, 100x fora de escala.

---

## 5. Cenário numérico (para o teste de regressão)

Cliente com **R$100** no TB (= 1.000.000 subcentavos). Ordem 5301:
- **Bloqueio de R$50** (`valor_bloqueio` = 5000 centavos): hoje congela **R$0,50** (5000 subcentavos) e reporta "executado". Correto: congelar 500.000 subcentavos.
- **Desbloqueio parcial de R$30** referenciando um bloqueio de R$100: hoje **libera R$0,00** (nenhum block satisfaz `sum + blocked(subcent) <= requested(cent)`). Correto: liberar R$30.
- **Transferência pedindo mais que o bloqueado**: hoje o código 11 não dispara (fail-open); transfere o bloqueado inteiro respondendo 01.

---

## 6. Por que os testes atuais não pegam (blind spot)

- Nenhum teste sob `judicial/` ou `regulatory/sisbajud/` toca TigerBeetle/`hold_funds`/saldo real — os fixtures usam uma **escala fictícia única e auto-consistente** (ex.: `transfer_integration_test.exs:104` `blocked_amount:100_00` e `:128` `requested_amount:200_00`; `unblock_selection_test.exs:12-52`).
- Os block tests exercitam o curto-circuito de conta-salário ANTES do `get_balance` (`salary_account_test.exs:107,172`), então o caminho saldo→hold_funds não é atravessado.
- **Correção precisa de teste de integração** que deposite saldo real via `MoneyUnit.from_cents` e prove os 3 cenários da seção 5.

---

## 7. Plano de correção (recomendado)

**Estratégia: normalizar TUDO para base_units (subcentavos) internamente, e converter para centavos só nas bordas (wire 5302, COSIF, UI).** Espelha SPB/PIX.

### Frente A — ENTRADA (raiz, alinha as unidades internas)
Envolver `requested_amount` com `MoneyUnit.from_cents/1` (guarda de nil) onde ele é montado:
- `file_processor.ex:390` (BLOCK), `:615` (UNBLOCK), `:705` (TRANSFER), `:920` (CANCEL).
- Enrichment do preview: `file_processor.ex:291-292` (`valor_solicitado`).

Efeito: `remaining`/`requested_amount` viram base_units → o `cond` de `amount_to_block` (1538-1544) fica 100% base_units (mesma unidade de `balance`); `hold_funds` congela o valor certo; `blocked_amount` passa a ser **sempre subcentavos**; e as comparações `processor.ex:76-82` (status), `:617,971-973` (transfer), `:1668,1676` (select_unblock) ficam corretas **sem mudança adicional**.

### Frente B — SAÍDA (bordas que esperam centavos)
Aplicar `MoneyUnit.to_cents/1` antes de:
- **Wire 5302** — nos construtores das response maps: `file_processor.ex:501` (valor_resposta_total), `:503` (conta_salario), `:639` (valor_desbloqueado), `:662`/`processor.ex:1666-1683` (transfer/saldos), `:991`/`processor.ex:1127` (cancel).
- **COSIF** — `post_cosif_entry` (`processor.ex:1567/2042`) e `post_cosif_unblock` (`processor.ex:2085`).

### Frente C — UI
- "Saldo Disponível" precisa da conversão subcent→cent **no servidor** (espelhar `requisicao_info/processor.ex:102` `div(balance,100)`; fonte `file_processor.ex:289`).
- As colunas de valor (`Valor Bloqueado`, etc.) seguem `/100` **depois** de `blocked_amount` virar consistente (subcentavos) + `to_cents` no serializer.

### Frente D — TESTES
- Teste de integração com saldo real (via `MoneyUnit.from_cents`) provando os 3 cenários da seção 5.
- Revisar os fixtures fictícios para distinguir a unidade de entrada (centavos) da de storage/TB (subcentavos).

**Ordem sugerida:** A → D (provar a raiz) → B → C. A Frente B é **independente** e afeta HOJE até os caminhos que hoje selecionam/liberam certo (desbloqueio TOTAL, CANCEL) mas emitem 100x — não esquecer dela.

---

## 8. Cuidados

- É caminho de **dinheiro (TB) + regulatório (BCB) + contábil (COSIF)**. Cada mudança de unidade precisa de teste que prove os 3 juntos (TB congela X, COSIF lança X, wire emite X em centavos).
- Decidir a unidade canônica da **coluna** `blocked_amount`/`requested_amount` no schema `JudicialOrder`/`JudicialBlock`: recomendação = **base_units (subcentavos)**, coerente com o TB; ajustar todos os leitores (UI, wire, COSIF) para converter na borda.
- Cuidado com dados JÁ persistidos em unidade mista (se houver bloqueios reais gravados antes do fix) — pode precisar de migração/backfill de `blocked_amount`.

---

## 9. Ponteiros

- Verificação adversarial (confirma/refuta): workflow `verify-block-unit-mismatch` (veredito duplo BUG_CONFIRMADO).
- Levantamento do raio de alcance: workflow `kb-sisbajud-money-unit` (4 superfícies).
- Memória: `sisbajud-leiautes-e-fatos-locais` (entrada 🔴 BUG CRÍTICO).
- Padrão correto de referência: `spb/inbound_credits.ex:285,308`, `accounting_bridge.ex:159,257`.
</content>
