3 menit bacaDony Marsahid
Membangun Modul Finance ERP yang Dipercaya Akuntan
Ledger, jurnal, dan audit trail: aturan engineering yang menjaga angka keuangan tetap benar saat sistem bisnis tumbuh dari satu gedung menjadi banyak.
- ERP
- Finance
- MongoDB
- Node.js
01 / 02 · Membangun Modul Finance ERP yang Dipercaya Akuntan
Sebagian besar sistem bisnis lama-kelamaan menjadi sistem finance. Platform properti menagih penghuni; platform loyalty mengubah poin menjadi kewajiban; aplikasi gudang membayar kurir dan tukang. Begitu uang bergerak, tim akuntansi menjadi pengguna paling menuntut — dan mereka benar.
Artikel ini merangkum aturan yang saya pakai saat membangun modul finance dengan Node.js dan MongoDB. Tidak ada yang eksotis. Tapi bersama-sama, aturan ini membedakan dashboard yang sekadar menampilkan angka dengan sistem yang benar-benar mau ditandatangani akuntan.
Mulai dari jurnal, bukan laporan
Manajer meminta laporan: Laba Rugi, Trial Balance, daftar umur piutang. Godaannya adalah membuat setiap laporan sebagai query tersendiri atas invoice, pembayaran, dan biaya. Jangan.
Setiap fakta keuangan harus lebih dulu menjadi entri jurnal: catatan bertanggal dengan minimal dua baris yang total debitnya sama dengan total kreditnya. Laporan hanyalah tampilan dari baris jurnal yang sudah diposting, dikelompokkan per akun dan periode.
interface JournalLine {
accountCode: string; // mis. "1-1100" Kas, "4-1000" Pendapatan
debit: number; // dalam satuan mata uang terkecil
credit: number;
}
interface JournalEntry {
number: string; // JV-2026-10-000123, berurutan per periode
date: Date;
source: { type: "invoice" | "payment" | "expense"; id: string };
lines: JournalLine[];
status: "draft" | "posted" | "void";
}
Kalau General Ledger, Trial Balance, dan Laba Rugi membaca dari baris jurnal yang sama, ketiganya tidak mungkin saling bertentangan. Di satu platform properti, memindahkan laporan ke jurnal memangkas rekap bulanan dari sekitar seminggu menjadi satu-dua jam, karena tidak ada lagi yang perlu mencocokkan laporan satu dengan lainnya.
Simpan uang sebagai integer
Bilangan floating-point tidak bisa merepresentasikan sebagian besar nilai desimal secara tepat. Simpan nominal sebagai integer dalam satuan terkecil (sen, atau rupiah utuh bila tanpa desimal), dan format hanya di tampilan. Validasi setiap entri seimbang sebelum disimpan:
const totalDebit = lines.reduce((s, l) => s + l.debit, 0);
const totalCredit = lines.reduce((s, l) => s + l.credit, 0);
if (totalDebit !== totalCredit) throw new Error("Unbalanced journal entry");
Sudah diposting berarti tidak bisa diubah
Akuntan tidak menghapus sejarah; mereka mengoreksinya. Setelah entri diposting, jangan pernah update atau hapus. Untuk memperbaiki kesalahan, posting entri pembalik lalu entri yang benar. API Anda sebaiknya membuat cara yang salah menjadi mustahil: tidak ada PATCH untuk entri yang sudah diposting, dan ada endpoint /reverse khusus yang membuat entri cerminan lalu menautkan keduanya.
Hal yang sama berlaku untuk dokumen yang menghasilkan entri. Invoice yang sudah dibayar sebaiknya di-void dan diterbitkan ulang, bukan diedit di tempat. Soft delete dengan halaman trash boleh untuk draft dan master data, tidak untuk transaksi yang sudah diposting.
Jadikan setiap penulisan idempotent
Payment gateway mengulang webhook. Pengguna klik dua kali. Jaringan mobile mengirim ulang request. Kalau notifikasi pembayaran datang dua kali, sistem Anda harus mencatat satu pembayaran, bukan dua.
- Terima idempotency key di setiap endpoint yang menciptakan pergerakan uang, dan simpan dengan unique index.
- Untuk webhook gateway, pakai ID transaksi dari gateway sebagai key.
- Kembalikan hasil awal saat key duplikat datang, alih-alih gagal dengan keras.
await payments.createIndex({ idempotencyKey: 1 }, { unique: true });
Pakai transaksi saat dokumen harus sepakat
Menandai invoice lunas, mencatat pembayaran, dan memposting jurnal harus berhasil atau gagal bersamaan. MongoDB mendukung transaksi multi-dokumen di replica set, jadi pakai untuk beberapa jalur kritis ini:
const session = await mongoose.startSession();
await session.withTransaction(async () => {
await Payment.create([payment], { session });
await Invoice.updateOne({ _id: invoiceId }, { status: "paid" }, { session });
await JournalEntry.create([entry], { session });
});
Buat transaksi sesingkat mungkin, dan jauhkan pekerjaan lambat seperti membuat PDF atau mengirim email dari jalur request; kerjakan setelah commit.
Tinggalkan audit trail yang bisa dibaca orang
Setiap perubahan data keuangan butuh siapa, kapan, apa yang berubah, dan kenapa. Simpan pelaku, waktu, nilai sebelum dan sesudah, serta alasan wajib untuk pembalikan. Lalu tampilkan di UI. Audit log yang hanya bisa di-query developer tidak membantu tim finance di akhir bulan.
Status verifikasi juga membantu. Di sistem finance gudang yang saya bangun, setiap biaya bergerak melalui langkah yang jelas — dicatat, dicek, dibayar, dicek setelah bayar, selesai — dan setiap langkah mencatat pengguna dan tanggalnya. Sengketa cukup diselesaikan dengan membaca riwayat, bukan menggali percakapan chat.
Tutup periode
Setelah satu bulan dilaporkan, kunci bulan itu. Tambahkan tabel periode dengan status open / closed dan tolak setiap posting bertanggal di dalam periode yang sudah ditutup. Koreksi yang terlambat masuk ke periode berjalan dengan referensi yang jelas. Satu aturan ini melindungi setiap laporan yang sudah dikirim ke manajemen.
Rekonsiliasi otomatis, review manual
Bangun tampilan rekonsiliasi yang membandingkan apa yang seharusnya cocok: settlement gateway dengan pembayaran, mutasi bank dengan akun kas, total invoice dengan saldo piutang. Biarkan sistem menyorot selisihnya, dan biarkan manusia yang memutuskan tindakannya.
Checklist sebelum rilis
- Setiap pergerakan uang menghasilkan entri jurnal yang seimbang.
- Nominal disimpan sebagai integer; format hanya di UI.
- Entri yang diposting tidak bisa diubah; koreksi memakai pembalikan.
- Endpoint yang menciptakan uang bersifat idempotent.
- Update multi-dokumen yang kritis berjalan di dalam transaksi.
- Setiap perubahan bisa diaudit — dan audit trail-nya terlihat.
- Periode yang ditutup menolak posting.
Modul finance menghargai engineering yang membosankan dan teliti. Benahi fondasinya, dan laporan menjadi bagian yang paling mudah.
Dony Marsahid
Senior Fullstack Engineer (Tech Lead) · ERP & Sistem Bisnis Terintegrasi AI
→
Artikel lainnya
4 menit baca
AI-Assisted Development dengan Disiplin Engineering
Spec, memory project, gerbang verifikasi, dan catatan handover: cara membiarkan AI agent menulis sebagian besar kode tanpa menyerahkan keputusan kepadanya.
- AI
- Claude Code
- MCP
- Workflow
4 menit baca
Merilis React Native Tanpa Expo: Checklist Rilis untuk iOS dan Android
Signing, versioning, review store, dan detail native kecil yang menentukan apakah rilis React Native bare berjalan mulus atau tertahan seminggu.
- React Native
- iOS
- Android
- Release
Mulai project
Ceritakan apa yang perlu dikerjakan sistemnya. Saya balas dengan pertanyaan, pendekatan, dan timeline yang realistis.

