feat(employees): add filtering capabilities for employee retrieval

- Introduced EmployeeFilter value object for filtering criteria.
- Updated EmployeeDirectory and EloquentEmployeeDirectory to support filtering.
- Modified FindAllEmployees use case to accept filter input.
- Enhanced EmployeesIndexRequest to handle filter parameters.
- Added tests for filtering functionality in EmployeesIndexTest and EloquentEmployeeDirectoryTest.
This commit is contained in:
Jérémy Munsch 2026-08-13 15:09:37 +02:00
parent b7f2de4f92
commit b64c1ef366
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
10 changed files with 206 additions and 5 deletions

View file

@ -26,7 +26,8 @@ final readonly class FindAllEmployees implements Usecase
$page = $this->directory->findAll(
$input->pagination,
$input->sorter
$input->sorter,
$input->filter
);
return $this->toOutput($page);

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Application\Usecase\Employees\FindAll;
use Domain\Models\ValueObjects\Employees\EmployeeFilter;
use Domain\Models\ValueObjects\Employees\EmployeeSorter;
use Shared\DDD\Usecase\Input as UsecaseInput;
use Shared\Pagination;
@ -13,6 +14,7 @@ final readonly class Input implements UsecaseInput
public function __construct(
public Pagination $pagination,
public EmployeeSorter $sorter,
public ?EmployeeFilter $filter = null,
) {
}
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects\Employees;
use Domain\Models\ValueObjects\EstablishmentCode;
use Domain\Models\ValueObjects\ServiceId;
use Domain\Models\ValueObjects\SiteId;
final readonly class EmployeeFilter
{
public function __construct(
public ?EstablishmentCode $establishment = null,
public ?SiteId $site = null,
public ?ServiceId $service = null,
public ?\DateTimeInterface $since = null,
) {
if ($establishment === null && $site === null && $service === null && $since === null) {
throw new \DomainException('EmployeeFilter requires at least one filtering criterion.');
}
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Domain\Ports\Directories;
use Domain\Models\Entities\Employee;
use Domain\Models\ValueObjects\Employees\EmployeeFilter;
use Domain\Models\ValueObjects\Employees\EmployeeSorter;
use Domain\Models\ValueObjects\Entities\ArcadiaEmployeeId;
use Shared\DDD\Directory;
@ -15,7 +16,11 @@ use Shared\Pagination;
interface EmployeeDirectory extends Directory
{
/** @return Paginated<Employee> */
public function findAll(Pagination $pagination, EmployeeSorter $sorter): Paginated;
public function findAll(
Pagination $pagination,
EmployeeSorter $sorter,
?EmployeeFilter $filter = null,
): Paginated;
public function find(ArcadiaEmployeeId $id): ?Employee;
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Infra\Adapters\Directories;
use Domain\Models\Entities\Employee;
use Domain\Models\ValueObjects\Employees\EmployeeFilter;
use Domain\Models\ValueObjects\Employees\EmployeeSortColumn;
use Domain\Models\ValueObjects\Employees\EmployeeSorter;
use Domain\Models\ValueObjects\Entities\ArcadiaEmployeeId;
@ -19,10 +20,34 @@ use Shared\Pagination;
final class EloquentEmployeeDirectory implements EmployeeDirectory
{
#[\Override]
public function findAll(Pagination $pagination, EmployeeSorter $sorter): Paginated
{
public function findAll(
Pagination $pagination,
EmployeeSorter $sorter,
?EmployeeFilter $filter = null,
): Paginated {
$query = EmployeeModel::query();
if ($filter !== null) {
if ($filter->establishment !== null) {
$query->where('establishment', $filter->establishment->value);
}
if ($filter->site !== null) {
$query->where('site', $filter->site->value);
}
if ($filter->service !== null) {
$query->where('service', $filter->service->value);
}
if ($filter->since !== null) {
$query->where(static function ($q) use ($filter): void {
$q->where('updated_at', '>=', $filter->since)
->orWhere('created_at', '>=', $filter->since);
});
}
}
$total = (int) (clone $query)->count();
/** @var \Illuminate\Support\Collection<int, EmployeeModel> $rows */

View file

@ -28,6 +28,7 @@ final readonly class EmployeesIndexController
$input = new Input(
pagination: $request->pagination(),
sorter: $request->sorter(),
filter: $request->filter(),
);
/** @var \Presentation\Api\V1\Presenters\Employees\EmployeesIndexView $response */

View file

@ -4,10 +4,15 @@ declare(strict_types=1);
namespace Presentation\Api\V1\Http\Requests\Employees;
use Domain\Models\ValueObjects\Employees\EmployeeFilter;
use Domain\Models\ValueObjects\Employees\EmployeeSortColumn;
use Domain\Models\ValueObjects\Employees\EmployeeSorter;
use Domain\Models\ValueObjects\EstablishmentCode;
use Domain\Models\ValueObjects\ServiceId;
use Domain\Models\ValueObjects\SiteId;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Shared\Helpers\DatesHelper;
use Shared\Pagination;
final class EmployeesIndexRequest extends FormRequest
@ -57,6 +62,25 @@ final class EmployeesIndexRequest extends FormRequest
return new EmployeeSorter($this->sortBy(), $this->direction());
}
public function filter(): ?EmployeeFilter
{
$since = $this->filled('since') ? DatesHelper::parseDateTime($this->string('since')->value()) : null;
$establishment = $this->filled('establishment') ? new EstablishmentCode($this->string('establishment')->value()) : null;
$site = $this->filled('siteId') ? new SiteId($this->string('siteId')->value()) : null;
$service = $this->filled('serviceId') ? new ServiceId($this->string('serviceId')->value()) : null;
if ($establishment === null && $site === null && $service === null && $since === null) {
return null;
}
return new EmployeeFilter(
establishment: $establishment,
site: $site,
service: $service,
since: $since,
);
}
public function sortBy(): EmployeeSortColumn
{
$value = $this->string('sortBy', EmployeeSortColumn::arcadiaId->name)->value();

View file

@ -87,8 +87,24 @@ describe('Employees Index Feature API', function (): void {
$response->assertStatus(200);
});
it('returns 200 OK for establishment, site, and service query filters', function (): void {
it('returns 200 OK and filters results for establishment, site, and service query filters', function (): void {
// GIVEN
Employee::query()->delete();
Employee::factory()->create([
'public_employee_number' => '01155902',
'establishment' => '01',
'site' => 'LDCSER',
'service' => '097',
]);
Employee::factory()->create([
'public_employee_number' => '1234567',
'establishment' => 'LY',
'site' => 'LDCSER',
'service' => '101',
]);
/** @var \Tests\TestCase $test */
$test = $this;
@ -97,5 +113,7 @@ describe('Employees Index Feature API', function (): void {
// THEN
$response->assertStatus(200);
$response->assertJsonCount(1, 'employees');
$response->assertJsonPath('employees.0.id', '01155902');
});
});

View file

@ -5,9 +5,13 @@ declare(strict_types=1);
namespace Tests\Integration\Infra\Adapters\Directories;
use Domain\Models\Entities\Employee;
use Domain\Models\ValueObjects\Employees\EmployeeFilter;
use Domain\Models\ValueObjects\Employees\EmployeeSortColumn;
use Domain\Models\ValueObjects\Employees\EmployeeSorter;
use Domain\Models\ValueObjects\Entities\ArcadiaEmployeeId;
use Domain\Models\ValueObjects\EstablishmentCode;
use Domain\Models\ValueObjects\ServiceId;
use Domain\Models\ValueObjects\SiteId;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Infra\Adapters\Directories\EloquentEmployeeDirectory;
use Infra\Database\Models\Employee as EmployeeModel;
@ -110,6 +114,48 @@ describe('EloquentEmployeeDirectory Integration Tests', function (): void {
->toThrow(\UnexpectedValueException::class, 'Employee created_at cannot be null.');
});
it('filters employees when EmployeeFilter parameters are provided', function (): void {
// GIVEN
$directory = new EloquentEmployeeDirectory();
EmployeeModel::factory()->create([
'public_employee_number' => '1000001',
'establishment' => '01',
'site' => 'LDCSER',
'service' => '097',
'created_at' => '2024-01-01 08:00:00',
'updated_at' => '2024-01-01 09:00:00',
]);
EmployeeModel::factory()->create([
'public_employee_number' => '1000002',
'establishment' => 'LY',
'site' => 'LDCSER',
'service' => '101',
'created_at' => '2024-01-01 08:00:00',
'updated_at' => '2024-01-01 09:00:00',
]);
$filter = new EmployeeFilter(
establishment: new EstablishmentCode('01'),
site: new SiteId('LDCSER'),
service: new ServiceId('097'),
since: new \DateTimeImmutable('2024-01-01 00:00:00')
);
$pagination = Pagination::ofPage(1, 10);
$sorter = new EmployeeSorter(EmployeeSortColumn::arcadiaId, \SortDirection::Ascending);
// WHEN
$result = $directory->findAll($pagination, $sorter, $filter);
// THEN
expect($result)->toBeInstanceOf(Paginated::class)
->and($result->getTotal())->toBe(1)
->and(count($result->getItems()))->toBe(1)
->and($result->getItems()[0]->id()->value)->toBe('1000001');
});
it('returns paginated employees sorted by specified columns', function (EmployeeSortColumn $sortColumn, \SortDirection $direction): void {
// GIVEN
$directory = new EloquentEmployeeDirectory();

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Employees;
use Domain\Models\ValueObjects\Employees\EmployeeFilter;
use Domain\Models\ValueObjects\EstablishmentCode;
use Domain\Models\ValueObjects\ServiceId;
use Domain\Models\ValueObjects\SiteId;
covers(EmployeeFilter::class);
describe('EmployeeFilter Value Object Unit Tests', function (): void {
it('throws DomainException when instantiated with all null parameters', function (): void {
// GIVEN, WHEN & THEN
expect(fn () => new EmployeeFilter())
->toThrow(\DomainException::class, 'EmployeeFilter requires at least one filtering criterion.');
});
it('instantiates filter with a single parameter', function (): void {
// GIVEN
$establishment = new EstablishmentCode('01');
// WHEN
$filter = new EmployeeFilter(establishment: $establishment);
// THEN
expect($filter->establishment)->toBe($establishment)
->and($filter->site)->toBeNull()
->and($filter->service)->toBeNull()
->and($filter->since)->toBeNull();
});
it('instantiates filter with all parameters', function (): void {
// GIVEN
$establishment = new EstablishmentCode('01');
$site = new SiteId('LDCSER');
$service = new ServiceId('097');
$since = new \DateTimeImmutable('2024-01-01');
// WHEN
$filter = new EmployeeFilter(
establishment: $establishment,
site: $site,
service: $service,
since: $since,
);
// THEN
expect($filter->establishment)->toBe($establishment)
->and($filter->site)->toBe($site)
->and($filter->service)->toBe($service)
->and($filter->since)->toBe($since);
});
});