$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.')],
];
}
*
* @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' => [
$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);
}
$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'],
];
}
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[] = [
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];
+ }
+
}
/**
* 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() {