diff --git a/docs/changes_2025_08_02.md b/docs/changes_2025_08_02.md new file mode 100644 index 00000000..a6364cc9 --- /dev/null +++ b/docs/changes_2025_08_02.md @@ -0,0 +1,68 @@ +# Changes Implemented on 2025-08-02 + +## Issue Description + +1. Create a command to remove duplicate Places, keeping only one. Sort places by OSM object type and OSM ID, and delete if the same information is found twice in a row. + +2. When processing a city ("labourage"), use the city name found in the API that links INSEE code to city name to define the city name if it's different. Also use this for the command that creates Stats objects from Requests, not the place name from the request. + +## Changes Made + +### 1. New Command to Remove Duplicate Places + +Created a new command `app:remove-duplicate-places` that: +- Gets all places sorted by OSM type and ID +- Finds duplicates by comparing consecutive places +- Removes the duplicates, keeping the first occurrence + +The command supports: +- `--dry-run` option to show duplicates without removing them +- `--zip-code` option to process only places with a specific ZIP code + +Usage: +```bash +# Show duplicates without removing them +php bin/console app:remove-duplicate-places --dry-run + +# Remove duplicates for a specific ZIP code +php bin/console app:remove-duplicate-places --zip-code=75001 + +# Remove all duplicates +php bin/console app:remove-duplicate-places +``` + +### 2. City Name Updates from API + +#### During Labourage Process + +Modified `ProcessLabourageQueueCommand` to update the city name from the API after labourage: +- After setting the labourage completion date, it gets the city name from the API using the `get_city_osm_from_zip_code` method +- If the API returns a city name and it's different from the current name, it updates the Stats entity with the new name +- It logs the name change for information + +#### When Creating Stats from Requests + +Modified `CreateStatsFromDemandesCommand` to use the API-provided city name: +- It now tries to get the city name from the API based on the INSEE code +- If the API returns a city name, it uses that for the Stats object +- If the API doesn't return a name, it falls back to using the query from the first Demande (the previous behavior) +- It logs which source was used for the city name + +## Testing + +To test these changes: + +1. Test the duplicate removal command: + ```bash + php bin/console app:remove-duplicate-places --dry-run + ``` + +2. Test the city name updates during labourage: + ```bash + php bin/console app:process-labourage-queue + ``` + +3. Test the Stats creation from Requests: + ```bash + php bin/console app:create-stats-from-demandes + ``` \ No newline at end of file diff --git a/src/Command/CreateStatsFromDemandesCommand.php b/src/Command/CreateStatsFromDemandesCommand.php index 93558c85..34a71028 100644 --- a/src/Command/CreateStatsFromDemandesCommand.php +++ b/src/Command/CreateStatsFromDemandesCommand.php @@ -6,6 +6,7 @@ use App\Entity\Demande; use App\Entity\Stats; use App\Repository\DemandeRepository; use App\Repository\StatsRepository; +use App\Service\Motocultrice; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -22,16 +23,19 @@ class CreateStatsFromDemandesCommand extends Command private EntityManagerInterface $entityManager; private DemandeRepository $demandeRepository; private StatsRepository $statsRepository; + private Motocultrice $motocultrice; public function __construct( EntityManagerInterface $entityManager, DemandeRepository $demandeRepository, - StatsRepository $statsRepository + StatsRepository $statsRepository, + Motocultrice $motocultrice ) { parent::__construct(); $this->entityManager = $entityManager; $this->demandeRepository = $demandeRepository; $this->statsRepository = $statsRepository; + $this->motocultrice = $motocultrice; } protected function execute(InputInterface $input, OutputInterface $output): int @@ -74,11 +78,19 @@ class CreateStatsFromDemandesCommand extends Command $stats = new Stats(); $stats->setZone((string) $insee); - // Try to set the city name from the first Demande - $firstDemande = $demandes[0]; - if ($firstDemande->getQuery()) { - // Use the query as a fallback name (will be updated during labourage) - $stats->setName($firstDemande->getQuery()); + // Try to get the city name from the API based on INSEE code + $apiCityName = $this->motocultrice->get_city_osm_from_zip_code($insee); + if ($apiCityName) { + // Use the API-provided city name + $stats->setName($apiCityName); + $io->text(sprintf('Using API city name: %s', $apiCityName)); + } else { + // Fallback to the query from the first Demande if API doesn't return a name + $firstDemande = $demandes[0]; + if ($firstDemande->getQuery()) { + $stats->setName($firstDemande->getQuery()); + $io->text(sprintf('Using query as fallback name: %s', $firstDemande->getQuery())); + } } $stats->setDateCreated(new \DateTime()); diff --git a/src/Command/ProcessLabourageQueueCommand.php b/src/Command/ProcessLabourageQueueCommand.php index 5570c752..a4309b57 100644 --- a/src/Command/ProcessLabourageQueueCommand.php +++ b/src/Command/ProcessLabourageQueueCommand.php @@ -142,6 +142,14 @@ class ProcessLabourageQueueCommand extends Command } } $stats->setDateLabourageDone(new \DateTime()); + + // Update city name from API if available + $apiCityName = $this->motocultrice->get_city_osm_from_zip_code($stats->getZone()); + if ($apiCityName && $apiCityName !== $stats->getName()) { + $io->info(sprintf('Updating city name from "%s" to "%s" based on API data', $stats->getName(), $apiCityName)); + $stats->setName($apiCityName); + } + $io->info('Récupération des followups de cette ville...'); // $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager); diff --git a/src/Command/RemoveDuplicatePlacesCommand.php b/src/Command/RemoveDuplicatePlacesCommand.php new file mode 100644 index 00000000..f8c66a0c --- /dev/null +++ b/src/Command/RemoveDuplicatePlacesCommand.php @@ -0,0 +1,123 @@ +entityManager = $entityManager; + } + + protected function configure(): void + { + $this + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show duplicates without removing them') + ->addOption('zip-code', null, InputOption::VALUE_REQUIRED, 'Process only places with this ZIP code'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title('Removing duplicate Places based on OSM type and ID'); + + $dryRun = $input->getOption('dry-run'); + $zipCode = $input->getOption('zip-code'); + + // Build the query to get all places sorted by OSM type and ID + $queryBuilder = $this->entityManager->createQueryBuilder() + ->select('p') + ->from(Place::class, 'p') + ->orderBy('p.osm_kind', 'ASC') + ->addOrderBy('p.osmId', 'ASC'); + + // Add ZIP code filter if provided + if ($zipCode) { + $queryBuilder->andWhere('p.zip_code = :zip_code') + ->setParameter('zip_code', $zipCode); + $io->info(sprintf('Processing only places with ZIP code: %s', $zipCode)); + } + + $places = $queryBuilder->getQuery()->getResult(); + + if (empty($places)) { + $io->warning('No places found.'); + return Command::SUCCESS; + } + + $io->info(sprintf('Found %d places.', count($places))); + + // Find duplicates by comparing consecutive places + $duplicates = []; + $previousPlace = null; + $duplicateCount = 0; + + foreach ($places as $place) { + if ($previousPlace !== null && + $previousPlace->getOsmKind() === $place->getOsmKind() && + $previousPlace->getOsmId() === $place->getOsmId()) { + // This is a duplicate + $duplicates[] = [ + 'keep' => $previousPlace, + 'remove' => $place + ]; + $duplicateCount++; + } + $previousPlace = $place; + } + + if (empty($duplicates)) { + $io->success('No duplicate places found.'); + return Command::SUCCESS; + } + + $io->info(sprintf('Found %d duplicate places.', $duplicateCount)); + + // Process duplicates + $removedCount = 0; + foreach ($duplicates as $duplicate) { + $keep = $duplicate['keep']; + $remove = $duplicate['remove']; + + $io->text(sprintf( + 'Duplicate found: Keep #%d, Remove #%d (OSM %s/%s)', + $keep->getId(), + $remove->getId(), + $keep->getOsmKind(), + $keep->getOsmId() + )); + + if (!$dryRun) { + // Remove the duplicate + $this->entityManager->remove($remove); + $removedCount++; + } + } + + if (!$dryRun && $removedCount > 0) { + $this->entityManager->flush(); + $io->success(sprintf('Removed %d duplicate places.', $removedCount)); + } elseif ($dryRun) { + $io->info('Dry run completed. No places were removed.'); + } + + return Command::SUCCESS; + } +} \ No newline at end of file