# 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 ```yaml 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:** ```typescript // 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:** ```typescript 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:** ```typescript await this.paymentService.cancelSubscription( subscriptionId, userId ); // Se cancela al final del periodo actual ``` **Obtener suscripción activa:** ```typescript 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:** ```typescript @Controller('webhooks') export class WebhookController { constructor(private readonly paymentService: PaymentService) {} @Post('stripe') async handleStripeWebhook( @Headers('stripe-signature') signature: string, @Req() req: RawBodyRequest, ) { await this.paymentService.handleWebhook( signature, req.rawBody, // Importante: usar raw body ); return { received: true }; } } ``` --- ## NOTAS DE ADAPTACIÓN ### Variables a reemplazar ```typescript // 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`: ```typescript 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 ```sql -- 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 ```typescript // 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 { const req = context.switchToHttp().getRequest(); const subscription = await this.paymentService.getActiveSubscription(req.user.id); return !!subscription; } } ``` ### 2. Pago único de producto ```typescript @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 ```bash # 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" ```typescript // Solución: Verificar que el priceId existe en Stripe dashboard // Usar precios de test (price_test_...) en desarrollo ``` ### Error: "Webhook signature verification failed" ```typescript // 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" ```typescript // Solución: Ya manejado en getOrCreateCustomer() // Busca customer existente antes de crear ``` ### Suscripción queda en "incomplete" ```typescript // 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: 1. **Webhook signature verification**: Evita requests maliciosos 2. **Refresh token hashing**: Nunca guardar tokens planos 3. **Metadata validation**: Validar userId en webhooks 4. **API versioning**: Fijar versión de API Stripe 5. **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