- Agrega helper withJitOff en impuestos.service.ts - Ejecuta getResumenIva, getIvaMensual y readResumenIvaFromCache con SET LOCAL jit = off - Evita compilación JIT de ~17s en queries con costo estimado alto feat(contribuyentes): auto-asignar a cartera del supervisor - Al crear contribuyente con supervisorUserId, se agrega automáticamente a todas las carteras top-level del supervisor feat(permisos): restricciones de UI por rol en contribuyentes - Oculta botón Add-ons para roles distintos de owner/cfo - Oculta botón Eliminar contribuyente para no-owner - Oculta botón Agregar RFC para auxiliar/visor/cliente/contador feat(cfdi): ver CFDI desde conceptos y forma de pago en Excel - Agrega botón Ver CFDI en cada fila de la tabla de Conceptos - Agrega columna Forma de Pago en export Excel de CFDIs - Agrega columna Forma de Pago en export individual de CFDI chore(migraciones): índices GIN para relaciones de activos - 048: índices btree parciales para activos - 049: índices GIN para cfdis_relacionados y uuid_relacionado
344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
import type { Pool } from 'pg';
|
|
import { prisma } from '../config/database.js';
|
|
|
|
// ── Asignación de obligaciones ──
|
|
|
|
export async function asignarObligacion(
|
|
pool: Pool,
|
|
obligacionId: string,
|
|
auxiliarUserId: string,
|
|
asignadoPor: string,
|
|
): Promise<void> {
|
|
await pool.query(
|
|
`INSERT INTO obligacion_asignaciones (obligacion_id, auxiliar_user_id, asignado_por)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (obligacion_id)
|
|
DO UPDATE SET auxiliar_user_id = $2, asignado_por = $3, asignado_at = now()`,
|
|
[obligacionId, auxiliarUserId, asignadoPor],
|
|
);
|
|
}
|
|
|
|
export async function desasignarObligacion(pool: Pool, obligacionId: string): Promise<void> {
|
|
await pool.query('DELETE FROM obligacion_asignaciones WHERE obligacion_id = $1', [obligacionId]);
|
|
}
|
|
|
|
// ── Asignación de tareas ──
|
|
|
|
export async function asignarTarea(
|
|
pool: Pool,
|
|
tareaId: string,
|
|
auxiliarUserId: string,
|
|
asignadoPor: string,
|
|
): Promise<void> {
|
|
await pool.query(
|
|
`INSERT INTO tarea_asignaciones (tarea_id, auxiliar_user_id, asignado_por)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (tarea_id)
|
|
DO UPDATE SET auxiliar_user_id = $2, asignado_por = $3, asignado_at = now()`,
|
|
[tareaId, auxiliarUserId, asignadoPor],
|
|
);
|
|
}
|
|
|
|
export async function desasignarTarea(pool: Pool, tareaId: string): Promise<void> {
|
|
await pool.query('DELETE FROM tarea_asignaciones WHERE tarea_id = $1', [tareaId]);
|
|
}
|
|
|
|
// ── Listados ──
|
|
|
|
export interface AsignacionObligacion {
|
|
id: string;
|
|
obligacionId: string;
|
|
obligacionNombre: string;
|
|
contribuyenteId: string;
|
|
contribuyenteRfc: string;
|
|
contribuyenteRazonSocial: string;
|
|
auxiliarUserId: string;
|
|
auxiliarNombre: string | null;
|
|
asignadoPor: string;
|
|
asignadoAt: string;
|
|
}
|
|
|
|
export interface AsignacionTarea {
|
|
id: string;
|
|
tareaId: string;
|
|
tareaNombre: string;
|
|
contribuyenteId: string;
|
|
contribuyenteRfc: string;
|
|
contribuyenteRazonSocial: string;
|
|
auxiliarUserId: string;
|
|
auxiliarNombre: string | null;
|
|
asignadoPor: string;
|
|
asignadoAt: string;
|
|
}
|
|
|
|
async function resolveUserNames(userIds: string[]): Promise<Map<string, string>> {
|
|
const map = new Map<string, string>();
|
|
if (userIds.length === 0) return map;
|
|
const users = await prisma.user.findMany({
|
|
where: { id: { in: userIds } },
|
|
select: { id: true, nombre: true },
|
|
});
|
|
for (const u of users) {
|
|
map.set(u.id, u.nombre);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Devuelve todas las asignaciones de obligaciones y tareas de los auxiliares
|
|
* que pertenecen al supervisor indicado (vía auxiliar_supervisores).
|
|
* Owner ve todas las asignaciones del tenant.
|
|
*/
|
|
export async function getAsignacionesPorSupervisor(
|
|
pool: Pool,
|
|
supervisorUserId: string,
|
|
role: string,
|
|
): Promise<{ obligaciones: AsignacionObligacion[]; tareas: AsignacionTarea[] }> {
|
|
const isOwner = role === 'owner' || role === 'cfo' || role === 'contador';
|
|
|
|
// Relación supervisor → auxiliar se infiere desde carteras (directas y
|
|
// subcarteras) con fallback a la tabla legacy auxiliar_supervisores.
|
|
const supervisorFilter = isOwner
|
|
? ''
|
|
: `AND EXISTS (
|
|
SELECT 1 FROM (
|
|
SELECT c.auxiliar_user_id
|
|
FROM carteras c
|
|
WHERE c.supervisor_user_id = $1
|
|
AND c.auxiliar_user_id IS NOT NULL
|
|
UNION
|
|
SELECT sub.auxiliar_user_id
|
|
FROM carteras sub
|
|
JOIN carteras p ON p.id = sub.parent_id
|
|
WHERE p.supervisor_user_id = $1
|
|
AND sub.auxiliar_user_id IS NOT NULL
|
|
UNION
|
|
SELECT auxiliar_user_id FROM auxiliar_supervisores WHERE supervisor_user_id = $1
|
|
) sup_aux WHERE sup_aux.auxiliar_user_id = __AUX_COL__
|
|
)`;
|
|
const whereObl = isOwner
|
|
? 'WHERE 1=1'
|
|
: `WHERE 1=1 ${supervisorFilter.replace(/__AUX_COL__/g, 'oa.auxiliar_user_id')}`;
|
|
const whereTarea = isOwner
|
|
? 'WHERE 1=1'
|
|
: `WHERE 1=1 ${supervisorFilter.replace(/__AUX_COL__/g, 'ta.auxiliar_user_id')}`;
|
|
const params = isOwner ? [] : [supervisorUserId];
|
|
|
|
const { rows: obligaciones } = await pool.query<AsignacionObligacion>(
|
|
`SELECT
|
|
oa.id,
|
|
oa.obligacion_id AS "obligacionId",
|
|
oc.nombre AS "obligacionNombre",
|
|
oc.contribuyente_id AS "contribuyenteId",
|
|
c.rfc AS "contribuyenteRfc",
|
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial",
|
|
oa.auxiliar_user_id AS "auxiliarUserId",
|
|
oa.asignado_por AS "asignadoPor",
|
|
oa.asignado_at AS "asignadoAt"
|
|
FROM obligacion_asignaciones oa
|
|
JOIN obligaciones_contribuyente oc ON oc.id = oa.obligacion_id
|
|
JOIN contribuyentes c ON c.entidad_id = oc.contribuyente_id
|
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
|
${whereObl}
|
|
ORDER BY oa.asignado_at DESC`,
|
|
params,
|
|
);
|
|
|
|
const { rows: tareas } = await pool.query<AsignacionTarea>(
|
|
`SELECT
|
|
ta.id,
|
|
ta.tarea_id AS "tareaId",
|
|
tc.nombre AS "tareaNombre",
|
|
tc.contribuyente_id AS "contribuyenteId",
|
|
c.rfc AS "contribuyenteRfc",
|
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial",
|
|
ta.auxiliar_user_id AS "auxiliarUserId",
|
|
ta.asignado_por AS "asignadoPor",
|
|
ta.asignado_at AS "asignadoAt"
|
|
FROM tarea_asignaciones ta
|
|
JOIN tareas_catalogo tc ON tc.id = ta.tarea_id
|
|
JOIN contribuyentes c ON c.entidad_id = tc.contribuyente_id
|
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
|
${whereTarea}
|
|
ORDER BY ta.asignado_at DESC`,
|
|
params,
|
|
);
|
|
|
|
const allAuxIds = [...new Set([
|
|
...obligaciones.map(o => o.auxiliarUserId),
|
|
...tareas.map(t => t.auxiliarUserId),
|
|
])];
|
|
const names = await resolveUserNames(allAuxIds);
|
|
|
|
return {
|
|
obligaciones: obligaciones.map(o => ({ ...o, auxiliarNombre: names.get(o.auxiliarUserId) ?? null })),
|
|
tareas: tareas.map(t => ({ ...t, auxiliarNombre: names.get(t.auxiliarUserId) ?? null })),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Devuelve las asignaciones del auxiliar logueado.
|
|
*/
|
|
export async function getAsignacionesPorAuxiliar(
|
|
pool: Pool,
|
|
auxiliarUserId: string,
|
|
): Promise<{ obligaciones: AsignacionObligacion[]; tareas: AsignacionTarea[] }> {
|
|
const { rows: obligaciones } = await pool.query<AsignacionObligacion>(
|
|
`SELECT
|
|
oa.id,
|
|
oa.obligacion_id AS "obligacionId",
|
|
oc.nombre AS "obligacionNombre",
|
|
oc.contribuyente_id AS "contribuyenteId",
|
|
c.rfc AS "contribuyenteRfc",
|
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial",
|
|
oa.auxiliar_user_id AS "auxiliarUserId",
|
|
oa.asignado_por AS "asignadoPor",
|
|
oa.asignado_at AS "asignadoAt"
|
|
FROM obligacion_asignaciones oa
|
|
JOIN obligaciones_contribuyente oc ON oc.id = oa.obligacion_id
|
|
JOIN contribuyentes c ON c.entidad_id = oc.contribuyente_id
|
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
|
WHERE oa.auxiliar_user_id = $1
|
|
ORDER BY oa.asignado_at DESC`,
|
|
[auxiliarUserId],
|
|
);
|
|
|
|
const { rows: tareas } = await pool.query<AsignacionTarea>(
|
|
`SELECT
|
|
ta.id,
|
|
ta.tarea_id AS "tareaId",
|
|
tc.nombre AS "tareaNombre",
|
|
tc.contribuyente_id AS "contribuyenteId",
|
|
c.rfc AS "contribuyenteRfc",
|
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial",
|
|
ta.auxiliar_user_id AS "auxiliarUserId",
|
|
ta.asignado_por AS "asignadoPor",
|
|
ta.asignado_at AS "asignadoAt"
|
|
FROM tarea_asignaciones ta
|
|
JOIN tareas_catalogo tc ON tc.id = ta.tarea_id
|
|
JOIN contribuyentes c ON c.entidad_id = tc.contribuyente_id
|
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
|
WHERE ta.auxiliar_user_id = $1
|
|
ORDER BY ta.asignado_at DESC`,
|
|
[auxiliarUserId],
|
|
);
|
|
|
|
const names = await resolveUserNames([auxiliarUserId]);
|
|
const auxName = names.get(auxiliarUserId) ?? null;
|
|
|
|
return {
|
|
obligaciones: obligaciones.map(o => ({ ...o, auxiliarNombre: auxName })),
|
|
tareas: tareas.map(t => ({ ...t, auxiliarNombre: auxName })),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Devuelve obligaciones activas sin asignar para los contribuyentes indicados.
|
|
*/
|
|
export async function getObligacionesSinAsignar(
|
|
pool: Pool,
|
|
entidadIds: string[],
|
|
): Promise<Omit<AsignacionObligacion, 'id' | 'auxiliarUserId' | 'auxiliarNombre' | 'asignadoPor' | 'asignadoAt'>[]> {
|
|
if (entidadIds.length === 0) return [];
|
|
const { rows } = await pool.query(
|
|
`SELECT
|
|
oc.id AS "obligacionId",
|
|
oc.nombre AS "obligacionNombre",
|
|
oc.contribuyente_id AS "contribuyenteId",
|
|
c.rfc AS "contribuyenteRfc",
|
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial"
|
|
FROM obligaciones_contribuyente oc
|
|
JOIN contribuyentes c ON c.entidad_id = oc.contribuyente_id
|
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
|
LEFT JOIN obligacion_asignaciones oa ON oa.obligacion_id = oc.id
|
|
WHERE oc.activa = true AND oa.id IS NULL AND oc.contribuyente_id = ANY($1)
|
|
ORDER BY c.rfc, oc.nombre`,
|
|
[entidadIds],
|
|
);
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* Devuelve tareas activas sin asignar para los contribuyentes indicados.
|
|
*/
|
|
export async function getTareasSinAsignar(
|
|
pool: Pool,
|
|
entidadIds: string[],
|
|
): Promise<Omit<AsignacionTarea, 'id' | 'auxiliarUserId' | 'auxiliarNombre' | 'asignadoPor' | 'asignadoAt'>[]> {
|
|
if (entidadIds.length === 0) return [];
|
|
const { rows } = await pool.query(
|
|
`SELECT
|
|
tc.id AS "tareaId",
|
|
tc.nombre AS "tareaNombre",
|
|
tc.contribuyente_id AS "contribuyenteId",
|
|
c.rfc AS "contribuyenteRfc",
|
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial"
|
|
FROM tareas_catalogo tc
|
|
JOIN contribuyentes c ON c.entidad_id = tc.contribuyente_id
|
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
|
LEFT JOIN tarea_asignaciones ta ON ta.tarea_id = tc.id
|
|
WHERE tc.active = true AND ta.id IS NULL AND tc.contribuyente_id = ANY($1)
|
|
ORDER BY c.rfc, tc.nombre`,
|
|
[entidadIds],
|
|
);
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* Resuelve el auxiliar asignado a una obligación (o null).
|
|
*/
|
|
export async function getAuxiliarAsignadoObligacion(
|
|
pool: Pool,
|
|
obligacionId: string,
|
|
): Promise<{ auxiliarUserId: string; auxiliarNombre: string | null } | null> {
|
|
const { rows } = await pool.query<{ auxiliar_user_id: string }>(
|
|
`SELECT oa.auxiliar_user_id
|
|
FROM obligacion_asignaciones oa
|
|
WHERE oa.obligacion_id = $1`,
|
|
[obligacionId],
|
|
);
|
|
if (rows.length === 0) return null;
|
|
const auxId = rows[0].auxiliar_user_id;
|
|
const names = await resolveUserNames([auxId]);
|
|
return { auxiliarUserId: auxId, auxiliarNombre: names.get(auxId) ?? null };
|
|
}
|
|
|
|
/**
|
|
* Resuelve el auxiliar asignado a una tarea (o null).
|
|
*/
|
|
export async function getAuxiliarAsignadoTarea(
|
|
pool: Pool,
|
|
tareaId: string,
|
|
): Promise<{ auxiliarUserId: string; auxiliarNombre: string | null } | null> {
|
|
const { rows } = await pool.query<{ auxiliar_user_id: string }>(
|
|
`SELECT ta.auxiliar_user_id
|
|
FROM tarea_asignaciones ta
|
|
WHERE ta.tarea_id = $1`,
|
|
[tareaId],
|
|
);
|
|
if (rows.length === 0) return null;
|
|
const auxId = rows[0].auxiliar_user_id;
|
|
const names = await resolveUserNames([auxId]);
|
|
return { auxiliarUserId: auxId, auxiliarNombre: names.get(auxId) ?? null };
|
|
}
|
|
|
|
/**
|
|
* Devuelve los userIds de auxiliares que tienen al contribuyente en alguna
|
|
* de sus subcarteras (carteras con auxiliar_user_id no nulo que contienen
|
|
* al contribuyente en cartera_entidades).
|
|
*/
|
|
export async function getAuxiliaresElegibles(
|
|
pool: Pool,
|
|
contribuyenteId: string,
|
|
): Promise<string[]> {
|
|
const { rows } = await pool.query<{ auxiliar_user_id: string }>(
|
|
`SELECT DISTINCT c.auxiliar_user_id
|
|
FROM carteras c
|
|
JOIN cartera_entidades ce ON ce.cartera_id = c.id
|
|
WHERE ce.entidad_id = $1
|
|
AND c.auxiliar_user_id IS NOT NULL`,
|
|
[contribuyenteId],
|
|
);
|
|
return rows.map(r => r.auxiliar_user_id);
|
|
}
|