prompt inicial do projeto

This commit is contained in:
2026-07-22 15:55:55 -03:00
commit 6fb920d333
19 changed files with 2937 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# FASE 7 — Escalonamento, fila de operador e recado
## Objetivo
Completar a retaguarda: escalonamento entre moradores, fila de operador humano com contexto, app do operador, e recado em vídeo. É o que transforma "chamada não atendida" em "visita resolvida".
## Pré-requisitos
FASES 06 concluídas. Ler `docs/03-FLUXOS-E-CONTRATOS.md` §4 e `docs/00-VISAO-E-PRODUTO.md` §1.
## Por que esta fase existe
A tese do produto é que o morador atende. A realidade é que ele nem sempre atende — gente em reunião, dirigindo, dormindo. É por isso que o mercado consolidado usa operador humano 24h.
Esta fase é o que torna a aposta vendável: quando o morador não atende, o visitante **não fica sem saída**. E é o que sustenta comercialmente o plano Assistido.
## Tarefas
### 1. Escalonamento entre moradores
`VISIT_RING_TIMEOUT` (20s) → `ESCALONADA`. Toca para os demais `unit_members` da unidade, **em paralelo**, respeitando `ring_order` para ordenar a exibição e `receives_calls`.
O primeiro que atender vence; os demais recebem cancelamento da notificação — deixar notificação órfã tocando depois de resolvido é ruído que faz o morador desativar o app.
### 2. Fila de operador
Ativa apenas com `tenant_features.operator_queue` habilitado. Sem o módulo, `ESCALONADA` vai direto para `RECADO_EM_VIDEO`.
Fila ordenada por tempo de espera, com prioridade para `VISITA` sobre `ENTREGA`. `POST /operator/queue/claim` usa `FOR UPDATE SKIP LOCKED` — dois operadores nunca pegam a mesma visita.
Posição na fila enviada ao visitante por `QUEUE_POSITION` no WebSocket. Esperar é tolerável; esperar sem saber quanto falta, não.
`VISIT_QUEUE_TIMEOUT` (120s) → `RECADO_EM_VIDEO`.
### 3. App do operador
Mesmo binário Compose Multiplatform, perfil decidido por `GET /me`. Layout mais denso que o do morador.
**Fila** à esquerda: foto, tipo, unidade, tempo de espera e **por que escalonou** (ninguém atendeu / quiet hours / unidade inexistente).
**Contexto** à direita, antes de assumir: histórico da unidade, visitas recentes, regra de entrega, nomes dos moradores, telefone. **O operador nunca atende sem contexto** — atender às cegas é o que faz a portaria remota parecer pior que a física.
Presença: `DISPONIVEL` / `EM_ATENDIMENTO` / `OFFLINE`, com heartbeat. Operador que perde conexão volta para `OFFLINE` automaticamente e suas visitas retornam à fila.
Ao resolver, o operador registra `resolution_reason` — obrigatório, e é o que alimenta a análise de por que o modelo autônomo falhou naquele caso.
### 4. Recado em vídeo
`RECADO_EM_VIDEO`: o visitante grava até 30s. Salvo em `media_assets` com `kind = RECADO_VIDEO` e retenção de 30 dias.
Notificação ao morador **sem urgência** — não é CallKit, não é full-screen intent. É pendência, não chamada.
Aparece no topo da home do app até ser resolvida. O morador pode autorizar retroativamente (gerando `access_grant` com validade estendida), negar, ou apenas marcar como visto.
### 5. Quiet hours
Dentro de `condominiums.quiet_hours`, visitas não pré-autorizadas **pulam o toque ao morador**: vão direto para `FILA_OPERADOR` (se o módulo estiver ativo) ou `RECADO_EM_VIDEO`.
É a defesa contra tocar em todos os apartamentos de madrugada, e precisa ser óbvia para o visitante: "Fora do horário. Transferindo para a portaria."
### 6. Cancelamento de notificações
Ao resolver a visita, cancele as notificações pendentes em todos os dispositivos que foram acionados. Android usa `notificationId` estável por visita; iOS usa `CXProvider.reportCall(with:endedAt:reason:)`.
Uma chamada CallKit que continua tocando depois de resolvida é bug grave de percepção — o morador acha que o app está quebrado.
### 7. Métricas de escalonamento
`portaria.escalation.ratio` (quantas visitas escalaram) · `portaria.queue.wait_time` · `portaria.queue.abandoned` · `portaria.message.recorded` · `portaria.message.unresolved_24h`.
`escalation.ratio` é a métrica que decide se um condomínio fica no plano Autônomo ou precisa migrar para o Assistido.
## Critérios de aceite
- [ ] Escalonamento toca para os demais moradores após 20s
- [ ] Primeiro a atender vence; notificações dos outros são canceladas
- [ ] Sem `operator_queue`, escalonada vai direto para recado
- [ ] Dois operadores não pegam a mesma visita (teste concorrente)
- [ ] Posição na fila chega ao visitante em tempo real
- [ ] Operador que perde conexão devolve a visita à fila
- [ ] Recado gravado, armazenado e notificado sem urgência
- [ ] Recado aparece como pendência na home até resolver
- [ ] Quiet hours desvia sem tocar em ninguém, com aviso ao visitante
- [ ] CallKit não continua tocando após resolução
- [ ] Fluxo completo sem morador algum: QR → escalonamento → fila → operador → autorizado
## Não faça nesta fase
- Pré-autorização e recorrentes (v2)
- WhatsApp (FASE 8)
- Escalação para telefone/PSTN