From 99626b89daa63ce161c3c21d52a4a88ed895d06d Mon Sep 17 00:00:00 2001 From: matthieu Date: Wed, 24 Jun 2026 20:36:46 +0200 Subject: [PATCH] feat(mcp) : outils MCP Directory pour prestataires, contacts, adresses et rapports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Mcp/Tool/CreateAddressTool.php | 95 ++++++++++++++++ .../Mcp/Tool/CreateCommercialReportTool.php | 103 ++++++++++++++++++ .../Mcp/Tool/CreateContactTool.php | 90 +++++++++++++++ .../Mcp/Tool/CreatePrestataireTool.php | 43 ++++++++ .../Mcp/Tool/DeleteAddressTool.php | 41 +++++++ .../Mcp/Tool/DeleteCommercialReportTool.php | 41 +++++++ .../Mcp/Tool/DeleteContactTool.php | 41 +++++++ .../Mcp/Tool/DeletePrestataireTool.php | 42 +++++++ .../Mcp/Tool/GetAddressTool.php | 37 +++++++ .../Infrastructure/Mcp/Tool/GetClientTool.php | 24 +++- .../Mcp/Tool/GetCommercialReportTool.php | 37 +++++++ .../Mcp/Tool/GetContactTool.php | 37 +++++++ .../Mcp/Tool/GetPrestataireTool.php | 57 ++++++++++ .../Mcp/Tool/GetProspectTool.php | 24 +++- .../Mcp/Tool/ListAddressesTool.php | 54 +++++++++ .../Mcp/Tool/ListCommercialReportsTool.php | 54 +++++++++ .../Mcp/Tool/ListContactsTool.php | 54 +++++++++ .../Mcp/Tool/ListPrestatairesTool.php | 34 ++++++ .../Mcp/Tool/UpdateAddressTool.php | 73 +++++++++++++ .../Mcp/Tool/UpdateCommercialReportTool.php | 73 +++++++++++++ .../Mcp/Tool/UpdateContactTool.php | 70 ++++++++++++ .../Mcp/Tool/UpdatePrestataireTool.php | 59 ++++++++++ src/Shared/Infrastructure/Mcp/Serializer.php | 97 +++++++++++++++++ 23 files changed, 1276 insertions(+), 4 deletions(-) create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/CreateAddressTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/CreateCommercialReportTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/CreateContactTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/CreatePrestataireTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/DeleteAddressTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/DeleteCommercialReportTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/DeleteContactTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/DeletePrestataireTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/GetAddressTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/GetCommercialReportTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/GetContactTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/GetPrestataireTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/ListAddressesTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/ListCommercialReportsTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/ListContactsTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/ListPrestatairesTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/UpdateAddressTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/UpdateCommercialReportTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/UpdateContactTool.php create mode 100644 src/Module/Directory/Infrastructure/Mcp/Tool/UpdatePrestataireTool.php diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/CreateAddressTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/CreateAddressTool.php new file mode 100644 index 0000000..9bef3b5 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/CreateAddressTool.php @@ -0,0 +1,95 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/CreateCommercialReportTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/CreateCommercialReportTool.php new file mode 100644 index 0000000..aa04696 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/CreateCommercialReportTool.php @@ -0,0 +1,103 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/CreateContactTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/CreateContactTool.php new file mode 100644 index 0000000..134e923 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/CreateContactTool.php @@ -0,0 +1,90 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/CreatePrestataireTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/CreatePrestataireTool.php new file mode 100644 index 0000000..6295a30 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/CreatePrestataireTool.php @@ -0,0 +1,43 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteAddressTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteAddressTool.php new file mode 100644 index 0000000..c552d05 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteAddressTool.php @@ -0,0 +1,41 @@ +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)]); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteCommercialReportTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteCommercialReportTool.php new file mode 100644 index 0000000..46818ba --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteCommercialReportTool.php @@ -0,0 +1,41 @@ +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)]); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteContactTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteContactTool.php new file mode 100644 index 0000000..b7595b0 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/DeleteContactTool.php @@ -0,0 +1,41 @@ +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)]); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/DeletePrestataireTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/DeletePrestataireTool.php new file mode 100644 index 0000000..5d8d861 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/DeletePrestataireTool.php @@ -0,0 +1,42 @@ +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)]); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/GetAddressTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/GetAddressTool.php new file mode 100644 index 0000000..0923cfd --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/GetAddressTool.php @@ -0,0 +1,37 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/GetClientTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/GetClientTool.php index 676c1b0..8a7f8b8 100644 --- a/src/Module/Directory/Infrastructure/Mcp/Tool/GetClientTool.php +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/GetClientTool.php @@ -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); } } diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/GetCommercialReportTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/GetCommercialReportTool.php new file mode 100644 index 0000000..2ecac13 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/GetCommercialReportTool.php @@ -0,0 +1,37 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/GetContactTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/GetContactTool.php new file mode 100644 index 0000000..d484250 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/GetContactTool.php @@ -0,0 +1,37 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/GetPrestataireTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/GetPrestataireTool.php new file mode 100644 index 0000000..0193c7e --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/GetPrestataireTool.php @@ -0,0 +1,57 @@ +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); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/GetProspectTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/GetProspectTool.php index 06db797..2a2d13c 100644 --- a/src/Module/Directory/Infrastructure/Mcp/Tool/GetProspectTool.php +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/GetProspectTool.php @@ -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); } } diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/ListAddressesTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/ListAddressesTool.php new file mode 100644 index 0000000..8d3d9dc --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/ListAddressesTool.php @@ -0,0 +1,54 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/ListCommercialReportsTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/ListCommercialReportsTool.php new file mode 100644 index 0000000..d548e3a --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/ListCommercialReportsTool.php @@ -0,0 +1,54 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/ListContactsTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/ListContactsTool.php new file mode 100644 index 0000000..4fdc3a0 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/ListContactsTool.php @@ -0,0 +1,54 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/ListPrestatairesTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/ListPrestatairesTool.php new file mode 100644 index 0000000..8ed55f2 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/ListPrestatairesTool.php @@ -0,0 +1,34 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateAddressTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateAddressTool.php new file mode 100644 index 0000000..598239d --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateAddressTool.php @@ -0,0 +1,73 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateCommercialReportTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateCommercialReportTool.php new file mode 100644 index 0000000..64ffa87 --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateCommercialReportTool.php @@ -0,0 +1,73 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateContactTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateContactTool.php new file mode 100644 index 0000000..737ae7c --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/UpdateContactTool.php @@ -0,0 +1,70 @@ +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)); + } +} diff --git a/src/Module/Directory/Infrastructure/Mcp/Tool/UpdatePrestataireTool.php b/src/Module/Directory/Infrastructure/Mcp/Tool/UpdatePrestataireTool.php new file mode 100644 index 0000000..2845b6a --- /dev/null +++ b/src/Module/Directory/Infrastructure/Mcp/Tool/UpdatePrestataireTool.php @@ -0,0 +1,59 @@ +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)); + } +} diff --git a/src/Shared/Infrastructure/Mcp/Serializer.php b/src/Shared/Infrastructure/Mcp/Serializer.php index bbe17f6..21e3fbc 100644 --- a/src/Shared/Infrastructure/Mcp/Serializer.php +++ b/src/Shared/Infrastructure/Mcp/Serializer.php @@ -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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 */