# API do aplicativo móvel — contrato congelado

Fonte de verdade do contrato entre o backend e o aplicativo do cliente final.
Substitui o `ApiDocsController` (que, ironicamente, exige login para ser lido —
ao contrário da própria API que documenta).

**Estes endpoints não podem mudar durante a migração.** O aplicativo instalado
nos celulares não é atualizado junto com o backend, e a convivência de rotas
`/api`, `/api/v2` e `/api/v3` mostra que versões antigas seguem em uso.

**A frase volta a valer para as dez rotas do aplicativo instalado em 14/10/2026,
e vale até o aplicativo novo, que fala o `/api/v4`, estar em campo** (a resposta
189 (b) do dono: *"A etapa 1 sobe com as dez rotas abertas ao aplicativo de
hoje: como estão hoje na produção, e elas fecham quando o aplicativo novo
estiver em campo"*). De 03/09 a 14/10/2026 as seis leituras do D59
(`clienteSelecionar`, `indicacoesListar`, `getClienteAgenda`, `cuponsSelecionar`,
`sorteiosSelecionarCupons`, `clienteConsumoSelecionar`) e as quatro escritas do
D91 (`agendaDesmarcar`, `agendaSalvarV2`, `agendaSalvarV3`, `indicacaoCadastrar`)
exigiram a sessão do ERP e responderam 401 ao aplicativo; hoje respondem como a
produção, sem sessão, com o `Access-Control-Allow-Origin: *` que nove delas
mandavam (a V3 nunca o mandou). O interruptor é a constante
`AplicativoController::ROTAS_DO_APLICATIVO_INSTALADO`: fechar é trocá-la para
`fechadas`, e a suíte guarda os snapshots dos dois estados (os
`-sem-autenticacao` são os de fechadas).

O formato de cada resposta está congelado em `tests/Contrato/snapshots/`, e a
suíte em `tests/Contrato/` falha se qualquer campo mudar de nome ou de tipo.

Atualizado em 15/08/2026, e em 14/10/2026 (as dez rotas do aplicativo instalado).

---

## Autenticação: não existe

`AplicativoController` declara `$tokenPadrao` no construtor, mas **o valor nunca
é lido em lugar nenhum**. O único listener de autenticação do sistema
(`ISConfiguracao\Module::handleAuthRedirect`) protege apenas rotas cujo nome
contém `admin`, a rota `home` e as rotas `isapi-docs-*`.

Consequência verificada no ambiente: **todos os endpoints abaixo respondem sem
credencial alguma**. Ver débito D13.

## Os 22 endpoints do aplicativo

| Método | Rota | Ação | Observação |
|---|---|---|---|
| GET | `/api/versao/app` | `selecionarVersao` | Lê `public/app.json` |
| GET | `/api/agendas/getUnidades` | `unidadesSelecionar` | |
| GET | `/api/agendas/getUnidade/:id` | `unidadeSelecionar` | |
| GET | `/api/v2/agendas/getUnidade/:id` | `unidadeSelecionarV2` | |
| GET | `/api/agendas/getServicos/:id` | `servicosSelecionar` | ⚠️ falha — ver D15 |
| GET | `/api/cupons/selecionar/:id` | `cuponsSelecionar` | |
| GET | `/api/sorteios/selecionarCupons/:id` | `sorteiosSelecionarCupons` | |
| GET | `/api/agendas/getAgenda/:id` | `agendaSelecionar` | |
| POST | `/api/v2/agendas/getAgenda` | `agendaSelecionar` | Mesma ação, sem `:id` |
| GET | `/api/cliente/agenda/:id` | `getClienteAgenda` | ⚠️ falha — ver D15 |
| GET | `/api/cliente/indicacoes/:id` | `indicacoesListar` | ⚠️ falha — ver D15 |
| GET | `/api/banners/selecionar` | `bannersSelecionar` | |
| GET | `/api/paises/selecionar` | `paisesSelecionar` | |
| POST | `/api/clientes/novo` | `clienteNovo` | |
| POST | `/api/clientes/sac` | `sac` | Dispara e-mail |
| POST | `/api/cliente/indicacao` | `indicacaoCadastrar` | |
| POST | `/api/agendas/getCliente` | `clienteSelecionar` | |
| POST | `/api/agendas/getConsumoCliente` | `clienteConsumoSelecionar` | |
| POST | `/api/agendas/agendar` | `agendaSalvar` | ⛔ **quebrado** — ver D14 |
| POST | `/api/v2/agendas/agendar` | `agendaSalvarV2` | Aceita form-urlencoded |
| POST | `/api/v3/agendas/agendar` | `agendaSalvarV3` | Exige **corpo JSON** |
| POST | `/api/agendas/cancelar` | `agendaDesmarcar` | |

Mais três endpoints de visagismo, que dependem da API externa Zoomtec:
`/api/visagismo/foto-upload`, `/api/visagismo/dados-processar` e
`/api/visagismo/cliente-testes-mes-atual`.

### Diferença entre V2 e V3 do agendamento

O V2 lê os dados como formulário (`application/x-www-form-urlencoded`) e recusa
com `{"sucesso": false, "mensagem": "Informe o cliente."}`. O V3 espera **corpo
JSON** e recusa com `{"sucesso": false, "mensagem": "JSON inválido ou
malformado."}`. São contratos de entrada diferentes na mesma operação — quem for
reescrever precisa manter os dois.

### Formato geral das respostas

Todas seguem `{"sucesso": boolean, ...}`. Uma sutileza que o aplicativo depende:
quando não há resultado, **a chave da lista some** em vez de vir vazia. Por
exemplo, `/api/agendas/getUnidades` sem unidades retorna apenas
`{"sucesso": false}`, sem a chave `unidades`. Quem reescrever a API precisa
preservar esse comportamento, ou o app quebra ao ler a chave ausente.

---

## Como rodar a suíte de contrato

```bash
docker compose exec -e APP_BASE_URL=http://nginx app phpunit
```

Registrar contrato novo, ou atualizar um que mudou **de forma intencional**:

```bash
docker compose exec -e APP_BASE_URL=http://nginx -e ATUALIZAR_SNAPSHOTS=1 app phpunit
```

Um snapshot alterado num diff é sempre motivo de revisão: significa que o
formato entregue ao aplicativo mudou. Antes de aceitar, confirme que **todas as
versões do app em uso** continuam funcionando.

### O que a suíte cobre, e o que não cobre

Ela compara **estrutura** (nomes de campo e tipos), não valores — o app quebra
quando um campo some ou muda de tipo, não quando um nome muda de conteúdo.

A linha de base foi capturada contra o **banco de desenvolvimento vazio**. Isso
congela o caminho de erro e de lista vazia, que é justamente o que o app precisa
saber interpretar, e tem a vantagem de os testes não criarem registro nenhum ao
rodar. Mas **não cobre o caminho feliz**: as respostas com dados reais ainda não
estão congeladas.

Fechar essa lacuna depende da base anonimizada da W2. Assim que houver um dump
anonimizado carregado, rode a suíte com `ATUALIZAR_SNAPSHOTS=1` sobre ele e
revise o diff — os contratos passam a cobrir também as respostas populadas.

---

## Pendências que dependem de acesso ao servidor

Duas frentes da W3 não puderam ser executadas:

**Inventário do crontab.** As 29 rotas `/api/cronjobs/*` existem, mas não se sabe
quais são efetivamente chamadas, com que frequência, nem por qual mecanismo.
Precisa do `crontab -l` do servidor. Ver `operacao-atual.md`.

**Versões do aplicativo ainda ativas.** A existência de `V2` e `V3` indica
versões antigas em uso, mas só os logs de acesso dizem quais. O procedimento:
cruzar os logs do nginx dos últimos 90 dias com as rotas versionadas. Enquanto
isso não for feito, a premissa conservadora vale — **todas** as versões de rota
precisam continuar funcionando.
