From 53b0dd9f1e99b503fcb25e6baca018ef7a6fda4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Fri, 14 Aug 2026 09:29:41 +0200 Subject: [PATCH] feat(employees): implement filtering and sorting for employee retrieval Added Filter and Sorter classes to handle employee filtering and sorting criteria. Updated FindAllEmployees use case to utilize these new classes for improved employee data retrieval. --- .../Usecase/Employees/FindAll/Filter.php | 28 +++++++ .../Employees/FindAll/FindAllEmployees.php | 21 ++++- .../Usecase/Employees/FindAll/Input.php | 6 +- .../Usecase/Employees/FindAll/SortColumn.php | 15 ++++ .../Usecase/Employees/FindAll/Sorter.php | 14 ++++ .../ValueObjects/Employees/EmployeeFilter.php | 15 ++-- .../Ports/Directories/EmployeeDirectory.php | 2 +- .../Directories/EloquentEmployeeDirectory.php | 6 +- .../Employees/EmployeesIndexRequest.php | 39 +++++----- .../EloquentEmployeeDirectoryTest.php | 6 +- .../Employees/FindAllEmployeesTest.php | 74 ++++++++++++++++++ .../Usecase/Employees/FindEmployeeTest.php | 76 +++++++++++++++++++ 12 files changed, 259 insertions(+), 43 deletions(-) create mode 100644 src/Application/Usecase/Employees/FindAll/Filter.php create mode 100644 src/Application/Usecase/Employees/FindAll/SortColumn.php create mode 100644 src/Application/Usecase/Employees/FindAll/Sorter.php create mode 100644 tests/Unit/Application/Usecase/Employees/FindAllEmployeesTest.php create mode 100644 tests/Unit/Application/Usecase/Employees/FindEmployeeTest.php diff --git a/src/Application/Usecase/Employees/FindAll/Filter.php b/src/Application/Usecase/Employees/FindAll/Filter.php new file mode 100644 index 0000000..fff65cc --- /dev/null +++ b/src/Application/Usecase/Employees/FindAll/Filter.php @@ -0,0 +1,28 @@ + */ + public array $sites; + + public function __construct( + public ?EstablishmentCode $establishment = null, + public ?ServiceId $service = null, + public ?\DateTimeInterface $since = null, + SiteId ...$sites, + ) { + $this->sites = $sites; + + if ($establishment === null && empty($this->sites) && $service === null && $since === null) { + throw new \DomainException('Filter requires at least one filtering criterion.'); + } + } +} diff --git a/src/Application/Usecase/Employees/FindAll/FindAllEmployees.php b/src/Application/Usecase/Employees/FindAll/FindAllEmployees.php index c7127b3..d64f62b 100644 --- a/src/Application/Usecase/Employees/FindAll/FindAllEmployees.php +++ b/src/Application/Usecase/Employees/FindAll/FindAllEmployees.php @@ -6,6 +6,9 @@ namespace Application\Usecase\Employees\FindAll; use Domain\Models\Entities\Employee; use Domain\Ports\Directories\EmployeeDirectory; +use Domain\Models\ValueObjects\Employees\EmployeeFilter as DirectoryEmployeeFilter; +use Domain\Models\ValueObjects\Employees\EmployeeSortColumn as DirectoryEmployeeSortColumn; +use Domain\Models\ValueObjects\Employees\EmployeeSorter as DirectoryEmployeeSorter; use Shared\DDD\Usecase\Input as BaseInput; use Shared\DDD\Usecase\Usecase; use Shared\Paginated; @@ -24,10 +27,24 @@ final readonly class FindAllEmployees implements Usecase { Assert::isInstanceOf($input, Input::class); + $filter = $input->filter === null + ? null + : new DirectoryEmployeeFilter( + establishment: $input->filter->establishment, + sites: $input->filter->sites, + service: $input->filter->service, + since: $input->filter->since, + ); + + $sorter = new DirectoryEmployeeSorter( + sortBy: DirectoryEmployeeSortColumn::from($input->sorter->sortBy->value), + direction: $input->sorter->direction, + ); + $page = $this->directory->findAll( $input->pagination, - $input->sorter, - $input->filter + $sorter, + $filter ); return $this->toOutput($page); diff --git a/src/Application/Usecase/Employees/FindAll/Input.php b/src/Application/Usecase/Employees/FindAll/Input.php index 7387807..011613a 100644 --- a/src/Application/Usecase/Employees/FindAll/Input.php +++ b/src/Application/Usecase/Employees/FindAll/Input.php @@ -4,8 +4,6 @@ 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,8 +11,8 @@ final readonly class Input implements UsecaseInput { public function __construct( public Pagination $pagination, - public EmployeeSorter $sorter, - public ?EmployeeFilter $filter = null, + public Sorter $sorter, + public ?Filter $filter = null, ) { } } diff --git a/src/Application/Usecase/Employees/FindAll/SortColumn.php b/src/Application/Usecase/Employees/FindAll/SortColumn.php new file mode 100644 index 0000000..83f6dc9 --- /dev/null +++ b/src/Application/Usecase/Employees/FindAll/SortColumn.php @@ -0,0 +1,15 @@ + $sites */ + /** + * @param array $sites + */ public function __construct( public ?EstablishmentCode $establishment = null, public array $sites = [], public ?ServiceId $service = null, public ?\DateTimeInterface $since = null, ) { - foreach ($this->sites as $site) { - if (! $site instanceof SiteId) { - throw new \InvalidArgumentException('Every item in sites array must be an instance of SiteId.'); - } - } - - if ($establishment === null && empty($this->sites) && $service === null && $since === null) { + if ($establishment === null && empty($sites) && $service === null && $since === null) { throw new \DomainException('EmployeeFilter requires at least one filtering criterion.'); } + + Assert::allIsInstanceOf($sites, SiteId::class, 'Every item in sites array must be an instance of SiteId.'); } } diff --git a/src/Domain/Ports/Directories/EmployeeDirectory.php b/src/Domain/Ports/Directories/EmployeeDirectory.php index 1bb759f..76038ec 100644 --- a/src/Domain/Ports/Directories/EmployeeDirectory.php +++ b/src/Domain/Ports/Directories/EmployeeDirectory.php @@ -5,9 +5,9 @@ declare(strict_types=1); namespace Domain\Ports\Directories; use Domain\Models\Entities\Employee; +use Domain\Models\ValueObjects\Entities\ArcadiaEmployeeId; use Domain\Models\ValueObjects\Employees\EmployeeFilter; use Domain\Models\ValueObjects\Employees\EmployeeSorter; -use Domain\Models\ValueObjects\Entities\ArcadiaEmployeeId; use Shared\DDD\Directory; use Shared\Paginated; use Shared\Pagination; diff --git a/src/Infra/Adapters/Directories/EloquentEmployeeDirectory.php b/src/Infra/Adapters/Directories/EloquentEmployeeDirectory.php index 456dc70..21f86e5 100644 --- a/src/Infra/Adapters/Directories/EloquentEmployeeDirectory.php +++ b/src/Infra/Adapters/Directories/EloquentEmployeeDirectory.php @@ -5,14 +5,14 @@ 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; use Domain\Models\ValueObjects\EstablishmentCode; use Domain\Models\ValueObjects\ServiceId; use Domain\Models\ValueObjects\SiteId; use Domain\Ports\Directories\EmployeeDirectory; +use Domain\Models\ValueObjects\Employees\EmployeeFilter; +use Domain\Models\ValueObjects\Employees\EmployeeSortColumn; +use Domain\Models\ValueObjects\Employees\EmployeeSorter; use Infra\Database\Models\Employee as EmployeeModel; use Shared\Paginated; use Shared\Pagination; diff --git a/src/Presentation/Api/V1/Http/Requests/Employees/EmployeesIndexRequest.php b/src/Presentation/Api/V1/Http/Requests/Employees/EmployeesIndexRequest.php index cada6bd..28e54df 100644 --- a/src/Presentation/Api/V1/Http/Requests/Employees/EmployeesIndexRequest.php +++ b/src/Presentation/Api/V1/Http/Requests/Employees/EmployeesIndexRequest.php @@ -4,9 +4,9 @@ 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 Application\Usecase\Employees\FindAll\Filter; +use Application\Usecase\Employees\FindAll\SortColumn; +use Application\Usecase\Employees\FindAll\Sorter; use Domain\Models\ValueObjects\EstablishmentCode; use Domain\Models\ValueObjects\ServiceId; use Domain\Models\ValueObjects\SiteId; @@ -26,8 +26,8 @@ final class EmployeesIndexRequest extends FormRequest 'page' => ['sometimes', 'integer', 'min:1'], 'pageSize' => ['sometimes', 'integer', 'min:1', 'max:' . Pagination::MAX_PAGE_SIZE], 'sortBy' => ['sometimes', Rule::in(array_map( - static fn (EmployeeSortColumn $case): string => $case->name, - EmployeeSortColumn::cases() + static fn (SortColumn $case): string => $case->name, + SortColumn::cases() ))], 'sortDirection' => ['sometimes', Rule::in(['Ascending', 'Descending'])], 'since' => ['sometimes', 'date'], @@ -59,12 +59,12 @@ final class EmployeesIndexRequest extends FormRequest ); } - public function sorter(): EmployeeSorter + public function sorter(): Sorter { - return new EmployeeSorter($this->sortBy(), $this->direction()); + return new Sorter($this->sortBy(), $this->direction()); } - public function filter(): ?EmployeeFilter + public function filter(): ?Filter { $since = $this->filled('since') ? DatesHelper::parseDateTime($this->string('since')->value()) : null; $establishment = $this->filled('establishment') ? new EstablishmentCode($this->string('establishment')->value()) : null; @@ -88,25 +88,20 @@ final class EmployeesIndexRequest extends FormRequest return null; } - return new EmployeeFilter( - establishment: $establishment, - sites: $sites, - service: $service, - since: $since, - ); + return new Filter($establishment, $service, $since, ...$sites); } - public function sortBy(): EmployeeSortColumn + public function sortBy(): SortColumn { - $value = $this->string('sortBy', EmployeeSortColumn::arcadiaId->name)->value(); + $value = $this->string('sortBy', SortColumn::arcadiaId->name)->value(); return match ($value) { - 'startDate' => EmployeeSortColumn::startDate, - 'endDate' => EmployeeSortColumn::endDate, - 'plannedEndDate' => EmployeeSortColumn::plannedEndDate, - 'createdAt' => EmployeeSortColumn::createdAt, - 'updatedAt' => EmployeeSortColumn::updatedAt, - default => EmployeeSortColumn::arcadiaId, + 'startDate' => SortColumn::startDate, + 'endDate' => SortColumn::endDate, + 'plannedEndDate' => SortColumn::plannedEndDate, + 'createdAt' => SortColumn::createdAt, + 'updatedAt' => SortColumn::updatedAt, + default => SortColumn::arcadiaId, }; } diff --git a/tests/Integration/Infra/Adapters/Directories/EloquentEmployeeDirectoryTest.php b/tests/Integration/Infra/Adapters/Directories/EloquentEmployeeDirectoryTest.php index 3dd8373..cee942a 100644 --- a/tests/Integration/Infra/Adapters/Directories/EloquentEmployeeDirectoryTest.php +++ b/tests/Integration/Infra/Adapters/Directories/EloquentEmployeeDirectoryTest.php @@ -5,13 +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 Domain\Models\ValueObjects\Employees\EmployeeFilter; +use Domain\Models\ValueObjects\Employees\EmployeeSortColumn; +use Domain\Models\ValueObjects\Employees\EmployeeSorter; use Illuminate\Foundation\Testing\RefreshDatabase; use Infra\Adapters\Directories\EloquentEmployeeDirectory; use Infra\Database\Models\Employee as EmployeeModel; diff --git a/tests/Unit/Application/Usecase/Employees/FindAllEmployeesTest.php b/tests/Unit/Application/Usecase/Employees/FindAllEmployeesTest.php new file mode 100644 index 0000000..68ee087 --- /dev/null +++ b/tests/Unit/Application/Usecase/Employees/FindAllEmployeesTest.php @@ -0,0 +1,74 @@ +shouldReceive('findAll') + ->once() + ->withArgs(function (Pagination $actualPagination, DirectoryEmployeeSorter $actualSorter, mixed $actualFilter) use ($pagination): bool { + return $actualPagination === $pagination + && $actualSorter->sortBy === DirectoryEmployeeSortColumn::arcadiaId + && $actualSorter->direction === \SortDirection::Ascending + && $actualFilter === null; + }) + ->andReturn($paginatedResult); + + // WHEN + $result = $usecase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(Output::class) + ->and($result->total)->toBe(1) + ->and($result->page)->toBe(1) + ->and($result->perPage)->toBe(10) + ->and($result->employees)->toHaveCount(1) + ->and($result->employees[0]->firstName)->toBe('John'); + }); +}); diff --git a/tests/Unit/Application/Usecase/Employees/FindEmployeeTest.php b/tests/Unit/Application/Usecase/Employees/FindEmployeeTest.php new file mode 100644 index 0000000..7f849f0 --- /dev/null +++ b/tests/Unit/Application/Usecase/Employees/FindEmployeeTest.php @@ -0,0 +1,76 @@ +shouldReceive('find') + ->once() + ->with($employeeId) + ->andReturn($employee); + + // WHEN + $result = $usecase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(Output::class) + ->and($result->id)->toBe($employeeId) + ->and($result->firstName)->toBe('John') + ->and($result->lastName)->toBe('DOE') + ->and($result->establishment)->toBe('EST01') + ->and($result->site)->toBe('LDCSER') + ->and($result->service)->toBe('097'); + }); + + it('throws NotFoundException when employee does not exist', function (): void { + // GIVEN + $employeeId = new ArcadiaEmployeeId('01155902'); + $input = new Input($employeeId); + $employeeDirectory = \Mockery::mock(EmployeeDirectory::class); + $usecase = new FindEmployee($employeeDirectory); + + $employeeDirectory->shouldReceive('find') + ->once() + ->with($employeeId) + ->andReturnNull(); + + // WHEN & THEN + expect(fn () => $usecase->execute($input)) + ->toThrow(NotFoundException::class, "Employee '01155902' not found."); + }); +});