Files
HoruxDespachosNuevo/apps/api/src/controllers/tenants.controller.ts

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);
}
}