chore: catálogo obligaciones, cierre automático, fixes SAT y facturación

- Catálogo de obligaciones fiscales expandido a 30 entradas con campo requierePago.
- Soporte de frecuencia cuatrimestral en obligaciones y declaraciones.
- Automatización de cierre de obligaciones fiscales desde Documentos › Declaraciones.
- Nuevas tablas obligacion_evidencias, obligacion_periodos estados y declaracion_obligaciones.
- Nuevo servicio obligacion-evidencias.service.ts y endpoints REST.
- Refactor de declaraciones.service.ts para vincular obligaciones y crear evidencias.
- Notificaciones por email para evidencias de obligaciones.
- Adjuntar PDFs en correo de declaración subida.
- Fix drill-down de CFDIs: carga completa al visualizar.
- Fix sincronización SAT: tipos P/N, UUID case-insensitive, no reutilizar requestId.
- Fix suscripciones pending en /configuracion/planes-despacho.
- Fix sugerencias de Clave Producto SAT: importar catálogo y robustecer autocomplete.
- Quitar toggle manual de completado en Configuración › Obligaciones fiscales › Tareas.
- Scripts de soporte para Demo Ventas y utilerías (change-user-email, resend-welcome, import-clave-prod-serv).
- Documentación de cambios en docs/CAMBIOS-2026-05-04.md.
This commit is contained in:
Horux Dev
2026-06-22 04:53:59 +00:00
parent b217342a96
commit 7df27ce66d
39 changed files with 2791 additions and 191 deletions

View File

@@ -1,6 +1,11 @@
import type { Pool } from 'pg';
import { OBLIGACIONES_CATALOGO, getRecomendaciones, type ObligacionFiscal } from '../constants/obligaciones-fiscales.js';
function requierePagoPorCatalogo(catalogoId: string | null): boolean {
if (!catalogoId) return true;
return OBLIGACIONES_CATALOGO.find((o) => o.id === catalogoId)?.requierePago ?? true;
}
/**
* Keyword-based matching: each catalog entry has discriminant keywords
* that must ALL appear in the SAT description (normalized, lowercase, no accents).
@@ -255,6 +260,7 @@ export async function initRecomendaciones(
function inferirFrecuencia(vencimiento: string): string {
const lower = vencimiento.toLowerCase();
if (lower.includes('mensual') || lower.includes('mes')) return 'mensual';
if (lower.includes('cuatrimest')) return 'cuatrimestral';
if (lower.includes('bimest')) return 'bimestral';
if (lower.includes('trimest')) return 'trimestral';
if (lower.includes('anual') || lower.includes('ejercicio') || lower.includes('tres meses siguientes')) return 'anual';
@@ -351,13 +357,22 @@ export async function getObligacionesPorPeriodo(
const [year, month] = periodo.split('-').map(Number);
const currentPeriodo = new Date().toISOString().substring(0, 7);
const results: Array<ObligacionContribuyente & { periodStatus: string; periodoAplica: string; declaracion: DeclaracionLink | null }> = [];
const results: Array<ObligacionContribuyente & {
periodStatus: string;
periodoAplica: string;
declaracion: DeclaracionLink | null;
declaracionPresentada: boolean;
pagoPresentado: boolean;
requierePago: boolean;
}> = [];
// Get all completion records + associated declaration info for this contribuyente
const { rows: completions } = await pool.query<{
obligacion_id: string;
periodo: string;
completada: boolean;
declaracion_presentada: boolean;
pago_presentado: boolean;
declaracion_id: number | null;
decl_año: number | null;
decl_mes: number | null;
@@ -365,6 +380,7 @@ export async function getObligacionesPorPeriodo(
decl_pdf_filename: string | null;
}>(`
SELECT op.obligacion_id, op.periodo, op.completada,
op.declaracion_presentada, op.pago_presentado,
op.declaracion_id,
dp.año AS decl_año,
dp.mes AS decl_mes,
@@ -377,10 +393,14 @@ export async function getObligacionesPorPeriodo(
`, [contribuyenteId]);
const completionMap = new Map<string, boolean>();
const declaracionPresentadaMap = new Map<string, boolean>();
const pagoPresentadoMap = new Map<string, boolean>();
const declaracionMap = new Map<string, DeclaracionLink | null>();
for (const c of completions) {
const key = `${c.obligacion_id}:${c.periodo}`;
completionMap.set(key, c.completada);
declaracionPresentadaMap.set(key, c.declaracion_presentada);
pagoPresentadoMap.set(key, c.pago_presentado);
if (c.declaracion_id && c.decl_año != null && c.decl_mes != null && c.decl_tipo) {
declaracionMap.set(key, {
id: c.declaracion_id,
@@ -407,6 +427,9 @@ export async function getObligacionesPorPeriodo(
periodStatus: isCompleted ? 'completada' : 'pendiente',
periodoAplica: periodo,
declaracion: declaracionMap.get(key) ?? null,
declaracionPresentada: declaracionPresentadaMap.get(key) === true,
pagoPresentado: pagoPresentadoMap.get(key) === true,
requierePago: requierePagoPorCatalogo(ob.catalogoId),
});
}
@@ -434,6 +457,9 @@ export async function getObligacionesPorPeriodo(
periodStatus: 'atrasada',
periodoAplica: pastPeriodo,
declaracion: null,
declaracionPresentada: declaracionPresentadaMap.get(pastKey) === true,
pagoPresentado: pagoPresentadoMap.get(pastKey) === true,
requierePago: requierePagoPorCatalogo(ob.catalogoId),
});
}
}
@@ -448,7 +474,14 @@ export async function getObligacionesPorPeriodo(
return a.nombre.localeCompare(b.nombre);
});
return results as Array<ObligacionContribuyente & { periodStatus: 'pendiente' | 'completada' | 'atrasada'; periodoAplica: string; declaracion: DeclaracionLink | null }>;
return results as Array<ObligacionContribuyente & {
periodStatus: 'pendiente' | 'completada' | 'atrasada';
periodoAplica: string;
declaracion: DeclaracionLink | null;
declaracionPresentada: boolean;
pagoPresentado: boolean;
requierePago: boolean;
}>;
}
function appliesTo(frecuencia: string | null, periodo: string): boolean {
@@ -457,6 +490,7 @@ function appliesTo(frecuencia: string | null, periodo: string): boolean {
case 'mensual': return true;
case 'bimestral': return month % 2 === 1; // Jan, Mar, May...
case 'trimestral': return [1, 4, 7, 10].includes(month);
case 'cuatrimestral': return [1, 5, 9].includes(month);
case 'anual': return month === 3 || month === 4; // March (PM) or April (PF) — show in both
case 'eventual': return false; // Don't auto-show
default: return true;