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',
39 * @throws \CRM_Core_Exception
41 public function createDupeContacts() {
42 // create a group to hold contacts, so that dupe checks don't consider any other contacts in the DB
44 'name' => 'Test Dupe Merger Group',
45 'title' => 'Test Dupe Merger Group',
48 'visibility' => 'Public Pages',
51 $result = $this->callAPISuccess('group', 'create', $params);
52 $this->_groupId
= $result['id'];
56 // make dupe checks based on based on following contact sets:
57 // FIRST - LAST - EMAIL
58 // ---------------------------------
59 // robin - hood - robin@example.com
60 // robin - hood - robin@example.com
61 // robin - hood - hood@example.com
62 // robin - dale - robin@example.com
63 // little - dale - dale@example.com
64 // little - dale - dale@example.com
65 // will - dale - dale@example.com
66 // will - dale - will@example.com
67 // will - dale - will@example.com
70 'first_name' => 'robin',
71 'last_name' => 'hood',
72 'email' => 'robin@example.com',
73 'contact_type' => 'Individual',
76 'first_name' => 'robin',
77 'last_name' => 'hood',
78 'email' => 'robin@example.com',
79 'contact_type' => 'Individual',
82 'first_name' => 'robin',
83 'last_name' => 'hood',
84 'email' => 'hood@example.com',
85 'contact_type' => 'Individual',
88 'first_name' => 'robin',
89 'last_name' => 'dale',
90 'email' => 'robin@example.com',
91 'contact_type' => 'Individual',
94 'first_name' => 'little',
95 'last_name' => 'dale',
96 'email' => 'dale@example.com',
97 'contact_type' => 'Individual',
100 'first_name' => 'little',
101 'last_name' => 'dale',
102 'email' => 'dale@example.com',
103 'contact_type' => 'Individual',
106 'first_name' => 'will',
107 'last_name' => 'dale',
108 'email' => 'dale@example.com',
109 'contact_type' => 'Individual',
112 'first_name' => 'will',
113 'last_name' => 'dale',
114 'email' => 'will@example.com',
115 'contact_type' => 'Individual',
118 'first_name' => 'will',
119 'last_name' => 'dale',
120 'email' => 'will@example.com',
121 'contact_type' => 'Individual',
126 foreach ($params as $param) {
127 $param['version'] = 3;
128 $contact = civicrm_api('contact', 'create', $param);
129 $this->_contactIds
[$count++
] = $contact['id'];
132 'contact_id' => $contact['id'],
133 'group_id' => $this->_groupId
,
136 $this->callAPISuccess('group_contact', 'create', $grpParams);
141 * Delete all created contacts.
143 public function deleteDupeContacts() {
144 foreach ($this->_contactIds
as $contactId) {
145 $this->contactDelete($contactId);
147 $this->groupDelete($this->_groupId
);
151 * Test the batch merge.
153 * @throws \CRM_Core_Exception
155 public function testBatchMergeSelectedDuplicates() {
156 $this->createDupeContacts();
158 // verify that all contacts have been created separately
159 $this->assertEquals(count($this->_contactIds
), 9, 'Check for number of contacts.');
161 $dao = new CRM_Dedupe_DAO_RuleGroup();
162 $dao->contact_type
= 'Individual';
163 $dao->name
= 'IndividualSupervised';
164 $dao->is_default
= 1;
167 $foundDupes = CRM_Dedupe_Finder
::dupesInGroup($dao->id
, $this->_groupId
);
169 // -------------------------------------------------------------------------
170 // Name and Email (reserved) Matches ( 3 pairs )
171 // --------------------------------------------------------------------------
172 // robin - hood - robin@example.com
173 // robin - hood - robin@example.com
174 // little - dale - dale@example.com
175 // little - dale - dale@example.com
176 // will - dale - will@example.com
177 // will - dale - will@example.com
178 // so 3 pairs for - first + last + mail
179 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
181 // Run dedupe finder as the browser would
182 //avoid invalid key error
183 $_SERVER['REQUEST_METHOD'] = 'GET';
184 $object = new CRM_Contact_Page_DedupeFind();
185 $object->set('gid', $this->_groupId
);
186 $object->set('rgid', $dao->id
);
187 $object->set('action', CRM_Core_Action
::UPDATE
);
188 $object->setEmbedded(TRUE);
191 // Retrieve pairs from prev next cache table
192 $select = ['pn.is_selected' => 'is_selected'];
193 $cacheKeyString = CRM_Dedupe_Merger
::getMergeCacheKeyString($dao->id
, $this->_groupId
, [], TRUE, 0);
194 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
195 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
197 // mark first two pairs as selected
198 CRM_Core_DAO
::singleValueQuery("UPDATE civicrm_prevnext_cache SET is_selected = 1 WHERE id IN ({$pnDupePairs[0]['prevnext_id']}, {$pnDupePairs[1]['prevnext_id']})");
200 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
201 $this->assertEquals($pnDupePairs[0]['is_selected'], 1, 'Check if first record in dupe pairs is marked as selected.');
202 $this->assertEquals($pnDupePairs[0]['is_selected'], 1, 'Check if second record in dupe pairs is marked as selected.');
204 // batch merge selected dupes
205 $result = CRM_Dedupe_Merger
::batchMerge($dao->id
, $this->_groupId
, 'safe', 5, 1);
206 $this->assertEquals(count($result['merged']), 2, 'Check number of merged pairs.');
208 $stats = $this->callAPISuccess('Dedupe', 'getstatistics', [
209 'group_id' => $this->_groupId
,
210 'rule_group_id' => $dao->id
,
211 'check_permissions' => TRUE,
213 $this->assertEquals(['merged' => 2, 'skipped' => 0], $stats);
215 // retrieve pairs from prev next cache table
216 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
217 $this->assertEquals(count($pnDupePairs), 1, 'Check number of remaining dupe pairs in prev next cache.');
219 $this->deleteDupeContacts();
223 * Test the batch merge.
225 public function testBatchMergeAllDuplicates() {
226 $this->createDupeContacts();
228 // verify that all contacts have been created separately
229 $this->assertEquals(count($this->_contactIds
), 9, 'Check for number of contacts.');
231 $dao = new CRM_Dedupe_DAO_RuleGroup();
232 $dao->contact_type
= 'Individual';
233 $dao->name
= 'IndividualSupervised';
234 $dao->is_default
= 1;
237 $foundDupes = CRM_Dedupe_Finder
::dupesInGroup($dao->id
, $this->_groupId
);
239 // -------------------------------------------------------------------------
240 // Name and Email (reserved) Matches ( 3 pairs )
241 // --------------------------------------------------------------------------
242 // robin - hood - robin@example.com
243 // robin - hood - robin@example.com
244 // little - dale - dale@example.com
245 // little - dale - dale@example.com
246 // will - dale - will@example.com
247 // will - dale - will@example.com
248 // so 3 pairs for - first + last + mail
249 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
251 // Run dedupe finder as the browser would
252 //avoid invalid key error
253 $_SERVER['REQUEST_METHOD'] = 'GET';
254 $object = new CRM_Contact_Page_DedupeFind();
255 $object->set('gid', $this->_groupId
);
256 $object->set('rgid', $dao->id
);
257 $object->set('action', CRM_Core_Action
::UPDATE
);
258 $object->setEmbedded(TRUE);
261 // Retrieve pairs from prev next cache table
262 $select = ['pn.is_selected' => 'is_selected'];
263 $cacheKeyString = CRM_Dedupe_Merger
::getMergeCacheKeyString($dao->id
, $this->_groupId
, [], TRUE, 0);
264 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
266 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
268 // batch merge all dupes
269 $result = CRM_Dedupe_Merger
::batchMerge($dao->id
, $this->_groupId
, 'safe', 5, 2);
270 $this->assertEquals(count($result['merged']), 3, 'Check number of merged pairs.');
272 $stats = $this->callAPISuccess('Dedupe', 'getstatistics', [
273 'rule_group_id' => $dao->id
,
274 'group_id' => $this->_groupId
,
276 // retrieve pairs from prev next cache table
277 $pnDupePairs = CRM_Core_BAO_PrevNextCache
::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
278 $this->assertEquals(count($pnDupePairs), 0, 'Check number of remaining dupe pairs in prev next cache.');
280 $this->deleteDupeContacts();
284 * The goal of this function is to test that all required tables are returned.
286 public function testGetCidRefs() {
287 $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__
, 'Contacts');
288 $this->assertEquals($this->getStaticCIDRefs(), CRM_Dedupe_Merger
::cidRefs());
289 $this->assertEquals($this->getCalculatedCIDRefs(), CRM_Dedupe_Merger
::cidRefs());
293 * Test function that gets duplicate pairs.
295 * It turns out there are 2 code paths retrieving this data so my initial
296 * focus is on ensuring they match.
298 * @throws \CRM_Core_Exception
300 public function testGetMatches() {
301 $this->setupMatchData();
303 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
304 'rule_group_id' => 1,
306 $this->assertEquals([
308 'srcID' => $this->contacts
[1]['id'],
309 'srcName' => 'Mr. Mickey Mouse II',
310 'dstID' => $this->contacts
[0]['id'],
311 'dstName' => 'Mr. Mickey Mouse II',
316 'srcID' => $this->contacts
[3]['id'],
317 'srcName' => 'Mr. Minnie Mouse II',
318 'dstID' => $this->contacts
[2]['id'],
319 'dstName' => 'Mr. Minnie Mouse II',
327 * Test results are returned when criteria are passed in.
329 * @throws \CRM_Core_Exception
331 public function testGetMatchesCriteriaMatched() {
332 $this->setupMatchData();
333 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
334 'rule_group_id' => 1,
335 'criteria' => ['contact' => ['id' => ['>' => 1]]],
337 $this->assertCount(2, $pairs);
341 * Test results are returned when criteria are passed in & limit is respected.
343 * @throws \CRM_Core_Exception
345 public function testGetMatchesCriteriaMatchedWithLimit() {
346 $this->setupMatchData();
347 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
348 'rule_group_id' => 1,
349 'criteria' => ['contact' => ['id' => ['>' => 1]]],
350 'options' => ['limit' => 1],
352 $this->assertCount(1, $pairs);
356 * Test results are returned when criteria are passed in & limit is respected.
358 * @throws \CRM_Core_Exception
360 public function testGetMatchesCriteriaMatchedWithSearchLimit() {
361 $this->setupMatchData();
362 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
363 'rule_group_id' => 1,
364 'criteria' => ['contact' => ['id' => ['>' => 1]]],
367 $this->assertCount(1, $pairs);
371 * Test getting matches where there are no criteria.
373 * @throws \CRM_Core_Exception
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 * @throws \CRM_Core_Exception
388 public function testGetMatchesNoCriteriaButLimit() {
389 $this->setupMatchData();
390 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
391 'rule_group_id' => 1,
392 'options' => ['limit' => 1],
394 $this->assertCount(1, $pairs);
398 * Test that if criteria are passed and there are no matching contacts no matches are returned.
400 public function testGetMatchesCriteriaNotMatched() {
401 $this->setupMatchData();
402 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
403 'rule_group_id' => 1,
404 'criteria' => ['contact' => ['id' => ['>' => 100000]]],
406 $this->assertCount(0, $pairs);
410 * Test function that gets organization pairs.
412 * Note the rule will match on organization_name OR email - hence lots of
417 public function testGetOrganizationMatches() {
418 $this->setupMatchData();
419 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
420 'contact_type' => 'Organization',
421 'used' => 'Supervised',
424 $pairs = CRM_Dedupe_Merger
::getDuplicatePairs(
434 'srcID' => $this->contacts
[5]['id'],
435 'srcName' => 'Walt Disney Ltd',
436 'dstID' => $this->contacts
[4]['id'],
437 'dstName' => 'Walt Disney Ltd',
442 'srcID' => $this->contacts
[7]['id'],
443 'srcName' => 'Walt Disney',
444 'dstID' => $this->contacts
[6]['id'],
445 'dstName' => 'Walt Disney',
450 'srcID' => $this->contacts
[6]['id'],
451 'srcName' => 'Walt Disney',
452 'dstID' => $this->contacts
[4]['id'],
453 'dstName' => 'Walt Disney Ltd',
458 'srcID' => $this->contacts
[6]['id'],
459 'srcName' => 'Walt Disney',
460 'dstID' => $this->contacts
[5]['id'],
461 'dstName' => 'Walt Disney Ltd',
466 usort($pairs, [__CLASS__
, 'compareDupes']);
467 usort($expectedPairs, [__CLASS__
, 'compareDupes']);
468 $this->assertEquals($expectedPairs, $pairs);
472 * Function to sort $duplicate records in a stable way.
479 public static function compareDupes($a, $b) {
480 foreach (['srcName', 'dstName', 'srcID', 'dstID'] as $field) {
481 if ($a[$field] != $b[$field]) {
482 return ($a[$field] < $b[$field]) ?
1 : -1;
489 * Test function that gets organization duplicate pairs.
493 public function testGetOrganizationMatchesInGroup() {
494 $this->setupMatchData();
495 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
496 'contact_type' => 'Organization',
497 'used' => 'Supervised',
500 $groupID = $this->groupCreate(['title' => 'she-mice']);
502 $this->callAPISuccess('GroupContact', 'create', [
503 'group_id' => $groupID,
504 'contact_id' => $this->contacts
[4]['id'],
507 $pairs = CRM_Dedupe_Merger
::getDuplicatePairs(
515 $this->assertEquals([
517 'srcID' => $this->contacts
[5]['id'],
518 'srcName' => 'Walt Disney Ltd',
519 'dstID' => $this->contacts
[4]['id'],
520 'dstName' => 'Walt Disney Ltd',
525 'srcID' => $this->contacts
[6]['id'],
526 'srcName' => 'Walt Disney',
527 'dstID' => $this->contacts
[4]['id'],
528 'dstName' => 'Walt Disney Ltd',
534 $this->callAPISuccess('GroupContact', 'create', [
535 'group_id' => $groupID,
536 'contact_id' => $this->contacts
[5]['id'],
538 CRM_Core_DAO
::executeQuery("DELETE FROM civicrm_prevnext_cache");
539 $pairs = CRM_Dedupe_Merger
::getDuplicatePairs(
547 $this->assertEquals([
549 'srcID' => $this->contacts
[5]['id'],
550 'srcName' => 'Walt Disney Ltd',
551 'dstID' => $this->contacts
[4]['id'],
552 'dstName' => 'Walt Disney Ltd',
557 'srcID' => $this->contacts
[6]['id'],
558 'srcName' => 'Walt Disney',
559 'dstID' => $this->contacts
[4]['id'],
560 'dstName' => 'Walt Disney Ltd',
565 'srcID' => $this->contacts
[6]['id'],
566 'srcName' => 'Walt Disney',
567 'dstID' => $this->contacts
[5]['id'],
568 'dstName' => 'Walt Disney Ltd',
576 * Test function that gets duplicate pairs.
578 * It turns out there are 2 code paths retrieving this data so my initial
579 * focus is on ensuring they match.
581 public function testGetMatchesInGroup() {
582 $this->setupMatchData();
584 $groupID = $this->groupCreate(['title' => 'she-mice']);
586 $this->callAPISuccess('GroupContact', 'create', [
587 'group_id' => $groupID,
588 'contact_id' => $this->contacts
[3]['id'],
591 $pairs = CRM_Dedupe_Merger
::getDuplicatePairs(
599 $this->assertEquals([
601 'srcID' => $this->contacts
[3]['id'],
602 'srcName' => 'Mr. Minnie Mouse II',
603 'dstID' => $this->contacts
[2]['id'],
604 'dstName' => 'Mr. Minnie Mouse II',
612 * Test the special info handling is unchanged after cleanup.
614 * Note the handling is silly - we are testing to lock in over short term
615 * changes not to imply any contract on the function.
617 * @throws \CRM_Core_Exception
618 * @throws \CiviCRM_API3_Exception
620 public function testGetRowsElementsAndInfoSpecialInfo() {
621 $contact1 = $this->individualCreate([
622 'preferred_communication_method' => [],
623 'communication_style_id' => 'Familiar',
624 'prefix_id' => 'Mrs.',
625 'suffix_id' => 'III',
627 $contact2 = $this->individualCreate([
628 'preferred_communication_method' => [
632 'communication_style_id' => 'Formal',
633 'gender_id' => 'Female',
635 $rowsElementsAndInfo = CRM_Dedupe_Merger
::getRowsElementsAndInfo($contact1, $contact2);
636 $rows = $rowsElementsAndInfo['rows'];
637 $this->assertEquals([
640 'title' => 'Individual Prefix',
641 ], $rows['move_prefix_id']);
642 $this->assertEquals([
645 'title' => 'Individual Suffix',
646 ], $rows['move_suffix_id']);
647 $this->assertEquals([
651 ], $rows['move_gender_id']);
652 $this->assertEquals([
653 'main' => 'Familiar',
655 'title' => 'Communication Style',
656 ], $rows['move_communication_style_id']);
657 $this->assertEquals(1, $rowsElementsAndInfo['migration_info']['move_communication_style_id']);
658 $this->assertEquals([
660 'other' => 'SMS, Fax',
661 'title' => 'Preferred Communication Method',
662 ], $rows['move_preferred_communication_method']);
663 $this->assertEquals('\ 14\ 15\ 1', $rowsElementsAndInfo['migration_info']['move_preferred_communication_method']);
667 * Test migration of Membership.
669 public function testMergeMembership() {
671 $this->setupMatchData();
672 $originalContactID = $this->contacts
[0]['id'];
673 $duplicateContactID = $this->contacts
[1]['id'];
675 //Add Membership for the duplicate contact.
676 $memTypeId = $this->membershipTypeCreate();
677 $this->callAPISuccess('Membership', 'create', [
678 'membership_type_id' => $memTypeId,
679 'contact_id' => $duplicateContactID,
681 //Assert if 'add new' checkbox is enabled on the merge form.
682 $rowsElementsAndInfo = CRM_Dedupe_Merger
::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
683 foreach ($rowsElementsAndInfo['elements'] as $element) {
684 if (!empty($element[3]) && $element[3] == 'add new') {
685 $checkedAttr = ['checked' => 'checked'];
686 $this->checkArrayEquals($element[4], $checkedAttr);
690 //Merge and move the mem to the main contact.
691 $this->mergeContacts($originalContactID, $duplicateContactID, [
692 'move_rel_table_memberships' => 1,
693 'operation' => ['move_rel_table_memberships' => ['add' => 1]],
696 //Check if membership is correctly transferred to original contact.
697 $originalContactMembership = $this->callAPISuccess('Membership', 'get', [
698 'membership_type_id' => $memTypeId,
699 'contact_id' => $originalContactID,
701 $this->assertEquals(1, $originalContactMembership['count']);
705 * CRM-19653 : Test that custom field data should/shouldn't be overriden on
706 * selecting/not selecting option to migrate data respectively
708 * @throws \CRM_Core_Exception
710 public function testCustomDataOverwrite() {
711 // Create Custom Field
712 $createGroup = $this->setupCustomGroupForIndividual();
713 $createField = $this->setupCustomField('Graduation', $createGroup);
714 $customFieldName = "custom_" . $createField['id'];
717 $this->setupMatchData();
719 $originalContactID = $this->contacts
[0]['id'];
720 // used as duplicate contact in 1st use-case
721 $duplicateContactID1 = $this->contacts
[1]['id'];
722 // used as duplicate contact in 2nd use-case
723 $duplicateContactID2 = $this->contacts
[2]['id'];
725 // update the text custom field for original contact with value 'abc'
726 $this->callAPISuccess('Contact', 'create', [
727 'id' => $originalContactID,
728 "{$customFieldName}" => 'abc',
730 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
732 // update the text custom field for duplicate contact 1 with value 'def'
733 $this->callAPISuccess('Contact', 'create', [
734 'id' => $duplicateContactID1,
735 "{$customFieldName}" => 'def',
737 $this->assertCustomFieldValue($duplicateContactID1, 'def', $customFieldName);
739 // update the text custom field for duplicate contact 2 with value 'ghi'
740 $this->callAPISuccess('Contact', 'create', [
741 'id' => $duplicateContactID2,
742 "{$customFieldName}" => 'ghi',
744 $this->assertCustomFieldValue($duplicateContactID2, 'ghi', $customFieldName);
746 /*** USE-CASE 1: DO NOT OVERWRITE CUSTOM FIELD VALUE **/
747 $this->mergeContacts($originalContactID, $duplicateContactID1, [
748 "move_{$customFieldName}" => NULL,
750 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
752 /*** USE-CASE 2: OVERWRITE CUSTOM FIELD VALUE **/
753 $this->mergeContacts($originalContactID, $duplicateContactID2, [
754 "move_{$customFieldName}" => 'ghi',
756 $this->assertCustomFieldValue($originalContactID, 'ghi', $customFieldName);
758 // cleanup created custom set
759 $this->callAPISuccess('CustomField', 'delete', ['id' => $createField['id']]);
760 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
764 * Creatd Date merge cases
767 public function createdDateMergeCases() {
769 // Normal pattern merge into the lower id
771 // Check if we flipped the contacts that it still does right thing
777 * dev/core#996 Ensure that the oldest created date is retained even if duplicates have been flipped
779 * @dataProvider createdDateMergeCases
781 * @param $keepContactKey
782 * @param $duplicateContactKey
784 * @throws \API_Exception
785 * @throws \CRM_Core_Exception
786 * @throws \CiviCRM_API3_Exception
787 * @throws \Civi\API\Exception\UnauthorizedException
789 public function testCreatedDatePostMerge($keepContactKey, $duplicateContactKey) {
790 $this->setupMatchData();
791 $lowerContactCreatedDate = $this->callAPISuccess('Contact', 'getsingle', [
792 'id' => $this->contacts
[0]['id'],
793 'return' => ['created_date'],
795 // Assume contats have been flipped in the UL so merging into the higher id
796 $this->mergeContacts($this->contacts
[$keepContactKey]['id'], $this->contacts
[$duplicateContactKey]['id'], []);
797 $this->assertEquals($lowerContactCreatedDate, $this->callAPISuccess('Contact', 'getsingle', ['id' => $this->contacts
[$keepContactKey]['id'], 'return' => ['created_date']])['created_date']);
801 * Verifies that when a contact with a custom field value is merged into a
802 * contact without a record int its corresponding custom group table, and none
803 * of the custom fields of that custom table are selected, the value is not
806 public function testMigrationOfUnselectedCustomDataOnEmptyCustomRecord() {
807 // Create Custom Fields
808 $createGroup = $this->setupCustomGroupForIndividual();
809 $customField1 = $this->setupCustomField('TestField', $createGroup);
811 // Create multi-value custom field
812 $multiGroup = $this->CustomGroupMultipleCreateByParams();
813 $multiField = $this->customFieldCreate([
814 'custom_group_id' => $multiGroup['id'],
815 'label' => 'field_1' . $multiGroup['id'],
820 $this->setupMatchData();
821 $originalContactID = $this->contacts
[0]['id'];
822 $duplicateContactID = $this->contacts
[1]['id'];
824 // Update the text custom fields for duplicate contact
825 $this->callAPISuccess('Contact', 'create', [
826 'id' => $duplicateContactID,
827 "custom_{$customField1['id']}" => 'abc',
828 "custom_{$multiField['id']}" => 'def',
830 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
831 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$multiField['id']}");
833 // Merge, and ensure that no value was migrated
834 $this->mergeContacts($originalContactID, $duplicateContactID, [
835 "move_custom_{$customField1['id']}" => NULL,
836 "move_rel_table_custom_{$multiGroup['id']}" => NULL,
838 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
839 $this->assertCustomFieldValue($originalContactID, '', "custom_{$multiField['id']}");
841 // cleanup created custom set
842 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField1['id']]);
843 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
844 $this->callAPISuccess('CustomField', 'delete', ['id' => $multiField['id']]);
845 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]);
849 * Tests that if only part of the custom fields of a custom group are selected
850 * for a merge, only those values are merged, while all other fields of the
851 * custom group retain their original value, specifically for a contact with
852 * no records on the custom group table.
854 * @throws \CRM_Core_Exception
856 public function testMigrationOfSomeCustomDataOnEmptyCustomRecord() {
857 // Create Custom Fields
858 $createGroup = $this->setupCustomGroupForIndividual();
859 $customField1 = $this->setupCustomField('Test1', $createGroup);
860 $customField2 = $this->setupCustomField('Test2', $createGroup);
862 // Create multi-value custom field
863 $multiGroup = $this->CustomGroupMultipleCreateByParams();
864 $multiField = $this->customFieldCreate([
865 'custom_group_id' => $multiGroup['id'],
866 'label' => 'field_1' . $multiGroup['id'],
871 $this->setupMatchData();
872 $originalContactID = $this->contacts
[0]['id'];
873 $duplicateContactID = $this->contacts
[1]['id'];
875 // Update the text custom fields for duplicate contact
876 $this->callAPISuccess('Contact', 'create', [
877 'id' => $duplicateContactID,
878 "custom_{$customField1['id']}" => 'abc',
879 "custom_{$customField2['id']}" => 'def',
880 "custom_{$multiField['id']}" => 'ghi',
882 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
883 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$customField2['id']}");
884 $this->assertCustomFieldValue($duplicateContactID, 'ghi', "custom_{$multiField['id']}");
887 $this->mergeContacts($originalContactID, $duplicateContactID, [
888 "move_custom_{$customField1['id']}" => NULL,
889 "move_custom_{$customField2['id']}" => 'def',
890 "move_rel_table_custom_{$multiGroup['id']}" => '1',
892 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
893 $this->assertCustomFieldValue($originalContactID, 'def', "custom_{$customField2['id']}");
894 $this->assertCustomFieldValue($originalContactID, 'ghi', "custom_{$multiField['id']}");
896 // cleanup created custom set
897 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField1['id']]);
898 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField2['id']]);
899 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
900 $this->callAPISuccess('CustomField', 'delete', ['id' => $multiField['id']]);
901 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]);
905 * Test that ContactReference fields are updated to point to the main contact
906 * after a merge is performed and the duplicate contact is deleted.
908 * @throws \API_Exception
909 * @throws \CRM_Core_Exception
910 * @throws \CiviCRM_API3_Exception
911 * @throws \Civi\API\Exception\UnauthorizedException
913 public function testMigrationOfContactReferenceCustomField() {
914 // Create Custom Fields
915 $contactGroup = $this->setupCustomGroupForIndividual();
916 $activityGroup = $this->customGroupCreate([
917 'name' => 'test_group_activity',
918 'extends' => 'Activity',
920 $refFieldContact = $this->customFieldCreate([
921 'custom_group_id' => $contactGroup['id'],
922 'label' => 'field_1' . $contactGroup['id'],
923 'data_type' => 'ContactReference',
924 'default_value' => NULL,
926 $refFieldActivity = $this->customFieldCreate([
927 'custom_group_id' => $activityGroup['id'],
928 'label' => 'field_1' . $activityGroup['id'],
929 'data_type' => 'ContactReference',
930 'default_value' => NULL,
934 $this->setupMatchData();
935 $originalContactID = $this->contacts
[0]['id'];
936 $duplicateContactID = $this->contacts
[1]['id'];
938 // create a contact that won't be merged but has a ContactReference field
939 // pointing to the duplicate (to be deleted) contact
940 $unrelatedContact = $this->individualCreate([
941 'first_name' => 'Unrelated',
942 'last_name' => 'Contact',
943 'email' => 'unrelated@example.com',
944 "custom_{$refFieldContact['id']}" => $duplicateContactID,
946 // also create an activity with a ContactReference custom field
947 $activity = $this->activityCreate([
948 'target_contact_id' => $unrelatedContact,
949 "custom_{$refFieldActivity['id']}" => $duplicateContactID,
952 // verify that the fields were set
953 $this->assertCustomFieldValue($unrelatedContact, $duplicateContactID, "custom_{$refFieldContact['id']}");
954 $this->assertEntityCustomFieldValue('Activity', $activity['id'], $duplicateContactID, "custom_{$refFieldActivity['id']}_id");
957 $this->mergeContacts($originalContactID, $duplicateContactID, []);
959 // verify that the ContactReference fields were updated to point to the surviving contact post-merge
960 $this->assertCustomFieldValue($unrelatedContact, $originalContactID, "custom_{$refFieldContact['id']}");
961 $this->assertEntityCustomFieldValue('Activity', $activity['id'], $originalContactID, "custom_{$refFieldActivity['id']}_id");
963 // cleanup created custom set
964 $this->callAPISuccess('CustomField', 'delete', ['id' => $refFieldContact['id']]);
965 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $contactGroup['id']]);
966 $this->callAPISuccess('CustomField', 'delete', ['id' => $refFieldActivity['id']]);
967 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $activityGroup['id']]);
971 * Calls merge method on given contacts, with values given in $params array.
973 * @param $originalContactID
974 * ID of target contact
975 * @param $duplicateContactID
976 * ID of contact to be merged
978 * Array of fields to be merged from source into target contact, of the form
979 * ['move_<fieldName>' => <fieldValue>]
981 * @throws \API_Exception
982 * @throws \CRM_Core_Exception
983 * @throws \CiviCRM_API3_Exception
984 * @throws \Civi\API\Exception\UnauthorizedException
986 private function mergeContacts($originalContactID, $duplicateContactID, $params) {
987 $rowsElementsAndInfo = CRM_Dedupe_Merger
::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
990 'main_details' => $rowsElementsAndInfo['main_details'],
991 'other_details' => $rowsElementsAndInfo['other_details'],
994 // Migrate data of duplicate contact
995 CRM_Dedupe_Merger
::moveAllBelongings($originalContactID, $duplicateContactID, array_merge($migrationData, $params));
999 * Checks if the expected value for the given field corresponds to what is
1000 * stored in the database for the given contact ID.
1003 * @param $expectedValue
1004 * @param $customFieldName
1006 * @throws \CRM_Core_Exception
1008 private function assertCustomFieldValue($contactID, $expectedValue, $customFieldName) {
1009 $this->assertEntityCustomFieldValue('Contact', $contactID, $expectedValue, $customFieldName);
1013 * Check if the custom field of the given field and entity id matches the
1018 * @param $expectedValue
1019 * @param $customFieldName
1021 * @throws \CRM_Core_Exception
1023 private function assertEntityCustomFieldValue($entity, $id, $expectedValue, $customFieldName) {
1024 $data = $this->callAPISuccess($entity, 'getsingle', [
1026 'return' => [$customFieldName],
1029 $this->assertEquals($expectedValue, $data[$customFieldName], "Custom field value was supposed to be '{$expectedValue}', '{$data[$customFieldName]}' found.");
1033 * Creates a custom group to run tests on contacts that are individuals.
1036 * Data for the created custom group record
1037 * @throws \CRM_Core_Exception
1039 private function setupCustomGroupForIndividual() {
1040 $customGroup = $this->callAPISuccess('custom_group', 'get', [
1041 'name' => 'test_group',
1044 if ($customGroup['count'] > 0) {
1045 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $customGroup['id']]);
1048 $customGroup = $this->callAPISuccess('custom_group', 'create', [
1049 'title' => 'Test_Group',
1050 'name' => 'test_group',
1051 'extends' => ['Individual'],
1052 'style' => 'Inline',
1053 'is_multiple' => FALSE,
1057 return $customGroup;
1061 * Creates a custom field on the provided custom group with the given field
1064 * @param string $fieldLabel
1065 * @param array $createGroup
1068 * Data for the created custom field record
1069 * @throws \CRM_Core_Exception
1071 private function setupCustomField($fieldLabel, $createGroup) {
1072 return $this->callAPISuccess('custom_field', 'create', [
1073 'label' => $fieldLabel,
1074 'data_type' => 'Alphanumeric',
1075 'html_type' => 'Text',
1076 'custom_group_id' => $createGroup['id'],
1081 * Set up some contacts for our matching.
1083 * @throws \CRM_Core_Exception
1085 public function setupMatchData() {
1088 'first_name' => 'Mickey',
1089 'last_name' => 'Mouse',
1090 'email' => 'mickey@mouse.com',
1093 'first_name' => 'Mickey',
1094 'last_name' => 'Mouse',
1095 'email' => 'mickey@mouse.com',
1098 'first_name' => 'Minnie',
1099 'last_name' => 'Mouse',
1100 'email' => 'mickey@mouse.com',
1103 'first_name' => 'Minnie',
1104 'last_name' => 'Mouse',
1105 'email' => 'mickey@mouse.com',
1108 foreach ($fixtures as $fixture) {
1109 $contactID = $this->individualCreate($fixture);
1110 $this->contacts
[] = array_merge($fixture, ['id' => $contactID]);
1113 $organizationFixtures = [
1115 'organization_name' => 'Walt Disney Ltd',
1116 'email' => 'walt@disney.com',
1119 'organization_name' => 'Walt Disney Ltd',
1120 'email' => 'walt@disney.com',
1123 'organization_name' => 'Walt Disney',
1124 'email' => 'walt@disney.com',
1127 'organization_name' => 'Walt Disney',
1128 'email' => 'walter@disney.com',
1131 foreach ($organizationFixtures as $fixture) {
1132 $contactID = $this->organizationCreate($fixture);
1133 $this->contacts
[] = array_merge($fixture, ['id' => $contactID]);
1138 * Get the list of tables that refer to the CID.
1140 * This is a statically maintained (in this test list).
1142 * There is also a check against an automated list but having both seems to
1143 * add extra stability to me. They do not change often.
1145 public function getStaticCIDRefs() {
1147 'civicrm_acl_cache' => [
1150 'civicrm_acl_contact_cache' => [
1153 'civicrm_action_log' => [
1156 'civicrm_activity_contact' => [
1159 'civicrm_address' => [
1162 'civicrm_batch' => [
1166 'civicrm_campaign' => [
1168 1 => 'last_modified_id',
1170 'civicrm_case_contact' => [
1173 'civicrm_contact' => [
1174 0 => 'primary_contact_id',
1177 'civicrm_contribution' => [
1180 'civicrm_contribution_page' => [
1183 'civicrm_contribution_recur' => [
1186 'civicrm_contribution_soft' => [
1189 'civicrm_custom_group' => [
1192 'civicrm_dashboard_contact' => [
1195 'civicrm_dedupe_exception' => [
1199 'civicrm_domain' => [
1202 'civicrm_email' => [
1205 'civicrm_event' => [
1208 'civicrm_event_carts' => [
1211 'civicrm_financial_account' => [
1214 'civicrm_financial_item' => [
1217 'civicrm_grant' => [
1220 'civicrm_group' => [
1224 'civicrm_group_contact' => [
1227 'civicrm_group_contact_cache' => [
1230 'civicrm_group_organization' => [
1231 0 => 'organization_id',
1239 'civicrm_mailing' => [
1241 1 => 'scheduled_id',
1247 'civicrm_mailing_abtest' => [
1250 'civicrm_mailing_event_queue' => [
1253 'civicrm_mailing_event_subscribe' => [
1256 'civicrm_mailing_recipients' => [
1259 'civicrm_membership' => [
1262 'civicrm_membership_log' => [
1265 'civicrm_membership_type' => [
1266 0 => 'member_of_contact_id',
1271 'civicrm_openid' => [
1274 'civicrm_participant' => [
1277 1 => 'transferred_to_contact_id',
1279 'civicrm_payment_token' => [
1286 'civicrm_phone' => [
1289 'civicrm_pledge' => [
1292 'civicrm_print_label' => [
1295 'civicrm_relationship' => [
1296 0 => 'contact_id_a',
1297 1 => 'contact_id_b',
1299 'civicrm_report_instance' => [
1303 'civicrm_setting' => [
1307 'civicrm_subscription_history' => [
1310 'civicrm_survey' => [
1312 1 => 'last_modified_id',
1317 'civicrm_uf_group' => [
1320 'civicrm_uf_match' => [
1323 'civicrm_value_testgetcidref_1' => [
1326 'civicrm_website' => [
1333 * Get a list of CIDs that is calculated off the schema.
1335 * Note this is an expensive and table locking query. Should be safe in tests
1338 public function getCalculatedCIDRefs() {
1344 FROM information_schema.key_column_usage
1346 referenced_table_schema = database() AND
1347 referenced_table_name = 'civicrm_contact' AND
1348 referenced_column_name = 'id';
1350 $dao = CRM_Core_DAO
::executeQuery($sql);
1351 while ($dao->fetch()) {
1352 $cidRefs[$dao->table_name
][] = $dao->column_name
;
1354 // Do specific re-ordering changes to make this the same as the ref validated one.
1355 // The above query orders by FK alphabetically.
1356 // There might be cleverer ways to do this but it shouldn't change much.
1357 $cidRefs['civicrm_contact'][0] = 'primary_contact_id';
1358 $cidRefs['civicrm_contact'][1] = 'employer_id';
1359 $cidRefs['civicrm_acl_contact_cache'][0] = 'contact_id';
1360 $cidRefs['civicrm_mailing'][0] = 'created_id';
1361 $cidRefs['civicrm_mailing'][1] = 'scheduled_id';
1362 $cidRefs['civicrm_mailing'][2] = 'approver_id';