Files
HRM-System/app/Services/PayrollService.php
2026-04-23 13:57:43 +08:00

207 lines
8.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Models\Employee;
use App\Models\AttendanceRecord;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class PayrollService
{
/**
* Compute cumulative payroll: daily_rate × days_worked.
* Philippine statutory contributions based on full monthly salary bracket.
*/
public function calculateForPeriod(Employee $employee, $startDate, $endDate)
{
// 1. Fetch salary config
$salaryData = \App\Models\EmployeeSalary::where('employee_id', $employee->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: 10PM6AM
$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,
]
];
}
}