Module type Payfit (étapes 1+2 de la spec V1) : demande d'absence, validation admin, soldes à jour. - Enums : AbsenceType, AbsenceStatus, HalfDay, ContractType, FamilySituation - Entités : AbsencePolicy, AbsenceBalance, AbsenceRequest + champs RH sur User - Services : PublicHolidayProvider (fériés FR métropole en PHP pur, Computus), AbsenceDayCalculator (décompte jours ouvrés/ouvrables + demi-journées, TDD), AbsenceBalanceService (périodes + pending/taken/recrédit) - API Platform : providers/processors (création, approve/reject/cancel) + RBAC me/admin, contrôleurs preview (dry-run), upload/download justificatif, calendrier - Migrations : une par table + colonnes RH user (DEFAULT puis DROP DEFAULT) - Fixtures : 5 policies par défaut, salariés démo, soldes et demandes - Tests unitaires : PublicHolidayProvider, AbsenceDayCalculator (12 tests) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
3.9 KiB
PHP
123 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
use App\Entity\AbsenceBalance;
|
|
use App\Entity\AbsenceRequest;
|
|
use App\Entity\User;
|
|
use App\Enum\AbsenceType;
|
|
use App\Repository\AbsenceBalanceRepository;
|
|
use DateTimeInterface;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
|
|
/**
|
|
* Maintains per-employee leave balances as absence requests move through their
|
|
* lifecycle: a PENDING request reserves days in `pending`, an APPROVED one
|
|
* moves them to `taken`, and a cancellation gives them back.
|
|
*/
|
|
final readonly class AbsenceBalanceService
|
|
{
|
|
public function __construct(
|
|
private EntityManagerInterface $entityManager,
|
|
private AbsenceBalanceRepository $balanceRepository,
|
|
) {}
|
|
|
|
/**
|
|
* Reference period string for a request: paid leave follows the employee's
|
|
* reference period (e.g. "2025-2026"), other types are tracked yearly.
|
|
*/
|
|
public function periodFor(User $user, AbsenceType $type, DateTimeInterface $date): string
|
|
{
|
|
if (AbsenceType::PaidLeave !== $type) {
|
|
return $date->format('Y');
|
|
}
|
|
|
|
$year = (int) $date->format('Y');
|
|
$startMonthDay = $user->getReferencePeriodStart(); // e.g. "06-01"
|
|
$currentMonthDay = $date->format('m-d');
|
|
|
|
$startYear = $currentMonthDay >= $startMonthDay ? $year : $year - 1;
|
|
|
|
return sprintf('%d-%d', $startYear, $startYear + 1);
|
|
}
|
|
|
|
public function getOrCreateBalance(User $user, AbsenceType $type, string $period): AbsenceBalance
|
|
{
|
|
$balance = $this->balanceRepository->findOneForPeriod($user, $type, $period);
|
|
|
|
if (null === $balance) {
|
|
$balance = new AbsenceBalance()
|
|
->setUser($user)
|
|
->setType($type)
|
|
->setPeriod($period)
|
|
;
|
|
$this->entityManager->persist($balance);
|
|
}
|
|
|
|
return $balance;
|
|
}
|
|
|
|
/** Reserve the requested days in the PENDING bucket. */
|
|
public function reservePending(AbsenceRequest $request): void
|
|
{
|
|
if (!$this->shouldTrack($request)) {
|
|
return;
|
|
}
|
|
|
|
$balance = $this->balanceForRequest($request);
|
|
$balance->setPending($balance->getPending() + $request->getCountedDays());
|
|
}
|
|
|
|
/** Move reserved days from PENDING to TAKEN on approval. */
|
|
public function applyApproval(AbsenceRequest $request): void
|
|
{
|
|
if (!$this->shouldTrack($request)) {
|
|
return;
|
|
}
|
|
|
|
$balance = $this->balanceForRequest($request);
|
|
$balance->setPending(max(0.0, $balance->getPending() - $request->getCountedDays()));
|
|
$balance->setTaken($balance->getTaken() + $request->getCountedDays());
|
|
}
|
|
|
|
/**
|
|
* Give days back when a request is cancelled or rejected.
|
|
*
|
|
* @param bool $wasApproved true if the request had already been approved
|
|
* (days were in TAKEN), false if still PENDING
|
|
*/
|
|
public function release(AbsenceRequest $request, bool $wasApproved): void
|
|
{
|
|
if (!$this->shouldTrack($request)) {
|
|
return;
|
|
}
|
|
|
|
$balance = $this->balanceForRequest($request);
|
|
|
|
if ($wasApproved) {
|
|
$balance->setTaken(max(0.0, $balance->getTaken() - $request->getCountedDays()));
|
|
} else {
|
|
$balance->setPending(max(0.0, $balance->getPending() - $request->getCountedDays()));
|
|
}
|
|
}
|
|
|
|
private function balanceForRequest(AbsenceRequest $request): AbsenceBalance
|
|
{
|
|
/** @var User $user */
|
|
$user = $request->getUser();
|
|
$type = $request->getType();
|
|
$period = $this->periodFor($user, $type, $request->getStartDate());
|
|
|
|
return $this->getOrCreateBalance($user, $type, $period);
|
|
}
|
|
|
|
private function shouldTrack(AbsenceRequest $request): bool
|
|
{
|
|
$type = $request->getType();
|
|
|
|
return null !== $type && $type->decrementsBalance() && null !== $request->getUser();
|
|
}
|
|
}
|