Initial commit - Horux Despachos NL
This commit is contained in:
277
apps/api/src/controllers/cartera.controller.ts
Normal file
277
apps/api/src/controllers/cartera.controller.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
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 supervisores = await carteraService.getSupervisores(req.tenantPool!, req.user!.tenantId);
|
||||
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); }
|
||||
}
|
||||
Reference in New Issue
Block a user