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.
This commit is contained in:
parent
86e6de84ef
commit
53b0dd9f1e
12 changed files with 259 additions and 43 deletions
28
src/Application/Usecase/Employees/FindAll/Filter.php
Normal file
28
src/Application/Usecase/Employees/FindAll/Filter.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Usecase\Employees\FindAll;
|
||||
|
||||
use Domain\Models\ValueObjects\EstablishmentCode;
|
||||
use Domain\Models\ValueObjects\ServiceId;
|
||||
use Domain\Models\ValueObjects\SiteId;
|
||||
|
||||
final readonly class Filter
|
||||
{
|
||||
/** @var array<SiteId> */
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/Application/Usecase/Employees/FindAll/SortColumn.php
Normal file
15
src/Application/Usecase/Employees/FindAll/SortColumn.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Usecase\Employees\FindAll;
|
||||
|
||||
enum SortColumn: string
|
||||
{
|
||||
case arcadiaId = 'arcadiaId';
|
||||
case startDate = 'startDate';
|
||||
case endDate = 'endDate';
|
||||
case plannedEndDate = 'plannedEndDate';
|
||||
case createdAt = 'createdAt';
|
||||
case updatedAt = 'updatedAt';
|
||||
}
|
||||
14
src/Application/Usecase/Employees/FindAll/Sorter.php
Normal file
14
src/Application/Usecase/Employees/FindAll/Sorter.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Usecase\Employees\FindAll;
|
||||
|
||||
final readonly class Sorter
|
||||
{
|
||||
public function __construct(
|
||||
public SortColumn $sortBy,
|
||||
public \SortDirection $direction,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -7,24 +7,23 @@ namespace Domain\Models\ValueObjects\Employees;
|
|||
use Domain\Models\ValueObjects\EstablishmentCode;
|
||||
use Domain\Models\ValueObjects\ServiceId;
|
||||
use Domain\Models\ValueObjects\SiteId;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class EmployeeFilter
|
||||
{
|
||||
/** @param array<SiteId> $sites */
|
||||
/**
|
||||
* @param array<SiteId> $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.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\Usecase\Employees;
|
||||
|
||||
use Application\Usecase\Employees\FindAll\FindAllEmployees;
|
||||
use Application\Usecase\Employees\FindAll\Input;
|
||||
use Application\Usecase\Employees\FindAll\Output;
|
||||
use Application\Usecase\Employees\FindAll\SortColumn;
|
||||
use Application\Usecase\Employees\FindAll\Sorter;
|
||||
use Domain\Models\Entities\Employee;
|
||||
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\EmployeeSortColumn as DirectoryEmployeeSortColumn;
|
||||
use Domain\Models\ValueObjects\Employees\EmployeeSorter as DirectoryEmployeeSorter;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
covers(FindAllEmployees::class);
|
||||
|
||||
describe('FindAllEmployees Usecase Unit Tests', function (): void {
|
||||
it('executes successfully and returns paginated output DTO', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::ofPage(1, 10);
|
||||
$sorter = new Sorter(
|
||||
SortColumn::arcadiaId,
|
||||
\SortDirection::Ascending
|
||||
);
|
||||
$input = new Input($pagination, $sorter);
|
||||
$employeeDirectory = \Mockery::mock(EmployeeDirectory::class);
|
||||
$usecase = new FindAllEmployees($employeeDirectory);
|
||||
|
||||
$employeeId = new ArcadiaEmployeeId('01155902');
|
||||
$employee = new Employee(
|
||||
publicEmployeeNumber: $employeeId,
|
||||
lastName: 'DOE',
|
||||
firstName: 'John',
|
||||
startDate: new \DateTimeImmutable('2024-01-01'),
|
||||
endDate: null,
|
||||
plannedEndDate: null,
|
||||
establishment: new EstablishmentCode('EST01'),
|
||||
site: new SiteId('LDCSER'),
|
||||
service: new ServiceId('097'),
|
||||
createdAt: new \DateTimeImmutable('2024-01-01'),
|
||||
updatedAt: null,
|
||||
);
|
||||
|
||||
$paginatedResult = new Paginated($pagination, [$employee], 1);
|
||||
$employeeDirectory->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');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\Usecase\Employees;
|
||||
|
||||
use Application\Exceptions\NotFoundException;
|
||||
use Application\Usecase\Employees\Find\FindEmployee;
|
||||
use Application\Usecase\Employees\Find\Input;
|
||||
use Application\Usecase\Employees\Find\Output;
|
||||
use Domain\Models\Entities\Employee;
|
||||
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;
|
||||
|
||||
covers(FindEmployee::class);
|
||||
|
||||
describe('FindEmployee Usecase Unit Tests', function (): void {
|
||||
it('executes successfully and returns Output when employee exists', function (): void {
|
||||
// GIVEN
|
||||
$employeeId = new ArcadiaEmployeeId('01155902');
|
||||
$input = new Input($employeeId);
|
||||
$employeeDirectory = \Mockery::mock(EmployeeDirectory::class);
|
||||
$usecase = new FindEmployee($employeeDirectory);
|
||||
|
||||
$employee = new Employee(
|
||||
publicEmployeeNumber: $employeeId,
|
||||
lastName: 'DOE',
|
||||
firstName: 'John',
|
||||
startDate: new \DateTimeImmutable('2024-01-01'),
|
||||
endDate: null,
|
||||
plannedEndDate: null,
|
||||
establishment: new EstablishmentCode('EST01'),
|
||||
site: new SiteId('LDCSER'),
|
||||
service: new ServiceId('097'),
|
||||
createdAt: new \DateTimeImmutable('2024-01-01'),
|
||||
updatedAt: null,
|
||||
);
|
||||
|
||||
$employeeDirectory->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.");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue