miinventario-v2/apps/backend/src/modules/payments/payments.service.ts
rckrdmrd 1a53b5c4d3 [MIINVENTARIO] feat: Initial commit - Sistema de inventario con análisis de video IA
- Backend NestJS con módulos de autenticación, inventario, créditos
- Frontend React con dashboard y componentes UI
- Base de datos PostgreSQL con migraciones
- Tests E2E configurados
- Configuración de Docker y deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 02:25:48 -06:00

417 lines
12 KiB
TypeScript

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<Payment>,
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;
}
}