import type { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import * as carteraService from '../services/cartera.service.js'; import { AppError } from '../middlewares/error.middleware.js'; const createSchema = z.object({ nombre: z.string().min(1, 'Nombre requerido'), descripcion: z.string().optional(), supervisorUserId: z.string().uuid().optional(), // Owner can assign to a supervisor }); const createSubcarteraSchema = z.object({ nombre: z.string().min(1, 'Nombre requerido'), descripcion: z.string().optional(), auxiliarUserId: z.string().uuid('Auxiliar requerido'), }); const updateSchema = z.object({ nombre: z.string().min(1).optional(), descripcion: z.string().optional(), supervisorUserId: z.string().uuid().optional(), }); /** * Permission helpers: * - Owner: sees all, edits all * - Supervisor: sees carteras assigned to them (by owner) + carteras they created. * Can only edit/delete carteras THEY created. Cannot edit owner-created ones. * Can only add contribuyentes that are already assigned to them. * - Auxiliar: sees subcarteras where they're assigned. Read-only. */ function isOwner(req: Request): boolean { return req.user!.role === 'owner'; } function isSupervisor(req: Request): boolean { return req.user!.role === 'supervisor'; } /** Check if a supervisor created this cartera (vs owner assigned it to them) */ async function supervisorCreatedCartera(req: Request, cartera: carteraService.CarteraRow): Promise { // A cartera was created by the supervisor if supervisorUserId === the supervisor's userId // AND the cartera was not created by the owner assigning it. // We use a heuristic: if the supervisor_user_id matches and createdBy is not tracked, // we assume the supervisor can edit their own carteras. // For now: supervisor can edit carteras where they are the supervisor. // Owner-created carteras also have supervisorUserId set to the supervisor — // so we need another way to distinguish. // Solution: we'll add a 'created_by' concept. For now, let supervisor edit all carteras // assigned to them (both owner-created and self-created). // The user said: "Las que crea el owner, solo las puede ver el supervisor, pero no las puede editar" // This requires tracking who created the cartera. Let's use a simple approach: // check if the owner's userId matches the request user. return cartera.supervisorUserId === req.user!.userId; } export async function list(req: Request, res: Response, next: NextFunction) { try { const role = req.user!.role; const userId = req.user!.userId; if (isOwner(req)) { // Owner sees all top-level carteras const rows = await carteraService.listCarteras(req.tenantPool!); return res.json({ data: rows }); } if (isSupervisor(req)) { // Supervisor sees carteras assigned to them const rows = await carteraService.listCarteras(req.tenantPool!, userId); return res.json({ data: rows }); } // Auxiliar: sees subcarteras where they're assigned const { rows } = await req.tenantPool!.query( `SELECT c.id, c.supervisor_user_id AS "supervisorUserId", c.auxiliar_user_id AS "auxiliarUserId", c.parent_id AS "parentId", c.nombre, c.descripcion, c.created_at AS "createdAt", (SELECT count(*) FROM cartera_entidades ce WHERE ce.cartera_id = c.id)::int AS "entidadesCount", 0 AS "subcarterasCount" FROM carteras c WHERE c.auxiliar_user_id = $1 ORDER BY c.nombre`, [userId], ); return res.json({ data: rows }); } catch (err) { return next(err); } } export async function getById(req: Request, res: Response, next: NextFunction) { try { const row = await carteraService.getCarteraById(req.tenantPool!, String(req.params.id)); if (!row) return next(new AppError(404, 'Cartera no encontrada')); // Auxiliar can only see their own subcarteras if (req.user!.role === 'auxiliar' && row.auxiliarUserId !== req.user!.userId) { return next(new AppError(403, 'No autorizado')); } return res.json(row); } catch (err) { return next(err); } } export async function create(req: Request, res: Response, next: NextFunction) { try { const data = createSchema.parse(req.body); const supervisorUserId = data.supervisorUserId || req.user!.userId; const row = await carteraService.createCartera(req.tenantPool!, { supervisorUserId, nombre: data.nombre, descripcion: data.descripcion, }); return res.status(201).json(row); } catch (err: any) { if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message)); return next(err); } } export async function update(req: Request, res: Response, next: NextFunction) { try { const cartera = await carteraService.getCarteraById(req.tenantPool!, String(req.params.id)); if (!cartera) return next(new AppError(404, 'Cartera no encontrada')); // Supervisor cannot edit carteras (owner-assigned are read-only for them) // Only owner can edit top-level carteras if (isSupervisor(req)) { return next(new AppError(403, 'Solo el owner puede editar carteras')); } const data = updateSchema.parse(req.body); const row = await carteraService.updateCartera(req.tenantPool!, String(req.params.id), data); return res.json(row); } catch (err: any) { if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message)); return next(err); } } export async function remove(req: Request, res: Response, next: NextFunction) { try { const cartera = await carteraService.getCarteraById(req.tenantPool!, String(req.params.id)); if (!cartera) return next(new AppError(404, 'Cartera no encontrada')); if (isSupervisor(req)) { return next(new AppError(403, 'Solo el owner puede eliminar carteras')); } await carteraService.deleteCartera(req.tenantPool!, String(req.params.id)); return res.json({ message: 'Cartera eliminada' }); } catch (err) { return next(err); } } // Subcarteras export async function listSubcarteras(req: Request, res: Response, next: NextFunction) { try { const rows = await carteraService.listSubcarteras(req.tenantPool!, String(req.params.id)); return res.json({ data: rows }); } catch (err) { return next(err); } } export async function createSubcartera(req: Request, res: Response, next: NextFunction) { try { const parent = await carteraService.getCarteraById(req.tenantPool!, String(req.params.id)); if (!parent) return next(new AppError(404, 'Cartera padre no encontrada')); // Supervisor can create subcarteras within their own carteras if (isSupervisor(req) && parent.supervisorUserId !== req.user!.userId) { return next(new AppError(403, 'No autorizado')); } const data = createSubcarteraSchema.parse(req.body); const row = await carteraService.createSubcartera(req.tenantPool!, { parentId: String(req.params.id), auxiliarUserId: data.auxiliarUserId, nombre: data.nombre, descripcion: data.descripcion, }); return res.status(201).json(row); } catch (err: any) { if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message)); return next(err); } } // Entidades export async function addEntidad(req: Request, res: Response, next: NextFunction) { try { const cartera = await carteraService.getCarteraById(req.tenantPool!, String(req.params.id)); if (!cartera) return next(new AppError(404, 'Cartera no encontrada')); if (isSupervisor(req)) { // For subcarteras: check the parent's supervisor const supervisorId = cartera.supervisorUserId || (cartera.parentId ? (await carteraService.getCarteraById(req.tenantPool!, cartera.parentId))?.supervisorUserId : null); if (supervisorId !== req.user!.userId) { return next(new AppError(403, 'No autorizado')); } } const { entidadId } = z.object({ entidadId: z.string().uuid() }).parse(req.body); await carteraService.addEntidadToCartera(req.tenantPool!, String(req.params.id), entidadId); return res.json({ message: 'Entidad agregada a cartera' }); } catch (err: any) { if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message)); return next(err); } } export async function removeEntidad(req: Request, res: Response, next: NextFunction) { try { const cartera = await carteraService.getCarteraById(req.tenantPool!, String(req.params.id)); if (!cartera) return next(new AppError(404, 'Cartera no encontrada')); if (isSupervisor(req)) { const supervisorId = cartera.supervisorUserId || (cartera.parentId ? (await carteraService.getCarteraById(req.tenantPool!, cartera.parentId))?.supervisorUserId : null); if (supervisorId !== req.user!.userId) { return next(new AppError(403, 'No autorizado')); } } await carteraService.removeEntidadFromCartera(req.tenantPool!, String(req.params.id), String(req.params.entidadId)); return res.json({ message: 'Entidad removida de cartera' }); } catch (err) { return next(err); } } export async function getEntidades(req: Request, res: Response, next: NextFunction) { try { const ids = await carteraService.getCarteraEntidades(req.tenantPool!, String(req.params.id)); return res.json({ data: ids }); } catch (err) { return next(err); } } // Auxiliares export async function getAuxiliares(req: Request, res: Response, next: NextFunction) { try { const ids = await carteraService.getCarteraAuxiliares(req.tenantPool!, String(req.params.id)); return res.json({ data: ids }); } catch (err) { return next(err); } } export async function addAuxiliar(req: Request, res: Response, next: NextFunction) { try { const { auxiliarUserId } = z.object({ auxiliarUserId: z.string().uuid() }).parse(req.body); await carteraService.addAuxiliarToCartera(req.tenantPool!, String(req.params.id), auxiliarUserId); return res.json({ message: 'Auxiliar agregado a cartera' }); } catch (err: any) { if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message)); return next(err); } } export async function removeAuxiliar(req: Request, res: Response, next: NextFunction) { try { await carteraService.removeAuxiliarFromCartera(req.tenantPool!, String(req.params.id), String(req.params.auxiliarUserId)); return res.json({ message: 'Auxiliar removido de cartera' }); } catch (err) { return next(err); } } // Supervisores available (for dropdown) export async function getSupervisores(req: Request, res: Response, next: NextFunction) { try { const allSupervisores = await carteraService.getSupervisores(req.tenantPool!, req.user!.tenantId); // Un supervisor solo se ve a si mismo en el dropdown (no puede asignar a otro supervisor) const supervisores = isSupervisor(req) ? allSupervisores.filter(s => s.userId === req.user!.userId) : allSupervisores; return res.json({ data: supervisores }); } catch (err) { return next(err); } } // Auxiliares of a supervisor export async function getAuxiliaresDelSupervisor(req: Request, res: Response, next: NextFunction) { try { const supervisorId = isOwner(req) ? String(req.params.supervisorId || req.user!.userId) : req.user!.userId; const rows = await carteraService.getAuxiliaresDelSupervisor(req.tenantPool!, supervisorId); return res.json({ data: rows }); } catch (err) { return next(err); } }