# Integração Master API ↔ BFF NestJS

Documentação da integração com os endpoints administrativos **master** da Planfi API (`/v1/master/*`), consumidos pelo **BFF** via `MasterApiService` e cache do token master. Também há um endpoint HTTP **`POST /master-api/users/resolve-uuids-by-emails`** para consumidores autorizados (ex.: Atlas), protegido pelo **`x-api-key`** global do BFF.

## 1. Visão geral

A API Laravel expõe rotas **master** para acesso administrativo e serviço-a-serviço, separadas do fluxo normal de usuários autenticados com JWT. No BFF, o **`MasterApiService`** usa a base **`LARAVEL_URL`** (host **sem** o prefixo `/api`, típico quando o proxy encaminha `*/v1/master/*` diretamente) e faz `POST {LARAVEL_URL}/v1/master/token` para obter o `pfm_...`.

O cliente **`LaravelApiService`** usa apenas **`LARAVEL_API_URL`** — **origem HTTP** (`scheme://host:porta`, **sem** repetir `/api`; os endpoints no código já são `/api/...`). Opcionalmente pode escrever **`LARAVEL_API_URL=${LARAVEL_URL}`** no `.env`: no arranque substitui-se apenas esse placeholder (sem mexer no valor de **`LARAVEL_URL`** nem derivar uma URL a partir da outra).

No upstream, o `MasterApiTokenService` valida IP (allowlist) exceto em ambientes configurados como abertos (por exemplo `local` / `dev`). Com isso, **em produção** o BFF precisa rodar com IP permitido ou o tráfego deve passar por um proxy confiável.

Esta integração **não substitui** o `LaravelApiService` usado com JWT de usuário: é um canal paralelo apenas para cenários administrativos e impersonation.

## 2. Funcionalidades

- Emissão e **cache local** do token master com margem de expiração (60 segundos antes do `expires_at` retornado pelo upstream).
- **Refresh em caso de HTTP 401** nas chamadas autenticadas com o token master (`/v1/master/users` ou rotas impersonadas): cache invalidado, nova emissão, **uma nova tentativa**; segundo 401 consecutivo gera erro interno.
- **Anti-stampede**: uma única requisição de emissão em voo quando várias goroutines/async competem pelo token.
- **Listagem paginada** de usuários (`GET /v1/master/users`) com mapeamento de query camelCase ↔ snake_case.
- **Resolução em lote email → UUID** (`MasterApiService.resolveUuidsByEmails` e `POST /master-api/users/resolve-uuids-by-emails`): consulta o Laravel pelo master com filtro **`LIKE`**, aplicando sempre **match exato** por email normalizado antes de responder (§ 5.1).
- **Impersonation**: qualquer rota REST da Planfi pode ser chamada com `Authorization: Bearer <token_master>` mais o cabeçalho configurável (por padrão `X-Master-User-UUID`), fazendo o upstream resolver o contexto como o usuário-alvo.

## 3. Configuração

| Variável | Obrigatoriedade | Descrição |
|----------|-----------------|------------|
| `LARAVEL_URL` | Sim | Base do host **sem** `/api`: `MasterApiService` chama **`{LARAVEL_URL}/v1/master/*`** quando o upstream expõe master fora do prefixo `/api`. |
| `LARAVEL_API_URL` | Sim | Origem da API Laravel para JWT / `LaravelApiService` (**não** acrescentar `/api` aqui; paths no código já incluem `/api/...`). Opcional: igual a `LARAVEL_URL` ou `${LARAVEL_URL}` no ficheiro. |
| `MASTER_API_SECRET` | Sim | Igual ao `MASTER_API_SECRET` da API Laravel. |
| `MASTER_API_TOKEN_TTL_SECONDS` | Opcional | Padrão `3600`. Usado apenas como *fallback* se `expires_at` da resposta de token não puder ser interpretado (segundos). |
| `MASTER_API_ALLOWED_METHODS` | Opcional | Lista CSV; padrão `GET,POST,PUT,PATCH,DELETE`. Enviado no corpo `{ methods }` na emissão do token e limitado pela intersecção com o que o upstream aceita — necessário cobrir impersonation ampla.
| `MASTER_API_IMPERSONATION_HEADER` | Opcional | Nome do cabeçalho UUID; padrão `X-Master-User-UUID` (alinhado a `api/config/master_api.php`). |

Exemplo em `.env` do BFF:

```env
LARAVEL_URL=http://localhost
LARAVEL_API_URL=http://localhost
# ou: LARAVEL_API_URL=${LARAVEL_URL}
MASTER_API_SECRET=copiar_de_api_dot_env
MASTER_API_TOKEN_TTL_SECONDS=3600
MASTER_API_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE
MASTER_API_IMPERSONATION_HEADER=X-Master-User-UUID
```

No projeto Laravel correspondente deve existir `MASTER_API_ENABLED=true` e o mesmo `MASTER_API_SECRET`. Em desenvolvimento local o allowlist de IP costuma estar relaxado pelo ambiente.

## 4. Arquitetura do token

Fluxo simplificado:

1. O consumidor chama `MasterApiService` (por exemplo `listUsers`).
2. O serviço chama `getToken()` que:
   - devolve cache se `expires_at - 60s` ainda está no futuro;
   - senão registra uma promessa única (`inflightTokenPromise`) e chama `POST /v1/master/token` com `{ secret, methods }`.
3. Corpo efemérico `{ access_token, expires_at, allowed_methods }` alimenta o cache em memória (por instância do serviço).
4. Todas as requisições autenticadas usam `Authorization: Bearer <access_token>`.

Se o segredo Laravel for rotacionado, tokens antigos passam a retornar HTTP 401 (fingerprint diferente): o cliente BFF não trata esse caso como especial — o fluxo de **refresh-on-401** passa pela invalidação do cache e nova emissão com o novo `MASTER_API_SECRET` configurado localmente.

## 5. API do serviço

```typescript
await masterApi.listUsers({ perPage: 25, subscriptionStatus: 'active' });

const usuario = await masterApi.findUserByUuid('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
// null quando não há registro compatível na página retornada (uuid + per_page 1).

const mesmoEmailLike = await masterApi.searchUsersByEmail('@planfi.dev');
// faz listUsers({ email: '...', perPage: 100 }) e devolve `data`.

const portfolios = await masterApi.makeImpersonatedRequest<PortfoliosEnvelope>(
  'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
  { method: 'GET', path: '/api/v1/portfolios' },
);

const emailsParaUuid = await masterApi.resolveUuidsByEmails(['a@dominio.com', 'b@outro.br']);
// { resolved: [{ email, uuid }], notFound: string[], ambiguous: string[] }
```

### 5.1 Resolver UUIDs Laravel a partir de emails (HTTP + fluxo Atlas)

No Laravel o filtro de email em `/v1/master/users` usa **`LIKE`**. Para evitar associações erradas quando o servidor devolver várias linhas “vizinhas”, o BFF só considera **igualdade após normalização** (`trim` + `.toLowerCase()` em ambos os lados: email pedido × `MasterUser.email`).

**HTTP no BFF** (usa o **`x-api-key`** global):

| Item | Valor |
|------|--------|
| Método e caminho | `POST /master-api/users/resolve-uuids-by-emails` |
| Corpo JSON | `{ "emails": string[] }` — até **200** endereços, cada um válido segundo `@IsEmail` |
| Resposta JSON | `{ "resolved": [{ "email": string, "uuid": string }], "notFound": string[], "ambiguous": string[] }` |

- **`resolved`** — exatamente **um** utilizador Laravel coincide com aquele email (por igualdade); o email na resposta vem sempre normalizado.
- **`notFound`** — nenhum utilizador coincide após filtro exato (inclui listagem Laravel vazia).
- **`ambiguous`** — mais de um utilizador com o mesmo email exato após filtro (defesa; com `users.email` único no Laravel espera-se irreal).

O método deduplica pedidos já normalizados e limita a **concorrência interna** (várias chamadas ao master por lote).

**Atlas → BFF → Laravel:**

1. **`POST /api/admin/populate-customer-api-uuid-from-bff`** (`Atlas/src/app/api/admin/populate-customer-api-uuid-from-bff/route.ts`): lê customers com **`customerApiUuid` nulo**, até **`limit`** (default `500`, máx. `2000`), em páginas de **`batchSize`** (default `100`, máx. `200`). Proteção igual às demais **`/api/*`** via middleware (Clerk e/ou **`atlas-key`**).2. Para cada lote monta **`emails`** e chama **`api.post(...)`** de `Atlas/src/lib/api-client.ts` (`BFF_URL` + `BFF_API_KEY`).
3. Por cada entrada em **`resolved`** aplica atualização **`customer_api_uuid`** com a mesma regra que **`apply-customer-api-uuid-by-email`** (UUID válido, conflito se o UUID já estiver noutro customer).
4. A resposta Atlas devolve contagens (**`processed`**, **`updated`**, **`notFoundOnApi`**, etc.) e **`customersStillMissing`** (contagem de customers ainda sem UUID após a execução).

Exemplo (ajuste **`PORT`** se o Nest não estiver na `3333`):

```bash
curl -sS -X POST "http://localhost:3333/master-api/users/resolve-uuids-by-emails" \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails":["conta@planfi.dev","nobody@invalid.test"]}'
```

DTO correspondente em `src/master-api/dto/resolve-uuids-by-emails.dto.ts`.

## 6. DTOs e parâmetros

### `MasterUser`

| Campo | Tipo |
|-------|------|
| `uuid` | `string` |
| `name` | `string` |
| `firstName` | `string` |
| `lastName` | `string` |
| `email` | `string` |
| `emailVerifiedAt` | `string \| null` |
| `createdAt` | `string` |
| `updatedAt` | `string` |

### `ListMasterUsersParams`

| Parâmetro BFF | Query Laravel |
|---------------|---------------|
| `perPage` | `per_page` |
| `subscriptionStatus` | `subscription_status` |
| Demais iguais | `uuid`, `email`, `search` |

Validação opcional com `class-validator` na classe `ListMasterUsersQueryDto` (uso futuro ou pipes).

### `PaginatedResponse<T>`

Campos principais:

- `data: T[]`
- `pagination: { currentPage, perPage, total, lastPage }` populados de `meta` Laravel.

### `ResolveUuidsByEmailsDto` (entrada HTTP e serviço)

- **`emails`** — até **200** endereços, todos validados como email (`class-validator`).
- Resposta (**`ResolveUuidsByEmailsResponse`**): **`resolved`** (pares `{ email normalizado, uuid }`), **`notFound`** e **`ambiguous`** — igual ao corpo HTTP do `POST /master-api/users/resolve-uuids-by-emails` (§ 5.1).

### `ImpersonatedRequest`

| Campo | Descrição |
|-------|-----------|
| `method` | Um de `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
| `path` | Caminho absoluto após host (`LARAVEL_URL`). Rotas da API Laravel em `/api`: ex. **`/api/v1/portfolios`**; master usa **`/v1/master/...`** sem `/api`. |
| `body?` | Corpo JSON (métodos com payload) |
| `query?` | Query string objeto simples (`string \| number \| boolean`) |
| `headers?` | Cabeçalhos extras mesclados (nunca substituir o Bearer master) |

## 7. Impersonation

1. Emite/obtém token master válido (`getToken`).
2. Chama axios com `{ Authorization: Bearer pfm_* }`.
3. Inclui o cabeçalho configurado (`MASTER_API_IMPERSONATION_HEADER`) com UUID de usuário **válido** (RFC 4122, validação sintática local).
4. O upstream resolve o utilizador impersonado antes de políticas típicas de autenticação.

Exemplo típico: listar dados que dependeriam do JWT do próprio cliente — resultados esperados devem coincidir com uma chamada autenticada como aquele utilizador quando as políticas Laravel forem iguais.

## 8. Mapeamento de erros

| HTTP upstream | Exceção NestJS | Observação |
|---------------|----------------|------------|
| 401 ao emitir token | `InternalServerErrorException` | Mensagem específica verificável em código; verifique `MASTER_API_SECRET`.
| 401 em `/v1/master/users` ou impersonation (após 1ª tentativa) | `InternalServerErrorException` | Texto `"Master API authentication failed after refresh"`.
| 403 | `UnauthorizedException` | Inclui mensagem upstream (feature desabilitada, IP não permitido, método não autorizado pelo token quando aplicável upstream).
| 422 | `BadRequestException` | Corpo típico com `message` e `errors` Laravel.
| 5xx ou rede | `BadGatewayException` | Texto `"Master API upstream error"` onde não há cópia de mensagem.
| Outros 4xx/erros tratados genericamente | `BadGatewayException` | Mensagem repassada onde possível.

**Logging:** nunca logar `MASTER_API_SECRET` nem `access_token` bruto — em payloads de erro, `access_token` é mascarado quando reconhecível.

## 9. Uso em outros módulos Nest

```typescript
import { Module } from '@nestjs/common';
import { MasterApiModule } from '../master-api/master-api.module';

@Module({
  imports: [MasterApiModule],
})
export class BackofficeInternalsModule {}

// dentro de um @Injectable(): constructor(private readonly master: MasterApiService) {}
```

O **`MasterApiModule`** exporta **`MasterApiService`** e regista **`MasterApiController`**. Módulos internos continuam a injetar só o serviço; sistemas externos (ex.: Atlas) podem usar o endpoint **`POST /master-api/users/resolve-uuids-by-emails`** com **`x-api-key`**.

## 10. Testes

```bash
cd bff-project && npm test -- master-api
```

Com `NODE_ENV=test`, **apenas `issueToken` curto-circuita** para um token sintético `pfm_mock_test_token`; os testes utilizam mocks de Axios para cenários mais ricos quando `NODE_ENV` é temporariamente `development` dentro do arquivo de teste.

## 11. Verificação manual (curl)

Upstream direto:

```bash
curl -sS -X POST "$LARAVEL_URL/v1/master/token" \
  -H 'Content-Type: application/json' \
  -d '{"secret":"<MASTER_API_SECRET>","methods":["GET","POST","PUT","PATCH","DELETE"]}'
```

Depois lista usuários paginados:

```bash
curl -sS -H "Authorization: Bearer $TOKEN_MASTER" \
  "$LARAVEL_URL/v1/master/users?per_page=5"
```

Fluxos opcionais de validação end-to-end com o código Nest podem ficar sob `src/master-api/__manual__/` (pastas ignoradas no Git).

## 11.1 Nginx/`502`/`404 HTML` no upstream

- **JWT / `LaravelApiService`**: **`LARAVEL_API_URL`** é só a **origem**; o pedido real é `{LARAVEL_API_URL}` + path do código (ex. `/api/user`). **`404` HTML do nginx** quando a origem ou o proxy não correspondem ao host real.

- **Master**: o BFF usa **`LARAVEL_URL`** (sem `/api`). Se o nginx encaminhar master noutro `location`, ajuste **`LARAVEL_URL`** até o curl abaixo responder JSON Laravel:

```bash
curl -sSi -X POST "$LARAVEL_URL/v1/master/token" \
  -H "Content-Type: application/json" \
  -d '{"secret":"SEU_MASTER_API_SECRET","methods":["GET"]}'
```

Deve responder **JSON** Laravel (401/422/403/200 conforme configurado), não HTML do nginx no corpo.

O Atlas recebe **`502 Bad Gateway`** do BFF porque o **`MasterApiService`** trata o `404`/HTML como falha upstream e projeta **`BadGatewayException`**.

## 12. Segurança

- Rotacione `MASTER_API_SECRET` em conjunto com a API Laravel; após troca tokens antigos deixam de ser aceitos até o próximo emit (ou refresh-on-401 no BFF).
- Restrinja IPs do master no Laravel em produção; monitore 403 relacionados ao allowlist.
- `methods` no token devem seguir política minimalista sempre que impersonation não precisar de verbos específicos.
- Nunca commitar valores reais de segredo.

## 13. Referências (código upstream)

Rotas Laravel:

`/Users/grundler/Development/projetos/planfi/api/routes/api/v1/master.php`

Controlador principal:

`/Users/grundler/Development/projetos/planfi/api/app/Http/Controllers/MasterAccessController.php`

Serviço de token / impersonation:

`/Users/grundler/Development/projetos/planfi/api/app/Services/MasterApiTokenService.php`

Configuração Laravel:

`/Users/grundler/Development/projetos/planfi/api/config/master_api.php`

Este documento segue o estilo técnico de `docs/laravel-integration.md`: variáveis, responsabilidades e exemplos pontuais, sem sobrescrever a documentação de proxy JWT já existente.
