Memodelkan Kasbon: Kas Bon Karyawan di Sebuah ERP

Foto oleh JasonParis via Openverse (CC BY 2.0)
Kasbon adalah uang muka tunai karyawan — perusahaan memberi karyawan uang sebelum gajian dan menariknya kembali dari gaji berikutnya. Ia tanpa bunga dan tanpa kontrak pinjaman formal, berbeda dari pinjaman bank. Secara akuntansi ia adalah piutang dari karyawan (piutang karyawan), dicatat sebagai aset lancar sampai lunas.
Kasbon dilunasi dengan memotong jumlahnya dari pembayaran gaji karyawan berikutnya, bukan lewat cicilan terpisah. Jika uang muka besar, potongan disebar ke beberapa periode gaji. Di ERP saya mesin penggajian menetralkan saldo terutang terhadap gaji bersih secara otomatis setiap penggajian.
Modul pinjaman mengasumsikan bunga, jadwal amortisasi tetap, dan pencairan bank — semuanya tidak cocok untuk kasbon. Kasbon tanpa bunga, penyetujunya informal, uang dibayar dari laci kasir, dan pelunasannya sekadar dikurangkan dari gaji. Memaksakannya ke modul pinjaman menghasilkan jadwal bunga-nol palsu dan mesin gaji yang tak bisa menetralkan uang muka dengan benar.
Peraturan Pemerintah 36/2021 membatasi total potongan upah, termasuk pelunasan utang karyawan, maksimal 50 persen dari satu kali pembayaran upah, dan mensyaratkan persetujuan tertulis dari karyawan. ERP saya menegakkannya dengan tak pernah memotong lebih dari separuh gaji bersih dalam satu penggajian, menyebar kasbon besar lintas bulan sampai lunas sepenuhnya.
Saldo tersimpan adalah cache yang bisa menyimpang jika dua penggajian berlomba atau sebuah potongan dibalik. Menghitung piutang terutang sebagai jumlah dicairkan dikurangi total semua potongan selalu tepat. Ini pola sumber-kebenaran yang sama yang dipakai untuk stok, di mana total pergerakan bersifat otoritatif dan kolom kuantitas hanya cache.

Foto oleh JasonParis via Openverse (CC BY 2.0)
Ringkasan Utama
Kasbon adalah pinjaman tunai karyawan khas Indonesia: perusahaan memberi uang sekarang dan menariknya kembali dari gaji berikutnya. Di ERP ia butuh empat tahap terkait — pengajuan dan persetujuan, pencairan dari sesi kas yang terbuka, saldo piutang berjalan, dan potong gaji otomatis yang dibatasi hukum. ERP jadi hampir tak pernah menyediakannya, jadi saya membangunnya sendiri.
Setiap UKM Indonesia yang pernah saya tangani berjalan dengan kasbon. Seorang tukang cuci butuh Rp300.000 sebelum gajian untuk keadaan darurat keluarga, supervisor shift menyetujuinya, uang langsung keluar dari laci kasir, dan diam-diam terpotong dari gaji bulan depan. Tanpa bunga, tanpa kontrak pinjaman formal — hanya kepercayaan dan catatan di buku. Ketika saya membangun ERP JID Carwash, pertanyaan pertama pemiliknya bukan soal laporan atau inventaris. Melainkan: bisakah sistem menangani kasbon.
Pertanyaan itu lebih sulit dari kelihatannya. Kasbon menyentuh penanganan kas, data karyawan, piutang akuntansi, dan penggajian sekaligus, dan harus tetap benar bahkan saat shift ditutup di tengah transaksi atau ponsel kehilangan sinyal di lapangan. Berikut cara saya memodelkan semuanya di NestJS, Prisma, dan PostgreSQL — beserta invarian yang menjaganya agar tidak menguapkan uang.
Kasbon adalah piutang dari karyawan, dilunasi dengan memotong upah — akuntan menyebutnya uang muka karyawan atau piutang lain-lain, dan ia berada di sisi aset lancar neraca. ERP global memodelkan pinjaman karyawan sebagai jadwal pinjaman penuh dengan bunga dan amortisasi. Itu bentuk yang salah. Kasbon tidak berbunga, penyetujunya informal, dan pelunasannya sekadar dikurangkan dari penggajian berikutnya. Memaksakannya ke modul pinjaman berarti jadwal bunga-nol palsu dan mesin gaji yang tidak tahu cara menetralkan uang muka terhadap gaji bersih.
Saya memberi kasbon modul Prisma-nya sendiri alih-alih menempelkannya ke penggajian. Record intinya menyimpan siapa yang mengajukan, jumlah dalam rupiah bilangan bulat, statusnya, dan — yang krusial — cakupan cabang, karena setiap kueri daftar dan detail di basis kode ini menyaring berdasarkan branchId. Cakupan cabang itu bukan hiasan: itu perbaikan untuk bug IDOR yang saya temui di awal, di mana supervisor satu cabang bisa membaca record kasbon cabang lain hanya dengan mengubah id di URL.
model Kasbon {
id String @id @default(cuid())
branchId String // every query scopes by this (IDOR fix)
employeeId String
amount Int // integer IDR — never a float
reason String
status KasbonStatus @default(PENDING)
approvedBy String?
disbursedAt DateTime?
// outstanding piutang is DERIVED, not stored here
deductions KasbonDeduction[]
createdAt DateTime @default(now())
@@index([branchId, status])
}
enum KasbonStatus {
PENDING
APPROVED
DISBURSED
SETTLING // partially deducted across payroll runs
SETTLED
REJECTED
}Uang adalah bilangan bulat rupiah biasa, tidak pernah nilai floating-point. Uang muka Rp300.000 disimpan sebagai 300000. Float diam-diam kehilangan rupiah terakhir saat pembagian — dan potong gaji terus-menerus membagi. Satu kolom float saja cukup untuk membuat saldo tak pernah mencapai nol persis.
Sebuah kasbon lahir sebagai PENDING. Karyawan mengajukannya dari aplikasi lapangan Flutter atau supervisor memasukkannya di POS. Persetujuan adalah transisi status, bukan alur tanda tangan — UKM Indonesia tidak mau rantai lima langkah untuk uang muka Rp200.000. Namun saya tetap menegakkan beberapa aturan, dan setiap transisi ditulis ke AuditLog yang hanya-tambah dengan PII disunting, sehingga pemilik selalu bisa menjawab siapa menyetujui apa.
Di sinilah kasbon berhenti menjadi konsep SDM dan menjadi uang tunai nyata yang keluar dari laci. Pencairan hanya diizinkan terhadap sesi kas yang terbuka — record shift yang melacak uang fisik di laci. Membayar kasbon menulis pergerakan kas-keluar yang terikat ke sesi itu, sehingga saat kasir menghitung laci di penutupan, kasbon sudah terhitung dan hitungan tetap seimbang. Jika tidak ada sesi terbuka, pencairan ditolak; Anda tidak bisa mengeluarkan uang yang tidak menjadi tanggung jawab shift mana pun.
Buku kas (CashLedgerEntry) di sistem ini adalah tampilan gabungan turunan, bukan tabel yang ditulis. Pencairan kasbon, penjualan, dan pengeluaran masing-masing disimpan di tabelnya sendiri; buku kas sekadar menggabungkannya saat dibaca. Artinya pencairan kasbon muncul di buku kas otomatis begitu dicairkan — tidak ada penulisan kedua untuk disinkronkan dan tidak ada yang bisa menyimpang.
Kesalahan paling menggoda adalah menyimpan kolom outstandingAmount di kasbon dan menguranginya setiap potongan. Saya menolaknya. Piutang terutang selalu dihitung sebagai jumlah yang dicairkan dikurangi total semua potongan tercatat. Saldo tersimpan adalah cache yang bisa berbohong begitu dua penggajian berlomba atau sebuah potongan dibalik. Saldo turunan tidak bisa — ini pola yang sama yang saya pakai untuk stok, di mana Product.stockQty hanya cache dan SUM pergerakan stok adalah kebenarannya.
// outstanding piutang = disbursed - sum(deductions)
async outstandingFor(kasbonId: string, branchId: string) {
const k = await this.prisma.kasbon.findFirstOrThrow({
where: { id: kasbonId, branchId }, // always branch-scoped
});
const agg = await this.prisma.kasbonDeduction.aggregate({
where: { kasbonId },
_sum: { amount: true },
});
const deducted = agg._sum.amount ?? 0;
return k.amount - deducted; // integer IDR, always exact
}Ketika penggajian dibuat, mesin menarik setiap kasbon DISBURSED atau SETTLING untuk tiap karyawan di cabang itu dan menetralkan saldo terutang terhadap gaji bersih. Dua aturan penting. Pertama, hukum Indonesia (Peraturan Pemerintah 36/2021) membatasi total potongan upah maksimal 50 persen dari satu periode gaji dan mensyaratkan persetujuan tertulis, jadi mesin tak pernah memotong lebih dari separuh gaji bersih dalam satu penggajian — kasbon besar cukup disebar lintas bulan dan statusnya tetap SETTLING sampai mencapai nol. Kedua, tiap potongan membawa kunci idempotensi yang diturunkan dari kasbon dan periode gaji, pola settlement yang sama yang saya pakai untuk pembayaran, sehingga menjalankan ulang atau mencoba lagi penggajian tak pernah memotong ganda.
| Aspek | Modul pinjaman (ERP jadi) | Modul kasbon (yang saya bangun) |
|---|---|---|
| Bunga | Wajib, dipalsukan nol | Tidak ada — bukan kolom |
| Pelunasan | Jadwal amortisasi tetap | Netto ke gaji berikutnya, dibatasi 50 persen |
| Pencairan | Entri transfer bank | Kas keluar dari sesi laci terbuka |
| Saldo | Disimpan, dikurangi | Diturunkan dari potongan |
Hasilnya adalah kasbon yang berperilaku seperti yang sudah dipikirkan pemilik carwash Indonesia — cepat diberikan, terlihat di laci kas, dan hilang dari gaji tanpa siapa pun menyentuh spreadsheet. Di baliknya ia tetap jujur karena uangnya rupiah bilangan bulat, piutangnya selalu dihitung ulang, buku kas menurunkan dirinya sendiri, dan potongan bersifat idempoten serta dibatasi hukum. Tidak ada yang eksotis di situ. Itu sekadar domain yang dimodelkan sebagaimana ia benar-benar bekerja, bukan dibengkokkan agar muat ke modul pinjaman yang dirancang untuk negara lain.