[#SIRH-36] corriger calcule rtt contrat custom (#27)
Auto Tag Develop / tag (push) Successful in 7s
Auto Tag Develop / tag (push) Successful in 7s
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #27 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #27.
This commit is contained in:
@@ -80,6 +80,23 @@ final class RttClosingBalanceServiceTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testCustomDeficitWeekReducesClosingBalance(): void
|
||||
{
|
||||
// CUSTOM (4h) : une semaine de récup +3h puis une semaine déficitaire -1h
|
||||
// (toutes deux sans tranches 25/50). Le déficit doit réduire la clôture.
|
||||
$recovery = new WeekRecoveryDetail(totalMinutes: 180, isFlatRecovery: true); // +3h
|
||||
$deficit = new WeekRecoveryDetail(totalMinutes: -60, isFlatRecovery: true); // -1h
|
||||
|
||||
$closing = $this->service()->fold(new WeekRecoveryDetail(), [$recovery, $deficit], $this->payments());
|
||||
|
||||
// 3h - 1h = 2h reportées, et la somme des buckets égale toujours le total.
|
||||
self::assertSame(120, $closing->totalMinutes);
|
||||
self::assertSame(
|
||||
120,
|
||||
$closing->base25Minutes + $closing->bonus25Minutes + $closing->base50Minutes + $closing->bonus50Minutes,
|
||||
);
|
||||
}
|
||||
|
||||
public function testBucketSumAlwaysEqualsTotalInvariant(): void
|
||||
{
|
||||
$opening = new WeekRecoveryDetail(base25Minutes: 200, bonus25Minutes: 50, base50Minutes: 100, bonus50Minutes: 50, totalMinutes: 400);
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -113,6 +114,159 @@ final class RttRecoveryComputationServiceTest extends TestCase
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service\Rtt;
|
||||
|
||||
use App\Service\Rtt\SolidarityDayResolver;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class SolidarityDayResolverTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Lundi de Pentecôte = dimanche de Pâques + 50 jours.
|
||||
* 2024 : Pâques 31/03 → 20/05 ; 2025 : Pâques 20/04 → 09/06 ; 2026 : Pâques 05/04 → 25/05.
|
||||
*/
|
||||
#[DataProvider('pentecostCases')]
|
||||
public function testPentecostMonday(int $year, string $expected): void
|
||||
{
|
||||
$resolver = new SolidarityDayResolver();
|
||||
|
||||
self::assertSame($expected, $resolver->pentecostMonday($year)->format('Y-m-d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{int, string}>
|
||||
*/
|
||||
public static function pentecostCases(): iterable
|
||||
{
|
||||
yield '2024' => [2024, '2024-05-20'];
|
||||
|
||||
yield '2025' => [2025, '2025-06-09'];
|
||||
|
||||
yield '2026' => [2026, '2026-05-25'];
|
||||
|
||||
// Century-boundary year: Easter 2000-04-23 → Whit Monday 2000-06-12
|
||||
// (verified with: easter_date(2000) → date('+50 days'))
|
||||
yield '2000' => [2000, '2000-06-12'];
|
||||
|
||||
// Late-April Easter (2011-04-24) → Whit Monday 2011-06-13
|
||||
// (verified with: easter_date(2011) → date('+50 days'))
|
||||
yield '2011' => [2011, '2011-06-13'];
|
||||
|
||||
// Easter on April 25 — exercises the computus corrective $m branch:
|
||||
// Easter 2038-04-25 → Whit Monday 2038-06-14
|
||||
// (verified with: easter_date(2038) → date('+50 days'))
|
||||
yield '2038' => [2038, '2038-06-14'];
|
||||
}
|
||||
|
||||
/**
|
||||
* The returned date must always be a Monday (ISO weekday = 1).
|
||||
* Verified for 2025 as a representative case.
|
||||
*/
|
||||
public function testPentecostMondayIsAMonday(): void
|
||||
{
|
||||
$resolver = new SolidarityDayResolver();
|
||||
|
||||
self::assertSame('1', $resolver->pentecostMonday(2025)->format('N'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user