base commit
This commit is contained in:
69
tests/Feature/ActivityLogTest.php
Normal file
69
tests/Feature/ActivityLogTest.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Position;
|
||||
use App\Models\Shift;
|
||||
use App\Services\Employee\EmployeeService;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
it('logs activity when an employee is created', function (): void {
|
||||
$department = Department::factory()->create();
|
||||
$position = Position::factory()->create();
|
||||
$shift = Shift::factory()->create();
|
||||
|
||||
$employee = app(EmployeeService::class)->create([
|
||||
'employee_number' => 'EMP-LOG-001',
|
||||
'full_name' => 'Activity Test Employee',
|
||||
'gender' => 'male',
|
||||
'birth_date' => '1992-08-20',
|
||||
'phone' => '+993 61 92 92 48',
|
||||
'department_id' => $department->id,
|
||||
'position_id' => $position->id,
|
||||
'shift_id' => $shift->id,
|
||||
'employment_status' => 'active',
|
||||
'hire_date' => '2023-01-15',
|
||||
]);
|
||||
|
||||
$activity = Activity::query()
|
||||
->where('subject_type', Employee::class)
|
||||
->where('subject_id', $employee->id)
|
||||
->first();
|
||||
|
||||
expect($activity)->not->toBeNull()
|
||||
->and($activity->event)->toBe('created')
|
||||
->and($activity->description)->toContain('created');
|
||||
});
|
||||
|
||||
it('logs activity when an employee is created via the model', function (): void {
|
||||
$employee = Employee::factory()->create([
|
||||
'employee_number' => 'EMP-LOG-002',
|
||||
]);
|
||||
|
||||
expect(
|
||||
Activity::query()
|
||||
->where('subject_type', Employee::class)
|
||||
->where('subject_id', $employee->id)
|
||||
->where('event', 'created')
|
||||
->exists(),
|
||||
)->toBeTrue();
|
||||
});
|
||||
|
||||
it('logs updated attributes when an employee is modified', function (): void {
|
||||
$employee = Employee::factory()->create([
|
||||
'full_name' => 'Original Name',
|
||||
]);
|
||||
|
||||
$employee->update(['full_name' => 'Updated Name']);
|
||||
|
||||
$activity = Activity::query()
|
||||
->where('subject_type', Employee::class)
|
||||
->where('subject_id', $employee->id)
|
||||
->where('event', 'updated')
|
||||
->first();
|
||||
|
||||
expect($activity)->not->toBeNull()
|
||||
->and($activity->properties->get('attributes'))->toHaveKey('full_name', 'Updated Name');
|
||||
});
|
||||
19
tests/Feature/ExampleTest.php
Normal file
19
tests/Feature/ExampleTest.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
28
tests/Feature/Filament/PanelAccessTest.php
Normal file
28
tests/Feature/Filament/PanelAccessTest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
it('redirects guests from the admin panel to login', function (): void {
|
||||
$this->get('/admin')
|
||||
->assertRedirect('/admin/login');
|
||||
});
|
||||
|
||||
it('allows authenticated super admin users to access the admin panel', function (): void {
|
||||
$admin = createAdminUser();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get('/admin')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('redirects authenticated non-admin users without panel access', function (): void {
|
||||
seedRoles();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/admin')
|
||||
->assertForbidden();
|
||||
});
|
||||
122
tests/Feature/Import/EmployeeImportTest.php
Normal file
122
tests/Feature/Import/EmployeeImportTest.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\DTOs\Import\EmployeeRowDto;
|
||||
use App\Enums\ImportType;
|
||||
use App\Models\Department;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Position;
|
||||
use App\Models\Shift;
|
||||
use App\Services\Import\EmployeeImportTransformer;
|
||||
use App\Services\Import\ImportService;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->department = Department::factory()->create(['name' => 'Engineering', 'code' => 'ENG']);
|
||||
$this->position = Position::factory()->create(['name' => 'Developer']);
|
||||
$this->shift = Shift::factory()->create(['name' => 'Day Shift', 'code' => 'DAY']);
|
||||
$this->importService = app(ImportService::class);
|
||||
$this->transformer = app(EmployeeImportTransformer::class);
|
||||
});
|
||||
|
||||
it('skips duplicate employees during import', function (): void {
|
||||
Employee::factory()->create([
|
||||
'employee_number' => 'EMP-90001',
|
||||
'national_id' => '1234567890',
|
||||
]);
|
||||
|
||||
$rows = [[
|
||||
'employee_number' => 'EMP-90001',
|
||||
'full_name' => 'Duplicate Employee',
|
||||
'national_id' => '1234567890',
|
||||
'department' => 'Engineering',
|
||||
'position' => 'Developer',
|
||||
'shift' => 'Day Shift',
|
||||
'hire_date' => '2024-01-01',
|
||||
]];
|
||||
|
||||
$result = $this->importService->import(ImportType::Employees, $rows);
|
||||
|
||||
expect($result->imported)->toBe(0)
|
||||
->and($result->skipped)->toBe(1)
|
||||
->and($result->failed)->toBe(0)
|
||||
->and(Employee::query()->where('employee_number', 'EMP-90001')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('imports valid employee rows', function (): void {
|
||||
$rows = [[
|
||||
'employee_number' => 'EMP-90002',
|
||||
'full_name' => 'Jane Smith',
|
||||
'gender' => 'female',
|
||||
'birth_date' => '1990-01-15',
|
||||
'department' => 'Engineering',
|
||||
'position' => 'Developer',
|
||||
'shift' => 'Day Shift',
|
||||
'hire_date' => '2024-03-15',
|
||||
]];
|
||||
|
||||
$result = $this->importService->import(ImportType::Employees, $rows);
|
||||
|
||||
expect($result->imported)->toBe(1)
|
||||
->and($result->skipped)->toBe(0)
|
||||
->and($result->failed)->toBe(0);
|
||||
|
||||
$this->assertDatabaseHas('employees', [
|
||||
'employee_number' => 'EMP-90002',
|
||||
'full_name' => 'Jane Smith',
|
||||
'department_id' => $this->department->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports validation errors for invalid employee rows', function (): void {
|
||||
$rows = [[
|
||||
'employee_number' => '',
|
||||
'full_name' => '',
|
||||
'department' => 'Unknown Department',
|
||||
]];
|
||||
|
||||
$result = $this->importService->import(ImportType::Employees, $rows);
|
||||
|
||||
expect($result->imported)->toBe(0)
|
||||
->and($result->failed)->toBe(1)
|
||||
->and($result->errors)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('validates required fields and unknown references', function (): void {
|
||||
$dto = EmployeeRowDto::fromMappedRow([
|
||||
'employee_number' => '',
|
||||
'full_name' => '',
|
||||
'department' => 'Missing Department',
|
||||
'position' => 'Missing Position',
|
||||
'shift' => 'Missing Shift',
|
||||
]);
|
||||
|
||||
$errors = $this->transformer->validate($dto);
|
||||
|
||||
expect($errors)->toContain('Employee number is required.')
|
||||
->and($errors)->toContain('Full name is required.')
|
||||
->and($errors)->toContain('Department "Missing Department" was not found.')
|
||||
->and($errors)->toContain('Position "Missing Position" was not found.')
|
||||
->and($errors)->toContain('Shift "Missing Shift" was not found.');
|
||||
});
|
||||
|
||||
it('detects duplicates by employee number or national id', function (): void {
|
||||
Employee::factory()->create([
|
||||
'employee_number' => 'EMP-80001',
|
||||
'national_id' => '9988776655',
|
||||
]);
|
||||
|
||||
$duplicateByNumber = EmployeeRowDto::fromMappedRow([
|
||||
'employee_number' => 'EMP-80001',
|
||||
'full_name' => 'Someone Else',
|
||||
]);
|
||||
|
||||
$duplicateByNationalId = EmployeeRowDto::fromMappedRow([
|
||||
'employee_number' => 'EMP-80002',
|
||||
'full_name' => 'Another Person',
|
||||
'national_id' => '9988776655',
|
||||
]);
|
||||
|
||||
expect($this->transformer->isDuplicate($duplicateByNumber))->toBeTrue()
|
||||
->and($this->transformer->isDuplicate($duplicateByNationalId))->toBeTrue();
|
||||
});
|
||||
99
tests/Feature/Models/EmployeeTest.php
Normal file
99
tests/Feature/Models/EmployeeTest.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Bonus;
|
||||
use App\Models\Department;
|
||||
use App\Models\DisciplinaryReport;
|
||||
use App\Models\Employee;
|
||||
use App\Models\EmployeeDocument;
|
||||
use App\Models\Explanation;
|
||||
use App\Models\Gift;
|
||||
use App\Models\Position;
|
||||
use App\Models\Shift;
|
||||
use App\Models\SickLeave;
|
||||
use App\Models\UnpaidLeave;
|
||||
use App\Models\Vacation;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
beforeEach(function (): void {
|
||||
seedRoles();
|
||||
Notification::fake();
|
||||
});
|
||||
|
||||
it('defines expected employee relationships', function (): void {
|
||||
$department = Department::factory()->create();
|
||||
$position = Position::factory()->create();
|
||||
$shift = Shift::factory()->create();
|
||||
|
||||
$employee = Employee::factory()->create([
|
||||
'department_id' => $department->id,
|
||||
'position_id' => $position->id,
|
||||
'shift_id' => $shift->id,
|
||||
]);
|
||||
|
||||
Vacation::factory()->count(2)->pending()->create([
|
||||
'employee_id' => $employee->id,
|
||||
'start_date' => '2024-06-01',
|
||||
'end_date' => '2024-06-05',
|
||||
'days' => 5,
|
||||
]);
|
||||
SickLeave::factory()->create([
|
||||
'employee_id' => $employee->id,
|
||||
'start_date' => '2024-03-01',
|
||||
'end_date' => '2024-03-03',
|
||||
'days' => 3,
|
||||
]);
|
||||
UnpaidLeave::factory()->pending()->create([
|
||||
'employee_id' => $employee->id,
|
||||
'start_date' => '2024-04-01',
|
||||
'end_date' => '2024-04-10',
|
||||
'days' => 10,
|
||||
]);
|
||||
DisciplinaryReport::factory()->create(['employee_id' => $employee->id]);
|
||||
Explanation::factory()->create(['employee_id' => $employee->id]);
|
||||
Bonus::factory()->create(['employee_id' => $employee->id]);
|
||||
Gift::factory()->create(['employee_id' => $employee->id]);
|
||||
EmployeeDocument::factory()->create(['employee_id' => $employee->id]);
|
||||
|
||||
$employee->load([
|
||||
'department',
|
||||
'position',
|
||||
'shift',
|
||||
'vacations',
|
||||
'sickLeaves',
|
||||
'unpaidLeaves',
|
||||
'disciplinaryReports',
|
||||
'explanations',
|
||||
'bonuses',
|
||||
'gifts',
|
||||
'documents',
|
||||
]);
|
||||
|
||||
expect($employee->department->is($department))->toBeTrue()
|
||||
->and($employee->position->is($position))->toBeTrue()
|
||||
->and($employee->shift->is($shift))->toBeTrue()
|
||||
->and($employee->vacations)->toHaveCount(2)
|
||||
->and($employee->sickLeaves)->toHaveCount(1)
|
||||
->and($employee->unpaidLeaves)->toHaveCount(1)
|
||||
->and($employee->disciplinaryReports)->toHaveCount(1)
|
||||
->and($employee->explanations)->toHaveCount(1)
|
||||
->and($employee->bonuses)->toHaveCount(1)
|
||||
->and($employee->gifts)->toHaveCount(1)
|
||||
->and($employee->documents)->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('enforces a unique employee number', function (): void {
|
||||
Employee::factory()->create(['employee_number' => 'EMP-UNIQUE']);
|
||||
|
||||
expect(fn (): Employee => Employee::factory()->create(['employee_number' => 'EMP-UNIQUE']))
|
||||
->toThrow(QueryException::class);
|
||||
});
|
||||
|
||||
it('allows different employees to have distinct employee numbers', function (): void {
|
||||
$first = Employee::factory()->create(['employee_number' => 'EMP-00001']);
|
||||
$second = Employee::factory()->create(['employee_number' => 'EMP-00002']);
|
||||
|
||||
expect($first->employee_number)->not->toBe($second->employee_number);
|
||||
});
|
||||
41
tests/Feature/Policies/DepartmentPolicyTest.php
Normal file
41
tests/Feature/Policies/DepartmentPolicyTest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Department;
|
||||
|
||||
beforeEach(function (): void {
|
||||
seedRoles();
|
||||
});
|
||||
|
||||
it('allows viewers to read departments but not modify them', function (): void {
|
||||
$viewer = createUserWithRole('viewer');
|
||||
$department = Department::factory()->create();
|
||||
|
||||
expect($viewer->can('viewAny', Department::class))->toBeTrue()
|
||||
->and($viewer->can('view', $department))->toBeTrue()
|
||||
->and($viewer->can('create', Department::class))->toBeFalse()
|
||||
->and($viewer->can('update', $department))->toBeFalse()
|
||||
->and($viewer->can('delete', $department))->toBeFalse();
|
||||
});
|
||||
|
||||
it('grants administrators full department access', function (): void {
|
||||
$admin = createUserWithRole('administrator');
|
||||
$department = Department::factory()->create();
|
||||
|
||||
expect($admin->can('viewAny', Department::class))->toBeTrue()
|
||||
->and($admin->can('view', $department))->toBeTrue()
|
||||
->and($admin->can('create', Department::class))->toBeTrue()
|
||||
->and($admin->can('update', $department))->toBeTrue()
|
||||
->and($admin->can('delete', $department))->toBeTrue();
|
||||
});
|
||||
|
||||
it('grants super admin full department access', function (): void {
|
||||
$superAdmin = createAdminUser();
|
||||
$department = Department::factory()->create();
|
||||
|
||||
expect($superAdmin->can('viewAny', Department::class))->toBeTrue()
|
||||
->and($superAdmin->can('create', Department::class))->toBeTrue()
|
||||
->and($superAdmin->can('update', $department))->toBeTrue()
|
||||
->and($superAdmin->can('delete', $department))->toBeTrue();
|
||||
});
|
||||
66
tests/Feature/Policies/EmployeePolicyTest.php
Normal file
66
tests/Feature/Policies/EmployeePolicyTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\Employee;
|
||||
|
||||
beforeEach(function (): void {
|
||||
seedRoles();
|
||||
});
|
||||
|
||||
it('allows department managers to manage employees in their own department', function (): void {
|
||||
$department = Department::factory()->create();
|
||||
$otherDepartment = Department::factory()->create();
|
||||
|
||||
$manager = createUserWithRole('department_manager', [
|
||||
'department_id' => $department->id,
|
||||
]);
|
||||
|
||||
$ownEmployee = Employee::factory()->create([
|
||||
'department_id' => $department->id,
|
||||
]);
|
||||
|
||||
$otherEmployee = Employee::factory()->create([
|
||||
'department_id' => $otherDepartment->id,
|
||||
]);
|
||||
|
||||
expect($manager->can('view', $ownEmployee))->toBeTrue()
|
||||
->and($manager->can('update', $ownEmployee))->toBeTrue()
|
||||
->and($manager->can('delete', $ownEmployee))->toBeTrue()
|
||||
->and($manager->can('view', $otherEmployee))->toBeFalse()
|
||||
->and($manager->can('update', $otherEmployee))->toBeFalse()
|
||||
->and($manager->can('delete', $otherEmployee))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows department managers to create employees', function (): void {
|
||||
$department = Department::factory()->create();
|
||||
$manager = createUserWithRole('department_manager', [
|
||||
'department_id' => $department->id,
|
||||
]);
|
||||
|
||||
expect($manager->can('create', Employee::class))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies department managers access to employees without a matching department', function (): void {
|
||||
$department = Department::factory()->create();
|
||||
$manager = createUserWithRole('department_manager', [
|
||||
'department_id' => $department->id,
|
||||
]);
|
||||
|
||||
$unassignedEmployee = Employee::factory()->create([
|
||||
'department_id' => $otherDepartment = Department::factory()->create()->id,
|
||||
]);
|
||||
|
||||
expect($manager->can('view', $unassignedEmployee))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows hr managers to access employees across departments', function (): void {
|
||||
$hrManager = createUserWithRole('hr_manager');
|
||||
$employee = Employee::factory()->create();
|
||||
|
||||
expect($hrManager->can('viewAny', Employee::class))->toBeTrue()
|
||||
->and($hrManager->can('view', $employee))->toBeTrue()
|
||||
->and($hrManager->can('update', $employee))->toBeTrue()
|
||||
->and($hrManager->can('delete', $employee))->toBeTrue();
|
||||
});
|
||||
66
tests/Feature/Services/EmployeeServiceTest.php
Normal file
66
tests/Feature/Services/EmployeeServiceTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\EmploymentStatus;
|
||||
use App\Models\Department;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Position;
|
||||
use App\Models\Shift;
|
||||
use App\Notifications\EmployeeTerminatedNotification;
|
||||
use App\Services\Employee\EmployeeService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
beforeEach(function (): void {
|
||||
seedRoles();
|
||||
Notification::fake();
|
||||
|
||||
$this->hrManager = createUserWithRole('hr_manager');
|
||||
$this->service = app(EmployeeService::class);
|
||||
});
|
||||
|
||||
it('creates an employee with the provided data', function (): void {
|
||||
$department = Department::factory()->create();
|
||||
$position = Position::factory()->create();
|
||||
$shift = Shift::factory()->create();
|
||||
|
||||
$employee = $this->service->create([
|
||||
'employee_number' => 'EMP-10001',
|
||||
'full_name' => 'John Doe',
|
||||
'gender' => 'male',
|
||||
'birth_date' => '1990-05-15',
|
||||
'phone' => '+993 61 92 92 48',
|
||||
'department_id' => $department->id,
|
||||
'position_id' => $position->id,
|
||||
'shift_id' => $shift->id,
|
||||
'employment_status' => EmploymentStatus::Active,
|
||||
'hire_date' => '2024-01-01',
|
||||
]);
|
||||
|
||||
expect($employee)->toBeInstanceOf(Employee::class)
|
||||
->and($employee->employee_number)->toBe('EMP-10001')
|
||||
->and($employee->full_name)->toBe('John Doe')
|
||||
->and($employee->employment_status)->toBe(EmploymentStatus::Active);
|
||||
|
||||
$this->assertDatabaseHas('employees', [
|
||||
'employee_number' => 'EMP-10001',
|
||||
'full_name' => 'John Doe',
|
||||
]);
|
||||
});
|
||||
|
||||
it('terminates an employee and notifies hr managers', function (): void {
|
||||
$employee = Employee::factory()->active()->create();
|
||||
$terminationDate = Carbon::parse('2025-06-30');
|
||||
|
||||
$result = $this->service->terminate($employee, $terminationDate);
|
||||
|
||||
expect($result->employment_status)->toBe(EmploymentStatus::Terminated)
|
||||
->and($result->termination_date->toDateString())->toBe('2025-06-30');
|
||||
|
||||
Notification::assertSentTo(
|
||||
[$this->hrManager],
|
||||
EmployeeTerminatedNotification::class,
|
||||
fn (EmployeeTerminatedNotification $notification): bool => $notification->employee->is($result),
|
||||
);
|
||||
});
|
||||
89
tests/Feature/Services/VacationServiceTest.php
Normal file
89
tests/Feature/Services/VacationServiceTest.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\ApprovalStatus;
|
||||
use App\Enums\VacationType;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use App\Models\Vacation;
|
||||
use App\Notifications\VacationApprovedNotification;
|
||||
use App\Notifications\VacationSubmittedNotification;
|
||||
use App\Services\Vacation\VacationService;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
beforeEach(function (): void {
|
||||
seedRoles();
|
||||
Notification::fake();
|
||||
|
||||
$this->hrManager = createUserWithRole('hr_manager');
|
||||
$this->approver = createUserWithRole('administrator');
|
||||
$this->employee = Employee::factory()->create();
|
||||
$this->service = app(VacationService::class);
|
||||
});
|
||||
|
||||
it('submits a vacation request with calculated days and pending status', function (): void {
|
||||
config(['hr.leave_calculation' => 'calendar']);
|
||||
|
||||
$vacation = $this->service->submit([
|
||||
'employee_id' => $this->employee->id,
|
||||
'type' => VacationType::Annual,
|
||||
'start_date' => '2025-01-06',
|
||||
'end_date' => '2025-01-10',
|
||||
'return_date' => '2025-01-11',
|
||||
'reason' => 'Family trip',
|
||||
]);
|
||||
|
||||
expect($vacation)->toBeInstanceOf(Vacation::class)
|
||||
->and($vacation->status)->toBe(ApprovalStatus::Pending)
|
||||
->and($vacation->days)->toBe(5)
|
||||
->and($vacation->employee_id)->toBe($this->employee->id);
|
||||
|
||||
Notification::assertSentTo(
|
||||
[$this->hrManager],
|
||||
VacationSubmittedNotification::class,
|
||||
fn (VacationSubmittedNotification $notification): bool => $notification->vacation->is($vacation),
|
||||
);
|
||||
});
|
||||
|
||||
it('approves a vacation and notifies hr managers', function (): void {
|
||||
$vacation = Vacation::factory()->pending()->create([
|
||||
'employee_id' => $this->employee->id,
|
||||
'start_date' => '2025-02-01',
|
||||
'end_date' => '2025-02-05',
|
||||
'days' => 5,
|
||||
]);
|
||||
|
||||
$this->service->approve($vacation, $this->approver);
|
||||
|
||||
$vacation->refresh();
|
||||
|
||||
expect($vacation->status)->toBe(ApprovalStatus::Approved)
|
||||
->and($vacation->approved_by)->toBe($this->approver->id)
|
||||
->and($vacation->approved_at)->not->toBeNull();
|
||||
|
||||
Notification::assertSentTo(
|
||||
[$this->hrManager],
|
||||
VacationApprovedNotification::class,
|
||||
fn (VacationApprovedNotification $notification): bool => $notification->vacation->is($vacation),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a vacation without sending approval notifications', function (): void {
|
||||
$vacation = Vacation::factory()->pending()->create([
|
||||
'employee_id' => $this->employee->id,
|
||||
'start_date' => '2025-03-01',
|
||||
'end_date' => '2025-03-05',
|
||||
'days' => 5,
|
||||
]);
|
||||
|
||||
$this->service->reject($vacation, $this->approver);
|
||||
|
||||
$vacation->refresh();
|
||||
|
||||
expect($vacation->status)->toBe(ApprovalStatus::Rejected)
|
||||
->and($vacation->approved_by)->toBe($this->approver->id)
|
||||
->and($vacation->approved_at)->not->toBeNull();
|
||||
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
36
tests/Pest.php
Normal file
36
tests/Pest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use BezhanSalleh\FilamentShield\Support\Utils;
|
||||
use Database\Seeders\RoleSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class)->in('Feature', 'Unit');
|
||||
|
||||
function seedRoles(): void
|
||||
{
|
||||
(new RoleSeeder)->run();
|
||||
}
|
||||
|
||||
function createAdminUser(array $attributes = []): User
|
||||
{
|
||||
seedRoles();
|
||||
|
||||
$user = User::factory()->create($attributes);
|
||||
$user->assignRole(Utils::getSuperAdminName());
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function createUserWithRole(string $role, array $attributes = []): User
|
||||
{
|
||||
seedRoles();
|
||||
|
||||
$user = User::factory()->create($attributes);
|
||||
$user->assignRole($role);
|
||||
|
||||
return $user;
|
||||
}
|
||||
10
tests/TestCase.php
Normal file
10
tests/TestCase.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
16
tests/Unit/ExampleTest.php
Normal file
16
tests/Unit/ExampleTest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
54
tests/Unit/Services/LeaveDaysCalculatorTest.php
Normal file
54
tests/Unit/Services/LeaveDaysCalculatorTest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Services\Leave\LeaveDaysCalculator;
|
||||
use Carbon\Carbon;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->calculator = new LeaveDaysCalculator;
|
||||
});
|
||||
|
||||
it('calculates inclusive calendar days between two dates', function (): void {
|
||||
config(['hr.leave_calculation' => 'calendar']);
|
||||
|
||||
$days = $this->calculator->between(
|
||||
Carbon::parse('2025-01-06'),
|
||||
Carbon::parse('2025-01-12'),
|
||||
);
|
||||
|
||||
expect($days)->toBe(7);
|
||||
});
|
||||
|
||||
it('calculates business days excluding weekends', function (): void {
|
||||
config(['hr.leave_calculation' => 'business']);
|
||||
|
||||
$days = $this->calculator->between(
|
||||
Carbon::parse('2025-01-06'),
|
||||
Carbon::parse('2025-01-12'),
|
||||
);
|
||||
|
||||
expect($days)->toBe(5);
|
||||
});
|
||||
|
||||
it('returns zero when the end date is before the start date', function (): void {
|
||||
config(['hr.leave_calculation' => 'calendar']);
|
||||
|
||||
$days = $this->calculator->between(
|
||||
Carbon::parse('2025-01-10'),
|
||||
Carbon::parse('2025-01-06'),
|
||||
);
|
||||
|
||||
expect($days)->toBe(0);
|
||||
});
|
||||
|
||||
it('counts a single day when start and end are the same', function (): void {
|
||||
config(['hr.leave_calculation' => 'calendar']);
|
||||
|
||||
$days = $this->calculator->between(
|
||||
Carbon::parse('2025-01-06'),
|
||||
Carbon::parse('2025-01-06'),
|
||||
);
|
||||
|
||||
expect($days)->toBe(1);
|
||||
});
|
||||
86
tests/Unit/Services/VacationBalanceServiceTest.php
Normal file
86
tests/Unit/Services/VacationBalanceServiceTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\ApprovalStatus;
|
||||
use App\Enums\VacationType;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Vacation;
|
||||
use App\Services\Vacation\VacationBalanceService;
|
||||
|
||||
beforeEach(function (): void {
|
||||
config(['hr.annual_leave_days' => 24]);
|
||||
|
||||
$this->service = new VacationBalanceService;
|
||||
$this->employee = Employee::factory()->create();
|
||||
});
|
||||
|
||||
it('returns annual leave entitlement from configuration', function (): void {
|
||||
expect($this->service->entitlement())->toBe(24);
|
||||
});
|
||||
|
||||
it('sums approved annual vacation days for the given year', function (): void {
|
||||
Vacation::factory()->approved()->create([
|
||||
'employee_id' => $this->employee->id,
|
||||
'type' => VacationType::Annual,
|
||||
'start_date' => '2025-03-01',
|
||||
'end_date' => '2025-03-05',
|
||||
'days' => 5,
|
||||
]);
|
||||
|
||||
Vacation::factory()->approved()->create([
|
||||
'employee_id' => $this->employee->id,
|
||||
'type' => VacationType::Annual,
|
||||
'start_date' => '2025-06-01',
|
||||
'end_date' => '2025-06-03',
|
||||
'days' => 3,
|
||||
]);
|
||||
|
||||
Vacation::factory()->pending()->create([
|
||||
'employee_id' => $this->employee->id,
|
||||
'type' => VacationType::Annual,
|
||||
'start_date' => '2025-07-01',
|
||||
'end_date' => '2025-07-10',
|
||||
'days' => 10,
|
||||
]);
|
||||
|
||||
Vacation::factory()->approved()->create([
|
||||
'employee_id' => $this->employee->id,
|
||||
'type' => VacationType::Marriage,
|
||||
'start_date' => '2025-04-01',
|
||||
'end_date' => '2025-04-05',
|
||||
'days' => 5,
|
||||
]);
|
||||
|
||||
expect($this->service->takenAnnualDays($this->employee, 2025))->toBe(8);
|
||||
});
|
||||
|
||||
it('calculates remaining annual leave days', function (): void {
|
||||
$year = (int) now()->year;
|
||||
|
||||
Vacation::factory()->approved()->create([
|
||||
'employee_id' => $this->employee->id,
|
||||
'type' => VacationType::Annual,
|
||||
'start_date' => "{$year}-02-01",
|
||||
'end_date' => "{$year}-02-06",
|
||||
'days' => 6,
|
||||
]);
|
||||
|
||||
expect($this->service->remainingAnnualDays($this->employee))->toBe(18);
|
||||
});
|
||||
|
||||
it('ignores rejected annual vacations when calculating taken days', function (): void {
|
||||
$year = (int) now()->year;
|
||||
|
||||
Vacation::factory()->pending()->create([
|
||||
'employee_id' => $this->employee->id,
|
||||
'type' => VacationType::Annual,
|
||||
'status' => ApprovalStatus::Rejected,
|
||||
'start_date' => "{$year}-01-01",
|
||||
'end_date' => "{$year}-01-05",
|
||||
'days' => 5,
|
||||
]);
|
||||
|
||||
expect($this->service->takenAnnualDays($this->employee))->toBe(0)
|
||||
->and($this->service->remainingAnnualDays($this->employee))->toBe(24);
|
||||
});
|
||||
Reference in New Issue
Block a user