feat(mcp) : outils MCP Directory pour prestataires, contacts, adresses et rapports
Ajoute 20 nouveaux outils MCP pour permettre à Claude (ou tout client MCP) de remplir un dossier client / prospect / prestataire complet — onglets Information, Contact, Adresse et Rapport — sans passer par l'UI. Entités couvertes (CRUD complet, 5 outils chacune) : - Prestataire : create / update / get / list / delete - Contact : create / update / get / list / delete - Address : create / update / get / list / delete - CommercialReport : create / update / get / list / delete Détails : - Contact / Address / CommercialReport doivent être rattachés à exactement un parent parmi clientId, prospectId, prestataireId (validation côté tool). - get-client, get-prospect et get-prestataire renvoient désormais un payload enrichi avec la liste de leurs contacts, adresses et rapports liés : un seul appel pour reconstruire l'onglet entier. - Pour CommercialReport, le type (note / call / meeting / email) et la date occurredAt sont validés ; l'auteur est rempli automatiquement par le listener existant. - Sécurité : ROLE_ADMIN aligné sur les autres outils MCP de Directory (pas de migration vers les permissions RBAC fines pour rester cohérent). Plumbing : - Repositories Contact / Address / CommercialReport : ajout de findBy() sur les interfaces (l'implémentation Doctrine l'a déjà via ServiceEntityRepository). - Bindings interface -> implémentation Doctrine ajoutés dans services.yaml pour Prestataire / Contact / Address / CommercialReport. - Sérialiseur partagé étendu : prestataire / contact / address / commercialReport / reportDocument. Vérification : 86 outils MCP exposés au total (66 avant + 20 ajoutés), test end-to-end via le transport HTTP (create-prestataire + create-contact + create-address + create-commercial-report + get-prestataire renvoyant le dossier complet). Suite PHPUnit verte. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\Address;
|
||||
use App\Module\Directory\Domain\Repository\ClientRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-address',
|
||||
description: 'Create an address (admin) attached to exactly one of clientId / prospectId / prestataireId. Country defaults to FR (ISO 3166 alpha-2).'
|
||||
)]
|
||||
class CreateAddressTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ClientRepositoryInterface $clientRepository,
|
||||
private readonly ProspectRepositoryInterface $prospectRepository,
|
||||
private readonly PrestataireRepositoryInterface $prestataireRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
?int $clientId = null,
|
||||
?int $prospectId = null,
|
||||
?int $prestataireId = null,
|
||||
?string $label = null,
|
||||
?string $street = null,
|
||||
?string $streetComplement = null,
|
||||
?string $postalCode = null,
|
||||
?string $city = null,
|
||||
?string $country = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$parents = array_filter([$clientId, $prospectId, $prestataireId], static fn ($v) => null !== $v);
|
||||
if (1 !== count($parents)) {
|
||||
throw new InvalidArgumentException('Exactly one of clientId, prospectId or prestataireId must be provided.');
|
||||
}
|
||||
|
||||
$address = new Address();
|
||||
|
||||
if (null !== $clientId) {
|
||||
$client = $this->clientRepository->findById($clientId);
|
||||
if (null === $client) {
|
||||
throw new InvalidArgumentException(sprintf('Client with ID %d not found.', $clientId));
|
||||
}
|
||||
$address->setClient($client);
|
||||
}
|
||||
if (null !== $prospectId) {
|
||||
$prospect = $this->prospectRepository->findById($prospectId);
|
||||
if (null === $prospect) {
|
||||
throw new InvalidArgumentException(sprintf('Prospect with ID %d not found.', $prospectId));
|
||||
}
|
||||
$address->setProspect($prospect);
|
||||
}
|
||||
if (null !== $prestataireId) {
|
||||
$prestataire = $this->prestataireRepository->findById($prestataireId);
|
||||
if (null === $prestataire) {
|
||||
throw new InvalidArgumentException(sprintf('Prestataire with ID %d not found.', $prestataireId));
|
||||
}
|
||||
$address->setPrestataire($prestataire);
|
||||
}
|
||||
|
||||
$address->setLabel($label);
|
||||
$address->setStreet($street);
|
||||
$address->setStreetComplement($streetComplement);
|
||||
$address->setPostalCode($postalCode);
|
||||
$address->setCity($city);
|
||||
if (null !== $country) {
|
||||
if (2 !== strlen($country)) {
|
||||
throw new InvalidArgumentException('country must be a 2-letter ISO 3166 alpha-2 code (e.g., FR, BE).');
|
||||
}
|
||||
$address->setCountry(strtoupper($country));
|
||||
}
|
||||
|
||||
$this->entityManager->persist($address);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::address($address));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\CommercialReport;
|
||||
use App\Module\Directory\Domain\Enum\ReportType;
|
||||
use App\Module\Directory\Domain\Repository\ClientRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
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-commercial-report',
|
||||
description: 'Create a commercial report (admin) attached to exactly one of clientId / prospectId / prestataireId. Type defaults to "note". Allowed types: note, call, meeting, email. Date defaults to today if omitted.'
|
||||
)]
|
||||
class CreateCommercialReportTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ClientRepositoryInterface $clientRepository,
|
||||
private readonly ProspectRepositoryInterface $prospectRepository,
|
||||
private readonly PrestataireRepositoryInterface $prestataireRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
string $subject,
|
||||
?int $clientId = null,
|
||||
?int $prospectId = null,
|
||||
?int $prestataireId = null,
|
||||
?string $body = null,
|
||||
?string $occurredAt = null,
|
||||
?string $type = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$parents = array_filter([$clientId, $prospectId, $prestataireId], static fn ($v) => null !== $v);
|
||||
if (1 !== count($parents)) {
|
||||
throw new InvalidArgumentException('Exactly one of clientId, prospectId or prestataireId must be provided.');
|
||||
}
|
||||
|
||||
$report = new CommercialReport();
|
||||
$report->setSubject($subject);
|
||||
$report->setBody($body);
|
||||
|
||||
if (null !== $clientId) {
|
||||
$client = $this->clientRepository->findById($clientId);
|
||||
if (null === $client) {
|
||||
throw new InvalidArgumentException(sprintf('Client with ID %d not found.', $clientId));
|
||||
}
|
||||
$report->setClient($client);
|
||||
}
|
||||
if (null !== $prospectId) {
|
||||
$prospect = $this->prospectRepository->findById($prospectId);
|
||||
if (null === $prospect) {
|
||||
throw new InvalidArgumentException(sprintf('Prospect with ID %d not found.', $prospectId));
|
||||
}
|
||||
$report->setProspect($prospect);
|
||||
}
|
||||
if (null !== $prestataireId) {
|
||||
$prestataire = $this->prestataireRepository->findById($prestataireId);
|
||||
if (null === $prestataire) {
|
||||
throw new InvalidArgumentException(sprintf('Prestataire with ID %d not found.', $prestataireId));
|
||||
}
|
||||
$report->setPrestataire($prestataire);
|
||||
}
|
||||
|
||||
try {
|
||||
$date = null === $occurredAt
|
||||
? new DateTimeImmutable('today')
|
||||
: new DateTimeImmutable($occurredAt);
|
||||
} catch (Exception $e) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid occurredAt "%s": %s', $occurredAt, $e->getMessage()));
|
||||
}
|
||||
$report->setOccurredAt($date);
|
||||
|
||||
if (null !== $type) {
|
||||
$typeEnum = ReportType::tryFrom($type);
|
||||
if (null === $typeEnum) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid type "%s". Allowed: note, call, meeting, email.', $type));
|
||||
}
|
||||
$report->setType($typeEnum);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($report);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::commercialReport($report));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\Contact;
|
||||
use App\Module\Directory\Domain\Repository\ClientRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-contact',
|
||||
description: 'Create a contact (admin) attached to exactly one of clientId / prospectId / prestataireId. All fields except the parent are optional.'
|
||||
)]
|
||||
class CreateContactTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ClientRepositoryInterface $clientRepository,
|
||||
private readonly ProspectRepositoryInterface $prospectRepository,
|
||||
private readonly PrestataireRepositoryInterface $prestataireRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
?int $clientId = null,
|
||||
?int $prospectId = null,
|
||||
?int $prestataireId = null,
|
||||
?string $firstName = null,
|
||||
?string $lastName = null,
|
||||
?string $jobTitle = null,
|
||||
?string $email = null,
|
||||
?string $phonePrimary = null,
|
||||
?string $phoneSecondary = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$parents = array_filter([$clientId, $prospectId, $prestataireId], static fn ($v) => null !== $v);
|
||||
if (1 !== count($parents)) {
|
||||
throw new InvalidArgumentException('Exactly one of clientId, prospectId or prestataireId must be provided.');
|
||||
}
|
||||
|
||||
$contact = new Contact();
|
||||
|
||||
if (null !== $clientId) {
|
||||
$client = $this->clientRepository->findById($clientId);
|
||||
if (null === $client) {
|
||||
throw new InvalidArgumentException(sprintf('Client with ID %d not found.', $clientId));
|
||||
}
|
||||
$contact->setClient($client);
|
||||
}
|
||||
if (null !== $prospectId) {
|
||||
$prospect = $this->prospectRepository->findById($prospectId);
|
||||
if (null === $prospect) {
|
||||
throw new InvalidArgumentException(sprintf('Prospect with ID %d not found.', $prospectId));
|
||||
}
|
||||
$contact->setProspect($prospect);
|
||||
}
|
||||
if (null !== $prestataireId) {
|
||||
$prestataire = $this->prestataireRepository->findById($prestataireId);
|
||||
if (null === $prestataire) {
|
||||
throw new InvalidArgumentException(sprintf('Prestataire with ID %d not found.', $prestataireId));
|
||||
}
|
||||
$contact->setPrestataire($prestataire);
|
||||
}
|
||||
|
||||
$contact->setFirstName($firstName);
|
||||
$contact->setLastName($lastName);
|
||||
$contact->setJobTitle($jobTitle);
|
||||
$contact->setEmail($email);
|
||||
$contact->setPhonePrimary($phonePrimary);
|
||||
$contact->setPhoneSecondary($phoneSecondary);
|
||||
|
||||
$this->entityManager->persist($contact);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::contact($contact));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\Prestataire;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
#[McpTool(name: 'create-prestataire', description: 'Create a prestataire / service provider (admin). Only name is required.')]
|
||||
class CreatePrestataireTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
string $name,
|
||||
?string $email = null,
|
||||
?string $phone = null,
|
||||
?string $website = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$prestataire = new Prestataire();
|
||||
$prestataire->setName($name);
|
||||
$prestataire->setEmail($email);
|
||||
$prestataire->setPhone($phone);
|
||||
$prestataire->setWebsite($website);
|
||||
|
||||
$this->entityManager->persist($prestataire);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::prestataire($prestataire));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
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-address', description: 'Delete an address (admin).')]
|
||||
class DeleteAddressTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AddressRepositoryInterface $addressRepository,
|
||||
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.');
|
||||
}
|
||||
|
||||
$address = $this->addressRepository->findById($id);
|
||||
if (null === $address) {
|
||||
throw new InvalidArgumentException(sprintf('Address with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
$this->entityManager->remove($address);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(['success' => true, 'message' => sprintf('Address #%d deleted.', $id)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
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-commercial-report', description: 'Delete a commercial report (admin). Cascade removes its attached documents.')]
|
||||
class DeleteCommercialReportTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CommercialReportRepositoryInterface $reportRepository,
|
||||
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.');
|
||||
}
|
||||
|
||||
$report = $this->reportRepository->findById($id);
|
||||
if (null === $report) {
|
||||
throw new InvalidArgumentException(sprintf('CommercialReport with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
$this->entityManager->remove($report);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(['success' => true, 'message' => sprintf('CommercialReport #%d deleted.', $id)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
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-contact', description: 'Delete a contact (admin).')]
|
||||
class DeleteContactTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactRepositoryInterface $contactRepository,
|
||||
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.');
|
||||
}
|
||||
|
||||
$contact = $this->contactRepository->findById($id);
|
||||
if (null === $contact) {
|
||||
throw new InvalidArgumentException(sprintf('Contact with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
$this->entityManager->remove($contact);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(['success' => true, 'message' => sprintf('Contact #%d deleted.', $id)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
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-prestataire', description: 'Delete a prestataire (admin). Cascade removes its contacts, addresses and reports.')]
|
||||
class DeletePrestataireTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PrestataireRepositoryInterface $prestataireRepository,
|
||||
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.');
|
||||
}
|
||||
|
||||
$prestataire = $this->prestataireRepository->findById($id);
|
||||
if (null === $prestataire) {
|
||||
throw new InvalidArgumentException(sprintf('Prestataire with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
$name = $prestataire->getName();
|
||||
$this->entityManager->remove($prestataire);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(['success' => true, 'message' => sprintf('Prestataire "%s" deleted.', $name)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-address', description: 'Get an address by ID.')]
|
||||
class GetAddressTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AddressRepositoryInterface $addressRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(int $id): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$address = $this->addressRepository->findById($id);
|
||||
if (null === $address) {
|
||||
throw new InvalidArgumentException(sprintf('Address with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
return json_encode(Serializer::address($address));
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ClientRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
@@ -13,11 +16,14 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'get-client', description: 'Get a client by ID with full contact details.')]
|
||||
#[McpTool(name: 'get-client', description: 'Get a client by ID, with its linked contacts, addresses, and commercial reports.')]
|
||||
class GetClientTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClientRepositoryInterface $clientRepository,
|
||||
private readonly ContactRepositoryInterface $contactRepository,
|
||||
private readonly AddressRepositoryInterface $addressRepository,
|
||||
private readonly CommercialReportRepositoryInterface $reportRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
@@ -32,6 +38,20 @@ class GetClientTool
|
||||
throw new InvalidArgumentException(sprintf('Client with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
return json_encode(Serializer::client($client));
|
||||
$payload = Serializer::client($client);
|
||||
$payload['contacts'] = array_map(
|
||||
fn ($c) => Serializer::contact($c),
|
||||
$this->contactRepository->findBy(['client' => $client], ['lastName' => 'ASC'])
|
||||
);
|
||||
$payload['addresses'] = array_map(
|
||||
fn ($a) => Serializer::address($a),
|
||||
$this->addressRepository->findBy(['client' => $client], ['id' => 'ASC'])
|
||||
);
|
||||
$payload['reports'] = array_map(
|
||||
fn ($r) => Serializer::commercialReport($r),
|
||||
$this->reportRepository->findBy(['client' => $client], ['occurredAt' => 'DESC'])
|
||||
);
|
||||
|
||||
return json_encode($payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-commercial-report', description: 'Get a commercial report by ID, including its attached documents.')]
|
||||
class GetCommercialReportTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CommercialReportRepositoryInterface $reportRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(int $id): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$report = $this->reportRepository->findById($id);
|
||||
if (null === $report) {
|
||||
throw new InvalidArgumentException(sprintf('CommercialReport with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
return json_encode(Serializer::commercialReport($report));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-contact', description: 'Get a contact by ID.')]
|
||||
class GetContactTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactRepositoryInterface $contactRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(int $id): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$contact = $this->contactRepository->findById($id);
|
||||
if (null === $contact) {
|
||||
throw new InvalidArgumentException(sprintf('Contact with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
return json_encode(Serializer::contact($contact));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-prestataire', description: 'Get a prestataire by ID, with its linked contacts, addresses, and commercial reports.')]
|
||||
class GetPrestataireTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PrestataireRepositoryInterface $prestataireRepository,
|
||||
private readonly ContactRepositoryInterface $contactRepository,
|
||||
private readonly AddressRepositoryInterface $addressRepository,
|
||||
private readonly CommercialReportRepositoryInterface $reportRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(int $id): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$prestataire = $this->prestataireRepository->findById($id);
|
||||
if (null === $prestataire) {
|
||||
throw new InvalidArgumentException(sprintf('Prestataire with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
$payload = Serializer::prestataire($prestataire);
|
||||
$payload['contacts'] = array_map(
|
||||
fn ($c) => Serializer::contact($c),
|
||||
$this->contactRepository->findBy(['prestataire' => $prestataire], ['lastName' => 'ASC'])
|
||||
);
|
||||
$payload['addresses'] = array_map(
|
||||
fn ($a) => Serializer::address($a),
|
||||
$this->addressRepository->findBy(['prestataire' => $prestataire], ['id' => 'ASC'])
|
||||
);
|
||||
$payload['reports'] = array_map(
|
||||
fn ($r) => Serializer::commercialReport($r),
|
||||
$this->reportRepository->findBy(['prestataire' => $prestataire], ['occurredAt' => 'DESC'])
|
||||
);
|
||||
|
||||
return json_encode($payload);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
use InvalidArgumentException;
|
||||
@@ -13,11 +16,14 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'get-prospect', description: 'Get a prospect by ID with full details.')]
|
||||
#[McpTool(name: 'get-prospect', description: 'Get a prospect by ID, with its linked contacts, addresses, and commercial reports.')]
|
||||
class GetProspectTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProspectRepositoryInterface $prospectRepository,
|
||||
private readonly ContactRepositoryInterface $contactRepository,
|
||||
private readonly AddressRepositoryInterface $addressRepository,
|
||||
private readonly CommercialReportRepositoryInterface $reportRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
@@ -32,6 +38,20 @@ class GetProspectTool
|
||||
throw new InvalidArgumentException(sprintf('Prospect with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
return json_encode(Serializer::prospect($prospect));
|
||||
$payload = Serializer::prospect($prospect);
|
||||
$payload['contacts'] = array_map(
|
||||
fn ($c) => Serializer::contact($c),
|
||||
$this->contactRepository->findBy(['prospect' => $prospect], ['lastName' => 'ASC'])
|
||||
);
|
||||
$payload['addresses'] = array_map(
|
||||
fn ($a) => Serializer::address($a),
|
||||
$this->addressRepository->findBy(['prospect' => $prospect], ['id' => 'ASC'])
|
||||
);
|
||||
$payload['reports'] = array_map(
|
||||
fn ($r) => Serializer::commercialReport($r),
|
||||
$this->reportRepository->findBy(['prospect' => $prospect], ['occurredAt' => 'DESC'])
|
||||
);
|
||||
|
||||
return json_encode($payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
#[McpTool(
|
||||
name: 'list-addresses',
|
||||
description: 'List addresses, optionally filtered by parent (at most one of clientId / prospectId / prestataireId).'
|
||||
)]
|
||||
class ListAddressesTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AddressRepositoryInterface $addressRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
?int $clientId = null,
|
||||
?int $prospectId = null,
|
||||
?int $prestataireId = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$filters = array_filter([$clientId, $prospectId, $prestataireId], static fn ($v) => null !== $v);
|
||||
if (count($filters) > 1) {
|
||||
throw new InvalidArgumentException('At most one of clientId, prospectId or prestataireId can be provided.');
|
||||
}
|
||||
|
||||
$criteria = [];
|
||||
if (null !== $clientId) {
|
||||
$criteria['client'] = $clientId;
|
||||
}
|
||||
if (null !== $prospectId) {
|
||||
$criteria['prospect'] = $prospectId;
|
||||
}
|
||||
if (null !== $prestataireId) {
|
||||
$criteria['prestataire'] = $prestataireId;
|
||||
}
|
||||
|
||||
$addresses = $this->addressRepository->findBy($criteria, ['id' => 'ASC']);
|
||||
|
||||
return json_encode(array_map(fn ($a) => Serializer::address($a), $addresses));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
#[McpTool(
|
||||
name: 'list-commercial-reports',
|
||||
description: 'List commercial reports, optionally filtered by parent (at most one of clientId / prospectId / prestataireId). Returns reports ordered by occurredAt DESC.'
|
||||
)]
|
||||
class ListCommercialReportsTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CommercialReportRepositoryInterface $reportRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
?int $clientId = null,
|
||||
?int $prospectId = null,
|
||||
?int $prestataireId = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$filters = array_filter([$clientId, $prospectId, $prestataireId], static fn ($v) => null !== $v);
|
||||
if (count($filters) > 1) {
|
||||
throw new InvalidArgumentException('At most one of clientId, prospectId or prestataireId can be provided.');
|
||||
}
|
||||
|
||||
$criteria = [];
|
||||
if (null !== $clientId) {
|
||||
$criteria['client'] = $clientId;
|
||||
}
|
||||
if (null !== $prospectId) {
|
||||
$criteria['prospect'] = $prospectId;
|
||||
}
|
||||
if (null !== $prestataireId) {
|
||||
$criteria['prestataire'] = $prestataireId;
|
||||
}
|
||||
|
||||
$reports = $this->reportRepository->findBy($criteria, ['occurredAt' => 'DESC']);
|
||||
|
||||
return json_encode(array_map(fn ($r) => Serializer::commercialReport($r), $reports));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
#[McpTool(
|
||||
name: 'list-contacts',
|
||||
description: 'List contacts, optionally filtered by parent (at most one of clientId / prospectId / prestataireId).'
|
||||
)]
|
||||
class ListContactsTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactRepositoryInterface $contactRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
?int $clientId = null,
|
||||
?int $prospectId = null,
|
||||
?int $prestataireId = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$filters = array_filter([$clientId, $prospectId, $prestataireId], static fn ($v) => null !== $v);
|
||||
if (count($filters) > 1) {
|
||||
throw new InvalidArgumentException('At most one of clientId, prospectId or prestataireId can be provided.');
|
||||
}
|
||||
|
||||
$criteria = [];
|
||||
if (null !== $clientId) {
|
||||
$criteria['client'] = $clientId;
|
||||
}
|
||||
if (null !== $prospectId) {
|
||||
$criteria['prospect'] = $prospectId;
|
||||
}
|
||||
if (null !== $prestataireId) {
|
||||
$criteria['prestataire'] = $prestataireId;
|
||||
}
|
||||
|
||||
$contacts = $this->contactRepository->findBy($criteria, ['lastName' => 'ASC']);
|
||||
|
||||
return json_encode(array_map(fn ($c) => Serializer::contact($c), $contacts));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
#[McpTool(name: 'list-prestataires', description: 'List all prestataires with their IDs, names, and emails. Use this to discover valid prestataire IDs.')]
|
||||
class ListPrestatairesTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PrestataireRepositoryInterface $prestataireRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(): string
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$prestataires = $this->prestataireRepository->findBy([], ['name' => 'ASC']);
|
||||
|
||||
return json_encode(array_map(fn ($p) => [
|
||||
'id' => $p->getId(),
|
||||
'name' => $p->getName(),
|
||||
'email' => $p->getEmail(),
|
||||
], $prestataires));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-address',
|
||||
description: 'Update an address (admin). Only provided fields change. Parent (client/prospect/prestataire) is immutable.'
|
||||
)]
|
||||
class UpdateAddressTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AddressRepositoryInterface $addressRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
int $id,
|
||||
?string $label = null,
|
||||
?string $street = null,
|
||||
?string $streetComplement = null,
|
||||
?string $postalCode = null,
|
||||
?string $city = null,
|
||||
?string $country = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$address = $this->addressRepository->findById($id);
|
||||
if (null === $address) {
|
||||
throw new InvalidArgumentException(sprintf('Address with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
if (null !== $label) {
|
||||
$address->setLabel($label);
|
||||
}
|
||||
if (null !== $street) {
|
||||
$address->setStreet($street);
|
||||
}
|
||||
if (null !== $streetComplement) {
|
||||
$address->setStreetComplement($streetComplement);
|
||||
}
|
||||
if (null !== $postalCode) {
|
||||
$address->setPostalCode($postalCode);
|
||||
}
|
||||
if (null !== $city) {
|
||||
$address->setCity($city);
|
||||
}
|
||||
if (null !== $country) {
|
||||
if (2 !== strlen($country)) {
|
||||
throw new InvalidArgumentException('country must be a 2-letter ISO 3166 alpha-2 code (e.g., FR, BE).');
|
||||
}
|
||||
$address->setCountry(strtoupper($country));
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::address($address));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Enum\ReportType;
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
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-commercial-report',
|
||||
description: 'Update a commercial report (admin). Only provided fields change. Parent (client/prospect/prestataire) is immutable.'
|
||||
)]
|
||||
class UpdateCommercialReportTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CommercialReportRepositoryInterface $reportRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
int $id,
|
||||
?string $subject = null,
|
||||
?string $body = null,
|
||||
?string $occurredAt = null,
|
||||
?string $type = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$report = $this->reportRepository->findById($id);
|
||||
if (null === $report) {
|
||||
throw new InvalidArgumentException(sprintf('CommercialReport with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
if (null !== $subject) {
|
||||
$report->setSubject($subject);
|
||||
}
|
||||
if (null !== $body) {
|
||||
$report->setBody($body);
|
||||
}
|
||||
if (null !== $occurredAt) {
|
||||
try {
|
||||
$report->setOccurredAt(new DateTimeImmutable($occurredAt));
|
||||
} catch (Exception $e) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid occurredAt "%s": %s', $occurredAt, $e->getMessage()));
|
||||
}
|
||||
}
|
||||
if (null !== $type) {
|
||||
$typeEnum = ReportType::tryFrom($type);
|
||||
if (null === $typeEnum) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid type "%s". Allowed: note, call, meeting, email.', $type));
|
||||
}
|
||||
$report->setType($typeEnum);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::commercialReport($report));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-contact',
|
||||
description: 'Update a contact (admin). Only provided fields change. The parent (client/prospect/prestataire) is immutable — delete then recreate to re-attach.'
|
||||
)]
|
||||
class UpdateContactTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContactRepositoryInterface $contactRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
int $id,
|
||||
?string $firstName = null,
|
||||
?string $lastName = null,
|
||||
?string $jobTitle = null,
|
||||
?string $email = null,
|
||||
?string $phonePrimary = null,
|
||||
?string $phoneSecondary = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$contact = $this->contactRepository->findById($id);
|
||||
if (null === $contact) {
|
||||
throw new InvalidArgumentException(sprintf('Contact with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
if (null !== $firstName) {
|
||||
$contact->setFirstName($firstName);
|
||||
}
|
||||
if (null !== $lastName) {
|
||||
$contact->setLastName($lastName);
|
||||
}
|
||||
if (null !== $jobTitle) {
|
||||
$contact->setJobTitle($jobTitle);
|
||||
}
|
||||
if (null !== $email) {
|
||||
$contact->setEmail($email);
|
||||
}
|
||||
if (null !== $phonePrimary) {
|
||||
$contact->setPhonePrimary($phonePrimary);
|
||||
}
|
||||
if (null !== $phoneSecondary) {
|
||||
$contact->setPhoneSecondary($phoneSecondary);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::contact($contact));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Shared\Infrastructure\Mcp\Serializer;
|
||||
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-prestataire', description: 'Update a prestataire (admin). Only provided fields change.')]
|
||||
class UpdatePrestataireTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PrestataireRepositoryInterface $prestataireRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
int $id,
|
||||
?string $name = null,
|
||||
?string $email = null,
|
||||
?string $phone = null,
|
||||
?string $website = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
}
|
||||
|
||||
$prestataire = $this->prestataireRepository->findById($id);
|
||||
if (null === $prestataire) {
|
||||
throw new InvalidArgumentException(sprintf('Prestataire with ID %d not found.', $id));
|
||||
}
|
||||
|
||||
if (null !== $name) {
|
||||
$prestataire->setName($name);
|
||||
}
|
||||
if (null !== $email) {
|
||||
$prestataire->setEmail($email);
|
||||
}
|
||||
if (null !== $phone) {
|
||||
$prestataire->setPhone($phone);
|
||||
}
|
||||
if (null !== $website) {
|
||||
$prestataire->setWebsite($website);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return json_encode(Serializer::prestataire($prestataire));
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,13 @@ 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\Directory\Domain\Entity\Address;
|
||||
use App\Module\Directory\Domain\Entity\Client;
|
||||
use App\Module\Directory\Domain\Entity\CommercialReport;
|
||||
use App\Module\Directory\Domain\Entity\Contact;
|
||||
use App\Module\Directory\Domain\Entity\Prestataire;
|
||||
use App\Module\Directory\Domain\Entity\Prospect;
|
||||
use App\Module\Directory\Domain\Entity\ReportDocument;
|
||||
use App\Module\ProjectManagement\Domain\Entity\Project;
|
||||
use App\Module\ProjectManagement\Domain\Entity\TaskDocument;
|
||||
use App\Module\ProjectManagement\Domain\Entity\TaskEffort;
|
||||
@@ -374,6 +379,98 @@ final class Serializer
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function prestataire(Prestataire $p): array
|
||||
{
|
||||
return [
|
||||
'id' => $p->getId(),
|
||||
'name' => $p->getName(),
|
||||
'email' => $p->getEmail(),
|
||||
'phone' => $p->getPhone(),
|
||||
'website' => $p->getWebsite(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function contact(Contact $c): array
|
||||
{
|
||||
return [
|
||||
'id' => $c->getId(),
|
||||
'firstName' => $c->getFirstName(),
|
||||
'lastName' => $c->getLastName(),
|
||||
'jobTitle' => $c->getJobTitle(),
|
||||
'email' => $c->getEmail(),
|
||||
'phonePrimary' => $c->getPhonePrimary(),
|
||||
'phoneSecondary' => $c->getPhoneSecondary(),
|
||||
'clientId' => $c->getClient()?->getId(),
|
||||
'prospectId' => $c->getProspect()?->getId(),
|
||||
'prestataireId' => $c->getPrestataire()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function address(Address $a): array
|
||||
{
|
||||
return [
|
||||
'id' => $a->getId(),
|
||||
'label' => $a->getLabel(),
|
||||
'street' => $a->getStreet(),
|
||||
'streetComplement' => $a->getStreetComplement(),
|
||||
'postalCode' => $a->getPostalCode(),
|
||||
'city' => $a->getCity(),
|
||||
'country' => $a->getCountry(),
|
||||
'clientId' => $a->getClient()?->getId(),
|
||||
'prospectId' => $a->getProspect()?->getId(),
|
||||
'prestataireId' => $a->getPrestataire()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function reportDocument(ReportDocument $d): array
|
||||
{
|
||||
return [
|
||||
'id' => $d->getId(),
|
||||
'originalName' => $d->getOriginalName(),
|
||||
'mimeType' => $d->getMimeType(),
|
||||
'size' => $d->getSize(),
|
||||
'createdAt' => $d->getCreatedAt()?->format('c'),
|
||||
'uploadedBy' => self::user($d->getUploadedBy()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function commercialReport(CommercialReport $r): array
|
||||
{
|
||||
return [
|
||||
'id' => $r->getId(),
|
||||
'subject' => $r->getSubject(),
|
||||
'body' => $r->getBody(),
|
||||
'occurredAt' => $r->getOccurredAt()?->format('Y-m-d'),
|
||||
'type' => $r->getType()->value,
|
||||
'typeLabel' => $r->getType()->label(),
|
||||
'author' => self::user($r->getAuthor()),
|
||||
'clientId' => $r->getClient()?->getId(),
|
||||
'prospectId' => $r->getProspect()?->getId(),
|
||||
'prestataireId' => $r->getPrestataire()?->getId(),
|
||||
'documents' => array_map(
|
||||
fn (ReportDocument $d) => self::reportDocument($d),
|
||||
$r->getDocuments()->toArray()
|
||||
),
|
||||
'createdAt' => $r->getCreatedAt()?->format('c'),
|
||||
'updatedAt' => $r->getUpdatedAt()?->format('c'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user