169 lines
6.6 KiB
PHP
169 lines
6.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Service\Rtt;
|
|
|
|
use App\Entity\Contract;
|
|
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);
|
|
}
|
|
|
|
private function invokePrivate(object $obj, string $method, mixed ...$args): mixed
|
|
{
|
|
return new ReflectionClass($obj::class)->getMethod($method)->invoke($obj, ...$args);
|
|
}
|
|
}
|