# Plano: exposição pública + API externa + onboarding PF/PJ Nextcode

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** colocar api/docs/ib/merchant (.monetarie.com e -h) no ar com WAF, corrigir o NGINX cross-ambiente do IB/Merchant, ligar o onboarding PF+PJ via jornadas Nextcode ponta a ponta, e subir a API externa de cliente final no modelo AVIV (ApiKey + whitelist obrigatória + HMAC-SHA512).

**Architecture:** cresce o ALB público existente do qrcode em cada ambiente (SNI + regras por host fail-closed + TGs novos), WAF WebACL novo em COUNT antes de BLOCK, backend Elixir ganha pipeline externo e gatilhos de proposals Nextcode, frontends Vue ganham telas/ajustes. Design validado: `docs/plans/2026-07-21-exposicao-publica-api-externa-onboarding-nextcode-design.md` (LER PRIMEIRO).

**Tech Stack:** AWS CLI (profile `vulcimonetarie`, sa-east-1), Elixir/Phoenix (core/backend), Vue 3 (core/apps/{banking,admin,merchant}), nginx envsubst, VitePress (docs), Nextcode proposals API.

**Convenções desta base (obrigatórias):**
- Trabalho direto na `main`, commits locais; PUSH e DEPLOY PRD só com OK do dono.
- AWS sempre via wrapper: `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 "$@"; }`
- Builds de imagem: `linux/arm64`, tag imutável `homolog|prod-<sha>-<slug>-<yyyymmdd>`, PRD = retag por `docker buildx imagetools create` com guard de digest MATCH.
- Testes backend: rodar `mix test` do diretório `core/backend` (atenção ao gotcha do túnel SSM 15432 sombreando o PG de teste: `lsof -i :15432` antes).
- NUNCA gravar a ApiKey da Nextcode ou qualquer segredo em arquivo/markdown/repo. Somente Secrets Manager.
- Screenshots para declarar tela validada (regra 11).

**IDs vivos (verificados 2026-07-21):**
- ALB público HML `monetarie-qrcode-homolog` (SG `sg-0189c5b5898b78688`, VPC `vpc-03e25a932e9fd22a8`, DNS `monetarie-qrcode-homolog-830783211.sa-east-1.elb.amazonaws.com`).
- ALB público PRD `monetarie-qrcode-prod` (SG `sg-0f99d10cacf15594a`, VPC `vpc-0da523dbdbac50b5d`, DNS `monetarie-qrcode-prod-1233236150.sa-east-1.elb.amazonaws.com`).
- SG tasks Fargate: HML `sg-037cd2d0b9f9ae74b` (monetarie-fargate-homolog), PRD `sg-068b1630380e1785f` (monetarie-fargate-prod). Ambos já têm ingress 4003 a partir do SG do ALB público (padrão a replicar p/ 4000/8080). core-api roda em EC2: descobrir o SG real do service antes (Task 4).
- Clusters: `monetarie-greenfield-homolog` / `monetarie-greenfield-prod`. Services: `core-api`, `core-banking-ui`, `core-merchant-ui`, `docs-portal`.
- Cloudflare gerencia `monetarie.com` (dono cria os registros; gerar arquivo de instruções na Task 2).

---

## Fase 1 — Borda (certs, ALB, TGs, SG, ECS)

### Task 1: Solicitar os 2 certs ACM (HML e PRD)

**Step 1:** solicitar HML:
```bash
awsmon acm request-certificate \
  --domain-name api-h.monetarie.com \
  --subject-alternative-names docs-h.monetarie.com ib-h.monetarie.com merchant-h.monetarie.com \
  --validation-method DNS \
  --tags Key=Name,Value=monetarie-public-homolog --query CertificateArn --output text
```
Guardar o ARN. Repetir para PRD com `api.monetarie.com` + SANs `docs/ib/merchant.monetarie.com`, tag `monetarie-public-prod`.

**Step 2:** obter os registros de validação dos 2 certs:
```bash
awsmon acm describe-certificate --certificate-arn <ARN> \
  --query 'Certificate.DomainValidationOptions[].{Domain:DomainName,Name:ResourceRecord.Name,Value:ResourceRecord.Value}' --output table
```
Esperado: 4 CNAMEs de validação por cert (8 no total).

### Task 2: Gerar instruções Cloudflare para o dono

**Files:** Create: `docs/operator/2026-07-21-cloudflare-cnames-monetarie-com.md`

**Step 1:** escrever o arquivo com DUAS tabelas (sem nenhum segredo):
1. CNAMEs de VALIDAÇÃO ACM (8 linhas: Name -> Value da Task 1), DNS-only.
2. CNAMEs de TRÁFEGO (8 linhas), TODOS DNS-only (nuvem CINZA, nunca proxy):
   - `api-h`, `docs-h`, `ib-h`, `merchant-h` -> `monetarie-qrcode-homolog-830783211.sa-east-1.elb.amazonaws.com`
   - `api`, `docs`, `ib`, `merchant` -> `monetarie-qrcode-prod-1233236150.sa-east-1.elb.amazonaws.com`
Incluir o motivo do DNS-only (whitelist de IP + WAF próprios) e aviso de não tocar nos registros do qrcode.

**Step 2:** commit do arquivo e AVISAR O DONO para aplicar (bloqueia a Task 3).

### Task 3: Confirmar certs ISSUED

**Step 1:** aguardar validação (poll):
```bash
awsmon acm describe-certificate --certificate-arn <ARN> --query 'Certificate.Status' --output text
```
Esperado: `ISSUED` nos 2. Não seguir para a Task 5 sem isso.

### Task 4: Target groups + security groups

**Step 1:** criar 4 TGs por ambiente (nomes <=32 chars). HML (`vpc-03e25a932e9fd22a8`):
```bash
for spec in "mon-extapi-pub-h:4000" "mon-ib-pub-h:8080" "mon-mer-pub-h:8080" "mon-docs-pub-h:8080"; do
  name="${spec%%:*}"; port="${spec##*:}"
  awsmon elbv2 create-target-group --name "$name" --protocol HTTP --port "$port" \
    --vpc-id vpc-03e25a932e9fd22a8 --target-type ip --health-check-path /health \
    --health-check-interval-seconds 30 --health-check-timeout-seconds 5 \
    --healthy-threshold-count 2 --unhealthy-threshold-count 3 --matcher HttpCode=200-399 \
    --query 'TargetGroups[0].TargetGroupArn' --output text
done
```
PRD igual com sufixo `-p` e `vpc-0da523dbdbac50b5d`.

**Step 2:** descobrir o SG do core-api (EC2) em cada ambiente:
```bash
awsmon ecs describe-services --cluster monetarie-greenfield-homolog --services core-api \
  --query 'services[0].networkConfiguration.awsvpcConfiguration.securityGroups' --output json
```
(repetir no cluster prod).

**Step 3:** ingress novos (replica o padrão 4003 já existente):
```bash
# HML: UIs (fargate SG) 8080 + core-api (SG da Step 2) 4000, origem = SG do ALB público
awsmon ec2 authorize-security-group-ingress --group-id sg-037cd2d0b9f9ae74b \
  --protocol tcp --port 8080 --source-group sg-0189c5b5898b78688
awsmon ec2 authorize-security-group-ingress --group-id <SG_CORE_API_HML> \
  --protocol tcp --port 4000 --source-group sg-0189c5b5898b78688
# ALB público HML: abrir :80 (para o redirect da Task 5)
awsmon ec2 authorize-security-group-ingress --group-id sg-0189c5b5898b78688 \
  --protocol tcp --port 80 --cidr 0.0.0.0/0
awsmon ec2 authorize-security-group-ingress --group-id sg-0189c5b5898b78688 \
  --ip-permissions 'IpProtocol=tcp,FromPort=80,ToPort=80,Ipv6Ranges=[{CidrIpv6=::/0}]'
```
PRD espelhado (`sg-068b1630380e1785f`, `sg-0f99d10cacf15594a`, SG core-api prod). Se o core-api usar o mesmo SG fargate, só a regra 4000 nesse SG.

### Task 5: Listener 443 (SNI + regras) e listener 80 (redirect)

**Step 1:** anexar cert por SNI (por ambiente):
```bash
LARN=$(awsmon elbv2 describe-listeners --load-balancer-arn <ALB_ARN> --query 'Listeners[?Port==`443`].ListenerArn' --output text)
awsmon elbv2 add-listener-certificates --listener-arn "$LARN" --certificates CertificateArn=<CERT_ARN>
```

**Step 2:** criar listener :80 redirect:
```bash
awsmon elbv2 create-listener --load-balancer-arn <ALB_ARN> --protocol HTTP --port 80 \
  --default-actions 'Type=redirect,RedirectConfig={Protocol=HTTPS,Port=443,StatusCode=HTTP_301}'
```

**Step 3:** regras por host no listener 443. HML:
```bash
awsmon elbv2 create-rule --listener-arn "$LARN" --priority 20 \
  --conditions '[{"Field":"host-header","HostHeaderConfig":{"Values":["api-h.monetarie.com"]}},{"Field":"path-pattern","PathPatternConfig":{"Values":["/api/external/*","/api/webhooks/nextcode/*"]}}]' \
  --actions Type=forward,TargetGroupArn=<TG_EXTAPI_H>
awsmon elbv2 create-rule --listener-arn "$LARN" --priority 30 \
  --conditions '[{"Field":"host-header","HostHeaderConfig":{"Values":["ib-h.monetarie.com"]}}]' \
  --actions Type=forward,TargetGroupArn=<TG_IB_H>
awsmon elbv2 create-rule --listener-arn "$LARN" --priority 40 \
  --conditions '[{"Field":"host-header","HostHeaderConfig":{"Values":["merchant-h.monetarie.com"]}}]' \
  --actions Type=forward,TargetGroupArn=<TG_MER_H>
awsmon elbv2 create-rule --listener-arn "$LARN" --priority 50 \
  --conditions '[{"Field":"host-header","HostHeaderConfig":{"Values":["docs-h.monetarie.com"]}}]' \
  --actions Type=forward,TargetGroupArn=<TG_DOCS_H>
```
PRD igual com hosts sem `-h`. A regra prio 10 do qrcode e o default 404 NÃO são tocados.

### Task 6: ECS — segundo target group por service

**Step 1:** por service, montar a lista COMPLETA de load balancers (o update-service SUBSTITUI a lista; incluir o TG interno atual + o novo):
```bash
awsmon ecs describe-services --cluster monetarie-greenfield-homolog --services core-banking-ui \
  --query 'services[0].loadBalancers' --output json
```

**Step 2:** aplicar (exemplo banking HML; container/porta vêm da Step 1):
```bash
awsmon ecs update-service --cluster monetarie-greenfield-homolog --service core-banking-ui \
  --load-balancers '[{"targetGroupArn":"<TG_INTERNO_ATUAL>","containerName":"<container>","containerPort":8080},{"targetGroupArn":"<TG_IB_PUB_H>","containerName":"<container>","containerPort":8080}]' \
  --force-new-deployment
```
Repetir: core-merchant-ui, docs-portal (8080) e core-api (4000), nos 2 ambientes. ATENÇÃO core-api PRD: é deploy de service em produção; fazer com o dono ciente e fora de operação de money-path ao vivo.

**Step 3:** verificar targets healthy nos 8 TGs novos:
```bash
awsmon elbv2 describe-target-health --target-group-arn <TG> --query 'TargetHealthDescriptions[].TargetHealth.State'
```
Esperado: `healthy` em todos.

**Step 4:** validação viva da borda (antes da propagação DNS usar `--resolve`):
```bash
curl -sS -o /dev/null -w '%{http_code}\n' --resolve docs-h.monetarie.com:443:<IP_ALB_HML> https://docs-h.monetarie.com/
curl -sS --resolve api-h.monetarie.com:443:<IP_ALB_HML> https://api-h.monetarie.com/api/external/ping   # 404 esperado ATE a Fase 6
curl -sS --resolve api-h.monetarie.com:443:<IP_ALB_HML> https://api-h.monetarie.com/admin/anything      # 404 fixo do default (prova do fail-closed)
```

**Step 5:** commit de um registro operacional `docs/operator/2026-07-21-borda-publica-monetarie-com.md` com ARNs criados e evidências.

---

## Fase 2 — NGINX banking/merchant parametrizado por ambiente

### Task 7: banking — template envsubst (espelho do admin)

**Files:**
- Modify: `core/apps/banking/nginx.conf` (upstream vira `${CORE_API_UPSTREAM}` / `${CORE_API_HOST}`)
- Create: `core/apps/banking/docker-entrypoint.sh` (copiar de `core/apps/admin/docker-entrypoint.sh`)
- Modify: `core/apps/banking/Dockerfile`

**Step 1:** em `nginx.conf`: trocar as 3 ocorrências hardcoded:
```nginx
map $host $monetarie_core_api {
    default "${CORE_API_UPSTREAM}";
}
# ... nos 2 blocos proxy:
    proxy_set_header   Host              ${CORE_API_HOST};
```

**Step 2:** no `Dockerfile` (estágio runtime), espelhar o admin:
```dockerfile
RUN apk add --no-cache curl tini gettext
COPY apps/banking/nginx.conf /etc/nginx/templates/default.conf.template
COPY apps/banking/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ENV CORE_API_UPSTREAM="http://core-api-h.monetarie.internal:80" \
    CORE_API_HOST="core-api-h.monetarie.internal"
```
(mantendo chowns/PID/tini já existentes; adicionar chmod +x do entrypoint e chown de /etc/nginx/conf.d, igual ao admin; ENTRYPOINT via docker-entrypoint.sh como no admin).

**Step 3:** teste local do template:
```bash
docker buildx build --platform linux/arm64 -f core/apps/banking/Dockerfile -t bank-nginx-test core/ --load
docker run --rm -e CORE_API_UPSTREAM=http://core-api.monetarie.internal:80 -e CORE_API_HOST=core-api.monetarie.internal bank-nginx-test sh -c 'sleep 1; cat /etc/nginx/conf.d/default.conf' | grep core-api
```
Esperado: upstream/Host renderizados SEM `-h` (prova de parametrização) e nginx sobe sem erro de sintaxe.

**Step 4:** commit `fix(ib): nginx upstream parametrizado por ambiente (PRD apontava p/ HML)`.

### Task 8: merchant — idêntico

Mesmos steps da Task 7 em `core/apps/merchant/{nginx.conf,Dockerfile}` + `docker-entrypoint.sh`. Commit próprio.

### Task 9: build + deploy das 2 UIs (HML, depois PRD)

**Step 1:** build/push `homolog-<sha>-nginxenv-<data>` das 2 imagens (contexto `core/`).
**Step 2:** HML: nova task-def SEM env extra (default já é HML), update-service, aguardar COMPLETED, TG interno e público healthy.
**Step 3:** validação viva HML: login IB por `https://ib-h.monetarie.com` (DNS já propagado) e uma chamada `/api` real (dashboard carrega). Screenshot.
**Step 4:** PRD (com OK do dono): retag por digest, task-def com `CORE_API_UPSTREAM=http://core-api.monetarie.internal:80` e `CORE_API_HOST=core-api.monetarie.internal` no container, update-service.
**Step 5:** validação viva PRD: `https://ib.monetarie.com` login + `/api` respondendo (hoje está morto por timeout; sucesso = correção provada). Screenshot. Idem merchant.

---

## Fase 3 — WAF em COUNT

### Task 10: IP set + WebACL por ambiente

**Step 1:** IP set (vazio; o sync worker da Fase 6 alimenta):
```bash
awsmon wafv2 create-ip-set --name monetarie-extapi-clients-h --scope REGIONAL \
  --ip-address-version IPV4 --addresses [] --description "IPs whitelistados das api_keys ativas"
```

**Step 2:** WebACL `monetarie-public-homolog` com TODAS as regras em COUNT (managed groups Common/KnownBadInputs/SQLi/IpReputation/AnonymousIp com `overrideAction: {"count":{}}` e scope-down excluindo `/api/external/*` das regras de payload; rate-based `blanket-rate` limit 100000 por IP/5min em count). Gravar o JSON completo usado em `docs/operator/2026-07-21-waf-webacl-monetarie.md` (sem segredos) para reproduzir em PRD.

**Step 3:** associar ao ALB:
```bash
awsmon wafv2 associate-web-acl --web-acl-arn <ACL_ARN> --resource-arn <ALB_ARN>
```

**Step 4:** verificar métricas `CountedRequests` no CloudWatch após tráfego de teste. NENHUMA regra em BLOCK sem OK do dono (rate piso do sistema: 120.000 req/60s — limites por IP ficam ACIMA do uso legítimo esperado por origem, e o IP set de clientes fica isento quando virar BLOCK).

**Step 5:** repetir em PRD (`monetarie-extapi-clients-p`, `monetarie-public-prod`).

---

## Fase 4 — Onboarding PF/PJ Nextcode (backend TDD + telas)

### Task 11: Segredo Nextcode + env

**Step 1:** gravar a ApiKey (fornecida pelo dono em 21/07; NUNCA em arquivo) nos 2 ambientes:
```bash
awsmon secretsmanager create-secret --name monetarie/homolog/kyc/nextcode/api_key --secret-string '<COLAR_AQUI_NA_EXECUCAO>'
awsmon secretsmanager create-secret --name monetarie/prod/kyc/nextcode/api_key --secret-string '<COLAR_AQUI_NA_EXECUCAO>'
```
**Step 2:** task-def do core-api (nas 2): `secrets` += `NEXTCODE_API_KEY` (valueFrom = ARN acima). `NEXTCODE_ENABLED` fica `false` até o flip autorizado. Entra no deploy da Task 18.
**Step 3:** anotar pendência: confirmar com a Nextcode a base_url da key (`onboarding-api.nxcd.app` vs `staging.nxcd.app`) ANTES do flip em HML.

### Task 12: Migration — proposals/registrations aceitam PF

**Files:**
- Create: `core/backend/priv/repo/migrations/20260721150000_add_pf_support_to_nextcode_proposals.exs`
- Test: `core/backend/test/monetarie/onboarding/nextcode/proposals_pf_test.exs` (nasce na Task 13)

**Step 1:** migration aditiva: `nextcode_proposals` += `tax_id :string` + `person_type :string` (default `"pj"`), backfill `tax_id = cnpj`; `onboarding_registrations` += `cpf :string`, índice em `tax_id`. `cnpj` continua (compat).
**Step 2:** `mix ecto.migrate` local + rollback + migrate de novo (prova de reversibilidade).
**Step 3:** commit.

### Task 13: Builder alinhado aos JSONs oficiais

**Files:**
- Create: `core/backend/priv/nextcode/journey_pf.json` e `journey_pj.json` (cópia EXATA dos 2 JSONs do Desktop; contêm só config/branding, sem segredo)
- Modify: `core/backend/lib/monetarie/use_cases/onboarding/nextcode/proposals.ex` (`build_template_from_settings/1` e vizinhos)
- Test: `core/backend/test/monetarie/onboarding/nextcode/proposals_pf_test.exs`

**Step 1:** testes RED provando o contrato oficial:
- PJ prod: `products == ["receita-federal-cnpj-qsa","survey","ocr","liveness"]`, `postProcessingProducts == ["faceMatch"]`, `surveyId == "6a5e237f912cbb919313b84c"` (da config), `acceptedDocuments` do JSON, `deliveryTypes == ["email"]`, `redirectTo`, `expirationTimeInMs == 259200000`, layout com primary `#CE8F32`.
- PF prod: `products == ["ocr","liveness"]`, SEM `surveyId`, mesmos demais campos.
- `create_proposal` com `person_type: "pf"` persiste `tax_id` = CPF e `person_type` = `"pf"`.
**Step 2:** rodar, ver falhar pelos motivos certos (products divergentes hoje: `backgroundCheckLegalEntity`).
**Step 3:** implementar: builder carrega os JSONs de `priv/nextcode/` como base do template (merge com settings da `kyc_provider_configs`: survey_id, test_mode, logo custom); `create_proposal/2` aceita `%{person_type: "pf"|"pj", tax_id: cpf_ou_cnpj, email: ...}`.
**Step 4:** suite alvo verde: `mix test test/monetarie/onboarding/nextcode/`.
**Step 5:** commit.

### Task 14: Gatilho coreadmin manual (PF e PJ)

**Files:**
- Modify: `core/backend/lib/monetarie_web/router.ex` (scope admin onboarding)
- Create: `core/backend/lib/monetarie_web/controllers/admin/nextcode_journey_controller.ex`
- Test: `core/backend/test/monetarie_web/controllers/admin/nextcode_journey_controller_test.exs`

**Step 1:** teste RED: `POST /api/admin/onboarding/nextcode-journey` body `{type: "pf", name, email, tax_id}` (admin autenticado) -> 201 com `{id, link, status}`; CPF inválido -> 422; `type: "pj"` com CNPJ -> 201; sem permissão -> 403; Nextcode fora (client mockado com erro) -> 502 legível SEM persistir proposal órfã.
**Step 2:** implementar controller fino chamando `Proposals.create_proposal` (client via behaviour mockável, padrão já usado no repo).
**Step 3:** verde + commit.

### Task 15: Approve = promoção completa (user + member + conta)

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/admin/kyc_controller.ex` (approve)
- Test: `core/backend/test/monetarie_web/controllers/admin/kyc_controller_promotion_test.exs`

**Step 1:** teste RED: approve de proposal PF finished cria `users` (`member_pf`) + `cooperative_members` (`pessoa_fisica`) + conta via `OpenAccount` (wallet TB inclusa) atomicamente; PJ idem (`member_pj`/`pessoa_juridica`); tax_id duplicado -> erro legível sem meia-escrita; reject não cria nada.
**Step 2:** implementar delegando a `Monetarie.UseCases.Onboarding.Promotion` (reusar; não duplicar lógica no controller). O moduledoc mentiroso do controller é corrigido.
**Step 3:** verde + commit.

### Task 16: Gatilhos IB (PF pós-OTP; PJ no step kyc)

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/onboarding_controller.ex` (fluxo PF, pós `verify_otp`)
- Modify: `core/backend/lib/monetarie/use_cases/onboarding/business.ex` (step `kyc`)
- Tests: ampliar os testes dos 2 fluxos

**Step 1:** teste RED PF: com `NEXTCODE_ENABLED=true` (config de teste) o `verify_otp` bem-sucedido dispara `create_proposal` PF (client mock) e a application guarda `nextcode_proposal_id` + expõe `journey_link` no status; com flag OFF, comportamento atual intacto (byte a byte).
**Step 2:** teste RED PJ: completar o step `kyc` dispara proposal PJ (CNPJ da business_onboarding) e persiste vínculo; flag OFF = comportamento atual.
**Step 3:** implementar fail-soft: falha na Nextcode NÃO derruba o fluxo (loga + telemetria + status `kyc_pending_retry`); jamais bloquear o caminho que hoje funciona.
**Step 4:** verde + commit.

### Task 17: Telas — admin (registro PF/PJ + fila de review) e IB (acompanhamento)

**Files:**
- Modify: `core/apps/admin/src/views/onboarding/OnboardingNewView.vue` (opção "Jornada Nextcode" com radio PF/PJ, campos nome/e-mail/documento, POST no endpoint da Task 14)
- Create: `core/apps/admin/src/views/onboarding/NextcodeReviewListView.vue` + `NextcodeReviewDetailView.vue` (usar o composable órfão `src/composables/useOnboarding.ts` — rotas `/kyc/pending`, approve/reject/fetch-document)
- Modify: `core/apps/admin/src/router/index.ts` (2 rotas novas sob o menu de onboarding)
- Modify: `core/apps/banking/src/views/onboarding/*` (tela pós-OTP mostra link/status da jornada quando o backend devolver `journey_link`; PJ idem no step kyc)
- Tests: vitest dos componentes novos (render + chamadas mockadas)

**Step 1:** vitest RED dos 2 views novos do admin (lista renderiza pendências mockadas; approve chama endpoint certo).
**Step 2:** implementar; i18n pt/en/es das chaves novas; PrimeVue Select para PF/PJ.
**Step 3:** `pnpm --filter @monetarie/admin test` e build verdes; idem banking. Commit.

### Task 18: Deploy HML Frente 2 + validação viva

**Step 1:** build core-api + core-admin-ui + core-banking-ui (`homolog-<sha>-nextcodepfpj-<data>`); migration `20260721150000` via run-task rpc ANTES do swap (receita padrão do repo); deploy dos 3.
**Step 2:** flip `NEXTCODE_ENABLED=true` HML na task-def (base_url confirmada na Task 11.3) + registro da config na tela `NextcodeProviderView` (survey_id PJ, e-mails).
**Step 3:** validação viva: jornada PF real e PJ real disparadas do admin (e-mail chega, link abre), webhook finish recebido por `https://api-h.monetarie.com/api/webhooks/nextcode/...` (prova de que a borda pública fecha o ciclo), review aparece na tela nova, approve cria user+member+conta (conferir no banco + TB). Screenshots (regra 11).
**Step 4:** PRD: só com OK explícito do dono (mesma sequência, retag por digest, migration antes do swap).

---

## Fase 5 — API externa `/api/external` (modelo AVIV)

### Task 19: Plug HmacValidation (port AVIV)

**Files:**
- Create: `core/backend/lib/monetarie_web/plugs/hmac_validation.ex`
- Test: `core/backend/test/monetarie_web/plugs/hmac_validation_test.exs`

**Step 1:** testes RED (contrato AVIV exato): header `hmac` ausente em POST -> 401; corpo JSON com chaves fora de ordem assinado sobre a forma canônica (ordenada alfabeticamente, compacta) -> 200; assinatura errada -> 401; algoritmo = HMAC-SHA512 hex minúsculo de 128 chars; chave = `conn.assigns[:api_key_secret]`; comparação constant-time; GET/DELETE não exigem.
**Step 2:** implementar: `:crypto.mac(:hmac, :sha512, secret, canonical_body)`; canônico = `Jason.decode!` -> ordenar chaves recursivamente -> `Jason.encode!` compacto; `Plug.Crypto.secure_compare/2`. Ler o raw body via o `CachingBodyReader` já existente no repo.
**Step 3:** verde + commit.

### Task 20: ApiKeyAuth fail-closed p/ pipeline externo

**Files:**
- Modify: `core/backend/lib/monetarie_web/plugs/api_key_auth.ex`
- Test: ampliar `core/backend/test/monetarie_web/plugs/api_key_auth_test.exs`

**Step 1:** teste RED: `plug ApiKeyAuth, require_ip_whitelist: true` -> chave SEM whitelist = 403 "ip whitelist required"; com whitelist e IP fora = 403; IP dentro = passa; sem a opção, comportamento atual intacto. Resolução de IP: XFF right-most-trusted (padrão já corrigido no repo em c50fc04a — reusar).
**Step 2:** implementar opção; expor `api_key_secret` no assigns no sucesso (necessário pro HMAC).
**Step 3:** verde + commit.

### Task 21: Pipeline + scope + ping

**Files:**
- Modify: `core/backend/lib/monetarie_web/router.ex`
- Create: `core/backend/lib/monetarie_web/controllers/external/ping_controller.ex`
- Test: `core/backend/test/monetarie_web/controllers/external/ping_controller_test.exs`

**Step 1:** pipeline `:external_api = [:api, ApiKeyAuth {require_ip_whitelist}, :audited, HmacValidation, RateLimiterPerKey, Idempotency]`; scope `/api/external` com `GET /ping`.
**Step 2:** teste integração: sem ApiKey -> 401; chave válida + IP whitelistado -> `{"status":"ok"}`; via chave sem whitelist -> 403.
**Step 3:** verde + commit.

### Task 22: Endpoints fase A — saldo, extrato, chaves

**Files:** Create: `core/backend/lib/monetarie_web/controllers/external/{account_controller,statement_controller,keys_controller}.ex` + testes.

Contrato: valores em CENTAVOS (contrato v2/partner do repo — `monetarie-money-unit-scale-contract`). Reusar os use cases que o Partner v1 e o v2 já usam (localizar por `AccountsController`/`StatementController` do partner_v1; a chave externa é escopada por `merchant_id` -> resolver conta primária do user). TDD por endpoint: 200 feliz, 401/403 do pipeline, conta de outro merchant inacessível (IDOR test).

### Task 23: Endpoints fase B — PIX cash-out (chave/EMV), cash-in QR, consultas, devolução

Reusar o funil v2 real de pagamento (lookup-to-pay com E2E do DICT reusado — obrigatório, ver memória `monetarie-money-path-submissao-inferencia-0716`) e o motor de QR da cabine via os subjects já existentes. NUNCA criar caminho novo de money-path: os controllers externos são adaptadores finos sobre os use cases v2 validados. TDD: happy-path com cabine mockada no seam `pix_provider()`, hold devolvido em rejeição, idempotency-key repetida não duplica, unidade CENTAVOS na fronteira.

### Task 24: Endpoints fase C — MED/infrações + defesa, validação CPF, webhooks CRUD

Webhooks: reusar `WebhookController`/use cases v1 existentes (escopo por api_key). MED: espelho dos endpoints do partner (fluxo 2.12.1 vigente). CPF validate: usar o validador existente do onboarding. TDD por endpoint.

### Task 25: Sync do IP set no WAF (port AwsWafSyncWorker)

**Files:**
- Create: `core/backend/lib/monetarie/workers/aws_waf_sync_worker.ex` + `core/backend/lib/monetarie/security/aws_waf_client.ex`
- Test: testes unitários do bundle (união dos ip_whitelist ativos, normalização /32, dedupe) com client HTTP mockado.

Config por env: `AWS_WAF_SYNC_ENABLED` (default false), nome/ID do IP set. Fila Oban própria de baixa concorrência. Flip só depois do WebACL existir (Fase 3).

### Task 26: Deploy API externa HML + validação viva

Build/deploy core-api HML; criar api_key de teste com whitelist do IP do túnel de validação; provar vivo: ping/saldo/extrato 200 assinados com HMAC; POST sem HMAC 401; IP fora da whitelist 403; rota interna via api-h 404 na borda. PRD com OK do dono. Screenshots/transcript no relatório operacional.

---

## Fase 6 — docs.monetarie.com (formato AVIV)

### Task 27: Estrutura do portal da API externa

**Files:** Create: `docs/external-portal/` (VitePress novo, base na estrutura do `docs/partner-portal/`): `index.md`, `visao-geral.md`, `ciclo-de-vida-pix.md`, `ambientes.md` (api-h vs api), `autenticacao.md` (ApiKey), `hmac.md` (página dedicada, exemplos JS/Python/PHP/Java/C#/curl sobre o algoritmo EXATO da Task 19), `postman.md` + collection, endpoints por área (cash-out, cash-in, conta, chaves, devolução, MED, CPF, webhooks com payloads), `en/` e `es/`.
Gate de paridade doc==código (reusar o harness do F4). O conteúdo da Partner API NÃO entra no build público (permanece no portal interno).

### Task 28: Build + deploy docs-portal

Dockerfile próprio (espelho do `docs/partner-portal/Dockerfile`), imagem para o ECR `monetarie/docs-portal`, deploy HML -> validação viva `https://docs-h.monetarie.com` (screenshot) -> PRD com OK.

---

## Checkpoints com o dono (obrigatórios)
1. Task 2: aplicar CNAMEs no Cloudflare (bloqueante).
2. Task 6/9/18/26/28: qualquer deploy PRD.
3. Task 11.3: base_url da key Nextcode + flip `NEXTCODE_ENABLED`.
4. Fase 3 -> BLOCK mode do WAF (nunca sem OK; calibrar com o piso 120k req/60s).
5. Push da main.
