employeeRepository->find($employeeId); if (!$employee instanceof Employee) { throw new NotFoundHttpException('Employee not found.'); } if ($data->month < 1 || $data->month > 12) { throw new UnprocessableEntityHttpException('month must be between 1 and 12.'); } $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) { $payment = new EmployeeRttPayment(); $payment->setEmployee($employee); $payment->setYear($year); $payment->setMonth($data->month); $this->entityManager->persist($payment); } $payment->setBase25Minutes($data->base25Minutes); $payment->setBonus25Minutes($data->bonus25Minutes); $payment->setBase50Minutes($data->base50Minutes); $payment->setBonus50Minutes($data->bonus50Minutes); $payment->touch(); $empName = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? '')); $this->auditLogger->log( $employee, 'update', 'rtt_payment', $payment->getId(), sprintf('Paiement RTT modifié pour %s (%02d/%d)', $empName, $data->month, $year), ['new' => ['month' => $data->month, 'year' => $year, 'base25' => $data->base25Minutes, 'bonus25' => $data->bonus25Minutes, 'base50' => $data->base50Minutes, 'bonus50' => $data->bonus50Minutes]], ); // 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; return $data; } private function resolveCurrentExerciseYear(): int { return $this->exerciseYearResolver->forDate($this->clock->now()); } /** * 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 || $year === $currentExerciseYear - 1) { return; } $phases = $this->phaseResolver->resolvePhases($employee); foreach ($phases as $phase) { if ($phase->isCurrent || null === $phase->endDate) { continue; } if ($year === $this->exerciseYearResolver->forDate($phase->endDate)) { return; } } throw new UnprocessableEntityHttpException( '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.' ); } } }