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(`${summaryRows}
`)} ${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.

`); }