# Arquitetura da Cabine SPB (Monetarie SCD, ISPB 46026562)

Data: 2026-07-10. Mapeamento por leitura direta do código em `/Users/luizpenha/monetarie/spb/` e `/Users/luizpenha/monetarie/infra/aws/greenfield/`. Salvo indicação em contrário, caminhos de módulos Elixir são relativos a `/Users/luizpenha/monetarie/spb/services/bacen_gateway/`.

## 1. Visão geral

A cabine SPB troca mensagens do catálogo SFN (STR, GEN, LDL, PAG, SEL, RDC, DDA e demais grupos, catálogo v5.12) com o BACEN via IBM MQ da RTM, assina e decifra no HSM RTM, persiste em Aurora PostgreSQL (`mon_spb`) e integra com o Core por NATS JetStream. Um único serviço Elixir/Phoenix concentra tudo: o **`bacen_gateway`** (imagem ECR `monetarie/spb-api`). O diretório `spb/services/` contém outros serviços da fase de microsserviços (api_gateway, transaction_service, cash_service, settlement_service, forex_service, securities_service, extract_service, message_processor, user_management), que não são deployados no ECS: `infra/aws/greenfield/ecs.tf:28-351` só define `spb-api` e `spb-admin-ui` para o SPB.

Componentes vivos:

| Componente | Diretório | O que é |
|---|---|---|
| spb-api (bacen_gateway) | `spb/services/bacen_gateway` | Phoenix/Bandit, ~100+ endpoints REST, pipeline SFN completo, porta 4010 |
| spb-mq-sidecar | `spb/services/bacen_gateway_mq_sidecar` | Java 21 + IBM MQ allclient (JMS), ponte HTTP:9080 ↔ IBM MQ RTM, mesma task ECS do spb-api |
| spb-admin-ui (frontend-vue) | `spb/frontend-vue` | Vue 3 + TypeScript + Vuetify 3, portal admin/operador, porta 8080 |
| simulator (+ simulator-frontend) | `spb/simulator`, `spb/simulator-frontend` | Phoenix que simula o BACEN (cenários, filas simuladas); roda fora do ECS greenfield |

Supervisão do `bacen_gateway` (~40 workers): `lib/bacen_gateway/application.ex:51-242`. Inclui Repo, PrometheusExporter, Cache Redis, Gnat (NATS), StreamManager, CertificateRegistry, CertMonitor, DomainManager, MQBridge, ReferenceData (ETS com ~20k regras de validação), engines de validação/status/sanções/alçada, MessageProcessor, os consumers JetStream, STR.Supervisor (grade, correlação, fluxo), Liquidante (spliceado por flag, `application.ex:173`), AlertEngine, sweepers, Outbox.Worker, MQResponsePoller, CCS Scheduler, FileTransfer Orchestrator e o Endpoint Phoenix.

## 2. Diagrama de componentes

```mermaid
flowchart LR
  subgraph Core["Core (core-api)"]
    CORE[Monetarie Core]
  end

  subgraph NATS["NATS JetStream (3 nos)"]
    S1[(BACEN_OUTBOUND)]
    S2[(BACEN_INBOUND)]
    S3[(BACEN_DLQ)]
    S4[(BACEN_EVENTS)]
    S5[(MONETARIE_CORE)]
    S6[(MONETARIE_SPB - criado pelo Core)]
  end

  subgraph Task["Task ECS spb-api"]
    subgraph GW["bacen_gateway :4010"]
      API[Router + Controllers]
      CEC[CoreEventConsumer]
      LE[LifecycleEngine + PostIntegration]
      OB[Outbox.Worker - pending_publish_events]
      PACK[MessagePacker C14/C15 header 588B]
      DM[MQ.DomainManager]
      MB[MQBridge + MQConsumer]
      CN[NATS.CoreNotifier]
      WS[Phoenix Channels /monitoring]
    end
    SC[spb-mq-sidecar Java :9080]
  end

  UI[spb-admin-ui Vue :8080] -->|REST /api + WebSocket| API
  DB[(Aurora mon_spb)] --- GW
  HSM[HSM RTM EcosCryptoServer :443] ---|sign C15 / decrypt C14| PACK

  CORE -->|monetarie.core.spb.* em S5| CEC
  CN -->|monetarie.spb.transactions.* em S6| CORE
  OB -->|nats_publish / mq_send| S1 & S2 & S4
  DM -->|POST /ibmmq/rest/v2/messaging| SC
  SC -->|PUT QR.REQ.46026562...| MQ[(IBM MQ RTM SPB01 :1514 / MES01 :12522)]
  MQ -->|poll QL.RSP.00038166.46026562...| SC
  SC -->|POST /api/mq/webhook/:domain| MB
  MQ === BACEN[BACEN RSFN]
```

## 3. Pipeline de mensagens

### 3.1 Construção e parse do XML SFN

- Cerca de 1.570 módulos gerados de mensagem em `lib/bacen_gateway/messages/<grupo>/` (1.587 arquivos `.ex` no diretório, dos quais 17 são de infraestrutura), ex. `str/str0008.ex`, cada um com `message_code/0`, `category/0`, `build/1` e `response_types/0`. O grupo STR sozinho tem 143 módulos (base + R1/R2/R3 + E).
- Registro/roteamento: `lib/bacen_gateway/messages/registry.ex` (36 categorias; grupos fictícios BVF/CBL/CLI/HDT/RGT/SCG e o proprietário BMA foram removidos, `registry.ex:9-16`).
- Envelope DOC/BCMSG/SISMSG/USERMSG: `lib/bacen_gateway/messages/bcmsg_envelope.ex`.
- Validação em camadas: `Messages.Validator` + `SPB.ReferenceData` (ETS com as ~20.639 regras de `bacen_validation_rules`, `application.ex:99-103`), `Validation.XSDValidator` (XSDs em `priv/xsd/`, 31 diretórios de grupo) e `Engines.MessageTagValidator`.
- Números de controle: `NumCtrlIF` sequencial por ISPB+data com advisory lock e contador em `num_ctrl_if_seq` (`lib/bacen_gateway/str/num_ctrl_if.ex:57-89`); NUOp por ISPB+data+domínio (`:118`).

### 3.2 Prefixo de controle MON (`BACEN_CONTROL_PREFIX`)

O protocolo exibido/gerado pela tela de envio usa `<prefixo 3 letras><AAAAMMDD><9 dígitos aleatórios>`: `lib/bacen_gateway_web/controllers/messages_controller.ex:1690-1717` (`control_prefix/0` lê `BACEN_CONTROL_PREFIX`, sanitiza para 3 chars maiúsculos, default `CEC`; em produção a env define `MON`). O mesmo prefixo é usado nas consultas agendadas (`lib/bacen_gateway/queries/scheduled_query_executor.ex:924` e `scheduled_query_catalog.ex:497`).

### 3.3 Assinatura e decifragem (HSM RTM)

Criptografia de fio conforme Manual de Segurança do SFN v5.07 (não é XMLDSig; é o header binário de 588 bytes):

- `lib/bacen_gateway/crypto/message_packer.ex:1-38`: emissão = SHA-256 do XML → assinatura RSA PKCS#1 v1.5 (**C15**, 256B) → chave simétrica de sessão (v3: AES-256-GCM, chave 32B + IV 12B; v2: 3DES) → cifra da chave com o certificado público do destinatário (**C14**, 256B) → header 588B (`crypto/header_bacen.ex:10`) + corpo cifrado. Recepção = decifra C14 com nossa chave privada, decifra o corpo e verifica a assinatura C15 fail-closed.
- HSM: `lib/bacen_gateway/crypto/rtm_hsm.ex:1-18`: assinatura C15 e decifra do C14 inbound são delegadas ao EcosCryptoServer da RTM quando `RTM_HSM_ENABLED=true` (contrato RSAHASH: envia o hash cru, o HSM aplica DigestInfo + padding). A cifragem C14 de saída usa só o certificado público do destinatário (não precisa de chave privada). Sessões cacheadas em ETS com TTL 5min (`rtm_hsm.ex:29-31`).
- Certificados: `Crypto.CertificateRegistry` (ETS no boot; nosso cert público via `BACEN_CERT_PEM_FILE`, `certificate_registry.ex:313`), `BacenCertStore`/`bacen_domain_certificates` (cert do BACEN por domínio, cifra C14), `OurCertStore`/`our_signing_certificates` (nossa identidade T010/P001, UIDs de chave no HSM, ativação via GEN0006), `institution_certificates` (verificação de assinatura inbound por serial).

### 3.4 IBM MQ (sidecar) e filas

- Envio: `MQ.DomainManager` (GenServer) empacota via `MessagePacker.pack` e faz `POST {IBM_MQ_SIDECAR_URL}/ibmmq/rest/v2/messaging/qmgr/{qm}/queue/{queue}/message` (`mq/domain_manager.ex:621-680`). Filas: envio `QR.REQ.{ISPB_LOCAL}.{ISPB_BACEN}.{seq}`, recepção `QL.RSP.{ISPB_BACEN}.{ISPB_LOCAL}.{seq}` (`domain_manager.ex:21-22,1012-1013`).
- Sidecar Java (`spb/services/bacen_gateway_mq_sidecar/src/.../Sidecar.java`): HTTP `:9080` (`Sidecar.java:100`), endpoints `/healthz`, `/readyz`, `/statusz` e o espelho da REST API de messaging (`:152-158`). QMs por domínio: SPB01 → `QM.46026562.01` porta 1514, MES01 → `QM.46026562.02` porta 12522 (`Sidecar.java:105-131`); canal `APP.SVRCONN` via env. Poller lê as filas `QL.{REQ,RSP,REP,SUP}.00038166.46026562.*` a cada `POLL_INTERVAL_MS` e entrega por webhook HMAC (`X-Webhook-Signature`) em `POST /api/mq/webhook/{domain}` (`Sidecar.java:359-600`), com commit JMS somente após 2xx.
- Recepção no Elixir: `MQWebhookController.receive/2` (`lib/bacen_gateway_web/controllers/mq_webhook_controller.ex:11`) → `MQ.MQBridge.handle_webhook/3` (`mq/mq_bridge.ex:151`) → `MQ.MQConsumer.process_message/3` (`mq/mq_consumer.ex:63`: unpack do header 588B, verificação C15, extração de `<CodMsg>`) → `MessageProcessor.receive_from_bacen/1`.
- Poison messages: acima de `SIDECAR_POISON_MAX_DELIVERIES` (default 5) o sidecar posta em `/api/mq/webhook/:domain/quarantine` e a mensagem vai para `mq_quarantine` (`lib/bacen_gateway/mq/quarantine.ex:39`, migration `20260704120000`).

### 3.5 R-legs (R1/R2/R3) e roteiro de mensagens

- Cada módulo base declara suas variantes de resposta em `response_types/0`; a semântica operacional (R1/erro = resposta de status do envio, R2 = mensagem recebida/integrada) fica em `lib/bacen_gateway/messages/response_expectation.ex:1-28` e nos handlers `messages/response_handler.ex`.
- Correlação da resposta ao envio pai: `lib/bacen_gateway/spb/response_correlation.ex:33-42` casa por `control_number → nuop → correlation_id` com o tipo base via `regexp_replace(cd_msg,'(R[123]|E)$','')`.
- O "roteiro" data-driven vive nas tabelas: `message_type_config` (driver por tipo, 1.448 linhas, ETS via `Engines.MessageTypeDriver`), `message_flow_types` (O/A/C/R/E), `group_status_map` (495 regras de de-para de status por grupo), `post_integration_config` (flags financeiras e tags de valor) e `grade_message_map` + `bacen_operating_schedule` (+ overrides) para o enforcement de grade horária (`STR.GradeSchedule`).

### 3.6 Máquina de estados (operação vs mensagem)

Modelo de duas tabelas: `spb_operations` (pai, 1 por operação) e `bacen_messages` (filha, N por operação via `operation_id`). Ver o dicionário de dados (`docs/architecture/2026-07-10-dicionario-dados-spb.md`).

- Estados da operação (`lib/bacen_gateway/operations/spb_operation.ex:12-19`): `created`, `awaiting_approval`, `sent`, `sent_to_mq`, `processing`, `coa_received`, `cod_received`, `r1_confirmed`, `r1_error`, `r1_rejected`, `r2_confirmed`, `r2_rejected`, `r3_settled`, `cancelled`, `expired`, `failed`, `dead_letter`, `confirmed`.
- Transições por evento (`lib/bacen_gateway/lifecycle_engine.ex:2041-2053`): EMSG→created, EMQS→sent_to_mq, R0ACK→processing, COA→coa_received, COD→cod_received, RSPR1→r1_confirmed, RSPR2/RCRED→r2_confirmed, RSPR3→r3_settled, RSPER(8)→r1_rejected, RSPER(9)→r1_error, RSPER(11)→r2_rejected.
- Quem transiciona: `LifecycleEngine` (ex. `mark_sent_to_mq/1`, `lifecycle_engine.ex:154-195`) e `PostIntegration.Dispatcher.maybe_update_operation_status` (`post_integration/dispatcher.ex:429-455`). Sweepers: `Lifecycle.StaleOperationSweeper` (sent_to_mq preso > 60s → error `STALE_NO_MQ_ACK`) e `Lifecycle.StaleInboundSweeper` (inbound preso > 15min → failed).
- Agrupamento em operação: a operação é criada a partir da mensagem com `correlation_id = COALESCE(correlation_id, nuop, control_number)` (`lifecycle_engine.ex:97-132`). Observação de nomenclatura: o agrupamento por `NUMORIGEM` é do MODELO DE IMPORTAÇÃO do acervo AutBank (operação = `COALESCE(NUMORIGEMOR, NUMORIGEM)`, ver `/Users/luizpenha/monetarie/etl/legacy_hml_import/21_project_spb.sql:6`); no runtime da cabine o análogo é o trio correlation_id/nuop/control_number.

## 4. Fluxo TED-out (STR0008 + R1)

Origem: `lib/bacen_gateway/consumers/core_event_consumer.ex` (evento `transfer_request`, `:102`; despacho em `dispatch_real_str0008`, `:196-248`).

```mermaid
sequenceDiagram
  participant Core as Core (IB/core-api)
  participant N as NATS JetStream
  participant CEC as CoreEventConsumer
  participant LE as LifecycleEngine
  participant RD as RealDispatcher + MessagePacker
  participant HSM as HSM RTM
  participant SC as MQ Sidecar :9080
  participant MQ as IBM MQ RTM (SPB01)
  participant BC as BACEN STR

  Core->>N: publica monetarie.core.spb.> (transfer_request STR0008)
  N->>CEC: Gnat.sub monetarie.core.spb.> (core_event_consumer.ex:23,41)
  CEC->>CEC: STR0008Builder.build (core_integration/str0008_builder.ex)
  CEC->>LE: Messages.create_message + process_outgoing (cria spb_operations)
  CEC->>RD: RealDispatcher.dispatch (dispatch/real_dispatcher.ex:22)
  RD->>HSM: assina C15 (RtmHsm.sign, hash SHA-256)
  RD->>RD: cifra corpo AES-256-GCM + C14 p/ cert BACEN + header 588B
  RD->>SC: POST /ibmmq/rest/v2/messaging/qmgr/QM.46026562.01/queue/QR.REQ.46026562.00038166.xx/message
  SC->>MQ: JMS PUT (BytesMessage persistente, commit transacionado)
  MQ->>BC: entrega RSFN
  CEC->>LE: mark_sent_to_mq (state=sent_to_mq)
  CEC->>N: outbox monetarie.spb.transactions.accepted (core_event_consumer.ex:356)
  N->>Core: Core libera/registra o hold

  BC-->>MQ: STR0008R1 (resposta de status)
  MQ-->>SC: poll QL.RSP.00038166.46026562.xx
  SC->>CEC: POST /api/mq/webhook/SPB01 (HMAC)
  Note over CEC: MQWebhookController -> MQBridge -> MQConsumer<br/>(decifra C14 no HSM, verifica C15)
  CEC->>LE: MessageProcessor + InboundConsumer correlacionam R1<br/>(response_correlation.ex:33) e transicionam r1_confirmed
  LE->>N: CoreNotifier.notify_status_change -> monetarie.spb.transactions.r1_confirmed (nats/core_notifier.ex:27)
  N->>Core: Core atualiza o status da TED
```

Falhas em qualquer etapa publicam `monetarie.spb.transactions.rejected` com `error_reason` (`core_event_consumer.ex:330-368`), sempre via outbox (durável).

## 5. Fluxo TED-in (STR0008R2 chega e credita o Core)

O BACEN liquida e entrega o R2 ao recebedor; não existe pacs/aceite prévio no STR. ATENÇÃO: hoje existem DOIS emissores de crédito TED-in convivendo, ambos sem flag: o hook legado `monetarie.spb.transactions.settled` (`message_processor.ex:739` -> `core_notifier.ex:100`) e a ponte canônica `monetarie.spb.credits.inbound` (`lifecycle_engine.ex:1524`, outbox na mesma transação da operação). O Core consome os dois: o settled cai no catch-all `SpbHandler.handle_transaction_updated` (`spb_consumer.ex:93`), que para transação externa não rastreada chama `handle_transaction_ledger` e credita TB + extrato com referência NumCtrlIF/NUOp; o credits.inbound cai no handler dedicado `SpbInboundCreditHandler` (`spb_consumer.ex:66`), que credita TB + extrato com referência `num_ctrl_str` e árbitro de idempotência próprio (`spb_inbound_credits.num_ctrl_str` UNIQUE). Não foi encontrado dedup CRUZADO entre os dois caminhos: risco provável de crédito duplo na próxima TED-in orgânica. Ver `docs/reports/2026-07-10-ted-in-duplo-credito-verificacao.md`.

```mermaid
sequenceDiagram
  participant BC as BACEN STR
  participant MQ as IBM MQ RTM (SPB01)
  participant SC as MQ Sidecar :9080
  participant WH as MQWebhookController/MQBridge
  participant MC as MQConsumer + MessageProcessor
  participant IC as InboundConsumer (JetStream)
  participant LE as LifecycleEngine
  participant N as NATS JetStream
  participant Core as Core (SpbConsumer)

  BC->>MQ: STR0008R2 (credito para 46026562)
  MQ->>SC: poll QL.RSP.00038166.46026562.xx (Sidecar.java:1074-1091)
  SC->>WH: POST /api/mq/webhook/SPB01 (X-MQ-*, HMAC) (Sidecar.java:570-600)
  WH->>MC: MQBridge.handle_webhook -> MQConsumer.process_message (mq_consumer.ex:63)
  MC->>MC: unpack header 588B + decifra (HSM C14) + verifica C15 + extrai CodMsg
  MC->>MC: MessageProcessor.receive_from_bacen grava bacen_messages
  MC->>N: outbox bacen.inbound.spb01.str0008r2 (message_processor.ex:722, stream BACEN_INBOUND)
  MC->>N: outbox monetarie.spb.transactions.settled (CoreNotifier.notify_inbound_settled, message_processor.ex:739 -> core_notifier.ex:100)
  N->>IC: pull durable bacen-gateway-receiver (inbound_consumer.ex:35-36)
  IC->>LE: receiver_side_credit? -> create_inbound_str_credit_operation (inbound_consumer.ex:1077-1108, lifecycle_engine.ex:1108-1141)
  Note over LE: spb_operations direction=inbound, dedup por unique parcial<br/>(message_type, control_number_clearing) WHERE direction='inbound'
  LE->>N: outbox monetarie.spb.credits.inbound (lifecycle_engine.ex:1524,1628)
  N->>Core: monetarie.spb.transactions.settled (payload com account_id=CtCredtd, amount=VlrLanc, NumCtrlIF/NUOp)
  Core->>Core: catch-all SpbHandler.handle_transaction_ledger credita TB + extrato (spb_handler.ex:60,159)
  N->>Core: monetarie.spb.credits.inbound (payload credit_received, num_ctrl_str)
  Core->>Core: SpbInboundCreditHandler credita TB + extrato com arbitro num_ctrl_str (spb_consumer.ex:66)
  Note over Core: RISCO: sem dedup cruzado entre os dois caminhos (verificacao P0 pendente)
```

Alertas derivados: inbound `STR0010R2`/`PAG0111R2` gera alerta `devolucao_recebida` (`lib/bacen_gateway/alerts/alert_engine.ex:141-199`), sempre excluindo acervo legado (`source NOT LIKE 'legacy_%'`, `alert_engine.ex:153`).

## 6. Integração NATS (subjects exatos)

Streams criados pelo SPB (`lib/bacen_gateway/nats/stream_manager.ex:19-92`; réplicas via `NATS_STREAM_REPLICAS`): `BACEN_OUTBOUND` (`bacen.outbound.>`), `BACEN_INBOUND` (`bacen.inbound.>`), `BACEN_DLQ` (`bacen.dlq.>`), `BACEN_EVENTS` (`bacen.events.>`), `MONETARIE_CORE` (`monetarie.core.>`). O stream `MONETARIE_SPB` (que captura `monetarie.spb.>`) é criado pelo CORE; o SPB apenas publica nele (`lib/bacen_gateway/outbox/worker.ex:168`, `lifecycle_engine.ex:1624`).

Publicados (todos via outbox `pending_publish_events`, drenado por `Outbox.Worker`, dedup JetStream por `Nats-Msg-Id` em `nats/publisher.ex:223`):

| Subject | Onde | Propósito |
|---|---|---|
| `bacen.outbound.<dominio>.<cod>` | `nats/publisher.ex:93`; `consumers/core_event_consumer.ex:285` | espelho outbound |
| `bacen.inbound.<dominio>.<cod>` | `message_processor.ex:722` | inbound normalizado |
| `bacen.dlq.<motivo>.<cod>` / `bacen.dlq.added` | `publisher.ex:115`; `dead_letter_queue.ex:201` | dead-letter |
| `bacen.events.<categoria>.<evento>` | `publisher.ex:125` (ex. `consumers/inbound_consumer.ex:198`, `str/pending_queue.ex:362`) | trilha de eventos |
| `bacen.message.sent` / `bacen.message.failed` | `message_processor.ex:676,697` | notificação de envio |
| `monetarie.spb.transactions.<status>` | `core_event_consumer.ex:356`; `nats/core_notifier.ex:27` | status TED-out ao Core (accepted/rejected/r1_confirmed/...) |
| `monetarie.spb.transactions.settled` | `nats/core_notifier.ex:100` | crédito TED-in liquidado (hook legado; no Core cai no catch-all `SpbHandler`) |
| `monetarie.spb.cosif.entry` | `nats/core_notifier.ex:156` | lançamento COSIF ao Core (gated por `cosif_to_core_enabled`) |
| `monetarie.spb.credits.inbound` | `lifecycle_engine.ex:1524,1628` | evento `credit_received` (ponte canônica; no Core tem handler dedicado `SpbInboundCreditHandler`, `spb_consumer.ex:66`) |
| `monetarie.spb.credits.return_status` | `devolution/devolution_engine.ex:368` | desfecho de devolução STR0010 |

Assinados:

| Subject | Onde | Mecanismo |
|---|---|---|
| `monetarie.core.spb.>` | `consumers/core_event_consumer.ex:23,41` | Gnat.sub plano (transfer_request, devolution_request) |
| `spb.health.check` | `core_event_consumer.ex:24,42` | request/reply de saúde |
| `bacen.outbound.>` | `consumers/message_consumer.ex:24-25` | pull durable `bacen-gateway-sender` |
| `bacen.inbound.>` | `consumers/inbound_consumer.ex:35-36` | pull durable `bacen-gateway-receiver` |
| `bacen.events.>` | `consumers/events_consumer.ex:30-31` | pull durable `bacen-events-processor` (audit) |
| `spb.messages.>` | `monitoring/broadcaster.ex:16,184` | broadcast para Phoenix Channels |

## 7. Subsistemas de negócio

- **Alertas (AlertEngine)**: GenServer com poll de 60s (`alerts/alert_engine.ex:72`), tipos `devolucao_pendente` (devolução parada > 50min), `rdc_intradia` (checagens 15/16/17h), `devolucao_recebida`, `contabil`; dedupe por unique parcial e roteamento UI/e-mail por `alert_user_routes`.
- **DDA**: habilitado por `DDA_ENABLED` (default true, `config/runtime.exs:71-72`; gate `lib/bacen_gateway/dda.ex:42-46`). Não-financeiro (`flag_financ='N'`); DDA0400 = abertura de dia DDA (`post_integration/specific_handlers/dda_handler.ex:14-16`). ~117 módulos de mensagem DDA.
- **Liquidante**: DESLIGADO por padrão (`LIQUIDANTE_ENABLED`, default false, `config/runtime.exs:55-56`; `lib/bacen_gateway/liquidante.ex:44-61` devolve `child_specs()=[]`). Quando ligado sobe BalanceManager/Processor/DedupCache; foi desligado porque auto-respondia avisos e usava saldos fabricados (R$100M/50M em ETS, `config/config.exs:113-116`).
- **COSIF/contabilidade**: trilha canônica `Accounting.Posting` (partida dobrada em `accounting_entries`, gate `system_config.cnt_ativa` default ON, `accounting/posting.ex:338-348`) dirigida pelo roteiro `spb_cnt_roteiro`/`spb_cnt_roteiro_tags` (`accounting/roteiro.ex`, porte de `spb_fc_get_evento_contabil`); espelho ao Core via `monetarie.spb.cosif.entry` (default OFF).
- **Alçada**: `Alcada.AlcadaEngine` (regras de `spb_alcadas` + `alcada_tag_rules` + `alcada_counterparty_rules` em ETS; vistos em `spb_vistos`) e o gate de envio 4-olhos `Alcada.SendGate` (`SPB_ALCADA_GATE_ENABLED`, default ligado, `alcada/send_gate.ex:124-128`).
- **Cancelamento protocolar**: `Cancellation.CancellationEngine` com 6 variantes (selic, cir, rdc, ecr, dda, generic), fluxo validar → alçada → mensagem de cancelamento → estorno de saldo (`cancellation/cancellation_engine.ex:55-93`).
- **Tarifas**: `Tariff.TariffEngine`, porte de `spb_sp_trata_tarifas_bc`: faixa por mensagem × janela de hora × quantidade diária × vigência, `vl_emissor` × `vl_destinatario`, sobre `bacen_tariff_bands` (9.473 faixas); movimentos em `tariff_movements`.
- **Dia operacional**: `Day.SystemDate` (ponteiro em `spb_operational_date`, avanço/retroação GEN0021), `Day.DayAutoClose` (sinal de fechamento = STR0016 inbound confirmado; abertura = STR0017), política manual/str0016/time (`day/system_date_policy.ex:17-22`). GEN0001 aqui é teste de conectividade MQ (`mq/gen0001_test.ex`), não abertura de dia.
- **Devolução**: `Devolution.DevolutionEngine` (STR0010 comandado pelo Core via `devolution_request`, desfecho por `monetarie.spb.credits.return_status`).

## 8. Catálogo e construtor de mensagens (587 mensagens / 33 grupos)

O catálogo de ENVIO é derivado em runtime dos módulos compilados: `GET /api/messages/send/catalog` → `MessagesController.send_catalog` (`lib/bacen_gateway_web/controllers/messages_controller.ex:247-250`), que enumera todo módulo `BacenGateway.Messages.<Grupo>.<Msg>` com `message_code/0`, `category/0` e `build/1`, excluindo apenas respostas (R1/R2/R3) e schemas de erro (E), sem filtro por papel (decisão do dono, comentário em `messages_controller.ex:913-920`): resultado 587 mensagens em 33 grupos. Metadados (nome, versão real do catálogo, papéis emissor/destino) vêm de `Messages.CatalogMetadata`, fonte `priv/catalog/v512_message_routes.json` (v5.12; papéis herdados do v5.11, `messages/catalog_metadata.ex:1-14`). Parâmetros obrigatórios por mensagem saem de `catalog_required_parameters` + `Messages.CatalogParameters`. Observação: o nome `MessageFormCatalog` é o módulo análogo da cabine PIX; no SPB o construtor é este `send_catalog`.

## 9. API admin, portas e frontend

- Porta do spb-api: `PORT` (container ECS = 4010, `infra/aws/greenfield/ecs.tf:147-153`; dev `config/dev.exs:5`). Router: `lib/bacen_gateway_web/router.ex` (987 linhas) com pipelines `:api`, `:authenticated` (JWT), `:rate_limited_auth` (login 5/60s), `:webhook_auth` (HMAC + IP para o sidecar). Grupos: auth (`:43-73`), webhook MQ (`:52-55`), mensagens (`:91-116`), transações/operações (`:119-137`, `:215`), alçadas/vistos (`:140-154`), relatórios (`:186-189`), admin (`:215+`), health (`:39-40`). WebSocket `/monitoring/websocket` (`endpoint.ex:5-8`), tópico `monitoring:live`.
- Paginação server-side de Transações: `spb/frontend-vue/src/apps/admin/views/TransactionsView.vue:284-292` usa `v-data-table-server` e envia `page`/`per_page` (`:1379-1380`); o filtro Categoria envia `tp_oper_grid` sempre explícito, com Todos = `'todos'` (`src/utils/spbCategory.ts:85-87`), porque a ausência do parâmetro cai no default `F` do backend.
- Frontend: Vue 3 + TS + Vuetify, rotas admin em `src/router/admin-routes.ts` (command-center, messages, message-watch, transactions etc.).

## 10. Env vars por função

| Função | Vars (onde são lidas) |
|---|---|
| HTTP/Phoenix | `PORT`, `PHX_HOST`, `SECRET_KEY_BASE`, `CORS_ORIGINS` (`config/runtime.exs:15-31`) |
| Banco | `DATABASE_URL`, `POOL_SIZE` (`runtime.exs:24-25`) |
| NATS | `NATS_URL` (obrigatório em prod), `NATS_CLUSTER_NODES`, `NATS_STREAM_REPLICAS`, `NATS_STREAM_MAX_BYTES` (`runtime.exs:34-35`; `application.ex:247-271`; `stream_manager.ex:161-204`) |
| Auth | `GUARDIAN_SECRET_KEY`/`JWT_SECRET`, `JWT_TTL_SECONDS`, `MONETARIE_SSO_SECRET` (`runtime.exs:37`) |
| IBM MQ | `IBM_MQ_ENABLED`, `IBM_MQ_TRANSPORT` (=`sidecar`), `IBM_MQ_SIDECAR_URL` (default `http://localhost:9080`), `IBM_MQ_QM`, `IBM_MQ_USER`, `IBM_MQ_PASSWORD`, `IBM_MQ_TLS_*`, `IBM_MQ_RECONNECT_INTERVAL` (`runtime.exs:121-140`); canais por domínio `IBM_MQ_SPB01_CHANNEL`, `IBM_MQ_SPB02_*`, `IBM_MQ_MES01/02/03_CHANNEL` |
| Webhook MQ | `MQ_WEBHOOK_SECRET` (`runtime.exs:39`), `WEBHOOK_ALLOW_UNSIGNED_INTERNAL` (`web/plugs/webhook_auth_plug.ex:79`) |
| HSM RTM | `RTM_HSM_ENABLED`, `RTM_HSM_BASE_URL` (default `https://monetarie-hsm-hml.priv.rtmcloud.net.br`, HTTPS 443), `RTM_HSM_VHSM`, `RTM_HSM_CRYPTO_USER`, `RTM_HSM_TOKEN`, `RTM_HSM_PRIVATE_KEY_UID`, `RTM_HSM_PUBLIC_KEY_UID`, `RTM_HSM_KEY_SIZE_BITS`, `RTM_HSM_TIMEOUT_MS` (`runtime.exs:99-115`) |
| Identidade/certs | `ISPB_LOCAL` (46026562), `ISPB_BACEN` (00038166), `BACEN_CERT_PEM_FILE` (T010/P001, `crypto/certificate_registry.ex:313`), `BACEN_CERT_KEY_FILE`/`BACEN_CERT_KEY_PEM` (fallback sem HSM), `BACEN_CONTROL_PREFIX` (=MON, `messages_controller.ex:1705`), `BACEN_ENVIRONMENT`/`SPB_ENVIRONMENT`, `BACEN_DEFAULT_RECEIVER_ISPB`, `BACEN_ALLOWED_RECEIVER_ISPBS`, `BACEN_RECEIVER_LOCKED` (`runtime.exs:81-96,119-120`) |
| Flags de negócio | `LIQUIDANTE_ENABLED` (+`LIQUIDANTE_AUTO_APPROVE_LIMIT`, `_DEFAULT_RESERVE`, `_DEFAULT_SELIC`, `_RESERVATION_TTL_MS`, `_SYNC_INTERVAL_MS`), `DDA_ENABLED`, `SPB_ALCADA_GATE_ENABLED`, `STR_DIRECTORY_REFRESH_ENABLED`, `SCHEDULED_QUERY_MQ_DISPATCH_ENABLED`, `SCHEDULED_QUERY_POLL_INTERVAL_MS`, `SIMULATOR_MODE` |
| Redis/e-mail | `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD` (`runtime.exs:45-47`); `SMTP_HOST/PORT/USER/PASS/FROM` (AlertEngine) |
| Sidecar Java | `PORT` (9080), `IBM_MQ_HOST/PORT/QM/CHANNEL/USER/PASSWORD`, `WEBHOOK_BASE_URL` (=`http://localhost:4010` na task), `MQ_WEBHOOK_SECRET`, `POLL_INTERVAL_MS`, `DOMAINS_TO_POLL`, `SIDECAR_POISON_MAX_DELIVERIES` (`Sidecar.java:68-101`) |

## 11. Nomenclatura ECS e hosts

- Serviço **spb-api**: cluster `monetarie-greenfield-{homolog,prod}`, imagem `monetarie/spb-api`, containerPort 4010, cpu 1024 / mem 2048 (`infra/aws/greenfield/ecs.tf:147-153`); env MQ na task (`ecs.tf:169-191`). O sidecar **spb-mq-sidecar** roda como container não-essential na MESMA task (imagem `monetarie/spb-mq-sidecar`, cpu 256 / mem 512, `ecs.tf:468-490,512-563`), com `WEBHOOK_BASE_URL=http://localhost:4010` e `DOMAINS_TO_POLL=SPB01`. O build do spb-api é MANUAL (retag + task-def) por causa do sidecar acoplado.
- Serviço **spb-admin-ui**: imagem `monetarie/spb-admin-ui`, containerPort 8080, cpu 256 / mem 512 (`ecs.tf:312-318`).
- Hosts internos (ALB `monetarie-internal-*`): HML `spbapi-h.monetarie.internal` / `spb-api-h` / `spbadmin-h`; produção sem sufixo (`infra/aws/greenfield/alb-internal.tf:24-29,92-95`; sufixo em `dns.tf:6`).
- IBM MQ RTM: `172.31.2.50`, SPB01 `QM.46026562.01:1514` e MES01 `QM.46026562.02:12522`, canal `APP.SVRCONN`.
- Simulador: não é serviço ECS; roda local/isolado (backend Phoenix porta 4001/4100, frontend Vite 5181, `spb/simulator/config/runtime.exs:17-26`, `spb/simulator-frontend/vite.config.ts:8-13`).

## 12. Pontos de atenção conhecidos

- Duas rotas de ingestão inbound convivem: webhook MQ (síncrona, notifica o Core direto em `settled`) e o pull JetStream `InboundConsumer` (materializa a operação). A idempotência do crédito fica no unique parcial de `spb_operations` e no dedup do JetStream.
- `monetarie.spb.credits.inbound` está implementado, publicando E consumido pelo Core (handler dedicado `SpbInboundCreditHandler`, `core/backend/lib/monetarie/infra/nats/consumers/spb_consumer.ex:66`). O hook legado `monetarie.spb.transactions.settled` também segue publicando para a mesma TED-in e o catch-all do Core também credita ledger para transação externa não rastreada (`spb_handler.ex:60-78,159`). Sem dedup cruzado identificado entre os dois caminhos: verificação P0 pendente em `docs/reports/2026-07-10-ted-in-duplo-credito-verificacao.md`.
- O doc do `CoreEventConsumer` cita stream `MONETARIE_CORE_SPB`, mas o StreamManager cria `MONETARIE_CORE` e a subscrição real é Gnat.sub plano (`core_event_consumer.ex:6` vs `stream_manager.ex:77-91`).
- `spb.messages.>` tem subscriber (broadcaster de monitoramento) mas nenhum publisher neste repositório.
