Ajout des notification + page employé (#6)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #6 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #6.
This commit is contained in:
396
src/Service/Leave/LeaveBalanceComputationService.php
Normal file
396
src/Service/Leave/LeaveBalanceComputationService.php
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Leave;
|
||||
|
||||
use App\Entity\Absence;
|
||||
use App\Entity\Employee;
|
||||
use App\Enum\LeaveRuleCode;
|
||||
use App\Repository\AbsenceRepository;
|
||||
use App\Repository\EmployeeContractPeriodRepository;
|
||||
use App\Repository\EmployeeLeaveBalanceRepository;
|
||||
use App\Service\PublicHolidayServiceInterface;
|
||||
use DateTimeImmutable;
|
||||
use Throwable;
|
||||
|
||||
final readonly class LeaveBalanceComputationService
|
||||
{
|
||||
private const int FORFAIT_TARGET_WORKED_DAYS = 218;
|
||||
private const float STANDARD_ANNUAL_DAYS = 25.0;
|
||||
private const float STANDARD_ANNUAL_SATURDAYS = 5.0;
|
||||
private const float STANDARD_ACCRUAL_PER_MONTH = self::STANDARD_ANNUAL_DAYS / 12.0;
|
||||
private const float STANDARD_SATURDAY_ACCRUAL_PER_MONTH = self::STANDARD_ANNUAL_SATURDAYS / 12.0;
|
||||
private const float FOUR_HOUR_ANNUAL_DAYS = 10.0;
|
||||
private const float FOUR_HOUR_ACCRUAL_PER_MONTH = 0.83;
|
||||
|
||||
public function __construct(
|
||||
private AbsenceRepository $absenceRepository,
|
||||
private EmployeeContractPeriodRepository $periodRepository,
|
||||
private EmployeeLeaveBalanceRepository $leaveBalanceRepository,
|
||||
private PublicHolidayServiceInterface $publicHolidayService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{float, float}
|
||||
*/
|
||||
public function computeDynamicClosingForYear(Employee $employee, LeaveRuleCode $ruleCode, int $targetYear): array
|
||||
{
|
||||
$firstYear = $this->resolveFirstComputationYear($employee, $ruleCode, $targetYear);
|
||||
if ($targetYear < $firstYear) {
|
||||
return [0.0, 0.0];
|
||||
}
|
||||
|
||||
$previousRemainingDays = 0.0;
|
||||
$previousRemainingSaturdays = 0.0;
|
||||
|
||||
for ($year = $firstYear; $year <= $targetYear; ++$year) {
|
||||
[$from, $to] = $this->resolvePeriodBounds($ruleCode, $year);
|
||||
|
||||
$carryDays = 0.0;
|
||||
$carrySaturdays = 0.0;
|
||||
if ($year > $firstYear) {
|
||||
[$previousFrom, $previousTo] = $this->resolvePeriodBounds($ruleCode, $year - 1);
|
||||
$hasSettlementOnPreviousYear = $this->periodRepository->hasPaidLeaveSettledClosureBetween($employee, $previousFrom, $previousTo);
|
||||
if (!$hasSettlementOnPreviousYear) {
|
||||
$carryDays = $previousRemainingDays;
|
||||
$carrySaturdays = LeaveRuleCode::CDI_CDD_NON_FORFAIT === $ruleCode ? $previousRemainingSaturdays : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
$effectiveFrom = $this->resolveEffectivePeriodStart($employee, $from, $to);
|
||||
if ($effectiveFrom > $from) {
|
||||
$carryDays = 0.0;
|
||||
$carrySaturdays = 0.0;
|
||||
}
|
||||
|
||||
$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;
|
||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
|
||||
[$takenDays] = $this->computeTakenAbsences($absences, $effectiveFrom, $to, false, false);
|
||||
$previousRemainingDays = max(0.0, $acquiredDays - $takenDays);
|
||||
$previousRemainingSaturdays = 0.0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$generatedDays = $this->computeAccruedDays(
|
||||
$this->resolveAnnualDays($employee),
|
||||
$this->resolveDaysAccrualPerMonth($employee),
|
||||
$effectiveFrom,
|
||||
$to
|
||||
);
|
||||
$generatedSaturdays = $this->computeAccruedDays(
|
||||
$this->resolveAnnualSaturdays($employee),
|
||||
$this->resolveSaturdayAccrualPerMonth($employee),
|
||||
$effectiveFrom,
|
||||
$to
|
||||
);
|
||||
|
||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
|
||||
[$takenDays, $takenSaturdays] = $this->computeTakenAbsences($absences, $effectiveFrom, $to, true, true);
|
||||
|
||||
$acquiredWithFractioned = $carryDays + $fractionedDays;
|
||||
$takenFromAcquired = min(max(0.0, $acquiredWithFractioned), $takenDays);
|
||||
$remainingAcquired = $acquiredWithFractioned - $takenFromAcquired;
|
||||
$remainingToImpute = max(0.0, $takenDays - $takenFromAcquired);
|
||||
$remainingGenerated = $generatedDays - $remainingToImpute;
|
||||
|
||||
$takenFromAcquiredSaturdays = min(max(0.0, $carrySaturdays), $takenSaturdays);
|
||||
$remainingAcquiredSaturdays = $carrySaturdays - $takenFromAcquiredSaturdays;
|
||||
$remainingSaturdaysToImpute = max(0.0, $takenSaturdays - $takenFromAcquiredSaturdays);
|
||||
$remainingGeneratedSaturdays = $generatedSaturdays - $remainingSaturdaysToImpute;
|
||||
|
||||
$previousRemainingDays = $remainingAcquired + $remainingGenerated;
|
||||
$previousRemainingSaturdays = $remainingAcquiredSaturdays + $remainingGeneratedSaturdays;
|
||||
}
|
||||
|
||||
return [$previousRemainingDays, $previousRemainingSaturdays];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{DateTimeImmutable, DateTimeImmutable}
|
||||
*/
|
||||
public function resolvePeriodBounds(LeaveRuleCode $ruleCode, int $year): array
|
||||
{
|
||||
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
|
||||
return [
|
||||
new DateTimeImmutable(sprintf('%d-01-01', $year)),
|
||||
new DateTimeImmutable(sprintf('%d-12-31', $year)),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
new DateTimeImmutable(sprintf('%d-06-01', $year - 1)),
|
||||
new DateTimeImmutable(sprintf('%d-05-31', $year)),
|
||||
];
|
||||
}
|
||||
|
||||
public function hasPaidLeaveSettledClosureBetween(
|
||||
Employee $employee,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to
|
||||
): bool {
|
||||
return $this->periodRepository->hasPaidLeaveSettledClosureBetween($employee, $from, $to);
|
||||
}
|
||||
|
||||
private function resolveFirstComputationYear(Employee $employee, LeaveRuleCode $ruleCode, int $fallbackYear): int
|
||||
{
|
||||
$history = $employee->getContractHistory();
|
||||
if ([] === $history) {
|
||||
return $fallbackYear;
|
||||
}
|
||||
|
||||
$oldestStartDate = null;
|
||||
foreach ($history as $item) {
|
||||
$start = DateTimeImmutable::createFromFormat('Y-m-d', $item->startDate);
|
||||
if (!$start) {
|
||||
continue;
|
||||
}
|
||||
if (null === $oldestStartDate || $start < $oldestStartDate) {
|
||||
$oldestStartDate = $start;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $oldestStartDate) {
|
||||
return $fallbackYear;
|
||||
}
|
||||
|
||||
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
|
||||
return (int) $oldestStartDate->format('Y');
|
||||
}
|
||||
|
||||
$startYear = (int) $oldestStartDate->format('Y');
|
||||
$startMonth = (int) $oldestStartDate->format('n');
|
||||
|
||||
return $startMonth >= 6 ? $startYear + 1 : $startYear;
|
||||
}
|
||||
|
||||
private function resolveEffectivePeriodStart(
|
||||
Employee $employee,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to
|
||||
): DateTimeImmutable {
|
||||
$latestSettledClosure = $this->periodRepository->findLatestPaidLeaveSettledClosureDateBetween($employee, $from, $to);
|
||||
$start = $from;
|
||||
if (null !== $latestSettledClosure) {
|
||||
$nextDay = $latestSettledClosure->modify('+1 day');
|
||||
if ($nextDay > $start) {
|
||||
$start = $nextDay;
|
||||
}
|
||||
}
|
||||
|
||||
$earliestContractStart = $this->resolveEarliestContractStartWithinRange($employee, $from, $to);
|
||||
if (null !== $earliestContractStart && $earliestContractStart > $start) {
|
||||
$start = $earliestContractStart;
|
||||
}
|
||||
|
||||
return $start;
|
||||
}
|
||||
|
||||
private function resolveEarliestContractStartWithinRange(
|
||||
Employee $employee,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to
|
||||
): ?DateTimeImmutable {
|
||||
$earliest = null;
|
||||
foreach ($employee->getContractHistory() as $period) {
|
||||
$start = DateTimeImmutable::createFromFormat('Y-m-d', $period->startDate);
|
||||
if (!$start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$end = null;
|
||||
if (null !== $period->endDate && '' !== trim($period->endDate)) {
|
||||
$end = DateTimeImmutable::createFromFormat('Y-m-d', $period->endDate);
|
||||
}
|
||||
|
||||
if ($start > $to) {
|
||||
continue;
|
||||
}
|
||||
if ($end instanceof DateTimeImmutable && $end < $from) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidate = $start < $from ? $from : $start;
|
||||
if (null === $earliest || $candidate < $earliest) {
|
||||
$earliest = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $earliest;
|
||||
}
|
||||
|
||||
private function resolveFractionedDays(Employee $employee, LeaveRuleCode $ruleCode, int $year): float
|
||||
{
|
||||
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year);
|
||||
|
||||
return null !== $balance ? $balance->getFractionedDays() : 0.0;
|
||||
}
|
||||
|
||||
private function resolveAnnualDays(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
? self::FOUR_HOUR_ANNUAL_DAYS
|
||||
: self::STANDARD_ANNUAL_DAYS;
|
||||
}
|
||||
|
||||
private function resolveAnnualSaturdays(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
? 0.0
|
||||
: self::STANDARD_ANNUAL_SATURDAYS;
|
||||
}
|
||||
|
||||
private function resolveDaysAccrualPerMonth(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
? self::FOUR_HOUR_ACCRUAL_PER_MONTH
|
||||
: self::STANDARD_ACCRUAL_PER_MONTH;
|
||||
}
|
||||
|
||||
private function resolveSaturdayAccrualPerMonth(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
? 0.0
|
||||
: self::STANDARD_SATURDAY_ACCRUAL_PER_MONTH;
|
||||
}
|
||||
|
||||
private function computeAccruedDays(
|
||||
float $annualCap,
|
||||
float $accrualPerMonth,
|
||||
DateTimeImmutable $periodStart,
|
||||
DateTimeImmutable $periodEnd
|
||||
): float {
|
||||
if ($accrualPerMonth <= 0.0 || $periodEnd < $periodStart) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$monthsElapsed = ((int) $periodEnd->format('Y') - (int) $periodStart->format('Y')) * 12
|
||||
+ ((int) $periodEnd->format('n') - (int) $periodStart->format('n'))
|
||||
+ 1;
|
||||
|
||||
return min($annualCap, $monthsElapsed * $accrualPerMonth);
|
||||
}
|
||||
|
||||
private function countBusinessDays(DateTimeImmutable $from, DateTimeImmutable $to): int
|
||||
{
|
||||
$publicHolidays = $this->buildPublicHolidayMap($from, $to);
|
||||
$count = 0;
|
||||
for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) {
|
||||
$weekDay = (int) $cursor->format('N');
|
||||
$dayKey = $cursor->format('Y-m-d');
|
||||
if ($weekDay <= 5 && !isset($publicHolidays[$dayKey])) {
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildPublicHolidayMap(DateTimeImmutable $from, DateTimeImmutable $to): array
|
||||
{
|
||||
$map = [];
|
||||
$startYear = (int) $from->format('Y');
|
||||
$endYear = (int) $to->format('Y');
|
||||
|
||||
try {
|
||||
for ($year = $startYear; $year <= $endYear; ++$year) {
|
||||
$holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
|
||||
foreach ($holidays as $date => $label) {
|
||||
$map[(string) $date] = (string) $label;
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Absence> $absences
|
||||
*
|
||||
* @return array{float, float}
|
||||
*/
|
||||
private function computeTakenAbsences(
|
||||
array $absences,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to,
|
||||
bool $countOnlyCp,
|
||||
bool $splitSaturdays
|
||||
): array {
|
||||
$takenDays = 0.0;
|
||||
$takenSaturdays = 0.0;
|
||||
|
||||
foreach ($absences as $absence) {
|
||||
if ($countOnlyCp) {
|
||||
$typeCode = strtoupper((string) $absence->getType()?->getCode());
|
||||
if ('C' !== $typeCode) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $absence->getType()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = DateTimeImmutable::createFromInterface($absence->getStartDate());
|
||||
$end = DateTimeImmutable::createFromInterface($absence->getEndDate());
|
||||
$rangeStart = $start < $from ? $from : $start;
|
||||
$rangeEnd = $end > $to ? $to : $end;
|
||||
if ($rangeEnd < $rangeStart) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($cursor = $rangeStart; $cursor <= $rangeEnd; $cursor = $cursor->modify('+1 day')) {
|
||||
[$am, $pm] = $this->resolveSegmentsForDate($absence, $cursor->format('Y-m-d'));
|
||||
$dayAmount = ($am ? 0.5 : 0.0) + ($pm ? 0.5 : 0.0);
|
||||
if ($dayAmount <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isSaturday = $splitSaturdays && '6' === $cursor->format('N');
|
||||
if ($isSaturday) {
|
||||
$takenSaturdays += $dayAmount;
|
||||
} else {
|
||||
$takenDays += $dayAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$takenDays, $takenSaturdays];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{bool, bool}
|
||||
*/
|
||||
private function resolveSegmentsForDate(Absence $absence, string $date): array
|
||||
{
|
||||
$startYmd = DateTimeImmutable::createFromInterface($absence->getStartDate())->format('Y-m-d');
|
||||
$endYmd = DateTimeImmutable::createFromInterface($absence->getEndDate())->format('Y-m-d');
|
||||
$startHalf = $absence->getStartHalf()->value;
|
||||
$endHalf = $absence->getEndHalf()->value;
|
||||
|
||||
$isSingleDay = $startYmd === $endYmd;
|
||||
$isStartDay = $date === $startYmd;
|
||||
$isEndDay = $date === $endYmd;
|
||||
|
||||
if ($isSingleDay) {
|
||||
return ['AM' === $startHalf, 'PM' === $endHalf];
|
||||
}
|
||||
if ($isStartDay) {
|
||||
return ['AM' === $startHalf, true];
|
||||
}
|
||||
if ($isEndDay) {
|
||||
return [true, 'PM' === $endHalf];
|
||||
}
|
||||
|
||||
return [true, true];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user