Desain Approval Matrix ERP: Berhenti Hardcode Rantai Approval

Foto oleh RDNE Stock project

Foto oleh RDNE Stock project
Fitur approval pertama yang saya rilis untuk ANCoraPRO, ERP yang saya bangun untuk klien nyata dengan React, NestJS, dan PostgreSQL, rantai approval-nya tertulis langsung di kode service: supervisor, lalu finance manager, lalu direktur. Berjalan sempurna selama tepat empat bulan — sampai klien melakukan restrukturisasi, menambah departemen procurement, dan ingin invoice di atas nominal tertentu butuh dua tanda tangan finance. Setiap perubahan itu berarti deployment kode.
Penulisan ulangnya mengajarkan pelajaran yang menjadi inti artikel ini: logika approval adalah konfigurasi bisnis, bukan kode aplikasi. Deliverable-nya adalah approval matrix — aturan yang disimpan sebagai data, dikunci pada band nominal, jenis dokumen, role, dan departemen — yang bisa diedit admin dari layar. Berikut cara saya mendesainnya sekarang, lengkap dengan skema, plus edge case yang pasti akan menemukan Anda di production, didesain atau tidak.
Hardcode terasa lebih cepat di minggu pertama, dan untuk rantai dua langkah memang benar. Tapi organisasi mengganti approver jauh lebih sering daripada perkiraan developer — promosi, resign, restrukturisasi, temuan audit, kebijakan pengeluaran baru. Bandingkan kedua pendekatan secara jujur dan matrix menang di mana-mana kecuali effort awal:
| Aspek | Rantai hardcoded | Matrix configurable |
|---|---|---|
| Mengganti approver atau threshold | Perubahan kode, review, deployment — berhari-hari, dan selamanya butuh developer. | Admin mengedit baris rule di layar — hitungan menit, finance yang memegang kendali. |
| Restrukturisasi organisasi | Tulis ulang semua branch terdampak; risiko regresi tinggi di alur yang tidak disentuh. | Nonaktifkan rule lama, masukkan rule baru dengan tanggal berlaku; dokumen lama tetap menyimpan historinya. |
| Menjawab audit — siapa boleh approve apa, kapan | Membaca git history dan berdoa tanggal deploy cocok dengan pertanyaannya. | Query baris rule dengan valid_from dan valid_until; jawabannya adalah data. |
| Effort build awal | Rendah — rantai if dan dua cek role. | Lebih tinggi — tabel rule, engine resolusi, UI admin. Balik modal di reorg pertama. |
Setiap requirement approval yang pernah saya kumpulkan dari tim finance terurai menjadi empat dimensi. Modelkan keempatnya secara eksplisit dan hampir semua kebijakan muat tanpa perubahan skema:
Jenis dokumen
Purchase order, invoice AP, klaim biaya, pengajuan cuti. Tiap jenis punya rule set sendiri — matrix PO dan matrix expense jarang berbagi threshold.
Band nominal
Rentang half-open dalam IDR: nol sampai 10 juta, 10 sampai 100 juta, 100 juta ke atas. Interval half-open (min inklusif, max eksklusif) mencegah double-match klasik di angka batas.
Departemen atau cost center
Procurement approve dengan cara berbeda dari marketing. Departemen NULL pada rule berarti berlaku se-perusahaan, dan rule departemen spesifik meng-override-nya.
Definisi step approver
Setiap rule yang cocok menghasilkan step berurutan. Step menunjuk ke role, user bernama, atau relasi seperti manager-dari-pemohon, plus quorum untuk approval N-dari-M.
Dokumentasi workflow Dynamics 365 dari Microsoft menggambarkan bentuk persis ini di contoh expense report-nya: laporan 7.000 dolar melewati dua approver, yang 11.000 dolar menambah approver ketiga. Threshold dan partisipannya adalah konfigurasi workflow, bukan logika ter-compile — itu standar yang dipasang sistem besar, dan ERP custom tidak boleh lebih buruk.
Dua tabel menanggung seluruh desain: approval_rule menyimpan dimensi pencocokan, approval_rule_step menyimpan approver berurutan untuk tiap rule. Resolusi memilih rule aktif paling spesifik — departemen spesifik mengalahkan se-perusahaan, lalu priority eksplisit memutus seri:
-- Rules are data, not code
CREATE TABLE approval_rule (
id BIGSERIAL PRIMARY KEY,
document_type TEXT NOT NULL, -- 'PO' | 'AP_INVOICE' | 'EXPENSE'
department_id BIGINT REFERENCES department(id), -- NULL = any
amount_min NUMERIC(18,0) NOT NULL DEFAULT 0, -- IDR, inclusive
amount_max NUMERIC(18,0), -- NULL = no cap
priority INT NOT NULL DEFAULT 100, -- lower wins on overlap
is_active BOOLEAN NOT NULL DEFAULT true,
valid_from DATE NOT NULL,
valid_until DATE -- NULL = open ended
);
CREATE TABLE approval_rule_step (
id BIGSERIAL PRIMARY KEY,
rule_id BIGINT NOT NULL REFERENCES approval_rule(id),
step_order INT NOT NULL, -- 1, 2, 3...
approver_type TEXT NOT NULL, -- 'ROLE' | 'USER' | 'MANAGER_OF'
approver_ref BIGINT NOT NULL,
quorum INT NOT NULL DEFAULT 1, -- N-of-M approvers
UNIQUE (rule_id, step_order)
);
-- Resolution: most specific active rule wins
SELECT r.* FROM approval_rule r
WHERE r.document_type = $1
AND (r.department_id = $2 OR r.department_id IS NULL)
AND $3 >= r.amount_min
AND ($3 < r.amount_max OR r.amount_max IS NULL)
AND r.is_active
AND CURRENT_DATE BETWEEN r.valid_from
AND COALESCE(r.valid_until, CURRENT_DATE)
ORDER BY (r.department_id IS NULL), r.priority
LIMIT 1;Query resolusi berjalan sekali, saat submission. Momen itu penting: engine men-snapshot step hasil resolusi ke tabel approval_instance yang menempel ke dokumen. Dokumen lalu menjalani salinan rantainya sendiri yang dibekukan. Kalau admin mengedit matrix besok, dokumen yang sedang berjalan tidak tersentuh — hanya submission baru yang melihat rule baru.
Pro tip: snapshot-saat-submission adalah perilaku tunggal terpenting yang harus benar. Alternatifnya — me-resolve ulang rule di setiap klik approve — berarti editan admin bisa diam-diam membelokkan dokumen yang sedang di tengah rantai, dan audit trail Anda tidak lagi bisa menjelaskan kenapa dokumen pergi ke siapa.
Lima skenario ini tidak eksotis. Semuanya muncul dalam tahun pertama ERP saya berjalan di production, dan masing-masing butuh keputusan desain eksplisit:
Apa pun keputusan Anda untuk tiap edge case, tuliskan ke engine sebagai kode dan ke SOP klien sebagai satu kalimat per kasus. Mode kegagalan yang mahal bukan memilih kebijakan yang salah — melainkan dua layar yang diam-diam mengimplementasikan kebijakan berbeda.
Memigrasikan ERP yang sudah live dari rantai hardcoded ke matrix adalah migrasi data plus perubahan perilaku, dan urutannya penting:
Approval matrix adalah pembeda antara ERP yang dioperasikan klien dan ERP yang ditunggui developer. Rule sebagai data, snapshot saat submission, resolusi fail-closed, dan delegasi dengan kedaluwarsa — empat keputusan itu mencakup sembilan puluh persen permintaan finance, dan mengubah gejolak struktur organisasi dari deployment menjadi edit form.
Kalau Anda memulai build NestJS hari ini: tahan godaan if-statement. Skema dua tabel di atas berbiaya mungkin seminggu termasuk layar admin. Versi hardcoded juga berbiaya seminggu — hanya saja dicicil di setiap reorganisasi sepanjang umur sistem.