Ringkasan Utama
Otomasi email ERP paling baik ketika business logic tidak pernah memanggil layanan email secara langsung. Pengajuan cuti yang disetujui menulis sebuah event, lalu worker terpisah merender dan mengirim pesannya. Pemisahan ini mencegah panggilan SMTP yang gagal membatalkan transaksi yang sudah disetujui, dan membuat pengiriman bisa diulang tanpa menyentuh kode bisnis sama sekali.
Aplikasi bisnis tanpa notifikasi email itu menjengkelkan. Dalam ERP custom yang saya bangun sebagai konsultan ERP lepas, 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 saya bangun.
Arsitektur: Event-Driven dengan Queue
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 EventEmitter2 sebagai Event Bus
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 untuk Pemrosesan Job yang Andal
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.
Template Email dengan Handlebars
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: Transport SMTP
Nodemailer menangani pengiriman SMTP. Transporter dibuat sekali di processor untuk menggunakan kembali connection pool. Untuk produksi, saya 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: `"HR System" <${process.env.SMTP_FROM}>`,
to: employee.email,
subject: 'Leave Request Approved',
html,
});
}
}Melacak Email yang Terkirim
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.
Email Terjadwal: Pengingat Faktur Jatuh Tempo
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.
Pemantauan dan Alerting
BullMQ mengekspos metrik queue melalui API-nya. Saya mengekspos sebagai metrik Prometheus dan men-scrape dengan stack Grafana saya. Alert muncul ketika failed queue melebihi ambang batas. Dashboard menampilkan tren throughput email untuk perencanaan kapasitas.

