dev/core/#/233 Add apis to retrieve information about the ultimate destination contac...
authoreileen <emcnaughton@wikimedia.org>
Tue, 17 Jul 2018 04:38:31 +0000 (16:38 +1200)
committereileen <emcnaughton@wikimedia.org>
Tue, 17 Jul 2018 07:33:32 +0000 (19:33 +1200)
This 'data structure' is 'owned' by core & somewhat subject to future change so establishing a tested methodology for retrieving contact history in
core can support extensions such as
- extended reports - which exposes an address_history tab
- privacy extensions - which need to recover and potentially delete contacts that were merged
into the current contact

api/v3/Contact.php
tests/phpunit/api/v3/ContactTest.php

index 5d3ecc7af1cdd6145a74723f76bfab4f9572178f..f06c4046f7bbd082acaf8177529480fc6b023edb 100644 (file)
@@ -1162,6 +1162,149 @@ function _civicrm_api3_contact_merge_spec(&$params) {
   );
 }
 
+/**
+ * Get the ultimate contact a contact was merged to.
+ *
+ * @param array $params
+ *
+ * @return array
+ *   API Result Array
+ * @throws API_Exception
+ */
+function civicrm_api3_contact_getmergedto($params) {
+  $contactID = _civicrm_api3_contact_getmergedto($params);
+  if ($contactID) {
+    $values = [$contactID => ['id' => $contactID]];
+  }
+  else {
+    $values = [];
+  }
+  return civicrm_api3_create_success($values, $params);
+}
+
+/**
+ * Get the contact our contact was finally merged to.
+ *
+ * If the contact has been merged multiple times the crucial parent activity will have
+ * wound up on the ultimate contact so we can figure out the final resting place of the
+ * contact with only 2 activities even if 50 merges took place.
+ *
+ * @param array $params
+ *
+ * @return int|false
+ */
+function _civicrm_api3_contact_getmergedto($params) {
+  $contactID = FALSE;
+  $deleteActivity = civicrm_api3('ActivityContact', 'get', [
+    'contact_id' => $params['contact_id'],
+    'activity_id.activity_type_id' => 'Contact Deleted By Merge',
+    'is_deleted' => 0,
+    'is_test' => $params['is_test'],
+    'record_type_id' => 'Activity Targets',
+    'return' => ['activity_id.parent_id'],
+    'sequential' => 1,
+    'options' => [
+      'limit' => 1,
+      'sort' => 'activity_id.activity_date_time DESC'
+    ],
+  ])['values'];
+  if (!empty($deleteActivity)) {
+    $contactID = civicrm_api3('ActivityContact', 'getvalue', [
+      'activity_id' => $deleteActivity[0]['activity_id.parent_id'],
+      'record_type_id' => 'Activity Targets',
+      'return' => 'contact_id',
+    ]);
+  }
+  return $contactID;
+}
+
+/**
+ * Adjust metadata for contact_merge api function.
+ *
+ * @param array $params
+ */
+function _civicrm_api3_contact_getmergedto_spec(&$params) {
+  $params['contact_id'] = [
+    'title' => ts('ID of contact to find ultimate contact for'),
+    'type' => CRM_Utils_Type::T_INT,
+    'api.required' => TRUE,
+  ];
+  $params['is_test'] = [
+    'title' => ts('Get test deletions rather than live?'),
+    'type' => CRM_Utils_Type::T_BOOLEAN,
+    'api.default' => 0,
+  ];
+}
+
+/**
+ * Get the ultimate contact a contact was merged to.
+ *
+ * @param array $params
+ *
+ * @return array
+ *   API Result Array
+ * @throws API_Exception
+ */
+function civicrm_api3_contact_getmergedfrom($params) {
+  $contacts = _civicrm_api3_contact_getmergedfrom($params);
+  return civicrm_api3_create_success($contacts, $params);
+}
+
+/**
+ * Get all the contacts merged into our contact.
+ *
+ * @param array $params
+ *
+ * @return array
+ */
+function _civicrm_api3_contact_getmergedfrom($params) {
+  $activities = [];
+  $deleteActivities = civicrm_api3('ActivityContact', 'get', [
+    'contact_id' => $params['contact_id'],
+    'activity_id.activity_type_id' => 'Contact Merged',
+    'is_deleted' => 0,
+    'is_test' => $params['is_test'],
+    'record_type_id' => 'Activity Targets',
+    'return' => 'activity_id',
+  ])['values'];
+
+  foreach ($deleteActivities as $deleteActivity) {
+    $activities[] = $deleteActivity['activity_id'];
+  }
+  if (empty($activities)) {
+    return [];
+  }
+
+  $activityContacts = civicrm_api3('ActivityContact', 'get', [
+    'activity_id.parent_id' => ['IN' => $activities],
+    'record_type_id' => 'Activity Targets',
+    'return' => 'contact_id',
+  ])['values'];
+  $contacts = [];
+  foreach ($activityContacts as $activityContact) {
+    $contacts[$activityContact['contact_id']] = ['id' => $activityContact['contact_id']];
+  }
+  return $contacts;
+}
+
+/**
+ * Adjust metadata for contact_merge api function.
+ *
+ * @param array $params
+ */
+function _civicrm_api3_contact_getmergedfrom_spec(&$params) {
+  $params['contact_id'] = [
+    'title' => ts('ID of contact to find ultimate contact for'),
+    'type' => CRM_Utils_Type::T_INT,
+    'api.required' => TRUE,
+  ];
+  $params['is_test'] = [
+    'title' => ts('Get test deletions rather than live?'),
+    'type' => CRM_Utils_Type::T_BOOLEAN,
+    'api.default' => 0,
+  ];
+}
+
 /**
  * Adjust metadata for contact_proximity api function.
  *
index 78c378b4ec73869483332027ffe8827e5f20cf61..3e2e9b0c2622d6a785a499df23d16071d68fcafa 100644 (file)
@@ -256,7 +256,7 @@ class api_v3_ContactTest extends CiviUnitTestCase {
     ));
 
     // create a parent
-    $contact = $this->callAPISuccess('contact', 'create', array(
+    $this->callAPISuccess('contact', 'create', array(
       'email' => 'parent@example.com',
       'contact_type' => 'Individual',
     ));
@@ -3388,6 +3388,38 @@ class api_v3_ContactTest extends CiviUnitTestCase {
 
   }
 
+  /**
+   * Test retrieving merged contacts.
+   *
+   * The goal here is to start with a contact deleted by merged and find out the contact that is the current version of them.
+   */
+  public function testMergedGet() {
+    $this->contactIDs[] = $this->individualCreate();
+    $this->contactIDs[] = $this->individualCreate();
+    $this->contactIDs[] = $this->individualCreate();
+    $this->contactIDs[] = $this->individualCreate();
+
+    // First do an 'unnatural merge' - they 'like to merge into the lowest but this will mean that contact 0 merged to contact [3].
+    // When the batch merge runs.... the new lowest contact is contact[1]. All contacts will merge into that contact,
+    // including contact[3], resulting in only 3 existing at the end. For each contact the correct answer to 'who did I eventually
+    // wind up being should be [1]
+    $this->callAPISuccess('Contact', 'merge', ['to_remove_id' => $this->contactIDs[0], 'to_keep_id' => $this->contactIDs[3]]);
+
+    $this->callAPISuccess('Job', 'process_batch_merge', []);
+    foreach ($this->contactIDs as $contactID) {
+      if ($contactID === $this->contactIDs[1]) {
+        continue;
+      }
+      $result = $this->callAPIAndDocument('Contact', 'getmergedto', ['sequential' => 1, 'contact_id' => $contactID], __FUNCTION__, __FILE__);
+      $this->assertEquals(1, $result['count']);
+      $this->assertEquals($this->contactIDs[1], $result['values'][0]['id']);
+    }
+
+    $result = $this->callAPIAndDocument('Contact', 'getmergedfrom', ['contact_id' => $this->contactIDs[1]], __FUNCTION__, __FILE__)['values'];
+    $mergedContactIds = array_merge(array_diff($this->contactIDs, [$this->contactIDs[1]]));
+    $this->assertEquals($mergedContactIds, array_keys($result));
+  }
+
   /**
    * Test merging 2 contacts with delete to trash off.
    *