|
|
|
|
@@ -173,6 +173,12 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
*/
|
|
|
|
|
public function computeYearSummary(Employee $employee, int $targetYear, float $paidLeaveDays = 0.0, ?DateTimeImmutable $asOfDate = null, ?ContractPhase $phase = null): ?array
|
|
|
|
|
{
|
|
|
|
|
// Track whether a phase was provided explicitly. When the caller supplies $phase,
|
|
|
|
|
// we apply the phase-end cap on period bounds. When we fall back to resolveCurrentPhase
|
|
|
|
|
// (legacy callers without phase awareness, e.g. LeaveRecapRowBuilder), we preserve
|
|
|
|
|
// the pre-phase-cap behavior to avoid changing observable results for terminated
|
|
|
|
|
// employees (the resolved fallback phase would otherwise unduly cap `to`).
|
|
|
|
|
$applyPhaseEndCap = null !== $phase;
|
|
|
|
|
$phase ??= $this->resolveCurrentPhase($employee);
|
|
|
|
|
if (null === $phase) {
|
|
|
|
|
return null;
|
|
|
|
|
@@ -188,7 +194,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
$targetSummary = null;
|
|
|
|
|
|
|
|
|
|
for ($year = $firstYear; $year <= $targetYear; ++$year) {
|
|
|
|
|
[$from, $to] = $this->resolvePeriodBounds($employee, $year, $phase);
|
|
|
|
|
[$from, $to] = $this->resolvePeriodBounds($employee, $year, $phase, $applyPhaseEndCap);
|
|
|
|
|
$leavePolicy = $this->resolveLeavePolicy($employee, $phase, $from, $to);
|
|
|
|
|
if (null === $leavePolicy) {
|
|
|
|
|
if ($year === $targetYear) {
|
|
|
|
|
@@ -233,8 +239,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$effectiveAsOfDate = ($year === $targetYear) ? $asOfDate : null;
|
|
|
|
|
$accrualCalculationEnd = $this->resolveAccrualCalculationEndDate($leavePolicy['ruleCode'], $year, $to, $employee, $phase, $effectiveAsOfDate);
|
|
|
|
|
$takenCalculationEnd = $this->resolveTakenCalculationEndDate($to, $employee, $phase, $effectiveAsOfDate);
|
|
|
|
|
$accrualCalculationEnd = $this->resolveAccrualCalculationEndDate($leavePolicy['ruleCode'], $year, $to, $employee, $phase, $effectiveAsOfDate, $applyPhaseEndCap);
|
|
|
|
|
$takenCalculationEnd = $this->resolveTakenCalculationEndDate($to, $employee, $phase, $effectiveAsOfDate, $applyPhaseEndCap);
|
|
|
|
|
$suspensions = $this->suspensionDaysCalculator->applyFirstMonthGrace(
|
|
|
|
|
$this->resolveSuspensionsForPeriod($employee, $effectiveFrom, $to)
|
|
|
|
|
);
|
|
|
|
|
@@ -474,21 +480,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
|
|
|
|
|
private function clampYearToPhase(int $year, ContractPhase $phase, bool $isForfait): int
|
|
|
|
|
{
|
|
|
|
|
$firstYear = $isForfait
|
|
|
|
|
? (int) $phase->startDate->format('Y')
|
|
|
|
|
: ((int) $phase->startDate->format('n') >= 6
|
|
|
|
|
? (int) $phase->startDate->format('Y') + 1
|
|
|
|
|
: (int) $phase->startDate->format('Y'));
|
|
|
|
|
|
|
|
|
|
$endDate = $phase->endDate;
|
|
|
|
|
$lastYear = null;
|
|
|
|
|
if ($endDate instanceof DateTimeImmutable) {
|
|
|
|
|
$lastYear = $isForfait
|
|
|
|
|
? (int) $endDate->format('Y')
|
|
|
|
|
: ((int) $endDate->format('n') >= 6
|
|
|
|
|
? (int) $endDate->format('Y') + 1
|
|
|
|
|
: (int) $endDate->format('Y'));
|
|
|
|
|
}
|
|
|
|
|
$firstYear = $this->exerciseYearForDate($phase->startDate, $isForfait);
|
|
|
|
|
$lastYear = $phase->endDate instanceof DateTimeImmutable
|
|
|
|
|
? $this->exerciseYearForDate($phase->endDate, $isForfait)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if ($year < $firstYear) {
|
|
|
|
|
return $firstYear;
|
|
|
|
|
@@ -500,6 +495,22 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
return $year;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map a date to the leave exercise year it belongs to.
|
|
|
|
|
* - Forfait: exercise = calendar year.
|
|
|
|
|
* - Non-forfait: exercise N runs from June (N-1) to May (N); dates in June-December
|
|
|
|
|
* map to N+1, January-May map to N.
|
|
|
|
|
*/
|
|
|
|
|
private function exerciseYearForDate(DateTimeImmutable $date, bool $isForfait): int
|
|
|
|
|
{
|
|
|
|
|
$year = (int) $date->format('Y');
|
|
|
|
|
if ($isForfait) {
|
|
|
|
|
return $year;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (int) $date->format('n') >= 6 ? $year + 1 : $year;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function resolveTargetPhase(Employee $employee): ContractPhase
|
|
|
|
|
{
|
|
|
|
|
$raw = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
|
|
|
|
|
@@ -622,16 +633,18 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
DateTimeImmutable $periodEnd,
|
|
|
|
|
Employee $employee,
|
|
|
|
|
ContractPhase $phase,
|
|
|
|
|
?DateTimeImmutable $asOfDate = null
|
|
|
|
|
?DateTimeImmutable $asOfDate = null,
|
|
|
|
|
bool $applyPhaseEndCap = true
|
|
|
|
|
): ?DateTimeImmutable {
|
|
|
|
|
$reference = $asOfDate ?? new DateTimeImmutable('today');
|
|
|
|
|
$currentYear = LeaveRuleCode::FORFAIT_218->value === $ruleCode
|
|
|
|
|
? (int) $reference->format('Y')
|
|
|
|
|
: $this->resolveCurrentLeaveYear($reference);
|
|
|
|
|
|
|
|
|
|
// When viewing a closed phase, treat its end date as the reference cutoff:
|
|
|
|
|
// When viewing a closed phase explicitly, treat its end date as the reference cutoff:
|
|
|
|
|
// accrual is bounded to the phase end, never running to "today".
|
|
|
|
|
if (!$phase->isCurrent && null !== $phase->endDate) {
|
|
|
|
|
// Legacy callers (no explicit phase) skip this cap to preserve pre-phase behavior.
|
|
|
|
|
if ($applyPhaseEndCap && !$phase->isCurrent && null !== $phase->endDate) {
|
|
|
|
|
$end = $phase->endDate < $periodEnd ? $phase->endDate : $periodEnd;
|
|
|
|
|
} elseif ($year < $currentYear) {
|
|
|
|
|
$end = $periodEnd;
|
|
|
|
|
@@ -648,7 +661,9 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
|
|
|
|
|
// Cap at contract end date if the employee has left (only meaningful when
|
|
|
|
|
// viewing the current phase; closed phases are already capped above).
|
|
|
|
|
if ($phase->isCurrent) {
|
|
|
|
|
// Legacy callers (no explicit phase) always evaluate this branch to mimic
|
|
|
|
|
// the pre-phase behavior, which relied on getCurrentContractEndDate().
|
|
|
|
|
if (!$applyPhaseEndCap || $phase->isCurrent) {
|
|
|
|
|
$contractEndRaw = $employee->getCurrentContractEndDate();
|
|
|
|
|
if (null !== $end && null !== $contractEndRaw && '' !== trim($contractEndRaw)) {
|
|
|
|
|
$contractEnd = $this->parseYmdDate($contractEndRaw);
|
|
|
|
|
@@ -665,7 +680,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
DateTimeImmutable $periodEnd,
|
|
|
|
|
Employee $employee,
|
|
|
|
|
ContractPhase $phase,
|
|
|
|
|
?DateTimeImmutable $asOfDate = null
|
|
|
|
|
?DateTimeImmutable $asOfDate = null,
|
|
|
|
|
bool $applyPhaseEndCap = true
|
|
|
|
|
): ?DateTimeImmutable {
|
|
|
|
|
$end = $periodEnd;
|
|
|
|
|
|
|
|
|
|
@@ -674,11 +690,14 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Closed phase: cap taken-absence accounting at the phase end.
|
|
|
|
|
if (!$phase->isCurrent && null !== $phase->endDate && $phase->endDate < $end) {
|
|
|
|
|
// Skip for legacy callers (no explicit phase) to preserve pre-phase behavior.
|
|
|
|
|
if ($applyPhaseEndCap && !$phase->isCurrent && null !== $phase->endDate && $phase->endDate < $end) {
|
|
|
|
|
$end = $phase->endDate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($phase->isCurrent) {
|
|
|
|
|
// Legacy callers (no explicit phase) always use the live contract end date,
|
|
|
|
|
// mirroring the pre-phase implementation.
|
|
|
|
|
if (!$applyPhaseEndCap || $phase->isCurrent) {
|
|
|
|
|
$contractEndRaw = $employee->getCurrentContractEndDate();
|
|
|
|
|
if (null !== $end && null !== $contractEndRaw && '' !== trim($contractEndRaw)) {
|
|
|
|
|
$contractEnd = $this->parseYmdDate($contractEndRaw);
|
|
|
|
|
@@ -733,8 +752,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolve nature from the period defining the phase (use the phase's first period).
|
|
|
|
|
$nature = $this->resolveNatureForPhase($employee, $phase);
|
|
|
|
|
// Resolve nature directly from the phase DTO (populated by EmployeeContractPhaseResolver).
|
|
|
|
|
$nature = $phase->contractNature;
|
|
|
|
|
if (ContractNature::CDI !== $nature && ContractNature::CDD !== $nature) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
@@ -934,19 +953,21 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
/**
|
|
|
|
|
* @return array{DateTimeImmutable, DateTimeImmutable}
|
|
|
|
|
*/
|
|
|
|
|
private function resolvePeriodBounds(Employee $employee, int $year, ContractPhase $phase): array
|
|
|
|
|
private function resolvePeriodBounds(Employee $employee, int $year, ContractPhase $phase, bool $applyPhaseEndCap = true): array
|
|
|
|
|
{
|
|
|
|
|
if (ContractType::FORFAIT === $phase->contractType) {
|
|
|
|
|
[$from, $to] = $this->resolveForfaitYearBounds($employee, $year, $phase);
|
|
|
|
|
} else {
|
|
|
|
|
[$from, $to] = $this->resolveLeavePeriodBounds($year, $phase);
|
|
|
|
|
[$from, $to] = $this->resolveLeavePeriodBounds($year);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cap to the phase boundaries (applies to both modes).
|
|
|
|
|
// The end cap is skipped when the phase was not explicitly provided (legacy callers),
|
|
|
|
|
// to preserve pre-phase-cap behavior for terminated employees.
|
|
|
|
|
if ($phase->startDate > $from) {
|
|
|
|
|
$from = $phase->startDate;
|
|
|
|
|
}
|
|
|
|
|
if (null !== $phase->endDate && $phase->endDate < $to) {
|
|
|
|
|
if ($applyPhaseEndCap && null !== $phase->endDate && $phase->endDate < $to) {
|
|
|
|
|
$to = $phase->endDate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -956,7 +977,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
/**
|
|
|
|
|
* @return array{DateTimeImmutable, DateTimeImmutable}
|
|
|
|
|
*/
|
|
|
|
|
private function resolveLeavePeriodBounds(int $leaveYear, ContractPhase $phase): array
|
|
|
|
|
private function resolveLeavePeriodBounds(int $leaveYear): array
|
|
|
|
|
{
|
|
|
|
|
// Exercice CP "2026" = du 1er juin 2025 au 31 mai 2026.
|
|
|
|
|
$from = new DateTimeImmutable(sprintf('%d-06-01 00:00:00', $leaveYear - 1));
|
|
|
|
|
@@ -1020,11 +1041,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
: $this->resolveCurrentLeaveYear(new DateTimeImmutable('today'));
|
|
|
|
|
|
|
|
|
|
// Do not go before the exercice containing $phase->startDate.
|
|
|
|
|
$phaseFirstYear = $isForfait
|
|
|
|
|
? (int) $phase->startDate->format('Y')
|
|
|
|
|
: ((int) $phase->startDate->format('n') >= 6
|
|
|
|
|
? (int) $phase->startDate->format('Y') + 1
|
|
|
|
|
: (int) $phase->startDate->format('Y'));
|
|
|
|
|
$phaseFirstYear = $this->exerciseYearForDate($phase->startDate, $isForfait);
|
|
|
|
|
|
|
|
|
|
$history = $employee->getContractHistory();
|
|
|
|
|
if ([] === $history) {
|
|
|
|
|
@@ -1049,11 +1066,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
return max($phaseFirstYear, $candidate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$firstYear = $isForfait
|
|
|
|
|
? (int) $oldestStartDate->format('Y')
|
|
|
|
|
: ((int) $oldestStartDate->format('n') >= 6
|
|
|
|
|
? (int) $oldestStartDate->format('Y') + 1
|
|
|
|
|
: (int) $oldestStartDate->format('Y'));
|
|
|
|
|
$firstYear = $this->exerciseYearForDate($oldestStartDate, $isForfait);
|
|
|
|
|
|
|
|
|
|
$oldestBalanceYear = $this->leaveBalanceRepository->findEarliestYearForEmployee($employee);
|
|
|
|
|
if (null !== $oldestBalanceYear && $oldestBalanceYear < $firstYear) {
|
|
|
|
|
@@ -1063,19 +1076,6 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|
|
|
|
return max($phaseFirstYear, $firstYear);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function resolveNatureForPhase(Employee $employee, ContractPhase $phase): ?ContractNature
|
|
|
|
|
{
|
|
|
|
|
// Find the period at the start of the phase to determine its nature.
|
|
|
|
|
foreach ($employee->getContractPeriods() as $period) {
|
|
|
|
|
if ((int) $period->getId() === $phase->id) {
|
|
|
|
|
return $period->getContractNatureEnum();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: nature of the current period (legacy behavior).
|
|
|
|
|
return ContractNature::tryFrom($employee->getCurrentContractNature());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function parseYmdDate(string $value): ?DateTimeImmutable
|
|
|
|
|
{
|
|
|
|
|
$date = DateTimeImmutable::createFromFormat('!Y-m-d', trim($value));
|
|
|
|
|
|