skailan-core
Version:
Servicio de autenticación y multitenancy para Skailan.
305 lines • 9.87 kB
JavaScript
import moment from 'moment';
import cron from 'node-cron';
export class TrialService {
static instance;
prisma;
constructor(prisma) {
this.prisma = prisma;
this.initializeCronJobs();
}
static getInstance(prisma) {
if (!TrialService.instance) {
TrialService.instance = new TrialService(prisma);
}
return TrialService.instance;
}
/**
* Inicializa los trabajos cron para monitorear trials
*/
initializeCronJobs() {
// Verificar trials expirados diariamente a las 2 AM
cron.schedule('0 2 * * *', () => {
this.checkExpiredTrials();
});
// Enviar advertencias 3 días antes del vencimiento
cron.schedule('0 10 * * *', () => {
this.sendTrialWarnings();
});
// Verificar organizaciones bloqueadas por trial expirado
cron.schedule('0 */6 * * *', () => {
this.blockExpiredTrials();
});
}
/**
* Inicia el trial para una organización
*/
async startTrial(organizationId, trialDays = 14) {
const startDate = new Date();
const endDate = moment().add(trialDays, 'days').toDate();
await this.prisma.organization.update({
where: { id: organizationId },
data: {
trialStart: startDate,
trialEnd: endDate,
isTrialActive: true,
paymentStatus: 'TRIAL',
trialDays,
},
});
await this.logTrialAction(organizationId, 'STARTED', {
trialDays,
startDate,
endDate,
});
}
/**
* Obtiene el estado actual del trial
*/
async getTrialStatus(organizationId) {
const organization = await this.prisma.organization.findUnique({
where: { id: organizationId },
});
if (!organization || !organization.trialEnd) {
return {
isActive: false,
daysRemaining: 0,
endDate: null,
isExpired: true,
isWarningSent: false,
canExtend: false,
};
}
const now = new Date();
const endDate = organization.trialEnd;
const isExpired = now > endDate;
const daysRemaining = Math.max(0, moment(endDate).diff(now, 'days'));
// Verificar si ya se envió una advertencia
const warningLog = await this.prisma.trialLog.findFirst({
where: {
organizationId,
action: 'WARNING_SENT',
},
orderBy: { createdAt: 'desc' },
});
const isWarningSent = !!warningLog;
return {
isActive: organization.isTrialActive && !isExpired,
daysRemaining,
endDate,
isExpired,
isWarningSent,
canExtend: daysRemaining <= 3 && !isExpired,
};
}
/**
* Extiende el trial de una organización
*/
async extendTrial(request) {
const { organizationId, daysToExtend, reason, requestedBy } = request;
const organization = await this.prisma.organization.findUnique({
where: { id: organizationId },
});
if (!organization) {
throw new Error('Organización no encontrada');
}
if (!organization.trialEnd) {
throw new Error('La organización no tiene un trial activo');
}
const newEndDate = moment(organization.trialEnd).add(daysToExtend, 'days').toDate();
await this.prisma.organization.update({
where: { id: organizationId },
data: {
trialEnd: newEndDate,
trialDays: organization.trialDays + daysToExtend,
},
});
await this.logTrialAction(organizationId, 'EXTENDED', {
daysExtended: daysToExtend,
newEndDate,
reason,
requestedBy,
});
}
/**
* Convierte un trial a suscripción pagada
*/
async convertTrialToPaid(organizationId, planId) {
await this.prisma.organization.update({
where: { id: organizationId },
data: {
isTrialActive: false,
trialEnd: null,
paymentStatus: 'CONFIRMED',
},
});
await this.logTrialAction(organizationId, 'CONVERTED', {
planId,
convertedAt: new Date(),
});
}
/**
* Verifica y bloquea organizaciones con trial expirado
*/
async blockExpiredTrials() {
const expiredOrganizations = await this.prisma.organization.findMany({
where: {
trialEnd: {
lt: new Date(),
},
isTrialActive: true,
isBlocked: false,
},
});
for (const org of expiredOrganizations) {
await this.prisma.organization.update({
where: { id: org.id },
data: {
isBlocked: true,
isTrialActive: false,
paymentStatus: 'FAILED',
},
});
await this.logTrialAction(org.id, 'EXPIRED', {
expiredAt: new Date(),
});
}
}
/**
* Envía advertencias a organizaciones próximas a expirar
*/
async sendTrialWarnings() {
const threeDaysFromNow = moment().add(3, 'days').toDate();
const twoDaysFromNow = moment().add(2, 'days').toDate();
const organizationsToWarn = await this.prisma.organization.findMany({
where: {
trialEnd: {
gte: twoDaysFromNow,
lte: threeDaysFromNow,
},
isTrialActive: true,
isBlocked: false,
},
});
for (const org of organizationsToWarn) {
// Verificar si ya se envió una advertencia
const existingWarning = await this.prisma.trialLog.findFirst({
where: {
organizationId: org.id,
action: 'WARNING_SENT',
},
});
if (!existingWarning) {
await this.logTrialAction(org.id, 'WARNING_SENT', {
warningSentAt: new Date(),
daysRemaining: moment(org.trialEnd).diff(new Date(), 'days'),
});
// Aquí se podría integrar con un servicio de notificaciones
console.log(`Advertencia enviada a organización ${org.name} (${org.id})`);
}
}
}
/**
* Verifica trials expirados y los marca como expirados
*/
async checkExpiredTrials() {
const expiredOrganizations = await this.prisma.organization.findMany({
where: {
trialEnd: {
lt: new Date(),
},
isTrialActive: true,
},
});
for (const org of expiredOrganizations) {
await this.prisma.organization.update({
where: { id: org.id },
data: {
isTrialActive: false,
},
});
await this.logTrialAction(org.id, 'EXPIRED', {
expiredAt: new Date(),
});
}
}
/**
* Registra una acción del trial
*/
async logTrialAction(organizationId, action, details) {
await this.prisma.trialLog.create({
data: {
organizationId,
action,
details,
},
});
}
/**
* Obtiene el historial de acciones del trial
*/
async getTrialHistory(organizationId) {
return await this.prisma.trialLog.findMany({
where: { organizationId },
orderBy: { createdAt: 'desc' },
});
}
/**
* Verifica si una organización puede acceder al sistema
*/
async canAccess(organizationId) {
const organization = await this.prisma.organization.findUnique({
where: { id: organizationId },
});
if (!organization) {
return false;
}
// Si está bloqueada, no puede acceder
if (organization.isBlocked) {
return false;
}
// Si tiene trial activo, puede acceder
if (organization.isTrialActive && organization.trialEnd && new Date() <= organization.trialEnd) {
return true;
}
// Si tiene suscripción activa, puede acceder
const activeSubscription = await this.prisma.subscription.findFirst({
where: {
organizationId,
status: 'ACTIVE',
},
});
return !!activeSubscription;
}
/**
* Obtiene estadísticas de trials
*/
async getTrialStats() {
const totalOrganizations = await this.prisma.organization.count();
const activeTrials = await this.prisma.organization.count({
where: {
isTrialActive: true,
isBlocked: false,
},
});
const expiredTrials = await this.prisma.organization.count({
where: {
isTrialActive: false,
trialEnd: {
lt: new Date(),
},
},
});
const blockedOrganizations = await this.prisma.organization.count({
where: {
isBlocked: true,
},
});
return {
totalOrganizations,
activeTrials,
expiredTrials,
blockedOrganizations,
};
}
}
//# sourceMappingURL=TrialService.js.map