From caa7c19fec6105cffa091f9b1df58bb7ab827ed8 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 28 Apr 2026 18:37:14 +0800 Subject: [PATCH] feat(payroll): implemented compounded DOLE multipliers for OT and Night Differential --- app/Services/PayrollService.php | 84 +++++++++++++++++++++++++++++---- storage/logs/laravel.log | 3 ++ 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/app/Services/PayrollService.php b/app/Services/PayrollService.php index 15b43fc6a..df2884a7d 100644 --- a/app/Services/PayrollService.php +++ b/app/Services/PayrollService.php @@ -73,7 +73,7 @@ class PayrollService // Expected if (!$isRestDay) { $expectedWorkingDays++; - $expectedHours += 9; + $expectedHours += 8; // Standard 8 hours } // Holiday Check @@ -97,7 +97,7 @@ class PayrollService $didWork = $att && !$att->is_absent && $att->status !== 'absent' && $att->total_hours > 0; - // DOLE Multiplier Logic + // DOLE Multiplier Logic (Day Premium) $dayMultiplier = 1.0; if ($isRegularHoliday && $isRestDay) { $dayMultiplier = 2.60; @@ -117,7 +117,7 @@ class PayrollService $renderedHours += $att->total_hours; $lateHours += (float) ($att->late_hours ?? 0); - // If they worked, they earn Base Rate * Day Multiplier. + // 1. Basic Pay for the day (Daily Rate * Day Multiplier) $earnedDaily = $actualDailyRate * $dayMultiplier; // Separate out the holiday/premium pay from the basic salary for the payslip breakdown @@ -126,16 +126,32 @@ class PayrollService $holidayPay += ($earnedDaily - $actualDailyRate); } - // Night Diff (from engine) - $ndHours = (float) ($att->night_diff_hours ?? 0); - $nightDiffHours += $ndHours; - $nightDiffAmount += $ndHours * ($hourlyRate * $dayMultiplier * 0.10); - - // Overtime (from engine) + // 2. Overtime Calculation (Compounded) $otHours = (float) ($att->overtime_hours ?? 0); $totalOvertimeHours += $otHours; - // Overtime DOLE is +25% on ordinary days, +30% on premium days + + // OT Rate Multiplier: 1.25 for regular, 1.30 for premium days $otRateMultiplier = ($dayMultiplier > 1.0) ? 1.30 : 1.25; + + // 3. Night Differential Calculation (Compounded) + $ndHours = (float) ($att->night_diff_hours ?? 0); + $nightDiffHours += $ndHours; + + // Standard Shift Working Hours (to detect OT overlap) + $standardHours = ($att->shift && $att->shift->working_hours > 0) ? (float)$att->shift->working_hours : 8.0; + + // Calculate ND that overlaps with OT + $ndOtHours = $this->calculateNdOtOverlap($att, $standardHours); + $ndRegHours = max(0, $ndHours - $ndOtHours); + + // ND Amount (Premium part only: 10%) + // Compounded: Base * DayMult * 10% for reg, Base * DayMult * OTMult * 10% for OT + $ndRegAmount = $ndRegHours * ($hourlyRate * $dayMultiplier * 0.10); + $ndOtAmount = $ndOtHours * ($hourlyRate * $dayMultiplier * $otRateMultiplier * 0.10); + + $nightDiffAmount += ($ndRegAmount + $ndOtAmount); + + // Final OT Total (already includes the Day Multiplier) $regularOtTotal += $otHours * ($hourlyRate * $dayMultiplier * $otRateMultiplier); } else { // Did NOT work. @@ -293,6 +309,54 @@ class PayrollService ]; } + /** + * Calculate how many Night Differential hours fall within Overtime. + * Assumes OT starts after standard hours of work. + */ + private function calculateNdOtOverlap($record, $standardHours) + { + if (!$record->clock_in || !$record->clock_out || $record->overtime_hours <= 0 || $record->night_diff_hours <= 0) { + return 0; + } + + $clockIn = Carbon::parse($record->date->format('Y-m-d') . ' ' . $record->clock_in); + $clockOut = Carbon::parse($record->date->format('Y-m-d') . ' ' . $record->clock_out); + if ($clockOut->lt($clockIn)) { $clockOut->addDay(); } + + // OT starts after $standardHours of rendered time + // Note: For simplicity, we assume continuous work. + // We include break time in the offset if it happened between clockIn and OT start. + $breakMinutes = ($record->shift && $record->shift->break_duration) ? $record->shift->break_duration : 0; + $otStartOffsetMinutes = ($standardHours * 60) + $breakMinutes; + + $otStart = $clockIn->copy()->addMinutes($otStartOffsetMinutes); + + // ND period is 10PM to 6AM + $ndStart = $clockIn->copy()->setTime(22, 0, 0); + $ndEnd = $ndStart->copy()->addHours(8); // 6AM next day + + // Handle case where ND start is before clockIn (e.g. shift starts at 5AM) + if ($ndStart->gt($clockIn->copy()->addHours(12))) { + $ndStart->subDay(); + $ndEnd->subDay(); + } + + $calculateOverlap = function($s1, $e1, $s2, $e2) { + $latestStart = $s1->max($s2); + $earliestEnd = $e1->min($e2); + return $latestStart->lt($earliestEnd) ? $latestStart->diffInMinutes($earliestEnd) / 60 : 0; + }; + + $overlap = $calculateOverlap($otStart, $clockOut, $ndStart, $ndEnd); + + // Check next ND period too (just in case) + $ndStart2 = $ndStart->copy()->addDay(); + $ndEnd2 = $ndEnd->copy()->addDay(); + $overlap += $calculateOverlap($otStart, $clockOut, $ndStart2, $ndEnd2); + + return round($overlap, 2); + } + private function calculateStatutoryContribution($type, $grossMonthly) { $bracket = \App\Models\StatutoryBracket::where('type', $type) diff --git a/storage/logs/laravel.log b/storage/logs/laravel.log index 4513a3a0e..8dc038487 100755 --- a/storage/logs/laravel.log +++ b/storage/logs/laravel.log @@ -3555,3 +3555,6 @@ Error: proc_open(/dev/tty): Failed to open stream: Device not configured at /Use [2026-04-28 10:23:59] local.INFO: Calendar Debug Dinh Chavez 2026-04-04: {"workedHours":5.016666666666667,"workingHours":9.0,"shiftStart":"00:00","clockIn":"00:00","isLateDynamic":false,"db_status":"present"} [2026-04-28 10:23:59] local.INFO: Calendar Debug Dinh Chavez 2026-04-05: {"workedHours":5.016666666666667,"workingHours":9.0,"shiftStart":"00:00","clockIn":"00:00","isLateDynamic":false,"db_status":"present"} [2026-04-28 10:23:59] local.INFO: Calendar Debug Dinh Chavez 2026-04-06: {"workedHours":6.016666666666667,"workingHours":9.0,"shiftStart":"00:00","clockIn":"23:10","isLateDynamic":false,"db_status":"present"} +[2026-04-28 10:32:51] local.INFO: Calendar Debug Dinh Chavez 2026-04-04: {"workedHours":5.016666666666667,"workingHours":9.0,"shiftStart":"00:00","clockIn":"00:00","isLateDynamic":false,"db_status":"present"} +[2026-04-28 10:32:51] local.INFO: Calendar Debug Dinh Chavez 2026-04-05: {"workedHours":5.016666666666667,"workingHours":9.0,"shiftStart":"00:00","clockIn":"00:00","isLateDynamic":false,"db_status":"present"} +[2026-04-28 10:32:51] local.INFO: Calendar Debug Dinh Chavez 2026-04-06: {"workedHours":6.016666666666667,"workingHours":9.0,"shiftStart":"00:00","clockIn":"23:10","isLateDynamic":false,"db_status":"present"}