diff --git a/documentation/Bruno/Arcadia Api/Api/V1/Employees/List Employees.yml b/documentation/Bruno/Arcadia Api/Api/V1/Employees/List Employees.yml index 35a7b46..af0844e 100644 --- a/documentation/Bruno/Arcadia Api/Api/V1/Employees/List Employees.yml +++ b/documentation/Bruno/Arcadia Api/Api/V1/Employees/List Employees.yml @@ -40,6 +40,16 @@ http: type: query description: Filter by site ID (e.g. LDCSER, SLELA) disabled: true + - name: siteIds[] + value: LDCSER + type: query + description: Filter by multiple site IDs + disabled: true + - name: siteIds[] + value: SLELA + type: query + description: Filter by multiple site IDs + disabled: true - name: serviceId value: "097" type: query diff --git a/documentation/OpenApi/openapi.yaml b/documentation/OpenApi/openapi.yaml index c07f2b2..f5e46aa 100644 --- a/documentation/OpenApi/openapi.yaml +++ b/documentation/OpenApi/openapi.yaml @@ -3,7 +3,6 @@ info: title: Arcadia API description: | Arcadia API in order to abstract corporate metamodel to a proper and stable implemented API. - Architecture Clean Architecture, Domain-Driven Design (DDD) & Ports & Adapters (Hexagonale). contact: name: Uwil Engineering Team email: digital@ldc.fr @@ -45,6 +44,7 @@ paths: - $ref: '#/components/parameters/Since' - $ref: '#/components/parameters/Establishment' - $ref: '#/components/parameters/SiteId' + - $ref: '#/components/parameters/SiteIds' - $ref: '#/components/parameters/ServiceId' - $ref: '#/components/parameters/SortBy' - $ref: '#/components/parameters/SortDirection' @@ -207,6 +207,21 @@ components: pattern: '^[A-Z]{2,}$' example: 'LDCSER' + SiteIds: + name: siteIds + in: query + required: false + description: "Filtrer par un ou plusieurs codes site (ex: siteIds[]=LDCSER&siteIds[]=SLELA)." + style: form + explode: true + schema: + type: array + minItems: 1 + items: + type: string + pattern: '^[A-Z]{2,}$' + example: ['LDCSER', 'SLELA'] + ServiceId: name: serviceId in: query diff --git a/src/Domain/Models/ValueObjects/Employees/EmployeeFilter.php b/src/Domain/Models/ValueObjects/Employees/EmployeeFilter.php index bc96a6b..d49737f 100644 --- a/src/Domain/Models/ValueObjects/Employees/EmployeeFilter.php +++ b/src/Domain/Models/ValueObjects/Employees/EmployeeFilter.php @@ -10,13 +10,20 @@ use Domain\Models\ValueObjects\SiteId; final readonly class EmployeeFilter { + /** @param array $sites */ public function __construct( public ?EstablishmentCode $establishment = null, - public ?SiteId $site = null, + public array $sites = [], public ?ServiceId $service = null, public ?\DateTimeInterface $since = null, ) { - if ($establishment === null && $site === null && $service === null && $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) { throw new \DomainException('EmployeeFilter requires at least one filtering criterion.'); } } diff --git a/src/Infra/Adapters/Directories/EloquentEmployeeDirectory.php b/src/Infra/Adapters/Directories/EloquentEmployeeDirectory.php index 9b009a7..456dc70 100644 --- a/src/Infra/Adapters/Directories/EloquentEmployeeDirectory.php +++ b/src/Infra/Adapters/Directories/EloquentEmployeeDirectory.php @@ -32,8 +32,9 @@ final class EloquentEmployeeDirectory implements EmployeeDirectory $query->where('establishment', $filter->establishment->value); } - if ($filter->site !== null) { - $query->where('site', $filter->site->value); + if (! empty($filter->sites)) { + $siteValues = array_map(static fn (SiteId $s): string => $s->value, $filter->sites); + $query->whereIn('site', $siteValues); } if ($filter->service !== null) { diff --git a/src/Presentation/Api/V1/Http/Requests/Employees/EmployeesIndexRequest.php b/src/Presentation/Api/V1/Http/Requests/Employees/EmployeesIndexRequest.php index 6685e1c..cada6bd 100644 --- a/src/Presentation/Api/V1/Http/Requests/Employees/EmployeesIndexRequest.php +++ b/src/Presentation/Api/V1/Http/Requests/Employees/EmployeesIndexRequest.php @@ -33,6 +33,8 @@ final class EmployeesIndexRequest extends FormRequest 'since' => ['sometimes', 'date'], 'establishment' => ['sometimes', 'string'], 'siteId' => ['sometimes', 'string'], + 'siteIds' => ['sometimes', 'array'], + 'siteIds.*' => ['sometimes', 'string'], 'serviceId' => ['sometimes', 'string'], ]; } @@ -66,16 +68,29 @@ final class EmployeesIndexRequest extends FormRequest { $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) { + $sites = []; + $rawSiteIds = $this->input('siteIds'); + if (is_array($rawSiteIds)) { + foreach ($rawSiteIds as $s) { + if (is_string($s) && $s !== '') { + $sites[] = new SiteId($s); + } + } + } + + if ($this->filled('siteId')) { + $sites[] = new SiteId($this->string('siteId')->value()); + } + + if ($establishment === null && empty($sites) && $service === null && $since === null) { return null; } return new EmployeeFilter( establishment: $establishment, - site: $site, + sites: $sites, service: $service, since: $since, ); diff --git a/tests/Feature/Api/V1/Employee/EmployeesIndexTest.php b/tests/Feature/Api/V1/Employee/EmployeesIndexTest.php index 2af30a8..c474b67 100644 --- a/tests/Feature/Api/V1/Employee/EmployeesIndexTest.php +++ b/tests/Feature/Api/V1/Employee/EmployeesIndexTest.php @@ -116,4 +116,34 @@ describe('Employees Index Feature API', function (): void { $response->assertJsonCount(1, 'employees'); $response->assertJsonPath('employees.0.id', '01155902'); }); + + it('returns 200 OK and filters results for siteIds array query filter', function (): void { + // GIVEN + Employee::query()->delete(); + + Employee::factory()->create([ + 'public_employee_number' => '01155902', + 'site' => 'LDCSER', + ]); + + Employee::factory()->create([ + 'public_employee_number' => '01155903', + 'site' => 'SLELA', + ]); + + Employee::factory()->create([ + 'public_employee_number' => '01155904', + 'site' => 'OTHER', + ]); + + /** @var \Tests\TestCase $test */ + $test = $this; + + // WHEN + $response = $test->getJson('/api/v1/employees?siteIds[]=LDCSER&siteIds[]=SLELA'); + + // THEN + $response->assertStatus(200); + $response->assertJsonCount(2, 'employees'); + }); }); diff --git a/tests/Feature/OpenApi/V1/Employee/EmployeeOpenApiTest.php b/tests/Feature/OpenApi/V1/Employee/EmployeeOpenApiTest.php index 07410be..e00fb22 100644 --- a/tests/Feature/OpenApi/V1/Employee/EmployeeOpenApiTest.php +++ b/tests/Feature/OpenApi/V1/Employee/EmployeeOpenApiTest.php @@ -39,6 +39,21 @@ describe('Employee OpenAPI Conformance', function (): void { $response->assertValidResponse(200); }); + it('validates GET /api/v1/employees list response with siteIds filter against openapi spec', function (): void { + // GIVEN + /** @var \Tests\TestCase $test */ + $test = $this; + + // WHEN + /** @var \Illuminate\Testing\TestResponse<\Symfony\Component\HttpFoundation\Response> $response */ + $response = $test->getJson('/api/v1/employees?siteIds[]=LDCSER&siteIds[]=SLELA'); + + // THEN + $response->assertStatus(200); + $response->assertValidRequest(); + $response->assertValidResponse(200); + }); + it('validates GET /api/v1/employees/{id} detail response against openapi spec', function (): void { // GIVEN /** @var \Tests\TestCase $test */ diff --git a/tests/Integration/Infra/Adapters/Directories/EloquentEmployeeDirectoryTest.php b/tests/Integration/Infra/Adapters/Directories/EloquentEmployeeDirectoryTest.php index d881010..3dd8373 100644 --- a/tests/Integration/Infra/Adapters/Directories/EloquentEmployeeDirectoryTest.php +++ b/tests/Integration/Infra/Adapters/Directories/EloquentEmployeeDirectoryTest.php @@ -138,7 +138,7 @@ describe('EloquentEmployeeDirectory Integration Tests', function (): void { $filter = new EmployeeFilter( establishment: new EstablishmentCode('01'), - site: new SiteId('LDCSER'), + sites: [new SiteId('LDCSER')], service: new ServiceId('097'), since: new \DateTimeImmutable('2024-01-01 00:00:00') ); diff --git a/tests/Unit/Domain/Models/ValueObjects/Employees/EmployeeFilterTest.php b/tests/Unit/Domain/Models/ValueObjects/Employees/EmployeeFilterTest.php index 6724b9e..24c66db 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Employees/EmployeeFilterTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Employees/EmployeeFilterTest.php @@ -18,6 +18,16 @@ describe('EmployeeFilter Value Object Unit Tests', function (): void { ->toThrow(\DomainException::class, 'EmployeeFilter requires at least one filtering criterion.'); }); + it('throws InvalidArgumentException when sites array contains an item that is not a SiteId', function (): void { + // GIVEN + $invalidSites = [new SiteId('LDCSER'), 'invalid-string']; + + // WHEN & THEN + /** @phpstan-ignore-next-line */ + expect(fn () => new EmployeeFilter(sites: $invalidSites)) + ->toThrow(\InvalidArgumentException::class, 'Every item in sites array must be an instance of SiteId.'); + }); + it('instantiates filter with a single parameter', function (): void { // GIVEN $establishment = new EstablishmentCode('01'); @@ -27,7 +37,7 @@ describe('EmployeeFilter Value Object Unit Tests', function (): void { // THEN expect($filter->establishment)->toBe($establishment) - ->and($filter->site)->toBeNull() + ->and($filter->sites)->toBeEmpty() ->and($filter->service)->toBeNull() ->and($filter->since)->toBeNull(); }); @@ -42,14 +52,14 @@ describe('EmployeeFilter Value Object Unit Tests', function (): void { // WHEN $filter = new EmployeeFilter( establishment: $establishment, - site: $site, + sites: [$site], service: $service, since: $since, ); // THEN expect($filter->establishment)->toBe($establishment) - ->and($filter->site)->toBe($site) + ->and($filter->sites)->toBe([$site]) ->and($filter->service)->toBe($service) ->and($filter->since)->toBe($since); });