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