Back to the work

Case notes · Web app

Pegasus

A payroll system for a church organization: point-based salaries, different rules for every worker type, and an approval flow that will not let a mistake reach anyone's pay.

Java · Spring Boot · React · Docker · Live and in daily use · Screenshots show sample data

Why it exists

The church's finance team ran payroll by hand, in spreadsheets, and payroll here has an unusual number of moving parts. The organization pays pastors, retired pastors, evangelists, honorary workers, and office staff, and each group has its own allowance rules and its own tax treatment. On top of that sat a staff loan program, also counted by hand, whose balances had to be reconciled against salary deductions every month.

Every one of those rules was a formula in a sheet, and every month was another chance for a formula to be wrong. A mistyped cell did not announce itself; it just quietly paid someone the wrong amount, or taxed them at the wrong rate, and surfaced weeks later as an awkward conversation.

Pegasus encodes the rules once, computes every slip the same way every time, and puts a review gate between the calculation and anyone's bank account.

The finance team's morning view

Headcount, active loans, outstanding loan balance, and the run that still needs attention, on one screen. The interface is in Bahasa Indonesia because that is who uses it every day.

Before Pegasus, this picture did not exist anywhere. It lived across several spreadsheets and in the head of whoever maintained them.

Pegasus dashboard with employee count, active loans, and pending payroll, sample data
The dashboard: who is paid, what is owed, what still needs review.

Every worker type, its own rules

A church does not have one kind of worker. Pastors, retired pastors on half rate, evangelists, honorary workers, office staff, contract workers: eight types in total, and each carries its own configuration for allowances, social security, tithe, and tax.

Those rules live in per-type configuration, not in code. Office staff get meal and transport allowances, pastors get seniority, retired pastors are exempt from income tax, and one type exists purely for tax reporting. The payroll engine is written once and reads the flags.

Pegasus employee list with different worker types and tax statuses, sample data
Employees: several worker types, each with its own pay and tax rules.

One slip, every rule applied

Salaries are point based: a worker's level maps to points, points times the type's point value makes base pay. On top of that one slip stacks position, spouse, children, and education allowances, employer-paid BPJS premiums, income tax by the government's TER method, a ten percent tithe, and pension contributions.

Every number on the slip is computed from configuration. When the finance team disputes a figure, the slip itself shows the tax category, the rate, and the points that produced it.

Pegasus payslip detail with base salary from points, allowances, and employer contributions, sample data
A single payslip: points times point value, then every allowance and deduction, itemized.

A run is a draft until it is reviewed

Generating a month's payroll computes a slip for every active employee, but the run stays a draft. Each employee must be marked reviewed before the run can be finalized, and the button stays locked until then. The API enforces it too: ask it to finalize early and it refuses, telling you how many people still need eyes on them.

Recalculating a draft is safe by design. Manual one-off adjustments are snapshotted and reattached; recurring ones are re-seeded from current configuration, so a recalculation can never silently duplicate or lose an adjustment.

Pegasus draft payroll run with review progress and locked finalize button, sample data
A draft run: the review gate has to reach everyone before finalize unlocks.

The permanent record

Finalizing is a one-way door. The run locks, payslip PDFs can go out by email, and every finalized month stays in the history exactly as it was approved.

Pegasus payroll history with draft and finalized runs, sample data
Payroll history: drafts are yellow, finalized months are permanent.

The loan book counts itself

Workers can borrow against salary, and this used to be its own hand-maintained sheet. In Pegasus a loan is a schedule: total, monthly installment, start month. When a payroll run is finalized, that month's installment is deducted and the balance moves, so the loan book and the payroll can never disagree.

A loan dated in the future contributes nothing until its start month arrives, and that gate is enforced in two places, because the finalize step deliberately does not trust the slip's loan figure.

Pegasus loan list with installment progress and remaining balance, sample data
Loans: installments deducted by the payroll run itself, never counted by hand.

The hard parts

Eight worker types, one engine

The tempting design was a code path per worker type, and it would have rotted immediately. Instead every type is a row of configuration: which allowances apply, whether BPJS or income tax or tithe is deducted, whether pay lands this period or next. New rules from the finance team become config changes, and the engine stays one engine.

Tax that follows the regulation

Indonesian payroll tax uses the TER method: the family status maps to a category, the category and gross pay map to a monthly rate. The subtle part is the tax base. Employer-paid contributions are added to gross income for transparency but must be subtracted back out before tax, or every worker is overtaxed. The slip shows the category and rate so the number can be checked against the regulation directly.

Recalculation that cannot lose work

A draft run gets recalculated many times as the team fixes data. Each recalculation throws the slips away and computes fresh, so manual adjustments are snapshotted first and reattached after, while recurring adjustments are re-seeded from configuration. One-off decisions survive, config changes propagate, and neither can duplicate the other.

Notes on the build

Spring Boot and MSSQL behind a React frontend, shipped as Docker containers. The schema has grown through thirty-nine Flyway migrations as the finance team's rules evolved, and payslips go out as generated PDFs. The payroll engine's rules are documented in a reference the code is held to, because in payroll the specification is the product.

If you want to talk about this build, or something you want built, say hello on WhatsApp or write to philip.kamdani@gmail.com.

Back to the work