Files
HRM-System/app/Console/Commands/AuditPayroll.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;
}
}