docs: add Jornada do Cliente (lifecycle automation) design spec
Design para a feature de automação de mensagens WhatsApp baseada em eventos do ciclo de vida de reserva — 4 componentes isolados (rules engine, scheduler event-driven, dispatcher pipeline, concierge AI Sofia), multi-tenant desde o dia 1, com guards anti-ban e injeção dinâmica de knowledge por unidade via Liquid. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
69f0b124bb
commit
af5e2723aa
563
docs/superpowers/specs/2026-04-15-jornada-do-cliente-design.md
Normal file
563
docs/superpowers/specs/2026-04-15-jornada-do-cliente-design.md
Normal file
@ -0,0 +1,563 @@
|
||||
# Jornada do Cliente — Lifecycle Automation
|
||||
|
||||
**Data:** 2026-04-15
|
||||
**Status:** Design aprovado, pronto pra plano de implementação
|
||||
**Sub-skill de implementação recomendada:** superpowers:writing-plans
|
||||
|
||||
## 1. Visão geral
|
||||
|
||||
Sistema de automação de mensagens WhatsApp acionadas por eventos do ciclo de vida de uma reserva. O operador do hotel (via Chatwoot fazer.ai) cria regras pela UI — cada regra define "quando" (evento + offset), "pra quem" (filtros de unidade/categoria/permanência) e "o que" (mensagem com variáveis e botões interativos). O sistema dispara as mensagens via WuzAPI na inbox da concierge da unidade. Se o cliente responder, uma AI concierge ("Sofia") assume a conversa com base de conhecimento específica daquela unidade.
|
||||
|
||||
**Arquitetura multi-tenant:** hybrid (C) — já nasce com banco/código preparados pra qualquer conta do fazer.ai configurar suas próprias jornadas, mas o MVP é validado com as 4 unidades do Grupo 1001.
|
||||
|
||||
## 2. Contexto e motivação
|
||||
|
||||
Hoje, depois que o cliente paga o Pix da reserva, ele só ouve do hotel no check-in ou no check-out (se ouvir). Isso é oportunidade perdida de 3 formas:
|
||||
|
||||
1. **Pré-estadia**: cliente chega inseguro (não sabe onde estacionar, wi-fi, regras). Pequena fricção que reduz NPS.
|
||||
2. **In-stay**: cliente não sabe que existe cardápio, upgrades, extras. Receita adicional perdida.
|
||||
3. **Pós-estadia**: cliente sai sem oportunidade fácil de avaliar no Google ou responder NPS. Taxa de review orgânico cai.
|
||||
|
||||
Lifecycle marketing resolve os três. Hotéis grandes lá fora já fazem isso há anos. A maioria dos concorrentes brasileiros não faz, então entrega vantagem competitiva real.
|
||||
|
||||
Como a Chatwoot fazer.ai é a plataforma que o Grupo 1001 usa (e que é vendida pra outros hotéis), a feature tem que nascer como produto reutilizável, não hack interno.
|
||||
|
||||
## 3. Arquitetura
|
||||
|
||||
Quatro componentes isolados. Cada um tem uma responsabilidade:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. Rules Engine (config) │
|
||||
│ O usuário cria/edita regras pela UI: evento + offset + │
|
||||
│ filtro + mensagem + guards. Vive em banco (jsonb). │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2. Scheduler (event-driven, NÃO cron) │
|
||||
│ Escuta eventos do domínio (reservation.confirmed etc). │
|
||||
│ Pra cada reserva que bate no filtro de alguma regra, │
|
||||
│ calcula fire_at e enfileira Sidekiq job agendado. │
|
||||
│ Ajusta se a reserva mudar (cancelou → cancela jobs). │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3. Dispatcher (runtime) │
|
||||
│ Quando o job dispara: checa guards (quiet hours, max-5, │
|
||||
│ label opt-out, etc), renderiza template Liquid com as │
|
||||
│ variáveis, chama Wuzapi::Client pela inbox concierge, │
|
||||
│ grava resultado na tabela de delivery (auditoria). │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 4. Concierge AI (Sofia, reativa) │
|
||||
│ Separada. Captain::Assistant compartilhado entre unidades, │
|
||||
│ ligado à inbox concierge. Quando o cliente responde, a │
|
||||
│ Sofia assume a conversa e responde com base no knowledge │
|
||||
│ da unidade da reserva ativa (injetado dinamicamente via │
|
||||
│ Liquid no system prompt). │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Por que event-driven e não cron de 1-em-1 minuto:** a gente sabe exatamente quando cada mensagem precisa sair no momento em que a reserva é criada (`check_in_at` + regras + offsets → `fire_at` determinístico). Calcula uma vez, enfileira `Sidekiq.perform_at(fire_at)` e deixa o Sidekiq acordar no horário. Barato, preciso, sem risco de "e se o cron atrasar". Um cron mínimo de 1-em-1-min existe apenas pra liberar mensagens que estavam paradas por quiet hours ou retry de erro.
|
||||
|
||||
**Regra de privacidade dura:** uma reserva feita em Águas Lindas só pode gerar mensagem saindo da inbox concierge de Águas Lindas. Não existe opção de "mandar a partir de outra unidade". Isso não é configuração, é invariante do sistema.
|
||||
|
||||
**Single number, many units (cérebro dinâmico):** a ideia é que um único número WhatsApp possa servir de concierge pra várias unidades de uma conta, mas cada unidade tem sua própria base de conhecimento. Quando um cliente responde, o sistema identifica qual unidade ele reservou (via `conversation.custom_attributes['current_unit_id']` setado pelo lifecycle dispatcher) e injeta **só o knowledge daquela unidade** no system prompt da Sofia via Liquid render. Zero vazamento cruzado.
|
||||
|
||||
## 4. Eventos disponíveis
|
||||
|
||||
Lista hardcoded (usuário não cria eventos novos, escolhe da lista). Adicionar evento novo é mudança de schema + scheduler.
|
||||
|
||||
| Evento | O que dispara |
|
||||
|---|---|
|
||||
| `reservation.confirmed` | Pix pago (ou marcado como pago manualmente) |
|
||||
| `checkin.scheduled_at` | Horário agendado de check-in (base pra offsets negativos tipo -10min, -1h, -1dia) |
|
||||
| `checkin.detected` | Sistema detectou entrada real (fora de escopo no MVP — deferido, ver backlog) |
|
||||
| `checkout.scheduled_at` | Horário agendado de checkout |
|
||||
| `checkout.detected` | Saída real detectada (fora de escopo no MVP) |
|
||||
| `reservation.cancelled` | Cliente ou operador cancelou |
|
||||
| `reservation.no_show` | Passou do horário + tolerância e não entrou |
|
||||
|
||||
**Offset suportado por regra:** `N minutos/horas/dias antes` ou `depois` do evento. Armazenado como `integer signed` em minutos. Exemplo: `-10` = 10min antes, `+600` = 10h depois.
|
||||
|
||||
**Caso especial suportado:** `"às HH:MM do dia anterior ao evento"` (ex: enviar lembrete sempre às 18h do dia antes do check-in). Implementado como offset relativo à meia-noite do dia anterior.
|
||||
|
||||
**`checkin.detected` e `checkout.detected`** aparecem na lista mas geram erro "evento ainda não disponível" no MVP. A infra do scheduler já reserva os slots; a detecção real entra em fase 2.
|
||||
|
||||
## 5. Modelo de dados
|
||||
|
||||
Três tabelas novas + duas colunas em `captain_units`.
|
||||
|
||||
### 5.1. `captain_lifecycle_rules`
|
||||
|
||||
| Coluna | Tipo | Descrição |
|
||||
|---|---|---|
|
||||
| `id` | bigint | PK |
|
||||
| `account_id` | bigint | FK, scope por conta |
|
||||
| `name` | string | "Lembrete 10min antes check-in" |
|
||||
| `description` | text | opcional, ajuda o usuário |
|
||||
| `enabled` | boolean | liga/desliga sem deletar |
|
||||
| `event` | string | um dos 7 eventos da seção 4 |
|
||||
| `offset_minutes` | integer | signed |
|
||||
| `filters` | jsonb | `{ unit_ids: [1,2], categorias: ["Alexa"], permanencias: ["Pernoite"] }` |
|
||||
| `message_type` | string | `text`, `buttons`, `list`, `url_button` |
|
||||
| `message_body` | text | template Liquid |
|
||||
| `message_payload` | jsonb | botões/lista estruturados (null se `text`) |
|
||||
| `priority` | integer | 0-100. Quando 2 regras disparam juntas, menor prioridade vai primeiro |
|
||||
| `created_by_user_id` | bigint | auditoria |
|
||||
| `created_at/updated_at` | datetime | |
|
||||
|
||||
**Nota:** não existe campo `send_via_inbox_id`. A inbox é sempre derivada de `reservation.unit.concierge_inbox`, inviolável.
|
||||
|
||||
**Formato do `message_payload` pra botões interativos (estruturado):**
|
||||
|
||||
```json
|
||||
// Quick reply
|
||||
{ "type": "quick_reply", "body": "Curtiu sua estadia?", "buttons": [
|
||||
{ "id": "rate_now", "text": "Avaliar agora" },
|
||||
{ "id": "rate_later", "text": "Mais tarde" }
|
||||
]}
|
||||
|
||||
// URL button
|
||||
{ "type": "url_button", "body": "Avalie no Google e ganhe um brinde",
|
||||
"button": { "url": "{{ hotel.google_review_link }}", "text": "Avaliar" }}
|
||||
|
||||
// List menu (até 10 opções)
|
||||
{ "type": "list", "body": "Nosso cardápio:", "button_text": "Ver opções",
|
||||
"sections": [{ "title": "Bebidas", "rows": [...] }]}
|
||||
```
|
||||
|
||||
Strings dentro do payload também passam pelo Liquid render.
|
||||
|
||||
### 5.2. `captain_lifecycle_deliveries` (audit log)
|
||||
|
||||
Toda tentativa de envio vira 1 linha aqui, não importa se saiu, foi pulada ou falhou. Fonte da verdade pro guard max-5, histórico por reserva e dashboard.
|
||||
|
||||
| Coluna | Tipo | Descrição |
|
||||
|---|---|---|
|
||||
| `id` | bigint | PK |
|
||||
| `account_id` | bigint | FK |
|
||||
| `lifecycle_rule_id` | bigint | FK (nullable — se regra foi deletada depois) |
|
||||
| `captain_reservation_id` | bigint | FK — a reserva alvo |
|
||||
| `conversation_id` | bigint | FK (null até mensagem ser enviada) |
|
||||
| `message_id` | bigint | FK (null até mensagem ser enviada) |
|
||||
| `inbox_id` | bigint | FK |
|
||||
| `fire_at` | datetime | quando estava agendado pra sair |
|
||||
| `sent_at` | datetime | quando saiu de fato (null se skipped/failed) |
|
||||
| `status` | string | `scheduled`, `sent`, `skipped`, `failed`, `cancelled` |
|
||||
| `skip_reason` | string | `quiet_hours`, `too_stale`, `max_reached`, `opt_out_label`, `customer_replied`, `min_interval`, `reservation_cancelled` |
|
||||
| `failure_reason` | text | erro da API se `failed` |
|
||||
| `rendered_body` | text | o texto final enviado (variáveis já substituídas) — auditoria forense |
|
||||
| `origin` | string | `scheduled_lifecycle` — marca as que contam no cap de 5 |
|
||||
|
||||
**Índices críticos:**
|
||||
- `(captain_reservation_id, origin, status)` — pro cap de 5
|
||||
- `(account_id, status, fire_at)` — pro dashboard
|
||||
- `(lifecycle_rule_id)` — pro histórico da regra
|
||||
- `(fire_at)` parcial WHERE status='scheduled' — pra retries e re-enfileiramento
|
||||
|
||||
**Política de retenção:** job noturno apaga linhas `sent/skipped/cancelled` com mais de 180 dias. Linhas `failed` e `scheduled` nunca expiram automaticamente.
|
||||
|
||||
### 5.3. `captain_lifecycle_configs` (guards globais por conta)
|
||||
|
||||
Uma linha por `account_id`. Guards configuráveis via UI.
|
||||
|
||||
| Coluna | Tipo | Default | Descrição |
|
||||
|---|---|---|---|
|
||||
| `account_id` | bigint | — | PK + FK |
|
||||
| `quiet_hours_enabled` | boolean | `false` | desligado por default |
|
||||
| `quiet_hours_from` | time | `"23:00"` | |
|
||||
| `quiet_hours_to` | time | `"08:00"` | |
|
||||
| `min_interval_minutes` | integer | `30` | `0` desativa |
|
||||
| `pause_on_customer_reply` | boolean | `false` | desligado por default |
|
||||
| `pause_on_customer_reply_within_minutes` | integer | `60` | |
|
||||
| `opt_out_label_id` | bigint | null | FK → `labels` |
|
||||
|
||||
**Factory default forçado (não fica na tabela, é constante no código):** max 5 mensagens `origin=scheduled_lifecycle` por reserva. Não dá pra desligar nem aumentar.
|
||||
|
||||
### 5.4. Novas colunas em `captain_units`
|
||||
|
||||
| Coluna | Tipo | Descrição |
|
||||
|---|---|---|
|
||||
| `concierge_inbox_id` | bigint | FK → inboxes. Qual inbox WhatsApp é a concierge dessa unidade. Pode ser a mesma pra várias unidades. |
|
||||
| `concierge_config` | jsonb | `{ persona_name, knowledge, variables }` |
|
||||
|
||||
**Schema do `concierge_config`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"persona_name": "Sofia",
|
||||
"knowledge": "## Sobre o hotel\n...\n## Regras\n...\n## Café da manhã\n...",
|
||||
"variables": {
|
||||
"wifi_password": "hotel1001",
|
||||
"menu_link": "https://menu.hotel.com",
|
||||
"google_review_link": "https://g.page/r/...",
|
||||
"address": "Rua X, 123 - Águas Lindas"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`knowledge` é markdown longo que vai pro system prompt da Sofia. `variables` é key/value curto pra substituição em templates (`{{ hotel.wifi_password }}`).
|
||||
|
||||
## 6. Fluxo de execução
|
||||
|
||||
### 6.1. Criação de reserva
|
||||
|
||||
```
|
||||
Captain::Reservation after_commit(:create)
|
||||
│
|
||||
▼
|
||||
LifecycleRuleScheduler.schedule_for(reservation)
|
||||
│
|
||||
├─ Query: lifecycle_rules WHERE enabled AND event matches
|
||||
│ AND filters match (unit, categoria, permanencia, brand)
|
||||
│
|
||||
└─ Pra cada regra que bateu:
|
||||
├─ fire_at = resolve_event_timestamp(reservation, rule.event) + rule.offset_minutes
|
||||
├─ Se fire_at < Time.current → skip (já passou)
|
||||
├─ Cria captain_lifecycle_deliveries com status='scheduled'
|
||||
└─ Sidekiq: LifecycleDispatcherJob.perform_at(fire_at, delivery.id)
|
||||
```
|
||||
|
||||
### 6.2. Mudanças na reserva
|
||||
|
||||
```
|
||||
Captain::Reservation after_update
|
||||
│
|
||||
▼
|
||||
Se status mudou pra cancelled/no_show:
|
||||
└─ Marca todas as deliveries ainda scheduled dessa reserva como cancelled
|
||||
(jobs Sidekiq acordam mas veem status != scheduled e abortam)
|
||||
|
||||
Se check_in_at mudou:
|
||||
├─ Marca deliveries scheduled baseadas em checkin.* como cancelled
|
||||
└─ Reagenda com novo fire_at chamando LifecycleRuleScheduler de novo
|
||||
```
|
||||
|
||||
**Por que marca como cancelled em vez de apagar da fila do Sidekiq:** mexer na fila é caro e flaky. Mais confiável o job acordar e conferir o status da delivery.
|
||||
|
||||
### 6.3. Mudanças na regra (editada pela UI)
|
||||
|
||||
```
|
||||
LifecycleRule after_save
|
||||
│
|
||||
▼
|
||||
Se enabled virou false OU evento/offset/filtros mudaram:
|
||||
├─ Cancela deliveries scheduled dessa regra
|
||||
└─ Se enabled=true, recompila pra reservas futuras cujo evento ainda não passou
|
||||
```
|
||||
|
||||
Política: **edição só afeta pra frente**. Reservas cujo evento já aconteceu não recebem reenvio retroativo.
|
||||
|
||||
### 6.4. Dispatcher pipeline (job disparou)
|
||||
|
||||
```
|
||||
LifecycleDispatcherJob.perform(delivery_id)
|
||||
│
|
||||
1. Carrega delivery. Se status != scheduled → abort
|
||||
│
|
||||
2. Guards (ordem importa, mais barato primeiro):
|
||||
├─ Reserva ainda existe e não está cancelled? Se não → skip(reservation_cancelled)
|
||||
├─ Label opt-out está no contato? → skip(opt_out_label)
|
||||
├─ Max-5 atingido pra essa reserva (count deliveries status=sent, origin=scheduled_lifecycle)? → skip(max_reached)
|
||||
├─ Está dentro do quiet hours (se habilitado)? → reagenda OU skip(too_stale) conforme regra Opção C
|
||||
├─ Intervalo mínimo violado? → reagenda +N min
|
||||
└─ Cliente respondeu nos últimos N min (se guard habilitado)? → reagenda +N min
|
||||
│
|
||||
3. Renderiza o template (Liquid strict mode):
|
||||
├─ Context: customer.*, reservation.*, hotel.* (do concierge_config.variables da unit)
|
||||
├─ Liquid.render(message_body)
|
||||
├─ Se message_type != text: renderiza message_payload (botões/lista) recursivamente
|
||||
└─ Salva em delivery.rendered_body
|
||||
│
|
||||
4. Resolve a inbox:
|
||||
└─ inbox = reservation.unit.concierge_inbox (FAIL se null)
|
||||
│
|
||||
5. Envia:
|
||||
├─ Resolve/cria Conversation (contact + inbox)
|
||||
├─ Seta conversation.custom_attributes['current_unit_id'] = reservation.captain_unit_id
|
||||
├─ Messages::MessageBuilder(concierge_assistant, conversation, {content, message_type: outgoing}).perform
|
||||
├─ Se tiver botões/lista: WuzapiService.send_buttons/list(...) (método novo)
|
||||
└─ Atualiza delivery: status='sent', sent_at, message_id, conversation_id
|
||||
│
|
||||
6. Erro HTTP/exceção não capturada:
|
||||
└─ status='failed', failure_reason=erro, Sidekiq retry policy (3 tentativas com backoff)
|
||||
```
|
||||
|
||||
### 6.5. Quiet hours — Opção C (limite de 2h de atraso)
|
||||
|
||||
Se `fire_at` cai dentro da janela `quiet_hours_from..quiet_hours_to`:
|
||||
|
||||
- Calcula `delayed_fire_at` = próximo horário válido após a janela
|
||||
- Se `delayed_fire_at - fire_at > 2h` → **skip com `skip_reason=too_stale`** (mensagem perdeu relevância)
|
||||
- Senão → reagenda delivery pra `delayed_fire_at`, enfileira novo job
|
||||
|
||||
Limite de 2h é constante global no código, não configurável no MVP.
|
||||
|
||||
### 6.6. Cliente responde → Sofia assume
|
||||
|
||||
Nenhum código adicional além de garantir que `conversation.custom_attributes['current_unit_id']` foi setado no passo 5.5. O Chatwoot já roteia mensagens entrantes pro `Captain::Assistant` da inbox. A Sofia processa normalmente — o orchestrator prompt dela já está configurado com Liquid pra ler `current_unit_id` e injetar o knowledge correto.
|
||||
|
||||
## 7. Concierge AI (Sofia)
|
||||
|
||||
### 7.1. Modelo
|
||||
|
||||
Um `Captain::Assistant` novo (tipo concierge, ou flag) por conta. Ligado a 1+ inboxes via `captain_inboxes`. Normalmente 1 Sofia por conta compartilhada entre todas as unidades, mas o usuário pode criar múltiplas se quiser personas diferentes.
|
||||
|
||||
### 7.2. Injeção dinâmica de contexto (core)
|
||||
|
||||
O `orchestrator_prompt` da Sofia é um template Liquid. O framework disponibiliza as seguintes variáveis no render context, resolvidas a partir de `conversation.custom_attributes['current_unit_id']` → `captain_units.find(id)`:
|
||||
|
||||
- `{{ concierge.persona_name }}` — `concierge_config.persona_name` (default "Sofia")
|
||||
- `{{ concierge.unit_name }}` — `captain_units.name`
|
||||
- `{{ concierge.knowledge }}` — `concierge_config.knowledge` (markdown)
|
||||
- `{{ concierge.variables.X }}` — qualquer chave de `concierge_config.variables`
|
||||
- `{{ reservation.suite }}`, `{{ reservation.check_in_at }}`, `{{ reservation.check_out_at }}`, `{{ reservation.amount }}`, `{{ reservation.permanencia }}`
|
||||
- `{{ customer.name }}`, `{{ customer.first_name }}`, `{{ customer.phone }}`
|
||||
|
||||
A implementação usa o mecanismo Liquid que o Captain já tem (`{% render 'conversation' %}`, `{% render 'contact' %}`) — adiciona um novo `{% render 'concierge' %}` que resolve via `current_unit_id`.
|
||||
|
||||
### 7.3. O que é hardcoded pelo framework
|
||||
|
||||
- Tipo/flag do assistente + mecanismo de roteamento
|
||||
- Variáveis Liquid disponíveis no contexto
|
||||
- Tools disponíveis no catálogo: `handoff`, `add_label_to_conversation`
|
||||
- Dispatcher setando `current_unit_id` antes de enviar
|
||||
|
||||
### 7.4. O que é configurável pelo dono do hotel
|
||||
|
||||
- `orchestrator_prompt` inteiro — ele escreve tom, persona, regras de handoff, transparência de IA, tudo
|
||||
- `concierge_config.persona_name` por unidade
|
||||
- `concierge_config.knowledge` por unidade
|
||||
- `concierge_config.variables` por unidade
|
||||
- Quais tools a Sofia usa
|
||||
|
||||
### 7.5. Template inicial sugerido
|
||||
|
||||
O framework entrega um template de `orchestrator_prompt` pronto pra usuário partir dele. Estrutura proposta:
|
||||
|
||||
```liquid
|
||||
Você é {{ concierge.persona_name }}, assistente virtual do {{ concierge.unit_name }}.
|
||||
|
||||
## Base de Conhecimento
|
||||
{{ concierge.knowledge }}
|
||||
|
||||
## Dados da Estadia
|
||||
- Suíte: {{ reservation.suite }}
|
||||
- Check-in: {{ reservation.check_in_at }}
|
||||
- Check-out: {{ reservation.check_out_at }}
|
||||
|
||||
## Como se comportar
|
||||
[Escreva aqui o tom, transparência de IA, regras de handoff, etc]
|
||||
```
|
||||
|
||||
Template é só sugestão. Usuário apaga e reescreve se quiser.
|
||||
|
||||
### 7.6. Modelo LLM
|
||||
|
||||
Mesmo da Jasmine: `gpt-4o` via provider `openai` já configurado.
|
||||
|
||||
### 7.7. Sem cenários
|
||||
|
||||
Sofia não usa `Captain::Scenario`. Knowledge é monolítico no orchestrator, persona única, fluxo único (pós-venda de uma estadia).
|
||||
|
||||
## 8. Variáveis disponíveis
|
||||
|
||||
### 8.1. Variáveis do cliente
|
||||
|
||||
- `{{ customer.name }}` — Nome completo
|
||||
- `{{ customer.first_name }}` — Só o primeiro nome
|
||||
- `{{ customer.phone }}` — Telefone
|
||||
- `{{ customer.cpf }}` — CPF (se disponível)
|
||||
|
||||
### 8.2. Variáveis da reserva
|
||||
|
||||
- `{{ reservation.suite }}` — "Alexa", "Stilo", etc
|
||||
- `{{ reservation.unit_name }}` — "Prime Águas Lindas"
|
||||
- `{{ reservation.check_in_at }}` — Formatado amigável (ex: "hoje às 22h", "amanhã às 14h")
|
||||
- `{{ reservation.check_out_at }}` — Idem
|
||||
- `{{ reservation.amount }}` — "R$ 160,00"
|
||||
- `{{ reservation.permanencia }}` — "Pernoite", "2hrs", etc
|
||||
|
||||
### 8.3. Variáveis da unidade (hotel)
|
||||
|
||||
Qualquer chave definida em `captain_units.concierge_config.variables`. Exemplos:
|
||||
|
||||
- `{{ hotel.wifi_password }}`
|
||||
- `{{ hotel.menu_link }}`
|
||||
- `{{ hotel.google_review_link }}`
|
||||
- `{{ hotel.address }}`
|
||||
|
||||
O usuário cadastra quais chaves existem na config da unidade. O editor de mensagens mostra autocomplete com as chaves disponíveis na unidade selecionada.
|
||||
|
||||
## 9. UI — Jornada do Cliente
|
||||
|
||||
Nova aba dentro do menu Captain, chamada **"Jornada do Cliente"**, com 3 tabs.
|
||||
|
||||
### 9.1. Tab Regras
|
||||
|
||||
**Lista principal** em tabela:
|
||||
|
||||
| Nome | Evento | Offset | Filtro | Último envio | Taxa sucesso | Status | Ações |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Lembrete pré check-in | `checkin.scheduled_at` | -10min | Todas unidades · Pernoite | há 2h | 98% | ✅ ativo | Editar / Duplicar / Desativar |
|
||||
|
||||
**Templates prontos** (cards no topo da tab): 5-8 templates populares que o usuário pode clonar com 1 clique — lembrete pré check-in, welcome in-stay, pedido de review, etc. Ao clicar, abre o wizard com campos já preenchidos.
|
||||
|
||||
**Wizard de 4 passos** pra criar/editar regra:
|
||||
|
||||
1. **Quando?** — dropdown de evento + offset (número + unidade minutos/horas/dias + direção antes/depois)
|
||||
2. **Pra quem?** — multi-select de unidades (obrigatório) + filtros opcionais (categorias, permanências)
|
||||
3. **O quê?** — editor de mensagem com autocomplete de variáveis + toggle "Incluir botões interativos"
|
||||
4. **Revisão** — resumo + estimativa "quantas reservas dos últimos 30 dias teriam recebido essa msg" (sanity check)
|
||||
|
||||
**Editor de mensagem** tem:
|
||||
- Textarea com autocomplete quando digita `{{`, listando variáveis disponíveis
|
||||
- Tooltip em cada variável explicando o que ela vira no render
|
||||
- Botão "Ver preview" com reserva exemplo
|
||||
- Seção de botões interativos (se toggle ligado): escolhe tipo (quick_reply / url_button / list), adiciona opções
|
||||
|
||||
### 9.2. Tab Configurações
|
||||
|
||||
Formulário simples:
|
||||
|
||||
- **Quiet hours** (toggle + 2 time pickers, default desligado)
|
||||
- **Intervalo mínimo entre mensagens** (default 30min, 0 desliga)
|
||||
- **Pausar se cliente respondeu** (toggle + input "nos últimos X min", default desligado)
|
||||
- **Label de opt-out** (dropdown de labels da conta)
|
||||
- **Max mensagens por estadia: 5** (info, não editável)
|
||||
|
||||
**Seção "Concierge (Sofia) por Unidade":** lista as `captain_units` com expand por unidade:
|
||||
|
||||
- `concierge_inbox_id` (dropdown das inboxes WhatsApp)
|
||||
- `concierge_config.persona_name` (input text, default "Sofia")
|
||||
- `concierge_config.knowledge` (textarea markdown grande)
|
||||
- `concierge_config.variables` (tabela editável de key/value)
|
||||
|
||||
### 9.3. Tab Histórico
|
||||
|
||||
Lista paginada da `captain_lifecycle_deliveries` dos últimos 30 dias. Filtros: regra, reserva, status, data. Colunas:
|
||||
|
||||
| Regra | Cliente | Reserva (link) | Status | Disparado em | Motivo (se skip) | Ações |
|
||||
|
||||
Coluna "Ações" tem um botão **Preview** que abre modal com `rendered_body` daquela entrega específica — serve pra debug ("por que a mensagem saiu estranha?") e auditoria.
|
||||
|
||||
## 10. Guards anti-spam
|
||||
|
||||
### 10.1. Factory default forçado (hardcoded)
|
||||
|
||||
**Max 5 mensagens `origin=scheduled_lifecycle` por reserva.** Não é configurável, não pode ser desligado. Se a 6ª regra tentar disparar pra mesma reserva, é descartada silenciosamente com `skip_reason=max_reached`. Cinto de segurança anti-ban crítico pra evitar que uma configuração errada do usuário derrube o número.
|
||||
|
||||
### 10.2. Configuráveis (Tab Configurações)
|
||||
|
||||
- **Quiet hours** — janela de silêncio. Se habilitado, mensagem dentro da janela usa regra Opção C (reagenda ou skip by too_stale conforme limite de 2h)
|
||||
- **Intervalo mínimo** — tempo mínimo entre 2 mensagens proativas pro mesmo contato
|
||||
- **Pausar se cliente respondeu** — se o contato enviou qualquer mensagem nos últimos N min
|
||||
- **Label de opt-out** — se o contato tem a label escolhida, pula todas as mensagens silenciosamente
|
||||
|
||||
### 10.3. Origin distinction
|
||||
|
||||
Só mensagens com `origin=scheduled_lifecycle` contam no cap de 5 e no intervalo mínimo. Mensagens geradas pela Sofia respondendo cliente em tempo real (`origin=concierge_reply`) não contam — ela pode trocar 50 mensagens com o cliente sem restrição.
|
||||
|
||||
## 11. Fases do MVP
|
||||
|
||||
### Fase A — Infraestrutura base
|
||||
|
||||
- Migrations: 3 tabelas novas + 2 colunas em `captain_units`
|
||||
- Models: `Captain::Lifecycle::Rule`, `Captain::Lifecycle::Delivery`, `Captain::Lifecycle::Config`
|
||||
- Event scheduler: listener do `Captain::Reservation` lifecycle, enfileira `LifecycleDispatcherJob`
|
||||
- Dispatcher job: pipeline de guards + render Liquid + envio via inbox
|
||||
- Extensão do `Wuzapi::Client`: `send_buttons`, `send_list`, `send_url_button`
|
||||
- Extensão do `Whatsapp::Providers::WuzapiService`: wiring dos novos métodos
|
||||
|
||||
### Fase B — UI
|
||||
|
||||
- Rota nova "Jornada do Cliente" no menu Captain
|
||||
- Tab Regras: lista + wizard de 4 passos + templates prontos
|
||||
- Tab Configurações: guards + Sofia por unidade
|
||||
- Tab Histórico: paginada + modal de preview
|
||||
- Editor de mensagem com autocomplete + tooltip + botão preview
|
||||
|
||||
### Fase C — Sofia
|
||||
|
||||
- Tipo/flag de `Captain::Assistant` concierge
|
||||
- Liquid render context pra `concierge.*`, injeção via `{% render 'concierge' %}`
|
||||
- Template inicial sugerido
|
||||
- Dispatcher setando `current_unit_id` em `conversation.custom_attributes`
|
||||
- Tools `handoff` e `add_label_to_conversation` já existem no catálogo, só garantir que aparecem pra Sofia
|
||||
|
||||
### Fase D — QA e rollout piloto
|
||||
|
||||
- Teste end-to-end com 1 unidade real (Prime Águas Lindas)
|
||||
- Monitoramento: envios, taxa de sucesso, reclamações, fila Sidekiq
|
||||
- Ajustes finos antes de liberar pras outras unidades
|
||||
|
||||
## 12. Testes
|
||||
|
||||
### 12.1. Unit
|
||||
|
||||
- Cada guard isolado: quiet hours (Opção C), max-5, opt-out label, intervalo mínimo, customer replied
|
||||
- Resolver de `fire_at` pra cada evento × direção de offset
|
||||
- Liquid render com contexto completo
|
||||
- Rate limit de variáveis inválidas / chaves faltando
|
||||
- Validação de filtros (match positivo e negativo)
|
||||
|
||||
### 12.2. Integração
|
||||
|
||||
- Pipeline completa do dispatcher (schedule → fire → guards → render → send → log)
|
||||
- Event flow: reserva criada → job enfileirado; cancelada → deliveries viram cancelled; regra editada → recompilação
|
||||
- Race conditions: 2 regras disparando simultaneamente respeitam cap de 5 (optimistic lock)
|
||||
- WuzAPI stubbed pra `send_buttons/list/url_button` retornando sucesso/falha
|
||||
|
||||
### 12.3. End-to-end manual
|
||||
|
||||
- Criar regra pela UI, criar reserva de teste, aguardar `fire_at`, verificar mensagem chegando em número real
|
||||
- Cancelar reserva após agendamento, verificar que mensagens pendentes não saem
|
||||
- Editar regra, verificar que reservas futuras recompilam
|
||||
|
||||
## 13. Observabilidade
|
||||
|
||||
- Log estruturado por etapa do dispatcher (`rule_id`, `delivery_id`, `skip_reason`, `elapsed_ms`)
|
||||
- Métricas Prometheus:
|
||||
- `lifecycle_deliveries_total{rule_id, status}` — counter
|
||||
- `lifecycle_dispatch_duration_seconds` — histogram
|
||||
- `lifecycle_skip_reasons_total{reason}` — counter
|
||||
- Alertas:
|
||||
- Taxa de `failed` > 5% (erro no WuzAPI ou rule config quebrada)
|
||||
- Spike de `skipped` com motivo `max_reached` (possível loop de config)
|
||||
- Job com mais de 5min de delay entre `fire_at` e `sent_at`
|
||||
|
||||
## 14. Fora de escopo do MVP
|
||||
|
||||
Itens deferidos pra fase 2+ (salvos também em `Obsidian Vault/Ideias/Lifecycle Automation/backlog.md`):
|
||||
|
||||
- **Detecção heurística de `checkin.detected` / `checkout.detected`** — cruzar categoria reservada + unidades ocupadas pra inferir entrada real
|
||||
- **A/B testing de mensagens** — 50% recebem versão A, 50% versão B, compara métricas
|
||||
- **Dashboard analítico de funil** — taxa de resposta, conversão de review, NPS agregado
|
||||
- **Integração com Google Reviews API** — validar review deixado antes de disparar brinde
|
||||
- **Multi-idioma** — templates em inglês/espanhol pra hóspedes gringos
|
||||
- **Mensagens com mídia rica** — imagens, vídeos, PDFs no cardápio
|
||||
- **Cenários múltiplos pra Sofia** — se uma persona crescer e precisar ramificar
|
||||
- **Memória de longo prazo do cliente** — "sua terceira estadia com a gente!"
|
||||
- **Integração Wi-Fi do hotel** — mensagem automática com senha ao detectar entrada
|
||||
- **Override de guards por unidade** — hoje é só por conta
|
||||
|
||||
## 15. Riscos conhecidos
|
||||
|
||||
1. **Ban do número WhatsApp concierge** — mesmo com guards, volume alto pode acionar anti-spam da Meta. **Mitigação:** rollout gradual por unidade, aquecimento do número novo antes de ligar regras em massa, monitorar taxa de bloqueio via `skip_reason=failed`.
|
||||
|
||||
2. **LLM alucinando info errada sobre o hotel** — se `concierge_config.knowledge` estiver mal escrito ou incompleto, Sofia inventa. **Mitigação:** botão preview no editor, teste manual com perguntas difíceis antes de ativar, histórico de conversas da Sofia revisado semanalmente no rollout piloto.
|
||||
|
||||
3. **Liquid inseguro / template injection** — template malicioso no `message_body` pode vazar dado. **Mitigação:** usar Liquid strict mode como o Chatwoot Captain já faz, whitelist de filters permitidos, validação no save da regra.
|
||||
|
||||
4. **Race conditions no cap de 5** — 2 regras disparando simultaneamente, contador desatualizado. **Mitigação:** optimistic lock no counter, re-check dentro da transação do dispatcher.
|
||||
|
||||
5. **Mudança de `check_in_at` sem reagendar** — bug de regra que não escuta o `after_update` deixaria mensagens saindo no horário antigo. **Mitigação:** teste de integração obrigatório cobrindo esse caso.
|
||||
|
||||
6. **Volume explosivo de deliveries** — conta com 100 regras × 1000 reservas/dia = 100k linhas/dia na `deliveries`. **Mitigação:** retenção de 180 dias com job noturno, índices corretos, sanity check no wizard mostrando quantas reservas serão afetadas.
|
||||
|
||||
7. **Sofia respondendo com conhecimento errado** — bug na injeção do `current_unit_id` faria Sofia usar knowledge da unidade A pra cliente da B. **Mitigação:** teste explícito cobrindo multi-unit em mesmo inbox; assertions no dispatcher garantindo que `current_unit_id` foi setado antes de chamar `MessageBuilder`.
|
||||
Loading…
Reference in New Issue
Block a user