Interactive demo · synthetic dataEvery record on this page is fabricated.
PeopleOS Payroll Engine
A month of clock punches, priced to the rupiah by the payroll engine that ran in production until 7 Oct 2026, with every step inspectable.
The production code behind this demo is my own work — architecture, domain and rule design, implementation, statutory rule interpretation, release decision, directing Claude Code against written specs, review of every later change; it is developed further by Prada Dipa — LinkedIn profile, opens in a new tab.
The input
August 2026, one fictional roster. Pick a person, then pick a day. The punches below are EMP-D007's month; selecting anyone else re-runs those same punches through their schedule and pay profile, which is exactly how one payroll run differs person to person.
The day engine
Clock events in, eight payable minute buckets out — plus the flags that decide the allowances. The timing verdict and the money are separate decisions, and neither overrides the other.
2026-08-28 · WORKDAY · NONE
- 08:00
- 22:00
- 1.00 h
- 13.00 h
- 08:00
- 0.00 h
- 08:00–17:00
- 30 min · strict start
| BUCKET | HOURS | PRICED AT |
|---|---|---|
| Regular | 7.00 | × 1 hourly rate |
| Overtime, first hour | 1.00 | × 1.5 combined base |
| Overtime, after first | 5.00 | × 2 combined base |
| Sunday / red-day regular | 0.00 | × 2 hourly rate |
| Sunday overtime, first hour | 0.00 | × 3 combined base |
| Sunday overtime, after first | 0.00 | × 4 combined base |
| Paid annual leave | 0.00 | credited, not worked |
| Paid sick leave | 0.00 | credited, not worked |
- no
- YES
- YES
- no
- YES
- no
- no
- none
- no
The month, and the 78-hour line
Overtime is never requested and never approved here; it is computed from what happened. The month's overtime is then walked chronologically against the company's confirmed monthly threshold, and everything past the line re-prices without a single day being reordered.
CROSSED ON 2026-08-28 · 4.25 H RE-PRICED
- within the threshold, priced where it was earned
- weekday excess → Bonus O/T, × 2 on the combined base
- Sunday excess → Sunday Bonus O/T, kept at × 4
The 78-hour monthly threshold is the company's confirmed rule, not a statutory one. Overtime is walked in date order and, within a day, first hour before after-first — so a crossing splits a single day's bucket rather than moving the day. A Sunday hour never gets cheaper for having been worked late in the month.
| LINE | AMOUNT | ||
|---|---|---|---|
| Regular Hours | Rp3.893.750 | ||
| Overtime First Hour | Rp721.286 | ||
| Overtime After First Hour | Rp3.111.429 | ||
| Bonus Overtime After Threshold | Rp14.143 | ||
| Sunday Regular Hours | Rp1.400.000 | ||
| Sunday Overtime First Hour | Rp169.714 | ||
| Sunday Overtime After First Hour | Rp452.571 | ||
| Sunday Bonus Overtime | Rp452.571 | ||
| Regular Meal | Rp437.000 | ||
| Regular Meal (Sunday/red day) | Rp92.000 | ||
| Attendance Bonus | Rp285.000 | ||
| Overtime Meal | Rp420.000 | ||
| GROSS EARNINGS | Rp11.449.464 |
Exact decimal, not floating point
Every overtime rate is built on a base that does not divide evenly. That is where money quietly goes missing.
- 28285.714285714285714…
- 28285.714285714286234…
The base is a seventh, so it never terminates. A double diverges from it at decimal 12 — 5.2e-13 rupiah — and that error then travels through every overtime multiplier in the rule set, across every overtime minute in the month, before anything is rounded. The amount is trivial. The problem is that nobody can tell you where it came from, and payroll is the one place where “close enough” is a conversation with a person about their rent.
So the engine keeps every intermediate exact and rounds exactly once, half-up to Rp1, at the payroll line — HALF_UP_RP1 in the rule set, never per minute and never per day. Each formula in the table above is that single rounding, and the amount beside it is its result.
This is the base production used until 7 Oct 2026. Since v10.3.0, production divides the 25-day amount by the statutory 173 hours instead: (((hourly rate × 7) + daily meal) × 25) / 173. The engine still keeps it exact and rounds once per payroll line.
Statutory deductions
Contributions first, then tax on a gross the contributions themselves changed. Both engines print their own arithmetic; neither of them is reimplemented above.
| CONTRIBUTION | EMPLOYEE | EMPLOYER |
|---|---|---|
| BPJS Kesehatan | Rp49.500 | Rp198.000 |
| JHT | Rp99.000 | Rp183.150 |
| JP | Rp49.500 | Rp99.000 |
| JKK | — | Rp44.055 |
| JKM | — | Rp14.850 |
| TOTAL | Rp198.000 | Rp539.055 |
JKK is priced by risk class — this employee is MEDIUM. Only the employee column leaves the payslip as cash; the employer column is paid alongside it, and three of its lines become taxable income below.
- K/2
- 2
- yes
- Rp11.706.369
- Rp11.600.001 – Rp12.600.000
- 3.00%
- 3.00%
- Rp351.191
Taxable gross is not the same figure as gross pay: JKK, JKM and BPJS Perusahaan are employer money that counts as the employee's income, while JHT Perusahaan and JP Perusahaan are excluded. That is the confirmed rule the composition function encodes, and it is why the number the tax is charged on is larger than the number in the earnings table.
PPh 21 itself is Indonesian public law. The effective-rate table shipped here is TARIF-PAJAK-2026-v1, and it carries its own note: “Company-configured PPh21 effective-rate table. Requires formal statutory verification before production payroll reliance. Float artifacts in the source normalized to 4-decimal rates.” The contribution rates, caps and floors in bpjs-v1-demo are the same — modelled on the legacy workbook and tracked as unverified. Every one of these was logged as pending confirmation with Accounting rather than quietly assumed correct, which is why the engine says so in its own output instead of leaving it to a comment.
Take-home
The end of the chain, and the only number on this page an employee would recognise.
- Gross earnings
- Rp11.449.464
- BPJS, employee side
- −Rp198.000
- PPh 21
- −Rp351.191
Rp10.900.273
This last subtraction is the only arithmetic on the page the engines did not do. In production it belongs to the orchestrator, along with the payslip, the approval chain and the bank file — none of which are here.
Rp4.375.000
THR uses basic salary only. Every allowance, bonus and overtime rupiah above is excluded from it, which is why a big overtime month does not move this figure at all.
The month grid belongs to this employee. Strict Start is on for his schedule, so an early tap is recorded where it happened and paid from 08:00 — and his August overtime crosses the 78-hour line, which is the one rule a visitor should watch fire.
- Rp25.000
- Rp23.000
- Rp15.000
- none
- K/2
- on file
- medium
- 0
- 2022-03-14
- 08:00–17:00
The day the month crosses 78 overtime hours — and it crosses in the middle of it. Overtime is walked chronologically, first hour before after-first, so this day's after-first bucket splits: most of it stays where it was earned and the last few minutes move to Bonus O/T. The day's own classification is never rewritten; the move is a payroll-level view of it.
- The engines as production ran them until 7 Oct 2026, with the rule sets attendance-v3-2026-08 and payroll-v2-2026-08, unchanged from 28 Aug through v10.1.0. That day production moved on and this port did not:
- v10.2.0 counts a paid leave day, sick leave included, as a regular-meal day (attendance-v3-2026-10).
- v10.3.0 divides the overtime base by the statutory 173 hours: (((hourly rate × 7) + daily meal) × 25) / 173 (payroll-v2-2026-10). The multipliers and the 78-hour threshold are unchanged.
- Indonesian PPh 21 and its monthly effective-rate method. Naming it discloses nothing.
- Operational, not statutory, and the employer is not named anywhere on this page or in the dataset:
- Saturday 5-hour shift credited as 7 regular hours
- Overtime meal threshold basis
- Sunday/red-day multipliers (2x / 3x / 4x)
- Monthly 78-hour overtime threshold
- Daily attendance bonus qualification
- The statutory rates, caps and floors were carried as pending confirmation with Accounting for as long as they were unconfirmed, and the rule sets say so in their own version strings and output. An unverified number that admits it is worth more than a confident one that does not.
What is real, and what is simulated
Runs in your browser
- day-engine.ts — the eight payable minute buckets, Half Day, the Saturday complete-shift credit, the worked-holiday decision tree and break-punch handling
- payroll-engine.ts — the chronological monthly 78-hour overtime threshold, the meal-inclusive combined overtime base, and one half-up-to-Rp1 rounding per payroll line
- bpjs-engine.ts — Kesehatan, JHT, JP, JKK and JKM, with the configured cap, floor and additional-family-member percentage
- pph21-engine.ts — golongan mapping, bracket lookup over the 2026 effective-rate table, the non-NPWP surcharge and the taxable-gross composition
- thr-engine.ts, attendance-rules.ts, effective-start.ts, late-rule.ts — the versioned rule sets, Strict Start resolution and the lateness comparison the day engine calls
- Eight files, 2,731 lines by wc -l, decimal.js as their only dependency. Every formula in the payroll-lines table, and every line behind the four engine traces, is the engine’s own output rather than this page’s description of it.
engine.parity.test.ts rebuilds a 26,623-case matrix across all eight engines and compares a SHA-256 of every output field — plus a per-engine digest and a field-by-field sample — against a fixture generated by the ORIGINAL files in their own repository on 28 Aug 2026. It runs in npm run verify, so a retuned multiplier, a moved threshold or a reworded explanation in the port fails the build. Those originals did not change until 7 Oct 2026, so the match is with production up to that day; production's two changes since are named beside the page, and are not in the port.
Simulated
- The orchestrator. Production’s calc-v2.ts is server-only Prisma, audit trail and session handling — 919 lines by wc -l at the version this page matches, 930 at v10.4.0 — and it was not ported. payroll-run.ts on this page is a much smaller stand-in that decides only the order the engines run in, and computes one number itself: gross − employee deductions − PPh 21.
- Which wage basis feeds the contributions. The engines are real; this page picks the company's fixed contribution value, ((hourly rate × 7) + daily meal) × 25, and lets the contribution engine apply its own cap and floor. In production that wiring lives in the orchestrator.
- Persistence, the HR → Accounting → Director approval chain, the payslip PDF and the bank export
- The attendance hardware: every punch on this page was typed into a fixture by hand, and the month was authored to reach branches a real month rarely reaches all at once
- Every employee, ID, rate and rupiah figure. The roster is invented; data.test.ts asserts that no employer name, national ID, bank account or contact field appears anywhere in it.
- The THR eligibility date, fixed at 2027-03-20 so the figure is stable rather than drifting with the calendar