skailan-core
Version:
Servicio de autenticación y multitenancy para Skailan.
356 lines • 11.7 kB
JavaScript
import Joi from "joi";
export class ProductService {
static instance;
prisma;
constructor(prisma) {
this.prisma = prisma;
}
static getInstance(prisma) {
if (!ProductService.instance) {
ProductService.instance = new ProductService(prisma);
}
return ProductService.instance;
}
/**
* Crea un nuevo plan
*/
async createPlan(request) {
await this.validateCreatePlanRequest(request);
const plan = await this.prisma.plan.create({
data: {
name: request.name,
description: request.description,
price: request.price,
type: request.type,
interval: request.interval,
trialDays: request.trialDays,
maxUsers: request.maxUsers,
maxProjects: request.maxProjects,
features: request.features,
isActive: request.isActive ?? true,
},
include: {
planFeatures: {
include: {
feature: true,
},
},
},
});
return plan;
}
/**
* Actualiza un plan existente
*/
async updatePlan(planId, request) {
await this.validateUpdatePlanRequest(request);
const plan = await this.prisma.plan.update({
where: { id: planId },
data: {
...(request.name && { name: request.name }),
...(request.description && { description: request.description }),
...(request.price !== undefined && { price: request.price }),
...(request.type && { type: request.type }),
...(request.interval && { interval: request.interval }),
...(request.trialDays !== undefined && {
trialDays: request.trialDays,
}),
...(request.maxUsers !== undefined && { maxUsers: request.maxUsers }),
...(request.maxProjects !== undefined && {
maxProjects: request.maxProjects,
}),
...(request.features && { features: request.features }),
...(request.isActive !== undefined && { isActive: request.isActive }),
},
include: {
planFeatures: {
include: {
feature: true,
},
},
},
});
return plan;
}
/**
* Obtiene un plan por ID
*/
async getPlan(planId) {
const plan = await this.prisma.plan.findUnique({
where: { id: planId },
include: {
planFeatures: {
include: {
feature: true,
},
},
},
});
return plan;
}
/**
* Lista todos los planes activos
*/
async listActivePlans() {
const plans = await this.prisma.plan.findMany({
where: { isActive: true },
include: {
planFeatures: {
include: {
feature: true,
},
},
},
orderBy: { price: "asc" },
});
return plans;
}
/**
* Lista todos los planes (activos e inactivos)
*/
async listAllPlans() {
const plans = await this.prisma.plan.findMany({
include: {
planFeatures: {
include: {
feature: true,
},
},
},
orderBy: { createdAt: "desc" },
});
return plans;
}
/**
* Elimina un plan (soft delete)
*/
async deletePlan(planId) {
// Verificar que no hay suscripciones activas con este plan
const activeSubscriptions = await this.prisma.subscription.count({
where: {
planId,
status: "ACTIVE",
},
});
if (activeSubscriptions > 0) {
throw new Error("No se puede eliminar un plan con suscripciones activas");
}
await this.prisma.plan.update({
where: { id: planId },
data: { isActive: false },
});
}
/**
* Crea una nueva característica
*/
async createFeature(request) {
await this.validateFeatureRequest(request);
const feature = await this.prisma.feature.create({
data: {
name: request.name,
description: request.description,
code: request.code,
isActive: request.isActive ?? true,
},
});
return feature;
}
/**
* Asigna características a un plan
*/
async assignFeaturesToPlan(planId, featureIds) {
// Verificar que el plan existe
const plan = await this.prisma.plan.findUnique({
where: { id: planId },
});
if (!plan) {
throw new Error("Plan no encontrado");
}
// Eliminar asignaciones existentes
await this.prisma.planFeature.deleteMany({
where: { planId },
});
// Crear nuevas asignaciones
if (featureIds.length > 0) {
await this.prisma.planFeature.createMany({
data: featureIds.map((featureId) => ({
planId,
featureId,
})),
});
}
}
/**
* Obtiene las características de un plan
*/
async getPlanFeatures(planId) {
const planFeatures = await this.prisma.planFeature.findMany({
where: { planId },
include: {
feature: true,
},
});
return planFeatures.map((pf) => pf.feature);
}
/**
* Lista todas las características activas
*/
async listActiveFeatures() {
return await this.prisma.feature.findMany({
where: { isActive: true },
orderBy: { name: "asc" },
});
}
/**
* Obtiene estadísticas de planes
*/
async getPlanStats() {
const [totalPlans, activePlans, totalFeatures, activeFeatures] = await Promise.all([
this.prisma.plan.count(),
this.prisma.plan.count({ where: { isActive: true } }),
this.prisma.feature.count(),
this.prisma.feature.count({ where: { isActive: true } }),
]);
const planTypes = await this.prisma.plan.groupBy({
by: ["type"],
_count: { type: true },
});
return {
totalPlans,
activePlans,
totalFeatures,
activeFeatures,
planTypes,
};
}
/**
* Verifica si un plan tiene una característica específica
*/
async planHasFeature(planId, featureCode) {
const planFeature = await this.prisma.planFeature.findFirst({
where: {
planId,
feature: {
code: featureCode,
isActive: true,
},
},
include: {
feature: true,
},
});
return !!planFeature;
}
/**
* Obtiene el plan gratuito por defecto
*/
async getFreePlan() {
const plan = await this.prisma.plan.findFirst({
where: {
type: "FREE",
isActive: true,
},
include: {
planFeatures: {
include: {
feature: true,
},
},
},
});
return plan;
}
/**
* Valida la solicitud de creación de plan
*/
async validateCreatePlanRequest(request) {
const schema = Joi.object({
name: Joi.string().min(1).max(100).required(),
description: Joi.string().min(1).max(500).required(),
price: Joi.number().min(0).required(),
type: Joi.string()
.valid("FREE", "TRIAL", "BASIC", "PRO", "ENTERPRISE")
.required(),
interval: Joi.string()
.valid("MONTHLY", "YEARLY", "WEEKLY", "DAILY")
.required(),
trialDays: Joi.number().min(0).max(365).required(),
maxUsers: Joi.number().min(1).optional(),
maxProjects: Joi.number().min(1).optional(),
features: Joi.object().optional(),
isActive: Joi.boolean().optional(),
});
const { error } = schema.validate(request);
if (error) {
throw new Error(`Datos de plan inválidos: ${error.message}`);
}
// Verificar que no existe un plan con el mismo nombre
const existingPlan = await this.prisma.plan.findFirst({
where: { name: request.name },
});
if (existingPlan) {
throw new Error("Ya existe un plan con ese nombre");
}
}
/**
* Valida la solicitud de actualización de plan
*/
async validateUpdatePlanRequest(request) {
const schema = Joi.object({
name: Joi.string().min(1).max(100).optional(),
description: Joi.string().min(1).max(500).optional(),
price: Joi.number().min(0).optional(),
type: Joi.string()
.valid("FREE", "TRIAL", "BASIC", "PRO", "ENTERPRISE")
.optional(),
interval: Joi.string()
.valid("MONTHLY", "YEARLY", "WEEKLY", "DAILY")
.optional(),
trialDays: Joi.number().min(0).max(365).optional(),
maxUsers: Joi.number().min(1).optional(),
maxProjects: Joi.number().min(1).optional(),
features: Joi.object().optional(),
isActive: Joi.boolean().optional(),
});
const { error } = schema.validate(request);
if (error) {
throw new Error(`Datos de plan inválidos: ${error.message}`);
}
if (request.name) {
// Verificar que no existe otro plan con el mismo nombre
const existingPlan = await this.prisma.plan.findFirst({
where: {
name: request.name,
// Excluir el plan actual
id: { not: request.name }, // Esto no es correcto, necesitaríamos el ID del plan
},
});
if (existingPlan) {
throw new Error("Ya existe un plan con ese nombre");
}
}
}
/**
* Valida la solicitud de característica
*/
async validateFeatureRequest(request) {
const schema = Joi.object({
name: Joi.string().min(1).max(100).required(),
description: Joi.string().min(1).max(500).required(),
code: Joi.string().min(1).max(50).required(),
isActive: Joi.boolean().optional(),
});
const { error } = schema.validate(request);
if (error) {
throw new Error(`Datos de característica inválidos: ${error.message}`);
}
// Verificar que no existe una característica con el mismo código
const existingFeature = await this.prisma.feature.findFirst({
where: { code: request.code },
});
if (existingFeature) {
throw new Error("Ya existe una característica con ese código");
}
}
}
//# sourceMappingURL=ProductService.js.map