94 lines
5.3 KiB
Markdown
94 lines
5.3 KiB
Markdown
# 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 0–6 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.
|
||
|
||
**Visitas-sombra** (`unit_id` nulo — busca cega, `03-FLUXOS-E-CONTRATOS.md` §2) chegam à fila marcadas como **"unidade não cadastrada"**, visível só para o operador. Ele trata como um porteiro trataria quem errou o número: pergunta, corrige a unidade (`PATCH` que resolve o `unit_id` e registra em `audit_log`) e redireciona — ou nega. Para o visitante, nada distingue esse atendimento de um normal.
|
||
|
||
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
|