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

464 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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