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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user