feat: coordonnées GPS auto (API IGN) + code comptable 411_ préfixé
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
33
migrations/Version20260404092101.php
Normal file
33
migrations/Version20260404092101.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260404092101 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -75,8 +75,10 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="md:col-span-2">
|
||||
<label for="codeComptable" class="block text-xs font-bold uppercase tracking-wider mb-2">Code comptable</label>
|
||||
<input type="text" id="codeComptable" name="codeComptable" placeholder="Laissez vide pour generation automatique (411_XXXX_XXXXX)"
|
||||
class="w-full px-4 py-3 input-glass text-sm font-medium font-mono">
|
||||
<div class="flex">
|
||||
<span class="px-4 py-3 glass-dark text-white text-sm font-bold font-mono flex items-center" style="border-radius: 6px 0 0 6px;">411_</span>
|
||||
<input type="text" id="codeComptable" name="codeComptable" placeholder="Vide = auto (XXXX_XXXXX)"
|
||||
class="flex-1 px-4 py-3 input-glass text-sm font-medium font-mono" style="border-radius: 0 6px 6px 0;">
|
||||
</div>
|
||||
<div>
|
||||
<label for="raisonSociale" class="block text-xs font-bold uppercase tracking-wider mb-2">Raison sociale</label>
|
||||
@@ -136,6 +138,8 @@
|
||||
class="w-full px-4 py-3 input-glass text-sm font-medium">
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="geoLat" name="geoLat">
|
||||
<input type="hidden" id="geoLong" name="geoLong">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user