From 29db3b5025ef412dc801c4073b5062f354dba0b9 Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 20 Mar 2026 11:26:17 +0100 Subject: [PATCH] fix : calcule des RTT sur les contrats 4h --- CLAUDE.md | 1 + doc/functional-rules.md | 4 ++ .../Rtt/RttRecoveryComputationService.php | 55 +++++++++++++++---- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7847d3f..4dd81ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,6 +43,7 @@ ## Overtime Rules - Contracts <= 35h: +25% from 35h to 43h, +50% beyond - Contracts >= 39h: +25% from 39h to 43h, +50% beyond +- CUSTOM contracts (weeklyHours ≠ 35 and ≠ 39, not INTERIM/FORFAIT): reference = actual contractual hours, no 25%/50% bonuses (1h overtime = 1h recovery), deficit doesn't impact balance - INTERIM: no overtime bonuses, no recovery time - Driver contracts: no overtime calculation diff --git a/doc/functional-rules.md b/doc/functional-rules.md index 95ca083..56b7e32 100644 --- a/doc/functional-rules.md +++ b/doc/functional-rules.md @@ -122,6 +122,10 @@ Documents complementaires: - Semaine en déficit (heures travaillées < heures contrat): - le déficit est déduit du cumul RTT : d'abord des heures à 50%, puis des heures à 25% - si aucun solde 50% ni 25%, les heures à 25% deviennent négatives +- Contrats CUSTOM (heures hebdo ≠ 35h et ≠ 39h, hors INTERIM/FORFAIT): + - référence heures sup = heures contractuelles réelles (ex: 4h → référence 4h) + - pas de bonus 25% ni 50% : 1 heure sup = 1 heure de récupération + - le déficit (travail < contrat) ne génère pas de récup mais n'impacte pas le solde - Nature `INTERIM`: - pas de bonus 25% - pas de bonus 50% diff --git a/src/Service/Rtt/RttRecoveryComputationService.php b/src/Service/Rtt/RttRecoveryComputationService.php index 3883ef1..7a0ddf9 100644 --- a/src/Service/Rtt/RttRecoveryComputationService.php +++ b/src/Service/Rtt/RttRecoveryComputationService.php @@ -206,20 +206,36 @@ final readonly class RttRecoveryComputationService continue; } - $weekAnchorNature = $naturesByDate[$employeeId][$weekDays[0]] ?? ContractNature::CDI; - $weekAnchorContract = $employeeContractsByDate[$weekDays[0]] ?? null; - $isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode(); - $disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract, $weekAnchorNature); - $overtimeReferenceMinutes = $this->computeWeeklyOvertimeReferenceMinutes($weekDays, $employeeContractsByDate); + $weekAnchorNature = $naturesByDate[$employeeId][$weekDays[0]] ?? ContractNature::CDI; + $weekAnchorContract = $employeeContractsByDate[$weekDays[0]] ?? null; + $isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode(); + $disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract, $weekAnchorNature); + $weekContractType = ContractType::resolve( + $weekAnchorContract?->getName(), + $weekAnchorContract?->getTrackingMode(), + $weekAnchorContract?->getWeeklyHours() + ); + $isCustomContract = ContractType::CUSTOM === $weekContractType; + $overtimeReferenceMinutes = $isCustomContract + ? $this->computeWeeklyCustomReferenceMinutes($weekDays, $employeeContractsByDate) + : $this->computeWeeklyOvertimeReferenceMinutes($weekDays, $employeeContractsByDate); $overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($weekDays, $employeeContractsByDate); $weeklyOvertimeTotalMinutes = $isWeekPresenceTracking ? 0 : $weeklyTotalMinutes - $overtimeReferenceMinutes; - $base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, min($weeklyTotalMinutes, 43 * 60) - $overtime25StartMinutes); - $bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base25 * 0.25); - $base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, $weeklyTotalMinutes - 43 * 60); - $bonus50 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base50 * 0.5); + $base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : max(0, min($weeklyTotalMinutes, 43 * 60) - $overtime25StartMinutes); + $bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : (int) round($base25 * 0.25); + $base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : max(0, $weeklyTotalMinutes - 43 * 60); + $bonus50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : (int) round($base50 * 0.5); + + if ($isWeekPresenceTracking || $disableOvertimeBonuses) { + $totalMinutes = 0; + } elseif ($isCustomContract) { + $totalMinutes = max(0, $weeklyOvertimeTotalMinutes); + } else { + $totalMinutes = $weeklyOvertimeTotalMinutes + $bonus25 + $bonus50; + } $results[$weekKey] = new WeekRecoveryDetail( overtimeMinutes: $weeklyOvertimeTotalMinutes, @@ -227,9 +243,7 @@ final readonly class RttRecoveryComputationService bonus25Minutes: $bonus25, base50Minutes: $base50, bonus50Minutes: $bonus50, - totalMinutes: ($isWeekPresenceTracking || $disableOvertimeBonuses) - ? 0 - : $weeklyOvertimeTotalMinutes + $bonus25 + $bonus50, + totalMinutes: $totalMinutes, ); } @@ -326,6 +340,23 @@ final readonly class RttRecoveryComputationService return max(0, $end - $start); } + /** + * @param list $days + * @param array $contractsByDate + */ + private function computeWeeklyCustomReferenceMinutes(array $days, array $contractsByDate): int + { + $total = 0; + foreach ($days as $date) { + $isoDay = (int) new DateTimeImmutable($date)->format('N'); + $contract = $contractsByDate[$date] ?? null; + $hours = $contract?->getWeeklyHours(); + $total += $this->resolveDailyReferenceMinutes($hours, $isoDay); + } + + return $total; + } + /** * @param list $days * @param array $contractsByDate