202 lines
8.3 KiB
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']);
|
|
});
|
|
}
|
|
}
|