import type { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import { signupDespacho } from '../services/despacho.service.js'; import { AppError } from '../middlewares/error.middleware.js'; import { prisma } from '../config/database.js'; import { getPlanPrice } from '../services/payment/subscription.service.js'; const signupSchema = z.object({ despacho: z.object({ nombre: z.string().min(2, 'Nombre del despacho requerido'), regimenFiscal: z.string().optional(), codigoPostal: z.string().regex(/^\d{5}$/, 'Código postal inválido').optional(), verticalProfile: z.enum(['CONTABLE', 'JURIDICO', 'ARQUITECTURA']), // plan y frequency ya no se escogen en el registro — todos empiezan con trial genérico. // Se mantienen opcionales para compatibilidad backward con clientes antiguos. plan: z.enum(['trial', 'mi_empresa', 'mi_empresa_plus', 'business_control', 'business_cloud']).optional(), frequency: z.enum(['monthly', 'annual']).optional(), }), owner: z.object({ nombre: z.string().min(2, 'Nombre del owner requerido'), email: z.string().email('Email inválido'), password: z.string().min(10, 'La contraseña debe tener al menos 10 caracteres'), }), }); export async function getMyPlan(req: Request, res: Response, next: NextFunction) { try { const tenantId = req.user!.tenantId; const tenant = await prisma.tenant.findUnique({ where: { id: tenantId }, select: { dbMode: true, trialEndsAt: true, verticalProfile: true, plan: true }, }); if (!tenant) { return next(new AppError(404, 'Tenant no encontrado')); } const now = new Date(); const isTrialActive = tenant.trialEndsAt ? tenant.trialEndsAt > now : false; // Mapea según trialEndsAt + tenant.plan (no dbMode). dbMode era proxy // antes de la introducción de Mi Empresa / Mi Empresa+ — para esos // planes, dbMode también es MANAGED y reportar `business_cloud` daba // mapeo equivocado. tenant.plan es la fuente de verdad post-migración // 20260426073942 (que añadió mi_empresa y mi_empresa_plus al enum). // // FIX: Si hay una subscription en trial con un plan específico (ej. // business_control desde una TrialInvitation), respetamos ese plan // para que el feature-gate y los límites funcionen correctamente. const subscription = await prisma.subscription.findFirst({ where: { tenantId, status: { in: ['authorized', 'pending', 'paused', 'trial', 'trial_expired'] } }, orderBy: { createdAt: 'desc' }, select: { status: true, amount: true, plan: true, currentPeriodStart: true, currentPeriodEnd: true, }, }); let currentPlan: string; if (subscription?.status === 'trial' && subscription.plan && subscription.plan !== 'trial') { currentPlan = subscription.plan; } else if (isTrialActive) { currentPlan = 'trial'; } else { currentPlan = String(tenant.plan); } // Precio de catálogo del plan actual (primer año, anual). La UI lo usa // cuando la suscripción aún no tiene monto (trial/trial_expired) para // mostrar el CTA de pago. let planPrice: number | null = null; if (currentPlan && currentPlan !== 'trial' && currentPlan !== 'custom') { try { planPrice = await getPlanPrice(currentPlan as any, 'annual', 'firstYear'); } catch { planPrice = null; } } // Estado de suscripción activa (si hay) — alimenta la UI con el monto // recurrente actual, fecha de próxima renovación y si el primer pago // (cuando aplica dualidad firstYear) ya fue completado. return res.json({ plan: currentPlan, dbMode: tenant.dbMode, trialEndsAt: tenant.trialEndsAt?.toISOString() ?? null, isTrialActive, planPrice, subscription: subscription ? { status: subscription.status, plan: subscription.plan, amount: Number(subscription.amount), currentPeriodStart: subscription.currentPeriodStart?.toISOString() ?? null, currentPeriodEnd: subscription.currentPeriodEnd?.toISOString() ?? null, } : null, }); } catch (error) { return next(error); } } export async function signup(req: Request, res: Response, next: NextFunction) { try { const data = signupSchema.parse(req.body); const result = await signupDespacho(data); return res.status(201).json(result); } catch (error: any) { if (error instanceof z.ZodError) { return next(new AppError(400, error.errors[0].message)); } if (error.message?.includes('Ya existe')) { return next(new AppError(409, error.message)); } return next(error); } }