dstpabuaran.com/app/Jobs/CheckAttendancePenaltiesJob.php

217 lines
6.9 KiB
PHP

<?php
namespace App\Jobs;
use App\Enums\PayrollAdjustmentType;
use App\Enums\PayrollStatus;
use App\Models\Attendance;
use App\Models\Employee;
use App\Models\Payroll;
use App\Models\PayrollAdjustment;
use App\Models\PayrollPeriod;
use App\Models\User;
use App\Services\NotificationService;
use App\Settings\HRSettings;
use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
class CheckAttendancePenaltiesJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public function handle(): void
{
$yesterday = now()->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,
]);
}
}