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:
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
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;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'cancel-absence-request', description: 'Cancel an absence request. A PENDING request releases its reserved days; an APPROVED request can only be cancelled by an admin and credits the taken days back. Already cancelled/rejected requests cannot be cancelled.')]
|
||||
class CancelAbsenceRequestTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly AbsenceRequestRepositoryInterface $requestRepository,
|
||||
private readonly AbsenceBalanceService $balanceService,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(int $id): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
||||
}
|
||||
|
||||
$request = $this->requestRepository->findById($id);
|
||||
if (null === $request) {
|
||||
throw new InvalidArgumentException(sprintf('AbsenceRequest with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
$isAdmin = $this->security->isGranted('ROLE_ADMIN');
|
||||
$status = $request->getStatus();
|
||||
|
||||
if (AbsenceStatus::Pending === $status) {
|
||||
$this->balanceService->release($request, false);
|
||||
} elseif (AbsenceStatus::Approved === $status) {
|
||||
if (!$isAdmin) {
|
||||
throw new AccessDeniedException('Only an admin can cancel an approved request.');
|
||||
}
|
||||
$this->balanceService->release($request, true);
|
||||
} else {
|
||||
throw new InvalidArgumentException('This request can no longer be cancelled.');
|
||||
}
|
||||
|
||||
$request->setStatus(AbsenceStatus::Cancelled);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::absenceRequest($request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
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;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'create-absence-request', description: 'Create an absence request on behalf of an employee (userId). Validates active policy + no overlap, computes deducted working days, sets status=pending and reserves the days in the pending balance. type: cp|mariage_pacs|conge_parental|deces|maladie. Dates YYYY-MM-DD. halfDay (matin|apres_midi) on a boundary subtracts 0.5.')]
|
||||
class CreateAbsenceRequestTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserRepositoryInterface $userRepository,
|
||||
private readonly AbsencePolicyRepositoryInterface $policyRepository,
|
||||
private readonly AbsenceRequestRepositoryInterface $requestRepository,
|
||||
private readonly AbsenceDayCalculator $calculator,
|
||||
private readonly AbsenceBalanceService $balanceService,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
int $userId,
|
||||
string $type,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
?string $startHalfDay = null,
|
||||
?string $endHalfDay = null,
|
||||
?string $reason = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
||||
}
|
||||
|
||||
$user = $this->userRepository->findById($userId)
|
||||
?? throw new InvalidArgumentException(sprintf('User with ID %d not found.', $userId));
|
||||
|
||||
$typeEnum = AbsenceType::tryFrom($type)
|
||||
?? throw new InvalidArgumentException(sprintf('Unknown absence type "%s".', $type));
|
||||
|
||||
$start = new DateTimeImmutable($startDate);
|
||||
$end = new DateTimeImmutable($endDate);
|
||||
if ($end < $start) {
|
||||
throw new InvalidArgumentException('End date must be on or after start date.');
|
||||
}
|
||||
|
||||
$startHd = null !== $startHalfDay
|
||||
? (HalfDay::tryFrom($startHalfDay) ?? throw new InvalidArgumentException(sprintf('Unknown half day "%s".', $startHalfDay)))
|
||||
: null;
|
||||
$endHd = null !== $endHalfDay
|
||||
? (HalfDay::tryFrom($endHalfDay) ?? throw new InvalidArgumentException(sprintf('Unknown half day "%s".', $endHalfDay)))
|
||||
: null;
|
||||
|
||||
$policy = $this->policyRepository->findOneByType($typeEnum);
|
||||
if (null === $policy || !$policy->isActive()) {
|
||||
throw new InvalidArgumentException('This absence type is not available.');
|
||||
}
|
||||
|
||||
// Bereavement has no fixed entitlement: the relationship to the deceased
|
||||
// drives the legal number of days, so the reason is mandatory.
|
||||
if (AbsenceType::Bereavement === $typeEnum && '' === trim((string) $reason)) {
|
||||
throw new InvalidArgumentException('A reason (relationship to the deceased) is required for bereavement leave.');
|
||||
}
|
||||
|
||||
if ($this->requestRepository->hasOverlap($user, $start, $end)) {
|
||||
throw new InvalidArgumentException('This request overlaps an existing absence.');
|
||||
}
|
||||
|
||||
$countedDays = $this->calculator->countWorkingDays($start, $end, $startHd, $endHd, $policy->isCountWorkingDaysOnly());
|
||||
if ($countedDays <= 0.0) {
|
||||
throw new InvalidArgumentException('The selected range contains no working day.');
|
||||
}
|
||||
|
||||
$request = new AbsenceRequest();
|
||||
$request->setUser($user);
|
||||
$request->setType($typeEnum);
|
||||
$request->setStartDate($start);
|
||||
$request->setEndDate($end);
|
||||
$request->setStartHalfDay($startHd);
|
||||
$request->setEndHalfDay($endHd);
|
||||
$request->setReason($reason);
|
||||
$request->setCountedDays($countedDays);
|
||||
$request->setStatus(AbsenceStatus::Pending);
|
||||
$request->setRejectionReason(null);
|
||||
$request->setCreatedAt(new DateTimeImmutable());
|
||||
|
||||
$this->entityManager->persist($request);
|
||||
$this->balanceService->reservePending($request);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::absenceRequest($request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'delete-absence-request', description: 'Permanently delete an absence request (admin). Does not adjust balances — use cancel-absence-request first if the days must be credited back.')]
|
||||
class DeleteAbsenceRequestTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbsenceRequestRepositoryInterface $requestRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(int $id): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$request = $this->requestRepository->findById($id);
|
||||
if (null === $request) {
|
||||
throw new InvalidArgumentException(sprintf('AbsenceRequest with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
$this->entityManager->remove($request);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(['success' => true, 'message' => sprintf('AbsenceRequest %d deleted.', $id)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
use App\Module\Absence\Domain\Repository\AbsenceRequestRepositoryInterface;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'get-absence-request', description: 'Get a single absence request by ID.')]
|
||||
class GetAbsenceRequestTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbsenceRequestRepositoryInterface $requestRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(int $id): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
||||
}
|
||||
|
||||
$request = $this->requestRepository->findById($id);
|
||||
if (null === $request) {
|
||||
throw new InvalidArgumentException(sprintf('AbsenceRequest with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
return json_encode(Serializer::absenceRequest($request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
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;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'list-absence-balances', description: 'List leave balances. Optional filters: userId, type (cp, mariage_pacs, conge_parental, deces, maladie), period (e.g. "2025-2026" or "2025").')]
|
||||
class ListAbsenceBalancesTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbsenceBalanceRepositoryInterface $balanceRepository,
|
||||
private readonly UserRepositoryInterface $userRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(?int $userId = null, ?string $type = null, ?string $period = null): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
||||
}
|
||||
|
||||
$criteria = [];
|
||||
if (null !== $userId) {
|
||||
$user = $this->userRepository->findById($userId);
|
||||
if (null === $user) {
|
||||
throw new InvalidArgumentException(sprintf('User with ID %d not found.', $userId));
|
||||
}
|
||||
$criteria['user'] = $user;
|
||||
}
|
||||
if (null !== $type) {
|
||||
$criteria['type'] = AbsenceType::tryFrom($type)
|
||||
?? throw new InvalidArgumentException(sprintf('Unknown absence type "%s".', $type));
|
||||
}
|
||||
if (null !== $period) {
|
||||
$criteria['period'] = $period;
|
||||
}
|
||||
|
||||
$balances = $this->balanceRepository->findBy($criteria, ['period' => 'DESC']);
|
||||
|
||||
return json_encode(array_map(Serializer::absenceBalance(...), $balances));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
use App\Module\Absence\Domain\Repository\AbsencePolicyRepositoryInterface;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
#[McpTool(name: 'list-absence-policies', description: 'List all absence policies (one per absence type) with their rules: days/year, days/event, notice period, working-days mode, active flag.')]
|
||||
class ListAbsencePoliciesTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbsencePolicyRepositoryInterface $policyRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
||||
}
|
||||
|
||||
$policies = $this->policyRepository->findBy([], ['type' => 'ASC']);
|
||||
|
||||
return json_encode(array_map(Serializer::absencePolicy(...), $policies));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
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;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'list-absence-requests', description: 'List absence requests. Optional filters: userId, status (pending, approved, rejected, cancelled), type (cp, mariage_pacs, conge_parental, deces, maladie), from/to (YYYY-MM-DD, overlap window). Without userId returns all employees.')]
|
||||
class ListAbsenceRequestsTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbsenceRequestRepositoryInterface $requestRepository,
|
||||
private readonly UserRepositoryInterface $userRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
?int $userId = null,
|
||||
?string $status = null,
|
||||
?string $type = null,
|
||||
?string $from = null,
|
||||
?string $to = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
||||
}
|
||||
|
||||
$user = null;
|
||||
if (null !== $userId) {
|
||||
$user = $this->userRepository->findById($userId)
|
||||
?? throw new InvalidArgumentException(sprintf('User with ID %d not found.', $userId));
|
||||
}
|
||||
|
||||
$statusEnum = null;
|
||||
if (null !== $status) {
|
||||
$statusEnum = AbsenceStatus::tryFrom($status)
|
||||
?? throw new InvalidArgumentException(sprintf('Unknown status "%s".', $status));
|
||||
}
|
||||
|
||||
$typeEnum = null;
|
||||
if (null !== $type) {
|
||||
$typeEnum = AbsenceType::tryFrom($type)
|
||||
?? throw new InvalidArgumentException(sprintf('Unknown absence type "%s".', $type));
|
||||
}
|
||||
|
||||
$requests = $this->requestRepository->findFiltered(
|
||||
$user,
|
||||
$statusEnum,
|
||||
$typeEnum,
|
||||
null !== $from ? new DateTimeImmutable($from) : null,
|
||||
null !== $to ? new DateTimeImmutable($to) : null,
|
||||
);
|
||||
|
||||
return json_encode(array_map(Serializer::absenceRequest(...), $requests));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
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;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function assert;
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'review-absence-request', description: 'Approve or reject a PENDING absence request (admin). decision = "approve" or "reject"; rejectionReason is required when rejecting. Approving moves the days from pending to taken; rejecting releases the reserved days.')]
|
||||
class ReviewAbsenceRequestTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly AbsenceRequestRepositoryInterface $requestRepository,
|
||||
private readonly AbsenceBalanceService $balanceService,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(int $id, string $decision, ?string $rejectionReason = null): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
if (!in_array($decision, ['approve', 'reject'], true)) {
|
||||
throw new InvalidArgumentException('decision must be "approve" or "reject".');
|
||||
}
|
||||
|
||||
$request = $this->requestRepository->findById($id);
|
||||
if (null === $request) {
|
||||
throw new InvalidArgumentException(sprintf('AbsenceRequest with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
if (AbsenceStatus::Pending !== $request->getStatus()) {
|
||||
throw new InvalidArgumentException('Only a pending request can be reviewed.');
|
||||
}
|
||||
|
||||
$admin = $this->security->getUser();
|
||||
assert($admin instanceof UserInterface);
|
||||
|
||||
if ('approve' === $decision) {
|
||||
// Never let an approval push the balance below zero (CP only).
|
||||
$available = $this->balanceService->availableForRequest($request);
|
||||
if (null !== $available && $request->getCountedDays() > $available + 1e-9) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Approving this request would put the balance below zero: %g day(s) requested but only %g available.',
|
||||
$request->getCountedDays(),
|
||||
$available,
|
||||
));
|
||||
}
|
||||
|
||||
$request->setStatus(AbsenceStatus::Approved);
|
||||
$request->setRejectionReason(null);
|
||||
$this->balanceService->applyApproval($request);
|
||||
} else {
|
||||
if (null === $rejectionReason || '' === trim($rejectionReason)) {
|
||||
throw new InvalidArgumentException('A reason is required when rejecting a request.');
|
||||
}
|
||||
$request->setStatus(AbsenceStatus::Rejected);
|
||||
$request->setRejectionReason($rejectionReason);
|
||||
$this->balanceService->release($request, false);
|
||||
}
|
||||
|
||||
$request->setReviewedAt(new DateTimeImmutable());
|
||||
$request->setReviewedBy($admin);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::absenceRequest($request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'update-absence-balance', description: 'Manually adjust a leave balance (admin regularisation). Only provided buckets change: acquired (N-1), acquiring (N), taken.')]
|
||||
class UpdateAbsenceBalanceTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbsenceBalanceRepositoryInterface $balanceRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
int $id,
|
||||
?float $acquired = null,
|
||||
?float $acquiring = null,
|
||||
?float $taken = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$balance = $this->balanceRepository->findById($id);
|
||||
if (null === $balance) {
|
||||
throw new InvalidArgumentException(sprintf('AbsenceBalance with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
if (null !== $acquired) {
|
||||
$balance->setAcquired($acquired);
|
||||
}
|
||||
if (null !== $acquiring) {
|
||||
$balance->setAcquiring($acquiring);
|
||||
}
|
||||
if (null !== $taken) {
|
||||
$balance->setTaken($taken);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::absenceBalance($balance));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Absence\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
use App\Module\Absence\Domain\Repository\AbsencePolicyRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'update-absence-policy', description: 'Update an absence policy (admin). Only provided fields change.')]
|
||||
class UpdateAbsencePolicyTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AbsencePolicyRepositoryInterface $policyRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
int $id,
|
||||
?float $daysPerYear = null,
|
||||
?float $daysPerEvent = null,
|
||||
?bool $justificationRequired = null,
|
||||
?int $noticeDays = null,
|
||||
?bool $countWorkingDaysOnly = null,
|
||||
?bool $active = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$policy = $this->policyRepository->findById($id);
|
||||
if (null === $policy) {
|
||||
throw new InvalidArgumentException(sprintf('AbsencePolicy with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
if (null !== $daysPerYear) {
|
||||
$policy->setDaysPerYear($daysPerYear);
|
||||
}
|
||||
if (null !== $daysPerEvent) {
|
||||
$policy->setDaysPerEvent($daysPerEvent);
|
||||
}
|
||||
if (null !== $justificationRequired) {
|
||||
$policy->setJustificationRequired($justificationRequired);
|
||||
}
|
||||
if (null !== $noticeDays) {
|
||||
$policy->setNoticeDays($noticeDays);
|
||||
}
|
||||
if (null !== $countWorkingDaysOnly) {
|
||||
$policy->setCountWorkingDaysOnly($countWorkingDaysOnly);
|
||||
}
|
||||
if (null !== $active) {
|
||||
$policy->setActive($active);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::absencePolicy($policy));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user