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>
This commit is contained in:
Rodribm10 2026-04-13 22:16:58 -03:00
parent cdc5149866
commit 944cec7136

View File

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