282 lines
11 KiB
TypeScript
282 lines
11 KiB
TypeScript
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<boolean> {
|
|
// 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); }
|
|
}
|