Merge pull request #12176 from vinuvarshith/dev-core-133-reply-to-check
[civicrm-core.git] / tests / phpunit / api / v3 / JobTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * File for the CiviCRM APIv3 job functions
30 *
31 * @package CiviCRM_APIv3
32 * @subpackage API_Job
33 *
34 * @copyright CiviCRM LLC (c) 2004-2018
35 */
36
37 /**
38 * Class api_v3_JobTest
39 * @group headless
40 */
41 class api_v3_JobTest extends CiviUnitTestCase {
42 protected $_apiversion = 3;
43
44 public $DBResetRequired = FALSE;
45 public $_entity = 'Job';
46 public $_params = array();
47 /**
48 * Created membership type.
49 *
50 * Must be created outside the transaction due to it breaking the transaction.
51 *
52 * @var
53 */
54 public $membershipTypeID;
55
56 /**
57 * Set up for tests.
58 */
59 public function setUp() {
60 parent::setUp();
61 $this->membershipTypeID = $this->membershipTypeCreate(array('name' => 'General'));
62 $this->useTransaction(TRUE);
63 $this->_params = array(
64 'sequential' => 1,
65 'name' => 'API_Test_Job',
66 'description' => 'A long description written by hand in cursive',
67 'run_frequency' => 'Daily',
68 'api_entity' => 'ApiTestEntity',
69 'api_action' => 'apitestaction',
70 'parameters' => 'Semi-formal explanation of runtime job parameters',
71 'is_active' => 1,
72 );
73 }
74
75 public function tearDown() {
76 parent::tearDown();
77 // The membershipType create breaks transactions so this extra cleanup is needed.
78 $this->membershipTypeDelete(array('id' => $this->membershipTypeID));
79 $this->cleanUpSetUpIDs();
80 }
81
82 /**
83 * Check with no name.
84 */
85 public function testCreateWithoutName() {
86 $params = array(
87 'is_active' => 1,
88 );
89 $this->callAPIFailure('job', 'create', $params,
90 'Mandatory key(s) missing from params array: run_frequency, name, api_entity, api_action'
91 );
92 }
93
94 /**
95 * Create job with an invalid "run_frequency" value.
96 */
97 public function testCreateWithInvalidFrequency() {
98 $params = array(
99 'sequential' => 1,
100 'name' => 'API_Test_Job',
101 'description' => 'A long description written by hand in cursive',
102 'run_frequency' => 'Fortnightly',
103 'api_entity' => 'ApiTestEntity',
104 'api_action' => 'apitestaction',
105 'parameters' => 'Semi-formal explanation of runtime job parameters',
106 'is_active' => 1,
107 );
108 $this->callAPIFailure('job', 'create', $params);
109 }
110
111 /**
112 * Create job.
113 */
114 public function testCreate() {
115 $result = $this->callAPIAndDocument('job', 'create', $this->_params, __FUNCTION__, __FILE__);
116 $this->assertNotNull($result['values'][0]['id']);
117
118 // mutate $params to match expected return value
119 unset($this->_params['sequential']);
120 //assertDBState compares expected values in $result to actual values in the DB
121 $this->assertDBState('CRM_Core_DAO_Job', $result['id'], $this->_params);
122 }
123
124 /**
125 * Clone job
126 */
127 public function testClone() {
128 $createResult = $this->callAPISuccess('job', 'create', $this->_params);
129 $params = array('id' => $createResult['id']);
130 $cloneResult = $this->callAPIAndDocument('job', 'clone', $params, __FUNCTION__, __FILE__);
131 $clonedJob = $cloneResult['values'][$cloneResult['id']];
132 $this->assertEquals($this->_params['name'] . ' - Copy', $clonedJob['name']);
133 $this->assertEquals($this->_params['description'], $clonedJob['description']);
134 $this->assertEquals($this->_params['parameters'], $clonedJob['parameters']);
135 $this->assertEquals($this->_params['is_active'], $clonedJob['is_active']);
136 $this->assertArrayNotHasKey('last_run', $clonedJob);
137 $this->assertArrayNotHasKey('scheduled_run_date', $clonedJob);
138 }
139
140 /**
141 * Check if required fields are not passed.
142 */
143 public function testDeleteWithoutRequired() {
144 $params = array(
145 'name' => 'API_Test_PP',
146 'title' => 'API Test Payment Processor',
147 'class_name' => 'CRM_Core_Payment_APITest',
148 );
149
150 $result = $this->callAPIFailure('job', 'delete', $params);
151 $this->assertEquals($result['error_message'], 'Mandatory key(s) missing from params array: id');
152 }
153
154 /**
155 * Check with incorrect required fields.
156 */
157 public function testDeleteWithIncorrectData() {
158 $params = array(
159 'id' => 'abcd',
160 );
161 $this->callAPIFailure('job', 'delete', $params);
162 }
163
164 /**
165 * Check job delete.
166 */
167 public function testDelete() {
168 $createResult = $this->callAPISuccess('job', 'create', $this->_params);
169 $params = array('id' => $createResult['id']);
170 $this->callAPIAndDocument('job', 'delete', $params, __FUNCTION__, __FILE__);
171 $this->assertAPIDeleted($this->_entity, $createResult['id']);
172 }
173
174 /**
175 *
176 * public function testCallUpdateGreetingMissingParams() {
177 * $result = $this->callAPISuccess($this->_entity, 'update_greeting', array('gt' => 1));
178 * $this->assertEquals('Mandatory key(s) missing from params array: ct', $result['error_message']);
179 * }
180 *
181 * public function testCallUpdateGreetingIncorrectParams() {
182 * $result = $this->callAPISuccess($this->_entity, 'update_greeting', array('gt' => 1, 'ct' => 'djkfhdskjfhds'));
183 * $this->assertEquals('ct `djkfhdskjfhds` is not valid.', $result['error_message']);
184 * }
185 * /*
186 * Note that this test is about tesing the metadata / calling of the function & doesn't test the success of the called function
187 */
188 public function testCallUpdateGreetingSuccess() {
189 $this->callAPISuccess($this->_entity, 'update_greeting', array(
190 'gt' => 'postal_greeting',
191 'ct' => 'Individual',
192 ));
193 }
194
195 public function testCallUpdateGreetingCommaSeparatedParamsSuccess() {
196 $gt = 'postal_greeting,email_greeting,addressee';
197 $ct = 'Individual,Household';
198 $this->callAPISuccess($this->_entity, 'update_greeting', array('gt' => $gt, 'ct' => $ct));
199 }
200
201 /**
202 * Test the call reminder success sends more than 25 reminders & is not incorrectly limited.
203 *
204 * Note that this particular test sends the reminders to the additional recipients only
205 * as no real reminder person is configured
206 *
207 * Also note that this is testing a 'job' api so is in this class rather than scheduled_reminder - which
208 * seems a cleaner place to build up a collection of scheduled reminder testing functions. However, it seems
209 * that the api itself would need to be moved to the scheduled_reminder fn to do that with the job wrapper being respected for legacy functions
210 */
211 public function testCallSendReminderSuccessMoreThanDefaultLimit() {
212 $membershipTypeID = $this->membershipTypeCreate();
213 $this->membershipStatusCreate();
214 $createTotal = 30;
215 for ($i = 1; $i <= $createTotal; $i++) {
216 $contactID = $this->individualCreate();
217 $groupID = $this->groupCreate(array('name' => $i, 'title' => $i));
218 $this->callAPISuccess('action_schedule', 'create', array(
219 'title' => " job $i",
220 'subject' => "job $i",
221 'entity_value' => $membershipTypeID,
222 'mapping_id' => 4,
223 'start_action_date' => 'membership_join_date',
224 'start_action_offset' => 0,
225 'start_action_condition' => 'before',
226 'start_action_unit' => 'hour',
227 'group_id' => $groupID,
228 'limit_to' => FALSE,
229 ));
230 $this->callAPISuccess('group_contact', 'create', array(
231 'contact_id' => $contactID,
232 'status' => 'Added',
233 'group_id' => $groupID,
234 ));
235 }
236 $this->callAPISuccess('job', 'send_reminder', array());
237 $successfulCronCount = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
238 $this->assertEquals($successfulCronCount, $createTotal);
239 }
240
241 /**
242 * Test scheduled reminders respect limit to (since above identified addition_to handling issue).
243 *
244 * We create 3 contacts - 1 is in our group, 1 has our membership & the chosen one has both
245 * & check that only the chosen one got the reminder
246 */
247 public function testCallSendReminderLimitTo() {
248 $membershipTypeID = $this->membershipTypeCreate();
249 $this->membershipStatusCreate();
250 $createTotal = 3;
251 $groupID = $this->groupCreate(array('name' => 'Texan drawlers', 'title' => 'a...'));
252 for ($i = 1; $i <= $createTotal; $i++) {
253 $contactID = $this->individualCreate();
254 if ($i == 2) {
255 $theChosenOneID = $contactID;
256 }
257 if ($i < 3) {
258 $this->callAPISuccess('group_contact', 'create', array(
259 'contact_id' => $contactID,
260 'status' => 'Added',
261 'group_id' => $groupID,
262 ));
263 }
264 if ($i > 1) {
265 $this->callAPISuccess('membership', 'create', array(
266 'contact_id' => $contactID,
267 'membership_type_id' => $membershipTypeID,
268 'join_date' => 'now',
269 'start_date' => '+ 1 day',
270 )
271 );
272 }
273 }
274 $this->callAPISuccess('action_schedule', 'create', array(
275 'title' => " remind all Texans",
276 'subject' => "drawling renewal",
277 'entity_value' => $membershipTypeID,
278 'mapping_id' => 4,
279 'start_action_date' => 'membership_start_date',
280 'start_action_offset' => 1,
281 'start_action_condition' => 'before',
282 'start_action_unit' => 'day',
283 'group_id' => $groupID,
284 'limit_to' => TRUE,
285 ));
286 $this->callAPISuccess('job', 'send_reminder', array());
287 $successfulCronCount = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
288 $this->assertEquals($successfulCronCount, 1);
289 $sentToID = CRM_Core_DAO::singleValueQuery("SELECT contact_id FROM civicrm_action_log");
290 $this->assertEquals($sentToID, $theChosenOneID);
291 }
292
293 public function testCallDisableExpiredRelationships() {
294 $individualID = $this->individualCreate();
295 $orgID = $this->organizationCreate();
296 CRM_Utils_Hook_UnitTests::singleton()->setHook('civicrm_pre', array($this, 'hookPreRelationship'));
297 $relationshipTypeID = $this->callAPISuccess('relationship_type', 'getvalue', array(
298 'return' => 'id',
299 'name_a_b' => 'Employee of',
300 ));
301 $result = $this->callAPISuccess('relationship', 'create', array(
302 'relationship_type_id' => $relationshipTypeID,
303 'contact_id_a' => $individualID,
304 'contact_id_b' => $orgID,
305 'is_active' => 1,
306 'end_date' => 'yesterday',
307 ));
308 $relationshipID = $result['id'];
309 $this->assertEquals('Hooked', $result['values'][$relationshipID]['description']);
310 $this->callAPISuccess($this->_entity, 'disable_expired_relationships', array());
311 $result = $this->callAPISuccess('relationship', 'get', array());
312 $this->assertEquals('Go Go you good thing', $result['values'][$relationshipID]['description']);
313 $this->contactDelete($individualID);
314 $this->contactDelete($orgID);
315 }
316
317 /**
318 * Test the batch merge function.
319 *
320 * We are just checking it returns without error here.
321 */
322 public function testBatchMerge() {
323 $this->callAPISuccess('Job', 'process_batch_merge', array());
324 }
325
326 /**
327 * Test the batch merge function actually works!
328 *
329 * @dataProvider getMergeSets
330 *
331 * @param $dataSet
332 */
333 public function testBatchMergeWorks($dataSet) {
334 foreach ($dataSet['contacts'] as $params) {
335 $this->callAPISuccess('Contact', 'create', $params);
336 }
337
338 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('mode' => $dataSet['mode']));
339 $this->assertEquals($dataSet['skipped'], count($result['values']['skipped']), 'Failed to skip the right number:' . $dataSet['skipped']);
340 $this->assertEquals($dataSet['merged'], count($result['values']['merged']));
341 $result = $this->callAPISuccess('Contact', 'get', array(
342 'contact_sub_type' => 'Student',
343 'sequential' => 1,
344 'is_deceased' => array('IN' => array(0, 1)),
345 'options' => array('sort' => 'id ASC'),
346 ));
347 $this->assertEquals(count($dataSet['expected']), $result['count']);
348 foreach ($dataSet['expected'] as $index => $contact) {
349 foreach ($contact as $key => $value) {
350 if ($key == 'gender_id') {
351 $key = 'gender';
352 }
353 $this->assertEquals($value, $result['values'][$index][$key]);
354 }
355 }
356 }
357
358 /**
359 * Check that the merge carries across various related entities.
360 *
361 * Note the group combinations & expected results:
362 */
363 public function testBatchMergeWithAssets() {
364 $contactID = $this->individualCreate();
365 $contact2ID = $this->individualCreate();
366 $this->contributionCreate(array('contact_id' => $contactID));
367 $this->contributionCreate(array('contact_id' => $contact2ID, 'invoice_id' => '2', 'trxn_id' => 2));
368 $this->contactMembershipCreate(array('contact_id' => $contactID));
369 $this->contactMembershipCreate(array('contact_id' => $contact2ID));
370 $this->activityCreate(array('source_contact_id' => $contactID, 'target_contact_id' => $contactID, 'assignee_contact_id' => $contactID));
371 $this->activityCreate(array('source_contact_id' => $contact2ID, 'target_contact_id' => $contact2ID, 'assignee_contact_id' => $contact2ID));
372 $this->tagCreate(array('name' => 'Tall'));
373 $this->tagCreate(array('name' => 'Short'));
374 $this->entityTagAdd(array('contact_id' => $contactID, 'tag_id' => 'Tall'));
375 $this->entityTagAdd(array('contact_id' => $contact2ID, 'tag_id' => 'Short'));
376 $this->entityTagAdd(array('contact_id' => $contact2ID, 'tag_id' => 'Tall'));
377 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('mode' => 'safe'));
378 $this->assertEquals(0, count($result['values']['skipped']));
379 $this->assertEquals(1, count($result['values']['merged']));
380 $this->callAPISuccessGetCount('Contribution', array('contact_id' => $contactID), 2);
381 $this->callAPISuccessGetCount('Contribution', array('contact_id' => $contact2ID), 0);
382 $this->callAPISuccessGetCount('FinancialItem', array('contact_id' => $contactID), 2);
383 $this->callAPISuccessGetCount('FinancialItem', array('contact_id' => $contact2ID), 0);
384 $this->callAPISuccessGetCount('Membership', array('contact_id' => $contactID), 2);
385 $this->callAPISuccessGetCount('Membership', array('contact_id' => $contact2ID), 0);
386 $this->callAPISuccessGetCount('EntityTag', array('contact_id' => $contactID), 2);
387 $this->callAPISuccessGetCount('EntityTag', array('contact_id' => $contact2ID), 0);
388 // 14 activities is one for each contribution (2), two (source + target) for each membership (+(2x2) = 6)
389 // 3 for each of the added activities as there are 3 roles (+6 = 12
390 // 2 for the (source & target) contact merged activity (+2 = 14)
391 $this->callAPISuccessGetCount('ActivityContact', array('contact_id' => $contactID), 14);
392 // 2 for the connection to the deleted by merge activity (source & target)
393 $this->callAPISuccessGetCount('ActivityContact', array('contact_id' => $contact2ID), 2);
394 }
395
396 /**
397 * Check that the merge carries across various related entities.
398 *
399 * Note the group combinations 'expected' results:
400 *
401 * Group 0 Added null Added
402 * Group 1 Added Added Added
403 * Group 2 Added Removed **** Added
404 * Group 3 Removed null **** null
405 * Group 4 Removed Added **** Added
406 * Group 5 Removed Removed **** null
407 * Group 6 null Added Added
408 * Group 7 null Removed **** null
409 *
410 * The ones with **** are the ones where I think a case could be made to change the behaviour.
411 */
412 public function testBatchMergeMergesGroups() {
413 $contactID = $this->individualCreate();
414 $contact2ID = $this->individualCreate();
415 $groups = array();
416 for ($i = 0; $i < 8; $i++) {
417 $groups[] = $this->groupCreate(array(
418 'name' => 'mergeGroup' . $i,
419 'title' => 'merge group' . $i,
420 ));
421 }
422
423 $this->callAPISuccess('GroupContact', 'create', array(
424 'contact_id' => $contactID,
425 'group_id' => $groups[0],
426 ));
427 $this->callAPISuccess('GroupContact', 'create', array(
428 'contact_id' => $contactID,
429 'group_id' => $groups[1],
430 ));
431 $this->callAPISuccess('GroupContact', 'create', array(
432 'contact_id' => $contactID,
433 'group_id' => $groups[2],
434 ));
435 $this->callAPISuccess('GroupContact', 'create', array(
436 'contact_id' => $contactID,
437 'group_id' => $groups[3],
438 'status' => 'Removed',
439 ));
440 $this->callAPISuccess('GroupContact', 'create', array(
441 'contact_id' => $contactID,
442 'group_id' => $groups[4],
443 'status' => 'Removed',
444 ));
445 $this->callAPISuccess('GroupContact', 'create', array(
446 'contact_id' => $contactID,
447 'group_id' => $groups[5],
448 'status' => 'Removed',
449 ));
450 $this->callAPISuccess('GroupContact', 'create', array(
451 'contact_id' => $contact2ID,
452 'group_id' => $groups[1],
453 ));
454 $this->callAPISuccess('GroupContact', 'create', array(
455 'contact_id' => $contact2ID,
456 'group_id' => $groups[2],
457 'status' => 'Removed',
458 ));
459 $this->callAPISuccess('GroupContact', 'create', array(
460 'contact_id' => $contact2ID,
461 'group_id' => $groups[4],
462 ));
463 $this->callAPISuccess('GroupContact', 'create', array(
464 'contact_id' => $contact2ID,
465 'group_id' => $groups[5],
466 'status' => 'Removed',
467 ));
468 $this->callAPISuccess('GroupContact', 'create', array(
469 'contact_id' => $contact2ID,
470 'group_id' => $groups[6],
471 ));
472 $this->callAPISuccess('GroupContact', 'create', array(
473 'contact_id' => $contact2ID,
474 'group_id' => $groups[7],
475 'status' => 'Removed',
476 ));
477 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('mode' => 'safe'));
478 $this->assertEquals(0, count($result['values']['skipped']));
479 $this->assertEquals(1, count($result['values']['merged']));
480 $groupResult = $this->callAPISuccess('GroupContact', 'get', array());
481 $this->assertEquals(5, $groupResult['count']);
482 $expectedGroups = array(
483 $groups[0],
484 $groups[1],
485 $groups[2],
486 $groups[4],
487 $groups[6],
488 );
489 foreach ($groupResult['values'] as $groupValues) {
490 $this->assertEquals($contactID, $groupValues['contact_id']);
491 $this->assertEquals('Added', $groupValues['status']);
492 $this->assertTrue(in_array($groupValues['group_id'], $expectedGroups));
493
494 }
495 }
496
497 /**
498 * Test the decisions made for addresses when merging.
499 *
500 * @dataProvider getMergeLocationData
501 *
502 * Scenarios:
503 * (the ones with **** could be disputed as whether it is the best outcome).
504 * 'matching_primary' - Primary matches, including location_type_id. One contact has an additional address.
505 * - result - primary is the shared one. Additional address is retained.
506 * 'matching_primary_reverse' - Primary matches, including location_type_id. Keep both. (opposite order)
507 * - result - primary is the shared one. Additional address is retained.
508 * 'only_one_has_address' - Only one contact has addresses (retain)
509 * - the (only) address is retained
510 * 'only_one_has_address_reverse'
511 * - the (only) address is retained
512 * 'different_primaries_with_different_location_type' Primaries are different but do not clash due to diff type
513 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
514 * 'different_primaries_with_different_location_type_reverse' Primaries are different but do not clash due to diff type
515 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
516 * 'different_primaries_location_match_only_one_address' per previous but a second address matches the primary but is not primary
517 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
518 * 'different_primaries_location_match_only_one_address_reverse' per previous but a second address matches the primary but is not primary
519 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
520 * 'same_primaries_different_location' Primary addresses are the same but have different location type IDs
521 * - result primary kept with the lowest ID. Other address retained too (to preserve location type info).
522 * 'same_primaries_different_location_reverse' Primary addresses are the same but have different location type IDs
523 * - result primary kept with the lowest ID. Other address retained too (to preserve location type info).
524 *
525 * @param array $dataSet
526 */
527 public function testBatchMergesAddresses($dataSet) {
528 $contactID1 = $this->individualCreate();
529 $contactID2 = $this->individualCreate();
530 foreach ($dataSet['contact_1'] as $address) {
531 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(array('contact_id' => $contactID1), $address));
532 }
533 foreach ($dataSet['contact_2'] as $address) {
534 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(array('contact_id' => $contactID2), $address));
535 }
536
537 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('mode' => 'safe'));
538 $this->assertEquals(1, count($result['values']['merged']));
539 $addresses = $this->callAPISuccess($dataSet['entity'], 'get', array('contact_id' => $contactID1, 'sequential' => 1));
540 $this->assertEquals(count($dataSet['expected']), $addresses['count'], "Did not get the expected result for " . $dataSet['entity'] . (!empty($dataSet['description']) ? " on dataset {$dataSet['description']}" : ''));
541 $locationTypes = $this->callAPISuccess($dataSet['entity'], 'getoptions', array('field' => 'location_type_id'));
542 foreach ($dataSet['expected'] as $index => $expectedAddress) {
543 foreach ($expectedAddress as $key => $value) {
544 if ($key == 'location_type_id') {
545 $this->assertEquals($locationTypes['values'][$addresses['values'][$index][$key]], $value);
546 }
547 else {
548 $this->assertEquals($addresses['values'][$index][$key], $value, "mismatch on $key" . (!empty($dataSet['description']) ? " on dataset {$dataSet['description']}" : ''));
549 }
550 }
551 }
552 }
553
554 /**
555 * Test altering the address decision by hook.
556 *
557 * @dataProvider getMergeLocationData
558 *
559 * @param array $dataSet
560 */
561 public function testBatchMergesAddressesHook($dataSet) {
562 $contactID1 = $this->individualCreate();
563 $contactID2 = $this->individualCreate();
564 $this->contributionCreate(array('contact_id' => $contactID1, 'receive_date' => '2010-01-01', 'invoice_id' => 1, 'trxn_id' => 1));
565 $this->contributionCreate(array('contact_id' => $contactID2, 'receive_date' => '2012-01-01', 'invoice_id' => 2, 'trxn_id' => 2));
566 foreach ($dataSet['contact_1'] as $address) {
567 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(array('contact_id' => $contactID1), $address));
568 }
569 foreach ($dataSet['contact_2'] as $address) {
570 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(array('contact_id' => $contactID2), $address));
571 }
572 $this->hookClass->setHook('civicrm_alterLocationMergeData', array($this, 'hookMostRecentDonor'));
573
574 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('mode' => 'safe'));
575 $this->assertEquals(1, count($result['values']['merged']));
576 $addresses = $this->callAPISuccess($dataSet['entity'], 'get', array('contact_id' => $contactID1, 'sequential' => 1));
577 $this->assertEquals(count($dataSet['expected_hook']), $addresses['count']);
578 $locationTypes = $this->callAPISuccess($dataSet['entity'], 'getoptions', array('field' => 'location_type_id'));
579 foreach ($dataSet['expected_hook'] as $index => $expectedAddress) {
580 foreach ($expectedAddress as $key => $value) {
581 if ($key == 'location_type_id') {
582 $this->assertEquals($locationTypes['values'][$addresses['values'][$index][$key]], $value);
583 }
584 else {
585 $this->assertEquals($value, $addresses['values'][$index][$key], $dataSet['entity'] . ': Unexpected value for ' . $key . (!empty($dataSet['description']) ? " on dataset {$dataSet['description']}" : ''));
586 }
587 }
588 }
589 }
590
591 /**
592 * Test the organization will not be matched to an individual.
593 */
594 public function testBatchMergeWillNotMergeOrganizationToIndividual() {
595 $individual = $this->callAPISuccess('Contact', 'create', array(
596 'contact_type' => 'Individual',
597 'organization_name' => 'Anon',
598 'email' => 'anonymous@hacker.com',
599 ));
600 $organization = $this->callAPISuccess('Contact', 'create', array(
601 'contact_type' => 'Organization',
602 'organization_name' => 'Anon',
603 'email' => 'anonymous@hacker.com',
604 ));
605 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('mode' => 'aggressive'));
606 $this->assertEquals(0, count($result['values']['skipped']));
607 $this->assertEquals(0, count($result['values']['merged']));
608 $this->callAPISuccessGetSingle('Contact', array('id' => $individual['id']));
609 $this->callAPISuccessGetSingle('Contact', array('id' => $organization['id']));
610
611 }
612
613 /**
614 * Test hook allowing modification of the data calculated for merging locations.
615 *
616 * We are testing a nuanced real life situation where the address data of the
617 * most recent donor gets priority - resulting in the primary address being set
618 * to the primary address of the most recent donor and address data on a per
619 * location type basis also being set to the most recent donor. Hook also excludes
620 * a fully matching address with a different location.
621 *
622 * This has been added to the test suite to ensure the code supports more this
623 * type of intervention.
624 *
625 * @param array $blocksDAO
626 * Array of location DAO to be saved. These are arrays in 2 keys 'update' & 'delete'.
627 * @param int $mainId
628 * Contact_id of the contact that survives the merge.
629 * @param int $otherId
630 * Contact_id of the contact that will be absorbed and deleted.
631 * @param array $migrationInfo
632 * Calculated migration info, informational only.
633 *
634 * @return mixed
635 */
636 public function hookMostRecentDonor(&$blocksDAO, $mainId, $otherId, $migrationInfo) {
637
638 $lastDonorID = $this->callAPISuccessGetValue('Contribution', array(
639 'return' => 'contact_id',
640 'contact_id' => array('IN' => array($mainId, $otherId)),
641 'options' => array('sort' => 'receive_date DESC', 'limit' => 1),
642 ));
643 // Since the last donor is not the main ID we are prioritising info from the last donor.
644 // In the test this should always be true - but keep the check in case
645 // something changes that we need to detect.
646 if ($lastDonorID != $mainId) {
647 foreach ($migrationInfo['other_details']['location_blocks'] as $blockType => $blocks) {
648 foreach ($blocks as $block) {
649 if ($block['is_primary']) {
650 $primaryAddressID = $block['id'];
651 if (!empty($migrationInfo['main_details']['location_blocks'][$blockType])) {
652 foreach ($migrationInfo['main_details']['location_blocks'][$blockType] as $mainBlock) {
653 if (empty($blocksDAO[$blockType]['update'][$block['id']]) && $mainBlock['location_type_id'] == $block['location_type_id']) {
654 // This was an address match - we just need to check the is_primary
655 // is true on the matching kept address.
656 $primaryAddressID = $mainBlock['id'];
657 $blocksDAO[$blockType]['update'][$primaryAddressID] = _civicrm_api3_load_DAO($blockType);
658 $blocksDAO[$blockType]['update'][$primaryAddressID]->id = $primaryAddressID;
659 }
660 $mainLocationTypeID = $mainBlock['location_type_id'];
661 // We also want to be more ruthless about removing matching addresses.
662 unset($mainBlock['location_type_id']);
663 if (CRM_Dedupe_Merger::locationIsSame($block, $mainBlock)
664 && (!isset($blocksDAO[$blockType]['update']) || !isset($blocksDAO[$blockType]['update'][$mainBlock['id']]))
665 && (!isset($blocksDAO[$blockType]['delete']) || !isset($blocksDAO[$blockType]['delete'][$mainBlock['id']]))
666 ) {
667 $blocksDAO[$blockType]['delete'][$mainBlock['id']] = _civicrm_api3_load_DAO($blockType);
668 $blocksDAO[$blockType]['delete'][$mainBlock['id']]->id = $mainBlock['id'];
669 }
670 // Arguably the right way to handle this is just to set is_primary for the primary
671 // and for the merge fn to call something like BAO::add & hooks to work etc.
672 // if that happens though this should keep working...
673 elseif ($mainBlock['is_primary'] && $mainLocationTypeID != $block['location_type_id']) {
674 $blocksDAO['address']['update'][$mainBlock['id']] = _civicrm_api3_load_DAO($blockType);
675 $blocksDAO['address']['update'][$mainBlock['id']]->is_primary = 0;
676 $blocksDAO['address']['update'][$mainBlock['id']]->id = $mainBlock['id'];
677 }
678
679 }
680 $blocksDAO[$blockType]['update'][$primaryAddressID]->is_primary = 1;
681 }
682 }
683 }
684 }
685 }
686 }
687
688 /**
689 * Get address combinations for the merge test.
690 *
691 * @return array
692 */
693 public function getMergeLocationData() {
694 $address1 = array('street_address' => 'Buckingham Palace', 'city' => 'London');
695 $address2 = array('street_address' => 'The Doghouse', 'supplemental_address_1' => 'under the blanket');
696 $data = $this->getMergeLocations($address1, $address2, 'Address');
697 $data = array_merge($data, $this->getMergeLocations(array('phone' => '12345', 'phone_type_id' => 1), array('phone' => '678910', 'phone_type_id' => 1), 'Phone'));
698 $data = array_merge($data, $this->getMergeLocations(array('phone' => '12345'), array('phone' => '678910'), 'Phone'));
699 $data = array_merge($data, $this->getMergeLocations(array('email' => 'mini@me.com'), array('email' => 'mini@me.org'), 'Email', array(array(
700 'email' => 'anthony_anderson@civicrm.org',
701 'location_type_id' => 'Home',
702 ))));
703 return $data;
704
705 }
706
707 /**
708 * Test the batch merge does not create duplicate emails.
709 *
710 * Test CRM-18546, a 4.7 regression whereby a merged contact gets duplicate emails.
711 */
712 public function testBatchMergeEmailHandling() {
713 for ($x = 0; $x <= 4; $x++) {
714 $id = $this->individualCreate(array('email' => 'batman@gotham.met'));
715 }
716 $result = $this->callAPISuccess('Job', 'process_batch_merge', array());
717 $this->assertEquals(4, count($result['values']['merged']));
718 $this->callAPISuccessGetCount('Contact', array('email' => 'batman@gotham.met'), 1);
719 $contacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 0));
720 $deletedContacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 1));
721 $this->callAPISuccessGetCount('Email', array(
722 'email' => 'batman@gotham.met',
723 'contact_id' => array('IN' => array_keys($contacts['values'])),
724 ), 1);
725 $this->callAPISuccessGetCount('Email', array(
726 'email' => 'batman@gotham.met',
727 'contact_id' => array('IN' => array_keys($deletedContacts['values'])),
728 ), 4);
729 }
730
731 /**
732 * Test the batch merge respects email "on hold".
733 *
734 * Test CRM-19148, Batch merge - Email on hold data lost when there is a conflict.
735 *
736 * @dataProvider getOnHoldSets
737 *
738 * @param
739 */
740 public function testBatchMergeEmailOnHold($onHold1, $onHold2, $merge) {
741 $contactID1 = $this->individualCreate(array(
742 'api.email.create' => array(
743 'email' => 'batman@gotham.met',
744 'location_type_id' => 'Work',
745 'is_primary' => 1,
746 'on_hold' => $onHold1,
747 ),
748 ));
749 $contactID2 = $this->individualCreate(array(
750 'api.email.create' => array(
751 'email' => 'batman@gotham.met',
752 'location_type_id' => 'Work',
753 'is_primary' => 1,
754 'on_hold' => $onHold2,
755 ),
756 ));
757 $result = $this->callAPISuccess('Job', 'process_batch_merge', array());
758 $this->assertEquals($merge, count($result['values']['merged']));
759 }
760
761 /**
762 * Data provider for testBatchMergeEmailOnHold: combinations of on_hold & expected outcomes.
763 */
764 public function getOnHoldSets() {
765 // Each row specifies: contact 1 on_hold, contact 2 on_hold, merge? (0 or 1),
766 $sets = array(
767 array(0, 0, 1),
768 array(0, 1, 0),
769 array(1, 0, 0),
770 array(1, 1, 1),
771 );
772 return $sets;
773 }
774
775 /**
776 * Test the batch merge does not fatal on an empty rule.
777 *
778 * @dataProvider getRuleSets
779 *
780 * @param string $contactType
781 * @param string $used
782 * @param bool $isReserved
783 * @param int $threshold
784 */
785 public function testBatchMergeEmptyRule($contactType, $used, $name, $isReserved, $threshold) {
786 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', array(
787 'contact_type' => $contactType,
788 'threshold' => $threshold,
789 'used' => $used,
790 'name' => $name,
791 'is_reserved' => $isReserved,
792 ));
793 $this->callAPISuccess('Job', 'process_batch_merge', array('rule_group_id' => $ruleGroup['id']));
794 $this->callAPISuccess('RuleGroup', 'delete', array('id' => $ruleGroup['id']));
795 }
796
797 /**
798 * Get the various rule combinations.
799 */
800 public function getRuleSets() {
801 $contactTypes = array('Individual', 'Organization', 'Household');
802 $useds = array('Unsupervised', 'General', 'Supervised');
803 $ruleGroups = array();
804 foreach ($contactTypes as $contactType) {
805 foreach ($useds as $used) {
806 $ruleGroups[] = array($contactType, $used, 'Bob', FALSE, 0);
807 $ruleGroups[] = array($contactType, $used, 'Bob', FALSE, 10);
808 $ruleGroups[] = array($contactType, $used, 'Bob', TRUE, 10);
809 $ruleGroups[] = array($contactType, $used, $contactType . $used, FALSE, 10);
810 $ruleGroups[] = array($contactType, $used, $contactType . $used, TRUE, 10);
811 }
812 }
813 return $ruleGroups;
814 }
815
816 /**
817 * Test the batch merge does not create duplicate emails.
818 *
819 * Test CRM-18546, a 4.7 regression whereby a merged contact gets duplicate emails.
820 */
821 public function testBatchMergeMatchingAddress() {
822 for ($x = 0; $x <= 2; $x++) {
823 $this->individualCreate(array(
824 'api.address.create' => array(
825 'location_type_id' => 'Home',
826 'street_address' => 'Appt 115, The Batcave',
827 'city' => 'Gotham',
828 'postal_code' => 'Nananananana',
829 ),
830 ));
831 }
832 // Different location type, still merge, identical.
833 $this->individualCreate(array(
834 'api.address.create' => array(
835 'location_type_id' => 'Main',
836 'street_address' => 'Appt 115, The Batcave',
837 'city' => 'Gotham',
838 'postal_code' => 'Nananananana',
839 ),
840 ));
841
842 $this->individualCreate(array(
843 'api.address.create' => array(
844 'location_type_id' => 'Home',
845 'street_address' => 'Appt 115, The Batcave',
846 'city' => 'Gotham',
847 'postal_code' => 'Batman',
848 ),
849 ));
850
851 $result = $this->callAPISuccess('Job', 'process_batch_merge', array());
852 $this->assertEquals(3, count($result['values']['merged']));
853 $this->assertEquals(1, count($result['values']['skipped']));
854 $this->callAPISuccessGetCount('Contact', array('street_address' => 'Appt 115, The Batcave'), 2);
855 $contacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 0));
856 $deletedContacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 1));
857 $this->callAPISuccessGetCount('Address', array(
858 'street_address' => 'Appt 115, The Batcave',
859 'contact_id' => array('IN' => array_keys($contacts['values'])),
860 ), 3);
861
862 $this->callAPISuccessGetCount('Address', array(
863 'street_address' => 'Appt 115, The Batcave',
864 'contact_id' => array('IN' => array_keys($deletedContacts['values'])),
865 ), 2);
866 }
867
868 /**
869 * Test the batch merge by id range.
870 *
871 * We have 2 sets of 5 matches & set the merge only to merge the lower set.
872 */
873 public function testBatchMergeIDRange() {
874 for ($x = 0; $x <= 4; $x++) {
875 $id = $this->individualCreate(array('email' => 'batman@gotham.met'));
876 }
877 for ($x = 0; $x <= 4; $x++) {
878 $this->individualCreate(array('email' => 'robin@gotham.met'));
879 }
880 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('criteria' => array('contact' => array('id' => array('<' => $id)))));
881 $this->assertEquals(4, count($result['values']['merged']));
882 $this->callAPISuccessGetCount('Contact', array('email' => 'batman@gotham.met'), 1);
883 $this->callAPISuccessGetCount('Contact', array('email' => 'robin@gotham.met'), 5);
884 $contacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 0));
885 $deletedContacts = $this->callAPISuccess('Contact', 'get', array('is_deleted' => 0));
886 $this->callAPISuccessGetCount('Email', array(
887 'email' => 'batman@gotham.met',
888 'contact_id' => array('IN' => array_keys($contacts['values'])),
889 ), 1);
890 $this->callAPISuccessGetCount('Email', array(
891 'email' => 'batman@gotham.met',
892 'contact_id' => array('IN' => array_keys($deletedContacts['values'])),
893 ), 1);
894 $this->callAPISuccessGetCount('Email', array(
895 'email' => 'robin@gotham.met',
896 'contact_id' => array('IN' => array_keys($contacts['values'])),
897 ), 5);
898
899 }
900
901 /**
902 * Test the batch merge copes with view only custom data field.
903 */
904 public function testBatchMergeCustomDataViewOnlyField() {
905 CRM_Core_Config::singleton()->userPermissionClass->permissions = array('access CiviCRM', 'edit my contact');
906 $mouseParams = ['first_name' => 'Mickey', 'last_name' => 'Mouse', 'email' => 'tha_mouse@mouse.com'];
907 $this->individualCreate($mouseParams);
908
909 $customGroup = $this->CustomGroupCreate();
910 $customField = $this->customFieldCreate(array('custom_group_id' => $customGroup['id'], 'is_view' => 1));
911 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 'blah']));
912
913 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('check_permissions' => 0, 'mode' => 'safe'));
914 $this->assertEquals(1, count($result['values']['merged']));
915 $mouseParams['return'] = 'custom_' . $customField['id'];
916 $mouse = $this->callAPISuccess('Contact', 'getsingle', $mouseParams);
917 $this->assertEquals('blah', $mouse['custom_' . $customField['id']]);
918
919 $this->customFieldDelete($customGroup['id']);
920 $this->customGroupDelete($customGroup['id']);
921 }
922
923 /**
924 * Test the batch merge function actually works!
925 *
926 * @dataProvider getMergeSets
927 *
928 * @param $dataSet
929 */
930 public function testBatchMergeWorksCheckPermissionsTrue($dataSet) {
931 CRM_Core_Config::singleton()->userPermissionClass->permissions = array('access CiviCRM', 'administer CiviCRM');
932 foreach ($dataSet['contacts'] as $params) {
933 $this->callAPISuccess('Contact', 'create', $params);
934 }
935
936 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('check_permissions' => 1, 'mode' => $dataSet['mode']));
937 $this->assertEquals(0, count($result['values']['merged']), 'User does not have permission to any contacts, so no merging');
938 $this->assertEquals(0, count($result['values']['skipped']), 'User does not have permission to any contacts, so no skip visibility');
939 }
940
941 /**
942 * Test the batch merge function actually works!
943 *
944 * @dataProvider getMergeSets
945 *
946 * @param $dataSet
947 */
948 public function testBatchMergeWorksCheckPermissionsFalse($dataSet) {
949 CRM_Core_Config::singleton()->userPermissionClass->permissions = array('access CiviCRM', 'edit my contact');
950 foreach ($dataSet['contacts'] as $params) {
951 $this->callAPISuccess('Contact', 'create', $params);
952 }
953
954 $result = $this->callAPISuccess('Job', 'process_batch_merge', array('check_permissions' => 0, 'mode' => $dataSet['mode']));
955 $this->assertEquals($dataSet['skipped'], count($result['values']['skipped']), 'Failed to skip the right number:' . $dataSet['skipped']);
956 $this->assertEquals($dataSet['merged'], count($result['values']['merged']));
957 }
958
959 /**
960 * Get data for batch merge.
961 */
962 public function getMergeSets() {
963 $data = array(
964 array(
965 array(
966 'mode' => 'safe',
967 'contacts' => array(
968 array(
969 'first_name' => 'Michael',
970 'last_name' => 'Jackson',
971 'email' => 'michael@neverland.com',
972 'contact_type' => 'Individual',
973 'contact_sub_type' => 'Student',
974 'api.Address.create' => array(
975 'street_address' => 'big house',
976 'location_type_id' => 'Home',
977 ),
978 ),
979 array(
980 'first_name' => 'Michael',
981 'last_name' => 'Jackson',
982 'email' => 'michael@neverland.com',
983 'contact_type' => 'Individual',
984 'contact_sub_type' => 'Student',
985 ),
986 ),
987 'skipped' => 0,
988 'merged' => 1,
989 'expected' => array(
990 array(
991 'first_name' => 'Michael',
992 'last_name' => 'Jackson',
993 'email' => 'michael@neverland.com',
994 'contact_type' => 'Individual',
995 ),
996 ),
997 ),
998 ),
999 array(
1000 array(
1001 'mode' => 'safe',
1002 'contacts' => array(
1003 array(
1004 'first_name' => 'Michael',
1005 'last_name' => 'Jackson',
1006 'email' => 'michael@neverland.com',
1007 'contact_type' => 'Individual',
1008 'contact_sub_type' => 'Student',
1009 'api.Address.create' => array(
1010 'street_address' => 'big house',
1011 'location_type_id' => 'Home',
1012 ),
1013 ),
1014 array(
1015 'first_name' => 'Michael',
1016 'last_name' => 'Jackson',
1017 'email' => 'michael@neverland.com',
1018 'contact_type' => 'Individual',
1019 'contact_sub_type' => 'Student',
1020 'api.Address.create' => array(
1021 'street_address' => 'bigger house',
1022 'location_type_id' => 'Home',
1023 ),
1024 ),
1025 ),
1026 'skipped' => 1,
1027 'merged' => 0,
1028 'expected' => array(
1029 array(
1030 'first_name' => 'Michael',
1031 'last_name' => 'Jackson',
1032 'email' => 'michael@neverland.com',
1033 'contact_type' => 'Individual',
1034 'street_address' => 'big house',
1035 ),
1036 array(
1037 'first_name' => 'Michael',
1038 'last_name' => 'Jackson',
1039 'email' => 'michael@neverland.com',
1040 'contact_type' => 'Individual',
1041 'street_address' => 'bigger house',
1042 ),
1043 ),
1044 ),
1045 ),
1046 array(
1047 array(
1048 'mode' => 'safe',
1049 'contacts' => array(
1050 array(
1051 'first_name' => 'Michael',
1052 'last_name' => 'Jackson',
1053 'email' => 'michael@neverland.com',
1054 'contact_type' => 'Individual',
1055 'contact_sub_type' => 'Student',
1056 'api.Email.create' => array(
1057 'email' => 'big.slog@work.co.nz',
1058 'location_type_id' => 'Work',
1059 ),
1060 ),
1061 array(
1062 'first_name' => 'Michael',
1063 'last_name' => 'Jackson',
1064 'email' => 'michael@neverland.com',
1065 'contact_type' => 'Individual',
1066 'contact_sub_type' => 'Student',
1067 'api.Email.create' => array(
1068 'email' => 'big.slog@work.com',
1069 'location_type_id' => 'Work',
1070 ),
1071 ),
1072 ),
1073 'skipped' => 1,
1074 'merged' => 0,
1075 'expected' => array(
1076 array(
1077 'first_name' => 'Michael',
1078 'last_name' => 'Jackson',
1079 'email' => 'michael@neverland.com',
1080 'contact_type' => 'Individual',
1081 ),
1082 array(
1083 'first_name' => 'Michael',
1084 'last_name' => 'Jackson',
1085 'email' => 'michael@neverland.com',
1086 'contact_type' => 'Individual',
1087 ),
1088 ),
1089 ),
1090 ),
1091 array(
1092 array(
1093 'mode' => 'safe',
1094 'contacts' => array(
1095 array(
1096 'first_name' => 'Michael',
1097 'last_name' => 'Jackson',
1098 'email' => 'michael@neverland.com',
1099 'contact_type' => 'Individual',
1100 'contact_sub_type' => 'Student',
1101 'api.Phone.create' => array(
1102 'phone' => '123456',
1103 'location_type_id' => 'Work',
1104 ),
1105 ),
1106 array(
1107 'first_name' => 'Michael',
1108 'last_name' => 'Jackson',
1109 'email' => 'michael@neverland.com',
1110 'contact_type' => 'Individual',
1111 'contact_sub_type' => 'Student',
1112 'api.Phone.create' => array(
1113 'phone' => '23456',
1114 'location_type_id' => 'Work',
1115 ),
1116 ),
1117 ),
1118 'skipped' => 1,
1119 'merged' => 0,
1120 'expected' => array(
1121 array(
1122 'first_name' => 'Michael',
1123 'last_name' => 'Jackson',
1124 'email' => 'michael@neverland.com',
1125 'contact_type' => 'Individual',
1126 ),
1127 array(
1128 'first_name' => 'Michael',
1129 'last_name' => 'Jackson',
1130 'email' => 'michael@neverland.com',
1131 'contact_type' => 'Individual',
1132 ),
1133 ),
1134 ),
1135 ),
1136 array(
1137 array(
1138 'mode' => 'aggressive',
1139 'contacts' => array(
1140 array(
1141 'first_name' => 'Michael',
1142 'last_name' => 'Jackson',
1143 'email' => 'michael@neverland.com',
1144 'contact_type' => 'Individual',
1145 'contact_sub_type' => 'Student',
1146 'api.Address.create' => array(
1147 'street_address' => 'big house',
1148 'location_type_id' => 'Home',
1149 ),
1150 ),
1151 array(
1152 'first_name' => 'Michael',
1153 'last_name' => 'Jackson',
1154 'email' => 'michael@neverland.com',
1155 'contact_type' => 'Individual',
1156 'contact_sub_type' => 'Student',
1157 'api.Address.create' => array(
1158 'street_address' => 'bigger house',
1159 'location_type_id' => 'Home',
1160 ),
1161 ),
1162 ),
1163 'skipped' => 0,
1164 'merged' => 1,
1165 'expected' => array(
1166 array(
1167 'first_name' => 'Michael',
1168 'last_name' => 'Jackson',
1169 'email' => 'michael@neverland.com',
1170 'contact_type' => 'Individual',
1171 'street_address' => 'big house',
1172 ),
1173 ),
1174 ),
1175 ),
1176 array(
1177 array(
1178 'mode' => 'safe',
1179 'contacts' => array(
1180 array(
1181 'first_name' => 'Michael',
1182 'last_name' => 'Jackson',
1183 'email' => 'michael@neverland.com',
1184 'contact_type' => 'Individual',
1185 'contact_sub_type' => 'Student',
1186 'api.Address.create' => array(
1187 'street_address' => 'big house',
1188 'location_type_id' => 'Home',
1189 ),
1190 ),
1191 array(
1192 'first_name' => 'Michael',
1193 'last_name' => 'Jackson',
1194 'email' => 'michael@neverland.com',
1195 'contact_type' => 'Individual',
1196 'contact_sub_type' => 'Student',
1197 'is_deceased' => 1,
1198 ),
1199 ),
1200 'skipped' => 1,
1201 'merged' => 0,
1202 'expected' => array(
1203 array(
1204 'first_name' => 'Michael',
1205 'last_name' => 'Jackson',
1206 'email' => 'michael@neverland.com',
1207 'contact_type' => 'Individual',
1208 'is_deceased' => 0,
1209 ),
1210 array(
1211 'first_name' => 'Michael',
1212 'last_name' => 'Jackson',
1213 'email' => 'michael@neverland.com',
1214 'contact_type' => 'Individual',
1215 'is_deceased' => 1,
1216 ),
1217 ),
1218 ),
1219 ),
1220 array(
1221 array(
1222 'mode' => 'safe',
1223 'contacts' => array(
1224 array(
1225 'first_name' => 'Michael',
1226 'last_name' => 'Jackson',
1227 'email' => 'michael@neverland.com',
1228 'contact_type' => 'Individual',
1229 'contact_sub_type' => 'Student',
1230 'api.Address.create' => array(
1231 'street_address' => 'big house',
1232 'location_type_id' => 'Home',
1233 ),
1234 'is_deceased' => 1,
1235 ),
1236 array(
1237 'first_name' => 'Michael',
1238 'last_name' => 'Jackson',
1239 'email' => 'michael@neverland.com',
1240 'contact_type' => 'Individual',
1241 'contact_sub_type' => 'Student',
1242 ),
1243 ),
1244 'skipped' => 1,
1245 'merged' => 0,
1246 'expected' => array(
1247 array(
1248 'first_name' => 'Michael',
1249 'last_name' => 'Jackson',
1250 'email' => 'michael@neverland.com',
1251 'contact_type' => 'Individual',
1252 'is_deceased' => 1,
1253 ),
1254 array(
1255 'first_name' => 'Michael',
1256 'last_name' => 'Jackson',
1257 'email' => 'michael@neverland.com',
1258 'contact_type' => 'Individual',
1259 'is_deceased' => 0,
1260 ),
1261 ),
1262 ),
1263 ),
1264 );
1265
1266 $conflictPairs = array(
1267 'first_name' => 'Dianna',
1268 'last_name' => 'McAndrew',
1269 'middle_name' => 'Prancer',
1270 'birth_date' => '2015-12-25',
1271 'gender_id' => 'Female',
1272 'job_title' => 'Thriller',
1273 );
1274
1275 foreach ($conflictPairs as $key => $value) {
1276 $contactParams = array(
1277 'first_name' => 'Michael',
1278 'middle_name' => 'Dancer',
1279 'last_name' => 'Jackson',
1280 'birth_date' => '2015-02-25',
1281 'email' => 'michael@neverland.com',
1282 'contact_type' => 'Individual',
1283 'contact_sub_type' => array('Student'),
1284 'gender_id' => 'Male',
1285 'job_title' => 'Entertainer',
1286 );
1287 $contact2 = $contactParams;
1288
1289 $contact2[$key] = $value;
1290 $data[$key . '_conflict'] = array(
1291 array(
1292 'mode' => 'safe',
1293 'contacts' => array($contactParams, $contact2),
1294 'skipped' => 1,
1295 'merged' => 0,
1296 'expected' => array($contactParams, $contact2),
1297 ),
1298 );
1299 }
1300
1301 return $data;
1302 }
1303
1304 /**
1305 * @param $op
1306 * @param string $objectName
1307 * @param int $id
1308 * @param array $params
1309 */
1310 public function hookPreRelationship($op, $objectName, $id, &$params) {
1311 if ($op == 'delete') {
1312 return;
1313 }
1314 if ($params['is_active']) {
1315 $params['description'] = 'Hooked';
1316 }
1317 else {
1318 $params['description'] = 'Go Go you good thing';
1319 }
1320 }
1321
1322 /**
1323 * Get the location data set.
1324 *
1325 * @param array $locationParams1
1326 * @param array $locationParams2
1327 * @param string $entity
1328 *
1329 * @return array
1330 */
1331 public function getMergeLocations($locationParams1, $locationParams2, $entity, $additionalExpected = array()) {
1332 $data = array(
1333 array(
1334 'matching_primary' => array(
1335 'entity' => $entity,
1336 'contact_1' => array(
1337 array_merge(array(
1338 'location_type_id' => 'Main',
1339 'is_primary' => 1,
1340 ), $locationParams1),
1341 array_merge(array(
1342 'location_type_id' => 'Work',
1343 'is_primary' => 0,
1344 ), $locationParams2),
1345 ),
1346 'contact_2' => array(
1347 array_merge(array(
1348 'location_type_id' => 'Main',
1349 'is_primary' => 1,
1350 ), $locationParams1),
1351 ),
1352 'expected' => array_merge($additionalExpected, array(
1353 array_merge(array(
1354 'location_type_id' => 'Main',
1355 'is_primary' => 1,
1356 ), $locationParams1),
1357 array_merge(array(
1358 'location_type_id' => 'Work',
1359 'is_primary' => 0,
1360 ), $locationParams2),
1361 )),
1362 'expected_hook' => array_merge($additionalExpected, array(
1363 array_merge(array(
1364 'location_type_id' => 'Main',
1365 'is_primary' => 1,
1366 ), $locationParams1),
1367 array_merge(array(
1368 'location_type_id' => 'Work',
1369 'is_primary' => 0,
1370 ), $locationParams2),
1371 )),
1372 ),
1373 ),
1374 array(
1375 'matching_primary_reverse' => array(
1376 'entity' => $entity,
1377 'contact_1' => array(
1378 array_merge(array(
1379 'location_type_id' => 'Main',
1380 'is_primary' => 1,
1381 ), $locationParams1),
1382 ),
1383 'contact_2' => array(
1384 array_merge(array(
1385 'location_type_id' => 'Main',
1386 'is_primary' => 1,
1387 ), $locationParams1),
1388 array_merge(array(
1389 'location_type_id' => 'Work',
1390 'is_primary' => 0,
1391 ), $locationParams2),
1392 ),
1393 'expected' => array_merge($additionalExpected, array(
1394 array_merge(array(
1395 'location_type_id' => 'Main',
1396 'is_primary' => 1,
1397 ), $locationParams1),
1398 array_merge(array(
1399 'location_type_id' => 'Work',
1400 'is_primary' => 0,
1401 ), $locationParams2),
1402 )),
1403 'expected_hook' => array_merge($additionalExpected, array(
1404 array_merge(array(
1405 'location_type_id' => 'Main',
1406 'is_primary' => 1,
1407 ), $locationParams1),
1408 array_merge(array(
1409 'location_type_id' => 'Work',
1410 'is_primary' => 0,
1411 ), $locationParams2),
1412 )),
1413 ),
1414 ),
1415 array(
1416 'only_one_has_address' => array(
1417 'entity' => $entity,
1418 'contact_1' => array(
1419 array_merge(array(
1420 'location_type_id' => 'Main',
1421 'is_primary' => 1,
1422 ), $locationParams1),
1423 array_merge(array(
1424 'location_type_id' => 'Work',
1425 'is_primary' => 0,
1426 ), $locationParams2),
1427 ),
1428 'contact_2' => array(),
1429 'expected' => array_merge($additionalExpected, array(
1430 array_merge(array(
1431 'location_type_id' => 'Main',
1432 'is_primary' => 1,
1433 ), $locationParams1),
1434 array_merge(array(
1435 'location_type_id' => 'Work',
1436 'is_primary' => 0,
1437 ), $locationParams2),
1438 )),
1439 'expected_hook' => array_merge($additionalExpected, array(
1440 array_merge(array(
1441 'location_type_id' => 'Main',
1442 // When dealing with email we don't have a clean slate - the existing
1443 // primary will be primary.
1444 'is_primary' => ($entity == 'Email' ? 0 : 1),
1445 ), $locationParams1),
1446 array_merge(array(
1447 'location_type_id' => 'Work',
1448 'is_primary' => 0,
1449 ), $locationParams2),
1450 )),
1451 ),
1452 ),
1453 array(
1454 'only_one_has_address_reverse' => array(
1455 'description' => 'The destination contact does not have an address. secondary contact should be merged in.',
1456 'entity' => $entity,
1457 'contact_1' => array(),
1458 'contact_2' => array(
1459 array_merge(array(
1460 'location_type_id' => 'Main',
1461 'is_primary' => 1,
1462 ), $locationParams1),
1463 array_merge(array(
1464 'location_type_id' => 'Work',
1465 'is_primary' => 0,
1466 ), $locationParams2),
1467 ),
1468 'expected' => array_merge($additionalExpected, array(
1469 array_merge(array(
1470 'location_type_id' => 'Main',
1471 // When dealing with email we don't have a clean slate - the existing
1472 // primary will be primary.
1473 'is_primary' => ($entity == 'Email' ? 0 : 1),
1474 ), $locationParams1),
1475 array_merge(array(
1476 'location_type_id' => 'Work',
1477 'is_primary' => 0,
1478 ), $locationParams2),
1479 )),
1480 'expected_hook' => array_merge($additionalExpected, array(
1481 array_merge(array(
1482 'location_type_id' => 'Main',
1483 'is_primary' => 1,
1484 ), $locationParams1),
1485 array_merge(array(
1486 'location_type_id' => 'Work',
1487 'is_primary' => 0,
1488 ), $locationParams2),
1489 )),
1490 ),
1491 ),
1492 array(
1493 'different_primaries_with_different_location_type' => array(
1494 'description' => 'Primaries are different with different location. Keep both addresses. Set primary to be that of lower id',
1495 'entity' => $entity,
1496 'contact_1' => array(
1497 array_merge(array(
1498 'location_type_id' => 'Main',
1499 'is_primary' => 1,
1500 ), $locationParams1),
1501 ),
1502 'contact_2' => array(
1503 array_merge(array(
1504 'location_type_id' => 'Work',
1505 'is_primary' => 1,
1506 ), $locationParams2),
1507 ),
1508 'expected' => array_merge($additionalExpected, array(
1509 array_merge(array(
1510 'location_type_id' => 'Main',
1511 'is_primary' => 1,
1512 ), $locationParams1),
1513 array_merge(array(
1514 'location_type_id' => 'Work',
1515 'is_primary' => 0,
1516 ), $locationParams2),
1517 )),
1518 'expected_hook' => array_merge($additionalExpected, array(
1519 array_merge(array(
1520 'location_type_id' => 'Main',
1521 'is_primary' => 0,
1522 ), $locationParams1),
1523 array_merge(array(
1524 'location_type_id' => 'Work',
1525 'is_primary' => 1,
1526 ), $locationParams2),
1527 )),
1528 ),
1529 ),
1530 array(
1531 'different_primaries_with_different_location_type_reverse' => array(
1532 'entity' => $entity,
1533 'contact_1' => array(
1534 array_merge(array(
1535 'location_type_id' => 'Work',
1536 'is_primary' => 1,
1537 ), $locationParams2),
1538 ),
1539 'contact_2' => array(
1540 array_merge(array(
1541 'location_type_id' => 'Main',
1542 'is_primary' => 1,
1543 ), $locationParams1),
1544 ),
1545 'expected' => array_merge($additionalExpected, array(
1546 array_merge(array(
1547 'location_type_id' => 'Work',
1548 'is_primary' => 1,
1549 ), $locationParams2),
1550 array_merge(array(
1551 'location_type_id' => 'Main',
1552 'is_primary' => 0,
1553 ), $locationParams1),
1554 )),
1555 'expected_hook' => array_merge($additionalExpected, array(
1556 array_merge(array(
1557 'location_type_id' => 'Work',
1558 'is_primary' => 0,
1559 ), $locationParams2),
1560 array_merge(array(
1561 'location_type_id' => 'Main',
1562 'is_primary' => 1,
1563 ), $locationParams1),
1564 )),
1565 ),
1566 ),
1567 array(
1568 'different_primaries_location_match_only_one_address' => array(
1569 'entity' => $entity,
1570 'contact_1' => array(
1571 array_merge(array(
1572 'location_type_id' => 'Main',
1573 'is_primary' => 1,
1574 ), $locationParams1),
1575 array_merge(array(
1576 'location_type_id' => 'Work',
1577 'is_primary' => 0,
1578 ), $locationParams2),
1579 ),
1580 'contact_2' => array(
1581 array_merge(array(
1582 'location_type_id' => 'Work',
1583 'is_primary' => 1,
1584 ), $locationParams2),
1585
1586 ),
1587 'expected' => array_merge($additionalExpected, array(
1588 array_merge(array(
1589 'location_type_id' => 'Main',
1590 'is_primary' => 1,
1591 ), $locationParams1),
1592 array_merge(array(
1593 'location_type_id' => 'Work',
1594 'is_primary' => 0,
1595 ), $locationParams2),
1596 )),
1597 'expected_hook' => array_merge($additionalExpected, array(
1598 array_merge(array(
1599 'location_type_id' => 'Main',
1600 'is_primary' => 0,
1601 ), $locationParams1),
1602 array_merge(array(
1603 'location_type_id' => 'Work',
1604 'is_primary' => 1,
1605 ), $locationParams2),
1606 )),
1607 ),
1608 ),
1609 array(
1610 'different_primaries_location_match_only_one_address_reverse' => array(
1611 'entity' => $entity,
1612 'contact_1' => array(
1613 array_merge(array(
1614 'location_type_id' => 'Work',
1615 'is_primary' => 1,
1616 ), $locationParams2),
1617 ),
1618 'contact_2' => array(
1619 array_merge(array(
1620 'location_type_id' => 'Main',
1621 'is_primary' => 1,
1622 ), $locationParams1),
1623 array_merge(array(
1624 'location_type_id' => 'Work',
1625 'is_primary' => 0,
1626 ), $locationParams2),
1627 ),
1628 'expected' => array_merge($additionalExpected, array(
1629 array_merge(array(
1630 'location_type_id' => 'Work',
1631 'is_primary' => 1,
1632 ), $locationParams2),
1633 array_merge(array(
1634 'location_type_id' => 'Main',
1635 'is_primary' => 0,
1636 ), $locationParams1),
1637 )),
1638 'expected_hook' => array_merge($additionalExpected, array(
1639 array_merge(array(
1640 'location_type_id' => 'Work',
1641 'is_primary' => 0,
1642 ), $locationParams2),
1643 array_merge(array(
1644 'location_type_id' => 'Main',
1645 'is_primary' => 1,
1646 ), $locationParams1),
1647 )),
1648 ),
1649 ),
1650 array(
1651 'same_primaries_different_location' => array(
1652 'entity' => $entity,
1653 'contact_1' => array(
1654 array_merge(array(
1655 'location_type_id' => 'Main',
1656 'is_primary' => 1,
1657 ), $locationParams1),
1658 ),
1659 'contact_2' => array(
1660 array_merge(array(
1661 'location_type_id' => 'Work',
1662 'is_primary' => 1,
1663 ), $locationParams1),
1664
1665 ),
1666 'expected' => array_merge($additionalExpected, array(
1667 array_merge(array(
1668 'location_type_id' => 'Main',
1669 'is_primary' => 1,
1670 ), $locationParams1),
1671 array_merge(array(
1672 'location_type_id' => 'Work',
1673 'is_primary' => 0,
1674 ), $locationParams1),
1675 )),
1676 'expected_hook' => array_merge($additionalExpected, array(
1677 array_merge(array(
1678 'location_type_id' => 'Work',
1679 'is_primary' => 1,
1680 ), $locationParams1),
1681 )),
1682 ),
1683 ),
1684 array(
1685 'same_primaries_different_location_reverse' => array(
1686 'entity' => $entity,
1687 'contact_1' => array(
1688 array_merge(array(
1689 'location_type_id' => 'Work',
1690 'is_primary' => 1,
1691 ), $locationParams1),
1692 ),
1693 'contact_2' => array(
1694 array_merge(array(
1695 'location_type_id' => 'Main',
1696 'is_primary' => 1,
1697 ), $locationParams1),
1698 ),
1699 'expected' => array_merge($additionalExpected, array(
1700 array_merge(array(
1701 'location_type_id' => 'Work',
1702 'is_primary' => 1,
1703 ), $locationParams1),
1704 array_merge(array(
1705 'location_type_id' => 'Main',
1706 'is_primary' => 0,
1707 ), $locationParams1),
1708 )),
1709 'expected_hook' => array_merge($additionalExpected, array(
1710 array_merge(array(
1711 'location_type_id' => 'Main',
1712 'is_primary' => 1,
1713 ), $locationParams1),
1714 )),
1715 ),
1716 ),
1717 );
1718 return $data;
1719 }
1720
1721 /**
1722 * Test processing membership for deceased contacts.
1723 */
1724 public function testProcessMembership() {
1725 $this->callAPISuccess('Job', 'process_membership', []);
1726 $deadManWalkingID = $this->individualCreate();
1727 $membershipID = $this->contactMembershipCreate(array('contact_id' => $deadManWalkingID));
1728 $this->callAPISuccess('Contact', 'create', ['id' => $deadManWalkingID, 'is_deceased' => 1]);
1729 $this->callAPISuccess('Job', 'process_membership', []);
1730 $membership = $this->callAPISuccessGetSingle('Membership', ['id' => $membershipID]);
1731 $deceasedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Deceased');
1732 $this->assertEquals($deceasedStatusId, $membership['status_id']);
1733 }
1734
1735 /**
1736 * Test we get an error is deceased status is disabled.
1737 */
1738 public function testProcessMembershipNoDeceasedStatus() {
1739 $deceasedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Deceased');
1740 $this->callAPISuccess('MembershipStatus', 'create', ['is_active' => 0, 'id' => $deceasedStatusId]);
1741 CRM_Core_PseudoConstant::flush();
1742
1743 $deadManWalkingID = $this->individualCreate();
1744 $this->contactMembershipCreate(array('contact_id' => $deadManWalkingID));
1745 $this->callAPISuccess('Contact', 'create', ['id' => $deadManWalkingID, 'is_deceased' => 1]);
1746 $this->callAPIFailure('Job', 'process_membership', []);
1747
1748 $this->callAPISuccess('MembershipStatus', 'create', ['is_active' => 1, 'id' => $deceasedStatusId]);
1749 }
1750
1751 }