UNPKG

skailan-core

Version:

Servicio de autenticación y multitenancy para Skailan.

303 lines 10.8 kB
import { EPaycoConfigService, } from "../config/epayco.config.js"; import { EPaycoSignatureService } from "./EPaycoSignatureService.js"; import { TrialService } from "./TrialService.js"; import Joi from "joi"; export class PaymentService { static instance; prisma; configService; signatureService; trialService; constructor(prisma) { this.prisma = prisma; this.configService = EPaycoConfigService.getInstance(); this.signatureService = EPaycoSignatureService.getInstance(); this.trialService = TrialService.getInstance(prisma); } static getInstance(prisma) { if (!PaymentService.instance) { PaymentService.instance = new PaymentService(prisma); } return PaymentService.instance; } /** * Inicia un proceso de pago */ async startPayment(request) { // Validar entrada await this.validatePaymentRequest(request); // Verificar que la organización existe const organization = await this.prisma.organization.findUnique({ where: { id: request.organizationId }, }); if (!organization) { throw new Error("Organización no encontrada"); } // Verificar que el plan existe y está activo const plan = await this.prisma.plan.findUnique({ where: { id: request.planId }, }); if (!plan || !plan.isActive) { throw new Error("Plan no encontrado o inactivo"); } // Verificar que el usuario existe const user = await this.prisma.user.findUnique({ where: { id: request.userId }, }); if (!user) { throw new Error("Usuario no encontrado"); } // Generar referencia única const reference = this.generatePaymentReference(request.organizationId); // Crear registro de pago const payment = await this.prisma.payment.create({ data: { organizationId: request.organizationId, userId: request.userId, planId: request.planId, amount: plan.price, status: "PENDING", reference, currency: this.configService.getConfig().currency, metadata: request.metadata, }, }); // Generar URL de checkout const checkoutUrl = this.generateCheckoutUrl(payment, plan); // Calcular fecha de expiración (24 horas) const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); return { checkoutUrl, reference, amount: plan.price, planName: plan.name, expiresAt, }; } /** * Confirma un pago recibido de ePayco */ async confirmPayment(request) { const { body, expectedAmount } = request; // Validar la respuesta de ePayco const validation = this.signatureService.validatePaymentResponse(body, expectedAmount); if (!validation.isValid) { console.error("Payment validation failed:", validation.errors); throw new Error(`Validación de pago fallida: ${validation.errors.join(", ")}`); } // Buscar el pago por referencia const payment = await this.prisma.payment.findUnique({ where: { reference: body.x_invoice }, include: { organization: true, plan: true, }, }); if (!payment) { throw new Error("Pago no encontrado"); } // Verificar que no se haya procesado ya if (payment.status === "CONFIRMED") { return { message: "Pago ya confirmado", paymentId: payment.id }; } // Actualizar el pago con la información de ePayco await this.prisma.payment.update({ where: { id: payment.id }, data: { status: "CONFIRMED", paymentDate: new Date(), epaycoRef: body.x_ref_payco, epaycoTransactionId: body.x_transaction_id, epaycoSignature: body.x_signature, paymentMethod: body.x_franchise, }, }); // Actualizar la organización await this.prisma.organization.update({ where: { id: payment.organizationId }, data: { isBlocked: false, paymentStatus: "CONFIRMED", }, }); // Convertir trial a suscripción pagada await this.trialService.convertTrialToPaid(payment.organizationId, payment.planId); // Crear o actualizar suscripción await this.createOrUpdateSubscription(payment); return { message: "Pago confirmado exitosamente", paymentId: payment.id, organizationId: payment.organizationId, }; } /** * Lista los pagos de una organización */ async listPayments(organizationId, limit = 50, offset = 0) { return await this.prisma.payment.findMany({ where: { organizationId }, include: { plan: true, user: { select: { id: true, name: true, email: true, }, }, }, orderBy: { createdAt: "desc" }, take: limit, skip: offset, }); } /** * Obtiene estadísticas de pagos */ async getPaymentStats(organizationId) { const [totalPayments, confirmedPayments, pendingPayments, failedPayments] = await Promise.all([ this.prisma.payment.count({ where: { organizationId } }), this.prisma.payment.count({ where: { organizationId, status: "CONFIRMED" }, }), this.prisma.payment.count({ where: { organizationId, status: "PENDING" }, }), this.prisma.payment.count({ where: { organizationId, status: "FAILED" }, }), ]); const totalAmount = await this.prisma.payment.aggregate({ where: { organizationId, status: "CONFIRMED" }, _sum: { amount: true }, }); return { totalPayments, confirmedPayments, pendingPayments, failedPayments, totalAmount: totalAmount._sum.amount || 0, }; } /** * Reembolsa un pago */ async refundPayment(paymentId, reason) { const payment = await this.prisma.payment.findUnique({ where: { id: paymentId }, }); if (!payment) { throw new Error("Pago no encontrado"); } if (payment.status !== "CONFIRMED") { throw new Error("Solo se pueden reembolsar pagos confirmados"); } // Aquí se integraría con la API de ePayco para procesar el reembolso // Por ahora solo actualizamos el estado await this.prisma.payment.update({ where: { id: paymentId }, data: { status: "REFUNDED", metadata: { ...(payment.metadata && typeof payment.metadata === "object" ? payment.metadata : {}), refundReason: reason, refundedAt: new Date(), }, }, }); } /** * Valida la solicitud de pago */ async validatePaymentRequest(request) { const schema = Joi.object({ organizationId: Joi.string().uuid().required(), planId: Joi.string().uuid().required(), userId: Joi.string().uuid().required(), metadata: Joi.object().optional(), }); const { error } = schema.validate(request); if (error) { throw new Error(`Datos de pago inválidos: ${error.message}`); } } /** * Genera una referencia única para el pago */ generatePaymentReference(organizationId) { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `PAY-${organizationId.substring(0, 8)}-${timestamp}-${random}`.toUpperCase(); } /** * Genera la URL de checkout de ePayco */ generateCheckoutUrl(payment, plan) { const config = this.configService.getConfig(); const params = { p_key: config.pKey, public_key: config.publicKey, amount: plan.price, name: plan.name, description: plan.description, invoice: payment.reference, currency: config.currency, tax: config.taxRate.toString(), tax_base: plan.price.toString(), country: config.country, lang: config.language, external: "false", confirmation: config.confirmationUrl, response: config.responseUrl, extra1: payment.organizationId, extra2: payment.userId, extra3: plan.id, }; const queryString = new URLSearchParams(params).toString(); return `${config.baseUrl}?${queryString}`; } /** * Crea o actualiza la suscripción después de un pago exitoso */ async createOrUpdateSubscription(payment) { const existingSubscription = await this.prisma.subscription.findFirst({ where: { organizationId: payment.organizationId, status: "ACTIVE", }, }); if (existingSubscription) { // Actualizar suscripción existente await this.prisma.subscription.update({ where: { id: existingSubscription.id }, data: { planId: payment.planId, status: "ACTIVE", startDate: new Date(), paymentInfo: { lastPaymentId: payment.id, lastPaymentDate: payment.paymentDate, }, }, }); } else { // Crear nueva suscripción await this.prisma.subscription.create({ data: { organizationId: payment.organizationId, planId: payment.planId, status: "ACTIVE", startDate: new Date(), paymentInfo: { lastPaymentId: payment.id, lastPaymentDate: payment.paymentDate, }, }, }); } } } //# sourceMappingURL=PaymentService.js.map