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:
2026-05-19 11:07:38 +02:00
parent 9efe0e81a0
commit 5a2a43bf51
6 changed files with 131 additions and 63 deletions

View File

@@ -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.
*/