Porting configurable HR & Payroll features from PulaHr to PulaHr Global
This document specifies the requirements for adapting selected features of PulaHr (the Botswana-specific HR & payroll platform) into PulaHr Global, a multi-region capable version. The goal is to extract hardcoded, country-specific business logic into a configurable Policy Engine so the same codebase can serve clients across different jurisdictions and company-specific rules (e.g. Two Six Seven Restaurant's overtime formula).
Scope covers: the Overtime Engine, Tax (PAYE) Engine, Statutory Contributions, Leave deduction logic, and the Earnings/Deductions taxonomy.
PulaHr Global retains the core engine (Identity, Auth, Employee database) while abstracting all calculation rules into per-company configuration records. A single shared service reads these records so every screen and backend function produces identical results — eliminating the inconsistencies present in the current PulaHr code.
| Module | Core Function | Scalability Approach |
|---|---|---|
| Identity Engine | Multi-tenancy, Auth, Employee DB | Shared across all instances |
| Policy Engine | OT, Tax, Leave, Statutory rules | Config-driven per company / region |
| Data Adapter | Standardised payroll interface | Unified service layer |
A full code audit of PulaHr identified the following business-logic points. These are the variables that must move into the Policy Engine for Global.
The codebase currently uses three different assumptions for the same concepts: overtime divides salary by 26 days; daily hours are sometimes 8 and sometimes 8.5; and leave deductions divide by 22 days. Overtime is also detected at 8.5h/day but priced at 8h/day.
| Logic | Location | Value | Issue |
|---|---|---|---|
| OT hourly divisor | TimesheetSection (admin save) | ÷26 ÷ hours_per_day(8) | Reads setting ✓ |
| OT hourly divisor | TimesheetSection (display fallback) | ÷26 ÷ 8 (hardcoded) | Ignores setting ✗ |
| OT hourly divisor | AdminPayroll / syncOvertimeToPayroll | ÷26 ÷ hours_per_day(8) | Reads setting ✓ |
| Standard shift length | TimesheetSection (OT detection) | 8.5 (hardcoded const) | Mismatch vs ÷8 pricing |
| Leave deduction divisor | calculatePayroll | ÷22 working days | Differs from OT's 26 |
| Domain | Hardcoded Values |
|---|---|
| Overtime | 26 working days, 8 / 8.5 hours/day, 1.5 multiplier, rollover toggle |
| Tax (PAYE) | Brackets 48k/84k/120k/156k; rates 5/12.5/18.75/25%; duplicated in 3 files |
| Currency | 'BWP' / 'P' symbol hardcoded throughout |
| Statutory | BPF employee 5%, BPF employer 5%, WCF 0.5% |
| Taxonomy | 15 earning labels + 11 deduction labels (payrollProcessor) |
| Leave | 22 working days for daily-rate; leave types fixed in enums |
| Source | Configurable Fields |
|---|---|
| CompanyPayrollSettings | overtime_multiplier, hours_per_day, rollover_overtime_to_next_month |
| SystemSettings (payroll) | bpf_employee_rate, bpf_employer_rate, wcf_rate |
RegionalPolicy record SHALL define currency, working_days_per_month, hours_per_day, ot_multiplier, tax brackets, and statutory rates per company/region.Formula: Basic ÷ 26 ÷ 8.5 × 1.5 × hours. With Basic = P5,000 and 2 OT hours: 5000 ÷ 26 ÷ 8.5 × 1.5 × 2 = P67.87. The Policy Engine must reproduce this exactly when hours_per_day = 8.5.
| Phase | Deliverable | Outcome |
|---|---|---|
| 1 | Create RegionalPolicy entity | Single source of truth for all rules |
| 2 | Build shared PolicyEngine service | Replaces duplicated PAYE + OT logic |
| 3 | Refactor calc sites to use service | Fixes 26/8/8.5/22 inconsistencies |
| 4 | Config-driven onboarding | New clients added via config record |
| 5 | Policy versioning on payslips | Full audit trail |