user_id)->first(); $grossMonthly = $salaryData ? $salaryData->basic_salary : 20000; $payFrequency = $salaryData ? $salaryData->pay_frequency : 'monthly'; $isSemiMonthly = ($payFrequency === 'semi-monthly'); $divisor = $isSemiMonthly ? 2 : 1; // Daily rate: monthly salary / 26 standard working days $actualDailyRate = $grossMonthly / 26; $hourlyRate = $actualDailyRate / 8; // 2. Fetch Attendance $attendances = AttendanceRecord::where('employee_id', $employee->user_id) ->whereBetween('date', [$startDate, $endDate]) ->get(); $daysWorked = 0; $renderedHours = 0; $lateHours = 0; $absencesCount = 0; $nightDiffHours = 0; $totalOvertimeHours = 0; $expectedWorkingDays = 0; $expectedHours = 0; $currentDate = Carbon::parse($startDate); $periodEndObj = Carbon::parse($endDate); // Index attendances by date for fast lookup $indexedAttendances = $attendances->keyBy(function($item) { return $item->date instanceof Carbon ? $item->date->toDateString() : substr((string)$item->date, 0, 10); }); while ($currentDate->lte($periodEndObj)) { $dateString = $currentDate->toDateString(); $att = $indexedAttendances->get($dateString); // Skip rest days — not a working day if ($att && $att->is_rest_day) { $currentDate->addDay(); continue; } // Count expected working days $expectedWorkingDays++; $expectedHours += 9; if (!$att || $att->is_absent || $att->status === 'absent') { // Absent — simply not paid (cumulative model) $absencesCount++; } else { $daysWorked++; if ($att->total_hours) { $renderedHours += $att->total_hours; } if ($att->is_late) { $lateHours += (float) ($att->late_hours ?? 0); } // --- NIGHT DIFFERENTIAL ENGINE --- if (!empty($att->clock_in) && !empty($att->clock_out)) { try { $in = Carbon::parse($dateString . ' ' . $att->clock_in); $out = Carbon::parse($dateString . ' ' . $att->clock_out); if ($out->lt($in)) { $out->addDay(); } // DOLE ND Windows: 10PM–6AM $windows = [ [Carbon::parse($dateString)->startOfDay(), Carbon::parse($dateString)->setTime(6, 0)], [Carbon::parse($dateString)->setTime(22, 0), Carbon::parse($dateString)->addDay()->setTime(6, 0)] ]; foreach ($windows as $window) { $overlapStart = $in->copy()->max($window[0]); $overlapEnd = $out->copy()->min($window[1]); if ($overlapEnd->gt($overlapStart)) { $nightDiffHours += $overlapEnd->diffInMinutes($overlapStart, true) / 60; } } } catch (\Exception $e) { // Skip malformed logs } } // --- OVERTIME ENGINE --- if ($att->overtime_hours) { $totalOvertimeHours += (float) $att->overtime_hours; } } $currentDate->addDay(); } // Apply Time Exemption (salaried exempt employees) if ($salaryData && $salaryData->is_time_exempt) { $lateHours = 0; $nightDiffHours = 0; $totalOvertimeHours = 0; } $holidayPay = 0; // 3. CUMULATIVE EARNINGS: daily_rate × days_worked $earnedBasicSalary = $daysWorked * $actualDailyRate; // Late deduction remains as explicit hourly penalty $lateDeduction = $lateHours * $hourlyRate; // OT and ND premiums $regularOtTotal = $totalOvertimeHours * ($hourlyRate * 1.25); $nightDiffAmount = $nightDiffHours * ($hourlyRate * 0.10); // 4. Statutory Contributions — only if employee has earnings (prevents negative payroll) $sssCutoff = 0; $phCutoff = 0; $pagibigCutoff = 0; $tax = 0.00; if ($daysWorked > 0) { // Based on FULL monthly salary bracket (PH law) $msc = min(floor($grossMonthly / 500) * 500, 30000); $sssMonthly = $salaryData->sss_fixed ?? ($msc * 0.045); $philhealthMonthly = $salaryData->philhealth_fixed ?? ($grossMonthly * 0.025); if ($salaryData && is_null($salaryData->philhealth_fixed) && $grossMonthly <= 10000) $philhealthMonthly = 250; if ($salaryData && is_null($salaryData->philhealth_fixed) && $grossMonthly >= 100000) $philhealthMonthly = 2500; $pagibigMonthly = $salaryData->pagibig_fixed ?? (100 * $divisor); $sssCutoff = $sssMonthly / $divisor; $phCutoff = $philhealthMonthly / $divisor; $pagibigCutoff = $pagibigMonthly / $divisor; } // 5. Build earnings array $earnings = [ ['name' => 'Basic Salary', 'amount' => number_format($earnedBasicSalary, 2, '.', '')], ['name' => 'Overtime (Regular)', 'amount' => number_format($regularOtTotal, 2, '.', '')], ['name' => 'Night Differential', 'amount' => number_format($nightDiffAmount, 2, '.', '')], ['name' => 'Holiday Pay', 'amount' => number_format($holidayPay, 2, '.', '')], ]; // 6. Build deductions array — NO absence deduction (cumulative model) $deductions = []; if ($lateDeduction > 0) { $deductions[] = ['name' => 'Late Deduction', 'amount' => number_format($lateDeduction, 2, '.', '')]; } $deductions[] = ['name' => 'SSS Contribution (EE)', 'amount' => number_format($sssCutoff, 2, '.', '')]; $deductions[] = ['name' => 'PhilHealth Contribution (EE)', 'amount' => number_format($phCutoff, 2, '.', '')]; $deductions[] = ['name' => 'Pag-IBIG Contribution (EE)', 'amount' => number_format($pagibigCutoff, 2, '.', '')]; $deductions[] = ['name' => 'Withholding Tax', 'amount' => number_format($tax, 2, '.', '')]; // 7. Totals $totalEarnings = $earnedBasicSalary + $nightDiffAmount + $regularOtTotal + $holidayPay; $totalDeductions = $lateDeduction + $sssCutoff + $phCutoff + $pagibigCutoff + $tax; $netPay = $totalEarnings - $totalDeductions; return [ 'gross_monthly' => $grossMonthly, 'pay_type' => $isSemiMonthly ? 'Semi-Monthly' : 'Monthly', 'daily_rate' => $actualDailyRate, 'basic_salary' => $earnedBasicSalary, 'earnings' => $earnings, 'deductions' => $deductions, 'total_earnings' => $totalEarnings, 'total_deductions' => $totalDeductions, 'net_pay' => $netPay, 'summary' => [ 'days_worked' => $daysWorked, 'expected_working_days' => $expectedWorkingDays, 'expected_hours' => number_format($expectedHours, 2), 'rendered_hours' => number_format($renderedHours, 2), 'night_diff_hours' => number_format($nightDiffHours, 2), 'absences' => $absencesCount, 'leaves' => 0, 'reg_ot_hours' => number_format($totalOvertimeHours, 2), 'reg_ot_amount' => $regularOtTotal, 'night_diff_amount' => $nightDiffAmount, 'holiday' => 0, ] ]; } }