de4aaa1d64
Ajoute la géolocalisation aux adresses Client et Fournisseur, socle de la tournée commerciale (M6 field-sales). Back : - migration : latitude/longitude NUMERIC(10,7), geo_manual BOOLEAN, geocoded_at TIMESTAMPTZ sur client_address et supplier_address (+ COMMENT ON COLUMN FR) - GeolocatableAddressInterface (Shared/Domain/Contract) implémenté par les deux entités ; bornes WGS84 validées (Range -90/90, -180/180, messages FR) - GeocoderInterface + BanGeocoder (api-adresse.data.gouv.fr), branché via AddressGeocoder dans les processors ; géocodage auto au create/update - RG-6.08 : geo_manual=true fige les coordonnées (pas de réécriture auto) - symfony/http-client passe en dépendance de production Front : - AddressGeoPin (Leaflet + OSM) : marqueur déplaçable -> PATCH lat/lng + geoManual=true, bouton Re-géocoder, badges « à géolocaliser » / « pin manuel » - intégration dans les blocs adresse Client et Fournisseur Tests : PHPUnit (géocodage create, non-réécriture RG-6.08, mapping BAN, bornes) + Vitest (drag du pin, badges, re-géocodage).
108 lines
3.6 KiB
PHP
108 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Shared\Geocoding;
|
|
|
|
use App\Shared\Infrastructure\Geocoding\BanGeocoder;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\NullLogger;
|
|
use Symfony\Component\HttpClient\MockHttpClient;
|
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
|
|
|
/**
|
|
* Mapping de la reponse BAN (api-adresse.data.gouv.fr/search/) vers le value
|
|
* object Coordinates (M6.1, spec § 7) — via MockHttpClient, sans reseau.
|
|
*
|
|
* Contrat teste : GeoJSON [longitude, latitude] inverse en (latitude,
|
|
* longitude), arrondi NUMERIC(10,7), score minimal, et tolerance aux pannes
|
|
* (toute erreur -> null, jamais d'exception).
|
|
*
|
|
* @internal
|
|
*/
|
|
final class BanGeocoderTest extends TestCase
|
|
{
|
|
public function testGeocodeMapsBanFeatureToCoordinates(): void
|
|
{
|
|
$geocoder = $this->geocoderReturning([
|
|
'features' => [[
|
|
'geometry' => ['coordinates' => [0.3404333, 46.5802596]],
|
|
'properties' => ['score' => 0.97],
|
|
]],
|
|
]);
|
|
|
|
$coordinates = $geocoder->geocode('1 rue du Test, 86100 Châtellerault');
|
|
|
|
self::assertNotNull($coordinates);
|
|
// GeoJSON = [longitude, latitude] : l'ordre doit etre inverse.
|
|
self::assertSame('46.5802596', $coordinates->latitude);
|
|
self::assertSame('0.3404333', $coordinates->longitude);
|
|
}
|
|
|
|
public function testGeocodeRoundsToSevenDecimals(): void
|
|
{
|
|
$geocoder = $this->geocoderReturning([
|
|
'features' => [[
|
|
'geometry' => ['coordinates' => [0.34043337777, 46.58025961111]],
|
|
'properties' => ['score' => 0.9],
|
|
]],
|
|
]);
|
|
|
|
$coordinates = $geocoder->geocode('1 rue du Test');
|
|
|
|
self::assertNotNull($coordinates);
|
|
self::assertSame('46.5802596', $coordinates->latitude);
|
|
self::assertSame('0.3404334', $coordinates->longitude);
|
|
}
|
|
|
|
public function testGeocodeReturnsNullWhenScoreTooLow(): void
|
|
{
|
|
// Score < 0.4 : resultat juge non fiable (adresse etrangere, lieu-dit
|
|
// inconnu) -> pas de coordonnees plutot qu'une position fantaisiste.
|
|
$geocoder = $this->geocoderReturning([
|
|
'features' => [[
|
|
'geometry' => ['coordinates' => [0.34, 46.58]],
|
|
'properties' => ['score' => 0.12],
|
|
]],
|
|
]);
|
|
|
|
self::assertNull($geocoder->geocode('Bahnhofstrasse 1, Zürich'));
|
|
}
|
|
|
|
public function testGeocodeReturnsNullWhenNoFeature(): void
|
|
{
|
|
$geocoder = $this->geocoderReturning(['features' => []]);
|
|
|
|
self::assertNull($geocoder->geocode('zzz introuvable'));
|
|
}
|
|
|
|
public function testGeocodeReturnsNullOnServerError(): void
|
|
{
|
|
$client = new MockHttpClient(new MockResponse('oops', ['http_code' => 500]));
|
|
$geocoder = new BanGeocoder($client, new NullLogger());
|
|
|
|
self::assertNull($geocoder->geocode('1 rue du Test'));
|
|
}
|
|
|
|
public function testGeocodeReturnsNullOnBlankAddressWithoutHttpCall(): void
|
|
{
|
|
$client = new MockHttpClient(static function (): never {
|
|
self::fail('Aucun appel HTTP attendu pour une adresse vide.');
|
|
});
|
|
$geocoder = new BanGeocoder($client, new NullLogger());
|
|
|
|
self::assertNull($geocoder->geocode(' '));
|
|
}
|
|
|
|
/** Fabrique un BanGeocoder repondant le payload JSON donne (HTTP 200). */
|
|
private function geocoderReturning(array $payload): BanGeocoder
|
|
{
|
|
$client = new MockHttpClient(new MockResponse(
|
|
json_encode($payload, JSON_THROW_ON_ERROR),
|
|
['response_headers' => ['content-type' => 'application/json']],
|
|
));
|
|
|
|
return new BanGeocoder($client, new NullLogger());
|
|
}
|
|
}
|