feat(absence) : migrate Absence domain into module (back)

LST-66 (2.3) backend. Behaviour-preserving move of the absences domain into
src/Module/Absence/. API operations, securities, routes and the 10 MCP tool
names are unchanged.

- 3 entities + 3 enums moved to Domain/{Entity,Enum}; user relations stay on
  UserInterface. 3 repositories split into Domain/Repository interfaces +
  Doctrine impls (bound in services.yaml); find() kept off interfaces
  (findById instead).
- Pure services (AbsenceDayCalculator, PublicHolidayProvider) -> Domain/Service;
  AbsenceBalanceService -> Application/Service; State (5), controllers (5),
  10 MCP tools and AccrueLeaveCommand -> Infrastructure/.
- New LeaveProfileInterface contract (Shared) exposes the HR getters used by
  AbsenceBalanceService/AccrueLeaveCommand; User implements it -> Absence no
  longer imports the concrete Core User. MCP tools/command inject
  UserRepositoryInterface (findById) instead of the concrete repository.
- Timestampable/Blamable added to AbsenceBalance and AbsencePolicy (additive
  migration: created_at/updated_at + created_by/updated_by FK ON DELETE SET
  NULL + COMMENT). AbsenceRequest untouched (already has createdAt/reviewedAt).
- AbsenceModule registered (id absence, 4 RBAC perms, not re-wired); doctrine
  mapping added; team-absences sidebar item gated by the module.

161 tests green, mapping valid, no API route regression, cs-fixer clean.
This commit is contained in:
Matthieu
2026-06-20 18:32:02 +02:00
parent 7446b7dca9
commit 306cfd34cd
51 changed files with 514 additions and 209 deletions
+5 -5
View File
@@ -4,15 +4,15 @@ declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\AbsenceBalance;
use App\Entity\AbsencePolicy;
use App\Entity\AbsenceRequest;
use App\Entity\Client;
use App\Entity\MailConfiguration;
use App\Entity\ZimbraConfiguration;
use App\Enum\AbsenceStatus;
use App\Enum\AbsenceType;
use App\Enum\ContractType;
use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Entity\AbsencePolicy;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Core\Application\Rbac\RbacSeeder;
use App\Module\Core\Domain\Entity\User;
use App\Module\ProjectManagement\Domain\Entity\Project;
+3 -3
View File
@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace App\Mcp\Tool;
use App\Entity\AbsenceBalance;
use App\Entity\AbsencePolicy;
use App\Entity\AbsenceRequest;
use App\Entity\Client;
use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Entity\AbsencePolicy;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Core\Domain\Entity\User;
use App\Module\ProjectManagement\Domain\Entity\Project;
use App\Module\ProjectManagement\Domain\Entity\Task;
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Module\Absence;
use App\Shared\Domain\Module\ModuleInterface;
final class AbsenceModule implements ModuleInterface
{
public static function id(): string
{
return 'absence';
}
public static function label(): string
{
return 'Absences';
}
public static function isRequired(): bool
{
return false;
}
/**
* Permissions RBAC fin du Module Absence.
*
* Additif : alimente le catalogue RBAC. La sécurité des opérations API
* reste en ROLE_USER/ROLE_ADMIN (non recâblée ici).
*
* @return list<array{code: string, label: string}>
*/
public static function permissions(): array
{
return [
['code' => 'absence.requests.view', 'label' => 'Voir les demandes d\'absence'],
['code' => 'absence.requests.manage', 'label' => 'Gérer les demandes d\'absence'],
['code' => 'absence.policies.manage', 'label' => 'Gérer les règles d\'absence'],
['code' => 'absence.balances.manage', 'label' => 'Gérer les soldes d\'absence'],
];
}
}
@@ -2,13 +2,14 @@
declare(strict_types=1);
namespace App\Service;
namespace App\Module\Absence\Application\Service;
use App\Entity\AbsenceBalance;
use App\Entity\AbsenceRequest;
use App\Enum\AbsenceType;
use App\Module\Core\Domain\Entity\User;
use App\Repository\AbsenceBalanceRepository;
use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use App\Shared\Domain\Contract\LeaveProfileInterface;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface;
@@ -21,21 +22,24 @@ final readonly class AbsenceBalanceService
{
public function __construct(
private EntityManagerInterface $entityManager,
private AbsenceBalanceRepository $balanceRepository,
private AbsenceBalanceRepositoryInterface $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
public function periodFor(UserInterface $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"
$year = (int) $date->format('Y');
// The reference-period start (e.g. "06-01") is an HR profile field,
// accessed through the LeaveProfileInterface contract to keep the
// Absence module decoupled from the concrete Core User entity.
$startMonthDay = $user instanceof LeaveProfileInterface ? $user->getReferencePeriodStart() : '01-01';
$currentMonthDay = $date->format('m-d');
$startYear = $currentMonthDay >= $startMonthDay ? $year : $year - 1;
@@ -43,7 +47,7 @@ final readonly class AbsenceBalanceService
return sprintf('%d-%d', $startYear, $startYear + 1);
}
public function getOrCreateBalance(User $user, AbsenceType $type, string $period): AbsenceBalance
public function getOrCreateBalance(UserInterface $user, AbsenceType $type, string $period): AbsenceBalance
{
$balance = $this->balanceRepository->findOneForPeriod($user, $type, $period);
@@ -85,7 +89,7 @@ final readonly class AbsenceBalanceService
return null;
}
/** @var User $user */
/** @var UserInterface $user */
$user = $request->getUser();
$period = $this->periodFor($user, $request->getType(), $request->getStartDate());
$balance = $this->balanceRepository->findOneForPeriod($user, $request->getType(), $period);
@@ -128,7 +132,7 @@ final readonly class AbsenceBalanceService
private function balanceForRequest(AbsenceRequest $request): AbsenceBalance
{
/** @var User $user */
/** @var UserInterface $user */
$user = $request->getUser();
$type = $request->getType();
$period = $this->periodFor($user, $type, $request->getStartDate());
@@ -2,16 +2,19 @@
declare(strict_types=1);
namespace App\Entity;
namespace App\Module\Absence\Domain\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use App\Enum\AbsenceType;
use App\Repository\AbsenceBalanceRepository;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Infrastructure\ApiPlatform\State\AbsenceBalanceProvider;
use App\Module\Absence\Infrastructure\Doctrine\DoctrineAbsenceBalanceRepository;
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Contract\UserInterface;
use App\State\AbsenceBalanceProvider;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
@@ -35,11 +38,13 @@ use Symfony\Component\Serializer\Attribute\Groups;
normalizationContext: ['groups' => ['absence_balance:read']],
denormalizationContext: ['groups' => ['absence_balance:write']],
)]
#[ORM\Entity(repositoryClass: AbsenceBalanceRepository::class)]
#[ORM\Entity(repositoryClass: DoctrineAbsenceBalanceRepository::class)]
#[ORM\Table(name: 'absence_balance')]
#[ORM\UniqueConstraint(name: 'uniq_absence_balance_user_type_period', columns: ['user_id', 'type', 'period'])]
class AbsenceBalance
class AbsenceBalance implements TimestampableInterface, BlamableInterface
{
use TimestampableBlamableTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
@@ -2,14 +2,17 @@
declare(strict_types=1);
namespace App\Entity;
namespace App\Module\Absence\Domain\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use App\Enum\AbsenceType;
use App\Repository\AbsencePolicyRepository;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Infrastructure\Doctrine\DoctrineAbsencePolicyRepository;
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
@@ -31,11 +34,13 @@ use Symfony\Component\Serializer\Attribute\Groups;
denormalizationContext: ['groups' => ['absence_policy:write']],
order: ['type' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: AbsencePolicyRepository::class)]
#[ORM\Entity(repositoryClass: DoctrineAbsencePolicyRepository::class)]
#[ORM\Table(name: 'absence_policy')]
#[ORM\UniqueConstraint(name: 'uniq_absence_policy_type', columns: ['type'])]
class AbsencePolicy
class AbsencePolicy implements TimestampableInterface, BlamableInterface
{
use TimestampableBlamableTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Entity;
namespace App\Module\Absence\Domain\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
@@ -10,15 +10,15 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Enum\AbsenceStatus;
use App\Enum\AbsenceType;
use App\Enum\HalfDay;
use App\Repository\AbsenceRequestRepository;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Enum\HalfDay;
use App\Module\Absence\Infrastructure\ApiPlatform\State\AbsenceCancelProcessor;
use App\Module\Absence\Infrastructure\ApiPlatform\State\AbsenceRequestProcessor;
use App\Module\Absence\Infrastructure\ApiPlatform\State\AbsenceRequestProvider;
use App\Module\Absence\Infrastructure\ApiPlatform\State\AbsenceReviewProcessor;
use App\Module\Absence\Infrastructure\Doctrine\DoctrineAbsenceRequestRepository;
use App\Shared\Domain\Contract\UserInterface;
use App\State\AbsenceCancelProcessor;
use App\State\AbsenceRequestProcessor;
use App\State\AbsenceRequestProvider;
use App\State\AbsenceReviewProcessor;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
@@ -64,7 +64,7 @@ use Symfony\Component\Validator\Constraints as Assert;
denormalizationContext: ['groups' => ['absence_request:write']],
order: ['createdAt' => 'DESC'],
)]
#[ORM\Entity(repositoryClass: AbsenceRequestRepository::class)]
#[ORM\Entity(repositoryClass: DoctrineAbsenceRequestRepository::class)]
#[ORM\Table(name: 'absence_request')]
class AbsenceRequest
{
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enum;
namespace App\Module\Absence\Domain\Enum;
enum AbsenceStatus: string
{
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enum;
namespace App\Module\Absence\Domain\Enum;
enum AbsenceType: string
{
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enum;
namespace App\Module\Absence\Domain\Enum;
enum HalfDay: string
{
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Module\Absence\Domain\Repository;
use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Shared\Domain\Contract\UserInterface;
interface AbsenceBalanceRepositoryInterface
{
public function findById(int $id): ?AbsenceBalance;
/**
* @param array<string, mixed> $criteria
* @param null|array<string, string> $orderBy
*
* @return AbsenceBalance[]
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
public function findOneForPeriod(UserInterface $user, AbsenceType $type, string $period): ?AbsenceBalance;
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Module\Absence\Domain\Repository;
use App\Module\Absence\Domain\Entity\AbsencePolicy;
use App\Module\Absence\Domain\Enum\AbsenceType;
interface AbsencePolicyRepositoryInterface
{
public function findById(int $id): ?AbsencePolicy;
/**
* @param array<string, mixed> $criteria
* @param null|array<string, string> $orderBy
*
* @return AbsencePolicy[]
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
public function findOneByType(AbsenceType $type): ?AbsencePolicy;
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Module\Absence\Domain\Repository;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeInterface;
interface AbsenceRequestRepositoryInterface
{
public function findById(int $id): ?AbsenceRequest;
/**
* Whether the user already has a PENDING or APPROVED absence that overlaps
* the given date range.
*/
public function hasOverlap(
UserInterface $user,
DateTimeInterface $startDate,
DateTimeInterface $endDate,
?int $excludeId = null,
): bool;
/**
* Absences (approved or pending) overlapping a date range, all employees.
*
* @return AbsenceRequest[]
*/
public function findInRange(DateTimeInterface $from, DateTimeInterface $to): array;
/**
* @return AbsenceRequest[]
*/
public function findFiltered(
?UserInterface $user = null,
?AbsenceStatus $status = null,
?AbsenceType $type = null,
?DateTimeInterface $from = null,
?DateTimeInterface $to = null,
): array;
}
@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\Service;
namespace App\Module\Absence\Domain\Service;
use App\Enum\HalfDay;
use App\Module\Absence\Domain\Enum\HalfDay;
use DateInterval;
use DatePeriod;
use DateTimeImmutable;
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Service;
namespace App\Module\Absence\Domain\Service;
use DateTimeImmutable;
use DateTimeInterface;
@@ -2,11 +2,12 @@
declare(strict_types=1);
namespace App\State;
namespace App\Module\Absence\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use App\Shared\Domain\Contract\UserInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
@@ -18,6 +19,7 @@ final readonly class AbsenceBalanceProvider implements ProviderInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private AbsenceBalanceRepositoryInterface $balanceRepository,
private Security $security,
) {}
@@ -26,11 +28,10 @@ final readonly class AbsenceBalanceProvider implements ProviderInterface
$user = $this->security->getUser();
assert($user instanceof UserInterface);
$repo = $this->entityManager->getRepository(AbsenceBalance::class);
$isAdmin = $this->security->isGranted('ROLE_ADMIN');
if (isset($uriVariables['id'])) {
$balance = $repo->find($uriVariables['id']);
$balance = $this->balanceRepository->findById((int) $uriVariables['id']);
if (null === $balance) {
return null;
}
@@ -41,7 +42,8 @@ final readonly class AbsenceBalanceProvider implements ProviderInterface
return $balance;
}
$qb = $repo->createQueryBuilder('b')
$qb = $this->entityManager->getRepository(AbsenceBalance::class)
->createQueryBuilder('b')
->orderBy('b.type', 'ASC')
;
@@ -2,13 +2,13 @@
declare(strict_types=1);
namespace App\State;
namespace App\Module\Absence\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\AbsenceRequest;
use App\Enum\AbsenceStatus;
use App\Service\AbsenceBalanceService;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -2,17 +2,17 @@
declare(strict_types=1);
namespace App\State;
namespace App\Module\Absence\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\AbsenceRequest;
use App\Enum\AbsenceStatus;
use App\Enum\AbsenceType;
use App\Repository\AbsencePolicyRepository;
use App\Repository\AbsenceRequestRepository;
use App\Service\AbsenceBalanceService;
use App\Service\AbsenceDayCalculator;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsencePolicyRepositoryInterface;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use App\Module\Absence\Domain\Service\AbsenceDayCalculator;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
@@ -32,8 +32,8 @@ final readonly class AbsenceRequestProcessor implements ProcessorInterface
private EntityManagerInterface $entityManager,
private Security $security,
private AbsenceDayCalculator $calculator,
private AbsencePolicyRepository $policyRepository,
private AbsenceRequestRepository $requestRepository,
private AbsencePolicyRepositoryInterface $policyRepository,
private AbsenceRequestRepositoryInterface $requestRepository,
private AbsenceBalanceService $balanceService,
) {}
@@ -2,11 +2,12 @@
declare(strict_types=1);
namespace App\State;
namespace App\Module\Absence\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use App\Shared\Domain\Contract\UserInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
@@ -18,6 +19,7 @@ final readonly class AbsenceRequestProvider implements ProviderInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private AbsenceRequestRepositoryInterface $requestRepository,
private Security $security,
) {}
@@ -26,12 +28,11 @@ final readonly class AbsenceRequestProvider implements ProviderInterface
$user = $this->security->getUser();
assert($user instanceof UserInterface);
$repo = $this->entityManager->getRepository(AbsenceRequest::class);
$isAdmin = $this->security->isGranted('ROLE_ADMIN');
// Single item: owner or admin only
if (isset($uriVariables['id'])) {
$request = $repo->find($uriVariables['id']);
$request = $this->requestRepository->findById((int) $uriVariables['id']);
if (null === $request) {
return null;
}
@@ -42,7 +43,8 @@ final readonly class AbsenceRequestProvider implements ProviderInterface
return $request;
}
$qb = $repo->createQueryBuilder('a')
$qb = $this->entityManager->getRepository(AbsenceRequest::class)
->createQueryBuilder('a')
->orderBy('a.createdAt', 'DESC')
;
@@ -2,13 +2,13 @@
declare(strict_types=1);
namespace App\State;
namespace App\Module\Absence\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\AbsenceRequest;
use App\Enum\AbsenceStatus;
use App\Service\AbsenceBalanceService;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
@@ -2,12 +2,13 @@
declare(strict_types=1);
namespace App\Command;
namespace App\Module\Absence\Infrastructure\Command;
use App\Enum\AbsenceType;
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
use App\Repository\AbsenceBalanceRepository;
use App\Service\AbsenceBalanceService;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
use App\Shared\Domain\Contract\LeaveProfileInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
@@ -37,8 +38,8 @@ use function sprintf;
class AccrueLeaveCommand extends Command
{
public function __construct(
private readonly DoctrineUserRepository $userRepository,
private readonly AbsenceBalanceRepository $balanceRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly AbsenceBalanceRepositoryInterface $balanceRepository,
private readonly AbsenceBalanceService $balanceService,
private readonly EntityManagerInterface $entityManager,
) {
@@ -88,7 +89,14 @@ class AccrueLeaveCommand extends Command
$skipped = 0;
foreach ($employees as $user) {
$rate = ($user->getAnnualLeaveDays() / 12) * $user->getWorkTimeRatio();
// RH leave profile fields are read through the contract to keep the
// Absence module decoupled from the concrete Core User entity.
$profile = $user instanceof LeaveProfileInterface ? $user : null;
if (null === $profile) {
continue;
}
$rate = ($profile->getAnnualLeaveDays() / 12) * $profile->getWorkTimeRatio();
$period = $this->balanceService->periodFor($user, AbsenceType::PaidLeave, $firstDay);
$balance = $this->balanceRepository->findOneForPeriod($user, AbsenceType::PaidLeave, $period);
@@ -104,7 +112,7 @@ class AccrueLeaveCommand extends Command
? $this->balanceRepository->findOneForPeriod($user, AbsenceType::PaidLeave, $previousPeriod)
: null;
$balance->setAcquired(
null !== $previousBalance ? $previousBalance->getAcquiring() : $user->getInitialLeaveBalance(),
null !== $previousBalance ? $previousBalance->getAcquiring() : $profile->getInitialLeaveBalance(),
);
}
@@ -119,7 +127,7 @@ class AccrueLeaveCommand extends Command
$balance->setLastAccruedMonth($monthKey);
++$accrued;
$seeded = $isNew && (null !== self::previousPeriod($period) || $user->getInitialLeaveBalance() > 0);
$seeded = $isNew && (null !== self::previousPeriod($period) || $profile->getInitialLeaveBalance() > 0);
$rows[] = [
$user->getUsername(),
$period,
@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\Controller\Absence;
namespace App\Module\Absence\Infrastructure\Controller;
use App\Repository\AbsenceRequestRepository;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -19,7 +19,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class AbsenceCalendarController extends AbstractController
{
public function __construct(
private readonly AbsenceRequestRepository $requestRepository,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
) {}
#[Route('/api/admin/absences/calendar', name: 'absence_calendar', methods: ['GET'], priority: 1)]
@@ -2,10 +2,9 @@
declare(strict_types=1);
namespace App\Controller\Absence;
namespace App\Module\Absence\Infrastructure\Controller;
use App\Entity\AbsenceRequest;
use Doctrine\ORM\EntityManagerInterface;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
@@ -21,7 +20,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class AbsenceJustificationDownloadController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
private readonly Security $security,
private readonly string $uploadDir,
) {}
@@ -30,7 +29,7 @@ class AbsenceJustificationDownloadController extends AbstractController
#[IsGranted('ROLE_USER')]
public function __invoke(int $id): BinaryFileResponse
{
$absence = $this->entityManager->getRepository(AbsenceRequest::class)->find($id);
$absence = $this->requestRepository->findById($id);
if (null === $absence) {
throw new NotFoundHttpException('Absence request not found.');
}
@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\Controller\Absence;
namespace App\Module\Absence\Infrastructure\Controller;
use App\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
@@ -34,6 +34,7 @@ class AbsenceJustificationUploadController extends AbstractController
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
private readonly Security $security,
private readonly string $uploadDir,
) {}
@@ -42,7 +43,7 @@ class AbsenceJustificationUploadController extends AbstractController
#[IsGranted('ROLE_USER')]
public function __invoke(int $id, Request $request): JsonResponse
{
$absence = $this->entityManager->getRepository(AbsenceRequest::class)->find($id);
$absence = $this->requestRepository->findById($id);
if (null === $absence) {
throw new NotFoundHttpException('Absence request not found.');
}
@@ -2,14 +2,14 @@
declare(strict_types=1);
namespace App\Controller\Absence;
namespace App\Module\Absence\Infrastructure\Controller;
use App\Enum\AbsenceType;
use App\Enum\HalfDay;
use App\Repository\AbsenceBalanceRepository;
use App\Repository\AbsencePolicyRepository;
use App\Service\AbsenceBalanceService;
use App\Service\AbsenceDayCalculator;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Enum\HalfDay;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use App\Module\Absence\Domain\Repository\AbsencePolicyRepositoryInterface;
use App\Module\Absence\Domain\Service\AbsenceDayCalculator;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -30,8 +30,8 @@ class AbsencePreviewController extends AbstractController
public function __construct(
private readonly Security $security,
private readonly AbsenceDayCalculator $calculator,
private readonly AbsencePolicyRepository $policyRepository,
private readonly AbsenceBalanceRepository $balanceRepository,
private readonly AbsencePolicyRepositoryInterface $policyRepository,
private readonly AbsenceBalanceRepositoryInterface $balanceRepository,
private readonly AbsenceBalanceService $balanceService,
) {}
@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\Controller\Absence;
namespace App\Module\Absence\Infrastructure\Controller;
use App\Service\PublicHolidayProvider;
use App\Module\Absence\Domain\Service\PublicHolidayProvider;
use DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -2,10 +2,11 @@
declare(strict_types=1);
namespace App\Repository;
namespace App\Module\Absence\Infrastructure\Doctrine;
use App\Entity\AbsenceBalance;
use App\Enum\AbsenceType;
use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use App\Shared\Domain\Contract\UserInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@@ -13,13 +14,18 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AbsenceBalance>
*/
class AbsenceBalanceRepository extends ServiceEntityRepository
class DoctrineAbsenceBalanceRepository extends ServiceEntityRepository implements AbsenceBalanceRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AbsenceBalance::class);
}
public function findById(int $id): ?AbsenceBalance
{
return $this->find($id);
}
public function findOneForPeriod(UserInterface $user, AbsenceType $type, string $period): ?AbsenceBalance
{
return $this->findOneBy([
@@ -2,23 +2,29 @@
declare(strict_types=1);
namespace App\Repository;
namespace App\Module\Absence\Infrastructure\Doctrine;
use App\Entity\AbsencePolicy;
use App\Enum\AbsenceType;
use App\Module\Absence\Domain\Entity\AbsencePolicy;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsencePolicyRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AbsencePolicy>
*/
class AbsencePolicyRepository extends ServiceEntityRepository
class DoctrineAbsencePolicyRepository extends ServiceEntityRepository implements AbsencePolicyRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AbsencePolicy::class);
}
public function findById(int $id): ?AbsencePolicy
{
return $this->find($id);
}
public function findOneByType(AbsenceType $type): ?AbsencePolicy
{
return $this->findOneBy(['type' => $type]);
@@ -2,11 +2,12 @@
declare(strict_types=1);
namespace App\Repository;
namespace App\Module\Absence\Infrastructure\Doctrine;
use App\Entity\AbsenceRequest;
use App\Enum\AbsenceStatus;
use App\Enum\AbsenceType;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
@@ -15,13 +16,18 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AbsenceRequest>
*/
class AbsenceRequestRepository extends ServiceEntityRepository
class DoctrineAbsenceRequestRepository extends ServiceEntityRepository implements AbsenceRequestRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AbsenceRequest::class);
}
public function findById(int $id): ?AbsenceRequest
{
return $this->find($id);
}
/**
* Whether the user already has a PENDING or APPROVED absence that overlaps
* the given date range. Two ranges overlap when start_a <= end_b and
@@ -2,12 +2,12 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Enum\AbsenceStatus;
use App\Mcp\Tool\Serializer;
use App\Repository\AbsenceRequestRepository;
use App\Service\AbsenceBalanceService;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Mcp\Capability\Attribute\McpTool;
@@ -21,7 +21,7 @@ class CancelAbsenceRequestTool
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly AbsenceRequestRepository $requestRepository,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
private readonly AbsenceBalanceService $balanceService,
private readonly Security $security,
) {}
@@ -32,7 +32,7 @@ class CancelAbsenceRequestTool
throw new AccessDeniedException('Access denied: ROLE_USER required.');
}
$request = $this->requestRepository->find($id);
$request = $this->requestRepository->findById($id);
if (null === $request) {
throw new InvalidArgumentException(sprintf('AbsenceRequest with ID %d not found.', $id));
}
@@ -2,18 +2,18 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Entity\AbsenceRequest;
use App\Enum\AbsenceStatus;
use App\Enum\AbsenceType;
use App\Enum\HalfDay;
use App\Mcp\Tool\Serializer;
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
use App\Repository\AbsencePolicyRepository;
use App\Repository\AbsenceRequestRepository;
use App\Service\AbsenceBalanceService;
use App\Service\AbsenceDayCalculator;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Enum\HalfDay;
use App\Module\Absence\Domain\Repository\AbsencePolicyRepositoryInterface;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use App\Module\Absence\Domain\Service\AbsenceDayCalculator;
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
@@ -28,9 +28,9 @@ class CreateAbsenceRequestTool
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly DoctrineUserRepository $userRepository,
private readonly AbsencePolicyRepository $policyRepository,
private readonly AbsenceRequestRepository $requestRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly AbsencePolicyRepositoryInterface $policyRepository,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
private readonly AbsenceDayCalculator $calculator,
private readonly AbsenceBalanceService $balanceService,
private readonly Security $security,
@@ -49,7 +49,7 @@ class CreateAbsenceRequestTool
throw new AccessDeniedException('Access denied: ROLE_USER required.');
}
$user = $this->userRepository->find($userId)
$user = $this->userRepository->findById($userId)
?? throw new InvalidArgumentException(sprintf('User with ID %d not found.', $userId));
$typeEnum = AbsenceType::tryFrom($type)
@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Repository\AbsenceRequestRepository;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Mcp\Capability\Attribute\McpTool;
@@ -17,7 +17,7 @@ use function sprintf;
class DeleteAbsenceRequestTool
{
public function __construct(
private readonly AbsenceRequestRepository $requestRepository,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
) {}
@@ -28,7 +28,7 @@ class DeleteAbsenceRequestTool
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
}
$request = $this->requestRepository->find($id);
$request = $this->requestRepository->findById($id);
if (null === $request) {
throw new InvalidArgumentException(sprintf('AbsenceRequest with ID %d not found.', $id));
}
@@ -2,10 +2,10 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Repository\AbsenceRequestRepository;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use InvalidArgumentException;
use Mcp\Capability\Attribute\McpTool;
use Symfony\Bundle\SecurityBundle\Security;
@@ -17,7 +17,7 @@ use function sprintf;
class GetAbsenceRequestTool
{
public function __construct(
private readonly AbsenceRequestRepository $requestRepository,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
private readonly Security $security,
) {}
@@ -27,7 +27,7 @@ class GetAbsenceRequestTool
throw new AccessDeniedException('Access denied: ROLE_USER required.');
}
$request = $this->requestRepository->find($id);
$request = $this->requestRepository->findById($id);
if (null === $request) {
throw new InvalidArgumentException(sprintf('AbsenceRequest with ID %d not found.', $id));
}
@@ -2,12 +2,12 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Enum\AbsenceType;
use App\Mcp\Tool\Serializer;
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
use App\Repository\AbsenceBalanceRepository;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
use InvalidArgumentException;
use Mcp\Capability\Attribute\McpTool;
use Symfony\Bundle\SecurityBundle\Security;
@@ -19,8 +19,8 @@ use function sprintf;
class ListAbsenceBalancesTool
{
public function __construct(
private readonly AbsenceBalanceRepository $balanceRepository,
private readonly DoctrineUserRepository $userRepository,
private readonly AbsenceBalanceRepositoryInterface $balanceRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly Security $security,
) {}
@@ -32,7 +32,7 @@ class ListAbsenceBalancesTool
$criteria = [];
if (null !== $userId) {
$user = $this->userRepository->find($userId);
$user = $this->userRepository->findById($userId);
if (null === $user) {
throw new InvalidArgumentException(sprintf('User with ID %d not found.', $userId));
}
@@ -2,10 +2,10 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Repository\AbsencePolicyRepository;
use App\Module\Absence\Domain\Repository\AbsencePolicyRepositoryInterface;
use Mcp\Capability\Attribute\McpTool;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
@@ -14,7 +14,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class ListAbsencePoliciesTool
{
public function __construct(
private readonly AbsencePolicyRepository $policyRepository,
private readonly AbsencePolicyRepositoryInterface $policyRepository,
private readonly Security $security,
) {}
@@ -2,13 +2,13 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Enum\AbsenceStatus;
use App\Enum\AbsenceType;
use App\Mcp\Tool\Serializer;
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
use App\Repository\AbsenceRequestRepository;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
use DateTimeImmutable;
use InvalidArgumentException;
use Mcp\Capability\Attribute\McpTool;
@@ -21,8 +21,8 @@ use function sprintf;
class ListAbsenceRequestsTool
{
public function __construct(
private readonly AbsenceRequestRepository $requestRepository,
private readonly DoctrineUserRepository $userRepository,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly Security $security,
) {}
@@ -39,7 +39,7 @@ class ListAbsenceRequestsTool
$user = null;
if (null !== $userId) {
$user = $this->userRepository->find($userId)
$user = $this->userRepository->findById($userId)
?? throw new InvalidArgumentException(sprintf('User with ID %d not found.', $userId));
}
@@ -2,12 +2,12 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Enum\AbsenceStatus;
use App\Mcp\Tool\Serializer;
use App\Repository\AbsenceRequestRepository;
use App\Service\AbsenceBalanceService;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
@@ -24,7 +24,7 @@ class ReviewAbsenceRequestTool
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly AbsenceRequestRepository $requestRepository,
private readonly AbsenceRequestRepositoryInterface $requestRepository,
private readonly AbsenceBalanceService $balanceService,
private readonly Security $security,
) {}
@@ -39,7 +39,7 @@ class ReviewAbsenceRequestTool
throw new InvalidArgumentException('decision must be "approve" or "reject".');
}
$request = $this->requestRepository->find($id);
$request = $this->requestRepository->findById($id);
if (null === $request) {
throw new InvalidArgumentException(sprintf('AbsenceRequest with ID %d not found.', $id));
}
@@ -2,10 +2,10 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Repository\AbsenceBalanceRepository;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Mcp\Capability\Attribute\McpTool;
@@ -18,7 +18,7 @@ use function sprintf;
class UpdateAbsenceBalanceTool
{
public function __construct(
private readonly AbsenceBalanceRepository $balanceRepository,
private readonly AbsenceBalanceRepositoryInterface $balanceRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
) {}
@@ -33,7 +33,7 @@ class UpdateAbsenceBalanceTool
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
}
$balance = $this->balanceRepository->find($id);
$balance = $this->balanceRepository->findById($id);
if (null === $balance) {
throw new InvalidArgumentException(sprintf('AbsenceBalance with ID %d not found.', $id));
}
@@ -2,10 +2,10 @@
declare(strict_types=1);
namespace App\Mcp\Tool\Absence;
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Repository\AbsencePolicyRepository;
use App\Module\Absence\Domain\Repository\AbsencePolicyRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Mcp\Capability\Attribute\McpTool;
@@ -18,7 +18,7 @@ use function sprintf;
class UpdateAbsencePolicyTool
{
public function __construct(
private readonly AbsencePolicyRepository $policyRepository,
private readonly AbsencePolicyRepositoryInterface $policyRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
) {}
@@ -36,7 +36,7 @@ class UpdateAbsencePolicyTool
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
}
$policy = $this->policyRepository->find($id);
$policy = $this->policyRepository->findById($id);
if (null === $policy) {
throw new InvalidArgumentException(sprintf('AbsencePolicy with ID %d not found.', $id));
}
+2 -1
View File
@@ -18,6 +18,7 @@ use App\Module\Core\Infrastructure\ApiPlatform\State\UserPasswordHasherProcessor
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
use App\Shared\Domain\Attribute\Auditable;
use App\Shared\Domain\Attribute\AuditIgnore;
use App\Shared\Domain\Contract\LeaveProfileInterface;
use App\Shared\Domain\Contract\UserInterface as SharedUserInterface;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -63,7 +64,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[Auditable]
#[ORM\Entity(repositoryClass: DoctrineUserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface, PasswordAuthenticatedUserInterface, SharedUserInterface
class User implements UserInterface, PasswordAuthenticatedUserInterface, SharedUserInterface, LeaveProfileInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
@@ -9,6 +9,8 @@ use DateTimeInterface;
interface UserRepositoryInterface
{
public function findById(int $id): ?UserInterface;
/**
* @return list<UserInterface>
*/
@@ -21,6 +21,11 @@ class DoctrineUserRepository extends ServiceEntityRepository implements UserRepo
parent::__construct($registry, User::class);
}
public function findById(int $id): ?UserInterface
{
return $this->find($id);
}
/**
* @return list<UserInterface>
*/
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Contract;
/**
* HR leave profile of an employee, consumed by the Absence module without
* coupling it to the concrete Core User entity.
*
* Exposes only the RH getters actually used by AbsenceBalanceService and
* AccrueLeaveCommand. This is a service-level typing contract, not a Doctrine
* relation (no resolve_target_entities entry).
*/
interface LeaveProfileInterface
{
public function getWorkTimeRatio(): float;
public function getAnnualLeaveDays(): float;
public function getReferencePeriodStart(): string;
public function getInitialLeaveBalance(): float;
}