# Handoff 2026-07-27: paridade com o coreproviders, motor de tarifa e split, caixa institucional, segurança de compliance

**Sessão dedicada.** Comparação total Monetarie x `/Users/luizpenha/coreproviders`
(branch `aviv-hml`), com discernimento entre o que é CloudPIX OnZ e o que é a nossa
cabine própria. Regra da sessão: **zero inferência, evidência `arquivo:linha` para tudo,
e o que não foi provado está escrito como não provado.**

**HEAD ao fechar:** `8cd10733`. `origin/main == main`. Working tree limpo.
**Suíte do Core:** 9078 testes, 55 properties, 25 doctests, **0 falhas**.

---

## 1. Instruções do dono nesta sessão (todas, na íntegra)

Estas são as regras que valem. Se algo neste documento contradisser alguma delas, a
regra vence.

### 1.1. Tarifa

1. **PIX NÃO PODE SER COBRADO DE PF. NENHUM TIPO.** Reafirmado de forma enfática
   depois de eu ter interpretado errado uma primeira vez. Os dez tipos `pix_*` do
   catálogo estão vedados.
2. **PF paga TED.** A vedação vale só para PIX.
3. **PJ sempre paga.**
4. "Demais itens podem ser cobrados" significa os **demais itens de tarifa** do
   catálogo (TED, boleto, conta, cartão, cheque), **não** outros tipos de PIX. Eu li
   errado uma vez; está corrigido e travado por teste de cobertura.
5. **TEF não tem tarifa hoje**, mas tem que ser **configurável**. Nada cravado no
   código.
6. **Nada de valor hardcoded** no caminho de tarifa.

### 1.2. Caixa institucional

7. Criar uma conta **nova**, `accounts.kind = 21`, como caixa institucional.
8. O número vem da **sequência de numeração de conta que o sistema já usa**
   (`account_number_seq` + dígito módulo 11). Nada escolhido a mão.
9. Ela recebe **o líquido**, depois de pagas as comissões dos intermediários.
10. Seguir o fluxo **como roda no coreproviders**, que opera 100% lá.
11. A indicação inicial era a conta `52600005-0`; **essa decisão foi revista**: conta
    nova, número novo. A `52600005-0` (id 2630, kind 3) fica como está.
12. O COSIF: o dono valida com o cliente e encaminha.

### 1.3. Segurança

13. **MFA obrigatório no merchant-front** para o operador humano.
14. **Whitelist de IP fail-closed para tudo que é API**, configurável pelo próprio
    merchant no painel e **totalmente integrada ao WAF da AWS**.

### 1.4. Escala

15. Alvo de **3 milhões de operações por dia**, com mix de **75% PIX-in praticamente
    todo por QR Code e 25% PIX-out**.
16. **Portar o TB Gateway em Go** do coreproviders, para lançamento em lote no
    TigerBeetle, e não uma conexão por operação.

### 1.5. Método

17. Comparação **total** com o coreproviders, com discernimento de que **aqui roda a
    nossa cabine própria e lá roda a CloudPIX OnZ**.
18. **Auditoria completa** antes de sair codificando.
19. **Fee split tem que ser igual ao que roda no coreproviders**, que é **geral**.
20. Push **só com OK explícito**, a cada vez (regra do `CLAUDE.md`, reafirmada na
    prática).

---

## 2. O discernimento que sustenta o porte

Os dois sistemas têm **a mesma costura de provedor**, e é isso que torna o porte viável.

| | coreproviders (AvivPay) | Monetarie |
|---|---|---|
| Contrato | `services/pix_providers/behaviour.ex` + `provider.ex` | idêntico |
| Adapter | `pix_providers/onz/` (24 arquivos) | `pix_providers/in_house/` (5 arquivos) |
| Quem fala com o BACEN | **a OnZ CloudPIX**, HTTP puro porta 80 via peering | **a nossa cabine**: `dict_service`, `spi_service`, `settlement_service` |
| Assinatura | delegada à OnZ | HSM RTM, chave privada nunca na aplicação |

**Regra operacional:** nada da pasta `pix_providers/onz/` se porta. Tudo acima da costura
(contas, subcontas, tarifas, split, API externa, MED, infração, webhooks, telas) é
portável, porque nos dois lados fala com a mesma `Provider`.

**Corolário que gerou o achado de segurança mais grave da sessão:** a cabine, como a
OnZ, responde por **ISPB da instituição** e **não conhece merchant nem subconta**. Todo
filtro de propriedade tem que ser nosso, do lado de cá.

---

## 3. O que foi entregue (8 commits, todos em `origin/main`)

| Commit | Entrega |
|---|---|
| `1557181d` | Teste de regressão da vedação PF. **O código foi para dentro do `349990cd` de uma sessão concorrente**, que commitou a árvore inteira sob uma mensagem que só falava de backfill. O commit `1557181d` registra o que aquele carrega de fato |
| `6d98912b` | Auditoria de MED e infração: acha o IDOR vivo |
| `0985abe8` | Fecha o vazamento de MED e infração entre merchants |
| `bc97a13c` | TEF vira tipo de tarifa configurável |
| `4d45cb0b` | Infraestrutura do caixa institucional kind 21 |
| `0510f745` | Provisionamento do caixa, com ensaio |
| `b6655dda` | Acrescenta `pix_in_purchase` à vedação |
| `8cd10733` | **PIX não se cobra de PF: nenhum tipo**, com trava por cobertura |

### 3.1. Motor de tarifa e split

- `@max_split_participants` de **10 para 6**. O motor usa `leg_id(root, 10 + idx)` e
  `BatchChain` tem `@max_batch_legs 16` com `raise` para `n >= 16`. Com 7 participantes o
  changeset aceitava e a exceção estourava **dentro do batch TigerBeetle do PIX-in, na
  liquidação**. O porte trouxe 10 e perdeu o comentário que explicava a matemática.
- **`client_type: :pj` fixo** em `outbound/pix.ex:426`, `ted.ex:250`, `tef.ex:178` e
  `outbound_requests.ex:213`, com o `account_id` chegando e sendo descartado. Toda conta
  PF era tarifada no PIX de saída. Novo módulo `UseCases.Fees.ClientType` resolve pela
  conta. **O coreproviders tem o mesmo defeito** (`outbound_payment/pix.ex:1066`); lá é
  latente porque o PIX-out sai de merchant PJ.
- No `outbound_requests.ex` eram **três** valores fixos: além do `client_type`,
  `act_type: :cooperativo` e `is_member: true`, que gravavam classificação de ato errada
  em `fee_transactions` e contaminavam o COSIF. Agora vêm do `ActClassifier`.
- **`merchant_id` sempre nil** no `AccountResolver` (`banking/account_resolver.ex:448`),
  com comentário dizendo "Monetarie has no merchants concept". **Desatualizado**: a
  tabela existe desde 22/07 (migration `20260722100000`). Com o nil, o
  `find_merchant_split/3` cortava na primeira cláusula e **o nível merchant do split
  ficava morto**. Corrigido, com normalização de UUID obrigatória
  (`accounts.merchant_id` é `binary_id`, `fee_split_configs.merchant_id` é `varchar`;
  sem carregar para a forma canônica a comparação nunca casa).
- **`TtlCache`** portado e ligado no `SplitResolver` (60s, cacheando o `nil`). A cascata
  custa até seis idas ao banco e, para a conta comum sem split, todas as seis erram.
- **`@institutional_zero_fee_kinds [21, 22]`** no `FeeCalculator`. Sem ele a instituição
  cobraria tarifa de si mesma na PACS.004 de sub-rogação da conta de devoluções.
- **TEF**: `:tef` não existia nem no catálogo do Elixir nem no enum `fee_type_enum` do
  Postgres. Um `SELECT ... WHERE fee_type = 'tef'` contra coluna enum sem esse rótulo
  **levanta exceção**, e o `rescue _ -> {:ok, 0}` engolia. A isenção era uma exceção de
  banco capturada. Migration `20260727140000` (aditiva) mais entrada no catálogo.
- **`fee_type_for("tef")` devolvia `:pix_out_transfer`**: as duas metades do motor
  discordavam. O cálculo usava `:tef` e a cobrança classificava como PIX de saída.
- **Vedação PF**: passou de 4 para **10 tipos**, todo `pix_*` do catálogo. O
  coreproviders veda três e ainda cobra saque e troco de PF depois da franquia. Aqui
  ficamos mais rigorosos, por decisão do dono.

### 3.2. Caixa institucional

- `Payments.CaixaInstitucionalMirror`, porte do `CaixaAvivMirror`, com as **quatro
  portas fail-safe** (flag desligada, caixa não mapeado, tarifa zero, conta ausente no
  TB). Todas devolvem `[]` e o batch segue sem a perna. **Falta de conta de apropriação
  nunca pode impedir o dinheiro do cliente de liquidar.**
- `AccountCode.caixa_institucional = 21` e
  `TransferCode.revenue_apropriation = code 53 / sharp_code 5099`, **idênticos aos do
  coreproviders**, para os dois sistemas seguirem comparáveis em conciliação. O 21
  coincide de propósito com `accounts.kind`, o que faz a checagem de existência no TB
  virar verificação de **tipo**.
- `Release.CaixaInstitucional.provision/1`: número pela sequência, `dry_run` ligado por
  padrão, **idempotente por entidade**, e o **ensaio não consome a sequência** (lê
  `last_value`, só a execução real chama `nextval`). `cosif_code` é **obrigatória e sem
  default**.
- `ensure_hot_path_caches/0` no boot. **`AccountResolver.ensure_cache_tables/0` não era
  chamado em lugar nenhum**, só nos testes: em produção a tabela ETS nunca existia e
  toda resolução de conta ia ao banco.

### 3.3. Segurança de compliance

- `UseCases.Compliance.ScopeGuard` novo, fechando **IDOR vivo** em duas superfícies.

---

## 4. Estado vivo, conferido contra a AWS e os bancos em 27/07

Nada aqui foi copiado de memória. Tudo veio de `./scripts/estado-vivo.sh`, `aws ecs
describe-*` e `bin/monetarie rpc` no nó vivo (ECS Exec, somente leitura).

| Item | Valor |
|---|---|
| `AWS_WAF_SYNC_ENABLED` | **`true` nos dois ambientes**, IPSets `monetarie-extapi-clients-h` e `-p` |
| `TB_POOL_SIZE` | homologação **4**, produção **1** |
| `core-api` PRD | `launchType EC2`, **desired 1**, task **512 CPU (meia vCPU)** e 1024 MB |
| Cluster EC2 PRD | **2 x `t4g.small`** burstable, 1846 MB registrados, 822 livres |
| `pix-api`, `spb-api`, `sta-api` | Fargate |
| `DICT_BUDGET_MODE` | `enforce` nos dois; categoria **A** em HML, **G** em PRD |
| Migrations aplicadas | 322 nos dois, maior é `20260725210000` |
| Conta `052600005` | existe em PRD: **id 2630, kind 3**, DV 0, COSIF `4.9.8.10.01.10.002` |
| `accounts.kind` em uso | **só kind 3**, nas 1607 contas de PRD |
| `merchants` / `merchant_users` | **0 e 0** em PRD |
| Contas com `merchant_id` | **0 de 1607** |
| `fee_configs` / `fee_split_configs` / `fee_split_transactions` / `fee_transactions` | **0 / 0 / 0 / 0** |
| Movimento real PRD | **140 PIX e 138 TED**, todos em julho de 2026 |
| Volume diário, 7 dias | entre **1 e 63**. Pico de **49 por hora** |
| `cosif_accounts` | **369 linhas em PRD, 229 em HML, ZERO com alias nos dois** |

**Duas leituras que precisam sobreviver a esta sessão:**

1. As 14.219 linhas de `transactions` com `type='fee'` são do **ETL do legado**, não do
   motor vivo. Vão de julho de 2025 a junho de 2026 e são **zero em julho de 2026**, que
   é o mês de todo o movimento PIX e TED real. O `FeeCharger` grava em
   `fee_transactions`, que está vazia. **O motor de tarifa portado nunca cobrou nada em
   produção.**
2. **Não há remediação de dados a fazer nas subcontas.** `merchants` está zerada, então
   nenhuma subconta nasceu pelo caminho fraco. Dá para consertar o `SubcontaCreator`
   antes do primeiro dado nascer torto.

---

## 5. PENDÊNCIAS

### 5.1. P0, bloqueia a frente de receita inteira

**Mapa COSIF não carrega em nenhum ambiente.** `cosif_accounts` tem 369 linhas em PRD e
229 em HML, e **zero com `alias`** nos dois. O `alias` é a coluna que liga o plano de
contas ao código. Sem ela:

```
MappedCosifAccounts.load!()  ->  levanta excecao pedindo as 20 chaves obrigatorias
MappedCosifAccounts.get()    ->  "MappedCosifAccounts not loaded!"
```

Só funciona no banco de teste, que é semeado. **É a causa única** que explica
`fee_transactions = 0`, `fee_split_transactions = 0` e o split nunca ter rodado,
independente das flags. Mesmo com a flag ligada, o primeiro `mapped.fee_pix.id`
levantaria exceção.

**Trabalho:** montar o de-para das 20 rubricas obrigatórias (`spi`,
`investment_asset`, `settlement_pool`, `client_liability`, `boleto_transit`, `capital`,
`cash_itau_main`, `cash_itau_boletos`, `cash_reserve_bank`, `pix_out_transit`,
`pix_in_transit`, `ted_out_transit`, `ted_in_transit`, `titulo_transit`,
`revenue_pf_monthly`, `revenue_mei_monthly`, `fee_pix`, `fee_ted`, `fee_titulo`,
`fee_conta`) sobre o plano de 369 contas que já existe, mais o alias novo
`caixa_institucional`. A conta "Caixa - moeda corrente" `1.1.1.10.01.10.001` já está no
plano. **Exige aval contábil do dono.** Ele ficou de encaminhar o COSIF.

O dono foi consultado sobre eu **propor** o de-para (código, nome e uso de cada rubrica
no motor) para ele revisar. **Não houve resposta ainda.**

### 5.2. P0, brecha de isolamento

**`SubcontaCreator` não portado.** `admin/merchants_controller.ex:713` tem 35 linhas
contra 568 do `SubcontaCreator`. Faltam 11 coisas, três graves:

1. **Sem `merchant_id` na conta.** É o campo que barra reuso entre merchants
   (`subconta_creator.ex:90`). Sem ele **não há isolamento**.
2. **Sem chave EVP registrada no DICT.** Lá é passo transacional dentro do `Ecto.Multi`:
   se falha, a subconta é revertida. E a chave vai com **CNPJ e razão social do
   merchant**, nunca com o CPF do operador (`subconta_creator.ex:184`).
3. **Senha temporária no corpo do JSON** (`temporaryPassword`), em vez de e-mail.

Faltam ainda: validação de CPF/CNPJ com dígito verificador, bloqueio do documento do
próprio titular, bloqueio de CPF já vinculado a outro e-mail, `MerchantUser` com `role` e
teto de `capabilities`, `AccountAccess`, `disable_ib`, e `"name" => email`.

**Adaptação obrigatória:** onde o coreproviders chama
`Provider.create_key(entity_id, dict_params, merchant_cnpj)` contra o adapter OnZ, aqui
chama a mesma função contra o `in_house`, que resolve por NATS no `dict_service`.

**Não bloqueia nada e não depende do COSIF.** Era a sugestão de próximo passo.

### 5.3. Claims sem escopo de propriedade

As ações de **claim** (reivindicação de chave) em `v2/dict_controller.ex` seguem sem
filtro: `list_claims`, `show_claim`, `confirm_claim`, `complete_claim`, `cancel_claim`.
Elas **não se amarram por E2E** e sim pela posse da **chave** (`pix_keys.account_id`),
critério diferente que precisa de desenho próprio. Registrado no `@moduledoc` do
controller.

### 5.4. Segurança do merchant-front e da API (tarefa 5)

- **MFA opcional no merchant-front.** `merchant/auth_controller.ex:94` faz
  `if user.totp_enabled do` e, no `else`, entrega token de **8 horas só com senha**. É o
  furo que o commit `c6bcc223` fechou nos 4 consoles admin e que **continua aberto na
  porta das subcontas**. A decisão do dono é MFA obrigatório, com enrollment forçado no
  primeiro acesso.
- **Integração com o WAF incompleta.** O coreproviders enfileira o sync **a cada mudança
  de chave** (`use_cases/api_keys/commands.ex:193`) e faz sync **no boot**
  (`application.ex:495`). Aqui só existe o reconciliador de cron a cada 5 minutos
  (`config/runtime.exs:659`), então há **drift de até 5 minutos** entre o banco e a borda.
  A flag está ligada nos dois ambientes.
- **`IpWhitelistRule` é CRUD órfão.** Tabela, schema e tela admin existem, e **nenhum
  plug consome**. Precisa de destino: vira a fonte da whitelist administrativa ou sai.
- Alerta herdado do coreproviders: o WAF **recusa CIDR `/0`** e recusa o `UpdateIPSet`
  inteiro, então uma entrada ruim congela a allowlist de todos. O `ApiKeyBundle` daqui já
  descarta `/0` na saída; falta confirmar a validação de entrada.

### 5.5. Fechar o fluxo do caixa institucional

- **Ligar a perna nos quatro caminhos.** Hoje o `SplitDistributor` é chamado em PIX-in
  (`tb_first/deposit.ex:124`) e MED (`med/hold_legs.ex:299`); **falta PIX-out e
  devolução**. E o `CaixaInstitucionalMirror` **não está ligado em lugar nenhum**,
  inclusive no PIX-in. O coreproviders chama em `outbound_payment/pix.ex:653,671` e
  `return.ex:250`.
- **Provisionar a conta kind 21**, com o COSIF que o dono vai encaminhar.
- **Pôr `CAIXA_INSTITUCIONAL_ACCOUNT_ID`** na task definition, depois do provisionamento.
- **Ligar as flags** `enable_fee_split` e `enable_caixa_institucional_mirror`, hoje
  desligadas nos dois ambientes. **Só depois do COSIF**, senão liga cano em caixa d'água
  vazia.
- **`execute_and_record/6`** (`split_distributor.ex:117`) **não existe no
  coreproviders**. Usa `Util.ID.int128()` como id da transferência, ou seja **id
  aleatório**, e submete **batch separado**. O TigerBeetle deduplica por id: **um retry
  paga a comissão duas vezes**. Hoje é código morto (só testes referenciam), mas está
  armado. Remover ou reescrever sobre `leg_id`.
- **Faltam 8 módulos** do motor: `EnsureVulciSplit` (instala a config global de
  entidade), `backfill_vulci_split`, `split_commands`, `split_queries`,
  `split_history_queries`, `commission_report`, `fee_resolver`, `monthly_batch`.

### 5.6. MED e infração: paridade de contrato

- **Superfície `/api/integration/*` ausente por inteiro.** 10 rotas de gestão
  programática de MED, com token de integração próprio e HMAC nas escritas. E a
  Monetarie **tem a tela que emite esses tokens** (`/med-integration-tokens`): **emite
  credencial para uma API que não existe**.
- **10 ações administrativas ausentes:** `GET /infractions/export`, `POST /infractions`,
  `POST /med-recoveries`, `GET /med-defenses`,
  `POST /med-cautelar-blocks/:id/evidence`, `POST /med-cautelar-blocks/:id/execute-return`,
  `GET /med-cautelar-blocks/:id/refunds`, `POST /med/cautelar/manual`,
  `POST /med/drain/:account_id` (o `unfunded_drainer.ex` existe, o endpoint não) e
  `GET /pix-compliance/med-notifications`.
- **Casos de uso ausentes:** `manual_cautelar_block`, `orphan_hold_reconciler`,
  `infraction_enrichment`, `close_reconciliation`, `recovery_queries`, `report`.
- **A Monetarie tem a mais:** `POST /pix/med/:id/refund` e `GET /pix/med/:id/graph` no
  `partner_v1`, e o fluxo de defesa com upload e remoção de documentos no merchant.

### 5.7. Escala para 3 milhões por dia

**O alvo é construção de capacidade, não otimização.** Produção faz hoje entre 1 e 63
transações por dia, com pico de 49 por hora. O alvo são 34,7 por segundo, **da ordem de
2.500 vezes o pico atual.**

- **Dimensionar o `core-api`** (tarefa 9). Ele faz ledger, materialização, tarifa e
  split, e roda em **meia vCPU de uma `t4g.small` burstable**, com `desired 1` e
  `TB_POOL_SIZE=1`. **É a primeira parede**, antes do TB Gateway e do cache de JWS.
- **Portar o TB Gateway em Go** (tarefa 6). A Monetarie tem a **mesma arquitetura NIF**
  que gerou o incidente na Aviv. `clients_max=64` é limite hard compilado na imagem
  oficial e o NIF é 1 request em voo por conexão. Subir o pool é o anti-padrão que a
  documentação do TigerBeetle proíbe. O gateway resolve estruturalmente: **um client
  oficial compartilhado por instância**, auto-batching até 8189, 2 a 3 instâncias ECS.
  **Faseamento inegociável:** fase 1 só leitura (`lookup_account`), fase 2 escrita
  (`create_transfers`) com idempotência, fase 3 o resto. NIF vivo como fallback por flag
  **por tipo de chamada**. Duas lições já pagas pela Aviv entram desde o dia 1: debounce
  de reconexão (`TB_GATEWAY_RECONNECT_DEBOUNCE_MS`, máximo um drop por 10s) e remoção do
  `GRPC.Stub.disconnect`, que causa `FunctionClauseError` no grpc-elixir 0.11.5.
  Referências: `docs/plans/2026-07-07-tb-gateway-grpc-design.md` e os runbooks fase 0/1/2
  do coreproviders.
- **Cache do JWS do QR** (tarefa 7). `payload_controller.ex:39` **re-assina a cada GET**
  do PSP pagador, sem cache. São 2,25 milhões de assinaturas RSA por dia no mix do dono.
  É assinatura local (`JwsSigner` usa `CertificateManager.get_private_key` mais
  `JOSE.JWS.sign`, **não o HSM**), barata a 26/s de média, mas desperdício puro e
  gargalo de CPU no pico.

**O balde do DICT NÃO é a parede neste mix.** PIX-in por QR não consome ficha nossa; a
ficha volta na liquidação da pacs.008 (implementado, ver
`dict_budget_calibration_worker.ex`); o balde G se comporta como **limite de
concorrência, não de vazão diária**: 8,7/s de média com latência de 3,1s dão ~27
consultas em voo contra teto de 250, folga de nove vezes. A tabela de categorias foi
conferida contra o Manual 8.2 item 13.1 (`dict_rate_limit.ex:31`).

### 5.8. Auditoria ainda não fechada (tarefa 1)

- Contrato `/api/external` payload a payload contra a documentação publicada.
- Conteúdo das 94 telas homônimas do merchant-front. **Nome igual não é código igual.** O
  único nome ausente é `WebhookDeliveriesView.vue`; os outros quatro são renomeação
  (`AcceptInvitation`/`InviteAccept`, `PixView`/`PixHub`, `SetPassword`/`ResetPassword`,
  `Setup2FA` + `Confirm2FARevocation`/`Verify2FA`).
- PIX-in e PIX-out ponta a ponta.

### 5.9. Aval pendente do dono

- **COSIF** das 20 rubricas e da conta de caixa.
- **`pix_automatic`**: mantive vedado, na leitura de que o PIX Automático debita o
  pagador, então é um PIX saindo da conta do PF. Se o dono entender que é cobrável, sai
  da lista. Não mexi por conta própria porque o efeito é liberar cobrança.
- **Vedações legais configuráveis?** `@pix_pf_vedado` e `@essential_pf_zero` estão
  cravadas de propósito: torná-las configuráveis permitiria a um admin **configurar
  cobrança ilegal**. O coreproviders faz igual e documenta igual. Se o dono quiser
  configuráveis, é decisão dele.

---

## 6. Avisos operacionais

### 6.1. Deploy do core-api em curso

O dono autorizou **outra sessão** a fazer build e deploy do `core-api`. A liberação foi
dada com o HEAD em **`0510f745`**. Depois disso entraram **dois commits de vedação**:
`b6655dda` e `8cd10733`.

**Se o build saiu de `0510f745`, a vedação total de PIX para PF não está nele** e
produção segue tarifando PF em PIX. Precisa de um segundo deploy em `8cd10733` ou
posterior.

### 6.2. Migration pendente

`20260727140000_add_tef_to_fee_type_enum`, nos dois ambientes (ambos em
`20260725210000`, 322 aplicadas). Faz `ALTER TYPE fee_type_enum ADD VALUE IF NOT EXISTS
'tef'`. **Puramente aditiva**, nenhum `UPDATE` em linha existente. Ordem correta é
migration antes do deploy, mas se inverterem **nada quebra**: sem ela o comportamento
volta ao de hoje (exceção capturada, tarifa zero).

### 6.3. Sessão concorrente commitou trabalho meu

O commit `349990cd` ("Backfill do nome do recebedor...") carrega, além do backfill dele,
**os 7 arquivos do motor de tarifa e o documento de desenho**. Não reescrevi o histórico
porque a outra sessão estava viva. O commit `1557181d` registra o que aquele carrega de
fato. **Se for auditar o que mudou no motor, olhe os dois juntos.**

### 6.4. Três mudanças de comportamento que sobem vivas

1. **Conta PF deixa de ser tarifada em PIX.** Diferença de receita antes e depois é isso,
   e é intencional.
2. **Merchant passa a ver só as próprias infrações e MEDs.** Quem estava acostumado a ver
   o acervo inteiro estava vendo o vazamento.
3. **Os caches ETS passam a existir de fato no boot.** Deve reduzir carga no Aurora.

---

## 7. Documentos desta sessão

- `docs/plans/2026-07-27-porte-coreproviders-caixa-split-seguranca-escala-design.md`:
  o desenho, com os 13 achados do motor, o fluxo do batch, a paridade de MED e infração,
  o estado vivo e o faseamento do TB Gateway.
- Este handoff.

## 8. Por onde retomar

1. **Se o COSIF chegou:** tarefa 11 (de-para das 20 rubricas), depois provisionar a conta
   kind 21, depois ligar a perna nos quatro caminhos, depois as flags. Nessa ordem.
2. **Se o COSIF não chegou:** tarefa 4 (`SubcontaCreator`). É o P0 de isolamento, não
   toca em TigerBeetle nem em contabilidade, e produção tem **zero subcontas criadas**,
   então dá para consertar antes do primeiro dado nascer torto.
3. **Em paralelo, sem dependência:** tarefa 5 (MFA obrigatório e integração do WAF) e
   tarefa 9 (dimensionamento do `core-api`).

**Antes de afirmar qualquer estado, rode `./scripts/estado-vivo.sh`.** Número copiado a
mão envelhece, comando não.

---

# PARTE II: sessão paralela do mesmo dia (MFA obrigatório, nome do recebedor, infraestrutura)

Esta parte foi escrita por uma **segunda sessão** que rodou em paralelo à de cima, a
pedido do dono, para consolidar tudo num só documento antes de abrir uma sessão nova e
limpa. Mesma regra: **estado vem de comando, não de memória.** Tudo abaixo foi conferido
contra a AWS, o git e o XSD do BACEN em 27/07 às 17h.

## 9. O que esta sessão entregou e está vivo

### 9.1. MFA obrigatório nos 4 consoles administrativos

Mandato do dono: MFA obrigatório para **login e para operação financeira** em coreadmin,
pixadmin, spbadmin e staadmin, **exceto `admin@monetarie.com`**, com flag por usuário e
cadastro forçado no próximo login.

**Entregue e ligado em produção.** Conferido nas task definitions vivas:

| Serviço | Revisão viva | Variável |
|---|---|---|
| `core-api` PRD `:123` | `prod-adda61bf-nome-20260727` | `MFA_ENFORCEMENT=members`, `REQUIRE_ADMIN_TRANSACTION_MFA=true`, `REQUIRE_TRANSACTION_MFA=true` |
| `pix-api` PRD `:101` | `prod-349990cd-backfill-20260727` | `REQUIRE_OPERATION_MFA=true` |
| `spb-api` PRD `:46` | `prod-228dab71-mfa-20260727` | `REQUIRE_OPERATION_MFA=true` |
| `sta-api` PRD `:22` | `prod-228dab71-mfa-20260727` | `REQUIRE_OPERATION_MFA=true` |

O commit `c6bcc223` está presente em **todas** as imagens vivas (conferido por
`git merge-base --is-ancestor`).

Seis cenários provados em HML por ECS Exec contra o HTTP real em `127.0.0.1:4000`, não
por leitura de código: login sem MFA não emite sessão; `setup_token` recebe 401 nas rotas
de negócio; cadastro forçado termina em sessão com 10 códigos de recuperação; código
errado devolve 401 `mfa_invalid`; operação sem código devolve 403 `mfa_required` e código
repetido devolve 403 `mfa_replay`; `admin@monetarie.com.br` entra com a senha real do
Secrets Manager sem MFA.

**Dois furos P0 fechados no caminho**, ambos reais e explorados nos testes:

1. `MonetarieWeb.Auth.Pipeline` não filtrava o tipo do token. O
   `Guardian.Plug.VerifyHeader` faz `claims_to_check = Keyword.get(opts, :claims, %{})`,
   ou seja **sem `claims:` ele não checa nada**, e o token de desafio `typ: "mfa"`,
   emitido depois de só a senha, era aceito como sessão completa. Corrigido com
   `claims: %{"typ" => "access"}`.
2. `Shared.Plugs.RequirePermission` no PIX caía fora para qualquer token com
   `iss: "monetarie"`, o que permitia a um papel qualquer do Core atravessar o RBAC da
   cabine. Agora só `super_admin` e `admin` passam, com log do papel recusado.

### 9.2. Nome do recebedor no PIX-in: o campo não existe na pacs.008

Chamado do dono: a cabine mostrava CNPJ no lugar do nome do recebedor.

**A causa não é defeito da contraparte.** O XSD oficial `pacs.008.spi.1.15.xsd` declara
tipos diferentes para as duas pontas:

```
<xs:element name="Cdtr" type="IdPrivateIdentification"/>    (so Id)
<xs:element name="Dbtr" type="NmIdPrivateIdentification"/>  (Nm + Id)
```

Existe **um único** `<xs:element name="Nm">` no schema inteiro e ele está no tipo do
pagador. O PSP pagador não conseguiria mandar o nome do recebedor nem se quisesse.
Conferido no fio: **41 de 41** pacs.008 recebidas em 60 dias em PRD sem nome de recebedor.

Preencher pelo nosso cadastro é a **única fonte que existe**, por desenho do protocolo.
Implementado em `SpiService.Inbound.OwnCreditorName` com três fontes em ordem: chave PIX
em `monetarie_dict.keys`, depois documento mais conta, depois o `holder_name` que o Core
já devolve no contrato NATS `pix.core.validate_account.request` que a cabine **já
chamava** em todo PIX-in. Não houve contrato novo.

Backfill em PRD: 15 candidatos, **9 resolvidos e atualizados**. A transação 16152 passou
a mostrar "M. C. P. DA SILVEIRA LTDA".

### 9.3. Infraestrutura do TigerBeetle e alarmes

- TB PRD nos 3 nós em **`m6g.xlarge`** (4 vCPU, 15 GB) com **2048 GB gp3, 6000 IOPS,
  500 MB/s** cada, idêntico ao que roda no coreproviders.
- **31 alarmes CloudWatch a 90%** e autoscaling nos serviços ECS.
- Cota de vCPU aumentada para **96** (uso atual 40).

## 10. O que foi tentado e NÃO é possível: core-api no Fargate

O dono pediu a migração do `core-api` para Fargate. **Ela não é viável**, e a prova é o
log do container:

```
error(io): io_uring is not available
error(io): likely cause: the syscall is disabled by sysctl
error(tb_client_context): failed to initialize IO: PermissionDenied
thread 50 panic: attempt to unwrap error: PermissionDenied
```

Exit code **133** nas duas tasks que tentaram subir. O cliente do TigerBeetle exige
`io_uring`, e o Fargate bloqueia essas syscalls no seccomp padrão, sem sysctl e sem
container privilegiado.

**É por isso que a task definition tem `privileged: true` e o serviço roda em EC2.** Não
é descuido de padronização: é o que faz o Core falar com o ledger. Fica registrado para
ninguém tentar de novo.

A migração foi feita com um serviço Fargate **ao lado** do EC2, no mesmo target group.
Se tivesse sido feita trocando o serviço existente, homologação teria caído sem volta.

## 11. Dimensionamento do core-api: resolve a tarefa 9 da Parte I, e acha o teto real

A Parte I registrou o `core-api` em "meia vCPU de uma `t4g.small` burstable" como a
primeira parede. Corrigido, com um achado que muda o diagnóstico.

**O que estava errado:** `desired=3` com apenas **2 rodando**. Cada `t4g.small` tem
1846 MB registrados, a task pede 1024, sobravam 822: não cabia a segunda. O autoscale
até 9 era ficção.

**O que foi feito:** launch template do ASG `monetarie-core-ecs-prod` de `t4g.small` para
**`m6g.large`** (2 vCPU, 7735 MB), com substituição rolante e a instância nova subindo
**antes** de qualquer remoção. **Mesmo número de vCPU**, então zero impacto na cota.
Resultado: **3 x `m6g.large`, `core-api` 3/3 pela primeira vez, 3 alvos healthy no ALB.**

**O teto real não é memória, é ENI.** Com o `core-api` em `awsvpc`, cada task consome uma
interface de rede. O evento do ECS diz textualmente `RESOURCE:ENI`, e
`awsvpcTrunking` está **`disabled`** na conta. `m6g.large` suporta 3 interfaces, uma é a
primária, então são **2 tasks por instância**. Três instâncias dão 6 vagas: 3 tasks mais
folga de 3 para o rolling deploy.

O autoscale foi corrigido de `max=9` (impossível) para **`min=3, max=5`**, que é o que as
6 vagas comportam deixando uma livre para o deploy.

**HML foi reduzido**, não aumentado: de 2 x `t4g.small` para **1 x `t4g.small`**, com
`core-api` em `desired=1`. Fica mais barato do que estava antes desta sessão.

## 12. Pendências que esta sessão deixa

### 12.1. P0 de comportamento: a vedação PF não está em nenhuma imagem viva

Confirmado por `git merge-base --is-ancestor`:

| Commit | Em PRD (`adda61bf`) | Em HML (`bc97a13c`) |
|---|---|---|
| `b6655dda` (`pix_in_purchase`) | **NÃO** | **NÃO** |
| `8cd10733` (PIX não se cobra de PF, nenhum tipo) | **NÃO** | **NÃO** |

Isto confirma o aviso §6.1 da Parte I: o build saiu antes dos dois commits de vedação.

**Atenuante conferido, não suposto:** pela evidência da Parte I, o motor de tarifa
**nunca cobrou nada em produção** (`fee_transactions = 0`, `fee_split_transactions = 0`,
`cosif_accounts` com **zero** `alias` nos dois ambientes, o que faz `MappedCosifAccounts`
levantar exceção antes de qualquer cobrança). Ou seja, a cobrança indevida de PF é
**latente, não ativa**. Continua sendo o primeiro deploy a fazer, mas não é incêndio.

### 12.2. RBAC dos consoles: uma frente aberta

`escrow_controller` e `coreproviders_parity_controller` seguem apenas atrás de
`:admin_only`, que aceita **qualquer** `platform_admin`, inclusive `viewer`. Nenhuma
feature de seed mapeia de forma limpa nesses dois. **Precisa de decisão do dono** sobre
qual papel deve alcançar cada um antes de fechar.

### 12.3. Infra

- **`awsvpcTrunking` desabilitado na conta.** Ligar elevaria a `m6g.large` de 2 para
  cerca de 10 tasks, o que permitiria voltar de 3 para 2 instâncias e **reduzir custo**
  mantendo a folga. Exige relançar as instâncias para registrarem com trunking. Não foi
  feito por ser mudança de escopo de conta.
- **Os dois ASGs estão em uma única AZ** (`sa-east-1a`). As tasks usam 3 subnets para
  ENI, mas as instâncias não. Perda da AZ derruba o `core-api` inteiro.
- **`TB_POOL_SIZE=1` em produção** (Parte I §4). Não foi tocado nesta sessão.
- A task do `core-api` segue em **512 CPU / 1024 MB**. Agora **há folga na instância**
  para aumentar, o que antes não havia.

### 12.4. Money path

- **Subject NATS legado `monetarie.core.pix.validate_account`** continua assinado
  (sid 20), mas **sempre perde** para o PubAck do JetStream, que devolve
  `{"stream":"MONETARIE_CORE","seq":...}` no lugar da resposta do responder. O contrato
  vivo é `pix.core.validate_account.request`. Remoção recomendada, 2 linhas, não feita.
- **6 de 8820 PIX-in seguem sem nome de recebedor**, em contas que o Core não resolve.
- **As 3 inscrições SNS de `monetarie-pix-alarms` seguem sem confirmação**
  (gabriel@monetarie.com, luiz@ e everton@vulci.com.br). Sem o clique no link, o alarme
  dispara para o vazio.

### 12.5. Higiene de histórico

O commit `349990cd` carrega, além do backfill do nome do recebedor, **os 7 arquivos do
motor de tarifa e o documento de desenho da outra sessão**, varridos por um `git add -A`
meu. O erro é meu. Não reescrevi o histórico porque a outra sessão já tinha construído em
cima. A Parte I §6.3 registra o mesmo fato pelo outro lado. **Para auditar o motor de
tarifa, olhe `349990cd` e `1557181d` juntos.**

## 13. Erros meus nesta sessão, para não se repetirem

Registrados porque custaram tempo e, num caso, capacidade de produção.

1. **Afirmei que o `<Nm>` do `<Cdtr>` era opcional e que os PSPs omitiam**, copiando um
   moduledoc sem ler o XSD, e ainda acusei a contraparte de usar `<PrvtId>` errado. O
   dono cobrou. O campo **não existe no tipo**. As três afirmações estavam erradas e o
   moduledoc mentiroso foi corrigido.
2. **Subi os nós do TigerBeetle sem conferir a cota de vCPU.** O nó 0 bateu
   `VcpuLimitExceeded` e **o cluster rodou em 2 de 3 réplicas**. Verifiquei quórum, IP,
   porta, disco e saúde do ledger a cada passo, e não verifiquei a única coisa que
   importava para uma operação que aumenta CPU.
3. **Usei HML como ensaio do redimensionamento e subi as duas instâncias para
   `m6g.large`**, jogando custo em homologação depois de o dono ter mandado **reduzir**
   HML. Revertido no mesmo dia para 1 x `t4g.small`.
4. **`git add -A` varreu o trabalho da outra sessão** para dentro de `349990cd`.
5. **Ecoei "alarmes refeitos"** enquanto as 4 chamadas de `put-metric-alarm` falhavam com
   "Period must not be null". Só apareceu porque conferi depois, não porque confiei na
   minha própria saída.

## 14. Por onde retomar, consolidado

Ordem sugerida juntando as duas partes:

1. **Deploy do `core-api` a partir do HEAD atual** (`4deb71e1` ou posterior), que fecha
   §12.1 e leva junto a migration `20260727140000` da Parte I §6.2.
2. **Decisão do dono sobre o COSIF** (Parte I §5.1), que destrava a frente de receita
   inteira.
3. **`SubcontaCreator`** (Parte I §5.2) se o COSIF não chegou: é P0 de isolamento, não
   toca contabilidade, e produção tem zero subcontas.
4. **RBAC dos dois controllers** (§12.2), que precisa de decisão do dono.
5. **`awsvpcTrunking` e multi-AZ do ASG** (§12.3), que juntos reduzem custo e removem o
   ponto único de falha do `core-api`.

**Antes de afirmar qualquer estado, rode `./scripts/estado-vivo.sh`.**
