iachat/docs/superpowers/plans/2026-04-14-reserva-1001-fase-3-5-angelina-prefill.md

20 KiB

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:
    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!(...):

    mark_conversation_as_awaiting_payment(conversation)

E adicionar no bloco private:

  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
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:

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
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:

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<ReservationFormState> {
  const out: Partial<ReservationFormState> = {}
  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
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 marcaNomemarcaId e aplicar
  3. Quando unidades carregar (depois da marca), resolver unidadeNomeunidadeId
  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:

import type { PrefillData } from '@/lib/prefill'
import { prefillSimpleFields } from '@/lib/prefill'

Mudar a assinatura:

export function useReservationForm(initialPrefill?: PrefillData) {

Mudar o estado inicial:

  const [form, setForm] = useState<ReservationFormState>(() => ({
    ...empty,
    ...prefillSimpleFields(initialPrefill ?? {}),
  }))

Adicionar um ref de "já aplicou prefill de marca/unidade/categoria/permanencia" pra não reaplicar:

  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:

  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):

  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
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
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:

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:

const initialPrefill = parsePrefillFromURL()

Mudar a chamada do hook:

const {
  form,
  update,
  ...
} = useReservationForm(initialPrefill)
  • Step 2: Rodar tudo
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
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:

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

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.

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:

# Notas: integração de tool custom no assistente Angelina

## Classe-base das tools
<descrever>

## Como tools declaram schema
<descrever>

## Como tools são registradas num assistente
<descrever>

## Assistente Angelina
- ID: <X>
- Account ID: <Y>
- Tools atuais: <lista>

Commitar:

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:

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  pré-preenchidos.
          Use esta ferramenta quando  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
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)