From af5e2723aabbdd02db8cb128ac522d04b6930b1e Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Wed, 15 Apr 2026 00:53:08 -0300 Subject: [PATCH] docs: add Jornada do Cliente (lifecycle automation) design spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../2026-04-15-jornada-do-cliente-design.md | 563 ++++++++++++++++++ 1 file changed, 563 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-15-jornada-do-cliente-design.md diff --git a/docs/superpowers/specs/2026-04-15-jornada-do-cliente-design.md b/docs/superpowers/specs/2026-04-15-jornada-do-cliente-design.md new file mode 100644 index 000000000..af466585f --- /dev/null +++ b/docs/superpowers/specs/2026-04-15-jornada-do-cliente-design.md @@ -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`.