Expose le module Absences via le serveur MCP et comble les trous CRUD existants (projets, groupes, métadonnées de tâches, clients, users RH). Absences (réutilise AbsenceDayCalculator + AbsenceBalanceService pour ne pas contourner la logique de soldes) : - list/get/create/review/cancel/delete-absence-request - list/update-absence-policy, list/update-absence-balance - create-absence-request prend un userId explicite (agir au nom d'un employé) ; review/cancel maintiennent les soldes (pending/taken) cohérents - AbsenceRequestRepository::findFiltered pour les filtres de liste Trous CRUD comblés : - delete-project, delete-group - CRUD tag, effort, priority - CRUD status (couplé au workflow, avec category) - CRUD client, get/update-user (champs RH, sans password ni roles) Sérialisation centralisée (Serializer::absenceRequest/Policy/Balance/client/userFull). Instructions MCP (mcp.yaml) mises à jour : statuts par workflow + domaine absences. Tests : tests/Functional/Mcp/AbsenceRequestLifecycleTest (création / approbation / annulation admin) vérifient le cycle complet et la cohérence des soldes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
178 lines
6.0 KiB
PHP
178 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Functional\Mcp;
|
|
|
|
use App\Entity\AbsencePolicy;
|
|
use App\Entity\User;
|
|
use App\Enum\AbsenceType;
|
|
use App\Mcp\Tool\Absence\CancelAbsenceRequestTool;
|
|
use App\Mcp\Tool\Absence\CreateAbsenceRequestTool;
|
|
use App\Mcp\Tool\Absence\ReviewAbsenceRequestTool;
|
|
use App\Repository\AbsenceBalanceRepository;
|
|
use App\Repository\AbsencePolicyRepository;
|
|
use App\Repository\AbsenceRequestRepository;
|
|
use App\Repository\UserRepository;
|
|
use App\Service\AbsenceBalanceService;
|
|
use App\Service\AbsenceDayCalculator;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class AbsenceRequestLifecycleTest extends KernelTestCase
|
|
{
|
|
private EntityManagerInterface $em;
|
|
private User $employee;
|
|
private User $admin;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
self::bootKernel();
|
|
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
// Employé dédié au test.
|
|
$this->employee = new User();
|
|
$this->employee->setUsername('mcp-test-employee-'.uniqid());
|
|
$this->employee->setPassword('x');
|
|
$this->employee->setRoles(['ROLE_USER']);
|
|
$this->employee->setIsEmployee(true);
|
|
$this->employee->setReferencePeriodStart('06-01');
|
|
$this->em->persist($this->employee);
|
|
|
|
$this->admin = new User();
|
|
$this->admin->setUsername('mcp-test-admin-'.uniqid());
|
|
$this->admin->setPassword('x');
|
|
$this->admin->setRoles(['ROLE_ADMIN']);
|
|
$this->em->persist($this->admin);
|
|
|
|
// Policy CP active (créée si absente — findOneByType peut déjà exister via fixtures).
|
|
$policy = $this->em->getRepository(AbsencePolicy::class)->findOneBy(['type' => AbsenceType::PaidLeave]);
|
|
if (null === $policy) {
|
|
$policy = new AbsencePolicy();
|
|
$policy->setType(AbsenceType::PaidLeave);
|
|
$policy->setCountWorkingDaysOnly(true);
|
|
$policy->setActive(true);
|
|
$this->em->persist($policy);
|
|
} else {
|
|
$policy->setActive(true);
|
|
}
|
|
|
|
$this->em->flush();
|
|
}
|
|
|
|
public function testCreateReservesPendingDays(): void
|
|
{
|
|
$tool = $this->createTool($this->admin);
|
|
|
|
// Lundi 2026-06-01 → vendredi 2026-06-05 = 5 jours ouvrés.
|
|
$json = ($tool)(
|
|
$this->employee->getId(),
|
|
'cp',
|
|
'2026-06-01',
|
|
'2026-06-05',
|
|
);
|
|
$data = json_decode($json, true);
|
|
|
|
self::assertSame('pending', $data['status']);
|
|
// JSON ne préserve pas le type float pour un entier (5.0 -> 5) : on compare la valeur.
|
|
self::assertSame(5.0, (float) $data['countedDays']);
|
|
self::assertSame($this->employee->getId(), $data['user']['id']);
|
|
|
|
$balance = self::getContainer()->get(AbsenceBalanceRepository::class)
|
|
->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
|
|
;
|
|
self::assertNotNull($balance);
|
|
self::assertSame(5.0, $balance->getPending());
|
|
}
|
|
|
|
public function testApproveMovesPendingToTaken(): void
|
|
{
|
|
$created = json_decode(
|
|
($this->createTool($this->admin))($this->employee->getId(), 'cp', '2026-06-01', '2026-06-05'),
|
|
true,
|
|
);
|
|
|
|
$json = ($this->reviewTool($this->admin))($created['id'], 'approve');
|
|
$data = json_decode($json, true);
|
|
|
|
self::assertSame('approved', $data['status']);
|
|
self::assertSame($this->admin->getId(), $data['reviewedBy']['id']);
|
|
|
|
$balance = self::getContainer()->get(AbsenceBalanceRepository::class)
|
|
->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
|
|
;
|
|
self::assertSame(0.0, $balance->getPending());
|
|
self::assertSame(5.0, $balance->getTaken());
|
|
}
|
|
|
|
public function testAdminCancelApprovedReleasesTaken(): void
|
|
{
|
|
$created = json_decode(
|
|
($this->createTool($this->admin))($this->employee->getId(), 'cp', '2026-06-01', '2026-06-05'),
|
|
true,
|
|
);
|
|
($this->reviewTool($this->admin))($created['id'], 'approve');
|
|
|
|
$data = json_decode(($this->cancelTool($this->admin))($created['id']), true);
|
|
self::assertSame('cancelled', $data['status']);
|
|
|
|
$balance = self::getContainer()->get(AbsenceBalanceRepository::class)
|
|
->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
|
|
;
|
|
self::assertSame(0.0, $balance->getTaken());
|
|
self::assertSame(0.0, $balance->getPending());
|
|
}
|
|
|
|
private function securityFor(User $user): Security
|
|
{
|
|
$security = $this->createMock(Security::class);
|
|
$security->method('isGranted')->willReturn(true);
|
|
$security->method('getUser')->willReturn($user);
|
|
|
|
return $security;
|
|
}
|
|
|
|
private function createTool(User $actor): CreateAbsenceRequestTool
|
|
{
|
|
$c = self::getContainer();
|
|
|
|
return new CreateAbsenceRequestTool(
|
|
$c->get(EntityManagerInterface::class),
|
|
$c->get(UserRepository::class),
|
|
$c->get(AbsencePolicyRepository::class),
|
|
$c->get(AbsenceRequestRepository::class),
|
|
$c->get(AbsenceDayCalculator::class),
|
|
$c->get(AbsenceBalanceService::class),
|
|
$this->securityFor($actor),
|
|
);
|
|
}
|
|
|
|
private function reviewTool(User $actor): ReviewAbsenceRequestTool
|
|
{
|
|
$c = self::getContainer();
|
|
|
|
return new ReviewAbsenceRequestTool(
|
|
$c->get(EntityManagerInterface::class),
|
|
$c->get(AbsenceRequestRepository::class),
|
|
$c->get(AbsenceBalanceService::class),
|
|
$this->securityFor($actor),
|
|
);
|
|
}
|
|
|
|
private function cancelTool(User $actor): CancelAbsenceRequestTool
|
|
{
|
|
$c = self::getContainer();
|
|
|
|
return new CancelAbsenceRequestTool(
|
|
$c->get(EntityManagerInterface::class),
|
|
$c->get(AbsenceRequestRepository::class),
|
|
$c->get(AbsenceBalanceService::class),
|
|
$this->securityFor($actor),
|
|
);
|
|
}
|
|
}
|