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.
| 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 |
| 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 |
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.
/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
[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]
| 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 |
| Nodemailer (SMTP) | Transacional (recibos, dunning, boas-vindas) |
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);- 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)
/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
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()
}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)
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%
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%
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
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)
// 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' };
}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
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
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"
}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
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
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
GET /api/coupons — Listar cupons
POST /api/coupons — Criar cupom
PUT /api/coupons/:id — Atualizar
DELETE /api/coupons/:id — Desativar
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
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
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
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
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
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)
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
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] │
└─────────────────────────────────────────────┘
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.
// 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)URL: https://paggo.me/admin
POST /api/auth/login → cookie JWT → redirect /admin/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)
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 →]
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 →]
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)
/health endpoint para monitoramentoPara $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.
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)
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)
#!/bin/bash
cd /home/glauko/checkout
git pull origin main
npm install --production
npx knex migrate:latest
pm2 reload paggo- PostgreSQL: pg_dump diário → S3/Cloudflare R2
- Redis: snapshot a cada 6h (RDB)
- .env: backup manual seguro
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)
[ ] 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)
| 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 |
{
"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"
}
}| 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 |
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
[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
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
| 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 |
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.
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
}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
}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);src/services/checkout.jsO 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 }
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.# 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 restrictionOs workers billingWorker.js e dunningWorker.js continuam ativos para outros gateways, mas para Stripe a cobrança recorrente é 100% gerenciada pela Stripe via webhooks.
REGRA FUNDAMENTAL: Assinaturas NUNCA são canceladas automaticamente pela plataforma ou pela Stripe. Apenas o admin pode cancelar manualmente através do painel Paggo.
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)
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)
// 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)| 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.
Criar as tabelas e APIs para área de membros: cursos, módulos, aulas, matrículas (enrollments), e progresso do aluno.
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);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
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.userJWT_SECRET mas o campo role diferenciarole='user' e vice-versaLista 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 |
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"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: '...' }Frontend SPA para o aluno acessar seus cursos, assistir aulas, e acompanhar progresso.
public/members/index.htmlURL: https://paggo.me/members/ (hash router: #/login, #/dashboard, #/course/:slug, #/lesson/:id)
Telas:
#/login)
#/dashboard)
#/course/:slug)
#/lesson/:id)
#/profile)
# Adicionar ao /etc/nginx/sites-enabled/paggo:
location /members/ {
alias /var/www/paggo/members/;
try_files $uri $uri/ /members/index.html;
}
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
Adicionar ao dashboard admin as telas de gestão de cursos, módulos e aulas.
Adicionar no sidebar do dashboard (public/dashboard/index.html): - Cursos → Lista de cursos com CRUD - Alunos → Lista de alunos matriculados com status de assinatura
#/courses)
#/courses/:id)
#/students)
#/students/:id)
Refatorar src/routes/webhooks.js para processar todos os eventos de lifecycle da Stripe e automatizar o controle de acesso da área de membros.
// 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;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
payment_behavior: 'default_incomplete' já faz issoLog de acesso do aluno — Registrar IP, horário, e aula acessada em tabela access_logs:
charge.dispute.created chega, coletar automaticamente:
stripe.disputes.update(dispute.id, { evidence: { ... } })Rate limiting no checkout — Já existe (10 req/60s) mas reforçar para evitar card testing
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
Testar o fluxo completo end-to-end e migrar os alunos existentes da DigitalGuru para o Paggo.
4242 4242 4242 4242 → verificar que cria Customer + Subscription na Stripe + enrollment no Paggo4000 0027 6000 3184 → verificar fluxo de autenticaçãoinvoice.payment_succeeded → enrollment mantido4000 0000 0000 0341 → verificar webhook invoice.payment_failed → email de aviso + retentativascustomer.subscription.deleted → enrollment revogado → aluno sem acessopast_due e NUNCA muda para cancelled → verificar que novo ciclo gera nova invoice → verificar emails de notificação em cada retryactivemigrations/migrate_from_guru.js):
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
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.
platform_settings — Migration 007_platform_settings.sqlCREATE 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');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 configuradasrc/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();// 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);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:
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
| 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 |
/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
Estas notas vêm do blueprint original e são regras que o Claude Code DEVE seguir durante a implementação.
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.
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.
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.
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).
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.
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.
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.
Vimeo domain restriction — Configurar no Vimeo para permitir embed APENAS no domínio paggo.me. Impede compartilhamento de links de vídeo.
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.
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.
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.
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.
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.
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.