fix(rtt) : anchor week contract type on first contracted day
Mid-week hire weeks (e.g. CDD starting Thursday) had their first day with no contract, so the week was classified CUSTOM and the 25%/50% overtime bonuses were disabled. Anchor the week's contract type/nature on the first contracted day instead, so a 35h/39h hire week keeps its overtime tiers. Dylan CHABOISSON week 12: 7h → 8h45 (7h × 1.25). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -221,8 +221,9 @@ final readonly class RttRecoveryComputationService
|
||||
continue;
|
||||
}
|
||||
|
||||
$weekAnchorNature = $naturesByDate[$employeeId][$weekDays[0]] ?? ContractNature::CDI;
|
||||
$weekAnchorContract = $employeeContractsByDate[$weekDays[0]] ?? null;
|
||||
$weekAnchorDate = $this->resolveWeekAnchorDate($weekDays, $employeeContractsByDate);
|
||||
$weekAnchorNature = $naturesByDate[$employeeId][$weekAnchorDate] ?? ContractNature::CDI;
|
||||
$weekAnchorContract = $employeeContractsByDate[$weekAnchorDate] ?? null;
|
||||
$isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode();
|
||||
$disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract, $weekAnchorNature);
|
||||
$weekContractType = ContractType::resolve(
|
||||
@@ -388,9 +389,29 @@ final readonly class RttRecoveryComputationService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $days
|
||||
* @param array<string, ?Contract> $contractsByDate
|
||||
*/
|
||||
/**
|
||||
* Date d'ancrage de la semaine pour résoudre le type/nature de contrat : premier jour
|
||||
* de la semaine couvert par un contrat. Évite qu'une semaine d'embauche en milieu de
|
||||
* semaine (premiers jours hors contrat) soit classée CUSTOM — ce qui désactiverait à
|
||||
* tort les bonus 25 %/50 % d'un contrat 35h/39h. Fallback sur le 1er jour si aucun jour
|
||||
* n'est contracté (semaine entièrement hors contrat → 0 de toute façon).
|
||||
*
|
||||
* @param list<string> $weekDays
|
||||
* @param array<string, ?Contract> $contractsByDate
|
||||
*/
|
||||
private function resolveWeekAnchorDate(array $weekDays, array $contractsByDate): string
|
||||
{
|
||||
foreach ($weekDays as $date) {
|
||||
if (null !== ($contractsByDate[$date] ?? null)) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
return $weekDays[0];
|
||||
}
|
||||
|
||||
private function computeWeeklyOvertimeReferenceMinutes(array $days, array $contractsByDate): int
|
||||
{
|
||||
$total = 0;
|
||||
|
||||
74
tests/Service/Rtt/RttRecoveryComputationServiceTest.php
Normal file
74
tests/Service/Rtt/RttRecoveryComputationServiceTest.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
private function invokePrivate(object $obj, string $method, mixed ...$args): mixed
|
||||
{
|
||||
return new ReflectionClass($obj::class)->getMethod($method)->invoke($obj, ...$args);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user