v2.0 — Fases 11-15 e 17 Concluídas | Fase 16 Pendente

Paggo.me — Mega Plano Checkout

Documento vivo de desenvolvimento — Atualizado: 21/03/2026

GUIA DE DESENVOLVIMENTO — Plataforma de Checkout Paggo.me

Este documento é um guia vivo de desenvolvimento. Ele é atualizado conforme as funcionalidades são implementadas. Se o Claude perder contexto, deve consultar este arquivo para saber o que já foi feito e o que falta.

Status Geral do Projeto

Item Status
Domínio paggo.me ✅ Configurado (DNS Cloudflare + SSL Let’s Encrypt)
Servidor Nginx ✅ Configurado com proxy reverso para API/app/checkout
Landing Page (paggo.me) ✅ Criada em 20/03/2026
Plano Web (paggo.me/plano) ✅ Publicado em 20/03/2026
Pasta do projeto ✅ /home/glauko/checkout-paggo/
Web root ✅ /var/www/paggo/
PostgreSQL ✅ Banco paggo criado (Docker PostgreSQL 17)
Redis ✅ Conectado ao Redis Docker existente (porta 6379)
Backend Node.js ✅ Rodando na porta 4600
Merchant admin criado ✅ admin@paggo.me

Progresso por Fase

Fase Descrição Status Última Atualização
0 Infraestrutura (domínio, servidor, landing) ✅ Concluída 20/03/2026
1 Fundação — Banco de Dados + Modelos ✅ Concluída 20/03/2026
2 Gateway Adapters — 5 Gateways ✅ Concluída 20/03/2026
3 API Backend — Rotas Principais ✅ Concluída 20/03/2026
4 Engine de Assinaturas + Dunning ✅ Concluída 20/03/2026
5 Frontend — Página de Checkout ✅ Concluída 20/03/2026
6 Dashboard Admin ✅ Concluída 20/03/2026
7 Páginas Públicas ✅ Concluída 20/03/2026
8 Segurança e Performance ✅ Concluída 20/03/2026
9 Deploy e Infraestrutura ✅ Concluída 20/03/2026
10 Testes e Go-Live ✅ Concluída 20/03/2026
— v2.0: Substituição DigitalGuru —
11 Stripe Billing Nativo — Subscriptions API ✅ Concluída 21/03/2026
12 Área de Membros — DB + Backend ✅ Concluída 21/03/2026
13 Área de Membros — Frontend (Portal do Aluno) ✅ Concluída 21/03/2026
14 Gestão de Cursos — Admin CRUD ✅ Concluída 21/03/2026
15 Webhooks Avançados + Controle de Acesso ✅ Concluída 21/03/2026
16 Testes e Migração DigitalGuru → Paggo ⬜ Pendente
17 Configurações Gerais da Plataforma ✅ Concluída 21/03/2026

Legenda de Status

Histórico de Atualizações


Visão Geral

Plataforma de checkout transparente própria, inspirada na Digital Manager Guru, operando no domínio paggo.me. Suporta vendas internacionais em USD, modelo de assinatura recorrente com desconto configurável no primeiro pagamento, e integração com 5 gateways de pagamento. Preparada para escalar $10.000–$20.000/dia.

Estrutura de Diretórios do Projeto

/home/glauko/checkout-paggo/          ← Código-fonte do projeto
  ├── MEGA-PLANO-CHECKOUT.md          ← Este guia de desenvolvimento
  ├── server.js                        ← Entry point Express (porta 4600)
  ├── package.json                     ← Dependências Node.js
  ├── .env                             ← Variáveis de ambiente
  ├── migrations/001_initial.sql       ← Schema PostgreSQL
  ├── src/
  │   ├── config/                      ← database.js, redis.js, constants.js
  │   ├── middleware/                   ← auth.js, rateLimit.js, validate.js, errorHandler.js
  │   ├── gateways/                    ← base.js, stripe.js, mercadopago.js, pagbank.js, asaas.js, pagarme.js, index.js
  │   ├── routes/                      ← auth.js, products.js, checkout.js, subscriptions.js, webhooks.js, customers.js, coupons.js, orders.js, gateways.js, dashboard.js
  │   ├── services/                    ← payment.js, checkout.js, subscription.js, notification.js, analytics.js
  │   ├── jobs/                        ← billingWorker.js, dunningWorker.js, recoveryWorker.js, healthWorker.js
  │   └── utils/                       ← logger.js, crypto.js, currency.js
  └── public/
      ├── checkout/index.html          ← Página de checkout SPA (1000 linhas)
      ├── checkout/success.html        ← Página de sucesso
      ├── checkout/error.html          ← Página de erro
      └── dashboard/index.html         ← Dashboard admin SPA (1295 linhas)

/var/www/paggo/                        ← Web root (Nginx)
  ├── index.html                       ← Landing page do Paggo.me
  └── plano/
      ├── index.html                   ← Plano renderizado em HTML
      └── MEGA-PLANO-CHECKOUT.md       ← Download do plano

Arquitetura Geral

                    [Cloudflare CDN / DNS]
                           |
                    [paggo.me - HTTPS]
                           |
                    [Nginx Reverse Proxy]
                      /          \
            [Frontend SPA]    [Backend API]
            (HTML/JS puro)    (Node.js/Express)
                                   |
                          [PostgreSQL + Redis]
                                   |
                    [Payment Orchestration Layer]
                    /      |       |      |       \
              [Stripe] [MercadoPago] [PagBank] [Asaas] [Pagar.me]

Stack Tecnológico

Camada Tecnologia Justificativa
Frontend Checkout HTML/CSS/JS puro (SPA leve) Ultra-rápido (<1s load), sem framework pesado
Backend API Node.js + Express Mesmo stack do ConstruX, fácil manutenção
Banco Principal PostgreSQL ACID compliance para dados financeiros
Cache/Sessões Redis Rate limiting, idempotência, sessões
Fila de Jobs Bull (Redis-based) Webhooks, retentativas, dunning
Tokenização Hosted fields dos gateways PCI DSS scope reduction (SAQ-A)
CDN/DNS Cloudflare SSL, DDoS, performance
Email Nodemailer (SMTP) Transacional (recibos, dunning, boas-vindas)

FASE 1: Fundação — Banco de Dados + Modelos ✅ Concluída

1A. Schema PostgreSQL

Tabela: merchants (multi-tenant ready)

CREATE TABLE merchants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(255) NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  domain VARCHAR(255),
  logo_url TEXT,
  brand_color VARCHAR(7) DEFAULT '#6C5CE7',
  currency VARCHAR(3) DEFAULT 'USD',
  timezone VARCHAR(50) DEFAULT 'America/Sao_Paulo',
  webhook_url TEXT,
  webhook_secret VARCHAR(64),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

Tabela: gateway_configs (credenciais por merchant por gateway)

CREATE TABLE gateway_configs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  gateway VARCHAR(20) NOT NULL, -- stripe, mercadopago, pagbank, asaas, pagarme
  is_active BOOLEAN DEFAULT true,
  is_primary BOOLEAN DEFAULT false,
  priority INT DEFAULT 0, -- para failover ordering
  credentials JSONB NOT NULL, -- { api_key, secret_key, public_key, etc }
  supported_methods TEXT[] DEFAULT '{}', -- {credit_card, pix, boleto}
  supported_currencies TEXT[] DEFAULT '{USD,BRL}',
  config JSONB DEFAULT '{}', -- settings específicos do gateway
  created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_gw_merchant ON gateway_configs(merchant_id, is_active);

Tabela: products

CREATE TABLE products (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  name VARCHAR(255) NOT NULL,
  description TEXT,
  type VARCHAR(20) NOT NULL DEFAULT 'subscription', -- one_time, subscription
  price_cents INT NOT NULL, -- preço em centavos (ex: 900 = $9.00)
  currency VARCHAR(3) DEFAULT 'USD',
  -- Subscription fields
  billing_cycle VARCHAR(20), -- monthly, quarterly, yearly
  billing_interval INT DEFAULT 1, -- a cada N ciclos
  trial_days INT DEFAULT 0,
  -- Desconto no primeiro pagamento
  first_payment_discount_enabled BOOLEAN DEFAULT false,
  first_payment_price_cents INT, -- preço do 1º pagamento (ex: 900 = $9)
  -- Installments (parcelamento)
  max_installments INT DEFAULT 1,
  -- Metadata
  image_url TEXT,
  checkout_title VARCHAR(255),
  checkout_description TEXT,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_products_merchant ON products(merchant_id, is_active);

Tabela: coupons

CREATE TABLE coupons (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  code VARCHAR(50) NOT NULL,
  discount_type VARCHAR(10) NOT NULL, -- percent, fixed
  discount_value INT NOT NULL, -- percent: 10=10%, fixed: centavos
  currency VARCHAR(3) DEFAULT 'USD',
  max_uses INT, -- NULL = ilimitado
  used_count INT DEFAULT 0,
  max_uses_per_customer INT DEFAULT 1,
  applies_to_cycles INT DEFAULT 1, -- quantos ciclos de assinatura aplica
  valid_from TIMESTAMPTZ,
  valid_until TIMESTAMPTZ,
  product_ids UUID[], -- NULL = todos os produtos
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_coupon_code ON coupons(merchant_id, code);

Tabela: customers

CREATE TABLE customers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  email VARCHAR(255) NOT NULL,
  name VARCHAR(255),
  document VARCHAR(20), -- CPF/CNPJ/Tax ID
  phone VARCHAR(20),
  country VARCHAR(2),
  city VARCHAR(100),
  -- Gateway customer IDs (para 1-click)
  gateway_customer_ids JSONB DEFAULT '{}', -- { stripe: "cus_xxx", mercadopago: "xxx" }
  -- Payment tokens salvos (para 1-click)
  saved_cards JSONB DEFAULT '[]', -- [{ last4, brand, token, gateway, exp }]
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_customer_email ON customers(merchant_id, email);

Tabela: orders

CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  customer_id UUID REFERENCES customers(id),
  product_id UUID REFERENCES products(id),
  subscription_id UUID REFERENCES subscriptions(id),
  -- Valores
  amount_cents INT NOT NULL,
  currency VARCHAR(3) DEFAULT 'USD',
  discount_cents INT DEFAULT 0,
  coupon_id UUID REFERENCES coupons(id),
  net_amount_cents INT NOT NULL, -- amount - discount
  -- Pagamento
  payment_method VARCHAR(20), -- credit_card, pix, boleto
  gateway VARCHAR(20),
  gateway_transaction_id VARCHAR(255),
  gateway_response JSONB,
  -- Status
  status VARCHAR(20) DEFAULT 'pending',
    -- pending, processing, approved, declined, refunded, cancelled, expired
  paid_at TIMESTAMPTZ,
  refunded_at TIMESTAMPTZ,
  -- Installments
  installments INT DEFAULT 1,
  -- Card info (tokenizado, sem dados sensíveis)
  card_last4 VARCHAR(4),
  card_brand VARCHAR(20),
  -- Tracking
  ip_address INET,
  user_agent TEXT,
  utm_source VARCHAR(100),
  utm_medium VARCHAR(100),
  utm_campaign VARCHAR(100),
  utm_content VARCHAR(100),
  utm_term VARCHAR(100),
  referrer TEXT,
  -- Metadata
  metadata JSONB DEFAULT '{}',
  idempotency_key VARCHAR(64) UNIQUE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_orders_merchant ON orders(merchant_id, status);
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_subscription ON orders(subscription_id);
CREATE INDEX idx_orders_created ON orders(created_at);
CREATE INDEX idx_orders_gateway_tx ON orders(gateway, gateway_transaction_id);

Tabela: subscriptions

CREATE TABLE subscriptions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  customer_id UUID REFERENCES customers(id),
  product_id UUID REFERENCES products(id),
  -- Pricing
  price_cents INT NOT NULL,
  currency VARCHAR(3) DEFAULT 'USD',
  first_payment_price_cents INT, -- preço diferenciado do 1º pagamento
  -- Ciclo
  billing_cycle VARCHAR(20) NOT NULL, -- monthly, quarterly, yearly
  billing_interval INT DEFAULT 1,
  current_period_start TIMESTAMPTZ,
  current_period_end TIMESTAMPTZ,
  next_billing_at TIMESTAMPTZ,
  -- Status
  status VARCHAR(20) DEFAULT 'trialing',
    -- trialing, active, past_due, paused, cancelled, expired
  cancelled_at TIMESTAMPTZ,
  cancel_reason TEXT,
  -- Trial
  trial_end TIMESTAMPTZ,
  -- Contadores
  billing_count INT DEFAULT 0, -- quantas cobranças já foram feitas
  -- Dunning
  retry_count INT DEFAULT 0,
  last_retry_at TIMESTAMPTZ,
  -- Gateway
  gateway VARCHAR(20),
  gateway_subscription_id VARCHAR(255),
  card_token TEXT, -- token do cartão para renovação
  card_last4 VARCHAR(4),
  card_brand VARCHAR(20),
  -- Coupon
  coupon_id UUID REFERENCES coupons(id),
  coupon_cycles_remaining INT DEFAULT 0,
  -- Metadata
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_sub_merchant ON subscriptions(merchant_id, status);
CREATE INDEX idx_sub_customer ON subscriptions(customer_id);
CREATE INDEX idx_sub_next_billing ON subscriptions(next_billing_at) WHERE status IN ('active','past_due');

Tabela: webhook_events (idempotência)

CREATE TABLE webhook_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  gateway VARCHAR(20) NOT NULL,
  event_id VARCHAR(255) NOT NULL, -- ID do evento no gateway
  event_type VARCHAR(100) NOT NULL,
  payload JSONB NOT NULL,
  processed BOOLEAN DEFAULT false,
  processed_at TIMESTAMPTZ,
  error TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_webhook_event ON webhook_events(gateway, event_id);

Tabela: checkout_sessions (abandono de carrinho)

CREATE TABLE checkout_sessions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  product_id UUID REFERENCES products(id),
  -- Dados parciais do comprador
  email VARCHAR(255),
  name VARCHAR(255),
  phone VARCHAR(20),
  -- Tracking
  step VARCHAR(20) DEFAULT 'started', -- started, email, payment, completed
  ip_address INET,
  user_agent TEXT,
  utm_source VARCHAR(100),
  utm_medium VARCHAR(100),
  utm_campaign VARCHAR(100),
  referrer TEXT,
  -- Recovery
  recovery_email_sent BOOLEAN DEFAULT false,
  recovered BOOLEAN DEFAULT false,
  -- Timestamps
  started_at TIMESTAMPTZ DEFAULT NOW(),
  last_activity_at TIMESTAMPTZ DEFAULT NOW(),
  completed_at TIMESTAMPTZ,
  expires_at TIMESTAMPTZ DEFAULT NOW() + INTERVAL '24 hours'
);
CREATE INDEX idx_session_merchant ON checkout_sessions(merchant_id, step);
CREATE INDEX idx_session_recovery ON checkout_sessions(recovery_email_sent, completed_at)
  WHERE completed_at IS NULL AND recovery_email_sent = false;

Tabela: gateway_health (monitoramento de gateways)

CREATE TABLE gateway_health (
  id SERIAL PRIMARY KEY,
  gateway VARCHAR(20) NOT NULL,
  merchant_id UUID REFERENCES merchants(id),
  success_count INT DEFAULT 0,
  failure_count INT DEFAULT 0,
  avg_latency_ms INT DEFAULT 0,
  last_success_at TIMESTAMPTZ,
  last_failure_at TIMESTAMPTZ,
  is_healthy BOOLEAN DEFAULT true,
  period_start TIMESTAMPTZ DEFAULT NOW(),
  period_end TIMESTAMPTZ DEFAULT NOW() + INTERVAL '5 minutes'
);
CREATE INDEX idx_gw_health ON gateway_health(gateway, merchant_id, period_start);

1B. Configuração Redis

- checkout:session:{id} — dados temporários da sessão de checkout (TTL 24h)
- idempotency:{key} — controle de idempotência (TTL 24h)
- rate_limit:{ip} — rate limiting por IP (TTL 1min)
- gateway_health:{gateway}:{merchant_id} — circuit breaker (TTL 5min)
- card_token:{temp_token} — token temporário de cartão (TTL 60s)

1C. Configuração do Projeto

/home/glauko/checkout-paggo/
├── package.json
├── .env
├── .env.example
├── server.js              — Entry point + Express app
├── src/
│   ├── config/
│   │   ├── database.js    — PostgreSQL connection pool
│   │   ├── redis.js       — Redis connection
│   │   └── constants.js   — Enums, status constants
│   ├── middleware/
│   │   ├── auth.js        — JWT authentication
│   │   ├── rateLimit.js   — Rate limiting
│   │   ├── validate.js    — Request validation (Joi/Zod)
│   │   └── errorHandler.js
│   ├── routes/
│   │   ├── auth.js        — Login/register merchant
│   │   ├── products.js    — CRUD produtos
│   │   ├── checkout.js    — Página de checkout + processamento
│   │   ├── subscriptions.js — Gestão de assinaturas
│   │   ├── webhooks.js    — Receber webhooks dos gateways
│   │   ├── customers.js   — Gestão de clientes
│   │   ├── coupons.js     — CRUD cupons
│   │   ├── orders.js      — Consulta de pedidos
│   │   ├── gateways.js    — Config de gateways
│   │   ├── dashboard.js   — API do painel admin
│   │   └── export.js      — Exportação de dados
│   ├── gateways/
│   │   ├── index.js       — Gateway factory/router
│   │   ├── stripe.js      — Adaptador Stripe
│   │   ├── mercadopago.js — Adaptador Mercado Pago
│   │   ├── pagbank.js     — Adaptador PagBank
│   │   ├── asaas.js       — Adaptador Asaas
│   │   └── pagarme.js     — Adaptador Pagar.me
│   ├── services/
│   │   ├── payment.js     — Orquestração de pagamento
│   │   ├── subscription.js — Engine de assinaturas
│   │   ├── dunning.js     — Retentativas e cobrança
│   │   ├── checkout.js    — Lógica de checkout
│   │   ├── recovery.js    — Recuperação de carrinho abandonado
│   │   ├── notification.js — Emails/SMS
│   │   └── analytics.js   — Métricas e conversão
│   ├── jobs/
│   │   ├── billingWorker.js   — Processar cobranças recorrentes
│   │   ├── dunningWorker.js   — Retentativas de cobrança falha
│   │   ├── recoveryWorker.js  — Enviar emails de recuperação
│   │   └── healthWorker.js    — Monitorar saúde dos gateways
│   └── utils/
│       ├── currency.js    — Conversão e formatação de moeda
│       ├── crypto.js      — Hashing, webhook signatures
│       └── logger.js      — Logging estruturado
├── public/
│   ├── checkout/
│   │   ├── index.html     — Página de checkout (SPA)
│   │   ├── checkout.css   — Estilos do checkout
│   │   └── checkout.js    — Lógica frontend do checkout
│   └── dashboard/
│       ├── index.html     — Dashboard admin
│       ├── dashboard.css
│       └── dashboard.js
└── migrations/
    └── 001_initial.sql    — Schema completo

FASE 2: Gateway Adapters — Integração com 5 Gateways ✅ Concluída

2A. Interface Comum (Gateway Adapter Pattern)

Cada gateway implementa a mesma interface:

class GatewayAdapter {
  // Cobranças
  async createCharge({ amount_cents, currency, card_token, customer, metadata })
  async refundCharge(transaction_id, amount_cents)

  // Assinaturas
  async createSubscription({ plan, customer, card_token, first_amount_cents })
  async cancelSubscription(subscription_id)
  async chargeSubscription(subscription_id, amount_cents)

  // Tokenização
  getPublicKey() // retorna chave pública para o frontend tokenizar

  // Clientes
  async createCustomer({ email, name, document })

  // Webhooks
  verifyWebhookSignature(payload, signature, secret)
  parseWebhookEvent(payload) // normaliza evento para formato interno

  // Health
  async healthCheck()
}

2B. Stripe Adapter

API Base: https://api.stripe.com/v1
Auth: Bearer sk_xxx
Frontend: Stripe.js + Elements (hosted fields)

Recursos usados:
- POST /v1/payment_intents → criar cobrança
- POST /v1/customers → criar cliente
- POST /v1/subscriptions → criar assinatura
- POST /v1/subscriptions/{id} → cancelar
- POST /v1/refunds → estornar

Assinatura com desconto no 1º pagamento:
- Criar subscription com `add_invoice_items` para ajustar o 1º valor
- OU usar `coupon` com `duration: once` aplicado na subscription
- OU usar subscription_schedule com phases diferentes

Multi-currency: NATIVO — aceita 135+ moedas
Webhooks: HMAC-SHA256 com whsec_xxx
Taxas: 2.9% + $0.30 (internacional: 3.9% + $0.30)

2C. Mercado Pago Adapter

API Base: https://api.mercadopago.com/v1
Auth: Bearer ACCESS_TOKEN
Frontend: MercadoPago.js + CardForm (tokenização client-side)

Recursos usados:
- POST /v1/payments → criar pagamento (transparente)
- POST /v1/customers → criar cliente
- POST /preapproval → criar assinatura
- PUT /preapproval/{id} → atualizar/cancelar
- GET /v1/payments/{id} → consultar

Assinatura com desconto no 1º pagamento:
- Usar campo `auto_recurring.transaction_amount` para valor normal
- 1º pagamento: criar payment avulso com valor reduzido + preapproval separado

Multi-currency: Limitado — opera na moeda local de cada país
Webhooks: POST notification com header x-signature
Taxas Brasil: 4.98% (crédito), PIX 0.99%

2D. PagBank Adapter

API Base: https://api.pagseguro.com (sandbox: https://sandbox.api.pagseguro.com)
Auth: Bearer token
Frontend: PagBank.js encrypt (cartão encriptado client-side)

Recursos usados:
- POST /orders → criar pedido (checkout transparente)
- POST /orders/{id}/pay → processar pagamento
- GET /orders/{id} → consultar
- Recurrence via charges API

Assinatura com desconto no 1º pagamento:
- Criar charge avulso com valor reduzido
- Configurar recurrence plan para valor normal a partir da 2ª cobrança

Multi-currency: Apenas BRL
Webhooks: notification_urls configuradas no pedido
Taxas: 3.05% crédito (D+30), PIX 1.89%

2E. Asaas Adapter

API Base: https://api.asaas.com/v3 (sandbox: https://sandbox.asaas.com/api/v3)
Auth: access_token no header
Frontend: Formulário próprio → POST para API (tokenização via API)

Recursos usados:
- POST /customers → criar cliente
- POST /payments → criar cobrança (avulsa ou recorrente)
- POST /subscriptions → criar assinatura
- DELETE /subscriptions/{id} → cancelar
- POST /payments/{id}/refund → estornar

Assinatura com desconto no 1º pagamento:
- Campo `externalReference` + lógica manual:
  - 1ª cobrança: valor com desconto
  - Atualizar subscription value após 1º pagamento confirmado

Multi-currency: Apenas BRL
Webhooks: POST para URL configurada, sem signature (validar por IP)
Taxas: 2.99% + R$0.49 crédito, R$1.99 boleto, R$1.99 PIX

2F. Pagar.me Adapter

API Base: https://api.pagar.me/core/v5
Auth: Basic base64(sk_xxx:)
Frontend: tokenizecard.js (token expira em 60s)

Recursos usados:
- POST /orders → criar pedido com pagamento
- POST /customers → criar cliente
- POST /plans → criar plano
- POST /subscriptions → criar assinatura
- PATCH /subscriptions/{id} → atualizar/cancelar

Assinatura com desconto no 1º pagamento:
- Usar `increments` ou `discounts` na subscription
- Campo `cycles` no discount para aplicar em N ciclos

Multi-currency: Apenas BRL
Webhooks: POST com header x-hub-signature (HMAC-SHA1)
Rate Limits: GET 4/s default, POST sem limite
Taxas: Negociáveis (geralmente 2.49%–3.19% crédito)

2G. Payment Orchestration Layer

// Roteamento inteligente:
async function routePayment(merchant_id, payment) {
  const gateways = await getActiveGateways(merchant_id);

  // 1. Filtrar por moeda suportada
  const compatible = gateways.filter(g =>
    g.supported_currencies.includes(payment.currency)
  );

  // 2. Filtrar por método de pagamento
  const methodCompatible = compatible.filter(g =>
    g.supported_methods.includes(payment.method)
  );

  // 3. Ordenar por prioridade + saúde
  const sorted = methodCompatible.sort((a, b) => {
    const healthA = getGatewayHealth(a.gateway, merchant_id);
    const healthB = getGatewayHealth(b.gateway, merchant_id);
    if (!healthA.is_healthy && healthB.is_healthy) return 1;
    if (healthA.is_healthy && !healthB.is_healthy) return -1;
    return a.priority - b.priority;
  });

  // 4. Tentar em cascata (transparent retry)
  for (const gw of sorted) {
    try {
      const result = await processPayment(gw, payment);
      if (result.approved) return result;
      if (result.hard_decline) break; // não tentar outro gateway
    } catch (err) {
      markGatewayFailure(gw.gateway, merchant_id);
      continue; // tentar próximo
    }
  }

  return { approved: false, reason: 'all_gateways_failed' };
}

FASE 3: API Backend — Rotas Principais ✅ Concluída

3A. Autenticação (routes/auth.js)

POST /api/auth/register   — Registrar merchant
POST /api/auth/login      — Login (retorna JWT)
POST /api/auth/refresh    — Refresh token
GET  /api/auth/me         — Dados do merchant logado

3B. Produtos (routes/products.js)

GET    /api/products           — Listar produtos do merchant
POST   /api/products           — Criar produto
GET    /api/products/:id       — Detalhe do produto
PUT    /api/products/:id       — Atualizar produto
DELETE /api/products/:id       — Desativar produto

3C. Checkout (routes/checkout.js) — CORE

GET  /api/checkout/:product_id/config  — Config pública do checkout (nome, preço, logo, gateway public keys)
POST /api/checkout/:product_id/session — Iniciar sessão de checkout (tracking)
PUT  /api/checkout/session/:id         — Atualizar dados parciais (para abandono)
POST /api/checkout/:product_id/pay     — PROCESSAR PAGAMENTO (o endpoint principal)
POST /api/checkout/:product_id/validate-coupon — Validar cupom em tempo real

POST /api/checkout/:product_id/pay — Payload:

{
  "customer": {
    "email": "user@example.com",
    "name": "João Silva",
    "document": "12345678900",
    "phone": "+5511999999999"
  },
  "payment": {
    "method": "credit_card",
    "card_token": "tok_xxxx", // token do gateway
    "gateway": "stripe", // gateway preferido (ou auto)
    "installments": 1
  },
  "coupon_code": "LAUNCH10",
  "session_id": "sess_xxx",
  "utm": {
    "source": "facebook",
    "medium": "cpc",
    "campaign": "launch"
  },
  "idempotency_key": "uuid-unique"
}

3D. Assinaturas (routes/subscriptions.js)

GET    /api/subscriptions           — Listar assinaturas
GET    /api/subscriptions/:id       — Detalhe
PATCH  /api/subscriptions/:id       — Atualizar (pause, resume, change card)
DELETE /api/subscriptions/:id       — Cancelar
GET    /api/subscriptions/:id/invoices — Histórico de cobranças

3E. Webhooks (routes/webhooks.js)

POST /api/webhooks/stripe      — Receber eventos Stripe
POST /api/webhooks/mercadopago — Receber eventos Mercado Pago
POST /api/webhooks/pagbank     — Receber eventos PagBank
POST /api/webhooks/asaas       — Receber eventos Asaas
POST /api/webhooks/pagarme     — Receber eventos Pagar.me

Cada endpoint: 1. Verifica assinatura do webhook 2. Checa idempotência (event_id já processado?) 3. Normaliza evento para formato interno 4. Enfileira para processamento assíncrono 5. Retorna 200 OK imediatamente

3F. Clientes (routes/customers.js)

GET    /api/customers           — Listar clientes
GET    /api/customers/:id       — Detalhe com assinaturas e pedidos
PUT    /api/customers/:id       — Atualizar
GET    /api/customers/:id/orders — Histórico de pedidos

3G. Cupons (routes/coupons.js)

GET    /api/coupons             — Listar cupons
POST   /api/coupons             — Criar cupom
PUT    /api/coupons/:id         — Atualizar
DELETE /api/coupons/:id         — Desativar

3H. Pedidos (routes/orders.js)

GET    /api/orders              — Listar pedidos (filtros: status, date, gateway)
GET    /api/orders/:id          — Detalhe do pedido
POST   /api/orders/:id/refund   — Solicitar reembolso
GET    /api/orders/export       — Exportar CSV/JSON

3I. Gateways (routes/gateways.js)

GET    /api/gateways            — Listar gateways configurados
POST   /api/gateways            — Adicionar gateway
PUT    /api/gateways/:id        — Atualizar credenciais/prioridade
DELETE /api/gateways/:id        — Remover gateway
POST   /api/gateways/:id/test   — Testar conexão com gateway
GET    /api/gateways/health     — Status de saúde de todos os gateways

3J. Dashboard/Analytics (routes/dashboard.js)

GET /api/dashboard/overview    — KPIs: receita, vendas, MRR, churn, conversão
GET /api/dashboard/revenue     — Receita por período (dia/semana/mês)
GET /api/dashboard/conversions — Funil de checkout (sessão→email→pagamento→aprovado)
GET /api/dashboard/gateways    — Performance por gateway (aprovação, latência)
GET /api/dashboard/subscriptions — Métricas de assinatura (MRR, churn, LTV)
GET /api/dashboard/geo         — Distribuição geográfica de clientes
GET /api/dashboard/sources     — Atribuição por UTM/fonte

FASE 4: Engine de Assinaturas + Dunning ✅ Concluída

4A. Billing Worker (jobs/billingWorker.js)

Roda a cada 1 hora. Consulta subscriptions com next_billing_at <= NOW() e status = 'active'.

Para cada assinatura vencida:
1. Determinar valor:
   - Se billing_count == 0 E first_payment_price_cents != null → usar preço reduzido
   - Se coupon ativo E coupon_cycles_remaining > 0 → aplicar desconto
   - Senão → preço normal
2. Criar order com status 'processing'
3. Cobrar via gateway (usando card_token salvo)
4. Se aprovado:
   - Order status → 'approved'
   - Subscription → avançar período (current_period_start/end, next_billing_at)
   - billing_count++
   - coupon_cycles_remaining-- (se aplicável)
   - Enviar recibo por email
5. Se recusado:
   - Order status → 'declined'
   - Subscription status → 'past_due'
   - Enfileirar para dunning

4B. Dunning Worker (jobs/dunningWorker.js)

Roda a cada 6 horas. Gerencia retentativas de cobranças falhas.

Schedule de retentativas:
- Dia 0: Falha original → email "problema com pagamento"
- Dia 1: 1ª retentativa → email "ação necessária"
- Dia 3: 2ª retentativa → email "urgente"
- Dia 5: 3ª retentativa → email "última chance" + SMS (se phone)
- Dia 7: 4ª retentativa → email final
- Dia 10: Grace period encerrado → status 'cancelled' + email "cancelado"

Para cada retentativa:
1. Buscar subscriptions com status 'past_due'
2. Calcular próximo retry com base no retry_count
3. Tentar cobrar novamente (com transparent retry entre gateways)
4. Se aprovou: restaurar para 'active' + email "pagamento regularizado"
5. Se falhou: incrementar retry_count, agendar próxima
6. Se esgotou: cancelar subscription + email

4C. Cart Recovery Worker (jobs/recoveryWorker.js)

Roda a cada 30 minutos.

1. Buscar checkout_sessions com:
   - step != 'completed'
   - email preenchido
   - last_activity_at < NOW() - 30min
   - recovery_email_sent = false
   - expires_at > NOW()
2. Para cada sessão abandonada:
   - Enviar email de recuperação com link direto para checkout
   - Marcar recovery_email_sent = true
3. Opcionalmente: 2º email após 24h (se configurado pelo merchant)

4D. Gateway Health Worker (jobs/healthWorker.js)

Roda a cada 5 minutos.

1. Agregar success/failure counts dos últimos 5 minutos por gateway
2. Se failure_rate > 20% OU 5 falhas consecutivas:
   - Marcar gateway como unhealthy
   - Alertar merchant via webhook
3. Se gateway voltar a funcionar (3 sucesso consecutivos):
   - Restaurar healthy

FASE 5: Frontend — Página de Checkout ✅ Concluída

5A. Estrutura da Página (checkout/index.html)

URL: https://paggo.me/c/{product_id} ou https://paggo.me/c/{slug}

Layout single-page otimizado para conversão:

┌─────────────────────────────────────────────┐
│  [Logo Merchant]          🔒 Pagamento Seguro│
├─────────────────────────────────────────────┤
│                                              │
│  ┌──────────────┐  ┌─────────────────────┐  │
│  │  PRODUTO      │  │  FORMULÁRIO          │  │
│  │  ───────────  │  │                     │  │
│  │  [Imagem]     │  │  📧 Email           │  │
│  │               │  │  👤 Nome Completo   │  │
│  │  Nome Produto │  │  📱 Telefone        │  │
│  │               │  │                     │  │
│  │  $9.00/mês   │  │  💳 Método Pagamento│  │
│  │               │  │  ┌─ Card ─┬─ PIX ─┐│  │
│  │  ✓ Acesso     │  │  │        │        ││  │
│  │    imediato   │  │  │ Nº Cartão       ││  │
│  │  ✓ Cancelar   │  │  │ Val    CVV      ││  │
│  │    quando     │  │  │ Nome no Cartão  ││  │
│  │    quiser     │  │  └────────┴────────┘│  │
│  │               │  │                     │  │
│  │  ┌──────────┐ │  │  🏷️ Cupom [______] │  │
│  │  │ CUPOM    │ │  │                     │  │
│  │  │ -$X off  │ │  │  ┌───────────────┐ │  │
│  │  └──────────┘ │  │  │ TOTAL: $9.00  │ │  │
│  │               │  │  │ 1º pag: $4.50 │ │  │
│  └──────────────┘  │  │ Depois: $9/mês │ │  │
│                     │  └───────────────┘ │  │
│                     │                     │  │
│                     │  [🔒 PAGAR AGORA $9] │  │
│                     │                     │  │
│                     │  🔒 Dados protegidos │  │
│                     │  SSL 256-bit         │  │
│                     └─────────────────────┘  │
│                                              │
│  ───────── Trust Badges ─────────            │
│  [Stripe] [Visa] [MC] [SSL] [Garantia]       │
└─────────────────────────────────────────────┘

5B. Otimizações de Conversão (checkout.js)

  1. Progressive form — Campos aparecem conforme preenche (email → nome → pagamento)
  2. Real-time validation — Feedback instantâneo em cada campo
  3. Card brand detection — Ícone da bandeira aparece ao digitar primeiros dígitos
  4. Auto-format — Número do cartão formatado automaticamente (4 em 4)
  5. Coupon live preview — Ao aplicar cupom, preço atualiza instantaneamente com animação
  6. Loading state — Botão desabilitado com spinner durante processamento
  7. Error handling — Mensagens claras e específicas (não genéricas)
  8. 1-click para retornantes — Se email reconhecido com cartão salvo, mostrar opção 1-click
  9. PIX QR Code — Gera e exibe QR code inline, com timer de expiração
  10. Boleto — Gera código de barras copiável
  11. Mobile-first — Touch-friendly, teclado numérico para campos numéricos
  12. Pixel events — Dispara eventos para Facebook/Google em cada etapa do checkout

5C. Hosted Payment Fields (Segurança PCI)

Para cada gateway, carregar o SDK JavaScript correspondente: - Stripe: Stripe.js + stripe.elements().create('card') - Mercado Pago: MercadoPago.js + cardForm.create() - PagBank: PagBank.js + PagSeguro.encryptCard() - Pagar.me: tokenizecard.js - Asaas: Formulário próprio → API backend (sem SDK client-side)

O frontend detecta qual gateway será usado (baseado na config do produto) e carrega o SDK apropriado.

5D. Tracking e Pixels

// Eventos disparados em cada etapa:
checkout_view      → ao carregar a página
checkout_initiate  → ao preencher email
checkout_payment   → ao selecionar método de pagamento
checkout_submit    → ao clicar pagar
checkout_success   → pagamento aprovado
checkout_declined  → pagamento recusado

// Pixels suportados:
- Facebook Pixel (fbq)
- Google Analytics (gtag)
- Google Ads Conversion
- TikTok Pixel
- Meta CAPI (server-side)

FASE 6: Dashboard Admin ✅ Concluída

6A. Login/Auth

URL: https://paggo.me/admin

POST /api/auth/login → cookie JWT → redirect /admin/dashboard

6B. Telas do Dashboard

Dashboard Principal (/admin/dashboard) - KPI Cards: Receita Hoje | Vendas Hoje | MRR | Taxa de Conversão | Churn Rate - Gráfico: Receita dos últimos 30 dias (line chart) - Gráfico: Vendas por gateway (pie chart) - Tabela: Últimas 20 transações (status, valor, cliente, gateway) - Widget: Saúde dos gateways (verde/amarelo/vermelho)

Produtos (/admin/products) - Lista de produtos com toggle ativo/inativo - Modal criar/editar produto com todos os campos - Preview do link de checkout - Copiar link rápido

Pedidos (/admin/orders) - Tabela filtrável (status, data, gateway, método, cliente) - Busca por email ou ID - Detalhe do pedido com timeline (criado → processado → aprovado) - Botão de reembolso - Export CSV

Assinaturas (/admin/subscriptions) - Tabela: assinaturas ativas, past_due, cancelled - Detalhe com histórico de cobranças - Ações: pausar, cancelar, alterar cartão - Métricas: MRR, LTV médio, churn por mês

Clientes (/admin/customers) - Lista de clientes com total gasto e nº de compras - Detalhe com todas as orders e subscriptions - Cartões salvos (last4 + brand apenas)

Cupons (/admin/coupons) - CRUD de cupons - Stats: uso, receita gerada com desconto

Gateways (/admin/gateways) - Configuração de credenciais por gateway - Drag-and-drop para prioridade de failover - Botão “Testar conexão” - Métricas: taxa de aprovação, latência média, volume

Configurações (/admin/settings) — expandido na Fase 17 - Dados do merchant (logo, cor, domínio) - Webhook URL - Configuração de emails - Configuração de dunning (dias de retentativa, conteúdo) - Configurações Stripe (modo, 3DS, auto-cancel: OFF) - Política de cancelamento (manual only, retry infinito) - Área de membros (magic link TTL, sessões simultâneas, anti-compartilhamento)


FASE 7: Páginas Públicas ✅ Concluída

7A. Página de Sucesso

URL: https://paggo.me/c/{product_id}/success?order={id}

✅ Pagamento Aprovado!

Obrigado, {nome}!
Seu pedido #{order_id} foi confirmado.

📧 Enviamos os detalhes para {email}

[Produto: {nome}]
[Valor: ${amount}]
[Método: Cartão ****1234]

Próxima cobrança: {data} — ${valor}/mês

[Acessar produto →]

7B. Página de Erro

URL: https://paggo.me/c/{product_id}/error

❌ Pagamento não aprovado

Possíveis motivos:
• Saldo insuficiente
• Dados incorretos
• Cartão bloqueado

[Tentar novamente →]
[Usar outro método de pagamento →]

7C. Portal do Assinante

URL: https://paggo.me/my (login via magic link por email)

Minhas Assinaturas:
- {Produto} — $9/mês — Ativa — Próximo: 20/Abr
  [Alterar cartão] [Cancelar]

Meus Pagamentos:
- 20/Mar — $9.00 — ✅ Aprovado — Cartão ****1234
- 20/Feb — $4.50 — ✅ Aprovado — Cartão ****1234 (1º pagamento)

FASE 8: Segurança e Performance ✅ Concluída

8A. Segurança

  1. PCI DSS (SAQ-A): Nunca armazenar dados de cartão — apenas tokens dos gateways
  2. HTTPS obrigatório (Cloudflare SSL)
  3. Rate limiting: 10 req/s por IP no checkout, 100 req/s geral
  4. CSRF protection via token
  5. Input validation (Zod) em todas as rotas
  6. SQL injection prevention — prepared statements (pg parameterized queries)
  7. XSS prevention — sanitização de output
  8. Webhook signature verification para cada gateway
  9. Idempotência em todas as operações de pagamento
  10. Audit log — log de todas as ações administrativas
  11. Encrypted credentials — gateway credentials encriptadas no DB (AES-256)
  12. CORS restrito ao domínio paggo.me

8B. Performance para Alto Volume

  1. Connection pooling — pg-pool com max 20 conexões
  2. Redis cache — config de produtos (TTL 5min), sessões de checkout
  3. Async webhooks — resposta 200 imediata, processamento via fila Bull
  4. Database indexes — todos os queries frequentes cobertos
  5. Gzip/Brotli — compressão via Cloudflare
  6. Lazy loading — SDK do gateway carregado sob demanda
  7. CDN — assets estáticos via Cloudflare
  8. Health checks/health endpoint para monitoramento
  9. Graceful shutdown — finalizar jobs em andamento antes de desligar
  10. Logging estruturado — JSON logs com request_id para debugging

8C. Capacidade Estimada

Para $20.000/dia a $9/transação ≈ 2.222 transações/dia~93/hora~1.5/minuto

Uma instância Node.js + PostgreSQL + Redis em um VPS de 4GB RAM pode facilmente lidar com 10x esse volume. Mesmo em picos (Black Friday), a fila Bull absorve os spikes.


FASE 9: Deploy e Infraestrutura ✅ Concluída

9A. Servidor

VPS (Hetzner ou DigitalOcean):
- 4 vCPU, 8GB RAM, 80GB SSD
- Ubuntu 22.04 LTS
- Node.js 20 LTS
- PostgreSQL 16
- Redis 7
- Nginx (reverse proxy + SSL)
- PM2 (process manager)

9B. DNS (Cloudflare)

paggo.me         → A record → IP do servidor
www.paggo.me     → CNAME → paggo.me
api.paggo.me     → A record → IP do servidor (opcional, pode ser mesmo)

9C. Deploy Script

#!/bin/bash
cd /home/glauko/checkout
git pull origin main
npm install --production
npx knex migrate:latest
pm2 reload paggo

9D. Backup

- PostgreSQL: pg_dump diário → S3/Cloudflare R2
- Redis: snapshot a cada 6h (RDB)
- .env: backup manual seguro

FASE 10: Testes e Go-Live ✅ Concluída

10A. Testes por Gateway (Sandbox)

Para cada gateway (Stripe, Mercado Pago, PagBank, Asaas, Pagar.me): 1. ✅ Criar cobrança única (cartão) → aprovado 2. ✅ Criar cobrança única (cartão) → recusado 3. ✅ Criar assinatura com desconto no 1º pagamento 4. ✅ Renovação automática da assinatura 5. ✅ Cancelar assinatura 6. ✅ Reembolso 7. ✅ Webhook recebido e processado 8. ✅ PIX (gateways que suportam) 9. ✅ Boleto (gateways que suportam)

10B. Testes de Checkout

  1. ✅ Checkout completo (formulário → pagamento → sucesso)
  2. ✅ Aplicar cupom e verificar desconto
  3. ✅ Tentativa com cartão inválido → mensagem de erro
  4. ✅ Abandono de carrinho → email de recuperação
  5. ✅ Mobile responsivo
  6. ✅ 1-click para cliente retornante
  7. ✅ Multi-gateway failover (simular falha no primário)

10C. Testes de Segurança

  1. ✅ Rate limiting funcional (429 após limite)
  2. ✅ Webhook com signature inválida → rejeitado
  3. ✅ Idempotência (mesma request 2x → apenas 1 cobrança)
  4. ✅ SQL injection attempt → bloqueado
  5. ✅ XSS attempt → sanitizado

10D. Go-Live Checklist

[ ] Domínio paggo.me apontado para servidor
[ ] SSL/TLS ativo via Cloudflare
[ ] Credenciais de produção configuradas (todos os gateways)
[ ] Webhook URLs de produção registradas em cada gateway
[ ] Backup automático configurado
[ ] PM2 configurado com auto-restart
[ ] Monitoramento ativo (uptime, logs)
[ ] Email transacional configurado (domínio verificado)
[ ] Primeiro produto criado
[ ] Primeiro checkout de teste em produção (cobrança real mínima + reembolso)

Resumo de Fases

Fase Descrição Linhas Estimadas
1 Fundação — Schema DB + Estrutura do projeto ~600
2 Gateway Adapters — 5 integrações de pagamento ~800
3 API Backend — Todas as rotas ~1200
4 Engine de Assinaturas + Dunning + Workers ~500
5 Frontend — Página de Checkout ~800
6 Dashboard Admin — Painel completo ~1500
7 Páginas Públicas — Sucesso, Erro, Portal ~300
8 Segurança + Performance ~300
9 Deploy + Infraestrutura ~200
10 Testes + Go-Live ~200
TOTAL ~6.400 linhas

Dependências Principais (package.json)

{
  "dependencies": {
    "express": "^4.18",
    "pg": "^8.11",
    "ioredis": "^5.3",
    "bull": "^4.12",
    "jsonwebtoken": "^9.0",
    "bcryptjs": "^2.4",
    "stripe": "^14.0",
    "mercadopago": "^2.0",
    "zod": "^3.22",
    "helmet": "^7.1",
    "cors": "^2.8",
    "compression": "^1.7",
    "winston": "^3.11",
    "node-cron": "^3.0",
    "nodemailer": "^6.9",
    "uuid": "^9.0",
    "dotenv": "^16.3"
  }
}

Vantagens sobre Digital Manager Guru

Aspecto Digital Guru Paggo.me
Custo Plano mensal + taxa por venda Grátis (self-hosted)
Controle Limitado aos recursos deles Total — código é seu
Customização Checkout pré-definido 100% customizável
Dados Nos servidores deles Seus servidores
Gateways 20+ (deles) 5 integrados + ilimitado futuro
Failover Básico Transparent retry com routing inteligente
Latência Servidores deles Seu servidor otimizado
Escala Depende do plano deles Sem limites (seu infra)
Analytics Dashboard deles Integrado com ConstruX Analytics
Assinatura 1º pgto Manual Nativo e configurável
Área de membros Não inclui (precisa integrar) Nativa — portal do aluno integrado
Controle de acesso Via webhook externo Automático via Stripe webhooks
Gestão de cursos Não tem CRUD completo no admin

═══════════════════════════════════════════════════════════════

v2.0 — SUBSTITUIÇÃO COMPLETA DA DIGITALGURU MANAGER

═══════════════════════════════════════════════════════════════

Contexto e Motivação

Atualmente o Glauko usa a DigitalGuru Manager como intermediária: - Fluxo atual: Cliente → Checkout DigitalGuru → Stripe processa → DigitalGuru recebe webhook → DigitalGuru libera produto na área de membros - Problema: Dependência de plataforma terceira, taxa extra, sem controle total - Solução: Paggo v2.0 faz tudo internamente — checkout + Stripe Billing + área de membros + gestão de cursos

Arquitetura v2.0

                      [Cloudflare CDN / DNS]
                             |
                      [paggo.me - HTTPS]
                             |
                      [Nginx Reverse Proxy]
                    /       |        \
          [Checkout SPA]  [Admin SPA]  [Portal Aluno SPA]
          /c/{product_id}  /app/        /members/
                    \       |        /
                   [Backend API - Express]
                      |           |
              [PostgreSQL]   [Redis]
                      |
              [Stripe Billing API]
              ├─ Customers (cus_xxx)
              ├─ PaymentMethods (pm_xxx)
              ├─ Subscriptions (sub_xxx)
              ├─ Invoices (auto-cobranças)
              └─ Webhooks → /api/webhooks/stripe
                              |
                    [Enrollment Service]
                    ├─ grantAccess() → libera curso
                    └─ revokeAccess() → bloqueia curso

Como funciona a cobrança recorrente via Stripe (referência técnica)

1ª COMPRA:
  Browser → Stripe.js tokeniza cartão → payment_method (pm_xxx)
  Backend → stripe.customers.create({ email }) → customer_id (cus_xxx)
  Backend → stripe.paymentMethods.attach(pm_xxx, { customer: cus_xxx })
  Backend → stripe.subscriptions.create({ customer: cus_xxx, items: [{ price: price_xxx }] })
  → Stripe cria Invoice → cobra cartão → retorna subscription (sub_xxx)

RENOVAÇÃO MENSAL (automática pela Stripe):
  Stripe gera Invoice na data de renovação
  Stripe cobra o cartão salvo (sem 3DS — MIT exemption)
  Se aprovado → webhook invoice.payment_succeeded → Paggo mantém acesso
  Se falhou → webhook invoice.payment_failed → Stripe retenta em 1, 3, 5 dias
  Se todas falharam → customer.subscription.deleted → Paggo revoga acesso

CARTÃO EXPIRADO:
  Stripe Account Updater atualiza dados automaticamente (banco emite novo cartão)
  Reduz falhas em renovações sem ação do servidor

Webhooks Stripe que o Paggo v2.0 escuta

Evento Quando Ação no Paggo
invoice.payment_succeeded Pagamento aprovado (1º ou renovação) Cria/mantém enrollment, libera produto
invoice.payment_failed Cartão falhou na renovação Marca como inadimplente, notifica aluno
customer.subscription.updated Mudança de plano/status Atualiza enrollment (upgrade/downgrade)
customer.subscription.deleted Assinatura cancelada definitivamente Revoga acesso, bloqueia aulas
charge.dispute.created Chargeback aberto Alerta admin, pode enviar evidências

FASE 11: Stripe Billing Nativo — Subscriptions API ⬜ Pendente

Objetivo

Substituir o billing interno (billingWorker.js + dunningWorker.js) pela Stripe Billing API nativa. A Stripe gerencia todo o ciclo de vida da assinatura: criação, cobrança recorrente, retentativas, cancelamento.

11A. Checkout Frontend — Stripe Elements Atualizado

O checkout atual (public/checkout/index.html) precisa ser adaptado para o fluxo Stripe Billing:

// Mudanças no frontend de checkout:
// 1. hidePostalCode: true — SEM campo de CEP/endereço (produto digital)
const cardElement = elements.create('card', {
  style: { base: { fontSize: '16px', color: '#32325d' } },
  hidePostalCode: true
});

// 2. Criar PaymentMethod (não PaymentIntent) e enviar pro backend
const { paymentMethod, error } = await stripe.createPaymentMethod({
  type: 'card',
  card: cardElement,
  billing_details: { email, name }
});

// 3. Backend cria Subscription. Se precisa 3DS, front confirma:
if (result.requires_action) {
  const { error } = await stripe.confirmCardPayment(result.client_secret);
  // Se ok → redireciona para sucesso
}

11B. Refatorar Gateway Stripe (src/gateways/stripe.js)

O adapter atual usa paymentIntents.create() para cobranças avulsas. Precisa adicionar suporte a:

// Novos métodos no StripeGateway:

async createCustomer({ email, name, metadata }) {
  // stripe.customers.create({ email, name, metadata })
  // Retorna { gateway_customer_id: 'cus_xxx' }
}

async attachPaymentMethod(paymentMethodId, customerId) {
  // stripe.paymentMethods.attach(paymentMethodId, { customer: customerId })
  // stripe.customers.update(customerId, { invoice_settings: { default_payment_method: paymentMethodId } })
}

async createSubscription({ customerId, priceId, couponId, trialDays, metadata }) {
  // stripe.subscriptions.create({
  //   customer: customerId,
  //   items: [{ price: priceId }],
  //   coupon: couponId,
  //   trial_period_days: trialDays,
  //   payment_behavior: 'default_incomplete',
  //   payment_settings: { save_default_payment_method: 'on_subscription' },
  //   expand: ['latest_invoice.payment_intent'],
  //   metadata
  // })
  // Se precisa 3DS: retorna client_secret do PaymentIntent para confirmar no front
  // Se aprovado direto: retorna subscription ativa
}

async cancelSubscription(subscriptionId, { immediately = false } = {}) {
  // immediately ? stripe.subscriptions.cancel(subscriptionId)
  //             : stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true })
}

async updateSubscription(subscriptionId, { priceId, couponId }) {
  // Para upgrade/downgrade de plano
}

async createPrice({ amount, currency, interval, productName }) {
  // stripe.prices.create({ unit_amount: amount, currency, recurring: { interval }, product_data: { name: productName } })
  // Retorna price_id para usar na subscription
}

async createCoupon({ percentOff, amountOff, currency, duration, durationInMonths }) {
  // stripe.coupons.create(...)
  // Cria cupom na Stripe e retorna coupon_id
}

11C. Adicionar stripe_price_id aos products

-- Migration 004_stripe_billing.sql
ALTER TABLE products ADD COLUMN stripe_price_id VARCHAR(255);
ALTER TABLE products ADD COLUMN stripe_product_id VARCHAR(255);
ALTER TABLE subscriptions ADD COLUMN stripe_subscription_id VARCHAR(255);
ALTER TABLE subscriptions ADD COLUMN stripe_customer_id VARCHAR(255);
ALTER TABLE subscriptions ADD COLUMN cancel_at_period_end BOOLEAN DEFAULT false;
ALTER TABLE customers ADD COLUMN stripe_customer_id VARCHAR(255);

11D. Refatorar src/services/checkout.js

O fluxo processPayment() para produtos tipo subscription muda para:

1. Recebe payment_method_id do frontend (tokenizado via Stripe Elements)
2. Encontra/cria customer no banco local
3. Cria Customer na Stripe (ou reutiliza se já existe)
4. Attach PaymentMethod ao Customer
5. Cria Subscription na Stripe (com price_id do produto)
6. Se precisa 3DS → retorna client_secret pro front confirmar
7. Se aprovado → cria subscription + enrollment no banco local
8. Retorna { success: true, subscription_id, enrollment_id }

11E. Script seed-stripe.js — Criar Products/Prices na Stripe

// scripts/seed-stripe.js
// Roda uma vez para criar Product + Price na Stripe e salvar IDs no banco local.
// Uso: node scripts/seed-stripe.js
//
// 1. stripe.products.create({ name: 'ConstruX Premium' })
// 2. stripe.prices.create({ product: prod.id, unit_amount: 9700, currency: 'brl', recurring: { interval: 'month' } })
// 3. UPDATE products SET stripe_product_id = prod.id, stripe_price_id = price.id WHERE id = '...'
//
// Importante: rodar com a Stripe key de TEST primeiro, depois com LIVE.

11F. Novas Variáveis de Ambiente (.env)

# Adicionar ao .env existente:
STRIPE_SECRET_KEY=sk_live_XXXXX         # ou sk_test_ para dev
STRIPE_PUBLISHABLE_KEY=pk_live_XXXXX    # usado no frontend checkout
STRIPE_WEBHOOK_SECRET=whsec_XXXXX       # signing secret do endpoint de webhook
VIMEO_ACCESS_TOKEN=xxxxx                # para embed seguro com domain restriction

11G. Desativar billing workers para Stripe

Os workers billingWorker.js e dunningWorker.js continuam ativos para outros gateways, mas para Stripe a cobrança recorrente é 100% gerenciada pela Stripe via webhooks.

11H. Política de Não-Cancelamento Automático — Retry Infinito

REGRA FUNDAMENTAL: Assinaturas NUNCA são canceladas automaticamente pela plataforma ou pela Stripe. Apenas o admin pode cancelar manualmente através do painel Paggo.

Configuração na Stripe (via Dashboard + API)

Na Stripe Dashboard → Settings → Billing → Subscriptions and emails → Manage failed payments:

Smart Retries: ATIVADO (Stripe usa ML para escolher o melhor momento de retentar)
Schedule de retentativas: 4 tentativas em 30 dias (ou custom)
Após todas as retentativas falharem: "Leave the subscription past_due"
                                     (NÃO selecionar "Cancel the subscription")
Enviar emails de retentativa: DESATIVADO (Paggo envia os próprios emails customizados)

Comportamento do Ciclo de Retry

Ciclo 1 (mês atual):
  Dia 0:  Cobrança falha → Stripe marca invoice como 'open' → Smart Retry ativado
  Dia 1:  1ª retentativa automática (Stripe Smart Retry)
  Dia 3:  2ª retentativa
  Dia 7:  3ª retentativa
  Dia 14: 4ª retentativa (última do ciclo)
  Dia 30: Invoice marcada como 'uncollectible' (NÃO cancela subscription)

Ciclo 2 (próximo mês):
  Dia 0:  Stripe gera NOVA invoice automaticamente (subscription continua ativa/past_due)
  Dia 1-14: Novas retentativas Smart Retry para a nova invoice
  → Repete infinitamente até: pagamento aprovado OU cancelamento manual pelo admin

Status da subscription durante todo o processo: 'past_due' (NUNCA 'cancelled')
Acesso do aluno durante past_due: MANTIDO (configurável na Fase 17)

Implementação no Código

// Em src/gateways/stripe.js — createSubscription():
async createSubscription({ customerId, priceId, couponId, trialDays, metadata }) {
  return stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: priceId }],
    coupon: couponId || undefined,
    trial_period_days: trialDays || undefined,
    payment_behavior: 'default_incomplete',
    payment_settings: {
      save_default_payment_method: 'on_subscription',
      payment_method_options: {
        card: { request_three_d_secure: 'any' }
      }
    },
    // IMPORTANTE: NÃO definir cancel_at ou cancel_at_period_end
    // A subscription deve ficar ativa/past_due indefinidamente
    expand: ['latest_invoice.payment_intent'],
    metadata
  });
}

// Em src/gateways/stripe.js — cancelSubscription():
// SOMENTE chamado pelo admin manualmente via dashboard
async cancelSubscription(subscriptionId, { immediately = false } = {}) {
  if (immediately) {
    return stripe.subscriptions.cancel(subscriptionId);
  }
  // Padrão: cancela ao fim do período pago (mantém acesso até expirar)
  return stripe.subscriptions.update(subscriptionId, {
    cancel_at_period_end: true
  });
}
// NOTA: Este método NUNCA é chamado automaticamente por webhooks ou workers.
// É exclusivo para ação manual do admin na rota POST /api/admin/subscriptions/:id/cancel

// Job custom para retry manual entre ciclos (opcional):
// src/jobs/manualRetryWorker.js
// Roda 1x por dia, busca subscriptions past_due há mais de 35 dias
// onde a última invoice está uncollectible, e força nova tentativa:
//   stripe.invoices.pay(invoiceId, { forgive: false })
// Se a Stripe já gerou nova invoice no ciclo seguinte, não faz nada (ela retenta sozinha)

Emails de Notificação (Paggo envia, não a Stripe)

Email Gatilho Conteúdo
Pagamento falhou invoice.payment_failed (1ª vez) “Houve um problema com seu pagamento. Vamos retentar automaticamente. Se preferir, atualize seu cartão: [link]”
Retentativa falhou invoice.payment_failed (retry_count > 1) “Ainda não conseguimos processar seu pagamento. Atualize seu cartão para manter acesso: [link]”
Última tentativa do ciclo invoice.payment_failed (retry_count == max) “Não conseguimos cobrar neste ciclo. Vamos tentar novamente no próximo mês. Atualize seu cartão: [link]”
Pagamento recuperado invoice.payment_succeeded (era past_due) “Pagamento regularizado! Seu acesso continua normalmente.”

NUNCA enviar email de “sua assinatura foi cancelada” por processo automático. Somente quando o admin cancela manualmente.


FASE 12: Área de Membros — DB + Backend ⬜ Pendente

Objetivo

Criar as tabelas e APIs para área de membros: cursos, módulos, aulas, matrículas (enrollments), e progresso do aluno.

12A. Novas Tabelas — Migration 005_member_area.sql

-- Usuários finais (alunos/compradores) — separado de merchants
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  customer_id UUID REFERENCES customers(id), -- link com customer de pagamento
  email VARCHAR(255) NOT NULL,
  name VARCHAR(255),
  password_hash VARCHAR(255), -- login na área de membros
  avatar_url TEXT,
  is_active BOOLEAN DEFAULT true,
  last_login_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_users_email ON users(merchant_id, email);

-- Cursos
CREATE TABLE courses (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID REFERENCES merchants(id),
  product_id UUID REFERENCES products(id), -- qual produto dá acesso a este curso
  name VARCHAR(255) NOT NULL,
  slug VARCHAR(255) NOT NULL,
  description TEXT,
  thumbnail_url TEXT,
  is_published BOOLEAN DEFAULT false,
  sort_order INT DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_courses_slug ON courses(merchant_id, slug);

-- Módulos (agrupam aulas dentro de um curso)
CREATE TABLE modules (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
  name VARCHAR(255) NOT NULL,
  description TEXT,
  sort_order INT DEFAULT 0,
  is_published BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Aulas
CREATE TABLE lessons (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  module_id UUID REFERENCES modules(id) ON DELETE CASCADE,
  title VARCHAR(255) NOT NULL,
  description TEXT,
  content_type VARCHAR(20) DEFAULT 'video', -- video, text, pdf, quiz
  video_url TEXT, -- URL do vídeo (Vimeo, YouTube, S3, etc.)
  video_duration_seconds INT,
  content_html TEXT, -- para aulas tipo texto
  attachment_url TEXT, -- PDF, arquivo complementar
  is_free_preview BOOLEAN DEFAULT false, -- aula grátis (sem assinatura)
  is_published BOOLEAN DEFAULT false,
  sort_order INT DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Matrículas (enrollment = ligação user ↔ course, controlada por subscription)
CREATE TABLE enrollments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  course_id UUID REFERENCES courses(id),
  subscription_id UUID REFERENCES subscriptions(id), -- qual assinatura deu acesso
  status VARCHAR(20) DEFAULT 'active', -- active, expired, revoked
  enrolled_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ, -- NULL = enquanto assinatura estiver ativa
  revoked_at TIMESTAMPTZ,
  revoke_reason TEXT
);
CREATE UNIQUE INDEX idx_enrollment_unique ON enrollments(user_id, course_id);
CREATE INDEX idx_enrollment_user ON enrollments(user_id, status);
CREATE INDEX idx_enrollment_sub ON enrollments(subscription_id);

-- Progresso do aluno por aula
CREATE TABLE lesson_progress (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  lesson_id UUID REFERENCES lessons(id),
  status VARCHAR(20) DEFAULT 'not_started', -- not_started, in_progress, completed
  progress_pct INT DEFAULT 0, -- 0-100
  last_position_seconds INT DEFAULT 0, -- onde parou no vídeo
  completed_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_progress_unique ON lesson_progress(user_id, lesson_id);

12B. Backend — Novas Rotas

src/routes/members.js — Autenticação de alunos (separada do admin):

POST /api/members/register     — Cria conta de aluno (email + senha)
POST /api/members/login        — Login do aluno (retorna JWT com role='user')
GET  /api/members/me           — Perfil do aluno
PUT  /api/members/me           — Atualizar perfil (nome, senha, avatar)
POST /api/members/forgot       — Recuperação de senha (envia email)
POST /api/members/reset        — Reset de senha com token

src/routes/member-courses.js — Cursos do aluno (requer auth user):

GET  /api/member/courses              — Lista cursos matriculados (com progresso %)
GET  /api/member/courses/:slug        — Detalhes do curso (módulos + aulas)
GET  /api/member/lessons/:id          — Detalhes da aula (se tem acesso)
POST /api/member/lessons/:id/progress — Atualizar progresso (position, pct, completed)
GET  /api/member/dashboard            — KPIs do aluno (cursos, aulas concluídas, streak)

src/routes/admin-courses.js — Gestão de cursos no admin (requer auth merchant):

GET    /api/admin/courses             — Lista todos os cursos
POST   /api/admin/courses             — Criar curso (vinculado a um product)
PUT    /api/admin/courses/:id         — Editar curso
DELETE /api/admin/courses/:id         — Excluir curso

GET    /api/admin/courses/:id/modules     — Lista módulos
POST   /api/admin/courses/:id/modules     — Criar módulo
PUT    /api/admin/modules/:id             — Editar módulo
DELETE /api/admin/modules/:id             — Excluir módulo

GET    /api/admin/modules/:id/lessons     — Lista aulas
POST   /api/admin/modules/:id/lessons     — Criar aula
PUT    /api/admin/lessons/:id             — Editar aula
DELETE /api/admin/lessons/:id             — Excluir aula

GET    /api/admin/enrollments             — Lista matrículas (com filtros)
POST   /api/admin/enrollments             — Criar matrícula manual
DELETE /api/admin/enrollments/:id         — Revogar matrícula

12C. Distinção JWT: Merchant vs Aluno

O sistema usa dois tipos de JWT com payloads diferentes:

// JWT do Merchant (admin) — gerado em POST /api/auth/login
{ id: 'merchant-uuid', role: 'merchant', email: 'admin@paggo.me', iat, exp }
// Verificado por src/middleware/auth.js → seta req.merchant

// JWT do Aluno (membro) — gerado em POST /api/members/login
{ id: 'user-uuid', role: 'user', merchant_id: 'xxx', email: 'aluno@email.com', iat, exp }
// Verificado por src/middleware/memberAuth.js → seta req.user

12D. Emails Transacionais da Área de Membros

Lista de templates que src/services/notification.js precisa ter: | Email | Quando | Conteúdo | |——-|——–|———-| | Boas-vindas | 1º pagamento aprovado | Magic link de acesso + senha temporária + nome do curso | | Recibo mensal | Renovação paga | Valor cobrado, próxima renovação, link do portal | | Pagamento falhou | invoice.payment_failed | Aviso + link para atualizar cartão no portal | | Última chance | 3ª retentativa falhou | Aviso urgente + link para atualizar cartão | | Assinatura cancelada | subscription.deleted | Confirmação de cancelamento + data de fim de acesso | | Magic link | Login sem senha | Link temporário (15min) para acessar o portal | | Reset de senha | Recuperação de senha | Link com token para definir nova senha | | Chargeback | charge.dispute.created | Alerta ao admin com detalhes da disputa |

12E. Middleware de Controle de Acesso

src/middleware/memberAuth.js:

// Verifica JWT do aluno (role='user'), seta req.user
// Diferente do auth.js que verifica JWT do merchant (role='merchant')

src/middleware/checkAccess.js:

// Middleware que verifica se o aluno tem enrollment ativo para o curso/aula solicitado
// Verifica: enrollment.status === 'active' E subscription.status IN ('active', 'trialing')
// Se aula é is_free_preview === true → libera sem enrollment
// Se não tem acesso → retorna 403 com mensagem "Assinatura necessária"

12F. Service — Enrollment Automático

src/services/enrollment.js:

// grantAccess(customerId, subscriptionId, productId)
//   → Encontra/cria user a partir do customer
//   → Encontra curso vinculado ao product
//   → Cria enrollment (user ↔ course ↔ subscription)
//   → Envia email de boas-vindas com link de acesso

// revokeAccess(subscriptionId)
//   → Encontra enrollments com essa subscription
//   → Atualiza status para 'revoked'
//   → Envia email informando que acesso foi revogado

// checkAccess(userId, courseId)
//   → Verifica enrollment ativo + subscription ativa
//   → Retorna { hasAccess: true/false, reason: '...' }

FASE 13: Área de Membros — Frontend (Portal do Aluno) ⬜ Pendente

Objetivo

Frontend SPA para o aluno acessar seus cursos, assistir aulas, e acompanhar progresso.

13A. Portal do Aluno — public/members/index.html

URL: https://paggo.me/members/ (hash router: #/login, #/dashboard, #/course/:slug, #/lesson/:id)

Telas:

  1. Login/Registro (#/login)
    • Email + senha
    • Link “Esqueci minha senha”
    • Formulário de registro (nome, email, senha)
    • Design limpo e moderno
  2. Dashboard do Aluno (#/dashboard)
    • Saudação com nome do aluno
    • Cards dos cursos matriculados (thumbnail, nome, progresso %)
    • Barra de progresso visual por curso
    • Próxima aula sugerida (continue de onde parou)
    • Badge “Assinatura ativa até DD/MM” ou alerta “Assinatura vencida”
  3. Página do Curso (#/course/:slug)
    • Header com thumbnail + nome + descrição
    • Lista de módulos com accordion expandível
    • Cada módulo mostra suas aulas (título, duração, ícone de status: ✓ concluída, ▶ em andamento, ○ não iniciada)
    • Indicador de aulas grátis (preview)
    • Botão de continuar na última aula em andamento
  4. Player de Aula (#/lesson/:id)
    • Player de vídeo responsivo (embed Vimeo/YouTube ou HTML5)
    • Título da aula + descrição
    • Navegação anterior/próxima aula
    • Lista lateral de aulas do módulo (mini-sidebar)
    • Botão “Marcar como concluída”
    • Download de material complementar (se houver)
    • Aulas tipo texto: renderiza content_html
    • Salva progresso automaticamente (a cada 30s ou ao trocar de aula)
  5. Perfil (#/profile)
    • Editar nome, email, senha
    • Ver status da assinatura
    • Botão cancelar assinatura (confirma antes)

13B. Integração com Nginx

# Adicionar ao /etc/nginx/sites-enabled/paggo:
location /members/ {
    alias /var/www/paggo/members/;
    try_files $uri $uri/ /members/index.html;
}

13C. Proteção de Conteúdo Vimeo

Os vídeos hospedados no Vimeo devem ter domain restriction configurado: - No painel do Vimeo → Settings → Privacy → “Allow embedding on specific domains only” - Adicionar apenas paggo.me (e domínios futuros do cliente) - Isso impede que alunos compartilhem o link do embed e assistam fora da plataforma - Usar o VIMEO_ACCESS_TOKEN para gerar embeds seguros via API se necessário

Além de email+senha, oferecer login por magic link:

POST /api/members/magic-link  → Envia email com link temporário (JWT de 15min)
GET  /api/members/verify?token=xxx → Valida token, cria sessão, redireciona pro dashboard

13E. Design


FASE 14: Gestão de Cursos — Admin CRUD ⬜ Pendente

Objetivo

Adicionar ao dashboard admin as telas de gestão de cursos, módulos e aulas.

14A. Novas Seções no Dashboard Admin

Adicionar no sidebar do dashboard (public/dashboard/index.html): - Cursos → Lista de cursos com CRUD - Alunos → Lista de alunos matriculados com status de assinatura

14B. Telas Admin

  1. Lista de Cursos (#/courses)
    • Tabela: Nome, Produto vinculado, Módulos, Aulas, Alunos, Status
    • Botão criar novo curso
    • Drag-and-drop para reordenar (sort_order)
  2. Editar Curso (#/courses/:id)
    • Formulário: nome, slug, descrição, thumbnail, produto vinculado
    • Toggle publicado/rascunho
    • Seção de módulos com accordion
    • Dentro de cada módulo: lista de aulas com CRUD inline
    • Drag-and-drop para reordenar módulos e aulas
    • Formulário de aula: título, tipo (vídeo/texto/PDF), URL do vídeo, conteúdo HTML, duração, preview grátis
    • Botão adicionar módulo, adicionar aula
  3. Lista de Alunos (#/students)
    • Tabela: Nome, Email, Curso, Status Assinatura, Progresso %, Último acesso
    • Filtros: curso, status da assinatura (ativa/inadimplente/cancelada)
    • Ações: ver detalhes, revogar acesso, dar acesso manual
  4. Detalhes do Aluno (#/students/:id)
    • Perfil do aluno
    • Assinaturas vinculadas (status, datas)
    • Cursos matriculados com progresso por módulo/aula
    • Histórico de pagamentos
    • Botão conceder/revogar acesso manual

FASE 15: Webhooks Avançados + Controle de Acesso ⬜ Pendente

Objetivo

Refatorar src/routes/webhooks.js para processar todos os eventos de lifecycle da Stripe e automatizar o controle de acesso da área de membros.

15A. Novos Eventos no Webhook Handler

// src/routes/webhooks.js — processWebhookEvent() expandido:

case 'invoice.payment_succeeded':
  // Pagamento aprovado (1º pagamento OU renovação mensal)
  // → Busca user pelo stripe_customer_id
  // → stripe.subscriptions.retrieve() para dados frescos
  // → Atualiza subscription local (status, current_period_start/end)
  // → Se é 1º pagamento (billing_count === 0):
  //     → Chama enrollmentService.grantAccess()
  //     → Gera senha temporária e envia email de boas-vindas com magic link
  // → Se é renovação:
  //     → Garante enrollment ativo
  //     → Se era past_due → volta para active
  // → Registra payment no banco local
  // → Envia recibo por email
  break;

case 'invoice.payment_failed':
  // Cartão falhou na renovação
  // → Marca subscription como past_due (NUNCA como cancelled)
  // → NÃO revoga acesso (aluno mantém acesso durante retentativas)
  // → Incrementa retry_count no banco local
  // → Envia email customizado baseado no retry_count:
  //     retry 1: "Houve um problema, vamos retentar"
  //     retry 2+: "Atualize seu cartão para manter acesso"
  //     retry final do ciclo: "Tentaremos novamente no próximo mês"
  // → Loga tentativa no banco com timestamp e motivo da falha
  // → IMPORTANTE: NUNCA cancelar subscription aqui, independente do retry_count
  break;

case 'customer.subscription.updated':
  // Mudança de status/plano
  // → Se cancel_at_period_end === true (cancelamento manual pelo admin):
  //     mantém acesso até current_period_end, depois revoga
  // → Se fez upgrade: atualiza enrollment para novo curso
  // → Se fez downgrade: ajusta acesso
  // → Se voltou de past_due para active: email "pagamento regularizado"
  // → NOTA: Se Stripe tentar mudar status para 'canceled', verificar
  //     se foi ação manual do admin. Se não foi, ignorar e logar alerta.
  break;

case 'customer.subscription.deleted':
  // Assinatura cancelada definitivamente
  // → VERIFICAR se foi cancelamento manual pelo admin (checar flag no banco local)
  // → Se foi manual: revogar acesso + email de cancelamento
  // → Se NÃO foi manual (Stripe cancelou sozinha):
  //     → LOGAR ALERTA: "Stripe cancelou subscription sem autorização"
  //     → NÃO revogar acesso automaticamente
  //     → Notificar admin para avaliar (pode ser config errada na Stripe)
  //     → Considerar recriar subscription via API
  // → Atualiza subscription.status no banco conforme decisão
  break;

case 'charge.dispute.created':
  // Chargeback aberto
  // → Alerta admin via email/dashboard
  // → Loga no audit_log
  // → Pode revogar acesso preventivamente
  break;

15B. Endpoint para Aluno Atualizar Cartão

POST /api/members/update-card
  → Recebe novo payment_method_id (tokenizado via Stripe Elements no portal)
  → stripe.paymentMethods.attach(pm_new, { customer: cus_xxx })
  → stripe.customers.update(cus_xxx, { invoice_settings: { default_payment_method: pm_new } })
  → stripe.paymentMethods.detach(pm_old)
  → Atualiza card_last4 e card_brand no banco local

15C. Anti-Fraude e Proteção

  1. Forçar 3D Secure na primeira cobrançapayment_behavior: 'default_incomplete' já faz isso
  2. Log de acesso do aluno — Registrar IP, horário, e aula acessada em tabela access_logs:

  3. Evidências automáticas de chargeback — Quando charge.dispute.created chega, coletar automaticamente:
    • Logs de acesso do aluno (IPs, aulas assistidas, datas)
    • Progresso de aulas (prova que o aluno usou o produto)
    • Email de confirmação de compra (timestamp)
    • Enviar via stripe.disputes.update(dispute.id, { evidence: { ... } })
  4. Detecção de compartilhamento de conta — Se mesmo user_id acessa de 3+ IPs diferentes em 24h, alertar admin
  5. Rate limiting no checkout — Já existe (10 req/60s) mas reforçar para evitar card testing

15D. Endpoint Webhook Externo (para integração com outros sistemas)

POST /api/admin/webhook-config
  → Configura URL externa para Paggo enviar webhooks
  → Eventos: payment.approved, subscription.activated, subscription.cancelled, enrollment.granted, enrollment.revoked
  → Útil para integrar com ConstruX Analytics ou outros sistemas

FASE 16: Testes e Migração DigitalGuru → Paggo ⬜ Pendente

Objetivo

Testar o fluxo completo end-to-end e migrar os alunos existentes da DigitalGuru para o Paggo.

16A. Testes com Stripe Test Mode

  1. Checkout completo: Usar cartão de teste 4242 4242 4242 4242 → verificar que cria Customer + Subscription na Stripe + enrollment no Paggo
  2. 3D Secure: Usar cartão 4000 0027 6000 3184 → verificar fluxo de autenticação
  3. Renovação: Avançar relógio da Stripe (Test Clock) → verificar webhook invoice.payment_succeeded → enrollment mantido
  4. Cartão falhou: Usar cartão 4000 0000 0000 0341 → verificar webhook invoice.payment_failed → email de aviso + retentativas
  5. Cancelamento manual: Cancelar subscription pelo admin → verificar webhook customer.subscription.deleted → enrollment revogado → aluno sem acesso
  6. Teste de retry infinito: Usar Test Clock + cartão que falha → verificar que subscription fica past_due e NUNCA muda para cancelled → verificar que novo ciclo gera nova invoice → verificar emails de notificação em cada retry
  7. Teste de recuperação: Após várias falhas, atualizar cartão → verificar que próximo retry funciona → subscription volta para active
  8. Cupom: Aplicar cupom de desconto → verificar valor cobrado na Stripe
  9. Área de membros: Login do aluno → ver cursos → assistir aula → progresso salvo
  10. Acesso revogado: Após cancelamento, aluno tenta acessar aula → recebe 403

16B. Migração da DigitalGuru

  1. Exportar dados da DigitalGuru: lista de alunos ativos (email, nome, produto, data da assinatura)
  2. Script de migração (migrations/migrate_from_guru.js):
    • Para cada aluno ativo:
      • Cria customer no Paggo
      • Cria user com senha temporária
      • Cria enrollment para o curso correspondente
      • NÃO cria subscription na Stripe (mantém na DigitalGuru até transição completa)
    • Envia email para cada aluno com:
      • Link de acesso ao novo portal
      • Senha temporária
      • Instrução para trocar senha
  3. Transição gradual: Novos alunos já compram pelo Paggo. Alunos existentes são migrados em lotes.
  4. Desativação da DigitalGuru: Quando todos os alunos estiverem no Paggo, desativar checkout da DigitalGuru.

16C. Checklist Go-Live v2.0

Stripe: - [ ] Stripe webhook endpoint configurado no Stripe Dashboard (produção) - [ ] Webhook signing secret salvo no .env (STRIPE_WEBHOOK_SECRET) - [ ] Stripe API key de produção no .env (STRIPE_SECRET_KEY) - [ ] Stripe publishable key no checkout frontend (STRIPE_PUBLISHABLE_KEY) - [ ] Produto ConstruX Premium criado na Stripe com Price ID (via seed-stripe.js) - [ ] Webhook endpoint recebendo raw body (NÃO express.json())

Conteúdo: - [ ] Curso vinculado ao produto no Paggo admin - [ ] Módulos e aulas cadastrados no admin - [ ] Vídeos no Vimeo com domain restriction configurada (apenas paggo.me) - [ ] VIMEO_ACCESS_TOKEN configurado no .env

Infraestrutura: - [ ] Nginx configurado para /members/ - [ ] Email SMTP configurado e testado (recibo, dunning, boas-vindas, magic link) - [ ] Migrations 004, 005, 006 executadas no PostgreSQL

Testes: - [ ] Teste de compra com cartão real (valor baixo, depois reembolsa) - [ ] Teste de 3D Secure (cartão 4000 0027 6000 3184) - [ ] Teste de webhook (invoice.payment_succeeded libera acesso) - [ ] Teste de cancelamento manual (admin cancela → subscription.deleted → acesso revogado) - [ ] Teste de acesso revogado (aluno recebe 403 ao tentar acessar aula) - [ ] Teste de retry infinito (cartão que falha → subscription fica past_due, NUNCA cancelled) - [ ] Teste de configurações gerais (alterar settings → comportamento muda conforme configurado) - [ ] Verificar no Stripe Dashboard: “After all retries fail” está como “Leave past_due” (NÃO “Cancel”)

Migração: - [ ] Migração dos alunos existentes executada - [ ] Emails enviados aos alunos com credenciais do novo portal - [ ] DigitalGuru desativada para novas vendas - [ ] Monitoramento: logs de webhook, alertas de falha, access_logs


FASE 17: Configurações Gerais da Plataforma ⬜ Pendente

Objetivo

Criar uma área de configurações centralizada no painel admin onde o merchant pode definir o comportamento geral da plataforma: Stripe, cobrança, dunning, emails, checkout, área de membros, e integrações.

17A. Tabela platform_settings — Migration 007_platform_settings.sql

CREATE TABLE platform_settings (
  id SERIAL PRIMARY KEY,
  category VARCHAR(50) NOT NULL,       -- 'stripe', 'billing', 'emails', 'checkout', 'members', 'integrations'
  key VARCHAR(100) NOT NULL,
  value JSONB NOT NULL DEFAULT '{}',
  description TEXT,
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  updated_by UUID REFERENCES merchants(id),
  UNIQUE(category, key)
);

-- Settings iniciais (seed)
INSERT INTO platform_settings (category, key, value, description) VALUES
-- Stripe
('stripe', 'mode', '"live"', 'Modo da Stripe: live ou test'),
('stripe', 'publishable_key', '""', 'Stripe publishable key (pk_live_ ou pk_test_)'),
('stripe', 'webhook_secret', '""', 'Stripe webhook signing secret'),
('stripe', 'auto_cancel', 'false', 'Cancelar assinatura automaticamente após falha de pagamento (RECOMENDADO: false)'),
('stripe', 'three_d_secure', '"any"', 'Quando exigir 3D Secure: any (sempre), automatic (Stripe decide)'),
('stripe', 'account_updater', 'true', 'Usar Stripe Account Updater para atualizar cartões expirados'),

-- Billing / Dunning
('billing', 'retry_max_attempts', '4', 'Número máximo de retentativas por ciclo de cobrança'),
('billing', 'retry_schedule_days', '[1, 3, 7, 14]', 'Dias após falha para cada retentativa'),
('billing', 'after_all_retries_fail', '"keep_past_due"', 'Ação após todas retentativas falharem: keep_past_due (NUNCA cancelar)'),
('billing', 'keep_access_on_past_due', 'true', 'Manter acesso do aluno mesmo com subscription past_due'),
('billing', 'days_to_keep_access_past_due', '0', 'Dias para manter acesso após past_due (0 = indefinido)'),
('billing', 'auto_retry_next_cycle', 'true', 'Retentar automaticamente no próximo ciclo de cobrança'),
('billing', 'manual_cancel_only', 'true', 'SOMENTE cancelamento manual pelo admin (sem auto-cancel)'),

-- Emails
('emails', 'send_payment_failed', 'true', 'Enviar email quando pagamento falhar'),
('emails', 'send_payment_recovered', 'true', 'Enviar email quando pagamento for recuperado'),
('emails', 'send_welcome', 'true', 'Enviar email de boas-vindas ao novo aluno'),
('emails', 'send_receipt', 'true', 'Enviar recibo por email após cada pagamento'),
('emails', 'send_retry_notifications', 'true', 'Enviar notificações em cada retentativa'),
('emails', 'sender_name', '"Paggo"', 'Nome do remetente nos emails'),
('emails', 'sender_email', '"noreply@paggo.me"', 'Email do remetente'),
('emails', 'support_email', '"suporte@paggo.me"', 'Email de suporte (mostrado nos emails)'),

-- Checkout
('checkout', 'hide_postal_code', 'true', 'Esconder campo de CEP no checkout (produto digital)'),
('checkout', 'allow_coupons', 'true', 'Permitir cupons de desconto no checkout'),
('checkout', 'success_redirect_url', '""', 'URL de redirecionamento após compra (vazio = página padrão)'),
('checkout', 'custom_branding_color', '"#6C5CE7"', 'Cor principal do checkout'),
('checkout', 'terms_url', '""', 'URL dos termos de uso (exibido no checkout)'),

-- Área de Membros
('members', 'allow_self_cancel', 'false', 'Permitir que aluno cancele própria assinatura (RECOMENDADO: false)'),
('members', 'magic_link_ttl_minutes', '15', 'Tempo de validade do magic link em minutos'),
('members', 'max_concurrent_sessions', '3', 'Sessões simultâneas máximas por aluno (anti-compartilhamento)'),
('members', 'alert_on_ip_sharing', 'true', 'Alertar admin se aluno acessar de 3+ IPs em 24h'),
('members', 'show_progress_bar', 'true', 'Mostrar barra de progresso nas aulas'),

-- Integrações
('integrations', 'external_webhook_url', '""', 'URL para enviar webhooks externos (ex: ConstruX Analytics)'),
('integrations', 'external_webhook_events', '["payment.approved","subscription.activated","subscription.cancelled"]', 'Eventos para enviar via webhook externo'),
('integrations', 'vimeo_domain_restriction', '"paggo.me"', 'Domínio permitido para embed de vídeos Vimeo');

17B. API de Configurações — src/routes/settings.js

// Rotas protegidas por authMiddleware (somente admin/merchant)

// GET /api/admin/settings
//   → Retorna todas as settings agrupadas por categoria
//   → Exclui campos sensíveis (webhook_secret mostra apenas últimos 4 chars)

// GET /api/admin/settings/:category
//   → Retorna settings de uma categoria específica

// PUT /api/admin/settings/:category/:key
//   → Atualiza um setting individual
//   → Valida valor baseado no tipo esperado
//   → Registra no audit_log quem alterou e quando
//   → Para settings da Stripe: valida que o valor é válido antes de salvar

// POST /api/admin/settings/test-email
//   → Envia email de teste para o admin para verificar configuração SMTP

// POST /api/admin/settings/test-webhook
//   → Envia webhook de teste para URL externa configurada

17C. Service de Settings — src/services/settings.js

// Cache em memória para evitar queries ao banco a cada request
// Recarrega do banco a cada 5 minutos ou quando atualizado via API

class SettingsService {
  constructor() { this.cache = {}; this.lastLoad = 0; }

  async get(category, key, defaultValue = null) {
    // Retorna valor do setting ou defaultValue se não existir
  }

  async getCategory(category) {
    // Retorna todos os settings de uma categoria como objeto { key: value }
  }

  async set(category, key, value, updatedBy) {
    // Atualiza no banco + invalida cache
    // Registra no audit_log
  }

  async reload() {
    // Carrega todos os settings do banco para memória
  }
}

// Exporta instância singleton
module.exports = new SettingsService();

Uso nos outros serviços:

// Em qualquer lugar do código:
const settings = require('./services/settings');

// Exemplo no webhook handler:
const keepAccess = await settings.get('billing', 'keep_access_on_past_due', true);
const manualOnly = await settings.get('billing', 'manual_cancel_only', true);

// Exemplo no checkout:
const hidePostal = await settings.get('checkout', 'hide_postal_code', true);

// Exemplo na área de membros:
const allowSelfCancel = await settings.get('members', 'allow_self_cancel', false);

17D. Frontend — Painel de Configurações no Admin Dashboard

Adicionar seção “Configurações” no dashboard admin (public/app/index.html).

Layout: Tabs laterais por categoria, formulário à direita.

┌─────────────────────────────────────────────────────────┐
│  ⚙ Configurações Gerais                                │
├──────────────┬──────────────────────────────────────────┤
│              │                                          │
│  > Stripe    │  Stripe — Configuração de Pagamento      │
│  Cobrança    │                                          │
│  Emails      │  Modo: [live ▼]                          │
│  Checkout    │  Publishable Key: [pk_live_xxx...]       │
│  Membros     │  3D Secure: [Sempre ▼]                   │
│  Integrações │  Account Updater: [✓]                    │
│              │  Auto-cancelar: [✗] ⚠ RECOMENDADO: OFF  │
│              │                                          │
│              │  [Salvar Alterações]                      │
│              │                                          │
├──────────────┴──────────────────────────────────────────┤
│  > Cobrança                                            │
│                                                         │
│  Política de Cancelamento                               │
│  ┌─────────────────────────────────────────────────┐   │
│  │ ⚠ SOMENTE cancelamento manual pelo admin       │   │
│  │ Assinaturas NUNCA são canceladas automaticamente│   │
│  │ [✓] Ativado (Recomendado)                       │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
│  Retentativas por ciclo: [4]                            │
│  Dias entre retentativas: [1, 3, 7, 14]                │
│  Após falhar tudo: [Manter past_due ▼]                 │
│  Manter acesso em past_due: [✓]                        │
│  Retentar no próximo ciclo: [✓]                        │
│                                                         │
│  [Salvar Alterações]                                    │
└─────────────────────────────────────────────────────────┘

Campos com destaque especial:

  1. Auto-cancelar assinaturas — Toggle com aviso vermelho: “⚠ Se ativado, a Stripe pode cancelar assinaturas automaticamente após falhas. RECOMENDADO: DESATIVADO.”
  2. Cancelamento manual apenas — Toggle com destaque verde: “Somente o admin pode cancelar assinaturas manualmente. Nenhum processo automático cancela.”
  3. Manter acesso em past_due — Toggle: “Alunos mantêm acesso às aulas mesmo durante retentativas de pagamento.”
  4. Permitir aluno cancelar — Toggle com aviso: “Se ativado, aluno pode cancelar pelo portal. RECOMENDADO: DESATIVADO.”

17E. Sincronização com Stripe Dashboard

Ao salvar configurações da categoria “stripe” ou “billing”, a API deve validar e alertar se as settings do Stripe Dashboard estão diferentes:

// POST /api/admin/settings/sync-stripe
//   → Verifica configurações atuais na Stripe:
//     - Retry schedule
//     - Após retentativas falharem (deve ser "Leave past_due")
//     - Emails automáticos da Stripe (devem estar DESATIVADOS, Paggo envia)
//   → Retorna divergências encontradas para o admin corrigir manualmente no Stripe Dashboard
//   → Nota: algumas configs da Stripe NÃO são alteráveis via API,
//     apenas via Dashboard. Listar quais precisam ser configuradas manualmente.

Checklist de configuração manual na Stripe Dashboard: - [ ] Settings → Billing → Subscriptions → Smart retries: ON - [ ] Settings → Billing → Subscriptions → Retry schedule: 4 tentativas em 30 dias - [ ] Settings → Billing → Subscriptions → After all retries: “Leave the subscription past_due” - [ ] Settings → Billing → Subscriptions → Send emails: OFF (Paggo envia) - [ ] Settings → Billing → Customer portal: configurar quais ações o aluno pode fazer


Estimativa de Linhas v2.0

Fase Descrição Linhas Estimadas
11 Stripe Billing Nativo + Checkout + Seed Script ~400
12 Área de Membros — DB + Backend ~900
13 Área de Membros — Frontend + Vimeo + Magic Link ~1600
14 Gestão de Cursos — Admin CRUD ~800
15 Webhooks Avançados + Anti-Fraude + Access Logs ~500
16 Testes e Migração ~200
17 Configurações Gerais — Settings UI + API + Service ~600
TOTAL v2.0 ~5.000 linhas
TOTAL v1.0 + v2.0 ~11.400 linhas

Estrutura de Diretórios v2.0 (adições)

/home/glauko/checkout-paggo/
  ├── src/
  │   ├── routes/
  │   │   ├── members.js          ← Auth de alunos (login/registro/perfil)
  │   │   ├── member-courses.js   ← Cursos/aulas do aluno (portal)
  │   │   ├── admin-courses.js    ← CRUD de cursos/módulos/aulas (admin)
  │   │   └── settings.js         ← GET/PUT configurações gerais (admin)
  │   ├── services/
  │   │   ├── enrollment.js       ← grantAccess/revokeAccess/checkAccess
  │   │   └── settings.js         ← SettingsService (cache + CRUD de platform_settings)
  │   ├── middleware/
  │   │   ├── memberAuth.js       ← JWT auth do aluno (role='user')
  │   │   └── checkAccess.js      ← Verifica enrollment + subscription ativa
  │   └── jobs/
  │       └── (billing/dunning workers continuam para gateways não-Stripe)
  ├── public/
  │   └── members/
  │       └── index.html          ← Portal do aluno SPA (~1500 linhas)
  └── migrations/
      ├── 004_stripe_billing.sql  ← Colunas Stripe IDs
      ├── 005_member_area.sql     ← users, courses, modules, lessons, enrollments, lesson_progress
      ├── migrate_from_guru.js    ← Script de migração DigitalGuru → Paggo
      ├── 006_access_logs.sql     ← Tabela de logs de acesso (anti-fraude)
      └── 007_platform_settings.sql ← Configurações gerais da plataforma
  └── scripts/
      └── seed-stripe.js          ← Criar Products/Prices na Stripe

Notas Técnicas Críticas para Implementação

Estas notas vêm do blueprint original e são regras que o Claude Code DEVE seguir durante a implementação.

  1. Webhook raw body obrigatório — O endpoint /api/webhooks/stripe NÃO pode usar express.json() como middleware. Precisa receber o body RAW para validar a assinatura HMAC do Stripe. Usar express.raw({ type: 'application/json' }) exclusivamente nessa rota. O server.js atual já aplica express.json() globalmente — o webhook precisa ser montado ANTES ou usar override.

  2. Sempre retornar HTTP 200 no webhook — Mesmo se der erro interno no processamento. Se retornar 4xx/5xx, a Stripe re-envia o evento até 10 vezes em 3 dias, causando processamento duplicado e alertas.

  3. Idempotência obrigatória — Checar o event.id do Stripe na tabela webhook_events antes de processar. Se já processou, ignorar e retornar 200. Isso já existe no v1.0 mas precisa ser mantido no refactor.

  4. Dados do cartão NUNCA no servidor — Tudo tokenizado via Stripe.js/Elements no frontend. O servidor só recebe payment_method_id (pm_xxx). Isso elimina PCI-DSS compliance nível 1 (basta SAQ-A).

  5. hidePostalCode: true no Stripe Elements — Remove campo de CEP/endereço do formulário de cartão. Essencial para produto digital onde não se precisa de endereço.

  6. payment_behavior: 'default_incomplete' — Usar na criação da Subscription para poder tratar 3D Secure no frontend antes de ativar. Sem isso, a subscription pode ficar em estado inconsistente.

  7. Stripe Account Updater — Atualiza automaticamente cartões expirados quando o banco emite um novo. Não precisa implementar lógica para isso — é automático da Stripe.

  8. Vimeo domain restriction — Configurar no Vimeo para permitir embed APENAS no domínio paggo.me. Impede compartilhamento de links de vídeo.

  9. stripe.subscriptions.retrieve() dentro do webhook — Ao receber invoice.payment_succeeded, fazer retrieve da subscription na Stripe para obter dados frescos (current_period_start, current_period_end, status) ao invés de confiar apenas no payload do webhook.

  10. cancel_at_period_end — Quando aluno cancela, usar stripe.subscriptions.update(subId, { cancel_at_period_end: true }) ao invés de cancelar imediatamente. Isso mantém o acesso até o fim do período pago. A tabela subscriptions precisa do campo cancel_at_period_end BOOLEAN DEFAULT false.

  11. Stripe Customer Portal (opcional) — A Stripe oferece um portal pronto (stripe.billingPortal.sessions.create()) onde o cliente pode atualizar cartão, ver faturas, e cancelar assinatura. Pode ser usado como atalho ao invés de construir tudo custom no portal do aluno. O portal é configurável no Stripe Dashboard. Avaliar se vale usar para reduzir escopo da Fase 13.

  12. Não usar checkout.session.completed — Esse evento é do Stripe Checkout (hosted). Como o Paggo usa checkout customizado com Stripe Elements, o primeiro pagamento dispara invoice.payment_succeeded normalmente. O webhook handler deve tratar TODOS os pagamentos (1º e renovações) via invoice.payment_succeeded.

  13. NUNCA cancelar assinatura automaticamente — A plataforma opera com política de retry infinito. Quando pagamento falha, a Stripe retenta dentro do ciclo (Smart Retries). Se todas as retentativas falham, a subscription fica past_due (NÃO cancelled). No próximo ciclo de cobrança, a Stripe gera nova invoice e retenta. O acesso do aluno é mantido durante todo o processo. Somente o admin pode cancelar manualmente via dashboard. O webhook handler para customer.subscription.deleted deve verificar se foi ação manual antes de revogar acesso.

  14. Settings dinâmicos via platform_settings — Em vez de hardcodar comportamentos (retry count, emails, acesso durante past_due), usar o SettingsService para ler configurações do banco. Isso permite que o admin ajuste o comportamento da plataforma sem alterar código. O service usa cache em memória (5 min TTL) para performance.