Otomasi Email ERP: Memicu Email Transaksional dari Event Bisnis

Foto oleh Unsplash

Foto oleh Unsplash
Aplikasi bisnis tanpa notifikasi email itu menjengkelkan. Di ERP custom kami di Commsult Indonesia, event bisnis penting seharusnya memberi tahu orang-orang yang relevan secara otomatis. Pendekatan naif mengirim email langsung di dalam metode service berhasil sampai tidak berhasil. Post ini menjelaskan sistem email yang terpisah dan didukung queue yang kami bangun.
Prinsip desain inti adalah pemisahan: logika bisnis seharusnya tidak tahu tentang pengiriman email. Service memancarkan event dan melanjutkan. Listener terpisah mengantrekan pekerjaan email. Worker email memproses secara asinkron dengan retry jika gagal.
NestJS memiliki modul EventEmitter bawaan berdasarkan EventEmitter2. Service bisnis memancarkan event yang diketik setelah transaksi inti di-commit. Event listener di modul email berlangganan event-event ini dan mengantrekan pekerjaan email yang sesuai.
BullMQ adalah job queue berbasis Redis untuk Node.js. Job disimpan di Redis sehingga bertahan saat aplikasi restart. Setiap job memiliki logika retry yang dapat dikonfigurasi. Job yang gagal berpindah ke failed queue di mana mereka dapat diperiksa dan dicoba ulang.
ERP Email Automation Architecture
───────────────────────────────────
ERP Event (leave approved, invoice due, etc.)
│
▼
┌─────────────────────────────────────────┐
│ Event Emitter (NestJS EventEmitter2) │
│ this.eventEmitter.emit('invoice.due', │
│ { invoiceId, recipientId }) │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Email Queue (BullMQ + Redis) │
│ Decouples sending from event handler │
│ Retries on failure (3x, exp backoff) │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Email Worker │
│ 1. Load template from DB or .hbs file │
│ 2. Render with Handlebars │
│ 3. Attach PDF if needed │
│ 4. Send via Nodemailer (SMTP / SES) │
└───────────────┬─────────────────────────┘
│
┌───────┴────────┐
│ │
┌─────▼──────┐ ┌──────▼──────┐
│ Sent log │ │ Retry │
│ (DB) │ │ (BullMQ) │
└────────────┘ └─────────────┘Gunakan fitur deduplication job BullMQ (opsi 'jobId') untuk email yang hanya boleh dikirim sekali per objek bisnis. Set jobId ke 'invoice-due-{invoiceId}'—jika pengingat diantrekan dua kali, BullMQ hanya memproses sekali. Ini mencegah duplikat email.
Setiap jenis email memiliki template Handlebars yang sesuai. Template dikompilasi sekali saat inisialisasi modul dan di-cache. Helper template menangani format tanggal Indonesia, format Rupiah, dan pluralisasi.
Nodemailer menangani pengiriman SMTP. Transporter dibuat sekali di processor untuk menggunakan kembali connection pool. Untuk produksi, kami menggunakan SMTP relay khusus yang dikonfigurasi melalui environment variable.
// email.module.ts - BullMQ queue setup
import { BullModule } from '@nestjs/bullmq';
@Module({
imports: [
BullModule.registerQueue({ name: 'email' }),
],
providers: [EmailService, EmailProcessor],
exports: [EmailService],
})
export class EmailModule {}
// email.service.ts - enqueue emails from ERP events
@Injectable()
export class EmailService {
constructor(@InjectQueue('email') private emailQueue: Queue) {}
async sendLeaveApproved(payload: LeaveApprovedPayload): Promise<void> {
await this.emailQueue.add('leave-approved', payload, {
attempts: 3,
backoff: { type: 'exponential', delay: 5000 },
removeOnComplete: 100,
removeOnFail: 500,
});
}
async sendInvoiceDueReminder(payload: InvoiceDuePayload): Promise<void> {
await this.emailQueue.add('invoice-due', payload, {
attempts: 3,
delay: 0,
backoff: { type: 'fixed', delay: 30000 },
});
}
}
// email.processor.ts - the actual sender
@Processor('email')
export class EmailProcessor {
private transporter: nodemailer.Transporter;
constructor(private templateService: TemplateService) {
this.transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
}
@Process('leave-approved')
async handleLeaveApproved(job: Job<LeaveApprovedPayload>) {
const { employee, leaveRequest } = job.data;
const html = await this.templateService.render('leave-approved', {
employeeName: employee.name,
startDate: leaveRequest.startDate,
endDate: leaveRequest.endDate,
daysCount: leaveRequest.daysCount,
});
await this.transporter.sendMail({
from: `"Commsult HR System" <${process.env.SMTP_FROM}>`,
to: employee.email,
subject: 'Leave Request Approved',
html,
});
}
}Setiap email yang terkirim dicatat ke tabel email_logs di Postgres: penerima, subjek, jenis event, entitas terkait, timestamp pengiriman, dan job ID BullMQ. Ini menyediakan audit trail untuk kepatuhan dan debugging.
Selalu gunakan mail catcher (Mailhog atau Mailpit) di lingkungan lokal dan staging—jangan pernah routing email staging ke alamat nyata. Di Docker Compose, tambahkan service mailhog dan arahkan pengaturan SMTP ke sana.
Pengingat faktur jatuh tempo dijadwalkan: cron job harian query faktur yang jatuh tempo dalam 7 hari dan 1 hari yang belum dibayar dan belum mendapat pengingat. Pendekatan ini idempoten—pemeriksaan email_logs mencegah pengiriman duplikat.
BullMQ mengekspos metrik queue melalui API-nya. Kami mengekspos sebagai metrik Prometheus dan men-scrape dengan stack Grafana. Alert muncul ketika failed queue melebihi ambang batas. Dashboard menampilkan tren throughput email untuk perencanaan kapasitas.