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\Security\EmployeeScopeService;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use App\Service\Exercise\ExerciseYearResolver;
|
||||
use App\Service\Leave\LeaveBalanceComputationService;
|
||||
use App\Service\Leave\LongMaladieService;
|
||||
use App\Service\Leave\SuspensionDaysCalculator;
|
||||
@@ -63,6 +64,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
private SuspensionDaysCalculator $suspensionDaysCalculator,
|
||||
private WorkHourRepository $workHourRepository,
|
||||
private EmployeeContractPhaseResolver $phaseResolver,
|
||||
private ExerciseYearResolver $exerciseYearResolver,
|
||||
string $dataStartDate = '',
|
||||
) {
|
||||
$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
|
||||
{
|
||||
$firstYear = $this->exerciseYearForDate($phase->startDate, $isForfait);
|
||||
$firstYear = $this->exerciseYearResolver->forDate($phase->startDate, $isForfait);
|
||||
$lastYear = $phase->endDate instanceof DateTimeImmutable
|
||||
? $this->exerciseYearForDate($phase->endDate, $isForfait)
|
||||
? $this->exerciseYearResolver->forDate($phase->endDate, $isForfait)
|
||||
: null;
|
||||
|
||||
if ($year < $firstYear) {
|
||||
@@ -495,22 +497,6 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
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
|
||||
{
|
||||
$raw = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
|
||||
@@ -1041,7 +1027,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
: $this->resolveCurrentLeaveYear(new DateTimeImmutable('today'));
|
||||
|
||||
// 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();
|
||||
if ([] === $history) {
|
||||
@@ -1066,7 +1052,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
return max($phaseFirstYear, $candidate);
|
||||
}
|
||||
|
||||
$firstYear = $this->exerciseYearForDate($oldestStartDate, $isForfait);
|
||||
$firstYear = $this->exerciseYearResolver->forDate($oldestStartDate, $isForfait);
|
||||
|
||||
$oldestBalanceYear = $this->leaveBalanceRepository->findEarliestYearForEmployee($employee);
|
||||
if (null !== $oldestBalanceYear && $oldestBalanceYear < $firstYear) {
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Repository\EmployeeRepository;
|
||||
use App\Repository\EmployeeRttPaymentRepository;
|
||||
use App\Service\AuditLogger;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use DateTimeImmutable;
|
||||
use App\Service\Exercise\ExerciseYearResolver;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
@@ -28,6 +28,7 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
||||
private AuditLogger $auditLogger,
|
||||
private EmployeeContractPhaseResolver $phaseResolver,
|
||||
private ClockInterface $clock,
|
||||
private ExerciseYearResolver $exerciseYearResolver,
|
||||
) {}
|
||||
|
||||
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
|
||||
{
|
||||
return $this->resolveExerciseYearForDate($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;
|
||||
return $this->exerciseYearResolver->forDate($this->clock->now());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +110,7 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
||||
if ($phase->isCurrent || null === $phase->endDate) {
|
||||
continue;
|
||||
}
|
||||
if ($year === $this->resolveExerciseYearForDate($phase->endDate)) {
|
||||
if ($year === $this->exerciseYearResolver->forDate($phase->endDate)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Repository\EmployeeRttPaymentRepository;
|
||||
use App\Repository\WorkHourRepository;
|
||||
use App\Security\EmployeeScopeService;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use App\Service\Exercise\ExerciseYearResolver;
|
||||
use App\Service\Rtt\RttRecoveryComputationService;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
@@ -41,6 +42,7 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
private RttRecoveryComputationService $rttRecoveryService,
|
||||
private WorkHourRepository $workHourRepository,
|
||||
private EmployeeContractPhaseResolver $phaseResolver,
|
||||
private ExerciseYearResolver $exerciseYearResolver,
|
||||
string $rttStartDate = '',
|
||||
) {
|
||||
$this->rttStartDate = '' !== $rttStartDate ? $rttStartDate : null;
|
||||
@@ -269,9 +271,9 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
|
||||
private function clampYearToPhase(int $year, ContractPhase $phase): int
|
||||
{
|
||||
$firstYear = $this->exerciseYearForDate($phase->startDate);
|
||||
$firstYear = $this->exerciseYearResolver->forDate($phase->startDate);
|
||||
$lastYear = $phase->endDate instanceof DateTimeImmutable
|
||||
? $this->exerciseYearForDate($phase->endDate)
|
||||
? $this->exerciseYearResolver->forDate($phase->endDate)
|
||||
: null;
|
||||
|
||||
if ($year < $firstYear) {
|
||||
@@ -284,17 +286,6 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
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
|
||||
{
|
||||
$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\TrackingMode;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use App\Service\Exercise\ExerciseYearResolver;
|
||||
use App\State\EmployeeLeaveSummaryProvider;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -371,6 +372,7 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase
|
||||
|
||||
$this->setReadonlyProperty($provider, 'requestStack', $requestStack);
|
||||
$this->setReadonlyProperty($provider, 'phaseResolver', new EmployeeContractPhaseResolver());
|
||||
$this->setReadonlyProperty($provider, 'exerciseYearResolver', new ExerciseYearResolver());
|
||||
$this->setReadonlyProperty($provider, 'dataStartDate', null);
|
||||
|
||||
return $provider;
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Entity\EmployeeContractPeriod;
|
||||
use App\Enum\ContractNature;
|
||||
use App\Enum\TrackingMode;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use App\Service\Exercise\ExerciseYearResolver;
|
||||
use App\State\EmployeeRttPaymentProcessor;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -141,6 +142,7 @@ final class EmployeeRttPaymentProcessorTest extends TestCase
|
||||
|
||||
$this->setReadonlyProperty($processor, 'phaseResolver', new EmployeeContractPhaseResolver());
|
||||
$this->setReadonlyProperty($processor, 'clock', $clock);
|
||||
$this->setReadonlyProperty($processor, 'exerciseYearResolver', new ExerciseYearResolver());
|
||||
|
||||
return $processor;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Enum\ContractNature;
|
||||
use App\Enum\ContractType;
|
||||
use App\Enum\TrackingMode;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use App\Service\Exercise\ExerciseYearResolver;
|
||||
use App\State\EmployeeRttSummaryProvider;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -264,6 +265,7 @@ final class EmployeeRttSummaryProviderTest extends TestCase
|
||||
|
||||
$this->setReadonlyProperty($provider, 'requestStack', $requestStack);
|
||||
$this->setReadonlyProperty($provider, 'phaseResolver', new EmployeeContractPhaseResolver());
|
||||
$this->setReadonlyProperty($provider, 'exerciseYearResolver', new ExerciseYearResolver());
|
||||
$this->setReadonlyProperty($provider, 'rttStartDate', null);
|
||||
|
||||
return $provider;
|
||||
|
||||
Reference in New Issue
Block a user