- 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.
135 lines
6.0 KiB
TypeScript
135 lines
6.0 KiB
TypeScript
import { baseTemplate, heading, infoBox, primaryButton, BRAND_COLORS as C } from './base.js';
|
|
|
|
export interface DocumentoSubidoData {
|
|
/** Kind: para el título/subject. */
|
|
kind: 'declaracion' | 'extra' | 'obligacion_evidencia';
|
|
/** Quién subió el documento (email). */
|
|
subidoPor: string;
|
|
/** RFC del contribuyente. */
|
|
contribuyenteRfc: string;
|
|
/** Razón social / nombre del contribuyente. */
|
|
contribuyenteNombre: string;
|
|
/** Nombre del despacho (opcional, se incluye en el body cuando existe). */
|
|
despachoNombre?: string;
|
|
/** Si es declaración: periodo + tipo + impuestos + monto. */
|
|
declaracion?: {
|
|
periodo: string; // "Abril 2026"
|
|
tipo: 'normal' | 'complementaria';
|
|
impuestos: string[]; // ['IVA', 'ISR']
|
|
montoPago: number | null;
|
|
};
|
|
/** Si es extra: nombre del documento + categoria. */
|
|
extra?: {
|
|
nombre: string;
|
|
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'
|
|
: data.kind === 'obligacion_evidencia'
|
|
? 'Nueva evidencia de obligación fiscal'
|
|
: 'Nuevo documento subido';
|
|
|
|
const contenidoEspecifico = data.kind === 'declaracion' && data.declaracion
|
|
? declaracionBlock(data.declaracion)
|
|
: 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ó ${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(`
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Contribuyente</p>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};font-weight:600;">${escapeHtml(data.contribuyenteNombre)}</p>
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">RFC</p>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};font-family:monospace;">${escapeHtml(data.contribuyenteRfc)}</p>
|
|
${contenidoEspecifico}
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Fecha</p>
|
|
<p style="margin:0;color:${C.textPrimary};">${new Date().toLocaleString('es-MX')}</p>
|
|
`)}
|
|
<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>
|
|
` : ''}
|
|
`);
|
|
}
|
|
|
|
function declaracionBlock(d: NonNullable<DocumentoSubidoData['declaracion']>): string {
|
|
const impuestosStr = d.impuestos.join(', ');
|
|
const tipoLabel = d.tipo === 'complementaria' ? 'Complementaria' : 'Normal';
|
|
const montoLabel = d.montoPago == null ? '—' : d.montoPago === 0 ? 'Sin pago' : formatCurrency(d.montoPago);
|
|
return `
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Periodo</p>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};">${escapeHtml(d.periodo)}</p>
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Tipo</p>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};">${tipoLabel}</p>
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Impuestos</p>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};">${escapeHtml(impuestosStr)}</p>
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Monto a pagar</p>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};">${montoLabel}</p>
|
|
`;
|
|
}
|
|
|
|
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>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};font-weight:600;">${escapeHtml(e.nombre)}</p>
|
|
${e.categoria ? `
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Categoría</p>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};">${escapeHtml(e.categoria)}</p>
|
|
` : ''}
|
|
${e.descripcion ? `
|
|
<p style="margin:0 0 6px;color:${C.textMuted};font-size:13px;">Descripción</p>
|
|
<p style="margin:0 0 12px;color:${C.textPrimary};">${escapeHtml(e.descripcion)}</p>
|
|
` : ''}
|
|
`;
|
|
}
|
|
|
|
function formatCurrency(n: number): string {
|
|
return n.toLocaleString('es-MX', { style: 'currency', currency: 'MXN', minimumFractionDigits: 2 });
|
|
}
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s.replace(/[&<>"']/g, (ch) => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
})[ch]!);
|
|
}
|