HRIS integrations eliminate manual data entry, errors, and compliance risks by connecting HR platforms with payroll, benefits, and other business systems. For Indonesian SMEs, payroll is one of the most operationally critical modules — get it wrong and you have unhappy staff and potential labor law violations. I've seen Indonesian companies spend 3-5 days per month on payroll processing using a combination of Excel files and manual bank transfers. At Commsult, I built a payroll integration module that connects attendance data, timesheets, and HR records to automated payslip generation and bank disbursement. This is what it took to make it production-ready.
Indonesian payroll has specific components that must be modeled correctly: Gaji Pokok (base salary), Tunjangan Jabatan (position allowance), Tunjangan Makan (meal allowance), Tunjangan Transport (transport allowance), Uang Lembur (overtime pay), THR (Tunjangan Hari Raya — religious holiday bonus), and various deductions (BPJS Kesehatan, BPJS Ketenagakerjaan, PPh 21 income tax, late/absence deductions). Each component has different tax treatment — some are tax-inclusive, some are tax-excluded, and THR has specific PPh 21 calculation rules.
Our payroll module reads attendance data from three possible sources: biometric attendance machine (via API or CSV import), timesheet approvals in the project module, or manual entry by HR. The attendance records are aggregated per employee per month to compute: total working days, late arrivals (which trigger deduction per company policy), absences without leave (penalty deduction), and overtime hours (hours beyond 8 per day or 40 per week, paid at 1.5x or 2x per UU Ketenagakerjaan). This attendance summary is the input to payroll computation.
Indonesian Payroll Computation Flow (NestJS + PostgreSQL)
Inputs:
┌──────────────┐ ┌─────────────────┐ ┌────────────────┐
│ Attendance │ │ Employee Master │ │ Timesheet │
│ (biometric/ │ │ Gaji Pokok │ │ (project hrs) │
│ manual) │ │ Tunjangan │ │ │
└──────┬───────┘ └────────┬────────┘ └───────┬────────┘
└──────────────────┬┘ │
▼ │
┌──────────────────────────────────────────────────────┐
│ PayrollComputationService │
│ │
│ Gaji Pokok + Tunjangan Jabatan │
│ + Tunjangan Makan + Tunjangan Transport │
│ + Uang Lembur (1.5x or 2x per UU Ketenagakerjaan) │
│ − BPJS Kesehatan (1% karyawan) │
│ − BPJS JHT (2% karyawan) − BPJS JP (1% karyawan) │
│ − PPh 21 (anualisasi method, progressive tariff) │
│ − Potongan Keterlambatan / Absensi │
│ ═══════════════════════════════════════════════ │
│ = Gaji Bersih (Net Pay) │
└──────────────────────────┬───────────────────────────┘
│
┌──────────────┼───────────────┐
▼ ▼ ▼
PayslipPDF DisbursementFile JournalEntry
(Puppeteer) (BCA/Mandiri CSV) (debit Payroll
Expense)From my experience building ERP systems at Commsult: always implement a payroll simulation run before the actual payroll. Let the HR manager run the full computation and review the payslips before any disbursement is triggered. We call this the 'dry run' — it generates all payslips with a DRAFT status and an exceptions report (employees with unusual deductions, overtime above a threshold, salary increases not yet approved). This catches errors before they become costly corrections. The dry run is the single highest-value feature in the payroll module.
PPh 21 (Article 21 Income Tax) calculation in Indonesia is complex: it uses a progressive rate structure (5%, 15%, 25%, 30%, 35%), takes into account PTKP (non-taxable income threshold based on marital status and dependents), and differs for permanent employees (gross method vs. net method). Our system implements the Annualization method required by Indonesian tax regulations: monthly gross is annualized, tax is computed on the annual amount, then divided by 12 for the monthly withholding. This must match the DJP (Directorate General of Taxes) calculation — we test against DJP's calculator on every payroll run.
BPJS Kesehatan (health insurance) and BPJS Ketenagakerjaan (social security) contributions are mandatory for all employees. BPJS Kesehatan: 4% employer + 1% employee of salary (capped at the BPJS premium ceiling). BPJS Ketenagakerjaan: JHT 3.7% employer + 2% employee, JKK 0.24-1.74% employer depending on business risk class, JKM 0.3% employer, JP 2% employer + 1% employee. These percentages are regulatory and must be updated whenever BPJS publishes new rates — storing them in a configuration table (not hard-coded) is essential.
// NestJS: PPh 21 Anualisasi (Annualization Method)
// Reference: PER-16/PJ/2016 (DJP)
function calculatePph21(employee: Employee, monthlyGross: number): number {
const ptkp = getPtkp(employee.maritalStatus, employee.dependents);
// K/0 = 54,000,000; K/1 = 58,500,000; K/2 = 63,000,000; K/3 = 67,500,000
// TK/0 = 54,000,000
const annualGross = monthlyGross * 12;
const annualPtkp = ptkp;
const annualPhkp = Math.max(0, annualGross - annualPtkp); // PKP
const annualTax = computeProgressiveTax(annualPhkp);
return Math.round(annualTax / 12); // monthly PPh 21
}
function computeProgressiveTax(pkp: number): number {
// Tarif Pasal 17 UU PPh (2023 update)
let tax = 0;
if (pkp > 500_000_000) tax += (pkp - 500_000_000) * 0.35;
if (pkp > 250_000_000) tax += (Math.min(pkp, 500_000_000) - 250_000_000) * 0.30;
if (pkp > 60_000_000) tax += (Math.min(pkp, 250_000_000) - 60_000_000) * 0.25;
if (pkp > 60_000_000) tax += (Math.min(pkp, 60_000_000) - 0) * 0.15; // 5% up to 60M
// Simplified — full implementation handles all brackets
return Math.round(tax);
}
// BPJS contribution rates (update when BPJS changes rates)
const BPJS_RATES = {
kesehatan: { employer: 0.04, employee: 0.01 },
jht: { employer: 0.037, employee: 0.02 },
jkk: { employer: 0.0024 }, // varies by risk class
jkm: { employer: 0.003 },
jp: { employer: 0.02, employee: 0.01 },
};The payroll module is a NestJS service that orchestrates: PayrollComputationService (reads attendance, applies formulas), TaxCalculationService (PPh 21 and BPJS), PayslipGenerationService (creates PDF payslips via Puppeteer), and DisbursementService (generates bank transfer file in BCA or Mandiri bulk transfer format). Each payroll run is a payroll_runs row with status: draft, approved, disbursed. Once disbursed, the run is locked and cannot be modified.
When a payroll error is discovered after disbursement — a missed attendance record, a wrong salary tier, a BPJS ceiling that wasn't updated — the correction must be handled carefully. Never delete a paid payroll run. Instead, create an adjustment payroll run that only includes the affected employees and posts the correction amounts. The adjustment run links to the original run for audit trail. We also had a client who tried to claw back overpayments via a single large deduction from the next payslip — this violated the UU Ketenagakerjaan which limits deductions to 50% of net pay. Know the legal constraints before implementing correction logic.
Each employee gets a PDF payslip generated by Puppeteer from an HTML template. The template shows: employee details, salary components, deductions, net pay, and a QR code linking to the online payslip for authenticity verification. Payslips are stored in Google Cloud Storage with employee-specific signed URLs (30-day expiry). The distribution email is sent automatically on payroll disbursement day — employees can access their payslip via the link without logging into the ERP.
Indonesian employees need a Bukti Potong PPh 21 (tax withholding proof) annually to file their SPT Tahunan (annual tax return). Our system generates Bukti Potong documents for all employees at year-end: total gross income, total PPh 21 withheld, NPWP of both employer and employee. These are exported in the XML format required by e-SPT, the DJP's filing application. This feature alone saves the HR team 2-3 days of annual work and ensures the Bukti Potong matches the company's PPh 21 tax deposits.