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
+39
View File
@@ -15,6 +15,8 @@ use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Core\Application\Rbac\RbacSeeder;
use App\Module\Core\Domain\Entity\User;
use App\Module\Directory\Domain\Entity\Client;
use App\Module\Directory\Domain\Entity\Prospect;
use App\Module\Directory\Domain\Enum\ProspectStatus;
use App\Module\ProjectManagement\Domain\Entity\Project;
use App\Module\ProjectManagement\Domain\Entity\Task;
use App\Module\ProjectManagement\Domain\Entity\TaskEffort;
@@ -104,6 +106,43 @@ class AppFixtures extends Fixture
$clientNova->setPostalCode('69007');
$manager->persist($clientNova);
// Prospects
$prospectLead = new Prospect();
$prospectLead->setName('Marie Dupont');
$prospectLead->setCompany('Atelier Dupont');
$prospectLead->setEmail('marie@atelier-dupont.fr');
$prospectLead->setPhone('06 11 22 33 44');
$prospectLead->setCity('Nantes');
$prospectLead->setPostalCode('44000');
$prospectLead->setStatus(ProspectStatus::New);
$prospectLead->setSource('Site web');
$prospectLead->setNotes('Demande de devis via le formulaire de contact.');
$manager->persist($prospectLead);
$prospectQualified = new Prospect();
$prospectQualified->setName('Jean Martin');
$prospectQualified->setCompany('Martin & Fils');
$prospectQualified->setEmail('contact@martin-fils.fr');
$prospectQualified->setPhone('07 55 66 77 88');
$prospectQualified->setStreet('22 rue du Commerce');
$prospectQualified->setCity('Bordeaux');
$prospectQualified->setPostalCode('33000');
$prospectQualified->setStatus(ProspectStatus::Qualified);
$prospectQualified->setSource('Salon professionnel');
$manager->persist($prospectQualified);
$prospectWon = new Prospect();
$prospectWon->setName('Sophie Bernard');
$prospectWon->setCompany('ACME Corp');
$prospectWon->setEmail('contact@acme.com');
$prospectWon->setPhone('01 23 45 67 89');
$prospectWon->setCity('Paris');
$prospectWon->setPostalCode('75002');
$prospectWon->setStatus(ProspectStatus::Won);
$prospectWon->setSource('Recommandation');
$prospectWon->setConvertedClient($clientAcme);
$manager->persist($prospectWon);
// Workflow par défaut
$standardWorkflow = new Workflow();
$standardWorkflow->setName('Standard');
+30
View File
@@ -9,6 +9,7 @@ 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\Client;
use App\Module\Directory\Domain\Entity\Prospect;
use App\Module\ProjectManagement\Domain\Entity\Project;
use App\Module\ProjectManagement\Domain\Entity\Task;
use App\Module\ProjectManagement\Domain\Entity\TaskDocument;
@@ -372,6 +373,35 @@ final class Serializer
];
}
/**
* @return array<string, mixed>
*/
public static function prospect(Prospect $p): array
{
$client = $p->getConvertedClient();
return [
'id' => $p->getId(),
'name' => $p->getName(),
'company' => $p->getCompany(),
'email' => $p->getEmail(),
'phone' => $p->getPhone(),
'street' => $p->getStreet(),
'city' => $p->getCity(),
'postalCode' => $p->getPostalCode(),
'status' => $p->getStatus()->value,
'statusLabel' => $p->getStatus()->label(),
'source' => $p->getSource(),
'notes' => $p->getNotes(),
'convertedClient' => null === $client ? null : [
'id' => $client->getId(),
'name' => $client->getName(),
],
'createdAt' => $p->getCreatedAt()?->format('c'),
'updatedAt' => $p->getUpdatedAt()?->format('c'),
];
}
/**
* @return array<string, mixed>
*/
+2
View File
@@ -36,6 +36,8 @@ final class DirectoryModule implements ModuleInterface
return [
['code' => 'directory.clients.view', 'label' => 'Voir les clients'],
['code' => 'directory.clients.manage', 'label' => 'Gérer les clients'],
['code' => 'directory.prospects.view', 'label' => 'Voir les prospects'],
['code' => 'directory.prospects.manage', 'label' => 'Gérer les prospects'],
];
}
}
@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Domain\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Module\Directory\Domain\Enum\ProspectStatus;
use App\Module\Directory\Infrastructure\ApiPlatform\State\ConvertProspectProcessor;
use App\Module\Directory\Infrastructure\Doctrine\DoctrineProspectRepository;
use App\Shared\Domain\Attribute\Auditable;
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\ClientInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[Auditable]
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
new Post(
uriTemplate: '/prospects/{id}/convert',
security: "is_granted('ROLE_ADMIN')",
processor: ConvertProspectProcessor::class,
),
],
normalizationContext: ['groups' => ['prospect:read']],
denormalizationContext: ['groups' => ['prospect:write']],
order: ['name' => 'ASC'],
)]
#[ApiFilter(SearchFilter::class, properties: ['status' => 'exact'])]
#[ORM\Entity(repositoryClass: DoctrineProspectRepository::class)]
#[ORM\Table(name: 'prospect')]
class Prospect implements TimestampableInterface, BlamableInterface
{
use TimestampableBlamableTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['prospect:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $name = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $company = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $email = null;
#[ORM\Column(length: 50, nullable: true)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $phone = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $street = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $city = null;
#[ORM\Column(length: 20, nullable: true)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $postalCode = null;
#[ORM\Column(type: Types::STRING, length: 32, enumType: ProspectStatus::class)]
#[Groups(['prospect:read', 'prospect:write'])]
private ProspectStatus $status = ProspectStatus::New;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $source = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Groups(['prospect:read', 'prospect:write'])]
private ?string $notes = null;
#[ORM\ManyToOne(targetEntity: ClientInterface::class)]
#[ORM\JoinColumn(name: 'converted_client_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
#[Groups(['prospect:read'])]
private ?ClientInterface $convertedClient = null;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getCompany(): ?string
{
return $this->company;
}
public function setCompany(?string $company): static
{
$this->company = $company;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): static
{
$this->email = $email;
return $this;
}
public function getPhone(): ?string
{
return $this->phone;
}
public function setPhone(?string $phone): static
{
$this->phone = $phone;
return $this;
}
public function getStreet(): ?string
{
return $this->street;
}
public function setStreet(?string $street): static
{
$this->street = $street;
return $this;
}
public function getCity(): ?string
{
return $this->city;
}
public function setCity(?string $city): static
{
$this->city = $city;
return $this;
}
public function getPostalCode(): ?string
{
return $this->postalCode;
}
public function setPostalCode(?string $postalCode): static
{
$this->postalCode = $postalCode;
return $this;
}
public function getStatus(): ProspectStatus
{
return $this->status;
}
public function setStatus(ProspectStatus $status): static
{
$this->status = $status;
return $this;
}
public function getSource(): ?string
{
return $this->source;
}
public function setSource(?string $source): static
{
$this->source = $source;
return $this;
}
public function getNotes(): ?string
{
return $this->notes;
}
public function setNotes(?string $notes): static
{
$this->notes = $notes;
return $this;
}
public function getConvertedClient(): ?ClientInterface
{
return $this->convertedClient;
}
public function setConvertedClient(?ClientInterface $convertedClient): static
{
$this->convertedClient = $convertedClient;
return $this;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Domain\Enum;
enum ProspectStatus: string
{
case New = 'new';
case Contacted = 'contacted';
case Qualified = 'qualified';
case Won = 'won';
case Lost = 'lost';
public function label(): string
{
return match ($this) {
self::New => 'Nouveau',
self::Contacted => 'Contacté',
self::Qualified => 'Qualifié',
self::Won => 'Gagné',
self::Lost => 'Perdu',
};
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Domain\Repository;
use App\Module\Directory\Domain\Entity\Prospect;
interface ProspectRepositoryInterface
{
public function findById(int $id): ?Prospect;
/**
* @param array<string, mixed> $criteria
* @param null|array<string, string> $orderBy
*
* @return Prospect[]
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Infrastructure\ApiPlatform\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Module\Directory\Domain\Entity\Client;
use App\Module\Directory\Domain\Entity\Prospect;
use App\Module\Directory\Domain\Enum\ProspectStatus;
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Converts a Prospect into a Client.
*
* Loads the Prospect via the URI id, creates a Client (name = company or name,
* copying contact details), links it back via convertedClient and flags the
* prospect as Won. Idempotent: if already converted, returns it unchanged.
*
* @implements ProcessorInterface<Prospect, Prospect>
*/
final readonly class ConvertProspectProcessor implements ProcessorInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private ProspectRepositoryInterface $prospectRepository,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Prospect
{
$id = $uriVariables['id'] ?? null;
$prospect = is_numeric($id) ? $this->prospectRepository->findById((int) $id) : null;
if (!$prospect instanceof Prospect) {
throw new NotFoundHttpException('Prospect not found.');
}
// Idempotent: already converted, return as-is.
if (null !== $prospect->getConvertedClient()) {
return $prospect;
}
$client = new Client();
$client->setName($prospect->getCompany() ?: (string) $prospect->getName());
$client->setEmail($prospect->getEmail());
$client->setPhone($prospect->getPhone());
$client->setStreet($prospect->getStreet());
$client->setCity($prospect->getCity());
$client->setPostalCode($prospect->getPostalCode());
$this->entityManager->persist($client);
$prospect->setConvertedClient($client);
$prospect->setStatus(ProspectStatus::Won);
$this->entityManager->flush();
return $prospect;
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Infrastructure\Doctrine;
use App\Module\Directory\Domain\Entity\Prospect;
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Prospect>
*/
final class DoctrineProspectRepository extends ServiceEntityRepository implements ProspectRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Prospect::class);
}
public function findById(int $id): ?Prospect
{
return $this->find($id);
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Module\Directory\Domain\Entity\Client;
use App\Module\Directory\Domain\Enum\ProspectStatus;
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
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: 'convert-prospect', description: 'Convert a prospect into a client (admin). Idempotent: returns the prospect unchanged if already converted.')]
class ConvertProspectTool
{
public function __construct(
private readonly ProspectRepositoryInterface $prospectRepository,
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.');
}
$prospect = $this->prospectRepository->findById($id);
if (null === $prospect) {
throw new InvalidArgumentException(sprintf('Prospect with ID %d not found.', $id));
}
if (null === $prospect->getConvertedClient()) {
$client = new Client();
$client->setName($prospect->getCompany() ?: (string) $prospect->getName());
$client->setEmail($prospect->getEmail());
$client->setPhone($prospect->getPhone());
$client->setStreet($prospect->getStreet());
$client->setCity($prospect->getCity());
$client->setPostalCode($prospect->getPostalCode());
$this->entityManager->persist($client);
$prospect->setConvertedClient($client);
$prospect->setStatus(ProspectStatus::Won);
$this->entityManager->flush();
}
return json_encode(Serializer::prospect($prospect));
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Module\Directory\Domain\Entity\Prospect;
use App\Module\Directory\Domain\Enum\ProspectStatus;
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-prospect', description: 'Create a prospect (admin). Only name is required. Status defaults to "new".')]
class CreateProspectTool
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
) {}
public function __invoke(
string $name,
?string $company = null,
?string $email = null,
?string $phone = null,
?string $street = null,
?string $city = null,
?string $postalCode = null,
?string $status = null,
?string $source = null,
?string $notes = null,
): string {
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
}
$prospect = new Prospect();
$prospect->setName($name);
$prospect->setCompany($company);
$prospect->setEmail($email);
$prospect->setPhone($phone);
$prospect->setStreet($street);
$prospect->setCity($city);
$prospect->setPostalCode($postalCode);
$prospect->setSource($source);
$prospect->setNotes($notes);
if (null !== $status) {
$statusEnum = ProspectStatus::tryFrom($status);
if (null === $statusEnum) {
throw new InvalidArgumentException(sprintf('Invalid status "%s". Allowed: new, contacted, qualified, won, lost.', $status));
}
$prospect->setStatus($statusEnum);
}
$this->entityManager->persist($prospect);
$this->entityManager->flush();
return json_encode(Serializer::prospect($prospect));
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
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-prospect', description: 'Delete a prospect (admin).')]
class DeleteProspectTool
{
public function __construct(
private readonly ProspectRepositoryInterface $prospectRepository,
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.');
}
$prospect = $this->prospectRepository->findById($id);
if (null === $prospect) {
throw new InvalidArgumentException(sprintf('Prospect with ID %d not found.', $id));
}
$name = $prospect->getName();
$this->entityManager->remove($prospect);
$this->entityManager->flush();
return json_encode(['success' => true, 'message' => sprintf('Prospect "%s" deleted.', $name)]);
}
}
@@ -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));
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Module\Directory\Domain\Enum\ProspectStatus;
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: 'list-prospects', description: 'List prospects, optionally filtered by status (new, contacted, qualified, won, lost).')]
class ListProspectsTool
{
public function __construct(
private readonly ProspectRepositoryInterface $prospectRepository,
private readonly Security $security,
) {}
public function __invoke(?string $status = null): string
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedException('Access denied: ROLE_USER required.');
}
$criteria = [];
if (null !== $status) {
$statusEnum = ProspectStatus::tryFrom($status);
if (null === $statusEnum) {
throw new InvalidArgumentException(sprintf('Invalid status "%s". Allowed: new, contacted, qualified, won, lost.', $status));
}
$criteria['status'] = $statusEnum;
}
$prospects = $this->prospectRepository->findBy($criteria, ['name' => 'ASC']);
return json_encode(array_map(static fn ($prospect) => Serializer::prospect($prospect), $prospects));
}
}
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Module\Directory\Infrastructure\Mcp\Tool;
use App\Mcp\Tool\Serializer;
use App\Module\Directory\Domain\Enum\ProspectStatus;
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
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-prospect', description: 'Update a prospect (admin). Only provided fields change.')]
class UpdateProspectTool
{
public function __construct(
private readonly ProspectRepositoryInterface $prospectRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
) {}
public function __invoke(
int $id,
?string $name = null,
?string $company = null,
?string $email = null,
?string $phone = null,
?string $street = null,
?string $city = null,
?string $postalCode = null,
?string $status = null,
?string $source = null,
?string $notes = null,
): string {
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
}
$prospect = $this->prospectRepository->findById($id);
if (null === $prospect) {
throw new InvalidArgumentException(sprintf('Prospect with ID %d not found.', $id));
}
if (null !== $name) {
$prospect->setName($name);
}
if (null !== $company) {
$prospect->setCompany($company);
}
if (null !== $email) {
$prospect->setEmail($email);
}
if (null !== $phone) {
$prospect->setPhone($phone);
}
if (null !== $street) {
$prospect->setStreet($street);
}
if (null !== $city) {
$prospect->setCity($city);
}
if (null !== $postalCode) {
$prospect->setPostalCode($postalCode);
}
if (null !== $status) {
$statusEnum = ProspectStatus::tryFrom($status);
if (null === $statusEnum) {
throw new InvalidArgumentException(sprintf('Invalid status "%s". Allowed: new, contacted, qualified, won, lost.', $status));
}
$prospect->setStatus($statusEnum);
}
if (null !== $source) {
$prospect->setSource($source);
}
if (null !== $notes) {
$prospect->setNotes($notes);
}
$this->entityManager->flush();
return json_encode(Serializer::prospect($prospect));
}
}