# Handoff — Incidente TED manual STR0008 sem débito no extrato (PRD 17/07) + pendências

**Data:** 2026-07-17 (fim da tarde/noite BRT)
**HEAD:** `origin/main = d7810ace`
**Deploy vivo:** PROD `core-api:64` + `spb-api:38` (COMPLETED, TGs healthy). HML validado antes em `core-api:160` + `spb-api:67`.
**Relatório detalhado (fonte de verdade da investigação):** `docs/reports/2026-07-17-ted-manual-str0008-sem-debito-extrato-prd.md`
**Memória:** [[monetarie-ted-manual-corenotifier-uuid-0717]]

> ⚠️ A ÁRVORE ESTAVA COMPARTILHADA com uma SESSÃO PARALELA (item 5 / devolução do cliente). No fecho desta sessão havia modificações NÃO COMMITADAS dela em `pix/backend/apps/shared/.../message_builder.ex`, `pix/backend/apps/spi_service/.../return_processor.ex` (+ teste). NÃO são desta frente — não commitar/reverter sem falar com a sessão paralela.

---

## 1. O que aconteceu (resumo executivo)

Teste de TED da Maiza (grupo "Migração Monetarie <> Vulci"): STR0008 manual de R$ 10,00 enviada pela tela do spb-admin (NumCtrl `MON20260717764179292`), com volta STR0006 do Bradesco 2 min depois. O extrato da conta **2630** (a própria Monetarie, user 2583, nº 052600005-0) só mostrava o **crédito da volta**, não o **débito da ida**.

**Não era "extrato que não mostra":** o débito **nunca foi efetivado no Core**. Cadeia provada (empírica, PRD read-only):

1. Envio manual criou hold + transação `processing` corretamente (lastro OK via `CoreDebitValidation.register`).
2. BACEN liquidou (STR0008R1, `SitLancSTR=1`, 14:07:28).
3. **CAUSA RAIZ:** no `r1_confirmed`, o `LifecycleEngine.notify_core_if_applicable` (spb) leu `bacen_messages.id` por `Repo.query` cru → uuid como 16 bytes binários → `CoreNotifier` pôs em `"message_id"` → **encode JSON estourou** (`[error] [CoreNotifier] Exception: invalid byte 0xD2 in <<210,108,...>>` = o uuid `d26c4b17-...`). O `rescue` engoliu. O evento `monetarie.spb.transactions.confirmed` **nunca foi publicado**. Core nunca soube; extrato nunca gravou (o débito de TED só nasce em `record_outbound_settled`, disparado pelo confirmed/settled).
4. **AGRAVANTE:** às 14:41 o `StaleHoldChecker` (30 min) aplicou timeout cego e **devolveu o hold de uma TED que o BACEN já tinha liquidado** — recriou saldo gastável sem lastro. Em escala/valor alto + operações em sequência = prejuízo sem lastro (o risco que o dono apontou).
5. **TERCEIRO defeito:** a rede de segurança `SpbStatusReconciliation` (consulta a cabine e materializa o desfecho certo) estava **INERTE em PROD** — task-def do core sem as credenciais I1. Provado no log: `SPB cabin API not configured` às 17:36Z varrendo exatamente essa TED.

A STR0006 (volta) funcionou 100% pelo trilho inbound (`spb_inbound_credits` → crédito TB + extrato + tx completed + COSIF).

---

## 2. O que já foi feito (COMPLETO, verificado vivo)

### Reparo de dado em PRD (ordem do dono)
Débito de R$ 10 efetivado na conta 2630 pelo fluxo normal: hold TB novo `8579386629d89b1153a89e6433719f01`, tx `MON20260717764179292` → `settled` com `bacen_reference = STR20260717033692271`, linha "TED enviada" gravada via `StatementEntries.record_outbound_settled`. **Verificado: TB = PG = 7.697.327.100 subcent (R$ 769.732,71), MATCH=true**, extrato com as 2 pernas. Round-trip zerado na reserva BACEN.
- GOTCHA registrado: `metadata || $X::jsonb` com param STRING JSON vira ARRAY e quebra o load Ecto — passar o MAP como param (Postgrex encoda jsonb).

### Fix 1 — CoreNotifier sobrevive ao uuid cru (commit `7c990cd1`, TDD)
`spb/services/bacen_gateway/lib/bacen_gateway/nats/core_notifier.ex` — `id_string/1` normaliza o uuid de 16 bytes na fronteira do payload (`Ecto.UUID.load`, fallback Base16). Testes reproduzindo o byte 0xD2 + fallback de transaction_id. Suítes: core_notifier 8/0, nats 10/0, consumers 47/0.

### Fix 2 — StaleHoldChecker nunca dá timeout cego em TED (commit `cbd21c1b`, TDD)
`core/backend/lib/monetarie/workers/stale_hold_checker.ex` — `quarantine_stale_ted/1`: TED outbound = quarentena barulhenta (hold PRESERVADO + telemetria `[:monetarie, :ted, :hold_stale]`); desfecho pertence ao `SpbStatusReconciliation`. Teste novo `stale_hold_checker_ted_skip_test.exs` + PIX preservado. Workers 201/0.

### Fix 3 — I1 (SpbStatusReconciliation) ligado em PROD
Task-def `core-api-prod:64` ganhou `SPB_CABIN_API_BASE_URL=http://spbapi.monetarie.internal`, `SPB_CABIN_ADMIN_LOGIN=admin@monetarie.com` (⚠️ PROD é `.com`, HML é `.com.br`), secret `SPB_CABIN_ADMIN_PASSWORD` → `monetarie/prod/admin/spb/initial_password`. Prova viva: `CabinStatusLookup.by_transaction_id("MON...")` retornou `state=r1_confirmed`; tick 18:21Z rodou `errors: 0` (sem o "not configured").

Commits `7c990cd1`, `cbd21c1b`, `d7810ace` na main (pushados).

---

## 3. PENDÊNCIAS mapeadas para outra sessão

### P1 — Espelho COSIF da perna de saída do STR0008 manual (CONFERIR + possível reparo)
O reparo de dado recriou o débito operacional (TB + extrato + tx), mas **NÃO** verifiquei se o lançamento COSIF (`spb_cosif_mirror` no Core / `cosif.entry` da cabine) da perna de saída existe para essa operação. Como o notify morreu no meio, é provável que o espelho contábil também não tenha sido emitido.
- **Ação:** conferir em PRD `SELECT * FROM spb_cosif_mirror WHERE ...` para o operation_id `706f2081-e6d6-420a-a1d4-d05b228d342d` (spb) / a tx `MON20260717764179292`. Se ausente, decidir com o dono se emite o espelho retroativo (COSIF é 3ª perna, reconciliável — não urgente, mas fecha o balancete).
- Contexto do trilho: `BacenGateway.NATS.CoreNotifier.notify_cosif_entry` → subject `monetarie.spb.cosif.entry` → consumidor Core grava `spb_cosif_mirror` (unique `operation_id, cd_evento, leg`).

### P2 — Audit do envio manual com operador nulo (RASTREABILIDADE)
A tabela `audit_logs` da cabine registrou o `message_send` (msg `d26c4b17-...`, IP 10.50.1.133, 17:07:25Z) com **`user_id` NULO**. Não se prova pela trilha QUEM disparou o envio manual pela tela.
- **Ação:** no fluxo de `POST /api/messages/send` (`messages_controller.ex`), amarrar o `user_id` da sessão autenticada ao `AuditContext`/linha de auditoria. Verificar por que o plug de auth não propaga o resource ao audit desse endpoint (pode ser a mesma classe do fix de audit do coreadmin de 07-10).

### P3 — CoreNotifier deveria ser DURÁVEL (robustez, não só o uuid)
O fix do uuid tira a causa imediata, mas o notify ainda é best-effort dentro de um `rescue` que só loga. A reentrega do R1 (17:07:37) foi tratada como idempotente sem repetir o notify perdido. Uma falha futura por OUTRA razão (NATS momentâneo, etc.) repetiria o padrão "TED presa em silêncio" — agora mitigado pelo I1, mas o ideal é outbox durável.
- **Ação:** avaliar mover o `notify_status_change` para o mesmo padrão outbox-durável que o `notify_cosif_entry` já usa (`Repo.transaction + publish_async`), OU garantir que uma falha de notify emita ALERTA operacional (não só `Logger.error`). Prioridade média — o I1 já é a rede de segurança.

### P4 — Fixar imagem do sidecar MQ por digest (higiene pré-existente, não desta frente)
O `spb-api-prod:38` carrega `spb-mq-sidecar:prod-329ffd0d-20260708` (tag mutável). Pendência antiga já registrada no CLAUDE.md — mencionada aqui só para não se perder.

---

## 4. Gotchas úteis para a próxima sessão

- **ECS exec rpc:** `sh -c "/app/bin/<release> rpc '<elixir>'"` funciona; a saída fecha com `Cannot perform start session: EOF` (normal). Core release = `monetarie` (repo `Monetarie.Infra.Repo.Base`); spb release = `bacen_gateway` (repo `BacenGateway.Repo`).
- **accounts tem 2 schemas no search_path** (`autbank_src` + `public`) — SELECT em `accounts` sem qualificar pode pegar colunas do schema errado (ex.: `name` não existe em `public.accounts`). Qualificar ou usar as colunas de `public` (`account_number`, `agency`, `user_id`).
- **Wallet:** o módulo é `Monetarie.UseCases.Wallet.get_balance/1` (não `Monetarie.Wallet`).
- **Unidades:** `transactions.amount` e `account_entries.amount` = SUBCENTAVOS; `spb_inbound_credits.amount_cents` e `scheduled_teds.amount` = CENTAVOS. Extrato converte subcent→cent na exibição.
- **Rastreamento ponta-a-ponta:** `transactions.transaction_id` (= o "MON..." ou "TED...") amarra Core↔cabine (`bacen_messages.correlation_id`). Inbound usa `num_ctrl_str`.
- **StaleHoldChecker:** cron `1-59/5`, threshold 30 min. **SpbStatusReconciliation:** cron `6-59/15`, exige `SPB_CABIN_API_*`.
- **Regra do dono reforçada nesta sessão:** o money-path de TED de saída NUNCA tinha sido exercitado vivo em PROD antes — foi dado como seguro sem prova. Lição: [[owner-no-tests-in-production]] vale também para trilhos "óbvios". Segunda 21/07 o cliente roda sequências; o fix do CoreNotifier era pré-requisito.

---

## 5. Como retomar

1. Ler este handoff + `docs/reports/2026-07-17-ted-manual-str0008-sem-debito-extrato-prd.md`.
2. Atacar P1 (COSIF) e P2 (audit) — ambos com decisão do dono antes de reparo de dado em PRD.
3. P3 (outbox durável do notify) como incremento TDD quando houver janela.
4. Coordenar QUALQUER deploy pix/spb com a sessão paralela (item 5 / capacidade extrema) — regra [[deploy-nunca-durante-operacao-money-path]].
