Merge pull request #14309 from pradpnayak/FinancialItem
[civicrm-core.git] / tests / phpunit / CRM / Dedupe / MergerTest.php
CommitLineData
0622d221 1<?php
0622d221 2
3/**
4 * Class CRM_Dedupe_DedupeMergerTest
763a0fec 5 *
acb109b7 6 * @group headless
0622d221 7 */
8class CRM_Dedupe_MergerTest extends CiviUnitTestCase {
9
10 protected $_groupId;
763a0fec 11
12 protected $_contactIds = [];
0622d221 13
2f10fa02 14 /**
15 * Tear down.
16 *
17 * @throws \Exception
18 */
3308aac0 19 public function tearDown() {
763a0fec 20 $this->quickCleanup([
21 'civicrm_contact',
22 'civicrm_group_contact',
23 'civicrm_group',
24 ]);
3308aac0 25 parent::tearDown();
26 }
27
0622d221 28 public function createDupeContacts() {
29 // create a group to hold contacts, so that dupe checks don't consider any other contacts in the DB
763a0fec 30 $params = [
31 'name' => 'Test Dupe Merger Group',
32 'title' => 'Test Dupe Merger Group',
33 'domain_id' => 1,
34 'is_active' => 1,
0622d221 35 'visibility' => 'Public Pages',
763a0fec 36 ];
87a56b12 37
38 $result = $this->callAPISuccess('group', 'create', $params);
0622d221 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
763a0fec 55 $params = [
56 [
0622d221 57 'first_name' => 'robin',
58 'last_name' => 'hood',
59 'email' => 'robin@example.com',
60 'contact_type' => 'Individual',
763a0fec 61 ],
62 [
0622d221 63 'first_name' => 'robin',
64 'last_name' => 'hood',
65 'email' => 'robin@example.com',
66 'contact_type' => 'Individual',
763a0fec 67 ],
68 [
0622d221 69 'first_name' => 'robin',
70 'last_name' => 'hood',
71 'email' => 'hood@example.com',
72 'contact_type' => 'Individual',
763a0fec 73 ],
74 [
0622d221 75 'first_name' => 'robin',
76 'last_name' => 'dale',
77 'email' => 'robin@example.com',
78 'contact_type' => 'Individual',
763a0fec 79 ],
80 [
0622d221 81 'first_name' => 'little',
82 'last_name' => 'dale',
83 'email' => 'dale@example.com',
84 'contact_type' => 'Individual',
763a0fec 85 ],
86 [
0622d221 87 'first_name' => 'little',
88 'last_name' => 'dale',
89 'email' => 'dale@example.com',
90 'contact_type' => 'Individual',
763a0fec 91 ],
92 [
0622d221 93 'first_name' => 'will',
94 'last_name' => 'dale',
95 'email' => 'dale@example.com',
96 'contact_type' => 'Individual',
763a0fec 97 ],
98 [
0622d221 99 'first_name' => 'will',
100 'last_name' => 'dale',
101 'email' => 'will@example.com',
102 'contact_type' => 'Individual',
763a0fec 103 ],
104 [
0622d221 105 'first_name' => 'will',
106 'last_name' => 'dale',
107 'email' => 'will@example.com',
108 'contact_type' => 'Individual',
763a0fec 109 ],
110 ];
0622d221 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
763a0fec 118 $grpParams = [
0622d221 119 'contact_id' => $contact['id'],
763a0fec 120 'group_id' => $this->_groupId,
121 'version' => 3,
122 ];
87a56b12 123 $this->callAPISuccess('group_contact', 'create', $grpParams);
0622d221 124 }
125 }
126
93ac19cd 127 /**
128 * Delete all created contacts.
129 */
0622d221 130 public function deleteDupeContacts() {
0622d221 131 foreach ($this->_contactIds as $contactId) {
93ac19cd 132 $this->contactDelete($contactId);
0622d221 133 }
87a56b12 134 $this->groupDelete($this->_groupId);
0622d221 135 }
136
c8ec0753 137 /**
a354251e 138 * Test the batch merge.
c8ec0753 139 */
0622d221 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
39b959db
SL
167 //avoid invalid key error
168 $_SERVER['REQUEST_METHOD'] = 'GET';
0622d221 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);
64fe2fe0 173 $object->setEmbedded(TRUE);
0622d221 174 @$object->run();
175
176 // Retrieve pairs from prev next cache table
763a0fec 177 $select = ['pn.is_selected' => 'is_selected'];
b1679439 178 $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($dao->id, $this->_groupId);
0622d221 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
d13a105a 191 $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', 5, 1);
0622d221 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
c8ec0753 201 /**
a354251e 202 * Test the batch merge.
c8ec0753 203 */
0622d221 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
39b959db
SL
231 //avoid invalid key error
232 $_SERVER['REQUEST_METHOD'] = 'GET';
0622d221 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);
64fe2fe0 237 $object->setEmbedded(TRUE);
0622d221 238 @$object->run();
239
240 // Retrieve pairs from prev next cache table
763a0fec 241 $select = ['pn.is_selected' => 'is_selected'];
b1679439 242 $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($dao->id, $this->_groupId);
0622d221 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
d13a105a 248 $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', 5, 2);
0622d221 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
bf17fa88 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() {
763a0fec 275 return [
276 'civicrm_entity_tag' => [
bf17fa88 277 0 => 'entity_id',
763a0fec 278 ],
279 ];
bf17fa88 280 }
281
2988f5c7 282 /**
283 * Test function that gets duplicate pairs.
284 *
763a0fec 285 * It turns out there are 2 code paths retrieving this data so my initial
286 * focus is on ensuring they match.
2988f5c7 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
763a0fec 298 $this->assertEquals([
299 0 => [
08cde01f 300 'srcID' => $this->contacts[1]['id'],
2988f5c7 301 'srcName' => 'Mr. Mickey Mouse II',
08cde01f 302 'dstID' => $this->contacts[0]['id'],
2988f5c7 303 'dstName' => 'Mr. Mickey Mouse II',
304 'weight' => 20,
305 'canMerge' => TRUE,
763a0fec 306 ],
307 1 => [
08cde01f 308 'srcID' => $this->contacts[3]['id'],
3308aac0 309 'srcName' => 'Mr. Minnie Mouse II',
08cde01f 310 'dstID' => $this->contacts[2]['id'],
3308aac0 311 'dstName' => 'Mr. Minnie Mouse II',
312 'weight' => 20,
313 'canMerge' => TRUE,
763a0fec 314 ],
315 ], $pairs);
2988f5c7 316 }
317
bc0f3965 318 /**
319 * Test function that gets organization pairs.
320 *
763a0fec 321 * Note the rule will match on organization_name OR email - hence lots of
322 * matches.
2f10fa02 323 *
324 * @throws \Exception
bc0f3965 325 */
326 public function testGetOrganizationMatches() {
327 $this->setupMatchData();
763a0fec 328 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
329 'contact_type' => 'Organization',
330 'used' => 'Supervised',
331 ]);
bc0f3965 332
333 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
334 $ruleGroups['id'],
335 NULL,
336 TRUE,
337 25,
338 FALSE
339 );
340
763a0fec 341 $expectedPairs = [
342 0 => [
bc0f3965 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,
763a0fec 349 ],
350 1 => [
bc0f3965 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,
763a0fec 357 ],
358 2 => [
bc0f3965 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,
763a0fec 365 ],
366 3 => [
bc0f3965 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,
763a0fec 373 ],
374 ];
375 usort($pairs, [__CLASS__, 'compareDupes']);
376 usort($expectedPairs, [__CLASS__, 'compareDupes']);
72475b30
TO
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
763a0fec 385 *
72475b30
TO
386 * @return int
387 */
388 public static function compareDupes($a, $b) {
763a0fec 389 foreach (['srcName', 'dstName', 'srcID', 'dstID'] as $field) {
72475b30
TO
390 if ($a[$field] != $b[$field]) {
391 return ($a[$field] < $b[$field]) ? 1 : -1;
392 }
393 }
394 return 0;
bc0f3965 395 }
396
397 /**
398 * Test function that gets organization duplicate pairs.
2f10fa02 399 *
400 * @throws \Exception
bc0f3965 401 */
402 public function testGetOrganizationMatchesInGroup() {
403 $this->setupMatchData();
763a0fec 404 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
405 'contact_type' => 'Organization',
406 'used' => 'Supervised',
407 ]);
bc0f3965 408
763a0fec 409 $groupID = $this->groupCreate(['title' => 'she-mice']);
bc0f3965 410
763a0fec 411 $this->callAPISuccess('GroupContact', 'create', [
412 'group_id' => $groupID,
413 'contact_id' => $this->contacts[4]['id'],
414 ]);
bc0f3965 415
416 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
417 $ruleGroups['id'],
418 $groupID,
419 TRUE,
420 25,
421 FALSE
422 );
423
763a0fec 424 $this->assertEquals([
425 0 => [
bc0f3965 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,
763a0fec 432 ],
433 1 => [
bc0f3965 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,
763a0fec 440 ],
441 ], $pairs);
be61083d 442
763a0fec 443 $this->callAPISuccess('GroupContact', 'create', [
444 'group_id' => $groupID,
445 'contact_id' => $this->contacts[5]['id'],
446 ]);
be61083d 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
763a0fec 456 $this->assertEquals([
457 0 => [
be61083d 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,
763a0fec 464 ],
465 1 => [
be61083d 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,
763a0fec 472 ],
473 2 => [
be61083d 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,
763a0fec 480 ],
481 ], $pairs);
bc0f3965 482 }
483
3308aac0 484 /**
485 * Test function that gets duplicate pairs.
486 *
763a0fec 487 * It turns out there are 2 code paths retrieving this data so my initial
488 * focus is on ensuring they match.
3308aac0 489 */
490 public function testGetMatchesInGroup() {
491 $this->setupMatchData();
492
763a0fec 493 $groupID = $this->groupCreate(['title' => 'she-mice']);
3308aac0 494
763a0fec 495 $this->callAPISuccess('GroupContact', 'create', [
496 'group_id' => $groupID,
497 'contact_id' => $this->contacts[3]['id'],
498 ]);
3308aac0 499
500 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
501 1,
502 $groupID,
503 TRUE,
504 25,
505 FALSE
506 );
507
763a0fec 508 $this->assertEquals([
509 0 => [
3308aac0 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,
763a0fec 516 ],
517 ], $pairs);
3308aac0 518 }
519
0608e1e0 520 /**
521 * Test the special info handling is unchanged after cleanup.
522 *
763a0fec 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.
0608e1e0 525 */
2f10fa02 526 public function testGetRowsElementsAndInfoSpecialInfo() {
763a0fec 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 ]);
0608e1e0 541 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($contact1, $contact2);
542 $rows = $rowsElementsAndInfo['rows'];
763a0fec 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']);
0608e1e0 563 $this->assertEquals(1, $rowsElementsAndInfo['migration_info']['move_communication_style_id']);
763a0fec 564 $this->assertEquals([
565 'main' => '',
566 'other' => 'SMS, Fax',
567 'title' => 'Preferred Communication Method',
568 ], $rows['move_preferred_communication_method']);
0608e1e0 569 $this->assertEquals('\ 14\ 15\ 1', $rowsElementsAndInfo['migration_info']['move_preferred_communication_method']);
570 }
571
c231c0dd
JP
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();
2f10fa02 583 $this->callAPISuccess('Membership', 'create', [
c231c0dd
JP
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,
39b959db 599 'operation' => ['move_rel_table_memberships' => ['add' => 1]],
c231c0dd
JP
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
0946ab3f 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() {
dba77ae8 615 // Create Custom Field
763a0fec 616 $createGroup = $this->setupCustomGroupForIndividual();
dba77ae8
CR
617 $createField = $this->setupCustomField('Graduation', $createGroup);
618 $customFieldName = "custom_" . $createField['id'];
619
620 // Contacts setup
0946ab3f 621 $this->setupMatchData();
622
623 $originalContactID = $this->contacts[0]['id'];
39b959db
SL
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'];
0946ab3f 628
0946ab3f 629 // update the text custom field for original contact with value 'abc'
763a0fec 630 $this->callAPISuccess('Contact', 'create', [
0946ab3f 631 'id' => $originalContactID,
dba77ae8 632 "{$customFieldName}" => 'abc',
763a0fec 633 ]);
dba77ae8
CR
634 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
635
0946ab3f 636 // update the text custom field for duplicate contact 1 with value 'def'
763a0fec 637 $this->callAPISuccess('Contact', 'create', [
0946ab3f 638 'id' => $duplicateContactID1,
dba77ae8 639 "{$customFieldName}" => 'def',
763a0fec 640 ]);
dba77ae8
CR
641 $this->assertCustomFieldValue($duplicateContactID1, 'def', $customFieldName);
642
0946ab3f 643 // update the text custom field for duplicate contact 2 with value 'ghi'
763a0fec 644 $this->callAPISuccess('Contact', 'create', [
0946ab3f 645 'id' => $duplicateContactID2,
dba77ae8 646 "{$customFieldName}" => 'ghi',
763a0fec 647 ]);
dba77ae8 648 $this->assertCustomFieldValue($duplicateContactID2, 'ghi', $customFieldName);
0946ab3f 649
650 /*** USE-CASE 1: DO NOT OVERWRITE CUSTOM FIELD VALUE **/
763a0fec 651 $this->mergeContacts($originalContactID, $duplicateContactID1, [
39b959db 652 "move_{$customFieldName}" => NULL,
763a0fec 653 ]);
dba77ae8 654 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
0946ab3f 655
656 /*** USE-CASE 2: OVERWRITE CUSTOM FIELD VALUE **/
763a0fec 657 $this->mergeContacts($originalContactID, $duplicateContactID2, [
dba77ae8 658 "move_{$customFieldName}" => 'ghi',
763a0fec 659 ]);
dba77ae8
CR
660 $this->assertCustomFieldValue($originalContactID, 'ghi', $customFieldName);
661
662 // cleanup created custom set
763a0fec 663 $this->callAPISuccess('CustomField', 'delete', ['id' => $createField['id']]);
664 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
dba77ae8
CR
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
763a0fec 675 $createGroup = $this->setupCustomGroupForIndividual();
dba77ae8
CR
676 $customField1 = $this->setupCustomField('TestField', $createGroup);
677
c1955865
J
678 // Create multi-value custom field
679 $multiGroup = $this->CustomGroupMultipleCreateByParams();
763a0fec 680 $multiField = $this->customFieldCreate([
c1955865
J
681 'custom_group_id' => $multiGroup['id'],
682 'label' => 'field_1' . $multiGroup['id'],
683 'in_selector' => 1,
763a0fec 684 ]);
c1955865 685
dba77ae8
CR
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
763a0fec 692 $this->callAPISuccess('Contact', 'create', [
dba77ae8
CR
693 'id' => $duplicateContactID,
694 "custom_{$customField1['id']}" => 'abc',
c1955865 695 "custom_{$multiField['id']}" => 'def',
763a0fec 696 ]);
dba77ae8 697 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
c1955865 698 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$multiField['id']}");
dba77ae8 699
c1955865 700 // Merge, and ensure that no value was migrated
763a0fec 701 $this->mergeContacts($originalContactID, $duplicateContactID, [
ee3b1d86 702 "move_custom_{$customField1['id']}" => NULL,
c1955865 703 "move_rel_table_custom_{$multiGroup['id']}" => NULL,
763a0fec 704 ]);
dba77ae8 705 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
c1955865 706 $this->assertCustomFieldValue($originalContactID, '', "custom_{$multiField['id']}");
dba77ae8
CR
707
708 // cleanup created custom set
763a0fec 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']]);
dba77ae8
CR
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
763a0fec 723 $createGroup = $this->setupCustomGroupForIndividual();
dba77ae8
CR
724 $customField1 = $this->setupCustomField('Test1', $createGroup);
725 $customField2 = $this->setupCustomField('Test2', $createGroup);
726
c1955865
J
727 // Create multi-value custom field
728 $multiGroup = $this->CustomGroupMultipleCreateByParams();
763a0fec 729 $multiField = $this->customFieldCreate([
c1955865
J
730 'custom_group_id' => $multiGroup['id'],
731 'label' => 'field_1' . $multiGroup['id'],
732 'in_selector' => 1,
763a0fec 733 ]);
c1955865 734
dba77ae8
CR
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
763a0fec 741 $this->callAPISuccess('Contact', 'create', [
dba77ae8
CR
742 'id' => $duplicateContactID,
743 "custom_{$customField1['id']}" => 'abc',
744 "custom_{$customField2['id']}" => 'def',
c1955865 745 "custom_{$multiField['id']}" => 'ghi',
763a0fec 746 ]);
dba77ae8
CR
747 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
748 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$customField2['id']}");
c1955865 749 $this->assertCustomFieldValue($duplicateContactID, 'ghi', "custom_{$multiField['id']}");
dba77ae8
CR
750
751 // Perform merge
763a0fec 752 $this->mergeContacts($originalContactID, $duplicateContactID, [
ee3b1d86 753 "move_custom_{$customField1['id']}" => NULL,
dba77ae8 754 "move_custom_{$customField2['id']}" => 'def',
c1955865 755 "move_rel_table_custom_{$multiGroup['id']}" => '1',
763a0fec 756 ]);
dba77ae8
CR
757 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
758 $this->assertCustomFieldValue($originalContactID, 'def', "custom_{$customField2['id']}");
c1955865 759 $this->assertCustomFieldValue($originalContactID, 'ghi', "custom_{$multiField['id']}");
dba77ae8
CR
760
761 // cleanup created custom set
763a0fec 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']]);
dba77ae8
CR
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>]
2f10fa02 779 *
780 * @throws \CRM_Core_Exception
781 * @throws \CiviCRM_API3_Exception
dba77ae8
CR
782 */
783 private function mergeContacts($originalContactID, $duplicateContactID, $params) {
784 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
785
763a0fec 786 $migrationData = [
0946ab3f 787 'main_details' => $rowsElementsAndInfo['main_details'],
788 'other_details' => $rowsElementsAndInfo['other_details'],
763a0fec 789 ];
dba77ae8
CR
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) {
763a0fec 804 $data = $this->callAPISuccess('Contact', 'getsingle', [
dba77ae8 805 'id' => $contactID,
763a0fec 806 'return' => [$customFieldName],
807 ]);
0946ab3f 808
dba77ae8
CR
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() {
763a0fec 819 $customGroup = $this->callAPISuccess('custom_group', 'get', [
dba77ae8 820 'name' => 'test_group',
763a0fec 821 ]);
dba77ae8
CR
822
823 if ($customGroup['count'] > 0) {
763a0fec 824 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $customGroup['id']]);
dba77ae8
CR
825 }
826
763a0fec 827 $customGroup = $this->callAPISuccess('custom_group', 'create', [
dba77ae8
CR
828 'title' => 'Test_Group',
829 'name' => 'test_group',
763a0fec 830 'extends' => ['Individual'],
dba77ae8
CR
831 'style' => 'Inline',
832 'is_multiple' => FALSE,
833 'is_active' => 1,
763a0fec 834 ]);
dba77ae8
CR
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) {
763a0fec 850 return $this->callAPISuccess('custom_field', 'create', [
dba77ae8
CR
851 'label' => $fieldLabel,
852 'data_type' => 'Alphanumeric',
853 'html_type' => 'Text',
854 'custom_group_id' => $createGroup['id'],
763a0fec 855 ]);
0946ab3f 856 }
857
3308aac0 858 /**
859 * Set up some contacts for our matching.
860 */
2988f5c7 861 public function setupMatchData() {
763a0fec 862 $fixtures = [
863 [
2988f5c7 864 'first_name' => 'Mickey',
865 'last_name' => 'Mouse',
866 'email' => 'mickey@mouse.com',
763a0fec 867 ],
868 [
2988f5c7 869 'first_name' => 'Mickey',
870 'last_name' => 'Mouse',
871 'email' => 'mickey@mouse.com',
763a0fec 872 ],
873 [
2988f5c7 874 'first_name' => 'Minnie',
875 'last_name' => 'Mouse',
876 'email' => 'mickey@mouse.com',
763a0fec 877 ],
878 [
3308aac0 879 'first_name' => 'Minnie',
880 'last_name' => 'Mouse',
881 'email' => 'mickey@mouse.com',
763a0fec 882 ],
883 ];
2988f5c7 884 foreach ($fixtures as $fixture) {
885 $contactID = $this->individualCreate($fixture);
763a0fec 886 $this->contacts[] = array_merge($fixture, ['id' => $contactID]);
bc0f3965 887 }
763a0fec 888 $organizationFixtures = [
889 [
bc0f3965 890 'organization_name' => 'Walt Disney Ltd',
891 'email' => 'walt@disney.com',
763a0fec 892 ],
893 [
bc0f3965 894 'organization_name' => 'Walt Disney Ltd',
895 'email' => 'walt@disney.com',
763a0fec 896 ],
897 [
bc0f3965 898 'organization_name' => 'Walt Disney',
899 'email' => 'walt@disney.com',
763a0fec 900 ],
901 [
bc0f3965 902 'organization_name' => 'Walt Disney',
903 'email' => 'walter@disney.com',
763a0fec 904 ],
905 ];
bc0f3965 906 foreach ($organizationFixtures as $fixture) {
907 $contactID = $this->organizationCreate($fixture);
763a0fec 908 $this->contacts[] = array_merge($fixture, ['id' => $contactID]);
2988f5c7 909 }
910 }
911
bf17fa88 912 /**
913 * Get the list of tables that refer to the CID.
914 *
915 * This is a statically maintained (in this test list).
916 *
763a0fec 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.
bf17fa88 919 */
920 public function getStaticCIDRefs() {
763a0fec 921 return [
922 'civicrm_acl_cache' => [
bf17fa88 923 0 => 'contact_id',
763a0fec 924 ],
925 'civicrm_acl_contact_cache' => [
dbb4e4f9 926 0 => 'contact_id',
763a0fec 927 ],
928 'civicrm_action_log' => [
bf17fa88 929 0 => 'contact_id',
763a0fec 930 ],
931 'civicrm_activity_contact' => [
bf17fa88 932 0 => 'contact_id',
763a0fec 933 ],
934 'civicrm_address' => [
bf17fa88 935 0 => 'contact_id',
763a0fec 936 ],
937 'civicrm_batch' => [
bf17fa88 938 0 => 'created_id',
939 1 => 'modified_id',
763a0fec 940 ],
941 'civicrm_campaign' => [
bf17fa88 942 0 => 'created_id',
943 1 => 'last_modified_id',
763a0fec 944 ],
945 'civicrm_case_contact' => [
bf17fa88 946 0 => 'contact_id',
763a0fec 947 ],
948 'civicrm_contact' => [
bf17fa88 949 0 => 'primary_contact_id',
950 1 => 'employer_id',
763a0fec 951 ],
952 'civicrm_contribution' => [
bf17fa88 953 0 => 'contact_id',
763a0fec 954 ],
955 'civicrm_contribution_page' => [
bf17fa88 956 0 => 'created_id',
763a0fec 957 ],
958 'civicrm_contribution_recur' => [
bf17fa88 959 0 => 'contact_id',
763a0fec 960 ],
961 'civicrm_contribution_soft' => [
bf17fa88 962 0 => 'contact_id',
763a0fec 963 ],
964 'civicrm_custom_group' => [
bf17fa88 965 0 => 'created_id',
763a0fec 966 ],
967 'civicrm_dashboard_contact' => [
bf17fa88 968 0 => 'contact_id',
763a0fec 969 ],
970 'civicrm_dedupe_exception' => [
bf17fa88 971 0 => 'contact_id1',
972 1 => 'contact_id2',
763a0fec 973 ],
974 'civicrm_domain' => [
bf17fa88 975 0 => 'contact_id',
763a0fec 976 ],
977 'civicrm_email' => [
bf17fa88 978 0 => 'contact_id',
763a0fec 979 ],
980 'civicrm_event' => [
bf17fa88 981 0 => 'created_id',
763a0fec 982 ],
983 'civicrm_event_carts' => [
bf17fa88 984 0 => 'user_id',
763a0fec 985 ],
986 'civicrm_financial_account' => [
bf17fa88 987 0 => 'contact_id',
763a0fec 988 ],
989 'civicrm_financial_item' => [
bf17fa88 990 0 => 'contact_id',
763a0fec 991 ],
992 'civicrm_grant' => [
bf17fa88 993 0 => 'contact_id',
763a0fec 994 ],
995 'civicrm_group' => [
bf17fa88 996 0 => 'created_id',
997 1 => 'modified_id',
763a0fec 998 ],
999 'civicrm_group_contact' => [
bf17fa88 1000 0 => 'contact_id',
763a0fec 1001 ],
1002 'civicrm_group_contact_cache' => [
bf17fa88 1003 0 => 'contact_id',
763a0fec 1004 ],
1005 'civicrm_group_organization' => [
bf17fa88 1006 0 => 'organization_id',
763a0fec 1007 ],
1008 'civicrm_im' => [
bf17fa88 1009 0 => 'contact_id',
763a0fec 1010 ],
1011 'civicrm_log' => [
bf17fa88 1012 0 => 'modified_id',
763a0fec 1013 ],
1014 'civicrm_mailing' => [
bf17fa88 1015 0 => 'created_id',
1016 1 => 'scheduled_id',
1017 2 => 'approver_id',
763a0fec 1018 ],
1019 'civicrm_file' => [
ae2c7e00 1020 'created_id',
763a0fec 1021 ],
1022 'civicrm_mailing_abtest' => [
bf17fa88 1023 0 => 'created_id',
763a0fec 1024 ],
1025 'civicrm_mailing_event_queue' => [
bf17fa88 1026 0 => 'contact_id',
763a0fec 1027 ],
1028 'civicrm_mailing_event_subscribe' => [
bf17fa88 1029 0 => 'contact_id',
763a0fec 1030 ],
1031 'civicrm_mailing_recipients' => [
bf17fa88 1032 0 => 'contact_id',
763a0fec 1033 ],
1034 'civicrm_membership' => [
bf17fa88 1035 0 => 'contact_id',
763a0fec 1036 ],
1037 'civicrm_membership_log' => [
bf17fa88 1038 0 => 'modified_id',
763a0fec 1039 ],
1040 'civicrm_membership_type' => [
bf17fa88 1041 0 => 'member_of_contact_id',
763a0fec 1042 ],
1043 'civicrm_note' => [
bf17fa88 1044 0 => 'contact_id',
763a0fec 1045 ],
1046 'civicrm_openid' => [
bf17fa88 1047 0 => 'contact_id',
763a0fec 1048 ],
1049 'civicrm_participant' => [
bf17fa88 1050 0 => 'contact_id',
39b959db
SL
1051 //CRM-16761
1052 1 => 'transferred_to_contact_id',
763a0fec 1053 ],
1054 'civicrm_payment_token' => [
bf17fa88 1055 0 => 'contact_id',
1056 1 => 'created_id',
763a0fec 1057 ],
1058 'civicrm_pcp' => [
bf17fa88 1059 0 => 'contact_id',
763a0fec 1060 ],
1061 'civicrm_phone' => [
bf17fa88 1062 0 => 'contact_id',
763a0fec 1063 ],
1064 'civicrm_pledge' => [
bf17fa88 1065 0 => 'contact_id',
763a0fec 1066 ],
1067 'civicrm_print_label' => [
bf17fa88 1068 0 => 'created_id',
763a0fec 1069 ],
1070 'civicrm_relationship' => [
bf17fa88 1071 0 => 'contact_id_a',
1072 1 => 'contact_id_b',
763a0fec 1073 ],
1074 'civicrm_report_instance' => [
bf17fa88 1075 0 => 'created_id',
1076 1 => 'owner_id',
763a0fec 1077 ],
1078 'civicrm_setting' => [
bf17fa88 1079 0 => 'contact_id',
1080 1 => 'created_id',
763a0fec 1081 ],
1082 'civicrm_subscription_history' => [
bf17fa88 1083 0 => 'contact_id',
763a0fec 1084 ],
1085 'civicrm_survey' => [
bf17fa88 1086 0 => 'created_id',
1087 1 => 'last_modified_id',
763a0fec 1088 ],
1089 'civicrm_tag' => [
bf17fa88 1090 0 => 'created_id',
763a0fec 1091 ],
1092 'civicrm_uf_group' => [
bf17fa88 1093 0 => 'created_id',
763a0fec 1094 ],
1095 'civicrm_uf_match' => [
bf17fa88 1096 0 => 'contact_id',
763a0fec 1097 ],
1098 'civicrm_value_testgetcidref_1' => [
bf17fa88 1099 0 => 'entity_id',
763a0fec 1100 ],
1101 'civicrm_website' => [
bf17fa88 1102 0 => 'contact_id',
763a0fec 1103 ],
1104 ];
bf17fa88 1105 }
1106
1107 /**
1108 * Get a list of CIDs that is calculated off the schema.
1109 *
763a0fec 1110 * Note this is an expensive and table locking query. Should be safe in tests
1111 * though.
bf17fa88 1112 */
1113 public function getCalculatedCIDRefs() {
763a0fec 1114 $cidRefs = [];
bf17fa88 1115 $sql = "
1116SELECT
1117 table_name,
1118 column_name
1119FROM information_schema.key_column_usage
1120WHERE
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';
dbb4e4f9 1134 $cidRefs['civicrm_acl_contact_cache'][0] = 'contact_id';
bf17fa88 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
0622d221 1141}