APIv4 - Add Contact::mergeDuplicates action
authorColeman Watts <coleman@civicrm.org>
Fri, 16 Sep 2022 13:34:29 +0000 (09:34 -0400)
committerColeman Watts <coleman@civicrm.org>
Fri, 16 Sep 2022 13:35:02 +0000 (09:35 -0400)
Civi/Api4/Action/Contact/GetDuplicates.php
Civi/Api4/Action/Contact/MergeDuplicates.php [new file with mode: 0644]
Civi/Api4/Contact.php
Civi/Test/Api3TestTrait.php
api/v3/Contact.php
tests/phpunit/api/v4/Action/ContactDuplicatesTest.php [moved from tests/phpunit/api/v4/Action/ContactGetDuplicatesTest.php with 77% similarity]

index 2986869de2af329a1ac0bc83a50ebce1e3ba945b..f39298c0034fb888dc4e63093390c75a19095ea5 100644 (file)
@@ -19,7 +19,7 @@ use Civi\Api4\Utils\FormattingUtil;
 
 /**
  * Get matching contacts based on a dedupe rule
- * @method setDedupeRule(string $dedupeRule)
+ * @method $this setDedupeRule(string $dedupeRule)
  * @method string getDedupeRule()
  */
 class GetDuplicates extends \Civi\Api4\Generic\DAOCreateAction {
diff --git a/Civi/Api4/Action/Contact/MergeDuplicates.php b/Civi/Api4/Action/Contact/MergeDuplicates.php
new file mode 100644 (file)
index 0000000..00b32ed
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\Contact;
+
+use Civi\Api4\Generic\BasicGetFieldsAction;
+use Civi\Api4\Generic\Result;
+
+/**
+ * Merge 2 contacts
+ *
+ * @method $this setMode(string $mode)
+ * @method string getMode()
+ * @method $this setContactId(int $contactId)
+ * @method int getContactId()
+ * @method $this setDuplicateId(int $duplicateId)
+ * @method int getDuplicateId()
+ */
+class MergeDuplicates extends \Civi\Api4\Generic\AbstractAction {
+
+  /**
+   * Main contact to keep with merged values
+   *
+   * @var int
+   * @required
+   */
+  protected $contactId;
+
+  /**
+   * Duplicate contact to delete
+   *
+   * @var int
+   * @required
+   */
+  protected $duplicateId;
+
+  /**
+   * Whether to run in "Safe Mode".
+   *
+   * Safe Mode skips the merge if there are conflicts. Does a force merge otherwise.
+   *
+   * @var string
+   * @required
+   * @options safe,aggressive
+   */
+  protected $mode = 'safe';
+
+  /**
+   * @param \Civi\Api4\Generic\Result $result
+   */
+  public function _run(Result $result) {
+    $merge = \CRM_Dedupe_Merger::merge(
+      [['srcID' => $this->duplicateId, 'dstID' => $this->contactId]],
+      [],
+      $this->mode,
+      FALSE,
+      $this->getCheckPermissions()
+    );
+    if ($merge) {
+      $result[] = $merge;
+    }
+    else {
+      throw new \CRM_Core_Exception('Merge failed');
+    }
+  }
+
+  /**
+   * @return array
+   */
+  public static function fields(BasicGetFieldsAction $action) {
+    return [];
+  }
+
+}
index 4b7db45231d0683f295e11971939754af166aa2c..b8ddca6aa0aa0959d0fec4f9e24e9ea9eee14c4a 100644 (file)
@@ -90,4 +90,13 @@ class Contact extends Generic\DAOEntity {
       ->setCheckPermissions($checkPermissions);
   }
 
+  /**
+   * @param bool $checkPermissions
+   * @return Action\Contact\MergeDuplicates
+   */
+  public static function mergeDuplicates($checkPermissions = TRUE) {
+    return (new Action\Contact\MergeDuplicates(__CLASS__, __FUNCTION__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
 }
index 57007fa4bcbf095210eae816e1fd2b9711053890..22e3058cfd4522bb997051d88184de8a9345cd24 100644 (file)
@@ -466,6 +466,12 @@ trait Api3TestTrait {
         }
         break;
 
+      case 'merge':
+        $v4Action = 'mergeDuplicates';
+        $v3Params['contact_id'] = $v3Params['to_keep_id'];
+        $v3Params['duplicate_id'] = $v3Params['to_remove_id'];
+        break;
+
       case 'getoptions':
         $indexBy = 0;
         $v4Action = 'getFields';
index 36d40f277eed30f2e075e44c64ee52ef983382eb..732f14be833ff9c12aa020199a7707ed62759354 100644 (file)
@@ -1150,8 +1150,8 @@ function _civicrm_api3_contact_deprecation() {
  *
  * @param array $params
  *   Allowed array keys are:
- *   -int main_id: main contact id with whom merge has to happen
- *   -int other_id: duplicate contact which would be deleted after merge operation
+ *   -int to_keep_id: main contact id with whom merge has to happen
+ *   -int to_remove_id: duplicate contact which would be deleted after merge operation
  *   -string mode: "safe" skips the merge if there are no conflicts. Does a force merge otherwise.
  *
  * @return array
similarity index 77%
rename from tests/phpunit/api/v4/Action/ContactGetDuplicatesTest.php
rename to tests/phpunit/api/v4/Action/ContactDuplicatesTest.php
index 977966a88b63ed1bac22018e1d5b071b00fb8f9c..5c2442ad7bfdd585cd0e40de0e4f13f3c0965871 100644 (file)
@@ -25,7 +25,7 @@ use Civi\Api4\Contact;
 /**
  * @group headless
  */
-class ContactGetDuplicatesTest extends CustomTestBase {
+class ContactDuplicatesTest extends CustomTestBase {
 
   public function testGetDuplicatesUnsupervised() {
     $email = uniqid('test@');
@@ -140,4 +140,43 @@ class ContactGetDuplicatesTest extends CustomTestBase {
     $this->assertContains($testContacts[4], $found);
   }
 
+  public function testMergeDuplicates():void {
+    $email = uniqid('test@');
+
+    $testContacts = $this->saveTestRecords('Contact', [
+      'records' => [['first_name' => 'Jo'], ['first_name' => 'Not']],
+      'defaults' => ['email_primary.email' => $email],
+    ])->column('id');
+
+    // Run merge in "safe mode" which will stop because of the name conflicts
+    $result = Contact::mergeDuplicates(FALSE)
+      ->setContactId($testContacts[0])
+      ->setDuplicateId($testContacts[1])
+      ->execute();
+
+    $this->assertCount(0, $result[0]['merged']);
+    $this->assertCount(1, $result[0]['skipped']);
+    $check = Contact::get(FALSE)
+      ->addWhere('is_deleted', '=', FALSE)
+      ->addWhere('id', 'IN', $testContacts)
+      ->execute();
+    $this->assertCount(2, $check);
+
+    // Run merge in "aggressive mode" which will overwrite the name conflicts
+    $result = Contact::mergeDuplicates(FALSE)
+      ->setContactId($testContacts[0])
+      ->setDuplicateId($testContacts[1])
+      ->setMode('aggressive')
+      ->execute();
+
+    $this->assertCount(1, $result[0]['merged']);
+    $this->assertCount(0, $result[0]['skipped']);
+    $check = Contact::get(FALSE)
+      ->addWhere('is_deleted', '=', FALSE)
+      ->addWhere('id', 'IN', $testContacts)
+      ->execute();
+    $this->assertCount(1, $check);
+    $this->assertEquals('Jo', $check[0]['first_name']);
+  }
+
 }