← systeric.com Interwise / Docs Open App →

Payroll Engine

Every payroll run in Interwise resolves to one call: PayrollCalculatorService.calculate(). Given an employee’s attendance, compensation items, overtime entries, tax configuration, and this year’s prior runs, it produces a PayrollMember — the full breakdown behind one line in a payroll run, and the source of one payslip. This page walks through that calculation in the exact order it runs, with the exact rates and formulas Interwise uses.

This is the deep technical reference. If you want the role model or the product-level tour first, start with Overview or Roles & Access — come back here when you need to know precisely how a number on a payslip was produced.


Calculation Order at a Glance

The engine runs as one pipeline, in six phases. Nothing here is optional or reordered per employee — every payroll member goes through all six phases every period; what varies is the inputs (attendance, compensation items, tax mode) and, in December or an employee’s final month, which tax formula phase five uses.

1. Prorate Working ratio, capped at 1.0 min(actual / expected hours, 1) 2–3. Earnings Compensation + overtime Basic salary identified 4. BPJS Kesehatan + JKK/JKM/JHT/JP Employee share deducted 5. Annualize Sum prior months this year Feeds the tax calculation 6–11. Tax TER monthly, bracket at year-end PKP, allowance, penalty, mode 12. Take-Home Earnings − deductions = THP on the payslip

The twelve numbered steps below map onto these six phases exactly as labeled in the diagram — phases 2 and 5 each bundle several steps because earnings and tax are where most of the real logic lives.


Phase 1 — Prorate

1. Working Ratio

The very first thing the engine computes is a single proration factor, the working ratio:

ratio = min((workingDays - unpaidLeaveDays) / workingDays, 1)

This ratio is applied to every compensation item flagged isAttendance: true — typically basic salary and any attendance-linked allowance. Items not flagged for attendance (fixed allowances, benefits-in-kind, etc.) pass through at their full configured amount regardless of attendance. The ratio is capped at 1 so a partial period can never inflate pay above the full-period amount.


Phase 2 — Earnings

2. Compensation Items

The engine walks every compensation item for the employee and, for each one:

  • Applies the working ratio if isAttendance is true; otherwise uses the item’s full amount.
  • Adds it to totalEarnings, totalDeductions, or totalBenefits depending on the item’s type (EARNING, DEDUCTION, or benefit).
  • If isTax is true, adds it to the taxable gross used later for PPh 21.
  • If isBpjsKesehatan or isBpjsKetenagakerjaan is true, adds it to the respective BPJS contribution base.
  • If the item’s code is BASIC or GAJI_POKOK, records its (post-ratio) amount as the employee’s basic salary — this is what overtime’s hourly rate is derived from in the next step.

3. Overtime

Overtime entries are filtered to only those that are isApproved && isPaid, then grouped by ISO week (Monday–Sunday). Within each week, hours are bucketed by the Indonesian labor-law multiplier rules:

  • Regular overtime: the first hour worked in the week is paid at 1.5×; every hour after that in the same week is paid at .
  • Public-holiday overtime: the first 7 hours are paid at , the 8th hour at , and every hour beyond 8 at .

The hourly rate used for all of these multipliers is a single constant derived from basic salary:

hourlyRate = basicSalary / 173

173 is the standard monthly working-hours constant used across Indonesian payroll practice. The total overtime amount is the sum of each bucket’s hours × multiplier × hourly rate, and it’s added to totalEarnings as a generated OVERTIME line item — it also counts toward taxable gross.


Phase 3 — BPJS

4. BPJS Contributions

BPJS is computed from the two bases accumulated in step 2 (bpjsKesBase, bpjsTkBase), split into employee and employer shares:

ComponentEmployeeEmployerCeiling
Kesehatan1%4%12,000,000 (applies to both shares)
JKK (Kecelakaan Kerja)per employee’s industry risk ratenone
JKM (Kematian)0.3% (default)none
JHT (Hari Tua)2%3.7%none
JP (Pensiun)1%2%11,086,300 (as of March 2026)

Only the employee shares (Kesehatan-EE + JHT-EE + JP-EE) are deducted from pay, as a single generated BPJS_EMPLOYEE line item. JKK and JKM are employer-only — the employee never sees them as a deduction.

Employer contributions don’t just disappear into overhead, though: per PMK 168/2023, the Kesehatan, JKK, and JKM employer shares (but not JHT-ER or JP-ER) are added back into the employee’s taxable gross for the PPh 21 calculation in phase 5, since they count as employer-provided income in kind. JHT-EE and JP-EE, meanwhile, become the employee’s tax deduction (currentTaxDeduction) — they reduce taxable income the way a pension contribution would.


Phase 4 — Annualize

5. Accumulated History

Before tax can be computed, the engine looks back at every payroll run already recorded for this employee this calendar year, up to (not including) the current month, and sums:

  • accumulatedGross — prior months’ gross salary
  • accumulatedTaxDeduction — prior months’ JHT + JP employee contributions
  • accumulatedMonthlyTax — prior months’ tax already withheld
  • accumulatedThr — prior months’ THR (religious holiday allowance) paid this year

It also determines earliestMonth (the earliest payroll month recorded this year, defaulting to the current month for a first-time run) and monthsWorked (current month − earliest month + 1). These feed directly into the annualization math in the next phase — they’re what let a mid-year joiner’s tax be computed correctly against a partial year rather than a full twelve months.


Phase 5 — Tax

Indonesia’s default PPh 21 method since PP 58/2023 is TER (Tarif Efektif Rata-rata — “average effective rate”): a flat rate looked up from a PTKP-category table and applied directly to gross, month by month, with a full bracket-progressive settlement once a year. Interwise implements TER as the default computationMethod, with the older bracket-every-month method available as a fallback configuration.

6. Tax — Non-December (TER)

For every month except an employee’s final month, the engine computes this month’s taxable gross — salary plus the taxable employer BPJS from phase 3 — and looks up a TER rate for it against the employee’s PTKP category (the category is derived from the employee’s PTKP code; the full PTKP table lives in the Compliance Reference). That rate is applied straight to the same monthly gross:

terGross = currentMonthGross + taxableEmployerBpjs
rate     = lookupTerRate(ptkpCategory, terGross)
tax      = round(terGross * rate / 100 * penalty)

No annualization, no PKP, no position allowance runs in this path — TER is deliberately a single lookup-and-multiply per month, which is what makes it fast to compute and easy to audit against DJP’s published tables.

7. Tax — December / Last Month (Bracket Settlement)

An employee’s final payroll month — December, or their last active month if they leave mid-year (isLastMonth) — is different: instead of another TER lookup, the engine settles the entire year’s tax liability using the progressive bracket table, then nets it against everything already withheld via TER:

annualizedGross = accumulatedGross + currentMonthGross + taxableEmployerBpjs + thrThisYear
annualizedNett  = annualizedGross - (accumulatedTaxDeduction + currentTaxDeduction)
yearlyTax       = progressiveTax(pkp, taxBrackets) * penalty
monthlyTax      = yearlyTax - accumulatedMonthlyTax   // this month's settlement

progressiveTax walks the bracket table applying each band’s rate only to the slice of PKP that falls in it:

PKP band (IDR/year)Rate
0 – 60,000,0005%
60,000,000 – 250,000,00015%
250,000,000 – 500,000,00025%
500,000,000 – 5,000,000,00030%
above 5,000,000,00035%

Because monthlyTax here is a settlement against everything already withheld, it can go negative — if TER over-withheld during the year (a common case when a large THR pushes one month’s TER band higher than the annual bracket rate would justify), the employee is owed a refund rather than another deduction. See step 11 for how that surfaces on the payslip.

8. Taxable Income (PKP)

PKP (Penghasilan Kena Pajak — taxable income) only comes into play in the bracket settlement path above (step 7, and every month if an employee is configured for the BRACKET method instead of TER). It’s derived from annualized net income, the position allowance (step 9), and PTKP:

pkpRaw = max(annualizedNett - positionAllowance - ptkp, 0)
pkp    = floor(pkpRaw / 1000) * 1000

PTKP (Penghasilan Tidak Kena Pajak — the non-taxable income threshold) depends on marital status and dependents; the full PTKP table by code is in the Compliance Reference.

9. Position Allowance

The position allowance (biaya jabatan) is a standard deduction meant to represent job-related costs, computed before PKP since PKP depends on it:

positionAllowance = min(annualizedBase * 5%, 6,000,000)

annualizedBase is normally annualizedGross; for an employee’s very first month (January, with no prior runs this year), it’s annualizedNett instead, per DJP’s specification for a first-month calculation with no accumulated history to annualize from.

10. No-Tax-ID Penalty

A flat 20% surcharge applies to the computed tax — in both the TER monthly path and the bracket settlement path — whenever the employee has no NIK on file:

function hasNoTaxIdPenalty(config) {
  return !config.nik?.trim();
}

This is intentionally a single, shared function (hasNoTaxIdPenalty() in tax-config.vo.ts) used by every tax path, so the rule can never drift between TER and bracket calculations. Since PMK 112/2022 (effective July 2024), the NIK is the sole tax ID Interwise tracks — there is no separate NPWP field — so the surcharge fires whenever the NIK is empty.

11. Tax Mode

Every employee has a TaxModeGROSS, NETT, or GROSS_UP — that determines who actually bears the computed tax and how it shows up on the payslip:

ModeWho bears the taxEffect on take-home payPayslip line item
GROSSEmployeeReduced by the tax amountPPH21_DEDUCTION — or PPH21_REFUND (an earning) if TER over-withheld during the year and December settles negative
NETTCompanyUnchanged — company absorbs the costNone on the payslip; the cost is tracked internally as taxPaidByCompany
GROSS_UPCompany, via an added allowanceUnchanged — the allowance exactly offsets the taxPPH21_ALLOWANCE (an earning)

GROSS_UP can’t compute its allowance in one pass, because adding the allowance to gross changes the tax owed on that gross, which changes the allowance needed, and so on. The engine resolves this by iterating: recompute tax with the current allowance folded into gross, compare to the previous iteration, and stop once the change is under 1 IDR (capped at 20 iterations as a safety bound). In practice this converges in only a few passes.


Phase 6 — Take-Home

12. Take-Home Pay

The last step is arithmetic, once every earning and deduction line item above has been appended:

THP = totalEarnings - totalDeductions

Everything upstream — proration, compensation items, overtime, the employee’s BPJS share, and (for GROSS mode) the tax deduction, or (for GROSS_UP) the offsetting tax allowance — has already been folded into totalEarnings and totalDeductions as individual line items by the time this runs. This is the number that lands on the payslip as take-home pay, and it’s what gets paid out through the bank disbursement file.


Where to Go Next

  • Compliance Reference — the full PTKP table by code, TER category mapping, and other statutory reference values used throughout this calculation.
  • Overview — how payroll fits into the rest of Interwise, from hiring through payslip generation.
  • Roles & Access — who can configure tax modes and run payroll for a company.