refactor(exercise) : extract ExerciseYearResolver to dedup year formula
Pull the "date -> leave/RTT exercise year" formula out of EmployeeRttPaymentProcessor, EmployeeRttSummaryProvider and EmployeeLeaveSummaryProvider into a single App\Service\Exercise\ExerciseYearResolver. Forfait flag is parameterised so the leave (calendar year) and RTT (Juin N-1 -> Mai N) variants share the same implementation. Pure refactor, no behavioural change.
This commit is contained in:
27
src/Service/Exercise/ExerciseYearResolver.php
Normal file
27
src/Service/Exercise/ExerciseYearResolver.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service\Exercise;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
final readonly class ExerciseYearResolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Convert a date to its leave/RTT exercise year.
|
||||||
|
*
|
||||||
|
* - Forfait: calendar year (Jan→Dec) — returns $date.Y.
|
||||||
|
* - Non-forfait: leave year (Juin N-1 → Mai N) — returns $date.Y+1 if month >= 6, else $date.Y.
|
||||||
|
*/
|
||||||
|
public function forDate(DateTimeImmutable $date, bool $isForfait = false): int
|
||||||
|
{
|
||||||
|
if ($isForfait) {
|
||||||
|
return (int) $date->format('Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $date->format('n') >= 6
|
||||||
|
? (int) $date->format('Y') + 1
|
||||||
|
: (int) $date->format('Y');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ use App\Repository\EmployeeRepository;
|
|||||||
use App\Repository\WorkHourRepository;
|
use App\Repository\WorkHourRepository;
|
||||||
use App\Security\EmployeeScopeService;
|
use App\Security\EmployeeScopeService;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
|
use App\Service\Exercise\ExerciseYearResolver;
|
||||||
use App\Service\Leave\LeaveBalanceComputationService;
|
use App\Service\Leave\LeaveBalanceComputationService;
|
||||||
use App\Service\Leave\LongMaladieService;
|
use App\Service\Leave\LongMaladieService;
|
||||||
use App\Service\Leave\SuspensionDaysCalculator;
|
use App\Service\Leave\SuspensionDaysCalculator;
|
||||||
@@ -63,6 +64,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
private SuspensionDaysCalculator $suspensionDaysCalculator,
|
private SuspensionDaysCalculator $suspensionDaysCalculator,
|
||||||
private WorkHourRepository $workHourRepository,
|
private WorkHourRepository $workHourRepository,
|
||||||
private EmployeeContractPhaseResolver $phaseResolver,
|
private EmployeeContractPhaseResolver $phaseResolver,
|
||||||
|
private ExerciseYearResolver $exerciseYearResolver,
|
||||||
string $dataStartDate = '',
|
string $dataStartDate = '',
|
||||||
) {
|
) {
|
||||||
$this->dataStartDate = '' !== $dataStartDate ? $dataStartDate : null;
|
$this->dataStartDate = '' !== $dataStartDate ? $dataStartDate : null;
|
||||||
@@ -480,9 +482,9 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
|
|
||||||
private function clampYearToPhase(int $year, ContractPhase $phase, bool $isForfait): int
|
private function clampYearToPhase(int $year, ContractPhase $phase, bool $isForfait): int
|
||||||
{
|
{
|
||||||
$firstYear = $this->exerciseYearForDate($phase->startDate, $isForfait);
|
$firstYear = $this->exerciseYearResolver->forDate($phase->startDate, $isForfait);
|
||||||
$lastYear = $phase->endDate instanceof DateTimeImmutable
|
$lastYear = $phase->endDate instanceof DateTimeImmutable
|
||||||
? $this->exerciseYearForDate($phase->endDate, $isForfait)
|
? $this->exerciseYearResolver->forDate($phase->endDate, $isForfait)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if ($year < $firstYear) {
|
if ($year < $firstYear) {
|
||||||
@@ -495,22 +497,6 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
return $year;
|
return $year;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a date to the leave exercise year it belongs to.
|
|
||||||
* - Forfait: exercise = calendar year.
|
|
||||||
* - Non-forfait: exercise N runs from June (N-1) to May (N); dates in June-December
|
|
||||||
* map to N+1, January-May map to N.
|
|
||||||
*/
|
|
||||||
private function exerciseYearForDate(DateTimeImmutable $date, bool $isForfait): int
|
|
||||||
{
|
|
||||||
$year = (int) $date->format('Y');
|
|
||||||
if ($isForfait) {
|
|
||||||
return $year;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) $date->format('n') >= 6 ? $year + 1 : $year;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveTargetPhase(Employee $employee): ContractPhase
|
private function resolveTargetPhase(Employee $employee): ContractPhase
|
||||||
{
|
{
|
||||||
$raw = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
|
$raw = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
|
||||||
@@ -1041,7 +1027,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
: $this->resolveCurrentLeaveYear(new DateTimeImmutable('today'));
|
: $this->resolveCurrentLeaveYear(new DateTimeImmutable('today'));
|
||||||
|
|
||||||
// Do not go before the exercice containing $phase->startDate.
|
// Do not go before the exercice containing $phase->startDate.
|
||||||
$phaseFirstYear = $this->exerciseYearForDate($phase->startDate, $isForfait);
|
$phaseFirstYear = $this->exerciseYearResolver->forDate($phase->startDate, $isForfait);
|
||||||
|
|
||||||
$history = $employee->getContractHistory();
|
$history = $employee->getContractHistory();
|
||||||
if ([] === $history) {
|
if ([] === $history) {
|
||||||
@@ -1066,7 +1052,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
return max($phaseFirstYear, $candidate);
|
return max($phaseFirstYear, $candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
$firstYear = $this->exerciseYearForDate($oldestStartDate, $isForfait);
|
$firstYear = $this->exerciseYearResolver->forDate($oldestStartDate, $isForfait);
|
||||||
|
|
||||||
$oldestBalanceYear = $this->leaveBalanceRepository->findEarliestYearForEmployee($employee);
|
$oldestBalanceYear = $this->leaveBalanceRepository->findEarliestYearForEmployee($employee);
|
||||||
if (null !== $oldestBalanceYear && $oldestBalanceYear < $firstYear) {
|
if (null !== $oldestBalanceYear && $oldestBalanceYear < $firstYear) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Repository\EmployeeRepository;
|
|||||||
use App\Repository\EmployeeRttPaymentRepository;
|
use App\Repository\EmployeeRttPaymentRepository;
|
||||||
use App\Service\AuditLogger;
|
use App\Service\AuditLogger;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
use DateTimeImmutable;
|
use App\Service\Exercise\ExerciseYearResolver;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Clock\ClockInterface;
|
use Psr\Clock\ClockInterface;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
@@ -28,6 +28,7 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
|||||||
private AuditLogger $auditLogger,
|
private AuditLogger $auditLogger,
|
||||||
private EmployeeContractPhaseResolver $phaseResolver,
|
private EmployeeContractPhaseResolver $phaseResolver,
|
||||||
private ClockInterface $clock,
|
private ClockInterface $clock,
|
||||||
|
private ExerciseYearResolver $exerciseYearResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EmployeeRttPaymentInput
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EmployeeRttPaymentInput
|
||||||
@@ -89,18 +90,7 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
|||||||
|
|
||||||
private function resolveCurrentExerciseYear(): int
|
private function resolveCurrentExerciseYear(): int
|
||||||
{
|
{
|
||||||
return $this->resolveExerciseYearForDate($this->clock->now());
|
return $this->exerciseYearResolver->forDate($this->clock->now());
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a date to the RTT exercise year it belongs to (Juin N-1 → Mai N convention).
|
|
||||||
*/
|
|
||||||
private function resolveExerciseYearForDate(DateTimeImmutable $date): int
|
|
||||||
{
|
|
||||||
$year = (int) $date->format('Y');
|
|
||||||
$month = (int) $date->format('n');
|
|
||||||
|
|
||||||
return $month >= 6 ? $year + 1 : $year;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,7 +110,7 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
|||||||
if ($phase->isCurrent || null === $phase->endDate) {
|
if ($phase->isCurrent || null === $phase->endDate) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ($year === $this->resolveExerciseYearForDate($phase->endDate)) {
|
if ($year === $this->exerciseYearResolver->forDate($phase->endDate)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use App\Repository\EmployeeRttPaymentRepository;
|
|||||||
use App\Repository\WorkHourRepository;
|
use App\Repository\WorkHourRepository;
|
||||||
use App\Security\EmployeeScopeService;
|
use App\Security\EmployeeScopeService;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
|
use App\Service\Exercise\ExerciseYearResolver;
|
||||||
use App\Service\Rtt\RttRecoveryComputationService;
|
use App\Service\Rtt\RttRecoveryComputationService;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
@@ -41,6 +42,7 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
|||||||
private RttRecoveryComputationService $rttRecoveryService,
|
private RttRecoveryComputationService $rttRecoveryService,
|
||||||
private WorkHourRepository $workHourRepository,
|
private WorkHourRepository $workHourRepository,
|
||||||
private EmployeeContractPhaseResolver $phaseResolver,
|
private EmployeeContractPhaseResolver $phaseResolver,
|
||||||
|
private ExerciseYearResolver $exerciseYearResolver,
|
||||||
string $rttStartDate = '',
|
string $rttStartDate = '',
|
||||||
) {
|
) {
|
||||||
$this->rttStartDate = '' !== $rttStartDate ? $rttStartDate : null;
|
$this->rttStartDate = '' !== $rttStartDate ? $rttStartDate : null;
|
||||||
@@ -269,9 +271,9 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
|||||||
|
|
||||||
private function clampYearToPhase(int $year, ContractPhase $phase): int
|
private function clampYearToPhase(int $year, ContractPhase $phase): int
|
||||||
{
|
{
|
||||||
$firstYear = $this->exerciseYearForDate($phase->startDate);
|
$firstYear = $this->exerciseYearResolver->forDate($phase->startDate);
|
||||||
$lastYear = $phase->endDate instanceof DateTimeImmutable
|
$lastYear = $phase->endDate instanceof DateTimeImmutable
|
||||||
? $this->exerciseYearForDate($phase->endDate)
|
? $this->exerciseYearResolver->forDate($phase->endDate)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if ($year < $firstYear) {
|
if ($year < $firstYear) {
|
||||||
@@ -284,17 +286,6 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
|||||||
return $year;
|
return $year;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a date to the RTT exercise year it belongs to (Juin N-1 → Mai N convention).
|
|
||||||
*/
|
|
||||||
private function exerciseYearForDate(DateTimeImmutable $date): int
|
|
||||||
{
|
|
||||||
$year = (int) $date->format('Y');
|
|
||||||
$month = (int) $date->format('n');
|
|
||||||
|
|
||||||
return $month >= 6 ? $year + 1 : $year;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveTargetPhase(Employee $employee): ContractPhase
|
private function resolveTargetPhase(Employee $employee): ContractPhase
|
||||||
{
|
{
|
||||||
$raw = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
|
$raw = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
|
||||||
|
|||||||
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')));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ use App\Enum\ContractNature;
|
|||||||
use App\Enum\ContractType;
|
use App\Enum\ContractType;
|
||||||
use App\Enum\TrackingMode;
|
use App\Enum\TrackingMode;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
|
use App\Service\Exercise\ExerciseYearResolver;
|
||||||
use App\State\EmployeeLeaveSummaryProvider;
|
use App\State\EmployeeLeaveSummaryProvider;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -371,6 +372,7 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase
|
|||||||
|
|
||||||
$this->setReadonlyProperty($provider, 'requestStack', $requestStack);
|
$this->setReadonlyProperty($provider, 'requestStack', $requestStack);
|
||||||
$this->setReadonlyProperty($provider, 'phaseResolver', new EmployeeContractPhaseResolver());
|
$this->setReadonlyProperty($provider, 'phaseResolver', new EmployeeContractPhaseResolver());
|
||||||
|
$this->setReadonlyProperty($provider, 'exerciseYearResolver', new ExerciseYearResolver());
|
||||||
$this->setReadonlyProperty($provider, 'dataStartDate', null);
|
$this->setReadonlyProperty($provider, 'dataStartDate', null);
|
||||||
|
|
||||||
return $provider;
|
return $provider;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Entity\EmployeeContractPeriod;
|
|||||||
use App\Enum\ContractNature;
|
use App\Enum\ContractNature;
|
||||||
use App\Enum\TrackingMode;
|
use App\Enum\TrackingMode;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
|
use App\Service\Exercise\ExerciseYearResolver;
|
||||||
use App\State\EmployeeRttPaymentProcessor;
|
use App\State\EmployeeRttPaymentProcessor;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -141,6 +142,7 @@ final class EmployeeRttPaymentProcessorTest extends TestCase
|
|||||||
|
|
||||||
$this->setReadonlyProperty($processor, 'phaseResolver', new EmployeeContractPhaseResolver());
|
$this->setReadonlyProperty($processor, 'phaseResolver', new EmployeeContractPhaseResolver());
|
||||||
$this->setReadonlyProperty($processor, 'clock', $clock);
|
$this->setReadonlyProperty($processor, 'clock', $clock);
|
||||||
|
$this->setReadonlyProperty($processor, 'exerciseYearResolver', new ExerciseYearResolver());
|
||||||
|
|
||||||
return $processor;
|
return $processor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Enum\ContractNature;
|
|||||||
use App\Enum\ContractType;
|
use App\Enum\ContractType;
|
||||||
use App\Enum\TrackingMode;
|
use App\Enum\TrackingMode;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
|
use App\Service\Exercise\ExerciseYearResolver;
|
||||||
use App\State\EmployeeRttSummaryProvider;
|
use App\State\EmployeeRttSummaryProvider;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -264,6 +265,7 @@ final class EmployeeRttSummaryProviderTest extends TestCase
|
|||||||
|
|
||||||
$this->setReadonlyProperty($provider, 'requestStack', $requestStack);
|
$this->setReadonlyProperty($provider, 'requestStack', $requestStack);
|
||||||
$this->setReadonlyProperty($provider, 'phaseResolver', new EmployeeContractPhaseResolver());
|
$this->setReadonlyProperty($provider, 'phaseResolver', new EmployeeContractPhaseResolver());
|
||||||
|
$this->setReadonlyProperty($provider, 'exerciseYearResolver', new ExerciseYearResolver());
|
||||||
$this->setReadonlyProperty($provider, 'rttStartDate', null);
|
$this->setReadonlyProperty($provider, 'rttStartDate', null);
|
||||||
|
|
||||||
return $provider;
|
return $provider;
|
||||||
|
|||||||
Reference in New Issue
Block a user