119 lines
4.3 KiB
PHP
119 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use App\Models\PayrollEntry;
|
|
use App\Services\PayrollService;
|
|
use Carbon\Carbon;
|
|
|
|
class AuditPayroll extends Command
|
|
{
|
|
protected $signature = 'payroll:audit {start_date?} {end_date?}';
|
|
protected $description = 'Audit existing payroll entries against dynamic calculation results to detect inconsistencies';
|
|
|
|
public function handle(PayrollService $payrollService)
|
|
{
|
|
$startDate = $this->argument('start_date');
|
|
$endDate = $this->argument('end_date');
|
|
|
|
$query = PayrollEntry::with(['employee.employee', 'payrollRun']);
|
|
|
|
if ($startDate && $endDate) {
|
|
$query->whereHas('payrollRun', function ($q) use ($startDate, $endDate) {
|
|
$q->where('pay_period_start', '>=', $startDate)
|
|
->where('pay_period_end', '<=', $endDate);
|
|
});
|
|
$this->info("Auditing payroll entries for period: $startDate to $endDate");
|
|
} else {
|
|
$this->info("Auditing all existing payroll entries");
|
|
}
|
|
|
|
$entries = $query->get();
|
|
|
|
if ($entries->isEmpty()) {
|
|
$this->warn("No payroll entries found to audit.");
|
|
return 0;
|
|
}
|
|
|
|
$totalChecked = 0;
|
|
$inconsistenciesFound = 0;
|
|
$headers = ['Employee', 'Period', 'Metric', 'Stored', 'Computed', 'Diff'];
|
|
$rows = [];
|
|
|
|
foreach ($entries as $entry) {
|
|
$user = $entry->employee;
|
|
if (!$user) {
|
|
$this->warn("Payroll Entry ID {$entry->id} has no associated user.");
|
|
continue;
|
|
}
|
|
|
|
$employee = $user->employee;
|
|
if (!$employee) {
|
|
$this->warn("User {$user->name} (ID: {$user->id}) has no employee profile.");
|
|
continue;
|
|
}
|
|
|
|
$run = $entry->payrollRun;
|
|
if (!$run) {
|
|
$this->warn("Payroll Entry ID {$entry->id} has no associated payroll run.");
|
|
continue;
|
|
}
|
|
|
|
$totalChecked++;
|
|
|
|
$computed = $payrollService->calculateForPeriod(
|
|
$employee,
|
|
$run->pay_period_start->toDateString(),
|
|
$run->pay_period_end->toDateString()
|
|
);
|
|
|
|
// Metrics to compare
|
|
$metrics = [
|
|
'Basic Salary' => [(float)$entry->basic_salary, (float)$computed['basic_salary']],
|
|
'Total Earnings' => [(float)$entry->total_earnings, (float)$computed['total_earnings']],
|
|
'Total Deductions' => [(float)$entry->total_deductions, (float)$computed['total_deductions']],
|
|
'Net Pay' => [(float)$entry->net_pay, (float)$computed['net_pay']],
|
|
];
|
|
|
|
$entryHasInconsistency = false;
|
|
foreach ($metrics as $metricName => $values) {
|
|
$storedVal = $values[0];
|
|
$computedVal = $values[1];
|
|
$diff = abs($storedVal - $computedVal);
|
|
|
|
// Flag if difference is greater than 0.05 PHP
|
|
if ($diff > 0.05) {
|
|
$entryHasInconsistency = true;
|
|
$rows[] = [
|
|
$user->name,
|
|
$run->pay_period_start->format('Y-m-d') . ' / ' . $run->pay_period_end->format('Y-m-d'),
|
|
$metricName,
|
|
number_format($storedVal, 2),
|
|
number_format($computedVal, 2),
|
|
number_format($storedVal - $computedVal, 2),
|
|
];
|
|
}
|
|
}
|
|
|
|
if ($entryHasInconsistency) {
|
|
$inconsistenciesFound++;
|
|
}
|
|
}
|
|
|
|
if ($inconsistenciesFound > 0) {
|
|
$this->error("\nFound $inconsistenciesFound inconsistent payroll entries out of $totalChecked checked:");
|
|
$this->table($headers, $rows);
|
|
|
|
$this->info("\nDetailed Breakdown of Inconsistencies:");
|
|
foreach ($rows as $row) {
|
|
$this->line(" - <fg=red>{$row[0]}</> ({$row[1]}): {$row[2]} differs by <fg=yellow>{$row[5]} PHP</> (Stored: {$row[3]} vs Computed: {$row[4]})");
|
|
}
|
|
} else {
|
|
$this->info("\nAll $totalChecked audited payroll entries match computed values perfectly! ✅");
|
|
}
|
|
|
|
return $inconsistenciesFound > 0 ? 1 : 0;
|
|
}
|
|
}
|