Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2745f4e476 | |||
| 1edb8d956f |
@@ -96,8 +96,15 @@
|
|||||||
- Service mutualisé : `App\Service\Rtt\RttClosingBalanceService` (méthode `computeClosingBalance` + `fold` pur testable). `fold` garantit `somme(tranches) = report + acquis − payés` ; la cascade des semaines déficitaires draine la tranche 50% avant la 25%, et la récup non bucketisée (CUSTOM 1h=1h, arrondis) atterrit en `base25` pour que la somme égale le total.
|
- Service mutualisé : `App\Service\Rtt\RttClosingBalanceService` (méthode `computeClosingBalance` + `fold` pur testable). `fold` garantit `somme(tranches) = report + acquis − payés` ; la cascade des semaines déficitaires draine la tranche 50% avant la 25%, et la récup non bucketisée (CUSTOM 1h=1h, arrondis) atterrit en `base25` pour que la somme égale le total.
|
||||||
- Options : `--force` (hors 01/06) ; `--recompute` (recalcule/écrase les lignes existantes au lieu de les sauter ; **ne touche jamais** une ligne verrouillée `is_locked`). Reprise d'une bascule erronée : `app:rtt:rollover --force --recompute`.
|
- Options : `--force` (hors 01/06) ; `--recompute` (recalcule/écrase les lignes existantes au lieu de les sauter ; **ne touche jamais** une ligne verrouillée `is_locked`). Reprise d'une bascule erronée : `app:rtt:rollover --force --recompute`.
|
||||||
- ⚠️ Bug historique : la 1ʳᵉ version ne reportait que `acquis(N-1)` (report d'ouverture perdu, paiements non déduits). Corrigé via `RttClosingBalanceService`.
|
- ⚠️ Bug historique : la 1ʳᵉ version ne reportait que `acquis(N-1)` (report d'ouverture perdu, paiements non déduits). Corrigé via `RttClosingBalanceService`.
|
||||||
|
- **Fallback provider** : quand aucune ligne `employee_rtt_balances` n'existe pour l'exercice affiché (avant la bascule), `EmployeeRttSummaryProvider::resolveCarry` calcule le report en direct via `RttClosingBalanceService::computeClosingBalance($year-1)` (et non plus `computeTotalRecoveryForExercise`) — le disponible reste donc correct (report d'ouverture + acquis − payés) même sans ligne stockée.
|
||||||
- Doc : `doc/rtt-rollover.md`.
|
- Doc : `doc/rtt-rollover.md`.
|
||||||
|
|
||||||
|
## Paiement RTT rétroactif (exercice précédent) — Option B
|
||||||
|
- Le paiement RTT est autorisé sur : l'**exercice courant**, l'**exercice immédiatement précédent** (N-1), ou le dernier exercice d'une phase clôturée. Garde back : `EmployeeRttPaymentProcessor::assertYearAllowedForPayment`. Garde front : `RttTab.vue` `isPayDisabled` (bouton actif sur `selectedYear === currentYear - 1`).
|
||||||
|
- **Cohérence du report** : un paiement sur N-1 modifie la clôture de N-1 = ouverture de N. Le processor **recalcule automatiquement** la ligne `employee_rtt_balances` de l'exercice courant (`computeClosingBalance(N-1)`) dans une **transaction** (le `flush` du paiement le rend visible au recalcul). Pas de double comptage.
|
||||||
|
- **Verrou** : si le report de l'exercice courant est `is_locked`, le paiement rétroactif est **refusé** (`assertReportNotLocked`) — la RH doit déverrouiller d'abord.
|
||||||
|
- Portée limitée à N-1 (chaîne de recalcul = 1 étape). Si la ligne courante n'existe pas encore, le fallback provider couvre l'affichage (cf. ci-dessus).
|
||||||
|
|
||||||
## Vue contrat (sélecteur de phase)
|
## Vue contrat (sélecteur de phase)
|
||||||
- Picker `Vue contrat` en haut de la fiche employé (`pages/employees/[id].vue`). Caché si l'employé n'a qu'une phase.
|
- Picker `Vue contrat` en haut de la fiche employé (`pages/employees/[id].vue`). Caché si l'employé n'a qu'une phase.
|
||||||
- Phase = groupe d'`EmployeeContractPeriod` consécutifs partageant la signature `(contract.type, weeklyHours, isDriver)`. Résolu par `App\Service\Contracts\EmployeeContractPhaseResolver`.
|
- Phase = groupe d'`EmployeeContractPeriod` consécutifs partageant la signature `(contract.type, weeklyHours, isDriver)`. Résolu par `App\Service\Contracts\EmployeeContractPhaseResolver`.
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.106'
|
app.version: '0.1.107'
|
||||||
|
|||||||
+11
-2
@@ -34,9 +34,18 @@ Comportement :
|
|||||||
|
|
||||||
## Verrouillage des éditions sur exercices passés
|
## Verrouillage des éditions sur exercices passés
|
||||||
|
|
||||||
Quand `selectedYear !== currentYear` (consultation d'un exercice antérieur), le bouton **+ Payer les RTT** est désactivé. Justification : un paiement rétroactif sur un exercice clos décalerait les soldes courants et le report N-1 calculé.
|
Quand `selectedYear !== currentYear` (consultation d'un exercice antérieur), le bouton **+ Payer les RTT** est désactivé — **sauf sur l'exercice immédiatement précédent** (`selectedYear === currentYear - 1`), où le paiement rétroactif est autorisé (Option B).
|
||||||
|
|
||||||
La consultation reste possible, l'édition non.
|
La consultation des exercices plus anciens reste possible, l'édition non.
|
||||||
|
|
||||||
|
### Paiement rétroactif sur l'exercice précédent (Option B)
|
||||||
|
|
||||||
|
Un paiement enregistré sur l'exercice N-1 modifie sa clôture, donc le **report d'ouverture de l'exercice courant N**. Pour éviter tout décalage / double comptage :
|
||||||
|
|
||||||
|
- garde back `EmployeeRttPaymentProcessor::assertYearAllowedForPayment` : accepte courant, **N-1**, ou dernier exercice d'une phase clôturée ;
|
||||||
|
- après enregistrement, le processor **recalcule automatiquement** la ligne `employee_rtt_balances` de l'exercice courant via `RttClosingBalanceService::computeClosingBalance(N-1)`, dans une **transaction** (le `flush` du paiement le rend visible au recalcul) ;
|
||||||
|
- si le report de l'exercice courant est **verrouillé** (`is_locked`), le paiement est **refusé** (`assertReportNotLocked`) : la RH doit déverrouiller d'abord ;
|
||||||
|
- portée volontairement limitée à N-1 (chaîne de recalcul = 1 étape). Si la ligne courante n'existe pas encore, l'affichage reste correct grâce au fallback de `EmployeeRttSummaryProvider::resolveCarry` (calcul dynamique de la clôture N-1).
|
||||||
|
|
||||||
## Sélecteur de phase de contrat
|
## Sélecteur de phase de contrat
|
||||||
|
|
||||||
|
|||||||
@@ -313,8 +313,16 @@ const isLastExerciseOfPhase = computed(() => {
|
|||||||
return props.selectedYear === endYear
|
return props.selectedYear === endYear
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Retroactive payment is allowed on the immediately previous exercise (Option B):
|
||||||
|
// the backend recomputes the next exercise's report so the carry stays correct.
|
||||||
|
const isPreviousExercise = computed(() =>
|
||||||
|
props.selectedYear !== null
|
||||||
|
&& props.currentYear !== null
|
||||||
|
&& props.selectedYear === props.currentYear - 1
|
||||||
|
)
|
||||||
|
|
||||||
const isPayDisabled = computed(() =>
|
const isPayDisabled = computed(() =>
|
||||||
isHistoricalYear.value && !isLastExerciseOfPhase.value
|
isHistoricalYear.value && !isLastExerciseOfPhase.value && !isPreviousExercise.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleYearChange = (event: Event) => {
|
const handleYearChange = (event: Event) => {
|
||||||
|
|||||||
@@ -534,6 +534,8 @@ export const documentationSections: DocSection[] = [
|
|||||||
blocks: [
|
blocks: [
|
||||||
{ type: 'paragraph', content: 'L\'administrateur RH peut enregistrer un paiement RTT depuis l\'onglet RTT de la fiche employé.' },
|
{ type: 'paragraph', content: 'L\'administrateur RH peut enregistrer un paiement RTT depuis l\'onglet RTT de la fiche employé.' },
|
||||||
{ type: 'list', content: 'Saisie : mois, nombre de minutes, taux (25% ou 50%)\nPlusieurs paiements possibles par mois\nLes heures payées sont soustraites du solde disponible' },
|
{ type: 'list', content: 'Saisie : mois, nombre de minutes, taux (25% ou 50%)\nPlusieurs paiements possibles par mois\nLes heures payées sont soustraites du solde disponible' },
|
||||||
|
{ type: 'paragraph', content: 'Le paiement est possible sur l\'exercice courant et sur l\'exercice immédiatement précédent (paiement rétroactif, ex. des RTT de mai réglés après la bascule du 1er juin).' },
|
||||||
|
{ type: 'note', content: 'Un paiement saisi sur l\'exercice précédent recalcule automatiquement le « Report N-1 » de l\'exercice courant : aucun double comptage. Si ce report a déjà été verrouillé (validé), le paiement rétroactif est refusé — déverrouillez-le d\'abord.' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ use ApiPlatform\Metadata\Operation;
|
|||||||
use ApiPlatform\State\ProcessorInterface;
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
use App\ApiResource\EmployeeRttPaymentInput;
|
use App\ApiResource\EmployeeRttPaymentInput;
|
||||||
use App\Entity\Employee;
|
use App\Entity\Employee;
|
||||||
|
use App\Entity\EmployeeRttBalance;
|
||||||
use App\Entity\EmployeeRttPayment;
|
use App\Entity\EmployeeRttPayment;
|
||||||
use App\Repository\EmployeeRepository;
|
use App\Repository\EmployeeRepository;
|
||||||
|
use App\Repository\EmployeeRttBalanceRepository;
|
||||||
use App\Repository\EmployeeRttPaymentRepository;
|
use App\Repository\EmployeeRttPaymentRepository;
|
||||||
use App\Service\AuditLogger;
|
use App\Service\AuditLogger;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
use App\Service\Exercise\ExerciseYearResolver;
|
use App\Service\Exercise\ExerciseYearResolver;
|
||||||
|
use App\Service\Rtt\RttClosingBalanceService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Clock\ClockInterface;
|
use Psr\Clock\ClockInterface;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
@@ -24,11 +27,13 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private EmployeeRepository $employeeRepository,
|
private EmployeeRepository $employeeRepository,
|
||||||
private EmployeeRttPaymentRepository $rttPaymentRepository,
|
private EmployeeRttPaymentRepository $rttPaymentRepository,
|
||||||
|
private EmployeeRttBalanceRepository $rttBalanceRepository,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private AuditLogger $auditLogger,
|
private AuditLogger $auditLogger,
|
||||||
private EmployeeContractPhaseResolver $phaseResolver,
|
private EmployeeContractPhaseResolver $phaseResolver,
|
||||||
private ClockInterface $clock,
|
private ClockInterface $clock,
|
||||||
private ExerciseYearResolver $exerciseYearResolver,
|
private ExerciseYearResolver $exerciseYearResolver,
|
||||||
|
private RttClosingBalanceService $rttClosingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EmployeeRttPaymentInput
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EmployeeRttPaymentInput
|
||||||
@@ -51,10 +56,20 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
|||||||
throw new UnprocessableEntityHttpException('month must be between 1 and 12.');
|
throw new UnprocessableEntityHttpException('month must be between 1 and 12.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$year = $data->year ?? $this->resolveCurrentExerciseYear();
|
$year = $data->year ?? $this->resolveCurrentExerciseYear();
|
||||||
|
$currentExerciseYear = $this->resolveCurrentExerciseYear();
|
||||||
|
|
||||||
$this->assertYearAllowedForPayment($employee, $year);
|
$this->assertYearAllowedForPayment($employee, $year);
|
||||||
|
|
||||||
|
// Option B — retroactive payment on the previous exercise: the next exercise's
|
||||||
|
// opening report (a frozen snapshot) must be recomputed so the carry stays correct.
|
||||||
|
// Refuse upfront if that report has been locked (validated) by RH.
|
||||||
|
$downstreamBalance = null;
|
||||||
|
if ($year === $currentExerciseYear - 1) {
|
||||||
|
$downstreamBalance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $currentExerciseYear);
|
||||||
|
$this->assertReportNotLocked($downstreamBalance);
|
||||||
|
}
|
||||||
|
|
||||||
$payment = $this->rttPaymentRepository->findOneByEmployeeYearMonth($employee, $year, $data->month);
|
$payment = $this->rttPaymentRepository->findOneByEmployeeYearMonth($employee, $year, $data->month);
|
||||||
|
|
||||||
if (null === $payment) {
|
if (null === $payment) {
|
||||||
@@ -81,7 +96,24 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
|||||||
['new' => ['month' => $data->month, 'year' => $year, 'base25' => $data->base25Minutes, 'bonus25' => $data->bonus25Minutes, 'base50' => $data->base50Minutes, 'bonus50' => $data->bonus50Minutes]],
|
['new' => ['month' => $data->month, 'year' => $year, 'base25' => $data->base25Minutes, 'bonus25' => $data->bonus25Minutes, 'base50' => $data->base50Minutes, 'bonus50' => $data->bonus50Minutes]],
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->entityManager->flush();
|
// Persist the payment and, atomically, refresh the next exercise's opening report.
|
||||||
|
// The flush inside the transaction makes the new payment visible to the closing
|
||||||
|
// recomputation (same DB connection), so the carry reflects it.
|
||||||
|
$this->entityManager->wrapInTransaction(function () use ($employee, $year, $downstreamBalance): void {
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
if (null !== $downstreamBalance) {
|
||||||
|
$closing = $this->rttClosingService->computeClosingBalance($employee, $year);
|
||||||
|
$downstreamBalance
|
||||||
|
->setOpeningBase25Minutes($closing->base25Minutes)
|
||||||
|
->setOpeningBonus25Minutes($closing->bonus25Minutes)
|
||||||
|
->setOpeningBase50Minutes($closing->base50Minutes)
|
||||||
|
->setOpeningBonus50Minutes($closing->bonus50Minutes)
|
||||||
|
->touch()
|
||||||
|
;
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$data->year = $year;
|
$data->year = $year;
|
||||||
|
|
||||||
@@ -94,14 +126,15 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow payment when the requested exercise is either the current one
|
* Allow payment when the requested exercise is the current one, the
|
||||||
* or the last exercise of a closed contract phase (the one containing
|
* immediately previous one (retroactive payment — Option B), or the last
|
||||||
* the phase end date). Reject any other exercise (past or future).
|
* exercise of a closed contract phase (the one containing the phase end
|
||||||
|
* date). Reject any other exercise (older past or future).
|
||||||
*/
|
*/
|
||||||
private function assertYearAllowedForPayment(Employee $employee, int $year): void
|
private function assertYearAllowedForPayment(Employee $employee, int $year): void
|
||||||
{
|
{
|
||||||
$currentExerciseYear = $this->resolveCurrentExerciseYear();
|
$currentExerciseYear = $this->resolveCurrentExerciseYear();
|
||||||
if ($year === $currentExerciseYear) {
|
if ($year === $currentExerciseYear || $year === $currentExerciseYear - 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +149,21 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new UnprocessableEntityHttpException(
|
throw new UnprocessableEntityHttpException(
|
||||||
'RTT payment is only allowed on the current exercise or the last exercise of a closed contract phase.'
|
'RTT payment is only allowed on the current exercise, the previous one, or the last exercise of a closed contract phase.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refuse a retroactive payment when the next exercise's opening report has
|
||||||
|
* been locked (validated) by RH: recomputing it would either be impossible
|
||||||
|
* or silently desync the carry. A missing report (null) never blocks.
|
||||||
|
*/
|
||||||
|
private function assertReportNotLocked(?EmployeeRttBalance $downstreamBalance): void
|
||||||
|
{
|
||||||
|
if (null !== $downstreamBalance && $downstreamBalance->isLocked()) {
|
||||||
|
throw new UnprocessableEntityHttpException(
|
||||||
|
'Impossible : le report RTT de l\'exercice suivant est verrouillé. Déverrouillez-le pour saisir un paiement rétroactif.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use App\Repository\WorkHourRepository;
|
|||||||
use App\Security\EmployeeScopeService;
|
use App\Security\EmployeeScopeService;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
use App\Service\Exercise\ExerciseYearResolver;
|
use App\Service\Exercise\ExerciseYearResolver;
|
||||||
|
use App\Service\Rtt\RttClosingBalanceService;
|
||||||
use App\Service\Rtt\RttRecoveryComputationService;
|
use App\Service\Rtt\RttRecoveryComputationService;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
@@ -43,6 +44,7 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
|||||||
private WorkHourRepository $workHourRepository,
|
private WorkHourRepository $workHourRepository,
|
||||||
private EmployeeContractPhaseResolver $phaseResolver,
|
private EmployeeContractPhaseResolver $phaseResolver,
|
||||||
private ExerciseYearResolver $exerciseYearResolver,
|
private ExerciseYearResolver $exerciseYearResolver,
|
||||||
|
private RttClosingBalanceService $rttClosingService,
|
||||||
string $rttStartDate = '',
|
string $rttStartDate = '',
|
||||||
) {
|
) {
|
||||||
$this->rttStartDate = '' !== $rttStartDate ? $rttStartDate : null;
|
$this->rttStartDate = '' !== $rttStartDate ? $rttStartDate : null;
|
||||||
@@ -231,8 +233,12 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No stored report row yet (before the 1st-June rollover materialises it):
|
||||||
|
// compute the previous exercise's full closing (opening + earned − paid) so the
|
||||||
|
// carry already reflects retroactive payments and the incoming report — matching
|
||||||
|
// what the rollover would persist. Falling back to earned-only would drop both.
|
||||||
return [
|
return [
|
||||||
$this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $year - 1),
|
$this->rttClosingService->computeClosingBalance($employee, $year - 1),
|
||||||
5,
|
5,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace App\Tests\State;
|
|||||||
use App\Entity\Contract;
|
use App\Entity\Contract;
|
||||||
use App\Entity\Employee;
|
use App\Entity\Employee;
|
||||||
use App\Entity\EmployeeContractPeriod;
|
use App\Entity\EmployeeContractPeriod;
|
||||||
|
use App\Entity\EmployeeRttBalance;
|
||||||
use App\Enum\ContractNature;
|
use App\Enum\ContractNature;
|
||||||
use App\Enum\TrackingMode;
|
use App\Enum\TrackingMode;
|
||||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||||
@@ -74,6 +75,54 @@ final class EmployeeRttPaymentProcessorTest extends TestCase
|
|||||||
$this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2030);
|
$this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2030);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPaymentAllowedOnPreviousExercise(): void
|
||||||
|
{
|
||||||
|
// Today = 2026-05-19 → current exercise = 2026. Retroactive payment on the
|
||||||
|
// immediately previous exercise (2025) is now allowed (Option B).
|
||||||
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
||||||
|
$processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));
|
||||||
|
|
||||||
|
$this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2025);
|
||||||
|
|
||||||
|
// No exception → previous exercise accepted.
|
||||||
|
self::assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPaymentStillRejectedTwoExercisesBack(): void
|
||||||
|
{
|
||||||
|
// 2024 is two exercises before current (2026) and not a closed-phase end → still rejected.
|
||||||
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
||||||
|
$processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));
|
||||||
|
|
||||||
|
$this->expectException(UnprocessableEntityHttpException::class);
|
||||||
|
$this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2024);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRetroactivePaymentRefusedWhenDownstreamReportLocked(): void
|
||||||
|
{
|
||||||
|
$processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));
|
||||||
|
|
||||||
|
$locked = new EmployeeRttBalance();
|
||||||
|
$locked->setIsLocked(true);
|
||||||
|
|
||||||
|
$this->expectException(UnprocessableEntityHttpException::class);
|
||||||
|
$this->invokePrivate($processor, 'assertReportNotLocked', $locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRetroactivePaymentAllowedWhenDownstreamReportMissingOrUnlocked(): void
|
||||||
|
{
|
||||||
|
$processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));
|
||||||
|
|
||||||
|
$unlocked = new EmployeeRttBalance();
|
||||||
|
$unlocked->setIsLocked(false);
|
||||||
|
|
||||||
|
// Neither a missing (null) nor an unlocked downstream report must block payment.
|
||||||
|
$this->invokePrivate($processor, 'assertReportNotLocked', null);
|
||||||
|
$this->invokePrivate($processor, 'assertReportNotLocked', $unlocked);
|
||||||
|
|
||||||
|
self::assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Test harness helpers.
|
// Test harness helpers.
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user