iachat/docs/superpowers/specs/2026-04-13-reserva-1001-design.md
Rodribm10 944cec7136 docs: adiciona spec de design da Reserva Rede 1001
Novo app publico de reserva (Vite + React + Supabase) separado do
Chatwoot, que reusa toda a tubulacao de PIX (CobService, PixCharge,
webhook Inter, ConfirmationService) via um endpoint novo no Chatwoot.

Cobre: arquitetura, paleta premium, modelo de dados reformado
(corrige bug de preco nos domingos), contrato da API nova, fluxo
do cliente, plano de entrega em 6 fases e riscos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:16:58 -03:00

19 KiB
Raw Blame History

Reserva Rede 1001 — Design

Data: 2026-04-13 Autor: Rodrigo Borba Machado (com assistência do Claude) Status: Aprovado para planejamento


1. Contexto e objetivo

O Grupo Nova opera 4 marcas de hotéis/motéis em Brasília e Natal: Hotel 1001 Noites, 1001 Noites Prime, 1001 Noites Express e Dolce Amore. Hoje a reserva é feita pela atendente via WhatsApp, que gera um PIX usando a ferramenta Captain::Tools::GeneratePix dentro do Chatwoot fazer.ai. Essa ferramenta pode falhar (erro de API, mTLS, autenticação), e quando falha não há fallback automatizado.

Objetivo: criar uma página pública de reserva onde o próprio cliente:

  1. Escolhe marca → unidade → permanência → categoria de suíte → data/hora de check-in
  2. Vê fotos da categoria e o preço total
  3. Preenche dados pessoais (nome, telefone, CPF, e-mail)
  4. Paga 50% de entrada via PIX (QR code + copia-e-cola) direto na página
  5. Ao confirmar o pagamento, uma mensagem automática aparece na conversa do WhatsApp da atendente no Chatwoot

É fallback pro fluxo manual e também canal direto de auto-atendimento.

Escopo EXCLUÍDO:

  • Disponibilidade em tempo real (é catálogo fixo — atendente resolve conflitos no check-in)
  • Integração com PMS externo
  • Login de cliente / histórico de reservas do cliente
  • Reserva de suíte específica (sempre por categoria)
  • Segunda metade do pagamento online (50% restantes ficam para o check-in)

2. Decisões-chave

Decisão Escolha Motivo
App separado do Chatwoot Sim Stack diferente (React vs Rails/Vue 2), deploy diferente, Chatwoot é dashboard interno ruim para página de conversão
Stack Vite + React 19 + TS Mantém o POC existente, evita overkill do Next.js para app de checkout
UI Tailwind v4 + shadcn/ui + Radix Mantém base do POC, adiciona shadcn oficial
Animação framer-motion (já tem) + anime.js (adicionar) Pedido do usuário
Banco Supabase Postgres + Auth + Storage Usuário já usa Supabase
Deploy Vercel Grátis, deploy em 30s
Geração de PIX Chatwoot API (novo endpoint) Reusa Captain::Inter::CobService, PixCharge, webhook Inter e ConfirmationService — zero duplicação, auto-mensagem no WhatsApp de graça
Detecção de pagamento Webhook Inter existente (/api/v1/captain/webhooks/inter_pix) Já funciona hoje; novo app só consulta status
Repositório fazer-ai-kanban/reserva-1001/ (irmão do chatwoot/) Separado mas próximo

3. Identidade visual — paleta premium

Inspiração: hotelaria de luxo boutique, tema árabe noturno de 1001 Noites, sensual sem ser brega.

Token Hex Uso
obsidian #0B0D12 Fundo principal
midnight #0F1A2E Superfícies elevadas (cards, modais)
champagne #C9A961 Ação primária, destaques de luxo, botão "Confirmar e Pagar"
rose-gold #E8B4A0 Ação secundária, acentos sensuais, labels de carinho
ivory #F5F1E8 Texto principal sobre fundos escuros
slate #6B7280 Texto secundário, placeholders
emerald #10B981 Estado de sucesso (pagamento confirmado)
ruby #E11D48 Estado de erro, validações

Tipografia: manter Inter (já no POC) para UI. Adicionar Fraunces (serif display) para hero e títulos — dá contraste editorial/luxo.

Gradientes: obsidian → midnight no fundo, champagne → rose-gold em CTAs.


4. Arquitetura de alto nível

┌────────────────────────┐       ┌────────────────────────┐
│   reserva-1001         │       │   Chatwoot (Rails)     │
│   React + Vite         │──API─▶│                        │
│   Supabase             │       │   Novo controller:     │
│   Vercel               │       │   PublicReservations   │
│                        │       │                        │
│   • Página pública     │       │   Reusa existente:     │
│   • Admin              │       │   • Captain::Inter::   │
│   • Checkout PIX       │       │     CobService         │
└─────────┬──────────────┘       │   • PixCharge          │
          │                      │   • ConfirmationSvc    │
          ▼                      │   • Webhook Inter      │
   ┌─────────────┐               │   • Auto-msg WhatsApp  │
   │  Supabase   │               └────────────────────────┘
   │  Postgres   │                           ▲
   │  Auth       │                           │
   │  Storage    │                      webhook
   └─────────────┘                           │
                                     ┌───────┴──────┐
                                     │ Banco Inter  │
                                     └──────────────┘

Separação de responsabilidades:

Sistema É dono de
reserva-1001 UX pública, UX admin, cadastro (marcas/unidades/categorias/preços/fotos/extras), formulário de reserva, tela de checkout, polling de status
Chatwoot Contato, conversa, inbox WhatsApp, geração de PIX via Inter, PixCharge, detecção de pagamento, mensagem automática na conversa da atendente

O novo app não duplica nada do Chatwoot — ele consome a API.


5. Modelo de dados (Supabase)

Reformulação sobre o schema do POC, corrigindo problemas e adicionando o que falta.

-- Marcas (4 fixas inicialmente)
brands (
  id            bigserial primary key,
  name          text not null,
  slug          text not null unique,
  logo_url      text,
  primary_color text,
  created_at    timestamptz default now()
)

-- Unidades físicas de cada marca
hotel_units (
  id                bigserial primary key,
  brand_id          bigint references brands(id) on delete cascade,
  name              text not null,
  slug              text not null,
  chatwoot_unit_id  bigint not null, -- FK lógica pro Captain::Unit do Chatwoot
  active            boolean default true,
  created_at        timestamptz default now(),
  unique (brand_id, slug)
)

-- Categorias de suíte (antes era JSON em hotel_units)
suite_categories (
  id          bigserial primary key,
  unit_id     bigint references hotel_units(id) on delete cascade,
  name        text not null,
  description text,
  sort_order  int default 0,
  active      boolean default true,
  created_at  timestamptz default now()
)

-- Fotos por categoria (antes era JSON)
suite_images (
  id            bigserial primary key,
  category_id   bigint references suite_categories(id) on delete cascade,
  storage_path  text not null,  -- path no Supabase Storage
  alt           text,
  sort_order    int default 0,
  created_at    timestamptz default now()
)

-- Preços por categoria + faixa de dia + tipo de permanência (em centavos)
pricing (
  id              bigserial primary key,
  category_id     bigint references suite_categories(id) on delete cascade,
  day_range       text not null, -- 'weekday' | 'weekend' (enum)
  stay_type       text not null, -- '2hrs' | '3hrs' | '4hrs' | 'pernoite' | 'diaria'
  price_cents     int not null check (price_cents >= 0),
  created_at      timestamptz default now(),
  unique (category_id, day_range, stay_type)
)

-- Extras (antes era localStorage)
extras (
  id            bigserial primary key,
  brand_id      bigint references brands(id) on delete cascade,
  title         text not null,
  description   text,
  price_cents   int not null,
  image_url     text,
  active        boolean default true,
  sort_order    int default 0,
  created_at    timestamptz default now()
)

-- Reservas (NOVA — hoje só vai pro N8N, perdendo histórico)
reservations (
  id                       bigserial primary key,
  brand_id                 bigint references brands(id),
  unit_id                  bigint references hotel_units(id),
  category_id              bigint references suite_categories(id),
  stay_type                text not null,
  checkin_at               timestamptz not null,
  customer_name            text not null,
  customer_phone           text not null,
  customer_cpf             text not null,
  customer_email           text,
  notes                    text,
  total_cents              int not null,
  deposit_cents            int not null,
  chatwoot_contact_id      bigint,
  chatwoot_conversation_id bigint,
  chatwoot_pix_charge_id   bigint,
  pix_txid                 text,
  status                   text not null default 'pending', -- pending|paid|expired|canceled
  created_at               timestamptz default now(),
  paid_at                  timestamptz
)

-- Reserva ↔ extras escolhidos
reservation_extras (
  reservation_id  bigint references reservations(id) on delete cascade,
  extra_id        bigint references extras(id),
  price_cents     int not null, -- snapshot do preço no momento
  primary key (reservation_id, extra_id)
)

RLS (Row Level Security):

  • Leitura pública: brands, hotel_units (active), suite_categories (active), suite_images, pricing, extras (active)
  • Escrita: só pro role admin (Supabase Auth)
  • reservations: leitura só da própria (via service_role no backend Vite)

Bug corrigido do POC:

  • day_range hoje checa dayOfWeek >= 1 && <= 3, mas domingo é 0, fazendo domingo cair em "quinta a domingo" errado.
  • Novo schema usa enum 'weekday' | 'weekend' e a função de mapeamento é:
    // weekday: segunda(1) a quinta(4)
    // weekend: sexta(5), sábado(6), domingo(0)
    const isWeekend = (d: Date) => [0, 5, 6].includes(d.getDay())
    

6. Integração Chatwoot — endpoint novo

Único código novo no Chatwoot: um controller público autenticado por token.

Arquivo novo

enterprise/app/controllers/public/api/v1/captain/public_reservations_controller.rb

Rotas

# config/routes.rb — dentro do enterprise namespace
namespace :public, defaults: { format: 'json' } do
  namespace :api do
    namespace :v1 do
      namespace :captain do
        resources :public_reservations, only: [:create, :show] do
          member do
            get :status
          end
        end
      end
    end
  end
end

Autenticação

Token opaco gerado como variável de ambiente no Chatwoot (RESERVA_1001_API_TOKEN), validado por before_action. Não é login de agente.

POST /public/api/v1/captain/public_reservations

Request:

{
  "chatwoot_unit_id": 42,
  "category": "Spa-Hidromassagem",
  "stay_type": "3hrs",
  "checkin_at": "2026-04-14T23:57:00-03:00",
  "customer": {
    "name": "João da Silva",
    "phone": "+5561999998888",
    "cpf": "123.456.789-00",
    "email": "joao@exemplo.com"
  },
  "total_cents": 17000,
  "deposit_cents": 8500,
  "notes": "Chegaremos com 15 min de atraso"
}

Fluxo interno do controller:

  1. Valida token
  2. Carrega Captain::Unit por chatwoot_unit_id
  3. Cria ou atualiza Contact no account da unit (match por CPF ou telefone)
  4. Cria Conversation no inbox WhatsApp da unit com nota inicial da reserva
  5. Cria Captain::Reservation (status pending)
  6. Chama Captain::Inter::CobService#call com CPF, valor do depósito, PIX key da unit
  7. Cria Captain::PixCharge vinculada à reservation
  8. Retorna payload de checkout

Response:

{
  "reservation_id": 9931,
  "conversation_id": 4421,
  "pix": {
    "txid": "a1b2c3d4e5...",
    "copia_e_cola": "00020126...",
    "qrcode_base64": "data:image/png;base64,iVBORw0...",
    "expires_at": "2026-04-14T00:57:00-03:00"
  }
}

GET /public/api/v1/captain/public_reservations/:id/status

Retorna { "status": "pending" | "paid" | "expired" }. Usado pelo front pra polling a cada 3s na tela de checkout.

Pagamento

  • Cliente paga no app do banco dele
  • Inter bate webhook no endpoint que já existe: /api/v1/captain/webhooks/inter_pix
  • Captain::Payments::ConfirmationService já marca a reservation como paid, adiciona labels pagamento_confirmado + reserva_feita, e já dispara a mensagem automática " Pagamento confirmado!" na conversa da atendente

Zero código novo pro pagamento. O controller novo só orquestra a criação.


7. Fluxo do cliente (UX)

1. Landing pública
   ↓
2. Hero section ("Reserve a experiência que você merece")
   ↓
3. Formulário em cascata:
   Marca → Unidade → Permanência → Categoria → Data/Hora
   ↓
4. Preview: fotos da categoria (carrossel + lightbox)
             + preço total destacado
             + valor do PIX (50%) destacado
             + extras opcionais (se tiver)
   ↓
5. Dados pessoais: nome, telefone, CPF, email, observação
   ↓
6. [Confirmar e Pagar Reserva]
   ↓
7. Loading (skeleton enquanto chama API)
   ↓
8. Tela de checkout:
   - QR code (com glow animado champagne)
   - Copia-e-cola (botão de copy com feedback)
   - Timer regressivo ("Válido por 59:48")
   - Polling de status a cada 3s
   ↓
9. Cliente paga no banco
   ↓
10. Status muda pra "paid" → animação de sucesso:
    - Confetti champagne + rose-gold
    - Check desenhado com anime.js (stroke draw)
    - Mensagem: "Reserva confirmada! Sua atendente já foi avisada."
   ↓
11. Botão "Voltar ao início" / link do WhatsApp da atendente

8. Página admin

/admin — Supabase Auth (email + senha).

Abas:

  • Marcas — CRUD simples (nome, logo, cor primária)
  • Unidades — CRUD vinculado a marca, com chatwoot_unit_id (dropdown buscando via API do Chatwoot)
  • Categorias — CRUD vinculado a unidade, ordem, ativo/inativo
  • Fotos — upload drag-and-drop pro Supabase Storage, preview, reorder
  • Preços — tabela editável (categoria × day_range × stay_type)
  • Extras — CRUD vinculado a marca
  • Reservas — só leitura, lista com filtros (status, data, unidade), detalhe com link pra conversa no Chatwoot

9. Refatoração do POC — o que herdar, o que refazer

Herdar (copiar direto ou com ajustes mínimos)

  • components/FormField.tsx, components/SelectField.tsx
  • components/ui/* (shadcn base)
  • Estrutura de seleção em cascata
  • index.css (design tokens base)
  • vite.config.ts, tsconfig.json

Refazer

  • App.tsx (730 linhas, monolito) → quebrar em:
    • pages/ReservationPage.tsx
    • components/reservation/HeroSection.tsx
    • components/reservation/ReservationForm.tsx
    • components/reservation/ImageCarousel.tsx
    • components/reservation/PriceSummary.tsx
    • components/reservation/ExtrasGrid.tsx
    • components/reservation/CustomerForm.tsx
    • components/checkout/PixCheckout.tsx
    • components/checkout/SuccessScreen.tsx
    • hooks/useReservationForm.ts (consolida os 14+ useState)
  • components/AdminPage.tsx (881 linhas) → uma aba por arquivo em pages/admin/*, com AuthGate
  • services/apiService.ts → novo services/chatwootApi.ts que fala com o controller novo
  • services/*.ts (brand/unit/pricing/suite) → regenerar após novo schema
  • supabaseClient.ts → mover chaves pra .env.local, usar import.meta.env.VITE_SUPABASE_*
  • index.html → remover importmap do esm.sh, Vite bundla tudo
  • types.ts → regenerar via supabase gen types typescript

Bug pra corrigir

  • Lógica de day_range (domingo no ramo errado) — ver seção 5

10. Melhorias visuais (animações + polish)

  1. Hero section — fundo com parallax suave, gradiente obsidian → midnight, título em Fraunces serif com reveal escalonado (framer-motion staggerChildren)
  2. Stagger entrance nas opções de categoria/unidade quando muda a marca
  3. Carrossel de fotos — swipe horizontal, indicadores, clique pra lightbox em tela cheia
  4. Pulse animation no preço total quando recalcula (anime.js scale: [1, 1.08, 1])
  5. QR code com glow — borda animada pulsando champagne (anime.js keyframes)
  6. Skeleton screens durante fetch inicial e submissão
  7. Tela de sucesso — confetti + check SVG desenhado com anime.js stroke-dashoffset (efeito de "desenhar" o check)
  8. Flip cards nos extras (CSS rotateY on hover)
  9. Transições entre etapasAnimatePresence do framer-motion, slide + fade
  10. Tipografia editorial — Fraunces nos títulos, Inter no resto, kerning generoso
  11. Microinterações em botões — hover com scale: 1.02 + glow champagne sutil
  12. Dark glassmorphism nos cards de categoria (backdrop-blur sobre fundo escuro)

11. Plano de entrega em fases

Fase Escopo Critério de pronto
1. Fundação Novo projeto Vite limpo em reserva-1001/, migração do POC, Supabase novo schema aplicado, Tailwind v4 com paleta premium, .env configurado, remover importmap do HTML pnpm dev roda, Tailwind tokens ativos, schema no Supabase
2. Backend Chatwoot Controller PublicReservationsController com 2 endpoints + token auth + testes RSpec de integração POST retorna PIX real, GET retorna status
3. Fluxo público core Refatoração do App em componentes, hook useReservationForm, integração com API nova, checkout com polling, tela de sucesso Cliente completa reserva end-to-end em ambiente de teste
4. Admin Abas separadas, Supabase Auth, upload de fotos pro Storage, CRUD de todas entidades Admin cadastra marca→unidade→categoria→fotos→preço
5. Polish visual Hero, carrossel, animações (framer-motion + anime.js), skeletons, tipografia, microinterações Revisão visual aprovada
6. Deploy + QA Vercel com subdomínio, teste ponta-a-ponta com unidade real do Chatwoot, ajustes finais Reserva real paga, mensagem cai na conversa da atendente

12. Riscos e mitigações

Risco Mitigação
mTLS Inter falhar no controller novo Controller reusa Captain::Inter::CobService — se falhar pra atendente, falha aqui também. Mesmo comportamento, mesmo suporte
Webhook Inter bate antes do front terminar de criar a reservation no Supabase Chatwoot é a fonte de verdade da reserva e do PIX. Supabase só cacheia o ID pra tela do front. Ordem: chama Chatwoot → salva IDs no Supabase
Cliente sai da tela de checkout antes de pagar Webhook Inter ainda dispara, reserva é marcada como paga no Chatwoot. Front perde a tela de sucesso mas a atendente recebe o aviso e entra em contato
Admin sem autenticação hoje (POC) Fase 4 adiciona Supabase Auth obrigatório
Chave anon do Supabase exposta no repo do POC Rotacionar antes do deploy, mover pra .env
Domingo precificado errado Corrigido no novo schema (seção 5)

13. Itens fora de escopo (V2)

  • Segunda metade do pagamento online
  • Disponibilidade em tempo real (bater PMS)
  • Login de cliente / área logada
  • Remarcação / cancelamento online
  • E-mail transacional (confirmação)
  • SMS de confirmação
  • Google Analytics / tracking de conversão
  • Multi-idioma (PT-BR apenas)
  • App mobile nativo

14. Glossário

  • PIX Copia-e-cola: string que o cliente cola no app do banco para pagar
  • txid: ID único de transação PIX na Inter
  • PixCharge: modelo no Chatwoot que rastreia uma cobrança PIX
  • Captain::Unit: modelo no Chatwoot que representa uma unidade do hotel, com credenciais Inter próprias
  • Day range: se o dia do check-in é dia de semana ou fim de semana, afeta preço
  • Stay type: tipo de permanência (2hrs, 3hrs, 4hrs, pernoite, diária)