store/app/Console/Commands/ApplyAttendancePenalties.php

205 lines
6.9 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Enums\PayrollAdjustmentType;
use App\Enums\Role;
use App\Models\Attendance;
use App\Models\Employee;
use App\Models\LeaveRequest;
use App\Models\Payroll;
use App\Models\PayrollPeriod;
use App\Models\User;
use App\Settings\HrSettings;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ApplyAttendancePenalties extends Command
{
protected $signature = 'attendance:apply-penalties {--date= : Tanggal yang diproses (default: kemarin)}';
protected $description = 'Buat potongan gaji otomatis untuk keterlambatan dan bolos';
public function handle(): int
{
$date = $this->option('date')
? Carbon::parse($this->option('date'))->startOfDay()
: Carbon::yesterday()->startOfDay();
$settings = app(HrSettings::class);
$latePenalty = $settings->late_penalty_amount;
$absentPenalty = $settings->absent_penalty_amount;
$scheduledTime = $settings->scheduled_check_in_time;
if ($latePenalty <= 0 && $absentPenalty <= 0) {
$this->info('Denda keterlambatan dan bolos belum diatur (0). Lewatkan.');
return self::SUCCESS;
}
$scheduledCheckIn = Carbon::createFromFormat('Y-m-d H:i', $date->format('Y-m-d').' '.$scheduledTime);
$createdBy = $this->resolveSystemUser();
if ($createdBy === null) {
$this->error('Tidak ditemukan user sistem (developer/owner) untuk mencatat penalti.');
return self::FAILURE;
}
$employees = Employee::query()
->whereHas('user', fn ($query) => $query->active())
->get();
$lateCount = 0;
$absentCount = 0;
$skippedLeave = 0;
$skippedDuplicate = 0;
try {
DB::transaction(function () use ($employees, $date, $scheduledCheckIn, $latePenalty, $absentPenalty, $createdBy, &$lateCount, &$absentCount, &$skippedLeave, &$skippedDuplicate): void {
foreach ($employees as $employee) {
$hasLeave = LeaveRequest::query()
->approved()
->where('employee_id', $employee->id)
->whereDate('start_date', '<=', $date)
->whereDate('end_date', '>=', $date)
->exists();
if ($hasLeave) {
$skippedLeave++;
continue;
}
$attendance = Attendance::query()
->where('employee_id', $employee->id)
->whereDate('attendance_date', $date)
->first();
if ($attendance === null) {
if ($absentPenalty <= 0) {
continue;
}
$payroll = $this->findOpenPayroll($employee->id);
if ($payroll === null) {
continue;
}
$alreadyExists = $payroll->adjustments()
->whereNull('attendance_id')
->deduction()
->where('description', 'like', "%Bolos {$date->format('d/m/Y')}%")
->exists();
if ($alreadyExists) {
$skippedDuplicate++;
continue;
}
$payroll->adjustments()->create([
'type' => PayrollAdjustmentType::DEDUCTION,
'amount' => $absentPenalty,
'description' => "Bolos {$date->format('d/m/Y')}",
'created_by_id' => $createdBy->id,
]);
$payroll->load('adjustments');
$payroll->recalculateAmounts();
$payroll->save();
$absentCount++;
continue;
}
if ($latePenalty <= 0) {
continue;
}
$checkInTime = Carbon::parse($attendance->check_in_at);
if ($checkInTime->lte($scheduledCheckIn)) {
continue;
}
$payroll = $this->findOpenPayroll($employee->id);
if ($payroll === null) {
continue;
}
$alreadyExists = $payroll->adjustments()
->where('attendance_id', $attendance->id)
->where('type', PayrollAdjustmentType::DEDUCTION)
->exists();
if ($alreadyExists) {
$skippedDuplicate++;
continue;
}
$minutesLate = (int) $scheduledCheckIn->diffInMinutes($checkInTime);
$payroll->adjustments()->create([
'type' => PayrollAdjustmentType::DEDUCTION,
'amount' => $latePenalty,
'description' => "Terlambat {$date->format('d/m/Y')} ({$minutesLate} menit)",
'attendance_id' => $attendance->id,
'created_by_id' => $createdBy->id,
]);
$payroll->load('adjustments');
$payroll->recalculateAmounts();
$payroll->save();
$lateCount++;
}
});
} catch (\Throwable $e) {
Log::error("Gagal menerapkan penalti presensi: {$e->getMessage()}", [
'trace' => $e->getTraceAsString(),
]);
$this->error('Gagal menerapkan penalti presensi: '.$e->getMessage());
return self::FAILURE;
}
$this->info("Selesai memproses tanggal {$date->format('d/m/Y')}:");
$this->info(" - Terlambat: {$lateCount} pegawai");
$this->info(" - Bolos: {$absentCount} pegawai");
$this->info(" - Lewati (cuti): {$skippedLeave} pegawai");
$this->info(" - Lewati (duplikat): {$skippedDuplicate} pegawai");
return self::SUCCESS;
}
private function findOpenPayroll(int $employeeId): ?Payroll
{
$period = PayrollPeriod::query()->open()->first();
if ($period === null) {
return null;
}
return Payroll::query()
->where('payroll_period_id', $period->id)
->where('employee_id', $employeeId)
->first();
}
private function resolveSystemUser(): ?User
{
return User::query()
->whereHas('roles', fn ($query) => $query->whereIn('name', [Role::DEVELOPER->value, Role::OWNER->value]))
->first()
?? User::query()->first();
}
}