feat: add email service with Nodemailer and 6 HTML templates
EmailService with mock fallback when SMTP not configured. Templates: welcome, fiel-notification, payment-confirmed, payment-failed, subscription-expiring, subscription-cancelled. Uses Google Workspace SMTP (STARTTLS). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
79
apps/api/src/services/email/email.service.ts
Normal file
79
apps/api/src/services/email/email.service.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createTransport, type Transporter } from 'nodemailer';
|
||||
import { env } from '../../config/env.js';
|
||||
|
||||
let transporter: Transporter | null = null;
|
||||
|
||||
function getTransporter(): Transporter {
|
||||
if (!transporter) {
|
||||
if (!env.SMTP_USER || !env.SMTP_PASS) {
|
||||
console.warn('[EMAIL] SMTP not configured. Emails will be logged to console.');
|
||||
return {
|
||||
sendMail: async (opts: any) => {
|
||||
console.log('[EMAIL] Would send:', { to: opts.to, subject: opts.subject });
|
||||
return { messageId: 'mock' };
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
transporter = createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: parseInt(env.SMTP_PORT),
|
||||
secure: false, // STARTTLS
|
||||
auth: {
|
||||
user: env.SMTP_USER,
|
||||
pass: env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
}
|
||||
return transporter;
|
||||
}
|
||||
|
||||
async function sendEmail(to: string, subject: string, html: string) {
|
||||
const transport = getTransporter();
|
||||
try {
|
||||
await transport.sendMail({
|
||||
from: env.SMTP_FROM,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
text: html.replace(/<[^>]*>/g, ''),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[EMAIL] Error sending email:', error);
|
||||
// Don't throw — email failure shouldn't break the main flow
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
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));
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user