f8f7571cc0
- 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)
102 lines
3.7 KiB
PHP
102 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\FieldSales\Api;
|
|
|
|
use App\Module\FieldSales\Domain\Entity\TourStop;
|
|
|
|
/**
|
|
* Tests fonctionnels de la feuille de route PDF (M6.4 — GET
|
|
* /api/tours/{id}/roadbook.pdf). Couvre la production du binaire PDF, le gating
|
|
* de permission (view) et l'isolation par proprietaire (RG-6.01).
|
|
*
|
|
* @internal
|
|
*/
|
|
final class TourRoadbookApiTest extends AbstractFieldSalesApiTestCase
|
|
{
|
|
private const string LD = 'application/ld+json';
|
|
|
|
public function testRoadbookReturnsPdfBinary(): void
|
|
{
|
|
$client = $this->authenticatedClient('admin', 'admin');
|
|
$admin = $this->getUserByUsername('admin');
|
|
$tour = $this->seedTour($admin, 'Tournée PDF');
|
|
|
|
// Une etape custom + une etape sur Tiers referentiel geolocalise.
|
|
$this->seedCustomStop($tour, 0, 47.0, -1.0, 'RDV prospect');
|
|
$tier = $this->seedClient('Ferme PDF');
|
|
$address = $this->seedClientAddress($tier);
|
|
$this->seedTierStop($tour, $tier, $address, 1);
|
|
|
|
$response = $client->request('GET', '/api/tours/'.$tour->getId().'/roadbook.pdf');
|
|
|
|
self::assertResponseStatusCodeSame(200);
|
|
self::assertResponseHeaderSame('Content-Type', 'application/pdf');
|
|
$content = $response->getContent();
|
|
self::assertStringStartsWith('%PDF', $content, 'Le corps est bien un binaire PDF.');
|
|
self::assertGreaterThan(800, strlen($content), 'Le PDF n\'est pas vide.');
|
|
}
|
|
|
|
public function testRoadbookRequiresViewPermission(): void
|
|
{
|
|
$creds = $this->createUserWithPermission('core.users.view');
|
|
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
|
$admin = $this->getUserByUsername('admin');
|
|
$tour = $this->seedTour($admin);
|
|
|
|
$client->request('GET', '/api/tours/'.$tour->getId().'/roadbook.pdf');
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
}
|
|
|
|
/**
|
|
* RG-6.01 : une Commerciale ne peut pas exporter la tournee d'un autre.
|
|
*/
|
|
public function testRoadbookHidesOthersTour(): void
|
|
{
|
|
$credsA = $this->createUserWithPermissions(['field_sales.tours.view', 'field_sales.tours.manage']);
|
|
$credsB = $this->createUserWithPermissions(['field_sales.tours.view', 'field_sales.tours.manage']);
|
|
|
|
$client = $this->authenticatedClient($credsA['username'], $credsA['password']);
|
|
$userB = $this->getUserByUsername($credsB['username']);
|
|
$tourB = $this->seedTour($userB, 'Privée B');
|
|
|
|
$client->request('GET', '/api/tours/'.$tourB->getId().'/roadbook.pdf');
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
}
|
|
|
|
private function seedCustomStop(\App\Module\FieldSales\Domain\Entity\Tour $tour, int $position, float $lat, float $lng, string $label): TourStop
|
|
{
|
|
$em = $this->getEm();
|
|
$stop = new TourStop();
|
|
$stop->setTour($tour);
|
|
$stop->setTierType(TourStop::TIER_TYPE_CUSTOM);
|
|
$stop->setCustomLabel($label);
|
|
$stop->setCustomAddress('5 place du Marché, 44000 Nantes');
|
|
$stop->setCustomLatitude($lat);
|
|
$stop->setCustomLongitude($lng);
|
|
$stop->setPosition($position);
|
|
$em->persist($stop);
|
|
$em->flush();
|
|
|
|
return $stop;
|
|
}
|
|
|
|
private function seedTierStop(\App\Module\FieldSales\Domain\Entity\Tour $tour, \App\Module\Commercial\Domain\Entity\Client $tier, \App\Module\Commercial\Domain\Entity\ClientAddress $address, int $position): TourStop
|
|
{
|
|
$em = $this->getEm();
|
|
$stop = new TourStop();
|
|
$stop->setTour($tour);
|
|
$stop->setTierType('client');
|
|
$stop->setTierId($tier->getId());
|
|
$stop->setAddressId($address->getId());
|
|
$stop->setPosition($position);
|
|
$em->persist($stop);
|
|
$em->flush();
|
|
|
|
return $stop;
|
|
}
|
|
}
|