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