164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
import { Request, Response, NextFunction } from 'express';
|
|
import { z } from 'zod';
|
|
import * as tenantsService from '../services/tenants.service.js';
|
|
import { AppError } from '../middlewares/error.middleware.js';
|
|
import { isGlobalAdmin } from '../utils/global-admin.js';
|
|
import { hasAnyPlatformRole } from '../utils/platform-admin.js';
|
|
import { isOwnerSomewhere } from '../utils/memberships.js';
|
|
|
|
async function requireGlobalAdmin(req: Request): Promise<void> {
|
|
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId))) {
|
|
throw new AppError(403, 'Solo el administrador global puede gestionar clientes');
|
|
}
|
|
}
|
|
|
|
export async function getAllTenants(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
// Admin global, TI y Vendedor pueden ver el listado completo de tenants.
|
|
// Vendedor lo necesita para enviar invitaciones de trial.
|
|
const canList = await hasAnyPlatformRole(req.user!.userId, 'platform_admin', 'platform_ti', 'platform_sales');
|
|
if (!canList) {
|
|
// Evita 403 en consola del frontend cuando componentes sin-gate hacen polling
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
return res.json([]);
|
|
}
|
|
|
|
const tenants = await tenantsService.getAllTenants();
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
res.json(tenants);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function getTenant(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
|
|
if (!isAdmin) {
|
|
return res.status(404).json({ message: 'Cliente no encontrado' });
|
|
}
|
|
|
|
const tenant = await tenantsService.getTenantById(String(req.params.id));
|
|
if (!tenant) {
|
|
throw new AppError(404, 'Cliente no encontrado');
|
|
}
|
|
|
|
res.json(tenant);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function createTenant(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
await requireGlobalAdmin(req);
|
|
|
|
const { nombre, rfc, plan, adminEmail, adminNombre, amount, firstPaymentDueAt, verticalProfile, codigoPostal } = req.body;
|
|
|
|
if (!nombre || !rfc || !adminEmail || !adminNombre) {
|
|
throw new AppError(400, 'Nombre, RFC, adminEmail y adminNombre son requeridos');
|
|
}
|
|
|
|
const result = await tenantsService.createTenant({
|
|
nombre,
|
|
rfc,
|
|
plan,
|
|
adminEmail,
|
|
adminNombre,
|
|
amount: amount || 0,
|
|
firstPaymentDueAt: firstPaymentDueAt || null,
|
|
verticalProfile: verticalProfile || 'CONTABLE',
|
|
codigoPostal,
|
|
});
|
|
|
|
res.status(201).json(result);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function updateTenant(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
await requireGlobalAdmin(req);
|
|
|
|
const id = String(req.params.id);
|
|
const { nombre, rfc, plan, active, amount, firstPaymentDueAt } = req.body;
|
|
|
|
const tenant = await tenantsService.updateTenant(id, {
|
|
nombre,
|
|
rfc,
|
|
plan,
|
|
active,
|
|
amount,
|
|
firstPaymentDueAt: firstPaymentDueAt || null,
|
|
});
|
|
|
|
res.json(tenant);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function deleteTenant(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
await requireGlobalAdmin(req);
|
|
|
|
await tenantsService.deleteTenant(String(req.params.id));
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Self-serve (multi-tenant memberships)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Lista detallada de empresas del caller con estado de suscripción. Usado por
|
|
* `/mis-empresas`. A diferencia de `/auth/me`, incluye datos de subscription
|
|
* (status, currentPeriodEnd, pendingPlan, etc.).
|
|
*/
|
|
export async function getMyTenants(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const data = await tenantsService.getMyTenantsDetailed(req.user!.userId);
|
|
res.json(data);
|
|
} catch (error) { next(error); }
|
|
}
|
|
|
|
const addTenantSchema = z.object({
|
|
nombre: z.string().min(2, 'Nombre de empresa requerido'),
|
|
rfc: z.string().min(12).max(13, 'RFC inválido'),
|
|
plan: z.enum(['mi_empresa', 'mi_empresa_plus', 'business_control', 'business_cloud']).optional(),
|
|
});
|
|
|
|
/**
|
|
* Agrega una empresa (tenant nuevo) bajo el user autenticado. El caller se
|
|
* vuelve owner automáticamente vía TenantMembership.
|
|
*/
|
|
export async function addMyTenant(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const data = addTenantSchema.parse(req.body);
|
|
// Gate: solo users que son owner en al menos un tenant pueden agregar
|
|
// un RFC adicional. Un contador invitado a una empresa ajena no puede.
|
|
if (!(await isOwnerSomewhere(req.user!.userId))) {
|
|
throw new AppError(403, 'Solo los dueños pueden registrar empresas adicionales.');
|
|
}
|
|
const result = await tenantsService.addTenantToOwner({
|
|
userId: req.user!.userId,
|
|
nombre: data.nombre,
|
|
rfc: data.rfc,
|
|
plan: data.plan,
|
|
});
|
|
res.status(201).json({ tenant: result.tenant });
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
|
|
if (error instanceof Error && error.message.includes('RFC')) {
|
|
return next(new AppError(400, error.message));
|
|
}
|
|
next(error);
|
|
}
|
|
}
|