docs: actualiza CAMBIOS-2026-05-04.md y commitea fixes pendientes
- Monitor de sincronización SAT (sat-sync-monitor.job + alerta por correo). - Scraper de CSF más robusto (iframes, blobs, popups, validación PDF). - Reactivación de contribuyentes desactivados y limpieza al desactivar. - Timeout de constancia aumentado a 5 min. - Variables de entorno SAT en .env.example y env.ts.
This commit is contained in:
194
apps/api/src/services/email/templates/sat-sync-alert.ts
Normal file
194
apps/api/src/services/email/templates/sat-sync-alert.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { baseTemplate, heading, infoBox, BRAND_COLORS as C } from './base.js';
|
||||
|
||||
export interface SatSyncAlertData {
|
||||
generatedAt: string;
|
||||
recipient: string;
|
||||
summary: {
|
||||
failed: number;
|
||||
stale: number;
|
||||
stuckRunning: number;
|
||||
pendingOld: number;
|
||||
missingInitial: number;
|
||||
};
|
||||
failed: Array<{
|
||||
tenantName: string;
|
||||
tenantRfc: string;
|
||||
contribuyenteName?: string | null;
|
||||
contribuyenteRfc?: string | null;
|
||||
type: string;
|
||||
errorMessage?: string | null;
|
||||
completedAt?: Date | string | null;
|
||||
}>;
|
||||
stale: Array<{
|
||||
id: string;
|
||||
tenantName: string;
|
||||
tenantRfc: string;
|
||||
contribuyenteName?: string | null;
|
||||
contribuyenteRfc?: string | null;
|
||||
type: string;
|
||||
kind: 'pending-stale' | 'running-stale';
|
||||
ageHours: number;
|
||||
}>;
|
||||
stuckRunning: Array<{
|
||||
id: string;
|
||||
tenantName: string;
|
||||
tenantRfc: string;
|
||||
contribuyenteName?: string | null;
|
||||
contribuyenteRfc?: string | null;
|
||||
type: string;
|
||||
progressPercent: number;
|
||||
startedAt?: Date | string | null;
|
||||
hoursRunning: number;
|
||||
}>;
|
||||
pendingOld: Array<{
|
||||
id: string;
|
||||
tenantName: string;
|
||||
tenantRfc: string;
|
||||
contribuyenteName?: string | null;
|
||||
contribuyenteRfc?: string | null;
|
||||
type: string;
|
||||
createdAt?: Date | string | null;
|
||||
nextRetryAt?: Date | string | null;
|
||||
hoursPending: number;
|
||||
}>;
|
||||
missingInitial: Array<{
|
||||
tenantName: string;
|
||||
tenantRfc: string;
|
||||
contribuyenteName: string;
|
||||
contribuyenteRfc: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function fmtDate(value?: Date | string | null): string {
|
||||
if (!value) return 'N/A';
|
||||
const d = typeof value === 'string' ? new Date(value) : value;
|
||||
return d.toLocaleString('es-MX', { timeZone: 'America/Mexico_City' });
|
||||
}
|
||||
|
||||
function tableHeader(cells: string[]): string {
|
||||
return `<tr>
|
||||
${cells.map(c => `<th align="left" style="padding:8px 12px;background-color:${C.bgLight};color:${C.textMuted};font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;border-bottom:1px solid ${C.border};">${c}</th>`).join('')}
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function tableRow(cells: string[]): string {
|
||||
return `<tr>
|
||||
${cells.map(c => `<td style="padding:10px 12px;border-bottom:1px solid ${C.border};color:${C.textPrimary};font-size:13px;vertical-align:top;">${c}</td>`).join('')}
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function section(title: string, color: string, rowsHtml: string, headers: string[]): string {
|
||||
return `
|
||||
<h3 style="font-family:'Inter', sans-serif;font-weight:600;color:${color};margin:28px 0 12px;font-size:16px;">${title}</h3>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
||||
<thead>${tableHeader(headers)}</thead>
|
||||
<tbody>${rowsHtml}</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
export function satSyncAlertEmail(data: SatSyncAlertData): string {
|
||||
const { summary } = data;
|
||||
|
||||
const summaryRows = [
|
||||
{ label: 'Jobs fallidos recientes', value: summary.failed, color: summary.failed > 0 ? '#dc2626' : C.textPrimary },
|
||||
{ label: 'Jobs stale detectados', value: summary.stale, color: summary.stale > 0 ? '#dc2626' : C.textPrimary },
|
||||
{ label: 'Running atorados sin progreso', value: summary.stuckRunning, color: summary.stuckRunning > 0 ? '#f59e0b' : C.textPrimary },
|
||||
{ label: 'Pending sin atender', value: summary.pendingOld, color: summary.pendingOld > 0 ? '#f59e0b' : C.textPrimary },
|
||||
{ label: 'Contribuyentes con FIEL sin sync inicial', value: summary.missingInitial, color: summary.missingInitial > 0 ? '#dc2626' : C.textPrimary },
|
||||
]
|
||||
.map(r => `<tr><td style="padding:6px 0;color:${C.textMuted};">${r.label}</td><td style="padding:6px 0;color:${r.color};font-weight:600;text-align:right;">${r.value}</td></tr>`)
|
||||
.join('');
|
||||
|
||||
const failedHtml = data.failed.length > 0
|
||||
? section(
|
||||
`Jobs fallidos (${data.failed.length})`,
|
||||
'#dc2626',
|
||||
data.failed.map(j => tableRow([
|
||||
`<strong>${j.tenantName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.tenantRfc}</span>`,
|
||||
j.contribuyenteName ? `<strong>${j.contribuyenteName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.contribuyenteRfc || ''}</span>` : '—',
|
||||
j.type,
|
||||
`<span style="color:#dc2626;">${j.errorMessage || 'Sin mensaje'}</span>`,
|
||||
fmtDate(j.completedAt),
|
||||
])).join(''),
|
||||
['Tenant', 'Contribuyente', 'Tipo', 'Error', 'Fecha fallo']
|
||||
)
|
||||
: '';
|
||||
|
||||
const staleHtml = data.stale.length > 0
|
||||
? section(
|
||||
`Jobs stale detectados por el watchdog (${data.stale.length})`,
|
||||
'#dc2626',
|
||||
data.stale.map(j => tableRow([
|
||||
`<strong>${j.tenantName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.tenantRfc}</span>`,
|
||||
j.contribuyenteName ? `<strong>${j.contribuyenteName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.contribuyenteRfc || ''}</span>` : '—',
|
||||
j.type,
|
||||
j.kind === 'running-stale' ? 'Running abandonado' : 'Pending abandonado',
|
||||
`${j.ageHours}h`,
|
||||
])).join(''),
|
||||
['Tenant', 'Contribuyente', 'Tipo', 'Problema', 'Antigüedad']
|
||||
)
|
||||
: '';
|
||||
|
||||
const stuckHtml = data.stuckRunning.length > 0
|
||||
? section(
|
||||
`Running atorados sin avance (${data.stuckRunning.length})`,
|
||||
'#f59e0b',
|
||||
data.stuckRunning.map(j => tableRow([
|
||||
`<strong>${j.tenantName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.tenantRfc}</span>`,
|
||||
j.contribuyenteName ? `<strong>${j.contribuyenteName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.contribuyenteRfc || ''}</span>` : '—',
|
||||
j.type,
|
||||
`${j.progressPercent}%`,
|
||||
`${j.hoursRunning}h`,
|
||||
fmtDate(j.startedAt),
|
||||
])).join(''),
|
||||
['Tenant', 'Contribuyente', 'Tipo', 'Progreso', 'Tiempo', 'Inicio']
|
||||
)
|
||||
: '';
|
||||
|
||||
const pendingHtml = data.pendingOld.length > 0
|
||||
? section(
|
||||
`Pending sin atender (${data.pendingOld.length})`,
|
||||
'#f59e0b',
|
||||
data.pendingOld.map(j => tableRow([
|
||||
`<strong>${j.tenantName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.tenantRfc}</span>`,
|
||||
j.contribuyenteName ? `<strong>${j.contribuyenteName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.contribuyenteRfc || ''}</span>` : '—',
|
||||
j.type,
|
||||
`${j.hoursPending}h`,
|
||||
j.nextRetryAt ? fmtDate(j.nextRetryAt) : 'Sin reintento',
|
||||
])).join(''),
|
||||
['Tenant', 'Contribuyente', 'Tipo', 'Tiempo pendiente', 'Próximo reintento']
|
||||
)
|
||||
: '';
|
||||
|
||||
const missingHtml = data.missingInitial.length > 0
|
||||
? section(
|
||||
`Contribuyentes con FIEL sin sync inicial (${data.missingInitial.length})`,
|
||||
'#dc2626',
|
||||
data.missingInitial.map(j => tableRow([
|
||||
`<strong>${j.tenantName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.tenantRfc}</span>`,
|
||||
`<strong>${j.contribuyenteName}</strong><br/><span style="color:${C.textMuted};font-size:11px;">${j.contribuyenteRfc}</span>`,
|
||||
])).join(''),
|
||||
['Tenant', 'Contribuyente']
|
||||
)
|
||||
: '';
|
||||
|
||||
return baseTemplate(`
|
||||
${heading('🚨 Alerta de sincronización SAT')}
|
||||
<p style="color:${C.textPrimary};margin:0 0 16px;">
|
||||
El monitoreo de sincronizaciones SAT detectó anomalías que requieren revisión interna.
|
||||
</p>
|
||||
${infoBox(`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">${summaryRows}</table>`)}
|
||||
|
||||
${failedHtml}
|
||||
${staleHtml}
|
||||
${stuckHtml}
|
||||
${pendingHtml}
|
||||
${missingHtml}
|
||||
|
||||
<p style="color:${C.textMuted};margin:24px 0 0;font-size:12px;">
|
||||
Reporte generado el ${data.generatedAt} para ${data.recipient}.<br/>
|
||||
Configura umbrales con SAT_STUCK_RUNNING_HOURS y SAT_FAILED_LOOKBACK_HOURS.
|
||||
</p>
|
||||
`);
|
||||
}
|
||||
Reference in New Issue
Block a user