# Design: REDA outbound (participante indireto) - 2026-06-29

Frente: Matriz de cobertura PIX/SPB. Gap escolhido: REDA outbound operacional
(reda.014 registrar, reda.022 atualizar responsaveis, reda.031 descadastrar
participante indireto) com correlacao da resposta reda.016. Full-stack: backend
(controller + envio assinado + correlacao + testes) e UI admin Vue.

Regra de continuidade do projeto: nao inferir. BACEN e verdade. Comprovar
empiricamente. pt-br sem travessao de IA. Estado vivo canonico em
docs/handoff/CURRENT.md e CLAUDE.md.

## 1. Contexto e objetivo

Permitir que a Monetarie (participante direto, ISPB 46026562) gerencie no BACEN os
participantes indiretos que representa: registrar, atualizar responsaveis e
descadastrar, lendo a resposta assincrona reda.016 e refletindo o estado real na
tela (nada otimista).

## 2. Estado atual verificado (empirico, file:line)

- Builders ja existem em `apps/shared/lib/shared/bacen/iso20022/message_builder.ex`:
  `build_reda014/1` (~659, exige `indirect_ispb` + `indirect_cnpj`), `build_reda022/1`
  (~1090, `party_ispb` + lista `modifications` contact/director/tech_address/cpf),
  `build_reda031/1` (~1305, exige `target_ispb`, raise se faltar). `build/2` (~56-141)
  monta AppHdr com `to_ispb` default BACEN `00038166` e `MsgDefIdr=reda.0xx.spi.<versao>`.
- Inbound reda.016/017/041 ja tratado em
  `apps/spi_service/lib/spi_service/workers/inbound_processor.ex`:
  `process_party_status_advice/2` (~1127) grava em `bacen_inbound` e faz broadcast
  `party.status.advice` com `original_msg_id`. Chave de correlacao = `original_msg_id`
  (de `OrgnlMsgId`, parser em message_parser.ex ~337) == nosso `message_id` (BizMsgIdr).
- Envio assinado: assinatura XMLDSig fail-closed em
  `apps/spi_service/lib/spi_service/workers/outbound_sender.ex` (`sign_outbound_xml/2`
  ~249, `XmlSigner.sign_spi/2`). Roteamento de canal:
  `apps/shared/lib/shared/bacen/channel_router.ex` (`route/1` ~113): REDA e nao-financeira
  -> canal secundario (CSM). Persistencia outbound: schema
  `apps/shared/lib/shared/schemas/spi/bacen_outbound.ex` (PK message_id + send_time).
  O dispatch generico de `/api/v1/messages/send` (message_controller.ex ~664,
  `Client.spi_request`) NAO necessariamente assina; por isso REDA deve reusar o caminho
  assinado de pacs.008/camt.060. O seam exato de enfileiramento sera confirmado no plano
  lendo como pacs.008/camt.060 enfileiram + assinam (sem inferir agora).
- Dados: existe `monetarie_settlement.participants` (writable, tem participant_type incl
  INDIRECT_PARTICIPANT) e `monetarie_spi_ref.bacen_pix_participants` (read-only, diretorio
  BACEN). NAO existe tabela para "indiretos que NOS gerenciamos" -> migracao nova.
- Padrao admin backend: scope `/api/v1/admin`, pipeline `[:gateway, :api,
  :gateway_authenticated]`, `Shared.Plugs.RequirePermission` por acao, respostas RFC7807
  via `send_problem`. Exemplo canonico:
  `apps/settlement_service/lib/settlement_service_web/controllers/admin/scheduled_messages_controller.ex`.
- Padrao frontend: axios em `src/services/*.ts` (api com CSRF + ISPB), telas em
  `src/views/**` (ex. RecurrenceListView.vue), nav em `src/components/layout/Sidebar.vue`,
  i18n em `src/locales/pt-BR.json`. (PIX admin NAO usa TanStack/composables como padrao.)
- Paridade legado (autorizado): `LegadoPIX/_decompiled/.../REDAController.cs`:
  `regPartIndireto` (reda.014: IspbDebtor, CnpjParticipante[14], NmParticipante[100]),
  `atualizaResponsaveis` (reda.022: Contato{Fone*, Email*, Celular, Fax},
  Diretor{Nome*, Fone*, Email*, Celular}, TecAddr[8 alfanumerico]*, CpfDiretorResp[11]*),
  `desRegPartIndireto` (reda.031: IspbParticipanteIndireto). Idempotencia legada por
  (IdSystem, UniqueId). Entidade legada `SpiPartIndireto` (IdInstFinanc, DsCnpjParticipante
  PK, NmParticipante, IcAtivo).

## 3. Modelo de dados

Migracao nova: `monetarie_settlement.indirect_participants`.
- `indirect_ispb` (8, unico), `indirect_cnpj` (14), `name` (<=100)
- `status`: PENDING -> ACTIVE | REJECTED (via reda.016); DEREGISTERING -> INACTIVE
- `client_request_id` (idempotencia, espelha UniqueId legado; unico)
- correlacao: `last_request_msg_id` (nosso BizMsgIdr), `last_response_msg_id`,
  `last_status_reason`
- responsaveis (reda.022): `director_name`, `director_cpf`, `director_phone`,
  `director_email`, `contact_phone`, `contact_email`, `tech_keyword`
- timestamps + `registered_at`, `deregistered_at`

Decisao: tabela dedicada (nao reusar `participants`), pois precisamos do ciclo de vida
do que gerenciamos, correlacao de msg_ids e campos de responsavel.

## 4. Backend: API

Gateway `settlement_service`, scope `/api/v1/admin`, RequirePermission por acao:
- GET `/reda/indirect-participants` (lista + filtro/paginacao)
- POST `/reda/indirect-participants` -> reda.014
- GET `/reda/indirect-participants/:id`
- PUT `/reda/indirect-participants/:id` -> reda.022
- DELETE `/reda/indirect-participants/:id` -> reda.031
- GET `/reda/indirect-participants/:id/history`

Contratos (paridade legado, validacao fail-closed):
- create: `indirect_ispb`(8), `indirect_cnpj`(14, valida digitos), `name`(<=100),
  `client_request_id`. Recusa duplicado por ISPB.
- update: `contact{phone*, email*, mobile, fax}`, `director{name*, phone*, email*, mobile}`,
  `tech_keyword` [A-Za-z0-9]{8}*, `director_cpf`(11)*.
- deregister: por `:id`; `target_ispb` da linha; exige status ACTIVE.

Dominio `Shared.Reda` (espelha `Shared.Automation`): list/get/create/update/deregister +
transicoes de estado + idempotencia.

## 5. Backend: envio assinado

`Shared.Reda` monta params -> `MessageBuilder.build("reda.0xx", ...)` e reusa o mesmo
caminho de saida assinado de pacs.008/camt.060 (XMLDSig no HSM, fail-closed; canal
secundario/CSM; persiste `bacen_outbound`). Nunca o dispatch generico sem assinatura.
Grava `message_id` (BizMsgIdr) na linha; status PENDING (ou DEREGISTERING no 031). Seam
exato de enfileiramento confirmado no plano.

## 6. Backend: correlacao reda.016

Estender `process_party_status_advice` (inbound_processor.ex ~1127): alem do broadcast
atual, buscar a linha por `original_msg_id` (= nosso `message_id`) e transicionar:
sucesso -> ACTIVE (014/022) | INACTIVE (031); rejeicao -> REJECTED + `last_status_reason`
(de `StsRsn/Rsn/Prtry`).

History sem tabela nova: derivada de `bacen_outbound` + `bacen_inbound` casados por msg_id
+ campos de status da linha.

## 7. Frontend (Vue 3)

Todas as strings via i18n (zero hardcode), acentuacao pt-br, sem travessao, tema dark com
as CSS vars existentes.
- `src/services/reda.ts` (axios): list/getById/create/update/deregister/getHistory.
- `src/views/reda/RedaListView.vue`: tabela (ISPB indireto, CNPJ, nome, status badge, data),
  filtro por status, paginacao, botao "Novo participante indireto".
- `src/views/reda/RedaDetailView.vue`: form criar (reda.014) / editar (reda.022, validacao
  client-side espelhando backend); acao "Descadastrar" (reda.031) so em ACTIVE com
  confirmacao; seccao "Historico" lendo getHistory.
- Status (badges pt-br): PENDING=Em processamento, ACTIVE=Ativo, REJECTED=Rejeitado (com
  motivo), DEREGISTERING=Descadastrando, INACTIVE=Inativo. Nada "concluido" antes da reda.016.
- Roteamento `src/router/index.ts` (/reda, /reda/new, /reda/:id) + item no Sidebar (secção
  operacional, protegido por permissao) + chaves em pt-BR.json (nav.reda, reda.*).

## 8. Testes

- ExUnit: validacao XSD dos reda.014/022/031 montados (vs priv/xsd/spi/v5.12.1);
  `Shared.Reda` (transicoes, idempotencia, validacoes CNPJ/CPF/tech_keyword/email);
  correlacao reda.016 (inbound -> linha ACTIVE/REJECTED/INACTIVE); controller (401 sem auth,
  422 invalido, 201/200 happy path, descadastro so em ACTIVE).
- Frontend: specs vitest das telas; `pnpm type-check` + `pnpm build` verdes.

## 9. Criterios de sucesso

1. `mix compile --force` + ExUnit focado verdes; build/type-check do front verdes.
2. Deploy: imagens pix-api + pix-admin-ui, rollout ECS COMPLETED, conferido por digest.
3. Prova ponta a ponta (homolog, SO com autorizacao explicita do dono p/ envio vivo):
   registrar indireto pela UI -> reda.014 assinado no HSM + enviado no CSM -> reda.016
   recebida -> linha ACTIVE na tela; depois atualizar (022) e descadastrar (031).
   Screenshots + metadados, validador zero erro (regra #11). Sem autorizacao de envio vivo:
   sucesso = XML valido no XSD + dry-run + smoke de UI; prova viva adiada.

## 10. Fora de escopo (YAGNI)

Importacao em massa de indiretos; re-registro agendado; UI para reda.017/041 (inbound ja
logado); tabela de eventos separada; multi-instituicao. SGCT/PIX Automatico segue diferido.

## 11. Guard-rails

REDA assinado fail-closed; reusar o caminho de assinatura provado de pacs.008/camt.060 sem
altera-lo; envio vivo so com autorizacao; nada de segredo em log/commit.
