From b498096af17b929d4239ada309c63f02ebf60b04 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Sat, 4 Apr 2026 11:24:52 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20coordonn=C3=A9es=20GPS=20auto=20(API=20?= =?UTF-8?q?IGN)=20+=20code=20comptable=20411=5F=20pr=C3=A9fix=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customer entity : - Ajout geoLat et geoLong (DECIMAL 10,7 nullable) - Migration : ALTER TABLE customer ADD geo_lat, geo_long Géocodage automatique : - API recherche entreprise : récupère siege.latitude/longitude directement - Fallback API IGN (data.geopf.fr/geocodage/search) si coords absentes mais adresse remplie — appelé côté PHP dans geocodeIfNeeded() - Champs hidden geoLat/geoLong dans le formulaire Code comptable 411_ : - Préfixe "411_" affiché en dur (glass-dark, non modifiable) - L'utilisateur saisit uniquement la partie après (ex: 0001_DUPON) - Si vide : génération automatique via generateUniqueCodeComptable() - Concaténation '411_' + saisie dans le contrôleur Tests mis à jour : testGeoCoordinates, HttpClientInterface ajouté dans tous les appels create(), Customer 100% (48/48, 82/82) Co-Authored-By: Claude Opus 4.6 (1M context) --- assets/modules/entreprise-search.js | 2 ++ migrations/Version20260404092101.php | 33 +++++++++++++++++++ src/Controller/Admin/ClientsController.php | 30 ++++++++++++++++- src/Entity/Customer.php | 30 +++++++++++++++++ templates/admin/clients/create.html.twig | 8 +++-- .../Admin/ClientsControllerTest.php | 9 +++-- tests/Entity/CustomerTest.php | 12 +++++++ 7 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 migrations/Version20260404092101.php diff --git a/assets/modules/entreprise-search.js b/assets/modules/entreprise-search.js index 0fe1360..0e24b70 100644 --- a/assets/modules/entreprise-search.js +++ b/assets/modules/entreprise-search.js @@ -79,6 +79,8 @@ const renderResult = (e, onSelect) => { fillField('address', addr) fillField('zipCode', s.code_postal || '') fillField('city', s.libelle_commune || '') + fillField('geoLat', s.latitude || '') + fillField('geoLong', s.longitude || '') const typeCompany = resolveTypeCompany(e.nature_juridique) if (typeCompany) fillField('typeCompany', typeCompany) diff --git a/migrations/Version20260404092101.php b/migrations/Version20260404092101.php new file mode 100644 index 0000000..ed3b716 --- /dev/null +++ b/migrations/Version20260404092101.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE customer ADD geo_lat NUMERIC(10, 7) DEFAULT NULL'); + $this->addSql('ALTER TABLE customer ADD geo_long NUMERIC(10, 7) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE customer DROP geo_lat'); + $this->addSql('ALTER TABLE customer DROP geo_long'); + } +} diff --git a/src/Controller/Admin/ClientsController.php b/src/Controller/Admin/ClientsController.php index 9ed32a1..ad36345 100644 --- a/src/Controller/Admin/ClientsController.php +++ b/src/Controller/Admin/ClientsController.php @@ -40,6 +40,7 @@ class ClientsController extends AbstractController MeilisearchService $meilisearch, UserManagementService $userService, LoggerInterface $logger, + HttpClientInterface $httpClient, #[Autowire(env: 'STRIPE_SK')] string $stripeSecretKey, ): Response { if ('POST' !== $request->getMethod()) { @@ -55,6 +56,7 @@ class ClientsController extends AbstractController $customer = new Customer($user); $this->populateCustomerData($request, $customer); + $this->geocodeIfNeeded($customer, $httpClient); $this->initStripeCustomer($customer, $stripeSecretKey); @@ -64,7 +66,7 @@ class ClientsController extends AbstractController $this->finalizeStripeCustomer($customer, $user, $stripeSecretKey); $codeComptable = trim($request->request->getString('codeComptable')); - $customer->setCodeComptable('' !== $codeComptable ? $codeComptable : $customerRepository->generateUniqueCodeComptable($customer)); + $customer->setCodeComptable('' !== $codeComptable ? '411_'.$codeComptable : $customerRepository->generateUniqueCodeComptable($customer)); $em->flush(); $this->indexInMeilisearch($meilisearch, $customer, $logger); @@ -98,6 +100,32 @@ class ClientsController extends AbstractController $customer->setZipCode(trim($request->request->getString('zipCode')) ?: null); $customer->setCity(trim($request->request->getString('city')) ?: null); $customer->setTypeCompany(trim($request->request->getString('typeCompany')) ?: null); + $customer->setGeoLat(trim($request->request->getString('geoLat')) ?: null); + $customer->setGeoLong(trim($request->request->getString('geoLong')) ?: null); + } + + /** @codeCoverageIgnore */ + private function geocodeIfNeeded(Customer $customer, HttpClientInterface $httpClient): void + { + if (null !== $customer->getGeoLat() || null === $customer->getAddress()) { + return; + } + + $q = implode(' ', array_filter([$customer->getAddress(), $customer->getZipCode(), $customer->getCity()])); + + try { + $response = $httpClient->request('GET', 'https://data.geopf.fr/geocodage/search', [ + 'query' => ['q' => $q, 'limit' => 1], + ]); + $data = $response->toArray(); + $coords = $data['features'][0]['geometry']['coordinates'] ?? null; + + if (null !== $coords) { + $customer->setGeoLong((string) $coords[0]); + $customer->setGeoLat((string) $coords[1]); + } + } catch (\Throwable) { + } } /** @codeCoverageIgnore */ diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 1336da1..e8f4241 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -74,6 +74,12 @@ class Customer #[ORM\Column(length: 255, nullable: true)] private ?string $city = null; + #[ORM\Column(type: 'decimal', precision: 10, scale: 7, nullable: true)] + private ?string $geoLat = null; + + #[ORM\Column(type: 'decimal', precision: 10, scale: 7, nullable: true)] + private ?string $geoLong = null; + #[ORM\Column(length: 14, nullable: true)] private ?string $siret = null; @@ -271,6 +277,30 @@ class Customer return $this; } + public function getGeoLat(): ?string + { + return $this->geoLat; + } + + public function setGeoLat(?string $geoLat): static + { + $this->geoLat = $geoLat; + + return $this; + } + + public function getGeoLong(): ?string + { + return $this->geoLong; + } + + public function setGeoLong(?string $geoLong): static + { + $this->geoLong = $geoLong; + + return $this; + } + public function getSiret(): ?string { return $this->siret; diff --git a/templates/admin/clients/create.html.twig b/templates/admin/clients/create.html.twig index 7bed114..5253849 100644 --- a/templates/admin/clients/create.html.twig +++ b/templates/admin/clients/create.html.twig @@ -75,8 +75,10 @@
- +
+ 411_ +
@@ -136,6 +138,8 @@ class="w-full px-4 py-3 input-glass text-sm font-medium">
+ +
diff --git a/tests/Controller/Admin/ClientsControllerTest.php b/tests/Controller/Admin/ClientsControllerTest.php index 7ad5b6c..cd9fe34 100644 --- a/tests/Controller/Admin/ClientsControllerTest.php +++ b/tests/Controller/Admin/ClientsControllerTest.php @@ -75,7 +75,7 @@ class ClientsControllerTest extends TestCase $userService = $this->createStub(UserManagementService::class); $logger = $this->createStub(LoggerInterface::class); - $response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, 'sk_test_***'); + $response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), 'sk_test_***'); $this->assertInstanceOf(Response::class, $response); } @@ -95,7 +95,7 @@ class ClientsControllerTest extends TestCase $userService->method('createBaseUser')->willThrowException(new \InvalidArgumentException('Champs requis')); $logger = $this->createStub(LoggerInterface::class); - $response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, 'sk_test_***'); + $response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), 'sk_test_***'); $this->assertInstanceOf(Response::class, $response); } @@ -115,7 +115,7 @@ class ClientsControllerTest extends TestCase $userService->method('createBaseUser')->willThrowException(new \RuntimeException('DB error')); $logger = $this->createStub(LoggerInterface::class); - $response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, 'sk_test_***'); + $response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), 'sk_test_***'); $this->assertInstanceOf(Response::class, $response); } @@ -287,6 +287,7 @@ class ClientsControllerTest extends TestCase $this->createStub(MeilisearchService::class), $userService, $this->createStub(LoggerInterface::class), + $this->createStub(HttpClientInterface::class), '', ); $this->assertSame(302, $response->getStatusCode()); @@ -324,6 +325,7 @@ class ClientsControllerTest extends TestCase $this->createStub(MeilisearchService::class), $userService, $this->createStub(LoggerInterface::class), + $this->createStub(HttpClientInterface::class), 'sk_test_***', ); $this->assertSame(302, $response->getStatusCode()); @@ -364,6 +366,7 @@ class ClientsControllerTest extends TestCase $meilisearch, $userService, $this->createStub(LoggerInterface::class), + $this->createStub(HttpClientInterface::class), '', ); $this->assertSame(302, $response->getStatusCode()); diff --git a/tests/Entity/CustomerTest.php b/tests/Entity/CustomerTest.php index cbc9cd9..ece4229 100644 --- a/tests/Entity/CustomerTest.php +++ b/tests/Entity/CustomerTest.php @@ -111,6 +111,18 @@ class CustomerTest extends TestCase $this->assertSame('Saint-Quentin', $c->getCity()); } + public function testGeoCoordinates(): void + { + $c = $this->createCustomer(); + $this->assertNull($c->getGeoLat()); + $this->assertNull($c->getGeoLong()); + + $c->setGeoLat('49.8486'); + $c->setGeoLong('3.2876'); + $this->assertSame('49.8486', $c->getGeoLat()); + $this->assertSame('3.2876', $c->getGeoLong()); + } + public function testLegal(): void { $c = $this->createCustomer();