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