# Defeito #7: telas de reserva (balance requests) com money-path LPI real

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans para implementar
> tarefa a tarefa. TDD RED primeiro em TODA tarefa. Commit local por tarefa. PUSH e
> DEPLOY somente com OK explicito do dono.

**Goal:** dar backend real as 3 telas de reserva do pix-admin (credito/debito/aporte),
com workflow de dupla aprovacao LSO/RSO na cabine PIX e emissao LPI0001-0004 real ao
BACEN delegada a cabine SPB (Opcao A), mantendo a operacao direta pela cabine SPB
disponivel e parametrizavel (Opcao B), e consertar o 500 do admin inject/adjust
mantendo o ajuste de espelho auditado (decisao do dono: ambos).

**Architecture:** a cabine PIX (spi_service) persiste a requisicao e o workflow
(PENDING_LSO -> PENDING_RSO -> PROCESSING -> COMPLETED/REJECTED), e ao aprovar RSO
publica um comando duravel via outbox no subject `monetarie.spb.lpi.request` (stream
MONETARIE_SPB). A cabine SPB consome, cria a operacao no rail vivo (o MESMO que ja
emite LPI0001 em PRD hoje: build -> assina HSM -> MQ -> R1/E), e publica o desfecho
em `monetarie.spb.lpi.result`, que a cabine PIX consome para fechar o status.
Paridade com o legado: SPI.Core.SPB.Application `MontaMsg.ConverteXML` +
`TrataRetornoIntegracao` (decompilado em `.scratch/legado-pix-decompiled/`).

**Tech stack:** Elixir/Phoenix umbrella (pix/backend: spi_service + settlement_service
+ shared), Elixir (spb/services/bacen_gateway), NATS JetStream (outbox pattern
ADR-008/009), Vue 3 (pix/frontend/admin), XSD v5.12 (`spb/.../priv/xsd/v512/{LPI,SME}`).

---

## Decisoes do dono (24/07, fechadas, nao re-perguntar)

1. A Monetarie e EMISSORA DE MOEDA ELETRONICA, JA TEM CCME e tem Conta de Liquidacao
   (RB e a unica conta que nao temos, e de bancos). Escopo = TODAS as pernas:
   LPI0001 (CL->PI), LPI0002 (CCME->PI), LPI0003 (PI->CL), LPI0004 (PI->CCME).
2. Arquitetura: Opcao A (workflow PIX delega emissao ao SPB) COM Opcao B disponivel,
   parametrizavel para escolha do cliente (operar direto pela cabine SPB).
3. Admin inject/adjust: AMBOS (consertar o 500 mantendo ajuste de espelho auditado E
   integrar ao fluxo de requisicoes).
4. Em PRD JA HA FLUXO LPI/SME real pela nossa cabine SPB (7 dias: LPI0001 x12 out com
   R1, LPI0006 x40 in, SME0001/2/3 com R1). Em producao nao existe teste.

## Verdades do legado (survey 24/07, fonte decompilada)

- Mapeamento (MontaMsg.ConverteXML, SPI.Core.SPB.Application):
  | Tela | Tipo requisicao | Mensagem | Campos chave |
  |---|---|---|---|
  | Credito | Propria/Liquidante | LPI0001 | NumCtrlIF, ISPBIF, ISPBPSPI, VlrLanc, DtMovto |
  | Credito | SME (= perna CCME) | LPI0002 | NumCtrlIEME, ISPBIEME, VlrLanc, DtMovto |
  | Debito | Propria/Liquidante | LPI0003 | NumCtrlPSPI, ISPBPSPI, ISPBIFCredtd, FinlddLPI, VlrLanc, DtMovto |
  | Debito | SME (= perna CCME) | LPI0004 | NumCtrlPSPI, ISPBPSPI, VlrLanc, DtMovto |
- Para a Monetarie (participante direto, propria): ISPBIF = ISPBPSPI = ISPBIFCredtd =
  46026562. FinlddLPI default 1; finalidade "devolucao de aporte indevido" exige
  NumCtrlSTROr e dispensa Grupo Conta Creditada (RLPI0005 do XSD).
- Status legado (TrataRetornoIntegracao): EntradaDoRegistro -> AguardandoRetorno
  (integracao OK, DtHrEnvio setado) / PendenteEnvio (nao executada) /
  ErroNaIntegracaoComSPB / Rejeitada; retorno R1/E fecha via TrataRetornoVerificacao.
  Historico completo por transicao (XML incluso). Correlacao: NumCtrlIF = MessageId =
  UniqueId; Protocolo do SPB persistido.
- RLPI0003 (XSD): sem saldo = rejeicao IMEDIATA, sem fila de pendencia. O desfecho de
  erro e sincrono e a tela precisa exibi-lo.
- Motor automatico legado (ChecaLimites, SPI.Core.Infrastructure:3795+): flags
  ReqCrdSaldoAuto/ReqDebSaldoAuto + MinimoReservaSPI/MaximoReservaSPI sobre camt.060;
  modo MetaSaldo calcula valor para trazer o saldo a meta; guarda ExisteLPIAberta por
  CdMsg no dia. FICA PARA FASE 2 (flags OFF por default) - nao bloqueia o defeito.
- Cancelamento legado: so LPI0001/0003 "aguardando retorno", e era escrituracao local
  (nao integrava). NOSSA regra: cancelar apenas em PENDING_LSO/PENDING_RSO (antes do
  dispatch); depois do envio nao ha mensagem BACEN de cancelamento de LPI.

## Estado atual (survey 24/07)

- Frontend pix-admin: 3 telas prontas atras de `VITE_BALANCE_REQUEST_FLOW_ENABLED`
  (OFF). Contrato: CreateRequestPayload {type CREDIT|DEBIT|APORTE, amount,
  justification, urgency, targetAccount, ...}; status PENDING_LSO/PENDING_RSO/
  PROCESSING/COMPLETED/REJECTED/CANCELLED; approve/reject com approvalLevel LSO|RSO.
  `AccountType = 'RB' | 'PI'` esta ERRADO (corrigir para CL e CCME).
- Gateway (settlement) `spi_proxy_controller.ex:536-620`: rotas completas com
  gambiarras (list chama /pending; get filtra em memoria). Corrigir para proxy real.
- spi_service: NAO tem POST /balance/requests nem approve/reject; GET /requests/pending
  devolve BLOCKS (`balance_controller.ex:307`). Admin inject/adjust 500 com codigo
  existente (`admin/balance_controller.ex` -> `SpiService.Balances.update_balance`);
  causa exata a reproduzir com teste RED (candidatos: EventPublisher raise dentro da
  Repo.transaction; tabela monetarie_spi.balances ausente em runtime).
- Cabine SPB: rail COMPLETO e VIVO (operations create -> send_to_clearing -> HSM ->
  MQ; LifecycleEngine processa R1 + tarifa; `messages/lpi/lpi0001..0008*.ex`;
  `post_integration/specific_handlers/lpi_handler.ex`). Ponte NATS request/reply ja
  existe para consulta (`spb.balance.request`, BacenGateway.Nats.BalanceResponder);
  comando com dinheiro sera DURAVEL (JetStream + outbox), nao request/reply.

## Contratos novos

- Tabela `monetarie_spi.balance_requests`: id uuid pk, request_type text
  (CREDIT|DEBIT), account_leg text (CL|CCME), amount :decimal em REAIS (Task 1
  PROVOU: monetarie_spi usa :decimal reais em Transaction.amount e Balances; XML
  VlrLanc = reais 2 casas), finalidade_lpi smallint default 1, num_ctrl_str_or text
  null (devolucao de aporte), lpi_code text (derivado, LPI0001..LPI0004), num_ctrl
  text NULL (Task 1 PROVOU: o rail SPB gera o NumCtrl no build, generate_num_ctrl_if;
  correlacao PIX<->SPB e por request_id e o num_ctrl volta no result), dt_movto date,
  status text (PENDING_LSO|PENDING_RSO|PROCESSING|COMPLETED|
  REJECTED|CANCELLED|INTEGRATION_ERROR), justification text, urgency text,
  requested_by uuid + requested_by_name, lso_approved_by/at, rso_approved_by/at,
  rejection_reason, protocolo text null, result_code text null, result_detail text
  null, inserted_at/updated_at.
- Tabela `monetarie_spi.balance_request_events`: request_id fk, at, status, actor,
  detail (historico por transicao, paridade SpiHistReqSaldo).
- Subjects NATS: comando `monetarie.spb.lpi.request` (payload: request_id, lpi_code,
  amount, dt_movto, num_ctrl, finalidade_lpi, num_ctrl_str_or, ispb=46026562),
  desfecho `monetarie.spb.lpi.result` (request_id, outcome ok|error, protocolo,
  result_code, detail). Ambos duraveis (JetStream), dedup por msg_id=request_id,
  publicados via outbox (`Shared.Nats.Publisher.publish_async` dentro de
  Repo.transaction no PIX; equivalente do SPB no result).
- Parametro Opcao A/B: env `BALANCE_REQUEST_EXECUTION_MODE` = `pix_workflow`
  (default; telas operam) | `spb_direct` (telas read-only + aviso apontando a cabine
  SPB). Exposto ao frontend via endpoint de config existente do gateway.
- Mapeamento leg -> mensagem (unico ponto de verdade, modulo novo
  `SpiService.BalanceRequests.LpiMapping`):
  CREDIT+CL -> LPI0001; CREDIT+CCME -> LPI0002; DEBIT+CL -> LPI0003 (FinlddLPI);
  DEBIT+CCME -> LPI0004. Tela APORTE = alias de CREDIT (mesma perna; o nome fica na
  UI; motor automatico por meta = fase 2).

## Tarefas (TDD, RED primeiro em todas)

### Task 1: fixar escala monetaria e gerador NumCtrl (leitura, sem codigo novo)
- Ler `Shared` money helpers (grep MoneyUnit/base_units em pix/backend/apps/shared) e
  1 endpoint vivo de balance para provar a escala; ler o gerador NumCtrlIF de TED
  (memoria monetarie-qr-cert-audit-tednumctrlif-0720: 20 chars) e decidir reuso.
- Saida: nota no topo da migration com a escala provada. Sem commit proprio.

### Task 2: migration + schemas + contexto BalanceRequests (spi_service)
- Create: migration em pix/backend/apps/shared/priv/repo/migrations (mesmo home das
  demais), `balance_requests` + `balance_request_events` conforme contrato.
- Create: `apps/spi_service/lib/spi_service/balance_requests/balance_request.ex`
  (schema), `.../balance_request_event.ex`, `.../lpi_mapping.ex`,
  `apps/spi_service/lib/spi_service/balance_requests.ex` (contexto: create_request/2,
  list_requests/1 com filtros status/type/paginacao, get_request/1, pending/0).
- Test: `apps/spi_service/test/spi_service/balance_requests_test.exs`.
- Passos: escrever testes de create (validacoes: amount > 0, leg valido, lpi_code
  derivado certo nas 4 pernas, num_ctrl 20 chars unico, evento EntradaDoRegistro
  gravado) -> rodar RED -> implementar -> GREEN -> commit.

### Task 3: maquina de estados (aprovacoes LSO/RSO, reject, cancel)
- Modify: contexto BalanceRequests: approve/3 (level LSO: PENDING_LSO->PENDING_RSO;
  level RSO: PENDING_RSO->PROCESSING + dispatch Task 4), reject/3 (qualquer
  PENDING_* -> REJECTED com motivo), cancel/2 (so PENDING_* -> CANCELLED).
- Regras RED: aprovar RSO sem LSO = erro; aprovador RSO != aprovador LSO (4 olhos);
  transicao invalida = {:error, :invalid_transition}; todo passo grava evento.
- Commit.

### Task 4: dispatch outbox PIX -> SPB + consumer de resultado (PIX)
- Modify: approve RSO publica `monetarie.spb.lpi.request` via
  `Shared.Nats.Publisher.publish_async(subject, payload, msg_id: request_id)` DENTRO
  da MESMA Repo.transaction que muda o status (outbox, ADR-008/009).
- Create: consumer duravel do `monetarie.spb.lpi.result` no spi_service Workers
  (padrao dos consumers existentes): ok -> COMPLETED (+protocolo/result_code);
  error -> REJECTED ou INTEGRATION_ERROR (integracao falhou; permitir re-dispatch
  admin). Idempotente por request_id + status atual.
- Tests: outbox atomicity (OutboxTestHelper), consumer com payloads ok/erro/duplicado.
- Commit.

### Task 5: rotas + controller REST no spi_service
- Modify: `apps/spi_service/lib/spi_service_web/router.ex` scope /balance: POST
  /requests, GET /requests, GET /requests/:id, POST (e PUT) /requests/:id/approve,
  POST (e PUT) /requests/:id/reject, POST /requests/:id/cancel; GET /requests/pending
  passa a devolver REQUESTS pendentes (blocks continuam so em /balance/blocks).
- Create: `apps/spi_service/lib/spi_service_web/controllers/balance_request_controller.ex`
  (render no shape que o frontend normaliza: campos camelCase/snake ok, ver
  normalizeRequest em services/balance.ts).
- Test: `apps/spi_service/test/spi_service_web/controllers/balance_request_controller_test.exs`
  (RED por 404 hoje; happy paths + 404/422/alcada; UUID invalido = 404).
- Commit.

### Task 6: gateway proxies reais (settlement_service)
- Modify: `spi_proxy_controller.ex` list_balance_requests -> GET /api/v1/balance/requests
  com query params; get_balance_request -> GET /:id direto; pending -> /requests/pending;
  create/approve/reject/cancel repasse direto; remover normalize_balance_requests se
  o shape novo ja atende o frontend. Router: adicionar POST /balance/requests/:id/cancel.
- Test: testes de proxy existentes do gateway (padrao dos outros proxies, com
  InternalClient mockado).
- Commit.

### Task 7: admin inject/adjust (defeito do 500) - AMBOS
- RED: teste de controller Admin.BalanceController inject/adjust reproduzindo o 500
  (rodar contra o codigo atual, capturar a causa REAL: candidato 1 EventPublisher
  raise dentro da transaction; candidato 2 tabela balances; NAO chutar, provar).
- Fix mantendo ajuste de espelho: operacao auditada (evento + motivo obrigatorio),
  erro vira 422 honesto, nunca 500. + parametro opcional `as_request=true` que cria
  uma balance_request (integra ao fluxo; decisao "ambos").
- Nota de honestidade: espelho e reconciliado por camt.060; ajuste manual e correcao
  operacional, nao verdade BACEN.
- Commit.

### Task 8: cabine SPB - consumer do comando + emissao no rail vivo + result
- Create: `spb/services/bacen_gateway/lib/bacen_gateway/nats/lpi_command_consumer.ex`
  (consumer duravel de `monetarie.spb.lpi.request`): valida payload, DEDUP por
  request_id (unique no banco), cria operacao/mensagem LPI pelo MESMO caminho do
  operations_controller create+send_to_clearing (extrair funcao interna reutilizavel,
  nao duplicar), correlaciona NumCtrl.
- Modify: no ponto de pos-integracao do R1/E de LPI (lifecycle/lpi_handler), publicar
  `monetarie.spb.lpi.result` para operacoes originadas por request (carregar
  request_id da operacao).
- Test: consumer test (payload valido cria operacao 1x, duplicado nao duplica,
  invalido rejeita sem crash) + XML das 4 pernas contra XSD v512 (padrao
  lpi0001_test.exs) para o nosso ispb proprio + result publicado no R1 simulado.
- Commit.

### Task 9: parametro Opcao A/B
- Env `BALANCE_REQUEST_EXECUTION_MODE` (runtime.exs do spi_service + exposto no
  gateway config): `pix_workflow` default; `spb_direct` faz POST /balance/requests
  retornar 409 com aviso claro e o frontend mostrar a tela read-only apontando a
  cabine SPB. Testes dos dois modos. Commit.

### Task 10: frontend pix-admin
- Modify: `services/balance.ts` AccountType 'RB'|'PI' -> 'CL'|'CCME' (+labels i18n 5
  idiomas); telas Credit/Debit ganham seletor da perna (CL default, CCME opcional);
  LiquidityInjectionView = atalho de CREDIT (rotulo aporte); cancel action;
  exibir rejeicao sincrona (RLPI0003) e INTEGRATION_ERROR.
- `useBalanceRequestFlow` passa a ler o modo A/B do config do gateway (alem da env).
- vue-tsc 0 + vitest verdes. Commit. (Flag VITE_BALANCE_REQUEST_FLOW_ENABLED=true e
  mudanca de BUILD/deploy: SO com OK do dono.)

### Task 11: suites completas + fecho
- mix test spi_service, settlement_service, shared; suite spb bacen_gateway; vitest +
  vue-tsc. Atualizar gap register (#7 -> corrigido), handoff e memoria. Commits
  locais; propor push/deploy ao dono (HML primeiro, validacao viva, depois PRD com
  digest match) e validacao visual regra #11.

## Riscos e guardas

- Money-path: TODA publicacao NATS do fluxo dentro de Repo.transaction via
  Publisher.publish_async (Credo OutboxAtomicity pega desvio).
- Nao tocar no caminho vivo de LPI do SPB alem de extrair funcao + publicar result
  (em PRD ha fluxo real; mudancas minimas e aditivas).
- Unidades: provar a escala na Task 1 antes de qualquer conversao (contrato
  monetarie-money-unit-scale-contract).
- Deploy: nada sobe sem OK explicito; HML valida vivo antes de PRD.
