KPI Media · Internal · For Arul, Agus and Anwar

Payroll: what Finance needs to decide

Payroll code does not start until this document comes back answered. That is deliberate. An engineer who misreads a statutory rule writes a test asserting the misreading, and it passes forever — so the definition of a correct payslip is written by Finance first, as arithmetic, and the code’s stated job is to match it.

20 questions 30 worked examples 2 rules no decision covers 6 places the old app disagrees with us

How to answer

Part A is twenty questions, and every one already has our assumption filled in. Most should be a plain “yes, that’s right”. The ones that are not are the ones worth a meeting. A one-line answer is enough — the reasoning matters more than the format.

Part B is thirty worked examples. We have filled in every input. You fill in what the payslip should say. That table then becomes the definition of correct, and each case turns into a check that runs automatically on every change from then on — so a rule change two years from now breaks a build instead of somebody’s pay.

If a case is wrong, or describes a person we do not have, say so and we delete it. A worked example nobody believes is worse than a missing one. And if a number depends on something we have not listed as an input, that is the most useful thing you can tell us: it means the platform is missing a field.

Nothing here needs anyone to read code.

01Where this sits

Everything to the right of the fixture table is engineering work that cannot start early. This document is the only thing on the critical path that engineering cannot do for itself.

%%{init: {"flowchart": {"padding": 16, "nodeSpacing": 44, "rankSpacing": 54, "useMaxWidth": true}}}%%
flowchart LR
  A["Part A
20 questions"] --> C B["Part B
30 worked examples"] --> C C["The signed fixture table
the definition of a correct payslip"] C --> D["Payroll tables,
permissions, access rules"] D --> E["The payroll engine,
written to make the examples pass"] E --> F["Shadow month
one full month computed in parallel
and compared, line by line"] F --> G["Go live"] style C fill:#E2EDEA,stroke:#14544C,stroke-width:2px style A fill:#F3EAD6,stroke:#8A6216 style B fill:#F3EAD6,stroke:#8A6216
Figure 1  Finance is the first step, not the last reviewer. The usual order — build it, then ask Finance to check it — was rejected because by then the only people who can check the arithmetic are reading code to do it.

02Part A · Twenty questions

Grouped by where the answer lives. Each carries what we currently assume, so the fastest possible answer is “yes” twenty times — and every “no” is worth the whole document.

Singapore · CPF

Q1  Which CPF rates and ceilings apply?

needs a value

Why it matters. The old app uses a flat 20% employee and 17% employer with no ceiling at all. Real CPF has an Ordinary Wage ceiling, a separate annual Additional Wage ceiling, and rates that step down with age. This is a filing error if it is wrong, not a display bug.

Our assumption
Standard under-55 rates, Ordinary Wage ceiling applied each month, Additional Wage ceiling applied across the year. We need the exact figures you want used.

Q2  Is anyone in a non-standard age band?

confirm

Why it matters. Rates step at 55, 60 and 65. If nobody is near a boundary we can leave those bands unbuilt and record that in writing, rather than shipping code nobody has ever tested against a real person.

Our assumption
Everyone is under 55, on standard rates.

Q3  Which pay is Ordinary Wage and which is Additional Wage?

needs a value

Why it matters. The Additional Wage ceiling is annual and depends entirely on this split, so if it is wrong then December is wrong. The platform has no field for this today — this question is the reason it needs one.

Our assumption
Ordinary: monthly salary, allowances, overtime. Additional: bonus, commission, leave encashment, directors’ fees.

Q4  Confirm the SDL floor and cap.

confirm

Why it matters. Small money, but it is hardcoded in the old app and nobody has checked it since it was written.

Our assumption
0.25% of monthly gross, a $2 minimum and an $11.25 maximum.

Q5  Who is CPF-exempt, and on what grounds?

confirm

Why it matters. The platform keeps an exemption flag per person. A flag whose rule nobody can state is a flag nobody will maintain correctly, and it silently removes 37% of someone’s statutory cost.

Our assumption
Foreigners on an Employment Pass. Citizens and PRs are never exempt.

Malaysia

Q6  Confirm EPF, SOCSO and EIS rates and wage ceilings.

needs a value

Why it matters. SOCSO and EIS are capped, and the cap is where a naive percentage calculation goes wrong first.

Our assumption
Statutory defaults, with SOCSO and EIS capped at the standard wage ceiling.

Q7  PCB — do we compute it, or apply the LHDN schedule?

needs a value

Why it matters. The two approaches give different answers, and only one of them is what LHDN expects to receive.

Our assumption
We compute monthly PCB from the annual brackets. This is the assumption we hold with the least confidence in the whole document.

Indonesia

Q8  Confirm BPJS rates and the Kesehatan ceiling.

confirm

Why it matters. Ketenagakerjaan and Kesehatan have different rates and only one has a wage ceiling.

Our assumption
BPJSTK 2% employee and 3.7% employer; BPJSK 1% employee and 4% employer, with the Kesehatan ceiling applied.

Q9  Who currently has no NPWP?

needs a value

Why it matters. No NPWP means a 1.2× surcharge on PPh 21. It is per person, and it changes the month they register — so it has to be a field somebody owns, not a note.

Our assumption
None. We need the list; it is not in any data we hold.

Q10  Where does each person’s PTKP status come from?

needs a value

Why it matters. PPh 21 depends on marital status and number of dependants. The platform stores neither today, and what somebody says in conversation is not necessarily what was filed.

Our assumption
Finance holds this and it is stable year to year. If so it becomes a field on the employee record, and it needs a named owner who updates it.

Q11  Does this platform run THR?

no decision covers this

Why it matters. Tunjangan Hari Raya is a statutory obligation — a month’s wages before the religious holiday, prorated under a year of service — and nothing we have written down mentions it at all. If payroll runs it, it needs a rule, a month and a tax treatment. If it is paid outside the platform, we need that in writing, or the year-end reconciliation will quietly understate what people were paid.

Our assumption
None. This is a genuine gap and we found it while writing this document.

Money, rates and rounding

%%{init: {"flowchart": {"padding": 16, "nodeSpacing": 46, "rankSpacing": 50, "useMaxWidth": true}}}%%
flowchart TB
  SAL["Salary
contracted in SGD"] SAL -->|"Statutory Rate
fixed for the payroll month"| STAT["Local figure used to compute
contributions and tax"] SAL -->|"Settlement Rate
the day the money moved"| PAY["Local figure that actually
reached the bank"] STAT --- X(["these two are
never reconciled"]) PAY --- X style X fill:#F6E3DE,stroke:#8F3A2C style SAL fill:#E2EDEA,stroke:#14544C
Figure 2  Two different rates doing two different jobs. The old app has only one, which means it is currently doing both with whichever number happened to be loaded. The gap between them is not an error anybody needs to chase — it is what it costs to move money.

Q12  Which rate computes, and which rate pays?

confirm

Why it matters. See Figure 2. Getting this backwards makes every statutory figure move whenever the bank’s rate moves, which would make a closed month impossible to reprint.

Our assumption
Contributions and tax compute at the Statutory Rate for the month. The transfer happens at the Settlement Rate and is recorded as it happened, never recomputed.

Q13  What does the payslip show — SGD, local, or both?

confirm

Why it matters. Salary is contracted in SGD, people are paid in local currency, and statutory figures are inherently local. A payslip showing one of the three cannot be checked by the person holding it.

Our assumption
Both, with SGD as the contractual figure and local as the paid figure, and the rate used printed on the slip.

Q14  Rounding, per currency.

confirm

Why it matters. The old app rounds everything to two decimals including rupiah, which has no cents. One-rupiah differences reconcile to nothing and look like errors forever.

Our assumption
SGD and MYR to two decimals, IDR to whole rupiah, rounded as each statutory figure is computed rather than at the end.

Q15  What happens when a rate is missing?

confirm

Why it matters. The old app carries a built-in fallback rate for every currency, so a missing rate produces a plausible wrong number instead of a stop.

Our assumption
The run refuses to compute and names the missing rate. Nobody is paid on a guessed number.

Claims, leave and one-offs

Q16  What is the claim cut-off day?

needs a value

Why it matters. A claim approved after the cut-off settles in the following payroll month. The old app has this as a setting with no value in it.

Our assumption
None. We need the number, and whether it is the claim date or the approval date it applies to.

Q17  Does a taxable allowance claim attract CPF, or only income tax?

confirm

Why it matters. It decides whether a claim reaches the statutory calculation at all, or only appears on the year-end form.

Our assumption
It is Additional Wage and attracts both.

Q18  Directors’ fees — how often, and to whom?

needs a value

Why it matters. A fee is a payroll line — taxable, no CPF. The old app only captures it as a year-end adjustment, so it never appears on a payslip in the month it was actually paid.

Our assumption
Paid irregularly rather than monthly, to a known short list, and it should show as its own line in the month it is paid.

Q19  Leave encashment at exit — which month, which wage type?

confirm

Why it matters. If encashment is Additional Wage it competes with everything else against the annual ceiling, in somebody’s final month, when there is no later month to correct it in.

Our assumption
Paid in the final payroll month, treated as Additional Wage.

Q20  Is there an AWS or thirteenth-month payment?

no decision covers this

Why it matters. Same shape of gap as THR — nothing we have written down mentions one. If it exists it is Additional Wage, it lands in a specific month, and it changes the December ceiling arithmetic for everyone who gets it.

Our assumption
None exists.

03Part B · The thirty worked examples

Inputs are filled in. You fill in what the payslip should say. The figures are invented rather than anyone’s real salary, and they are chosen to sit near the boundaries that break things — a ceiling, a joining date, a missing rate — because an example in the comfortable middle of the range proves the least.

Itemised, not just the total

For each case we need the parts, not only the bottom line: base salary after proration, the proration factor if the month is incomplete, each employee deduction separately — CPF, EPF, SOCSO, EIS, PCB, BPJSTK, BPJSK, PPh 21 — each employer contribution separately, then net pay and total cost to the company.

A total that comes out right because two errors cancelled is exactly the failure this table exists to catch, and it is invisible unless the parts are written down.

Standing inputs

Unless a case says otherwise: the month is March 2026, a full month is worked, there are no claims, the person is under 55, holds every statutory registration, and no exemption applies. Statutory rates for the month: 1 SGD = 3.15 MYR = 11,500 IDR. Settlement rate the same unless the case varies it.

Singapore

CaseWhat it testsInputs
SG-01Standard full monthSalary 5,000 SGD
SG-02Mid-month joinerSalary 5,000 SGD, joined 16 March
SG-03Leaver mid-monthSalary 5,000 SGD, last day 13 March
SG-04CPF-exemptSalary 8,000 SGD, Employment Pass
SG-05Above the Ordinary Wage ceilingSalary 9,000 SGD
SG-06Age band 55 to 60Salary 5,000 SGD, born March 1970
SG-07Bonus against the annual ceilingSalary 5,000 SGD, bonus 15,000 SGD, December
SG-08Directors’ fee in the monthSalary 5,000 SGD plus a 20,000 SGD fee
SG-09Taxable allowance claimSalary 5,000 SGD, claim 800 SGD settling this month
SG-10Reimbursement claimSalary 5,000 SGD, claim 240 SGD settling this month
SG-11Encashment at exitSalary 5,000 SGD, last day 31 March, 9 days encashed

Malaysia

CaseWhat it testsInputs
MY-01Standard full monthSalary 2,200 SGD
MY-02Mid-month joinerSalary 2,200 SGD, joined 10 March
MY-03Leaver mid-monthSalary 2,200 SGD, last day 20 March
MY-04Above the SOCSO ceilingSalary 4,500 SGD
MY-05Taxable allowance claimSalary 2,200 SGD, claim 500 SGD settling this month
MY-06Bonus monthSalary 2,200 SGD, bonus 4,400 SGD, December
MY-07Low wage, EIS boundarySalary 900 SGD

Indonesia

CaseWhat it testsInputs
ID-01Standard full monthSalary 1,800 SGD, NPWP held, PTKP TK/0
ID-02No NPWP — the 1.2× surchargeSalary 1,800 SGD, no NPWP, PTKP TK/0
ID-03Dependants change PTKPSalary 1,800 SGD, NPWP held, PTKP K/2
ID-04Mid-month joinerSalary 1,800 SGD, joined 9 March
ID-05Leaver with leave overdrawnSalary 1,800 SGD, last day 31 March, 4 days taken beyond earned
ID-06Above the Kesehatan ceilingSalary 3,500 SGD
ID-07THR — only if Q11 says we run itSalary 1,800 SGD, THR due
ID-08Taxable allowance claimSalary 1,800 SGD, claim 400 SGD settling this month
ID-09Rounding to whole rupiahSalary 1,777 SGD — chosen not to divide cleanly

Cases that cross all three

CaseWhat it testsInputs and expected behaviour
X-01Nobody workedJoined 2 April, run for March
X-02Missing rateIndonesian employee, no confirmed rate for the month. Expected: the run refuses and names the rate
X-03Claim past the cut-offExpected: settles next month, not this one
X-04Part-timeSalary 2,500 SGD at three days a week
X-05Settlement differs from statutoryStatutory 11,500, settlement 11,720. Expected: both recorded, neither reconciled

X-02, X-03 and X-05 have no payslip to fill in. What they expect is a behaviour rather than a number, and they are in the table because those three are the ones most likely to be built as a quiet default instead of a refusal.

04What happens after this comes back

  1. Every case becomes an automatic check. A rule that changes eighteen months from now breaks a build, not a payslip.
  2. Then the payroll tables are built, shaped by what the examples turned out to need.
  3. Then the calculation is written, with the stated job of making all thirty pass.
  4. Then the shadow month — one full month computed in parallel with the real one and compared line by line, before anything goes live. That gate stays regardless of how this document comes back.

Sign-off is against the arithmetic in Part B, not against any code. That is the whole point of doing it in this order.