From: eileen Date: Mon, 3 Jun 2019 03:42:33 +0000 (+1200) Subject: Update get_merge_conflicts api X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=712ee28f835ef2b7df22648c87f0f1e18fcbf6d2;p=civicrm-core.git Update get_merge_conflicts api Increase result nuance in response to review --- diff --git a/Civi/Test/ContactTestTrait.php b/Civi/Test/ContactTestTrait.php index 66c25f29bb..c98e457b05 100644 --- a/Civi/Test/ContactTestTrait.php +++ b/Civi/Test/ContactTestTrait.php @@ -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); diff --git a/api/v3/Contact.php b/api/v3/Contact.php index e7bddf9f0c..e52a46093f 100644 --- a/api/v3/Contact.php +++ b/api/v3/Contact.php @@ -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'], ]; } diff --git a/tests/phpunit/CRMTraits/Custom/CustomDataTrait.php b/tests/phpunit/CRMTraits/Custom/CustomDataTrait.php index 3b5d9a197f..0dae46f4ba 100644 --- a/tests/phpunit/CRMTraits/Custom/CustomDataTrait.php +++ b/tests/phpunit/CRMTraits/Custom/CustomDataTrait.php @@ -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]; + } + } diff --git a/tests/phpunit/api/v3/ContactTest.php b/tests/phpunit/api/v3/ContactTest.php index 4decfb56f9..6c3e9c9b18 100644 --- a/tests/phpunit/api/v3/ContactTest.php +++ b/tests/phpunit/api/v3/ContactTest.php @@ -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() {