import { Injectable, BadRequestException, NotFoundException, Logger, Inject, forwardRef, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ConfigService } from '@nestjs/config'; import Stripe from 'stripe'; import { Payment, PaymentStatus, PaymentMethod, } from './entities/payment.entity'; import { CreatePaymentDto } from './dto/create-payment.dto'; import { CreditsService } from '../credits/credits.service'; import { TransactionType } from '../credits/entities/credit-transaction.entity'; import { UsersService } from '../users/users.service'; import { NotificationsService } from '../notifications/notifications.service'; @Injectable() export class PaymentsService { private readonly logger = new Logger(PaymentsService.name); private stripe: Stripe; constructor( @InjectRepository(Payment) private readonly paymentsRepository: Repository, private readonly creditsService: CreditsService, private readonly usersService: UsersService, private readonly configService: ConfigService, @Inject(forwardRef(() => NotificationsService)) private readonly notificationsService: NotificationsService, ) { const stripeKey = this.configService.get('STRIPE_SECRET_KEY'); if (stripeKey && stripeKey !== 'sk_test_your_stripe_key') { this.stripe = new Stripe(stripeKey, { apiVersion: '2023-10-16' }); } } async createPayment(userId: string, dto: CreatePaymentDto) { // Get package const pkg = await this.creditsService.getPackageById(dto.packageId); // Get or create Stripe customer const user = await this.usersService.findById(userId); if (!user) { throw new BadRequestException('Usuario no encontrado'); } let customerId = user.stripeCustomerId; if (!customerId && this.stripe) { const customer = await this.stripe.customers.create({ phone: user.phone || undefined, name: user.name || undefined, metadata: { userId }, }); customerId = customer.id; await this.usersService.update(userId, { stripeCustomerId: customerId }); } // Create payment record const payment = this.paymentsRepository.create({ userId, packageId: pkg.id, amountMXN: Number(pkg.priceMXN), creditsGranted: pkg.credits, method: dto.method, status: PaymentStatus.PENDING, provider: 'stripe', }); await this.paymentsRepository.save(payment); try { switch (dto.method) { case PaymentMethod.CARD: return this.processCardPayment(payment, customerId, dto.paymentMethodId); case PaymentMethod.OXXO: return this.processOxxoPayment(payment, customerId); case PaymentMethod.SEVEN_ELEVEN: return this.process7ElevenPayment(payment); default: throw new BadRequestException('Metodo de pago no soportado'); } } catch (error) { payment.status = PaymentStatus.FAILED; payment.errorMessage = error.message; await this.paymentsRepository.save(payment); throw error; } } private async processCardPayment( payment: Payment, customerId: string, paymentMethodId?: string, ) { if (!this.stripe) { // Development mode - simulate success return this.simulatePaymentSuccess(payment); } if (!paymentMethodId) { // Create PaymentIntent for new card const paymentIntent = await this.stripe.paymentIntents.create({ amount: Math.round(payment.amountMXN * 100), currency: 'mxn', customer: customerId, metadata: { paymentId: payment.id }, }); payment.externalId = paymentIntent.id; await this.paymentsRepository.save(payment); return { paymentId: payment.id, clientSecret: paymentIntent.client_secret, status: 'requires_payment_method', }; } // Charge with existing payment method const paymentIntent = await this.stripe.paymentIntents.create({ amount: Math.round(payment.amountMXN * 100), currency: 'mxn', customer: customerId, payment_method: paymentMethodId, confirm: true, metadata: { paymentId: payment.id }, }); payment.externalId = paymentIntent.id; if (paymentIntent.status === 'succeeded') { await this.completePayment(payment); return { paymentId: payment.id, status: 'completed', creditsGranted: payment.creditsGranted, }; } payment.status = PaymentStatus.PROCESSING; await this.paymentsRepository.save(payment); return { paymentId: payment.id, status: paymentIntent.status, }; } private async processOxxoPayment(payment: Payment, customerId: string) { if (!this.stripe) { return this.simulateOxxoVoucher(payment); } // Create OXXO payment intent const paymentIntent = await this.stripe.paymentIntents.create({ amount: Math.round(payment.amountMXN * 100), currency: 'mxn', customer: customerId, payment_method_types: ['oxxo'], metadata: { paymentId: payment.id }, }); // Confirm with OXXO const confirmedIntent = await this.stripe.paymentIntents.confirm( paymentIntent.id, { payment_method_data: { type: 'oxxo', billing_details: { name: 'Cliente MiInventario', email: 'cliente@miinventario.com', }, }, }, ); payment.externalId = paymentIntent.id; payment.status = PaymentStatus.PENDING; payment.expiresAt = new Date(Date.now() + 72 * 60 * 60 * 1000); // 72 hours // Get OXXO voucher details const nextAction = confirmedIntent.next_action; if (nextAction?.oxxo_display_details) { payment.voucherCode = nextAction.oxxo_display_details.number || undefined; payment.voucherUrl = nextAction.oxxo_display_details.hosted_voucher_url || undefined; } await this.paymentsRepository.save(payment); return { paymentId: payment.id, status: 'pending', method: 'oxxo', voucherCode: payment.voucherCode, voucherUrl: payment.voucherUrl, expiresAt: payment.expiresAt, amountMXN: payment.amountMXN, }; } private async process7ElevenPayment(payment: Payment) { // TODO: Implement Conekta 7-Eleven integration // For now, return simulated voucher return this.simulate7ElevenVoucher(payment); } private async simulatePaymentSuccess(payment: Payment) { this.logger.warn('Development mode: Simulating payment success'); payment.externalId = `sim_${Date.now()}`; await this.completePayment(payment); return { paymentId: payment.id, status: 'completed', creditsGranted: payment.creditsGranted, simulated: true, }; } private async simulateOxxoVoucher(payment: Payment) { this.logger.warn('Development mode: Simulating OXXO voucher'); payment.externalId = `sim_oxxo_${Date.now()}`; payment.voucherCode = Math.random().toString().slice(2, 16); payment.voucherUrl = `https://example.com/voucher/${payment.id}`; payment.expiresAt = new Date(Date.now() + 72 * 60 * 60 * 1000); await this.paymentsRepository.save(payment); return { paymentId: payment.id, status: 'pending', method: 'oxxo', voucherCode: payment.voucherCode, voucherUrl: payment.voucherUrl, expiresAt: payment.expiresAt, amountMXN: payment.amountMXN, simulated: true, }; } private async simulate7ElevenVoucher(payment: Payment) { this.logger.warn('Development mode: Simulating 7-Eleven voucher'); payment.externalId = `sim_7eleven_${Date.now()}`; payment.voucherCode = Math.random().toString().slice(2, 14); payment.expiresAt = new Date(Date.now() + 48 * 60 * 60 * 1000); await this.paymentsRepository.save(payment); return { paymentId: payment.id, status: 'pending', method: '7eleven', voucherCode: payment.voucherCode, expiresAt: payment.expiresAt, amountMXN: payment.amountMXN, simulated: true, }; } async handleStripeWebhook(payload: Buffer, signature: string) { const webhookSecret = this.configService.get('STRIPE_WEBHOOK_SECRET'); if (!this.stripe || !webhookSecret) { this.logger.warn('Stripe webhook not configured'); return { received: true, processed: false }; } 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('Webhook signature verification failed'); } this.logger.log(`Processing Stripe webhook: ${event.type}`); switch (event.type) { case 'payment_intent.succeeded': await this.handlePaymentSuccess(event.data.object as Stripe.PaymentIntent); break; case 'payment_intent.payment_failed': await this.handlePaymentFailed(event.data.object as Stripe.PaymentIntent); break; case 'payment_intent.processing': await this.handlePaymentProcessing(event.data.object as Stripe.PaymentIntent); break; case 'charge.succeeded': // Additional confirmation for OXXO/cash payments this.logger.log(`Charge succeeded: ${(event.data.object as Stripe.Charge).id}`); break; case 'charge.pending': // OXXO payment is pending (customer needs to pay at store) this.logger.log(`Charge pending: ${(event.data.object as Stripe.Charge).id}`); break; default: this.logger.log(`Unhandled event type: ${event.type}`); } return { received: true, processed: true, eventType: event.type }; } private async handlePaymentProcessing(paymentIntent: Stripe.PaymentIntent) { const paymentId = paymentIntent.metadata.paymentId; if (!paymentId) return; await this.paymentsRepository.update(paymentId, { status: PaymentStatus.PROCESSING, }); this.logger.log(`Payment ${paymentId} is now processing`); } private async handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) { const paymentId = paymentIntent.metadata.paymentId; if (!paymentId) return; const payment = await this.paymentsRepository.findOne({ where: { id: paymentId }, }); if (payment && payment.status !== PaymentStatus.COMPLETED) { await this.completePayment(payment); } } private async handlePaymentFailed(paymentIntent: Stripe.PaymentIntent) { const paymentId = paymentIntent.metadata.paymentId; if (!paymentId) return; const payment = await this.paymentsRepository.findOne({ where: { id: paymentId }, }); if (payment) { await this.paymentsRepository.update(paymentId, { status: PaymentStatus.FAILED, errorMessage: paymentIntent.last_payment_error?.message, }); // Send notification try { await this.notificationsService.notifyPaymentFailed(payment.userId, paymentId); } catch (error) { this.logger.error(`Failed to send payment failed notification: ${error.message}`); } } } private async completePayment(payment: Payment) { payment.status = PaymentStatus.COMPLETED; payment.completedAt = new Date(); await this.paymentsRepository.save(payment); // Grant credits await this.creditsService.addCredits( payment.userId, payment.creditsGranted, TransactionType.PURCHASE, `Compra de ${payment.creditsGranted} creditos`, payment.id, 'payment', ); // Send notification try { await this.notificationsService.notifyPaymentComplete( payment.userId, payment.id, payment.creditsGranted, ); } catch (error) { this.logger.error(`Failed to send payment notification: ${error.message}`); } this.logger.log( `Payment ${payment.id} completed. Granted ${payment.creditsGranted} credits to user ${payment.userId}`, ); } async getPaymentHistory(userId: string, page = 1, limit = 20) { const [payments, total] = await this.paymentsRepository.findAndCount({ where: { userId }, order: { createdAt: 'DESC' }, skip: (page - 1) * limit, take: limit, }); return { payments, total }; } async getPaymentById(paymentId: string, userId: string) { const payment = await this.paymentsRepository.findOne({ where: { id: paymentId, userId }, }); if (!payment) { throw new NotFoundException('Pago no encontrado'); } return payment; } }