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