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('address', addr)
|
||||||
fillField('zipCode', s.code_postal || '')
|
fillField('zipCode', s.code_postal || '')
|
||||||
fillField('city', s.libelle_commune || '')
|
fillField('city', s.libelle_commune || '')
|
||||||
|
fillField('geoLat', s.latitude || '')
|
||||||
|
fillField('geoLong', s.longitude || '')
|
||||||
|
|
||||||
const typeCompany = resolveTypeCompany(e.nature_juridique)
|
const typeCompany = resolveTypeCompany(e.nature_juridique)
|
||||||
if (typeCompany) fillField('typeCompany', typeCompany)
|
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,
|
MeilisearchService $meilisearch,
|
||||||
UserManagementService $userService,
|
UserManagementService $userService,
|
||||||
LoggerInterface $logger,
|
LoggerInterface $logger,
|
||||||
|
HttpClientInterface $httpClient,
|
||||||
#[Autowire(env: 'STRIPE_SK')] string $stripeSecretKey,
|
#[Autowire(env: 'STRIPE_SK')] string $stripeSecretKey,
|
||||||
): Response {
|
): Response {
|
||||||
if ('POST' !== $request->getMethod()) {
|
if ('POST' !== $request->getMethod()) {
|
||||||
@@ -55,6 +56,7 @@ class ClientsController extends AbstractController
|
|||||||
|
|
||||||
$customer = new Customer($user);
|
$customer = new Customer($user);
|
||||||
$this->populateCustomerData($request, $customer);
|
$this->populateCustomerData($request, $customer);
|
||||||
|
$this->geocodeIfNeeded($customer, $httpClient);
|
||||||
|
|
||||||
$this->initStripeCustomer($customer, $stripeSecretKey);
|
$this->initStripeCustomer($customer, $stripeSecretKey);
|
||||||
|
|
||||||
@@ -64,7 +66,7 @@ class ClientsController extends AbstractController
|
|||||||
$this->finalizeStripeCustomer($customer, $user, $stripeSecretKey);
|
$this->finalizeStripeCustomer($customer, $user, $stripeSecretKey);
|
||||||
|
|
||||||
$codeComptable = trim($request->request->getString('codeComptable'));
|
$codeComptable = trim($request->request->getString('codeComptable'));
|
||||||
$customer->setCodeComptable('' !== $codeComptable ? $codeComptable : $customerRepository->generateUniqueCodeComptable($customer));
|
$customer->setCodeComptable('' !== $codeComptable ? '411_'.$codeComptable : $customerRepository->generateUniqueCodeComptable($customer));
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->indexInMeilisearch($meilisearch, $customer, $logger);
|
$this->indexInMeilisearch($meilisearch, $customer, $logger);
|
||||||
@@ -98,6 +100,32 @@ class ClientsController extends AbstractController
|
|||||||
$customer->setZipCode(trim($request->request->getString('zipCode')) ?: null);
|
$customer->setZipCode(trim($request->request->getString('zipCode')) ?: null);
|
||||||
$customer->setCity(trim($request->request->getString('city')) ?: null);
|
$customer->setCity(trim($request->request->getString('city')) ?: null);
|
||||||
$customer->setTypeCompany(trim($request->request->getString('typeCompany')) ?: 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 */
|
/** @codeCoverageIgnore */
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ class Customer
|
|||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $city = null;
|
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)]
|
#[ORM\Column(length: 14, nullable: true)]
|
||||||
private ?string $siret = null;
|
private ?string $siret = null;
|
||||||
|
|
||||||
@@ -271,6 +277,30 @@ class Customer
|
|||||||
return $this;
|
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
|
public function getSiret(): ?string
|
||||||
{
|
{
|
||||||
return $this->siret;
|
return $this->siret;
|
||||||
|
|||||||
@@ -75,8 +75,10 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label for="codeComptable" class="block text-xs font-bold uppercase tracking-wider mb-2">Code comptable</label>
|
<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)"
|
<div class="flex">
|
||||||
class="w-full px-4 py-3 input-glass text-sm font-medium font-mono">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label for="raisonSociale" class="block text-xs font-bold uppercase tracking-wider mb-2">Raison sociale</label>
|
<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">
|
class="w-full px-4 py-3 input-glass text-sm font-medium">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<input type="hidden" id="geoLat" name="geoLat">
|
||||||
|
<input type="hidden" id="geoLong" name="geoLong">
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class ClientsControllerTest extends TestCase
|
|||||||
$userService = $this->createStub(UserManagementService::class);
|
$userService = $this->createStub(UserManagementService::class);
|
||||||
$logger = $this->createStub(LoggerInterface::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);
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ class ClientsControllerTest extends TestCase
|
|||||||
$userService->method('createBaseUser')->willThrowException(new \InvalidArgumentException('Champs requis'));
|
$userService->method('createBaseUser')->willThrowException(new \InvalidArgumentException('Champs requis'));
|
||||||
$logger = $this->createStub(LoggerInterface::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);
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ class ClientsControllerTest extends TestCase
|
|||||||
$userService->method('createBaseUser')->willThrowException(new \RuntimeException('DB error'));
|
$userService->method('createBaseUser')->willThrowException(new \RuntimeException('DB error'));
|
||||||
$logger = $this->createStub(LoggerInterface::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);
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
}
|
}
|
||||||
@@ -287,6 +287,7 @@ class ClientsControllerTest extends TestCase
|
|||||||
$this->createStub(MeilisearchService::class),
|
$this->createStub(MeilisearchService::class),
|
||||||
$userService,
|
$userService,
|
||||||
$this->createStub(LoggerInterface::class),
|
$this->createStub(LoggerInterface::class),
|
||||||
|
$this->createStub(HttpClientInterface::class),
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
$this->assertSame(302, $response->getStatusCode());
|
$this->assertSame(302, $response->getStatusCode());
|
||||||
@@ -324,6 +325,7 @@ class ClientsControllerTest extends TestCase
|
|||||||
$this->createStub(MeilisearchService::class),
|
$this->createStub(MeilisearchService::class),
|
||||||
$userService,
|
$userService,
|
||||||
$this->createStub(LoggerInterface::class),
|
$this->createStub(LoggerInterface::class),
|
||||||
|
$this->createStub(HttpClientInterface::class),
|
||||||
'sk_test_***',
|
'sk_test_***',
|
||||||
);
|
);
|
||||||
$this->assertSame(302, $response->getStatusCode());
|
$this->assertSame(302, $response->getStatusCode());
|
||||||
@@ -364,6 +366,7 @@ class ClientsControllerTest extends TestCase
|
|||||||
$meilisearch,
|
$meilisearch,
|
||||||
$userService,
|
$userService,
|
||||||
$this->createStub(LoggerInterface::class),
|
$this->createStub(LoggerInterface::class),
|
||||||
|
$this->createStub(HttpClientInterface::class),
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
$this->assertSame(302, $response->getStatusCode());
|
$this->assertSame(302, $response->getStatusCode());
|
||||||
|
|||||||
@@ -111,6 +111,18 @@ class CustomerTest extends TestCase
|
|||||||
$this->assertSame('Saint-Quentin', $c->getCity());
|
$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
|
public function testLegal(): void
|
||||||
{
|
{
|
||||||
$c = $this->createCustomer();
|
$c = $this->createCustomer();
|
||||||
|
|||||||
Reference in New Issue
Block a user