Initial commit - Horux Despachos NL
This commit is contained in:
210
apps/api/src/services/email/email.service.ts
Normal file
210
apps/api/src/services/email/email.service.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user