# Melhorias de produção: PIX-in (money path), Relatório Saldo Diário, exibição de pagador/recebedor e acesso do core-admin

Data: 2026-07-13
Origem: documento "MELHORIAS MONETARIE.docx" do cliente (Murilo Bayer) + screenshot do relatório Saldo Diário de Conta Corrente + investigação empírica em produção desta sessão.
Status: diagnóstico PROVADO em produção (banco vivo, logs CloudWatch, reprodução de chamadas). Aguarda implementação e, para os itens de dinheiro e de permissão, GO do dono.

## 1. Sumário executivo

O relato do cliente ("o PIX recebido em 13/07 não aparece na Lista de Transações") revelou um problema muito maior: **o money path do PIX-in orgânico está quebrado em produção**. Os dois únicos PIX orgânicos recebidos até hoje (R$ 1.370,00 em 13/07 e R$ 2,00 em 10/07) **não foram creditados aos clientes e nem devolvidos ao pagador**: estão em limbo na Conta PI. A cadeia tem 5 defeitos nossos, todos provados abaixo.

Os demais pontos do cliente também foram diagnosticados até a causa-raiz:

| # | Item do cliente | Causa-raiz provada | Prioridade |
|---|---|---|---|
| 1 | Saldo Diário de CC mostra o mesmo saldo para todas as contas | Produto cartesiano 18x18 no join member x account (user 2583, a própria SCD, tem 18 matrículas e 18 contas) | P1 |
| 2 | Core Admin "Erro ao carregar" (dashboard e contas correntes) | murilo.bayer@monbank.net tem 0 grupos; RBAC fail-closed devolve 403 e o frontend mostra mensagem genérica | P3 |
| 3 | SPI Monitor: recebedor mostra a instituição, não o correntista | Resumo técnico cai para o nome do participante quando Cdtr/Nm está vazio; CdtrAcct nunca é extraído | P2 |
| 4 | SPI Lista: PIX recebido em 13/07 "não aparece" | O que aparece é o fantasma da devolução pacs.004 (Envio/Pendente/R$ 0,00); a linha do pacs.008 nunca foi criada. Ver P0 | P0 |
| 5 | SPB Transações: pagador/recebedor sem abrir XML | Parser dos dados de cliente existe, mas colunas payer_*/beneficiary_* nunca são gravadas nem exibidas | P2 |

## 2. P0: money path PIX-in quebrado em produção

### 2.1 Linha do tempo provada (13/07, horários UTC, logs de /ecs/monetarie/prod/pix-api)

1. 07:05:39.826: pacs.008 recebida do SPI. Pagador UNIDAS LOCADORA S.A. (via Bradesco, ISPB 60746948), R$ 1.370,00, recebedor CNPJ 59019090000105, conta 6930, E2E `E6074694820260713062552011661914`.
2. 07:05:39.850: validação de conta respondeu `account_not_found`; rejeição interna AC03.
3. 07:05:40.050: espelho da Conta PI creditado (+1.370,00, available 78.730,51).
4. 07:05:40.059: **pacs.002 ACSP enviada ao BACEN** (aceitamos o pagamento) e devolução automática pacs.004 despachada.
5. 07:05:40.858 em diante: OutboundSender tentou enviar a pacs.004 6+ vezes; todas falharam ANTES da transmissão com `XML parse error: bad_character 233`.
6. Estado final: cliente não creditado, devolução não transmitida, R$ 1.370,00 parados na Conta PI com balance block `ac600186-61de-4b29-856a-092d2592bddc`.

O mesmo padrão ocorreu em 10/07 com R$ 2,00 para a chave `murilo.bayer@monbank.net` (E2E `E0000000020260710165548761545451`, CPF 01264764030): o próprio cliente testando.

### 2.2 Os cinco defeitos (todos nossos, todos provados)

**D1. O parser não extrai os dados do recebedor do pacs.008.**
`Shared.Bacen.Iso20022.MessageParser` (apps/shared/lib/shared/bacen/iso20022/message_parser.ex) não tem NENHUMA extração de `Cdtr`, `CdtrAcct` ou `Prxy`. O envelope NATS do ICOM (`icom/nats_bridge.ex`) só carrega `message_type/message_id/xml_content`. Resultado: `validate_creditor_account` (inbound_processor.ex:971) manda `creditor_account`, `creditor_pix_key` e `creditor_cpf_cnpj` todos nulos ao Core.
Prova: `AccountResolver.validate_account` em produção HOJE resolve a conta 2949 (M. C. P. DA SILVEIRA LTDA, agência 0001, conta 000000693-0) com `"6930"` OU com o CNPJ; com tudo nulo devolve `account_not_found`. O cliente existe, a conta está ativa, o crédito era 100% aplicável.

**D2. O request/reply de validação mora DENTRO de um stream JetStream.**
`monetarie.core.pix.validate_account` é capturado por `MONETARIE_CORE` (`monetarie.core.>`). O servidor NATS envia um PubAck para o reply-to; esse PubAck corre contra a resposta real do Core e pode vencer (reproduzido em produção: `Gnat.request` devolveu `{"stream":"MONETARIE_CORE","seq":13}`). Quando o PubAck vence, o InboundProcessor entra em NAK/retry e o SPI acaba devolvendo por timeout (AB03). É a mesma classe de defeito já documentada no gotcha #7 do core/CLAUDE.md. Varredura feita nos 3 sistemas: este é o ÚNICO req/reply aplicativo dentro de um capture (dict.*.request e $JS.API.* estão fora; `monetarie.pix.tbfirst.settled` usa o PubAck de propósito).

**D3. A pacs.004 gerada é rejeitada pelo nosso próprio validador de XML.**
A descrição "Crédito recusado pelo destino (AC03)..." (com acento) é embutida no XML da devolução; o validador estrutural (xmerl com `String.to_charlist`) explode com `wfc_Legal_Character bad_character 233` (o "é"). A devolução NUNCA passa do gate de validação, fica presa em PDNG para sempre. Qualquer texto acentuado em qualquer mensagem outbound reproduz o problema.

**D4. Atribuição do pagador usa o AppHdr Fr (transporte) em vez do corpo.**
`build_transaction_attrs` (inbound_processor.ex:2720-2725) e `build_auto_return_event` (inbound_processor.ex:807/820) usam `parsed[:from_ispb]` = AppHdr `Fr` = 00038166 (o SPI), quando o pagador real está no corpo (`DbtrAgt` = 60746948, Bradesco). Consequência dupla: o registro inbound nasce com pagador errado E a devolução é endereçada ao ISPB errado (creditor da pacs.004 = 00038166). Era o follow-up conhecido "payer attribution Fr vs DbtrAgt", agora provado com dano real.

**D5. O fluxo de auto-devolução não cria a linha do pacs.008 e a lista mostra um fantasma.**
`process_credit_returned` (inbound_processor.ex:736-795) não chama `create_transaction_in_tx`; a única linha em `monetarie_spi.messages` para o E2E é o tracking da pacs.004 (`ReturnProcessor.upsert_return_tracking`, return_processor.ex:254-320): OUTBOUND, PDNG, sem linha em `payments` (valor R$ 0,00), pagador 46026562, recebedor 00038166. É exatamente a linha "Envio/Pendente/R$ 0,00" que o cliente viu. O pacs.008 recebido só aparece no Monitor porque o Monitor lê outra tabela (`monetarie_audit.xml_audit_logs`, com o XML íntegro).

### 2.3 Desenho da correção (PIX)

Ordem de dependência: D1 e D4 (parser/atribuição) alimentam D5; D2 é pré-requisito para o fluxo validar conta de forma confiável; D3 destrava a devolução legítima futura.

1. **MessageParser (D1, D4)**: extrair do corpo do pacs.008 (e do pacs.004): `debtor_name`, `debtor_document`, `debtor_account`, `debtor_agent_ispb` (DbtrAgt/MmbId), `creditor_name`, `creditor_document` (Cdtr/Id/PrvtId|OrgId/Othr/Id), `creditor_account` (CdtrAcct/Id/Othr/Id), `creditor_issuer` (Issr, agência), `creditor_account_type` (Tp/Cd), `creditor_pix_key` (CdtrAcct/Prxy/Id), `amount` (IntrBkSttlmAmt). Testes com o XML REAL do fio (copiado do audit log de produção, dados mascarados).
2. **InboundProcessor (D4)**: atribuição de ISPB com precedência corpo > E2E > AppHdr: `debtor_ispb = body.debtor_agent_ispb || ispb_from_e2e(e2e) || parsed[:from_ispb]`. Mesma regra no `build_auto_return_event` (a devolução passa a ser endereçada ao Bradesco).
3. **validate_creditor_account (D1)**: preencher o payload com os novos campos do parser.
4. **Subject novo (D2)**: cabine passa a chamar `pix.core.validate_account.request`; o responder do Core (`PixAccountValidator`) assina o subject novo E o legado durante a transição. Deploy do Core ANTES do PIX. Adicionar teste/guard de que nenhum subject de request/reply casa com captures de stream.
5. **Sanitização e validador XML (D3)**: (a) builder escapa/translitera texto livre para o charset aceito pelo SPI (AddtlInf e RstrctdFINXMax*Text são ASCII restrito no catálogo; remover acentos via transliteração); (b) validador estrutural passa a alimentar o xmerl de forma segura para UTF-8 (binário, não charlist de codepoints), para nunca mais reprovar XML válido por acentuação.
6. **Persistência do inbound (D5)**: no fluxo de devolução automática, criar TAMBÉM a linha `messages` do pacs.008 INBOUND (com `payments`: amount, nomes, contas dos dois lados) antes do tracking da pacs.004; no fluxo de crédito normal, criar a linha `payments` com os dados do parser (hoje só o envio via Core cria payment). A lista "Transações PIX" passa a mostrar: o recebimento (Recebimento/valor/pagador/recebedor corretos) e, quando houver, a devolução vinculada.
7. **Monitor (item 3 do cliente)**: `business_parties/1` (monitor_controller.ex:554-596) extrai também `CdtrAcct` e CPF/CNPJ; a UI (OperationsMonitorView.vue:1014-1017) mostra "correntista (nome se houver, CPF/CNPJ, agência/conta)" e NUNCA substitui pelo nome da instituição; a instituição fica em campo próprio ("Instituição recebedora").

### 2.4 Remediação do dinheiro em limbo (exige GO do dono)

Estado externo: o BACEN liquidou os dois PIX e nós respondemos ACSP; para pagador e Bradesco, os pagamentos FORAM CONCLUÍDOS. Estado interno: recebedores não creditados; devoluções nunca transmitidas.

Recomendação: **creditar os recebedores** (não devolver), porque é o estado que o mundo externo já enxerga e o que os clientes esperam:

1. R$ 1.370,00 para M. C. P. DA SILVEIRA LTDA (user 2889, conta 000000693-0), E2E `E6074694820260713062552011661914`.
2. R$ 2,00 para Murilo Bayer (CPF 01264764030, conta 052600003-0), E2E `E0000000020260710165548761545451`.

Passos (após deploy dos fixes, pelo caminho de crédito provado no incidente de 09/07): publicar o evento de crédito INBOUND ao Core (dedup por E2E garante 1 crédito), marcar as 2 pacs.004 como canceladas (`status_id` 9 CANC), liberar os 2 balance blocks do espelho PI, registrar audit trail. Validar: TB, extrato, linha PIXIN, saldo PI espelho sem block.

Alternativa rejeitada: transmitir as devoluções (após D3/D4). Deixaria os recebedores sem o dinheiro que os pagadores já consideram pago, criando atrito comercial para os clientes da Monetarie.

## 3. P1: Relatório Saldo Diário de Conta Corrente

### 3.1 Resposta direta à pergunta do cliente

O correto é exibir **o saldo de cada conta (matrícula)**, não um agregado por CNPJ. O relatório é "clientes com saldo positivo em conta corrente": cada linha é uma conta corrente com seu saldo naquele dia. Um mesmo CNPJ com N contas aparece em N linhas, cada uma com o saldo próprio. Hoje a tela repete o MESMO saldo agregado nas 18 linhas da instituição, e o KPI de total soma esse valor 18 vezes (o "Saldo Total" exibido está inflado).

### 3.2 Causa-raiz provada em produção

`checking_daily` (core/backend/lib/monetarie/use_cases/reports/accounts_balance.ex:38) junta `accounts` a `cooperative_members` só por `user_id`. O user 2583 (a própria MONETARIE SCD) tem 18 members E 18 accounts: o join produz 18x18 = 324 pares, e cada matrícula soma os lançamentos de TODAS as 18 contas (mesmo saldo repetido; total superconta 18x). Verificado no banco vivo: 1.606 members, 1.542 user_ids distintos; top duplicados: user 2583 (18), 2048 (3), 1972 (3).

### 3.3 Correção

Vínculo determinístico validado no banco vivo: **`member_number` = `account_number`** em 1.606 de 1.606 contas (correspondência 100%, zero órfãos nos dois sentidos). Correção:

- `checking_daily`: join `accounts` + `cooperative_members` por `cm.user_id == a.user_id AND cm.member_number == a.account_number` (o saldo volta a ser por conta; KPIs corrigidos).
- `member_statement` (mesmo arquivo): tem o MESMO fan-out latente (resolve contas só por `user_id`; extrato de uma matrícula da instituição mistura as 18 contas). Escopar pela conta da matrícula: `a.user_id == m.user_id AND a.account_number == m.member_number`.
- Auditar os "relatórios irmãos" do commit 3dcf70a3 (fees etc.) para o mesmo padrão.

## 4. P2: pagador/recebedor sem abrir XML (SPI e SPB)

### 4.1 SPI (Monitor de Transações)

Coberto no item 7 da seção 2.3 (extração de CdtrAcct/documentos + UI mostrando correntista final e instituição em campos separados).

### 4.2 SPB (Transações SPB, card "Informações da Transação")

O parser dos dados de cliente da STR0008R2 JÁ EXISTE (`lifecycle_engine.ex:1174-1229`, tags NomCliDebtd/CNPJ_CPFCliDebtd/CtDebtd e equivalentes Credtd) e o schema JÁ TEM as colunas (`spb_operations.payer_account/payer_cpf_cnpj/payer_name/beneficiary_*`, migration 20260425200001:73-78), mas ninguém grava nem exibe. Correção em 3 pontos:

1. Materialização: popular payer_*/beneficiary_* em `insert_inbound_str_credit` (lifecycle_engine.ex:1431-1466) a partir do `parsed` (e nos demais fluxos STR que tenham os dados).
2. Backend: incluir as colunas no SELECT de `do_show_transaction` (admin_controller.ex:2467-2481).
3. Frontend: exibir Pagador (nome, CPF/CNPJ, agência/conta) e Recebedor (idem) no card "Informações da Transação" (TransactionsView.vue:318-436) quando presentes.
4. ISPB origem "00000000": mapear `ISPBIFDebtd` para `sender_ispb` na ingestão/materialização da R2 (message_store.ex:296-307 e lifecycle_engine.ex:1455) para nunca cair no placeholder.
5. Backfill: script idempotente que reprocessa o `xml_content` do acervo e preenche payer_*/beneficiary_* das operações já existentes (somente colunas NULL; sem tocar em dinheiro).

## 5. P3: acesso do core-admin (Murilo Bayer) e UX de erro

1. **Dados (GO do dono)**: `murilo.bayer@monbank.net` tem 0 grupos (Wagner já tem "Administrador"). Proposta: atribuir o grupo "Administrador" ao Murilo pela tela Editar Usuário (corrigida em f00704ac) ou por operação assistida. Sem isso, TODAS as telas com RBAC devolvem 403.
2. **UX**: o interceptor do admin (core/apps/admin/src/lib/api.ts:51-68) só trata 401; 403 e 500 caem na mesma mensagem genérica. Corrigir: 403 vira mensagem específica "Você não tem permissão para ver este módulo. Solicite acesso ao administrador." nas telas (Dashboard, Contas Correntes) e/ou no interceptor, distinta de falha de carregamento.

## 6. Plano de execução, deploy e validação

Ordem de deploy (D2 exige Core antes de PIX):

1. Código + testes (TDD nos módulos de money path; testes com XML real mascarado).
2. Deploy HML: core-api, depois pix-api, spb-api, e UIs (pix-admin, spb-admin, core-admin). Validação viva em HML: simulador PIX-in ponta a ponta (crédito E devolução com acento na descrição), relatório Saldo Diário, telas SPB e admin.
3. Deploy PROD na mesma ordem, com rollback anotado por revisão de task-def.
4. Remediação do dinheiro (seção 2.4) SOMENTE após GO do dono, com evidência ao centavo antes/depois.
5. Validação final: reexecutar o PIX de teste do Murilo (R$ 2,00 novo) ponta a ponta com crédito em conta e linha correta na Lista de Transações.

## 7. Varredura "sem margem para falhas" (sincronização)

- Req/reply NATS dentro de captures de stream: varrido nos 3 sistemas; único caso é o D2 (corrigido neste plano).
- `monetarie_spi.accounts` (tabela local da cabine) está vazia em PROD, mas NÃO é usada pelo caminho de validação (que consulta o Core). Nenhuma ação além de documentar.
- pacs.002 recebida do SPI para pacs.008-in: acontece no fio real (recebemos uma em 13/07); hoje loga "SEM transacao correlata" porque a linha inbound não existia (D5). Com D5 corrigido, o status liquida na linha certa. Atualiza o entendimento canônico de 09/07 ("recebedor não recebe pacs.002"): o SPI real PODE entregar pacs.002 ao recebedor.
- Os 4 pacs.008 SENT de 09/07 e o restante do acervo não são afetados pelos fixes (somente caminho inbound e devolução).
- Índice único de E2E em `monetarie_spi.messages`: o schema declara `unique_constraint`, mas o índice não existe no banco, e HOJE isso é o que permite pacs.008 + pacs.004 coexistirem com o mesmo E2E. Não criar o índice; se quiser integridade, índice parcial por (end_to_end_id, message_code).

## 8. Achados adicionais durante a execução (2026-07-13)

Defeitos NÃO previstos no diagnóstico inicial, encontrados pela validação empírica em produção e todos corrigidos:

- **D6 — código interno de rejeição no `Rsn/Cd` da pacs.004.** O XSD `pacs.004.spi.1.5` fecha `Rsn/Cd` no enum {BE08, FR01, MD06, SL02}; a devolução automática com AC03 no fio reprovava na validação XSD pós-assinatura (fail-CLOSED) e ficava PDNG. Devolução por crédito inaplicável passa a ir como BE08; o código interno segue auditável no AddtlInf. Provado com xmllint e com o E2E sintético em HML (pacs.004 `Sent successfully HTTP 200`).
- **D8 — evento `transaction.created` sem `amount`.** O envelope do NatsBridge e a linha `messages` não carregam valor; o Core recebia `amount` 0 e o crédito explodia (`Wallet.deposit/4 no clause` → NAK → DLQ; cliente nunca creditado). Agora o InboundProcessor enriquece o evento com o `Decimal` em REAIS do `IntrBkSttlmAmt` — contrato da fronteira do Core (`MoneyBoundary/from_brl`, ADR-005), que trata o campo como reais.
- **Teto do `Wallet` em unidade errada.** `@max_amount` estava `1_000_000_000` com comentário "R$ 10M em centavos", mas o ledger é subcentavo/base_units → teto efetivo de **R$ 100.000,00**. Bloqueava crédito legítimo acima disso: a STR0006 de R$ 292.836,91 (Fundo Estadual de Saúde de Roraima) ia para a DLQ com `{:tb_deposit_failed, :amount_exceeds_limit}`. Corrigido para `100_000_000_000` (R$ 10M reais). Republicado o evento; conta 000000768 creditada, TB=PG ao centavo.
- **STR0006 sem nome/documento no card SPB.** A STR0006 (transferência de cliente para IF) usa tags de wire diferentes da STR0008 (`NomDestinatario`/`NomCliDebtd_Remet`) e aninha `CtDebtd` em `Grupo_STR0006R2_CtDebtd`. O mapa de campos passou a tentar uma lista de tags candidatas por campo com xpath descendente. Provado com o XML real (pagador = Fundo Estadual de Saúde de Roraima, recebedor = Monetarie).

### Remediação monetária executada (com GO do dono)

Reconciliada ao centavo (TB = PG em todas):

| Recebedor | Conta | E2E / Controle | Valor |
|---|---|---|---|
| M. C. P. DA SILVEIRA LTDA | 000000693 | E6074694820260713062552011661914 | R$ 1.370,00 |
| Murilo de Mello Bayer | 052600003 | E0000000020260710165548761545451 | R$ 2,00 |
| Fundo Est. Saúde RR → Monetarie | 000000768 | STR20260713033657892 | R$ 292.836,91 |

Em todos os casos: crédito pelo caminho canônico (evento outbox → PixHandler/SpbHandler), pacs.004 de devolução canceladas (status CANC) e balance blocks do espelho PI liberados, sem replay de fila.

## 9. Varredura completa das telas (2026-07-13, pós-implementação)

Varredura empírica de TODAS as rotas dos três admins em produção, com coletor de erros injetado no browser (erros de JS, promessas rejeitadas e HTTP 4xx/5xx via XHR/fetch):

| Admin | Rotas varridas | Erros de JS | Erros HTTP |
|---|---|---|---|
| core-admin | 177 | 0 | ~30 (404 de features não construídas + 6 crashes 500) |
| pix-admin | 99 | 0 | 0 |
| spb-admin | 137 | 0 | 1 (404 `/api/entities/status`) |

Nenhuma tela quebrada (zero tela branca) e nenhum erro nas telas alteradas por este plano.

### 9.1 REGRESSÕES INTRODUZIDAS POR ESTE PLANO (encontradas na varredura e corrigidas)

1. **Relatórios CC derrubariam cliente novo** (commit `289dcd2c` → corrigido em `cbed6739`). O join acoplado `member_number == account_number` vale para os 1.606 registros do ETL, mas o onboarding grava `member_number = CPF/CNPJ` (`promotion.ex:102/136`), que NUNCA é igual ao número sequencial da conta: um cliente novo com saldo **sumiria** do Saldo Diário e teria extrato/drill de tarifas vazios. Correção: `checking_daily` agrega por CONTA e resolve a matrícula depois (homônima → única do usuário → nil, sem nunca sumir); `member_statement` e `fees_by_member` caem para a conta única; `fees_consolidated` usa LEFT JOIN (tarifa nunca descartada do total). +2 testes travando a regressão.

2. **Crédito SPB podia ser bloqueado silenciosamente** (commit `5ad19098` → corrigido em `724887ee`). Passar a popular `payer_cpf_cnpj`/`beneficiary_cpf_cnpj` no INSERT do crédito recebido colidia com as CHECK constraints `^[0-9]{11}$|^[0-9A-Z]{12}[0-9]{2}$ OR NULL` (VIVAS em produção): `party_document` só removia não-dígitos, podendo emitir 10/12/13 dígitos ou corromper CNPJ alfanumérico (IN RFB 2.229/2024) → INSERT levanta → `Repo.rollback` → **cliente nunca creditado**, silencioso e repetível. Correção: só emite documento que satisfaça a constraint, senão `nil` (coluna NULL). +2 testes.

### 9.2 BUGS PRÉ-EXISTENTES corrigidos na mesma passada (não eram deste plano)

- **`analytics/fees` e `analytics/qr`** (500): `date_trunc` com bind param repetido em SELECT/GROUP BY vira expressão distinta no Postgres (erro 42803 `grouping_error`). Corrigido com `selected_as` (agrupa pelo alias do SELECT).
- **`analytics/payer-banks`, `consolidated_statement` (treasury settlement), top-clients, accounts, transactions timeseries** (500): `$n::uuid` recebendo `entity_id` como string → `DBConnection.EncodeError`. Todos para `$n::text::uuid`. Zero binds crus restantes no controller.
- **`nextcode/config`** (500): schema declara `field :settings, :map` e o código usa `config.settings`, mas a coluna nunca foi criada (`undefined_column`). Migration aditiva `20260713180000`.
- **e-Financeira `declarantes`** (500): 1 linha em produção com o CNPJ em TEXTO PURO num campo do tipo `EFinanceiraEncryptedDocument` (`cannot load ... as type`, o `load(_) -> :error` derrubava a tela). Não era código: a linha nunca passou pelo backfill de cifragem. Cifrada com o backfill oficial (`Release.encrypt_efinanceira_documents`, idempotente) + hash populado; `list_declarantes` volta a decifrar corretamente.

Todos validados em produção: os 5 endpoints que retornavam 500 agora retornam **200**.

### 9.3 Pendências conhecidas (NÃO são erros de código)

- ~30 rotas do core-admin retornam 404 porque a feature não foi construída no backend (ATM, cheques, comunicações, corporate/*, custódia, desconto, cartões, NF-e, QR codes admin). São telas existindo sem endpoint — decisão de produto, não bug.
- spb-admin: 1 rota (`/operator/entity-status`) chama `/api/entities/status`, inexistente.

## 10. Telas com 404/400: 26 eram BUG DE PATH, não feature ausente (2026-07-13)

O diagnóstico inicial da seção 9.3 (~30 telas "sem backend") estava ERRADO. Investigação com o roteador do Phoenix consultado em produção (`Phoenix.Router.route_info/4`) e teste HTTP endpoint a endpoint provou: **o backend existe, implementado, com query no banco — o frontend chamava o path errado**.

Causa: o cliente axios do core-admin usa `baseURL: '/api'` (`lib/api.ts:9`), mas o domínio de negócio vive em `scope "/api/v1"` (`router.ex:251`). As telas que funcionavam já usavam `/v1`; as quebradas esqueceram o prefixo. Prova (produção, token de admin): os mesmos 25 endpoints retornam **404 sem `/v1` e 200 com `/v1``.

### Corrigido (commits `55069458`, `5e373f41`)

- **87 chamadas** em 9 composables + 1 view ganharam o prefixo `/v1`. Preservada `POST /corporate/insurance/claims/:id/review`, que legitimamente vive em `/api` (confirmado no roteador).
- **19 telas destravadas**: Talonários, Devoluções de Cheques, Novidades, Campanhas, Seguros, Ouvidoria, Suporte, Explorador de Dados, Declarações, Compensação, Itens em Custódia, Backlist, Produtos e Solicitações de Desconto, Transações e Terminais ATM, Cartões, Notas Fiscais, Empresas NF-e.
- **Entidades Externas (spb-admin)**: `GET /entities/status` era sombreada por `/entities/:id` (Phoenix casa por ordem de declaração → `id="status"` → 404). Rota movida para antes da curinga; `/entities/:id` segue funcionando (testado com id real).
- **Cheques**: pós-`/v1` os 3 endpoints resolviam mas devolviam 400 — a listagem é POR CONTA (`account_id` obrigatório, `checkbook_controller.ex:22`) e a tela consultava sem conta. Guard `enabled` no TanStack Query: só consulta com conta escolhida.

### Varredura final (produção, 177 telas do core-admin)

| Métrica | Antes | Depois |
|---|---|---|
| Erros de JavaScript | 0 | **0** |
| Crashes 5xx | 6 | **0** |
| Chamadas 4xx | 31 | **6** |

### As 6 restantes: features a construir (decisão de produto, nenhuma toca dinheiro)

1. `GET /api/admin/analytics/compliance/account-ranking` — **Ranking de Contas**. Parcial: só o `/export` existe; o SQL da listagem (`account_ranking_rows/1`) já está pronto, falta expor a action JSON. É o mais barato.
2. `GET /api/admin/qr-codes` e 3. `/api/admin/qr-codes/metrics` — **QR Codes**. O dado existe (tabela `qrcodes`, alimenta o analytics); o CRUD existe só na cabine PIX, não no Core.
4. `GET /api/admin/pix-out-queue/status` — **PIX OUT em Trânsito**. Nada no backend (nem schema, nem controller).
5. `GET /api/treasury/caixa-institucional/` — **Caixa Institucional**. Só rotas de escrita; a própria tela trata o 404 como "indisponível nesta fase".
6. `GET /api/webhooks/deliveries` (400) — **Entregas de Webhooks**. Não existe listagem global, só `/webhooks/:id/deliveries`; o 400 vem de cair em `/webhooks/:id` com `id="deliveries"`. O replay funciona.

## 11. Schema `stg.*` no banco do SPB (produção)

Staging bruto do import do legado AutBank: `stg.corpo` (XML cru, 10.662 linhas) e `stg.tip` (10.662), **8,7 MB no total**. Nenhum código lê essas tabelas (grep em todo o `spb/`: zero referências) — o acervo final vive em `spb_operations`/`bacen_messages`. A diferença de contagem (10.662 vs 7.397 mensagens) é esperada: o staging traz tudo do arquivo, inclusive o que não vira operação (ecos GEN0001 de conectividade).

Recomendação: **manter**. São 8,7 MB, não atrapalham nada e servem de trilha de auditoria da origem do import legado. Remover é seguro (nada lê), mas sem ganho real — e a regra da casa é não apagar em produção. Se for remover, fazer dump para o S3 antes.

## 12. Auditoria do Dashboard do core-admin (2026-07-13)

Auditoria dos 18 widgets contra o banco de produção. O card "Operações do Dia" estava **estruturalmente cego**; vários outros mostravam número inventado.

### Corrigido (commit `b6a90760`, PROD core-api:33 + core-admin-ui:12)

| Widget | Mostrava | Verdade | Defeito |
|---|---|---|---|
| Operações do Dia · PIX entrada | R$ 0,00 · 0 tx | R$ 1.372,00 · 2 tx | `pix_in`/`pix_out`/`boletos` eram **hard-coded zero** (TODO de "mapear kind" nunca feito) |
| Operações do Dia · **TED** | **não existia** | R$ 849.532,01 · 10 tx | linha ausente na tela; é o grosso do movimento |
| Operações do Dia · Total | R$ 0,00 · 0 | R$ 850.904,01 · 12 | filtrava `status == 1` (accepted), mas `PaymentTransactions.create/1` grava `status: 0` fixo e o desfecho vai para `payment_status` — **nenhuma transação jamais atinge 1** |
| MEDs abertos | hard-coded 0 | `med_cautelar_blocks` ativos | valor fixo se passando por dado |
| Clientes Ativos | 135 | 150 | contava `accounts.user_id` distinto (contas do mesmo titular colapsam; a SCD tem 18) em vez de `cooperative_members` |
| Crescimento de clientes | +135 no mês | 0 | usava `accounts.inserted_at` = data da **carga do ETL** (as 1.606 contas têm inserted_at = 2026-07-09), não a abertura. Agora `admission_date`/`exclusion_date` |
| CADOC vencido | 6 | 1 vencido + 5 no prazo | comparava **competência** (`reference_date`) com hoje → toda competência fechada virava "vencida" no dia seguinte. Agora calcula o **prazo real**: 4111 = D+3 dias úteis, 3040/4010 = D+10 úteis, 4016 = D+60 corridos, 5300 = dia 22 (+5 testes) |
| PLD/FT em análise | hard-coded 0 | `compliance_cases` abertos | comentário desatualizado ("schema ainda não existe") |
| Pendências hoje | somava `outros: 0` fake | — | chave hard-coded entrava no total |
| Inadimplência | só `status="active"` | `active` + `defaulted` | KPI divergia da seção regulatória: dois números de inadimplência na mesma tela |
| Originação do mês | sem filtro | só desembolsado e até hoje | contava `rejected`/`draft` e desembolso **futuro** |

61 testes do dashboard verdes.

### Zeros honestos (NÃO mexidos)

Capital Social, Carteira de Crédito, Inadimplência, Cobertura de Provisão, Originação, Concentração e Crédito por Modalidade mostram R$ 0,00 / 0,00% porque a tabela `loans` está **vazia** em produção: o `TRUNCATE ... cooperative_members ... CASCADE` da re-migração do ETL (`etl/autbank/remigrate.ex:216`) levou `loans` junto (FK `loans.member_id`). Não há dado a exibir — não se inventa número.

### Erro evitado (registro honesto)

Cheguei a aplicar `MoneyUnit.to_cents` nos cards de dinheiro achando que o COSIF estava em base_units. **Errado**: `cosif_account_balances.balance_sheet` já está em CENTAVOS (provado no banco: o escrow de R$ 523.174,21 está gravado como 52.317.421). Revertido antes do deploy — teria exibido 100× menos.

### PENDÊNCIA — exige decisão do dono

Os cards **Conta de Pagamento (R$ 196.143,48)** e **Tesouraria/Disponibilidades (R$ 1.026.374,42)** leem um **snapshot COSIF congelado**, não o dinheiro real:

- Não existe rotina diária que gere o snapshot (`use_cases/cosif/balance_sheet.ex:454-460`, `TODO(daily-snapshot-worker)`). O último `reference_date` vem do ETL/provisão.
- Consequência: ignoram todo o movimento — inclusive os R$ 849 mil de TED-in de hoje. O razão vivo tem R$ 2,11 milhões.
- O filtro de `captacao` (`like "4.9.8.%"`) ainda **esconde R$ 523.174,21** de fundos vinculados (grupo `4.1.1.85`), que o próprio `Account` permite.
- O "142,7% de liquidez imediata" é artefato contábil (disponibilidades ÷ grupo 4 dos mesmos lançamentos do ETL), não liquidez real.

Dois caminhos: (a) esses cards passam a ler o **razão vivo** (`account_entries`/TB), como o resto do admin; ou (b) criar a **rotina diária de snapshot** COSIF e manter a semântica de balancete. Decisão de produto/contabilidade.
