import { createEmailTransport } from '@horux/core'; import { env } from '../../config/env.js'; const transport = createEmailTransport( env.SMTP_USER && env.SMTP_PASS ? { host: env.SMTP_HOST, port: parseInt(env.SMTP_PORT), user: env.SMTP_USER, pass: env.SMTP_PASS, from: env.SMTP_FROM, } : null ); async function sendEmail(to: string, subject: string, html: string) { await transport.send(to, subject, html); } export const emailService = { sendWelcome: async (to: string, data: { nombre: string; email: string; tempPassword: string }) => { const { welcomeEmail } = await import('./templates/welcome.js'); await sendEmail(to, 'Bienvenido a Horux360', welcomeEmail(data)); }, sendPasswordReset: async (to: string, data: { nombre: string; resetUrl: string }) => { const { passwordResetEmail } = await import('./templates/password-reset.js'); await sendEmail(to, 'Recuperación de contraseña - Horux360', passwordResetEmail(data)); }, sendFielNotification: async (data: { clienteNombre: string; clienteRfc: string }) => { const { fielNotificationEmail } = await import('./templates/fiel-notification.js'); await sendEmail(env.ADMIN_EMAIL, `[${data.clienteNombre}] subió su FIEL`, fielNotificationEmail(data)); }, sendPaymentConfirmed: async (to: string, data: { nombre: string; amount: number; plan: string; date: string }) => { const { paymentConfirmedEmail } = await import('./templates/payment-confirmed.js'); await sendEmail(to, 'Confirmación de pago - Horux360', paymentConfirmedEmail(data)); }, sendPaymentFailed: async (to: string, data: { nombre: string; amount: number; plan: string }) => { const { paymentFailedEmail } = await import('./templates/payment-failed.js'); await sendEmail(to, 'Problema con tu pago - Horux360', paymentFailedEmail(data)); await sendEmail(env.ADMIN_EMAIL, `Pago fallido: ${data.nombre}`, paymentFailedEmail(data)); }, sendSubscriptionExpiring: async (to: string, data: { nombre: string; plan: string; expiresAt: string }) => { const { subscriptionExpiringEmail } = await import('./templates/subscription-expiring.js'); await sendEmail(to, 'Tu suscripción vence en 5 días', subscriptionExpiringEmail(data)); }, sendSubscriptionCancelled: async (to: string, data: { nombre: string; plan: string }) => { const { subscriptionCancelledEmail } = await import('./templates/subscription-cancelled.js'); await sendEmail(to, 'Suscripción cancelada - Horux360', subscriptionCancelledEmail(data)); await sendEmail(env.ADMIN_EMAIL, `Suscripción cancelada: ${data.nombre}`, subscriptionCancelledEmail(data)); }, sendNewClientAdmin: async (data: { clienteNombre: string; clienteRfc: string; adminEmail: string; adminNombre: string; tempPassword: string; databaseName: string; plan: string; }) => { const { newClientAdminEmail } = await import('./templates/new-client-admin.js'); await sendEmail(env.ADMIN_EMAIL, `Nuevo cliente: ${data.clienteNombre} (${data.clienteRfc})`, newClientAdminEmail(data)); }, sendWeeklyUpdate: async (to: string, data: import('./templates/weekly-update.js').WeeklyUpdateData) => { const { weeklyUpdateEmail } = await import('./templates/weekly-update.js'); await sendEmail(to, `Actualización semanal — ${data.empresa}`, weeklyUpdateEmail(data)); }, sendDespachoWelcome: async (to: string, data: { nombre: string; despachoNombre: string; email: string }) => { const { despachoWelcomeEmail } = await import('./templates/despacho-welcome.js'); await sendEmail(to, `Bienvenido a Horux Despachos — ${data.despachoNombre}`, despachoWelcomeEmail(data)); }, sendTrialReminder: async (to: string, data: { nombre: string; despachoNombre: string; diasRestantes: number; wizardCompleto: boolean }) => { const { trialReminderEmail } = await import('./templates/trial-reminder.js'); const subject = data.diasRestantes <= 3 ? `⚠️ Tu trial termina en ${data.diasRestantes} días — ${data.despachoNombre}` : `Quedan ${data.diasRestantes} días de trial — ${data.despachoNombre}`; await sendEmail(to, subject, trialReminderEmail(data)); }, sendTrialExpired: async (to: string, data: { nombre: string; despachoNombre: string }) => { const { trialExpiredEmail } = await import('./templates/trial-reminder.js'); await sendEmail(to, `Prueba finalizada — ${data.despachoNombre}`, trialExpiredEmail(data)); }, /** * Notifica la subida de una declaración o documento extra al despacho. * `recipients` debe venir deduplicado por el caller. El subject se * genera a partir del kind y RFC del contribuyente. */ sendDocumentoSubido: async ( recipients: string[], data: import('./templates/documento-subido.js').DocumentoSubidoData, ) => { if (recipients.length === 0) return; const { documentoSubidoEmail } = await import('./templates/documento-subido.js'); const html = documentoSubidoEmail(data); const subject = data.kind === 'declaracion' ? `📄 Declaración subida — ${data.contribuyenteRfc}${data.declaracion ? ` (${data.declaracion.impuestos.join('/')} ${data.declaracion.periodo})` : ''}` : `📎 Documento subido — ${data.contribuyenteRfc}${data.extra ? `: ${data.extra.nombre}` : ''}`; // Envío secuencial; fire-and-forget a nivel del caller. Un error en un // destinatario NO debe impedir enviar al siguiente. for (const to of recipients) { try { await sendEmail(to, subject, html); } catch (err: any) { console.error(`[Email] Fallo enviando documento-subido a ${to}:`, err?.message || err); } } }, /** * Notifica al auxiliar de la cartera que un supervisor/owner marcó como * completada una tarea con `solo_supervisor_completa=true`. */ sendTareaCompletada: async ( to: string, data: import('./templates/tarea-completada.js').TareaCompletadaData, ) => { const { tareaCompletadaEmail } = await import('./templates/tarea-completada.js'); await sendEmail( to, `✓ ${data.tareaNombre} — ${data.contribuyenteRfc}`, tareaCompletadaEmail(data), ); }, /** Aprobadores reciben aviso cuando se sube papelería que requiere aprobación. */ sendPapeleriaAprobacionRequerida: async ( to: string, data: import('./templates/papeleria.js').PapeleriaAprobacionRequeridaData, ) => { const { papeleriaAprobacionRequeridaEmail } = await import('./templates/papeleria.js'); await sendEmail( to, `📋 Papelería pendiente — ${data.contribuyenteRfc} (${data.periodo})`, papeleriaAprobacionRequeridaEmail(data), ); }, /** Uploader recibe aviso cuando aprueban o rechazan su papelería. */ sendPapeleriaDecision: async ( to: string, data: import('./templates/papeleria.js').PapeleriaDecisionData, ) => { const { papeleriaDecisionEmail } = await import('./templates/papeleria.js'); const icon = data.estado === 'aprobado' ? '✅' : '❌'; await sendEmail( to, `${icon} Documento ${data.estado} — ${data.contribuyenteRfc}`, papeleriaDecisionEmail(data), ); }, /** * Cron 8:30 AM — alertas fiscales nuevas activadas hoy. Envía un solo * correo por destinatario con el batch completo. Caller debe deduplicar * recipients antes. Una alerta solo se notifica una vez (tracking en * `alertas_notificadas`). */ sendAlertasNuevas: async ( recipients: string[], data: import('./templates/alertas-nuevas.js').AlertasNuevasData, ) => { if (recipients.length === 0 || data.alertas.length === 0) return; const { alertasNuevasEmail } = await import('./templates/alertas-nuevas.js'); const html = alertasNuevasEmail(data); const total = data.alertas.length; const subject = `🚨 ${total} alerta${total === 1 ? '' : 's'} nueva${total === 1 ? '' : 's'} — ${data.contribuyenteRfc}`; for (const to of recipients) { try { await sendEmail(to, subject, html); } catch (err: any) { console.error(`[Email] Fallo enviando alertas-nuevas a ${to}:`, err?.message || err); } } }, /** * Cron 8:30 AM — recordatorio próximo a vencer. Envía un correo por * destinatario. Caller dedupea. Cada ventana (3d/1d/0d) se envía a lo más * una vez por recordatorio (tracking en columnas `email_Xd_at`). */ sendRecordatorioProximo: async ( recipients: string[], data: import('./templates/recordatorio-proximo.js').RecordatorioProximoData, ) => { if (recipients.length === 0) return; const { recordatorioProximoEmail } = await import('./templates/recordatorio-proximo.js'); const html = recordatorioProximoEmail(data); const prefix = data.ventana === '0d' ? '⏰' : data.ventana === '1d' ? '⚠️' : '🗓'; const ventanaLabel = data.ventana === '0d' ? 'HOY' : data.ventana === '1d' ? 'mañana' : 'en 3 días'; const subject = `${prefix} Recordatorio ${ventanaLabel}: ${data.titulo}`; for (const to of recipients) { try { await sendEmail(to, subject, html); } catch (err: any) { console.error(`[Email] Fallo enviando recordatorio-proximo a ${to}:`, err?.message || err); } } }, };