fix(leave) : do not cap from at phase.startDate for non-forfait phases

The CP exercise (Juin N-1 → Mai N) is annual and continuous across
contract-signature changes within the same leave rule (e.g. 35h → 39h,
isDriver flip, weeklyHours bump). Capping `from` at the phase start
truncated the accrual to just the months under the latest phase,
producing wrong "en cours d'acquisition" values and dropping presence
days from earlier months on the leave-tab calendar.

For Damien GUILLOT (35h until 2025-10-31, then 39h), this gave 15 days
acquired (6 months Nov→Apr) instead of the expected 27.5 days
(11 months Jun→Apr at 2.5/month). After this fix, the H39 view shows
the full annual accrual as expected.

FORFAIT phases keep the from cap: the 218-day target is calendar-year
scoped and only counts the FORFAIT portion of the year.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 14:31:56 +02:00
parent f48f1d2f3a
commit bbde6ddcf3
4 changed files with 113 additions and 10 deletions

View File

@@ -172,6 +172,44 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase
self::assertEqualsWithDelta(22.92, $acquired, 0.1);
}
public function testNonForfaitPhaseStartingMidExerciseUsesFullExerciseFromAsStart(): void
{
// Scenario: 35h CDI from 2014-07-01 to 2025-10-31, then 39h CDI from 2025-11-01.
// Both phases are non-forfait (same leave rule CDI_CDD_NON_FORFAIT).
// Viewing exercise 2026 on the current 39h phase, accrual must run from the
// exercise start (June 1, 2025), NOT from the phase start (November 1, 2025).
// Otherwise the 5 months of June-October under 35h would be lost from the
// annual CP accrual, which is wrong (CP exercise is annual, not per-phase).
$employee = $this->buildH35ToH39Transition('2014-07-01', '2025-10-31', '2025-11-01');
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
$h39Phase = $phases[0]; // current
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2026']);
[$from, $to] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2026, $h39Phase);
self::assertSame('2025-06-01', $from->format('Y-m-d'));
self::assertSame('2026-05-31', $to->format('Y-m-d'));
}
public function testForfaitPhaseStartingMidYearCapsFromAtPhaseStart(): void
{
// Scenario: 39h CDI ends 2026-04-30, FORFAIT from 2026-05-01.
// Viewing year 2026 on the FORFAIT phase, the period must be capped at
// phase start (May 1) so that only the FORFAIT portion of the calendar
// year is counted.
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
$forfaitPhase = $phases[0]; // current FORFAIT
$provider = $this->buildProvider(['phaseId' => (string) $forfaitPhase->id, 'year' => '2026']);
[$from, $to] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2026, $forfaitPhase);
self::assertSame('2026-05-01', $from->format('Y-m-d'));
self::assertSame('2026-12-31', $to->format('Y-m-d'));
}
public function testYearOutsidePhaseRangeIsSilentlyClampedToPhaseLastExercise(): void
{
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
@@ -311,6 +349,45 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase
/**
* Build a two-period employee transitioning from H39 to FORFAIT.
*/
private function buildH35ToH39Transition(string $h35Start, string $h35End, string $h39Start): Employee
{
$employee = new Employee();
$this->setEntityId($employee, 1);
$h35Contract = new Contract();
$h35Contract->setName('35H');
$h35Contract->setTrackingMode(TrackingMode::TIME->value);
$h35Contract->setWeeklyHours(35);
$h39Contract = new Contract();
$h39Contract->setName('39H');
$h39Contract->setTrackingMode(TrackingMode::TIME->value);
$h39Contract->setWeeklyHours(39);
$h35Period = new EmployeeContractPeriod();
$this->setEntityId($h35Period, 1);
$h35Period->setEmployee($employee);
$h35Period->setContract($h35Contract);
$h35Period->setStartDate(new DateTimeImmutable($h35Start));
$h35Period->setEndDate(new DateTimeImmutable($h35End));
$h35Period->setContractNature(ContractNature::CDI);
$h35Period->setIsDriver(false);
$h39Period = new EmployeeContractPeriod();
$this->setEntityId($h39Period, 2);
$h39Period->setEmployee($employee);
$h39Period->setContract($h39Contract);
$h39Period->setStartDate(new DateTimeImmutable($h39Start));
$h39Period->setEndDate(null);
$h39Period->setContractNature(ContractNature::CDI);
$h39Period->setIsDriver(false);
$employee->getContractPeriods()->add($h35Period);
$employee->getContractPeriods()->add($h39Period);
return $employee;
}
private function buildEmployeeWithTransition(string $h39Start, string $h39End, string $forfaitStart): Employee
{
$employee = new Employee();