dstpabuaran.com/tests/Feature/Admin/HR/AttendanceTest.php
Yoga Pangestu 67e5a6271f Add tests for CheckAttendancePenaltiesJob to validate attendance penalties logic
- Implement tests to ensure job skips execution on weekends, when penalties are zero, or when no payroll period exists.
- Validate late penalty creation for late check-ins and ensure no penalties for on-time or early check-ins.
- Test absent penalties for employees without attendance records.
- Ensure no duplicate penalties are created and that payroll recalculations are accurate after penalties are applied.
- Handle multiple employees and edge cases, including employees without associated users.
- Verify that the job can be dispatched to the queue and has the correct retry configuration.
2026-07-31 11:13:46 +07:00

734 lines
22 KiB
PHP

<?php
use App\Models\Attendance;
use App\Models\Employee;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
uses(RefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| AUTHENTICATION
|--------------------------------------------------------------------------
*/
test('guests are redirected to the login page', function () {
$response = $this->get(route('admin.hr.attendances.index'));
$response->assertRedirect(route('login'));
});
test('authenticated users can visit the attendance index page', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.hr.attendances.index'));
$response->assertOk();
});
/*
|--------------------------------------------------------------------------
| INDEX PAGE
|--------------------------------------------------------------------------
*/
test('index page displays attendances', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employeeUser = User::factory()->create();
$employeeUser->employee()->create([
'join_date' => '2024-01-01',
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
Attendance::factory()->create([
'employee_id' => $employeeUser->employee->id,
'attendance_date' => now()->toDateString(),
'check_in_at' => now()->setHour(8)->setMinute(0),
'check_out_at' => now()->setHour(17)->setMinute(0),
]);
$response = $this->get(route('admin.hr.attendances.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/hr/attendance/index')
->has('attendances', 1)
);
});
test('index page works with zero attendances', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.hr.attendances.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/hr/attendance/index')
->has('attendances', 0)
);
});
test('index page passes current year and month', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.hr.attendances.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->where('currentYear', now()->year)
->where('currentMonth', now()->month)
);
});
test('index page can filter by year and month', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employeeUser = User::factory()->create();
$employeeUser->employee()->create([
'join_date' => '2024-01-01',
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
Attendance::factory()->create([
'employee_id' => $employeeUser->employee->id,
'attendance_date' => '2024-03-15',
'check_in_at' => '2024-03-15 08:00:00',
'check_out_at' => '2024-03-15 17:00:00',
]);
$response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 3]));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->where('currentYear', 2024)
->where('currentMonth', 3)
->has('attendances', 1)
);
});
test('index page excludes attendances from other months', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employeeUser = User::factory()->create();
$employeeUser->employee()->create([
'join_date' => '2024-01-01',
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
Attendance::factory()->create([
'employee_id' => $employeeUser->employee->id,
'attendance_date' => '2024-03-15',
'check_in_at' => '2024-03-15 08:00:00',
]);
$response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 4]));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->has('attendances', 0)
);
});
test('index page passes hrSettings props', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.hr.attendances.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->has('hrSettings.scheduled_check_in_time')
->has('hrSettings.scheduled_check_out_time')
);
});
test('index page passes monthStats', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.hr.attendances.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->has('monthStats.working_days')
->has('monthStats.present')
->has('monthStats.absent')
->has('monthStats.leave')
);
});
test('index page passes todayAttendance as null when no attendance today', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.hr.attendances.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->where('todayAttendance', null)
);
});
/*
|--------------------------------------------------------------------------
| STORE / CHECK-IN
|--------------------------------------------------------------------------
*/
test('guest cannot check in', function () {
$response = $this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertRedirect(route('login'));
});
test('authenticated user without employee cannot check in', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertRedirect();
});
test('employee can check in successfully', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$response = $this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertRedirect();
$this->assertDatabaseHas('attendances', [
'employee_id' => $employee->id,
'attendance_date' => now()->toDateString(),
]);
});
test('employee cannot check in twice on the same day', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
Attendance::factory()->create([
'employee_id' => $employee->id,
'attendance_date' => now()->toDateString(),
'check_in_at' => now(),
]);
$response = $this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertRedirect();
$this->assertDatabaseCount('attendances', 1);
});
test('check in requires photo', function () {
$user = User::factory()->create();
$this->actingAs($user);
Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$response = $this->post(route('admin.hr.attendances.store'), [
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertRedirect();
$this->assertDatabaseCount('attendances', 0);
});
test('check in requires latitude', function () {
$user = User::factory()->create();
$this->actingAs($user);
Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$response = $this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'longitude' => 106.8456,
]);
$response->assertRedirect();
$this->assertDatabaseCount('attendances', 0);
});
test('check in requires longitude', function () {
$user = User::factory()->create();
$this->actingAs($user);
Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$response = $this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
]);
$response->assertRedirect();
$this->assertDatabaseCount('attendances', 0);
});
test('check in latitude must be numeric', function () {
$user = User::factory()->create();
$this->actingAs($user);
Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$response = $this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => 'not-a-number',
'longitude' => 106.8456,
]);
$response->assertRedirect();
$this->assertDatabaseCount('attendances', 0);
});
test('check in longitude must be numeric', function () {
$user = User::factory()->create();
$this->actingAs($user);
Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$response = $this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 'not-a-number',
]);
$response->assertRedirect();
$this->assertDatabaseCount('attendances', 0);
});
/*
|--------------------------------------------------------------------------
| UPDATE / CHECK-OUT
|--------------------------------------------------------------------------
*/
test('guest cannot check out', function () {
$attendance = Attendance::factory()->create([
'check_in_at' => now()->subHours(8),
'check_out_at' => null,
]);
$response = $this->put(route('admin.hr.attendances.update', $attendance), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertRedirect(route('login'));
});
test('employee can check out successfully', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$attendance = Attendance::factory()->create([
'employee_id' => $employee->id,
'attendance_date' => now()->toDateString(),
'check_in_at' => now()->subHours(8),
'check_out_at' => null,
]);
$response = $this->put(route('admin.hr.attendances.update', $attendance), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertRedirect();
$attendance->refresh();
$this->assertNotNull($attendance->check_out_at);
$this->assertEquals(-6.2088, $attendance->check_out_latitude);
$this->assertEquals(106.8456, $attendance->check_out_longitude);
});
test('check out requires photo', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$attendance = Attendance::factory()->create([
'employee_id' => $employee->id,
'attendance_date' => now()->toDateString(),
'check_in_at' => now()->subHours(8),
'check_out_at' => null,
]);
$response = $this->put(route('admin.hr.attendances.update', $attendance), [
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertRedirect();
$attendance->refresh();
$this->assertNull($attendance->check_out_at);
});
test('check out requires latitude', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$attendance = Attendance::factory()->create([
'employee_id' => $employee->id,
'attendance_date' => now()->toDateString(),
'check_in_at' => now()->subHours(8),
'check_out_at' => null,
]);
$response = $this->put(route('admin.hr.attendances.update', $attendance), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'longitude' => 106.8456,
]);
$response->assertRedirect();
$attendance->refresh();
$this->assertNull($attendance->check_out_at);
});
test('check out requires longitude', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$attendance = Attendance::factory()->create([
'employee_id' => $employee->id,
'attendance_date' => now()->toDateString(),
'check_in_at' => now()->subHours(8),
'check_out_at' => null,
]);
$response = $this->put(route('admin.hr.attendances.update', $attendance), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
]);
$response->assertRedirect();
$attendance->refresh();
$this->assertNull($attendance->check_out_at);
});
test('updating non-existent attendance returns 404', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->put(route('admin.hr.attendances.update', 999999), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$response->assertStatus(404);
});
/*
|--------------------------------------------------------------------------
| BY-DATE
|--------------------------------------------------------------------------
*/
test('guest cannot access by-date endpoint', function () {
$response = $this->get(route('admin.hr.attendances.by-date'));
$response->assertRedirect(route('login'));
});
test('by-date returns empty when no attendance for date', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->getJson(route('admin.hr.attendances.by-date', ['date' => '2024-01-01']));
$response->assertOk();
});
test('by-date defaults to today when no date parameter', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->getJson(route('admin.hr.attendances.by-date'));
$response->assertOk();
});
/*
|--------------------------------------------------------------------------
| MONTH STATS
|--------------------------------------------------------------------------
*/
test('month stats calculates correct working days for january 2024', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 1]));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->where('monthStats.working_days', 23)
);
});
test('month stats present count matches attendance records', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employeeUser = User::factory()->create();
$employeeUser->employee()->create([
'join_date' => '2024-01-01',
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
Attendance::factory()->create([
'employee_id' => $employeeUser->employee->id,
'attendance_date' => '2024-01-15',
'check_in_at' => '2024-01-15 08:00:00',
]);
Attendance::factory()->create([
'employee_id' => $employeeUser->employee->id,
'attendance_date' => '2024-01-16',
'check_in_at' => '2024-01-16 08:00:00',
]);
$response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 1]));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->where('monthStats.present', 2)
);
});
/*
|--------------------------------------------------------------------------
| MULTIPLE ATTENDANCES
|--------------------------------------------------------------------------
*/
test('multiple employees can have attendances on the same day', function () {
$user = User::factory()->create();
$this->actingAs($user);
$emp1User = User::factory()->create();
$emp1User->employee()->create([
'join_date' => '2024-01-01',
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$emp2User = User::factory()->create();
$emp2User->employee()->create([
'join_date' => '2024-01-01',
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
Attendance::factory()->create([
'employee_id' => $emp1User->employee->id,
'attendance_date' => '2024-01-15',
'check_in_at' => '2024-01-15 08:00:00',
]);
Attendance::factory()->create([
'employee_id' => $emp2User->employee->id,
'attendance_date' => '2024-01-15',
'check_in_at' => '2024-01-15 08:30:00',
]);
$response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 1]));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->has('attendances', 2)
);
});
test('employee can have multiple attendances across different days', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employeeUser = User::factory()->create();
$employeeUser->employee()->create([
'join_date' => '2024-01-01',
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
Attendance::factory()->create([
'employee_id' => $employeeUser->employee->id,
'attendance_date' => '2024-01-15',
'check_in_at' => '2024-01-15 08:00:00',
]);
Attendance::factory()->create([
'employee_id' => $employeeUser->employee->id,
'attendance_date' => '2024-01-16',
'check_in_at' => '2024-01-16 08:00:00',
]);
Attendance::factory()->create([
'employee_id' => $employeeUser->employee->id,
'attendance_date' => '2024-01-17',
'check_in_at' => '2024-01-17 08:00:00',
]);
$response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 1]));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->has('attendances', 3)
);
});
/*
|--------------------------------------------------------------------------
| DATA INTEGRITY
|--------------------------------------------------------------------------
*/
test('attendance stores correct check-in coordinates', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$attendance = Attendance::where('employee_id', $employee->id)->first();
$this->assertEquals(-6.2088, $attendance->check_in_latitude);
$this->assertEquals(106.8456, $attendance->check_in_longitude);
});
test('attendance has correct attendance_date', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$this->assertDatabaseHas('attendances', [
'employee_id' => $employee->id,
'attendance_date' => now()->toDateString(),
]);
});
test('check-in sets check_out_at to null', function () {
$user = User::factory()->create();
$this->actingAs($user);
$employee = Employee::factory()->create([
'user_id' => $user->id,
'join_date' => now()->subMonth()->toDateString(),
'employment_status' => 'full_time',
'base_salary' => 5000000,
]);
$this->post(route('admin.hr.attendances.store'), [
'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'),
'latitude' => -6.2088,
'longitude' => 106.8456,
]);
$attendance = Attendance::where('employee_id', $employee->id)->first();
$this->assertNull($attendance->check_out_at);
});