Files
HRM-System/app/Services/ThirteenthMonthService.php

202 lines
8.3 KiB
PHP

<?php
namespace App\Services;
use App\Models\Employee;
use App\Models\PayrollEntry;
use App\Models\ThirteenthMonthRun;
use App\Models\ThirteenthMonthEntry;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class ThirteenthMonthService
{
/**
* Calculate 13th Month Pay for a single employee over a custom month/year range.
* e.g., November 2025 to December 2026, or November 2025 to October 2026.
* Formula: Sum(Basic Salary + Paid Leaves + Holiday Pay earned in range) / 12
*/
public function calculateForEmployee(Employee $employee, int $startYear, int $startMonth, int $endYear, int $endMonth): array
{
$rangeStart = Carbon::createFromDate($startYear, $startMonth, 1)->startOfMonth();
$rangeEnd = Carbon::createFromDate($endYear, $endMonth, 1)->endOfMonth();
$monthlyEarnings = [];
$totalBaseEarned = 0.00;
// Build list of months in range as "YYYY-MM"
$cursor = $rangeStart->copy();
while ($cursor->lte($rangeEnd)) {
$key = $cursor->format('Y-m');
$monthlyEarnings[$key] = 0.00;
$cursor->addMonth();
}
// Query processed payroll entries for this employee falling within the range
$entries = PayrollEntry::where(function($q) use ($employee) {
$q->where('employee_id', $employee->user_id)
->orWhere('employee_id', $employee->id);
})
->whereHas('payrollRun', function($q) use ($rangeStart, $rangeEnd) {
$q->whereBetween('pay_period_start', [$rangeStart->format('Y-m-d'), $rangeEnd->format('Y-m-d')])
->orWhereBetween('pay_period_end', [$rangeStart->format('Y-m-d'), $rangeEnd->format('Y-m-d')]);
})
->with('payrollRun')
->get();
if ($entries->count() > 0) {
foreach ($entries as $entry) {
$periodDate = Carbon::parse($entry->payrollRun->pay_period_start ?? $entry->created_at);
$key = $periodDate->format('Y-m');
if (array_key_exists($key, $monthlyEarnings)) {
// Actual Basic Salary Earned (net of absences/tardiness)
$basic = (float) ($entry->basic_salary ?? 0);
// Add Paid Leaves (Sick Leave, Vacation Leave, Birthday Leave)
// Excludes: LWOP (Unpaid Leaves), Holiday Pay, Overtime, Night Differential, COLA
$paidLeaves = 0.00;
if (is_array($entry->earnings_breakdown)) {
foreach ($entry->earnings_breakdown as $item) {
$category = strtolower($item['name'] ?? '');
if (str_contains($category, 'paid leave')) {
$paidLeaves += (float) ($item['amount'] ?? 0);
}
}
}
$monthlyEarnings[$key] += ($basic + $paidLeaves);
}
}
} else {
// Fallback for active employees if no payroll entries exist yet
$salary = (float) ($employee->basic_salary ?? 20000.00);
if ($salary == 0 && $employee->user) {
$salaryData = \App\Models\EmployeeSalary::where('employee_id', $employee->user_id)->first();
$salary = $salaryData ? (float)$salaryData->basic_salary : 20000.00;
}
$joinDate = $employee->joining_date ? Carbon::parse($employee->joining_date) : null;
$cursor = $rangeStart->copy();
while ($cursor->lte($rangeEnd)) {
$key = $cursor->format('Y-m');
$mStart = $cursor->copy()->startOfMonth();
$mEnd = $cursor->copy()->endOfMonth();
if ($joinDate && $joinDate->gt($mEnd)) {
$monthlyEarnings[$key] = 0.00;
} elseif ($joinDate && $joinDate->between($mStart, $mEnd)) {
$daysInMonth = $mEnd->day;
$workedDays = $daysInMonth - $joinDate->day + 1;
$monthlyEarnings[$key] = round(($salary / $daysInMonth) * $workedDays, 2);
} else {
$monthlyEarnings[$key] = $salary;
}
$cursor->addMonth();
}
}
// Sum up base earnings across all months in the range
foreach ($monthlyEarnings as $key => $amt) {
$totalBaseEarned += $amt;
}
// DOLE Standard: 13th Month Pay = Total Base Earned / 12
$computedAmount = round($totalBaseEarned / 12, 2);
// TRAIN Law Tax Exemption: First P90,000 is tax-exempt
$taxExemptThreshold = 90000.00;
$nonTaxableAmount = min($computedAmount, $taxExemptThreshold);
$taxableAmount = max(0.00, $computedAmount - $taxExemptThreshold);
$finalPayout = $computedAmount;
return [
'monthly_earnings' => $monthlyEarnings,
'total_base_earned' => round($totalBaseEarned, 2),
'computed_amount' => $computedAmount,
'adjustment_amount' => 0.00,
'taxable_amount' => round($taxableAmount, 2),
'non_taxable_amount' => round($nonTaxableAmount, 2),
'final_payout' => round($finalPayout, 2),
];
}
/**
* Generate or update a batch 13th Month Pay run.
*/
public function generateRun(int $startYear, int $startMonth, int $endYear, int $endMonth, ?int $branchId = null, ?string $title = null, ?int $userId = null): ThirteenthMonthRun
{
return DB::transaction(function () use ($startYear, $startMonth, $endYear, $endMonth, $branchId, $title, $userId) {
$branchName = $branchId ? (\App\Models\Branch::find($branchId)->name ?? '') : 'All Branches';
$startName = Carbon::createFromDate($startYear, $startMonth, 1)->format('M Y');
$endName = Carbon::createFromDate($endYear, $endMonth, 1)->format('M Y');
$defaultTitle = "13th Month Pay ({$startName} - {$endName})" . ($branchName ? " [{$branchName}]" : '');
// Create or update existing draft run
$run = ThirteenthMonthRun::updateOrCreate(
[
'start_year' => $startYear,
'start_month' => $startMonth,
'end_year' => $endYear,
'end_month' => $endMonth,
'branch_id' => $branchId,
'status' => 'draft',
],
[
'year' => $endYear,
'title' => $title ?: $defaultTitle,
'created_by' => $userId,
]
);
// Fetch active employees
$query = Employee::query();
if ($branchId) {
$query->where('branch_id', $branchId);
}
$employees = $query->with('user')->get();
$totalBase = 0.00;
$totalPayout = 0.00;
$count = 0;
foreach ($employees as $employee) {
$calc = $this->calculateForEmployee($employee, $startYear, $startMonth, $endYear, $endMonth);
ThirteenthMonthEntry::updateOrCreate(
[
'thirteenth_month_run_id' => $run->id,
'employee_id' => $employee->id,
],
[
'user_id' => $employee->user_id,
'monthly_earnings' => $calc['monthly_earnings'],
'total_base_earned' => $calc['total_base_earned'],
'computed_amount' => $calc['computed_amount'],
'adjustment_amount' => 0.00,
'taxable_amount' => $calc['taxable_amount'],
'non_taxable_amount' => $calc['non_taxable_amount'],
'final_payout' => $calc['final_payout'],
]
);
$totalBase += $calc['total_base_earned'];
$totalPayout += $calc['final_payout'];
$count++;
}
$run->update([
'total_base_earnings' => round($totalBase, 2),
'total_payout' => round($totalPayout, 2),
'employee_count' => $count,
]);
return $run->load(['entries.employee.user', 'entries.employee.designation', 'branch']);
});
}
}