525c539d05cd84c69e9fea152252343c86ceeb85
[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 * Contacts created for the test.
16 *
17 * Overlaps contactIds....
18 *
19 * @var array
20 */
21 protected $contacts = [];
22
23 /**
24 * Tear down.
25 *
26 * @throws \CRM_Core_Exception
27 */
28 public function tearDown(): void {
29 $this->quickCleanup([
30 'civicrm_contact',
31 'civicrm_group_contact',
32 'civicrm_group',
33 'civicrm_prevnext_cache',
34 ]);
35 parent::tearDown();
36 }
37
38 /**
39 * @throws \CRM_Core_Exception
40 */
41 public function createDupeContacts(): void {
42 // create a group to hold contacts, so that dupe checks don't consider any other contacts in the DB
43 $params = [
44 'name' => 'Test Dupe Merger Group',
45 'title' => 'Test Dupe Merger Group',
46 'domain_id' => 1,
47 'is_active' => 1,
48 'visibility' => 'Public Pages',
49 ];
50
51 $result = $this->callAPISuccess('group', 'create', $params);
52 $this->_groupId = $result['id'];
53
54 // contact data set
55
56 // make dupe checks based on based on following contact sets:
57 // FIRST - LAST - EMAIL
58 // ---------------------------------
59 // robin - hood - robin@example.com
60 // robin - hood - robin@example.com
61 // robin - hood - hood@example.com
62 // robin - dale - robin@example.com
63 // little - dale - dale@example.com
64 // little - dale - dale@example.com
65 // will - dale - dale@example.com
66 // will - dale - will@example.com
67 // will - dale - will@example.com
68 $params = [
69 [
70 'first_name' => 'robin',
71 'last_name' => 'hood',
72 'email' => 'robin@example.com',
73 'contact_type' => 'Individual',
74 ],
75 [
76 'first_name' => 'robin',
77 'last_name' => 'hood',
78 'email' => 'robin@example.com',
79 'contact_type' => 'Individual',
80 ],
81 [
82 'first_name' => 'robin',
83 'last_name' => 'hood',
84 'email' => 'hood@example.com',
85 'contact_type' => 'Individual',
86 ],
87 [
88 'first_name' => 'robin',
89 'last_name' => 'dale',
90 'email' => 'robin@example.com',
91 'contact_type' => 'Individual',
92 ],
93 [
94 'first_name' => 'little',
95 'last_name' => 'dale',
96 'email' => 'dale@example.com',
97 'contact_type' => 'Individual',
98 ],
99 [
100 'first_name' => 'little',
101 'last_name' => 'dale',
102 'email' => 'dale@example.com',
103 'contact_type' => 'Individual',
104 ],
105 [
106 'first_name' => 'will',
107 'last_name' => 'dale',
108 'email' => 'dale@example.com',
109 'contact_type' => 'Individual',
110 ],
111 [
112 'first_name' => 'will',
113 'last_name' => 'dale',
114 'email' => 'will@example.com',
115 'contact_type' => 'Individual',
116 ],
117 [
118 'first_name' => 'will',
119 'last_name' => 'dale',
120 'email' => 'will@example.com',
121 'contact_type' => 'Individual',
122 ],
123 ];
124
125 $count = 1;
126 foreach ($params as $param) {
127 $param['version'] = 3;
128 $contact = civicrm_api('contact', 'create', $param);
129 $this->_contactIds[$count++] = $contact['id'];
130
131 $grpParams = [
132 'contact_id' => $contact['id'],
133 'group_id' => $this->_groupId,
134 'version' => 3,
135 ];
136 $this->callAPISuccess('group_contact', 'create', $grpParams);
137 }
138 }
139
140 /**
141 * Delete all created contacts.
142 */
143 public function deleteDupeContacts(): void {
144 foreach ($this->_contactIds as $contactId) {
145 $this->contactDelete($contactId);
146 }
147 $this->groupDelete($this->_groupId);
148 }
149
150 /**
151 * Test the batch merge.
152 *
153 * @throws \API_Exception
154 * @throws \CRM_Core_Exception
155 * @throws \CiviCRM_API3_Exception
156 */
157 public function testBatchMergeSelectedDuplicates(): void {
158 $this->createDupeContacts();
159
160 // verify that all contacts have been created separately
161 $this->assertEquals(count($this->_contactIds), 9, 'Check for number of contacts.');
162
163 $dao = new CRM_Dedupe_DAO_RuleGroup();
164 $dao->contact_type = 'Individual';
165 $dao->name = 'IndividualSupervised';
166 $dao->is_default = 1;
167 $dao->find(TRUE);
168
169 $foundDupes = CRM_Dedupe_Finder::dupesInGroup($dao->id, $this->_groupId);
170
171 // -------------------------------------------------------------------------
172 // Name and Email (reserved) Matches ( 3 pairs )
173 // --------------------------------------------------------------------------
174 // robin - hood - robin@example.com
175 // robin - hood - robin@example.com
176 // little - dale - dale@example.com
177 // little - dale - dale@example.com
178 // will - dale - will@example.com
179 // will - dale - will@example.com
180 // so 3 pairs for - first + last + mail
181 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
182
183 // Run dedupe finder as the browser would
184 //avoid invalid key error
185 $_SERVER['REQUEST_METHOD'] = 'GET';
186 $object = new CRM_Contact_Page_DedupeFind();
187 $object->set('gid', $this->_groupId);
188 $object->set('rgid', $dao->id);
189 $object->set('action', CRM_Core_Action::UPDATE);
190 $object->setEmbedded(TRUE);
191 @$object->run();
192
193 // Retrieve pairs from prev next cache table
194 $select = ['pn.is_selected' => 'is_selected'];
195 $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($dao->id, $this->_groupId, [], TRUE, 0);
196 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
197 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
198
199 // mark first two pairs as selected
200 CRM_Core_DAO::singleValueQuery("UPDATE civicrm_prevnext_cache SET is_selected = 1 WHERE id IN ({$pnDupePairs[0]['prevnext_id']}, {$pnDupePairs[1]['prevnext_id']})");
201
202 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
203 $this->assertEquals(1, $pnDupePairs[0]['is_selected'], 'Check if first record in dupe pairs is marked as selected.');
204 $this->assertEquals(1, $pnDupePairs[0]['is_selected'], 'Check if second record in dupe pairs is marked as selected.');
205
206 // batch merge selected dupes
207 $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', 5, 1);
208 $this->assertEquals(count($result['merged']), 2, 'Check number of merged pairs.');
209
210 $stats = $this->callAPISuccess('Dedupe', 'getstatistics', [
211 'group_id' => $this->_groupId,
212 'rule_group_id' => $dao->id,
213 'check_permissions' => TRUE,
214 ])['values'];
215 $this->assertEquals(['merged' => 2, 'skipped' => 0], $stats);
216
217 // retrieve pairs from prev next cache table
218 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
219 $this->assertEquals(count($pnDupePairs), 1, 'Check number of remaining dupe pairs in prev next cache.');
220
221 $this->deleteDupeContacts();
222 }
223
224 /**
225 * Test the batch merge.
226 */
227 public function testBatchMergeAllDuplicates() {
228 $this->createDupeContacts();
229
230 // verify that all contacts have been created separately
231 $this->assertEquals(count($this->_contactIds), 9, 'Check for number of contacts.');
232
233 $dao = new CRM_Dedupe_DAO_RuleGroup();
234 $dao->contact_type = 'Individual';
235 $dao->name = 'IndividualSupervised';
236 $dao->is_default = 1;
237 $dao->find(TRUE);
238
239 $foundDupes = CRM_Dedupe_Finder::dupesInGroup($dao->id, $this->_groupId);
240
241 // -------------------------------------------------------------------------
242 // Name and Email (reserved) Matches ( 3 pairs )
243 // --------------------------------------------------------------------------
244 // robin - hood - robin@example.com
245 // robin - hood - robin@example.com
246 // little - dale - dale@example.com
247 // little - dale - dale@example.com
248 // will - dale - will@example.com
249 // will - dale - will@example.com
250 // so 3 pairs for - first + last + mail
251 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
252
253 // Run dedupe finder as the browser would
254 //avoid invalid key error
255 $_SERVER['REQUEST_METHOD'] = 'GET';
256 $object = new CRM_Contact_Page_DedupeFind();
257 $object->set('gid', $this->_groupId);
258 $object->set('rgid', $dao->id);
259 $object->set('action', CRM_Core_Action::UPDATE);
260 $object->setEmbedded(TRUE);
261 @$object->run();
262
263 // Retrieve pairs from prev next cache table
264 $select = ['pn.is_selected' => 'is_selected'];
265 $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($dao->id, $this->_groupId, [], TRUE, 0);
266 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
267
268 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
269
270 // batch merge all dupes
271 $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', 5, 2);
272 $this->assertEquals(count($result['merged']), 3, 'Check number of merged pairs.');
273
274 $stats = $this->callAPISuccess('Dedupe', 'getstatistics', [
275 'rule_group_id' => $dao->id,
276 'group_id' => $this->_groupId,
277 ]);
278 // retrieve pairs from prev next cache table
279 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
280 $this->assertEquals(count($pnDupePairs), 0, 'Check number of remaining dupe pairs in prev next cache.');
281
282 $this->deleteDupeContacts();
283 }
284
285 /**
286 * The goal of this function is to test that all required tables are returned.
287 */
288 public function testGetCidRefs(): void {
289 $sortRefs = function($a) {
290 ksort($a);
291 foreach ($a as &$fields) {
292 sort($fields);
293 }
294 return $a;
295 };
296
297 $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, 'Contacts');
298
299 // These are deliberately unset.
300 $unsetRefs = array_fill_keys(['civicrm_group_contact_cache', 'civicrm_acl_cache', 'civicrm_acl_contact_cache'], 1);
301 $this->assertEquals($sortRefs(array_diff_key($this->getStaticCIDRefs(), $unsetRefs)), $sortRefs(CRM_Dedupe_Merger::cidRefs()));
302 $this->assertEquals($sortRefs(array_diff_key($this->getCalculatedCIDRefs(), $unsetRefs)), $sortRefs(CRM_Dedupe_Merger::cidRefs()));
303
304 // These are deliberately unset.
305 // $unsetRefs = array_fill_keys(['civicrm_group_contact_cache', 'civicrm_acl_cache', 'civicrm_acl_contact_cache', 'civicrm_relationship_cache'], 1);
306 // $this->assertEquals(array_diff_key($this->getStaticCIDRefs(), $unsetRefs), CRM_Dedupe_Merger::cidRefs());
307 // $this->assertEquals(array_diff_key($this->getCalculatedCIDRefs(), $unsetRefs), CRM_Dedupe_Merger::cidRefs());
308 }
309
310 /**
311 * Test function that gets duplicate pairs.
312 *
313 * It turns out there are 2 code paths retrieving this data so my initial
314 * focus is on ensuring they match.
315 *
316 * @throws \CRM_Core_Exception
317 */
318 public function testGetMatches(): void {
319 $this->setupMatchData();
320
321 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
322 'rule_group_id' => 1,
323 ])['values'];
324 $this->assertEquals([
325 0 => [
326 'srcID' => $this->contacts[1]['id'],
327 'srcName' => 'Mr. Mickey Mouse II',
328 'dstID' => $this->contacts[0]['id'],
329 'dstName' => 'Mr. Mickey Mouse II',
330 'weight' => 20,
331 'canMerge' => TRUE,
332 ],
333 1 => [
334 'srcID' => $this->contacts[3]['id'],
335 'srcName' => 'Mr. Minnie Mouse II',
336 'dstID' => $this->contacts[2]['id'],
337 'dstName' => 'Mr. Minnie Mouse II',
338 'weight' => 20,
339 'canMerge' => TRUE,
340 ],
341 ], $pairs);
342 }
343
344 /**
345 * Test function that gets duplicate pairs.
346 *
347 * It turns out there are 2 code paths retrieving this data so my initial
348 * focus is on ensuring they match.
349 *
350 * @dataProvider getBooleanDataProvider
351 *
352 * @param bool $isReverse
353 *
354 * @throws \CRM_Core_Exception
355 */
356 public function testGetMatchesExcludeDeleted(bool $isReverse): void {
357 $this->setupMatchData();
358 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
359 'rule_group_id' => 1,
360 'check_permissions' => TRUE,
361 'criteria' => ['Contact' => ['id' => 'IS NOT NULL']],
362 ])['values'];
363 $this->assertCount(2, $pairs);
364 $this->callAPISuccess('Contact', 'delete', ['id' => ($isReverse ? $pairs[0]['dstID'] : $pairs[0]['srcID'])]);
365 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
366 'rule_group_id' => 1,
367 'check_permissions' => TRUE,
368 'criteria' => ['Contact' => ['id' => ['>' => 1]]],
369 ])['values'];
370 $this->assertCount(1, $pairs);
371 }
372
373 /**
374 * Test that location type is ignored when deduping by postal address.
375 *
376 * @throws \CRM_Core_Exception
377 * @throws \CiviCRM_API3_Exception
378 */
379 public function testGetMatchesIgnoreLocationType(): void {
380 $contact1 = $this->individualCreate();
381 $contact2 = $this->individualCreate();
382 $this->callAPISuccess('address', 'create', [
383 'contact_id' => $contact1,
384 'state_province_id' => 1049,
385 'location_type_id' => 1,
386 ]);
387 $this->callAPISuccess('address', 'create', [
388 'contact_id' => $contact2,
389 'state_province_id' => 1049,
390 'location_type_id' => 2,
391 ]);
392 $ruleGroup = $this->createRuleGroup();
393 $this->callAPISuccess('Rule', 'create', [
394 'dedupe_rule_group_id' => $ruleGroup['id'],
395 'rule_table' => 'civicrm_address',
396 'rule_field' => 'state_province_id',
397 'rule_weight' => 8,
398 ]);
399 $dupeCount = $this->callAPISuccess('Dedupe', 'getduplicates', [
400 'rule_group_id' => $ruleGroup['id'],
401 ])['count'];
402 $this->assertEquals(1, $dupeCount);
403 }
404
405 /**
406 * Test results are returned when criteria are passed in.
407 *
408 * @throws \CRM_Core_Exception
409 * @throws \CiviCRM_API3_Exception
410 */
411 public function testGetMatchesCriteriaMatched(): void {
412 $this->setupMatchData();
413 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
414 'rule_group_id' => 1,
415 'criteria' => ['contact' => ['id' => ['>' => 1]]],
416 ])['values'];
417 $this->assertCount(2, $pairs);
418 }
419
420 /**
421 * Test results are returned when criteria are passed in & limit is respected.
422 *
423 * @throws \CRM_Core_Exception
424 * @throws \CiviCRM_API3_Exception
425 */
426 public function testGetMatchesCriteriaMatchedWithLimit(): void {
427 $this->setupMatchData();
428 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
429 'rule_group_id' => 1,
430 'criteria' => ['contact' => ['id' => ['>' => 1]]],
431 'options' => ['limit' => 1],
432 ])['values'];
433 $this->assertCount(1, $pairs);
434 }
435
436 /**
437 * Test results are returned when criteria are passed in & limit is
438 * respected.
439 *
440 * @throws \CRM_Core_Exception
441 * @throws \CiviCRM_API3_Exception
442 */
443 public function testGetMatchesCriteriaMatchedWithSearchLimit(): void {
444 $this->setupMatchData();
445 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
446 'rule_group_id' => 1,
447 'criteria' => ['contact' => ['id' => ['>' => 1]]],
448 'search_limit' => 1,
449 ])['values'];
450 $this->assertCount(1, $pairs);
451 }
452
453 /**
454 * Test getting matches where there are no criteria.
455 *
456 * @throws \CRM_Core_Exception
457 * @throws \CiviCRM_API3_Exception
458 */
459 public function testGetMatchesNoCriteria(): void {
460 $this->setupMatchData();
461 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
462 'rule_group_id' => 1,
463 ])['values'];
464 $this->assertCount(2, $pairs);
465 }
466
467 /**
468 * Test getting matches with a limit in play.
469 *
470 * @throws \CRM_Core_Exception
471 * @throws \CiviCRM_API3_Exception
472 */
473 public function testGetMatchesNoCriteriaButLimit(): void {
474 $this->setupMatchData();
475 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
476 'rule_group_id' => 1,
477 'options' => ['limit' => 1],
478 ])['values'];
479 $this->assertCount(1, $pairs);
480 }
481
482 /**
483 * Test that if criteria are passed and there are no matching contacts no matches are returned.
484 */
485 public function testGetMatchesCriteriaNotMatched() {
486 $this->setupMatchData();
487 $pairs = $this->callAPISuccess('Dedupe', 'getduplicates', [
488 'rule_group_id' => 1,
489 'criteria' => ['contact' => ['id' => ['>' => 100000]]],
490 ])['values'];
491 $this->assertCount(0, $pairs);
492 }
493
494 /**
495 * Test function that gets organization pairs.
496 *
497 * Note the rule will match on organization_name OR email - hence lots of
498 * matches.
499 *
500 * @throws \Exception
501 */
502 public function testGetOrganizationMatches(): void {
503 $this->setupMatchData();
504 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
505 'contact_type' => 'Organization',
506 'used' => 'Supervised',
507 ]);
508
509 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
510 $ruleGroups['id'],
511 NULL,
512 TRUE,
513 25,
514 FALSE
515 );
516
517 $expectedPairs = [
518 0 => [
519 'srcID' => $this->contacts[5]['id'],
520 'srcName' => 'Walt Disney Ltd',
521 'dstID' => $this->contacts[4]['id'],
522 'dstName' => 'Walt Disney Ltd',
523 'weight' => 20,
524 'canMerge' => TRUE,
525 ],
526 1 => [
527 'srcID' => $this->contacts[7]['id'],
528 'srcName' => 'Walt Disney',
529 'dstID' => $this->contacts[6]['id'],
530 'dstName' => 'Walt Disney',
531 'weight' => 10,
532 'canMerge' => TRUE,
533 ],
534 2 => [
535 'srcID' => $this->contacts[6]['id'],
536 'srcName' => 'Walt Disney',
537 'dstID' => $this->contacts[4]['id'],
538 'dstName' => 'Walt Disney Ltd',
539 'weight' => 10,
540 'canMerge' => TRUE,
541 ],
542 3 => [
543 'srcID' => $this->contacts[6]['id'],
544 'srcName' => 'Walt Disney',
545 'dstID' => $this->contacts[5]['id'],
546 'dstName' => 'Walt Disney Ltd',
547 'weight' => 10,
548 'canMerge' => TRUE,
549 ],
550 ];
551 usort($pairs, [__CLASS__, 'compareDupes']);
552 usort($expectedPairs, [__CLASS__, 'compareDupes']);
553 $this->assertEquals($expectedPairs, $pairs);
554 }
555
556 /**
557 * Function to sort $duplicate records in a stable way.
558 *
559 * @param array $a
560 * @param array $b
561 *
562 * @return int
563 */
564 public static function compareDupes($a, $b) {
565 foreach (['srcName', 'dstName', 'srcID', 'dstID'] as $field) {
566 if ($a[$field] != $b[$field]) {
567 return ($a[$field] < $b[$field]) ? 1 : -1;
568 }
569 }
570 return 0;
571 }
572
573 /**
574 * Test function that gets organization duplicate pairs.
575 *
576 * @throws \Exception
577 */
578 public function testGetOrganizationMatchesInGroup() {
579 $this->setupMatchData();
580 $ruleGroups = $this->callAPISuccessGetSingle('RuleGroup', [
581 'contact_type' => 'Organization',
582 'used' => 'Supervised',
583 ]);
584
585 $groupID = $this->groupCreate(['title' => 'she-mice']);
586
587 $this->callAPISuccess('GroupContact', 'create', [
588 'group_id' => $groupID,
589 'contact_id' => $this->contacts[4]['id'],
590 ]);
591
592 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
593 $ruleGroups['id'],
594 $groupID,
595 TRUE,
596 25,
597 FALSE
598 );
599
600 $this->assertEquals([
601 0 => [
602 'srcID' => $this->contacts[5]['id'],
603 'srcName' => 'Walt Disney Ltd',
604 'dstID' => $this->contacts[4]['id'],
605 'dstName' => 'Walt Disney Ltd',
606 'weight' => 20,
607 'canMerge' => TRUE,
608 ],
609 1 => [
610 'srcID' => $this->contacts[6]['id'],
611 'srcName' => 'Walt Disney',
612 'dstID' => $this->contacts[4]['id'],
613 'dstName' => 'Walt Disney Ltd',
614 'weight' => 10,
615 'canMerge' => TRUE,
616 ],
617 ], $pairs);
618
619 $this->callAPISuccess('GroupContact', 'create', [
620 'group_id' => $groupID,
621 'contact_id' => $this->contacts[5]['id'],
622 ]);
623 CRM_Core_DAO::executeQuery("DELETE FROM civicrm_prevnext_cache");
624 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
625 $ruleGroups['id'],
626 $groupID,
627 TRUE,
628 25,
629 FALSE
630 );
631
632 $this->assertEquals([
633 0 => [
634 'srcID' => $this->contacts[5]['id'],
635 'srcName' => 'Walt Disney Ltd',
636 'dstID' => $this->contacts[4]['id'],
637 'dstName' => 'Walt Disney Ltd',
638 'weight' => 20,
639 'canMerge' => TRUE,
640 ],
641 1 => [
642 'srcID' => $this->contacts[6]['id'],
643 'srcName' => 'Walt Disney',
644 'dstID' => $this->contacts[4]['id'],
645 'dstName' => 'Walt Disney Ltd',
646 'weight' => 10,
647 'canMerge' => TRUE,
648 ],
649 2 => [
650 'srcID' => $this->contacts[6]['id'],
651 'srcName' => 'Walt Disney',
652 'dstID' => $this->contacts[5]['id'],
653 'dstName' => 'Walt Disney Ltd',
654 'weight' => 10,
655 'canMerge' => TRUE,
656 ],
657 ], $pairs);
658 }
659
660 /**
661 * Test function that gets duplicate pairs.
662 *
663 * It turns out there are 2 code paths retrieving this data so my initial
664 * focus is on ensuring they match.
665 */
666 public function testGetMatchesInGroup() {
667 $this->setupMatchData();
668
669 $groupID = $this->groupCreate(['title' => 'she-mice']);
670
671 $this->callAPISuccess('GroupContact', 'create', [
672 'group_id' => $groupID,
673 'contact_id' => $this->contacts[3]['id'],
674 ]);
675
676 $pairs = CRM_Dedupe_Merger::getDuplicatePairs(
677 1,
678 $groupID,
679 TRUE,
680 25,
681 FALSE
682 );
683
684 $this->assertEquals([
685 0 => [
686 'srcID' => $this->contacts[3]['id'],
687 'srcName' => 'Mr. Minnie Mouse II',
688 'dstID' => $this->contacts[2]['id'],
689 'dstName' => 'Mr. Minnie Mouse II',
690 'weight' => 20,
691 'canMerge' => TRUE,
692 ],
693 ], $pairs);
694 }
695
696 /**
697 * Test the special info handling is unchanged after cleanup.
698 *
699 * Note the handling is silly - we are testing to lock in over short term
700 * changes not to imply any contract on the function.
701 *
702 * @throws \CRM_Core_Exception
703 * @throws \CiviCRM_API3_Exception
704 */
705 public function testGetRowsElementsAndInfoSpecialInfo() {
706 $contact1 = $this->individualCreate([
707 'preferred_communication_method' => [],
708 'communication_style_id' => 'Familiar',
709 'prefix_id' => 'Mrs.',
710 'suffix_id' => 'III',
711 ]);
712 $contact2 = $this->individualCreate([
713 'preferred_communication_method' => [
714 'SMS',
715 'Fax',
716 ],
717 'communication_style_id' => 'Formal',
718 'gender_id' => 'Female',
719 ]);
720 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($contact1, $contact2);
721 $rows = $rowsElementsAndInfo['rows'];
722 $this->assertEquals([
723 'main' => 'Mrs.',
724 'other' => 'Mr.',
725 'title' => 'Individual Prefix',
726 ], $rows['move_prefix_id']);
727 $this->assertEquals([
728 'main' => 'III',
729 'other' => 'II',
730 'title' => 'Individual Suffix',
731 ], $rows['move_suffix_id']);
732 $this->assertEquals([
733 'main' => '',
734 'other' => 'Female',
735 'title' => 'Gender',
736 ], $rows['move_gender_id']);
737 $this->assertEquals([
738 'main' => 'Familiar',
739 'other' => 'Formal',
740 'title' => 'Communication Style',
741 ], $rows['move_communication_style_id']);
742 $this->assertEquals(1, $rowsElementsAndInfo['migration_info']['move_communication_style_id']);
743 $this->assertEquals([
744 'main' => '',
745 'other' => 'SMS, Fax',
746 'title' => 'Preferred Communication Method',
747 ], $rows['move_preferred_communication_method']);
748 $this->assertEquals('\ 14\ 15\ 1', $rowsElementsAndInfo['migration_info']['move_preferred_communication_method']);
749 }
750
751 /**
752 * Test migration of Membership.
753 */
754 public function testMergeMembership() {
755 // Contacts setup
756 $this->setupMatchData();
757 $originalContactID = $this->contacts[0]['id'];
758 $duplicateContactID = $this->contacts[1]['id'];
759
760 //Add Membership for the duplicate contact.
761 $memTypeId = $this->membershipTypeCreate();
762 $this->callAPISuccess('Membership', 'create', [
763 'membership_type_id' => $memTypeId,
764 'contact_id' => $duplicateContactID,
765 ]);
766 //Assert if 'add new' checkbox is enabled on the merge form.
767 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
768 foreach ($rowsElementsAndInfo['elements'] as $element) {
769 if (!empty($element[3]) && $element[3] == 'add new') {
770 $checkedAttr = ['checked' => 'checked'];
771 $this->checkArrayEquals($element[4], $checkedAttr);
772 }
773 }
774
775 //Merge and move the mem to the main contact.
776 $this->mergeContacts($originalContactID, $duplicateContactID, [
777 'move_rel_table_memberships' => 1,
778 'operation' => ['move_rel_table_memberships' => ['add' => 1]],
779 ]);
780
781 //Check if membership is correctly transferred to original contact.
782 $originalContactMembership = $this->callAPISuccess('Membership', 'get', [
783 'membership_type_id' => $memTypeId,
784 'contact_id' => $originalContactID,
785 ]);
786 $this->assertEquals(1, $originalContactMembership['count']);
787 }
788
789 /**
790 * CRM-19653 : Test that custom field data should/shouldn't be overridden on
791 * selecting/not selecting option to migrate data respectively
792 *
793 * @throws \CRM_Core_Exception
794 */
795 public function testCustomDataOverwrite() {
796 // Create Custom Field
797 $createGroup = $this->setupCustomGroupForIndividual();
798 $createField = $this->setupCustomField('Graduation', $createGroup);
799 $customFieldName = 'custom_' . $createField['id'];
800
801 // Contacts setup
802 $this->setupMatchData();
803
804 $originalContactID = $this->contacts[0]['id'];
805 // used as duplicate contact in 1st use-case
806 $duplicateContactID1 = $this->contacts[1]['id'];
807 // used as duplicate contact in 2nd use-case
808 $duplicateContactID2 = $this->contacts[2]['id'];
809
810 // update the text custom field for original contact with value 'abc'
811 $this->callAPISuccess('Contact', 'create', [
812 'id' => $originalContactID,
813 $customFieldName => 'abc',
814 ]);
815 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
816
817 // update the text custom field for duplicate contact 1 with value 'def'
818 $this->callAPISuccess('Contact', 'create', [
819 'id' => $duplicateContactID1,
820 "{$customFieldName}" => 'def',
821 ]);
822 $this->assertCustomFieldValue($duplicateContactID1, 'def', $customFieldName);
823
824 // update the text custom field for duplicate contact 2 with value 'ghi'
825 $this->callAPISuccess('Contact', 'create', [
826 'id' => $duplicateContactID2,
827 "{$customFieldName}" => 'ghi',
828 ]);
829 $this->assertCustomFieldValue($duplicateContactID2, 'ghi', $customFieldName);
830
831 /*** USE-CASE 1: DO NOT OVERWRITE CUSTOM FIELD VALUE **/
832 $this->mergeContacts($originalContactID, $duplicateContactID1, [
833 "move_{$customFieldName}" => NULL,
834 ]);
835 $this->assertCustomFieldValue($originalContactID, 'abc', $customFieldName);
836
837 /*** USE-CASE 2: OVERWRITE CUSTOM FIELD VALUE **/
838 $this->mergeContacts($originalContactID, $duplicateContactID2, [
839 "move_{$customFieldName}" => 'ghi',
840 ]);
841 $this->assertCustomFieldValue($originalContactID, 'ghi', $customFieldName);
842
843 // cleanup created custom set
844 $this->callAPISuccess('CustomField', 'delete', ['id' => $createField['id']]);
845 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
846 }
847
848 /**
849 * Creatd Date merge cases
850 * @return array
851 */
852 public function createdDateMergeCases() {
853 $cases = [];
854 // Normal pattern merge into the lower id
855 $cases[] = [0, 1];
856 // Check if we flipped the contacts that it still does right thing
857 $cases[] = [1, 0];
858 return $cases;
859 }
860
861 /**
862 * dev/core#996 Ensure that the oldest created date is retained even if duplicates have been flipped
863 *
864 * @dataProvider createdDateMergeCases
865 *
866 * @param $keepContactKey
867 * @param $duplicateContactKey
868 *
869 * @throws \API_Exception
870 * @throws \CRM_Core_Exception
871 * @throws \CiviCRM_API3_Exception
872 * @throws \Civi\API\Exception\UnauthorizedException
873 */
874 public function testCreatedDatePostMerge($keepContactKey, $duplicateContactKey) {
875 $this->setupMatchData();
876 $lowerContactCreatedDate = $this->callAPISuccess('Contact', 'getsingle', [
877 'id' => $this->contacts[0]['id'],
878 'return' => ['created_date'],
879 ])['created_date'];
880 // Assume contacts have been flipped in the UL so merging into the higher id
881 $this->mergeContacts($this->contacts[$keepContactKey]['id'], $this->contacts[$duplicateContactKey]['id'], []);
882 $this->assertEquals($lowerContactCreatedDate, $this->callAPISuccess('Contact', 'getsingle', ['id' => $this->contacts[$keepContactKey]['id'], 'return' => ['created_date']])['created_date']);
883 }
884
885 /**
886 * Verifies that when a contact with a custom field value is merged into a
887 * contact without a record int its corresponding custom group table, and none
888 * of the custom fields of that custom table are selected, the value is not
889 * merged in.
890 */
891 public function testMigrationOfUnselectedCustomDataOnEmptyCustomRecord() {
892 // Create Custom Fields
893 $createGroup = $this->setupCustomGroupForIndividual();
894 $customField1 = $this->setupCustomField('TestField', $createGroup);
895
896 // Create multi-value custom field
897 $multiGroup = $this->CustomGroupMultipleCreateByParams();
898 $multiField = $this->customFieldCreate([
899 'custom_group_id' => $multiGroup['id'],
900 'label' => 'field_1' . $multiGroup['id'],
901 'in_selector' => 1,
902 ]);
903
904 // Contacts setup
905 $this->setupMatchData();
906 $originalContactID = $this->contacts[0]['id'];
907 $duplicateContactID = $this->contacts[1]['id'];
908
909 // Update the text custom fields for duplicate contact
910 $this->callAPISuccess('Contact', 'create', [
911 'id' => $duplicateContactID,
912 "custom_{$customField1['id']}" => 'abc',
913 "custom_{$multiField['id']}" => 'def',
914 ]);
915 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
916 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$multiField['id']}");
917
918 // Merge, and ensure that no value was migrated
919 $this->mergeContacts($originalContactID, $duplicateContactID, [
920 "move_custom_{$customField1['id']}" => NULL,
921 "move_rel_table_custom_{$multiGroup['id']}" => NULL,
922 ]);
923 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
924 $this->assertCustomFieldValue($originalContactID, '', "custom_{$multiField['id']}");
925
926 // cleanup created custom set
927 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField1['id']]);
928 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
929 $this->callAPISuccess('CustomField', 'delete', ['id' => $multiField['id']]);
930 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]);
931 }
932
933 /**
934 * Tests that if only part of the custom fields of a custom group are selected
935 * for a merge, only those values are merged, while all other fields of the
936 * custom group retain their original value, specifically for a contact with
937 * no records on the custom group table.
938 *
939 * @throws \CRM_Core_Exception
940 * @throws \CiviCRM_API3_Exception
941 */
942 public function testMigrationOfSomeCustomDataOnEmptyCustomRecord() {
943 // Create Custom Fields
944 $createGroup = $this->setupCustomGroupForIndividual();
945 $customField1 = $this->setupCustomField('Test1', $createGroup);
946 $customField2 = $this->setupCustomField('Test2', $createGroup);
947
948 // Create multi-value custom field
949 $multiGroup = $this->CustomGroupMultipleCreateByParams();
950 $multiField = $this->customFieldCreate([
951 'custom_group_id' => $multiGroup['id'],
952 'label' => 'field_1' . $multiGroup['id'],
953 'in_selector' => 1,
954 ]);
955
956 // Contacts setup
957 $this->setupMatchData();
958 $originalContactID = $this->contacts[0]['id'];
959 $duplicateContactID = $this->contacts[1]['id'];
960
961 // Update the text custom fields for duplicate contact
962 $this->callAPISuccess('Contact', 'create', [
963 'id' => $duplicateContactID,
964 "custom_{$customField1['id']}" => 'abc',
965 "custom_{$customField2['id']}" => 'def',
966 "custom_{$multiField['id']}" => 'ghi',
967 ]);
968 $this->assertCustomFieldValue($duplicateContactID, 'abc', "custom_{$customField1['id']}");
969 $this->assertCustomFieldValue($duplicateContactID, 'def', "custom_{$customField2['id']}");
970 $this->assertCustomFieldValue($duplicateContactID, 'ghi', "custom_{$multiField['id']}");
971
972 // Perform merge
973 $this->mergeContacts($originalContactID, $duplicateContactID, [
974 "move_custom_{$customField1['id']}" => NULL,
975 "move_custom_{$customField2['id']}" => 'def',
976 "move_rel_table_custom_{$multiGroup['id']}" => '1',
977 ]);
978 $this->assertCustomFieldValue($originalContactID, '', "custom_{$customField1['id']}");
979 $this->assertCustomFieldValue($originalContactID, 'def', "custom_{$customField2['id']}");
980 $this->assertCustomFieldValue($originalContactID, 'ghi', "custom_{$multiField['id']}");
981
982 // cleanup created custom set
983 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField1['id']]);
984 $this->callAPISuccess('CustomField', 'delete', ['id' => $customField2['id']]);
985 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $createGroup['id']]);
986 $this->callAPISuccess('CustomField', 'delete', ['id' => $multiField['id']]);
987 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]);
988 }
989
990 /**
991 * Test that ContactReference fields are updated to point to the main contact
992 * after a merge is performed and the duplicate contact is deleted.
993 *
994 * @throws \API_Exception
995 * @throws \CRM_Core_Exception
996 * @throws \CiviCRM_API3_Exception
997 * @throws \Civi\API\Exception\UnauthorizedException
998 */
999 public function testMigrationOfContactReferenceCustomField() {
1000 // Create Custom Fields
1001 $contactGroup = $this->setupCustomGroupForIndividual();
1002 $activityGroup = $this->customGroupCreate([
1003 'name' => 'test_group_activity',
1004 'extends' => 'Activity',
1005 ]);
1006 $refFieldContact = $this->customFieldCreate([
1007 'custom_group_id' => $contactGroup['id'],
1008 'label' => 'field_1' . $contactGroup['id'],
1009 'data_type' => 'ContactReference',
1010 'default_value' => NULL,
1011 ]);
1012 $refFieldActivity = $this->customFieldCreate([
1013 'custom_group_id' => $activityGroup['id'],
1014 'label' => 'field_1' . $activityGroup['id'],
1015 'data_type' => 'ContactReference',
1016 'default_value' => NULL,
1017 ]);
1018
1019 // Contacts setup
1020 $this->setupMatchData();
1021 $originalContactID = $this->contacts[0]['id'];
1022 $duplicateContactID = $this->contacts[1]['id'];
1023
1024 // create a contact that won't be merged but has a ContactReference field
1025 // pointing to the duplicate (to be deleted) contact
1026 $unrelatedContact = $this->individualCreate([
1027 'first_name' => 'Unrelated',
1028 'last_name' => 'Contact',
1029 'email' => 'unrelated@example.com',
1030 "custom_{$refFieldContact['id']}" => $duplicateContactID,
1031 ]);
1032 // also create an activity with a ContactReference custom field
1033 $activity = $this->activityCreate([
1034 'target_contact_id' => $unrelatedContact,
1035 "custom_{$refFieldActivity['id']}" => $duplicateContactID,
1036 ]);
1037
1038 // verify that the fields were set
1039 $this->assertCustomFieldValue($unrelatedContact, $duplicateContactID, "custom_{$refFieldContact['id']}");
1040 $this->assertEntityCustomFieldValue('Activity', $activity['id'], $duplicateContactID, "custom_{$refFieldActivity['id']}_id");
1041
1042 // Perform merge
1043 $this->mergeContacts($originalContactID, $duplicateContactID, []);
1044
1045 // verify that the ContactReference fields were updated to point to the surviving contact post-merge
1046 $this->assertCustomFieldValue($unrelatedContact, $originalContactID, "custom_{$refFieldContact['id']}");
1047 $this->assertEntityCustomFieldValue('Activity', $activity['id'], $originalContactID, "custom_{$refFieldActivity['id']}_id");
1048
1049 // cleanup created custom set
1050 $this->callAPISuccess('CustomField', 'delete', ['id' => $refFieldContact['id']]);
1051 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $contactGroup['id']]);
1052 $this->callAPISuccess('CustomField', 'delete', ['id' => $refFieldActivity['id']]);
1053 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $activityGroup['id']]);
1054 }
1055
1056 /**
1057 * Calls merge method on given contacts, with values given in $params array.
1058 *
1059 * @param $originalContactID
1060 * ID of target contact
1061 * @param $duplicateContactID
1062 * ID of contact to be merged
1063 * @param $params
1064 * Array of fields to be merged from source into target contact, of the form
1065 * ['move_<fieldName>' => <fieldValue>]
1066 *
1067 * @throws \API_Exception
1068 * @throws \CRM_Core_Exception
1069 * @throws \CiviCRM_API3_Exception
1070 */
1071 private function mergeContacts($originalContactID, $duplicateContactID, $params): void {
1072 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($originalContactID, $duplicateContactID);
1073
1074 $migrationData = [
1075 'main_details' => $rowsElementsAndInfo['main_details'],
1076 'other_details' => $rowsElementsAndInfo['other_details'],
1077 ];
1078
1079 // Migrate data of duplicate contact
1080 CRM_Dedupe_Merger::moveAllBelongings($originalContactID, $duplicateContactID, array_merge($migrationData, $params));
1081 }
1082
1083 /**
1084 * Checks if the expected value for the given field corresponds to what is
1085 * stored in the database for the given contact ID.
1086 *
1087 * @param $contactID
1088 * @param $expectedValue
1089 * @param $customFieldName
1090 *
1091 * @throws \CRM_Core_Exception
1092 */
1093 private function assertCustomFieldValue($contactID, $expectedValue, $customFieldName): void {
1094 $this->assertEntityCustomFieldValue('Contact', $contactID, $expectedValue, $customFieldName);
1095 }
1096
1097 /**
1098 * Check if the custom field of the given field and entity id matches the
1099 * expected value
1100 *
1101 * @param $entity
1102 * @param $id
1103 * @param $expectedValue
1104 * @param $customFieldName
1105 *
1106 * @throws \CRM_Core_Exception
1107 */
1108 private function assertEntityCustomFieldValue($entity, $id, $expectedValue, $customFieldName) {
1109 $data = $this->callAPISuccess($entity, 'getsingle', [
1110 'id' => $id,
1111 'return' => [$customFieldName],
1112 ]);
1113
1114 $this->assertEquals($expectedValue, $data[$customFieldName], "Custom field value was supposed to be '{$expectedValue}', '{$data[$customFieldName]}' found.");
1115 }
1116
1117 /**
1118 * Creates a custom group to run tests on contacts that are individuals.
1119 *
1120 * @return array
1121 * Data for the created custom group record
1122 * @throws \CRM_Core_Exception
1123 */
1124 private function setupCustomGroupForIndividual() {
1125 $customGroup = $this->callAPISuccess('custom_group', 'get', [
1126 'name' => 'test_group',
1127 ]);
1128
1129 if ($customGroup['count'] > 0) {
1130 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $customGroup['id']]);
1131 }
1132
1133 $customGroup = $this->callAPISuccess('custom_group', 'create', [
1134 'title' => 'Test_Group',
1135 'name' => 'test_group',
1136 'extends' => ['Individual'],
1137 'style' => 'Inline',
1138 'is_multiple' => FALSE,
1139 'is_active' => 1,
1140 ]);
1141
1142 return $customGroup;
1143 }
1144
1145 /**
1146 * Creates a custom field on the provided custom group with the given field
1147 * label.
1148 *
1149 * @param string $fieldLabel
1150 * @param array $createGroup
1151 *
1152 * @return array
1153 * Data for the created custom field record
1154 * @throws \CRM_Core_Exception
1155 */
1156 private function setupCustomField($fieldLabel, $createGroup) {
1157 return $this->callAPISuccess('custom_field', 'create', [
1158 'label' => $fieldLabel,
1159 'data_type' => 'Alphanumeric',
1160 'html_type' => 'Text',
1161 'custom_group_id' => $createGroup['id'],
1162 ]);
1163 }
1164
1165 /**
1166 * Set up some contacts for our matching.
1167 *
1168 * @throws \CiviCRM_API3_Exception
1169 */
1170 public function setupMatchData(): void {
1171 $fixtures = [
1172 [
1173 'first_name' => 'Mickey',
1174 'last_name' => 'Mouse',
1175 'email' => 'mickey@mouse.com',
1176 ],
1177 [
1178 'first_name' => 'Mickey',
1179 'last_name' => 'Mouse',
1180 'email' => 'mickey@mouse.com',
1181 ],
1182 [
1183 'first_name' => 'Minnie',
1184 'last_name' => 'Mouse',
1185 'email' => 'mickey@mouse.com',
1186 ],
1187 [
1188 'first_name' => 'Minnie',
1189 'last_name' => 'Mouse',
1190 'email' => 'mickey@mouse.com',
1191 ],
1192 ];
1193 foreach ($fixtures as $fixture) {
1194 $contactID = $this->individualCreate($fixture);
1195 $this->contacts[] = array_merge($fixture, ['id' => $contactID]);
1196 sleep(2);
1197 }
1198 $organizationFixtures = [
1199 [
1200 'organization_name' => 'Walt Disney Ltd',
1201 'email' => 'walt@disney.com',
1202 ],
1203 [
1204 'organization_name' => 'Walt Disney Ltd',
1205 'email' => 'walt@disney.com',
1206 ],
1207 [
1208 'organization_name' => 'Walt Disney',
1209 'email' => 'walt@disney.com',
1210 ],
1211 [
1212 'organization_name' => 'Walt Disney',
1213 'email' => 'walter@disney.com',
1214 ],
1215 ];
1216 foreach ($organizationFixtures as $fixture) {
1217 $contactID = $this->organizationCreate($fixture);
1218 $this->contacts[] = array_merge($fixture, ['id' => $contactID]);
1219 }
1220 }
1221
1222 /**
1223 * Get the list of tables that refer to the CID.
1224 *
1225 * This is a statically maintained (in this test list).
1226 *
1227 * There is also a check against an automated list but having both seems to
1228 * add extra stability to me. They do not change often.
1229 */
1230 public function getStaticCIDRefs() {
1231 return [
1232 'civicrm_acl_cache' => [
1233 0 => 'contact_id',
1234 ],
1235 'civicrm_acl_contact_cache' => [
1236 0 => 'contact_id',
1237 ],
1238 'civicrm_action_log' => [
1239 0 => 'contact_id',
1240 ],
1241 'civicrm_activity_contact' => [
1242 0 => 'contact_id',
1243 ],
1244 'civicrm_address' => [
1245 0 => 'contact_id',
1246 ],
1247 'civicrm_batch' => [
1248 0 => 'created_id',
1249 1 => 'modified_id',
1250 ],
1251 'civicrm_campaign' => [
1252 0 => 'created_id',
1253 1 => 'last_modified_id',
1254 ],
1255 'civicrm_case_contact' => [
1256 0 => 'contact_id',
1257 ],
1258 'civicrm_contact' => [
1259 0 => 'primary_contact_id',
1260 1 => 'employer_id',
1261 ],
1262 'civicrm_contribution' => [
1263 0 => 'contact_id',
1264 ],
1265 'civicrm_contribution_page' => [
1266 0 => 'created_id',
1267 ],
1268 'civicrm_contribution_recur' => [
1269 0 => 'contact_id',
1270 ],
1271 'civicrm_contribution_soft' => [
1272 0 => 'contact_id',
1273 ],
1274 'civicrm_custom_group' => [
1275 0 => 'created_id',
1276 ],
1277 'civicrm_dashboard_contact' => [
1278 0 => 'contact_id',
1279 ],
1280 'civicrm_dedupe_exception' => [
1281 0 => 'contact_id1',
1282 1 => 'contact_id2',
1283 ],
1284 'civicrm_domain' => [
1285 0 => 'contact_id',
1286 ],
1287 'civicrm_email' => [
1288 0 => 'contact_id',
1289 ],
1290 'civicrm_event' => [
1291 0 => 'created_id',
1292 ],
1293 'civicrm_event_carts' => [
1294 0 => 'user_id',
1295 ],
1296 'civicrm_financial_account' => [
1297 0 => 'contact_id',
1298 ],
1299 'civicrm_financial_item' => [
1300 0 => 'contact_id',
1301 ],
1302 'civicrm_grant' => [
1303 0 => 'contact_id',
1304 ],
1305 'civicrm_group' => [
1306 0 => 'created_id',
1307 1 => 'modified_id',
1308 ],
1309 'civicrm_group_contact' => [
1310 0 => 'contact_id',
1311 ],
1312 'civicrm_group_contact_cache' => [
1313 0 => 'contact_id',
1314 ],
1315 'civicrm_group_organization' => [
1316 0 => 'organization_id',
1317 ],
1318 'civicrm_im' => [
1319 0 => 'contact_id',
1320 ],
1321 'civicrm_log' => [
1322 0 => 'modified_id',
1323 ],
1324 'civicrm_mailing' => [
1325 0 => 'created_id',
1326 1 => 'scheduled_id',
1327 2 => 'approver_id',
1328 ],
1329 'civicrm_file' => [
1330 'created_id',
1331 ],
1332 'civicrm_mailing_abtest' => [
1333 0 => 'created_id',
1334 ],
1335 'civicrm_mailing_event_queue' => [
1336 0 => 'contact_id',
1337 ],
1338 'civicrm_mailing_event_subscribe' => [
1339 0 => 'contact_id',
1340 ],
1341 'civicrm_mailing_recipients' => [
1342 0 => 'contact_id',
1343 ],
1344 'civicrm_membership' => [
1345 0 => 'contact_id',
1346 ],
1347 'civicrm_membership_log' => [
1348 0 => 'modified_id',
1349 ],
1350 'civicrm_membership_type' => [
1351 0 => 'member_of_contact_id',
1352 ],
1353 'civicrm_note' => [
1354 0 => 'contact_id',
1355 ],
1356 'civicrm_openid' => [
1357 0 => 'contact_id',
1358 ],
1359 'civicrm_participant' => [
1360 0 => 'contact_id',
1361 //CRM-16761
1362 1 => 'transferred_to_contact_id',
1363 ],
1364 'civicrm_payment_token' => [
1365 0 => 'contact_id',
1366 1 => 'created_id',
1367 ],
1368 'civicrm_pcp' => [
1369 0 => 'contact_id',
1370 ],
1371 'civicrm_phone' => [
1372 0 => 'contact_id',
1373 ],
1374 'civicrm_pledge' => [
1375 0 => 'contact_id',
1376 ],
1377 'civicrm_print_label' => [
1378 0 => 'created_id',
1379 ],
1380 'civicrm_saved_search' => ['created_id', 'modified_id'],
1381 'civicrm_relationship' => [
1382 0 => 'contact_id_a',
1383 1 => 'contact_id_b',
1384 ],
1385 'civicrm_relationship_cache' => [
1386 0 => 'near_contact_id',
1387 1 => 'far_contact_id',
1388 ],
1389 'civicrm_report_instance' => [
1390 0 => 'created_id',
1391 1 => 'owner_id',
1392 ],
1393 'civicrm_setting' => [
1394 0 => 'contact_id',
1395 1 => 'created_id',
1396 ],
1397 'civicrm_subscription_history' => [
1398 0 => 'contact_id',
1399 ],
1400 'civicrm_survey' => [
1401 0 => 'created_id',
1402 1 => 'last_modified_id',
1403 ],
1404 'civicrm_tag' => [
1405 0 => 'created_id',
1406 ],
1407 'civicrm_uf_group' => [
1408 0 => 'created_id',
1409 ],
1410 'civicrm_uf_match' => [
1411 0 => 'contact_id',
1412 ],
1413 'civicrm_value_testgetcidref_1' => [
1414 0 => 'entity_id',
1415 ],
1416 'civicrm_website' => [
1417 0 => 'contact_id',
1418 ],
1419 ];
1420 }
1421
1422 /**
1423 * Get a list of CIDs that is calculated off the schema.
1424 *
1425 * Note this is an expensive and table locking query. Should be safe in tests
1426 * though.
1427 */
1428 public function getCalculatedCIDRefs() {
1429 $cidRefs = [];
1430 $sql = "
1431 SELECT
1432 table_name AS table_name,
1433 column_name AS column_name
1434 FROM information_schema.key_column_usage
1435 WHERE
1436 referenced_table_schema = database() AND
1437 referenced_table_name = 'civicrm_contact' AND
1438 referenced_column_name = 'id';
1439 ";
1440 $dao = CRM_Core_DAO::executeQuery($sql);
1441 while ($dao->fetch()) {
1442 $cidRefs[$dao->table_name][] = $dao->column_name;
1443 }
1444 // Do specific re-ordering changes to make this the same as the ref validated one.
1445 // The above query orders by FK alphabetically.
1446 // There might be cleverer ways to do this but it shouldn't change much.
1447 $cidRefs['civicrm_contact'][0] = 'primary_contact_id';
1448 $cidRefs['civicrm_contact'][1] = 'employer_id';
1449 $cidRefs['civicrm_acl_contact_cache'][0] = 'contact_id';
1450 $cidRefs['civicrm_mailing'][0] = 'created_id';
1451 $cidRefs['civicrm_mailing'][1] = 'scheduled_id';
1452 $cidRefs['civicrm_mailing'][2] = 'approver_id';
1453 return $cidRefs;
1454 }
1455
1456 }