117 lines
3.5 KiB
PHP
117 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service\Leave;
|
|
|
|
use App\Entity\Employee;
|
|
use App\Repository\AbsenceRepository;
|
|
use DateTimeImmutable;
|
|
|
|
use function count;
|
|
|
|
/**
|
|
* Detects continuous MALADIE (sick leave) periods and computes
|
|
* the date ranges where reduced accrual applies (after the first month grace).
|
|
*/
|
|
final readonly class LongMaladieService
|
|
{
|
|
private const int MAX_GAP_DAYS = 3;
|
|
|
|
public function __construct(
|
|
private AbsenceRepository $absenceRepository,
|
|
) {}
|
|
|
|
/**
|
|
* Returns date ranges where the reduced maladie accrual rate applies.
|
|
* For continuous maladie periods > 1 month, the first month is excluded (grace period).
|
|
*
|
|
* @return list<array{start: DateTimeImmutable, end: DateTimeImmutable}>
|
|
*/
|
|
public function findReducedRatePeriods(
|
|
Employee $employee,
|
|
DateTimeImmutable $from,
|
|
DateTimeImmutable $to
|
|
): array {
|
|
// Look back 13 months to catch maladie that started before the exercise period
|
|
$extendedFrom = $from->modify('-13 months');
|
|
$dates = $this->absenceRepository->findMaladieDatesByEmployee($employee, $extendedFrom, $to);
|
|
if ([] === $dates) {
|
|
return [];
|
|
}
|
|
|
|
$periods = $this->consolidateIntoPeriods($dates);
|
|
|
|
return $this->applyFirstMonthGrace($periods);
|
|
}
|
|
|
|
/**
|
|
* Count calendar days in [monthStart, monthEnd] that fall within reduced maladie periods.
|
|
*
|
|
* @param list<array{start: DateTimeImmutable, end: DateTimeImmutable}> $reducedPeriods
|
|
*/
|
|
public function countReducedDaysInMonth(
|
|
DateTimeImmutable $monthStart,
|
|
DateTimeImmutable $monthEnd,
|
|
array $reducedPeriods
|
|
): int {
|
|
$total = 0;
|
|
foreach ($reducedPeriods as $period) {
|
|
$overlapStart = $period['start'] > $monthStart ? $period['start'] : $monthStart;
|
|
$overlapEnd = $period['end'] < $monthEnd ? $period['end'] : $monthEnd;
|
|
|
|
if ($overlapStart > $overlapEnd) {
|
|
continue;
|
|
}
|
|
|
|
$total += ((int) $overlapEnd->diff($overlapStart)->format('%a')) + 1;
|
|
}
|
|
|
|
return $total;
|
|
}
|
|
|
|
/**
|
|
* @param list<DateTimeImmutable> $dates sorted chronologically
|
|
*
|
|
* @return list<array{start: DateTimeImmutable, end: DateTimeImmutable}>
|
|
*/
|
|
private function consolidateIntoPeriods(array $dates): array
|
|
{
|
|
$periods = [];
|
|
$start = $dates[0];
|
|
$prev = $start;
|
|
|
|
for ($i = 1, $count = count($dates); $i < $count; ++$i) {
|
|
$current = $dates[$i];
|
|
$gap = (int) $prev->diff($current)->format('%a');
|
|
if ($gap > self::MAX_GAP_DAYS) {
|
|
$periods[] = ['start' => $start, 'end' => $prev];
|
|
$start = $current;
|
|
}
|
|
$prev = $current;
|
|
}
|
|
$periods[] = ['start' => $start, 'end' => $prev];
|
|
|
|
return $periods;
|
|
}
|
|
|
|
/**
|
|
* @param list<array{start: DateTimeImmutable, end: DateTimeImmutable}> $periods
|
|
*
|
|
* @return list<array{start: DateTimeImmutable, end: DateTimeImmutable}>
|
|
*/
|
|
private function applyFirstMonthGrace(array $periods): array
|
|
{
|
|
$result = [];
|
|
foreach ($periods as $period) {
|
|
$gracedStart = $period['start']->modify('+1 month');
|
|
if ($gracedStart > $period['end']) {
|
|
continue;
|
|
}
|
|
$result[] = ['start' => $gracedStart, 'end' => $period['end']];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|