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 `
${cells.map(c => `| ${c} | `).join('')}
`;
}
function tableRow(cells: string[]): string {
return `
${cells.map(c => `| ${c} | `).join('')}
`;
}
function section(title: string, color: string, rowsHtml: string, headers: string[]): string {
return `
${title}
${tableHeader(headers)}
${rowsHtml}
`;
}
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 => `| ${r.label} | ${r.value} |
`)
.join('');
const failedHtml = data.failed.length > 0
? section(
`Jobs fallidos (${data.failed.length})`,
'#dc2626',
data.failed.map(j => tableRow([
`${j.tenantName}
${j.tenantRfc}`,
j.contribuyenteName ? `${j.contribuyenteName}
${j.contribuyenteRfc || ''}` : '—',
j.type,
`${j.errorMessage || 'Sin mensaje'}`,
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([
`${j.tenantName}
${j.tenantRfc}`,
j.contribuyenteName ? `${j.contribuyenteName}
${j.contribuyenteRfc || ''}` : '—',
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([
`${j.tenantName}
${j.tenantRfc}`,
j.contribuyenteName ? `${j.contribuyenteName}
${j.contribuyenteRfc || ''}` : '—',
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([
`${j.tenantName}
${j.tenantRfc}`,
j.contribuyenteName ? `${j.contribuyenteName}
${j.contribuyenteRfc || ''}` : '—',
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([
`${j.tenantName}
${j.tenantRfc}`,
`${j.contribuyenteName}
${j.contribuyenteRfc}`,
])).join(''),
['Tenant', 'Contribuyente']
)
: '';
return baseTemplate(`
${heading('🚨 Alerta de sincronización SAT')}
El monitoreo de sincronizaciones SAT detectó anomalías que requieren revisión interna.
${infoBox(``)}
${failedHtml}
${staleHtml}
${stuckHtml}
${pendingHtml}
${missingHtml}
Reporte generado el ${data.generatedAt} para ${data.recipient}.
Configura umbrales con SAT_STUCK_RUNNING_HOURS y SAT_FAILED_LOOKBACK_HOURS.
`);
}