Otomasi Email ERP: Memicu Email Transaksional dari Event Bisnis

Foto oleh Unsplash
Mengirim email langsung di dalam metode service menggabungkan pengiriman email dengan transaksi inti, sehingga gangguan SMTP bisa membuat operasi bisnis gagal. Dengan BullMQ yang didukung Redis, job email disimpan dan di-retry secara independen, artinya kegagalan SMTP sementara tidak pernah membatalkan atau memblokir event bisnis yang sedang berjalan.
Fitur deduplication job BullMQ digunakan dengan menetapkan jobId yang deterministik (misalnya, 'invoice-due-{invoiceId}') saat mengantrekan job. Jika job yang sama diantrekan dua kali, BullMQ hanya memprosesnya sekali. Untuk pengingat terjadwal, ada pemeriksaan tambahan ke tabel email_logs untuk memastikan pengingat tidak dikirim ulang ke faktur yang sudah dinotifikasi baru-baru ini.
Setiap jenis email memiliki template Handlebars (.hbs) tersendiri yang menghasilkan isi HTML. Template dikompilasi sekali saat inisialisasi modul dan di-cache. Helper template menangani format lokal seperti tanggal Indonesia dan format Rupiah. Dengan menyimpan layout di file .hbs, desainer bisa memperbarui tampilan email tanpa menyentuh kode TypeScript.
Cron job harian menggunakan modul @nestjs/schedule di NestJS menjalankan query untuk faktur yang jatuh tempo dalam 7 hari atau 1 hari, belum dibayar, dan belum mendapat pengingat baru-baru ini. Untuk setiap faktur yang memenuhi syarat, job pengingat BullMQ diantrekan. Cron harian ini idempoten—menjalankannya beberapa kali tidak menghasilkan pengingat duplikat karena pemeriksaan email_logs mencegah pengiriman ulang.
BullMQ mengekspos metrik queue melalui API-nya, yang diubah menjadi metrik Prometheus melalui middleware NestJS kustom dan di-scrape oleh stack Grafana/Prometheus. Alert muncul ketika jumlah failed queue melebihi ambang batas yang dikonfigurasi. Setiap email yang terkirim juga dicatat ke tabel email_logs di Postgres, menyediakan audit trail untuk kepatuhan dan debugging.

Foto oleh Unsplash
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.
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, 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: `"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. 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.