What It Is
Payfactor is a multi-tenant SaaS payroll and HR platform for Philippine enterprises. It runs the whole cycle: biometric punches into a Daily Time Record, DTR into a payroll batch, statutory deductions (SSS, PhilHealth, Pag-IBIG, BIR withholding tax), then payslips — plus the 13th-month and final-pay math at the ends.
It’s an Angular SPA over a PHP and MySQL API, with the heavy payroll math living in MySQL stored procedures. Each client company gets its own database, so one deployment serves many companies without their data ever touching.
Why Philippine Payroll Is Hard
A single pay run has to reconcile multiple payment frequencies (weekly, semi-monthly, monthly), shifting and rotating schedules, overtime and night differentials, government contributions whose rates change yearly, and graduated withholding tax — then produce 13th-month and final pay that hold up to audit.
In spreadsheets that’s slow and easy to get wrong:
- Manual DTR reconciliation. Turning raw punches into payable hours by hand is tedious, and no two timekeepers do it quite the same way.
- Rules that change every year. Contribution tables and tax brackets get revised regularly, so hard-coded logic rots immediately.
- No safe multi-company story. A payroll provider serving many clients can’t risk one company’s data showing up in another’s.
The job was to encode the rules once, configurably, and turn attendance into compliant payroll for many companies from one platform.
How It’s Built
- Angular SPA — timekeeping, payroll, HRIS, reports, and system setup as feature modules with standalone components, an RxJS service layer, and route guards for tenant-scoped access. Cypress for end-to-end tests, Karma and Jasmine for units.
- PHP + MySQL API behind Nginx and PHP-FPM, containerized with Docker Compose for both development and production.
- Payroll in stored procedures. The expensive work (
sp_dtrBatchProcess,sp_payrollBatchProcess, final pay, 13th-month) runs set-based in MySQL instead of looping per employee in PHP. That’s what keeps a company-wide run fast. - A database per tenant, selected dynamically per request — hard isolation, not a
company_idcolumn you hope every query remembered to filter on. - Statutory tables as data. SSS, PhilHealth, Pag-IBIG and withholding rates, including ceilings and exemptions, are maintained in setup screens alongside payroll periods, holidays, schedules, and cost centers. Annual changes don’t need a release.
- Period locking. Payroll periods carry multi-level locks (kiosk → approver → final) so a run can’t be processed or altered out of sequence.
- Tuned for long jobs, because company-wide runs mean large uploads and requests that take a while.
Results
- One deployment serves many client companies, each in its own database — isolation enforced by the connection itself, not by remembering a
company_idfilter on every query. - Company-wide pay runs finish in a usable window, because the DTR and payroll batches run set-based in MySQL instead of looping per employee in PHP.
- Annual statutory revisions became a data edit. SSS, PhilHealth, Pag-IBIG, and withholding rates — ceilings and exemptions included — are maintained in setup screens, so a rate change no longer needs a release.
- The whole cycle runs inside the product: biometric punches → DTR → statutory deductions → payslips, plus 13th-month and final pay, with no spreadsheet step in between.
- Runs can’t be processed out of sequence, thanks to multi-level period locks (kiosk → approver → final).
- Covered end-to-end with Cypress, and at unit level with Karma and Jasmine.
What Was Tricky
- Punches into a clean DTR. Split shifts, overlapping or overrun breaks, late conversions, cancelled overtime and leave — all of it has to resolve before payroll can even start.
- Schedules that move. Employees are tagged with schedules per assignment or cost center, and rotating versus perfect-attendance schedules follow different computation paths, so the engine branches for each.
- Picking the right path. Weekly, semi-monthly, monthly; hourly versus daily computation. Each combination routes to a specific stored procedure, chosen deterministically from the employee and period setup.
- Statutory accuracy. Contributions and graduated withholding tax have to respect ceilings, exemptions, and year-to-date cumulative computation so every payslip reconciles exactly.
- 13th-month and final pay. Proportional by tenure, plus accrued-leave monetization and separation clearance. Edge-case-heavy math that has to be exactly right.
- Tenant isolation at scale. Every query has to hit the correct company database, with no path for one tenant’s data to surface in another’s.