refactor(leave) : address Task 3 review (helper, dead param, phase nature, regression test)
- Extract private helper `exerciseYearForDate(date, isForfait)` to dedupe the date->leave-exercise-year expression duplicated across `clampYearToPhase` and `resolveFirstComputationYear` (4 copies collapsed into 1 helper + 4 call sites). - Remove the unused `ContractPhase $phase` parameter from `resolveLeavePeriodBounds`: the body never reads it (the phase cap is applied later by `resolvePeriodBounds`). - Add `ContractNature $contractNature` to `ContractPhase` DTO, populated from the first period of the group by `EmployeeContractPhaseResolver`. Drop the `resolveNatureForPhase` lookup in `EmployeeLeaveSummaryProvider` in favor of `$phase->contractNature`. Expose `contractNature` in `Employee::getContractPhases()` array shape for frontend use. - Fix regression for terminated employees calling `computeYearSummary` without an explicit phase (LeaveRecapRowBuilder, DumpVerificationSnapshotCommand). Before the refactor the period bounds, accrual end and taken end were NOT capped at the contract end for terminated employees, because `Employee::getCurrentContractEndDate()` returns null when no period covers "today". The new fallback phase (`isCurrent=false`, real `endDate`) was silently capping `to`. Add an internal `applyPhaseEndCap` flag, true when phase is explicit, false for legacy callers, threaded through `resolvePeriodBounds`, `resolveAccrualCalculationEndDate` and `resolveTakenCalculationEndDate`. - Add regression test `testTerminatedEmployeeWithoutExplicitPhaseSkipsPhaseEndCap` proving that legacy callers keep the natural exercise upper bound while explicit phase callers get the cap. - Add `contractNature` assertion in `EmployeeContractPhaseResolverTest`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ final class EmployeeContractPhaseResolverTest extends TestCase
|
||||
self::assertSame(ContractType::H39, $phases[0]->contractType);
|
||||
self::assertTrue($phases[0]->isCurrent);
|
||||
self::assertNull($phases[0]->endDate);
|
||||
self::assertSame(ContractNature::CDI, $phases[0]->contractNature);
|
||||
}
|
||||
|
||||
public function testThreeConsecutivePeriodsSameSignatureCollapseIntoSinglePhase(): void
|
||||
|
||||
@@ -241,10 +241,72 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase
|
||||
self::assertSame(2026, $year);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Regression: terminated-employee path through `computeYearSummary` without
|
||||
// an explicit phase (legacy callers: LeaveRecapRowBuilder,
|
||||
// DumpVerificationSnapshotCommand). Before the phase-aware refactor, the
|
||||
// period bounds were NOT capped at the contract end for terminated
|
||||
// employees (because Employee::getCurrentContractEndDate() returns null
|
||||
// when no period covers "today"). The new code resolves a fallback phase
|
||||
// whose `isCurrent` is false, which would otherwise cap `to` at the phase
|
||||
// end — a behavior change for legacy callers. The flag `applyPhaseEndCap`
|
||||
// toggles this cap so legacy callers get the pre-refactor behavior.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function testTerminatedEmployeeWithoutExplicitPhaseSkipsPhaseEndCap(): void
|
||||
{
|
||||
// Terminated employee: H39 phase ending 2024-12-31 (well in the past).
|
||||
$employee = $this->buildTerminatedEmployee('2020-06-01', '2024-12-31');
|
||||
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
||||
self::assertCount(1, $phases);
|
||||
$phase = $phases[0];
|
||||
self::assertFalse($phase->isCurrent, 'Sanity: terminated phase must not be flagged as current.');
|
||||
|
||||
$provider = $this->buildProvider([]);
|
||||
|
||||
// applyPhaseEndCap=false → mimics legacy callers (no explicit phase):
|
||||
// the upper bound MUST stay at the natural leave-year end (May 31).
|
||||
[$fromLegacy, $toLegacy] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2025, $phase, false);
|
||||
self::assertSame('2024-06-01', $fromLegacy->format('Y-m-d'));
|
||||
self::assertSame('2025-05-31', $toLegacy->format('Y-m-d'));
|
||||
|
||||
// applyPhaseEndCap=true → explicit-phase callers get the cap at phase end.
|
||||
[$fromCap, $toCap] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2025, $phase, true);
|
||||
self::assertSame('2024-06-01', $fromCap->format('Y-m-d'));
|
||||
self::assertSame('2024-12-31', $toCap->format('Y-m-d'));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test harness helpers.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a terminated-employee fixture: a single H39 period ending before today.
|
||||
*/
|
||||
private function buildTerminatedEmployee(string $start, string $end): Employee
|
||||
{
|
||||
$employee = new Employee();
|
||||
$this->setEntityId($employee, 2);
|
||||
|
||||
$contract = new Contract();
|
||||
$contract->setName('39H');
|
||||
$contract->setTrackingMode(TrackingMode::TIME->value);
|
||||
$contract->setWeeklyHours(39);
|
||||
|
||||
$period = new EmployeeContractPeriod();
|
||||
$this->setEntityId($period, 10);
|
||||
$period->setEmployee($employee);
|
||||
$period->setContract($contract);
|
||||
$period->setStartDate(new DateTimeImmutable($start));
|
||||
$period->setEndDate(new DateTimeImmutable($end));
|
||||
$period->setContractNature(ContractNature::CDI);
|
||||
$period->setIsDriver(false);
|
||||
|
||||
$employee->getContractPeriods()->add($period);
|
||||
|
||||
return $employee;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a two-period employee transitioning from H39 to FORFAIT.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user