fix(leave) : forfait work-target ignores bonus/fractioned days

The header target subtracted summary.acquiredDays, which includes fractionedDays
(and bonusDays via the full-year acquired), so a full-year forfait with weekend
work or HR fractioned days showed <218. Full year = contractual 218 (capped at
period business days); entry year = businessDays − entry acquired (repos + carried
CP, excluding bonus/fractioned). Extracted computeForfaitWorkTargetDays + 3 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 18:04:39 +02:00
parent 18fffeb411
commit 03add0d45a
2 changed files with 56 additions and 3 deletions

View File

@@ -134,11 +134,18 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year, $phase);
// Forfait : jours à travailler sur l'exercice = jours ouvrés de la période congés acquis.
// Année pleine → 218 (252 34) ; entrée en cours d'année → prorata (ex. 168 13 ≈ 155).
// Forfait : jours à travailler sur l'exercice.
// Année pleine → cible contractuelle 218 ; les bonus week-end/férié et les jours
// fractionnés sont des congés EN PLUS, ils ne réduisent pas la cible. Entrée en cours
// d'année → jours ouvrés de la période congés acquis de l'entrée (repos proratisés +
// CP reportés), via yearSummary['acquiredDays'] (hors fractionnés/bonus). Ex. Grégory : 168 13 ≈ 155.
if (LeaveRuleCode::FORFAIT_218->value === $summary->ruleCode) {
$businessDaysInPeriod = $this->countBusinessDays($periodFrom, $periodTo, $this->buildRawPublicHolidayMap($periodFrom, $periodTo));
$summary->forfaitWorkTargetDays = $businessDaysInPeriod - $summary->acquiredDays;
$summary->forfaitWorkTargetDays = $this->computeForfaitWorkTargetDays(
$businessDaysInPeriod,
$this->isForfaitEntryYear($phase, $year),
$yearSummary['acquiredDays'],
);
}
// Forfait-only: leaves taken from N-1 stock do NOT decrement presence days.
@@ -890,6 +897,24 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
return $repoDaysYear * $businessDaysPeriod / $businessDaysYear;
}
/**
* Jours à travailler d'un forfait sur l'exercice consulté.
*
* - Année pleine : cible contractuelle 218 (bornée aux jours ouvrés de la période si
* celle-ci en compte moins). Les bonus week-end/férié et jours fractionnés sont des
* congés EN PLUS et ne réduisent pas la cible.
* - Entrée en cours d'année : jours ouvrés de la période congés acquis de l'entrée
* (repos proratisés + CP reportés, hors fractionnés/bonus). Ex. Grégory : 168 13 ≈ 155.
*/
private function computeForfaitWorkTargetDays(int $businessDaysInPeriod, bool $isEntryYear, float $entryAcquiredDays): float
{
if ($isEntryYear) {
return $businessDaysInPeriod - $entryAcquiredDays;
}
return (float) min($businessDaysInPeriod, self::FORFAIT_TARGET_WORKED_DAYS);
}
/**
* Vrai si la phase FORFAIT démarre en cours de l'année civile consultée
* (donc avec une période partielle), faux pour une année pleine ou un démarrage le 1er janvier.

View File

@@ -86,6 +86,34 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase
self::assertEqualsWithDelta(25.0 / 12.0, $result, 0.0001);
}
public function testComputeForfaitWorkTargetDaysEntryYearProrates(): void
{
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
// Grégory : 168 jours ouvrés sur la période 12.94 congés acquis (repos + CP reportés) ≈ 155.06
$result = $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 168, true, 12.94);
self::assertEqualsWithDelta(155.06, $result, 0.001);
}
public function testComputeForfaitWorkTargetDaysFullYearIs218IgnoringBonusAndFractioned(): void
{
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
// Année pleine : la cible reste 218 quelle que soit la valeur des congés acquis
// (les bonus week-end/férié et jours fractionnés ne réduisent pas la cible).
self::assertSame(218.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 252, false, 34.0));
self::assertSame(218.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 252, false, 40.0));
}
public function testComputeForfaitWorkTargetDaysFullYearCapsAtBusinessDaysWhenFewer(): void
{
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
// Période avec moins de 218 jours ouvrés (phase forfait clôturée en cours d'année) → cap aux jours ouvrés.
self::assertSame(200.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 200, false, 5.0));
}
public function testComputeProratedForfaitRepoDaysGregoryCase(): void
{
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();