12 KiB
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.mdseção 9. O backend já está pronto (verdocs/superpowers/plans/2026-04-15-jornada-do-cliente-backend.mde o handoff emdocs/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:
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:
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):
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, masEventResolverretornanil, 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_deliveriescom preview modal dorendered_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):
# 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:
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