Merge pull request #14249 from yashodha/959_dev
[civicrm-core.git] / tests / phpunit / CRM / Dedupe / MergerTest.php
1 <?php
2
3 /**
4 * Class CRM_Dedupe_DedupeMergerTest
5 *
6 * @group headless
7 */
8 class CRM_Dedupe_MergerTest extends CiviUnitTestCase {
9
10 protected $_groupId;
11
12 protected $_contactIds = [];
13
14 /**
15 * Tear down.
16 *
17 * @throws \Exception
18 */
19 public function tearDown() {
20 $this->quickCleanup([
21 'civicrm_contact',
22 'civicrm_group_contact',
23 'civicrm_group',
24 ]);
25 parent::tearDown();
26 }
27
28 public function createDupeContacts() {
29 // create a group to hold contacts, so that dupe checks don't consider any other contacts in the DB
30 $params = [
31 'name' => 'Test Dupe Merger Group',
32 'title' => 'Test Dupe Merger Group',
33 'domain_id' => 1,
34 'is_active' => 1,
35 'visibility' => 'Public Pages',
36 ];
37
38 $result = $this->callAPISuccess('group', 'create', $params);
39 $this->_groupId = $result['id'];
40
41 // contact data set
42
43 // make dupe checks based on based on following contact sets:
44 // FIRST - LAST - EMAIL
45 // ---------------------------------
46 // robin - hood - robin@example.com
47 // robin - hood - robin@example.com
48 // robin - hood - hood@example.com
49 // robin - dale - robin@example.com
50 // little - dale - dale@example.com
51 // little - dale - dale@example.com
52 // will - dale - dale@example.com
53 // will - dale - will@example.com
54 // will - dale - will@example.com
55 $params = [
56 [
57 'first_name' => 'robin',
58 'last_name' => 'hood',
59 'email' => 'robin@example.com',
60 'contact_type' => 'Individual',
61 ],
62 [
63 'first_name' => 'robin',
64 'last_name' => 'hood',
65 'email' => 'robin@example.com',
66 'contact_type' => 'Individual',
67 ],
68 [
69 'first_name' => 'robin',
70 'last_name' => 'hood',
71 'email' => 'hood@example.com',
72 'contact_type' => 'Individual',
73 ],
74 [
75 'first_name' => 'robin',
76 'last_name' => 'dale',
77 'email' => 'robin@example.com',
78 'contact_type' => 'Individual',
79 ],
80 [
81 'first_name' => 'little',
82 'last_name' => 'dale',
83 'email' => 'dale@example.com',
84 'contact_type' => 'Individual',
85 ],
86 [
87 'first_name' => 'little',
88 'last_name' => 'dale',
89 'email' => 'dale@example.com',
90 'contact_type' => 'Individual',
91 ],
92 [
93 'first_name' => 'will',
94 'last_name' => 'dale',
95 'email' => 'dale@example.com',
96 'contact_type' => 'Individual',
97 ],
98 [
99 'first_name' => 'will',
100 'last_name' => 'dale',
101 'email' => 'will@example.com',
102 'contact_type' => 'Individual',
103 ],
104 [
105 'first_name' => 'will',
106 'last_name' => 'dale',
107 'email' => 'will@example.com',
108 'contact_type' => 'Individual',
109 ],
110 ];
111
112 $count = 1;
113 foreach ($params as $param) {
114 $param['version'] = 3;
115 $contact = civicrm_api('contact', 'create', $param);
116 $this->_contactIds[$count++] = $contact['id'];
117
118 $grpParams = [
119 'contact_id' => $contact['id'],
120 'group_id' => $this->_groupId,
121 'version' => 3,
122 ];
123 $this->callAPISuccess('group_contact', 'create', $grpParams);
124 }
125 }
126
127 /**
128 * Delete all created contacts.
129 */
130 public function deleteDupeContacts() {
131 foreach ($this->_contactIds as $contactId) {
132 $this->contactDelete($contactId);
133 }
134 $this->groupDelete($this->_groupId);
135 }
136
137 /**
138 * Test the batch merge.
139 */
140 public function testBatchMergeSelectedDuplicates() {
141 $this->createDupeContacts();
142
143 // verify that all contacts have been created separately
144 $this->assertEquals(count($this->_contactIds), 9, 'Check for number of contacts.');
145
146 $dao = new CRM_Dedupe_DAO_RuleGroup();
147 $dao->contact_type = 'Individual';
148 $dao->name = 'IndividualSupervised';
149 $dao->is_default = 1;
150 $dao->find(TRUE);
151
152 $foundDupes = CRM_Dedupe_Finder::dupesInGroup($dao->id, $this->_groupId);
153
154 // -------------------------------------------------------------------------
155 // Name and Email (reserved) Matches ( 3 pairs )
156 // --------------------------------------------------------------------------
157 // robin - hood - robin@example.com
158 // robin - hood - robin@example.com
159 // little - dale - dale@example.com
160 // little - dale - dale@example.com
161 // will - dale - will@example.com
162 // will - dale - will@example.com
163 // so 3 pairs for - first + last + mail
164 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
165
166 // Run dedupe finder as the browser would
167 //avoid invalid key error
168 $_SERVER['REQUEST_METHOD'] = 'GET';
169 $object = new CRM_Contact_Page_DedupeFind();
170 $object->set('gid', $this->_groupId);
171 $object->set('rgid', $dao->id);
172 $object->set('action', CRM_Core_Action::UPDATE);
173 $object->setEmbedded(TRUE);
174 @$object->run();
175
176 // Retrieve pairs from prev next cache table
177 $select = ['pn.is_selected' => 'is_selected'];
178 $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($dao->id, $this->_groupId);
179 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
180
181 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
182
183 // mark first two pairs as selected
184 CRM_Core_DAO::singleValueQuery("UPDATE civicrm_prevnext_cache SET is_selected = 1 WHERE id IN ({$pnDupePairs[0]['prevnext_id']}, {$pnDupePairs[1]['prevnext_id']})");
185
186 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
187 $this->assertEquals($pnDupePairs[0]['is_selected'], 1, 'Check if first record in dupe pairs is marked as selected.');
188 $this->assertEquals($pnDupePairs[0]['is_selected'], 1, 'Check if second record in dupe pairs is marked as selected.');
189
190 // batch merge selected dupes
191 $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', 5, 1);
192 $this->assertEquals(count($result['merged']), 2, 'Check number of merged pairs.');
193
194 // retrieve pairs from prev next cache table
195 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
196 $this->assertEquals(count($pnDupePairs), 1, 'Check number of remaining dupe pairs in prev next cache.');
197
198 $this->deleteDupeContacts();
199 }
200
201 /**
202 * Test the batch merge.
203 */
204 public function testBatchMergeAllDuplicates() {
205 $this->createDupeContacts();
206
207 // verify that all contacts have been created separately
208 $this->assertEquals(count($this->_contactIds), 9, 'Check for number of contacts.');
209
210 $dao = new CRM_Dedupe_DAO_RuleGroup();
211 $dao->contact_type = 'Individual';
212 $dao->name = 'IndividualSupervised';
213 $dao->is_default = 1;
214 $dao->find(TRUE);
215
216 $foundDupes = CRM_Dedupe_Finder::dupesInGroup($dao->id, $this->_groupId);
217
218 // -------------------------------------------------------------------------
219 // Name and Email (reserved) Matches ( 3 pairs )
220 // --------------------------------------------------------------------------
221 // robin - hood - robin@example.com
222 // robin - hood - robin@example.com
223 // little - dale - dale@example.com
224 // little - dale - dale@example.com
225 // will - dale - will@example.com
226 // will - dale - will@example.com
227 // so 3 pairs for - first + last + mail
228 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
229
230 // Run dedupe finder as the browser would
231 //avoid invalid key error
232 $_SERVER['REQUEST_METHOD'] = 'GET';
233 $object = new CRM_Contact_Page_DedupeFind();
234 $object->set('gid', $this->_groupId);
235 $object->set('rgid', $dao->id);
236 $object->set('action', CRM_Core_Action::UPDATE);
237 $object->setEmbedded(TRUE);
238 @$object->run();
239
240 // Retrieve pairs from prev next cache table
241 $select = ['pn.is_selected' => 'is_selected'];
242 $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($dao->id, $this->_groupId);
243 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
244
245 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
246
247 // batch merge all dupes
248 $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', 5, 2);
249 $this->assertEquals(count($result['merged']), 3, 'Check number of merged pairs.');
250
251 // retrieve pairs from prev next cache table
252 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
253 $this->assertEquals(count($pnDupePairs), 0, 'Check number of remaining dupe pairs in prev next cache.');
254
255 $this->deleteDupeContacts();
256 }
257
258 /**
259 * The goal of this function is to test that all required tables are returned.
260 */
261 public function testGetCidRefs() {
262 $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, 'Contacts');
263 $this->assertEquals(array_merge($this->getStaticCIDRefs(), $this->getHackedInCIDRef()), CRM_Dedupe_Merger::cidRefs());
264 $this->assertEquals(array_merge($this->getCalculatedCIDRefs(), $this->getHackedInCIDRef()), CRM_Dedupe_Merger::cidRefs());
265 }
266
267 /**
268 * Get the list of not-really-cid-refs that are currently hacked in.
269 *
270 * This is hacked into getCIDs function.
271 *
272 * @return array
273 */
274 public function getHackedInCIDRef() {
275 return [
276 'civicrm_entity_tag' => [
277 0 => 'entity_id',
278 ],
279 ];
280 }
281
282 /**
283 * Test function that gets duplicate pairs.
284 *
285 * It turns out there are 2 code paths retrieving this data so my initial
286 * focus is on ensuring they match.
287 */
288 public function testGetMatches() {
289 $this->setupMatchData();
290 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
291 1,
292 NULL,
293 TRUE,
294 25,
295 FALSE
296 );
297
298 $this->assertEquals([
299 0 => [
300 'srcID' => $this->contacts[1]['id'],
301 'srcName' => 'Mr. Mickey Mouse II',
302 'dstID' => $this->contacts[0]['id'],
303 'dstName' => 'Mr. Mickey Mouse II',
304 'weight' => 20,
305 'canMerge' => TRUE,
306 ],
307 1 => [
308 'srcID' => $this->contacts[3]['id'],
309 'srcName' => 'Mr. Minnie Mouse II',
310 'dstID' => $this->contacts[2]['id'],
311 'dstName' => 'Mr. Minnie Mouse II',
312 'weight' => 20,
313 'canMerge' => TRUE,
314 ],
315 ], $pairs);
316 }
317
318 /**
319 * Test function that gets organization pairs.
320 *
321 * Note the rule will match on organization_name OR email - hence lots of
322 * matches.
323 *
324 * @throws \Exception
325 */
326 public function testGetOrganizationMatches() {
327 $this->setupMatchData();
328 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
329 'contact_type' => 'Organization',
330 'used' => 'Supervised',
331 ]);
332
333 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
334 $ruleGroups['id'],
335 NULL,
336 TRUE,
337 25,
338 FALSE
339 );
340
341 $expectedPairs = [
342 0 => [
343 'srcID' => $this->contacts[5]['id'],
344 'srcName' => 'Walt Disney Ltd',
345 'dstID' => $this->contacts[4]['id'],
346 'dstName' => 'Walt Disney Ltd',
347 'weight' => 20,
348 'canMerge' => TRUE,
349 ],
350 1 => [
351 'srcID' => $this->contacts[7]['id'],
352 'srcName' => 'Walt Disney',
353 'dstID' => $this->contacts[6]['id'],
354 'dstName' => 'Walt Disney',
355 'weight' => 10,
356 'canMerge' => TRUE,
357 ],
358 2 => [
359 'srcID' => $this->contacts[6]['id'],
360 'srcName' => 'Walt Disney',
361 'dstID' => $this->contacts[4]['id'],
362 'dstName' => 'Walt Disney Ltd',
363 'weight' => 10,
364 'canMerge' => TRUE,
365 ],
366 3 => [
367 'srcID' => $this->contacts[6]['id'],
368 'srcName' => 'Walt Disney',
369 'dstID' => $this->contacts[5]['id'],
370 'dstName' => 'Walt Disney Ltd',
371 'weight' => 10,
372 'canMerge' => TRUE,
373 ],
374 ];
375 usort($pairs, [__CLASS__, 'compareDupes']);
376 usort($expectedPairs, [__CLASS__, 'compareDupes']);
377 $this->assertEquals($expectedPairs, $pairs);
378 }
379
380 /**
381 * Function to sort $duplicate records in a stable way.
382 *
383 * @param array $a
384 * @param array $b
385 *
386 * @return int
387 */
388 public static function compareDupes($a, $b) {
389 foreach (['srcName', 'dstName', 'srcID', 'dstID'] as $field) {
390 if ($a[$field] != $b[$field]) {
391 return ($a[$field] < $b[$field]) ? 1 : -1;
392 }
393 }
394 return 0;
395 }
396
397 /**
398 * Test function that gets organization duplicate pairs.
399 *
400 * @throws \Exception
401 */
402 public function testGetOrganizationMatchesInGroup() {
403 $this->setupMatchData();
404 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
405 'contact_type' => 'Organization',
406 'used' => 'Supervised',
407 ]);
408
409 $groupID = $this->groupCreate(['title' => 'she-mice']);
410
411 $this->callAPISuccess('GroupContact', 'create', [
412 'group_id' => $groupID,
413 'contact_id' => $this->contacts[4]['id'],
414 ]);
415
416 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
417 $ruleGroups['id'],
418 $groupID,
419 TRUE,
420 25,
421 FALSE
422 );
423
424 $this->assertEquals([
425 0 => [
426 'srcID' => $this->contacts[5]['id'],
427 'srcName' => 'Walt Disney Ltd',
428 'dstID' => $this->contacts[4]['id'],
429 'dstName' => 'Walt Disney Ltd',
430 'weight' => 20,
431 'canMerge' => TRUE,
432 ],
433 1 => [
434 'srcID' => $this->contacts[6]['id'],
435 'srcName' => 'Walt Disney',
436 'dstID' => $this->contacts[4]['id'],
437 'dstName' => 'Walt Disney Ltd',
438 'weight' => 10,
439 'canMerge' => TRUE,
440 ],
441 ], $pairs);
442
443 $this->callAPISuccess('GroupContact', 'create', [
444 'group_id' => $groupID,
445 'contact_id' => $this->contacts[5]['id'],
446 ]);
447 CRM_Core_DAO::executeQuery("DELETE FROM civicrm_prevnext_cache");
448 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
449 $ruleGroups['id'],
450 $groupID,
451 TRUE,
452 25,
453 FALSE
454 );
455
456 $this->assertEquals([
457 0 => [
458 'srcID' => $this->contacts[5]['id'],
459 'srcName' => 'Walt Disney Ltd',
460 'dstID' => $this->contacts[4]['id'],
461 'dstName' => 'Walt Disney Ltd',
462 'weight' => 20,
463 'canMerge' => TRUE,
464 ],
465 1 => [
466 'srcID' => $this->contacts[6]['id'],
467 'srcName' => 'Walt Disney',
468 'dstID' => $this->contacts[4]['id'],
469 'dstName' => 'Walt Disney Ltd',
470 'weight' => 10,
471 'canMerge' => TRUE,
472 ],
473 2 => [
474 'srcID' => $this->contacts[6]['id'],
475 'srcName' => 'Walt Disney',
476 'dstID' => $this->contacts[5]['id'],
477 'dstName' => 'Walt Disney Ltd',
478 'weight' => 10,
479 'canMerge' => TRUE,
480 ],
481 ], $pairs);
482 }
483
484 /**
485 * Test function that gets duplicate pairs.
486 *
487 * It turns out there are 2 code paths retrieving this data so my initial
488 * focus is on ensuring they match.
489 */
490 public function testGetMatchesInGroup() {
491 $this->setupMatchData();
492
493 $groupID = $this->groupCreate(['title' => 'she-mice']);
494
495 $this->callAPISuccess('GroupContact', 'create', [
496 'group_id' => $groupID,
497 'contact_id' => $this->contacts[3]['id'],
498 ]);
499
500 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
501 1,
502 $groupID,
503 TRUE,
504 25,
505 FALSE
506 );
507
508 $this->assertEquals([
509 0 => [
510 'srcID' => $this->contacts[3]['id'],
511 'srcName' => 'Mr. Minnie Mouse II',
512 'dstID' => $this->contacts[2]['id'],
513 'dstName' => 'Mr. Minnie Mouse II',
514 'weight' => 20,
515 'canMerge' => TRUE,
516 ],
517 ], $pairs);
518 }
519
520 /**
521 * Test the special info handling is unchanged after cleanup.
522 *
523 * Note the handling is silly - we are testing to lock in over short term
524 * changes not to imply any contract on the function.
525 */
526 public function testGetRowsElementsAndInfoSpecialInfo() {
527 $contact1 = $this->individualCreate([
528 'preferred_communication_method' => [],
529 'communication_style_id' => 'Familiar',
530 'prefix_id' => 'Mrs.',
531 'suffix_id' => 'III',
532 ]);
533 $contact2 = $this->individualCreate([
534 'preferred_communication_method' => [
535 'SMS',
536 'Fax',
537 ],
538 'communication_style_id' => 'Formal',
539 'gender_id' => 'Female',
540 ]);
541 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($contact1, $contact2);
542 $rows = $rowsElementsAndInfo['rows'];
543 $this->assertEquals([
544 'main' => 'Mrs.',
545 'other' => 'Mr.',
546 'title' => 'Individual Prefix',
547 ], $rows['move_prefix_id']);
548 $this->assertEquals([
549 'main' => 'III',
550 'other' => 'II',
551 'title' => 'Individual Suffix',
552 ], $rows['move_suffix_id']);
553 $this->assertEquals([
554 'main' => '',
555 'other' => 'Female',
556 'title' => 'Gender',
557 ], $rows['move_gender_id']);
558 $this->assertEquals([
559 'main' => 'Familiar',
560 'other' => 'Formal',
561 'title' => 'Communication Style',
562 ], $rows['move_communication_style_id']);
563 $this->assertEquals(1, $rowsElementsAndInfo['migration_info']['move_communication_style_id']);
564 $this->assertEquals([
565 'main' => '',
566 'other' => 'SMS, Fax',
567 'title' => 'Preferred Communication Method',
568 ], $rows['move_preferred_communication_method']);
569 $this->assertEquals('\ 14\ 15\ 1', $rowsElementsAndInfo['migration_info']['move_preferred_communication_method']);
570 }
571
572 /**
573 * Test migration of Membership.
574 */
575 public function testMergeMembership() {
576 // Contacts setup
577 $this->setupMatchData();
578 $originalContactID = $this->contacts[0]['id'];
579 $duplicateContactID = $this->contacts[1]['id'];
580
581 //Add Membership for the duplicate contact.
582 $memTypeId = $this->membershipTypeCreate();
583 $this->callAPISuccess('Membership', 'create', [
584 'membership_type_id' => $memTypeId,
585 'contact_id' => $duplicateContactID,
586 ]);
587 //Assert if 'add new' checkbox is enabled on the merge form.
588 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
589 foreach ($rowsElementsAndInfo['elements'] as $element) {
590 if (!empty($element[3]) && $element[3] == 'add new') {
591 $checkedAttr = ['checked' => 'checked'];
592 $this->checkArrayEquals($element[4], $checkedAttr);
593 }
594 }
595
596 //Merge and move the mem to the main contact.
597 $this->mergeContacts($originalContactID, $duplicateContactID, [
598 'move_rel_table_memberships' => 1,
599 'operation' => ['move_rel_table_memberships' => ['add' => 1]],
600 ]);
601
602 //Check if membership is correctly transferred to original contact.
603 $originalContactMembership = $this->callAPISuccess('Membership', 'get', [
604 'membership_type_id' => $memTypeId,
605 'contact_id' => $originalContactID,
606 ]);
607 $this->assertEquals(1, $originalContactMembership['count']);
608 }
609
610 /**
611 * CRM-19653 : Test that custom field data should/shouldn't be overriden on
612 * selecting/not selecting option to migrate data respectively
613 */
614 public function testCustomDataOverwrite() {
615 // Create Custom Field
616 $createGroup = $this->setupCustomGroupForIndividual();
617 $createField = $this->setupCustomField('Graduation', $createGroup);
618 $customFieldName = "custom_" . $createField['id'];
619
620 // Contacts setup
621 $this->setupMatchData();
622
623 $originalContactID = $this->contacts[0]['id'];
624 // used as duplicate contact in 1st use-case
625 $duplicateContactID1 = $this->contacts[1]['id'];
626 // used as duplicate contact in 2nd use-case
627 $duplicateContactID2 = $this->contacts[2]['id'];
628
629 // update the text custom field for original contact with value 'abc'
630 $this->callAPISuccess('Contact', 'create', [
631 'id' => $originalContactID,
632 "{$customFieldName}" => 'abc',
633 ]);
634 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
635
636 // update the text custom field for duplicate contact 1 with value 'def'
637 $this->callAPISuccess('Contact', 'create', [
638 'id' => $duplicateContactID1,
639 "{$customFieldName}" => 'def',
640 ]);
641 $this->assertCustomFieldValue($duplicateContactID1, 'def', $customFieldName);
642
643 // update the text custom field for duplicate contact 2 with value 'ghi'
644 $this->callAPISuccess('Contact', 'create', [
645 'id' => $duplicateContactID2,
646 "{$customFieldName}" => 'ghi',
647 ]);
648 $this->assertCustomFieldValue($duplicateContactID2, 'ghi', $customFieldName);
649
650 /*** USE-CASE 1: DO NOT OVERWRITE CUSTOM FIELD VALUE **/
651 $this->mergeContacts($originalContactID, $duplicateContactID1, [
652 "move_{$customFieldName}" => NULL,
653 ]);
654 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
655
656 /*** USE-CASE 2: OVERWRITE CUSTOM FIELD VALUE **/
657 $this->mergeContacts($originalContactID, $duplicateContactID2, [
658 "move_{$customFieldName}" => 'ghi',
659 ]);
660 $this->assertCustomFieldValue($originalContactID, 'ghi', $customFieldName);
661
662 // cleanup created custom set
663 $this->callAPISuccess('CustomField', 'delete', ['id' => $createField['id']]);
664 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
665 }
666
667 /**
668 * Verifies that when a contact with a custom field value is merged into a
669 * contact without a record int its corresponding custom group table, and none
670 * of the custom fields of that custom table are selected, the value is not
671 * merged in.
672 */
673 public function testMigrationOfUnselectedCustomDataOnEmptyCustomRecord() {
674 // Create Custom Fields
675 $createGroup = $this->setupCustomGroupForIndividual();
676 $customField1 = $this->setupCustomField('TestField', $createGroup);
677
678 // Create multi-value custom field
679 $multiGroup = $this->CustomGroupMultipleCreateByParams();
680 $multiField = $this->customFieldCreate([
681 'custom_group_id' => $multiGroup['id'],
682 'label' => 'field_1' . $multiGroup['id'],
683 'in_selector' => 1,
684 ]);
685
686 // Contacts setup
687 $this->setupMatchData();
688 $originalContactID = $this->contacts[0]['id'];
689 $duplicateContactID = $this->contacts[1]['id'];
690
691 // Update the text custom fields for duplicate contact
692 $this->callAPISuccess('Contact', 'create', [
693 'id' => $duplicateContactID,
694 "custom_{$customField1['id']}" => 'abc',
695 "custom_{$multiField['id']}" => 'def',
696 ]);
697 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
698 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$multiField['id']}");
699
700 // Merge, and ensure that no value was migrated
701 $this->mergeContacts($originalContactID, $duplicateContactID, [
702 "move_custom_{$customField1['id']}" => NULL,
703 "move_rel_table_custom_{$multiGroup['id']}" => NULL,
704 ]);
705 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
706 $this->assertCustomFieldValue($originalContactID, '', "custom_{$multiField['id']}");
707
708 // cleanup created custom set
709 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField1['id']]);
710 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
711 $this->callAPISuccess('CustomField', 'delete', ['id' => $multiField['id']]);
712 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]);
713 }
714
715 /**
716 * Tests that if only part of the custom fields of a custom group are selected
717 * for a merge, only those values are merged, while all other fields of the
718 * custom group retain their original value, specifically for a contact with
719 * no records on the custom group table.
720 */
721 public function testMigrationOfSomeCustomDataOnEmptyCustomRecord() {
722 // Create Custom Fields
723 $createGroup = $this->setupCustomGroupForIndividual();
724 $customField1 = $this->setupCustomField('Test1', $createGroup);
725 $customField2 = $this->setupCustomField('Test2', $createGroup);
726
727 // Create multi-value custom field
728 $multiGroup = $this->CustomGroupMultipleCreateByParams();
729 $multiField = $this->customFieldCreate([
730 'custom_group_id' => $multiGroup['id'],
731 'label' => 'field_1' . $multiGroup['id'],
732 'in_selector' => 1,
733 ]);
734
735 // Contacts setup
736 $this->setupMatchData();
737 $originalContactID = $this->contacts[0]['id'];
738 $duplicateContactID = $this->contacts[1]['id'];
739
740 // Update the text custom fields for duplicate contact
741 $this->callAPISuccess('Contact', 'create', [
742 'id' => $duplicateContactID,
743 "custom_{$customField1['id']}" => 'abc',
744 "custom_{$customField2['id']}" => 'def',
745 "custom_{$multiField['id']}" => 'ghi',
746 ]);
747 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
748 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$customField2['id']}");
749 $this->assertCustomFieldValue($duplicateContactID, 'ghi', "custom_{$multiField['id']}");
750
751 // Perform merge
752 $this->mergeContacts($originalContactID, $duplicateContactID, [
753 "move_custom_{$customField1['id']}" => NULL,
754 "move_custom_{$customField2['id']}" => 'def',
755 "move_rel_table_custom_{$multiGroup['id']}" => '1',
756 ]);
757 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
758 $this->assertCustomFieldValue($originalContactID, 'def', "custom_{$customField2['id']}");
759 $this->assertCustomFieldValue($originalContactID, 'ghi', "custom_{$multiField['id']}");
760
761 // cleanup created custom set
762 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField1['id']]);
763 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField2['id']]);
764 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
765 $this->callAPISuccess('CustomField', 'delete', ['id' => $multiField['id']]);
766 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]);
767 }
768
769 /**
770 * Calls merge method on given contacts, with values given in $params array.
771 *
772 * @param $originalContactID
773 * ID of target contact
774 * @param $duplicateContactID
775 * ID of contact to be merged
776 * @param $params
777 * Array of fields to be merged from source into target contact, of the form
778 * ['move_<fieldName>' => <fieldValue>]
779 *
780 * @throws \CRM_Core_Exception
781 * @throws \CiviCRM_API3_Exception
782 */
783 private function mergeContacts($originalContactID, $duplicateContactID, $params) {
784 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
785
786 $migrationData = [
787 'main_details' => $rowsElementsAndInfo['main_details'],
788 'other_details' => $rowsElementsAndInfo['other_details'],
789 ];
790
791 // Migrate data of duplicate contact
792 CRM_Dedupe_Merger::moveAllBelongings($originalContactID, $duplicateContactID, array_merge($migrationData, $params));
793 }
794
795 /**
796 * Checks if the expected value for the given field corresponds to what is
797 * stored in the database for the given contact ID.
798 *
799 * @param $contactID
800 * @param $expectedValue
801 * @param $customFieldName
802 */
803 private function assertCustomFieldValue($contactID, $expectedValue, $customFieldName) {
804 $data = $this->callAPISuccess('Contact', 'getsingle', [
805 'id' => $contactID,
806 'return' => [$customFieldName],
807 ]);
808
809 $this->assertEquals($expectedValue, $data[$customFieldName], "Custom field value was supposed to be '{$expectedValue}', '{$data[$customFieldName]}' found.");
810 }
811
812 /**
813 * Creates a custom group to run tests on contacts that are individuals.
814 *
815 * @return array
816 * Data for the created custom group record
817 */
818 private function setupCustomGroupForIndividual() {
819 $customGroup = $this->callAPISuccess('custom_group', 'get', [
820 'name' => 'test_group',
821 ]);
822
823 if ($customGroup['count'] > 0) {
824 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $customGroup['id']]);
825 }
826
827 $customGroup = $this->callAPISuccess('custom_group', 'create', [
828 'title' => 'Test_Group',
829 'name' => 'test_group',
830 'extends' => ['Individual'],
831 'style' => 'Inline',
832 'is_multiple' => FALSE,
833 'is_active' => 1,
834 ]);
835
836 return $customGroup;
837 }
838
839 /**
840 * Creates a custom field on the provided custom group with the given field
841 * label.
842 *
843 * @param $fieldLabel
844 * @param $createGroup
845 *
846 * @return array
847 * Data for the created custom field record
848 */
849 private function setupCustomField($fieldLabel, $createGroup) {
850 return $this->callAPISuccess('custom_field', 'create', [
851 'label' => $fieldLabel,
852 'data_type' => 'Alphanumeric',
853 'html_type' => 'Text',
854 'custom_group_id' => $createGroup['id'],
855 ]);
856 }
857
858 /**
859 * Set up some contacts for our matching.
860 */
861 public function setupMatchData() {
862 $fixtures = [
863 [
864 'first_name' => 'Mickey',
865 'last_name' => 'Mouse',
866 'email' => 'mickey@mouse.com',
867 ],
868 [
869 'first_name' => 'Mickey',
870 'last_name' => 'Mouse',
871 'email' => 'mickey@mouse.com',
872 ],
873 [
874 'first_name' => 'Minnie',
875 'last_name' => 'Mouse',
876 'email' => 'mickey@mouse.com',
877 ],
878 [
879 'first_name' => 'Minnie',
880 'last_name' => 'Mouse',
881 'email' => 'mickey@mouse.com',
882 ],
883 ];
884 foreach ($fixtures as $fixture) {
885 $contactID = $this->individualCreate($fixture);
886 $this->contacts[] = array_merge($fixture, ['id' => $contactID]);
887 }
888 $organizationFixtures = [
889 [
890 'organization_name' => 'Walt Disney Ltd',
891 'email' => 'walt@disney.com',
892 ],
893 [
894 'organization_name' => 'Walt Disney Ltd',
895 'email' => 'walt@disney.com',
896 ],
897 [
898 'organization_name' => 'Walt Disney',
899 'email' => 'walt@disney.com',
900 ],
901 [
902 'organization_name' => 'Walt Disney',
903 'email' => 'walter@disney.com',
904 ],
905 ];
906 foreach ($organizationFixtures as $fixture) {
907 $contactID = $this->organizationCreate($fixture);
908 $this->contacts[] = array_merge($fixture, ['id' => $contactID]);
909 }
910 }
911
912 /**
913 * Get the list of tables that refer to the CID.
914 *
915 * This is a statically maintained (in this test list).
916 *
917 * There is also a check against an automated list but having both seems to
918 * add extra stability to me. They do not change often.
919 */
920 public function getStaticCIDRefs() {
921 return [
922 'civicrm_acl_cache' => [
923 0 => 'contact_id',
924 ],
925 'civicrm_acl_contact_cache' => [
926 0 => 'contact_id',
927 ],
928 'civicrm_action_log' => [
929 0 => 'contact_id',
930 ],
931 'civicrm_activity_contact' => [
932 0 => 'contact_id',
933 ],
934 'civicrm_address' => [
935 0 => 'contact_id',
936 ],
937 'civicrm_batch' => [
938 0 => 'created_id',
939 1 => 'modified_id',
940 ],
941 'civicrm_campaign' => [
942 0 => 'created_id',
943 1 => 'last_modified_id',
944 ],
945 'civicrm_case_contact' => [
946 0 => 'contact_id',
947 ],
948 'civicrm_contact' => [
949 0 => 'primary_contact_id',
950 1 => 'employer_id',
951 ],
952 'civicrm_contribution' => [
953 0 => 'contact_id',
954 ],
955 'civicrm_contribution_page' => [
956 0 => 'created_id',
957 ],
958 'civicrm_contribution_recur' => [
959 0 => 'contact_id',
960 ],
961 'civicrm_contribution_soft' => [
962 0 => 'contact_id',
963 ],
964 'civicrm_custom_group' => [
965 0 => 'created_id',
966 ],
967 'civicrm_dashboard_contact' => [
968 0 => 'contact_id',
969 ],
970 'civicrm_dedupe_exception' => [
971 0 => 'contact_id1',
972 1 => 'contact_id2',
973 ],
974 'civicrm_domain' => [
975 0 => 'contact_id',
976 ],
977 'civicrm_email' => [
978 0 => 'contact_id',
979 ],
980 'civicrm_event' => [
981 0 => 'created_id',
982 ],
983 'civicrm_event_carts' => [
984 0 => 'user_id',
985 ],
986 'civicrm_financial_account' => [
987 0 => 'contact_id',
988 ],
989 'civicrm_financial_item' => [
990 0 => 'contact_id',
991 ],
992 'civicrm_grant' => [
993 0 => 'contact_id',
994 ],
995 'civicrm_group' => [
996 0 => 'created_id',
997 1 => 'modified_id',
998 ],
999 'civicrm_group_contact' => [
1000 0 => 'contact_id',
1001 ],
1002 'civicrm_group_contact_cache' => [
1003 0 => 'contact_id',
1004 ],
1005 'civicrm_group_organization' => [
1006 0 => 'organization_id',
1007 ],
1008 'civicrm_im' => [
1009 0 => 'contact_id',
1010 ],
1011 'civicrm_log' => [
1012 0 => 'modified_id',
1013 ],
1014 'civicrm_mailing' => [
1015 0 => 'created_id',
1016 1 => 'scheduled_id',
1017 2 => 'approver_id',
1018 ],
1019 'civicrm_file' => [
1020 'created_id',
1021 ],
1022 'civicrm_mailing_abtest' => [
1023 0 => 'created_id',
1024 ],
1025 'civicrm_mailing_event_queue' => [
1026 0 => 'contact_id',
1027 ],
1028 'civicrm_mailing_event_subscribe' => [
1029 0 => 'contact_id',
1030 ],
1031 'civicrm_mailing_recipients' => [
1032 0 => 'contact_id',
1033 ],
1034 'civicrm_membership' => [
1035 0 => 'contact_id',
1036 ],
1037 'civicrm_membership_log' => [
1038 0 => 'modified_id',
1039 ],
1040 'civicrm_membership_type' => [
1041 0 => 'member_of_contact_id',
1042 ],
1043 'civicrm_note' => [
1044 0 => 'contact_id',
1045 ],
1046 'civicrm_openid' => [
1047 0 => 'contact_id',
1048 ],
1049 'civicrm_participant' => [
1050 0 => 'contact_id',
1051 //CRM-16761
1052 1 => 'transferred_to_contact_id',
1053 ],
1054 'civicrm_payment_token' => [
1055 0 => 'contact_id',
1056 1 => 'created_id',
1057 ],
1058 'civicrm_pcp' => [
1059 0 => 'contact_id',
1060 ],
1061 'civicrm_phone' => [
1062 0 => 'contact_id',
1063 ],
1064 'civicrm_pledge' => [
1065 0 => 'contact_id',
1066 ],
1067 'civicrm_print_label' => [
1068 0 => 'created_id',
1069 ],
1070 'civicrm_relationship' => [
1071 0 => 'contact_id_a',
1072 1 => 'contact_id_b',
1073 ],
1074 'civicrm_report_instance' => [
1075 0 => 'created_id',
1076 1 => 'owner_id',
1077 ],
1078 'civicrm_setting' => [
1079 0 => 'contact_id',
1080 1 => 'created_id',
1081 ],
1082 'civicrm_subscription_history' => [
1083 0 => 'contact_id',
1084 ],
1085 'civicrm_survey' => [
1086 0 => 'created_id',
1087 1 => 'last_modified_id',
1088 ],
1089 'civicrm_tag' => [
1090 0 => 'created_id',
1091 ],
1092 'civicrm_uf_group' => [
1093 0 => 'created_id',
1094 ],
1095 'civicrm_uf_match' => [
1096 0 => 'contact_id',
1097 ],
1098 'civicrm_value_testgetcidref_1' => [
1099 0 => 'entity_id',
1100 ],
1101 'civicrm_website' => [
1102 0 => 'contact_id',
1103 ],
1104 ];
1105 }
1106
1107 /**
1108 * Get a list of CIDs that is calculated off the schema.
1109 *
1110 * Note this is an expensive and table locking query. Should be safe in tests
1111 * though.
1112 */
1113 public function getCalculatedCIDRefs() {
1114 $cidRefs = [];
1115 $sql = "
1116 SELECT
1117 table_name,
1118 column_name
1119 FROM information_schema.key_column_usage
1120 WHERE
1121 referenced_table_schema = database() AND
1122 referenced_table_name = 'civicrm_contact' AND
1123 referenced_column_name = 'id';
1124 ";
1125 $dao = CRM_Core_DAO::executeQuery($sql);
1126 while ($dao->fetch()) {
1127 $cidRefs[$dao->table_name][] = $dao->column_name;
1128 }
1129 // Do specific re-ordering changes to make this the same as the ref validated one.
1130 // The above query orders by FK alphabetically.
1131 // There might be cleverer ways to do this but it shouldn't change much.
1132 $cidRefs['civicrm_contact'][0] = 'primary_contact_id';
1133 $cidRefs['civicrm_contact'][1] = 'employer_id';
1134 $cidRefs['civicrm_acl_contact_cache'][0] = 'contact_id';
1135 $cidRefs['civicrm_mailing'][0] = 'created_id';
1136 $cidRefs['civicrm_mailing'][1] = 'scheduled_id';
1137 $cidRefs['civicrm_mailing'][2] = 'approver_id';
1138 return $cidRefs;
1139 }
1140
1141 }