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>
30 KiB
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:
- Pré-estadia: cliente chega inseguro (não sabe onde estacionar, wi-fi, regras). Pequena fricção que reduz NPS.
- In-stay: cliente não sabe que existe cardápio, upgrades, extras. Receita adicional perdida.
- 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):
// 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:
{
"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 comskip_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 deconcierge_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_idantes de enviar
7.4. O que é configurável pelo dono do hotel
orchestrator_promptinteiro — ele escreve tom, persona, regras de handoff, transparência de IA, tudoconcierge_config.persona_namepor unidadeconcierge_config.knowledgepor unidadeconcierge_config.variablespor 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:
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:
- Quando? — dropdown de evento + offset (número + unidade minutos/horas/dias + direção antes/depois)
- Pra quem? — multi-select de unidades (obrigatório) + filtros opcionais (categorias, permanências)
- O quê? — editor de mensagem com autocomplete de variáveis + toggle "Incluir botões interativos"
- 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::Reservationlifecycle, enfileiraLifecycleDispatcherJob - 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::Assistantconcierge - Liquid render context pra
concierge.*, injeção via{% render 'concierge' %} - Template inicial sugerido
- Dispatcher setando
current_unit_idemconversation.custom_attributes - Tools
handoffeadd_label_to_conversationjá 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_atpra 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_buttonretornando 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}— counterlifecycle_dispatch_duration_seconds— histogramlifecycle_skip_reasons_total{reason}— counter
- Alertas:
- Taxa de
failed> 5% (erro no WuzAPI ou rule config quebrada) - Spike de
skippedcom motivomax_reached(possível loop de config) - Job com mais de 5min de delay entre
fire_atesent_at
- Taxa de
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
-
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. -
LLM alucinando info errada sobre o hotel — se
concierge_config.knowledgeestiver 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. -
Liquid inseguro / template injection — template malicioso no
message_bodypode vazar dado. Mitigação: usar Liquid strict mode como o Chatwoot Captain já faz, whitelist de filters permitidos, validação no save da regra. -
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.
-
Mudança de
check_in_atsem reagendar — bug de regra que não escuta oafter_updatedeixaria mensagens saindo no horário antigo. Mitigação: teste de integração obrigatório cobrindo esse caso. -
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. -
Sofia respondendo com conhecimento errado — bug na injeção do
current_unit_idfaria 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 quecurrent_unit_idfoi setado antes de chamarMessageBuilder.