# Saldo da Conta PI, devolução que não sai e importação AutBank — apuração e correção

Data: 2026-07-13
Ambiente: PRODUÇÃO (`monetarie-greenfield-prod`, 10.50), BACEN real.

## 1. O que o dono viu

1. Tela de Bloqueios de Saldo do pix-admin com 50 bloqueios datados de 26/06/2026, quando a cabine não operava.
2. Conta PI com "Créditos Hoje R$ 1.370,00 / Débitos Hoje R$ 1.370,00 / Saldo Líquido R$ 0,00" e saldo que não contabiliza o que entrou.
3. Dashboard com posição de balancete no futuro (31/07).

## 2. Apuração (evidência viva, sem inferência)

### 2.1 Bloqueios de 26/06 — lixo da importação AutBank

`monetarie_spi.balance_blocks` em PROD tinha **4.392 bloqueios `active`, R$ 168.935.505,42**.
Prova de que são do import e não da operação:

- 4.392 de 4.392 têm `reference_id LIKE 'LEGACY-OP-%'`.
- `reason` nulo e `created_by` nulo em todos.
- Zero bloqueios ativos criados depois de **2026-07-08**, que é quando a cabine passou a trafegar com o BACEN
  (primeiro registro em `monetarie_audit.xml_audit_logs`).
- `inserted_at` espalhado por 4.386 segundos distintos entre 02/12/2025 e 26/06/2026: são os
  timestamps ORIGINAIS do AutBank, copiados pelo ETL.

Causa raiz — `etl/legacy_hml_import/20_project_pix.sql`, seção "4) BLOQUEIOS":

```sql
CASE upper(coalesce(j->>'ST','')) WHEN 'APROVADO' THEN 'confirmed'
                                  WHEN 'RECUSADO' THEN 'released'
                                  WHEN 'ERRO'     THEN 'cancelled'
                                  ELSE 'active' END   -- <<< todo status desconhecido virou hold VIVO
```

O dinheiro nunca foi travado: o ETL escreve direto na tabela de bloqueios, sem passar por
`SpiService.Balances.create_block/2`, que é quem move `available -> blocked`. `monetarie_spi.balances`
para o ISPB 46026562 mostrava `blocked = 0.00` antes e depois da limpeza.

### 2.2 Conta PI — o saldo é sobrescrito com o fechamento de sexta

O reconciliador materializa a posição da Conta PI a partir do SADP de um camt.053
(`InboundProcessor.maybe_upsert_conta_pi_position/1` -> `Balances.upsert_position/5`), mas:

- `SpiService.Camt060` mapeia `"saldo-atual"` para `%{reqd: "camt.053", prtry: "CSA", date: :closed_day}`,
  e `:closed_day` pede `from_date = hoje - 1`. **Extrato de dia fechado nunca traz a posição de hoje.**
- Segunda-feira 13/07, o último dia útil fechado é sexta 10/07. O BACEN respondeu, corretamente,
  com a posição de **10/07 16:56** (`<Bal><Dt><DtTm>2026-07-10T16:56:00.657Z</DtTm></Dt>`).
- O handler **ignora essa data**, grava `available = 77.453,82` e carimba `as_of` com a hora da
  MENSAGEM (18:47 de hoje). O número velho passa por atual e apaga o crédito de hoje.

Resultado: a PI aparece com o fechamento de sexta e o PIX de R$ 1.370,00 recebido hoje some da tela.
É exatamente o "saldo errado e não contabilizado".

### 2.3 A devolução (pacs.004) nunca saiu — um acento segurou o dinheiro

Log de produção, 07:05:40:

```
[OutboundSender] Outbound pacs.004 M46026562417cf13c00e294b4060e3f6 XML invalid — RR04:
XML parse error: {:bad_character, 233}, {:line, 26}, {:col, 58}
```

Caractere 233 = `é`. O motivo da devolução (`"Crédito recusado pelo destino"`) entra no XML e o
`Shared.Validation.MessageValidator` quebrava ao alimentar o xmerl com `String.to_charlist/1`
(charlist de codepoints em vez de bytes). A pacs.004 entrou em NAK/retry e morreu em status
9 (CANC), **sem sair e sem alertar ninguém**.

Cadeia completa do PIX de R$ 1.370,00 (E2E `E6074694820260713062552011661914`):

| Hora (UTC) | Evento | Prova |
|---|---|---|
| 07:05:39 | pacs.008 recebida, BACEN já liquidou | `xml_audit_logs` pacs.008 RECEIVED |
| 07:05:39 | conta não resolvida -> AC03 | log `account_not_found` |
| 07:05:40 | hold de devolução debita a PI (Debit-Then-Send) | `balance_history` `spi_outbound_hold` |
| 07:05:40 | pacs.004 **rejeitada pelo nosso próprio validador** (acento) | log RR04 bad_character 233 |
| 07:05:41 | enviamos **pacs.002 ACSP** (aceitamos o PIX) | XML: `<TxSts>ACSP</TxSts>` |
| 07:05:41 | BACEN responde **pacs.002 ACCC** (liquidação confirmada) | XML: `<TxSts>ACCC</TxSts>` |

Conclusão: **o dinheiro é nosso**. Aceitamos o crédito, não devolvemos nada, e o crédito de
R$ 1.370,00 lançado na conta 2949 (e o de R$ 2,00 na 1644) **tem lastro**. Não há estorno a fazer.

Filas conferidas antes de qualquer mudança: consumidores `outbound-sender` e `return-processor` com
`num_pending = 0`, `num_ack_pending = 0`; as duas pacs.004 em status terminal. Não há devolução
represada capaz de disparar e debitar a PI depois do fix do acento.

### 2.4 Buracos colaterais confirmados

- **Nenhum log de pacs.004 em `xml_audit_logs`, em dia nenhum.** Devolução que falha na validação não
  gera trilha. Não temos como provar devolução a um regulador.
- **Devolução que não sai morre em silêncio**: status CANC, sem alerta. O dinheiro do pagador ficaria
  parado na nossa PI indefinidamente e estaríamos em falta com o BACEN.
- **Download do detalhe do extrato não funciona em prod**: `BACEN_STATEMENT_DOWNLOAD_URL` não está
  configurado e o keyfile mTLS do canal `arq` não existe na task
  (`{:keyfile, '/tmp/pix/certs/client_key.pem', {:error, :enoent}}`). Por isso o camt.052 de hoje
  informa `<NbOfNtries>2</NbOfNtries>` e não conseguimos abrir quais são os 2 lançamentos.

## 3. Correções

### T1 — Saldo da Conta PI
1. `Camt053Parser`: extrair a data da posição (`Bal/Dt/DtTm`) além de SADP/SABK.
2. `maybe_upsert_conta_pi_position`: **jamais sobrescrever `available`/`blocked` com posição mais
   antiga que o último movimento local**. Posição antiga vira `confirmed` + registro de conciliação.
   Posição corrente sobrescreve (BACEN é verdade). Sem linha ainda: materializa (bootstrap).
3. `as_of` passa a ser a data REAL da posição, não a hora de chegada da mensagem.
4. `Camt060`: `"saldo-atual"` deixa de mentir. A tela mostra a data da posição.
5. Recompor o `available` vivo da PI em prod a partir da última posição confirmada + movimentos locais.

### T2 — Devolução (pacs.004)
1. Sanitizar o texto que entra no XML (o `TextSanitizer` já existe e não estava sendo usado nesse caminho).
2. Fail-LOUD: devolução que não sai gera alerta, não status CANC silencioso.
3. Auditar a pacs.004 em `xml_audit_logs` inclusive quando falha.
4. Teste de regressão com texto acentuado.

### T3 — Importação AutBank (a família inteira)
Regra de projeto: **todo `CASE` de status precisa de `ELSE` terminal, e todo INSERT em tabela com
coluna de estado precisa listar a coluna de estado**, porque o default do DDL é o estado VIVO
(`active`, `ACTIVE`, `OPEN`, `PENDING`, `status_id 1`). Import histórico nunca cria trabalho novo.

Defeitos a corrigir no SQL + remediar nos dados de PROD (sem DELETE):
- `stg.stmap` com `ELSE 1` (PDNG): pacs.008 legadas entram "em voo" e cegam o `StuckOutboundChecker`.
- `monetarie_dict.keys`: chaves legadas entram **ATIVAS** (coluna `status` omitida no INSERT, default
  `'ACTIVE'`) — ficam reivindicáveis/portáveis.
- `monetarie_dict.claims`: o INSERT dispara o trigger `trg_update_open_claim_date`, que faz UPDATE em
  `monetarie_dict.keys` carimbando `open_claim_creation_date` na chave VIVA.
- `statement_entries.status = 'legacy_erro'` e `balance_history.direction = lower(CD)`: valores fora do
  `Ecto.Enum` derrubam as telas de extrato/movimentação com 500 no load.
- `camt060_requests` importadas como `'sent'`: o sweeper de timeout trabalha em cima do acervo.
- SPB: `ELSE 'sent'`/`'pending'` e acervo em estado liquidado elegível a lançamento contábil em massa.
- Core: `Member.member_status/1` com `ELSE "active"` (fail-open no KYC); `Apply.run(reset: true)` sem
  gate; ETL de extrato sem idempotência.

### T4 — Canal de arquivos do BACEN
Configurar `BACEN_STATEMENT_DOWNLOAD_URL` e o material mTLS do pool `arq` em produção, para abrir o
detalhe do extrato e fechar a divergência dos 2 lançamentos.

## 4. O que já foi aplicado

- 4.392 bloqueios fantasma neutralizados em PROD e HML: `status = 'cancelled'`,
  `release_reason = 'LEGACY_IMPORT_AUTBANK'`, `released_at = inserted_at`. **Nada foi apagado.**
  Saldo da PI idêntico antes e depois (`available 77.453,82 / blocked 0.00`), provando que nunca
  travaram dinheiro. Em HML os 5 bloqueios reais de teste (R$ 1,06) foram preservados pela guarda.
- Raiz corrigida em `etl/legacy_hml_import/20_project_pix.sql` (`ELSE 'cancelled'` + `reference_type`,
  `release_reason` e `released_at` explícitos).
