# Validação viva F1 (Partner API Fase A) em HML, 2026-07-18

Execução do roteiro da Task 19 do plano `docs/plans/2026-07-18-w1-f1-partner-fase-a.md`, restrita às sondas SEGURAS (nenhum POST que crie objeto no BACEN; caminhos felizes de infração, MED, claim e devolução de TED ficam explicitamente para a janela com o dono).

## Ambiente e acesso

| Item | Valor conferido vivo |
|---|---|
| Cluster | `monetarie-greenfield-homolog` |
| core-api | task-def `:169`, imagem `monetarie/core-api:homolog-85dc7f2f-iniciacao-20260718`, rollout COMPLETED |
| pix-api | task-def `:189`, imagem `monetarie/pix-api:homolog-98d1631a-f5lote-20260718`, rollout COMPLETED |
| Acesso | túnel SSM local 18080 via EC2 `i-02ce3a3b6b3ad0d37` até o ALB interno `internal-monetarie-internal-homolog-45-2129766573...`, header `Host: coreapi-h.monetarie.internal` |
| Credencial | OAuth client_credentials do secret `monetarie/homolog/partner/herbeth-santana/api_credentials` |
| Token | 200, `token_type: Bearer`, `expires_in: 28800`, scope completo (pix, account, transfer, statement, payment, customer, webhook, fee) |
| Contas do parceiro | `GET /accounts` = 200 com 6 contas ativas (ids 10023469, 10024216, 10024347, 10024380, 10024413, 10024414) |

Conta usada nas sondas: `10023469`. UUID inexistente das sondas negativas: `00000000-0000-0000-0000-000000000000`.

## Tabela chamada -> resultado

| # | Chamada | Esperado | Resultado vivo | Veredito |
|---|---|---|---|---|
| 1 | `GET /webhooks/events` | 200 com os 7 eventos novos; total 42 | 200, `events` com **42** itens (35+7); os 7 novos PRESENTES: `pix.med.created`, `pix.med.completed`, `pix.med.cancelled`, `ted.refund.requested`, `ted.refund.completed`, `ted.refund.failed`, `pix.infraction.defense_submitted` | PASSA |
| 2 | `GET /pix/claims?accountId=10023469` | 200 escopado (vazio ok) | 200 `{"data":{"total":0,"claims":[]}}` | PASSA |
| 3 | `POST /pix/claims/<uuid-zero>/complete` | 404 legível | 404 `{"error":{"message":"reivindicação não encontrada","status":404}}` | PASSA |
| 4 | `GET /pix/med?accountId=10023469` (lista de recoveries) | 200 (vazio ok) | 200 `{"data":{"total":0,"recoveries":[]}}` | PASSA |
| 5 | `GET /pix/med/<uuid-zero>` | 404 | 404 `{"error":{"message":"Intervenção MED não encontrada","status":404}}` | PASSA |
| 6 | `GET /pix/med/<uuid-zero>/graph` | 404 | 404 (mesma mensagem) | PASSA |
| 7 | `POST /pix/med/<uuid-zero>/cancel` | 404 | 404 (mesma mensagem) | PASSA |
| 8 | `POST /pix/med/<uuid-zero>/refund` | 404 | 404 (mesma mensagem) | PASSA |
| 9 | `GET /pix/infractions?accountId=10023469` | 200 (vazio ok) | 200 `{"data":{"total":0,"infractions":[]}}` | PASSA |
| 10 | `POST /pix/infractions` SEM `endToEndId` | 400 listando obrigatórios | 400 `{"error":{"message":"Parâmetros obrigatórios: accountId, endToEndId","status":400}}` | PASSA |
| 11 | `POST /pix/infractions/<uuid-zero>/defense` | 404 | 404 `{"error":{"message":"Infração não encontrada","status":404}}` | PASSA |
| 12 | `GET /ted/credits?accountId=10023469` | 200 `{credits:[...]}` | 200 `{"data":{"total":0,"credits":[]}}` | PASSA |
| 13 | `POST /ted/credits/<uuid-zero>/refund` com `reason` inválido | recusa legível | 422 `"Motivo inválido. Motivos aceitos: 1, 2, 3, 31, 4, 5, 70, 72, 84, 9"` (motivo validado ANTES do lookup, contrato da Task 17) | PASSA |
| 14 | `POST /ted/credits/<uuid-zero>/refund` com `reason=1` | 404 | 404 `{"error":{"message":"Crédito TED não encontrado para esta conta","status":404}}` | PASSA |
| 15 | `GET /accounts/10023469/events` (conta própria) | 200 com trilha | 200 `{"data":{"events":[],"source":"audit_trail"}}`: rota, escopo e shape corretos, porém trilha VAZIA em TODAS as 6 contas do parceiro. Defeito real F1-D1 (abaixo) | PARCIAL |
| 16 | `GET /accounts/10024270/events` (conta alheia) | 404/403 | 403 `{"error":{"message":"account does not belong to this partner","status":403}}` | PASSA |
| 17 | Token com escopo reduzido `pix:read`: `POST /pix/infractions` | 403 `required_permission: pix:write` | **422** de negócio ("Transação não encontrada para esta conta"): o enforcement NÃO barrou a escrita | FALHA (gap F1-D2) |
| 18 | Token com escopo reduzido `pix:read`: `GET /ted/credits` | 403 `transfer:read` | **200** com a lista: o enforcement NÃO barrou a leitura de TED | FALHA (gap F1-D2) |

Nota de rota: não existe `GET /pix/med/recoveries`; a lista de recoveries do MED é `GET /pix/med` (router `core/backend/lib/monetarie_web/router.ex`, scope partner v1). A sonda do item 4 usa a rota real.

## Achados (defeitos reais, com prova)

### F1-D1: `GET /accounts/:id/events` sempre devolve trilha vazia (filtro de `resource_type` não bate com o acervo)

O endpoint filtra `resource_type == "account"` (`core/backend/lib/monetarie_web/controllers/partner_v1/accounts_controller.ex`, action `events`, linhas 227-231). Prova por SELECT read-only no Aurora HML (ECS exec rpc na task viva do core-api):

- As 6 contas do parceiro TÊM trilha real: **9 linhas** em `audit_logs`, todas com `resource_type = "Monetarie.Schemas.Relational.Account"` (CREATE/UPDATE de 2026-07-08 e 2026-07-17, `resource_id` = id da conta como string).
- Na base INTEIRA existe **1 única** linha com `resource_type = 'account'`, e ela tem `resource_id = nil` (nunca casaria com conta alguma).

Efeito: a trilha existe mas o endpoint nunca a devolve. Autenticação, escopo (403 em conta alheia), limite e shape (`source: "audit_trail"`, camelCase) estão corretos; o defeito é só o rótulo do filtro. Correção sugerida: aceitar os dois rótulos na leitura (`resource_type in ["account", "Monetarie.Schemas.Relational.Account"]`) ou normalizar o rótulo na escrita do audit (com backfill do acervo).

### F1-D2: escopo reduzido do token OAuth não é aplicado no enforcement (403 de escopo não é provável vivo com a chave do herbeth)

O endpoint de token anuncia e devolve a interseção de escopos (pedido `scope: "pix:read"` devolveu token com `scope: "pix:read"`), mas o `PermissionEnforcer` (`core/backend/lib/monetarie_web/plugs/permission_enforcer.ex`, linhas 109-135) decide por `api_key.permissions` (as permissões da CHAVE), e o `PartnerBearerAuth` (`partner_bearer_auth.ex`, linhas 48-52) atribui a `%ApiKey{}` inteira sem estreitar pelo claim de scope do token. Como a chave do herbeth tem todas as permissões, o token "só leitura" passou em escrita de infração (chegou à validação de negócio, 422) e na leitura de TED (200).

Dois fatos separados:

1. O mapa de rotas do enforcer ESTÁ correto para tudo desta frente (entradas method-keyed: `POST /api/partner/v1/pix` -> `pix:write`, `GET/POST /api/partner/v1/ted` -> `transfer:read|write`, `GET /accounts/:id/events` -> `account:read`). O 403 por CHAVE com permissões restritas é coberto por teste (`ted_controller_test.exs` "sem transfer:read = 403" e "sem transfer:write = 403"; `pix_controller_test.exs` `required_permission == "pix:read"|"pix:write"`).
2. O que NÃO existe é enforcement do scope CONCEDIDO no token (RFC 6749 §3.3: o escopo do token deveria limitar o acesso). Um cliente que pede um token de leitura recebe um token que na prática escreve. Decidir: ou o enforcer passa a intersectar com o claim de scope do token, ou a documentação para de anunciar a interseção como limite efetivo.

A prova viva do 403 de escopo exige uma api_key com permissões restritas (não existe hoje para este parceiro em HML); fica registrada para a janela com o dono junto da decisão sobre o gap.

## Veredito por item da frente

| Frente | Veredito |
|---|---|
| Catálogo de webhooks (42 eventos, 7 novos) | VALIDADO VIVO |
| Claims: lista escopada + sonda negativa do complete | VALIDADO VIVO (caminho feliz de portabilidade fica para janela) |
| MED: lista, show/graph/cancel/refund com 404 legível | VALIDADO VIVO (create/cancel real fica para janela) |
| Infrações: lista, 400 de obrigatórios, defense 404 | VALIDADO VIVO (infração REAL no DICT homolog fica para janela) |
| TED credits: lista, 422 de motivo legível, refund 404 | VALIDADO VIVO (refund de TED real, STR0010, fica para janela) |
| Eventos da conta: rota/escopo/shape | PARCIAL: mecânica ok, conteúdo bloqueado pelo F1-D1 (fix pendente) |
| Escopos por token reduzido | NÃO PROVADO VIVO: gap F1-D2 (enforcement por chave, coberto por teste; enforcement por scope de token inexistente) |

## Fica para a janela com o dono (explicitamente NÃO executado)

1. **Infração real** no DICT homolog do BACEN (create com E2E de transação entre contas nossas + cancel imediato), com a sonda rpc read-only antes/depois na cabine (`monetarie_dict.infraction_reports`, item 10 do roteiro).
2. **Defesa de infração caminho feliz**: exige bloco cautelar MED vivo (infração orgânica).
3. **Devolução de TED real**: TED homolog recebida na conta de teste, refund via partner, STR0010 contra o BACEN homolog, acompanhamento de `monetarie.spb.credits.return_status` e do webhook `ted.refund.completed`.
4. **MED create/cancel e claims caminho feliz** (objetos reais no DICT).
5. **Entrega real de webhook** dos eventos novos a receptor externo (`ted.refund.requested` ou `pix.med.created`, receita do PDF de 17/07).
6. **403 de escopo vivo**: provisionar chave restrita para o parceiro OU decidir o fix do F1-D2 (enforcement pelo scope do token).
7. **Fix do F1-D1** (filtro do audit trail) e re-sonda do item 15 com trilha populada.

Regra do dono preservada no relato ao cliente: os 4xx desta validação são recusas de proteção da API, nunca defeito interno.
