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