skailan-core
Version:
Servicio de autenticación y multitenancy para Skailan.
303 lines • 10.8 kB
JavaScript
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