# 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