- 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>