From 944cec7136f34f4e9bd14b58cd5d4762d6fd7aa2 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Mon, 13 Apr 2026 22:16:58 -0300 Subject: [PATCH] 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) --- .../specs/2026-04-13-reserva-1001-design.md | 463 ++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-13-reserva-1001-design.md diff --git a/docs/superpowers/specs/2026-04-13-reserva-1001-design.md b/docs/superpowers/specs/2026-04-13-reserva-1001-design.md new file mode 100644 index 000000000..8e6335eaf --- /dev/null +++ b/docs/superpowers/specs/2026-04-13-reserva-1001-design.md @@ -0,0 +1,463 @@ +# 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. + +```sql +-- 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 é: + ```ts + // 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 +```ruby +# 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:** +```json +{ + "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:** +```json +{ + "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 etapas** — `AnimatePresence` 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)