# Money path: itens que dependem de aprovação/decisão do dono (2026-07-16)

Este documento reúne o que NÃO foi implementado nesta sessão porque é
arquitetural (risco delicado em money-path) ou é um fluxo novo cross-repo que
merece um incremento revisado. Cada item tem: problema, correção proposta,
risco e plano de teste. Nada aqui foi tocado no código — aguarda o seu "ok".

Companheiros já implementados: `docs/reports/2026-07-16-money-path-submissao-taxonomia-falha.md` (11 defeitos de inferência) e os fixes de "dinheiro preso"/TED merchant desta sessão.

---

## A. Fluxo de agendamento da TED por data prevista de liquidação — IMPLEMENTADO 2026-07-17

**STATUS: IMPLEMENTADO com TDD (commit/deploy à parte).** Migration `20260717120000_create_scheduled_teds`; schema `Monetarie.Schemas.Spb.ScheduledTed`; use case `Monetarie.UseCases.Spb.ScheduledTed` (`classify_settlement/1`, `create/1` SEM hold, `execute_due/3` débito diferido, rejeição por insuficiência + webhook `ted.failed`, claim idempotente); executor `Monetarie.Workers.Spb.ScheduledTedExecutor` (cron `0,15,30 11 * * 1-5` = abertura da grade 8h BRT + catch-up); integração no `TedController` (`create` ramifica imediata/agendada via `classify_settlement`). Testes: use case 11/0, controller 4/0, cron parity + queue coverage OK. **Segue AWAITING deploy** (migration nova + executor cron). Extensão futura: partner_v1/v2 transfer controllers reusam o mesmo `ScheduledTed` (o `scheduledDate` da v2 hoje é ignorado).

**REGRA DO DONO (corrigida 2026-07-17):** um agendamento NÃO promove hold. O
dinheiro fica LIVRE na conta até a data de liquidação. Na criação, a TED
agendada só carrega um AVISO ao cliente para garantir saldo na data. O saldo é
conferido NA DATA, na abertura da grade (8h): se houver → hold + débito + envia;
se NÃO houver → REJEITA às 8h por insuficiência de saldo e avisa o cliente.

**Correção do design anterior:** a versão antiga desenhava o hold reservado do
agendamento até a liquidação (dias). ERRADO. O débito é DIFERIDO para a data de
execução; o hold só existe brevemente na execução (8h), igual a uma TED normal.

**Precedente pronto no Core (espelhar):** o PIX AGENDADO já faz exatamente isto —
`Monetarie.UseCases.Pix.ScheduledPix.execute_due/2` + `ScheduledPixExecutor`
(cron minuto a minuto): NÃO faz hold no agendamento; no dia da execução faz
`Wallet.get_balance` → `BalanceGuard.verify_with_overdraft` → `FundHold.hold_funds`
→ envia; libera o hold se qualquer passo falha. A TED agendada segue o MESMO
molde.

**Desenho corrigido (Core-side; a cabine continua sender burro):**
1. **Core (criação, SEM hold)** — a TED (partner/v2/merchant) aceita
   `settlement_date`. Se hoje/imediata → fluxo atual (hold+débito agora). Se
   futura em dia útil → grava uma TED AGENDADA (estado `scheduled`), **NÃO faz
   hold nem débito**, e responde ao cliente com o aviso "agendada para DD/MM;
   garanta saldo disponível na data". Se data inválida/passada/fim de
   semana/feriado → rejeita na hora com motivo legível (usa a lógica pura de
   `SettlementSchedule.decide`, que pode migrar para o Core ou ficar como regra
   compartilhada).
2. **Core (executor na data, molde do ScheduledPixExecutor)** — um cron roda na
   abertura da grade STR (~8h BRT) e pega as TEDs agendadas com
   `settlement_date <= hoje`. Para cada uma: confere saldo
   (`Wallet.get_balance` + `BalanceGuard`); se OK → `FundHold.hold_funds` +
   débito + publica `transfer_request` para a cabine (envio imediato, é a data);
   se INSUFICIENTE → rejeita `insufficient_balance` às 8h + notifica o cliente
   (webhook/extrato). Nunca deixa a TED agendada "presa": ou envia, ou rejeita
   por saldo na data.
3. **Cabine** — não precisa agendar nada: recebe o `transfer_request` só na data
   (quando o executor do Core dispara) e envia como uma TED imediata. O
   `ScheduledMessageProcessor` da cabine NÃO entra neste fluxo.

**O que muda vs. o que eu tinha registrado:**
- O `SettlementSchedule.decide` da cabine vira (no máximo) validação de data na
  submissão; o AGENDAMENTO e o DÉBITO DIFERIDO vivem no Core (onde está o
  ledger), espelhando o PIX agendado.
- **A preocupação com o `StaleHoldChecker` DESAPARECE:** como não há hold durante
  a janela de agendamento, não há hold para expirar. O hold só existe na
  execução (8h → envio), igual a uma TED normal — o watchdog não precisa
  conhecer `scheduled`.

**Risco:** médio. O ponto sensível é a conferência de saldo + rejeição por
insuficiência às 8h e a idempotência do executor (não executar a mesma TED
agendada duas vezes) — ambos já resolvidos no `ScheduledPix` para copiar.

**Plano de teste:** (a) TED futura NÃO faz hold nem débito na criação (saldo
intacto) e responde com aviso; (b) executor na data com saldo → hold+débito+
envia; (c) executor na data SEM saldo → rejeita `insufficient_balance` +
notifica, sem hold; (d) data inválida/passada/não-útil → rejeita na submissão;
(e) idempotência: rodar o executor 2x não duplica o envio.

---

## B. Outbox cross-repo (job commita fora do rollback da transação)

**Problema:** no `CoreEventProcessor`, `Publisher.publish_async` usa o Oban do
`Shared.Repo`, mas a transação é do `SettlementService.Repo`. O job
(`transaction.created`, `outbound.send`) commita independente do rollback da
linha `messages`. Um rollback após o enqueue pode deixar `transaction.created`
publicado para uma tx que não existe. (Mesma classe do que corrigi na queima de
E2E, que passei a rodar no repo da transação.)

**Correção proposta:** enfileirar o job de outbox no MESMO repo da transação
envolvente — nomear a instância Oban do `SettlementService.Repo`
(`SettlementService.Oban`) e fazer o `Publisher.publish_async` do money-path do
processor inserir nela; OU mover o publish para depois do commit com um outbox
durável próprio do `SettlementService.Repo`. Alinhar o `RuntimeGuard` para
exigir que o repo do job == repo da transação.

**Risco:** ALTO. É o núcleo do padrão outbox; mexer errado quebra a atomicidade
de TODO o money-path. Exige revisão cuidadosa e migração coordenada.

**Plano de teste:** transação que faz rollback após o enqueue NÃO deixa job
publicado (prova por `oban_jobs`/pending); sucesso publica exatamente uma vez.

---

## C. `resolve_sender_ispb` / `local_instrument` — IMPLEMENTADO 2026-07-17 (parcial, por design)

**STATUS: IMPLEMENTADO com TDD (commit; deploy à parte).** `resolve_sender_ispb`
virou EXPLÍCITO e FAIL-CLOSED: função pura `classify_sender_ispb/4` resolve com
confiança (participante conhecido → indireto/liquidante, direto/próprio; fallback
ao participante direto = a Monetarie no SCD; ou a própria ISPB) e, sem resolução
confiável, devolve `{:error, :unresolvable_sender_ispb}` — o `do_payment_request`
rejeita ao Core (`SENDER_ISPB_UNRESOLVABLE`) ANTES de assinar/bloquear (fim do
"usa o ISPB as-is"). Testes: classificador 7/0 + regressão core_event_processor
70/0. **`local_instrument` NÃO foi alterado de propósito:** a inferência atual JÁ
segue a regra do design (chave→DICT, agência/conta→MANU via builder; QR→QRES/QRDN
via `qr_codes.resolve_local_instrument`, validada pelo `Pacs008SendValidator`).
Mudar sem defeito = risco de money-path à toa. Segue AWAITING deploy (pix-api).

## C-original. `resolve_sender_ispb` / `local_instrument` por heurística

**Problema:** ISPB do remetente e instrumento de pagamento são resolvidos por
fallback (ISPB desconhecido → participante direto → ISPB cru; sem
`local_instrument` → infere DICT/MANU). Uma pacs.008 pode sair com ISPB/
instrumento "chutado" divergente da conta/consulta e ser barrada pelo BACEN.

**Correção proposta:** tornar a resolução explícita e fail-closed — o ISPB do
remetente vem do cadastro da conta/entity (não de fallback), e o
`local_instrument` vem do tipo de iniciação (chave→DICT, agência/conta→MANU,
QR→QRES/QRDN) validado contra a consulta. Sem resolução confiável → rejeição
clara ANTES de assinar, não uma pacs.008 divergente.

**Risco:** médio. Muda o valor que vai no fio; precisa de mapa exato dos casos
(participante indireto/direto, IP, entity) para não barrar envio legítimo.

**Plano de teste:** cada origem (direto, IP, entity) resolve o ISPB correto;
origem irresolvível rejeita com motivo; instrumento por tipo de iniciação.

---

## D. `event` ausente descartado em silêncio (payables/open_finance)

**Problema:** PIX de `payables`/`open_finance` sai sem o campo `event`; a cabine
descarta em silêncio (catch-all ACK). O pagamento some sem rastro.

**Correção proposta (baixo risco):** (1) Core — esses caminhos passam a incluir
`event: "payment_request"`; (2) cabine — o catch-all do `process_message` que
hoje dá ACK silencioso passa a logar em nível ERRO + telemetria/alerta (nunca
descartar um payload de money-path em silêncio).

**Risco:** baixo. É fail-fast/observabilidade, não muda o caminho feliz.

**Plano de teste:** payload sem `event` gera alerta (não ACK silencioso); os
caminhos payables/open_finance publicam com `event` e são processados.

---

## E. `transient?` catch-all trata desconhecido como transitório

**Problema:** no `CoreEventConsumer` (SPB), qualquer erro não catalogado é
tratado como transitório → NAK/retry até a DLQ. Um erro determinístico não
catalogado gera loop até esgotar tentativas. (Com o fix do duplo-débito desta
sessão, o rescue de exceção agora é suprimido do aviso ao Core — money-safe,
mas pode reter uma TED de falha permanente até a DLQ.)

**Correção proposta:** catálogo explícito de erros PERMANENTES (validação,
XSD, campos obrigatórios, constraint) que classificam como permanente →
rejeição imediata com motivo + terminalização; o catch-all transitório fica só
para o que é comprovadamente infra (conexão/timeout). Complementar: ao esgotar
retries (DLQ), publicar um evento terminal ao Core (a TED não pode ficar
`processing` para sempre após a DLQ).

**Risco:** médio. Errar a classificação de um erro real de infra como
permanente rejeitaria uma TED que só precisava de retry. Exige o catálogo
correto.

**Plano de teste:** cada erro catalogado (permanente vs transitório) roteia
para reject-imediato vs NAK; erro de infra segue transitório; pós-DLQ o Core é
avisado.

---

## Recomendação de ordem

1. **D** (baixo risco, observabilidade) — pode ir junto com os fixes já feitos.
2. **A** (sua decisão, fluxo novo) — incremento próprio, com o watchdog
   respeitando `scheduled`.
3. **E** (catálogo de permanência) — depois de A, junto com a terminalização
   pós-DLQ.
4. **C** (ISPB/instrumento) — precisa do mapa de origens.
5. **B** (outbox cross-repo) — o mais delicado; incremento isolado e revisado.
