113 lines
3.4 KiB
PHP
113 lines
3.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service\WorkHours;
|
|
|
|
use App\Entity\Absence;
|
|
use App\Enum\TrackingMode;
|
|
use App\Service\Contracts\EmployeeContractResolver;
|
|
use DateMalformedStringException;
|
|
use DateTimeImmutable;
|
|
|
|
final readonly class WorkedHoursCreditPolicy
|
|
{
|
|
public function __construct(
|
|
private EmployeeContractResolver $contractResolver,
|
|
) {}
|
|
|
|
/**
|
|
* @throws DateMalformedStringException
|
|
*/
|
|
public function computeCreditedMinutes(Absence $absence, string $dateYmd, bool $absentMorning, bool $absentAfternoon): int
|
|
{
|
|
$type = $absence->getType();
|
|
// Certaines absences ne doivent jamais générer d'heures créditées.
|
|
if (!$type?->getCountAsWorkedHours()) {
|
|
return 0;
|
|
}
|
|
|
|
$employee = $absence->getEmployee();
|
|
if (null === $employee) {
|
|
return 0;
|
|
}
|
|
$workDate = new DateTimeImmutable($dateYmd);
|
|
$contract = $this->contractResolver->resolveForEmployeeAndDate($employee, $workDate);
|
|
// Les contrats suivis en "présence" ne cumulent pas d'heures en minutes.
|
|
if (TrackingMode::TIME->value !== $contract?->getTrackingMode()) {
|
|
return 0;
|
|
}
|
|
|
|
$weekday = (int) $workDate->format('N');
|
|
// On applique la règle de crédit dépendante du contrat (35h / 39h / fallback).
|
|
$dayMinutes = $this->resolveContractDayMinutes($contract?->getWeeklyHours(), $weekday);
|
|
if ($dayMinutes <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Crédit en demi-journées: matin = 0.5, après-midi = 0.5.
|
|
$halfUnits = ($absentMorning ? 1 : 0) + ($absentAfternoon ? 1 : 0);
|
|
|
|
return (int) round(($dayMinutes / 2) * $halfUnits);
|
|
}
|
|
|
|
/**
|
|
* @throws DateMalformedStringException
|
|
*/
|
|
public function computeCreditedPresenceUnits(
|
|
Absence $absence,
|
|
string $dateYmd,
|
|
bool $absentMorning,
|
|
bool $absentAfternoon
|
|
): float {
|
|
$employee = $absence->getEmployee();
|
|
if (null === $employee) {
|
|
return 0.0;
|
|
}
|
|
$contract = $this->contractResolver->resolveForEmployeeAndDate($employee, new DateTimeImmutable($dateYmd));
|
|
if (TrackingMode::PRESENCE->value !== $contract?->getTrackingMode()) {
|
|
return 0.0;
|
|
}
|
|
|
|
// Règle forfait:
|
|
// - demi-journée d'absence => 0.5 travaillé
|
|
// - journée complète d'absence => 0 travaillé
|
|
if ($absentMorning xor $absentAfternoon) {
|
|
return 0.5;
|
|
}
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
public function resolveContractDayMinutes(?int $weeklyHours, int $isoWeekDay): int
|
|
{
|
|
// Week-end non travaillé dans cette politique.
|
|
if ($isoWeekDay >= 6) {
|
|
return 0;
|
|
}
|
|
|
|
// Règle fixe: 35h => 7h/jour.
|
|
if (35 === $weeklyHours) {
|
|
return 7 * 60;
|
|
}
|
|
|
|
// Règle fixe: 39h => 8h lundi-jeudi, 7h le vendredi.
|
|
if (39 === $weeklyHours) {
|
|
return 5 === $isoWeekDay ? 7 * 60 : 8 * 60;
|
|
}
|
|
|
|
// Cas spécifique métier: contrat 4h/semaine réparti sur 2 jours => 2h/jour.
|
|
if (4 === $weeklyHours) {
|
|
return 2 * 60;
|
|
}
|
|
|
|
// Contrat non renseigné/invalide: aucun crédit.
|
|
if (null === $weeklyHours || $weeklyHours <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Fallback générique: répartition homogène sur 5 jours ouvrés.
|
|
return (int) round(($weeklyHours * 60) / 5);
|
|
}
|
|
}
|