feat : ajout des suspensions et des jours de présence
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled

This commit is contained in:
2026-03-12 16:46:06 +01:00
parent e6819bc68a
commit 38f09914cb
25 changed files with 2969 additions and 21 deletions

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Service\Leave;
use App\Entity\Absence;
use App\Entity\ContractSuspension;
use App\Entity\Employee;
use App\Enum\LeaveRuleCode;
use App\Repository\AbsenceRepository;
@@ -29,6 +30,7 @@ final readonly class LeaveBalanceComputationService
private EmployeeContractPeriodRepository $periodRepository,
private EmployeeLeaveBalanceRepository $leaveBalanceRepository,
private PublicHolidayServiceInterface $publicHolidayService,
private SuspensionDaysCalculator $suspensionDaysCalculator,
) {}
/**
@@ -67,7 +69,18 @@ final readonly class LeaveBalanceComputationService
$fractionedDays = $this->resolveFractionedDays($employee, $ruleCode, $year);
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
$acquiredDays = $carryDays + (float) max(0, $this->countBusinessDays($from, $to) - self::FORFAIT_TARGET_WORKED_DAYS) + $fractionedDays;
$totalBusinessDays = $this->countBusinessDays($from, $to);
$baseAcquiredDays = (float) max(0, $totalBusinessDays - self::FORFAIT_TARGET_WORKED_DAYS);
$suspensions = $this->resolveSuspensionsForEmployeePeriod($employee, $from, $to);
$acquiredDays = $carryDays + $baseAcquiredDays + $fractionedDays;
if ([] !== $suspensions) {
$totalMonths = $this->countFractionalMonths($from, $to);
$suspendedMonths = $this->countSuspendedFractionalMonths($from, $to, $suspensions);
if ($totalMonths > 0) {
$ratio = max(0.0, ($totalMonths - $suspendedMonths) / $totalMonths);
$acquiredDays = $carryDays + $baseAcquiredDays * $ratio + $fractionedDays;
}
}
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
[$takenDays] = $this->computeTakenAbsences($absences, $effectiveFrom, $to, false, false);
$previousRemainingDays = max(0.0, $acquiredDays - $takenDays);
@@ -76,17 +89,20 @@ final readonly class LeaveBalanceComputationService
continue;
}
$suspensions = $this->resolveSuspensionsForEmployeePeriod($employee, $from, $to);
$generatedDays = $this->computeAccruedDays(
$this->resolveAnnualDays($employee),
$this->resolveDaysAccrualPerMonth($employee),
$effectiveFrom,
$to
$to,
$suspensions
);
$generatedSaturdays = $this->computeAccruedDays(
$this->resolveAnnualSaturdays($employee),
$this->resolveSaturdayAccrualPerMonth($employee),
$effectiveFrom,
$to
$to,
$suspensions
);
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
@@ -262,7 +278,8 @@ final readonly class LeaveBalanceComputationService
float $annualCap,
float $accrualPerMonth,
DateTimeImmutable $periodStart,
DateTimeImmutable $periodEnd
DateTimeImmutable $periodEnd,
array $suspensions = []
): float {
if ($accrualPerMonth <= 0.0 || $periodEnd < $periodStart) {
return 0.0;
@@ -280,6 +297,10 @@ final readonly class LeaveBalanceComputationService
}
$coveredDays = ((int) $monthEnd->diff($monthStart)->format('%a')) + 1;
if ([] !== $suspensions) {
$suspendedDays = $this->suspensionDaysCalculator->countSuspendedDaysInMonth($monthStart, $monthEnd, $suspensions);
$coveredDays = max(0, $coveredDays - $suspendedDays);
}
$daysInMonth = (int) $cursor->format('t');
$coveredMonths += $coveredDays / $daysInMonth;
@@ -404,6 +425,80 @@ final readonly class LeaveBalanceComputationService
return [$takenDays, $takenSaturdays];
}
private function countFractionalMonths(DateTimeImmutable $from, DateTimeImmutable $to): float
{
$from = $this->normalizeDate($from);
$to = $this->normalizeDate($to);
$months = 0.0;
$cursor = $from->modify('first day of this month')->setTime(0, 0);
while ($cursor <= $to) {
$monthStart = $cursor > $from ? $cursor : $from;
$monthEnd = $cursor->modify('last day of this month')->setTime(0, 0);
if ($monthEnd > $to) {
$monthEnd = $to;
}
$coveredDays = ((int) $monthEnd->diff($monthStart)->format('%a')) + 1;
$daysInMonth = (int) $cursor->format('t');
$months += $coveredDays / $daysInMonth;
$cursor = $cursor->modify('first day of next month');
}
return $months;
}
/**
* @param list<ContractSuspension> $suspensions
*/
private function countSuspendedFractionalMonths(DateTimeImmutable $from, DateTimeImmutable $to, array $suspensions): float
{
$from = $this->normalizeDate($from);
$to = $this->normalizeDate($to);
$months = 0.0;
$cursor = $from->modify('first day of this month')->setTime(0, 0);
while ($cursor <= $to) {
$monthStart = $cursor > $from ? $cursor : $from;
$monthEnd = $cursor->modify('last day of this month')->setTime(0, 0);
if ($monthEnd > $to) {
$monthEnd = $to;
}
$daysInMonth = (int) $cursor->format('t');
$suspendedDays = $this->suspensionDaysCalculator->countSuspendedDaysInMonth($monthStart, $monthEnd, $suspensions);
$months += $suspendedDays / $daysInMonth;
$cursor = $cursor->modify('first day of next month');
}
return $months;
}
/**
* @return list<ContractSuspension>
*/
private function resolveSuspensionsForEmployeePeriod(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array
{
$suspensions = [];
foreach ($employee->getContractPeriods() as $period) {
$periodStart = $period->getStartDate();
$periodEnd = $period->getEndDate();
if ($periodStart > $to) {
continue;
}
if ($periodEnd instanceof DateTimeImmutable && $periodEnd < $from) {
continue;
}
foreach ($period->getSuspensions() as $suspension) {
$suspensions[] = $suspension;
}
}
return $suspensions;
}
/**
* @return array{bool, bool}
*/

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Service\Leave;
use App\Entity\ContractSuspension;
use DateTimeImmutable;
final class SuspensionDaysCalculator
{
/**
* Count calendar days suspended within a month window [monthStart, monthEnd].
*
* @param list<ContractSuspension> $suspensions
*/
public function countSuspendedDaysInMonth(
DateTimeImmutable $monthStart,
DateTimeImmutable $monthEnd,
array $suspensions
): int {
$total = 0;
foreach ($suspensions as $suspension) {
$sStart = $suspension->getStartDate();
$sEnd = $suspension->getEndDate() ?? $monthEnd;
$overlapStart = $sStart > $monthStart ? $sStart : $monthStart;
$overlapEnd = $sEnd < $monthEnd ? $sEnd : $monthEnd;
if ($overlapStart > $overlapEnd) {
continue;
}
$total += ((int) $overlapEnd->diff($overlapStart)->format('%a')) + 1;
}
return $total;
}
/**
* Count business days (Mon-Fri, excl. public holidays) suspended within a period.
*
* @param list<ContractSuspension> $suspensions
* @param array<string, string> $publicHolidays map of Y-m-d => label
*/
public function countSuspendedBusinessDays(
DateTimeImmutable $periodStart,
DateTimeImmutable $periodEnd,
array $suspensions,
array $publicHolidays
): int {
$total = 0;
foreach ($suspensions as $suspension) {
$sStart = $suspension->getStartDate();
$sEnd = $suspension->getEndDate() ?? $periodEnd;
$overlapStart = $sStart > $periodStart ? $sStart : $periodStart;
$overlapEnd = $sEnd < $periodEnd ? $sEnd : $periodEnd;
if ($overlapStart > $overlapEnd) {
continue;
}
for ($cursor = $overlapStart; $cursor <= $overlapEnd; $cursor = $cursor->modify('+1 day')) {
$weekDay = (int) $cursor->format('N');
$dayKey = $cursor->format('Y-m-d');
if ($weekDay <= 5 && !isset($publicHolidays[$dayKey])) {
++$total;
}
}
}
return $total;
}
}