4 * Class CRM_Dedupe_DedupeMergerTest
8 class CRM_Dedupe_MergerTest
extends CiviUnitTestCase
{
12 protected $_contactIds = [];
15 * Contacts created for the test.
17 * Overlaps contactIds....
21 protected $contacts = [];
28 public function tearDown() {
31 'civicrm_group_contact',
33 'civicrm_prevnext_cache',
38 public function createDupeContacts() {
39 // create a group to hold contacts, so that dupe checks don't consider any other contacts in the DB
41 'name' => 'Test Dupe Merger Group',
42 'title' => 'Test Dupe Merger Group',
45 'visibility' => 'Public Pages',
48 $result = $this->callAPISuccess('group', 'create', $params);
49 $this->_groupId
= $result['id'];
53 // make dupe checks based on based on following contact sets:
54 // FIRST - LAST - EMAIL
55 // ---------------------------------
56 // robin - hood - robin@example.com
57 // robin - hood - robin@example.com
58 // robin - hood - hood@example.com
59 // robin - dale - robin@example.com
60 // little - dale - dale@example.com
61 // little - dale - dale@example.com
62 // will - dale - dale@example.com
63 // will - dale - will@example.com
64 // will - dale - will@example.com
67 'first_name' => 'robin',
68 'last_name' => 'hood',
69 'email' => 'robin@example.com',
70 'contact_type' => 'Individual',
73 'first_name' => 'robin',
74 'last_name' => 'hood',
75 'email' => 'robin@example.com',
76 'contact_type' => 'Individual',
79 'first_name' => 'robin',
80 'last_name' => 'hood',
81 'email' => 'hood@example.com',
82 'contact_type' => 'Individual',
85 'first_name' => 'robin',
86 'last_name' => 'dale',
87 'email' => 'robin@example.com',
88 'contact_type' => 'Individual',
91 'first_name' => 'little',
92 'last_name' => 'dale',
93 'email' => 'dale@example.com',
94 'contact_type' => 'Individual',
97 'first_name' => 'little',
98 'last_name' => 'dale',
99 'email' => 'dale@example.com',
100 'contact_type' => 'Individual',
103 'first_name' => 'will',
104 'last_name' => 'dale',
105 'email' => 'dale@example.com',
106 'contact_type' => 'Individual',
109 'first_name' => 'will',
110 'last_name' => 'dale',
111 'email' => 'will@example.com',
112 'contact_type' => 'Individual',
115 'first_name' => 'will',
116 'last_name' => 'dale',
117 'email' => 'will@example.com',
118 'contact_type' => 'Individual',
123 foreach ($params as $param) {
124 $param['version'] = 3;
125 $contact = civicrm_api('contact', 'create', $param);
126 $this->_contactIds
[$count++
] = $contact['id'];
129 'contact_id' => $contact['id'],
130 'group_id' => $this->_groupId
,
133 $this->callAPISuccess('group_contact', 'create', $grpParams);
138 * Delete all created contacts.
140 public function deleteDupeContacts() {
141 foreach ($this->_contactIds
as $contactId) {
142 $this->contactDelete($contactId);
144 $this->groupDelete($this->_groupId
);
148 * Test the batch merge.
150 public function testBatchMergeSelectedDuplicates() {
151 $this->createDupeContacts();
153 // verify that all contacts have been created separately
154 $this->assertEquals(count($this->_contactIds
), 9, 'Check for number of contacts.');
156 $dao = new CRM_Dedupe_DAO_RuleGroup();
157 $dao->contact_type
= 'Individual';
158 $dao->name
= 'IndividualSupervised';
159 $dao->is_default
= 1;
162 $foundDupes = CRM_Dedupe_Finder
::dupesInGroup($dao->id
, $this->_groupId
);
164 // -------------------------------------------------------------------------
165 // Name and Email (reserved) Matches ( 3 pairs )
166 // --------------------------------------------------------------------------
167 // robin - hood - robin@example.com
168 // robin - hood - robin@example.com
169 // little - dale - dale@example.com
170 // little - dale - dale@example.com
171 // will - dale - will@example.com
172 // will - dale - will@example.com
173 // so 3 pairs for - first + last + mail
174 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
176 // Run dedupe finder as the browser would
177 //avoid invalid key error
178 $_SERVER['REQUEST_METHOD'] = 'GET';
179 $object = new CRM_Contact_Page_DedupeFind();
180 $object->set('gid', $this->_groupId
);
181 $object->set('rgid', $dao->id
);
182 $object->set('action', CRM_Core_Action
::UPDATE
);
183 $object->setEmbedded(TRUE);
186 // Retrieve pairs from prev next cache table
187 $select = ['pn.is_selected' => 'is_selected'];
188 $cacheKeyString = CRM_Dedupe_Merger
::getMergeCacheKeyString($dao->id
, $this->_groupId
, [], TRUE, 0);
189 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
190 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
192 // mark first two pairs as selected
193 CRM_Core_DAO
::singleValueQuery("UPDATE civicrm_prevnext_cache SET is_selected = 1 WHERE id IN ({$pnDupePairs[0]['prevnext_id']}, {$pnDupePairs[1]['prevnext_id']})");
195 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
196 $this->assertEquals($pnDupePairs[0]['is_selected'], 1, 'Check if first record in dupe pairs is marked as selected.');
197 $this->assertEquals($pnDupePairs[0]['is_selected'], 1, 'Check if second record in dupe pairs is marked as selected.');
199 // batch merge selected dupes
200 $result = CRM_Dedupe_Merger
::batchMerge($dao->id
, $this->_groupId
, 'safe', 5, 1);
201 $this->assertEquals(count($result['merged']), 2, 'Check number of merged pairs.');
203 $stats = $this->callAPISuccess('Dedupe', 'getstatistics', [
204 'group_id' => $this->_groupId
,
205 'rule_group_id' => $dao->id
,
206 'check_permissions' => TRUE,
208 $this->assertEquals(['merged' => 2, 'skipped' => 0], $stats);
210 // retrieve pairs from prev next cache table
211 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
212 $this->assertEquals(count($pnDupePairs), 1, 'Check number of remaining dupe pairs in prev next cache.');
214 $this->deleteDupeContacts();
218 * Test the batch merge.
220 public function testBatchMergeAllDuplicates() {
221 $this->createDupeContacts();
223 // verify that all contacts have been created separately
224 $this->assertEquals(count($this->_contactIds
), 9, 'Check for number of contacts.');
226 $dao = new CRM_Dedupe_DAO_RuleGroup();
227 $dao->contact_type
= 'Individual';
228 $dao->name
= 'IndividualSupervised';
229 $dao->is_default
= 1;
232 $foundDupes = CRM_Dedupe_Finder
::dupesInGroup($dao->id
, $this->_groupId
);
234 // -------------------------------------------------------------------------
235 // Name and Email (reserved) Matches ( 3 pairs )
236 // --------------------------------------------------------------------------
237 // robin - hood - robin@example.com
238 // robin - hood - robin@example.com
239 // little - dale - dale@example.com
240 // little - dale - dale@example.com
241 // will - dale - will@example.com
242 // will - dale - will@example.com
243 // so 3 pairs for - first + last + mail
244 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
246 // Run dedupe finder as the browser would
247 //avoid invalid key error
248 $_SERVER['REQUEST_METHOD'] = 'GET';
249 $object = new CRM_Contact_Page_DedupeFind();
250 $object->set('gid', $this->_groupId
);
251 $object->set('rgid', $dao->id
);
252 $object->set('action', CRM_Core_Action
::UPDATE
);
253 $object->setEmbedded(TRUE);
256 // Retrieve pairs from prev next cache table
257 $select = ['pn.is_selected' => 'is_selected'];
258 $cacheKeyString = CRM_Dedupe_Merger
::getMergeCacheKeyString($dao->id
, $this->_groupId
, [], TRUE, 0);
259 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
261 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
263 // batch merge all dupes
264 $result = CRM_Dedupe_Merger
::batchMerge($dao->id
, $this->_groupId
, 'safe', 5, 2);
265 $this->assertEquals(count($result['merged']), 3, 'Check number of merged pairs.');
267 $stats = $this->callAPISuccess('Dedupe', 'getstatistics', [
268 'rule_group_id' => $dao->id
,
269 'group_id' => $this->_groupId
,
271 // retrieve pairs from prev next cache table
272 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
273 $this->assertEquals(count($pnDupePairs), 0, 'Check number of remaining dupe pairs in prev next cache.');
275 $this->deleteDupeContacts();
279 * The goal of this function is to test that all required tables are returned.
281 public function testGetCidRefs() {
282 $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__
, 'Contacts');
283 $this->assertEquals(array_merge($this->getStaticCIDRefs(), $this->getHackedInCIDRef()), CRM_Dedupe_Merger
::cidRefs());
284 $this->assertEquals(array_merge($this->getCalculatedCIDRefs(), $this->getHackedInCIDRef()), CRM_Dedupe_Merger
::cidRefs());
288 * Get the list of not-really-cid-refs that are currently hacked in.
290 * This is hacked into getCIDs function.
294 public function getHackedInCIDRef() {
296 'civicrm_entity_tag' => [
303 * Test function that gets duplicate pairs.
305 * It turns out there are 2 code paths retrieving this data so my initial
306 * focus is on ensuring they match.
308 public function testGetMatches() {
309 $this->setupMatchData();
311 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
312 'rule_group_id' => 1,
314 $this->assertEquals([
316 'srcID' => $this->contacts
[1]['id'],
317 'srcName' => 'Mr. Mickey Mouse II',
318 'dstID' => $this->contacts
[0]['id'],
319 'dstName' => 'Mr. Mickey Mouse II',
324 'srcID' => $this->contacts
[3]['id'],
325 'srcName' => 'Mr. Minnie Mouse II',
326 'dstID' => $this->contacts
[2]['id'],
327 'dstName' => 'Mr. Minnie Mouse II',
335 * Test results are returned when criteria are passed in.
337 public function testGetMatchesCriteriaMatched() {
338 $this->setupMatchData();
339 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
340 'rule_group_id' => 1,
341 'criteria' => ['contact' => ['id' => ['>' => 1]]],
343 $this->assertCount(2, $pairs);
347 * Test results are returned when criteria are passed in & limit is respected.
349 public function testGetMatchesCriteriaMatchedWithLimit() {
350 $this->setupMatchData();
351 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
352 'rule_group_id' => 1,
353 'criteria' => ['contact' => ['id' => ['>' => 1]]],
354 'options' => ['limit' => 1],
356 $this->assertCount(1, $pairs);
360 * Test results are returned when criteria are passed in & limit is respected.
362 public function testGetMatchesCriteriaMatchedWithSearchLimit() {
363 $this->setupMatchData();
364 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
365 'rule_group_id' => 1,
366 'criteria' => ['contact' => ['id' => ['>' => 1]]],
369 $this->assertCount(1, $pairs);
373 * Test getting matches where there are no criteria.
375 public function testGetMatchesNoCriteria() {
376 $this->setupMatchData();
377 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
378 'rule_group_id' => 1,
380 $this->assertCount(2, $pairs);
384 * Test getting matches with a limit in play.
386 public function testGetMatchesNoCriteriaButLimit() {
387 $this->setupMatchData();
388 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
389 'rule_group_id' => 1,
390 'options' => ['limit' => 1],
392 $this->assertCount(1, $pairs);
396 * Test that if criteria are passed and there are no matching contacts no matches are returned.
398 public function testGetMatchesCriteriaNotMatched() {
399 $this->setupMatchData();
400 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
401 'rule_group_id' => 1,
402 'criteria' => ['contact' => ['id' => ['>' => 100000]]],
404 $this->assertCount(0, $pairs);
408 * Test function that gets organization pairs.
410 * Note the rule will match on organization_name OR email - hence lots of
415 public function testGetOrganizationMatches() {
416 $this->setupMatchData();
417 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
418 'contact_type' => 'Organization',
419 'used' => 'Supervised',
422 $pairs = CRM_Dedupe_Merger
::getDuplicatePairs(
432 'srcID' => $this->contacts
[5]['id'],
433 'srcName' => 'Walt Disney Ltd',
434 'dstID' => $this->contacts
[4]['id'],
435 'dstName' => 'Walt Disney Ltd',
440 'srcID' => $this->contacts
[7]['id'],
441 'srcName' => 'Walt Disney',
442 'dstID' => $this->contacts
[6]['id'],
443 'dstName' => 'Walt Disney',
448 'srcID' => $this->contacts
[6]['id'],
449 'srcName' => 'Walt Disney',
450 'dstID' => $this->contacts
[4]['id'],
451 'dstName' => 'Walt Disney Ltd',
456 'srcID' => $this->contacts
[6]['id'],
457 'srcName' => 'Walt Disney',
458 'dstID' => $this->contacts
[5]['id'],
459 'dstName' => 'Walt Disney Ltd',
464 usort($pairs, [__CLASS__
, 'compareDupes']);
465 usort($expectedPairs, [__CLASS__
, 'compareDupes']);
466 $this->assertEquals($expectedPairs, $pairs);
470 * Function to sort $duplicate records in a stable way.
477 public static function compareDupes($a, $b) {
478 foreach (['srcName', 'dstName', 'srcID', 'dstID'] as $field) {
479 if ($a[$field] != $b[$field]) {
480 return ($a[$field] < $b[$field]) ?
1 : -1;
487 * Test function that gets organization duplicate pairs.
491 public function testGetOrganizationMatchesInGroup() {
492 $this->setupMatchData();
493 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
494 'contact_type' => 'Organization',
495 'used' => 'Supervised',
498 $groupID = $this->groupCreate(['title' => 'she-mice']);
500 $this->callAPISuccess('GroupContact', 'create', [
501 'group_id' => $groupID,
502 'contact_id' => $this->contacts
[4]['id'],
505 $pairs = CRM_Dedupe_Merger
::getDuplicatePairs(
513 $this->assertEquals([
515 'srcID' => $this->contacts
[5]['id'],
516 'srcName' => 'Walt Disney Ltd',
517 'dstID' => $this->contacts
[4]['id'],
518 'dstName' => 'Walt Disney Ltd',
523 'srcID' => $this->contacts
[6]['id'],
524 'srcName' => 'Walt Disney',
525 'dstID' => $this->contacts
[4]['id'],
526 'dstName' => 'Walt Disney Ltd',
532 $this->callAPISuccess('GroupContact', 'create', [
533 'group_id' => $groupID,
534 'contact_id' => $this->contacts
[5]['id'],
536 CRM_Core_DAO
::executeQuery("DELETE FROM civicrm_prevnext_cache");
537 $pairs = CRM_Dedupe_Merger
::getDuplicatePairs(
545 $this->assertEquals([
547 'srcID' => $this->contacts
[5]['id'],
548 'srcName' => 'Walt Disney Ltd',
549 'dstID' => $this->contacts
[4]['id'],
550 'dstName' => 'Walt Disney Ltd',
555 'srcID' => $this->contacts
[6]['id'],
556 'srcName' => 'Walt Disney',
557 'dstID' => $this->contacts
[4]['id'],
558 'dstName' => 'Walt Disney Ltd',
563 'srcID' => $this->contacts
[6]['id'],
564 'srcName' => 'Walt Disney',
565 'dstID' => $this->contacts
[5]['id'],
566 'dstName' => 'Walt Disney Ltd',
574 * Test function that gets duplicate pairs.
576 * It turns out there are 2 code paths retrieving this data so my initial
577 * focus is on ensuring they match.
579 public function testGetMatchesInGroup() {
580 $this->setupMatchData();
582 $groupID = $this->groupCreate(['title' => 'she-mice']);
584 $this->callAPISuccess('GroupContact', 'create', [
585 'group_id' => $groupID,
586 'contact_id' => $this->contacts
[3]['id'],
589 $pairs = CRM_Dedupe_Merger
::getDuplicatePairs(
597 $this->assertEquals([
599 'srcID' => $this->contacts
[3]['id'],
600 'srcName' => 'Mr. Minnie Mouse II',
601 'dstID' => $this->contacts
[2]['id'],
602 'dstName' => 'Mr. Minnie Mouse II',
610 * Test the special info handling is unchanged after cleanup.
612 * Note the handling is silly - we are testing to lock in over short term
613 * changes not to imply any contract on the function.
615 public function testGetRowsElementsAndInfoSpecialInfo() {
616 $contact1 = $this->individualCreate([
617 'preferred_communication_method' => [],
618 'communication_style_id' => 'Familiar',
619 'prefix_id' => 'Mrs.',
620 'suffix_id' => 'III',
622 $contact2 = $this->individualCreate([
623 'preferred_communication_method' => [
627 'communication_style_id' => 'Formal',
628 'gender_id' => 'Female',
630 $rowsElementsAndInfo = CRM_Dedupe_Merger
::getRowsElementsAndInfo($contact1, $contact2);
631 $rows = $rowsElementsAndInfo['rows'];
632 $this->assertEquals([
635 'title' => 'Individual Prefix',
636 ], $rows['move_prefix_id']);
637 $this->assertEquals([
640 'title' => 'Individual Suffix',
641 ], $rows['move_suffix_id']);
642 $this->assertEquals([
646 ], $rows['move_gender_id']);
647 $this->assertEquals([
648 'main' => 'Familiar',
650 'title' => 'Communication Style',
651 ], $rows['move_communication_style_id']);
652 $this->assertEquals(1, $rowsElementsAndInfo['migration_info']['move_communication_style_id']);
653 $this->assertEquals([
655 'other' => 'SMS, Fax',
656 'title' => 'Preferred Communication Method',
657 ], $rows['move_preferred_communication_method']);
658 $this->assertEquals('\ 14\ 15\ 1', $rowsElementsAndInfo['migration_info']['move_preferred_communication_method']);
662 * Test migration of Membership.
664 public function testMergeMembership() {
666 $this->setupMatchData();
667 $originalContactID = $this->contacts
[0]['id'];
668 $duplicateContactID = $this->contacts
[1]['id'];
670 //Add Membership for the duplicate contact.
671 $memTypeId = $this->membershipTypeCreate();
672 $this->callAPISuccess('Membership', 'create', [
673 'membership_type_id' => $memTypeId,
674 'contact_id' => $duplicateContactID,
676 //Assert if 'add new' checkbox is enabled on the merge form.
677 $rowsElementsAndInfo = CRM_Dedupe_Merger
::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
678 foreach ($rowsElementsAndInfo['elements'] as $element) {
679 if (!empty($element[3]) && $element[3] == 'add new') {
680 $checkedAttr = ['checked' => 'checked'];
681 $this->checkArrayEquals($element[4], $checkedAttr);
685 //Merge and move the mem to the main contact.
686 $this->mergeContacts($originalContactID, $duplicateContactID, [
687 'move_rel_table_memberships' => 1,
688 'operation' => ['move_rel_table_memberships' => ['add' => 1]],
691 //Check if membership is correctly transferred to original contact.
692 $originalContactMembership = $this->callAPISuccess('Membership', 'get', [
693 'membership_type_id' => $memTypeId,
694 'contact_id' => $originalContactID,
696 $this->assertEquals(1, $originalContactMembership['count']);
700 * CRM-19653 : Test that custom field data should/shouldn't be overriden on
701 * selecting/not selecting option to migrate data respectively
703 public function testCustomDataOverwrite() {
704 // Create Custom Field
705 $createGroup = $this->setupCustomGroupForIndividual();
706 $createField = $this->setupCustomField('Graduation', $createGroup);
707 $customFieldName = "custom_" . $createField['id'];
710 $this->setupMatchData();
712 $originalContactID = $this->contacts
[0]['id'];
713 // used as duplicate contact in 1st use-case
714 $duplicateContactID1 = $this->contacts
[1]['id'];
715 // used as duplicate contact in 2nd use-case
716 $duplicateContactID2 = $this->contacts
[2]['id'];
718 // update the text custom field for original contact with value 'abc'
719 $this->callAPISuccess('Contact', 'create', [
720 'id' => $originalContactID,
721 "{$customFieldName}" => 'abc',
723 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
725 // update the text custom field for duplicate contact 1 with value 'def'
726 $this->callAPISuccess('Contact', 'create', [
727 'id' => $duplicateContactID1,
728 "{$customFieldName}" => 'def',
730 $this->assertCustomFieldValue($duplicateContactID1, 'def', $customFieldName);
732 // update the text custom field for duplicate contact 2 with value 'ghi'
733 $this->callAPISuccess('Contact', 'create', [
734 'id' => $duplicateContactID2,
735 "{$customFieldName}" => 'ghi',
737 $this->assertCustomFieldValue($duplicateContactID2, 'ghi', $customFieldName);
739 /*** USE-CASE 1: DO NOT OVERWRITE CUSTOM FIELD VALUE **/
740 $this->mergeContacts($originalContactID, $duplicateContactID1, [
741 "move_{$customFieldName}" => NULL,
743 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
745 /*** USE-CASE 2: OVERWRITE CUSTOM FIELD VALUE **/
746 $this->mergeContacts($originalContactID, $duplicateContactID2, [
747 "move_{$customFieldName}" => 'ghi',
749 $this->assertCustomFieldValue($originalContactID, 'ghi', $customFieldName);
751 // cleanup created custom set
752 $this->callAPISuccess('CustomField', 'delete', ['id' => $createField['id']]);
753 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
757 * Creatd Date merge cases
760 public function createdDateMergeCases() {
762 // Normal pattern merge into the lower id
764 // Check if we flipped the contacts that it still does right thing
770 * dev/core#996 Ensure that the oldest created date is retained even if duplicates have been flipped
771 * @dataProvider createdDateMergeCases
773 public function testCreatedDatePostMerge($keepContactKey, $duplicateContactKey) {
774 $this->setupMatchData();
775 $lowerContactCreatedDate = $this->callAPISuccess('Contact', 'getsingle', [
776 'id' => $this->contacts
[0]['id'],
777 'return' => ['created_date'],
779 // Assume contats have been flipped in the UL so merging into the higher id
780 $this->mergeContacts($this->contacts
[$keepContactKey]['id'], $this->contacts
[$duplicateContactKey]['id'], []);
781 $this->assertEquals($lowerContactCreatedDate, $this->callAPISuccess('Contact', 'getsingle', ['id' => $this->contacts
[$keepContactKey]['id'], 'return' => ['created_date']])['created_date']);
785 * Verifies that when a contact with a custom field value is merged into a
786 * contact without a record int its corresponding custom group table, and none
787 * of the custom fields of that custom table are selected, the value is not
790 public function testMigrationOfUnselectedCustomDataOnEmptyCustomRecord() {
791 // Create Custom Fields
792 $createGroup = $this->setupCustomGroupForIndividual();
793 $customField1 = $this->setupCustomField('TestField', $createGroup);
795 // Create multi-value custom field
796 $multiGroup = $this->CustomGroupMultipleCreateByParams();
797 $multiField = $this->customFieldCreate([
798 'custom_group_id' => $multiGroup['id'],
799 'label' => 'field_1' . $multiGroup['id'],
804 $this->setupMatchData();
805 $originalContactID = $this->contacts
[0]['id'];
806 $duplicateContactID = $this->contacts
[1]['id'];
808 // Update the text custom fields for duplicate contact
809 $this->callAPISuccess('Contact', 'create', [
810 'id' => $duplicateContactID,
811 "custom_{$customField1['id']}" => 'abc',
812 "custom_{$multiField['id']}" => 'def',
814 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
815 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$multiField['id']}");
817 // Merge, and ensure that no value was migrated
818 $this->mergeContacts($originalContactID, $duplicateContactID, [
819 "move_custom_{$customField1['id']}" => NULL,
820 "move_rel_table_custom_{$multiGroup['id']}" => NULL,
822 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
823 $this->assertCustomFieldValue($originalContactID, '', "custom_{$multiField['id']}");
825 // cleanup created custom set
826 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField1['id']]);
827 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
828 $this->callAPISuccess('CustomField', 'delete', ['id' => $multiField['id']]);
829 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]);
833 * Tests that if only part of the custom fields of a custom group are selected
834 * for a merge, only those values are merged, while all other fields of the
835 * custom group retain their original value, specifically for a contact with
836 * no records on the custom group table.
838 public function testMigrationOfSomeCustomDataOnEmptyCustomRecord() {
839 // Create Custom Fields
840 $createGroup = $this->setupCustomGroupForIndividual();
841 $customField1 = $this->setupCustomField('Test1', $createGroup);
842 $customField2 = $this->setupCustomField('Test2', $createGroup);
844 // Create multi-value custom field
845 $multiGroup = $this->CustomGroupMultipleCreateByParams();
846 $multiField = $this->customFieldCreate([
847 'custom_group_id' => $multiGroup['id'],
848 'label' => 'field_1' . $multiGroup['id'],
853 $this->setupMatchData();
854 $originalContactID = $this->contacts
[0]['id'];
855 $duplicateContactID = $this->contacts
[1]['id'];
857 // Update the text custom fields for duplicate contact
858 $this->callAPISuccess('Contact', 'create', [
859 'id' => $duplicateContactID,
860 "custom_{$customField1['id']}" => 'abc',
861 "custom_{$customField2['id']}" => 'def',
862 "custom_{$multiField['id']}" => 'ghi',
864 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
865 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$customField2['id']}");
866 $this->assertCustomFieldValue($duplicateContactID, 'ghi', "custom_{$multiField['id']}");
869 $this->mergeContacts($originalContactID, $duplicateContactID, [
870 "move_custom_{$customField1['id']}" => NULL,
871 "move_custom_{$customField2['id']}" => 'def',
872 "move_rel_table_custom_{$multiGroup['id']}" => '1',
874 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
875 $this->assertCustomFieldValue($originalContactID, 'def', "custom_{$customField2['id']}");
876 $this->assertCustomFieldValue($originalContactID, 'ghi', "custom_{$multiField['id']}");
878 // cleanup created custom set
879 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField1['id']]);
880 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField2['id']]);
881 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
882 $this->callAPISuccess('CustomField', 'delete', ['id' => $multiField['id']]);
883 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]);
887 * Test that ContactReference fields are updated to point to the main contact
888 * after a merge is performed and the duplicate contact is deleted.
890 public function testMigrationOfContactReferenceCustomField() {
891 // Create Custom Fields
892 $contactGroup = $this->setupCustomGroupForIndividual();
893 $activityGroup = $this->customGroupCreate([
894 'name' => 'test_group_activity',
895 'extends' => 'Activity',
897 $refFieldContact = $this->customFieldCreate([
898 'custom_group_id' => $contactGroup['id'],
899 'label' => 'field_1' . $contactGroup['id'],
900 'data_type' => 'ContactReference',
901 'default_value' => NULL,
903 $refFieldActivity = $this->customFieldCreate([
904 'custom_group_id' => $activityGroup['id'],
905 'label' => 'field_1' . $activityGroup['id'],
906 'data_type' => 'ContactReference',
907 'default_value' => NULL,
911 $this->setupMatchData();
912 $originalContactID = $this->contacts
[0]['id'];
913 $duplicateContactID = $this->contacts
[1]['id'];
915 // create a contact that won't be merged but has a ContactReference field
916 // pointing to the duplicate (to be deleted) contact
917 $unrelatedContact = $this->individualCreate([
918 'first_name' => 'Unrelated',
919 'first_name' => 'Contact',
920 'email' => 'unrelated@example.com',
921 "custom_{$refFieldContact['id']}" => $duplicateContactID,
923 // also create an activity with a ContactReference custom field
924 $activity = $this->activityCreate([
925 'target_contact_id' => $unrelatedContact,
926 "custom_{$refFieldActivity['id']}" => $duplicateContactID,
929 // verify that the fields were set
930 $this->assertCustomFieldValue($unrelatedContact, $duplicateContactID, "custom_{$refFieldContact['id']}");
931 $this->assertEntityCustomFieldValue('Activity', $activity['id'], $duplicateContactID, "custom_{$refFieldActivity['id']}_id");
934 $this->mergeContacts($originalContactID, $duplicateContactID, []);
936 // verify that the ContactReference fields were updated to point to the surviving contact post-merge
937 $this->assertCustomFieldValue($unrelatedContact, $originalContactID, "custom_{$refFieldContact['id']}");
938 $this->assertEntityCustomFieldValue('Activity', $activity['id'], $originalContactID, "custom_{$refFieldActivity['id']}_id");
940 // cleanup created custom set
941 $this->callAPISuccess('CustomField', 'delete', ['id' => $refFieldContact['id']]);
942 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $contactGroup['id']]);
943 $this->callAPISuccess('CustomField', 'delete', ['id' => $refFieldActivity['id']]);
944 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $activityGroup['id']]);
948 * Calls merge method on given contacts, with values given in $params array.
950 * @param $originalContactID
951 * ID of target contact
952 * @param $duplicateContactID
953 * ID of contact to be merged
955 * Array of fields to be merged from source into target contact, of the form
956 * ['move_<fieldName>' => <fieldValue>]
958 * @throws \CRM_Core_Exception
959 * @throws \CiviCRM_API3_Exception
961 private function mergeContacts($originalContactID, $duplicateContactID, $params) {
962 $rowsElementsAndInfo = CRM_Dedupe_Merger
::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
965 'main_details' => $rowsElementsAndInfo['main_details'],
966 'other_details' => $rowsElementsAndInfo['other_details'],
969 // Migrate data of duplicate contact
970 CRM_Dedupe_Merger
::moveAllBelongings($originalContactID, $duplicateContactID, array_merge($migrationData, $params));
974 * Checks if the expected value for the given field corresponds to what is
975 * stored in the database for the given contact ID.
978 * @param $expectedValue
979 * @param $customFieldName
981 private function assertCustomFieldValue($contactID, $expectedValue, $customFieldName) {
982 $this->assertEntityCustomFieldValue('Contact', $contactID, $expectedValue, $customFieldName);
986 * Check if the custom field of the given field and entity id matches the
991 * @param $expectedValue
992 * @param $customFieldName
994 private function assertEntityCustomFieldValue($entity, $id, $expectedValue, $customFieldName) {
995 $data = $this->callAPISuccess($entity, 'getsingle', [
997 'return' => [$customFieldName],
1000 $this->assertEquals($expectedValue, $data[$customFieldName], "Custom field value was supposed to be '{$expectedValue}', '{$data[$customFieldName]}' found.");
1004 * Creates a custom group to run tests on contacts that are individuals.
1007 * Data for the created custom group record
1009 private function setupCustomGroupForIndividual() {
1010 $customGroup = $this->callAPISuccess('custom_group', 'get', [
1011 'name' => 'test_group',
1014 if ($customGroup['count'] > 0) {
1015 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $customGroup['id']]);
1018 $customGroup = $this->callAPISuccess('custom_group', 'create', [
1019 'title' => 'Test_Group',
1020 'name' => 'test_group',
1021 'extends' => ['Individual'],
1022 'style' => 'Inline',
1023 'is_multiple' => FALSE,
1027 return $customGroup;
1031 * Creates a custom field on the provided custom group with the given field
1034 * @param $fieldLabel
1035 * @param $createGroup
1038 * Data for the created custom field record
1040 private function setupCustomField($fieldLabel, $createGroup) {
1041 return $this->callAPISuccess('custom_field', 'create', [
1042 'label' => $fieldLabel,
1043 'data_type' => 'Alphanumeric',
1044 'html_type' => 'Text',
1045 'custom_group_id' => $createGroup['id'],
1050 * Set up some contacts for our matching.
1052 public function setupMatchData() {
1055 'first_name' => 'Mickey',
1056 'last_name' => 'Mouse',
1057 'email' => 'mickey@mouse.com',
1060 'first_name' => 'Mickey',
1061 'last_name' => 'Mouse',
1062 'email' => 'mickey@mouse.com',
1065 'first_name' => 'Minnie',
1066 'last_name' => 'Mouse',
1067 'email' => 'mickey@mouse.com',
1070 'first_name' => 'Minnie',
1071 'last_name' => 'Mouse',
1072 'email' => 'mickey@mouse.com',
1075 foreach ($fixtures as $fixture) {
1076 $contactID = $this->individualCreate($fixture);
1077 $this->contacts
[] = array_merge($fixture, ['id' => $contactID]);
1080 $organizationFixtures = [
1082 'organization_name' => 'Walt Disney Ltd',
1083 'email' => 'walt@disney.com',
1086 'organization_name' => 'Walt Disney Ltd',
1087 'email' => 'walt@disney.com',
1090 'organization_name' => 'Walt Disney',
1091 'email' => 'walt@disney.com',
1094 'organization_name' => 'Walt Disney',
1095 'email' => 'walter@disney.com',
1098 foreach ($organizationFixtures as $fixture) {
1099 $contactID = $this->organizationCreate($fixture);
1100 $this->contacts
[] = array_merge($fixture, ['id' => $contactID]);
1105 * Get the list of tables that refer to the CID.
1107 * This is a statically maintained (in this test list).
1109 * There is also a check against an automated list but having both seems to
1110 * add extra stability to me. They do not change often.
1112 public function getStaticCIDRefs() {
1114 'civicrm_acl_cache' => [
1117 'civicrm_acl_contact_cache' => [
1120 'civicrm_action_log' => [
1123 'civicrm_activity_contact' => [
1126 'civicrm_address' => [
1129 'civicrm_batch' => [
1133 'civicrm_campaign' => [
1135 1 => 'last_modified_id',
1137 'civicrm_case_contact' => [
1140 'civicrm_contact' => [
1141 0 => 'primary_contact_id',
1144 'civicrm_contribution' => [
1147 'civicrm_contribution_page' => [
1150 'civicrm_contribution_recur' => [
1153 'civicrm_contribution_soft' => [
1156 'civicrm_custom_group' => [
1159 'civicrm_dashboard_contact' => [
1162 'civicrm_dedupe_exception' => [
1166 'civicrm_domain' => [
1169 'civicrm_email' => [
1172 'civicrm_event' => [
1175 'civicrm_event_carts' => [
1178 'civicrm_financial_account' => [
1181 'civicrm_financial_item' => [
1184 'civicrm_grant' => [
1187 'civicrm_group' => [
1191 'civicrm_group_contact' => [
1194 'civicrm_group_contact_cache' => [
1197 'civicrm_group_organization' => [
1198 0 => 'organization_id',
1206 'civicrm_mailing' => [
1208 1 => 'scheduled_id',
1214 'civicrm_mailing_abtest' => [
1217 'civicrm_mailing_event_queue' => [
1220 'civicrm_mailing_event_subscribe' => [
1223 'civicrm_mailing_recipients' => [
1226 'civicrm_membership' => [
1229 'civicrm_membership_log' => [
1232 'civicrm_membership_type' => [
1233 0 => 'member_of_contact_id',
1238 'civicrm_openid' => [
1241 'civicrm_participant' => [
1244 1 => 'transferred_to_contact_id',
1246 'civicrm_payment_token' => [
1253 'civicrm_phone' => [
1256 'civicrm_pledge' => [
1259 'civicrm_print_label' => [
1262 'civicrm_relationship' => [
1263 0 => 'contact_id_a',
1264 1 => 'contact_id_b',
1266 'civicrm_report_instance' => [
1270 'civicrm_setting' => [
1274 'civicrm_subscription_history' => [
1277 'civicrm_survey' => [
1279 1 => 'last_modified_id',
1284 'civicrm_uf_group' => [
1287 'civicrm_uf_match' => [
1290 'civicrm_value_testgetcidref_1' => [
1293 'civicrm_website' => [
1300 * Get a list of CIDs that is calculated off the schema.
1302 * Note this is an expensive and table locking query. Should be safe in tests
1305 public function getCalculatedCIDRefs() {
1311 FROM information_schema.key_column_usage
1313 referenced_table_schema = database() AND
1314 referenced_table_name = 'civicrm_contact' AND
1315 referenced_column_name = 'id';
1317 $dao = CRM_Core_DAO
::executeQuery($sql);
1318 while ($dao->fetch()) {
1319 $cidRefs[$dao->table_name
][] = $dao->column_name
;
1321 // Do specific re-ordering changes to make this the same as the ref validated one.
1322 // The above query orders by FK alphabetically.
1323 // There might be cleverer ways to do this but it shouldn't change much.
1324 $cidRefs['civicrm_contact'][0] = 'primary_contact_id';
1325 $cidRefs['civicrm_contact'][1] = 'employer_id';
1326 $cidRefs['civicrm_acl_contact_cache'][0] = 'contact_id';
1327 $cidRefs['civicrm_mailing'][0] = 'created_id';
1328 $cidRefs['civicrm_mailing'][1] = 'scheduled_id';
1329 $cidRefs['civicrm_mailing'][2] = 'approver_id';