Files
HRM-System/docs/PLAN-payroll-parallel4-fix.md

150 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fix Payroll Parallel 4 — 3 Bugs
## Bug 1: LWOP Not Captured When Attendance Status is "Absent"
### Problem
Ana Regina has 2 LWOP (Apr 27 + May 9). May 9 attendance record has `status=absent` instead of `on_leave`. The PayrollService only checks `$att->status === 'on_leave'` to look up leave type, so it misses the leave application and counts it as a generic absence instead.
**Impact:** The payslip shows 1 LWOP instead of 2. The deduction amount may be the same (both are unpaid), but the breakdown is wrong.
### Current Code (PayrollService.php ~line 137)
```php
if ($att && $att->status === 'on_leave' && $att->leaveApplication) {
$isPaidLeave = $att->leaveApplication->leaveType->is_paid ?? false;
} else {
// Fallback: check leave applications table
foreach ($leaveApps as $leave) { ... }
}
```
### Fix
The fallback already checks `$leaveApps`, but it only runs when the attendance status is NOT `on_leave`. The issue is that when `status=absent` AND a leave app exists, the system counts it as an absence (line ~247) instead of recognizing it as a leave.
**Change:** In the "Did NOT work" block, before counting absences, also check if a leave application exists for that date:
```php
} else {
// Check if there's an approved leave for this date (even if status is 'absent')
$leaveForDate = null;
foreach ($leaveApps as $leave) {
if ($currentDate->betweenIncluded($leave->start_date, $leave->end_date)) {
$leaveForDate = $leave;
break;
}
}
if ($leaveForDate) {
$isLeaveForDatePaid = $leaveForDate->leaveType->is_paid ?? false;
if ($isLeaveForDatePaid) {
$paidLeavesAmount += $actualDailyRate;
$daysWorked++;
$paidLeavesCount++;
}
// If unpaid (LWOP), don't count as absence — it's tracked as LWOP
// The absence deduction still applies since LWOP is unpaid
} else if (!$isRestDay && !$isSpecialHoliday) {
$absencesCount++;
}
}
```
> [!IMPORTANT]
> This fix ensures LWOP is properly recognized even when the attendance record says "absent".
---
## Bug 2: Night Differential Too High for Rank & File
### Problem
Client says ND is "too high" for daily wage employees. Current formula compounds ND with the day multiplier:
```
ND = ND_hours × hourlyRate × dayMultiplier × 10%
```
On a regular holiday (multiplier 2.0), this doubles the ND premium. Client expects flat 10%.
### Example (Dinh Chavez, ₱765/day)
- Hourly: ₱765 / 8 = ₱95.625
- Current ND/hr on holiday: ₱95.625 × 2.0 × 10% = **₱19.13** (too high)
- Expected ND/hr: ₱95.625 × 10% = **₱9.56** (flat)
### Fix (PayrollService.php ~line 220)
```php
// BEFORE:
$ndRegAmount = $ndRegHours * ($hourlyRate * $dayMultiplier * 0.10);
$ndOtAmount = $ndOtHours * ($hourlyRate * $dayMultiplier * $otRateMultiplier * 0.10);
// AFTER:
$ndRegAmount = $ndRegHours * ($hourlyRate * 0.10);
$ndOtAmount = $ndOtHours * ($hourlyRate * $otRateMultiplier * 0.10);
```
Remove `$dayMultiplier` from ND calculation entirely. ND is a flat 10% of the base hourly rate.
> [!WARNING]
> Verify with client: DOLE technically compounds ND with day premium. But client explicitly says it's too high, so we follow their business rule.
---
## Bug 3: Holiday Pay Should Be Prorated by Hours Worked (Rank & File)
### Problem
Daily employees who worked **partial hours** on a holiday get the **full day** premium. Example:
- Dinh worked 7.28h on May 1 (not full 8h shift)
- System gives premium = ₱765 × 1.0 = ₱765 (full day)
- Client expects: premium = (7.28/8) × ₱765 = **₱696.15** (prorated)
### Current Code (PayrollService.php ~line 177-186)
```php
$earnedDaily = $actualDailyRate * $dayMultiplier; // Full daily rate
$earnedBasicSalary += $actualDailyRate; // Full basic
$premium = ($earnedDaily - $actualDailyRate); // Full premium
```
### Fix
For daily wage employees, prorate based on actual hours worked vs standard shift hours:
```php
if ($didWork) {
$standardHours = ($att->shift && $att->shift->working_hours > 0)
? (float)$att->shift->working_hours : 8.0;
$hoursWorked = min((float)$att->total_hours, $standardHours); // Cap at standard
$hoursRatio = $isDailyWage ? ($hoursWorked / $standardHours) : 1.0;
$earnedDaily = $actualDailyRate * $dayMultiplier * $hoursRatio;
if ($isDailyWage) {
$earnedBasicSalary += $actualDailyRate * $hoursRatio;
} else {
$earnedBasicSalary += $actualDailyRate;
}
if ($dayMultiplier > 1.0) {
$premium = ($earnedDaily - ($actualDailyRate * $hoursRatio));
if ($isRegularHoliday || $isSpecialHoliday) {
$holidayPay += $premium;
}
}
}
```
> [!IMPORTANT]
> This only applies to **daily wage** employees. Monthly employees always get the full daily rate regardless of hours (they're salaried).
---
## Files to Modify
### [MODIFY] `app/Services/PayrollService.php`
1. **Line ~137-149**: Fix leave detection for absent-status records
2. **Line ~220-221**: Remove dayMultiplier from ND formula
3. **Line ~177-188**: Prorate holiday premium for daily employees
4. **Line ~227-249**: Fix LWOP/absence detection in "Did NOT work" block
## Verification
- [ ] Ana Regina: 2 LWOP should show in payslip
- [ ] Dinh Chavez: ND amount should be lower (flat 10%)
- [ ] Dinh Chavez: Holiday premium prorated to 7.28/8 of daily rate
- [ ] Monthly employees (Paul, Mart): unchanged behavior
- [ ] Run payroll for full cut-off and compare with client's manual computation