Files
Starseed/tests/Module/FieldSales/Api/VisitableTierApiTest.php
T
Matthieu f8793ab359
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Failing after 56s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Failing after 21s
feat(field_sales) : carte interactive Leaflet + écran de planification de tournée (ERP-127)
- 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
2026-06-11 17:38:40 +02:00

176 lines
6.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Module\FieldSales\Api;
/**
* Tests fonctionnels de l'endpoint GET /api/visitable_tiers (M6.5 — pins de la
* carte de planification).
*
* Couvre : exposition des adresses geolocalisees Client/Fournisseur, exclusion
* des adresses sans coordonnees (RG-6.05) et des Tiers archives/supprimes,
* filtres bbox / q / type, securite RBAC, echappatoire ?pagination=false.
*
* @internal
*/
final class VisitableTierApiTest extends AbstractFieldSalesApiTestCase
{
private const string LD = 'application/ld+json';
/** Isole les Tiers seedes par ces tests (prefixe commun client + supplier). */
private const string ISOLATE_Q = 'TEST_FS_';
public function testExposesGeolocatedClientAndSupplierPins(): void
{
$client = $this->authenticatedClient('admin', 'admin');
$tierClient = $this->seedClient('Ferme Pin');
$this->seedClientAddress($tierClient, 47.218, -1.553);
$tierSupplier = $this->seedSupplier('Negoce Pin');
$this->seedSupplierAddress($tierSupplier, 47.220, -1.560);
$data = $client->request('GET', '/api/visitable_tiers', [
'headers' => ['Accept' => self::LD],
'query' => ['q' => self::ISOLATE_Q],
])->toArray();
self::assertSame(2, $data['totalItems'], 'Une adresse geolocalisee Client + une Fournisseur = 2 pins.');
$byType = [];
foreach ($data['member'] as $pin) {
$byType[$pin['tierType']] = $pin;
}
self::assertArrayHasKey('client', $byType);
self::assertArrayHasKey('supplier', $byType);
self::assertSame((float) 47.218, $byType['client']['latitude']);
self::assertSame($tierClient->getId(), $byType['client']['tierId']);
self::assertStringContainsString('NANTES', $byType['client']['address']);
self::assertSame('client-'.$byType['client']['addressId'], $byType['client']['id']);
}
public function testExcludesAddressesWithoutCoordinates(): void
{
$client = $this->authenticatedClient('admin', 'admin');
$tier = $this->seedClient('Sans Geo');
// Adresse sans lat/lng (RG-6.05 : exclue du calcul/carte).
$this->seedClientAddress($tier, 0, 0);
$this->getEm()->getConnection()->executeStatement(
"UPDATE client_address SET latitude = NULL, longitude = NULL WHERE city = 'NANTES' AND street = '1 rue de Test' AND client_id = :id",
['id' => $tier->getId()],
);
$data = $client->request('GET', '/api/visitable_tiers', [
'headers' => ['Accept' => self::LD],
'query' => ['q' => self::ISOLATE_Q],
])->toArray();
self::assertSame(0, $data['totalItems'], 'Une adresse sans coordonnees ne produit aucun pin (RG-6.05).');
}
public function testBboxRestrictsToVisibleArea(): void
{
$client = $this->authenticatedClient('admin', 'admin');
$inside = $this->seedClient('Dedans');
$this->seedClientAddress($inside, 47.0, -1.5);
$outside = $this->seedClient('Dehors');
$this->seedClientAddress($outside, 10.0, 10.0);
// bbox = minLng,minLat,maxLng,maxLat autour du point « dedans ».
$data = $client->request('GET', '/api/visitable_tiers', [
'headers' => ['Accept' => self::LD],
'query' => ['q' => self::ISOLATE_Q, 'bbox' => '-2.0,46.5,-1.0,47.5'],
])->toArray();
self::assertSame(1, $data['totalItems'], 'Seul le pin dans la bbox est retourne.');
self::assertSame($inside->getId(), $data['member'][0]['tierId']);
}
public function testTypeFilterRestrictsTiers(): void
{
$client = $this->authenticatedClient('admin', 'admin');
$this->seedClientAddress($this->seedClient('Client T'));
$this->seedSupplierAddress($this->seedSupplier('Supplier T'));
$data = $client->request('GET', '/api/visitable_tiers', [
'headers' => ['Accept' => self::LD],
'query' => ['q' => self::ISOLATE_Q, 'type' => 'supplier'],
])->toArray();
self::assertSame(1, $data['totalItems'], 'type=supplier ne retourne que les fournisseurs.');
self::assertSame('supplier', $data['member'][0]['tierType']);
}
public function testInvalidTypeReturns400(): void
{
$client = $this->authenticatedClient('admin', 'admin');
$client->request('GET', '/api/visitable_tiers', [
'headers' => ['Accept' => self::LD],
'query' => ['type' => 'prestataire_inexistant'],
]);
self::assertResponseStatusCodeSame(400, 'Un type hors whitelist est rejete en 400.');
}
public function testMalformedBboxReturns400(): void
{
$client = $this->authenticatedClient('admin', 'admin');
$client->request('GET', '/api/visitable_tiers', [
'headers' => ['Accept' => self::LD],
'query' => ['bbox' => '1,2,3'],
]);
self::assertResponseStatusCodeSame(400, 'Une bbox a 3 valeurs est rejetee en 400.');
}
public function testExcludesArchivedTier(): void
{
$client = $this->authenticatedClient('admin', 'admin');
$tier = $this->seedClient('Archive');
$this->seedClientAddress($tier);
$this->getEm()->getConnection()->executeStatement(
'UPDATE client SET is_archived = TRUE WHERE id = :id',
['id' => $tier->getId()],
);
$data = $client->request('GET', '/api/visitable_tiers', [
'headers' => ['Accept' => self::LD],
'query' => ['q' => self::ISOLATE_Q],
])->toArray();
self::assertSame(0, $data['totalItems'], 'Un Tiers archive ne produit aucun pin.');
}
public function testRequiresViewPermission(): void
{
$creds = $this->createUserWithPermission('core.users.view');
$client = $this->authenticatedClient($creds['username'], $creds['password']);
$client->request('GET', '/api/visitable_tiers', ['headers' => ['Accept' => self::LD]]);
self::assertResponseStatusCodeSame(403, 'Sans field_sales.tours.view -> 403.');
}
public function testPaginationFalseReturnsAllPins(): void
{
$client = $this->authenticatedClient('admin', 'admin');
for ($i = 0; $i < 12; ++$i) {
$this->seedClientAddress($this->seedClient('Bulk '.$i), 47.0 + $i / 1000, -1.5);
}
$data = $client->request('GET', '/api/visitable_tiers', [
'headers' => ['Accept' => self::LD],
'query' => ['q' => self::ISOLATE_Q, 'pagination' => 'false'],
])->toArray();
self::assertCount(12, $data['member'], 'pagination=false retourne tous les pins de la zone (carte).');
}
}