150 lines
5.5 KiB
Markdown
150 lines
5.5 KiB
Markdown
# 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
|