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