New projects created: - michangarrito (marketplace mobile) - template-saas (SaaS template) - clinica-dental (dental ERP) - clinica-veterinaria (veterinary ERP) Architecture updates: - Move catalog from core/ to shared/ - Add MCP servers structure and templates - Add git management scripts - Update SUBREPOSITORIOS.md with 15 new repos - Update .gitignore for new projects Repository infrastructure: - 4 main repositories - 11 subrepositorios - Gitea remotes configured 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
11 KiB
11 KiB
PAYMENTS - REFERENCE IMPLEMENTATION
Versión: 1.0.0 | Fecha: 2025-12-12 | Nivel: Catalog (3)
ÍNDICE DE ARCHIVOS
| Archivo | Descripción | LOC | Patrón Principal |
|---|---|---|---|
payment.service.reference.ts |
Servicio completo de pagos con Stripe | 296 | Checkout, Subscriptions, Webhooks |
CÓMO USAR
Flujo de adopción recomendado
PASO_1: Configurar cuenta Stripe
- Crear cuenta en https://stripe.com
- Obtener API keys (test + production)
- Configurar webhook endpoint
- Crear productos y precios en dashboard
PASO_2: Instalar dependencias
- npm install stripe
- npm install @nestjs/config (si no está instalado)
PASO_3: Configurar variables de entorno
- STRIPE_SECRET_KEY: sk_test_... (o sk_live_...)
- STRIPE_WEBHOOK_SECRET: whsec_... (del dashboard)
- STRIPE_API_VERSION: 2023-10-16 (o más reciente)
PASO_4: Copiar y adaptar archivo
- Copiar payment.service.reference.ts → payment.service.ts
- Ajustar imports de entidades (Payment, Subscription, Customer)
- Configurar conexión a BD correcta (@InjectRepository)
PASO_5: Implementar entidades requeridas
- Customer: user_id, stripe_customer_id, email
- Payment: user_id, stripe_payment_id, amount, currency, status
- Subscription: user_id, stripe_subscription_id, status, periods
PASO_6: Configurar webhook endpoint
- Crear endpoint POST /webhooks/stripe
- Usar raw body (no JSON parsed)
- Verificar firma con stripe.webhooks.constructEvent()
PASO_7: Validar integración
- Probar checkout session en modo test
- Simular webhooks desde Stripe CLI
- Verificar pagos en BD y dashboard Stripe
PATRONES IMPLEMENTADOS
1. Checkout Session (Pago único o suscripción)
Flujo:
1. Frontend solicita checkout session
2. Backend crea session en Stripe
3. Backend retorna URL de checkout
4. Usuario completa pago en Stripe
5. Stripe envía webhook checkout.session.completed
6. Backend guarda payment en BD
Ejemplo de uso:
// En tu controller
@Post('create-checkout')
async createCheckout(@Body() dto: CreateCheckoutDto, @Req() req) {
const session = await this.paymentService.createCheckoutSession({
userId: req.user.id,
email: req.user.email,
priceId: dto.priceId, // Del dashboard Stripe
successUrl: `${process.env.APP_URL}/payment/success`,
cancelUrl: `${process.env.APP_URL}/payment/cancel`,
mode: 'payment', // o 'subscription'
});
return { checkoutUrl: session.url };
}
2. Suscripciones
Crear suscripción:
const subscription = await this.paymentService.createSubscription({
userId: user.id,
email: user.email,
priceId: 'price_monthly_premium',
});
// Suscripción queda en estado "incomplete"
// Usuario debe completar pago (requiere payment method)
Cancelar suscripción:
await this.paymentService.cancelSubscription(
subscriptionId,
userId
);
// Se cancela al final del periodo actual
Obtener suscripción activa:
const subscription = await this.paymentService.getActiveSubscription(userId);
if (subscription) {
// Usuario tiene plan premium
}
3. Webhooks
Eventos soportados:
| Evento Stripe | Handler | Acción |
|---|---|---|
checkout.session.completed |
handleCheckoutCompleted |
Guarda Payment en BD |
invoice.paid |
handleInvoicePaid |
Actualiza Subscription a 'active' |
invoice.payment_failed |
handlePaymentFailed |
Marca Subscription como 'past_due' |
customer.subscription.updated |
handleSubscriptionUpdate |
Sincroniza estado de subscription |
customer.subscription.deleted |
handleSubscriptionUpdate |
Sincroniza cancelación |
Configurar webhook controller:
@Controller('webhooks')
export class WebhookController {
constructor(private readonly paymentService: PaymentService) {}
@Post('stripe')
async handleStripeWebhook(
@Headers('stripe-signature') signature: string,
@Req() req: RawBodyRequest<Request>,
) {
await this.paymentService.handleWebhook(
signature,
req.rawBody, // Importante: usar raw body
);
return { received: true };
}
}
NOTAS DE ADAPTACIÓN
Variables a reemplazar
// Entidades
Payment → Tu entidad de pagos
Subscription → Tu entidad de suscripciones
Customer → Tu entidad de clientes Stripe
// DTOs
CreateCheckoutDto → Tu DTO de checkout
CreateSubscriptionDto → Tu DTO de suscripción
Configurar raw body para webhooks
En main.ts:
const app = await NestFactory.create(AppModule, {
rawBody: true, // Habilitar raw body
});
// O usar middleware específico:
app.use('/webhooks/stripe', express.raw({ type: 'application/json' }));
Esquema de base de datos
-- Tabla customers
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID UNIQUE REFERENCES users(id) ON DELETE CASCADE,
stripe_customer_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Tabla payments
CREATE TABLE payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
stripe_payment_id VARCHAR(255) UNIQUE NOT NULL,
amount INTEGER NOT NULL, -- En centavos (ej: 1000 = $10.00)
currency VARCHAR(3) DEFAULT 'usd',
status VARCHAR(50) NOT NULL, -- completed, pending, failed
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Tabla subscriptions
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
stripe_subscription_id VARCHAR(255) UNIQUE NOT NULL,
stripe_customer_id VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL, -- active, past_due, canceled, incomplete
current_period_start TIMESTAMP NOT NULL,
current_period_end TIMESTAMP NOT NULL,
cancel_at_period_end BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_subscriptions_user_status ON subscriptions(user_id, status);
CASOS DE USO COMUNES
1. Implementar plan de suscripción mensual
// 1. Crear precio en Stripe dashboard:
// - Producto: "Premium Plan"
// - Precio: $9.99/mes
// - ID: price_premium_monthly
// 2. Endpoint de suscripción
@Post('subscribe')
async subscribe(@Req() req) {
const subscription = await this.paymentService.createSubscription({
userId: req.user.id,
email: req.user.email,
priceId: 'price_premium_monthly',
});
return {
subscriptionId: subscription.id,
status: subscription.status,
};
}
// 3. Verificar estado en guards/middlewares
@Injectable()
export class PremiumGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const subscription = await this.paymentService.getActiveSubscription(req.user.id);
return !!subscription;
}
}
2. Pago único de producto
@Post('buy-course')
async buyCourse(@Body() dto: BuyCourseDto, @Req() req) {
const session = await this.paymentService.createCheckoutSession({
userId: req.user.id,
email: req.user.email,
priceId: dto.coursePriceId,
mode: 'payment', // Pago único
successUrl: `${process.env.APP_URL}/courses/${dto.courseId}/access`,
cancelUrl: `${process.env.APP_URL}/courses/${dto.courseId}`,
metadata: {
courseId: dto.courseId,
type: 'course_purchase',
},
});
return { checkoutUrl: session.url };
}
// En webhook handler personalizado:
private async handleCheckoutCompleted(session: Stripe.Checkout.Session) {
if (session.metadata?.type === 'course_purchase') {
await this.coursesService.grantAccess(
session.metadata.userId,
session.metadata.courseId,
);
}
}
3. Pruebas locales con Stripe CLI
# Instalar Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Escuchar webhooks (forward a localhost)
stripe listen --forward-to http://localhost:3000/webhooks/stripe
# Copiar webhook secret que muestra (whsec_...)
# Actualizar .env: STRIPE_WEBHOOK_SECRET=whsec_...
# Simular eventos
stripe trigger checkout.session.completed
stripe trigger invoice.paid
MANEJO DE ERRORES COMUNES
Error: "No such price"
// Solución: Verificar que el priceId existe en Stripe dashboard
// Usar precios de test (price_test_...) en desarrollo
Error: "Webhook signature verification failed"
// Solución: Asegurar que se usa raw body
// Verificar que STRIPE_WEBHOOK_SECRET es correcto
// En desarrollo, usar Stripe CLI para obtener secret local
Error: "Customer already exists"
// Solución: Ya manejado en getOrCreateCustomer()
// Busca customer existente antes de crear
Suscripción queda en "incomplete"
// Solución: Usuario debe completar payment method
// Usar checkout.session para suscripciones (más fácil)
// O implementar setup intent para agregar payment method
CHECKLIST DE VALIDACIÓN
Antes de marcar como completo:
- Build pasa:
npm run build - Lint pasa:
npm run lint - Variables de entorno configuradas (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET)
- Entidades creadas en BD (Customer, Payment, Subscription)
- Checkout session funciona (modo test)
- Webhook endpoint configurado con raw body
- Webhooks verifican firma correctamente
- Eventos se guardan en BD
- Probado con Stripe CLI local
- Dashboard Stripe muestra eventos correctamente
REFERENCIAS CRUZADAS
Dependencias en @CATALOG
- auth: Para autenticar usuarios en endpoints de pago
- notifications: Notificar usuario sobre pagos/suscripciones
- multi-tenancy: Pagos por tenant (empresas)
Relacionado con SIMCO
- @OP_BACKEND: Operaciones de backend (crear service, webhooks)
- @SIMCO-REUTILIZAR: Este catálogo es candidato para reutilización
- @SIMCO-VALIDAR: Validar con Stripe CLI antes de deploy
Documentación adicional
- Stripe API: https://stripe.com/docs/api
- Checkout Sessions: https://stripe.com/docs/payments/checkout
- Webhooks: https://stripe.com/docs/webhooks
- Testing: https://stripe.com/docs/testing
SEGURIDAD
Mejores prácticas implementadas:
- Webhook signature verification: Evita requests maliciosos
- Refresh token hashing: Nunca guardar tokens planos
- Metadata validation: Validar userId en webhooks
- API versioning: Fijar versión de API Stripe
- Idempotency: Webhooks pueden repetirse (manejar duplicados)
Recomendaciones adicionales:
- Usar HTTPS en producción (requerido por Stripe)
- Limitar rate de endpoints de pago
- Logging detallado de eventos Stripe
- Monitorear webhooks fallidos en dashboard
- Implementar retry logic para webhooks críticos
Mantenido por: Core Team | Origen: Patrón base para integraciones Stripe