feat(directory) : add Prospect entity with conversion to Client (back)

LST-58 (2.4), part 2 — Prospect (new entity). Completes the Directory backend.

- ProspectStatus enum (new/contacted/qualified/won/lost) + Prospect entity
  (name, company, email, phone, address, status, source, notes,
  convertedClient -> ClientInterface) with Timestampable/Blamable + #[Auditable].
- API: GetCollection/Get (ROLE_USER), Post/Patch/Delete (ROLE_ADMIN),
  custom POST /prospects/{id}/convert (ConvertProspectProcessor: creates a
  Client from the prospect, links convertedClient, sets status=Won; idempotent).
  SearchFilter on status.
- Repository interface + Doctrine impl (bound); 6 MCP tools (list/get/create/
  update/delete/convert-prospect); Serializer::prospect(). Module perms
  directory.prospects.view/manage. Demo fixtures (3 prospects, one converted).
- Additive migration: CREATE TABLE prospect + FKs ON DELETE SET NULL + COMMENT.

163 tests green (incl. conversion test), mapping valid, cs-fixer clean.
This commit is contained in:
Matthieu
2026-06-20 19:09:12 +02:00
parent c5738d269b
commit d42b288434
17 changed files with 906 additions and 0 deletions
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
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-prospect', description: 'Get a prospect by ID with full details.')]
class GetProspectTool
{
public function __construct(
private readonly ProspectRepositoryInterface $prospectRepository,
private readonly Security $security,
) {}
public function __invoke(int $id): string
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedException('Access denied: ROLE_USER required.');
}
$prospect = $this->prospectRepository->findById($id);
if (null === $prospect) {
throw new InvalidArgumentException(sprintf('Prospect with ID %d not found.', $id));
}
return json_encode(Serializer::prospect($prospect));
}
}