202 lines
12 KiB
Markdown
202 lines
12 KiB
Markdown
# Jornada do Cliente — Backend Handoff (pra sessão nova)
|
|
|
|
**Data:** 2026-04-15
|
|
**Status:** Backend (Fases A + C do spec) 100% entregue. UI (Fase B) ainda não — próximo plano.
|
|
|
|
## Pra retomar numa sessão nova
|
|
|
|
Colar este prompt no início da sessão:
|
|
|
|
> Quero escrever o plano de implementação da Fase B — a UI "Jornada do Cliente" — do spec em `docs/superpowers/specs/2026-04-15-jornada-do-cliente-design.md` seção 9. O backend já está pronto (ver `docs/superpowers/plans/2026-04-15-jornada-do-cliente-backend.md` e o handoff em `docs/superpowers/plans/2026-04-15-jornada-do-cliente-backend-handoff.md`). Chama a skill writing-plans.
|
|
|
|
## O que está pronto (commits no main)
|
|
|
|
21 commits atômicos entre `1c89ef73f` e `325f05c3e`. Todos verdes, ~120 specs, 0 regressões, Rubocop limpo.
|
|
|
|
Mapa lógico:
|
|
|
|
| Camada | Arquivo principal | Papel |
|
|
|---|---|---|
|
|
| **Schema** | `db/migrate/20260415040927_*.rb` + `20260415040957_*.rb` | 3 tabelas `captain_lifecycle_*` + 2 colunas em `captain_units` |
|
|
| **Model: Config** | `enterprise/app/models/captain/lifecycle/config.rb` | Singleton por conta. Guards configuráveis. `.for_account(account)` helper |
|
|
| **Model: Rule** | `enterprise/app/models/captain/lifecycle/rule.rb` | `EVENTS` constant, `MESSAGE_TYPES`, `#matches_reservation?` com 3 filtros |
|
|
| **Model: Delivery** | `enterprise/app/models/captain/lifecycle/delivery.rb` | Audit log. `mark_sent!/skipped!/cancelled!/failed!`, `.count_sent_for_reservation(id)` |
|
|
| **Model ext** | `enterprise/app/models/captain/unit.rb` | `concierge_persona_name` (default Sofia), `concierge_knowledge`, `concierge_variables`, `concierge_configured?`, `belongs_to :concierge_inbox` |
|
|
| **Model ext** | `enterprise/app/models/captain/reservation.rb` | 3 hooks: `after_create_commit :schedule_lifecycle_rules`, `after_update_commit :handle_lifecycle_status_change` (on `saved_change_to_status?` + status in cancelled/no_show), `after_update_commit :handle_lifecycle_checkin_change` (on `saved_change_to_check_in_at?`). Todos com `rescue StandardError` pra nunca derrubar a reserva |
|
|
| **Service: EventResolver** | `enterprise/app/services/captain/lifecycle/event_resolver.rb` | `.resolve(reservation, event)` → timestamp. `checkin.detected`/`checkout.detected` retornam nil (unsupported no MVP) |
|
|
| **Service: Scheduler** | `enterprise/app/services/captain/lifecycle/scheduler.rb` | `.schedule_for(reservation)`, `.cancel_pending(reservation)`, `.reschedule_for_checkin_change(reservation)` — cria deliveries + enqueue Sidekiq |
|
|
| **Service: ContextBuilder** | `enterprise/app/services/captain/lifecycle/context_builder.rb` | `.build(reservation)` → `{ 'customer' => {...}, 'reservation' => {...}, 'hotel' => {...} }` |
|
|
| **Guards** | `enterprise/app/services/captain/lifecycle/guards/*.rb` | Base + 6 guards: ReservationActive, OptOutLabel, MaxPerReservation (CAP=5), QuietHours (Opção C 2h), MinInterval, CustomerReplied |
|
|
| **Service: Dispatcher** | `enterprise/app/services/captain/lifecycle/dispatcher.rb` | Pipeline: `call` → guards → render → `send_message` → `mark_sent!`. Order matters, stop on first non-pass |
|
|
| **Job** | `enterprise/app/jobs/captain/lifecycle/dispatcher_job.rb` | `perform(delivery_id)` chama Dispatcher. `self.perform_at(time, id)` delega pra `set(wait_until:).perform_later` |
|
|
| **Liquid** | `enterprise/app/models/concerns/agentable.rb` | `render_orchestrator_prompt` agora injeta `concierge:` e `reservation:` no enhanced_context, resolvidos a partir de `conversation.custom_attributes['current_unit_id']` |
|
|
| **Liquid templates** | `enterprise/lib/captain/prompts/concierge.liquid` + `snippets/concierge.liquid` | Template padrão da Sofia (o dono reescreve) |
|
|
| **WuzAPI** | `app/services/wuzapi/client.rb` | `send_buttons`, `send_list`, `send_url_button` |
|
|
| **WuzAPI provider** | `app/services/whatsapp/providers/wuzapi_service.rb` | `send_interactive_message(phone, payload)` roteia por `type` |
|
|
| **Seed** | `db/seeds/captain_lifecycle_demo.rb` | Smoke test via `bundle exec rails runner` |
|
|
|
|
## Gotchas descobertos durante a implementação (IMPORTANTE — não repetir)
|
|
|
|
Estas armadilhas custaram tempo nos subagentes — documento aqui pra próxima sessão não tropeçar:
|
|
|
|
### 1. `Captain::Reservation` associa `unit`, não `captain_unit`
|
|
|
|
A coluna é `captain_unit_id` mas a associação é `belongs_to :unit, class_name: 'Captain::Unit', foreign_key: 'captain_unit_id'`. Então no factory e nos testes é **`unit: unit`**, não `captain_unit: unit`. Cuidado com copy-paste de código do spec.
|
|
|
|
### 2. Factory `:captain_unit` foi consertada em `325f05c3e`
|
|
|
|
Antes, `association :brand, account: account` era quebrado (FactoryBot não avalia `account` lazily nessa forma). Fix aplicado: bloco `brand do ... end` que busca/cria brand na mesma conta. **Agora funciona:** `create(:captain_unit)` e `create(:captain_unit, account: some_account)` são idiomáticos de novo. Todos os specs do lifecycle usaram workaround `Captain::Unit.create!(account:, brand:, name:)` porque o bug existia quando foram escritos — não são wrong, só não-idiomáticos. Não precisa refatorar.
|
|
|
|
### 3. Factory `:captain_reservation` foi criada na T3
|
|
|
|
Estava ausente do repo antes desta feature. Criada em `spec/factories/captain/reservation.rb` com `unit { nil }` default. Qualquer spec que precise de reserva com unit específica deve passar `unit: unit`.
|
|
|
|
### 4. `Wuzapi::Client#request` retorna `Hash`, não `Faraday::Response`
|
|
|
|
O cliente tem `handle_response` interno que parseia o body. Então `client.send_buttons(...)` retorna um hash, não uma response. Tests não podem usar `be_success` — usam `be_a(Hash)` ou testam chaves do hash retornado.
|
|
|
|
### 5. `perform_at` não é nativo do ActiveJob (só Sidekiq)
|
|
|
|
A T9 reimplementou como class method em `DispatcherJob`:
|
|
|
|
```ruby
|
|
def self.perform_at(wait_until, delivery_id)
|
|
set(wait_until: wait_until).perform_later(delivery_id)
|
|
end
|
|
```
|
|
|
|
Isso é a forma idiomática do ActiveJob pra agendar. Scheduler e Dispatcher chamam via `Captain::Lifecycle::DispatcherJob.perform_at(fire_at, delivery_id)`.
|
|
|
|
### 6. `belongs_to :conversation` / `belongs_to :message` precisam de `class_name: '::Conversation'`
|
|
|
|
Rails autoload resolve `Conversation` dentro de `Captain::Lifecycle::Delivery` como `Captain::Lifecycle::Conversation` (inexistente), causando `ArgumentError` em runtime. A T14 consertou em `delivery.rb`. Mesma gotcha vale pra qualquer model novo dentro do namespace `Captain::Lifecycle::*` que referencie modelos top-level.
|
|
|
|
### 7. `build_stubbed` falha com FK constraints
|
|
|
|
No Dispatcher spec (T15), `mark_sent!` persiste `conversation_id` e `message_id`. Stubs feitos com `build_stubbed` quebram na constraint. Solução: usa `create(:conversation)` + `create(:message)` reais no before block, mesmo quando a intenção é só stub.
|
|
|
|
### 8. Lifecycle hook pode criar deliveries "fantasmas" em specs de outros models
|
|
|
|
Todo `create(:captain_reservation)` agora dispara `after_create_commit :schedule_lifecycle_rules` → Scheduler roda → se houver rules na DB, cria deliveries. Specs que contam deliveries precisam ou (a) criar reservation ANTES das rules, ou (b) stubar `Captain::Lifecycle::Scheduler.schedule_for`. A T9 e T14 já ajustaram os specs afetados, mas **qualquer spec novo que instancia reserva + conta deliveries precisa lembrar disso.**
|
|
|
|
### 9. `captain_unit` tem campo `brand_id` obrigatório — não `captain_brand_id`
|
|
|
|
A coluna é nomeada assim, diferente da tabela (`captain_brands`). Testes que criam unit precisam passar `brand:` (nome da associação), não `captain_brand:`.
|
|
|
|
## Specs — onde estão e como rodar
|
|
|
|
Suite completa do lifecycle:
|
|
|
|
```bash
|
|
eval "$(rbenv init - zsh)" && rbenv shell 3.4.4 && bundle exec rspec \
|
|
spec/enterprise/models/captain/lifecycle \
|
|
spec/enterprise/services/captain/lifecycle \
|
|
spec/enterprise/jobs/captain/lifecycle \
|
|
spec/enterprise/integration/captain \
|
|
spec/enterprise/models/captain/reservation_lifecycle_hooks_spec.rb \
|
|
spec/enterprise/models/captain/unit_concierge_accessors_spec.rb \
|
|
spec/enterprise/models/concerns/agentable_concierge_spec.rb \
|
|
spec/services/wuzapi/client_interactive_spec.rb \
|
|
spec/services/whatsapp/providers/wuzapi_interactive_spec.rb \
|
|
spec/factories/captain_unit_factory_spec.rb
|
|
```
|
|
|
|
Esperado no check final: **~120 examples, 0 failures, 0 pending.** Tempo total ~8s.
|
|
|
|
Regression gate (specs de modelos pré-existentes que foram tocados):
|
|
|
|
```bash
|
|
bundle exec rspec spec/enterprise/models/captain/reservation_spec.rb
|
|
```
|
|
|
|
## O que NÃO foi implementado (e por quê)
|
|
|
|
### Fora de escopo deste plano (estão no spec, fase 2+)
|
|
|
|
- **Detecção heurística de `checkin.detected` / `checkout.detected`** — os eventos existem na enum, mas `EventResolver` retorna `nil`, ou seja, rules com esses eventos nunca disparam. Deferido pro backlog (`Ideias/Lifecycle Automation/backlog.md`)
|
|
- **Dashboard analítico / métricas Prometheus** — logs estruturados sim, Prometheus não. Deferido
|
|
- **Retention job (purge de deliveries >180 dias)** — design no spec, não implementado. Pode virar uma task de 10min depois
|
|
- **Cron de 1-em-1-min pra destravar mensagens adiadas por quiet hours** — não necessário no MVP porque quiet hours é default false. Se algum cliente ligar, re-avaliar
|
|
|
|
### Próximo plano (Fase B — UI)
|
|
|
|
Seção 9 do spec. Três tabs dentro de uma aba nova "Jornada do Cliente" no menu Captain:
|
|
|
|
- **Regras** — lista + wizard de 4 passos + templates prontos + editor de mensagem com autocomplete
|
|
- **Configurações** — guards + Sofia por unidade (inbox, persona_name, knowledge textarea, variables key/value)
|
|
- **Histórico** — lista da `captain_lifecycle_deliveries` com preview modal do `rendered_body`
|
|
|
|
Estimativa: 15-20 tasks Vue 3 + Pinia + rotas Rails/jbuilder. Multilíngue pt_BR + en obrigatório. Vai precisar de endpoints REST que ainda não existem (CRUD de rules, update de config, GET de deliveries, GET/PATCH de concierge_config em captain_units).
|
|
|
|
## Como testar manualmente agora (sem UI)
|
|
|
|
Via console Rails (precisa ter `Account.first` + `Captain::Unit` + `Inbox`):
|
|
|
|
```ruby
|
|
# Console
|
|
account = Account.first
|
|
unit = account.captain_units.first
|
|
inbox = unit.inboxes.first || Inbox.first
|
|
|
|
unit.update!(
|
|
concierge_inbox_id: inbox.id,
|
|
concierge_config: {
|
|
'persona_name' => 'Sofia',
|
|
'knowledge' => '# Sobre\nHotel teste.\n',
|
|
'variables' => { 'wifi_password' => 'teste123', 'menu_link' => 'https://menu.x' }
|
|
}
|
|
)
|
|
|
|
rule = Captain::Lifecycle::Rule.create!(
|
|
account: account,
|
|
name: 'Demo pré check-in',
|
|
event: 'checkin.scheduled_at',
|
|
offset_minutes: -10,
|
|
message_body: 'Oi {{ customer.first_name }}! Wifi: {{ hotel.wifi_password }}'
|
|
)
|
|
|
|
# Agora cria uma reserva — delivery é agendada automaticamente via hook
|
|
contact = account.contacts.first || Contact.create!(account: account, name: 'Teste', phone_number: '+5561900000000')
|
|
reservation = Captain::Reservation.create!(
|
|
account: account,
|
|
unit: unit,
|
|
contact: contact,
|
|
suite_identifier: 'Alexa',
|
|
status: :scheduled,
|
|
total_amount: 160,
|
|
check_in_at: 20.minutes.from_now,
|
|
check_out_at: 8.hours.from_now,
|
|
inbox: inbox
|
|
)
|
|
|
|
# Ver a delivery agendada
|
|
delivery = Captain::Lifecycle::Delivery.where(captain_reservation_id: reservation.id).last
|
|
puts "Delivery ##{delivery.id} status=#{delivery.status} fire_at=#{delivery.fire_at}"
|
|
|
|
# Disparar manual (sem esperar fire_at)
|
|
Captain::Lifecycle::DispatcherJob.perform_now(delivery.id)
|
|
|
|
# Checar resultado
|
|
delivery.reload
|
|
puts "Status: #{delivery.status}"
|
|
puts "Rendered: #{delivery.rendered_body}"
|
|
puts "Skip reason: #{delivery.skip_reason}" if delivery.status == 'skipped'
|
|
```
|
|
|
|
Ou via seed:
|
|
|
|
```bash
|
|
eval "$(rbenv init - zsh)" && rbenv shell 3.4.4 && bundle exec rails runner db/seeds/captain_lifecycle_demo.rb
|
|
```
|
|
|
|
## Arquivos e pastas relevantes (resumo de ponteiros)
|
|
|
|
- **Spec de design:** `docs/superpowers/specs/2026-04-15-jornada-do-cliente-design.md`
|
|
- **Plano backend:** `docs/superpowers/plans/2026-04-15-jornada-do-cliente-backend.md`
|
|
- **Backlog de ideias:** `/Users/user/Documents/Obsidian Vault/Ideias/Lifecycle Automation/backlog.md`
|
|
- **Docs Captain prompts (Jasmine):** `/Users/user/Documents/Obsidian Vault/Dev/Prompts/Captain Chatwoot/`
|
|
- **Models:** `enterprise/app/models/captain/lifecycle/`
|
|
- **Services:** `enterprise/app/services/captain/lifecycle/`
|
|
- **Jobs:** `enterprise/app/jobs/captain/lifecycle/`
|
|
- **Specs:** `spec/enterprise/*/captain/lifecycle/` + `spec/enterprise/integration/captain/lifecycle_flow_spec.rb`
|