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

5.5 KiB
Raw Blame History

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)

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:

} 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)

// 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)

$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:

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