template-saas/apps/backend/dist/modules/superadmin/superadmin.service.js
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

321 lines
12 KiB
JavaScript

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SuperadminService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const tenant_entity_1 = require("../tenants/entities/tenant.entity");
const user_entity_1 = require("../auth/entities/user.entity");
const subscription_entity_1 = require("../billing/entities/subscription.entity");
let SuperadminService = class SuperadminService {
constructor(tenantRepository, userRepository, subscriptionRepository) {
this.tenantRepository = tenantRepository;
this.userRepository = userRepository;
this.subscriptionRepository = subscriptionRepository;
}
async listTenants(query) {
const { page = 1, limit = 10, search, status, sortBy = 'created_at', sortOrder = 'DESC' } = query;
const where = {};
if (search) {
where.name = (0, typeorm_2.ILike)(`%${search}%`);
}
if (status) {
where.status = status;
}
const [tenants, total] = await this.tenantRepository.findAndCount({
where,
order: { [sortBy]: sortOrder },
skip: (page - 1) * limit,
take: limit,
});
const tenantsWithStats = await Promise.all(tenants.map(async (tenant) => {
const userCount = await this.userRepository.count({
where: { tenant_id: tenant.id },
});
const subscription = await this.subscriptionRepository.findOne({
where: { tenant_id: tenant.id },
relations: ['plan'],
});
return {
...tenant,
userCount,
subscription,
};
}));
return {
data: tenantsWithStats,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async getTenant(id) {
const tenant = await this.tenantRepository.findOne({
where: { id },
});
if (!tenant) {
throw new common_1.NotFoundException('Tenant not found');
}
const userCount = await this.userRepository.count({
where: { tenant_id: id },
});
const subscription = await this.subscriptionRepository.findOne({
where: { tenant_id: id },
relations: ['plan'],
});
return {
...tenant,
userCount,
subscription,
};
}
async createTenant(dto) {
const existingTenant = await this.tenantRepository.findOne({
where: { slug: dto.slug },
});
if (existingTenant) {
throw new common_1.ConflictException('A tenant with this slug already exists');
}
const tenant = this.tenantRepository.create({
name: dto.name,
slug: dto.slug,
domain: dto.domain,
logo_url: dto.logo_url,
plan_id: dto.plan_id,
status: dto.status || 'trial',
trial_ends_at: dto.status === 'trial' ? new Date(Date.now() + 14 * 24 * 60 * 60 * 1000) : null,
});
return this.tenantRepository.save(tenant);
}
async updateTenant(id, dto) {
const tenant = await this.tenantRepository.findOne({
where: { id },
});
if (!tenant) {
throw new common_1.NotFoundException('Tenant not found');
}
Object.assign(tenant, dto);
return this.tenantRepository.save(tenant);
}
async updateTenantStatus(id, dto) {
const tenant = await this.tenantRepository.findOne({
where: { id },
});
if (!tenant) {
throw new common_1.NotFoundException('Tenant not found');
}
tenant.status = dto.status;
if (dto.reason) {
tenant.metadata = {
...tenant.metadata,
statusChangeReason: dto.reason,
statusChangedAt: new Date().toISOString(),
};
}
return this.tenantRepository.save(tenant);
}
async deleteTenant(id) {
const tenant = await this.tenantRepository.findOne({
where: { id },
});
if (!tenant) {
throw new common_1.NotFoundException('Tenant not found');
}
const userCount = await this.userRepository.count({
where: { tenant_id: id },
});
if (userCount > 0) {
throw new common_1.BadRequestException('Cannot delete tenant with active users. Please remove all users first or suspend the tenant.');
}
await this.tenantRepository.remove(tenant);
}
async getTenantUsers(tenantId, page = 1, limit = 10) {
const tenant = await this.tenantRepository.findOne({
where: { id: tenantId },
});
if (!tenant) {
throw new common_1.NotFoundException('Tenant not found');
}
const [users, total] = await this.userRepository.findAndCount({
where: { tenant_id: tenantId },
order: { created_at: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return {
data: users,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async getDashboardStats() {
const [totalTenants, activeTenants, trialTenants, suspendedTenants, totalUsers, newTenantsThisMonth,] = await Promise.all([
this.tenantRepository.count(),
this.tenantRepository.count({ where: { status: 'active' } }),
this.tenantRepository.count({ where: { status: 'trial' } }),
this.tenantRepository.count({ where: { status: 'suspended' } }),
this.userRepository.count(),
this.tenantRepository
.createQueryBuilder('tenant')
.where('tenant.created_at >= :startOfMonth', {
startOfMonth: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
})
.getCount(),
]);
return {
totalTenants,
activeTenants,
trialTenants,
suspendedTenants,
totalUsers,
newTenantsThisMonth,
};
}
async getTenantGrowth(months = 12) {
const result = [];
const now = new Date();
for (let i = months - 1; i >= 0; i--) {
const startDate = new Date(now.getFullYear(), now.getMonth() - i, 1);
const endDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59);
const count = await this.tenantRepository
.createQueryBuilder('tenant')
.where('tenant.created_at >= :startDate', { startDate })
.andWhere('tenant.created_at <= :endDate', { endDate })
.getCount();
result.push({
month: startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
count,
});
}
return result;
}
async getUserGrowth(months = 12) {
const result = [];
const now = new Date();
for (let i = months - 1; i >= 0; i--) {
const startDate = new Date(now.getFullYear(), now.getMonth() - i, 1);
const endDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59);
const count = await this.userRepository
.createQueryBuilder('user')
.where('user.created_at >= :startDate', { startDate })
.andWhere('user.created_at <= :endDate', { endDate })
.getCount();
result.push({
month: startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
count,
});
}
return result;
}
async getPlanDistribution() {
const subscriptions = await this.subscriptionRepository
.createQueryBuilder('sub')
.leftJoinAndSelect('sub.plan', 'plan')
.where('sub.status = :status', { status: 'active' })
.getMany();
const planCounts = {};
let total = 0;
for (const sub of subscriptions) {
const planName = sub.plan?.display_name || sub.plan?.name || 'Unknown';
planCounts[planName] = (planCounts[planName] || 0) + 1;
total++;
}
const tenantsWithSubscription = subscriptions.map(s => s.tenant_id);
const freeCount = await this.tenantRepository
.createQueryBuilder('tenant')
.where('tenant.id NOT IN (:...ids)', {
ids: tenantsWithSubscription.length > 0 ? tenantsWithSubscription : ['00000000-0000-0000-0000-000000000000']
})
.getCount();
if (freeCount > 0) {
planCounts['Free'] = freeCount;
total += freeCount;
}
return Object.entries(planCounts).map(([plan, count]) => ({
plan,
count,
percentage: total > 0 ? Math.round((count / total) * 100) : 0,
}));
}
async getStatusDistribution() {
const statuses = ['active', 'trial', 'suspended', 'canceled'];
const total = await this.tenantRepository.count();
const result = await Promise.all(statuses.map(async (status) => {
const count = await this.tenantRepository.count({ where: { status } });
return {
status: status.charAt(0).toUpperCase() + status.slice(1),
count,
percentage: total > 0 ? Math.round((count / total) * 100) : 0,
};
}));
return result;
}
async getTopTenants(limit = 10) {
const tenants = await this.tenantRepository.find({
order: { created_at: 'ASC' },
take: 100,
});
const tenantsWithCounts = await Promise.all(tenants.map(async (tenant) => {
const userCount = await this.userRepository.count({
where: { tenant_id: tenant.id },
});
const subscription = await this.subscriptionRepository.findOne({
where: { tenant_id: tenant.id },
relations: ['plan'],
});
return {
id: tenant.id,
name: tenant.name,
slug: tenant.slug,
userCount,
status: tenant.status,
planName: subscription?.plan?.display_name || 'Free',
};
}));
return tenantsWithCounts
.sort((a, b) => b.userCount - a.userCount)
.slice(0, limit);
}
async getMetricsSummary() {
const [tenantGrowth, userGrowth, planDistribution, statusDistribution, topTenants] = await Promise.all([
this.getTenantGrowth(12),
this.getUserGrowth(12),
this.getPlanDistribution(),
this.getStatusDistribution(),
this.getTopTenants(10),
]);
return {
tenantGrowth,
userGrowth,
planDistribution,
statusDistribution,
topTenants,
};
}
};
exports.SuperadminService = SuperadminService;
exports.SuperadminService = SuperadminService = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_1.InjectRepository)(tenant_entity_1.Tenant)),
__param(1, (0, typeorm_1.InjectRepository)(user_entity_1.User)),
__param(2, (0, typeorm_1.InjectRepository)(subscription_entity_1.Subscription)),
__metadata("design:paramtypes", [typeorm_2.Repository,
typeorm_2.Repository,
typeorm_2.Repository])
], SuperadminService);
//# sourceMappingURL=superadmin.service.js.map