- Migracion 046: tablas obligacion_asignaciones y tarea_asignaciones - Servicio y controller de asignaciones (CRUD + listados) - Fix: enviar correo welcome al invitar usuario nuevo - Fix: quitar JOIN users de queries tenant (usar Prisma en BD central) - Fix: req.params.obligacionId correcto en asignaciones controller - Fix: orden rutas estaticas antes de dinamicas en cartera.routes - Fix: owner/cfo ven todas las asignaciones en getAsignacionesPorSupervisor - Fix: validar que entidad pertenezca a cartera padre en subcartera - Nuevo endpoint GET /carteras/asignaciones/sin-asignar - Nuevo endpoint GET /tareas/mis-tareas
529 lines
18 KiB
TypeScript
529 lines
18 KiB
TypeScript
import type { Pool } from 'pg';
|
|
|
|
export type Recurrencia =
|
|
| 'semanal' | 'quincenal' | 'mensual'
|
|
| 'bimestral' | 'trimestral' | 'semestral' | 'anual';
|
|
|
|
export interface TareaCatalogo {
|
|
id: string;
|
|
contribuyenteId: string;
|
|
nombre: string;
|
|
descripcion: string | null;
|
|
recurrencia: Recurrencia;
|
|
diaSemana: number | null;
|
|
diaMes: number | null;
|
|
soloSupervisorCompleta: boolean;
|
|
esDefault: boolean;
|
|
active: boolean;
|
|
orden: number;
|
|
createdAt: Date;
|
|
auxiliarAsignadoId?: string | null;
|
|
auxiliarAsignadoNombre?: string | null;
|
|
}
|
|
|
|
export interface TareaPeriodo {
|
|
id: string;
|
|
tareaId: string;
|
|
periodo: string;
|
|
fechaLimite: Date;
|
|
completada: boolean;
|
|
completadaAt: Date | null;
|
|
completadaPor: string | null;
|
|
notas: string | null;
|
|
}
|
|
|
|
export interface TareaConPeriodo extends TareaCatalogo {
|
|
periodoActual: TareaPeriodo | null;
|
|
}
|
|
|
|
const ROW_TO_TAREA = (r: any): TareaCatalogo => ({
|
|
id: r.id,
|
|
contribuyenteId: r.contribuyente_id,
|
|
nombre: r.nombre,
|
|
descripcion: r.descripcion,
|
|
recurrencia: r.recurrencia,
|
|
diaSemana: r.dia_semana,
|
|
diaMes: r.dia_mes,
|
|
soloSupervisorCompleta: r.solo_supervisor_completa,
|
|
esDefault: r.es_default,
|
|
active: r.active,
|
|
orden: r.orden,
|
|
createdAt: r.created_at,
|
|
auxiliarAsignadoId: r.auxiliarAsignadoId ?? null,
|
|
auxiliarAsignadoNombre: r.auxiliarAsignadoNombre ?? null,
|
|
});
|
|
|
|
const ROW_TO_PERIODO = (r: any): TareaPeriodo => ({
|
|
id: r.id,
|
|
tareaId: r.tarea_id,
|
|
periodo: r.periodo,
|
|
fechaLimite: r.fecha_limite,
|
|
completada: r.completada,
|
|
completadaAt: r.completada_at,
|
|
completadaPor: r.completada_por,
|
|
notas: r.notas,
|
|
});
|
|
|
|
function sanitizeUuid(id: string): string {
|
|
return id.replace(/[^a-f0-9-]/gi, '');
|
|
}
|
|
|
|
// ─── Catálogo CRUD ───
|
|
|
|
export async function listTareas(pool: Pool, contribuyenteId: string): Promise<TareaCatalogo[]> {
|
|
const { rows } = await pool.query(
|
|
`SELECT
|
|
tc.*,
|
|
ta.auxiliar_user_id AS "auxiliarAsignadoId"
|
|
FROM tareas_catalogo tc
|
|
LEFT JOIN tarea_asignaciones ta ON ta.tarea_id = tc.id
|
|
WHERE tc.contribuyente_id = $1 AND tc.active = true
|
|
ORDER BY tc.orden, tc.nombre`,
|
|
[sanitizeUuid(contribuyenteId)],
|
|
);
|
|
return rows.map(ROW_TO_TAREA);
|
|
}
|
|
|
|
export interface TareaInput {
|
|
nombre: string;
|
|
descripcion?: string | null;
|
|
recurrencia: Recurrencia;
|
|
diaSemana?: number | null;
|
|
diaMes?: number | null;
|
|
soloSupervisorCompleta?: boolean;
|
|
orden?: number;
|
|
}
|
|
|
|
export async function createTarea(
|
|
pool: Pool,
|
|
contribuyenteId: string,
|
|
data: TareaInput,
|
|
): Promise<TareaCatalogo> {
|
|
const { rows: [r] } = await pool.query(
|
|
`INSERT INTO tareas_catalogo
|
|
(contribuyente_id, nombre, descripcion, recurrencia,
|
|
dia_semana, dia_mes, solo_supervisor_completa, orden)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING *`,
|
|
[
|
|
sanitizeUuid(contribuyenteId),
|
|
data.nombre,
|
|
data.descripcion ?? null,
|
|
data.recurrencia,
|
|
data.diaSemana ?? null,
|
|
data.diaMes ?? null,
|
|
data.soloSupervisorCompleta ?? false,
|
|
data.orden ?? 0,
|
|
],
|
|
);
|
|
return ROW_TO_TAREA(r);
|
|
}
|
|
|
|
export async function updateTarea(
|
|
pool: Pool,
|
|
tareaId: string,
|
|
data: Partial<TareaInput>,
|
|
): Promise<TareaCatalogo | null> {
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
let i = 1;
|
|
if (data.nombre !== undefined) { fields.push(`nombre = $${i++}`); values.push(data.nombre); }
|
|
if (data.descripcion !== undefined) { fields.push(`descripcion = $${i++}`); values.push(data.descripcion); }
|
|
if (data.recurrencia !== undefined) { fields.push(`recurrencia = $${i++}`); values.push(data.recurrencia); }
|
|
if (data.diaSemana !== undefined) { fields.push(`dia_semana = $${i++}`); values.push(data.diaSemana); }
|
|
if (data.diaMes !== undefined) { fields.push(`dia_mes = $${i++}`); values.push(data.diaMes); }
|
|
if (data.soloSupervisorCompleta !== undefined) { fields.push(`solo_supervisor_completa = $${i++}`); values.push(data.soloSupervisorCompleta); }
|
|
if (data.orden !== undefined) { fields.push(`orden = $${i++}`); values.push(data.orden); }
|
|
if (fields.length === 0) return null;
|
|
values.push(sanitizeUuid(tareaId));
|
|
const { rows: [r] } = await pool.query(
|
|
`UPDATE tareas_catalogo SET ${fields.join(', ')} WHERE id = $${i} RETURNING *`,
|
|
values,
|
|
);
|
|
return r ? ROW_TO_TAREA(r) : null;
|
|
}
|
|
|
|
export async function deleteTarea(pool: Pool, tareaId: string): Promise<boolean> {
|
|
const { rowCount } = await pool.query(
|
|
`UPDATE tareas_catalogo SET active = false WHERE id = $1`,
|
|
[sanitizeUuid(tareaId)],
|
|
);
|
|
return (rowCount ?? 0) > 0;
|
|
}
|
|
|
|
// ─── Materialización de periodos ───
|
|
|
|
/** Convierte una fecha a su `periodo` según recurrencia. */
|
|
function periodoForDate(date: Date, recurrencia: Recurrencia): string {
|
|
const año = date.getFullYear();
|
|
const mes = date.getMonth() + 1;
|
|
if (recurrencia === 'semanal' || recurrencia === 'quincenal') {
|
|
return `${año}-W${String(isoWeek(date)).padStart(2, '0')}`;
|
|
}
|
|
if (recurrencia === 'mensual') return `${año}-${String(mes).padStart(2, '0')}`;
|
|
if (recurrencia === 'bimestral') return `${año}-B${Math.ceil(mes / 2)}`;
|
|
if (recurrencia === 'trimestral') return `${año}-Q${Math.ceil(mes / 3)}`;
|
|
if (recurrencia === 'semestral') return `${año}-S${Math.ceil(mes / 6)}`;
|
|
return `${año}`;
|
|
}
|
|
|
|
function isoWeek(date: Date): number {
|
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
const dayNum = d.getUTCDay() || 7;
|
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
|
}
|
|
|
|
/**
|
|
* Calcula la fecha límite del próximo periodo de una tarea, basado en
|
|
* `recurrencia`, `dia_semana`/`dia_mes` y un punto de referencia.
|
|
*
|
|
* Para semanal/quincenal: primera ocurrencia del dia_semana en/después de
|
|
* `fromDate`. (Quincenal alterna con el mismo dia_semana cada 14 días.)
|
|
* Para mensual+: dia_mes del periodo en curso de `fromDate`. Si el dia_mes
|
|
* excede el último día del mes, se clampa al último día.
|
|
*/
|
|
function computeFechaLimite(
|
|
recurrencia: Recurrencia,
|
|
diaSemana: number | null,
|
|
diaMes: number | null,
|
|
fromDate: Date,
|
|
): Date {
|
|
if (recurrencia === 'semanal' || recurrencia === 'quincenal') {
|
|
const target = (diaSemana ?? 5);
|
|
const d = new Date(fromDate);
|
|
const current = d.getDay() === 0 ? 7 : d.getDay();
|
|
const diff = (target - current + 7) % 7;
|
|
d.setDate(d.getDate() + diff);
|
|
return d;
|
|
}
|
|
// mensual a anual: usar el mes "ancla" del periodo
|
|
const año = fromDate.getFullYear();
|
|
let mesAncla = fromDate.getMonth() + 1;
|
|
if (recurrencia === 'bimestral') mesAncla = Math.ceil(mesAncla / 2) * 2;
|
|
else if (recurrencia === 'trimestral') mesAncla = Math.ceil(mesAncla / 3) * 3;
|
|
else if (recurrencia === 'semestral') mesAncla = Math.ceil(mesAncla / 6) * 6;
|
|
else if (recurrencia === 'anual') mesAncla = 12;
|
|
const lastDay = new Date(año, mesAncla, 0).getDate();
|
|
const dia = Math.min(diaMes ?? lastDay, lastDay);
|
|
return new Date(año, mesAncla - 1, dia);
|
|
}
|
|
|
|
/**
|
|
* Asegura que existan los periodos vigentes (presente + futuros próximos)
|
|
* para todas las tareas activas del contribuyente. Solo crea periodos
|
|
* cuya `fecha_limite >= today` (no retroactivos, igual que obligaciones).
|
|
*
|
|
* Para cada tarea genera el periodo CURRENT (donde cae hoy) si su fecha
|
|
* límite aún no ha pasado, o el NEXT si ya pasó.
|
|
*/
|
|
export async function materializarPeriodos(
|
|
pool: Pool,
|
|
contribuyenteId: string,
|
|
): Promise<void> {
|
|
const tareas = await listTareas(pool, contribuyenteId);
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
for (const t of tareas) {
|
|
let fechaLimite = computeFechaLimite(t.recurrencia, t.diaSemana, t.diaMes, today);
|
|
// Si la fecha límite calculada para "hoy" ya pasó, salta al siguiente periodo
|
|
while (fechaLimite < today) {
|
|
const next = new Date(fechaLimite);
|
|
const recurrenciaIncrement: Record<Recurrencia, () => void> = {
|
|
semanal: () => next.setDate(next.getDate() + 7),
|
|
quincenal: () => next.setDate(next.getDate() + 14),
|
|
mensual: () => next.setMonth(next.getMonth() + 1),
|
|
bimestral: () => next.setMonth(next.getMonth() + 2),
|
|
trimestral: () => next.setMonth(next.getMonth() + 3),
|
|
semestral: () => next.setMonth(next.getMonth() + 6),
|
|
anual: () => next.setFullYear(next.getFullYear() + 1),
|
|
};
|
|
recurrenciaIncrement[t.recurrencia]();
|
|
fechaLimite = computeFechaLimite(t.recurrencia, t.diaSemana, t.diaMes, next);
|
|
}
|
|
|
|
const periodo = periodoForDate(fechaLimite, t.recurrencia);
|
|
await pool.query(
|
|
`INSERT INTO tarea_periodos (tarea_id, periodo, fecha_limite)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (tarea_id, periodo) DO NOTHING`,
|
|
[t.id, periodo, fechaLimite.toISOString().split('T')[0]],
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lee tareas activas del contribuyente con su periodo más cercano (vigente
|
|
* o pendiente). Materializa los faltantes antes de leer.
|
|
*/
|
|
export async function listTareasConPeriodoActual(
|
|
pool: Pool,
|
|
contribuyenteId: string,
|
|
): Promise<TareaConPeriodo[]> {
|
|
await materializarPeriodos(pool, contribuyenteId);
|
|
const tareas = await listTareas(pool, contribuyenteId);
|
|
if (tareas.length === 0) return [];
|
|
|
|
const ids = tareas.map(t => t.id);
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const { rows } = await pool.query(
|
|
`SELECT DISTINCT ON (tarea_id) *
|
|
FROM tarea_periodos
|
|
WHERE tarea_id = ANY($1::uuid[])
|
|
AND (completada = false OR fecha_limite >= $2::date)
|
|
ORDER BY tarea_id, fecha_limite ASC`,
|
|
[ids, today],
|
|
);
|
|
const periodos = new Map(rows.map(r => [r.tarea_id, ROW_TO_PERIODO(r)]));
|
|
return tareas.map(t => ({ ...t, periodoActual: periodos.get(t.id) ?? null }));
|
|
}
|
|
|
|
export interface TareaConContribuyente extends TareaConPeriodo {
|
|
contribuyenteRfc: string;
|
|
contribuyenteRazonSocial: string;
|
|
}
|
|
|
|
/**
|
|
* Lee tareas activas con periodo actual para una lista de contribuyentes.
|
|
* Útil para la vista "Mis Tareas".
|
|
*/
|
|
export async function listTareasConPeriodoPorContribuyentes(
|
|
pool: Pool,
|
|
contribuyenteIds: string[],
|
|
): Promise<TareaConContribuyente[]> {
|
|
if (contribuyenteIds.length === 0) return [];
|
|
|
|
// Materializar periodos para cada contribuyente en paralelo
|
|
await Promise.all(contribuyenteIds.map(id => materializarPeriodos(pool, id)));
|
|
|
|
const { rows: tareasRows } = await pool.query(
|
|
`SELECT tc.*, c.entidad_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)
|
|
WHERE tc.contribuyente_id = ANY($1::uuid[]) AND tc.active = true
|
|
ORDER BY c.rfc, tc.orden, tc.nombre`,
|
|
[contribuyenteIds],
|
|
);
|
|
|
|
if (tareasRows.length === 0) return [];
|
|
|
|
const tareaIds = tareasRows.map((r: any) => r.id);
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const { rows: periodoRows } = await pool.query(
|
|
`SELECT DISTINCT ON (tarea_id) *
|
|
FROM tarea_periodos
|
|
WHERE tarea_id = ANY($1::uuid[])
|
|
AND (completada = false OR fecha_limite >= $2::date)
|
|
ORDER BY tarea_id, fecha_limite ASC`,
|
|
[tareaIds, today],
|
|
);
|
|
const periodos = new Map(periodoRows.map((r: any) => [r.tarea_id, ROW_TO_PERIODO(r)]));
|
|
|
|
return tareasRows.map((r: any) => ({
|
|
...ROW_TO_TAREA(r),
|
|
contribuyenteId: r.contribuyenteId,
|
|
contribuyenteRfc: r.contribuyenteRfc,
|
|
contribuyenteRazonSocial: r.contribuyenteRazonSocial,
|
|
periodoActual: periodos.get(r.id) ?? null,
|
|
}));
|
|
}
|
|
|
|
// ─── Completar / descompletar periodo ───
|
|
|
|
const ROLES_SUPERVISOR = new Set(['owner', 'cfo', 'supervisor']);
|
|
|
|
/**
|
|
* Marca un periodo como completado. Si la tarea tiene
|
|
* `solo_supervisor_completa=true`, valida que el rol del usuario sea
|
|
* owner/cfo/supervisor. Devuelve el periodo actualizado y la tarea
|
|
* (para que el caller pueda disparar notificaciones).
|
|
*/
|
|
export async function completarPeriodo(
|
|
pool: Pool,
|
|
periodoId: string,
|
|
userId: string,
|
|
userRole: string,
|
|
notas: string | null = null,
|
|
): Promise<{ periodo: TareaPeriodo; tarea: TareaCatalogo } | null> {
|
|
const { rows: [pRow] } = await pool.query(
|
|
`SELECT tp.*, tc.solo_supervisor_completa
|
|
FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tp.id = $1`,
|
|
[sanitizeUuid(periodoId)],
|
|
);
|
|
if (!pRow) return null;
|
|
if (pRow.solo_supervisor_completa && !ROLES_SUPERVISOR.has(userRole)) {
|
|
throw new Error('Solo supervisor o owner pueden marcar esta tarea como completada');
|
|
}
|
|
const { rows: [updated] } = await pool.query(
|
|
`UPDATE tarea_periodos
|
|
SET completada = true, completada_at = NOW(), completada_por = $2, notas = $3
|
|
WHERE id = $1
|
|
RETURNING *`,
|
|
[sanitizeUuid(periodoId), userId, notas],
|
|
);
|
|
const { rows: [tareaRow] } = await pool.query(
|
|
`SELECT * FROM tareas_catalogo WHERE id = $1`,
|
|
[pRow.tarea_id],
|
|
);
|
|
return { periodo: ROW_TO_PERIODO(updated), tarea: ROW_TO_TAREA(tareaRow) };
|
|
}
|
|
|
|
export async function descompletarPeriodo(
|
|
pool: Pool,
|
|
periodoId: string,
|
|
): Promise<boolean> {
|
|
const { rowCount } = await pool.query(
|
|
`UPDATE tarea_periodos
|
|
SET completada = false, completada_at = NULL, completada_por = NULL
|
|
WHERE id = $1`,
|
|
[sanitizeUuid(periodoId)],
|
|
);
|
|
return (rowCount ?? 0) > 0;
|
|
}
|
|
|
|
// ─── Calendario / alertas ───
|
|
|
|
export interface TareaEventoCalendario {
|
|
titulo: string;
|
|
descripcion: string;
|
|
tipo: 'tarea';
|
|
fechaLimite: string;
|
|
recurrencia: string;
|
|
completado: boolean;
|
|
notas: string | null;
|
|
contribuyenteId: string;
|
|
tareaId: string;
|
|
periodoId: string;
|
|
}
|
|
|
|
/**
|
|
* Lee tareas + sus periodos del año para mostrar en /calendario. Materializa
|
|
* los faltantes antes de leer (mismo patrón que listTareasConPeriodoActual).
|
|
* Si no hay contribuyenteId, no retorna nada (las tareas son siempre por
|
|
* contribuyente).
|
|
*/
|
|
export async function getEventosTareasParaCalendario(
|
|
pool: Pool,
|
|
contribuyenteId: string,
|
|
año: number,
|
|
): Promise<TareaEventoCalendario[]> {
|
|
await materializarPeriodos(pool, contribuyenteId);
|
|
const { rows } = await pool.query(
|
|
`SELECT tp.id AS periodo_id, tp.tarea_id, tp.fecha_limite, tp.completada, tp.notas,
|
|
tc.nombre, tc.descripcion, tc.recurrencia, tc.contribuyente_id
|
|
FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tc.contribuyente_id = $1
|
|
AND tc.active = true
|
|
AND EXTRACT(YEAR FROM tp.fecha_limite) = $2`,
|
|
[sanitizeUuid(contribuyenteId), año],
|
|
);
|
|
return rows.map(r => ({
|
|
titulo: r.nombre,
|
|
descripcion: r.descripcion ?? '',
|
|
tipo: 'tarea' as const,
|
|
fechaLimite: r.fecha_limite instanceof Date ? r.fecha_limite.toISOString().split('T')[0] : String(r.fecha_limite),
|
|
recurrencia: r.recurrencia,
|
|
completado: r.completada,
|
|
notas: r.notas,
|
|
contribuyenteId: r.contribuyente_id,
|
|
tareaId: r.tarea_id,
|
|
periodoId: r.periodo_id,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Cuenta tareas próximas a vencer (≤3 días) para alertas auto. Solo cuenta
|
|
* los periodos pendientes del contribuyente filtrado o de todo el tenant.
|
|
*/
|
|
export async function contarTareasProximasVencer(
|
|
pool: Pool,
|
|
contribuyenteId: string | null | undefined,
|
|
): Promise<{ total: number; monto?: number }> {
|
|
const safeId = contribuyenteId ? sanitizeUuid(contribuyenteId) : null;
|
|
if (safeId) await materializarPeriodos(pool, safeId);
|
|
const cf = safeId ? `AND tc.contribuyente_id = '${safeId}'` : '';
|
|
const { rows: [r] } = await pool.query(
|
|
`SELECT COUNT(*)::int AS total
|
|
FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tc.active = true
|
|
AND tp.completada = false
|
|
AND tp.fecha_limite >= CURRENT_DATE
|
|
AND tp.fecha_limite <= CURRENT_DATE + INTERVAL '3 days'
|
|
${cf}`,
|
|
);
|
|
return { total: r?.total || 0 };
|
|
}
|
|
|
|
/**
|
|
* Resuelve el auxiliar asignado a la cartera del contribuyente (si existe).
|
|
* Busca primero en subcarteras (más específico) y luego en la top-level.
|
|
* Retorna null si el contribuyente no está en ninguna cartera o ninguna
|
|
* tiene auxiliar asignado.
|
|
*/
|
|
export async function getAuxiliarUserIdDeContribuyente(
|
|
pool: Pool,
|
|
contribuyenteId: string,
|
|
): Promise<string | null> {
|
|
const { rows } = await pool.query<{ auxiliar_user_id: string | null; parent_id: string | null }>(
|
|
`SELECT c.auxiliar_user_id, c.parent_id
|
|
FROM cartera_entidades ce
|
|
JOIN carteras c ON c.id = ce.cartera_id
|
|
WHERE ce.entidad_id = $1
|
|
ORDER BY (c.parent_id IS NOT NULL) DESC, c.created_at DESC`,
|
|
[sanitizeUuid(contribuyenteId)],
|
|
);
|
|
for (const row of rows) {
|
|
if (row.auxiliar_user_id) return row.auxiliar_user_id;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ─── Seed de tareas default ───
|
|
|
|
const TAREAS_DEFAULT: TareaInput[] = [
|
|
{ nombre: 'Solicitar estados de cuenta', descripcion: 'Pedir al cliente los estados de cuenta bancarios del mes', recurrencia: 'mensual', diaMes: 5, orden: 1 },
|
|
{ nombre: 'Realizar conciliación bancaria', descripcion: 'Conciliar los movimientos bancarios contra los CFDIs', recurrencia: 'mensual', diaMes: 10, orden: 2 },
|
|
{ nombre: 'Realizar contabilización', descripcion: 'Registrar los CFDIs y movimientos en la contabilidad', recurrencia: 'mensual', diaMes: 14, orden: 3 },
|
|
{ nombre: 'Revisión fiscal preliminar', descripcion: 'Revisar declaración antes de presentación (solo supervisor/owner)', recurrencia: 'mensual', diaMes: 15, soloSupervisorCompleta: true, orden: 4 },
|
|
];
|
|
|
|
export async function seedTareasDefault(
|
|
pool: Pool,
|
|
contribuyenteId: string,
|
|
): Promise<number> {
|
|
const existentes = await pool.query(
|
|
`SELECT 1 FROM tareas_catalogo WHERE contribuyente_id = $1 AND es_default = true LIMIT 1`,
|
|
[sanitizeUuid(contribuyenteId)],
|
|
);
|
|
if (existentes.rowCount && existentes.rowCount > 0) return 0;
|
|
|
|
let count = 0;
|
|
for (const t of TAREAS_DEFAULT) {
|
|
await pool.query(
|
|
`INSERT INTO tareas_catalogo
|
|
(contribuyente_id, nombre, descripcion, recurrencia, dia_mes, solo_supervisor_completa, es_default, orden)
|
|
VALUES ($1, $2, $3, $4, $5, $6, true, $7)`,
|
|
[
|
|
sanitizeUuid(contribuyenteId),
|
|
t.nombre,
|
|
t.descripcion ?? null,
|
|
t.recurrencia,
|
|
t.diaMes ?? null,
|
|
t.soloSupervisorCompleta ?? false,
|
|
t.orden ?? 0,
|
|
],
|
|
);
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|