feat(employees): add support for filtering by multiple site IDs

- Updated EmployeeFilter to accept an array of SiteId.
- Modified EloquentEmployeeDirectory to handle multiple site filters.
- Enhanced EmployeesIndexRequest to validate siteIds as an array.
- Added tests for siteIds filtering in EmployeesIndexTest and EmployeeOpenApiTest.
This commit is contained in:
Jérémy Munsch 2026-08-13 15:30:53 +02:00
parent b64c1ef366
commit 58749030bf
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
9 changed files with 115 additions and 12 deletions

View file

@ -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

View file

@ -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

View file

@ -10,13 +10,20 @@ use Domain\Models\ValueObjects\SiteId;
final readonly class EmployeeFilter
{
/** @param array<SiteId> $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.');
}
}

View file

@ -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) {

View file

@ -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,
);

View file

@ -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');
});
});

View file

@ -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 */

View file

@ -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')
);

View file

@ -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);
});