From a562690512ab7ddf1dac79eadb1ec5dd37408d79 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 4 Feb 2025 18:37:03 +0100 Subject: [PATCH 01/27] Create thirdparty merge manager --- .../ThirdpartyMergeManager.php | 125 ++++++++++++++++++ .../ChillThirdPartyExtension.php | 1 + .../config/services/actions.yaml | 6 + 3 files changed, 132 insertions(+) create mode 100644 src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php create mode 100644 src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml diff --git a/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php b/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php new file mode 100644 index 000000000..941288367 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php @@ -0,0 +1,125 @@ +transferData($toKeep, $toDelete); + + // Update linked entities + $this->updateReferences($toKeep, $toDelete); + + $this->em->remove($toDelete); + $this->em->flush(); + } + + private function transferData(ThirdParty $toKeep, ThirdParty $toDelete): void + { + $excludedProperties = ['id', 'createdAt']; + $reflection = new \ReflectionClass(ThirdParty::class); + + foreach ($reflection->getProperties() as $property) { + if (in_array($property->getName(), $excludedProperties, true)) { + continue; + } + + $toKeepValue = $property->getValue($toKeep); + $toDeleteValue = $property->getValue($toDelete); + + if (null === $toKeepValue && null !== $toDeleteValue) { + $property->setValue($toKeep, $toDeleteValue); + } + + if ($toKeepValue instanceof \Doctrine\Common\Collections\Collection + && $toDeleteValue instanceof \Doctrine\Common\Collections\Collection) { + foreach ($toDeleteValue as $item) { + if (!$toKeepValue->contains($item)) { + $toKeepValue->add($item); + } + } + } + } + } + + private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): void + { + $allMeta = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($allMeta as $meta) { + foreach ($meta->getAssociationMappings() as $assoc) { + if (ThirdParty::class !== $assoc['targetEntity']) { + continue; // Skip unrelated associations + } + + $entityClass = $meta->getName(); + $associationField = $assoc['fieldName']; + + if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_ONE) { + // Handle ManyToOne or OneToOne + $qb = $this->em->createQueryBuilder(); + $qb->update($entityClass, 'e') + ->set("e.{$associationField}", ':toKeep') + ->where("e.{$associationField} = :toDelete") + ->setParameter('toKeep', $toKeep) + ->setParameter('toDelete', $toDelete) + ->getQuery() + ->execute(); + } + + if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_MANY) { + // Handle ManyToMany or OneToMany (inverse side) + $repo = $this->em->getRepository($entityClass); + $linkedEntities = $repo->createQueryBuilder('e') + ->join("e.{$associationField}", 't') + ->where('t = :toDelete') + ->setParameter('toDelete', $toDelete) + ->getQuery() + ->getResult(); + + foreach ($linkedEntities as $entity) { + $getter = 'get'.ucfirst($associationField); + $setter = 'set'.ucfirst($associationField); + $adder = 'add'.ucfirst(rtrim($associationField, 's')); + $remover = 'remove'.ucfirst(rtrim($associationField, 's')); + + if (method_exists($entity, $getter) && method_exists($entity, $setter)) { + // For OneToMany owning side + $collection = $entity->{$getter}(); + if ($collection->contains($toDelete)) { + $collection->removeElement($toDelete); + if (!$collection->contains($toKeep)) { + $collection->add($toKeep); + } + } + } elseif (method_exists($entity, $adder) && method_exists($entity, $remover)) { + // For ManyToMany + $entity->{$remover}($toDelete); + $entity->{$adder}($toKeep); + } + + $this->em->persist($entity); + } + } + } + } + + $this->em->flush(); + } +} diff --git a/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php b/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php index 475d22049..e12807819 100644 --- a/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php +++ b/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php @@ -51,6 +51,7 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte $loader->load('services/serializer.yaml'); $loader->load('services/repository.yaml'); $loader->load('services/doctrineEventListener.yaml'); + $loader->load('services/actions.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml b/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml new file mode 100644 index 000000000..e65ed6600 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml @@ -0,0 +1,6 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + Chill\ThirdPartyBundle\Actions\MergeThirdparty\ThirdpartyMergeManager: ~ From ebfdc57fcfea8736398a65a0c44d943feef55def Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 4 Feb 2025 18:37:14 +0100 Subject: [PATCH 02/27] Create thirdparty merge test --- .../Action/ThirdpartyMergeManagerTest.php | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php new file mode 100644 index 000000000..a36a393bc --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php @@ -0,0 +1,100 @@ +mergeManager = $this->getContainer()->get(ThirdpartyMergeManager::class); + $this->em = $this->getContainer()->get(EntityManagerInterface::class); + $this->connection = $this->em->getConnection(); + + // Start a transaction before each test + $this->connection->beginTransaction(); + } + + protected function tearDown(): void + { + try { + // Rollback the transaction after each test to ensure no data is persisted + $this->connection->rollBack(); + } catch (\Exception $e) { + $this->connection->close(); + } + + parent::tearDown(); + } + + public function testThirdpartyMerge(): void + { + // Arrange: Create Thirdparty entities + $toKeep = new ThirdParty(); + $toKeep->setName('Thirdparty ToKeep'); + $toKeep->setEmail('keep@example.com'); + + $toDelete = new Thirdparty(); + $toDelete->setName('Thirdparty ToDelete'); // This should be ignored + $toDelete->setTelephone(new PhoneNumber('123456789')); + + // Related entities + $activity = new Activity(); + $activity->addThirdParty($toDelete); // This is a Many-to-Many relation + + $personResource = new PersonResource(); + $personResource->setThirdParty($toDelete); // This is a Many-to-One relation + + $this->em->persist($toKeep); + $this->em->persist($toDelete); + $this->em->persist($activity); + $this->em->persist($personResource); + $this->em->flush(); + + // Merge + $this->mergeManager->merge($toKeep, $toDelete); + $this->em->clear(); + + // Verify data was merged correctly + $mergedThirdparty = $this->em->getRepository(Thirdparty::class)->find($toKeep->getId()); + $this->assertNotNull($mergedThirdparty); + $this->assertEquals('Primary Name', $mergedThirdparty->getName(), 'Name should remain unchanged'); + $this->assertEquals('keep@example.com', $mergedThirdparty->getEmail(), 'Email should remain unchanged'); + $this->assertEquals('123456789', $mergedThirdparty->getPhone(), 'Phone should be transferred from Thirdparty ToDelete'); + + // Check that relationships are updated + $updatedActivity = $this->em->getRepository(Activity::class)->find($activity->getId()); + $this->assertTrue( + $updatedActivity->getThirdParties()->contains($mergedThirdparty), + 'Activity should be linked to the merged Thirdparty' + ); + $this->assertFalse( + $updatedActivity->getThirdParties()->exists(fn($key, $tp) => $tp->getId() === $toDelete->getId()), + 'Activity should no longer reference the deleted Thirdparty' + ); + + $updatedPersonResource = $this->em->getRepository(PersonResource::class)->find($personResource->getId()); + $this->assertEquals( + $mergedThirdparty->getId(), + $updatedPersonResource->getThirdParty()->getId(), + 'PersonResource should reference the merged Thirdparty' + ); + + // Ensure the 'toDelete' entity is removed + $deletedThirdparty = $this->em->getRepository(Thirdparty::class)->find($toDelete->getId()); + $this->assertNull($deletedThirdparty, 'The deleted Thirdparty should no longer exist in the database'); + } +} From 3b3659f13f4744e853c3607b3074120cc1412fc9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 4 Feb 2025 18:37:30 +0100 Subject: [PATCH 03/27] WIP Create thirdparty merge controller --- .../Controller/ThirdpartyDuplicateController.php | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php new file mode 100644 index 000000000..5bcb52db0 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -0,0 +1,10 @@ + Date: Wed, 5 Feb 2025 17:08:11 +0100 Subject: [PATCH 04/27] Renamen and reorganize thirdparty merge files --- .../DependencyInjection/ChillThirdPartyExtension.php | 1 - .../ThirdpartyMergeService.php} | 6 +++--- .../ThirdpartyMergeServiceTest.php} | 8 ++++---- src/Bundle/ChillThirdPartyBundle/config/services.yaml | 10 ++++++---- .../ChillThirdPartyBundle/config/services/actions.yaml | 6 ------ 5 files changed, 13 insertions(+), 18 deletions(-) rename src/Bundle/ChillThirdPartyBundle/{Actions/MergeThirdparty/ThirdpartyMergeManager.php => Service/ThirdpartyMergeService.php} (97%) rename src/Bundle/ChillThirdPartyBundle/Tests/{Action/ThirdpartyMergeManagerTest.php => Service/ThirdpartyMergeServiceTest.php} (94%) delete mode 100644 src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml diff --git a/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php b/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php index e12807819..475d22049 100644 --- a/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php +++ b/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php @@ -51,7 +51,6 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte $loader->load('services/serializer.yaml'); $loader->load('services/repository.yaml'); $loader->load('services/doctrineEventListener.yaml'); - $loader->load('services/actions.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php similarity index 97% rename from src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php rename to src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 941288367..8c4d0e9ef 100644 --- a/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -9,14 +9,14 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\ThirdPartyBundle\Actions\MergeThirdparty; +namespace Chill\ThirdPartyBundle\Service; use Chill\ThirdPartyBundle\Entity\ThirdParty; use Doctrine\ORM\EntityManagerInterface; -class ThirdpartyMergeManager +class ThirdpartyMergeService { - public function __construct(private readonly EntityManagerInterface $em, private iterable $handlers) {} + public function __construct(private readonly EntityManagerInterface $em) {} public function merge(ThirdParty $toKeep, ThirdParty $toDelete): void { diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php similarity index 94% rename from src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php rename to src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php index a36a393bc..de11cea7a 100644 --- a/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php @@ -1,16 +1,16 @@ mergeManager = $this->getContainer()->get(ThirdpartyMergeManager::class); + $this->mergeManager = $this->getContainer()->get(ThirdpartyMergeService::class); $this->em = $this->getContainer()->get(EntityManagerInterface::class); $this->connection = $this->em->getConnection(); diff --git a/src/Bundle/ChillThirdPartyBundle/config/services.yaml b/src/Bundle/ChillThirdPartyBundle/config/services.yaml index 572026771..ffe3207d3 100644 --- a/src/Bundle/ChillThirdPartyBundle/config/services.yaml +++ b/src/Bundle/ChillThirdPartyBundle/config/services.yaml @@ -1,14 +1,14 @@ ---- services: - Chill\ThirdPartyBundle\Serializer\Normalizer\: + _defaults: autowire: true + autoconfigure: true + + Chill\ThirdPartyBundle\Serializer\Normalizer\: resource: '../Serializer/Normalizer/' tags: - { name: 'serializer.normalizer', priority: 64 } Chill\ThirdPartyBundle\Export\: - autowire: true - autoconfigure: true resource: '../Export/' Chill\ThirdPartyBundle\Validator\: @@ -16,3 +16,5 @@ services: autowire: true resource: '../Validator/' + Chill\ThirdPartyBundle\Service\ThirdpartyMergeService: ~ + diff --git a/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml b/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml deleted file mode 100644 index e65ed6600..000000000 --- a/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml +++ /dev/null @@ -1,6 +0,0 @@ -services: - _defaults: - autowire: true - autoconfigure: true - - Chill\ThirdPartyBundle\Actions\MergeThirdparty\ThirdpartyMergeManager: ~ From bf14c9256747fac843eb5830ec315a90a4e47a4f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Feb 2025 17:08:42 +0100 Subject: [PATCH 05/27] Continue thirdparty merge controller and create views --- .../views/CRUD/_view_content.html.twig | 14 ++- .../ThirdpartyDuplicateController.php | 96 ++++++++++++++++++ .../Form/ThirdpartyFindDuplicateType.php | 32 ++++++ .../ThirdPartyDuplicate/_details.html.twig | 39 ++++++++ .../ThirdPartyDuplicate/confirm.html.twig | 97 +++++++++++++++++++ .../find_duplicate.html.twig | 38 ++++++++ .../translations/messages.fr.yml | 1 + 7 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php create mode 100644 src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig create mode 100644 src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig create mode 100644 src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig index 203a24926..7d0023a39 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig @@ -4,7 +4,7 @@ {% endblock crud_content_header %} {% block crud_content_view %} - + {% block crud_content_view_details %}
id
@@ -20,7 +20,7 @@ {{ 'Cancel'|trans }} - {% endblock %} + {% endblock %} {% block content_view_actions_before %}{% endblock %} {% block content_form_actions_delete %} {% if chill_crud_action_exists(crud_name, 'delete') %} @@ -32,7 +32,7 @@ {% endif %} {% endif %} - {% endblock content_form_actions_delete %} + {% endblock content_form_actions_delete %} {% block content_view_actions_duplicate_link %} {% if chill_crud_action_exists(crud_name, 'new') %} {% if is_granted(chill_crud_config('role', crud_name, 'new'), entity) %} @@ -44,6 +44,14 @@ {% endif %} {% endif %} {% endblock content_view_actions_duplicate_link %} + {% block content_view_actions_merge %} +
  • + {{ 'thirdparty_duplicate.merge'|trans }} +
  • + {% endblock %} {% block content_view_actions_edit_link %} {% if chill_crud_action_exists(crud_name, 'edit') %} {% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %} diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php index 5bcb52db0..a7fb87458 100644 --- a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -1,10 +1,106 @@ thirdPartyRepository->find($thirdparty_id); + dump($thirdparty_id); + dump($thirdparty); + + if (null === $thirdparty) { + throw $this->createNotFoundException("Thirdparty with id {$thirdparty_id} not".' found on this server'); + } + + $form = $this->createForm(ThirdpartyFindDuplicateType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $thirdparty2 = $form->get('thirdparty')->getData(); + + if (null === $thirdparty2) { + throw $this->createNotFoundException("Thirdparty with id {$thirdparty2->getId}() not".' found on this server'); + } + + $direction = $form->get('direction')->getData(); + + if ('starting' === $direction) { + $params = [ + 'thirdparty1_id' => $thirdparty->getId(), + 'thirdparty2_id' => $thirdparty2->getId(), + ]; + } else { + $params = [ + 'thirdparty1_id' => $thirdparty2->getId(), + 'thirdparty2_id' => $thirdparty->getId(), + ]; + } + + return $this->redirectToRoute('chill_thirdparty_duplicate_confirm', $params); + } + + return $this->render('@ChillThirdParty/ThirdPartyDuplicate/find_duplicate.html.twig', [ + 'thirdparty' => $thirdparty, + 'form' => $form->createView(), + ]); + } + + #[Route(path: '/{_locale}/3party/{thirdparty1_id}/duplicate/{thirdparty2_id}/confirm', name: 'chill_thirdparty_duplicate_confirm')] + public function confirmAction(mixed $thirdparty1_id, mixed $thirdparty2_id, Request $request) + { + if ($thirdparty1_id === $thirdparty2_id) { + throw new \InvalidArgumentException('Can not merge same thirdparty'); + } + + $thirdparty1 = $this->thirdPartyRepository->find($thirdparty1_id); + $thirdparty2 = $this->thirdPartyRepository->find($thirdparty2_id); + + if (null === $thirdparty1) { + throw $this->createNotFoundException("Thirdparty with id {$thirdparty1_id} not".' found on this server'); + } + + if (null === $thirdparty2) { + throw $this->createNotFoundException("Person with id {$thirdparty2_id} not".' found on this server'); + } + + $form = $this->createForm(PersonConfimDuplicateType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + + $this->thirdPartyMergeService->merge($thirdparty1, $thirdparty2); + + return $this->redirectToRoute('chill_crud_3party_3party_view', ['id' => $thirdparty1->getId()]); + } + + return $this->render('@ChillThirdParty/ThirdPartyDuplicate/confirm.html.twig', [ + 'thirdparty' => $thirdparty1, + 'thirdparty2' => $thirdparty2, + 'form' => $form->createView(), + ]); + } } diff --git a/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php new file mode 100644 index 000000000..8b2534bca --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php @@ -0,0 +1,32 @@ +add('thirdparty', PickThirdpartyDynamicType::class, [ + 'label' => 'Find duplicate', + 'mapped' => false, + ]) + ->add('direction', HiddenType::class, [ + 'data' => 'starting', + ]); + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return 'chill_thirdpartybundle_thirdparty_find_manually_duplicate'; + } + +} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig new file mode 100644 index 000000000..8896c8deb --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig @@ -0,0 +1,39 @@ +{%- macro details(thirdparty, options) -%} + +
      +
    • {{ 'name'|trans }}: + {{ thirdparty.name }}
    • +
    • {{ 'firstName'|trans }}: + {% if thirdparty.firstname %}{{ thirdparty.firstname }}{% endif %}
    • +
    • {{ 'thirdparty.Civility'|trans }}: + {% if thirdparty.getCivility %}{{ thirdparty.getCivility.name|localize_translatable_string }}{% endif %}
    • +
    • {{ 'thirdparty.NameCompany'|trans }}: + {% if thirdparty.nameCompany is not empty %}{{ thirdparty.nameCompany }}{% endif %}
    • +
    • {{ 'thirdparty.Acronym'|trans }}: + {% if thirdparty.acronym %}{{ thirdparty.acronym }}{% endif %}
    • +
    • {{ 'thirdparty.Profession'|trans }}: + {% if thirdparty.profession %}{{ thirdparty.profession }}{% endif %}
    • +
    • {{ 'telephone'|trans }}: + {% if thirdparty.telephone %}{{ thirdparty.telephone }}{% endif %}
    • +
    • {{ 'email'|trans }}: + {% if thirdparty.email is not null %}{{ thirdparty.email }}{% endif %}
    • +
    • {{ 'address'|trans }}: + {%- if thirdparty.getAddress is not empty -%} + {{ thirdparty.getAddress|chill_entity_render_box }} + {% endif %}
    • +
    • {{ 'thirdparty.Contact data are confidential'|trans }}: + {{ thirdparty.contactDataAnonymous }}
    • +
    +{% endmacro %} + +{#{%- macro links(thirdparty, options) -%}#} +{#
      #} +{##} +{#
    • {{ person.counters.nb_activity }} {{ (person.counters.nb_activity > 1)? 'échanges' : 'échange' }}
    • #} +{#
    • {{ person.counters.nb_task }} {{ (person.counters.nb_task > 1)? 'tâches' : 'tâche' }}
    • #} +{#
    • {{ person.counters.nb_document }} {{ (person.counters.nb_document > 1)? 'documents' : 'document' }}
    • #} +{#
    • {{ person.counters.nb_event }} {{ (person.counters.nb_event > 1)? 'événements' : 'événement' }}
    • #} +{#
    • {{ person.counters.nb_addresses }} {{ (person.counters.nb_addresses > 1)? 'adresses' : 'adresse' }}
    • #} +{##} +{#
    #} +{#{% endmacro %}#} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig new file mode 100644 index 000000000..4baa4552e --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig @@ -0,0 +1,97 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% import '@ChillThirdParty/ThirdPartyDuplicate/_details.html.twig' as details %} + +{% block title %}{{ 'thirdparty_duplicate.Thirdparty duplicate title'|trans ~ ' ' ~ thirdparty.name }}{% endblock %} + +{% block content %} + +
    + +

    {{ 'thirdparty_duplicate.Merge duplicate thirdparties'|trans }}

    + +
    +

    {{ 'thirdparty_duplicate.Thirdparty to delete'|trans }}: + {{ 'thirdparty_duplicate.Thirdparty to delete explanation'|trans }} +

    +
    + +

    + {{ thirdparty2 }} +

    + +

    {{ 'Deleted datas'|trans ~ ':' }}

    + {{ details.details(thirdparty2) }} + +{#

    {{ 'Moved links'|trans ~ ':' }}

    #} +{# {{ details.links(thirdparty2) }}#} +
    +
    + +
    +

    {{ 'thirdparty_duplicate.Thirdparty to keep'|trans }}: + {{ 'thirdparty_duplicate.Thirdparty to delete explanation'|trans }} +

    +
    + +

    + {{ thirdparty }} +

    + +

    {{ 'thirdparty_duplicate.data to keep'|trans ~ ':' }}

    + {{ details.details(thirdparty) }} + +{#

    {{ 'thirdparty_duplicate.links to keep'|trans ~ ':' }}

    #} +{# {{ sidepane.links(thirdparty) }}#} +
    +
    + + {{ form_start(form) }} + +
    + +
    +
    + {{ form_widget(form.confirm) }} +
    +
    + {{ form_label(form.confirm) }} +
    +
    +
    + + + + {{ form_end(form) }} + +
    +{% endblock %} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig new file mode 100644 index 000000000..9dd1a0992 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig @@ -0,0 +1,38 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% set activeRouteKey = 'chill_thirdparty_duplicate' %} + +{% block title %}{{ 'Find duplicate'|trans ~ ' ' ~ thirdparty.name|capitalize }}{% endblock %} + + +{% block content %} +
    + +

    {{ 'find duplicate thirdparty'|trans }}

    + + {{ form_start(form) }} + {{ form_rest(form) }} + + + + {{ form_end(form) }} + +
    +{% endblock %} + +{% block js %} + {{ encore_entry_script_tags('mod_pickentity_type') }} +{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('mod_pickentity_type') }} +{% endblock %} diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index 52bae6fee..9432fbdc0 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -133,6 +133,7 @@ is thirdparty: Le demandeur est un tiers Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers "Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" +find duplicate thirdparty: 'Désigner un tier doublon' # admin From 218579166506972f348acdb5a1c60dc05650d22d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 10 Feb 2025 17:32:19 +0100 Subject: [PATCH 06/27] Use sql statements for transferData method and fix updateReferences method --- .../Service/ThirdpartyMergeService.php | 147 ++++++++++-------- 1 file changed, 81 insertions(+), 66 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 8c4d0e9ef..820ec742e 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -11,8 +11,12 @@ declare(strict_types=1); namespace Chill\ThirdPartyBundle\Service; +use Chill\MainBundle\Entity\Address; use Chill\ThirdPartyBundle\Entity\ThirdParty; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; class ThirdpartyMergeService { @@ -26,96 +30,107 @@ class ThirdpartyMergeService // Update linked entities $this->updateReferences($toKeep, $toDelete); + // Safely remove the old ThirdParty $this->em->remove($toDelete); + $this->em->getConnection()->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit'); + $this->em->flush(); } + /** + * @throws Exception + */ private function transferData(ThirdParty $toKeep, ThirdParty $toDelete): void { - $excludedProperties = ['id', 'createdAt']; - $reflection = new \ReflectionClass(ThirdParty::class); + $conn = $this->em->getConnection(); - foreach ($reflection->getProperties() as $property) { - if (in_array($property->getName(), $excludedProperties, true)) { - continue; + $columns = ['profession', 'firstname', 'name', 'email', 'telephone', 'comment', 'kind', 'contact_data_anonymous', 'types', 'active', 'name_company']; + + $conn->beginTransaction(); + $metadata = $this->em->getClassMetadata(ThirdParty::class); + + foreach ($columns as $column) { + $columnType = $metadata->getTypeOfField($column); + + if ('string' === $columnType || 'text' === $columnType) { + $sqlUpdate = " + UPDATE chill_3party.third_party + SET {$column} = COALESCE((SELECT {$column} FROM chill_3party.third_party WHERE id = :toDelete), {$column}) + WHERE id = :toKeep AND ({$column} IS NULL OR {$column} = '')"; + } else { + $sqlUpdate = " + UPDATE chill_3party.third_party + SET {$column} = COALESCE((SELECT {$column} FROM chill_3party.third_party WHERE id = :toDelete), {$column}) + WHERE id = :toKeep AND {$column} IS NULL"; } - $toKeepValue = $property->getValue($toKeep); - $toDeleteValue = $property->getValue($toDelete); - - if (null === $toKeepValue && null !== $toDeleteValue) { - $property->setValue($toKeep, $toDeleteValue); - } - - if ($toKeepValue instanceof \Doctrine\Common\Collections\Collection - && $toDeleteValue instanceof \Doctrine\Common\Collections\Collection) { - foreach ($toDeleteValue as $item) { - if (!$toKeepValue->contains($item)) { - $toKeepValue->add($item); - } - } - } + // Execute the query + $conn->executeQuery($sqlUpdate, [ + 'toDelete' => $toDelete->getId(), + 'toKeep' => $toKeep->getId(), + ]); } + + $conn->commit(); } private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): void { $allMeta = $this->em->getMetadataFactory()->getAllMetadata(); + $conn = $this->em->getConnection(); foreach ($allMeta as $meta) { + if ($meta->isMappedSuperclass) { + continue; + } + + $tableName = $meta->getTableName(); + foreach ($meta->getAssociationMappings() as $assoc) { if (ThirdParty::class !== $assoc['targetEntity']) { - continue; // Skip unrelated associations + continue; } - $entityClass = $meta->getName(); - $associationField = $assoc['fieldName']; + if ($assoc['type'] & ClassMetadata::TO_ONE) { - if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_ONE) { - // Handle ManyToOne or OneToOne - $qb = $this->em->createQueryBuilder(); - $qb->update($entityClass, 'e') - ->set("e.{$associationField}", ':toKeep') - ->where("e.{$associationField} = :toDelete") - ->setParameter('toKeep', $toKeep) - ->setParameter('toDelete', $toDelete) - ->getQuery() - ->execute(); - } + $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); - if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_MANY) { - // Handle ManyToMany or OneToMany (inverse side) - $repo = $this->em->getRepository($entityClass); - $linkedEntities = $repo->createQueryBuilder('e') - ->join("e.{$associationField}", 't') - ->where('t = :toDelete') - ->setParameter('toDelete', $toDelete) - ->getQuery() - ->getResult(); - - foreach ($linkedEntities as $entity) { - $getter = 'get'.ucfirst($associationField); - $setter = 'set'.ucfirst($associationField); - $adder = 'add'.ucfirst(rtrim($associationField, 's')); - $remover = 'remove'.ucfirst(rtrim($associationField, 's')); - - if (method_exists($entity, $getter) && method_exists($entity, $setter)) { - // For OneToMany owning side - $collection = $entity->{$getter}(); - if ($collection->contains($toDelete)) { - $collection->removeElement($toDelete); - if (!$collection->contains($toKeep)) { - $collection->add($toKeep); - } - } - } elseif (method_exists($entity, $adder) && method_exists($entity, $remover)) { - // For ManyToMany - $entity->{$remover}($toDelete); - $entity->{$adder}($toKeep); - } - - $this->em->persist($entity); + if (ThirdParty::class === $assoc['sourceEntity']) { + $sql = "UPDATE chill_3party.{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; + } else { + $sql = "UPDATE {$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; } + + $conn->executeStatement($sql, [ + 'toKeep' => $toKeep->getId(), + 'toDelete' => $toDelete->getId(), + ]); + + if ('parent' === $assoc['fieldName'] && ThirdParty::class === $assoc['targetEntity']) { + // Refresh $toKeep to sync its children collection + $this->em->refresh($toKeep); + } + } + + if ($assoc['type'] & ClassMetadata::TO_MANY) { + if (!isset($assoc['joinTable'])) { + continue; + } + + $joinTable = $assoc['joinTable']['name']; + + if ($assoc['isOwningSide']) { + $joinColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name']; + } else { + $joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; + } + + $sql = "UPDATE {$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; + + $conn->executeStatement($sql, [ + 'toKeep' => $toKeep->getId(), + 'toDelete' => $toDelete->getId(), + ]); } } } From 4d6d40629fed4288cff8015828eabd8d3b429e2f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 4 Feb 2025 18:37:03 +0100 Subject: [PATCH 07/27] Create thirdparty merge manager --- .../ThirdpartyMergeManager.php | 125 ++++++++++++++++++ .../ChillThirdPartyExtension.php | 1 + .../config/services/actions.yaml | 6 + 3 files changed, 132 insertions(+) create mode 100644 src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php create mode 100644 src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml diff --git a/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php b/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php new file mode 100644 index 000000000..941288367 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php @@ -0,0 +1,125 @@ +transferData($toKeep, $toDelete); + + // Update linked entities + $this->updateReferences($toKeep, $toDelete); + + $this->em->remove($toDelete); + $this->em->flush(); + } + + private function transferData(ThirdParty $toKeep, ThirdParty $toDelete): void + { + $excludedProperties = ['id', 'createdAt']; + $reflection = new \ReflectionClass(ThirdParty::class); + + foreach ($reflection->getProperties() as $property) { + if (in_array($property->getName(), $excludedProperties, true)) { + continue; + } + + $toKeepValue = $property->getValue($toKeep); + $toDeleteValue = $property->getValue($toDelete); + + if (null === $toKeepValue && null !== $toDeleteValue) { + $property->setValue($toKeep, $toDeleteValue); + } + + if ($toKeepValue instanceof \Doctrine\Common\Collections\Collection + && $toDeleteValue instanceof \Doctrine\Common\Collections\Collection) { + foreach ($toDeleteValue as $item) { + if (!$toKeepValue->contains($item)) { + $toKeepValue->add($item); + } + } + } + } + } + + private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): void + { + $allMeta = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($allMeta as $meta) { + foreach ($meta->getAssociationMappings() as $assoc) { + if (ThirdParty::class !== $assoc['targetEntity']) { + continue; // Skip unrelated associations + } + + $entityClass = $meta->getName(); + $associationField = $assoc['fieldName']; + + if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_ONE) { + // Handle ManyToOne or OneToOne + $qb = $this->em->createQueryBuilder(); + $qb->update($entityClass, 'e') + ->set("e.{$associationField}", ':toKeep') + ->where("e.{$associationField} = :toDelete") + ->setParameter('toKeep', $toKeep) + ->setParameter('toDelete', $toDelete) + ->getQuery() + ->execute(); + } + + if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_MANY) { + // Handle ManyToMany or OneToMany (inverse side) + $repo = $this->em->getRepository($entityClass); + $linkedEntities = $repo->createQueryBuilder('e') + ->join("e.{$associationField}", 't') + ->where('t = :toDelete') + ->setParameter('toDelete', $toDelete) + ->getQuery() + ->getResult(); + + foreach ($linkedEntities as $entity) { + $getter = 'get'.ucfirst($associationField); + $setter = 'set'.ucfirst($associationField); + $adder = 'add'.ucfirst(rtrim($associationField, 's')); + $remover = 'remove'.ucfirst(rtrim($associationField, 's')); + + if (method_exists($entity, $getter) && method_exists($entity, $setter)) { + // For OneToMany owning side + $collection = $entity->{$getter}(); + if ($collection->contains($toDelete)) { + $collection->removeElement($toDelete); + if (!$collection->contains($toKeep)) { + $collection->add($toKeep); + } + } + } elseif (method_exists($entity, $adder) && method_exists($entity, $remover)) { + // For ManyToMany + $entity->{$remover}($toDelete); + $entity->{$adder}($toKeep); + } + + $this->em->persist($entity); + } + } + } + } + + $this->em->flush(); + } +} diff --git a/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php b/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php index 475d22049..e12807819 100644 --- a/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php +++ b/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php @@ -51,6 +51,7 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte $loader->load('services/serializer.yaml'); $loader->load('services/repository.yaml'); $loader->load('services/doctrineEventListener.yaml'); + $loader->load('services/actions.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml b/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml new file mode 100644 index 000000000..e65ed6600 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml @@ -0,0 +1,6 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + Chill\ThirdPartyBundle\Actions\MergeThirdparty\ThirdpartyMergeManager: ~ From 683a0bc4e991b53cc95b2d1452bfbdc2811c1e48 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 4 Feb 2025 18:37:14 +0100 Subject: [PATCH 08/27] Create thirdparty merge test --- .../Action/ThirdpartyMergeManagerTest.php | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php new file mode 100644 index 000000000..a36a393bc --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php @@ -0,0 +1,100 @@ +mergeManager = $this->getContainer()->get(ThirdpartyMergeManager::class); + $this->em = $this->getContainer()->get(EntityManagerInterface::class); + $this->connection = $this->em->getConnection(); + + // Start a transaction before each test + $this->connection->beginTransaction(); + } + + protected function tearDown(): void + { + try { + // Rollback the transaction after each test to ensure no data is persisted + $this->connection->rollBack(); + } catch (\Exception $e) { + $this->connection->close(); + } + + parent::tearDown(); + } + + public function testThirdpartyMerge(): void + { + // Arrange: Create Thirdparty entities + $toKeep = new ThirdParty(); + $toKeep->setName('Thirdparty ToKeep'); + $toKeep->setEmail('keep@example.com'); + + $toDelete = new Thirdparty(); + $toDelete->setName('Thirdparty ToDelete'); // This should be ignored + $toDelete->setTelephone(new PhoneNumber('123456789')); + + // Related entities + $activity = new Activity(); + $activity->addThirdParty($toDelete); // This is a Many-to-Many relation + + $personResource = new PersonResource(); + $personResource->setThirdParty($toDelete); // This is a Many-to-One relation + + $this->em->persist($toKeep); + $this->em->persist($toDelete); + $this->em->persist($activity); + $this->em->persist($personResource); + $this->em->flush(); + + // Merge + $this->mergeManager->merge($toKeep, $toDelete); + $this->em->clear(); + + // Verify data was merged correctly + $mergedThirdparty = $this->em->getRepository(Thirdparty::class)->find($toKeep->getId()); + $this->assertNotNull($mergedThirdparty); + $this->assertEquals('Primary Name', $mergedThirdparty->getName(), 'Name should remain unchanged'); + $this->assertEquals('keep@example.com', $mergedThirdparty->getEmail(), 'Email should remain unchanged'); + $this->assertEquals('123456789', $mergedThirdparty->getPhone(), 'Phone should be transferred from Thirdparty ToDelete'); + + // Check that relationships are updated + $updatedActivity = $this->em->getRepository(Activity::class)->find($activity->getId()); + $this->assertTrue( + $updatedActivity->getThirdParties()->contains($mergedThirdparty), + 'Activity should be linked to the merged Thirdparty' + ); + $this->assertFalse( + $updatedActivity->getThirdParties()->exists(fn($key, $tp) => $tp->getId() === $toDelete->getId()), + 'Activity should no longer reference the deleted Thirdparty' + ); + + $updatedPersonResource = $this->em->getRepository(PersonResource::class)->find($personResource->getId()); + $this->assertEquals( + $mergedThirdparty->getId(), + $updatedPersonResource->getThirdParty()->getId(), + 'PersonResource should reference the merged Thirdparty' + ); + + // Ensure the 'toDelete' entity is removed + $deletedThirdparty = $this->em->getRepository(Thirdparty::class)->find($toDelete->getId()); + $this->assertNull($deletedThirdparty, 'The deleted Thirdparty should no longer exist in the database'); + } +} From 7f69f21b6469bcb716a829bdb340f5b95ec0177e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Feb 2025 17:08:11 +0100 Subject: [PATCH 09/27] Renamen and reorganize thirdparty merge files --- .../ThirdpartyMergeManager.php | 125 --------------- .../ChillThirdPartyExtension.php | 1 - .../Service/ThirdpartyMergeService.php | 145 ++++++++---------- .../Action/ThirdpartyMergeManagerTest.php | 100 ------------ .../config/services/actions.yaml | 6 - 5 files changed, 65 insertions(+), 312 deletions(-) delete mode 100644 src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php delete mode 100644 src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php delete mode 100644 src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml diff --git a/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php b/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php deleted file mode 100644 index 941288367..000000000 --- a/src/Bundle/ChillThirdPartyBundle/Actions/MergeThirdparty/ThirdpartyMergeManager.php +++ /dev/null @@ -1,125 +0,0 @@ -transferData($toKeep, $toDelete); - - // Update linked entities - $this->updateReferences($toKeep, $toDelete); - - $this->em->remove($toDelete); - $this->em->flush(); - } - - private function transferData(ThirdParty $toKeep, ThirdParty $toDelete): void - { - $excludedProperties = ['id', 'createdAt']; - $reflection = new \ReflectionClass(ThirdParty::class); - - foreach ($reflection->getProperties() as $property) { - if (in_array($property->getName(), $excludedProperties, true)) { - continue; - } - - $toKeepValue = $property->getValue($toKeep); - $toDeleteValue = $property->getValue($toDelete); - - if (null === $toKeepValue && null !== $toDeleteValue) { - $property->setValue($toKeep, $toDeleteValue); - } - - if ($toKeepValue instanceof \Doctrine\Common\Collections\Collection - && $toDeleteValue instanceof \Doctrine\Common\Collections\Collection) { - foreach ($toDeleteValue as $item) { - if (!$toKeepValue->contains($item)) { - $toKeepValue->add($item); - } - } - } - } - } - - private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): void - { - $allMeta = $this->em->getMetadataFactory()->getAllMetadata(); - - foreach ($allMeta as $meta) { - foreach ($meta->getAssociationMappings() as $assoc) { - if (ThirdParty::class !== $assoc['targetEntity']) { - continue; // Skip unrelated associations - } - - $entityClass = $meta->getName(); - $associationField = $assoc['fieldName']; - - if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_ONE) { - // Handle ManyToOne or OneToOne - $qb = $this->em->createQueryBuilder(); - $qb->update($entityClass, 'e') - ->set("e.{$associationField}", ':toKeep') - ->where("e.{$associationField} = :toDelete") - ->setParameter('toKeep', $toKeep) - ->setParameter('toDelete', $toDelete) - ->getQuery() - ->execute(); - } - - if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_MANY) { - // Handle ManyToMany or OneToMany (inverse side) - $repo = $this->em->getRepository($entityClass); - $linkedEntities = $repo->createQueryBuilder('e') - ->join("e.{$associationField}", 't') - ->where('t = :toDelete') - ->setParameter('toDelete', $toDelete) - ->getQuery() - ->getResult(); - - foreach ($linkedEntities as $entity) { - $getter = 'get'.ucfirst($associationField); - $setter = 'set'.ucfirst($associationField); - $adder = 'add'.ucfirst(rtrim($associationField, 's')); - $remover = 'remove'.ucfirst(rtrim($associationField, 's')); - - if (method_exists($entity, $getter) && method_exists($entity, $setter)) { - // For OneToMany owning side - $collection = $entity->{$getter}(); - if ($collection->contains($toDelete)) { - $collection->removeElement($toDelete); - if (!$collection->contains($toKeep)) { - $collection->add($toKeep); - } - } - } elseif (method_exists($entity, $adder) && method_exists($entity, $remover)) { - // For ManyToMany - $entity->{$remover}($toDelete); - $entity->{$adder}($toKeep); - } - - $this->em->persist($entity); - } - } - } - } - - $this->em->flush(); - } -} diff --git a/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php b/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php index e12807819..475d22049 100644 --- a/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php +++ b/src/Bundle/ChillThirdPartyBundle/DependencyInjection/ChillThirdPartyExtension.php @@ -51,7 +51,6 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte $loader->load('services/serializer.yaml'); $loader->load('services/repository.yaml'); $loader->load('services/doctrineEventListener.yaml'); - $loader->load('services/actions.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 820ec742e..8c4d0e9ef 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -11,12 +11,8 @@ declare(strict_types=1); namespace Chill\ThirdPartyBundle\Service; -use Chill\MainBundle\Entity\Address; use Chill\ThirdPartyBundle\Entity\ThirdParty; -use Doctrine\Common\Collections\Collection; -use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; class ThirdpartyMergeService { @@ -30,107 +26,96 @@ class ThirdpartyMergeService // Update linked entities $this->updateReferences($toKeep, $toDelete); - // Safely remove the old ThirdParty $this->em->remove($toDelete); - $this->em->getConnection()->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit'); - $this->em->flush(); } - /** - * @throws Exception - */ private function transferData(ThirdParty $toKeep, ThirdParty $toDelete): void { - $conn = $this->em->getConnection(); + $excludedProperties = ['id', 'createdAt']; + $reflection = new \ReflectionClass(ThirdParty::class); - $columns = ['profession', 'firstname', 'name', 'email', 'telephone', 'comment', 'kind', 'contact_data_anonymous', 'types', 'active', 'name_company']; - - $conn->beginTransaction(); - $metadata = $this->em->getClassMetadata(ThirdParty::class); - - foreach ($columns as $column) { - $columnType = $metadata->getTypeOfField($column); - - if ('string' === $columnType || 'text' === $columnType) { - $sqlUpdate = " - UPDATE chill_3party.third_party - SET {$column} = COALESCE((SELECT {$column} FROM chill_3party.third_party WHERE id = :toDelete), {$column}) - WHERE id = :toKeep AND ({$column} IS NULL OR {$column} = '')"; - } else { - $sqlUpdate = " - UPDATE chill_3party.third_party - SET {$column} = COALESCE((SELECT {$column} FROM chill_3party.third_party WHERE id = :toDelete), {$column}) - WHERE id = :toKeep AND {$column} IS NULL"; + foreach ($reflection->getProperties() as $property) { + if (in_array($property->getName(), $excludedProperties, true)) { + continue; } - // Execute the query - $conn->executeQuery($sqlUpdate, [ - 'toDelete' => $toDelete->getId(), - 'toKeep' => $toKeep->getId(), - ]); - } + $toKeepValue = $property->getValue($toKeep); + $toDeleteValue = $property->getValue($toDelete); - $conn->commit(); + if (null === $toKeepValue && null !== $toDeleteValue) { + $property->setValue($toKeep, $toDeleteValue); + } + + if ($toKeepValue instanceof \Doctrine\Common\Collections\Collection + && $toDeleteValue instanceof \Doctrine\Common\Collections\Collection) { + foreach ($toDeleteValue as $item) { + if (!$toKeepValue->contains($item)) { + $toKeepValue->add($item); + } + } + } + } } private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): void { $allMeta = $this->em->getMetadataFactory()->getAllMetadata(); - $conn = $this->em->getConnection(); foreach ($allMeta as $meta) { - if ($meta->isMappedSuperclass) { - continue; - } - - $tableName = $meta->getTableName(); - foreach ($meta->getAssociationMappings() as $assoc) { if (ThirdParty::class !== $assoc['targetEntity']) { - continue; + continue; // Skip unrelated associations } - if ($assoc['type'] & ClassMetadata::TO_ONE) { + $entityClass = $meta->getName(); + $associationField = $assoc['fieldName']; - $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); - - if (ThirdParty::class === $assoc['sourceEntity']) { - $sql = "UPDATE chill_3party.{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; - } else { - $sql = "UPDATE {$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; - } - - $conn->executeStatement($sql, [ - 'toKeep' => $toKeep->getId(), - 'toDelete' => $toDelete->getId(), - ]); - - if ('parent' === $assoc['fieldName'] && ThirdParty::class === $assoc['targetEntity']) { - // Refresh $toKeep to sync its children collection - $this->em->refresh($toKeep); - } + if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_ONE) { + // Handle ManyToOne or OneToOne + $qb = $this->em->createQueryBuilder(); + $qb->update($entityClass, 'e') + ->set("e.{$associationField}", ':toKeep') + ->where("e.{$associationField} = :toDelete") + ->setParameter('toKeep', $toKeep) + ->setParameter('toDelete', $toDelete) + ->getQuery() + ->execute(); } - if ($assoc['type'] & ClassMetadata::TO_MANY) { - if (!isset($assoc['joinTable'])) { - continue; + if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_MANY) { + // Handle ManyToMany or OneToMany (inverse side) + $repo = $this->em->getRepository($entityClass); + $linkedEntities = $repo->createQueryBuilder('e') + ->join("e.{$associationField}", 't') + ->where('t = :toDelete') + ->setParameter('toDelete', $toDelete) + ->getQuery() + ->getResult(); + + foreach ($linkedEntities as $entity) { + $getter = 'get'.ucfirst($associationField); + $setter = 'set'.ucfirst($associationField); + $adder = 'add'.ucfirst(rtrim($associationField, 's')); + $remover = 'remove'.ucfirst(rtrim($associationField, 's')); + + if (method_exists($entity, $getter) && method_exists($entity, $setter)) { + // For OneToMany owning side + $collection = $entity->{$getter}(); + if ($collection->contains($toDelete)) { + $collection->removeElement($toDelete); + if (!$collection->contains($toKeep)) { + $collection->add($toKeep); + } + } + } elseif (method_exists($entity, $adder) && method_exists($entity, $remover)) { + // For ManyToMany + $entity->{$remover}($toDelete); + $entity->{$adder}($toKeep); + } + + $this->em->persist($entity); } - - $joinTable = $assoc['joinTable']['name']; - - if ($assoc['isOwningSide']) { - $joinColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name']; - } else { - $joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; - } - - $sql = "UPDATE {$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; - - $conn->executeStatement($sql, [ - 'toKeep' => $toKeep->getId(), - 'toDelete' => $toDelete->getId(), - ]); } } } diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php deleted file mode 100644 index a36a393bc..000000000 --- a/src/Bundle/ChillThirdPartyBundle/Tests/Action/ThirdpartyMergeManagerTest.php +++ /dev/null @@ -1,100 +0,0 @@ -mergeManager = $this->getContainer()->get(ThirdpartyMergeManager::class); - $this->em = $this->getContainer()->get(EntityManagerInterface::class); - $this->connection = $this->em->getConnection(); - - // Start a transaction before each test - $this->connection->beginTransaction(); - } - - protected function tearDown(): void - { - try { - // Rollback the transaction after each test to ensure no data is persisted - $this->connection->rollBack(); - } catch (\Exception $e) { - $this->connection->close(); - } - - parent::tearDown(); - } - - public function testThirdpartyMerge(): void - { - // Arrange: Create Thirdparty entities - $toKeep = new ThirdParty(); - $toKeep->setName('Thirdparty ToKeep'); - $toKeep->setEmail('keep@example.com'); - - $toDelete = new Thirdparty(); - $toDelete->setName('Thirdparty ToDelete'); // This should be ignored - $toDelete->setTelephone(new PhoneNumber('123456789')); - - // Related entities - $activity = new Activity(); - $activity->addThirdParty($toDelete); // This is a Many-to-Many relation - - $personResource = new PersonResource(); - $personResource->setThirdParty($toDelete); // This is a Many-to-One relation - - $this->em->persist($toKeep); - $this->em->persist($toDelete); - $this->em->persist($activity); - $this->em->persist($personResource); - $this->em->flush(); - - // Merge - $this->mergeManager->merge($toKeep, $toDelete); - $this->em->clear(); - - // Verify data was merged correctly - $mergedThirdparty = $this->em->getRepository(Thirdparty::class)->find($toKeep->getId()); - $this->assertNotNull($mergedThirdparty); - $this->assertEquals('Primary Name', $mergedThirdparty->getName(), 'Name should remain unchanged'); - $this->assertEquals('keep@example.com', $mergedThirdparty->getEmail(), 'Email should remain unchanged'); - $this->assertEquals('123456789', $mergedThirdparty->getPhone(), 'Phone should be transferred from Thirdparty ToDelete'); - - // Check that relationships are updated - $updatedActivity = $this->em->getRepository(Activity::class)->find($activity->getId()); - $this->assertTrue( - $updatedActivity->getThirdParties()->contains($mergedThirdparty), - 'Activity should be linked to the merged Thirdparty' - ); - $this->assertFalse( - $updatedActivity->getThirdParties()->exists(fn($key, $tp) => $tp->getId() === $toDelete->getId()), - 'Activity should no longer reference the deleted Thirdparty' - ); - - $updatedPersonResource = $this->em->getRepository(PersonResource::class)->find($personResource->getId()); - $this->assertEquals( - $mergedThirdparty->getId(), - $updatedPersonResource->getThirdParty()->getId(), - 'PersonResource should reference the merged Thirdparty' - ); - - // Ensure the 'toDelete' entity is removed - $deletedThirdparty = $this->em->getRepository(Thirdparty::class)->find($toDelete->getId()); - $this->assertNull($deletedThirdparty, 'The deleted Thirdparty should no longer exist in the database'); - } -} diff --git a/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml b/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml deleted file mode 100644 index e65ed6600..000000000 --- a/src/Bundle/ChillThirdPartyBundle/config/services/actions.yaml +++ /dev/null @@ -1,6 +0,0 @@ -services: - _defaults: - autowire: true - autoconfigure: true - - Chill\ThirdPartyBundle\Actions\MergeThirdparty\ThirdpartyMergeManager: ~ From 580366de6d995ff0b15f6d074b89c71282c684fa Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Feb 2025 17:08:42 +0100 Subject: [PATCH 10/27] Continue thirdparty merge controller and create views --- src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index 9432fbdc0..a16d2b622 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -134,6 +134,7 @@ is thirdparty: Le demandeur est un tiers Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers "Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" find duplicate thirdparty: 'Désigner un tier doublon' +find duplicate thirdparty: 'Désigner un tier doublon' # admin From 5999c73c98fd9c1557cfa80b5b86919a61bf08f5 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 18 Feb 2025 16:41:41 +0100 Subject: [PATCH 11/27] Refactor merge --- .../Service/ThirdpartyMergeService.php | 155 +++++++++--------- 1 file changed, 75 insertions(+), 80 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 8c4d0e9ef..dee1f987c 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -12,7 +12,9 @@ declare(strict_types=1); namespace Chill\ThirdPartyBundle\Service; use Chill\ThirdPartyBundle\Entity\ThirdParty; +use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; class ThirdpartyMergeService { @@ -20,106 +22,99 @@ class ThirdpartyMergeService public function merge(ThirdParty $toKeep, ThirdParty $toDelete): void { - // Transfer non-duplicate data - $this->transferData($toKeep, $toDelete); + $conn = $this->em->getConnection(); + $conn->beginTransaction(); - // Update linked entities - $this->updateReferences($toKeep, $toDelete); + try { + $queries = array_merge( + $this->transferData($toKeep, $toDelete), + $this->updateReferences($toKeep, $toDelete), + $this->removeThirdparty($toDelete) + ); - $this->em->remove($toDelete); - $this->em->flush(); - } - - private function transferData(ThirdParty $toKeep, ThirdParty $toDelete): void - { - $excludedProperties = ['id', 'createdAt']; - $reflection = new \ReflectionClass(ThirdParty::class); - - foreach ($reflection->getProperties() as $property) { - if (in_array($property->getName(), $excludedProperties, true)) { - continue; + foreach ($queries as $query) { + $conn->executeStatement($query['sql'], $query['params']); } - $toKeepValue = $property->getValue($toKeep); - $toDeleteValue = $property->getValue($toDelete); - - if (null === $toKeepValue && null !== $toDeleteValue) { - $property->setValue($toKeep, $toDeleteValue); - } - - if ($toKeepValue instanceof \Doctrine\Common\Collections\Collection - && $toDeleteValue instanceof \Doctrine\Common\Collections\Collection) { - foreach ($toDeleteValue as $item) { - if (!$toKeepValue->contains($item)) { - $toKeepValue->add($item); - } - } - } + $conn->commit(); + } catch (\Exception $e) { + $conn->rollBack(); + throw $e; } } - private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): void + /** + * @throws Exception + */ + private function transferData(ThirdParty $toKeep, ThirdParty $toDelete): array { + $queries = []; + $columns = ['profession', 'firstname', 'name', 'email', 'telephone', 'comment', 'kind', 'contact_data_anonymous', 'types', 'active', 'name_company']; + $metadata = $this->em->getClassMetadata(ThirdParty::class); + + foreach ($columns as $column) { + $columnType = $metadata->getTypeOfField($column); + $condition = ('string' === $columnType || 'text' === $columnType) + ? "({$column} IS NULL OR {$column} = '')" + : "{$column} IS NULL"; + + $queries[] = [ + 'sql' => " + UPDATE chill_3party.third_party + SET {$column} = COALESCE((SELECT {$column} FROM chill_3party.third_party WHERE id = :toDelete), {$column}) + WHERE id = :toKeep AND {$condition}", + 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()] + ]; + } + + return $queries; + } + + private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): array + { + $queries = []; $allMeta = $this->em->getMetadataFactory()->getAllMetadata(); foreach ($allMeta as $meta) { + if ($meta->isMappedSuperclass) { + continue; + } + + $tableName = $meta->getTableName(); foreach ($meta->getAssociationMappings() as $assoc) { - if (ThirdParty::class !== $assoc['targetEntity']) { - continue; // Skip unrelated associations + if (ThirdParty::class !== $assoc['targetEntity'] && ThirdParty::class !== $assoc['sourceEntity']) { + continue; } - $entityClass = $meta->getName(); - $associationField = $assoc['fieldName']; + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); + $sql = "UPDATE {$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; - if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_ONE) { - // Handle ManyToOne or OneToOne - $qb = $this->em->createQueryBuilder(); - $qb->update($entityClass, 'e') - ->set("e.{$associationField}", ':toKeep') - ->where("e.{$associationField} = :toDelete") - ->setParameter('toKeep', $toKeep) - ->setParameter('toDelete', $toDelete) - ->getQuery() - ->execute(); + $queries[] = ['sql' => $sql, 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()]]; } - if ($assoc['type'] & \Doctrine\ORM\Mapping\ClassMetadata::TO_MANY) { - // Handle ManyToMany or OneToMany (inverse side) - $repo = $this->em->getRepository($entityClass); - $linkedEntities = $repo->createQueryBuilder('e') - ->join("e.{$associationField}", 't') - ->where('t = :toDelete') - ->setParameter('toDelete', $toDelete) - ->getQuery() - ->getResult(); - - foreach ($linkedEntities as $entity) { - $getter = 'get'.ucfirst($associationField); - $setter = 'set'.ucfirst($associationField); - $adder = 'add'.ucfirst(rtrim($associationField, 's')); - $remover = 'remove'.ucfirst(rtrim($associationField, 's')); - - if (method_exists($entity, $getter) && method_exists($entity, $setter)) { - // For OneToMany owning side - $collection = $entity->{$getter}(); - if ($collection->contains($toDelete)) { - $collection->removeElement($toDelete); - if (!$collection->contains($toKeep)) { - $collection->add($toKeep); - } - } - } elseif (method_exists($entity, $adder) && method_exists($entity, $remover)) { - // For ManyToMany - $entity->{$remover}($toDelete); - $entity->{$adder}($toKeep); - } - - $this->em->persist($entity); - } + if ($assoc['type'] & ClassMetadata::TO_MANY && isset($assoc['joinTable'])) { + $joinTable = $assoc['joinTable']['name']; + $joinColumn = ThirdParty::class === $assoc['targetEntity'] + ? $assoc['joinTable']['inverseJoinColumns'][0]['name'] + : $assoc['joinTable']['joinColumns'][0]['name']; +//TODO problem remaining for ManyToMany relations when UPDATE triggers a UNIQUE CONSTRAINT...in the case of Thirdparty -> thirdparty_category and thirdparty_center + $queries[] = [ + 'sql' => "UPDATE {$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ]; } } } - $this->em->flush(); + return $queries; + } + + public function removeThirdparty(ThirdParty $toDelete): array + { + return [[ + 'sql' => 'DELETE FROM chill_3party.third_party WHERE id = :toDelete', + 'params' => ['toDelete' => $toDelete->getId()], + ]]; } } From 13b1d20ade183c34903bbfcb795c4c93aff29c7b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Feb 2025 12:35:25 +0100 Subject: [PATCH 12/27] Resolve manyToMany cases --- .../ThirdpartyDuplicateController.php | 6 +-- .../Form/ThirdpartyFindDuplicateType.php | 10 ++++- .../Service/ThirdpartyMergeService.php | 40 +++++++++++++------ .../Service/ThirdpartyMergeServiceTest.php | 24 ++++++++--- 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php index a7fb87458..0c0671aaf 100644 --- a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -21,7 +21,7 @@ use Symfony\Component\Routing\Annotation\Route; class ThirdpartyDuplicateController extends AbstractController { - public function __construct(private ThirdPartyRepository $thirdPartyRepository, private ThirdpartyMergeService $thirdPartyMergeService) {} + public function __construct(private readonly ThirdPartyRepository $thirdPartyRepository, private readonly ThirdpartyMergeService $thirdPartyMergeService) {} #[Route(path: '/{_locale}/3party/{thirdparty_id}/find-manually', name: 'chill_thirdparty_find_duplicate')] public function findManuallyDuplicateAction(mixed $thirdparty_id, Request $request) @@ -41,10 +41,6 @@ class ThirdpartyDuplicateController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { $thirdparty2 = $form->get('thirdparty')->getData(); - if (null === $thirdparty2) { - throw $this->createNotFoundException("Thirdparty with id {$thirdparty2->getId}() not".' found on this server'); - } - $direction = $form->get('direction')->getData(); if ('starting' === $direction) { diff --git a/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php index 8b2534bca..e16cddf54 100644 --- a/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php +++ b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php @@ -1,5 +1,14 @@ ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()] + 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], ]; } @@ -86,24 +86,40 @@ class ThirdpartyMergeService continue; } - if ($assoc['type'] & ClassMetadata::TO_ONE) { + if (ClassMetadata::TO_ONE === $assoc['type']) { $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); - $sql = "UPDATE {$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; - $queries[] = ['sql' => $sql, 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()]]; - } + if (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' !== $joinColumn) { + // TODO what with 'address_id' and 'civility_id'? This condition also contains columns like updatedBy_id which we want to ignore... + continue; + } + + $schemaPrefix = (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' === $joinColumn) + ? 'chill_3party.' + : ''; - if ($assoc['type'] & ClassMetadata::TO_MANY && isset($assoc['joinTable'])) { - $joinTable = $assoc['joinTable']['name']; - $joinColumn = ThirdParty::class === $assoc['targetEntity'] - ? $assoc['joinTable']['inverseJoinColumns'][0]['name'] - : $assoc['joinTable']['joinColumns'][0]['name']; -//TODO problem remaining for ManyToMany relations when UPDATE triggers a UNIQUE CONSTRAINT...in the case of Thirdparty -> thirdparty_category and thirdparty_center $queries[] = [ - 'sql' => "UPDATE {$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", + 'sql' => "UPDATE {$schemaPrefix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], ]; } + + if (ClassMetadata::TO_MANY === $assoc['type'] && isset($assoc['joinTable'])) { + + $joinTable = $assoc['joinTable']['name']; + $joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; + if ('thirdparty_id' === $joinColumn) { + $queries[] = [ + 'sql' => "DELETE FROM {$joinTable} WHERE {$joinColumn} = :toDelete", + 'params' => ['toDelete' => $toDelete->getId()], + ]; + } else { + $queries[] = [ + 'sql' => "UPDATE {$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ]; + } + } } } diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php index de11cea7a..42e7ec0d1 100644 --- a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php @@ -1,5 +1,14 @@ connection->rollBack(); - } catch (\Exception $e) { + } catch (\Exception) { $this->connection->close(); } @@ -47,7 +61,7 @@ class ThirdpartyMergeServiceTest extends KernelTestCase $toKeep->setName('Thirdparty ToKeep'); $toKeep->setEmail('keep@example.com'); - $toDelete = new Thirdparty(); + $toDelete = new ThirdParty(); $toDelete->setName('Thirdparty ToDelete'); // This should be ignored $toDelete->setTelephone(new PhoneNumber('123456789')); @@ -69,7 +83,7 @@ class ThirdpartyMergeServiceTest extends KernelTestCase $this->em->clear(); // Verify data was merged correctly - $mergedThirdparty = $this->em->getRepository(Thirdparty::class)->find($toKeep->getId()); + $mergedThirdparty = $this->em->getRepository(ThirdParty::class)->find($toKeep->getId()); $this->assertNotNull($mergedThirdparty); $this->assertEquals('Primary Name', $mergedThirdparty->getName(), 'Name should remain unchanged'); $this->assertEquals('keep@example.com', $mergedThirdparty->getEmail(), 'Email should remain unchanged'); @@ -82,7 +96,7 @@ class ThirdpartyMergeServiceTest extends KernelTestCase 'Activity should be linked to the merged Thirdparty' ); $this->assertFalse( - $updatedActivity->getThirdParties()->exists(fn($key, $tp) => $tp->getId() === $toDelete->getId()), + $updatedActivity->getThirdParties()->exists(fn ($key, $tp) => $tp->getId() === $toDelete->getId()), 'Activity should no longer reference the deleted Thirdparty' ); @@ -94,7 +108,7 @@ class ThirdpartyMergeServiceTest extends KernelTestCase ); // Ensure the 'toDelete' entity is removed - $deletedThirdparty = $this->em->getRepository(Thirdparty::class)->find($toDelete->getId()); + $deletedThirdparty = $this->em->getRepository(ThirdParty::class)->find($toDelete->getId()); $this->assertNull($deletedThirdparty, 'The deleted Thirdparty should no longer exist in the database'); } } From ed60c6aaa31ac4625622d3c04600fb9e6800e46d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 25 Feb 2025 16:35:30 +0100 Subject: [PATCH 13/27] Remove dumps and inject thirdparty directly into controller action --- .../ThirdpartyDuplicateController.php | 34 +++++++------------ .../Form/ThirdpartyFindDuplicateType.php | 8 ----- .../ThirdPartyDuplicate/_details.html.twig | 12 ------- .../find_duplicate.html.twig | 2 +- 4 files changed, 13 insertions(+), 43 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php index 0c0671aaf..23238cd22 100644 --- a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -12,28 +12,25 @@ declare(strict_types=1); namespace Chill\ThirdPartyBundle\Controller; use Chill\PersonBundle\Form\PersonConfimDuplicateType; +use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Form\ThirdpartyFindDuplicateType; use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; class ThirdpartyDuplicateController extends AbstractController { public function __construct(private readonly ThirdPartyRepository $thirdPartyRepository, private readonly ThirdpartyMergeService $thirdPartyMergeService) {} + /** + * @ParamConverter("thirdparty", options={"id": "thirdparty_id"}) + */ #[Route(path: '/{_locale}/3party/{thirdparty_id}/find-manually', name: 'chill_thirdparty_find_duplicate')] - public function findManuallyDuplicateAction(mixed $thirdparty_id, Request $request) + public function findManuallyDuplicateAction(ThirdParty $thirdparty, Request $request) { - $thirdparty = $this->thirdPartyRepository->find($thirdparty_id); - dump($thirdparty_id); - dump($thirdparty); - - if (null === $thirdparty) { - throw $this->createNotFoundException("Thirdparty with id {$thirdparty_id} not".' found on this server'); - } - $form = $this->createForm(ThirdpartyFindDuplicateType::class); $form->handleRequest($request); @@ -64,24 +61,17 @@ class ThirdpartyDuplicateController extends AbstractController ]); } + /** + * @ParamConverter("thirdparty1", options={"id": "thirdparty1_id"}) + * @ParamConverter("thirdparty2", options={"id": "thirdparty2_id"}) + */ #[Route(path: '/{_locale}/3party/{thirdparty1_id}/duplicate/{thirdparty2_id}/confirm', name: 'chill_thirdparty_duplicate_confirm')] - public function confirmAction(mixed $thirdparty1_id, mixed $thirdparty2_id, Request $request) + public function confirmAction(ThirdParty $thirdparty1, ThirdParty $thirdparty2, Request $request) { - if ($thirdparty1_id === $thirdparty2_id) { + if ($thirdparty1 === $thirdparty2) { throw new \InvalidArgumentException('Can not merge same thirdparty'); } - $thirdparty1 = $this->thirdPartyRepository->find($thirdparty1_id); - $thirdparty2 = $this->thirdPartyRepository->find($thirdparty2_id); - - if (null === $thirdparty1) { - throw $this->createNotFoundException("Thirdparty with id {$thirdparty1_id} not".' found on this server'); - } - - if (null === $thirdparty2) { - throw $this->createNotFoundException("Person with id {$thirdparty2_id} not".' found on this server'); - } - $form = $this->createForm(PersonConfimDuplicateType::class); $form->handleRequest($request); diff --git a/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php index e16cddf54..db1b99481 100644 --- a/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php +++ b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php @@ -29,12 +29,4 @@ class ThirdpartyFindDuplicateType extends AbstractType 'data' => 'starting', ]); } - - /** - * @return string - */ - public function getBlockPrefix() - { - return 'chill_thirdpartybundle_thirdparty_find_manually_duplicate'; - } } diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig index 8896c8deb..ae751b8c9 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig @@ -25,15 +25,3 @@ {{ thirdparty.contactDataAnonymous }} {% endmacro %} - -{#{%- macro links(thirdparty, options) -%}#} -{#
      #} -{##} -{#
    • {{ person.counters.nb_activity }} {{ (person.counters.nb_activity > 1)? 'échanges' : 'échange' }}
    • #} -{#
    • {{ person.counters.nb_task }} {{ (person.counters.nb_task > 1)? 'tâches' : 'tâche' }}
    • #} -{#
    • {{ person.counters.nb_document }} {{ (person.counters.nb_document > 1)? 'documents' : 'document' }}
    • #} -{#
    • {{ person.counters.nb_event }} {{ (person.counters.nb_event > 1)? 'événements' : 'événement' }}
    • #} -{#
    • {{ person.counters.nb_addresses }} {{ (person.counters.nb_addresses > 1)? 'adresses' : 'adresse' }}
    • #} -{##} -{#
    #} -{#{% endmacro %}#} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig index 9dd1a0992..aea022791 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig @@ -2,7 +2,7 @@ {% set activeRouteKey = 'chill_thirdparty_duplicate' %} -{% block title %}{{ 'Find duplicate'|trans ~ ' ' ~ thirdparty.name|capitalize }}{% endblock %} +{% block title %}{{ 'thirdparty_duplicate.find'|trans ~ ' ' ~ thirdparty.name|capitalize }}{% endblock %} {% block content %} From 6db16e6d0b77072ec02f0520bae92bfc3bd52731 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 25 Feb 2025 16:35:36 +0100 Subject: [PATCH 14/27] Add translations --- .../translations/messages.fr.yml | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index a16d2b622..0aa3bfda9 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -133,24 +133,6 @@ is thirdparty: Le demandeur est un tiers Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers "Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" -find duplicate thirdparty: 'Désigner un tier doublon' -find duplicate thirdparty: 'Désigner un tier doublon' - - -# admin -admin: - export_description: Liste des tiers (format CSV) - -Profession: Profession -Firstname: Prénom -Name_company: Service/Département -Address: Adresse -Civility: Civilité -Id: Identifiant -Contact id: Identifiant du contact -Contact name: Nom du contact -Contact firstname: Prénom du contact -Contact phone: Téléphone du contact -Contact email: Courrier électronique du contact -Contact address: Adresse du contact -Contact profession: Profession du contact +thirdparty_duplicate: + merge: Fussioner + find: 'Désigner un tiers doublon' From d84f3ee5ad6329be1d4c758ff9e5e8c1ef08ad49 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Feb 2025 10:55:43 +0100 Subject: [PATCH 15/27] Modify translations and if conditions in updateReference method --- .../views/CRUD/_view_content.html.twig | 4 +-- .../find_duplicate.html.twig | 2 +- .../Service/ThirdpartyMergeService.php | 25 ++++++++----------- .../translations/messages.fr.yml | 4 +-- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig index 7d0023a39..4102f8a21 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig @@ -48,8 +48,8 @@
  • {{ 'thirdparty_duplicate.merge'|trans }} + title="{{ 'Merge'|trans }}" + class="btn btn-misc">{{ 'Merge'|trans }}
  • {% endblock %} {% block content_view_actions_edit_link %} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig index aea022791..53f7ddc5d 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig @@ -8,7 +8,7 @@ {% block content %}
    -

    {{ 'find duplicate thirdparty'|trans }}

    +

    {{ 'find_thirdparty_duplicate'|trans }}

    {{ form_start(form) }} {{ form_rest(form) }} diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 250144efd..0c1d88e52 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -86,7 +86,7 @@ class ThirdpartyMergeService continue; } - if (ClassMetadata::TO_ONE === $assoc['type']) { + if ($assoc['type'] & ClassMetadata::TO_ONE) { $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); if (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' !== $joinColumn) { @@ -104,21 +104,18 @@ class ThirdpartyMergeService ]; } - if (ClassMetadata::TO_MANY === $assoc['type'] && isset($assoc['joinTable'])) { - + if ($assoc['type'] === 8 && isset($assoc['joinTable'])) { $joinTable = $assoc['joinTable']['name']; $joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; - if ('thirdparty_id' === $joinColumn) { - $queries[] = [ - 'sql' => "DELETE FROM {$joinTable} WHERE {$joinColumn} = :toDelete", - 'params' => ['toDelete' => $toDelete->getId()], - ]; - } else { - $queries[] = [ - 'sql' => "UPDATE {$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", - 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], - ]; - } + $queries[] = [ + 'sql' => "UPDATE {$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete AND NOT EXISTS (SELECT 1 FROM {$joinTable} WHERE {$joinColumn} = :toKeep)", + 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], + ]; + + $queries[] = [ + 'sql' => "DELETE FROM {$joinTable} WHERE {$joinColumn} = :toDelete", + 'params' => ['toDelete' => $toDelete->getId()], + ]; } } } diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index 0aa3bfda9..908e045e3 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -133,6 +133,4 @@ is thirdparty: Le demandeur est un tiers Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers "Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" -thirdparty_duplicate: - merge: Fussioner - find: 'Désigner un tiers doublon' +find_thirdparty_duplicate: 'Désigner un tiers doublon' From 3d397c0145e2ea47e3bc372c8e7e8cff21cd15e3 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 3 Mar 2025 20:15:17 +0100 Subject: [PATCH 16/27] Adjust templates and add translations --- .../Resources/views/CRUD/_view_content.html.twig | 5 ++++- .../Resources/views/Entity/thirdparty.html.twig | 8 +++++++- .../Resources/views/ThirdParty/view.html.twig | 2 +- .../views/ThirdPartyDuplicate/_details.html.twig | 9 ++++++++- .../views/ThirdPartyDuplicate/confirm.html.twig | 8 ++++---- .../views/ThirdPartyDuplicate/find_duplicate.html.twig | 2 +- .../ChillThirdPartyBundle/translations/messages.fr.yml | 9 ++++++++- 7 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig index 4102f8a21..71755cc11 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig @@ -49,7 +49,10 @@ {{ 'Merge'|trans }} + class="btn btn-misc"> + + {{ 'Merge'|trans }} + {% endblock %} {% block content_view_actions_edit_link %} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/Entity/thirdparty.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/Entity/thirdparty.html.twig index 4c56dfa54..0883f0a91 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/Entity/thirdparty.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/Entity/thirdparty.html.twig @@ -161,7 +161,13 @@ - {% else %} + {% elseif is_granted('CHILL_3PARTY_3PARTY_UPDATE', thirdparty) %} +
  • + +
  • {% endif %} {% if options['customButtons']['after'] is defined %} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/view.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/view.html.twig index 4e7fd159d..9496f3c2c 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/view.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/view.html.twig @@ -116,7 +116,7 @@
    {% for tp in thirdParty.activeChildren %}
    - {{ tp|chill_entity_render_box({'render': 'bloc', 'addLink': false, 'isConfidential': tp.contactDataAnonymous ? true : false }) }} + {{ tp|chill_entity_render_box({'render': 'bloc', 'addLink': false, 'isConfidential': tp.contactDataAnonymous ? true : false, 'showFusion': true }) }}
    {% endfor %}
    diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig index ae751b8c9..e4b47b909 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig @@ -17,11 +17,18 @@ {% if thirdparty.telephone %}{{ thirdparty.telephone }}{% endif %}
  • {{ 'email'|trans }}: {% if thirdparty.email is not null %}{{ thirdparty.email }}{% endif %}
  • -
  • {{ 'address'|trans }}: +
  • {{ 'Address'|trans }}: {%- if thirdparty.getAddress is not empty -%} {{ thirdparty.getAddress|chill_entity_render_box }} {% endif %}
  • {{ 'thirdparty.Contact data are confidential'|trans }}: {{ thirdparty.contactDataAnonymous }}
  • +
  • {{ 'Contacts'|trans }}: +
      + {% for c in thirdparty.getChildren %} +
    • {{ c.name }} {{ c.firstName }}
    • + {% endfor %} +
    +
  • {% endmacro %} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig index 4baa4552e..540b10ca7 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig @@ -21,7 +21,7 @@
    -

    {{ 'thirdparty_duplicate.Merge duplicate thirdparties'|trans }}

    +

    {{ 'thirdparty_duplicate.title'|trans }}

    {{ 'thirdparty_duplicate.Thirdparty to delete'|trans }}: @@ -41,9 +41,9 @@

    -
    +

    {{ 'thirdparty_duplicate.Thirdparty to keep'|trans }}: - {{ 'thirdparty_duplicate.Thirdparty to delete explanation'|trans }} + {{ 'thirdparty_duplicate.Thirdparty to keep explanation'|trans }}

    @@ -51,7 +51,7 @@ {{ thirdparty }} -

    {{ 'thirdparty_duplicate.data to keep'|trans ~ ':' }}

    +

    {{ 'thirdparty_duplicate.Data to keep'|trans ~ ':' }}

    {{ details.details(thirdparty) }} {#

    {{ 'thirdparty_duplicate.links to keep'|trans ~ ':' }}

    #} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig index 53f7ddc5d..a5d463a39 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig @@ -8,7 +8,7 @@ {% block content %}
    -

    {{ 'find_thirdparty_duplicate'|trans }}

    +

    {{ 'thirdparty_duplicate.find'|trans }}

    {{ form_start(form) }} {{ form_rest(form) }} diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index 908e045e3..b4340bde1 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -133,4 +133,11 @@ is thirdparty: Le demandeur est un tiers Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers "Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" -find_thirdparty_duplicate: 'Désigner un tiers doublon' +thirdparty_duplicate: + title: Fusionner les tiers doublons + find: Désigner un tiers doublon + Thirdparty to keep: Tiers à conserver + Thirdparty to delete: Tiers à supprimer + Thirdparty to delete explanation: Ce tiers sera supprimé. Seuls les contacts de ce tiers, énumérés ci-dessous, seront transférés. + Thirdparty to keep explanation: Ce tiers sera conserver + Data to keep: Données conservées From 58291c74029368671445a7f7486f098cb951aca4 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 3 Mar 2025 20:16:41 +0100 Subject: [PATCH 17/27] Add suggested thirdparties, with same parent, for thirdparties of kind 'child' --- .../Controller/ThirdpartyDuplicateController.php | 8 +++++++- .../Form/ThirdpartyFindDuplicateType.php | 9 +++++++++ .../Templating/Entity/ThirdPartyRender.php | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php index 23238cd22..78f99e8a9 100644 --- a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -31,7 +31,13 @@ class ThirdpartyDuplicateController extends AbstractController #[Route(path: '/{_locale}/3party/{thirdparty_id}/find-manually', name: 'chill_thirdparty_find_duplicate')] public function findManuallyDuplicateAction(ThirdParty $thirdparty, Request $request) { - $form = $this->createForm(ThirdpartyFindDuplicateType::class); + $suggested = []; + + if ('child' === $thirdparty->getKind()) { + $suggested = $thirdparty->getParent()->getChildren(); + } + + $form = $this->createForm(ThirdpartyFindDuplicateType::class, null, ['suggested' => $suggested]); $form->handleRequest($request); diff --git a/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php index db1b99481..275b6d21c 100644 --- a/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php +++ b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php @@ -15,6 +15,7 @@ use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; class ThirdpartyFindDuplicateType extends AbstractType { @@ -24,9 +25,17 @@ class ThirdpartyFindDuplicateType extends AbstractType ->add('thirdparty', PickThirdpartyDynamicType::class, [ 'label' => 'Find duplicate', 'mapped' => false, + 'suggested' => $options['suggested'], ]) ->add('direction', HiddenType::class, [ 'data' => 'starting', ]); } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'suggested' => [], + ]); + } } diff --git a/src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php b/src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php index a36ceda4e..427f2b9e3 100644 --- a/src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php +++ b/src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php @@ -39,6 +39,7 @@ class ThirdPartyRender implements ChillEntityRenderInterface 'showContacts' => $options['showContacts'] ?? false, 'showParent' => $options['showParent'] ?? true, 'isConfidential' => $options['isConfidential'] ?? false, + 'showFusion' => $options['showFusion'] ?? false, ]; return From 9979378e78f23728dd36be5123c4ac28afda2866 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 3 Mar 2025 20:52:05 +0100 Subject: [PATCH 18/27] validate thirdparty merge --- .../ThirdpartyDuplicateController.php | 56 +++++++++++++------ .../Service/ThirdpartyMergeService.php | 5 +- .../translations/messages.fr.yml | 3 + 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php index 78f99e8a9..56967a9ce 100644 --- a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -14,16 +14,16 @@ namespace Chill\ThirdPartyBundle\Controller; use Chill\PersonBundle\Form\PersonConfimDuplicateType; use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Form\ThirdpartyFindDuplicateType; -use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; +use Symfony\Contracts\Translation\TranslatorInterface; class ThirdpartyDuplicateController extends AbstractController { - public function __construct(private readonly ThirdPartyRepository $thirdPartyRepository, private readonly ThirdpartyMergeService $thirdPartyMergeService) {} + public function __construct(private readonly ThirdpartyMergeService $thirdPartyMergeService, private readonly TranslatorInterface $translator) {} /** * @ParamConverter("thirdparty", options={"id": "thirdparty_id"}) @@ -74,25 +74,45 @@ class ThirdpartyDuplicateController extends AbstractController #[Route(path: '/{_locale}/3party/{thirdparty1_id}/duplicate/{thirdparty2_id}/confirm', name: 'chill_thirdparty_duplicate_confirm')] public function confirmAction(ThirdParty $thirdparty1, ThirdParty $thirdparty2, Request $request) { - if ($thirdparty1 === $thirdparty2) { - throw new \InvalidArgumentException('Can not merge same thirdparty'); + try { + $this->validateThirdpartyMerge($thirdparty1, $thirdparty2); + $form = $this->createForm(PersonConfimDuplicateType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + + $this->thirdPartyMergeService->merge($thirdparty1, $thirdparty2); + + return $this->redirectToRoute('chill_crud_3party_3party_view', ['id' => $thirdparty1->getId()]); + } + + return $this->render('@ChillThirdParty/ThirdPartyDuplicate/confirm.html.twig', [ + 'thirdparty' => $thirdparty1, + 'thirdparty2' => $thirdparty2, + 'form' => $form->createView(), + ]); + } catch (\InvalidArgumentException $e) { + $this->addFlash('error', $this->translator->trans($e->getMessage())); + + return $this->redirectToRoute('chill_thirdparty_find_duplicate', [ + 'thirdparty_id' => $thirdparty1->getId(), + ]); } + } - $form = $this->createForm(PersonConfimDuplicateType::class); + private function validateThirdpartyMerge(Thirdparty $thirdparty1, Thirdparty $thirdparty2): void + { + $constraints = [ + [$thirdparty1 === $thirdparty2, 'thirdparty_duplicate.You cannot merge a thirdparty with itself. Please choose a different thirdparty'], + [$thirdparty1->getKind() !== $thirdparty2->getKind(), 'thirdparty_duplicate.A thirdparty can only be merged with a thirdparty of the same kind'], + [$thirdparty1->getParent() !== $thirdparty2->getParent(), 'thirdparty_duplicate.Two child thirdparties must have the same parent'], + ]; - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - - $this->thirdPartyMergeService->merge($thirdparty1, $thirdparty2); - - return $this->redirectToRoute('chill_crud_3party_3party_view', ['id' => $thirdparty1->getId()]); + foreach ($constraints as [$condition, $message]) { + if ($condition) { + throw new \InvalidArgumentException($message); + } } - - return $this->render('@ChillThirdParty/ThirdPartyDuplicate/confirm.html.twig', [ - 'thirdparty' => $thirdparty1, - 'thirdparty2' => $thirdparty2, - 'form' => $form->createView(), - ]); } } diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 0c1d88e52..89412e5f6 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -90,7 +90,6 @@ class ThirdpartyMergeService $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); if (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' !== $joinColumn) { - // TODO what with 'address_id' and 'civility_id'? This condition also contains columns like updatedBy_id which we want to ignore... continue; } @@ -102,9 +101,7 @@ class ThirdpartyMergeService 'sql' => "UPDATE {$schemaPrefix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], ]; - } - - if ($assoc['type'] === 8 && isset($assoc['joinTable'])) { + } elseif (8 === $assoc['type'] && isset($assoc['joinTable'])) { $joinTable = $assoc['joinTable']['name']; $joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; $queries[] = [ diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index b4340bde1..85f7b2405 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -141,3 +141,6 @@ thirdparty_duplicate: Thirdparty to delete explanation: Ce tiers sera supprimé. Seuls les contacts de ce tiers, énumérés ci-dessous, seront transférés. Thirdparty to keep explanation: Ce tiers sera conserver Data to keep: Données conservées + You cannot merge a thirdparty with itself. Please choose a different thirdparty: Vous ne pouvez pas fusionner un tiers avec lui-même. Veuillez choisir un autre tiers. + A thirdparty can only be merged with a thirdparty of the same kind: Un tiers ne peut être fusionné qu'avec un tiers de même type. + Two child thirdparties must have the same parent: Deux tiers de type « contact » doivent avoir le même tiers parent. From 535409e529fb60dd127056fc54f836f6f6897525 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 3 Mar 2025 21:05:00 +0100 Subject: [PATCH 19/27] phpstan and php cs fixer --- .../Controller/ThirdpartyDuplicateController.php | 2 +- .../ChillThirdPartyBundle/Service/ThirdpartyMergeService.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php index 56967a9ce..0c27d13ff 100644 --- a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -101,7 +101,7 @@ class ThirdpartyDuplicateController extends AbstractController } } - private function validateThirdpartyMerge(Thirdparty $thirdparty1, Thirdparty $thirdparty2): void + private function validateThirdpartyMerge(ThirdParty $thirdparty1, ThirdParty $thirdparty2): void { $constraints = [ [$thirdparty1 === $thirdparty2, 'thirdparty_duplicate.You cannot merge a thirdparty with itself. Please choose a different thirdparty'], diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 89412e5f6..d779cccb8 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -85,8 +85,8 @@ class ThirdpartyMergeService if (ThirdParty::class !== $assoc['targetEntity'] && ThirdParty::class !== $assoc['sourceEntity']) { continue; } - - if ($assoc['type'] & ClassMetadata::TO_ONE) { + // phpstan wants boolean for if condition + if (($assoc['type'] & ClassMetadata::TO_ONE) !== 0) { $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); if (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' !== $joinColumn) { From 6ba5c91ee685ca7def0e375c48ad8d90be359117 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 3 Mar 2025 21:28:45 +0100 Subject: [PATCH 20/27] redo test --- .../Service/ThirdpartyMergeServiceTest.php | 102 ++++++------------ 1 file changed, 33 insertions(+), 69 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php index 42e7ec0d1..3e95870a6 100644 --- a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php @@ -11,12 +11,11 @@ declare(strict_types=1); namespace Chill\ThirdPartyBundle\Tests\Service; -use Chill\ActivityBundle\Entity\Activity; -use Chill\PersonBundle\Entity\Person\PersonResource; use Chill\ThirdPartyBundle\Entity\ThirdParty; +use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory; use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService; use Doctrine\ORM\EntityManagerInterface; -use libphonenumber\PhoneNumber; +use Doctrine\ORM\Tools\SchemaTool; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** @@ -26,89 +25,54 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; */ class ThirdpartyMergeServiceTest extends KernelTestCase { - private $mergeManager; - private $em; - private $connection; + private EntityManagerInterface $em; + private ThirdpartyMergeService $service; protected function setUp(): void { self::bootKernel(); + $this->em = self::getContainer()->get(EntityManagerInterface::class); - $this->mergeManager = $this->getContainer()->get(ThirdpartyMergeService::class); - $this->em = $this->getContainer()->get(EntityManagerInterface::class); - $this->connection = $this->em->getConnection(); + $schemaTool = new SchemaTool($this->em); + $schemaTool->updateSchema($this->em->getMetadataFactory()->getAllMetadata()); - // Start a transaction before each test - $this->connection->beginTransaction(); + $this->service = new ThirdpartyMergeService($this->em); } - protected function tearDown(): void + public function testMergeUpdatesReferencesAndDeletesThirdparty(): void { - try { - // Rollback the transaction after each test to ensure no data is persisted - $this->connection->rollBack(); - } catch (\Exception) { - $this->connection->close(); - } - - parent::tearDown(); - } - - public function testThirdpartyMerge(): void - { - // Arrange: Create Thirdparty entities + // Create ThirdParty entities $toKeep = new ThirdParty(); - $toKeep->setName('Thirdparty ToKeep'); - $toKeep->setEmail('keep@example.com'); + $this->em->persist($toKeep); $toDelete = new ThirdParty(); - $toDelete->setName('Thirdparty ToDelete'); // This should be ignored - $toDelete->setTelephone(new PhoneNumber('123456789')); - - // Related entities - $activity = new Activity(); - $activity->addThirdParty($toDelete); // This is a Many-to-Many relation - - $personResource = new PersonResource(); - $personResource->setThirdParty($toDelete); // This is a Many-to-One relation - - $this->em->persist($toKeep); $this->em->persist($toDelete); - $this->em->persist($activity); - $this->em->persist($personResource); + + // Create a related entity with TO_ONE relation (thirdparty parent) + $relatedToOneEntity = new ThirdParty(); + $this->em->persist($relatedToOneEntity); + $toDelete->setParent($relatedToOneEntity); + $this->em->persist($toDelete); + + // Create a related entity with TO_MANY relation (thirdparty category) + $relatedManyEntity = new ThirdPartyCategory(); + $this->em->persist($relatedManyEntity); + $toDelete->addCategory($relatedManyEntity); + $this->em->persist($toDelete); + $this->em->flush(); - // Merge - $this->mergeManager->merge($toKeep, $toDelete); - $this->em->clear(); + // Run merge + $this->service->merge($toKeep, $toDelete); - // Verify data was merged correctly - $mergedThirdparty = $this->em->getRepository(ThirdParty::class)->find($toKeep->getId()); - $this->assertNotNull($mergedThirdparty); - $this->assertEquals('Primary Name', $mergedThirdparty->getName(), 'Name should remain unchanged'); - $this->assertEquals('keep@example.com', $mergedThirdparty->getEmail(), 'Email should remain unchanged'); - $this->assertEquals('123456789', $mergedThirdparty->getPhone(), 'Phone should be transferred from Thirdparty ToDelete'); + // Check that references were updated + $this->assertEquals($toKeep->getParent()->getId(), $relatedToOneEntity->getId(), 'The parent thirdparty was succesfully merged'); - // Check that relationships are updated - $updatedActivity = $this->em->getRepository(Activity::class)->find($activity->getId()); - $this->assertTrue( - $updatedActivity->getThirdParties()->contains($mergedThirdparty), - 'Activity should be linked to the merged Thirdparty' - ); - $this->assertFalse( - $updatedActivity->getThirdParties()->exists(fn ($key, $tp) => $tp->getId() === $toDelete->getId()), - 'Activity should no longer reference the deleted Thirdparty' - ); + $updatedRelatedManyEntity = $this->em->find(ThirdPartyCategory::class, $relatedManyEntity->getId()); + $this->assertContains($updatedRelatedManyEntity, $toKeep->getCategories(), 'The thirdparty category was found in the toKeep entity'); - $updatedPersonResource = $this->em->getRepository(PersonResource::class)->find($personResource->getId()); - $this->assertEquals( - $mergedThirdparty->getId(), - $updatedPersonResource->getThirdParty()->getId(), - 'PersonResource should reference the merged Thirdparty' - ); - - // Ensure the 'toDelete' entity is removed - $deletedThirdparty = $this->em->getRepository(ThirdParty::class)->find($toDelete->getId()); - $this->assertNull($deletedThirdparty, 'The deleted Thirdparty should no longer exist in the database'); + // Check that toDelete was removed + $deletedThirdParty = $this->em->find(ThirdParty::class, $toDelete->getId()); + $this->assertNull($deletedThirdParty); } } From 09b7558e920d38898de168e0358b7a36f498361d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Mar 2025 19:21:50 +0100 Subject: [PATCH 21/27] Remove the transferData method from mergeService --- .../Service/ThirdpartyMergeService.php | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index d779cccb8..c9d126f14 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -27,7 +27,6 @@ class ThirdpartyMergeService try { $queries = array_merge( - $this->transferData($toKeep, $toDelete), $this->updateReferences($toKeep, $toDelete), $this->removeThirdparty($toDelete) ); @@ -43,33 +42,6 @@ class ThirdpartyMergeService } } - /** - * @throws Exception - */ - private function transferData(ThirdParty $toKeep, ThirdParty $toDelete): array - { - $queries = []; - $columns = ['profession', 'firstname', 'name', 'email', 'telephone', 'comment', 'kind', 'contact_data_anonymous', 'types', 'active', 'name_company']; - $metadata = $this->em->getClassMetadata(ThirdParty::class); - - foreach ($columns as $column) { - $columnType = $metadata->getTypeOfField($column); - $condition = ('string' === $columnType || 'text' === $columnType) - ? "({$column} IS NULL OR {$column} = '')" - : "{$column} IS NULL"; - - $queries[] = [ - 'sql' => " - UPDATE chill_3party.third_party - SET {$column} = COALESCE((SELECT {$column} FROM chill_3party.third_party WHERE id = :toDelete), {$column}) - WHERE id = :toKeep AND {$condition}", - 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], - ]; - } - - return $queries; - } - private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): array { $queries = []; From 43025064712428f11222bc054fc9ee7145cb896b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 5 Mar 2025 19:43:16 +0100 Subject: [PATCH 22/27] Fix translation and cs-fixes --- .../Resources/public/vuejs/_components/BadgeEntity.vue | 3 ++- .../ChillThirdPartyBundle/Service/ThirdpartyMergeService.php | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/BadgeEntity.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/BadgeEntity.vue index 3357d6463..6358d0683 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/BadgeEntity.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/BadgeEntity.vue @@ -28,7 +28,7 @@ {{ trans(THIRDPARTY_A_CONTACT) }} - {{ $t("thirdparty.contact") }} + {{ trans(THIRDPARTY_CONTACT) }} @@ -56,6 +56,7 @@ import { THIRDPARTY_CONTACT_OF, PERSON, THIRDPARTY, + THIRDPARTY_CONTACT, } from "translator"; const props = defineProps({ diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index c9d126f14..22956c8d0 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\ThirdPartyBundle\Service; use Chill\ThirdPartyBundle\Entity\ThirdParty; -use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; From 95972399a131d228d782a141b61a7e1f9bd948c0 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 24 Mar 2025 15:28:58 +0100 Subject: [PATCH 23/27] Remove redundant lines in test --- .../Tests/Service/ThirdpartyMergeServiceTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php index 3e95870a6..a1d694c43 100644 --- a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php @@ -15,7 +15,6 @@ use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory; use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Tools\SchemaTool; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** @@ -33,9 +32,6 @@ class ThirdpartyMergeServiceTest extends KernelTestCase self::bootKernel(); $this->em = self::getContainer()->get(EntityManagerInterface::class); - $schemaTool = new SchemaTool($this->em); - $schemaTool->updateSchema($this->em->getMetadataFactory()->getAllMetadata()); - $this->service = new ThirdpartyMergeService($this->em); } From b3bf405c5bed217e3f54a379e2cbc47448dc4e1b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 1 Apr 2025 16:15:34 +0200 Subject: [PATCH 24/27] Fix merge service and test --- .../Service/ThirdpartyMergeService.php | 23 ++++++++++--------- .../Service/ThirdpartyMergeServiceTest.php | 14 +++++++---- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 22956c8d0..650065cc0 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -60,18 +60,19 @@ class ThirdpartyMergeService if (($assoc['type'] & ClassMetadata::TO_ONE) !== 0) { $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); - if (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' !== $joinColumn) { - continue; + $suffix = (ThirdParty::class === $assoc['sourceEntity']) ? 'chill_3party.' : ''; + + if (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' === $joinColumn) { + $queries[] = [ + 'sql' => "UPDATE {$suffix}{$tableName} SET parent_id = (SELECT parent_id FROM chill_3party.third_party WHERE id = :toDelete) WHERE id = :toKeep", + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ]; + } else { + $queries[] = [ + 'sql' => "UPDATE {$suffix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ]; } - - $schemaPrefix = (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' === $joinColumn) - ? 'chill_3party.' - : ''; - - $queries[] = [ - 'sql' => "UPDATE {$schemaPrefix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", - 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], - ]; } elseif (8 === $assoc['type'] && isset($assoc['joinTable'])) { $joinTable = $assoc['joinTable']['name']; $joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php index a1d694c43..f25da94e9 100644 --- a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php @@ -39,35 +39,41 @@ class ThirdpartyMergeServiceTest extends KernelTestCase { // Create ThirdParty entities $toKeep = new ThirdParty(); + $toKeep->setName('Thirdparty to keep'); $this->em->persist($toKeep); $toDelete = new ThirdParty(); + $toDelete->setName('Thirdparty to delete'); $this->em->persist($toDelete); // Create a related entity with TO_ONE relation (thirdparty parent) $relatedToOneEntity = new ThirdParty(); + $relatedToOneEntity->setName('RelatedToOne thirdparty'); $this->em->persist($relatedToOneEntity); $toDelete->setParent($relatedToOneEntity); $this->em->persist($toDelete); // Create a related entity with TO_MANY relation (thirdparty category) - $relatedManyEntity = new ThirdPartyCategory(); - $this->em->persist($relatedManyEntity); - $toDelete->addCategory($relatedManyEntity); + $thirdpartyCategory = new ThirdPartyCategory(); + $thirdpartyCategory->setName(['fr' => 'Thirdparty category']); + $this->em->persist($thirdpartyCategory); + $toDelete->addCategory($thirdpartyCategory); $this->em->persist($toDelete); $this->em->flush(); // Run merge $this->service->merge($toKeep, $toDelete); + $this->em->refresh($toKeep); // Check that references were updated $this->assertEquals($toKeep->getParent()->getId(), $relatedToOneEntity->getId(), 'The parent thirdparty was succesfully merged'); - $updatedRelatedManyEntity = $this->em->find(ThirdPartyCategory::class, $relatedManyEntity->getId()); + $updatedRelatedManyEntity = $this->em->find(ThirdPartyCategory::class, $thirdpartyCategory->getId()); $this->assertContains($updatedRelatedManyEntity, $toKeep->getCategories(), 'The thirdparty category was found in the toKeep entity'); // Check that toDelete was removed + $this->em->clear(); $deletedThirdParty = $this->em->find(ThirdParty::class, $toDelete->getId()); $this->assertNull($deletedThirdParty); } From e701e96187efffdb77e7a071dcf1e80db468e434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 14 Apr 2025 15:49:51 +0200 Subject: [PATCH 25/27] Fix merging: update the thirdparty related column and not the other one --- .../Service/ThirdpartyMergeService.php | 55 ++++++++++--------- .../Service/ThirdpartyMergeServiceTest.php | 12 +++- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php index 650065cc0..6c0014a2d 100644 --- a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -25,10 +25,10 @@ class ThirdpartyMergeService $conn->beginTransaction(); try { - $queries = array_merge( - $this->updateReferences($toKeep, $toDelete), - $this->removeThirdparty($toDelete) - ); + $queries = [ + ...$this->updateReferences($toKeep, $toDelete), + ...$this->removeThirdparty($toKeep, $toDelete), + ]; foreach ($queries as $query) { $conn->executeStatement($query['sql'], $query['params']); @@ -53,31 +53,26 @@ class ThirdpartyMergeService $tableName = $meta->getTableName(); foreach ($meta->getAssociationMappings() as $assoc) { - if (ThirdParty::class !== $assoc['targetEntity'] && ThirdParty::class !== $assoc['sourceEntity']) { + if (ThirdParty::class !== $assoc['targetEntity']) { continue; } + // phpstan wants boolean for if condition if (($assoc['type'] & ClassMetadata::TO_ONE) !== 0) { $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); $suffix = (ThirdParty::class === $assoc['sourceEntity']) ? 'chill_3party.' : ''; - if (ThirdParty::class === $assoc['sourceEntity'] && 'parent_id' === $joinColumn) { - $queries[] = [ - 'sql' => "UPDATE {$suffix}{$tableName} SET parent_id = (SELECT parent_id FROM chill_3party.third_party WHERE id = :toDelete) WHERE id = :toKeep", - 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], - ]; - } else { - $queries[] = [ - 'sql' => "UPDATE {$suffix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", - 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], - ]; - } - } elseif (8 === $assoc['type'] && isset($assoc['joinTable'])) { - $joinTable = $assoc['joinTable']['name']; - $joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; $queries[] = [ - 'sql' => "UPDATE {$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete AND NOT EXISTS (SELECT 1 FROM {$joinTable} WHERE {$joinColumn} = :toKeep)", + 'sql' => "UPDATE {$suffix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ]; + } elseif (ClassMetadata::MANY_TO_MANY === $assoc['type'] && isset($assoc['joinTable'])) { + $joinTable = $assoc['joinTable']['name']; + $prefix = null !== ($assoc['joinTable']['schema'] ?? null) ? $assoc['joinTable']['schema'].'.' : ''; + $joinColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name']; + $queries[] = [ + 'sql' => "UPDATE {$prefix}{$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete AND NOT EXISTS (SELECT 1 FROM {$prefix}{$joinTable} WHERE {$joinColumn} = :toKeep)", 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], ]; @@ -92,11 +87,21 @@ class ThirdpartyMergeService return $queries; } - public function removeThirdparty(ThirdParty $toDelete): array + public function removeThirdparty(ThirdParty $toKeep, ThirdParty $toDelete): array { - return [[ - 'sql' => 'DELETE FROM chill_3party.third_party WHERE id = :toDelete', - 'params' => ['toDelete' => $toDelete->getId()], - ]]; + return [ + [ + 'sql' => 'UPDATE chill_3party.third_party SET parent_id = :toKeep WHERE parent_id = :toDelete', + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ], + [ + 'sql' => 'UPDATE chill_3party.thirdparty_category SET thirdparty_id = :toKeep WHERE thirdparty_id = :toDelete AND NOT EXISTS (SELECT 1 FROM chill_3party.thirdparty_category WHERE thirdparty_id = :toKeep)', + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ], + [ + 'sql' => 'DELETE FROM chill_3party.third_party WHERE id = :toDelete', + 'params' => ['toDelete' => $toDelete->getId()], + ], + ]; } } diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php index f25da94e9..4b2819751 100644 --- a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\ThirdPartyBundle\Tests\Service; +use Chill\ActivityBundle\Entity\Activity; use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory; use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService; @@ -49,9 +50,8 @@ class ThirdpartyMergeServiceTest extends KernelTestCase // Create a related entity with TO_ONE relation (thirdparty parent) $relatedToOneEntity = new ThirdParty(); $relatedToOneEntity->setName('RelatedToOne thirdparty'); + $relatedToOneEntity->setParent($toDelete); $this->em->persist($relatedToOneEntity); - $toDelete->setParent($relatedToOneEntity); - $this->em->persist($toDelete); // Create a related entity with TO_MANY relation (thirdparty category) $thirdpartyCategory = new ThirdPartyCategory(); @@ -60,14 +60,20 @@ class ThirdpartyMergeServiceTest extends KernelTestCase $toDelete->addCategory($thirdpartyCategory); $this->em->persist($toDelete); + $activity = new Activity(); + $activity->setDate(new \DateTime()); + $activity->addThirdParty($toDelete); + $this->em->persist($activity); + $this->em->flush(); // Run merge $this->service->merge($toKeep, $toDelete); $this->em->refresh($toKeep); + $this->em->refresh($relatedToOneEntity); // Check that references were updated - $this->assertEquals($toKeep->getParent()->getId(), $relatedToOneEntity->getId(), 'The parent thirdparty was succesfully merged'); + $this->assertEquals($toKeep->getId(), $relatedToOneEntity->getParent()->getId(), 'The parent thirdparty was succesfully merged'); $updatedRelatedManyEntity = $this->em->find(ThirdPartyCategory::class, $thirdpartyCategory->getId()); $this->assertContains($updatedRelatedManyEntity, $toKeep->getCategories(), 'The thirdparty category was found in the toKeep entity'); From f85b0c8cf79feb6fd900acfa82453928948ef0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 14 Apr 2025 15:50:39 +0200 Subject: [PATCH 26/27] Add flash message for successful third-party merge This commit introduces a flash message to inform users when a third-party merge is successful. It uses Symfony's `FlashBag` and `TranslatableMessage` for better user feedback and localization support. --- .../Controller/ThirdpartyDuplicateController.php | 7 +++++++ .../ChillThirdPartyBundle/translations/messages.fr.yml | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php index 0c27d13ff..ab42b5656 100644 --- a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -17,8 +17,10 @@ use Chill\ThirdPartyBundle\Form\ThirdpartyFindDuplicateType; use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Routing\Annotation\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Contracts\Translation\TranslatorInterface; class ThirdpartyDuplicateController extends AbstractController @@ -84,6 +86,11 @@ class ThirdpartyDuplicateController extends AbstractController $this->thirdPartyMergeService->merge($thirdparty1, $thirdparty2); + $session = $request->getSession(); + if ($session instanceof Session) { + $session->getFlashBag()->add('success', new TranslatableMessage('thirdparty_duplicate.Merge successful')); + } + return $this->redirectToRoute('chill_crud_3party_3party_view', ['id' => $thirdparty1->getId()]); } diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index 85f7b2405..2c794ef99 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -139,8 +139,9 @@ thirdparty_duplicate: Thirdparty to keep: Tiers à conserver Thirdparty to delete: Tiers à supprimer Thirdparty to delete explanation: Ce tiers sera supprimé. Seuls les contacts de ce tiers, énumérés ci-dessous, seront transférés. - Thirdparty to keep explanation: Ce tiers sera conserver + Thirdparty to keep explanation: Ce tiers sera conservé Data to keep: Données conservées You cannot merge a thirdparty with itself. Please choose a different thirdparty: Vous ne pouvez pas fusionner un tiers avec lui-même. Veuillez choisir un autre tiers. A thirdparty can only be merged with a thirdparty of the same kind: Un tiers ne peut être fusionné qu'avec un tiers de même type. Two child thirdparties must have the same parent: Deux tiers de type « contact » doivent avoir le même tiers parent. + Merge successful: La fusion a été effectuée avec succès From 4661ba99323978759fa68895d862eb1a84289fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 14 Apr 2025 15:51:23 +0200 Subject: [PATCH 27/27] Fix translation key casing for 'First name' label Updated the translation key from 'firstName' to 'First name' in the details view template to ensure consistency with the expected translation format. This improves clarity and alignment with other labels. --- .../Resources/views/ThirdPartyDuplicate/_details.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig index e4b47b909..7b59c758d 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig @@ -3,7 +3,7 @@
    • {{ 'name'|trans }}: {{ thirdparty.name }}
    • -
    • {{ 'firstName'|trans }}: +
    • {{ 'First name'|trans }}: {% if thirdparty.firstname %}{{ thirdparty.firstname }}{% endif %}
    • {{ 'thirdparty.Civility'|trans }}: {% if thirdparty.getCivility %}{{ thirdparty.getCivility.name|localize_translatable_string }}{% endif %}