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