Sistem menggunakan tiga 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; dan Tier 3 (Direktur) di atas Rp 100M. Kategori vendor juga memengaruhi routing — pembelian capex selalu memerlukan persetujuan Direktur terlepas dari nilainya. Yang terpenting, aturan-aturan ini disimpan di tabel approval_rules dan dikelola melalui UI admin, sehingga admin keuangan dapat menyesuaikan threshold tanpa deployment kode.
Setiap approver dapat mendelegasikan approval yang pending kepada rekan kerja untuk rentang tanggal tertentu, dan delegasi tersebut dicatat dalam audit trail. Jika tidak ada delegasi dan step melewati deadline-nya (default: 48 jam kerja), sistem secara otomatis mengekskalasi ke approver tier berikutnya dan mengirim notifikasi ke manajer approver asli. Mekanisme ini menghilangkan masalah 'PO terhenti di inbox seseorang' yang sebelumnya menghambat kecepatan pengadaan.
Setiap step approval memicu email transaksional melalui SMTP2GO yang berisi tautan approve/reject langsung, yang menyematkan signed JWT token berumur pendek berisi step ID dan user ID approver. Endpoint NestJS memvalidasi token, memastikan approver masih menjadi approver aktif untuk step tersebut, lalu memproses tindakannya. Approval email stateless ini dibangun agar direktur yang sibuk dan tidak menggunakan ERP setiap hari tetap bisa bertindak tanpa login — hasilnya waktu siklus PO rata-rata turun dari 3,5 hari menjadi 18 jam di bulan pertama setelah peluncuran.
Ketika workflow memungkinkan approval paralel di tier yang sama, dua approver yang bertindak di saat bersamaan dapat membuat rekord approval duplikat atau salah memajukan status workflow. Solusinya adalah PostgreSQL advisory lock pada PO ID — tepatnya SELECT pg_advisory_xact_lock(hashtext(po_id::text)) di dalam transaksi sebelum membaca atau menulis approval_steps. Application-level locks sengaja dihindari karena tidak bertahan saat koneksi terputus.
Data workflow di PostgreSQL menggerakkan dashboard React yang menampilkan: rata-rata waktu siklus approval per departemen dan tier, tingkat approval per approver (untuk mengidentifikasi bottleneck), approval yang terlambat berdasarkan pemegang saat ini, dan pengeluaran PO bulanan per vendor dan kategori. Metrik waktu siklus sangat bernilai — untuk pertama kalinya CFO klien dapat melihat dengan jelas di mana permintaan pembelian terhenti, sehingga investasi ERP pun terjustifikasi.
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. Dalam pekerjaan konsultasi ERP freelance saya, 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 yang saya bangun 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 sebagai konsultan freelance: 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. Alur kerja yang saya rancang 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 saya 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. Saya 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 klien dari 3,5 hari menjadi 18 jam di bulan pertama setelah peluncuran.
Setelah data workflow ada di PostgreSQL, membangun pelaporan menjadi mudah. Saya 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.