Sistema de Gestão de Parceiros e Cupons com Integração Stripe
Documento prescritivo e único de referência para implementação. Versionar como
docs/ARCHITECTURE.md.
Plataforma fullstack Next.js que permite a uma organização gerenciar parceiros comerciais e seus cupons de desconto sincronizados com Stripe, registrando o uso real desses cupons em transações reais e expondo dashboards de performance individualizados (por parceiro) e consolidados (admin). O sistema é a fonte de verdade local de uso e receita atribuída, e o Stripe é a fonte de verdade dos cupons e das transações financeiras — reconciliados via webhooks e cron.
| Persona | Objetivo | Fluxo nuclear |
|---|---|---|
| Admin | Operar a base de parceiros e a economia de cupons | Login → cria parceiro → cria cupom (gera no Stripe) → associa cupom ao parceiro → acompanha analytics global |
| Parceiro | Acompanhar a performance dos próprios cupons | Login → dashboard pessoal → filtra período/cupom → exporta extrato |
| Stripe (sistema externo) | Emite eventos financeiros reais | Webhook → registra coupon_usage + transaction → atualiza dashboards |
| Tecnologia | Versão | Justificativa |
|---|---|---|
| Next.js | 15.x (App Router) | Server Components, Server Actions, streaming, RSC payload. Padrão atual da Vercel. |
| React | 19.x | useActionState, useOptimistic, suporte nativo a Actions. |
| TypeScript | 5.6+ | strict: true obrigatório. Sem any. |
| Node runtime | 20.x | Runtime padrão Vercel para rotas server. Webhook Stripe exige runtime Node (não Edge). |
| PostgreSQL | 16 (Neon) | Branching, autoscale, integração nativa Vercel. |
| Stripe Node SDK | stripe 17.x |
SDK oficial, tipos atualizados. |
| Camada | Biblioteca | Justificativa |
|---|---|---|
| ORM | Drizzle ORM + drizzle-kit |
SQL-first, type-safety por inferência sem codegen runtime, bundle pequeno, performático em serverless, migrations via SQL. |
| Driver Postgres | @neondatabase/serverless |
HTTP-fetch driver compatível com Edge e cold starts da Vercel. |
| Validação | Zod 3.x | Schemas compartilhados client/server, inferência de tipos. |
| Auth | Auth.js v5 (NextAuth) | Ver §2.3. |
| Hash de senha | @node-rs/bcrypt |
Rust-based, rápido em ambiente serverless. |
| UI | shadcn/ui + Tailwind CSS 4 | Componentes copiáveis, ownership do código, tematização via CSS vars. |
| Tabelas | TanStack Table v8 | Headless, ordenação/filtros/paginação tipados. |
| Gráficos | Recharts | Maduro, declarativo, integra com shadcn/ui (componente Chart). |
| Datas/timezone | date-fns + date-fns-tz |
Tree-shakeable; timezone explícito (ver §8.4). |
| Resend + React Email | DX excelente, templates JSX, free tier generoso. | |
| CSV | papaparse (export client) ou stream Node csv-stringify (server) |
Export de relatórios. |
| Logs estruturados | Pino + Vercel Logs | JSON estruturado, baixo overhead. |
| Erros | Sentry (@sentry/nextjs) |
Tracing distribuído server+client, source maps automáticos na Vercel. |
| Rate limit | @upstash/ratelimit + Upstash Redis |
Free tier; protege login e webhooks. |
| Idempotência | Tabela webhook_events (ver §3) |
Sem dependência externa para deduplicação. |
| Testes | Vitest + Playwright | Vitest para unit/integration, Playwright para E2E. |
Decisão: Auth.js v5 (NextAuth) com adapter Drizzle, provider Credentials, sessão JWT em cookie httpOnly.
Justificativa técnica:
auth() helper funciona em Server Components, Server Actions, Route Handlers e middleware.database adicionam latência em serverless).password_reset_tokens), pois o provider Credentials não cobre esse fluxo nativamente. É uma feature isolada, simples e idiomática.Trade-off aceito: Auth.js tem mais "magia" que uma solução própria com JWT puro, mas o ganho em manutenção, integração e callbacks tipados compensa.
Convenções: UUID v7 (
gen_random_uuid()enquanto v7 não estiver no core; usar extensãopg_uuidv7se disponível no Neon),snake_case,created_at/updated_atem todas as entidades de domínio, timestamps emtimestamptz.
-- Extensions
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "citext";
-- ============================================================
-- USERS (Auth.js + domínio)
-- ============================================================
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email citext NOT NULL UNIQUE,
password_hash text,
name text NOT NULL,
role text NOT NULL CHECK (role IN ('admin', 'partner')),
email_verified timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deactivated_at timestamptz
);
CREATE INDEX idx_users_role ON users(role) WHERE deactivated_at IS NULL;
CREATE INDEX idx_users_deactivated_at ON users(deactivated_at);
-- ============================================================
-- PARTNERS
-- ============================================================
CREATE TABLE partners (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL UNIQUE REFERENCES users(id) ON DELETE RESTRICT,
company_name text NOT NULL,
contact_phone text,
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_partners_company_name ON partners USING gin (company_name gin_trgm_ops);
-- ============================================================
-- COUPONS
-- ============================================================
CREATE TABLE coupons (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
stripe_coupon_id text NOT NULL UNIQUE,
partner_id uuid REFERENCES partners(id) ON DELETE SET NULL,
code citext NOT NULL UNIQUE,
name text NOT NULL,
discount_type text NOT NULL CHECK (discount_type IN ('percent', 'amount')),
discount_value integer NOT NULL CHECK (discount_value > 0),
currency text CHECK (currency ~ '^[a-z]{3}