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

@@ -2,7 +2,7 @@ import { baseTemplate, heading, infoBox, primaryButton, BRAND_COLORS as C } from
export interface DocumentoSubidoData {
/** Kind: para el título/subject. */
kind: 'declaracion' | 'extra';
kind: 'declaracion' | 'extra' | 'obligacion_evidencia';
/** Quién subió el documento (email). */
subidoPor: string;
/** RFC del contribuyente. */
@@ -24,25 +24,38 @@ export interface DocumentoSubidoData {
descripcion?: string | null;
categoria?: string | null;
};
/** Si es evidencia de obligación fiscal. */
evidencia?: {
obligacionNombre: string;
periodo: string;
tipoDocumento: string;
filename: string;
};
/** URL al sistema (ej. https://despachos.horuxfin.com/documentos). */
link: string;
/** Solo para declaraciones: los adjuntos se omitieron por exceder el límite de tamaño. */
attachmentsOmitted?: boolean;
}
export function documentoSubidoEmail(data: DocumentoSubidoData): string {
const titulo = data.kind === 'declaracion'
? 'Nueva declaración subida'
: 'Nuevo documento subido';
: data.kind === 'obligacion_evidencia'
? 'Nueva evidencia de obligación fiscal'
: 'Nuevo documento subido';
const contenidoEspecifico = data.kind === 'declaracion' && data.declaracion
? declaracionBlock(data.declaracion)
: data.extra
? extraBlock(data.extra)
: '';
: data.kind === 'obligacion_evidencia' && data.evidencia
? evidenciaBlock(data.evidencia)
: data.extra
? extraBlock(data.extra)
: '';
return baseTemplate(`
${heading(titulo)}
<p style="color:${C.textPrimary};margin:0 0 16px;">
<strong>${escapeHtml(data.subidoPor)}</strong> subió un ${data.kind === 'declaracion' ? 'acuse de declaración' : 'documento'}
<strong>${escapeHtml(data.subidoPor)}</strong> subió ${data.kind === 'obligacion_evidencia' ? 'una evidencia de obligación fiscal' : data.kind === 'declaracion' ? 'un acuse de declaración' : 'un documento'}
para <strong>${escapeHtml(data.contribuyenteNombre)}</strong>.
</p>
${infoBox(`
@@ -57,6 +70,12 @@ export function documentoSubidoEmail(data: DocumentoSubidoData): string {
<div style="margin-top:24px;">
${primaryButton('Ver en el sistema', data.link)}
</div>
${data.kind === 'declaracion' && data.attachmentsOmitted ? `
<p style="color:${C.textMuted};font-size:13px;margin-top:16px;">
Los documentos no se adjuntaron porque exceden el tamaño permitido por correo.
Puedes descargarlos desde el sistema.
</p>
` : ''}
`);
}
@@ -76,6 +95,19 @@ function declaracionBlock(d: NonNullable<DocumentoSubidoData['declaracion']>): s
`;
}
function evidenciaBlock(e: NonNullable<DocumentoSubidoData['evidencia']>): string {
return `
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Obligación</p>
<p style="margin:0 0 12px;color:${C.textPrimary};font-weight:600;">${escapeHtml(e.obligacionNombre)}</p>
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Periodo</p>
<p style="margin:0 0 12px;color:${C.textPrimary};">${escapeHtml(e.periodo)}</p>
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Tipo de documento</p>
<p style="margin:0 0 12px;color:${C.textPrimary};text-transform:capitalize;">${escapeHtml(e.tipoDocumento)}</p>
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Archivo</p>
<p style="margin:0 0 12px;color:${C.textPrimary};">${escapeHtml(e.filename)}</p>
`;
}
function extraBlock(e: NonNullable<DocumentoSubidoData['extra']>): string {
return `
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Documento</p>