Update get_merge_conflicts api
authoreileen <emcnaughton@wikimedia.org>
Mon, 3 Jun 2019 03:42:33 +0000 (15:42 +1200)
committereileen <emcnaughton@wikimedia.org>
Thu, 6 Jun 2019 02:08:14 +0000 (14:08 +1200)
Increase result nuance in response to review

Civi/Test/ContactTestTrait.php
api/v3/Contact.php
tests/phpunit/CRMTraits/Custom/CustomDataTrait.php
tests/phpunit/api/v3/ContactTest.php

index 66c25f29bbfbd0fe63b6793423cedc64180db265..c98e457b05d76247544ae7461cbd6475572e0e6f 100644 (file)
@@ -71,6 +71,7 @@ trait ContactTestTrait {
    *
    * @return int
    *   id of Individual created
+   * @throws \Exception
    */
   public function individualCreate($params = array(), $seq = 0, $random = FALSE) {
     $params = array_merge($this->sampleContact('Individual', $seq, $random), $params);
index e7bddf9f0c006aca450d4a5c997b88b33850a410..e52a46093f92ae2a65da993bb9337aa9ea99f941 100644 (file)
@@ -1229,7 +1229,8 @@ function _civicrm_api3_contact_merge_spec(&$params) {
   $params['mode'] = [
     'title' => ts('Dedupe mode'),
     'description' => ts("In 'safe' mode conflicts will result in no merge. In 'aggressive' mode the merge will still proceed (hook dependent)"),
-    'api.default' => 'safe',
+    'api.default' => ['safe', 'aggressive'],
+    'options' => ['safe' => ts('Abort on unhandled conflict'), 'aggressive' => ts('Proceed on unhandled conflict. Note hooks may change handling here.')],
   ];
 }
 
@@ -1247,18 +1248,40 @@ function _civicrm_api3_contact_merge_spec(&$params) {
  *
  * @throws \CRM_Core_Exception
  * @throws \CiviCRM_API3_Exception
+ * @throws \API_Exception
  */
 function civicrm_api3_contact_get_merge_conflicts($params) {
   $migrationInfo = [];
-  $result = CRM_Dedupe_Merger::getConflicts(
-    $migrationInfo,
+  $contactFieldsToCompare = [];
+  $entitiesToCompare = [];
+  $return = [];
+  foreach ((array) $params['mode'] as $mode) {
+    $result[$mode] = CRM_Dedupe_Merger::getConflicts(
+      $migrationInfo,
       $params['to_remove_id'], $params['to_keep_id'],
       $params['mode']
-  );
-  $return = [];
-  foreach (array_keys($result) as $index) {
-    $return[str_replace('move_', '', $index)] = [];
+    );
+    $return = [];
+    foreach (array_keys($result[$mode]) as $index) {
+      if (substr($index, 0, 14) === 'move_location_') {
+        $parts = explode('_', $index);
+        $entity = $parts[2];
+        $locationTypeID = $migrationInfo['location_blocks'][$entity][$parts[3]]['locTypeId'];
+        $return[$mode]['conflicts'][$entity][] = ['location_type_id' => $locationTypeID];
+        $entitiesToCompare[$entity][] = $locationTypeID;
+      }
+      elseif (substr($index, 0, 5) === 'move_') {
+        $contactFieldsToCompare[] = str_replace('move_', '', $index);
+        $return[$mode]['conflicts']['contact'][0][str_replace('move_', '', $index)] = [];
+      }
+      else {
+        // Can't think of why this would be the case but perhaps it's ensuring it isn't as we
+        // refactor this.
+        throw new API_Exception(ts('Unknown parameter') . $index);
+      }
+    }
   }
+  // We get the contact & location details once, now we know what we need for both modes (if both being fetched).
   $contacts = civicrm_api3('Contact', 'get', [
     'id' => [
       'IN' => [
@@ -1266,11 +1289,50 @@ function civicrm_api3_contact_get_merge_conflicts($params) {
         $params['to_remove_id'],
       ],
     ],
-    'return' => array_keys($return),
+    'return' => $contactFieldsToCompare,
   ])['values'];
-  foreach (array_keys($return) as $fieldName) {
-    $return[$fieldName][$params['to_keep_id']] = CRM_Utils_Array::value($fieldName, $contacts[$params['to_keep_id']]);
-    $return[$fieldName][$params['to_remove_id']] = CRM_Utils_Array::value($fieldName, $contacts[$params['to_remove_id']]);
+  foreach ($contactFieldsToCompare as $fieldName) {
+    foreach ((array) $params['mode'] as $mode) {
+      if (isset($return[$mode]['conflicts']['contact'][0][$fieldName])) {
+        $return[$mode]['conflicts']['contact'][0][$fieldName][$params['to_keep_id']] = CRM_Utils_Array::value($fieldName, $contacts[$params['to_keep_id']]);
+        $return[$mode]['conflicts']['contact'][0][$fieldName][$params['to_remove_id']] = CRM_Utils_Array::value($fieldName, $contacts[$params['to_remove_id']]);
+      }
+    }
+  }
+  foreach ($entitiesToCompare as $entity => $locations) {
+    $contactLocationDetails = civicrm_api3($entity, 'get', [
+      'contact_id' => ['IN' => [$params['to_keep_id'], $params['to_remove_id']]],
+      'location_type_id' => ['IN' => $locations],
+    ])['values'];
+    $detailsByLocation = [];
+    foreach ($contactLocationDetails as $locationDetail) {
+      if ((int) $locationDetail['contact_id'] === $params['to_keep_id']) {
+        $detailsByLocation[$locationDetail['location_type_id']]['to_keep'] = $locationDetail;
+      }
+      elseif ((int) $locationDetail['contact_id'] === $params['to_remove_id']) {
+        $detailsByLocation[$locationDetail['location_type_id']]['to_remove'] = $locationDetail;
+      }
+      else {
+        // Can't think of why this would be the case but perhaps it's ensuring it isn't as we
+        // refactor this.
+        throw new API_Exception(ts('Unknown parameter') . $index);
+      }
+    }
+    foreach ((array) $params['mode'] as $mode) {
+      foreach ($return[$mode]['conflicts'][$entity] as $index => $entityData) {
+        $locationTypeID = $entityData['location_type_id'];
+        foreach ($detailsByLocation[$locationTypeID]['to_keep'] as $fieldName => $keepContactValue) {
+          $fieldsToIgnore = ['id', 'contact_id', 'is_primary', 'is_billing', 'manual_geo_code', 'contact_id', 'reset_date', 'hold_date'];
+          if (in_array($fieldName, $fieldsToIgnore)) {
+            continue;
+          }
+          $otherContactValue = $detailsByLocation[$locationTypeID]['to_remove'][$fieldName];
+          if (!empty($keepContactValue) && !empty($otherContactValue) && $keepContactValue !== $otherContactValue) {
+            $return[$mode]['conflicts'][$entity][$index][$fieldName] = [$params['to_keep_id'] => $keepContactValue, $params['to_remove_id'] => $otherContactValue];
+          }
+        }
+      }
+    }
   }
   return civicrm_api3_create_success($return, $params);
 }
@@ -1294,7 +1356,7 @@ function _civicrm_api3_contact_get_merge_conflicts_spec(&$params) {
   $params['mode'] = [
     'title' => ts('Dedupe mode'),
     'description' => ts("'safe' or 'aggressive'  - these modes map to the merge actions & may affect resolution done by hooks "),
-    'api.default' => 'safe',
+    'api.default' => ['safe'],
   ];
 }
 
index 3b5d9a197fe1c45906d92822e82dc428daa83b93..0dae46f4bac82e100bf9cf399a32ebd06f2a1879 100644 (file)
@@ -61,23 +61,31 @@ trait CRMTraits_Custom_CustomDataTrait {
     return $this->ids['CustomGroup'][$params['title']];
   }
 
+  /**
+   * Create a custom group with a single field.
+   *
+   * @param array $groupParams
+   * @param string $customFieldType
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function createCustomGroupWithFieldOfType($groupParams = [], $customFieldType = 'text') {
+    if ($customFieldType !== 'text') {
+      throw new CRM_Core_Exception('we have not yet extracted other custom field types from createCustomFieldsOfAllTypes, Use consistent syntax when you do');
+    }
+    $groupParams['title'] = empty($groupParams['title']) ? 'Group with field ' . $customFieldType : $groupParams['title'];
+    $this->createCustomGroup($groupParams);
+    $customField = $this->createTextCustomField(['custom_group_id' => $this->ids['CustomGroup'][$groupParams['title']]]);
+    $this->ids['CustomField'][$customFieldType] = $customField['id'];
+  }
+
   /**
    * @return array
    */
   public function createCustomFieldsOfAllTypes() {
     $customGroupID = $this->ids['CustomGroup']['Custom Group'];
     $ids = [];
-    $params = [
-      'custom_group_id' => $customGroupID,
-      'label' => 'Enter text here',
-      'html_type' => 'Text',
-      'data_type' => 'String',
-      'default_value' => 'xyz',
-      'weight' => 1,
-      'is_required' => 1,
-    ];
-
-    $customField = $this->callAPISuccess('CustomField', 'create', $params);
+    $customField = $this->createTextCustomField(['custom_group_id' => $customGroupID]);
     $ids['text'] = $customField['id'];
 
     $optionValue[] = [
@@ -180,4 +188,26 @@ trait CRMTraits_Custom_CustomDataTrait {
     return $linkField;
   }
 
+  /**
+   * Create a custom text fields.
+   *
+   * @param array $params
+   *   Parameter overrides, must include custom_group_id.
+   *
+   * @return array
+   */
+  protected function createTextCustomField($params = []) {
+    $params = array_merge([
+      'label' => 'Enter text here',
+      'html_type' => 'Text',
+      'data_type' => 'String',
+      'default_value' => 'xyz',
+      'weight' => 1,
+      'is_required' => 1,
+      'sequential' => 1,
+    ], $params);
+
+    return $this->callAPISuccess('CustomField', 'create', $params)['values'][0];
+  }
+
 }
index 4decfb56f9f5c735dccdabb889ac35e92445658d..6c3e9c9b18c56bc89cf1ccf792d6c85f6fa185f7 100644 (file)
@@ -1652,12 +1652,64 @@ class api_v3_ContactTest extends CiviUnitTestCase {
 
   /**
    * Test the function that determines if 2 contacts have conflicts.
+   *
+   * @throws \Exception
    */
   public function testMergeGetConflicts() {
-    $contact1 = $this->individualCreate();
-    $contact2 = $this->individualCreate(['first_name' => 'different']);
+    $this->createCustomGroupWithFieldOfType();
+    $contact1 = $this->individualCreate([
+      'email' => 'bob@example.com',
+      'api.address.create' => ['location_type_id' => 'work', 'street_address' => 'big office', 'city' => 'small city'],
+      'api.address.create.2' => ['location_type_id' => 'home', 'street_address' => 'big house', 'city' => 'small city'],
+      'external_identifier' => 'unique and special',
+      $this->getCustomFieldName('text') => 'mummy loves me',
+    ]);
+    $contact2 = $this->individualCreate([
+      'first_name' => 'different',
+      'api.address.create.1' => ['location_type_id' => 'home', 'street_address' => 'medium house', 'city' => 'small city'],
+      'api.address.create.2' => ['location_type_id' => 'work', 'street_address' => 'medium office', 'city' => 'small city'],
+      'external_identifier' => 'uniquer and specialler',
+      $this->getCustomFieldName('text') => 'mummy loves me more',
+    ]);
     $conflicts = $this->callAPISuccess('Contact', 'get_merge_conflicts', ['to_keep_id' => $contact1, 'to_remove_id' => $contact2])['values'];
-    $this->assertEquals(['first_name' => [$contact1 => 'Anthony', $contact2 => 'different']], $conflicts);
+    $this->assertEquals([
+      'safe' => [
+        'conflicts' => [
+          'contact' => [
+            [
+              'first_name' => [$contact1 => 'Anthony', $contact2 => 'different'],
+              'external_identifier' => [$contact1 => 'unique and special', $contact2 => 'uniquer and specialler'],
+              $this->getCustomFieldName('text') => [$contact1 => 'mummy loves me', $contact2 => 'mummy loves me more'],
+            ],
+          ],
+          'address' => [
+            [
+              'location_type_id' => '1',
+              'street_address' => [
+                $contact1 => 'big house',
+                $contact2 => 'medium house',
+              ],
+            ],
+            [
+              'location_type_id' => '2',
+              'street_address' => [
+                $contact1 => 'big office',
+                $contact2 => 'medium office',
+              ],
+            ],
+          ],
+          'email' => [
+            [
+              'location_type_id' => '1',
+              'email' => [
+                $contact1 => 'bob@example.com',
+                $contact2 => 'anthony_anderson@civicrm.org',
+              ],
+            ],
+          ],
+        ],
+      ],
+    ], $conflicts);
   }
 
   private function createEmployerOfMembership() {