Files
SIRH/tests/Service/Rtt/RttRecoveryComputationServiceTest.php
T

275 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Tests\Service\Rtt;
use App\Entity\Contract;
use App\Enum\TrackingMode;
use App\Service\Rtt\RttRecoveryComputationService;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
/**
* The service constructor takes several final-class collaborators that PHPUnit cannot
* double. Pure helpers are exercised via newInstanceWithoutConstructor + reflection.
*
* @internal
*/
final class RttRecoveryComputationServiceTest extends TestCase
{
public function testResolveWeekAnchorDateReturnsFirstContractedDayWhenWeekStartsBeforeHire(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$contract = new Contract();
$weekDays = ['2026-03-16', '2026-03-17', '2026-03-18', '2026-03-19', '2026-03-20', '2026-03-21', '2026-03-22'];
$contractsByDate = [
'2026-03-16' => null,
'2026-03-17' => null,
'2026-03-18' => null,
'2026-03-19' => $contract,
'2026-03-20' => $contract,
'2026-03-21' => $contract,
'2026-03-22' => $contract,
];
$anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);
self::assertSame('2026-03-19', $anchor);
}
public function testResolveWeekAnchorDateReturnsFirstDayWhenItIsContracted(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$contract = new Contract();
$weekDays = ['2026-03-23', '2026-03-24', '2026-03-25'];
$contractsByDate = [
'2026-03-23' => $contract,
'2026-03-24' => $contract,
'2026-03-25' => $contract,
];
$anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);
self::assertSame('2026-03-23', $anchor);
}
public function testResolveWeekAnchorDateFallsBackToFirstDayWhenNoContract(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$weekDays = ['2026-03-16', '2026-03-17'];
$contractsByDate = ['2026-03-16' => null, '2026-03-17' => null];
$anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);
self::assertSame('2026-03-16', $anchor);
}
public function testResolveOvertime25BandWidthIs4hForH39(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$contract = new Contract()->setWeeklyHours(39);
self::assertSame(4 * 60, $this->invokePrivate($service, 'resolveOvertime25BandWidthMinutes', $contract));
}
public function testResolveOvertime25BandWidthIs8hForH35(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$contract = new Contract()->setWeeklyHours(35);
self::assertSame(8 * 60, $this->invokePrivate($service, 'resolveOvertime25BandWidthMinutes', $contract));
}
/**
* Dylan Chaboisson, semaine 12 : embauché le jeudi sur un contrat 39h.
* Total travaillé 22h (1320 min), départ 25 % proraté aux jours contractés = 15h (900 min),
* plafond 25 %/50 % = 15h + bande 4h = 19h (1140 min). Le plafond se décale avec
* l'embauche au lieu de rester bloqué à 43h, ouvrant la tranche 50 %.
*/
public function testMidWeekHireSplitsOvertimeAcross25And50(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
[$base25, $base50] = $this->invokePrivate($service, 'computeOvertimeBaseMinutes', 1320, 900, 1140);
self::assertSame(4 * 60, $base25);
self::assertSame(3 * 60, $base50);
}
/**
* Régression : semaine pleine 39h (départ 39h, plafond 43h), 46h travaillées →
* 4h à 25 % (39→43) et 3h à 50 % (43→46), comportement inchangé.
*/
public function testFullWeekOvertimeSplitUnchanged(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
[$base25, $base50] = $this->invokePrivate($service, 'computeOvertimeBaseMinutes', 2760, 2340, 2580);
self::assertSame(4 * 60, $base25);
self::assertSame(3 * 60, $base50);
}
public function testBuildWeekDetailCustomDeficitKeepsSignedTotalAndFlatFlag(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
// CUSTOM, semaine sous les heures : overtime -120 (worked 2h sur réf 4h).
$detail = $this->invokePrivate(
$service,
'buildWeekRecoveryDetail',
false, // isPresence
false, // disableBonuses
true, // isCustom
-120, // overtimeTotalMinutes
0, // rawBase25
0, // rawBase50
[], // dailyMinutes
);
self::assertSame(-120, $detail->totalMinutes);
self::assertTrue($detail->isFlatRecovery);
self::assertSame(0, $detail->base25Minutes);
self::assertSame(0, $detail->base50Minutes);
}
public function testBuildWeekDetailCustomPositiveIsFlatOneToOne(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$detail = $this->invokePrivate($service, 'buildWeekRecoveryDetail', false, false, true, 180, 0, 0, []);
self::assertSame(180, $detail->totalMinutes); // 1h = 1h
self::assertTrue($detail->isFlatRecovery);
}
public function testBuildWeekDetailStandardKeepsBucketsAndBonuses(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
// 39h : overtime 300, base25 240, base50 60.
$detail = $this->invokePrivate($service, 'buildWeekRecoveryDetail', false, false, false, 300, 240, 60, []);
self::assertFalse($detail->isFlatRecovery);
self::assertSame(240, $detail->base25Minutes);
self::assertSame(60, $detail->bonus25Minutes); // round(240 * 0.25)
self::assertSame(60, $detail->base50Minutes);
self::assertSame(30, $detail->bonus50Minutes); // round(60 * 0.5)
self::assertSame(300 + 60 + 30, $detail->totalMinutes);
}
/**
* CUSTOM 4h, jour de solidarité non travaillé (RTT posé ou vide) : delta = (attendu 0) prorata.
* attendu lundi = workDaysHours = 120 ; prorata = round(4×12) = 48 ; delta = 120 48 = 72.
* (Combiné au naturel 120 de la semaine, donne 48 min.).
*/
public function testSolidarityAdjustmentCustomNotWorkedNeutralisesToProrata(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$delta = $this->invokePrivate(
$service,
'computeSolidarityDeficitAdjustment',
self::customContract(4),
120, // expectedMinutes (workDaysHours du lundi)
0, // workedMinutes (RTT posé / vide)
);
self::assertSame(72, $delta);
}
/**
* CUSTOM 4h, jour de solidarité travaillé normalement (120) : delta = (120 120) 48 = 48.
*/
public function testSolidarityAdjustmentCustomWorkedNormallyChargesProrata(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(4), 120, 120);
self::assertSame(-48, $delta);
}
/**
* CUSTOM 4h, jour de solidarité travaillé en plus (240) : delta = (120 240) 48 = 168.
* Le surplus du jour de solidarité n'est PAS crédité (jour neutralisé, net forcé à prorata).
*/
public function testSolidarityAdjustmentCustomWorkedExtraStillNetsProrata(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(4), 120, 240);
self::assertSame(-168, $delta);
}
/**
* CUSTOM 28h : prorata = round(28×12) = 336 (5h36). worked 0, expected 336 → delta 0.
* Le delta est nul ici par coïncidence du fallback uniforme (expected = prorata) ; avec un vrai
* workDaysHours où la valeur du lundi diffère, expected ≠ prorata et le delta serait non nul.
*/
public function testSolidarityAdjustmentCustom28hUsesProrata(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(28), 336, 0);
self::assertSame(0, $delta);
}
/**
* CUSTOM ≥ 35h (36h) : hors périmètre → delta 0.
*/
public function testSolidarityAdjustmentCustom36hOutOfScope(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(36), 999, 0);
self::assertSame(0, $delta);
}
/**
* 35h : type H35 (pas CUSTOM) → delta 0 (comportement inchangé, RTT posé fait foi).
*/
public function testSolidarityAdjustment35hOutOfScope(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$contract = new Contract()->setName('35h')->setTrackingMode(TrackingMode::TIME)->setWeeklyHours(35);
$delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', $contract, 420, 0);
self::assertSame(0, $delta);
}
/**
* Aucun contrat ce jour-là (salarié parti / pas encore embauché) → delta 0.
*/
public function testSolidarityAdjustmentNoContractIsZero(): void
{
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
$delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', null, 0, 0);
self::assertSame(0, $delta);
}
private static function customContract(int $weeklyHours): Contract
{
return new Contract()
->setName('Temps partiel')
->setTrackingMode(TrackingMode::TIME)
->setWeeklyHours($weeklyHours)
;
}
private function invokePrivate(object $obj, string $method, mixed ...$args): mixed
{
return new ReflectionClass($obj::class)->getMethod($method)->invoke($obj, ...$args);
}
}