[#SIRH-36] corriger calcule rtt contrat custom (#27)
Auto Tag Develop / tag (push) Successful in 7s
Auto Tag Develop / tag (push) Successful in 7s
| 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: #27 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #27.
This commit is contained in:
@@ -33,6 +33,7 @@ final readonly class RttRecoveryComputationService
|
||||
private EmployeeContractResolver $contractResolver,
|
||||
private DailyReferenceMinutesResolver $dailyReferenceResolver,
|
||||
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
|
||||
private SolidarityDayResolver $solidarityDayResolver,
|
||||
string $rttStartDate = '',
|
||||
) {
|
||||
$this->rttStartDate = '' !== $rttStartDate ? new DateTimeImmutable($rttStartDate) : null;
|
||||
@@ -162,7 +163,8 @@ final readonly class RttRecoveryComputationService
|
||||
}
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$results = [];
|
||||
$solidarityDates = $this->resolveSolidarityDatesInRange($periodFrom, $periodTo);
|
||||
foreach ($weeks as $week) {
|
||||
$weekStart = $week['start'];
|
||||
$weekEnd = $week['end'];
|
||||
@@ -235,7 +237,7 @@ final readonly class RttRecoveryComputationService
|
||||
$overtimeReferenceMinutes = $isCustomContract
|
||||
? $this->computeWeeklyCustomReferenceMinutes($weekDays, $employeeContractsByDate)
|
||||
: $this->computeWeeklyOvertimeReferenceMinutes($weekDays, $employeeContractsByDate);
|
||||
$overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($weekDays, $employeeContractsByDate);
|
||||
$overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($weekDays, $employeeContractsByDate);
|
||||
// Plafond séparant 25 %/50 % : seuil de départ proraté + largeur de la bande +25 %
|
||||
// (4h pour un 39h, 8h pour un 35h). Il se décale ainsi avec une embauche en milieu
|
||||
// de semaine au lieu de rester bloqué à 43h, ce qui ouvre la tranche 50 %.
|
||||
@@ -244,35 +246,96 @@ final readonly class RttRecoveryComputationService
|
||||
? 0
|
||||
: $weeklyTotalMinutes - $overtimeReferenceMinutes;
|
||||
|
||||
[$rawBase25, $rawBase50] = $this->computeOvertimeBaseMinutes($weeklyTotalMinutes, $overtime25StartMinutes, $overtime50StartMinutes);
|
||||
foreach ($solidarityDates as $solidarityDate) {
|
||||
// isset ⇒ le jour de solidarité fait partie du sommage de CETTE semaine
|
||||
// (donc ≤ limitDate et ≥ rttStartDate). Sinon : jour futur ou hors service → pas de déficit.
|
||||
if (!isset($dailyWorkedMinutes[$solidarityDate])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : $rawBase25;
|
||||
$bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : (int) round($base25 * 0.25);
|
||||
$base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : $rawBase50;
|
||||
$bonus50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : (int) round($base50 * 0.5);
|
||||
$contractAtSolidarity = $employeeContractsByDate[$solidarityDate] ?? null;
|
||||
// Le Lundi de Pentecôte est toujours un lundi (ISO 1), mais on le dérive pour rester explicite.
|
||||
$solidarityIsoDay = (int) new DateTimeImmutable($solidarityDate)->format('N');
|
||||
// Attendu RÉEL du jour (planning workDaysHours), pas la répartition uniforme :
|
||||
// c'est ce qui rend la neutralisation correcte (cf. spec).
|
||||
$solidarityExpected = $this->dailyReferenceResolver->resolve(
|
||||
$contractAtSolidarity?->getWeeklyHours(),
|
||||
$solidarityIsoDay,
|
||||
$workDaysByDate[$employeeId][$solidarityDate] ?? null,
|
||||
);
|
||||
|
||||
if ($isWeekPresenceTracking || $disableOvertimeBonuses) {
|
||||
$totalMinutes = 0;
|
||||
} elseif ($isCustomContract) {
|
||||
$totalMinutes = max(0, $weeklyOvertimeTotalMinutes);
|
||||
} else {
|
||||
$totalMinutes = $weeklyOvertimeTotalMinutes + $bonus25 + $bonus50;
|
||||
$weeklyOvertimeTotalMinutes += $this->computeSolidarityDeficitAdjustment(
|
||||
$contractAtSolidarity,
|
||||
$solidarityExpected,
|
||||
$dailyWorkedMinutes[$solidarityDate],
|
||||
);
|
||||
}
|
||||
|
||||
$results[$weekKey] = new WeekRecoveryDetail(
|
||||
overtimeMinutes: $weeklyOvertimeTotalMinutes,
|
||||
base25Minutes: $base25,
|
||||
bonus25Minutes: $bonus25,
|
||||
base50Minutes: $base50,
|
||||
bonus50Minutes: $bonus50,
|
||||
totalMinutes: $totalMinutes,
|
||||
dailyMinutes: $dailyWorkedMinutes,
|
||||
[$rawBase25, $rawBase50] = $this->computeOvertimeBaseMinutes($weeklyTotalMinutes, $overtime25StartMinutes, $overtime50StartMinutes);
|
||||
|
||||
$results[$weekKey] = $this->buildWeekRecoveryDetail(
|
||||
$isWeekPresenceTracking,
|
||||
$disableOvertimeBonuses,
|
||||
$isCustomContract,
|
||||
$weeklyOvertimeTotalMinutes,
|
||||
$rawBase25,
|
||||
$rawBase50,
|
||||
$dailyWorkedMinutes,
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble le détail de récupération d'une semaine à partir des drapeaux résolus et
|
||||
* des bandes d'heures sup brutes.
|
||||
*
|
||||
* - PRESENCE / INTERIM (bonus désactivés) : aucune récupération.
|
||||
* - CUSTOM : récupération plate 1h = 1h, sans tranches 25/50 ; l'heure sup signée EST
|
||||
* le total, donc une semaine travaillée sous les heures contractuelles produit un
|
||||
* total négatif (déficit qui réduit le solde). Marquée isFlatRecovery pour que le
|
||||
* provider ne draine pas les tranches 25/50.
|
||||
* - Standard 35h/39h : heures sup + bonus 25 %/50 %.
|
||||
*
|
||||
* @param array<string, int> $dailyMinutes
|
||||
*/
|
||||
private function buildWeekRecoveryDetail(
|
||||
bool $isPresence,
|
||||
bool $disableBonuses,
|
||||
bool $isCustom,
|
||||
int $overtimeTotalMinutes,
|
||||
int $rawBase25,
|
||||
int $rawBase50,
|
||||
array $dailyMinutes,
|
||||
): WeekRecoveryDetail {
|
||||
$noBands = $isPresence || $disableBonuses || $isCustom;
|
||||
|
||||
$base25 = $noBands ? 0 : $rawBase25;
|
||||
$bonus25 = $noBands ? 0 : (int) round($base25 * 0.25);
|
||||
$base50 = $noBands ? 0 : $rawBase50;
|
||||
$bonus50 = $noBands ? 0 : (int) round($base50 * 0.5);
|
||||
|
||||
if ($isPresence || $disableBonuses) {
|
||||
$totalMinutes = 0;
|
||||
} elseif ($isCustom) {
|
||||
$totalMinutes = $overtimeTotalMinutes; // signé : le déficit réduit le solde
|
||||
} else {
|
||||
$totalMinutes = $overtimeTotalMinutes + $bonus25 + $bonus50;
|
||||
}
|
||||
|
||||
return new WeekRecoveryDetail(
|
||||
overtimeMinutes: $overtimeTotalMinutes,
|
||||
base25Minutes: $base25,
|
||||
bonus25Minutes: $bonus25,
|
||||
base50Minutes: $base50,
|
||||
bonus50Minutes: $bonus50,
|
||||
totalMinutes: $totalMinutes,
|
||||
dailyMinutes: $dailyMinutes,
|
||||
isFlatRecovery: $isCustom,
|
||||
);
|
||||
}
|
||||
|
||||
private function computeMetrics(WorkHour $workHour): WorkMetrics
|
||||
{
|
||||
$driverDay = $workHour->getDayHoursMinutes() ?? 0;
|
||||
@@ -415,6 +478,59 @@ final readonly class RttRecoveryComputationService
|
||||
return $weekDays[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lundi(s) de Pentecôte (jour de solidarité) inclus dans [from, to]. Un exercice
|
||||
* Juin N-1 → Mai N couvre les années civiles N-1 et N ; on retient les dates dans la fenêtre.
|
||||
*
|
||||
* @return list<string> dates au format 'Y-m-d'
|
||||
*/
|
||||
private function resolveSolidarityDatesInRange(DateTimeImmutable $from, DateTimeImmutable $to): array
|
||||
{
|
||||
$dates = [];
|
||||
$firstYear = (int) $from->format('Y');
|
||||
$lastYear = (int) $to->format('Y');
|
||||
|
||||
for ($year = $firstYear; $year <= $lastYear; ++$year) {
|
||||
$candidate = $this->solidarityDayResolver->pentecostMonday($year);
|
||||
if ($candidate >= $from && $candidate <= $to) {
|
||||
$dates[] = $candidate->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Déficit forfaitaire du jour de solidarité pour les contrats CUSTOM < 35h.
|
||||
*
|
||||
* Le jour est neutralisé puis chargé du prorata légal : on remplace la valeur réelle
|
||||
* du jour ($workedMinutes : RTT posé, heures saisies, vide, ou crédit férié virtuel)
|
||||
* par l'attendu contractuel du jour ($expectedMinutes = workDaysHours), puis on
|
||||
* retranche le prorata = 7h/35h × heuresHebdo = 12 min par heure hebdo. Sur une
|
||||
* semaine par ailleurs normale, le net vaut exactement −prorata. Renvoie le delta à
|
||||
* ajouter à weeklyOvertimeTotalMinutes (0 hors périmètre : non-CUSTOM ou ≥ 35h).
|
||||
*/
|
||||
private function computeSolidarityDeficitAdjustment(
|
||||
?Contract $contractAtSolidarity,
|
||||
int $expectedMinutes,
|
||||
int $workedMinutes,
|
||||
): int {
|
||||
$weeklyHours = $contractAtSolidarity?->getWeeklyHours();
|
||||
$type = ContractType::resolve(
|
||||
$contractAtSolidarity?->getName(),
|
||||
$contractAtSolidarity?->getTrackingMode(),
|
||||
$weeklyHours,
|
||||
);
|
||||
|
||||
if (ContractType::CUSTOM !== $type || null === $weeklyHours || $weeklyHours >= 35) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$prorata = (int) round($weeklyHours * 12);
|
||||
|
||||
return ($expectedMinutes - $workedMinutes) - $prorata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $days
|
||||
* @param array<string, ?Contract> $contractsByDate
|
||||
|
||||
Reference in New Issue
Block a user