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.

Read the case study

01The 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.

EMPLOYEE

THE MONTH · AUGUST 2026

MON
TUE
WED
THU
FRI
SAT
SUN
DROP A DIFFERENT DAY ONTO THE MONTH

One lived month cannot honestly contain every branch — nobody is on paid sick leave and unpaid leave at once. These substitute a what-if day for the recorded one, and the whole month re-prices underneath it. Two of them pay nothing at all, on purpose.

02The 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

RULE SET ATTENDANCE-V3-2026-08

CLOCK IN
08:00
CLOCK OUT
22:00
BREAK DEDUCTED
1.00 h
ACTUAL PAYABLE
13.00 h
PAYABLE WORK BEGAN
08:00
UNPAID PRE-SHIFT
0.00 h
SCHEDULED
08:00–17:00
LATENESS GRACE
30 min · strict start
THE EIGHT PAYABLE BUCKETS
BUCKETHOURSPRICED AT
Regular7.00× 1 hourly rate
Overtime, first hour1.00× 1.5 combined base
Overtime, after first5.00× 2 combined base
Sunday / red-day regular0.00× 2 hourly rate
Sunday overtime, first hour0.00× 3 combined base
Sunday overtime, after first0.00× 4 combined base
Paid annual leave0.00credited, not worked
Paid sick leave0.00credited, not worked
  • HALF DAY no
  • ATTENDANCE BONUS YES
  • REGULAR MEAL YES
  • SUNDAY MEAL no
  • OVERTIME MEAL YES
  • NO BREAK PUNCHED no
  • FIXED HOLIDAY PAID no
  • HOLIDAY TREATMENT none
  • SAVING DAY EARNED no

03The 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.

MONTHLY OVERTIME82.25 h

THRESHOLD78.00 h

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.

PAYROLL LINES · EACH ROUNDED ONCE, HALF-UP TO RP1
LINEAMOUNT
Regular HoursREG155.75 h · 155.75 h x Rp25.000Rp3.893.750
Overtime First HourOT117.00 h · 17.00 h x Combined Base x 1.5Rp721.286
Overtime After First HourOT255.00 h · 55.00 h x Combined Base x 2Rp3.111.429
Bonus Overtime After ThresholdBONUS_OT0.25 h · 0.25 h x Combined Base x 2 (above the monthly 78h threshold)Rp14.143
Sunday Regular HoursSUN_REG28.00 h · 28.00 h x Rp25.000 x 2Rp1.400.000
Sunday Overtime First HourSUN_OT12.00 h · 2.00 h x Combined Base x 3Rp169.714
Sunday Overtime After First HourSUN_OT24.00 h · 4.00 h x Combined Base x 4Rp452.571
Sunday Bonus OvertimeSUN_BONUS_OT4.00 h · 4.00 h x Combined Base x 4 (Sunday excess keeps the Sunday rate)Rp452.571
Regular MealMEAL19 day(s) · 19 qualifying day(s) x Rp23.000Rp437.000
Regular Meal (Sunday/red day)SUN_MEAL4 day(s) · 4 Sunday/red day(s) x Rp23.000 (same regular-meal amount)Rp92.000
Attendance BonusATT_BONUS19 day(s) · 19 qualifying day(s) x Rp15.000Rp285.000
Overtime MealOT_MEAL14 day(s) · 14 qualifying day(s) x Rp30.000Rp420.000
GROSS EARNINGSRp11.449.464

04Exact decimal, not floating point

Every overtime rate is built on a base that does not divide evenly. That is where money quietly goes missing.

COMBINED OVERTIME BASE · ((Rp25.000 × 7) + Rp23.000) / 7

THE ENGINE · decimal.js, KEPT UNROUNDED
28285.714285714285714…
THE SAME EXPRESSION IN AN IEEE-754 DOUBLE
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.

05Statutory 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 BASIS · ((Rp25.000 × 7) + Rp23.000) × 25 = Rp4.950.000

CONTRIBUTIONEMPLOYEEEMPLOYER
BPJS KesehatanRp49.500Rp198.000
JHTRp99.000Rp183.150
JPRp49.500Rp99.000
JKK—Rp44.055
JKM—Rp14.850
TOTALRp198.000Rp539.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.

PPh 21 · MONTHLY EFFECTIVE-RATE METHOD

PTKP STATUS
K/2
GOLONGAN
2
NPWP ON FILE
yes
TAXABLE GROSS
Rp11.706.369
BRACKET
Rp11.600.001 – Rp12.600.000
TABLE RATE
3.00%
APPLIED RATE
3.00%
PPH 21
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.

VERIFICATION STATUS

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.

06Take-home

The end of the chain, and the only number on this page an employee would recognise.

Gross earningsthe payroll lines above
Rp11.449.464
BPJS, employee sideKesehatan + JHT + JP
−Rp198.000
PPh 213.00% of the taxable gross
−Rp351.191

TAKE-HOME

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.

BEYOND THE MONTH · THR

Rp4.375.000 · 60 MONTHS OF SERVICE · FULL · ELIGIBILITY 2027-03-20

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.

WHAT THIS REPLACED — THE SHEETS ATTENDANCE SYSTEM

EMP-D007 · SEWING · GOLONGAN 2

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.

HOURLY RATE
Rp25.000
REGULAR MEAL
Rp23.000
ATTENDANCE BONUS
Rp15.000
SKILL BONUS
none
PTKP STATUS
K/2
NPWP
on file
JKK CLASS
medium
EXTRA FAMILY
0
SERVICE SINCE
2022-03-14
SCHEDULE
08:00–17:00

2026-08-28

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.

WHERE EACH RULE COMES FROM

WHICH PRODUCTION VERSION
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.
PUBLIC LAW
Indonesian PPh 21 and its monthly effective-rate method. Naming it discloses nothing.
THE COMPANY'S CONFIRMED RULES
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
TRACKED AS UNVERIFIED
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