From 45e86543a1465ffd12f5587e3fe62272fe0a88d9 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 26 May 2026 10:05:42 +0200 Subject: [PATCH] =?UTF-8?q?fix(absences)=20:=20demi-journ=C3=A9e=20mono-jo?= =?UTF-8?q?ur=20d=C3=A9compt=C3=A9e=200,5=20(et=20non=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur une demande d'un seul jour, le formulaire recopie la demi-journée de début sur la fin (même date), si bien que les deux bornes portaient une demi-journée et 0,5 était soustrait deux fois (1 - 0,5 - 0,5 = 0). Quand start et end tombent le même jour, les deux bornes se confondent : on ne soustrait désormais 0,5 qu'une seule fois. Comparaison par getTimestamp() pour rester compatible strict_comparison (=== sur deux DateTimeImmutable distincts teste l'identité d'instance, pas la valeur). Couverture complétée : mono-jour plein, week-end, férié, inversion de dates, demi-journée de fin seule, demi sur samedi en mode ouvrables. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Service/AbsenceDayCalculator.php | 18 +++-- .../Unit/Service/AbsenceDayCalculatorTest.php | 76 +++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/Service/AbsenceDayCalculator.php b/src/Service/AbsenceDayCalculator.php index 2a4d667..d30a598 100644 --- a/src/Service/AbsenceDayCalculator.php +++ b/src/Service/AbsenceDayCalculator.php @@ -54,11 +54,19 @@ final readonly class AbsenceDayCalculator // A half-day only subtracts 0.5 when its boundary day is actually // counted (otherwise a half-day posted on a weekend/holiday would // wrongly under-count the absence). - if (null !== $startHalfDay && $this->isCountedDay($start, $workingDaysOnly)) { - $days -= 0.5; - } - if (null !== $endHalfDay && $this->isCountedDay($end, $workingDaysOnly)) { - $days -= 0.5; + if ($start->getTimestamp() === $end->getTimestamp()) { + // Single-day request: both boundaries collapse onto the same day, + // so a half-day must subtract 0.5 once, never twice. + if ((null !== $startHalfDay || null !== $endHalfDay) && $this->isCountedDay($start, $workingDaysOnly)) { + $days -= 0.5; + } + } else { + if (null !== $startHalfDay && $this->isCountedDay($start, $workingDaysOnly)) { + $days -= 0.5; + } + if (null !== $endHalfDay && $this->isCountedDay($end, $workingDaysOnly)) { + $days -= 0.5; + } } return max(0.0, $days); diff --git a/tests/Unit/Service/AbsenceDayCalculatorTest.php b/tests/Unit/Service/AbsenceDayCalculatorTest.php index 0eecdcd..33b96a3 100644 --- a/tests/Unit/Service/AbsenceDayCalculatorTest.php +++ b/tests/Unit/Service/AbsenceDayCalculatorTest.php @@ -68,6 +68,15 @@ class AbsenceDayCalculatorTest extends TestCase )); } + public function testSingleFullDayCountsOne(): void + { + // Most common request: a single full working day = 1.0 (no half-day). + self::assertSame(1.0, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-01'), + new DateTimeImmutable('2026-06-01'), + )); + } + public function testSingleHalfDay(): void { self::assertSame(0.5, $this->calculator->countWorkingDays( @@ -77,6 +86,73 @@ class AbsenceDayCalculatorTest extends TestCase )); } + public function testHalfDayEndOnlySubtractsHalf(): void + { + // Mirror of testHalfDayStartSubtractsHalf: end half-day only, on a + // counted multi-day range => 5 - 0.5 = 4.5. + self::assertSame(4.5, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-01'), + new DateTimeImmutable('2026-06-05'), + null, + HalfDay::Morning, + )); + } + + public function testEndBeforeStartCountsZero(): void + { + // Inverted range guard (end < start) => 0, never negative. + self::assertSame(0.0, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-05'), + new DateTimeImmutable('2026-06-01'), + )); + } + + public function testSingleDayOnWeekendCountsZero(): void + { + // Saturday, jours ouvrés: not counted, even with a half-day => 0. + self::assertSame(0.0, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-06'), + new DateTimeImmutable('2026-06-06'), + HalfDay::Morning, + HalfDay::Morning, + )); + } + + public function testSingleDayOnHolidayCountsZero(): void + { + // Fri 2026-05-08 (Victoire 1945): public holiday => 0. + self::assertSame(0.0, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-05-08'), + new DateTimeImmutable('2026-05-08'), + )); + } + + public function testHalfDayOnSaturdayCountedInOpenDays(): void + { + // Fri + Sat in "ouvrables" mode = 2; the end half-day sits on Saturday, + // which IS counted here => 2 - 0.5 = 1.5. + self::assertSame(1.5, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-05'), + new DateTimeImmutable('2026-06-06'), + null, + HalfDay::Morning, + false, + )); + } + + public function testSingleDayWithBothHalfDaysIsStillHalf(): void + { + // Real-world single-day request: the form mirrors the start half-day + // onto the end (same day), so both boundaries carry a half-day. It must + // still count as 0.5, not 0 (the two boundaries collapse onto one day). + self::assertSame(0.5, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-01'), + new DateTimeImmutable('2026-06-01'), + HalfDay::Morning, + HalfDay::Morning, + )); + } + public function testHalfDayOnNonCountedStartIsIgnored(): void { // Sat 2026-06-06 → Mon 2026-06-08, jours ouvrés : Sat & Sun skipped, Mon = 1.