feat(rtt) : paiement RTT rétroactif sur l'exercice précédent (#23)
Auto Tag Develop / tag (push) Successful in 7s
Auto Tag Develop / tag (push) Successful in 7s
## Besoin RH Pouvoir saisir un paiement RTT sur l'exercice précédent (ex. RTT de mai réglés après la bascule du 1er juin). ## Implémentation (Option B) - Paiement autorisé sur l'exercice courant + l'exercice immédiatement précédent (N-1). - Après saisie sur N-1, le report d'ouverture de l'exercice courant est recalculé automatiquement (computeClosingBalance) dans une transaction → aucun double comptage. - Refus si ce report est verrouillé (is_locked) : la RH le déverrouille d'abord. - Fallback EmployeeRttSummaryProvider::resolveCarry aligné sur computeClosingBalance : disponible correct même sans ligne stockée. - Front : bouton « + Payer les RTT » actif sur l'exercice précédent. - Docs : CLAUDE.md, doc/rtt-tab.md, documentation-content.ts. ## Vérification - ✅ 172 tests OK, cs-fixer OK, conteneur compile. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #23 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #23.
This commit is contained in:
@@ -8,12 +8,15 @@ use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\ApiResource\EmployeeRttPaymentInput;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeRttBalance;
|
||||
use App\Entity\EmployeeRttPayment;
|
||||
use App\Repository\EmployeeRepository;
|
||||
use App\Repository\EmployeeRttBalanceRepository;
|
||||
use App\Repository\EmployeeRttPaymentRepository;
|
||||
use App\Service\AuditLogger;
|
||||
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
||||
use App\Service\Exercise\ExerciseYearResolver;
|
||||
use App\Service\Rtt\RttClosingBalanceService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
@@ -24,11 +27,13 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
||||
public function __construct(
|
||||
private EmployeeRepository $employeeRepository,
|
||||
private EmployeeRttPaymentRepository $rttPaymentRepository,
|
||||
private EmployeeRttBalanceRepository $rttBalanceRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private AuditLogger $auditLogger,
|
||||
private EmployeeContractPhaseResolver $phaseResolver,
|
||||
private ClockInterface $clock,
|
||||
private ExerciseYearResolver $exerciseYearResolver,
|
||||
private RttClosingBalanceService $rttClosingService,
|
||||
) {}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
$year = $data->year ?? $this->resolveCurrentExerciseYear();
|
||||
$year = $data->year ?? $this->resolveCurrentExerciseYear();
|
||||
$currentExerciseYear = $this->resolveCurrentExerciseYear();
|
||||
|
||||
$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);
|
||||
|
||||
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]],
|
||||
);
|
||||
|
||||
$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;
|
||||
|
||||
@@ -94,14 +126,15 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow payment when the requested exercise is either the current one
|
||||
* or the last exercise of a closed contract phase (the one containing
|
||||
* the phase end date). Reject any other exercise (past or future).
|
||||
* Allow payment when the requested exercise is the current one, the
|
||||
* immediately previous one (retroactive payment — Option B), or the last
|
||||
* 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
|
||||
{
|
||||
$currentExerciseYear = $this->resolveCurrentExerciseYear();
|
||||
if ($year === $currentExerciseYear) {
|
||||
if ($year === $currentExerciseYear || $year === $currentExerciseYear - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,7 +149,21 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
||||
}
|
||||
|
||||
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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user