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(); } }