Initial commit - Horux Despachos NL
This commit is contained in:
211
apps/api/src/services/papeleria.service.ts
Normal file
211
apps/api/src/services/papeleria.service.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type { Pool } from 'pg';
|
||||
|
||||
export const ALLOWED_MIMES = new Set([
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // xlsx
|
||||
]);
|
||||
|
||||
export const MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
export type EstadoPapeleria = 'pendiente' | 'aprobado' | 'rechazado';
|
||||
|
||||
export interface PapeleriaItem {
|
||||
id: number;
|
||||
contribuyenteId: string;
|
||||
nombre: string;
|
||||
descripcion: string | null;
|
||||
archivoFilename: string;
|
||||
archivoMime: string;
|
||||
archivoSize: number;
|
||||
anio: number;
|
||||
mes: number;
|
||||
requiereAprobacion: boolean;
|
||||
estado: EstadoPapeleria | null;
|
||||
aprobadoPor: string | null;
|
||||
aprobadoAt: Date | null;
|
||||
comentarioRechazo: string | null;
|
||||
subidoPor: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const SELECT = `
|
||||
id, contribuyente_id, nombre, descripcion,
|
||||
archivo_filename, archivo_mime, archivo_size,
|
||||
anio, mes,
|
||||
requiere_aprobacion, estado, aprobado_por, aprobado_at, comentario_rechazo,
|
||||
subido_por, created_at
|
||||
`;
|
||||
|
||||
const ROW = (r: any): PapeleriaItem => ({
|
||||
id: r.id,
|
||||
contribuyenteId: r.contribuyente_id,
|
||||
nombre: r.nombre,
|
||||
descripcion: r.descripcion,
|
||||
archivoFilename: r.archivo_filename,
|
||||
archivoMime: r.archivo_mime,
|
||||
archivoSize: r.archivo_size,
|
||||
anio: r.anio,
|
||||
mes: r.mes,
|
||||
requiereAprobacion: r.requiere_aprobacion,
|
||||
estado: r.estado,
|
||||
aprobadoPor: r.aprobado_por,
|
||||
aprobadoAt: r.aprobado_at,
|
||||
comentarioRechazo: r.comentario_rechazo,
|
||||
subidoPor: r.subido_por,
|
||||
createdAt: r.created_at,
|
||||
});
|
||||
|
||||
function sanitizeUuid(id: string): string {
|
||||
return id.replace(/[^a-f0-9-]/gi, '');
|
||||
}
|
||||
|
||||
export interface UploadInput {
|
||||
contribuyenteId: string;
|
||||
nombre: string;
|
||||
descripcion: string | null;
|
||||
anio: number;
|
||||
mes: number;
|
||||
requiereAprobacion: boolean;
|
||||
archivo: Buffer;
|
||||
archivoFilename: string;
|
||||
archivoMime: string;
|
||||
subidoPor: string;
|
||||
}
|
||||
|
||||
export async function uploadPapeleria(
|
||||
pool: Pool,
|
||||
input: UploadInput,
|
||||
): Promise<PapeleriaItem> {
|
||||
if (!ALLOWED_MIMES.has(input.archivoMime)) {
|
||||
throw new Error(`Formato no permitido: ${input.archivoMime}. Solo PDF, Word y Excel.`);
|
||||
}
|
||||
if (input.archivo.length > MAX_SIZE_BYTES) {
|
||||
throw new Error(`Archivo excede el máximo de 5 MB (recibido ${(input.archivo.length / 1024 / 1024).toFixed(1)} MB).`);
|
||||
}
|
||||
|
||||
const estadoInicial = input.requiereAprobacion ? 'pendiente' : null;
|
||||
|
||||
const { rows: [r] } = await pool.query(
|
||||
`INSERT INTO papeleria_trabajo
|
||||
(contribuyente_id, nombre, descripcion, archivo, archivo_filename, archivo_mime, archivo_size,
|
||||
anio, mes, requiere_aprobacion, estado, subido_por)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING ${SELECT}`,
|
||||
[
|
||||
sanitizeUuid(input.contribuyenteId),
|
||||
input.nombre,
|
||||
input.descripcion,
|
||||
input.archivo,
|
||||
input.archivoFilename,
|
||||
input.archivoMime,
|
||||
input.archivo.length,
|
||||
input.anio,
|
||||
input.mes,
|
||||
input.requiereAprobacion,
|
||||
estadoInicial,
|
||||
input.subidoPor,
|
||||
],
|
||||
);
|
||||
return ROW(r);
|
||||
}
|
||||
|
||||
export interface ListFilters {
|
||||
contribuyenteId: string;
|
||||
anio?: number;
|
||||
mes?: number;
|
||||
estado?: EstadoPapeleria | 'sin_aprobacion';
|
||||
}
|
||||
|
||||
export async function listPapeleria(pool: Pool, f: ListFilters): Promise<PapeleriaItem[]> {
|
||||
const conds: string[] = ['contribuyente_id = $1'];
|
||||
const vals: unknown[] = [sanitizeUuid(f.contribuyenteId)];
|
||||
let i = 2;
|
||||
if (f.anio) { conds.push(`anio = $${i++}`); vals.push(f.anio); }
|
||||
if (f.mes) { conds.push(`mes = $${i++}`); vals.push(f.mes); }
|
||||
if (f.estado === 'sin_aprobacion') {
|
||||
conds.push('requiere_aprobacion = false');
|
||||
} else if (f.estado) {
|
||||
conds.push(`estado = $${i++}`); vals.push(f.estado);
|
||||
}
|
||||
const { rows } = await pool.query(
|
||||
`SELECT ${SELECT} FROM papeleria_trabajo
|
||||
WHERE ${conds.join(' AND ')}
|
||||
ORDER BY anio DESC, mes DESC, created_at DESC`,
|
||||
vals,
|
||||
);
|
||||
return rows.map(ROW);
|
||||
}
|
||||
|
||||
export async function getById(pool: Pool, id: number): Promise<PapeleriaItem | null> {
|
||||
const { rows: [r] } = await pool.query(
|
||||
`SELECT ${SELECT} FROM papeleria_trabajo WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
return r ? ROW(r) : null;
|
||||
}
|
||||
|
||||
export async function downloadArchivo(
|
||||
pool: Pool,
|
||||
id: number,
|
||||
): Promise<{ archivo: Buffer; filename: string; mime: string } | null> {
|
||||
const { rows: [r] } = await pool.query(
|
||||
`SELECT archivo, archivo_filename, archivo_mime FROM papeleria_trabajo WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
if (!r) return null;
|
||||
return { archivo: r.archivo, filename: r.archivo_filename, mime: r.archivo_mime };
|
||||
}
|
||||
|
||||
const ROLES_APROBADOR = new Set(['owner', 'cfo', 'supervisor']);
|
||||
|
||||
export async function aprobar(
|
||||
pool: Pool,
|
||||
id: number,
|
||||
userId: string,
|
||||
userRole: string,
|
||||
): Promise<PapeleriaItem | null> {
|
||||
if (!ROLES_APROBADOR.has(userRole)) {
|
||||
throw new Error('Solo owner o supervisor pueden aprobar papelería');
|
||||
}
|
||||
const { rows: [r] } = await pool.query(
|
||||
`UPDATE papeleria_trabajo
|
||||
SET estado = 'aprobado', aprobado_por = $2, aprobado_at = NOW(),
|
||||
comentario_rechazo = NULL
|
||||
WHERE id = $1 AND requiere_aprobacion = true
|
||||
RETURNING ${SELECT}`,
|
||||
[id, userId],
|
||||
);
|
||||
return r ? ROW(r) : null;
|
||||
}
|
||||
|
||||
export async function rechazar(
|
||||
pool: Pool,
|
||||
id: number,
|
||||
userId: string,
|
||||
userRole: string,
|
||||
comentario: string | null,
|
||||
): Promise<PapeleriaItem | null> {
|
||||
if (!ROLES_APROBADOR.has(userRole)) {
|
||||
throw new Error('Solo owner o supervisor pueden rechazar papelería');
|
||||
}
|
||||
const { rows: [r] } = await pool.query(
|
||||
`UPDATE papeleria_trabajo
|
||||
SET estado = 'rechazado', aprobado_por = $2, aprobado_at = NOW(),
|
||||
comentario_rechazo = $3
|
||||
WHERE id = $1 AND requiere_aprobacion = true
|
||||
RETURNING ${SELECT}`,
|
||||
[id, userId, comentario],
|
||||
);
|
||||
return r ? ROW(r) : null;
|
||||
}
|
||||
|
||||
export async function eliminar(pool: Pool, id: number): Promise<boolean> {
|
||||
const { rowCount } = await pool.query(
|
||||
`DELETE FROM papeleria_trabajo WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
return (rowCount ?? 0) > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user