Files
Projeto-Portaria/prompts/FASE-2-backend-api.md
2026-07-22 15:55:55 -03:00

108 lines
5.3 KiB
Markdown

# FASE 2 — API do backend
## Objetivo
Todos os endpoints REST, WebSocket de sinalização, armazenamento de mídia e o ciclo de vida da visita funcionando ponta a ponta — verificável por teste de integração, ainda sem interface.
## Pré-requisitos
FASES 0 e 1 concluídas. Ler `docs/03-FLUXOS-E-CONTRATOS.md` inteiro e `docs/06-LGPD-E-SEGURANCA.md` §5.
## Tarefas
### 1. Validação de QR e sessão de visitante
`GET /api/v1/visitor/gates/{gateId}/preview?s={sig}` — valida assinatura HMAC com `gates.qr_secret` e confere `qr_version`. Devolve nome do condomínio e o aviso de tratamento vigente. Sem autenticação.
`POST /api/v1/visitor/sessions` — recebe coordenadas, valida geofence com `ST_DWithin(gates.location, ponto, geofence_meters)`, registra `privacy_notices` (versão do aviso, IP, user agent) e emite o JWT efêmero.
Localização ausente ou negada → `403 LOCATION_REQUIRED`. Fora do raio → `403 OUTSIDE_GEOFENCE` com `detail` em português pronto para exibir.
### 2. Criação de visita — busca cega
`POST /api/v1/visitor/visits`, multipart (JSON + mídia).
**Esta é a implementação mais sensível do sistema.** Requisitos, todos obrigatórios:
- Resolve `unit_id` a partir de bloco + unidade; **se não existir, cria a visita com `unit_id = NULL`** e preserva `unit_input`
- Resposta **idêntica** — mesmo corpo, mesmo status, mesmos headers — nos dois casos
- **Tempo de resposta constante:** meça o caminho mais lento e aplique delay artificial no mais rápido. Sem isso o ataque vira timing attack e a busca cega não serve para nada
- Nome de morador nunca aparece na resposta
- Rate limit por IP e por dispositivo, com bloqueio progressivo
Verifica `quiet_hours` do condomínio: dentro da janela, a visita pula o toque ao morador e vai direto para `FILA_OPERADOR` ou `RECADO_EM_VIDEO`.
Enfileira `VisitaCriada` no outbox e agenda `VISIT_RING_TIMEOUT` (20s) e `VISIT_EXPIRE` (10 min).
### 3. Ciclo de vida da visita
Serviço de aplicação com as transições de `03-FLUXOS-E-CONTRATOS.md` §1, sempre validadas por `VisitStateMachine` e sempre com optimistic locking em `visits.version`.
Handlers de job:
| Job | Ação |
|---|---|
| `VISIT_RING_TIMEOUT` | → `ESCALONADA`; toca para os demais `unit_members` por `ring_order`; agenda `VISIT_ESCALATE` |
| `VISIT_ESCALATE` | → `FILA_OPERADOR` se `operator_queue` ativo, senão `RECADO_EM_VIDEO` |
| `VISIT_QUEUE_TIMEOUT` | → `RECADO_EM_VIDEO` |
| `VISIT_EXPIRE` | → `EXPIRADA` |
| `DELIVERY_DEFAULT_RULE` | aplica `units.delivery_rule` |
`POST /api/v1/app/visits/{id}/resolve` exige `Idempotency-Key`. Conflito de versão devolve `409 VISIT_ALREADY_RESOLVED` com `resolvedBy` e `resolvedAt` — o app precisa mostrar "Maria já autorizou", não um erro técnico.
### 4. Armazenamento de mídia
Porta `StoragePort` com adaptador MinIO/S3. Ao receber mídia:
- Valida `Content-Type` por **magic bytes**, nunca por extensão
- Limites: 8MB foto, 30MB vídeo
- **Remove EXIF** — carrega GPS próprio, que não é o dado que coletamos e não passou pelo aviso
- Calcula `sha256` e grava em `media_assets`
- Nome de objeto gerado pelo servidor
- Define `expires_at` conforme a retenção do tipo (`06-LGPD-E-SEGURANCA.md` §4)
URL assinada de 5 min para leitura, **sempre** registrando acesso em `audit_log`. Nenhuma URL pública, em nenhuma hipótese.
### 5. WebSocket
`/ws/visitor/{visitId}` e `/ws/app`, com o envelope e os tipos de `03-FLUXOS-E-CONTRATOS.md` §7. Heartbeat de 20s.
**O WebSocket nunca é fonte de verdade.** Toda mudança de estado é confirmada por REST ou push. Se o WS cair, o cliente faz polling — e a portaria continua funcionando.
Com múltiplas réplicas do backend, a difusão de eventos usa Redis pub/sub (só transporte; a durabilidade fica no outbox).
### 6. Endpoints de app e admin
Todos os de `03-FLUXOS-E-CONTRATOS.md` §6, com autorização por método:
```kotlin
@PreAuthorize("@access.canViewVisit(#visitId, authentication)")
```
Morador vê apenas visitas das suas unidades; operador, apenas as da fila; admin, apenas as do seu tenant.
Importação CSV de unidades com **dry-run obrigatório** — devolve o que seria criado, alterado e rejeitado antes de gravar.
### 7. Tratamento de erros
`application/problem+json` (RFC 7807) com os `code` da tabela de `03-FLUXOS-E-CONTRATOS.md` §9. **`detail` sempre em português e pronto para exibir ao visitante** — a tela da portaria não é lugar para mensagem técnica.
## Critérios de aceite
- [ ] Testes de integração com Testcontainers cobrindo os fluxos A, B e C ponta a ponta
- [ ] **Teste de busca cega:** unidade existente e inexistente produzem resposta idêntica, e a diferença de tempo fica abaixo do ruído de medição
- [ ] Teste: geofence rejeita fora do raio e aceita dentro
- [ ] Teste: `quiet_hours` desvia do morador
- [ ] Teste: dois `resolve` concorrentes — um vence, outro recebe `409` com o autor
- [ ] Teste: `resolve` repetido com mesma `Idempotency-Key` não duplica
- [ ] Teste: escalonamento dispara após restart do backend (job persistido)
- [ ] Teste: EXIF removido da imagem armazenada
- [ ] Teste: acesso a mídia gera linha em `audit_log`
- [ ] OpenAPI completo em `/v3/api-docs`
## Não faça nesta fase
- Integração com LiveKit (FASE 4) — `room_name` fica como placeholder
- Push real (FASE 5) — `NotificationChannel` com implementação de log
- Qualquer interface