# W1 F4: Portal do parceiro + Postman + docs (Fase D) Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Gates do plano mestre (`docs/plans/2026-07-18-aceleracao-wave1-plano-mestre.md`) valem em TODA task: G1 contrato provado antes de editar, G2 gate de paridade RED antes de GREEN, G3 validacao viva como definition of done. Regra de ouro da frente: doc que diverge do codigo e DEFEITO. Regras do dono nos textos: pt-br com acentuacao perfeita na prosa, SEM travessao, literais da API intactos (mesmo sem acento, ex.: `expiracao`), cliente nunca ve defeito interno.

**Goal:** Regenerar o portal trilingue do parceiro e as 2 copias identicas da collection Postman cobrindo as APIs novas (ping, QR estatico, refund do item 5, webhooks novos), com gate automatizado doc vs codigo, proposta de publicacao sem Terraform novo para decisao do dono, e relatorio de validacao do cliente no padrao de 17/07.

**Architecture:** Fonte unica de conteudo: o codigo real (`core/backend/lib/monetarie_web/router.ex` + controllers `partner_v1/*` + catalogo `schemas/webhooks/webhook.ex`). A collection editavel e `docs/postman/Monetarie-Partner-API.postman_collection.json`; a copia `docs/partner-portal/public/...` e SEMPRE gerada por copia byte a byte. O portal VitePress (`docs/partner-portal/`, pt raiz + `/en/` + `/es/`) e editado nos .md e rebuildado (`npm run build`, dist gitignorado). Um script de paridade (`scripts/check_partner_docs_parity.exs`) extrai as rotas do router e diffa contra Postman e portal: ele nasce RED com os gaps conhecidos e as tasks de conteudo o levam a GREEN.

**Tech Stack:** VitePress 1.5 (ja vendorizado em `docs/partner-portal/`), Postman collection v2.1 (JSON editado via python3), Elixir/Mix (script de paridade, roda em `core/backend`), newman via `npx` (node v24 disponivel, newman NAO instalado globalmente), tunel SSM 18080 ou VPN para HML.

**Restricao da frente (plano mestre):** F4 e "so docs". A UNICA excecao e o script de paridade (Task 2), que vive em `scripts/`, nao entra em imagem nenhuma e nao toca runtime. NENHUMA alteracao em `infra/aws/greenfield/` (a Task 14 apenas INVENTARIA e recomenda; mudar infra e decisao do dono executada pelo orquestrador).

---

## Sondas G1 ja executadas (fase de planejamento, 2026-07-18)

Evidencia colhida do codigo em `/Users/luizpenha/monetarie` (main, working tree limpa) e dos artefatos reais. As tasks referenciam estes fatos como "CONTRATO PROVADO"; se a main mudar antes da execucao, re-sondar o arquivo:linha citado.

**S1. Rotas partner vivas no router (`core/backend/lib/monetarie_web/router.ex:133-222`):**
- `GET /api/partner/v1/ping` (fora do PermissionEnforcer de proposito; qualquer token de parceiro valido). Resposta (`controllers/partner_v1/ping_controller.ex:13-18`): `{"status": "ok", "partner_id": <id>}` (top-level, sem envelope `data`).
- `POST /pix/qrcodes/static` (`pix_controller.ex:1519`): body `account_id` (obrigatorio), `amount` OPCIONAL em CENTAVOS (ausente = valor em aberto, sem tag 54), `pix_key`/`pixKey`, `description`, `city`. Resposta 201: `{"data": {"account_id", "type": "static", "brcode", "qrcode_base64", "amount", "description", "txId", "pixKey"}}`. ATENCAO: a resposta MISTURA snake_case e camelCase; documentar EXATAMENTE assim (literais intactos).
- `POST /pix/payments/:id/refund` (SINGULAR, `pix_controller.ex:1058`) e `GET /pix/payments/:id/refunds` (PLURAL, lista, `pix_controller.ex:1273`). O escopo da frente citava "POST /pix/payments/:id/refunds"; o codigo real e `/refund` no POST. O codigo MANDA.
- Refund POST: valida original (existencia, conta do parceiro, direcao INBOUND, liquidada, janela 90 dias, restante devolvivel considerando parciais pendentes) ANTES do hold. Erro de janela: 422 com "Janela de devolução pelo fluxo original (90 dias) expirada" (`pix_controller.ex:1172`). Resposta 202: `{"data": {"refundId", "originalTransactionId", "endToEndId", "amount"(centavos), "remainingRefundable", "status": "processing", ...}}`. Webhook `pix.refund.requested` no aceite.
- Whitelist de razoes (`core/backend/lib/monetarie/use_cases/payment_transactions.ex:334-342`): 13 codigos `MD06 SL02 BE08 FR01 AC03 AC06 AC07 AC14 AG03 AG13 AM09 AM18 RR04`; `reason` ausente = default MD06; fora da whitelist = invalido. Janela: `@return_window_days 90`.
- Refund GET (trilha): `{"data": {"original": {"transactionId", "endToEndId", "amount"(centavos), "status", "completedAt"}, "remainingRefundable", "refunds": [{"refundId", "rtrId", "amount", "status", "reasonCode", "reasonDescription", "requestedAt", "completedAt"}]}}`; original de outra conta ou inexistente = 404.

**S2. Catalogo de webhooks (`core/backend/lib/monetarie/schemas/webhooks/webhook.ex:33-49`):** 35 eventos, incluindo `pix.refund.requested|completed|failed`, `pix.payout.returned`, `pix.charge.paid`. `GET /webhooks/events` responde `{"events": [...]}` (`partner_v1/webhooks_controller.ex`, action `events`).

**S3. Shapes de dispatch dos webhooks novos (`core/backend/lib/monetarie/infra/nats/handlers/pix_handler.ex`):**
- `pix.payout.returned` (:2046-2060): `{event, account_id, transactionId, endToEndId, returnId, amount, reason, status: "returned"}`, event_id `pix.payout.returned:<RtrId>`.
- `pix.refund.completed` (:435, :1262) e `pix.refund.failed` (:1336) via `dispatch_payout_webhook`: `{event, account_id, transactionId, endToEndId, end_to_end_id, amount, status, errorReason, recipient}`.
- PENDENCIA G1 (fecha na Task 7): `amount` no dispatch e base units com comentario "a camada de webhook converte ao contrato do parceiro"; PROVAR a unidade no fio (serializer de entrega em `Webhooks.dispatch_event`/worker de entrega) antes de escrever exemplo de payload.

**S4. Postman (2 copias byte-identicas, md5 `f2eb950d48964d6fde1d739884e908fa`, 354061 bytes):** `docs/postman/Monetarie-Partner-API.postman_collection.json` e `docs/partner-portal/public/Monetarie-Partner-API.postman_collection.json`. Variaveis: `baseUrl` (default `http://coreapi-h.monetarie.internal`), `client_id`, `client_secret`, `access_token`, `webhook_receiver_url`. GAPS PROVADOS: nao existem requests para `GET /ping`, `POST /pix/qrcodes/static`, `GET /pix/payments/:id/refunds`; o request "Devolver PIX" usa id de exemplo `PIXOUT...` (PIX ENVIADO, semanticamente errado no trilho novo: refund e de PIX RECEBIDO) e corpo pre item 5; a pasta "Webhooks (exemplos de payload)" tem 16 exemplos de 35 eventos do catalogo (faltam, entre outros, `pix.refund.requested|completed|failed`, `pix.payout.queued|processing|failed|returned`, `pix.infraction.resolved`, `ted.failed`, `transfer.failed`, `account.unblocked|closed`, `pix.key.deleted|blocked|unblocked`, `pix.claim.acknowledged|confirmed|cancelled|completed`).

**S5. Portal (`docs/partner-portal/`, VitePress trilingue):** `endpoints/pix.md` pt/en/es ja documenta o QR estatico (atualizado 16/07, shape confere com S1) mas a secao "Devolver PIX" esta PRE item 5 (diz "Resposta 202 com transactionId, originalTransactionId e amount"; o codigo devolve `refundId`, `remainingRefundable`, `status`; sem 90 dias, sem whitelist) e NAO existe secao da trilha `GET .../refunds`. `GET /ping` nao aparece em NENHUMA pagina. `endpoints/webhooks.md` (3 linguas) cita 34 eventos: falta `pix.refund.failed` (paridade 1:1 com o catalogo QUEBRADA). `.vitepress/dist` esta gitignorado e stale (build de 14/07, anterior ao pix.md de 16/07).

**S6. Infra para publicacao (INVENTARIO read-only, nada foi alterado):**
- `docs-portal`: service ECS + ECR + task-def vivos em HML e PROD (`infra/aws/greenfield/ecs.tf:350-357`), ALB rule host `docs<suffix>.monetarie.internal` prioridade 190 (`alb-internal.tf:119-125`). A imagem ATUAL (`docs-portal/Dockerfile`) copia TODA a doc interna para `/raw/` (exposicao mapeada em 24/06, takedown PENDENTE por ordem do dono, memoria `monetarie-docs-h-internal-exposure`).
- `docs/partner-portal/Dockerfile`: imagem nginx propria, limpa (serve so `.vitepress/dist`, health em `/health`, porta 8080), SEM infra Terraform associada.
- NENHUM `aws_s3_bucket` no Terraform greenfield (grep vazio). Clientes ja possuem perfis OpenVPN (memoria `monetarie-openvpn-client-issuance`), entao host interno E alcancavel por cliente.

**S7. Relatorio de validacao de 17/07:** o harness da campanha (`harness.py`, `validate_all.py`, `gen_pdf.py`, `ledger.jsonl`) morava no scratchpad da sessao de 17/07 e NAO existe mais em disco; o PDF `Monetarie-Validacao-Partner-API-2026-07-17.pdf` tambem nao foi encontrado (Desktop, Downloads, Documents, Spotlight). O padrao sobrevive DESCRITO em `docs/handoff/2026-07-17-partner-api-deploy-validacao-fixes-handoff.md:30` (17 paginas; evidencia chamada+retorno por item; recusas com erro real; secao de responsabilidade do cliente e modelo de envio de evidencias; regras do dono: defeito interno nunca aparece, 4xx so como "recusa de protecao", acentuacao pt-br na prosa, literais da API intactos). A Task 15 reconstroi o harness de forma versionada para nao perder de novo.

**S8. Credencial de parceiro para G3:** secret `monetarie/homolog/partner/herbeth-santana/api_credentials` (Secrets Manager, via `awsmon`). HML atual: core-api:163 (inclui item 5, QR estatico e ping). Acesso: VPN ou tunel SSM 18080 (roteia por Host header).

---

## Dependencias e ordem

- Tasks 1 a 13 sao executaveis JA (nao dependem de F1).
- Task 7 (exemplos de webhook) tem uma sonda propria de unidade monetaria (S3, pendencia G1).
- Tasks 11a a 11e sao PARAMETRIZADAS: bloqueadas ate a frente F1 fechar e mergear; cada uma comeca re-sondando o router real.
- Task 14 (proposta de publicacao) e Task 15 (relatorio PDF) fecham a frente; a Task 15 tem uma re-emissao parametrizada pos F1.
- DECISAO DO DONO pendente (nao bloqueia nada antes da Task 14): onde publicar o portal e se o takedown do docs-h acontece junto.

---

### Task 1: Worktree e baseline

**Files:** nenhum (setup).

**Step 1:** Criar worktree da frente F4 a partir da main atual (`git worktree add .claude/worktrees/f4-portal-postman -b w1/f4-portal-postman`). Confirmar `git status` limpo.

**Step 2:** Registrar o SHA base no log da frente (comentario no PR/commit final). Conferir que `docs/postman/Monetarie-Partner-API.postman_collection.json` e `docs/partner-portal/public/Monetarie-Partner-API.postman_collection.json` continuam byte-identicos: `md5 <arquivo1> <arquivo2>` (esperado: mesmo hash). Se divergirem, PARAR e reportar (alguem editou uma copia so).

### Task 2: Gate automatizado de paridade doc vs codigo (nasce RED)

Este e o "teste que falha" da frente inteira (G2). Ele codifica a regra de ouro: doc que diverge do codigo e defeito.

**Files:**
- Create: `scripts/check_partner_docs_parity.exs`
- Create: `scripts/check_partner_docs_parity.sh` (wrapper: roda o .exs de `core/backend` com `mix run`)

**CONTRATO PROVADO:** rotas reais em S1 (router.ex:133-222); catalogo em S2 (webhook.ex:33-49); gaps atuais em S4/S5. O script le o router COMPILADO (`Phoenix.Router.routes(MonetarieWeb.Router)`), nunca por regex no fonte.

**Step 1:** Escrever `scripts/check_partner_docs_parity.exs` com 4 verificacoes, cada uma imprimindo diff legivel e acumulando falhas (exit 1 no fim se houver qualquer uma):

```elixir
# scripts/check_partner_docs_parity.exs
# Paridade doc vs codigo da Partner API. Rodar da raiz do monorepo:
#   ./scripts/check_partner_docs_parity.sh
# (o wrapper faz: cd core/backend && mix run ../../scripts/check_partner_docs_parity.exs)
#
# Verifica:
#  V1: cada rota /api/partner/v1 do router tem request na collection Postman
#      (metodo + path, com :param casando qualquer segmento) e vice-versa.
#  V2: as 2 copias da collection sao byte-identicas.
#  V3: todo evento de Webhook.valid_events() aparece em endpoints/webhooks.md
#      nas 3 linguas (pt raiz, en, es) e nenhum evento fantasma e citado.
#  V4: todo evento do catalogo tem exemplo na pasta "Webhooks (exemplos de payload)".
root = Path.expand("../..", File.cwd!())  # core/backend -> raiz do monorepo

routes =
  Phoenix.Router.routes(MonetarieWeb.Router)
  |> Enum.filter(&String.starts_with?(&1.path, "/api/partner/v1"))
  |> Enum.map(&{String.upcase(to_string(&1.verb)), &1.path})
  |> Enum.uniq()

# Rotas sem cobertura Postman por decisao registrada (manter minimo):
allowlist = [
  {"PATCH", "/api/partner/v1/webhooks/:id"} # duplicata do PUT
]

collection_path = Path.join(root, "docs/postman/Monetarie-Partner-API.postman_collection.json")
mirror_path = Path.join(root, "docs/partner-portal/public/Monetarie-Partner-API.postman_collection.json")
collection = collection_path |> File.read!() |> Jason.decode!()

flatten = fn flatten, items ->
  Enum.flat_map(items, fn
    %{"item" => children} = folder -> flatten.(flatten, children) |> Enum.map(&Map.put(&1, :folder, folder["name"]))
    leaf -> [%{name: leaf["name"], request: leaf["request"], folder: nil}]
  end)
end

requests = flatten.(flatten, collection["item"])

postman_routes =
  requests
  |> Enum.reject(&(&1.folder == "Webhooks (exemplos de payload)"))
  |> Enum.map(fn r ->
    raw = get_in(r.request, ["url", "raw"]) || ""
    path = raw |> String.replace("{{baseUrl}}", "") |> String.split("?") |> hd()
    {r.request["method"], path, r.name}
  end)
  |> Enum.filter(fn {_m, p, _n} -> String.starts_with?(p, "/api/partner/v1") end)

match? = fn route_path, concrete_path ->
  rs = String.split(route_path, "/", trim: true)
  cs = String.split(concrete_path, "/", trim: true)
  length(rs) == length(cs) and
    Enum.zip(rs, cs) |> Enum.all?(fn {r, c} -> String.starts_with?(r, ":") or r == c end)
end

failures = []

# V1a: rota sem request
uncovered =
  for {verb, path} <- routes,
      {verb, path} not in allowlist,
      not Enum.any?(postman_routes, fn {m, p, _} -> m == verb and match?.(path, p) end),
      do: {verb, path}

# V1b: request sem rota
orphans =
  for {m, p, name} <- postman_routes,
      not Enum.any?(routes, fn {verb, path} -> m == verb and match?.(path, p) end),
      do: {m, p, name}

failures = if uncovered != [], do: [{:v1_uncovered, uncovered} | failures], else: failures
failures = if orphans != [], do: [{:v1_orphans, orphans} | failures], else: failures

# V2: copias identicas
failures =
  if File.read!(collection_path) == File.read!(mirror_path),
    do: failures,
    else: [{:v2_copies_differ, [collection_path, mirror_path]} | failures]

# V3: catalogo vs webhooks.md nas 3 linguas
events = MapSet.new(Monetarie.Schemas.Webhooks.Webhook.valid_events())
webhook_docs = [
  "docs/partner-portal/endpoints/webhooks.md",
  "docs/partner-portal/en/endpoints/webhooks.md",
  "docs/partner-portal/es/endpoints/webhooks.md"
]

failures =
  Enum.reduce(webhook_docs, failures, fn doc, acc ->
    cited =
      Path.join(root, doc)
      |> File.read!()
      |> then(&Regex.scan(~r/`((?:pix|ted|transfer|account|fee|webhook)\.[a-z_.]+)`/, &1))
      |> Enum.map(fn [_, e] -> e end)
      |> MapSet.new()

    missing = MapSet.difference(events, cited) |> MapSet.to_list()
    ghost = MapSet.difference(cited, events) |> MapSet.to_list()
    acc = if missing != [], do: [{:v3_missing, {doc, missing}} | acc], else: acc
    if ghost != [], do: [{:v3_ghost, {doc, ghost}} | acc], else: acc
  end)

# V4: catalogo vs exemplos da collection (nome do exemplo contem "(evento)")
example_events =
  requests
  |> Enum.filter(&(&1.folder == "Webhooks (exemplos de payload)"))
  |> Enum.flat_map(fn r ->
    case Regex.run(~r/\(([a-z_.]+)\)/, r.name), do: ([_, e] -> [e]; _ -> [])
  end)
  |> MapSet.new()

missing_examples = MapSet.difference(events, example_events) |> MapSet.to_list()
failures = if missing_examples != [], do: [{:v4_missing_examples, missing_examples} | failures], else: failures

if failures == [] do
  IO.puts("PARIDADE OK: #{length(routes)} rotas, #{MapSet.size(events)} eventos, copias identicas.")
else
  Enum.each(Enum.reverse(failures), &IO.inspect(&1, label: "FALHA", limit: :infinity))
  System.halt(1)
end
```

Ajustar sintaxe/detalhes ao rodar (o snippet e o contrato do gate, nao copiar cego: `case ... do` do V4 esta abreviado).

**Step 2:** Wrapper `scripts/check_partner_docs_parity.sh`:

```bash
#!/bin/sh
set -e
cd "$(dirname "$0")/../core/backend"
MIX_ENV=dev mix run ../../scripts/check_partner_docs_parity.exs
```

`chmod +x scripts/check_partner_docs_parity.sh`.

**Step 3 (RED esperado):** Rodar `./scripts/check_partner_docs_parity.sh`. Esperado: FALHA listando exatamente os gaps de S4/S5, no minimo: V1 uncovered `GET /api/partner/v1/ping`, `POST .../pix/qrcodes/static`, `GET .../pix/payments/:id/refunds`; V3 missing `pix.refund.failed` nas 3 linguas; V4 missing ~19 exemplos. Se o RED nao bater com o previsto, investigar antes de seguir (ou o script esta errado, ou a base mudou).

**Step 4:** Commit: `git commit -m "chore(f4): gate de paridade Partner API doc vs codigo (nasce RED)"`.

### Task 3: Postman: `GET /ping`

**Files:**
- Modify: `docs/postman/Monetarie-Partner-API.postman_collection.json` (pasta OAuth, apos "Obter token")

**CONTRATO PROVADO:** S1 (ping_controller.ex:13-18): resposta 200 `{"status": "ok", "partner_id": <id>}`, top-level sem envelope `data`; rota fora do PermissionEnforcer (qualquer token valido); pipeline `[:api, :partner_authenticated, :idempotent]`.

**Step 1:** Inserir request "Verificar credencial (ping)" na pasta OAuth via python3 (carregar JSON, inserir objeto, gravar com `ensure_ascii=False, indent=1` IGUAL ao estilo do arquivo atual; conferir com `git diff --stat` que a mudanca e localizada). Request: `GET {{baseUrl}}/api/partner/v1/ping`, header `Authorization: Bearer {{access_token}}`, description em pt-br explicando que ecoa o `partner_id` da credencial e serve como primeira chamada de sanidade da integracao, com o JSON de resposta REAL de S1.

**Step 2:** Rodar o gate. Esperado: V1 uncovered NAO lista mais o ping (demais falhas continuam).

**Step 3:** Commit.

### Task 4: Postman: `POST /pix/qrcodes/static` (valor aberto e valor fixo)

**Files:**
- Modify: `docs/postman/Monetarie-Partner-API.postman_collection.json` (pasta PIX, antes de "Enviar PIX")

**CONTRATO PROVADO:** S1 (pix_controller.ex:1519-1560): body e resposta exatos, amount em CENTAVOS opcional, resposta 201 com mistura snake/camel (`account_id`, `brcode`, `qrcode_base64`, `txId`, `pixKey`): documentar LITERAL.

**Step 1:** Inserir DOIS requests: "Criar QR estático (valor em aberto)" com body `{"account_id": 1001, "description": "Doacao"}` e "Criar QR estático (valor fixo)" com body `{"account_id": 1001, "amount": 2599, "description": "Pedido 4210"}`. Descriptions: valor em aberto = sem tag 54, pagador digita; amount em centavos; QR reutilizavel e sem expiracao; resposta de exemplo com o shape literal de S1.

**Step 2:** Rodar o gate. Esperado: V1 uncovered nao lista mais `POST /pix/qrcodes/static`.

**Step 3:** Commit.

### Task 5: Postman: refund do item 5 (POST atualizado + trilha GET nova)

**Files:**
- Modify: `docs/postman/Monetarie-Partner-API.postman_collection.json` (pasta PIX)

**CONTRATO PROVADO:** S1 (refund em pix_controller.ex:1058-1140 e list_refunds :1273-1330; whitelist e janela em payment_transactions.ex:334-345). O id de exemplo atual `PIXOUT20260624abc123` e um PIX ENVIADO: o trilho novo so devolve PIX RECEBIDO, entao o exemplo precisa trocar para um id de credito (ex.: `PIXIN20260718abc123`).

**Step 1:** Atualizar o request "Devolver PIX": URL `POST {{baseUrl}}/api/partner/v1/pix/payments/PIXIN20260718abc123/refund`; body `{"account_id": 1001, "amount": 5000, "reason": "MD06", "description": "Devolução solicitada pelo pagador"}`; description nova cobrindo: devolve um PIX RECEBIDO pela conta (a pacs.004 e emitida por nos); janela regulamentar de 90 dias; `reason` opcional (default MD06) restrito a whitelist `MD06 SL02 BE08 FR01 AC03 AC06 AC07 AC14 AG03 AG13 AM09 AM18 RR04`; devolucao parcial permitida ate o restante devolvivel; resposta 202 de exemplo com `refundId`, `originalTransactionId`, `endToEndId`, `amount`, `remainingRefundable`, `status: "processing"`; desfecho chega por `pix.refund.completed` ou `pix.refund.failed`.

**Step 2:** Inserir request novo "Trilha de devoluções da transação" `GET {{baseUrl}}/api/partner/v1/pix/payments/PIXIN20260718abc123/refunds?account_id=1001` com resposta de exemplo literal de S1 (original + remainingRefundable + lista com `rtrId` etc.) e nota: `rtrId` fica nulo enquanto a devolucao processa.

**Step 3:** Rodar o gate. Esperado: V1 uncovered vazio (todas as rotas cobertas); restam V3/V4.

**Step 4:** Commit.

### Task 6: Sonda G1 da unidade monetaria dos webhooks no fio

Pre-requisito da Task 7. NAO escrever exemplo de payload sem isto.

**Files:** nenhum (sonda read-only + registro no proprio plano/commit message).

**Step 1:** Ler a camada de entrega: `core/backend/lib/monetarie/use_cases/webhooks.ex` (ou onde `Webhooks.dispatch_event/3` normaliza o payload) e o worker de entrega HTTP. Provar em que unidade `amount` chega ao receptor do parceiro (o dispatch em S3 envia base units com comentario de conversao na camada de webhook).

**Step 2:** Confrontar com evidencia viva ja existente: os exemplos atuais da pasta "Webhooks (exemplos de payload)" (escritos em 14/07 e validados na campanha de 16-17/07 com receptor externo real para `pix.charge.created` e `pix.key.registered`) e, se houver duvida, um evento real na tabela de entregas de webhook em HML (read-only via rpc ou endpoint de replay).

**Step 3:** Registrar o veredito (unidade + campo a campo de um payload real) num bloco "CONTRATO PROVADO" no commit da Task 7. Se a unidade divergir dos exemplos ATUAIS da collection, isso e defeito de doc pre-existente: corrigir os exemplos antigos na Task 7 tambem.

### Task 7: Postman: exemplos de payload de webhook (paridade 1:1 com o catalogo)

**Files:**
- Modify: `docs/postman/Monetarie-Partner-API.postman_collection.json` (pasta "Webhooks (exemplos de payload)")

**CONTRATO PROVADO:** S2 (35 eventos), S3 (shapes de `pix.payout.returned`, `pix.refund.completed|failed`), Task 6 (unidade no fio). Faltam ~19 exemplos (lista em S4). Para cada evento novo, o shape vem do SITE DE DISPATCH real no codigo (gravar arquivo:linha na description interna do commit, nao no material do cliente):
- `pix.refund.requested`: `dispatch_lifecycle_webhook` no `partner_v1/pix_controller.ex` (aceite do refund).
- `pix.refund.completed|failed`: `dispatch_payout_webhook` (pix_handler.ex:435/1262/1336), campos de S3.
- `pix.payout.returned`: pix_handler.ex:2046-2060, campos de S3 (inclui `returnId` = RtrId).
- Demais (payout.queued/processing/failed, key.deleted/blocked/unblocked, claim.*, account.unblocked/closed, ted.failed, transfer.failed, infraction.resolved): localizar cada `dispatch_event` com `grep -rn "\"<evento>\"" core/backend/lib` e copiar o shape real.

**Step 1:** Para cada evento faltante, inserir um exemplo `POST {{webhook_receiver_url}}` nomeado no padrao existente `Descricao curta (evento.nome)` (o gate V4 parseia o `(evento)` do nome), com body = payload realista no shape provado e description curta em pt-br. Manter o padrao dos 16 exemplos existentes (headers de assinatura HMAC etc.).

**Step 2:** Rodar o gate. Esperado: V4 vazio.

**Step 3:** Commit.

### Task 8: Espelhar a copia 2 do Postman (public/) e conferir byte a byte

**Files:**
- Modify: `docs/partner-portal/public/Monetarie-Partner-API.postman_collection.json` (copia gerada)

**Step 1:** `cp docs/postman/Monetarie-Partner-API.postman_collection.json docs/partner-portal/public/Monetarie-Partner-API.postman_collection.json`

**Step 2:** `md5` dos 2 arquivos: hashes IGUAIS. Rodar o gate: V2 verde.

**Step 3:** Commit: `docs(f4): sincroniza copia public/ da collection (byte a byte)`.

### Task 9: Portal: refund do item 5 + trilha + ping (pt, en, es)

**Files:**
- Modify: `docs/partner-portal/endpoints/pix.md` (secao "Devolver PIX", linhas 74-84, + secao nova)
- Modify: `docs/partner-portal/en/endpoints/pix.md` (secao equivalente, ~:79)
- Modify: `docs/partner-portal/es/endpoints/pix.md` (secao equivalente, ~:79)
- Modify: `docs/partner-portal/autenticacao.md`, `en/authentication.md`, `es/autenticacion.md` (ping como primeira chamada)
- Verify: `docs/partner-portal/casos-de-uso.md` + en/es (se citarem devolucao com semantica velha, atualizar)

**CONTRATO PROVADO:** S1 e S5. A secao pt atual afirma resposta com `transactionId` (errado: e `refundId`) e nao menciona janela/whitelist/parciais/trilha.

**Step 1 (pt):** Reescrever "## Devolver PIX": PIX RECEBIDO pela conta; janela de 90 dias; corpo com `account_id`, `amount` (centavos), `reason` opcional (tabela ou lista dos 13 codigos, default MD06), `description`; devolucao parcial ate o restante devolvivel; resposta 202 literal (refundId, originalTransactionId, endToEndId, amount, remainingRefundable, status); recusas: 404 original inexistente ou de outra conta, 422 fora da janela/valor acima do restante/razao invalida/saldo insuficiente; desfecho por `pix.refund.completed` e `pix.refund.failed`, e devolucao de PIX ENVIADO chega por `pix.payout.returned` (nao por este endpoint). Adicionar "## Trilha de devoluções" com o GET e a resposta literal de S1.

**Step 2 (pt):** Em `autenticacao.md`, apos o fluxo do token, subsecao curta "Verifique a credencial" com `GET /api/partner/v1/ping` e a resposta literal `{"status": "ok", "partner_id": ...}`.

**Step 3:** Replicar 1:1 em en (prosa em ingles) e es (prosa em espanhol), literais da API identicos nas 3 linguas.

**Step 4:** Conferir `casos-de-uso.md` (3 linguas) com `grep -n "refund\|devolu\|devoluc"` e alinhar qualquer fluxo que descreva a semantica velha.

**Step 5:** Commit.

### Task 10: Portal: webhooks.md com paridade 35/35 (pt, en, es)

**Files:**
- Modify: `docs/partner-portal/endpoints/webhooks.md`
- Modify: `docs/partner-portal/en/endpoints/webhooks.md`
- Modify: `docs/partner-portal/es/endpoints/webhooks.md`

**CONTRATO PROVADO:** S2 (35 eventos), S5 (falta `pix.refund.failed` nas 3 linguas), S3 + Task 6 (shapes). O doc afirma paridade 1:1 com `GET /webhooks/events`: essa afirmacao tem que voltar a ser verdadeira.

**Step 1:** Adicionar `pix.refund.failed` na tabela de eventos e uma secao de payload (mesmo formato do `pix.refund.completed` ja documentado em :222, com `status: "rejected"` e `errorReason` com o motivo real). Conferir que a secao existente de `pix.payout.returned` bate com o shape de S3 (returnId, reason, status "returned"); corrigir se divergir.

**Step 2:** Replicar em en e es.

**Step 3:** Rodar o gate. Esperado: V3 verde nas 3 linguas. Gate COMPLETO verde (V1 a V4).

**Step 4:** Commit.

### Task 11: README do Postman + guia

**Files:**
- Modify: `docs/postman/README-Partner-API.md`

**Step 1:** Acrescentar: ping como primeira chamada apos obter o token; mencao curta aos recursos novos (QR estatico com valor em aberto, devolucao de PIX recebido com trilha de acompanhamento) apontando para o portal. Prosa pt-br, sem travessao, sem detalhe interno.

**Step 2:** Commit.

### Tasks 11a-11e (PARAMETRIZADAS, bloqueadas ate F1 mergear na main)

Gatilho comum: plano F1 fechado + merge na main. Cada task comeca com sonda G1 fresca: `grep -n` no `router.ex` para as rotas REAIS (nome/verbo/singular-plural podem divergir do plano F1; o codigo manda), leitura do controller para request/response literais, e `Webhook.valid_events()` para os eventos novos. Depois: portal 3 linguas + requests Postman + exemplos de webhook + `cp` para public/ + gate verde (o gate acusa AUTOMATICAMENTE as rotas novas nao documentadas: rodar o gate contra a main pos-F1 e usar o proprio RED como checklist).

- **11a Claims:** `GET /pix/claims` (listagem) e `POST /pix/claims/:id/complete`; atualizar a secao "Portabilidade de chave" do pix.md (3 linguas) e a pasta PIX do Postman.
- **11b MED 2.0:** rotas cancel/list/refund/graph do plano F1 + webhooks `pix.med.*` (entram no catalogo: gate V3/V4 acusam); secao MED do pix.md + Postman + exemplos.
- **11c Infracoes:** superficie partner nova (create/list/get/close/cancel + defesa do lojista); pagina ou secao nova no portal + Postman; webhooks `pix.infraction.*` ja documentados, conferir mudancas.
- **11d Devolucao de TED:** rota partner + webhooks `ted.refund.*`; secao em `endpoints/transferencias.md` (3 linguas) + pasta Transferencias do Postman + exemplos.
- **11e `GET /accounts/:id/events`:** secao em `endpoints/contas.md` (3 linguas) + Postman.

Cada uma: commit proprio + gate verde + sincronizacao public/ (md5).

### Task 12: Build do portal + navegacao viva (G3 parte 1)

**Files:** nenhum versionado (dist e gitignorado).

**Step 1:** `cd docs/partner-portal && npm ci && npm run build`. Esperado: build sem erro e sem warning de link morto (VitePress falha em dead links por default).

**Step 2:** `npm run preview` (porta local) e navegar com o browser (Playwright/Chrome): home pt, `endpoints/pix` (secao Devolver PIX nova + Trilha + QR estatico), `autenticacao` (ping), `endpoints/webhooks` (pix.refund.failed presente), e as paginas equivalentes em `/en/` e `/es/`. Baixar `/Monetarie-Partner-API.postman_collection.json` pelo preview e conferir md5 igual ao da fonte.

**Step 3:** Evidencia: screenshots das paginas novas (3 linguas) no fecho da task (regra 11 do CLAUDE.md: tela so vale com screenshot).

### Task 13: Executar a collection inteira contra HML (G3 parte 2)

**Files:**
- Create: `docs/reports/2026-07-18-f4-postman-hml-execucao.md` (tabela request x resultado esperado x resultado obtido)

**Pre-condicoes:** HML core-api revisao >= :163 (conferir `awsmon ecs describe-services`); credencial S8; VPN ativa (preferido) ou tunel SSM 18080 com Host header. NUNCA executar durante operacao de money-path ao vivo do dono; avisar o orquestrador antes de rodar.

**Step 1:** Obter `client_id`/`client_secret` do secret S8 via `awsmon secretsmanager get-secret-value` (NUNCA gravar em arquivo versionado). Exportar como variaveis de ambiente do newman.

**Step 2:** Rodar por pasta com `npx newman run docs/postman/Monetarie-Partner-API.postman_collection.json --folder <pasta> --env-var baseUrl=http://coreapi-h.monetarie.internal --env-var client_id=... --env-var client_secret=...`, EXCETO a pasta "Webhooks (exemplos de payload)" (material de doc, aponta para receptor do cliente; nao executar). Requests encadeados (token -> access_token) podem exigir rodar a pasta OAuth primeiro e propagar o token via `--env-var`.

**Step 3:** Tabela de resultado esperado por request (colar no relatorio e preencher o obtido):

| Pasta / request | Esperado |
|---|---|
| OAuth: Obter token | 200 com access_token |
| OAuth: ping | 200 `{"status":"ok","partner_id":...}` |
| Clientes: criar | 201 (CPF sintetico valido novo por rodada) |
| Clientes: consultar / conta adicional | 200 / 201 |
| Contas: listar, detalhar, saldo, extrato | 200 |
| Contas: comprovante | 200 com entry real do extrato; 404 com o exemplo estatico e recusa esperada |
| Contas: limites GET/PUT | 200 |
| Contas: block/unblock | 200, SOMENTE em conta descartavel criada nesta rodada |
| Contas: close | 200, SOMENTE conta descartavel (terminal); pular se nao houver |
| PIX: registrar chave EVP | 202 |
| PIX: listar chaves / excluir chave | 200 / 202 |
| PIX: consultar chave externa (DICT) | 200 found (chave KANASTRA de 16/07) ou not_found legitimo |
| PIX: QR estatico valor aberto | 201, brcode SEM tag 54, CRC valido (validar com decode) |
| PIX: QR estatico valor fixo | 201, tag 54 presente, CRC valido |
| PIX: enviar PIX R$0,01 | 202; desfecho RJCT AB09 do recebedor homolog e o esperado (nao e defeito nosso); SO com OK do orquestrador (money-path) |
| PIX: status | 200 |
| PIX: devolver (id de PIX enviado ou inexistente) | 404/422 recusa de protecao (teste negativo do trilho novo) |
| PIX: trilha de devolucoes (id inexistente) | 404 |
| PIX: criar cobranca / CobV / consultar | 201 / 201 / 200 |
| PIX: decodificar BR Code | 200 (usar o brcode gerado no QR estatico) |
| PIX: claims (criar/consultar/confirmar/cancelar) | criar tende a 4xx recusa em HML sem contraparte real; ids sinteticos = 404; registrar como recusa |
| PIX: MED (abrir com e2e inexistente / consultar) | 422 recusa / 404 |
| Transferencias: TED R$0,01 | 202 dentro da grade; fora da grade, recusa/agendamento conforme codigo; SO com OK do orquestrador |
| Transferencias: interna / status / favoritos | 202 (2 contas do parceiro) / 200 / 201-200-200 |
| Tarifas: catalogo / cobradas | 200 |
| Webhooks: events | 200 com os 35 eventos (conferir contra `Webhook.valid_events()`) |
| Webhooks: CRUD + rotate + test + replay | 201/200/200/200/200/200/200-404; DELETE 200 no webhook criado na rodada |

**Step 4:** Toda divergencia entre esperado e obtido e triada: defeito de DOC (corrigir na task correspondente e re-rodar o gate) ou defeito de API (NAO corrigir aqui; reportar ao orquestrador para a frente dona). Relatorio commitado.

### Task 14: Proposta de publicacao do portal (decisao do dono; zero Terraform novo)

**Files:**
- Create: `docs/reports/2026-07-18-proposta-publicacao-portal-parceiro.md`

**CONTRATO PROVADO:** S6 (inventario). NENHUMA execucao de infra nesta task; documento de decisao apenas.

**Step 1:** Escrever a proposta comparando as opcoes REAIS:
- **Opcao A (recomendada): reaproveitar o service `docs-portal` existente.** Buildar a imagem a partir de `docs/partner-portal/Dockerfile` (nginx limpo servindo so o dist do portal + a collection em `/Monetarie-Partner-API.postman_collection.json`), push para o ECR `monetarie/docs-portal` e force-new-deployment no service existente (HML e PROD). Zero Terraform. BONUS: executa na pratica o takedown pendente da exposicao interna do docs-h (memoria `monetarie-docs-h-internal-exposure`, ordem do dono de 24/06), porque a imagem nova NAO contem `/raw/`. Acesso do cliente: host interno via perfil OpenVPN ja emitido. Riscos: o host `docs-h`/`docs` passa a servir SO o portal do parceiro (a doc interna sai do ar: e o objetivo do takedown, mas confirmar com o dono); tag mutavel vs digest (recomendar tag imutavel no update da task-def, padrao dos demais deploys).
- **Opcao B: takedown puro + entrega como artefato.** `desired-count 0` no docs-portal e entregar o portal como zip/PDF ao cliente (status quo de 14-17/07). Zero infra, mas sem link vivo.
- **Opcao C: S3/CloudFront publico.** NAO existe bucket no Terraform; exigiria infra nova (vetado nesta frente). Registrar como caminho futuro se o dono quiser acesso sem VPN.
- Recomendacao: A, com a pergunta explicita ao dono: (1) aprova o conteudo do host docs virar exclusivamente o portal do parceiro (takedown junto)? (2) HML apenas, ou HML + PROD? (3) quer tag imutavel na task-def nova?

**Step 2:** Commit. A EXECUCAO da opcao escolhida e do orquestrador, fora deste plano.

### Task 15: Relatorio de validacao do cliente (padrao do PDF de 17/07)

**Files:**
- Create: `scripts/partner_validation_report/` (harness versionado: `harness.py` de chamadas, `gen_pdf.py`, template)
- Output: `~/Desktop/Monetarie-Validacao-Partner-API-<data>.pdf` (NAO versionar; sem segredo no repo)

**CONTRATO PROVADO:** S7. O harness original e o PDF de 17/07 nao existem mais em disco; o padrao vem descrito no handoff de 17/07 (:30). Regras do dono (obrigatorias): relatorio do cliente NUNCA expoe defeito interno; 4xx aparece apenas como "recusa de protecao" em teste negativo, nunca como sucesso; acentuacao pt-br perfeita na prosa; literais da API intactos; evidencia = chamada + retorno (+ erro quando teste negativo); secao final de responsabilidade do cliente com modelo de envio de evidencias.

**Step 1:** Reconstruir o harness versionado em `scripts/partner_validation_report/`: le um ledger JSONL de evidencias (request, response, timestamp) produzido na Task 13 (adaptar o newman com `--reporters json` como fonte, ou re-executar as chamadas das APIs novas pelo harness) e gera o PDF com capa, sumario, um item por API validada, evidencias formatadas e as secoes fixas de responsabilidade do cliente.

**Step 2:** Emitir o PDF v1 cobrindo as APIs NOVAS desta frente: `GET /ping`, `POST /pix/qrcodes/static` (aberto e fixo), `POST /pix/payments/:id/refund` + `GET .../refunds` (testes negativos como recusa de protecao; se houver PIX recebido real de teste em HML, incluir o ciclo positivo), webhooks novos (`pix.charge.paid`, `pix.refund.*`, `pix.payout.returned`) com entrega a receptor externo quando exercitavel.

**Step 3:** Revisao adversarial do texto contra as regras do dono (checklist: zero mencao a defeito/fix interno, zero detalhe de rede interna alem do necessario, acentuacao na prosa, literais intactos). So entao entregar no Desktop.

**Step 4 (parametrizado):** Quando F1 fechar e as tasks 11a-11e concluirem, re-emitir o PDF v2 incluindo as APIs de F1 (claims lista/complete, MED, infracoes, TED refund, account events), mesmo padrao.

### Task 16: Fecho da frente

**Step 1:** Rodar o gate completo: `./scripts/check_partner_docs_parity.sh`. Esperado: `PARIDADE OK`.

**Step 2:** `md5` das 2 copias do Postman: iguais. `npm run build` limpo. Suite nao se aplica (frente de docs; o script de paridade nao entra no runtime), mas rodar `cd core/backend && mix compile --warnings-as-errors` para garantir que nada do app foi tocado.

**Step 3:** Revisao adversarial da frente (agente fresco): diff inteiro vs este plano + regras do dono nos textos.

**Step 4:** Merge na main pelo ORQUESTRADOR (F4 nao deploya nada; a publicacao do portal depende da decisao da Task 14).

---

## Riscos e notas

- A collection tem 354KB: toda edicao via python3 com dump identico ao estilo atual, e `git diff` conferido para nao reserializar o arquivo inteiro sem necessidade (um diff gigante esconde regressao de conteudo).
- `POST /pix/payments/:id/refund` e SINGULAR no codigo; qualquer doc/plano que fale `/refunds` no POST esta errado (o GET da trilha e que e plural).
- Exemplos de webhook so nascem de shape provado em site de dispatch real (Task 6/7); exemplo inventado e a classe de defeito que esta frente existe para matar.
- Enviar PIX/TED na Task 13 e money-path em HML: valores minimos, janela combinada com o orquestrador, nunca durante operacao ao vivo do dono.
- As 2 copias do Postman: editar SEMPRE `docs/postman/` e copiar para `public/`; editar a copia errada quebra o gate V2.
