import type { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import * as planService from '../services/plan-catalogo.service.js'; import { prisma } from '../config/database.js'; import { invalidateDespachoPlanCache } from '../services/plan-catalogo.service.js'; import { canEditPrices } from '../utils/platform-admin.js'; import { AppError } from '../middlewares/error.middleware.js'; export async function getPlans(req: Request, res: Response, next: NextFunction) { try { const vertical = req.query.vertical as string | undefined; const plans = await planService.listPlans(vertical); return res.json({ data: plans }); } catch (err) { return next(err); } } export async function getAddons(req: Request, res: Response, next: NextFunction) { try { const vertical = req.query.vertical as string | undefined; const addons = await planService.listAddons(vertical); return res.json({ data: addons }); } catch (err) { return next(err); } } export async function getPlan(req: Request, res: Response, next: NextFunction) { try { const plan = await planService.getPlanByCodename(String(req.params.codename)); if (!plan) return res.status(404).json({ message: 'Plan no encontrado' }); return res.json(plan); } catch (err) { return next(err); } } // ============================================================================ // Catálogo despacho — limits + precios editables por admin global // ============================================================================ /** GET /api/planes/despacho — devuelve los 6 planes con limits + precios. */ export async function listDespachoCatalogo(_req: Request, res: Response, next: NextFunction) { try { const plans = await planService.getAllDespachoPlanLimits(); return res.json({ data: plans }); } catch (err) { return next(err); } } const updateSchema = z.object({ nombre: z.string().min(1).max(50).optional(), monthly: z.number().nullable().optional(), firstYear: z.number().nullable().optional(), renewal: z.number().nullable().optional(), permiteMonthly: z.boolean().optional(), maxRfcs: z.number().int().optional(), maxUsers: z.number().int().optional(), timbresIncluidosMes: z.number().int().nonnegative().optional(), dbMode: z.enum(['BYO', 'MANAGED']).optional(), permiteServidorBackup: z.boolean().optional(), permiteSatIncremental: z.boolean().optional(), }); /** PATCH /api/planes/despacho/:plan — actualiza precios/limits. Solo admin con canEditPrices. */ export async function updateDespachoCatalogo(req: Request, res: Response, next: NextFunction) { try { if (!req.user || !(await canEditPrices(req.user.userId))) { throw new AppError(403, 'Solo admin global puede editar el catálogo'); } const plan = String(req.params.plan); const data = updateSchema.parse(req.body); const existing = await prisma.despachoPlanPrice.findUnique({ where: { plan } }); if (!existing) throw new AppError(404, `Plan '${plan}' no encontrado`); const updated = await prisma.despachoPlanPrice.update({ where: { plan }, data }); invalidateDespachoPlanCache(); return res.json({ plan: updated.plan, nombre: updated.nombre, monthly: updated.monthly !== null ? Number(updated.monthly) : null, firstYear: updated.firstYear !== null ? Number(updated.firstYear) : null, renewal: updated.renewal !== null ? Number(updated.renewal) : null, permiteMonthly: updated.permiteMonthly, maxRfcs: updated.maxRfcs, maxUsers: updated.maxUsers, timbresIncluidosMes: updated.timbresIncluidosMes, dbMode: updated.dbMode, permiteServidorBackup: updated.permiteServidorBackup, permiteSatIncremental: updated.permiteSatIncremental, }); } catch (err) { if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message)); return next(err); } }