fix(employees): filter out invalid matricules and comments from Arcadia database

This commit is contained in:
Jérémy Munsch 2026-08-31 16:32:11 +02:00
parent f3c59d8ba4
commit 00f38b7d41
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
5 changed files with 110 additions and 13 deletions

View file

@ -32,6 +32,7 @@ final class ArcadiaEmployeeDirectory implements EmployeeDirectory
?EmployeeFilter $filter = null,
): Paginated {
$query = $this->connection->table(RuPersonneSirhTable::table($this->connection));
$this->applyValidMatriculeFilter($query);
if ($filter !== null) {
if ($filter->establishment !== null) {
@ -69,7 +70,10 @@ final class ArcadiaEmployeeDirectory implements EmployeeDirectory
$items = [];
foreach ($rows as $row) {
$items[] = EmployeeMapper::toDomain($row);
$employee = EmployeeMapper::tryToDomain($row);
if ($employee !== null) {
$items[] = $employee;
}
}
/** @var Employee[] $items */
@ -79,16 +83,33 @@ final class ArcadiaEmployeeDirectory implements EmployeeDirectory
#[\Override]
public function find(ArcadiaEmployeeId $id): ?Employee
{
$row = $this->connection
$query = $this->connection
->table(RuPersonneSirhTable::table($this->connection))
->where('MATRICULE_FIC', $id->value)
->first();
->where('MATRICULE_FIC', $id->value);
$this->applyValidMatriculeFilter($query);
$row = $query->first();
if ($row === null) {
return null;
}
return EmployeeMapper::toDomain($row);
return EmployeeMapper::tryToDomain($row);
}
private function applyValidMatriculeFilter(Builder $query): void
{
$driver = $this->connection->getDriverName();
match ($driver) {
'oracle' => $query->whereRaw("REGEXP_LIKE(TRIM(MATRICULE_FIC), '^[0-9]{1,8}$')"),
'pgsql' => $query->whereRaw("TRIM(MATRICULE_FIC) ~ '^[0-9]{1,8}$'"),
'mysql' => $query->whereRaw("TRIM(MATRICULE_FIC) REGEXP '^[0-9]{1,8}$'"),
'sqlite' => $query->whereRaw(
"TRIM(MATRICULE_FIC) != '' AND length(TRIM(MATRICULE_FIC)) <= 8 AND TRIM(MATRICULE_FIC) NOT GLOB '*[^0-9]*'"
),
default => $query->whereRaw("MATRICULE_FIC NOT LIKE '#%'"),
};
}
private function toSqlColumn(EmployeeSortColumn $sortBy): string

View file

@ -13,6 +13,18 @@ use Shared\Helpers\DatesHelper;
final readonly class EmployeeMapper
{
/**
* @param array<string, mixed>|object $row
*/
public static function tryToDomain(array | object $row): ?Employee
{
try {
return self::toDomain($row);
} catch (\DomainException) {
return null;
}
}
/**
* @param array<string, mixed>|object $row
*/
@ -23,10 +35,10 @@ final readonly class EmployeeMapper
/** @var array<string, mixed> $data */
$data = is_object($row) ? (array) $row : $row;
$rawMatricule = self::castString(
$rawMatricule = trim(self::castString(
$data['MATRICULE_FIC'] ?? $data['matricule_fic'] ?? $data['public_employee_number'] ?? null,
''
);
));
// Garantir un format 7 ou 8 chiffres pour ArcadiaEmployeeId
if (is_numeric($rawMatricule) && strlen($rawMatricule) < 7) {
$rawMatricule = str_pad($rawMatricule, 7, '0', STR_PAD_LEFT);

View file

@ -147,4 +147,45 @@ describe('Employees Index Feature API', function (): void {
$response->assertStatus(200);
$response->assertJsonCount(2, 'employees');
});
it('ignores database rows with invalid matricules like comments or alphanumeric strings', function (): void {
// GIVEN
RuPersonneSirhFactory::truncate();
// Valid employee
RuPersonneSirhFactory::new()->create([
'MATRICULE_FIC' => '01155902',
'NOM_FIC' => 'DOE',
'PRENOM_FIC' => 'John',
]);
// Invalid employees / comments in prod database
RuPersonneSirhFactory::new()->create([
'MATRICULE_FIC' => '# s996008',
'NOM_FIC' => 'ARRIVE SAINT FULGENT',
'PRENOM_FIC' => 'Baie de stockage',
]);
RuPersonneSirhFactory::new()->create([
'MATRICULE_FIC' => 'd05600013',
'NOM_FIC' => 'PYPOPS',
'PRENOM_FIC' => 'Diana',
]);
RuPersonneSirhFactory::new()->create([
'MATRICULE_FIC' => 'P99900029',
'NOM_FIC' => 'DPE BEL',
'PRENOM_FIC' => 'Bel',
]);
/** @var \Tests\TestCase $test */
$test = $this;
// WHEN
$response = $test->getJson('/api/v1/employees');
// THEN
$response->assertStatus(200);
$response->assertJsonCount(1, 'employees');
$response->assertJsonPath('employees.0.id', '01155902');
$response->assertJsonPath('pagination.totalItems', 1);
});
});

View file

@ -61,12 +61,15 @@ describe('ArcadiaEmployeeId Value Object Instantiation', function (): void {
});
describe('ArcadiaEmployeeId Value Object Domain Rules', function (): void {
it('throws a DomainException when given an invalid matricule pattern', function (): void {
// GIVEN
$invalidMatricule = '12345';
// WHEN & THEN
it('throws a DomainException when given an invalid matricule pattern', function (string $invalidMatricule): void {
expect(fn () => new ArcadiaEmployeeId($invalidMatricule))
->toThrow(\DomainException::class);
});
})->with([
'too short' => '12345',
'too long' => '123456789',
'comment with hash' => '# s996008',
'alphanumeric d prefix' => 'd05600013',
'alphanumeric P prefix' => 'P99900029',
'empty string' => '',
]);
});

View file

@ -71,4 +71,24 @@ describe('EmployeeMapper', function (): void {
->and($employee->lastName)->toBe('DUPONT')
->and($employee->firstName)->toBe('Louis');
});
it('returns null on tryToDomain when matricule is invalid', function (): void {
$row = [
'MATRICULE_FIC' => '# s996008',
'NOM_FIC' => 'ARRIVE SAINT FULGENT',
'PRENOM_FIC' => 'Baie de stockage',
];
expect(EmployeeMapper::tryToDomain($row))->toBeNull();
});
it('returns Employee on tryToDomain when matricule is valid', function (): void {
$row = [
'MATRICULE_FIC' => '01155902',
'NOM_FIC' => 'DOE',
'PRENOM_FIC' => 'John',
];
expect(EmployeeMapper::tryToDomain($row))->toBeInstanceOf(Employee::class);
});
});