f8793ab359
- API visitable_tiers (provider DBAL bbox/q/type, paginé) pour les pins de la carte
- POST /tours/{id}/reorder (drag & drop) : renumérotation atomique + recompute
- Layer front field-sales : TourMap (pins, popup, polyline, sélection rectangle),
liste d'étapes draggable (vuedraggable), composable de planification + Vitest
- Pages /tours, /tours/new, /tours/[id]/plan (split responsive, point custom géocodé)
- i18n FR, deep links Waze/Google/Apple, état 100% local
194 lines
6.5 KiB
PHP
194 lines
6.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\FieldSales\Api;
|
|
|
|
use App\Module\Commercial\Domain\Entity\Client;
|
|
use App\Module\Commercial\Domain\Entity\ClientAddress;
|
|
use App\Module\Commercial\Domain\Entity\Supplier;
|
|
use App\Module\Commercial\Domain\Entity\SupplierAddress;
|
|
use App\Module\Core\Domain\Entity\Role;
|
|
use App\Module\Core\Domain\Entity\User;
|
|
use App\Module\FieldSales\Domain\Entity\Tour;
|
|
use App\Tests\Module\Core\Api\AbstractApiTestCase;
|
|
use DateTimeImmutable;
|
|
|
|
/**
|
|
* Base des tests fonctionnels du module FieldSales (M6 — tournees).
|
|
*
|
|
* Mutualise :
|
|
* - des factories EM (sans API) pour seeder vite un Client + une adresse
|
|
* geolocalisee (cible d'etape), et une tournee appartenant a un user donne ;
|
|
* - un helper d'indexation des violations 422 par propertyPath ;
|
|
* - le cleanup des donnees jetables (tournees, clients de test, users/roles test_*).
|
|
*
|
|
* Les imports cross-module (Commercial / Core) sont autorises dans les TESTS
|
|
* (la regle ABSOLUE n°1 vise le code de production).
|
|
*
|
|
* @internal
|
|
*/
|
|
abstract class AbstractFieldSalesApiTestCase extends AbstractApiTestCase
|
|
{
|
|
protected const string TEST_CLIENT_PREFIX = 'TEST_FS_CLIENT_';
|
|
protected const string TEST_SUPPLIER_PREFIX = 'TEST_FS_SUPPLIER_';
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->cleanupFieldSalesTestData();
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* Seede un Client minimal (companyName uniquement — les categories sont une
|
|
* contrainte Assert non rejouee hors API).
|
|
*/
|
|
protected function seedClient(string $companyName): Client
|
|
{
|
|
$em = $this->getEm();
|
|
$client = new Client();
|
|
$client->setCompanyName(self::TEST_CLIENT_PREFIX.mb_strtoupper($companyName, 'UTF-8'));
|
|
$em->persist($client);
|
|
$em->flush();
|
|
|
|
return $client;
|
|
}
|
|
|
|
/**
|
|
* Seede une adresse de prospection geolocalisee rattachee a $client. Les
|
|
* sites/categories (Assert\Count min 1) ne sont pas rejoues hors API : on
|
|
* persiste directement les colonnes NOT NULL + des coordonnees.
|
|
*/
|
|
protected function seedClientAddress(Client $client, float $lat = 47.218, float $lng = -1.553): ClientAddress
|
|
{
|
|
$em = $this->getEm();
|
|
$address = new ClientAddress();
|
|
$address->setClient($client);
|
|
$address->setIsProspect(true);
|
|
$address->setPostalCode('44000');
|
|
$address->setCity('NANTES');
|
|
$address->setStreet('1 rue de Test');
|
|
$address->setLatitude($lat);
|
|
$address->setLongitude($lng);
|
|
$em->persist($address);
|
|
$em->flush();
|
|
|
|
return $address;
|
|
}
|
|
|
|
/**
|
|
* Seede un Fournisseur minimal (companyName uniquement).
|
|
*/
|
|
protected function seedSupplier(string $companyName): Supplier
|
|
{
|
|
$em = $this->getEm();
|
|
$supplier = new Supplier();
|
|
$supplier->setCompanyName(self::TEST_SUPPLIER_PREFIX.mb_strtoupper($companyName, 'UTF-8'));
|
|
$em->persist($supplier);
|
|
$em->flush();
|
|
|
|
return $supplier;
|
|
}
|
|
|
|
/**
|
|
* Seede une adresse geolocalisee rattachee a $supplier (type PROSPECT).
|
|
*/
|
|
protected function seedSupplierAddress(Supplier $supplier, float $lat = 47.218, float $lng = -1.553): SupplierAddress
|
|
{
|
|
$em = $this->getEm();
|
|
$address = new SupplierAddress();
|
|
$address->setSupplier($supplier);
|
|
$address->setAddressType('PROSPECT');
|
|
$address->setPostalCode('44000');
|
|
$address->setCity('NANTES');
|
|
$address->setStreet('2 rue de Test');
|
|
$address->setLatitude($lat);
|
|
$address->setLongitude($lng);
|
|
$em->persist($address);
|
|
$em->flush();
|
|
|
|
return $address;
|
|
}
|
|
|
|
/**
|
|
* Seede une tournee appartenant a $owner (sans passer par l'API).
|
|
*/
|
|
protected function seedTour(User $owner, string $label = 'Tournée test'): Tour
|
|
{
|
|
$em = $this->getEm();
|
|
$tour = new Tour();
|
|
$tour->setOwner($owner);
|
|
$tour->setLabel($label);
|
|
$tour->setTourDate(new DateTimeImmutable('2026-07-01'));
|
|
$em->persist($tour);
|
|
$em->flush();
|
|
|
|
return $tour;
|
|
}
|
|
|
|
/**
|
|
* Recupere un User par username (ex: 'admin', ou un username jetable cree par
|
|
* createUserWithPermission).
|
|
*/
|
|
protected function getUserByUsername(string $username): User
|
|
{
|
|
$user = $this->getEm()->getRepository(User::class)->findOneBy(['username' => $username]);
|
|
self::assertInstanceOf(User::class, $user, sprintf('User "%s" introuvable.', $username));
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Indexe les violations d'un corps 422 par propertyPath.
|
|
*
|
|
* @param array<string, mixed> $body
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
protected function violationsByPath(array $body): array
|
|
{
|
|
$byPath = [];
|
|
foreach ($body['violations'] ?? [] as $v) {
|
|
$byPath[$v['propertyPath']] = $v['message'];
|
|
}
|
|
|
|
return $byPath;
|
|
}
|
|
|
|
private function cleanupFieldSalesTestData(): void
|
|
{
|
|
$em = $this->getEm();
|
|
|
|
// Etapes purgees par CASCADE a la suppression des tournees.
|
|
$em->createQuery('DELETE FROM '.Tour::class)->execute();
|
|
|
|
// Adresses puis clients de test (FK client_address.client_id CASCADE).
|
|
$em->createQuery(
|
|
'DELETE FROM '.ClientAddress::class.' a WHERE a.client IN ('
|
|
.'SELECT c.id FROM '.Client::class.' c WHERE c.companyName LIKE :prefix)',
|
|
)->setParameter('prefix', self::TEST_CLIENT_PREFIX.'%')->execute();
|
|
|
|
$em->createQuery(
|
|
'DELETE FROM '.Client::class.' c WHERE c.companyName LIKE :prefix',
|
|
)->setParameter('prefix', self::TEST_CLIENT_PREFIX.'%')->execute();
|
|
|
|
// Adresses puis fournisseurs de test (FK supplier_address.supplier_id CASCADE).
|
|
$em->createQuery(
|
|
'DELETE FROM '.SupplierAddress::class.' a WHERE a.supplier IN ('
|
|
.'SELECT s.id FROM '.Supplier::class.' s WHERE s.companyName LIKE :prefix)',
|
|
)->setParameter('prefix', self::TEST_SUPPLIER_PREFIX.'%')->execute();
|
|
|
|
$em->createQuery(
|
|
'DELETE FROM '.Supplier::class.' s WHERE s.companyName LIKE :prefix',
|
|
)->setParameter('prefix', self::TEST_SUPPLIER_PREFIX.'%')->execute();
|
|
|
|
$em->createQuery(
|
|
'DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix',
|
|
)->setParameter('prefix', 'testuser_%')->execute();
|
|
|
|
$em->createQuery(
|
|
'DELETE FROM '.Role::class.' r WHERE r.code LIKE :prefix',
|
|
)->setParameter('prefix', 'test_%')->execute();
|
|
}
|
|
}
|