Gestion du changement de type de contrat + correction du calcule des RTT sur un contrat qui commence en milieu de semaine (#19)
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:
2026-05-22 06:42:33 +00:00
committed by Autin
parent b541f9ded8
commit abdaf809f8
40 changed files with 5021 additions and 153 deletions
+95 -5
View File
@@ -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,8 @@ use App\Repository\EmployeeRttBalanceRepository;
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;
@@ -38,6 +41,8 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
private EmployeeRttPaymentRepository $rttPaymentRepository,
private RttRecoveryComputationService $rttRecoveryService,
private WorkHourRepository $workHourRepository,
private EmployeeContractPhaseResolver $phaseResolver,
private ExerciseYearResolver $exerciseYearResolver,
string $rttStartDate = '',
) {
$this->rttStartDate = '' !== $rttStartDate ? $rttStartDate : null;
@@ -64,12 +69,25 @@ 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 periodTo at the phase endDate for closed phases so the RTT table does
// not extend past the date the phase ended.
// Do NOT cap periodFrom at phase.startDate: keep the full exercise
// displayed so weeks before the employee's hire (or before a past phase
// started) appear at 0, matching the previous behavior. Weeks outside the
// contract range contribute 0 minutes to the cumul naturally (no contract
// ⇒ no reference, no worked hours).
if (!$phase->isCurrent && null !== $phase->endDate && $phase->endDate < $periodTo) {
$periodTo = $phase->endDate;
}
$weeks = $this->rttRecoveryService->buildWeeksForExercise($periodFrom, $periodTo);
$weekRanges = array_map(
static fn (array $week): array => [
'weekNumber' => (int) $week['weekNumber'],
'start' => $week['start'],
@@ -96,6 +114,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 +237,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 +263,64 @@ 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->exerciseYearResolver->forDate($phase->startDate);
$lastYear = $phase->endDate instanceof DateTimeImmutable
? $this->exerciseYearResolver->forDate($phase->endDate)
: null;
if ($year < $firstYear) {
return $firstYear;
}
if (null !== $lastYear && $year > $lastYear) {
return $lastYear;
}
return $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');