# Handoff Monetarie PIX, integração nativa BACEN (DICT v2.11.0 + SPI/ICOM v5.12) e CID sync

Data: 2026-06-23. Autor: sessão Claude Code. Conta AWS `990933657879`, região `sa-east-1`, profile `vulcimonetarie`.

HEAD `0a5f5e7` em `main` (pushed, working tree limpa). Imagem `pix-api` viva por DIGEST `sha256:1ed34838fdb550d8a28e8b029f96b6a9d4cbb5e8511873f6d95d497e6bb0bf42` (tag mutável `homolog-latest`, task-def `monetarie-pix-api-homolog:20`, rollout `COMPLETED`, target group `healthy`).

## 1. Resumo executivo

A cabine PIX da Monetarie passou a falar com o BACEN de homologação de forma 100 por cento nativa, sem nenhum agente externo, proxy ou sidecar no caminho da transação. Foram validados empiricamente, contra o ambiente real do BACEN (`dict-h`, `dict-np-h`, `icom-h`), os quatro fluxos centrais: consulta DICT (GetEntry), eventos de CID (sincronização do diretório), echo PIBR.001 no SPI e consulta de saldo CAMT.060. Todos retornaram sucesso, com latência abaixo de 200 ms, sobre mTLS ICP-Brasil com `verify_peer`.

A decisão de arquitetura do dono foi respeitada: nada de stunnel, HAProxy, relay Cecresa ou qualquer intermediário que adicione latência ou se torne permanente. A solução é a pilha TLS do próprio Erlang (`:ssl` via Finch/Mint), corrigida nos dois pontos que travavam o handshake e a montagem da requisição.

## 2. O que mudou nesta sessão (commits `ad5d5a1`..`0a5f5e7`)

- `ad5d5a1` feat(core): seed do plano de contas COSIF para SCD (202 contas).
- `ebac87c` fix(pix): extrai .pfx legado (ICP-Brasil/e-CNPJ) com fallback `openssl -legacy`.
- `9e4f4e4` feat(pix): DICT/SPI real do BACEN, mTLS `verify_peer` + truststore ICP-Brasil.
- `a19e748` fix(pix): pool BACEN lê `certfile` do caminho materializado (lazy), sem sync pré-Finch.
- `0f68c6f` fix(pix): BACEN usa `conn_opts` do pool Finch; remove `transport_opts` por requisição.
- `02aeb6a` feat(pix): `xmllint` para XMLDSig SPI (c14n) + camt.060 assinada no endpoint ICOM.
- `e62b55a` feat(pix): camt.060 com envelope SPI (AppHdr) assinável + ativa CID sync.
- `695a500` docs(pix): DICT client moduledoc reflete v2.11.0 (em vigência no BACEN).
- `6e7b21e` fix(pix): CID sync usa paths reais do BACEN (`/cids/*`) por querystring.
- `0a5f5e7` fix(pix): DICT GetEntry envia `PI-EndToEndId` (exigido na v2.11.0).

## 3. A pilha mTLS do BACEN (a parte difícil, resolvida)

### 3.1 Truststore ICP-Brasil

Arquivo `apps/shared/priv/certs/icp_brasil_ca_chain.pem`, 5 certificados: Raiz Brasileira v5, v10, v11, v12 (ITI) e a intermediária AC do SERPRO SSLv1 (emitida pela Raiz v10). É exatamente a cadeia que valida o certificado servidor do gateway RSFN do BACEN (emitido por "Autoridade Certificadora do SERPRO Final SSL"). Carregado por `Shared.Bacen.CertLoader.load_cacerts/1`, que lê o PEM, decodifica as entradas Certificate para DER e nunca levanta exceção (portado do mwbank).

### 3.2 Os dois bugs que travavam tudo (causa-raiz)

1. Host com porta. O BACEN rejeita `Host: dict-h.pi.rsfn.net.br:16522` (Manual Canal Secundário do BCB, parágrafos 4.1.2 e 4.2.1). A montagem da requisição passou a injetar um `Host` sem porta (`URI.parse(url).host`) em `build_finch_request`.
2. `transport_opts` por requisição. O Finch honra `transport_opts: ssl_opts` por requisição abrindo uma conexão separada e quebrada (sem `partial_chain`, sem `customize_hostname_check`), que ficava pendurada até o timeout. A correção foi remover o `transport_opts` por requisição de `do_request` e `do_icom_request`, deixando o mTLS exclusivamente nas `conn_opts` do pool Finch, montadas uma vez no boot por `Shared.Application.build_bacen_transport_opts` (verify_peer + cacerts DER + `customize_hostname_check` + `depth: 5` + `partial_chain`). Em seguida removemos o código morto `build_ssl_opts`/`put_tls_server_name_from_url`.

### 3.3 Certificados do cliente (cofre + materialização)

- Cofre em AWS Secrets Manager (cabine PIX): material PIA (assinatura XMLDSig, CPIA) e PIC (canal mTLS, CPIC). `CertMaterializer` escreve `client_cert.pem`/`client_key.pem` em `PIX_CERT_DIR` (default `/tmp/pix/certs`) de forma assíncrona; o pool aponta `certfile`/`keyfile` para esse caminho determinístico e a leitura é lazy pelo `:ssl` no primeiro handshake (sem chamada síncrona pré-Finch, que falhava com "unknown registry: Shared.Finch").
- Importação de `.pfx` legado ICP-Brasil (RC2-40-CBC) resolvida com retry `openssl -legacy` em `Shared.Crypto.Pkcs12` (o provider legacy do OpenSSL 3 não vem carregado por padrão). Resolveu o "pfx extraction failed" do upload pelo front.

## 4. DICT API v2.11.0 (em vigência no BACEN)

- GetEntry exige `PI-EndToEndId`. Empiricamente, `GET /entries/{Key}` sem esse header retorna 400 "Missing request header 'PI-EndToEndId'" (o BACEN correlaciona a consulta ao pagamento que será iniciado, antifraude/MED). `DictClient.get_entry/2` passou a injetar o header: usa o E2E real do pagamento se vier em `opts[:pi_end_to_end_id]`, senão gera um válido de 32 caracteres (`E` + ISPB + AAAAMMDDHHmm + 11 alfanuméricos) via `MessageBuilder.generate_e2e_id/1`, reaproveitável na pacs.008 seguinte (E2ECache). O `client.ex` já suportava o opt para o header.
- CID sync com paths reais. Os endpoints fictícios `/cid-set-files/...` foram trocados pelos reais do BACEN/LegadoPIX, validados contra o homolog:
  - `POST /cids/files` (criação, XML assinado).
  - `GET /cids/files/{FileId}` (metadados do arquivo).
  - `GET /cids/entries/{Cid}` (entrada por CID).
  - `GET /cids/events?Participant=&KeyType=&StartTime=&EndTime=&Limit=` (eventos por QUERYSTRING, não path-scoped por FileId).
  O `list_cid_events` deixou de exigir um `fileId` e passou a filtrar por `Participant` + `KeyType`. O `CidSyncService` (poll de eventos a cada 5 min, full sync a cada 6 h) deixou de derivar `bacen_file_id` do banco e passa os filtros direto. `CID_FULL_SYNC_ENABLED=true` e `CID_EVENT_POLL_ENABLED=true`.

## 5. SPI/ICOM (Catálogo SFN v5.12, baseline v5.12.1)

- PIBR.001 echo assinado, enviado ao endpoint ICOM. A canonicalização exclusiva (c14n) do XMLDSig usa `xmllint` (libxml2); o pacote `libxml2-utils` foi adicionado ao estágio de runtime do `Dockerfile` (faltava, dava `:enoent`).
- CAMT.060 (consulta de saldo) reescrita com envelope SPI completo: `Envelope > AppHdr (Fr/To/BizMsgIdr/MsgDefIdr camt.060.spi.{versão}/CreDt/Sgntr) + Document (AcctRptgReq)`, assinada por `XmlSigner.sign_spi/2` e enviada a `/api/v1/in/{ispb}/msgs`. Antes ia sem AppHdr (quebrava com `{:element_not_found, "AppHdr"}`).
- Versões por tipo via `Shared.Bacen.Iso20022.SpiVersion` (`version_for/1`, `namespace_for/1`).

## 6. Evidência empírica (validação contra o BACEN homolog, imagem `sha256:1ed3483...`)

Execução via ECS Exec rpc na task viva do `pix-api` (`BACEN_ENABLED=true`, `DICT_EXTERNAL_MODE=bacen`, `SIMULATOR_ENABLED=false`):

| Fluxo | Chamada | Resultado | Latência |
| --- | --- | --- | --- |
| DICT GetEntry | `GET /entries/62188010000150` (PI-PayerId 32189410835, PI-EndToEndId gerado) | 200, entrada completa: titular PENHOTA GESTAO E INTERMEDIACAO LTDA, CNPJ 62188010000150, participante 37839059, conta 0000000019 TRAN, LEGAL_PERSON, CID retornado | 143-152 ms |
| CID eventos | `GET /cids/events?Participant=46026562&KeyType=CPF` | 200, 100 eventos ADDED/REMOVED reais | 36-154 ms |
| CID eventos | `GET /cids/events?Participant=46026562&KeyType=CNPJ` | 200, eventos reais + `has_more_elements=false` + `sync_verifier_start/end` | ~150 ms |
| PIBR.001 echo | `SpiClient.echo()` no ICOM | `{:ok, %{latency_ms: 196}}` | 196 ms |
| CAMT.060 saldo | `SpiClient.get_balance("46026562")` | `{:ok, ...}` (envelope SPI assinado aceito) | 191 ms |

Prova de que o mTLS está íntegro mesmo no caso de erro de aplicação: antes do fix do `PI-EndToEndId`, o GetEntry voltou 400 com um RFC 7807 assinado pelo SERPRO em 136 ms, ou seja, o handshake mTLS completou, o BACEN autenticou o certificado de canal e processou a requisição; faltava só o header.

E2E gerado de exemplo, 32 caracteres: `E46026562202606231833ehzklwe2pbd`.

## 7. Deploy (mecanismo e rollback)

- Build: `docker buildx --builder mkbuilder --platform linux/arm64 -t .../monetarie/pix-api:homolog-latest --push .` no diretório `pix/backend`. Tag mutável, sem nova revisão de task-def (segue em `:20`). Conferir SEMPRE por DIGEST, não por revisão.
- Deploy: `aws ecs update-service --cluster monetarie-greenfield-homolog --service pix-api --force-new-deployment`.
- Digests desta sessão: `sha256:8e71eee...` (CID sync) e o vivo `sha256:1ed3483...` (CID sync + PI-EndToEndId).
- Rollback: re-tag/push de uma imagem anterior em `homolog-latest` + `force-new-deployment`, ou apontar a task-def para um digest imutável anterior. A revisão de task-def não mudou.

## 8. Estado AWS validado

- 15 serviços ECS no cluster `monetarie-greenfield-homolog`, todos `running=desired=1`, rollout `COMPLETED` (backoffice, clst-api, core-admin-ui, core-api, core-banking-ui, core-merchant-ui, docs-portal, npc-admin-ui, npc-api, pix-admin-ui, pix-api, spb-admin-ui, spb-api, sta-admin-ui, sta-api).
- `terraform plan` em `infra/aws/greenfield`: `0 to add, 1 to change, 0 to destroy`, e a única mudança é `image_id` do `aws_launch_template.core_ecs`, drift benigno do data source que resolve a AMI ECS-optimized mais recente. Não há divergência entre a working tree e a AWS para nada que tenhamos escrito.
- git: HEAD `0a5f5e7` == `origin/main`, working tree limpa.

## 9. Pendências e próximos passos

1. Saldo CAMT.060 e eventos podem voltar vazios em homolog conforme a conta/ISPB configurados no BACEN; o importante é que o envelope assinado é aceito (sem erro de schema/TLS). Validar com dados de teste formais quando o cliente fornecer.
2. Reaproveitar o E2E entre a consulta DICT e a pacs.008 do mesmo pagamento via `Shared.E2ECache` no fluxo de pagamento real (hoje o GetEntry gera um E2E quando o chamador não passa o do pagamento).
3. Demanda nova do BACEN (segregação CERTPIC/CERTPIA já está no código): itens com vigência a partir de 08/07 ficam para depois, conforme orientação do dono.
4. HSM RTM (`RTM_HSM_ENABLED=false`) e IBM MQ SPB (`IBM_MQ_ENABLED=false`) seguem desabilitados, dependentes da RTM (credencial KMIP/UIDs; abrir MQ MES01 `:12522`). Ver `docs/handoff/2026-06-22-monetarie-hml-complete-handoff-claude.md` e a memória `monetarie-rtm-rsfn-liberacao`.
5. Re-captura estrita de telas (regra 11) ainda pendente para a marca nova; ver a tarefa de revisão de layout/design direcionada ao Codex.

## 10. Comandos úteis

Wrapper AWS (profile correto):

```bash
awsmon() { env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN AWS_PROFILE=vulcimonetarie AWS_REGION=sa-east-1 aws "$@"; }
```

Validação BACEN via rpc na task viva (base64 evita inferno de aspas):

```bash
TASK=$(awsmon ecs list-tasks --cluster monetarie-greenfield-homolog --service-name pix-api --query 'taskArns[0]' --output text)
B64=$(base64 < script.exs | tr -d '\n')
( sleep 20 ) | awsmon ecs execute-command --cluster monetarie-greenfield-homolog --task "$TASK" \
  --container pix-api --interactive \
  --command "/app/bin/monetarie_pix rpc \"Base.decode64!(\\\"$B64\\\") |> Code.eval_string() |> elem(0)\""
```
