/** * PAYMENT SERVICE - REFERENCE IMPLEMENTATION * * @description Servicio de pagos con integración Stripe. * Soporta pagos únicos, suscripciones y webhooks. * * @usage * ```typescript * // Crear checkout session * const session = await this.paymentService.createCheckoutSession({ * userId: req.user.id, * priceId: 'price_xxx', * successUrl: 'https://app.com/success', * cancelUrl: 'https://app.com/cancel', * }); * // Redirigir a session.url * ``` * * @origin Patrón base para proyectos con pagos */ import { Injectable, Logger, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import Stripe from 'stripe'; // Adaptar imports según proyecto // import { Payment, Subscription, Customer } from '../entities'; @Injectable() export class PaymentService { private readonly logger = new Logger(PaymentService.name); private readonly stripe: Stripe; constructor( private readonly configService: ConfigService, @InjectRepository(Payment, 'payments') private readonly paymentRepo: Repository, @InjectRepository(Subscription, 'payments') private readonly subscriptionRepo: Repository, @InjectRepository(Customer, 'payments') private readonly customerRepo: Repository, ) { this.stripe = new Stripe(this.configService.get('STRIPE_SECRET_KEY'), { apiVersion: '2023-10-16', }); } /** * Crear o recuperar customer de Stripe */ async getOrCreateCustomer(userId: string, email: string): Promise { // Buscar customer existente const existing = await this.customerRepo.findOne({ where: { user_id: userId } }); if (existing) return existing.stripe_customer_id; // Crear en Stripe const stripeCustomer = await this.stripe.customers.create({ email, metadata: { userId }, }); // Guardar en BD const customer = this.customerRepo.create({ user_id: userId, stripe_customer_id: stripeCustomer.id, email, }); await this.customerRepo.save(customer); return stripeCustomer.id; } /** * Crear sesión de checkout */ async createCheckoutSession(dto: CreateCheckoutDto): Promise<{ sessionId: string; url: string }> { const customerId = await this.getOrCreateCustomer(dto.userId, dto.email); const session = await this.stripe.checkout.sessions.create({ customer: customerId, payment_method_types: ['card'], line_items: [ { price: dto.priceId, quantity: dto.quantity || 1, }, ], mode: dto.mode || 'payment', success_url: dto.successUrl, cancel_url: dto.cancelUrl, metadata: { userId: dto.userId, ...dto.metadata, }, }); return { sessionId: session.id, url: session.url, }; } /** * Crear suscripción directa */ async createSubscription(dto: CreateSubscriptionDto): Promise { const customerId = await this.getOrCreateCustomer(dto.userId, dto.email); const stripeSubscription = await this.stripe.subscriptions.create({ customer: customerId, items: [{ price: dto.priceId }], payment_behavior: 'default_incomplete', expand: ['latest_invoice.payment_intent'], }); const subscription = this.subscriptionRepo.create({ user_id: dto.userId, stripe_subscription_id: stripeSubscription.id, stripe_customer_id: customerId, status: stripeSubscription.status, current_period_start: new Date(stripeSubscription.current_period_start * 1000), current_period_end: new Date(stripeSubscription.current_period_end * 1000), }); return this.subscriptionRepo.save(subscription); } /** * Cancelar suscripción */ async cancelSubscription(subscriptionId: string, userId: string): Promise { const subscription = await this.subscriptionRepo.findOne({ where: { id: subscriptionId, user_id: userId }, }); if (!subscription) { throw new BadRequestException('Subscription not found'); } await this.stripe.subscriptions.update(subscription.stripe_subscription_id, { cancel_at_period_end: true, }); subscription.cancel_at_period_end = true; await this.subscriptionRepo.save(subscription); } /** * Obtener suscripción activa del usuario */ async getActiveSubscription(userId: string): Promise { return this.subscriptionRepo.findOne({ where: { user_id: userId, status: 'active' }, }); } /** * Procesar webhook de Stripe */ async handleWebhook(signature: string, payload: Buffer): Promise { const webhookSecret = this.configService.get('STRIPE_WEBHOOK_SECRET'); let event: Stripe.Event; try { event = this.stripe.webhooks.constructEvent(payload, signature, webhookSecret); } catch (err) { this.logger.error(`Webhook signature verification failed: ${err.message}`); throw new BadRequestException('Invalid webhook signature'); } this.logger.log(`Processing webhook event: ${event.type}`); switch (event.type) { case 'checkout.session.completed': await this.handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session); break; case 'invoice.paid': await this.handleInvoicePaid(event.data.object as Stripe.Invoice); break; case 'invoice.payment_failed': await this.handlePaymentFailed(event.data.object as Stripe.Invoice); break; case 'customer.subscription.updated': case 'customer.subscription.deleted': await this.handleSubscriptionUpdate(event.data.object as Stripe.Subscription); break; default: this.logger.debug(`Unhandled webhook event: ${event.type}`); } } // ============ WEBHOOK HANDLERS ============ private async handleCheckoutCompleted(session: Stripe.Checkout.Session) { const userId = session.metadata?.userId; if (!userId) return; const payment = this.paymentRepo.create({ user_id: userId, stripe_payment_id: session.payment_intent as string, amount: session.amount_total, currency: session.currency, status: 'completed', }); await this.paymentRepo.save(payment); this.logger.log(`Payment completed for user: ${userId}`); } private async handleInvoicePaid(invoice: Stripe.Invoice) { const subscriptionId = invoice.subscription as string; if (!subscriptionId) return; await this.subscriptionRepo.update( { stripe_subscription_id: subscriptionId }, { status: 'active' }, ); } private async handlePaymentFailed(invoice: Stripe.Invoice) { const subscriptionId = invoice.subscription as string; if (!subscriptionId) return; await this.subscriptionRepo.update( { stripe_subscription_id: subscriptionId }, { status: 'past_due' }, ); } private async handleSubscriptionUpdate(stripeSubscription: Stripe.Subscription) { await this.subscriptionRepo.update( { stripe_subscription_id: stripeSubscription.id }, { status: stripeSubscription.status, current_period_end: new Date(stripeSubscription.current_period_end * 1000), cancel_at_period_end: stripeSubscription.cancel_at_period_end, }, ); } } // ============ TIPOS ============ interface CreateCheckoutDto { userId: string; email: string; priceId: string; quantity?: number; mode?: 'payment' | 'subscription'; successUrl: string; cancelUrl: string; metadata?: Record; } interface CreateSubscriptionDto { userId: string; email: string; priceId: string; } interface Payment { id: string; user_id: string; stripe_payment_id: string; amount: number; currency: string; status: string; } interface Subscription { id: string; user_id: string; stripe_subscription_id: string; stripe_customer_id: string; status: string; current_period_start: Date; current_period_end: Date; cancel_at_period_end?: boolean; } interface Customer { id: string; user_id: string; stripe_customer_id: string; email: string; }