feat(field_sales) : calcul de trajet, optimisation, duplication & roadbook PDF (ERP-125)
- RouteEngineInterface (computeMatrix/optimizeOrder/estimateLegDurations) + HaversineRouteEngine V1 (vitesse moyenne parametrable, plus proche voisin)
- TourRouteCalculator : resolution coords, ETA (RG-6.11), exclusion sans coords (RG-6.05), totaux ; optimize = reorder + recompute
- Endpoints API Platform POST /tours/{id}/compute, /optimize, /duplicate (TourDuplicator, RG-6.13) + Processors, security manage
- Feuille de route PDF GET /tours/{id}/roadbook.pdf (Dompdf + Twig) via PdfRendererInterface (Shared), controller priority:1, security view
- TierAddressResolver etendu (coords + location DBAL)
- Tests : HaversineRouteEngine (unit), compute/optimize/duplicate/roadbook (API)
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\FieldSales\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Module\FieldSales\Application\Route\TourRouteCalculator;
|
||||
use App\Module\FieldSales\Domain\Entity\Tour;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use function assert;
|
||||
|
||||
/**
|
||||
* Processor de l'operation POST /api/tours/{id}/compute (M6 § 5).
|
||||
*
|
||||
* La tournee est chargee en amont par TourProvider (controle RG-6.01 + soft
|
||||
* delete). L'operation ne porte pas de corps : on recalcule simplement legs +
|
||||
* eta + totaux (HaversineRouteEngine via TourRouteCalculator) puis on persiste.
|
||||
*
|
||||
* @implements ProcessorInterface<Tour, Tour>
|
||||
*/
|
||||
final class TourComputeProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TourRouteCalculator $calculator,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Tour
|
||||
{
|
||||
assert($data instanceof Tour);
|
||||
|
||||
$this->calculator->compute($data);
|
||||
$this->em->flush();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\FieldSales\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use ApiPlatform\Validator\Exception\ValidationException;
|
||||
use App\Module\FieldSales\Application\Duplication\TourDuplicator;
|
||||
use App\Module\FieldSales\Domain\Entity\Tour;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Validator\ConstraintViolation;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
|
||||
use function assert;
|
||||
|
||||
/**
|
||||
* Processor de l'operation POST /api/tours/{id}/duplicate (M6 § 5, RG-6.13).
|
||||
*
|
||||
* La tournee source est chargee par TourProvider (RG-6.01). Le corps porte la
|
||||
* nouvelle date (`tourDate`). On delegue la copie a {@see TourDuplicator} (sans
|
||||
* calculs), on persiste la copie et on la retourne (201).
|
||||
*
|
||||
* Operation deserialize:false : le corps n'est pas mappe sur la source, on lit
|
||||
* `tourDate` manuellement et on leve une 422 (propertyPath `tourDate`) si elle
|
||||
* est absente ou invalide — consommable par useFormErrors cote front.
|
||||
*
|
||||
* @implements ProcessorInterface<Tour, Tour>
|
||||
*/
|
||||
final class TourDuplicateProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TourDuplicator $duplicator,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Tour
|
||||
{
|
||||
assert($data instanceof Tour);
|
||||
|
||||
$tourDate = $this->readTourDate();
|
||||
|
||||
$copy = $this->duplicator->duplicate($data, $tourDate);
|
||||
$this->em->persist($copy);
|
||||
$this->em->flush();
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit et valide `tourDate` depuis le corps JSON de la requete. Format attendu
|
||||
* `Y-m-d`. Leve une 422 portee sur `tourDate` si absente ou invalide.
|
||||
*/
|
||||
private function readTourDate(): DateTimeImmutable
|
||||
{
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$payload = null !== $request ? json_decode($request->getContent(), true) : null;
|
||||
$raw = is_array($payload) ? ($payload['tourDate'] ?? null) : null;
|
||||
|
||||
if (is_string($raw) && '' !== trim($raw)) {
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', trim($raw));
|
||||
if (false !== $date) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
$this->throwTourDateViolation(
|
||||
is_string($raw) && '' !== trim($raw)
|
||||
? 'La date de la tournée doit être au format AAAA-MM-JJ.'
|
||||
: 'La date de la tournée dupliquée est obligatoire.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return never
|
||||
*/
|
||||
private function throwTourDateViolation(string $message): void
|
||||
{
|
||||
$violations = new ConstraintViolationList();
|
||||
$violations->add(new ConstraintViolation($message, null, [], null, 'tourDate', null));
|
||||
|
||||
throw new ValidationException($violations);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\FieldSales\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Module\FieldSales\Application\Route\TourRouteCalculator;
|
||||
use App\Module\FieldSales\Domain\Entity\Tour;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use function assert;
|
||||
|
||||
/**
|
||||
* Processor de l'operation POST /api/tours/{id}/optimize (M6 § 5).
|
||||
*
|
||||
* Reordonne les etapes via le moteur (plus proche voisin) puis recompute legs +
|
||||
* eta + totaux. La tournee est chargee en amont par TourProvider (RG-6.01).
|
||||
*
|
||||
* @implements ProcessorInterface<Tour, Tour>
|
||||
*/
|
||||
final class TourOptimizeProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TourRouteCalculator $calculator,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Tour
|
||||
{
|
||||
assert($data instanceof Tour);
|
||||
|
||||
$this->calculator->optimize($data);
|
||||
$this->em->flush();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user