feat(rtt) : phaseId support in EmployeeRttSummaryProvider
Mirror Task 3 (leave provider) on the RTT side: accept an optional `?phaseId` query parameter and cap the exercise window to the phase boundaries when set. - Inject EmployeeContractPhaseResolver. - New helpers: resolveTargetPhase, clampYearToPhase, exerciseYearForDate. - resolveYear now takes the phase: default year falls back to the phase end date when phaseId is provided; explicit year is silently clamped to the phase range. - provide() narrows periodFrom / periodTo / limitDate to the phase end date for past phases. - Default behavior (no phaseId) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ namespace App\State;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\EmployeeRttSummary;
|
||||
use App\Dto\Contracts\ContractPhase;
|
||||
use App\Dto\Rtt\EmployeeRttWeekSummary;
|
||||
use App\Dto\Rtt\RttMonthPayment;
|
||||
use App\Dto\Rtt\WeekRecoveryDetail;
|
||||
@@ -17,6 +18,7 @@ use App\Repository\EmployeeRttBalanceRepository;
|
||||
use App\Repository\EmployeeRttPaymentRepository;
|
||||
use App\Repository\WorkHourRepository;
|
||||
use App\Security\EmployeeScopeService;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use App\Service\Rtt\RttRecoveryComputationService;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
@@ -38,6 +40,7 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
private EmployeeRttPaymentRepository $rttPaymentRepository,
|
||||
private RttRecoveryComputationService $rttRecoveryService,
|
||||
private WorkHourRepository $workHourRepository,
|
||||
private EmployeeContractPhaseResolver $phaseResolver,
|
||||
string $rttStartDate = '',
|
||||
) {
|
||||
$this->rttStartDate = '' !== $rttStartDate ? $rttStartDate : null;
|
||||
@@ -64,12 +67,22 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
throw new AccessDeniedHttpException('Employee outside your scope.');
|
||||
}
|
||||
|
||||
$year = $this->resolveYear();
|
||||
$phase = $this->resolveTargetPhase($employee);
|
||||
$year = $this->resolveYear($phase);
|
||||
$today = new DateTimeImmutable('today');
|
||||
$currentExerciseYear = $this->resolveCurrentExerciseYear($today);
|
||||
[$periodFrom, $periodTo] = $this->rttRecoveryService->resolveExerciseBounds($year);
|
||||
$weeks = $this->rttRecoveryService->buildWeeksForExercise($periodFrom, $periodTo);
|
||||
$weekRanges = array_map(
|
||||
|
||||
// Cap exercise bounds to the phase boundaries.
|
||||
if (!$phase->isCurrent && null !== $phase->endDate && $phase->endDate < $periodTo) {
|
||||
$periodTo = $phase->endDate;
|
||||
}
|
||||
if ($phase->startDate > $periodFrom) {
|
||||
$periodFrom = $phase->startDate;
|
||||
}
|
||||
|
||||
$weeks = $this->rttRecoveryService->buildWeeksForExercise($periodFrom, $periodTo);
|
||||
$weekRanges = array_map(
|
||||
static fn (array $week): array => [
|
||||
'weekNumber' => (int) $week['weekNumber'],
|
||||
'start' => $week['start'],
|
||||
@@ -96,6 +109,12 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
}
|
||||
}
|
||||
|
||||
// For a closed phase: cap the week-computation limit at the phase end date,
|
||||
// so weeks beyond the phase are not counted.
|
||||
if (!$phase->isCurrent && null !== $phase->endDate && $phase->endDate < $limitDate) {
|
||||
$limitDate = $phase->endDate;
|
||||
}
|
||||
|
||||
$currentByWeekStart = $this->rttRecoveryService->computeRecoveryByWeek($employee, $weekRanges, $periodFrom, $periodTo, $limitDate);
|
||||
[$carry, $carryMonth] = $this->resolveCarry($employee, $year);
|
||||
|
||||
@@ -213,10 +232,21 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveYear(): int
|
||||
private function resolveYear(ContractPhase $phase): int
|
||||
{
|
||||
$raw = (string) ($this->requestStack->getCurrentRequest()?->query->get('year') ?? '');
|
||||
$raw = (string) ($this->requestStack->getCurrentRequest()?->query->get('year') ?? '');
|
||||
$phaseIdRaw = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
|
||||
$phaseIdProvided = null !== $phaseIdRaw && '' !== (string) $phaseIdRaw;
|
||||
|
||||
if ('' === $raw) {
|
||||
// When a phaseId is explicitly provided, default to the exercise year derived from
|
||||
// the phase's end date (or today if the phase is still current).
|
||||
if ($phaseIdProvided) {
|
||||
$reference = $phase->endDate ?? new DateTimeImmutable('today');
|
||||
|
||||
return $this->resolveCurrentExerciseYear($reference);
|
||||
}
|
||||
|
||||
return $this->resolveCurrentExerciseYear(new DateTimeImmutable('today'));
|
||||
}
|
||||
if (!preg_match('/^\d{4}$/', $raw)) {
|
||||
@@ -228,9 +258,75 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
throw new UnprocessableEntityHttpException('year must be between 2000 and 2100.');
|
||||
}
|
||||
|
||||
// When a phaseId is explicit, silently clamp the requested year to the
|
||||
// first/last exercise covered by the phase.
|
||||
if ($phaseIdProvided) {
|
||||
$year = $this->clampYearToPhase($year, $phase);
|
||||
}
|
||||
|
||||
return $year;
|
||||
}
|
||||
|
||||
private function clampYearToPhase(int $year, ContractPhase $phase): int
|
||||
{
|
||||
$firstYear = $this->exerciseYearForDate($phase->startDate);
|
||||
$lastYear = $phase->endDate instanceof DateTimeImmutable
|
||||
? $this->exerciseYearForDate($phase->endDate)
|
||||
: null;
|
||||
|
||||
if ($year < $firstYear) {
|
||||
return $firstYear;
|
||||
}
|
||||
if (null !== $lastYear && $year > $lastYear) {
|
||||
return $lastYear;
|
||||
}
|
||||
|
||||
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');
|
||||
$phases = $this->phaseResolver->resolvePhases($employee);
|
||||
if ([] === $phases) {
|
||||
throw new UnprocessableEntityHttpException('Employee has no contract phase.');
|
||||
}
|
||||
|
||||
if (null === $raw || '' === (string) $raw) {
|
||||
// Phase courante par défaut = celle marquée isCurrent ou, à défaut, la plus récente.
|
||||
foreach ($phases as $phase) {
|
||||
if ($phase->isCurrent) {
|
||||
return $phase;
|
||||
}
|
||||
}
|
||||
|
||||
return $phases[0];
|
||||
}
|
||||
|
||||
if (!preg_match('/^\d+$/', (string) $raw)) {
|
||||
throw new UnprocessableEntityHttpException('phaseId must be a positive integer.');
|
||||
}
|
||||
$phaseId = (int) $raw;
|
||||
foreach ($phases as $phase) {
|
||||
if ($phase->id === $phaseId) {
|
||||
return $phase;
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnprocessableEntityHttpException('phaseId does not match any phase of this employee.');
|
||||
}
|
||||
|
||||
private function resolveCurrentExerciseYear(DateTimeImmutable $today): int
|
||||
{
|
||||
$year = (int) $today->format('Y');
|
||||
|
||||
Reference in New Issue
Block a user