subDay(); $dayOfWeek = (int) $yesterday->format('w'); if ($dayOfWeek === 0 || $dayOfWeek === 6) { return; } $hrSettings = app(HRSettings::class); if ($hrSettings->late_penalty_amount <= 0 && $hrSettings->absent_penalty_amount <= 0) { return; } $cuttingDay = 5; if ($yesterday->day < $cuttingDay) { $periodMonth = $yesterday->month; $periodYear = $yesterday->year; } else { $periodMonth = $yesterday->month + 1; $periodYear = $yesterday->year; if ($periodMonth > 12) { $periodMonth = 1; $periodYear++; } } $period = PayrollPeriod::where('year', $periodYear) ->where('month', $periodMonth) ->first(); if (! $period) { return; } $systemUser = User::first(); if (! $systemUser) { return; } $employees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true)) ->where(fn ($q) => $q->whereNull('resign_date')->orWhere('resign_date', '>=', $yesterday->toDateString())) ->get(); foreach ($employees as $employee) { $payroll = Payroll::where('payroll_period_id', $period->id) ->where('employee_id', $employee->id) ->where('status', PayrollStatus::UNPAID) ->first(); if (! $payroll) { continue; } $attendance = Attendance::where('employee_id', $employee->id) ->where('attendance_date', $yesterday->toDateString()) ->first(); if ($attendance) { $this->handleLate($attendance, $payroll, $hrSettings, $systemUser, $yesterday); } else { $this->handleAbsent($payroll, $hrSettings, $systemUser, $yesterday, $employee); } } } private function handleLate( Attendance $attendance, Payroll $payroll, HRSettings $hrSettings, User $systemUser, CarbonInterface $date ): void { if ($hrSettings->late_penalty_amount <= 0) { return; } $checkInTime = $attendance->check_in_at; [$officeHour, $officeMinute] = explode(':', $hrSettings->scheduled_check_in_time); $officeStart = $date->copy()->setTime((int) $officeHour, (int) $officeMinute, 0); if (! $checkInTime || $checkInTime->lte($officeStart)) { return; } $lateMinutes = (int) $checkInTime->diffInMinutes($officeStart); $existing = PayrollAdjustment::where('payroll_id', $payroll->id) ->where('attendance_id', $attendance->id) ->where('type', PayrollAdjustmentType::DEDUCTION) ->exists(); if ($existing) { return; } DB::transaction(function () use ($payroll, $attendance, $hrSettings, $systemUser, $date, $lateMinutes) { PayrollAdjustment::create([ 'payroll_id' => $payroll->id, 'created_by_id' => $systemUser->id, 'attendance_id' => $attendance->id, 'type' => PayrollAdjustmentType::DEDUCTION, 'amount' => $hrSettings->late_penalty_amount, 'description' => 'Denda keterlambatan '.$date->translatedFormat('l, d F Y').' - '.$lateMinutes.' menit', ]); $this->recalculatePayroll($payroll); }); $employeeUser = $payroll->employee->user ?? null; if ($employeeUser) { NotificationService::notifyUsers( [$employeeUser], 'Denda Keterlambatan', 'Anda terkena denda keterlambatan sebesar Rp '.number_format($hrSettings->late_penalty_amount, 0, ',', '.')." pada {$date->translatedFormat('l, d F Y')} ({$lateMinutes} menit).", route('admin.finance.payroll-periods.index'), ); } } private function handleAbsent( Payroll $payroll, HRSettings $hrSettings, User $systemUser, CarbonInterface $date, Employee $employee ): void { if ($hrSettings->absent_penalty_amount <= 0) { return; } $description = 'Denda ketidakhadiran '.$date->translatedFormat('l, d F Y'); $existing = PayrollAdjustment::where('payroll_id', $payroll->id) ->where('type', PayrollAdjustmentType::DEDUCTION) ->where('description', $description) ->exists(); if ($existing) { return; } DB::transaction(function () use ($payroll, $hrSettings, $systemUser, $description) { PayrollAdjustment::create([ 'payroll_id' => $payroll->id, 'created_by_id' => $systemUser->id, 'attendance_id' => null, 'type' => PayrollAdjustmentType::DEDUCTION, 'amount' => $hrSettings->absent_penalty_amount, 'description' => $description, ]); $this->recalculatePayroll($payroll); }); $employeeUser = $employee->user ?? null; if ($employeeUser) { NotificationService::notifyUsers( [$employeeUser], 'Denda Ketidakhadiran', 'Anda terkena denda ketidakhadiran sebesar Rp '.number_format($hrSettings->absent_penalty_amount, 0, ',', '.')." pada {$date->translatedFormat('l, d F Y')}.", route('admin.finance.payroll-periods.index'), ); } } private function recalculatePayroll(Payroll $payroll): void { $payroll->refresh(); $bonuses = $payroll->payrollAdjustments() ->where('type', PayrollAdjustmentType::BONUS) ->sum('amount'); $deductions = $payroll->payrollAdjustments() ->where('type', PayrollAdjustmentType::DEDUCTION) ->sum('amount'); $payroll->update([ 'bonus_amount' => $bonuses, 'deduction_amount' => $deductions, 'total_amount' => $payroll->base_salary + $bonuses - $deductions, ]); } }