Modeling Kasbon: Employee Cash Advance in an ERP

Photo by JasonParis via Openverse (CC BY 2.0)
Kasbon is an employee cash advance — the company gives an employee money before payday and recovers it from a later salary. It carries no interest and no formal loan contract, unlike a bank loan. In accounting terms it is a receivable from the employee (piutang karyawan), recorded as a current asset until repaid.
Kasbon is repaid by deducting the amount from the employee's next salary payment, not through separate installments. If the advance is large, the deduction is spread across several pay periods. In my ERP the payroll engine nets the outstanding balance against net pay automatically each run.
Loan modules assume interest, a fixed amortization schedule, and bank disbursement — none of which fit kasbon. A kasbon has no interest, an informal approver, cash paid from a till, and repayment that is simply subtracted from payroll. Forcing it into a loan module produces fake zero-interest schedules and a payroll engine that cannot net advances correctly.
Indonesian Government Regulation 36/2021 caps total wage deductions, including employee debt repayment, at 50 percent of a single wage payment, and requires written consent from the employee. My ERP enforces this by never deducting more than half of net pay in one payroll run, spreading a large kasbon across months until it is fully settled.
A stored balance is a cache that can drift if two payroll runs race or a deduction is reversed. Computing the outstanding piutang as the disbursed amount minus the sum of all deductions is always exact. It is the same source-of-truth pattern used for stock, where the sum of movements is authoritative and the quantity field is only a cache.

Photo by JasonParis via Openverse (CC BY 2.0)
Key Takeaway
Kasbon is an Indonesian employee cash advance: the company hands over cash now and recovers it from a later paycheck. In an ERP it needs four linked stages — request and approval, disbursement from an open cash session, a running piutang (receivable) balance, and automatic payroll deduction capped by law. Off-the-shelf ERPs almost never ship this, so I built it.
Every Indonesian SME I have worked with runs on kasbon. A car washer needs 300.000 rupiah before payday for a family emergency, the shift supervisor approves it, cash comes straight out of the till, and it quietly disappears from next month's salary. No interest, no formal loan contract — just trust and a note in a book. When I built the JID Carwash ERP, the owner's first question was not about reports or inventory. It was: can the system handle kasbon.
That question is harder than it looks. Kasbon touches cash handling, employee records, accounting receivables, and payroll all at once, and it has to stay correct even when a shift closes mid-transaction or a phone goes offline in the field. Here is how I modeled the whole thing in NestJS, Prisma, and PostgreSQL — and the invariants that kept it from leaking money.
Kasbon is a receivable from an employee, repaid by deducting from wages — accountants call it an advance to employees or other receivables, and it sits on the current-asset side of the balance sheet. Global ERPs model employee loans as full loan schedules with interest and amortization. That is the wrong shape. A kasbon has no interest, an informal approver, and a repayment that is simply subtracted from the next payroll run. Forcing it into a loan module means fake zero-interest schedules and a payroll engine that does not know how to net an advance against take-home pay.
I gave kasbon its own Prisma module rather than bolting it onto payroll. The core record holds who requested it, the integer-rupiah amount, its status, and — critically — a branch scope, because every list and detail query in this codebase filters by branchId. That branch scoping is not cosmetic: it is the fix for an IDOR bug I hit early, where a supervisor at one branch could read kasbon records from another simply by changing an id in the 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
}Money is a plain integer number of rupiah, never a floating-point value. A 300.000 rupiah advance is stored as 300000. Floats silently lose the last rupiah on division — and payroll deduction divides constantly. One float column is all it takes to make a balance never reach exactly zero.
A kasbon starts life as PENDING. An employee requests it from the Flutter field app or a supervisor enters it at the POS. Approval is a status transition, not a signature workflow — Indonesian SMEs do not want a five-step chain for a 200.000 rupiah advance. But I still enforce a few rules, and every transition is written to the append-only AuditLog with PII redacted, so the owner can always answer who approved what.
This is where kasbon stops being an HR concept and becomes real cash leaving a drawer. Disbursement is only allowed against an open cash session — the shift record that tracks the physical float in the till. Paying out a kasbon writes a cash-out movement tied to that session, so when the cashier counts the drawer at close, the kasbon is already accounted for and the count still balances. If there is no open session, disbursement is refused; you cannot hand out cash that no shift is responsible for.
The cash book (CashLedgerEntry) in this system is a derived union view, never a written table. Kasbon disbursements, sales, and expenses are each stored in their own tables; the ledger simply unions them on read. That means a kasbon payout appears in the cash book automatically the instant it is disbursed — there is no second write to keep in sync and nothing to drift.
The most tempting mistake is to keep an outstandingAmount column on the kasbon and decrement it on each deduction. I refused to. The outstanding piutang is always computed as the disbursed amount minus the sum of all recorded deductions. A stored balance is a cache that can lie the moment two payroll runs race or a deduction is reversed. A derived balance cannot — it is the same pattern I use for stock, where Product.stockQty is only a cache and the SUM of stock movements is the truth.
// 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
}When a payroll run is generated, the engine pulls every DISBURSED or SETTLING kasbon for each employee at that branch and nets the outstanding balance against net pay. Two rules matter. First, Indonesian law (Government Regulation 36/2021) caps total wage deductions at 50 percent of a single pay period and requires written consent, so the engine never deducts more than half of net pay in one run — a large kasbon simply spreads across months and the status stays SETTLING until it reaches zero. Second, each deduction carries an idempotency key derived from the kasbon and the payroll period, the same settlement pattern I use for payments, so re-running or retrying a payroll never double-deducts.
| Concern | Loan module (off-the-shelf) | Kasbon module (what I built) |
|---|---|---|
| Interest | Required, faked as zero | None — not a field |
| Repayment | Fixed amortization schedule | Net against next payroll, capped 50 percent |
| Disbursement | Bank transfer entry | Cash out of an open till session |
| Balance | Stored, decremented | Derived from deductions |
The result is a kasbon that behaves the way an Indonesian carwash owner already thinks about it — quick to grant, visible in the cash drawer, and gone from the paycheck without anyone touching a spreadsheet. Under the hood it stays honest because the money is integer rupiah, the receivable is always recomputed, the cash book derives itself, and deductions are idempotent and legally capped. None of that is exotic. It is just the domain modeled as it actually works, instead of bent to fit a loan module that was designed for a different country.