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