feat(payroll): implemented compounded DOLE multipliers for OT and Night Differential
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user