Gestion du changement de type de contrat + correction du calcule des RTT sur un contrat qui commence en milieu de semaine (#19)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [x] TU/TI/TF rédigée - [x] TU/TI/TF OK - [x] CHANGELOG modifié Reviewed-on: #19 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #19.
This commit is contained in:
196
tests/Service/Contracts/EmployeeContractPhaseResolverTest.php
Normal file
196
tests/Service/Contracts/EmployeeContractPhaseResolverTest.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service\Contracts;
|
||||
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Enum\ContractNature;
|
||||
use App\Enum\ContractType;
|
||||
use App\Enum\TrackingMode;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionProperty;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EmployeeContractPhaseResolverTest extends TestCase
|
||||
{
|
||||
public function testSinglePeriodYieldsSinglePhaseMarkedCurrent(): void
|
||||
{
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
||||
|
||||
self::assertCount(1, $phases);
|
||||
self::assertSame(ContractType::H39, $phases[0]->contractType);
|
||||
self::assertTrue($phases[0]->isCurrent);
|
||||
self::assertNull($phases[0]->endDate);
|
||||
self::assertSame(ContractNature::CDI, $phases[0]->contractNature);
|
||||
}
|
||||
|
||||
public function testThreeConsecutivePeriodsSameSignatureCollapseIntoSinglePhase(): void
|
||||
{
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2021-05-31'],
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2021-06-01', 'end' => '2022-05-31'],
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2022-06-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
||||
|
||||
self::assertCount(1, $phases);
|
||||
self::assertCount(3, $phases[0]->periodIds);
|
||||
self::assertSame('2020-06-01', $phases[0]->startDate->format('Y-m-d'));
|
||||
self::assertNull($phases[0]->endDate);
|
||||
}
|
||||
|
||||
public function testSwitchFromH39ToForfaitProducesTwoPhasesMostRecentFirst(): void
|
||||
{
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2026-04-30'],
|
||||
['type' => ContractType::FORFAIT, 'hours' => 39, 'driver' => false, 'start' => '2026-05-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
||||
|
||||
self::assertCount(2, $phases);
|
||||
self::assertSame(ContractType::FORFAIT, $phases[0]->contractType);
|
||||
self::assertTrue($phases[0]->isCurrent);
|
||||
self::assertSame(ContractType::H39, $phases[1]->contractType);
|
||||
self::assertFalse($phases[1]->isCurrent);
|
||||
self::assertSame('2026-04-30', $phases[1]->endDate?->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public function testInterimBetweenTwoH39PeriodsBreaksThePhases(): void
|
||||
{
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2023-12-31'],
|
||||
['type' => ContractType::INTERIM, 'hours' => null, 'driver' => false, 'start' => '2024-01-01', 'end' => '2024-04-30'],
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2024-05-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
||||
|
||||
self::assertCount(3, $phases);
|
||||
self::assertSame(ContractType::H39, $phases[0]->contractType);
|
||||
self::assertSame(ContractType::INTERIM, $phases[1]->contractType);
|
||||
self::assertSame(ContractType::H39, $phases[2]->contractType);
|
||||
}
|
||||
|
||||
public function testCustomPhasesSplitOnWeeklyHoursChange(): void
|
||||
{
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::CUSTOM, 'hours' => 28, 'driver' => false, 'start' => '2024-01-01', 'end' => '2024-12-31'],
|
||||
['type' => ContractType::CUSTOM, 'hours' => 30, 'driver' => false, 'start' => '2025-01-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
||||
|
||||
self::assertCount(2, $phases);
|
||||
self::assertSame(30, $phases[0]->weeklyHours);
|
||||
self::assertSame(28, $phases[1]->weeklyHours);
|
||||
}
|
||||
|
||||
public function testPhasesSplitOnIsDriverChange(): void
|
||||
{
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2023-01-01', 'end' => '2024-12-31'],
|
||||
['type' => ContractType::H35, 'hours' => 35, 'driver' => true, 'start' => '2025-01-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
||||
|
||||
self::assertCount(2, $phases);
|
||||
self::assertTrue($phases[0]->isDriver);
|
||||
self::assertFalse($phases[1]->isDriver);
|
||||
}
|
||||
|
||||
public function testPhasesEntirelyBeforeDataStartDateAreFilteredOut(): void
|
||||
{
|
||||
// H35 phase ends before 2026-02-23 → must be hidden; H39 phase spans the date → kept.
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2014-07-01', 'end' => '2025-10-31'],
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2025-11-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver('2026-02-23')->resolvePhases($employee);
|
||||
|
||||
self::assertCount(1, $phases);
|
||||
self::assertSame(ContractType::H39, $phases[0]->contractType);
|
||||
}
|
||||
|
||||
public function testPhaseEndingExactlyOnDataStartDateIsKept(): void
|
||||
{
|
||||
// Edge case: a phase whose endDate equals the data start date is kept
|
||||
// (the inequality is `>= $dataStart`, not strict).
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2020-01-01', 'end' => '2026-02-23'],
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2026-02-24', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver('2026-02-23')->resolvePhases($employee);
|
||||
|
||||
self::assertCount(2, $phases);
|
||||
}
|
||||
|
||||
public function testNoFilteringWhenDataStartDateIsEmpty(): void
|
||||
{
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2014-07-01', 'end' => '2020-12-31'],
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2021-01-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
||||
|
||||
self::assertCount(2, $phases);
|
||||
}
|
||||
|
||||
public function testInvalidDataStartDateStringIsTreatedAsNull(): void
|
||||
{
|
||||
$employee = $this->buildEmployee([
|
||||
['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2014-07-01', 'end' => '2020-12-31'],
|
||||
['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2021-01-01', 'end' => null],
|
||||
]);
|
||||
|
||||
$phases = new EmployeeContractPhaseResolver('not-a-date')->resolvePhases($employee);
|
||||
|
||||
self::assertCount(2, $phases);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{type: ContractType, hours: ?int, driver: bool, start: string, end: ?string}> $periodsSpec
|
||||
*/
|
||||
private function buildEmployee(array $periodsSpec): Employee
|
||||
{
|
||||
$employee = new Employee();
|
||||
$id = 0;
|
||||
foreach ($periodsSpec as $spec) {
|
||||
$contract = new Contract();
|
||||
$contract->setName($spec['type']->value);
|
||||
$contract->setTrackingMode(
|
||||
ContractType::FORFAIT === $spec['type'] ? TrackingMode::PRESENCE->value : TrackingMode::TIME->value
|
||||
);
|
||||
$contract->setWeeklyHours($spec['hours']);
|
||||
|
||||
$period = new EmployeeContractPeriod();
|
||||
$reflection = new ReflectionProperty(EmployeeContractPeriod::class, 'id');
|
||||
$reflection->setValue($period, ++$id);
|
||||
$period->setEmployee($employee);
|
||||
$period->setContract($contract);
|
||||
$period->setStartDate(new DateTimeImmutable($spec['start']));
|
||||
$period->setEndDate(null !== $spec['end'] ? new DateTimeImmutable($spec['end']) : null);
|
||||
$period->setContractNature(ContractNature::CDI);
|
||||
$period->setIsDriver($spec['driver']);
|
||||
$employee->getContractPeriods()->add($period);
|
||||
}
|
||||
|
||||
return $employee;
|
||||
}
|
||||
}
|
||||
62
tests/Service/Exercise/ExerciseYearResolverTest.php
Normal file
62
tests/Service/Exercise/ExerciseYearResolverTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service\Exercise;
|
||||
|
||||
use App\Service\Exercise\ExerciseYearResolver;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ExerciseYearResolverTest extends TestCase
|
||||
{
|
||||
public function testNonForfaitJuneMapsToNextYear(): void
|
||||
{
|
||||
$resolver = new ExerciseYearResolver();
|
||||
|
||||
self::assertSame(2026, $resolver->forDate(new DateTimeImmutable('2025-06-01')));
|
||||
self::assertSame(2026, $resolver->forDate(new DateTimeImmutable('2025-06-30')));
|
||||
}
|
||||
|
||||
public function testNonForfaitMayMapsToSameYear(): void
|
||||
{
|
||||
$resolver = new ExerciseYearResolver();
|
||||
|
||||
self::assertSame(2025, $resolver->forDate(new DateTimeImmutable('2025-05-01')));
|
||||
self::assertSame(2025, $resolver->forDate(new DateTimeImmutable('2025-05-31')));
|
||||
}
|
||||
|
||||
public function testNonForfaitDecemberMapsToNextYear(): void
|
||||
{
|
||||
$resolver = new ExerciseYearResolver();
|
||||
|
||||
self::assertSame(2026, $resolver->forDate(new DateTimeImmutable('2025-12-31')));
|
||||
}
|
||||
|
||||
public function testNonForfaitJanuaryMapsToSameYear(): void
|
||||
{
|
||||
$resolver = new ExerciseYearResolver();
|
||||
|
||||
self::assertSame(2025, $resolver->forDate(new DateTimeImmutable('2025-01-15')));
|
||||
}
|
||||
|
||||
public function testForfaitReturnsCalendarYearRegardlessOfMonth(): void
|
||||
{
|
||||
$resolver = new ExerciseYearResolver();
|
||||
|
||||
self::assertSame(2025, $resolver->forDate(new DateTimeImmutable('2025-01-15'), true));
|
||||
self::assertSame(2025, $resolver->forDate(new DateTimeImmutable('2025-06-01'), true));
|
||||
self::assertSame(2025, $resolver->forDate(new DateTimeImmutable('2025-12-31'), true));
|
||||
}
|
||||
|
||||
public function testForfaitFlagDefaultsToFalse(): void
|
||||
{
|
||||
$resolver = new ExerciseYearResolver();
|
||||
|
||||
// June without explicit flag must follow non-forfait rule (year + 1).
|
||||
self::assertSame(2026, $resolver->forDate(new DateTimeImmutable('2025-06-01')));
|
||||
}
|
||||
}
|
||||
74
tests/Service/Rtt/RttRecoveryComputationServiceTest.php
Normal file
74
tests/Service/Rtt/RttRecoveryComputationServiceTest.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service\Rtt;
|
||||
|
||||
use App\Entity\Contract;
|
||||
use App\Service\Rtt\RttRecoveryComputationService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* The service constructor takes several final-class collaborators that PHPUnit cannot
|
||||
* double. Pure helpers are exercised via newInstanceWithoutConstructor + reflection.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RttRecoveryComputationServiceTest extends TestCase
|
||||
{
|
||||
public function testResolveWeekAnchorDateReturnsFirstContractedDayWhenWeekStartsBeforeHire(): void
|
||||
{
|
||||
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
|
||||
$contract = new Contract();
|
||||
|
||||
$weekDays = ['2026-03-16', '2026-03-17', '2026-03-18', '2026-03-19', '2026-03-20', '2026-03-21', '2026-03-22'];
|
||||
$contractsByDate = [
|
||||
'2026-03-16' => null,
|
||||
'2026-03-17' => null,
|
||||
'2026-03-18' => null,
|
||||
'2026-03-19' => $contract,
|
||||
'2026-03-20' => $contract,
|
||||
'2026-03-21' => $contract,
|
||||
'2026-03-22' => $contract,
|
||||
];
|
||||
|
||||
$anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);
|
||||
|
||||
self::assertSame('2026-03-19', $anchor);
|
||||
}
|
||||
|
||||
public function testResolveWeekAnchorDateReturnsFirstDayWhenItIsContracted(): void
|
||||
{
|
||||
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
|
||||
$contract = new Contract();
|
||||
|
||||
$weekDays = ['2026-03-23', '2026-03-24', '2026-03-25'];
|
||||
$contractsByDate = [
|
||||
'2026-03-23' => $contract,
|
||||
'2026-03-24' => $contract,
|
||||
'2026-03-25' => $contract,
|
||||
];
|
||||
|
||||
$anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);
|
||||
|
||||
self::assertSame('2026-03-23', $anchor);
|
||||
}
|
||||
|
||||
public function testResolveWeekAnchorDateFallsBackToFirstDayWhenNoContract(): void
|
||||
{
|
||||
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
|
||||
|
||||
$weekDays = ['2026-03-16', '2026-03-17'];
|
||||
$contractsByDate = ['2026-03-16' => null, '2026-03-17' => null];
|
||||
|
||||
$anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);
|
||||
|
||||
self::assertSame('2026-03-16', $anchor);
|
||||
}
|
||||
|
||||
private function invokePrivate(object $obj, string $method, mixed ...$args): mixed
|
||||
{
|
||||
return new ReflectionClass($obj::class)->getMethod($method)->invoke($obj, ...$args);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user