# Arquitetura do Core Monetarie

Data: 2026-07-10. Documento produzido por leitura direta do código em `core/` (nenhuma inferência sem evidência; cada afirmação de integração traz arquivo:linha). Complementa o dicionário de dados em `docs/architecture/2026-07-10-dicionario-dados-core.md`.

## 1. Visão geral

O Core NÃO é um umbrella Elixir: é um único app OTP `:monetarie` em `core/backend` (`core/backend/mix.exs:6`, `mod: {Monetarie.Application, []}` em `core/backend/mix.exs:19`). O código se divide em duas árvores:

- `core/backend/lib/monetarie`: negócio e infraestrutura (schemas, use_cases, infra, services, workers, outbox, partners, monitoring, etl, sta, sisbajud, cadoc, dashboard, util).
- `core/backend/lib/monetarie_web`: camada Phoenix API-only (router, controllers, channels, plugs, OpenAPI).

Os frontends são três SPAs Vue 3 + PrimeVue em `core/apps/{admin,banking,merchant}`, compartilhando o pacote `@monetarie/shared` (`core/packages/shared`). Não há assets estáticos no backend.

Papel do Core no ecossistema:

- Core banking: contas, clientes, ledger financeiro, extrato, tarifas, crédito, tesouraria, contabilidade COSIF e regulatório BACEN/RFB.
- O Core NUNCA assina mensagem BACEN (TED, PIX, DICT, ICOM-SPI). Ele publica pedidos e consome eventos via NATS JetStream; as assinaturas ficam nas cabines PIX (`pix/`) e SPB (`spb/`).
- Ledger de dinheiro: TigerBeetle 0.17.3 (cliente `tigerbeetlex` vendorizado em `core/backend/vendor/tigerbeetlex`, versão em `core/backend/vendor/tigerbeetlex/mix.exs:4`) com espelho relacional no Postgres (`account_entries`, `transactions`, `cosif_journal_entries`).
- Persistência: Aurora PostgreSQL (database `mon_core`) via `Monetarie.Infra.Repo.Base`, com wrapper transparente de auditoria `Monetarie.Repo` (`core/backend/lib/monetarie/repo.ex:1`). Redis via Redix (`core/backend/lib/monetarie/application.ex:66`). Jobs via Oban (`core/backend/lib/monetarie/application.ex:151`).

## 2. Diagrama de componentes

```mermaid
flowchart TD
    subgraph UIs["Frontends Vue 3 (nginx :8080)"]
        ADMIN["core-admin-ui (core/apps/admin)"]
        IB["core-banking-ui / IB (core/apps/banking)"]
        MERCH["core-merchant-ui (core/apps/merchant)"]
    end
    PARTNER["Parceiros externos (Partner API OAuth2)"]

    subgraph CORE["core-api (:monetarie, Bandit :4000)"]
        WEB["MonetarieWeb.Router (2.114 rotas)"]
        UC["UseCases (Wallet, Payments, Fees, Compliance, Regulatory...)"]
        NATSIN["Infra.Nats (Connection, StreamSetup, Consumers)"]
        OUTBOX["Outbox: Publisher.publish_async -> Oban fila nats_publish"]
        TB["Infra.Tigerbeetle (pool :tb / :tb_bulk, breaker)"]
    end

    AURORA[("Aurora PostgreSQL mon_core")]
    REDIS[("Redis")]
    TBSRV[("TigerBeetle 0.17.3 cluster, IP:3001")]
    NATS[("NATS JetStream cluster :4222")]

    PIXCAB["Cabine PIX (pix-api) - assina pacs/DICT"]
    SPBCAB["Cabine SPB (spb-api) - assina STR/GEN via MQ"]
    NPC["NPC (boletos)"]
    STA["STA (arquivos BACEN)"]

    ADMIN -->|"/api proxy nginx"| WEB
    IB -->|"/api proxy nginx"| WEB
    MERCH -->|"/api proxy nginx"| WEB
    PARTNER -->|"/api/partner/v1 Bearer"| WEB

    WEB --> UC
    UC --> TB --> TBSRV
    UC --> AURORA
    UC --> REDIS
    UC --> OUTBOX --> NATS
    NATSIN --> NATS

    NATS <-->|"monetarie.spi.> / monetarie.core.pix.*"| PIXCAB
    NATS <-->|"monetarie.spb.> / monetarie.core.spb.*"| SPBCAB
    NATS <-->|"monetarie.npc.>"| NPC
    NATS <-->|"monetarie.sta.>"| STA

    UC -.->|"HTTP pontual por E2E (GET /api/v1/transactions)"| PIXCAB
    UC -.->|"HTTP pontual (GET /api/messages?search=)"| SPBCAB
```

## 3. Contextos principais (código)

| Contexto | Módulos chave | Evidência |
|---|---|---|
| Contas e resolução | `Monetarie.UseCases.Banking.AccountResolver` (resolve conta creditora, direção inbound), schema `Monetarie.Schemas.Relational.Account` | `core/backend/lib/monetarie/use_cases/banking/account_resolver.ex:167` (`inbound_direction?/1`); `core/backend/lib/monetarie/schemas/relational/account.ex:54` |
| Carteira (Wallet) | `Monetarie.UseCases.Wallet`: `create/3`, `deposit/4`, `withdraw/4`, `hold/4` (two-phase escrow), `internal_transfer/4` (book transfer mesmo ISPB), `inter_entity_transfer/4` | `core/backend/lib/monetarie/use_cases/wallet.ex:170` (deposit), `:221` (withdraw), `:268` (hold), `:318` (internal_transfer) |
| Extrato | `Monetarie.UseCases.Pix.StatementEntries` (`record_outbound_settled/2`, `record_inbound_settled/1`, idempotente por reference+source) sobre a tabela `account_entries` | `core/backend/lib/monetarie/use_cases/pix/statement_entries.ex:49`, `:110`; schema `core/backend/lib/monetarie/schemas/accounts/account_entry.ex:30` |
| Transações | Tabela única `transactions` (não existe `payment_transactions`); contexto `Monetarie.UseCases.PaymentTransactions`; PIX-out canônico via `Monetarie.UseCases.Payments.OutboundOrchestrator` + `OutboundRequests` | `core/backend/lib/monetarie/schemas/relational/transaction.ex:40`; `core/backend/lib/monetarie/use_cases/payments/outbound_orchestrator.ex:56` |
| Tarifas | `Monetarie.UseCases.Fees.FeeCalculator` (cálculo), `FeeCharger` (débito cliente TB, crédito receita, grava `fee_transactions` em centavos), `SplitDistributor` (fee split, flag `:enable_fee_split`), `FeeEvents` (NATS) | `core/backend/lib/monetarie/use_cases/fees/fee_charger.ex:87`; `core/backend/lib/monetarie/use_cases/fees/split_distributor.ex:55`; `core/backend/lib/monetarie/use_cases/fees/fee_events.ex:25` |
| PLD/FT | Contexto compliance (casos, regras de monitoramento, PEP, sanções, atividades suspeitas), schemas em `core/backend/lib/monetarie/schemas/compliance/` | ver seção 5 do dicionário de dados |
| e-Financeira | `core/backend/lib/monetarie/use_cases/regulatory/efinanceira/` (eventos, lotes, webservice SOAP/mTLS RFB, simulador), 14 tabelas `efinanceira_*` | envs em `core/backend/config/runtime.exs:134-253` |
| Webhooks | `Monetarie.UseCases.Webhooks.dispatch_event/3` (dedup por event_id, tenant scoping, entrega eager + Oban) e `Monetarie.Workers.Services.Webhooks.DeliveryJob` (POST assinado HMAC-SHA256, guarda SSRF) | `core/backend/lib/monetarie/use_cases/webhooks.ex:157`; `core/backend/lib/monetarie/workers/services/webhooks/delivery_job.ex:120` |
| Partner API | `Monetarie.Partners.OAuth` (client_credentials, JWT HS256 Bearer), keys na tabela `api_keys` escopadas por `partner_id`, controllers em `core/backend/lib/monetarie_web/controllers/partner_v1/` | `core/backend/lib/monetarie/partners/oauth.ex:37`; `core/backend/lib/monetarie/use_cases/api_keys.ex:60` |
| Onboarding/KYC | `core/backend/lib/monetarie/use_cases/onboarding/` + Nextcode (`core/backend/lib/monetarie/services/nextcode/client.ex`) | envs `NEXTCODE_*` em `core/backend/config/runtime.exs:746-748` |
| Contabilidade | COSIF: `cosif_accounts`, `cosif_journal_entries` (particionada), pontes `AccountingBridge`, mapeamento TB code para COSIF em `core/backend/lib/monetarie/util/codes/account_code.ex:261-292` | seção 9 do dicionário |

## 4. Árvore de supervisão e processos

Raiz: `Monetarie.Supervisor`, `strategy: :one_for_one` (`core/backend/lib/monetarie/application.ex:240`). Ordem relevante dos children (todas as linhas em `application.ex`):

1. Stores ETS de auth/onboarding e caches (`SmsCodeStore` :34, `EmailCodeStore` :37, `Dashboard.Cache` :46, `Limits.UsageCache` :52, `TxStatusCache` :53, `BalanceCache` :54).
2. `Redix` (:66), `Monetarie.Infra.Repo.Base` (:69); `Repo.Batch` e `Repo.Reader` condicionais (:298, :309).
3. Circuit breakers HTTP (Registry :76 + 7 breakers para BCB SGS, NFe, Nextcode, B3, Tesouro, ANBIMA, :77-140) e `Infra.Tigerbeetle.CircuitBreaker` lock-free (:145).
4. Pool TigerBeetle (`tigerbeetle_pool_children/0` :405): `pool_size <= 1` sobe `:tb`; maior sobe `:tb_0..:tb_{N-1}`; `:tb_bulk` extra quando `TB_BULK_READ_ENABLED` (:439-451). `DepositCoalescer` condicional a `TB_DEPOSIT_COALESCE_ENABLED` (:460).
5. `Oban` (:151), `Phoenix.PubSub` (:164), monitores `PixInCreditOkRateMonitor` (:180) e `LedgerPhasePanel` (:181).
6. Subárvore NATS quando `NATS_ENABLED` (default true, :26): `Monetarie.Infra.Nats.Supervisor` (:194), `Pix.Gateway` (:195), `Pix.IdempotencyLogConsumer` (:196), `TbFirst.PgWriterSupervisor` só com `TWO_PHASE_PIX_IN` (:203-209).
7. `PixProviders.Supervisor` (:225), `MonetarieWeb.Endpoint` (último da base, :184) e `Monetarie.Infra.Drainer` depois do endpoint para lame-duck no shutdown (:233-238).

Subárvore `Monetarie.Infra.Nats.Supervisor` (`core/backend/lib/monetarie/infra/nats/supervisor.ex:28-56`): `Connection`, `StreamSetup`, `SpbIntegration.MessageTracker`, consumers `PixConsumer`, `SpbConsumer`, `NpcConsumer`, `StaConsumer`, `ClstConsumer`, responder `PixAccountValidator`, `ScrEngine.Nats.Consumer` e o `Infra.Nats.Consumer` unificado.

`Monetarie.Infra.Nats.Connection` (`core/backend/lib/monetarie/infra/nats/connection.ex`): failover de hosts (`NATS_HOSTS` CSV ou `NATS_HOST`+`NATS_PORT`, :208-221), backoff exponencial 5s a 60s (:6-12), conexão em task com `spawn_monitor` e teto `@connect_timeout_ms 15_000` (:147-165). A cláusula de `:DOWN` da task sem resultado (:103-116) e o `{:connect_timeout, ref}` (:129-133) destravam o estado `connecting?`, correção do incidente de 2026-07-05/06 em que o Core ficou surdo ao NATS por ~24h.

Workers Oban relevantes ao money path (crons de produção em `core/backend/config/runtime.exs`):

| Worker | Cron | Função |
|---|---|---|
| `PartitionMaintenance` | `0 1 * * *` (runtime.exs:576) | cria partições futuras das tabelas particionadas |
| `StaleHoldChecker` | `1-59/5 * * * *` (runtime.exs:577) | libera holds TB travados |
| `PixInCreditRecoveryWorker` | `* * * * *` (runtime.exs:582) | recovery do WAL de crédito PIX-in (`received` reexecuta tudo; `tb_done` reexecuta só a perna PG via `PixHandler.replay_pg_leg/1`) |
| `Nats.DlqMonitorWorker` | `2-59/5 * * * *` (runtime.exs:589) | monitora profundidade da DLQ |
| `Reconciliation.TbPgReconciliationJob` | `2-59/15 * * * *` (runtime.exs:591) | reconciliação TB x Postgres |
| `PixStatusReconciliation` | `11-59/15 * * * *` (runtime.exs:595) | materializa PIX presos em processing consultando a cabine por E2E |
| `PixInOrphanReconciliation` | `13-59/15 * * * *` (runtime.exs:634) | órfãos PIX-in via camt.054 (alert-only por flag) |
| `BalanceCheckpointPrefold` | `5 * * * *` e rebuild `37 3 * * *` (runtime.exs:656-658) | prefold dos checkpoints de saldo |
| `OutboundPendingRecoverySweeper` | `*/2 * * * *` (config.exs:78) | resolve `outbound_requests` presas em stage 0 (PIX-out ambíguo no TB) |
| `StuckOutboundChecker` | `*/10 * * * *` (config.exs:84) | resolução assistida de PIX-out preso >30min, não destrutivo por default |

## 5. NATS: streams, consumo e publicação

Streams JetStream criados pelo Core no boot (`Monetarie.Infra.Nats.StreamSetup`, `core/backend/lib/monetarie/infra/nats/stream_setup.ex`; réplicas via `NATS_STREAM_REPLICAS`, default 3):

| Stream | Subjects | Retenção | Linha |
|---|---|---|---|
| `MONETARIE_SPI` | `monetarie.spi.>` | 7d, 10GB | stream_setup.ex:25 |
| `MONETARIE_SETTLEMENT` | `monetarie.settlement.>` | 7d, 5GB | :36 |
| `MONETARIE_DICT` | `monetarie.dict.>` | 7d, 5GB | :47 |
| `MONETARIE_SPB` | `monetarie.spb.>` | 7d, 1GB | :58 |
| `MONETARIE_NPC` | `monetarie.npc.>` | 7d, 1GB | :69 |
| `MONETARIE_STA` | `monetarie.sta.>` | 7d, 512MB | :80 |
| `MONETARIE_CLST` | `monetarie.clst.>` | 7d, 512MB | :91 |
| `MONETARIE_CORE` | `monetarie.core.>` | 7d, 1GB | :102 |
| `MONETARIE_AUDIT` | `monetarie.audit.>` | 90d, 20GB | :113 |
| `MONETARIE_DLQ` | `monetarie.dlq.>` | 30d, 1GB | :124 |
| `MONETARIE_SCR` | `monetarie.scr.>` | 7d, 512MB | :135 |

O stream legado `MONETARIE_PIX` é REMOVIDO a cada boot (`@deprecated_streams`, stream_setup.ex:21); o stream ativo de PIX é `MONETARIE_SPI`. Os subjects req/reply DICT (`dict.*.request`) ficam deliberadamente fora de JetStream.

Consumers duráveis com dedup e DLQ: `PixConsumer` e `SpbConsumer` usam `Monetarie.Infra.Nats.DurableConsumer` (push durável, ACK explícito após dedup+handler, NAK com backoff, DLQ após esgotar; `core/backend/lib/monetarie/infra/nats/durable_consumer.ex:143-195`). Dedup determinístico em `processed_messages` (`core/backend/lib/monetarie/infra/nats/message_dedup.ex`).

### 5.1 Subjects que o Core CONSOME

| Subject | Arquivo:linha | Módulo | Propósito |
|---|---|---|---|
| `monetarie.spi.transaction.*` | `core/backend/lib/monetarie/infra/nats/consumers/pix_consumer.ex:35` | PixConsumer -> AtomicPaymentHandler/PixHandler | ciclo de vida PIX (created/settled/accepted/rejected/status_update) |
| `monetarie.spi.payment.*` | pix_consumer.ex:36 | PixHandler.handle_payment | pagamentos QR |
| `monetarie.spi.return.*` (e cláusula dedicada `monetarie.spi.return.rejected` em pix_consumer.ex:88) | pix_consumer.ex:37 | PixHandler.handle_return | devoluções PIX |
| `monetarie.settlement.session.*` (crédito em pix_consumer.ex:98) | pix_consumer.ex:43 | SettlementHandler | sessões de liquidação |
| `monetarie.spb.messages.*` | `core/backend/lib/monetarie/infra/nats/consumers/spb_consumer.ex:35` | SpbHandler.handle_message_confirmed | confirmação de mensagem BACEN |
| `monetarie.spb.transactions.*` | spb_consumer.ex:36 | SpbHandler.handle_transaction_updated | status de TED |
| `monetarie.spb.credits.inbound` | spb_consumer.ex:37 (dispatch :66) | SpbInboundCreditHandler.handle | crédito SPB de entrada (STR0004/STR0008R2) |
| `monetarie.spb.credits.return_status` | spb_consumer.ex:38 (dispatch :74) | SpbInboundCreditHandler.handle_return_status | desfecho de devolução STR0010 |
| `monetarie.spb.cosif.entry` | spb_consumer.ex:39 (dispatch :83) | SpbCosifMirrorHandler | espelho de conciliação COSIF (`spb_cosif_mirror`) |
| `monetarie.npc.boleto.*`, `monetarie.npc.cycle.*`, `monetarie.npc.protocol.processed`, `monetarie.npc.titulo.*` | `core/backend/lib/monetarie/infra/nats/consumers/npc_consumer.ex:36-41` | NpcHandler | boletos, ciclos, CNAB retorno |
| `monetarie.sta.file.*`, `monetarie.sta.protocol.processed` | `core/backend/lib/monetarie/infra/nats/consumers/sta_consumer.ex:28-29` | StaHandler | arquivos STA |
| `monetarie.clst.settlement.>`, `monetarie.clst.clearing.*`, `monetarie.clst.reconciliation.*`, `monetarie.clst.liquidity.*` | `core/backend/lib/monetarie/infra/nats/consumers/clst_consumer.ex:34-37` | ClstHandler | clearing/liquidez |
| `monetarie.dict.infractions.*`, `monetarie.dict.recovery.*` | `core/backend/lib/monetarie/infra/nats/consumers/dict_consumer.ex:24-25` | PixHandler (MED) | infrações e recuperação MED |
| `monetarie.scr.event.*` | `core/backend/lib/monetarie/use_cases/scr_engine/nats/consumer.ex:24` | ScrEngine.Nats.Handler | eventos SCR (CADOC 3040) |
| `monetarie.core.pix.validate_account` | `core/backend/lib/monetarie/infra/nats/responders/pix_account_validator.ex:33` | Responder (req/reply) | o Core RESPONDE validação de conta creditora para a cabine |
| `monetarie.pix.tbfirst.settled` | `core/backend/lib/monetarie/use_cases/pix/tb_first/pg_writer_worker.ex:33` | TbFirst.PgWriterWorker | perna PG do caminho TB-first (só com flag) |
| `monetarie.pix.idempotency.log` | `core/backend/lib/monetarie/use_cases/pix/idempotency_log_consumer.ex:6` | IdempotencyLogConsumer | log de idempotência da cabine para PG |

### 5.2 Subjects que o Core PUBLICA

| Subject | Arquivo:linha | Quando |
|---|---|---|
| `monetarie.core.pix.payment_request` | `core/backend/lib/monetarie/infra/nats/publisher.ex:196` e `core/backend/lib/monetarie/services/pix_providers/in_house/adapter.ex:26` | envio de PIX-out (pacs.008) para a cabine PIX; também usado por Payables e Open Finance |
| `monetarie.core.pix.response` / `monetarie.core.pix.return` | in_house/adapter.ex (respond_pix / return_funds) | resposta e devolução de PIX |
| `monetarie.core.spb.transfer_request` | publisher.ex:224 e `core/backend/lib/monetarie/use_cases/payments/outbound/ted.ex:100` | envio de TED (STR0008) para a cabine SPB |
| `monetarie.core.spb.devolution_request` | `core/backend/lib/monetarie/use_cases/spb/inbound_credits.ex:827` | pedido de devolução STR0010 |
| `monetarie.core.spb.retry.<message_code>` | `core/backend/lib/monetarie/services/spb_integration/message_tracker.ex:274` | re-publicação em timeout de tracking |
| `monetarie.core.npc.boleto_request` | publisher.ex:242 | emissão de boleto |
| `monetarie.core.fees.charged` / `.reversed` / `.batch_completed` | `core/backend/lib/monetarie/use_cases/fees/fee_events.ex:25,49,66` | eventos de tarifa |
| `monetarie.core.notification.email` / `.sms` | `core/backend/lib/monetarie/use_cases/onboarding/contact_verification.ex:126,143` | OTP de onboarding |
| `monetarie.core.sisbajud.response.blocked` | `core/backend/lib/monetarie/use_cases/judicial/nats_handler.ex:44` | resposta de bloqueio SISBAJUD |
| `ledger.balance.updated` | publisher.ex:262 | notificação de saldo (fire-and-forget) |
| `monetarie.sta.regulatory.upload` | `core/backend/lib/monetarie/cadoc/sta_dispatcher.ex:15` | upload regulatório via STA |
| `monetarie.spi.recurrence.execute` | `core/backend/lib/monetarie/use_cases/pix_automatico.ex:298` | PIX automático |
| `monetarie.dict.infractions.response` | `core/backend/lib/monetarie/use_cases/med/processor.ex:608` | resposta de infração MED |
| `monetarie.dlq.<source>.failed` | `core/backend/lib/monetarie/infra/nats/dead_letter_queue.ex:76` | mensagem esgotada (DLQ) |
| `dict.lookup.request` / `dict.api.request` / `dict.list_keys.request` (req/reply) | in_house/adapter.ex e `core/backend/lib/monetarie/use_cases/pix/gateway.ex:32` | consulta DICT, claims, MED (fora de JetStream) |

### 5.3 Outbox

`Publisher.publish_async/3` (`core/backend/lib/monetarie/infra/nats/publisher.ex:172`) NÃO publica direto: enfileira `Monetarie.Workers.Nats.PublishJob` na fila Oban `:nats_publish` (persistida em `oban_jobs` no Aurora, `max_attempts: 10`, backoff exponencial; `core/backend/lib/monetarie/workers/nats/publish_job.ex:10-29`). `Monetarie.Outbox.RuntimeGuard.check!/2` (`core/backend/lib/monetarie/outbox/runtime_guard.ex:62`) exige que publicações de money path aconteçam dentro de `Repo.transaction` (telemetria de violação; strict mode levanta exceção). Descartes viram dead letter em `nats_dead_letters` (`core/backend/lib/monetarie/infra/nats/oban_telemetry.ex:39`).

## 6. TigerBeetle: cliente, contas, transfers e unidade

- Wrapper central: `Monetarie.Infra.Tigerbeetle` (`core/backend/lib/monetarie/infra/tigerbeetle.ex`): `create_account/5` (:81), `create_transfer/1` (:304), `create_transfers/1` (:315), `lookup_account/1` com contrato de erro estrito (timeout nunca vira `account_not_found`, :119-235), timeout `@tb_timeout 1_500` ms por causa do SLA BACEN de 1,6s (:614). Endereços obrigatoriamente IP:porta (`TIGERBEETLE_ADDRESSES`, resolução host para IP em `core/backend/config/runtime.exs:429-443`).
- Ledgers: default 1 (`DEFAULT_LEDGER_ID`), inter-entidade 99 (`INTER_ENTITY_LEDGER_ID`), escrow 1_004 (segregado; `core/backend/lib/monetarie/use_cases/escrow/provisioning.ex:7`).
- Codes de conta (`core/backend/lib/monetarie/util/codes/account_code.ex:53-94`): 10 cash_asset, 11 spi_asset, 20 settlement_pool, 30 client_liability, 40 revenue_equity, mais códigos de grau 7 (321/322 trânsito PIX, 411-414 tarifas etc.) e mapeamento TB para COSIF (:261-292).
- Codes de transfer (`core/backend/lib/monetarie/util/codes/transfer_code.ex:31-44`): 1 deposit, 2 withdrawal, 3 internal_transfer, 5 fee_collection, 6 pix_transfer, 8 ted_transfer, 1006 escrow_hold, além de sharp_codes de PIX-in/fee-split/MED.
- Two-phase (pending/post/void) é usado no PIX-out/TED: `Monetarie.UseCases.Payments.PendingTransfers.build_pending/2` cria transfers com `pending: true`, `build_post/1` com `post_pending_transfer`, `build_void/1` com `void_pending_transfer` (`core/backend/lib/monetarie/use_cases/payments/pending_transfers.ex:54-163`). O crédito PIX-in NÃO usa two-phase (transfer direto).
- Transfer id determinístico (idempotência em replay NATS): PIX-in = SHA-256 de `"monetarie_core:" <> reference` truncado a 128 bits (`core/backend/lib/monetarie/infra/nats/handlers/pix_handler.ex:1274-1291`); SPB = SHA-256 de `"monetarie_core:spb_inbound_credit:" <> num_ctrl_str` (`core/backend/lib/monetarie/use_cases/spb/inbound_credits.ex:1145-1150`).

### 6.1 Unidade monetária: SUBCENTAVO e o bug 100x

- Fonte única: `Monetarie.Util.MoneyUnit` com `@scale 100` sobre centavos (`core/backend/lib/monetarie/util/money_unit.ex:74`): a unidade interna do Core e do TB é o centésimo de centavo (1 unit = R$ 0,0001). `from_cents/1` multiplica por 100; `to_cents/1` divide por 100 (:82-89).
- Fronteira NATS: a cabine fala BRL/centavos e o Core fala subcentavos; a conversão acontece uma única vez em `Monetarie.Infra.Nats.MoneyBoundary` (`core/backend/lib/monetarie/infra/nats/money_boundary.ex:111` para entrada, :124 para saída), aplicada pelos consumers e pelo Publisher.
- Bug histórico 100x: depositar CENTAVOS crus no TB credita 100x menos. Incidente STR20260706033056377 (R$ 50.000 viraram R$ 500 no saldo do IB), documentado e corrigido em `core/backend/lib/monetarie/use_cases/spb/inbound_credits.ex:294-301`, onde o depósito usa `MoneyUnit.from_cents(row.amount_cents)`.
- Exceções de unidade: `fee_transactions.fee_amount` fica em centavos (`core/backend/lib/monetarie/use_cases/fees/fee_charger.ex:71`); `spb_inbound_credits.amount_cents` em centavos; o campo `amount_brl` do payload SPB chega como string em reais e é convertido de forma exata com Decimal (`core/backend/lib/monetarie/infra/nats/handlers/spb_inbound_credit_handler.ex:146-167`).

### 6.2 Otimizações atrás de flag (default OFF)

| Mecanismo | Módulo | Flag |
|---|---|---|
| Coalescer de depósitos PIX-in | `Monetarie.Infra.Tigerbeetle.DepositCoalescer` (`core/backend/lib/monetarie/infra/tigerbeetle/deposit_coalescer.ex`) | `TB_DEPOSIT_COALESCE_ENABLED` (runtime.exs:73) |
| Sessão TB dedicada a leituras pesadas | `:tb_bulk` (`core/backend/lib/monetarie/infra/tigerbeetle.ex:796-803`) | `TB_BULK_READ_ENABLED` (runtime.exs:82) |
| Circuit breaker lock-free | `Monetarie.Infra.Tigerbeetle.CircuitBreaker` (`core/backend/lib/monetarie/infra/tigerbeetle/circuit_breaker.ex`) | `TB_CIRCUIT_BREAKER_ENABLED` (default TRUE, :252-258) |
| Checkpoint de saldo | `Monetarie.UseCases.Payments.BalanceCheckpoint` (`core/backend/lib/monetarie/use_cases/payments/balance_checkpoint.ex`) | `BALANCE_CHECKPOINT_ENABLED` (runtime.exs:40) |
| Caches ETS (saldo/uso/status, display only) | `BalanceCache`, `UsageCache`, `TxStatusCache` | `BALANCE_CACHE_ENABLED`, `USAGE_CACHE_ENABLED`, `TX_STATUS_CACHE_ENABLED` (runtime.exs:48-50) |
| PIX-in two-phase (TB-first) | `Monetarie.UseCases.Pix.TbFirst.*` | `TWO_PHASE_PIX_IN` (runtime.exs:13). PROIBIDO ligar em produção: o gatilho settled só existe no simulador (canônico 2026-07-09) |

## 7. Money paths do ponto de vista do Core

### 7.1 Crédito PIX-in (evento `transaction.created` com `direction: INBOUND` vindo da cabine)

```mermaid
flowchart TD
    A["Cabine PIX publica monetarie.spi.transaction.created (direction INBOUND)"] --> B["PixConsumer (durável, dedup em processed_messages)\npix_consumer.ex:35"]
    B --> C["MoneyBoundary.convert_inbound: centavos -> subcentavos\npix_consumer.ex:63"]
    C --> D["AtomicPaymentHandler.maybe_handle: é PIX-out rastreado?\npix_consumer.ex:70"]
    D -->|nao rastreado| E["PixHandler.handle_transaction(payload, created)\npix_handler.ex:61"]
    E --> F{"Guardas (pix_handler.ex:70-96)"}
    F -->|"TWO_PHASE_PIX_IN ligada"| G["audit-only (sem credito)"]
    F -->|"nao inbound (AccountResolver.inbound_direction? = false)\naccount_resolver.ex:167"| G
    F -->|"tx ja rastreada (redelivery)"| G
    F -->|"INBOUND nao rastreada"| H["handle_transaction_ledger\npix_handler.ex:420"]
    H --> I["PixInWal.track_received (WAL antes do TB)\npix_handler.ex:485"]
    I --> J["Journal COSIF inline ou deferido\npix_handler.ex:490"]
    J --> K["Wallet.deposit no TigerBeetle\ntransfer_id = SHA-256 deterministico\npix_handler.ex:491,917; wallet.ex:170"]
    K --> L["PixInWal.mark_tb_done + publish_balance_update\npix_handler.ex:494,499"]
    L --> M["maybe_create_inbound_transaction: linha transactions\ntransaction_id = PIXIN + E2E, dedup por end_to_end_id\npix_handler.ex:503,868"]
    M --> N["StatementEntries.record_inbound_settled: account_entries\npix_handler.ex:508"]
    N --> O["PixInWal.mark_pg_done -> tarifa non-blocking (FeeCharger)\npix_handler.ex:510-522"]
    O --> P["PixInWal.mark_completed\npix_handler.ex:524"]
    P -.-> Q["Recovery: PixInCreditRecoveryWorker a cada minuto\n(received = replay total; tb_done = so perna PG)"]
```

Pontos canônicos: o evento `rejected` NUNCA credita (pix_handler.ex:107); o BACEN real liquida ANTES de entregar a pacs.008 e o recebedor não recebe pacs.002, então o caminho correto é este legado flag-OFF; três entregas do mesmo evento geram um único crédito (dedup do consumer + guarda de tx rastreada + transfer_id determinístico).

### 7.2 Crédito TED-in (STR0008R2 vindo do SPB)

```mermaid
flowchart TD
    A["Cabine SPB publica monetarie.spb.credits.inbound\n(num_ctrl_str, message_type STR0008, amount_brl string em reais, credit_kind)"] --> B["SpbConsumer (duravel, clausula dedicada antes do catch-all)\nspb_consumer.ex:37,66"]
    B --> C["SpbInboundCreditHandler.parse: valida payload,\namount_brl reais -> centavos exato com Decimal\nspb_inbound_credit_handler.ex:99,146"]
    C --> D["Arbitro de idempotencia: INSERT spb_inbound_credits\nstatus processing, ON CONFLICT (num_ctrl_str) DO NOTHING\nspb_inbound_credit_handler.ex:229"]
    D -->|ja terminal| Z["ok (nao reprocessa)"]
    D --> E["InboundCredits.process_row\ninbound_credits.ex:108"]
    E -->|credit_kind = client| F["resolve_member_account (conta ativa)\ninbound_credits.ex:150"]
    E -->|credit_kind = institutional| G["resolve_treasury_destination -> post_treasury_credit\n(Treasury.create_movement em CENTAVOS, historico SPBCR)\ninbound_credits.ex:483-543"]
    F --> H["post_member_credit: Wallet.deposit no TB com\nMoneyUnit.from_cents(amount_cents) (fix do bug 100x)\ntb_transfer_id deterministico por num_ctrl_str\ninbound_credits.ex:288-301,1145"]
    H --> I["Ecto.Multi atomico (inbound_credits.ex:317-405):\n1. account_entries (deposito, subcentavos)\n2. transactions (SPBCR + num_ctrl_str, type ted, inbound)\n3. ponte COSIF fail-soft\n4. UPDATE condicional status -> credited_member"]
    I --> J["Extrato, saldo IB e COSIF refletem o credito"]
    B -.-> K["monetarie.spb.cosif.entry -> SpbCosifMirrorHandler\nso espelho de conciliacao (spb_cosif_mirror), nao move dinheiro\nspb_consumer.ex:83"]
```

### 7.3 Débito PIX-out pelo IB (resumo)

Controller -> `OutboundOrchestrator.execute/1` (`core/backend/lib/monetarie/use_cases/payments/outbound_orchestrator.ex:56`): kill switches (`PIX_OUT_DISABLED`, `PIX_OUT_MAX_AMOUNT_BU`), checagem otimista de saldo, `locked_insert` (advisory lock + limites + `outbound_requests`), bloqueio TB two-phase (`PendingTransfers.build_pending`), publicação `monetarie.core.pix.payment_request` via adapter in-house (`core/backend/lib/monetarie/services/pix_providers/in_house/adapter.ex:26`) e `finish` (stage 2 ou void). A confirmação volta por `monetarie.spi.transaction.*`: settled faz post do pending e grava extrato (`StatementEntries.record_outbound_settled`, pix_handler.ex:261); rejected libera os fundos (void).

## 8. Integrações HTTP de saída

| Destino | Módulo cliente | Endpoint e uso | Env vars |
|---|---|---|---|
| Cabine PIX (API admin) | `Monetarie.Services.PixProviders.InHouse.CabinStatusLookup` | `GET /api/v1/transactions?end_to_end_id=<E2E>&per_page=1` (`core/backend/lib/monetarie/services/pix_providers/in_house/cabin_status_lookup.ex:176-179`); login `POST /api/v1/auth/login` (:145). Uso EXCLUSIVO da reconciliação batch e reprocesso pontual (`core/backend/lib/monetarie/workers/pix_status_reconciliation.ex:146`); comprovante/extrato nunca disparam a cabine. Fail-closed sem fallback SQL (:10-14) | `PIX_CABIN_API_BASE_URL` (fallbacks `PIX_ADMIN_BASE_URL`, `PIX_API_BASE_URL`), `PIX_CABIN_API_TOKEN` ou `PIX_CABIN_ADMIN_LOGIN`+`PIX_CABIN_ADMIN_PASSWORD` (:96-118) |
| Cabine PIX (operações camt.060) | `CabinOperationQuery` | `POST /api/v1/operations/query` (`core/backend/lib/monetarie/services/pix_providers/in_house/cabin_operation_query.ex:11`) | reusa as mesmas envs |
| Cabine PIX (tesouraria) | `CabinTreasury` | `GET /api/v1/balance` e `GET /api/v1/transactions` liquidadas por janela (`core/backend/lib/monetarie/services/pix_providers/in_house/cabin_treasury.ex:76,169`) | idem |
| Cabine SPB | `Monetarie.Services.SpbIntegration.CabinStatusLookup` | `GET /api/messages?search=<transaction_id>&limit=20` com pós-filtro exato e `by_transaction_id/1` (`core/backend/lib/monetarie/services/spb_integration/cabin_status_lookup.ex:230,48`); login `POST /api/auth/login` (:175), token cacheado 5min. Usado por `SpbStatusReconciliation` (`core/backend/lib/monetarie/workers/spb_status_reconciliation.ex:189`) | `SPB_CABIN_API_BASE_URL` (fallbacks `SPB_ADMIN_BASE_URL`, `SPB_API_BASE_URL`), `SPB_CABIN_API_TOKEN` ou `SPB_CABIN_ADMIN_LOGIN`+`SPB_CABIN_ADMIN_PASSWORD` (secret `monetarie/{env}/admin/spb/initial_password`) (:107-116) |
| Webhooks de parceiro | `Monetarie.Workers.Services.Webhooks.DeliveryJob` | POST na URL do parceiro, assinatura HMAC-SHA256 (`X-Monetarie-Signature/Timestamp/Event-Id/Event-Type`), bloqueio de IP privado e HTTPS obrigatório em prod (`core/backend/lib/monetarie/workers/services/webhooks/delivery_job.ex:109-220`) | `WEBHOOK_ENCRYPTION_KEY` (runtime.exs:463) |
| STA (regulatório) | `Monetarie.Sta.Client.Http` | `POST /api/v1/files`, `GET /api/v1/files/:id` (`core/backend/lib/monetarie/sta/client/http.ex:26,39`) | `STA_BASE_URL`, `STA_WEBHOOK_SECRET`, `MONETARIE_STA_WEBHOOK_URL`, `CCS_STA_MODE` (runtime.exs:757-779) |
| Twilio (SMS) | `Monetarie.UseCases.Auth.TwilioClient` | `POST api.twilio.com/.../Messages.json` (`core/backend/lib/monetarie/use_cases/auth/twilio_client.ex:19,33`) | `TWILIO_ACCOUNT_SID/AUTH_TOKEN/FROM_NUMBER/ENABLED` (runtime.exs:682-687) |
| Nextcode (KYC) | `Monetarie.Services.Nextcode.Client` | API biométrica/bureau | `NEXTCODE_API_URL/API_KEY/ENABLED` (runtime.exs:746-748) |
| NFe.io, Núclea, BCB Olinda PTAX, Tesouro/ANBIMA, e-Financeira RFB (SOAP mTLS) | ver `core/backend/lib/monetarie/use_cases/{integrations,dda,cambio,master_data,regulatory/efinanceira}/` | cotações, DDA/portabilidade, notas fiscais, declarações | `NFE_IO_*`, `NUCLEA_PCR_*`, `EFINANCEIRA_*` (runtime.exs:134-253, 728-742) |

## 9. Endpoints e portas

Backend `core-api`: Bandit HTTP na porta `PORT` (default 4000, `core/backend/config/runtime.exs:474`). Health checks públicos: `GET /health` e `GET /readiness` (`core/backend/lib/monetarie_web/router.ex:100-101`). Total de 2.114 rotas em `core/backend/lib/monetarie_web/router.ex` (3.940 linhas). Grupos principais:

| Grupo de rotas | Rotas | Pipelines | Consumidor |
|---|---|---|---|
| `GET /health`, `GET /readiness` | 2 | `:api` | ALB/ECS health check |
| `GET /api/openapi`, `/api/swaggerui` | 2 | `:api` | documentação pública |
| `POST /api/partner/v1/oauth/token` | 1 | `:api` (anônimo) | parceiros (troca client_id+secret por Bearer) |
| `/api/partner/v1/*` (customers, accounts, pix, transfers, webhooks) | 45 | `[:api, :partner_authenticated, :idempotent]` (router.ex:133) | Partner API server-to-server |
| `/api/v1/*` (wallets, pix, ted, spb, accounts, treasury, payables, cnab, regulatory, compliance, tenants, open-finance, reports...) | 1.015 | `[:api, :authenticated, :entity_scoped, :user_context, :staff_scoped, :staff_auth, :ensure_mfa, :audited]` (router.ex:251) | admin console e módulos internos |
| `/api/admin/*` + aliases `/api/*` de admin | 342 + 69 | `[:api, :authenticated, :admin_only, :entity_scoped, :ensure_mfa, :audited]` (router.ex:2297) | core-admin-ui |
| `/api/*` scope V2 (IB e merchant, inclui `/api/merchants/:merchant_id/*` com 133 rotas) | 242 + 339 | `[:api, :authenticated, ..., :rate_limited_api, :audited, :idempotent]` (router.ex:2982) | core-banking-ui e core-merchant-ui (não existem prefixos `/api/ib` ou `/api/merchant`) |
| `/api/auth/*` (login CPF v2, refresh, MFA) | 27 | `:rate_limited_auth` ou `:authenticated` (router.ex:2202-2263) | IB/merchant/admin login |
| `/api/onboarding/*` | 24 | `:rate_limited_auth` + `:authenticated` | onboarding PF/PJ |
| Webhooks de ENTRADA: `POST /webhooks/nfe`, `POST /api/webhooks/nextcode/...`, `/api/internal/*` (STA, HMAC) | 4 | `:rate_limited_api` / `:sta_webhook` (router.ex:218-237) | NFe.io, Nextcode, STA |
| WebSockets `/socket` e `/live` | | `core/backend/lib/monetarie_web/endpoint.ex:13-19` | channels (onboarding etc.) |

Frontends: nginx escutando na porta 8080 (non-root, uid 101) com proxy de `/api/` e `/socket` para o Core API (`core/apps/banking/nginx.conf:12,74`; admin usa `envsubst` com `CORE_API_UPSTREAM` default `core-api-h.monetarie.internal:80`, `core/apps/admin/nginx.conf:12,20` e `core/apps/admin/Dockerfile:76-77`).

Portas de infraestrutura consumidas pelo Core: Aurora 5432 (`DATABASE_URL`), NATS 4222 (`NATS_HOST/NATS_PORT`), TigerBeetle 3001 (`TIGERBEETLE_ADDRESSES`, IP obrigatório), Redis 6379 (`REDIS_URL`).

## 10. Env vars por função (principais)

| Função | Vars (default) | Evidência |
|---|---|---|
| Banco | `DATABASE_URL` (obrigatória), `POOL_SIZE` (30), `BATCH_POOL_SIZE` (10), `READ_REPLICA_URL`, `READ_POOL_SIZE` (20), `DB_STATEMENT_TIMEOUT` (30000) | runtime.exs:320-392 |
| NATS | `NATS_ENABLED` (true), `NATS_HOST` (localhost), `NATS_PORT` (4222), `NATS_HOSTS` (CSV failover), `NATS_STREAM_REPLICAS` (3), `NATS_STREAM_MAX_BYTES` | application.ex:26; runtime.exs:543-544; connection.ex:208; stream_setup.ex:254 |
| TigerBeetle | `TIGERBEETLE_ADDRESSES` (obrigatória, IP:porta), `TIGERBEETLE_CLUSTER_ID` (0), `TB_POOL_SIZE` (3), `DEFAULT_LEDGER_ID` (1), `INTER_ENTITY_LEDGER_ID` (99), `CASH_ASSET_ACCOUNT_ID` (1) | runtime.exs:421-459 |
| Flags TB/escala | `TB_DEPOSIT_COALESCE_ENABLED` (false), `TB_COALESCE_WINDOW_MS` (5), `TB_BULK_READ_ENABLED` (false), `TB_CIRCUIT_BREAKER_ENABLED` (true), `TB_LOOKUP_RETRY` (true) | runtime.exs:70-94; tigerbeetle.ex:245 |
| Flags money path | `TWO_PHASE_PIX_IN` (false, PROIBIDA em prod), `BALANCE_CHECKPOINT_ENABLED` (false), `BALANCE_CACHE_ENABLED`/`USAGE_CACHE_ENABLED`/`TX_STATUS_CACHE_ENABLED` (false), `PIX_OUT_DISABLED`, `PIX_OUT_MAX_AMOUNT_BU`, `PIX_OUT_PENDING_RECOVERY_*`, `PIX_OUT_RETRY_QUEUE_ENABLED` (false), `STUCK_OUTBOUND_*`, `MED_OBLIGATION_ENFORCEMENT_ENABLED` (false), `PIX_IN_COSIF_PROJECTION_ENABLED` (false), `ACCOUNT_ROLLUPS_ENABLED` (false), `SPB_AUTO_RETURN_ENABLED` (false), `ESCROW_ENABLED` (false) | runtime.exs:13-126, 412-416 |
| Cabine PIX (HTTP) | `PIX_CABIN_API_BASE_URL`, `PIX_CABIN_API_TOKEN`, `PIX_CABIN_ADMIN_LOGIN`, `PIX_CABIN_ADMIN_PASSWORD` (lidas direto no módulo, não em runtime.exs) | cabin_status_lookup.ex:96-118 |
| Cabine SPB (HTTP) | `SPB_CABIN_API_BASE_URL`, `SPB_CABIN_API_TOKEN`, `SPB_CABIN_ADMIN_LOGIN`, `SPB_CABIN_ADMIN_PASSWORD` | spb_integration/cabin_status_lookup.ex:107-116 |
| HTTP/endpoint | `PORT` (4000), `PHX_HOST`, `CORS_ORIGINS`, `SECRET_KEY_BASE` (obrigatória), `DRAIN_LAMEDUCK_MS` (5000), `DRAIN_SHUTDOWN_TIMEOUT_MS` (90000) | runtime.exs:466-499; endpoint.ex:60-64 |
| Auth/JWT | `GUARDIAN_SECRET_KEY` (obrigatória), `ADMIN_EMAIL`/`ADMIN_PASSWORD` (seeds), `MFA_ENFORCEMENT` (optional), `MFA_GRACE_DAYS` (7), `WEBHOOK_ENCRYPTION_KEY` | runtime.exs:463-537, 692-711 |
| Instituição | `INSTITUTION_CNPJ/ISPB/NAME/AGENCY` (obrigatórias em prod), `INSTITUTION_COMPE_CODE` (526) | runtime.exs:510-528; config.exs:12-16 |
| Oban | `OBAN_*_QUEUE_SIZE` (nats_publish 10, webhooks 5, regulatory 5, cosif 5, pix_in_pg_write 10 etc.) | runtime.exs:662-677 |
| Regulatório | `EFINANCEIRA_*` (modo, certificados, RFB), `DERE_*`, `STA_*`, `CCS_STA_MODE` | runtime.exs:134-253, 757-819 |
| Integrações | `TWILIO_*`, `NEXTCODE_*`, `NFE_IO_*`, `NUCLEA_PCR_*`, `REDIS_URL` | runtime.exs:540, 682-748 |

## 11. Serviços ECS e frontends

| Serviço ECS | Código fonte | Porta do container | Observações |
|---|---|---|---|
| `core-api` | `core/backend` (release `bin/monetarie`) | 4000 | Task family `monetarie-core-api-homolog` (HML, cluster `monetarie-greenfield-homolog`) e equivalente prod no cluster `monetarie-greenfield-prod`; health `/health`; migrations via one-off task `Monetarie.Release.migrate()` |
| `core-admin-ui` | `core/apps/admin` | 8080 (nginx) | Nome confirmado no health do nginx (`core/apps/admin/nginx.conf:82`); host `coreadmin*`; upstream configurável por `CORE_API_UPSTREAM` via envsubst (`core/apps/admin/docker-entrypoint.sh`) |
| `core-banking-ui` (IB) | `core/apps/banking` | 8080 (nginx) | Health nginx (`core/apps/banking/nginx.conf:64`); host `ib*`; upstream fixo `core-api-h.monetarie.internal` no nginx.conf:12 |
| `core-merchant-ui` | `core/apps/merchant` | 8080 (nginx) | Health nginx (`core/apps/merchant/nginx.conf:64`); host `merchant*`; upstream fixo idem |

Frontends (todos Vue 3 + PrimeVue + Pinia + axios; build Vite):

- Base da API: `import.meta.env.VITE_API_URL || '/api'` (`core/apps/admin/src/lib/api.ts:9`, `core/apps/banking/src/lib/api.ts:4`, `core/apps/merchant/src/http/api.ts:4`). Em produção `VITE_API_URL=/api` (`.env.production` de cada app): o caminho relativo é resolvido pelo nginx do próprio container, que faz proxy para o `core-api`. `VITE_*` é bakeado no build.
- Autenticação: admin usa token `admin_token`; IB e merchant usam `monetarie_token` (login por CPF, JWT Guardian).
- `@monetarie/shared` (`core/packages/shared/package.json`): exporta `./types`, `./api` (cliente HTTP compartilhado), `./i18n` (pt-BR/en), `./utils`, `./theme`, `./composables`, `./components` (SFCs servidos de `src/`).

## 12. O que NÃO se deve inferir deste documento

- A tabela `transactions` é única; `payment_transactions` não existe.
- O stream `MONETARIE_PIX` citado em documentos antigos foi descontinuado; o Core o remove no boot e usa `MONETARIE_SPI`.
- As envs das cabines PIX/SPB são lidas direto nos módulos cliente com `System.get_env`, então não aparecem em `runtime.exs`.
- `TWO_PHASE_PIX_IN` existe no código, mas ligá-la em produção fere o canônico de 2026-07-09 (o gatilho settled só existe no simulador; cliente nunca seria creditado).
- Unidade monetária não é uniforme entre tabelas: conferir a seção 6.1 e o dicionário de dados antes de ler valores.
