Files
HRM-System/app/Services/PayrollService.php
2026-04-20 00:20:10 +08:00

194 lines
8.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\Employee;
use App\Models\AttendanceRecord;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class PayrollService
{
/**
* Compute realistic simulation payroll based on real-world Phil. Statutory logic.
*/
public function calculateForPeriod(Employee $employee, $startDate, $endDate)
{
// 1. Fetch exact local salary config dynamically tied to frontend UI
$salaryData = \App\Models\EmployeeSalary::where('employee_id', $employee->user_id)->first();
// Safe defaults if local profile is not totally configured
$grossMonthly = $salaryData ? $salaryData->basic_salary : 20000;
$payFrequency = $salaryData ? $salaryData->pay_frequency : 'monthly';
$dailyRate = $grossMonthly / 26; // Strictly mathematical translation for Daily Rate
$isSemiMonthly = ($payFrequency === 'semi-monthly');
$divisor = $isSemiMonthly ? 2 : 1;
// Base Salary for the period
$periodBasicSalary = $grossMonthly / $divisor;
// Daily rate strictly derived if not explicitly set
$actualDailyRate = $grossMonthly / 26;
// 2. Fetch Attendance
$attendances = AttendanceRecord::where('employee_id', $employee->user_id)
->whereBetween('date', [$startDate, $endDate])
->get();
$daysWorked = 0;
$renderedHours = 0;
$lateHours = 0;
$absencesPaidCount = 0;
$absencesCount = 0;
$nightDiffHours = 0;
// Expected working hours for 15-day range excluding Sundays = ~12 days based on actual ranges?
$expectedHours = 0;
$currentDate = \Carbon\Carbon::parse($startDate);
$periodEndObj = \Carbon\Carbon::parse($endDate);
while ($currentDate->lte($periodEndObj)) {
$dateString = $currentDate->toDateString();
// Find if there's a physical database row for today
$att = $attendances->firstWhere('date', $dateString) ?: $attendances->firstWhere('date', $dateString . ' 00:00:00');
if ($att && $att->is_rest_day) {
// Safely isolated by the explicit Rest Day matrix
$currentDate->addDay();
continue;
}
// Standard Working Day math
$expectedHours += 9;
if (!$att || $att->is_absent || $att->status === 'absent') {
// If there's no log at all, or it explicitly says absent, they are absent.
$absencesCount++;
} else {
$daysWorked++;
if ($att->total_hours) {
$renderedHours += $att->total_hours;
}
if ($att->is_late) {
$lateHours += (float) ($att->late_hours ?? 0); // Exact decimal deduction from synced legacy log
}
// --- NIGHT DIFFERENTIAL ENGINE ---
if (!empty($att->clock_in) && !empty($att->clock_out)) {
try {
$in = \Carbon\Carbon::parse($dateString . ' ' . $att->clock_in);
$out = \Carbon\Carbon::parse($dateString . ' ' . $att->clock_out);
// Handle Midnight Crossover
if ($out->lt($in)) {
$out->addDay();
}
// DOLE Windows: 12:00AM-6:00AM (Today) and 10:00PM-6:00AM (Tonight -> Tomorrow)
$windows = [
[\Carbon\Carbon::parse($dateString)->startOfDay(), \Carbon\Carbon::parse($dateString)->setTime(6, 0)],
[\Carbon\Carbon::parse($dateString)->setTime(22, 0), \Carbon\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) / 60;
}
}
} catch (\Exception $e) {
// Skip unparseable malformed logs mechanically
}
}
// ---------------------------------
}
$currentDate->addDay();
}
// Apply Manager Exemptions
if ($employee->employee_id === 'SEB2018-2008T') {
$lateHours = 0; // Exempt from Late/Tardiness
}
// 3. Compute Attendance Deductions
$absenceDeduction = $absencesCount * $actualDailyRate;
$lateDeduction = $lateHours * ($actualDailyRate / 8);
// 4. Compute Statutory Contributions (2025/2026 Tables on FULL GROSS then divided by 2)
// SSS EE Share (4.5% of MSC up to P30k)
$msc = min(floor($grossMonthly / 500) * 500, 30000);
// Check exactly for native DB schema Overrides explicitly!
// Wait: The screenshot shows standard monthly values but $salaryData defines them!
$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;
$tax = 0.00; // Zero tax shown in screenshot simulation
// Night Differential Money Calculation (10% DOLE Premium)
$hourlyRate = $actualDailyRate / 8;
$nightDiffAmount = $nightDiffHours * ($hourlyRate * 0.10);
$earnings = [
['name' => 'Basic Salary', 'amount' => number_format($periodBasicSalary, 2, '.', '')],
['name' => 'Overtime (Regular)', 'amount' => number_format(0, 2, '.', '')],
['name' => 'Night Differential', 'amount' => number_format($nightDiffAmount, 2, '.', '')],
['name' => 'Holiday Pay', 'amount' => number_format(0, 2, '.', '')],
];
$deductions = [];
if ($lateDeduction > 0) {
$deductions[] = ['name' => 'Late Deduction', 'amount' => number_format($lateDeduction, 2, '.', '')];
}
$deductions[] = ['name' => 'Absence Deduction', 'amount' => number_format($absenceDeduction, 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, '.', '')];
$totalEarnings = $periodBasicSalary + $nightDiffAmount;
$totalDeductions = $absenceDeduction + $lateDeduction + $sssCutoff + $phCutoff + $pagibigCutoff + $tax;
$netPay = $totalEarnings - $totalDeductions;
return [
'gross_monthly' => $grossMonthly,
'pay_type' => $isSemiMonthly ? 'Semi-Monthly' : 'Monthly',
'daily_rate' => $actualDailyRate,
'basic_salary' => $periodBasicSalary,
'earnings' => $earnings,
'deductions' => $deductions,
'total_earnings' => $totalEarnings,
'total_deductions' => $totalDeductions,
'net_pay' => $netPay,
'summary' => [
'days_worked' => $daysWorked,
'expected_hours' => number_format($expectedHours, 2),
'rendered_hours' => number_format($renderedHours, 2),
'night_diff' => number_format($nightDiffHours, 2),
'absences' => $absencesCount,
'absence_amount' => $absenceDeduction,
'leaves' => 0,
'reg_ot' => 0,
'night_diff' => 0,
'holiday' => 0,
]
];
}
}