# Reserva Rede 1001 — Fase 3.5: Angelina preenche + fechamento Fase 2+3 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. **Goal:** A atendente de IA do Chatwoot (Angelina) conversa com o cliente no WhatsApp, coleta os dados da reserva, e envia um link da página `reserva-1001` já preenchida. Cliente só clica "Confirmar e Pagar". Também fecha lacunas da Fase 2+3 (label `aguardando_pagamento`). **Architecture:** Query params na URL (Opção A — MVP). Nova ferramenta Captain `GenerateReservationLink` que monta o URL a partir do contexto da conversa. Frontend reserva-1001 lê os params no boot via `URLSearchParams` e seed a o hook `useReservationForm`. **Tech Stack:** Rails 7 (nova ferramenta Captain) · React 19 + Vite (prefill logic) · Chatwoot Captain tools infra **Spec pai:** `docs/superpowers/specs/2026-04-13-reserva-1001-design.md` **Fase anterior:** `docs/superpowers/plans/2026-04-13-reserva-1001-fase-2-3-fluxo-completo.md` ✅ --- ## Dados conhecidos - `Captain::Tools::GeneratePix` em `enterprise/app/services/captain/tools/generate_pix_tool.rb` — referência de tool. Usa `BaseTool` (investigar assinatura). - `Captain::Unit` id=4 = Hotel 1001 Águas Lindas, inbox 2, tem Inter PIX configurado. - Assistente "Angelina" existe no Chatwoot, vinculado a alguma account. Precisa verificar qual e como adicionar uma tool a ela. - `mark_conversation_as_awaiting_payment` do GeneratePix (linhas 713-721) é o padrão canonical de label: ```ruby current = conversation.label_list merged = (current + ['aguardando_pagamento']).uniq merged -= %w[pagamento_confirmado reserva_feita] conversation.update_labels(merged) ``` --- ## File Structure **Backend (Chatwoot):** ``` enterprise/app/controllers/public/api/v1/captain/ └── public_reservations_controller.rb # modify (add label helper) enterprise/app/services/captain/tools/ └── generate_reservation_link_tool.rb # novo config/ # possivelmente novo seed/script pra registrar tool no assistant spec/enterprise/services/captain/tools/ └── generate_reservation_link_tool_spec.rb # novo (opcional mas ideal) ``` **Frontend (reserva-1001):** ``` src/lib/ └── prefill.ts # novo — parse query params src/hooks/ └── useReservationForm.ts # modify — aceita initial state src/App.tsx # modify — passa prefill pro hook ``` --- ## Pré-requisitos 1. Chatwoot dev rodando em `:3000` 2. reserva-1001 dev rodando em `:5180` 3. Identificar o `assistant_id` do Angelina (via SQL ou UI do Captain) --- ## Task 1: Fix do label `aguardando_pagamento` no controller (fechamento Fase 2+3) **Files:** - Modify: `chatwoot/app/controllers/public/api/v1/captain/public_reservations_controller.rb` - [ ] **Step 1: Adicionar helper + chamada** No método `create`, depois de criar a Reservation e ANTES de chamar CobService (ou depois, tanto faz — o importante é que aconteça), adicionar chamada ao helper. E no `private:` adicionar o método. Edit no `create`, logo após `reservation = Captain::Reservation.create!(...)`: ```ruby mark_conversation_as_awaiting_payment(conversation) ``` E adicionar no bloco `private`: ```ruby def mark_conversation_as_awaiting_payment(conversation) current = conversation.label_list merged = (current + ['aguardando_pagamento']).uniq merged -= %w[pagamento_confirmado reserva_feita] conversation.update_labels(merged) rescue StandardError => e Rails.logger.error("[PublicReservations] label update failed: #{e.message}") # Não falha a request por causa disso end ``` - [ ] **Step 2: Smoke test** ```bash curl -s -o /tmp/r.json -w "HTTP %{http_code}\n" -X POST http://localhost:3000/public/api/v1/captain/public_reservations \ -H "Content-Type: application/json" \ -H "X-Reserva-Token: dev-token-change-in-prod" \ -d '{ "chatwoot_unit_id": 4, "category": "Standard", "stay_type": "3hrs", "checkin_at": "2026-04-14T18:00:00Z", "customer": {"name":"Teste Label","phone":"+5561999997777","cpf":"12345678909","email":"t@l.com"}, "total_cents": 12000, "deposit_cents": 6000 }' ``` Expected: HTTP 201. Depois, via SQL, confirmar que a conversa tem label `aguardando_pagamento`: ```bash PGPASSWORD= psql -h localhost -U postgres -d chatwoot_dev -c \ "SELECT id, additional_attributes FROM conversations ORDER BY id DESC LIMIT 3;" ``` Ou via `conversation.label_list` no console Rails. - [ ] **Step 3: Commit** ```bash cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/chatwoot git add app/controllers/public/api/v1/captain/public_reservations_controller.rb git commit -m "feat: adiciona label aguardando_pagamento ao criar reserva (fecha fase 2+3)" ``` --- ## Task 2: Frontend — parser de query params **Files:** - Create: `reserva-1001/src/lib/prefill.ts` - [ ] **Step 1: Criar o parser** Create `reserva-1001/src/lib/prefill.ts`: ```ts import type { ReservationFormState } from '@/hooks/useReservationForm' // Mapeamento dos query params para o estado do form. // marca/unidade/categoria/permanencia vêm como NOMES (string), não IDs — // a UI resolve os IDs depois que o catálogo carrega (ver Task 3). export interface PrefillData { marcaNome?: string unidadeNome?: string permanencia?: string categoria?: string checkinAt?: string // ISO nome?: string telefone?: string cpf?: string email?: string observacao?: string } export function parsePrefillFromURL(): PrefillData { if (typeof window === 'undefined') return {} const params = new URLSearchParams(window.location.search) const get = (key: string) => params.get(key)?.trim() || undefined return { marcaNome: get('marca'), unidadeNome: get('unidade'), permanencia: get('permanencia'), categoria: get('categoria'), checkinAt: get('checkin'), nome: get('nome'), telefone: get('telefone'), cpf: get('cpf'), email: get('email'), observacao: get('obs'), } } // Converte PrefillData em atualizações pontuais no form. // Retorna apenas os campos "simples" (não dependentes de resolução de catálogo). export function prefillSimpleFields(prefill: PrefillData): Partial { const out: Partial = {} if (prefill.nome) out.nome = prefill.nome if (prefill.telefone) out.telefone = prefill.telefone if (prefill.cpf) out.cpf = prefill.cpf if (prefill.email) out.email = prefill.email if (prefill.observacao) out.observacao = prefill.observacao if (prefill.checkinAt) { // Converte ISO para datetime-local format (yyyy-MM-ddThh:mm) try { const d = new Date(prefill.checkinAt) if (!isNaN(d.getTime())) { const pad = (n: number) => String(n).padStart(2, '0') out.checkinAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` } } catch { // ignore } } return out } ``` - [ ] **Step 2: Typecheck + commit** ```bash cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001 pnpm typecheck git add src/lib/prefill.ts git commit -m "feat: parser de query params para prefill do formulario" ``` --- ## Task 3: Hook — suporte a prefill em cascata **Files:** - Modify: `reserva-1001/src/hooks/useReservationForm.ts` - [ ] **Step 1: Adicionar lógica de resolução** O hook já tem effects em cascata que carregam marcas/unidades quando marcaId/unidadeId mudam. Precisamos: 1. Aceitar `prefill: PrefillData` como argumento opcional 2. Quando `marcas` carregar, resolver `marcaNome` → `marcaId` e aplicar 3. Quando `unidades` carregar (depois da marca), resolver `unidadeNome` → `unidadeId` 4. Depois disso, setar `categoria` e `permanencia` direto (são strings, não IDs) 5. Aplicar os campos simples (nome, tel, etc) no estado inicial Substituir o início do hook para aceitar `initialPrefill` e processar a resolução assim que as listas carregarem. Mostrar apenas as mudanças (o resto do hook fica igual): No topo do arquivo, importar: ```ts import type { PrefillData } from '@/lib/prefill' import { prefillSimpleFields } from '@/lib/prefill' ``` Mudar a assinatura: ```ts export function useReservationForm(initialPrefill?: PrefillData) { ``` Mudar o estado inicial: ```ts const [form, setForm] = useState(() => ({ ...empty, ...prefillSimpleFields(initialPrefill ?? {}), })) ``` Adicionar um ref de "já aplicou prefill de marca/unidade/categoria/permanencia" pra não reaplicar: ```ts const appliedPrefillRef = useRef(false) ``` (e `import { useRef } from 'react'` no topo) Adicionar um `useEffect` novo que roda quando marcas carregam, faz o match por nome, chama `update('marcaId', ...)` e guarda as strings de categoria/permanencia pra aplicar depois: ```ts useEffect(() => { if (appliedPrefillRef.current) return if (!initialPrefill?.marcaNome) return if (marcas.length === 0) return const marca = marcas.find( (m) => m.nome.toLowerCase() === initialPrefill.marcaNome!.toLowerCase() ) if (marca) { setForm((prev) => ({ ...prev, marcaId: marca.id })) } }, [marcas, initialPrefill]) ``` Outro effect que roda quando unidades carregam (depende de marcaId): ```ts useEffect(() => { if (appliedPrefillRef.current) return if (!initialPrefill?.unidadeNome) return if (unidades.length === 0) return const unidade = unidades.find( (u) => u.nome.toLowerCase() === initialPrefill.unidadeNome!.toLowerCase() ) if (unidade) { setForm((prev) => ({ ...prev, unidadeId: unidade.id, permanencia: initialPrefill.permanencia ?? prev.permanencia, categoria: initialPrefill.categoria ?? prev.categoria, })) appliedPrefillRef.current = true } }, [unidades, initialPrefill]) ``` - [ ] **Step 2: Typecheck + test** ```bash cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001 pnpm typecheck pnpm test ``` Os 2 testes existentes do App devem continuar passando (o hook é mockado). - [ ] **Step 3: Commit** ```bash git add src/hooks/useReservationForm.ts git commit -m "feat: useReservationForm aceita prefill e resolve nomes em cascata" ``` --- ## Task 4: App.tsx — passa o prefill pro hook **Files:** - Modify: `reserva-1001/src/App.tsx` (na verdade, o ReservationFlow) - Modify: `reserva-1001/src/components/reservation/ReservationFlow.tsx` - [ ] **Step 1: Capturar prefill no boot** Edit `src/components/reservation/ReservationFlow.tsx`: No topo, importar: ```ts import { parsePrefillFromURL } from '@/lib/prefill' ``` Criar uma constante MODULE-LEVEL (fora do componente) pra capturar o prefill UMA vez no boot — evita re-parse em cada render: ```ts const initialPrefill = parsePrefillFromURL() ``` Mudar a chamada do hook: ```ts const { form, update, ... } = useReservationForm(initialPrefill) ``` - [ ] **Step 2: Rodar tudo** ```bash pnpm typecheck pnpm test pnpm build ``` - [ ] **Step 3: Teste manual no navegador** Abrir: ``` http://localhost:5180/?marca=Hotel%201001%20Noites&unidade=Hotel%201001%20%C3%81guas%20Lindas&permanencia=3hrs&categoria=Standard&checkin=2026-04-14T22:00:00Z&nome=Jo%C3%A3o%20Teste&telefone=61999998888&cpf=12345678909&email=joao@teste.com ``` Expected: ao carregar, marca já está selecionada, unidade também, permanência 3hrs, categoria Standard, data preenchida, nome/tel/cpf/email preenchidos. Só falta clicar "Confirmar e pagar". - [ ] **Step 4: Commit** ```bash git add src/components/reservation/ReservationFlow.tsx git commit -m "feat: ReservationFlow aplica prefill de query params no boot" ``` --- ## Task 5: Investigar infra de Captain Tools (antes de criar a nova tool) **Files:** nenhum. É uma etapa de pesquisa. - [ ] **Step 1: Mapear como ferramentas são registradas** Ler os arquivos: ```bash cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/chatwoot ls enterprise/app/services/captain/tools/ ``` Entender: - Qual a classe-base das tools (`BaseTool`? `Captain::Tools::Base`?) - Como uma tool declara seu schema/parâmetros pro LLM - Como ela é descoberta pelo assistant (registro explícito? autoload?) - Como o assistant invoca a tool Abrir `generate_pix_tool.rb` e identificar: - Qual método é o entry point (`execute`?) - Qual é a estrutura de retorno (`formatted_message`, `raw_payload`, `success`) - Como ela pega o contexto da conversa (via `@conversation`?) - [ ] **Step 2: Mapear o assistente Angelina no banco** ```bash PGPASSWORD= psql -h localhost -U postgres -d chatwoot_dev -c \ "SELECT id, name, account_id FROM captain_assistants ORDER BY id;" ``` Identificar o ID do assistente que representa a Angelina. ```bash PGPASSWORD= psql -h localhost -U postgres -d chatwoot_dev -c \ "SELECT id, assistant_id, title FROM captain_scenarios LIMIT 20;" ``` Ou investigar onde o assistente registra suas tools (pode ser em `tools`, `capabilities`, ou similar — checar o schema do `captain_assistants`). - [ ] **Step 3: Documentar findings num arquivo temporário de notas** Create `chatwoot/docs/superpowers/notes/angelina-tool-integration.md`: ```markdown # Notas: integração de tool custom no assistente Angelina ## Classe-base das tools ## Como tools declaram schema ## Como tools são registradas num assistente ## Assistente Angelina - ID: - Account ID: - Tools atuais: ``` Commitar: ```bash git add docs/superpowers/notes/angelina-tool-integration.md git commit -m "docs: notas sobre integracao de tool custom na angelina" ``` --- ## Task 6: Criar `GenerateReservationLinkTool` **Files:** - Create: `chatwoot/enterprise/app/services/captain/tools/generate_reservation_link_tool.rb` - [ ] **Step 1: Criar o skeleton baseado no GeneratePixTool** Copiar a assinatura básica do `GeneratePixTool` (herança de `BaseTool` ou equivalente descoberto na Task 5). A tool recebe params: `marca`, `unidade`, `permanencia`, `categoria`, `checkin_at`, `nome`, `telefone`, `cpf`, `email`, `observacao` (todos opcionais — a Angelina coleta o que consegue e manda). Estrutura do arquivo: ```ruby module Captain module Tools class GenerateReservationLinkTool < BaseTool # ou a classe correta da Task 5 DEFAULT_BASE_URL = 'http://localhost:5180' def name 'generate_reservation_link' end def description <<~DESC Gera um link da página de reserva pública com os dados já pré-preenchidos. Use esta ferramenta quando já tiver coletado na conversa: marca, unidade, categoria da suíte, permanência, data/hora de check-in e dados do cliente (nome, telefone, CPF, email opcional). Envie o link retornado ao cliente para ele confirmar e pagar. DESC end def parameters_schema # Formato que a tool-base espera. Investigar na Task 5 se é JSON schema, hash simbolizado, etc. { type: 'object', properties: { marca: { type: 'string', description: 'Nome da marca (ex: Hotel 1001 Noites)' }, unidade: { type: 'string', description: 'Nome da unidade do hotel' }, permanencia: { type: 'string', description: 'Permanência escolhida (ex: 3hrs, 4hrs, Pernoite)' }, categoria: { type: 'string', description: 'Categoria da suíte (ex: Standard, Hidromassagem)' }, checkin_at: { type: 'string', description: 'Data e horário de check-in em ISO 8601' }, nome: { type: 'string', description: 'Nome completo do cliente' }, telefone: { type: 'string', description: 'Telefone do cliente' }, cpf: { type: 'string', description: 'CPF do cliente (apenas números ou com formato)' }, email: { type: 'string', description: 'Email do cliente (opcional)' }, observacao: { type: 'string', description: 'Observação do cliente (opcional)' } }, required: [] } end def execute(*_args, **params) base = ENV.fetch('RESERVA_1001_BASE_URL', DEFAULT_BASE_URL) query = build_query(params) url = query.empty? ? base : "#{base}/?#{query}" { formatted_message: "Pronto! Clique no link para revisar e pagar a entrada via PIX:\n#{url}", url: url, success: true } end private def build_query(params) mapping = { marca: params[:marca], unidade: params[:unidade], permanencia: params[:permanencia], categoria: params[:categoria], checkin: params[:checkin_at], nome: params[:nome], telefone: params[:telefone], cpf: params[:cpf], email: params[:email], obs: params[:observacao] } URI.encode_www_form(mapping.compact.reject { |_, v| v.to_s.strip.empty? }) end end end end ``` **Nota:** a assinatura exata de `execute`, `name`, `description`, `parameters_schema` e a classe-base (`BaseTool`) devem vir da investigação da Task 5. Ajustar ali. - [ ] **Step 2: Adicionar `RESERVA_1001_BASE_URL` no .env.example** Edit `chatwoot/.env.example`, adicionar: ``` # Reserva Rede 1001 — URL base do app publico (usada pela Angelina pra gerar links prefill) RESERVA_1001_BASE_URL=http://localhost:5180 ``` Também adicionar ao `.env` local (não commitar). - [ ] **Step 3: Commit** ```bash git add enterprise/app/services/captain/tools/generate_reservation_link_tool.rb .env.example git commit -m "feat: nova tool GenerateReservationLink para angelina enviar link prefill" ``` --- ## Task 7: Registrar a tool no assistente Angelina **Files:** depende da investigação da Task 5. - [ ] **Step 1: Adicionar a tool ao assistente** Via SQL direto ou UI do Captain: - Via UI: abrir Settings → Captain → Assistants → Angelina → Tools → Add → `generate_reservation_link` - Via SQL: investigar onde tools são registradas e fazer `UPDATE captain_assistants SET tools = tools || ...` ou criar row em tabela de junção Registrar o comportamento: ``` A tool `generate_reservation_link` deve ser invocada quando o cliente já informou: marca, unidade, categoria, permanência, data/hora, e ao menos nome + telefone + CPF. O retorno da tool (a URL) deve ser enviado como mensagem outgoing ao cliente. ``` - [ ] **Step 2: Teste manual — conversar com Angelina** 1. Abrir Chatwoot web 2. Entrar numa conversa com um contato de teste no inbox da Hotel 1001 Águas Lindas 3. Digitar como cliente (simulando): "Quero reservar uma suíte Standard na Águas Lindas, 3hrs, amanhã às 22h. Meu nome é João, CPF 123.456.789-09, telefone 61 99999 8888" 4. Angelina deve chamar a tool e devolver o link preenchido 5. Abrir o link numa aba nova — confirmar que tudo está preenchido 6. Só faltar clicar "Confirmar e pagar" - [ ] **Step 3: Se algo não funcionar** Iterar no prompt do assistente (system instructions) pra garantir que ele invoca a tool no momento certo. Isso é ajuste no Captain, não código. --- ## Critérios de conclusão **Fechamento Fase 2+3:** - [ ] Label `aguardando_pagamento` aparece na conversa após POST `/public_reservations` - [ ] Após pagamento via webhook Inter, label é removida e `pagamento_confirmado` é adicionada (comportamento nativo do ConfirmationService) **Fase 3.5 — Angelina prefill:** - [ ] Abrir `http://localhost:5180/?marca=...&nome=...` mostra form já preenchido - [ ] Angelina conversa com cliente, chama `GenerateReservationLink`, envia URL - [ ] Cliente abre URL, vê tudo preenchido, clica "Confirmar e pagar", recebe PIX real --- ## Próximas fases (após esta) - **Fase 4** — Admin CRUD (Supabase Auth + telas de cadastro) - **Fase 5** — Polish visual (animações, hero, skeletons, confetti) - **Fase 6** — Deploy Vercel + subdomínio ## Evolução possível (V2) - Migrar de query params → token assinado (Opção B) quando o fluxo estiver validado - Angelina também busca `unidades` e `categorias` via catálogo Supabase pra confirmar opções disponíveis (hoje ela envia os nomes que o cliente falou; se não bater, a página mostra vazio)