Perusahaan yang mengotomatisasi approval purchase order menghemat rata-rata 16% biaya vendor per tahun dan menghilangkan 46 jam per bulan pekerjaan pengadaan manual, menurut riset ApproveIt. Namun di sebagian besar UKM Indonesia yang saya temui, purchase order masih berjalan melalui WhatsApp, disetujui oleh siapapun yang merespons pertama, dan tidak meninggalkan audit trail. Di Commsult Indonesia, saya membangun workflow approval PO multi-level yang merutekan permintaan ke approver yang tepat — berdasarkan ambang batas nilai, departemen, dan vendor — serta melakukan eskalasi otomatis jika approval terhenti.
Sebelum merancang solusi, saya mengidentifikasi kegagalan paling umum dalam proses approval PO manual: routing ke approver yang salah (tidak ada aturan jelas), approval yang diam-diam kadaluarsa, tidak ada catatan siapa yang menyetujui apa, dan tidak ada penegakan batas pengeluaran. Setiap masalah ini diterjemahkan ke dalam kebutuhan sistem: aturan routing yang dapat dikonfigurasi, eskalasi berbasis timeout, logging audit lengkap, dan penegakan threshold.
Budaya bisnis Indonesia biasanya memerlukan tanda tangan multi-level untuk pembelian di atas nilai tertentu — norma budaya yang sebenarnya selaras dengan kontrol internal. Sistem kami mendukung tier yang dapat dikonfigurasi: Tier 1 (Kepala Departemen) untuk PO di bawah Rp 10.000.000; Tier 2 (Manajer Keuangan) untuk Rp 10M–Rp 100M; Tier 3 (Direktur) di atas Rp 100M. Setiap tier juga bisa dibatasi oleh kategori vendor — pembelian capex selalu memerlukan persetujuan Direktur terlepas dari nilainya. Aturan-aturan ini disimpan dalam tabel database dan dapat dikonfigurasi oleh admin keuangan tanpa deployment kode.
PO Approval Workflow — NestJS + PostgreSQL
┌─────────────────────────────────────────────────────┐
│ Staff submits Purchase Request │
└──────────────────────┬──────────────────────────────┘
│ ApprovalRulesService
│ reads threshold + dept rules
▼
┌─────────────────────────────────────────────────────┐
│ approval_steps created (one row per tier) │
│ ┌─────────────────────────────────────────────┐ │
│ │ Step 1 │ Dept Head │ deadline: +48h │ PEND│ │
│ │ Step 2 │ Fin Manager │ deadline: +48h │ WAIT│ │
│ │ Step 3 │ Director │ deadline: +48h │ WAIT│ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
Email sent Step times out?
(signed JWT) │
│ EscalationService
Approver clicks escalates to
Approve/Reject next tier + notifies
│ manager
▼
┌──────────────────────┐
│ Step marked APPROVED │
│ Next step activates │
└──────────────────────┘Dari pengalaman saya membangun sistem ERP di Commsult: jangan pernah hardcode threshold approval. Direktur keuangan akan menyesuaikannya minimal dua kali setahun, dan deployment kode hanya untuk perubahan threshold adalah pemborosan waktu semua orang. Simpan threshold di tabel approval_rules dan bangun UI admin sederhana untuk mengelolanya. Dua jam pekerjaan konfigurasi di awal menghemat berminggu-minggu permintaan perubahan di masa depan.
Skema inti memiliki tiga tabel: purchase_orders (dokumen), approval_steps (satu baris per approver yang diperlukan per PO), dan approval_actions (event approve/reject/delegasi yang sebenarnya). Ketika PO disubmit, NestJS service membaca aturan approval yang berlaku dan membuat baris approval_steps secara berurutan. Setiap step memiliki status (pending, approved, rejected, delegated, timed_out) dan timestamp deadline.
Kebutuhan dunia nyata yang sering diabaikan: approver pergi cuti. Sistem kami memungkinkan approver mana pun mendelegasikan approval yang pending kepada rekan kerja untuk rentang tanggal tertentu — delegasi dicatat dalam audit trail. Jika tidak ada delegasi dan step timeout (default: 48 jam kerja), sistem secara otomatis mengekskalasi ke approver tier berikutnya dan mengirim notifikasi ke manajer approver asli.
-- PostgreSQL: Core approval tables
CREATE TABLE approval_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tier INT NOT NULL, -- 1, 2, 3
min_amount NUMERIC(15,2),
max_amount NUMERIC(15,2),
department_id UUID REFERENCES departments(id),
approver_role VARCHAR(50) NOT NULL, -- 'dept_head', 'finance_mgr', 'director'
timeout_hours INT NOT NULL DEFAULT 48
);
CREATE TABLE approval_steps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
po_id UUID NOT NULL REFERENCES purchase_orders(id),
tier INT NOT NULL,
approver_id UUID REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
deadline TIMESTAMPTZ NOT NULL,
acted_at TIMESTAMPTZ,
CHECK (status IN ('pending','approved','rejected','delegated','timed_out'))
);
-- NestJS: Advisory lock for race-condition safety
async processApproval(poId: string, stepId: string, action: string) {
await this.dataSource.transaction(async (em) => {
// Acquire advisory lock keyed on PO ID hash
await em.query(
"SELECT pg_advisory_xact_lock(hashtext($1))",
[poId]
);
const step = await em.findOneOrFail(ApprovalStep, {
where: { id: stepId, status: 'pending' }
});
step.status = action; // 'approved' | 'rejected'
step.actedAt = new Date();
await em.save(step);
if (action === 'approved') {
await this.activateNextStep(em, poId, step.tier);
}
});
}Implementasi kami menggunakan NestJS dengan TypeORM untuk workflow engine, PostgreSQL untuk penyimpanan status, dan Bull untuk background jobs. UI approval adalah komponen React yang menampilkan step saat ini, siapa yang telah menyetujui, siapa yang menunggu, dan timeline riwayat lengkap. Approver menerima notifikasi email dengan tautan approve/reject langsung menggunakan signed JWT token — mereka dapat menyetujui tanpa login ke ERP.
Jika workflow Anda memungkinkan approval paralel, waspadai race condition di mana dua approver bertindak bersamaan dan kode Anda membuat rekord approval duplikat. Kami menyelesaikan ini dengan PostgreSQL advisory lock pada PO ID selama transisi status apa pun. Polanya: SELECT pg_advisory_xact_lock(hashtext(po_id::text)) di dalam transaksi sebelum membaca dan menulis approval_steps. Jangan pernah gunakan application-level locks untuk ini — mereka tidak bertahan saat koneksi terputus.
Setiap event approval memicu email transaksional melalui SMTP2GO. Tautan approve/reject dalam email menyematkan token bertanda tangan berumur pendek yang berisi step ID dan user ID approver. Endpoint NestJS memvalidasi token, memeriksa apakah approver masih menjadi approver aktif untuk step tersebut, dan memproses tindakan. Approval email stateless ini adalah salah satu fitur dengan nilai tertinggi — ini memotong rata-rata waktu siklus PO kami dari 3,5 hari menjadi 18 jam di bulan pertama setelah peluncuran.
Setelah data workflow ada di PostgreSQL, membangun pelaporan menjadi mudah. Kami mengekspos: rata-rata waktu siklus approval per departemen dan tier, tingkat approval per approver, approval yang terlambat berdasarkan pemegang saat ini, dan pengeluaran PO bulanan per vendor dan kategori. Metrik waktu siklus saja sudah cukup membenarkan investasi ERP kepada CFO klien — mereka dapat melihat, untuk pertama kalinya, di mana permintaan pembelian terhenti.