Skip to main content

Payroll API

The largest module: payroll runs and payslips, the settings singleton, the versioned statutory rate tables, and PH-compliance outputs. The business rules these endpoints implement (premium matrix, contribution math, tax true-up): ../business-rules.md. Regenerating payroll after a rate fix: ../backend/payroll-regeneration.md. Conventions and Swagger: conventions.md, README.md.

Payroll runs — /api/payroll

PayrollController.java

MethodPathPermissionPurpose
GET/api/payrollpayroll.dashboard.view or payroll.manage or payroll.approvePaged/filtered runs
GET/api/payroll/{id}same threeOne run (with approvals)
POST/api/payrollpayroll.manageCreate a run (payrollRunDate, periodStartDate, periodEndDate)
POST/api/payroll/{id}/generate-payslipspayroll.manageRun the engine for every active employee
PATCH/api/payroll/{id}/decisionpayroll.approveApprove / reject a pending run
PATCH/api/payroll/{id}/statuspayroll.manageMark processed / cancel
DELETE/api/payroll/{id}payroll.manageDelete a run (body requires a reason)

Status lifecycle (verified in PayrollServiceImpl.java):

Draft ──generate-payslips──▶ Pending ──decision──▶ Approved ──status──▶ Processed
└────────▶ Rejected
Any non-Processed run ──status {"status":"Cancelled","reason":"..."}──▶ Cancelled

Guard rails, all 400s: creation rejects an end date before the start date and any period overlapping an existing non-Cancelled/Rejected run; decision accepts only Approved/Rejected and only from Pending; Processed is reachable only from Approved; cancelling requires a reason and is refused for Processed; deletion is refused for Processed runs and for any run that already has payslips ("cancel it instead"). Cancellations are written to the audit log; every status change lands in payroll-changes.

Deep-dive: POST /api/payroll/{id}/generate-payslips

No request body. Preconditions: the run must be in Draft and have no payslips yet. The engine then loads every non-archived, non-INACTIVE employee and computes a payslip per employee — attendance, holiday/suspension premiums, de minimis, statutory contributions, withholding tax, and any applicable custom deductions — then flips the run to Pending and logs a history row per payslip. Response: 200 with the updated PayrollDto.

The loud contribution-coverage failure: before a single payslip is written, the engine verifies that every employee's monthly rate is covered by an SSS, PhilHealth, and Pag-IBIG bracket in the rate-table version that applies to the period. Any gap aborts the entire run — nothing partial is saved — with a message that lists every failing employee and table:

Cannot generate payslips: 10012 (Ana Reyes): no SSS contribution bracket covers monthly salary 250000.00; 10047 (Ben Cruz): no Pag-IBIG contribution bracket covers monthly salary 180000.00

This (like the "already generated" and "no active employees" guards) is thrown as IllegalStateException, which has no dedicated handler in GlobalControllerAdvice — so it surfaces as HTTP 500 with the full message, not a 400. Fix the rate table (see rate tables below) and re-run.

Payslips — /api/payslips, history — /api/payslip-history

PayslipController.java, PayslipHistoryController.java

MethodPathPermissionPurpose
GET/api/payslips/meemployee.payslip.viewOwn payslips
GET/api/payslips/me/yearsemployee.payslip.viewYears with payslips (for the picker)
GET/api/payslips/me/ytdemployee.payslip.viewOwn year-to-date totals
GET/api/payslips/me/{id}employee.payslip.viewOwn single payslip
GET/api/payslipspayroll.payslip.view or payroll.manage or payroll.approvePaged/filtered list
GET/api/payslips/{id}same threeOne payslip
GET/api/payslip-history?payslipId=same threeChange history of one payslip

Payroll transactions — /api/payroll-transactions

PayrollTransactionController.java — ad-hoc pay adjustments outside the engine.

MethodPathPermission
GET /, GET /types, GET /{id}/api/payroll-transactions...payroll.transactions.view or payroll.transactions.manage
POST, PUT /{id}, DELETE /{id}/api/payroll-transactions...payroll.transactions.manage

Payroll changes — /api/payroll-changes

PayrollChangesController.java — audit trail of run-level field changes.

MethodPathPermission
GET /api/payroll-changes?payrollId=payroll.dashboard.view or payroll.manage or payroll.approve

Payroll settings — /api/payroll-settings (+ changes, template)

PayrollSettingsController.java, PayrollSettingsChangesController.java, PayslipTemplateSettingsController.java

MethodPathPermissionPurpose
GET/api/payroll-settingspayroll.settings.manage or payroll.manage or payroll.ewt.manage or hr.employees.manageThe settings singleton
GET/api/payroll-settings/employee-viewany authenticated userEmployee-safe subset (pay schedule + payslip display)
GET/api/payroll-settings/compliance-checklistpayroll.settings.manageAutomated compliance checks over current settings
PUT/api/payroll-settingspayroll.settings.manageReplace the singleton
GET/api/payroll-settings-changespayroll.settings.manageField-level change history
GET/api/payslip-template-settingsany authenticated userPayslip PDF template preferences
PUT/api/payslip-template-settingspayroll.settings.manageUpdate template preferences

Deep-dive: GET / PUT /api/payroll-settings

The settings object is a singleton — one row that parameterizes the whole engine. The DTO (~60 fields; see PayrollSettingsDto in Swagger for all of them) groups into: working-time basics (workingDaysPerMonth, hoursPerDay, workStartTime…), premium multipliers (overtimeMultiplier, nightDiffMultiplier, regularHolidayMultiplier, specialDayMultiplier, rest-day combinations…), night-shift window, statutory caps/floors (philhealthPremiumFloor/Ceiling, pagibigContributionCap, thirteenthMonthNonTaxableCap…), pay schedule (payFrequency, cutoff and pay-date days), rate-table version pins (sssTableVersion etc. — see rate tables below), display text, and feature switches such as customDeductionsEnabled (a master switch that makes payslip generation skip all employee deductions without touching the assignments).

PUT takes the full DTO back (fetch, modify, send — it is not a PATCH) and requires payroll.settings.manage. Every field change is diffed and recorded, retrievable via GET /api/payroll-settings-changes. Settings are read at generation time: changing them affects the next generate-payslips, never already-generated payslips (see the regeneration runbook).

Bonuses — /api/bonuses

BonusController.java — class-level payroll.bonus.manage on every endpoint: GET /, GET /tax-preview, GET /{id}, POST, PUT /{id}, DELETE /{id}. /tax-preview computes the tax impact of a prospective bonus before saving.

Deductions & de minimis

DeductionTypeController.java, EmployeeDeductionController.java, DeMinimisBenefitTypeController.java

PrefixEndpointsPermission
/api/deduction-typesGET, POST, PUT /{id}, DELETE /{id}class-level payroll.deduction.manage
/api/employee-deductionsGET (paged/filtered), GET /{id}, POST, PUT /{id}, DELETE /{id}class-level payroll.deduction.manage
/api/de-minimis-benefit-typesGETpayroll.settings.manage or payroll.manage
/api/de-minimis-benefit-typesPOST, PUT /{id}, DELETE /{id}payroll.settings.manage

Employee deductions are per-employee recurring assignments (type + amount + active window) applied during payslip generation when they overlap the pay period — and only while customDeductionsEnabled is on.

Reimbursements

ReimbursementRequestController.java, ReimbursementTransactionController.java

MethodPathPermissionPurpose
GET/api/reimbursement-requests/meemployee.reimbursement.requestOwn requests
POST / PUT /{id} / DELETE /{id}/api/reimbursement-requests...employee.reimbursement.requestFile / edit / cancel own
GET, GET /{id}/api/reimbursement-requests...payroll.reimbursement.managePaged list / one request
PATCH/api/reimbursement-requests/{id}/decisionpayroll.reimbursement.manageApprove / reject
GET, GET /payment-methods, GET /{id}/api/reimbursement-transactions...payroll.reimbursement.transactions.view or ...managePayout records
POST, PUT /{id}, DELETE /{id}/api/reimbursement-transactions...payroll.reimbursement.transactions.manageRecord / edit payouts

Statutory rate tables (versioned cohorts)

Four controllers with an identical surface — only the bracket row shape and permission differ:

PrefixPermission (class-level, all endpoints)
/api/sss-contribution-ratespayroll.sss.rates.manage
/api/philhealth-contribution-ratespayroll.philhealth.rates.manage
/api/pagibig-contribution-ratespayroll.pagibig.rates.manage
/api/withholding-tax-bracketspayroll.withholdingtax.rates.manage

Each exposes: GET / (all rows), GET /{id}, POST, PUT /{id}, DELETE /{id}, GET /effective-dates, POST /cohorts/duplicate, PUT /cohorts.

Cohort semantics: every bracket row carries an effectiveDate; all rows sharing one date form a cohort — one immutable-in-spirit version of the official table. At generation time the engine resolves the cohort in effect for the pay period (a version can also be pinned in payroll settings), falling back to the earliest cohort for dates before the first version. So a yearly rate change is: duplicate the current cohort to the new date, then edit rows.

  • GET /effective-dates — the list of cohort dates (version picker).
  • POST /cohorts/duplicate{"sourceEffectiveDate":"2025-01-01","targetEffectiveDate":"2026-01-01"} copies every bracket to the new date in one transaction, so a new version starts from full coverage.
  • PUT /cohorts{"effectiveDate":"2026-01-01","rows":[...]} atomically replaces the entire cohort (used by CSV import; a partial import can never leave coverage gaps).

Deep-dive: POST /api/sss-contribution-rates

One bracket row in the official SSS breakdown shape (SssContributionRateRequest.java — every field required, amounts ≥ 0):

{
"salaryBracketFrom": 20250.00,
"salaryBracketTo": 20749.99,
"mscRegular": 20000.00,
"mscMpf": 500.00,
"regularEmployerShare": 1900.00,
"regularEmployeeShare": 900.00,
"ecEmployerShare": 30.00,
"mpfEmployerShare": 47.50,
"mpfEmployeeShare": 22.50,
"effectiveDate": "2025-01-01"
}

Response 200 with the saved row (adds id). An employee whose monthly rate falls in [salaryBracketFrom, salaryBracketTo] under the resolved cohort gets these shares; leave no salary gaps between rows or generation will fail loudly (see above).

Government forms, filings, EWT, TIN compliance

GovernmentFormsController.java, GovernmentFilingController.java, EwtController.java, EwtFormsController.java, TinComplianceController.java

MethodPathPermissionPurpose
GET/api/government-forms/bir-2316, /bir-1601c, /alphalist-dat, /thirteenth-month, /remittance, /dole-thirteenth-month, /payroll-register, /dtrhr.employees.view or payroll.manageGenerated statutory forms/exports (1601-C field mapping: ../reference/compliance/bir-1601c.md)
GET/api/government-filingshr.employees.view or payroll.manageFiling log
POST/api/government-filings (multipart)payroll.manageRecord a filing + upload the receipt file
GET/api/government-filings/{id}/receipthr.employees.view or payroll.manageDownload the receipt
DELETE/api/government-filings/{id}payroll.manageRemove a filing record
GET/POST/PUT/DELETE/api/ewt-payees, /api/ewt-transactionsclass-level payroll.ewt.manageExpanded-withholding-tax registry
GET/api/government-forms/ewt/0619e, /1601eq, /2307, /2307/payeesclass-level payroll.ewt.manageEWT return forms
GET/POST/PUT/DELETE/api/tin-compliance (paged GET, /{id})class-level payroll.tin.compliance.manageTIN validation worklist

Holidays — /api/holidays

PhHolidayController.java

MethodPathPermission
GET/api/holidayspayroll.holidays.manage or payroll.manage or employee.holidays.view
POST, PUT /{id}, DELETE /{id}/api/holidays...payroll.holidays.manage
POST/api/holidays/generatepayroll.holidays.manage — seed a year's PH holiday calendar

When a regular holiday and a special day fall on the same date, the engine pays the regular-holiday premium (the higher one).

Company profile — /api/company-profile

CompanyProfileController.java: GET (any authenticated user), PUT (payroll.manage). Holds employer identity for forms plus flags like BMBE income-tax exemption, which the engine reads at generation time.

Reports — /api/reports

ReportingController.java — read-only aggregates for dashboards; all GET:

PathPermission
/api/reports/payroll-monthlypayroll.dashboard.view or hr.employees.view
/api/reports/headcount-by-department, /bir-alphalisthr.employees.view
/api/reports/year-end-tax-adjustment?year=hr.employees.view or payroll.manage
/api/reports/leave-by-statushr.leave.requests.view
/api/reports/overtime-monthlyhr.overtime.requests.view or payroll.dashboard.view
/api/reports/reimbursement-by-statuspayroll.reimbursement.manage
/api/reports/recruitment-summaryhr.recruitment.view
/api/reports/hr-analytics/* (summary, workforce, attendance-monthly, headcount-movement, leave, leave-monthly, overtime-by-department, drilldown)hr.dashboard.view