# Design: auth PJ, merchant como subconta e API somente PJ (Bloco A)

Data: 2026-07-22. Status: **aguardando aprovacao do dono**. Nada de auth foi tocado ate aqui.

Mandato do dono (verbatim da sessao):
1. IB = login por CPF e CNPJ.
2. Merchant = login SOMENTE por e-mail, e somente subconta de PJ.
3. Conta PF nao pode ter NENHUM acesso de API. API e so PJ.
4. Invasor nao pode logar no merchant com cpf/cnpj, e isso tem que ser recusado no BACKEND, nao so na tela.

Decisoes ja tomadas pelo dono nesta sessao:
- Modelo de dados: **espelho AVIV completo** (tabelas `merchants` e `merchant_users`).
- Leitura read-only de HML e PRD: **autorizada** (executada, resultados na secao 1).
- Cut-over do login: **admin cadastra o operador e ele faz primeiro acesso** (nada de senha herdada).
- Escopo do mandato 3: **a chave e presa ao CNPJ**. O socio pessoa fisica pode administrar as chaves
  da empresa dele (secao 2).
- **A Partner API esta FORA deste escopo.** Chaves de parceiro (`partner_id` preenchido) nao sao
  tocadas por este bloco.
- **Qualquer API nossa apoiada em conta PF tem que ser mapeada e removida** (mapa na secao 1.2).
- Gestao de chaves de API fica **no portal merchant e tambem no IB para o usuario PJ**.
- Credencial do merchant: **coluna nova `users.login_email`**, sem sanear o legado.

---

## 1. Fatos empiricos (medidos, nao inferidos)

Levantamento read-only executado em 2026-07-22 por `bin/monetarie rpc` dentro das tasks vivas
`core-api` de HML (`monetarie-greenfield-homolog`) e PRD (`monetarie-greenfield-prod`).

### 1.1 Nossa base hoje

| Fato | HML | PRD |
|---|---|---|
| Usuarios totais | 1.553 | 1.542 |
| `member_pj` | 856 | 856 |
| `member_pf` | 697 | 686 |
| Usuarios sem e-mail | 31 | 25 |
| Usuarios com e-mail | 1.522 | 1.517 |
| E-mails distintos | 1.324 | 1.319 |
| **E-mails repetidos (aparecem em mais de um usuario)** | **121** | **121** |
| **PJ cujo e-mail colide com outro usuario** | **277** | **277** |
| PJ com e-mail unico hoje | 569 | 569 |
| PJ com `tax_id` de 14 digitos | 856 de 856 | 856 de 856 |
| PJ com conta | 856 | 856 |
| Indice unico em `users.email` | **nao existe** | **nao existe** |

Distribuicao das colisoes (identica nos dois ambientes): 1 e-mail em 9 usuarios, 2 em 7, 4 em 6,
3 em 5, 5 em 4, 25 em 3 e 81 em 2. Total de 319 usuarios compartilhando e-mail. Dominios que mais
aparecem entre os repetidos: `gmail.com` (103), `hotmail.com` (45), depois dominios corporativos
com varios usuarios cada (`cegcorporate.com.br` 9, `monbank.net` 8, `cassbusiness.com.br` 7).
O padrao e claro: a mesma pessoa reaproveitou o e-mail pessoal na PF e na PJ.

**Consequencia direta para o desenho:** `users.email` NAO serve como credencial hoje. Promove-lo a
login exigiria sanear 121 e-mails e 319 usuarios antes, com risco de trocar a identidade de quem ja
opera. O desenho abaixo evita isso por construcao.

### 1.2 Chaves de API hoje

| Fato | HML | PRD |
|---|---|---|
| `api_keys` total | 8 | **0** |
| Chaves de parceiro (`partner_id` preenchido, `merchant_id` nulo) | 4 | 0 |
| Chaves de merchant ativas de dono `member_pj` | 1 | 0 |
| **Chaves de merchant ATIVAS de dono `member_pf`** | **2** | 0 |
| Chaves de merchant revogadas de dono `member_pf` | 1 | 0 |

**A falha do mandato 3 esta materializada em HML: existem 2 chaves de API ativas pertencentes a
usuarios pessoa fisica.** Em PRD nao existe nenhuma chave, o que torna esta a janela ideal para
mudar a chave estrutural antes de existir cliente externo em producao.

#### Mapa nominal das chaves de HML (pedido do dono: mapear e remover o que estiver em PF)

Chaves de MERCHANT, que sao o escopo deste bloco (`partner_id` nulo):

| Nome | Dono (user id) | Tipo | Status | Criada | Ultimo uso | Veredito |
|---|---|---|---|---|---|---|
| `extapi-validador-hml` | 25362, "Cliente Campanha V..." | **member_pf** (CPF) | **active** | 2026-07-22 | nunca usada | **remover** |
| `dev vulci` | 24022, "GABRIEL CARDOSO" | **member_pf** (CPF) | **active** | 2026-07-16 | nunca usada | **remover** |
| `dev vulci` | 24022, "GABRIEL CARDOSO" | **member_pf** (CPF) | revoked | 2026-07-16 | nunca usada | **remover** (ja inerte) |
| `extapi-validador-funded` | 24415, "ALDEIA MIRIM EDUCA..." | member_pj (CNPJ) | active | 2026-07-22 | nunca usada | manter, legitima |

As tres chaves PF carregam permissao de escrita de dinheiro (`pix:write`, `transfer:write`,
`payment:write`). Nenhuma delas tem `last_used_at`, ou seja, nenhuma foi usada em chamada real, e
nenhum webhook esta pendurado nelas (conferido: zero webhooks ligados a chave de dono PF).

**Verdade desconfortavel a registrar:** a chave `extapi-validador-hml` foi criada em 2026-07-22, ou
seja, durante a nossa propria campanha de validacao da Fase 5. Parte daquela validacao foi feita com
uma chave escopada a uma conta pessoa fisica. Sob a regra nova essa chave nao poderia existir. Nao houve
incidente de dinheiro, mas o fato entra aqui porque o dono cobrou zero inferencia e nenhuma omissao.

Chaves de PARCEIRO em HML (4, todas ativas: `Herbeth Santana`, `campanha-18jul`, `Dev vulci`,
`chave-time-integracao-homolog`): **fora deste escopo por decisao do dono, nao serao tocadas.**

Varredura complementar por outras portas de API em conta PF: `sub_merchants` e `merchant_configs`
estao vazias nos dois ambientes, e nao ha webhook ligado a chave PF. Fora as chaves acima, a unica
outra superficie que alcanca usuarios PF e a propria Partner API (12 usuarios PF em HML criados por
parceiro), que o dono colocou explicitamente fora deste escopo.

### 1.3 Como o codigo se comporta hoje

- `MonetarieWeb.V2.AuthController.login` (`core/backend/lib/monetarie_web/controllers/v2/auth_controller.ex:42`)
  e document-only: faz `String.replace(document, ~r/\D/, "")` e busca por `users.tax_id`. E-mail vira
  string vazia no strip e devolve 401. **Login por e-mail nao existe no backend.**
- Nao existe sinal de portal. IB e merchant batem no mesmo endpoint, com o mesmo corpo
  `{document, password}`. O backend nao tem como distinguir os dois hoje.
- `MonetarieWeb.V2.ApiKeyController.authorized?/2` (`api_key_controller.ex:190`) so compara id e role.
  **Zero checagem de PF ou PJ** em create, update, delete, status e ip-whitelist.
- **Nao existe tabela `merchants`.** O `api_keys.merchant_id` e `:integer` e vale `users.id`. O proprio
  codigo da API externa documenta isso: "A api_key externa e escopada por `merchant_id` (User.id)"
  (`controllers/external/helpers.ex:7`), e resolve a conta por `accounts.user_id == merchant_id`
  (`helpers.ex:37`). O `ApiKeyAuth` tambem faz `Repo.get(User, merchant_id)` (`plugs/api_key_auth.ex:187`).
- Existe `user_entities` (usuario x entidade x role) e `collaborator_invites` (convite por e-mail com
  token e expiracao), mas ambos sao ancorados em `entity_id` (a instituicao), nao no merchant. Nao
  cobrem o eixo que precisamos.
- `users.user_type` aceita `member_pf member_pj admin operator supplier viewer`. E a fonte
  autoritativa de PF e PJ.

### 1.4 O que a referencia AVIV realmente faz (lido no fonte, nao no resumo)

- `merchants` tem `document` com `validate_length(is: 14)` e unique, `access_mode` em
  `full api_only custom` e `capability_ceiling`. `merchant_users` faz a juncao merchant x usuario com
  `role` em `owner admin financial operator readonly` e unique `[merchant_id, user_id]`.
- **O login do merchant do AVIV NAO e e-mail-only.** O `classify_credential`
  (`merchant/auth/login_controller.ex:48`) aceita e-mail, CNPJ de 14 digitos e tambem **CPF de 11
  digitos**, que cai em `do_legacy_login`. Ou seja, o mandato do dono e mais rigido que a referencia.
  **Vamos fechar esse ramo de proposito e registrar a divergencia.**

---

## 2. Interpretacao do mandato 3 (decidida pelo dono)

"Conta PF nao pode ter nenhum acesso de API" tem duas leituras. O dono escolheu a primeira:

- **Adotada:** nenhuma chave de API pode ser escopada a um titular ou conta PF. Toda chave nasce presa
  a um merchant, e todo merchant e um CNPJ de 14 digitos. Logo a API fica PJ-only por construcao, nao
  por um `if`.
- **Descartada:** proibir que uma pessoa fisica administre chaves. Quebraria o caso legitimo do socio
  da empresa (uma pessoa fisica) gerenciar as chaves do CNPJ dele. O que a regra protege e o dinheiro
  da conta PF, e esse ponto fica coberto: sem merchant, sem chave, sem conta PF alcancavel pela API.

A Partner API tem produto e ciclo proprios e **fica fora deste escopo por decisao do dono**. Chaves com
`partner_id` preenchido nao sao migradas nem revogadas aqui.

---

## 3. Arquitetura proposta

### 3.1 Tabelas novas

**`merchants`**

| Coluna | Tipo | Regra |
|---|---|---|
| `id` | uuid | PK |
| `entity_id` | uuid | a instituicao, espelha o AVIV |
| `user_id` | integer | **NOT NULL, UNIQUE**, aponta para o `users` PJ legado |
| `document` | varchar(14) | **NOT NULL, UNIQUE**, somente digitos, `validate_length(is: 14)` |
| `name`, `trading_name` | text | |
| `status` | text | `active suspended closed` |
| `access_mode` | text | `full api_only custom` |
| `capability_ceiling` | text[] | teto de capacidades |
| timestamps | | |

**Divergencia consciente do AVIV, e o ponto mais importante deste desenho:** no AVIV as contas
pertencem ao merchant. Na nossa base as contas pertencem ao `users` PJ (`accounts.user_id`), e esse
caminho e money-path vivo (API externa, PIX, TED, extrato). Por isso `merchants.user_id` existe: ele e
a ponte para o legado. A resolucao de conta passa a ser `api_key.merchant_id` (uuid) para
`merchants.user_id` para `accounts`. **Nenhuma linha de `accounts` e tocada.** Um salto a mais em
troca de zero risco no dinheiro.

**`merchant_users`**

| Coluna | Tipo | Regra |
|---|---|---|
| `id` | uuid | PK |
| `merchant_id` | uuid | FK `merchants` |
| `user_id` | integer | a pessoa operadora |
| `role` | text | `owner admin financial operator readonly` |
| `status` | text | `active invited suspended` |
| `capabilities` | text[] | validadas contra o catalogo |
| unique | | `[merchant_id, user_id]` |

### 3.2 Credencial de login do merchant

Coluna nova `users.login_email` (texto, nula por padrao) com
`CREATE UNIQUE INDEX ON users (lower(login_email)) WHERE login_email IS NOT NULL`.

Motivo, com o numero na mao: `users.email` tem 121 valores repetidos e 277 PJ colidindo nos dois
ambientes. Promover `users.email` a credencial exigiria sanear 319 usuarios antes de qualquer login
funcionar. Com `login_email` a unicidade nasce garantida no banco, o campo `email` legado continua
sendo apenas contato, e o backfill de dados sujos deixa de existir. A divergencia em relacao ao AVIV
(que usa `users.email` unico) fica registrada aqui, com a causa medida.

Uma pessoa pode operar varios merchants: `login_email` e unico por pessoa, e a pessoa ganha uma linha
em `merchant_users` por CNPJ que opera.

### 3.3 Endpoints de login separados, nao um campo de canal

- IB continua em `POST /api/auth/login`, document-only. Passa a **recusar explicitamente** qualquer
  credencial que contenha `@`.
- Merchant ganha `POST /api/merchant/auth/login`, **e-mail e senha apenas**. Esse endpoint nunca
  interpreta documento, entao nao existe caminho de codigo em que um CPF ou CNPJ autentique no
  merchant. Fluxo: `login_email` para `users` para `merchant_users` com status `active` para
  `merchants` com status `active`. Qualquer elo faltando devolve 401 generico.

Escolhi endpoint separado em vez de um campo `channel` no corpo porque um campo esquecido pelo
frontend volta ao comportamento do IB, que e exatamente a brecha do mandato 4. Rota separada e
fail-closed por construcao.

O token do merchant leva `portal: "merchant"` e `merchant_id` nos claims. Plug `MerchantFromToken`
(paridade AVIV) devolve 403 quando o claim nao existe. Token de IB nao abre rota de merchant e o
inverso tambem nao.

### 3.4 Gate PJ na API, estrutural

1. `api_keys` ganha `merchant_ref uuid` com FK para `merchants`, **NOT NULL para chave de merchant**.
   Como todo merchant e um CNPJ de 14 digitos validado no changeset, uma chave so existe para PJ.
   Chave de parceiro continua com `merchant_ref` nulo e `partner_id` preenchido, sem alteracao.
2. `V2.ApiKeyController` para de usar o `merchant_id` da URL e passa a **derivar o merchant do token**,
   por dois caminhos (decisao do dono: gestao no merchant E no IB para o PJ):
   - token com `portal: "merchant"`: usa o `merchant_id` do claim, e exige a capability `apikeys.manage`;
   - token de IB: exige `users.user_type == "member_pj"` e resolve o merchant por `merchants.user_id`;
   - qualquer outro caso, inclusive usuario PF logado no IB, devolve **403**.
   Fim do `authorized?` por comparacao de id, que e a origem da falha de hoje.
3. `ApiKeyAuth` e `External.Helpers.resolve_account` passam a resolver por
   `merchant_ref` para `merchants.user_id` para `accounts`.
4. Camada de cinto e suspensorio (defesa em profundidade, nao a defesa principal): o caso de uso
   recusa criar chave quando o `users` alvo nao e `member_pj`.

### 3.5 Capabilities

Catalogo proprio espelhando o AVIV, com tres diferencas deliberadas:

1. **Sem modo log-only.** O AVIV roda `RequireCapability` com `require_capability_enforce=false` por
   default, ou seja, deixa passar e apenas registra. Aqui a decisao e sempre tomada e negar e o default.
2. **Sem fail-open no frontend.** O `hasCapability` do AVIV libera tudo quando a lista vem vazia,
   rotulado como "UX only". Aqui lista vazia nunca significa liberado. O frontend so esconde menu, e a
   decisao de seguranca e sempre do backend.
3. **Catalogo enxuto ao que a Monetarie oferece.** A SCD nao oferta boleto, investimento nem Open
   Finance, entao essas capacidades nao existem no catalogo.

Onde mantemos paridade de proposito: **owner sem capacidade explicita herda o teto do merchant**. O dono
do CNPJ nao se tranca para fora da propria conta, e o teto continua cercando (um merchant `api_only` nao
vira `full` porque a pessoa e owner). Qualquer outro papel sem capacidade explicita fica com lista vazia.

Preset `api_only` = `["dashboard.view", "apikeys.manage", "webhooks.manage"]`, como no AVIV.
Efetivo = capacidades da pessoa interseccao teto do merchant, na ordem canonica do catalogo.

### 3.6 Cut-over do acesso (decisao do dono: admin cadastra e o operador faz primeiro acesso)

O admin cria o operador pelo core-admin informando e-mail e role. O sistema gera convite com token e
expiracao, o operador define a propria senha no primeiro acesso e a MFA segue a politica atual. Nenhuma
senha e herdada e nenhum `users.email` legado vira credencial automaticamente.

Reaproveitamos a maquinaria de `collaborator_invites` (token, expiracao, status), adicionando uma
coluna `merchant_id` nula. Alternativa considerada e descartada por ora: tabela propria
`merchant_user_invites`, que duplicaria codigo vivo sem ganho claro.

---

## 4. Migracao, na ordem exata

Padrao expand and contract, sem janela quebrada, com rollback de codigo livre em todos os passos.

1. **DDL aditivo**: cria `merchants`, `merchant_users`, `users.login_email` com indice unico parcial,
   `api_keys.merchant_ref`, `collaborator_invites.merchant_id`. Nada e removido.
2. **NAO existe backfill de `merchants`** (decisao do dono em 2026-07-22, durante a execucao).
   Nenhum cliente existente e ativado. Os 856 usuarios PJ de HML e PRD seguem sem merchant, e o
   merchant passa a ser criado deliberadamente pelo admin quando um PJ real for habilitado para o
   portal merchant ou para a API.

   Consequencia assumida e desejada: **enquanto um PJ nao tiver merchant, ele recebe 403 na gestao de
   chaves de API pelo IB**, e ninguem entra no portal merchant. E o comportamento correto sob a regra
   nova, e nao uma regressao: hoje esses usuarios tambem nao usam o portal merchant por e-mail, que
   nem existia.
3. **Backfill de `api_keys.merchant_ref`** a partir do inteiro antigo. Em PRD e no-op (zero chaves).
   Em HML sao 4 chaves de merchant.
4. **Remocao das chaves PF de HML** (ordem do dono: mapear e remover). As 3 chaves de dono `member_pf`
   do mapa da secao 1.2 sao removidas com registro de auditoria. Risco operacional zero medido: nenhuma
   delas tem `last_used_at` e nenhuma tem webhook pendurado. As 4 chaves de parceiro ficam intactas, e a
   chave PJ `extapi-validador-funded` e preservada e migrada para o `merchant_ref` novo. Em PRD este
   passo e no-op (zero chaves).
5. **Codigo novo lendo `merchant_ref`**, com o inteiro antigo preservado para rollback.
6. **Deploy HML, prova viva, deploy PRD.**
7. **Contracao em migration separada e posterior**: derrubar `api_keys.merchant_id` inteiro somente
   depois do codigo novo provado nos dois ambientes.

Migrations sempre aplicadas ANTES do swap, via `run-task` com a imagem nova, conforme o padrao ja
usado no repositorio.

---

## 5. Plano de testes (TDD, RED primeiro)

Backend:
1. Login de merchant com e-mail valido e operador ativo devolve 200 com claim `portal: "merchant"`.
2. **Login de merchant com CPF valido devolve 401** (mandato 4).
3. **Login de merchant com CNPJ valido devolve 401** (mandato 4).
4. Login de merchant com e-mail de operador suspenso ou de merchant suspenso devolve 401.
5. Login de IB com CPF e com CNPJ segue 200 (nao regredir).
6. **Login de IB com e-mail devolve 401** (nao abrir porta nova no IB).
7. `login_email` duplicado e recusado pelo banco (prova do indice unico).
8. **Criar api_key com token de usuario PF logado no IB devolve 403** (mandato 3), e o mesmo para
   update, delete, status e ip-whitelist.
9. **Criar api_key com token de usuario PJ logado no IB devolve 201** (decisao do dono: gestao tambem
   pelo IB), e a chave nasce com `merchant_ref` do merchant daquele CNPJ.
10. Criar api_key com token de merchant sem a capability `apikeys.manage` devolve 403.
11. Token de merchant nao consegue criar chave para merchant de terceiro (o `merchant_id` da URL e
    ignorado, vale o do token).
12. Chave de parceiro (`partner_id` preenchido) continua funcionando sem `merchant_ref`, provando que
    a Partner API ficou fora do escopo.
13. Chave criada resolve conta pelo caminho novo (`merchant_ref` para `merchants.user_id` para
    `accounts`) e a API externa continua respondendo igual (regressao da Fase 5).
14. Backfill idempotente: rodar duas vezes nao duplica merchant.

Frontend: merchant recusa entrada que nao seja e-mail, esconde a area de API sem capability, e o IB
continua aceitando CPF e CNPJ.

---

## 6. Prova viva exigida (sem ela nada e "pronto")

1. Login no IB com CPF e com CNPJ, nos dois ambientes.
2. Login no merchant com e-mail de operador real.
3. `curl` direto no backend, sem passar pela tela, mandando CPF e depois CNPJ no endpoint do merchant:
   as duas tentativas tem que voltar 401.
4. `curl` de criacao de api_key com token de usuario PF: 403.
5. Uma rota da API externa da Fase 5 respondendo igual ao de hoje, provando que nao houve regressao.
6. Screenshot das telas envolvidas, conforme a regra 11.

---

## 7. Riscos e como cada um e contido

| Risco | Contencao |
|---|---|
| Quebrar a API externa da Fase 5 (viva, com WAF em block) | PRD tem zero chaves. Coluna nova em vez de troca de tipo. Coluna antiga preservada. Teste de regressao obrigatorio no plano. |
| Mexer em `accounts` e afetar money-path | `accounts` nao e tocada. A ponte e `merchants.user_id`. |
| E-mail sujo no legado | Credencial vai para `login_email`, coluna nova com unicidade no banco. `users.email` nao vira credencial. |
| Operador atual perder acesso no corte | Corte so acontece depois que o admin cadastrou os operadores e eles fizeram o primeiro acesso. O merchant email-only da working tree so entra depois do backend. |
| Copiar os pontos fracos do AVIV | Capability fail-closed, sem ramo legacy de CPF no merchant, sem fail-open no frontend. Registrado como divergencia proposital. |
| Remover chave PF de HML que esteja em uso | Medido antes de agir: as 3 chaves PF nunca foram usadas (`last_used_at` nulo) e nao tem webhook ligado. Remocao com registro de auditoria. |
| Quebrar a Partner API por tabela junto | `merchant_ref` fica nulo para chave de parceiro e existe teste dedicado (item 12 da secao 5). |

---

## 8. O que este desenho NAO faz

- Nao mexe em pix, spb, ledger, TigerBeetle nem em qualquer fluxo de dinheiro.
- Nao migra `accounts` para o merchant.
- Nao apaga a coluna antiga `api_keys.merchant_id` na mesma leva.
- Nao cria cadastro self-service de merchant na borda publica.
- Nao trata o Bloco B (QA visual da jornada de onboarding), que vem depois e ja com o modelo PF e PJ
  decidido aqui.

---

## 9. Decisoes do dono ja incorporadas

| Ponto | Decisao | Onde entrou |
|---|---|---|
| Modelo de dados | Espelho AVIV completo (`merchants` + `merchant_users`) | secao 3.1 |
| Interpretacao do mandato 3 | Chave presa ao CNPJ; socio PF administra as chaves da empresa | secao 2 |
| Partner API | Fora deste escopo, nao tocar | secoes 2 e 4.4 |
| API sobre conta PF | Mapear e remover (3 chaves em HML, zero em PRD) | secoes 1.2 e 4.4 |
| Gestao de chaves | Portal merchant **e tambem** IB para o usuario PJ | secao 3.4 |
| Credencial do merchant | Coluna nova `users.login_email`, sem sanear o legado | secao 3.2 |
| Cut-over | Admin cadastra o operador, operador faz primeiro acesso | secao 3.6 |
| Leitura de HML e PRD | Autorizada, ja executada | secao 1 |

Aval pendente: **aprovacao geral deste desenho**. Enquanto nao houver, nenhuma linha de auth e escrita.
