a8e4c1d280e8ecd710439c3caec980ce4cf1c25d
[civicrm-core.git] / tests / phpunit / CRM / Case / BAO / CaseTest.php
1 <?php
2
3 /**
4 * Class CRM_Case_BAO_CaseTest
5 * @group headless
6 */
7 class CRM_Case_BAO_CaseTest extends CiviUnitTestCase {
8
9 public function setUp(): void {
10 parent::setUp();
11
12 $this->tablesToTruncate = [
13 'civicrm_activity',
14 'civicrm_contact',
15 'civicrm_custom_group',
16 'civicrm_custom_field',
17 'civicrm_case',
18 'civicrm_case_contact',
19 'civicrm_case_activity',
20 'civicrm_case_type',
21 'civicrm_activity_contact',
22 'civicrm_managed',
23 'civicrm_relationship',
24 'civicrm_relationship_type',
25 ];
26
27 $this->quickCleanup($this->tablesToTruncate);
28
29 $this->loadAllFixtures();
30
31 // I don't understand why need to disable but if don't then only one
32 // case type is defined on 2nd and subsequent dataprovider runs.
33 CRM_Core_BAO_ConfigSetting::disableComponent('CiviCase');
34 CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
35 }
36
37 /**
38 * Make sure that the latest case activity works accurately.
39 */
40 public function testCaseActivity() {
41 $userID = $this->createLoggedInUser();
42
43 $addTimeline = civicrm_api3('Case', 'addtimeline', [
44 'case_id' => 1,
45 'timeline' => "standard_timeline",
46 ]);
47
48 $query = CRM_Case_BAO_Case::getCaseActivityQuery('recent', $userID, ' civicrm_case.id IN( 1 )');
49 $res = CRM_Core_DAO::executeQuery($query);
50 $openCaseType = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Open Case');
51 while ($res->fetch()) {
52 $message = 'Failed asserting that the case activity query has a activity_type_id property:';
53 $this->assertObjectHasAttribute('activity_type_id', $res, $message . PHP_EOL . print_r($res, TRUE));
54 $message = 'Failed asserting that the latest activity from Case ID 1 was "Open Case":';
55 $this->assertEquals($openCaseType, $res->activity_type_id, $message . PHP_EOL . print_r($res, TRUE));
56 }
57 }
58
59 protected function tearDown(): void {
60 parent::tearDown();
61 $this->quickCleanup($this->tablesToTruncate, TRUE);
62 }
63
64 public function testAddCaseToContact() {
65 $params = [
66 'case_id' => 1,
67 'contact_id' => 17,
68 ];
69 CRM_Case_BAO_CaseContact::create($params);
70
71 $recent = CRM_Utils_Recent::get();
72 $this->assertEquals('Test Contact - Housing Support', $recent[0]['title']);
73 }
74
75 /**
76 * Create case role relationship between given contacts for provided case ID.
77 *
78 * @param $contactIdA
79 * @param $contactIdB
80 * @param $caseId
81 * @param bool $isActive
82 */
83 private function createCaseRoleRelationship($contactIdA, $contactIdB, $caseId, $isActive = TRUE) {
84 $relationshipType = $this->relationshipTypeCreate([
85 'contact_type_b' => 'Individual',
86 ]);
87
88 $this->callAPISuccess('Relationship', 'create', [
89 'contact_id_a' => $contactIdA,
90 'contact_id_b' => $contactIdB,
91 'relationship_type_id' => $relationshipType,
92 'case_id' => $caseId,
93 'is_active' => $isActive,
94 ]);
95 }
96
97 /**
98 * Asserts number of cases for given logged in user.
99 *
100 * @param $loggedInUser
101 * @param $caseId
102 * @param $caseCount
103 */
104 private function assertCasesOfUser($loggedInUser, $caseId, $caseCount) {
105 $summary = CRM_Case_BAO_Case::getCasesSummary(FALSE);
106 $upcomingCases = CRM_Case_BAO_Case::getCases(FALSE, [], 'dashboard', TRUE);
107 $caseRoles = CRM_Case_BAO_Case::getCaseRoles($loggedInUser, $caseId);
108
109 $this->assertEquals($caseCount, $upcomingCases, 'Upcoming case count must be ' . $caseCount);
110 if ($caseCount === 0) {
111 // If there really are 0 cases then there won't be any subelements for
112 // status and count, so we get a false error if we use the assertEquals
113 // check since it tries to get a subelement on type int. In this case
114 // the summary rows are just the case type pseudoconstant list.
115 $this->assertSame(array_flip(CRM_Case_PseudoConstant::caseType()), $summary['rows']);
116 }
117 else {
118 $this->assertEquals($caseCount, $summary['rows']['Housing Support']['Ongoing']['count'], 'Housing Support Ongoing case summary must be ' . $caseCount);
119 }
120 $this->assertEquals($caseCount, count($caseRoles), 'Total case roles for logged in users must be ' . $caseCount);
121 }
122
123 /**
124 * core/issue-1623: My Case dashlet doesn't sort by name but contact_id instead
125 *
126 * @throws \CRM_Core_Exception
127 */
128 public function testSortByCaseContact() {
129 // delete any cases if present
130 $this->callAPISuccess('Case', 'get', ['api.Case.delete' => ['id' => '$value.id']]);
131
132 // create three contacts with different name, later used in respective cases
133 $contacts = [
134 $this->individualCreate(['first_name' => 'Antonia', 'last_name' => 'D`souza']),
135 $this->individualCreate(['first_name' => 'Darric', 'last_name' => 'Roy']),
136 $this->individualCreate(['first_name' => 'Adam', 'last_name' => 'Pitt']),
137 ];
138 $loggedInUser = $this->createLoggedInUser();
139 $relationshipType = $this->relationshipTypeCreate([
140 'contact_type_b' => 'Individual',
141 ]);
142
143 // create cases for each contact
144 $cases = [];
145 foreach ($contacts as $contactID) {
146 $cases[] = $caseID = $this->createCase($contactID)->id;
147 $this->callAPISuccess('Relationship', 'create', [
148 'contact_id_a' => $contactID,
149 'contact_id_b' => $loggedInUser,
150 'relationship_type_id' => $relationshipType,
151 'case_id' => $caseID,
152 'is_active' => TRUE,
153 ]);
154 }
155
156 // USECASE A: fetch all cases using the AJAX fn without any sorting criteria, and match the result
157 global $_GET;
158 $_GET = [
159 'start' => 0,
160 'length' => 10,
161 'type' => 'any',
162 'all' => 1,
163 'is_unittest' => 1,
164 ];
165
166 $cases = [];
167 try {
168 CRM_Case_Page_AJAX::getCases();
169 }
170 catch (CRM_Core_Exception_PrematureExitException $e) {
171 $cases = $e->errorData['data'];
172 }
173
174 // list of expected sorted names in order the respective cases were created
175 $unsortedExpectedContactNames = [
176 'D`souza, Antonia',
177 'Roy, Darric',
178 'Pitt, Adam',
179 ];
180 $unsortedActualContactNames = CRM_Utils_Array::collect('sort_name', $cases);
181 foreach ($unsortedExpectedContactNames as $key => $name) {
182 // Something has changed recently that has exposed one of the problems with queries that are not full-groupby-compliant. Temporarily commenting this out until figure out what to do since this exact query doesn't seem to come up anywhere on common screens.
183 //$this->assertContains($name, $unsortedActualContactNames[$key]);
184 }
185
186 // USECASE B: fetch all cases using the AJAX fn based any 'Contact' sorting criteria, and match the result against expected sequence of names
187 $_GET = [
188 'start' => 0,
189 'length' => 10,
190 'type' => 'any',
191 'all' => 1,
192 'is_unittest' => 1,
193 'columns' => [
194 1 => [
195 'data' => 'sort_name',
196 'name' => NULL,
197 'searchable' => TRUE,
198 'orderable' => TRUE,
199 'search' => [
200 'value' => NULL,
201 'regex' => FALSE,
202 ],
203 ],
204 ],
205 'order' => [
206 [
207 'column' => 1,
208 'dir' => 'asc',
209 ],
210 ],
211 ];
212
213 $cases = [];
214 try {
215 CRM_Case_Page_AJAX::getCases();
216 }
217 catch (CRM_Core_Exception_PrematureExitException $e) {
218 $cases = $e->errorData['data'];
219 }
220
221 // list of expected sorted names in ASC order
222 $sortedExpectedContactNames = [
223 'D`souza, Antonia',
224 'Pitt, Adam',
225 'Roy, Darric',
226 ];
227 $sortedActualContactNames = CRM_Utils_Array::collect('sort_name', $cases);
228 foreach ($sortedExpectedContactNames as $key => $name) {
229 $this->assertStringContainsString($name, $sortedActualContactNames[$key]);
230 }
231 }
232
233 /**
234 * Test that Case count is exactly one for logged in user for user's active role.
235 *
236 * @throws \CRM_Core_Exception
237 */
238 public function testActiveCaseRole() {
239 $individual = $this->individualCreate();
240 $caseObj = $this->createCase($individual);
241 $caseId = $caseObj->id;
242 $loggedInUser = $this->createLoggedInUser();
243 $this->createCaseRoleRelationship($individual, $loggedInUser, $caseId);
244 $this->assertCasesOfUser($loggedInUser, $caseId, 1);
245 }
246
247 /**
248 * Test that case count is zero for logged in user for user's inactive role.
249 */
250 public function testInactiveCaseRole() {
251 $individual = $this->individualCreate();
252 $caseObj = $this->createCase($individual);
253 $caseId = $caseObj->id;
254 $loggedInUser = $this->createLoggedInUser();
255 $this->createCaseRoleRelationship($individual, $loggedInUser, $caseId, FALSE);
256 $this->assertCasesOfUser($loggedInUser, $caseId, 0);
257 }
258
259 public function testGetCaseType() {
260 $caseTypeLabel = CRM_Case_BAO_Case::getCaseType(1);
261 $this->assertEquals('Housing Support', $caseTypeLabel);
262 }
263
264 public function testRetrieveCaseIdsByContactId() {
265 $caseIds = CRM_Case_BAO_Case::retrieveCaseIdsByContactId(3, FALSE, 'housing_support');
266 $this->assertEquals([1], $caseIds);
267 }
268
269 /**
270 * Test that all custom files are migrated to new case when case is assigned to new client.
271 */
272 public function testCaseReassignForCustomFiles() {
273 $individual = $this->individualCreate();
274 $customGroup = $this->customGroupCreate(array(
275 'extends' => 'Case',
276 ));
277 $customGroup = $customGroup['values'][$customGroup['id']];
278
279 $customFileFieldA = $this->customFieldCreate(array(
280 'custom_group_id' => $customGroup['id'],
281 'html_type' => 'File',
282 'is_active' => 1,
283 'default_value' => 'null',
284 'label' => 'Custom File A',
285 'data_type' => 'File',
286 ));
287
288 $customFileFieldB = $this->customFieldCreate(array(
289 'custom_group_id' => $customGroup['id'],
290 'html_type' => 'File',
291 'is_active' => 1,
292 'default_value' => 'null',
293 'label' => 'Custom File B',
294 'data_type' => 'File',
295 ));
296
297 // Create two files to attach to the new case
298 $filepath = Civi::paths()->getPath('[civicrm.files]/custom');
299
300 CRM_Utils_File::createFakeFile($filepath, 'Bananas do not bend themselves without a little help.', 'i_bend_bananas.txt');
301 $fileA = $this->callAPISuccess('File', 'create', ['uri' => "$filepath/i_bend_bananas.txt"]);
302
303 CRM_Utils_File::createFakeFile($filepath, 'Wombats will bite your ankles if you run from them.', 'wombats_bite_your_ankles.txt');
304 $fileB = $this->callAPISuccess('File', 'create', ['uri' => "$filepath/wombats_bite_your_ankles.txt"]);
305
306 $caseObj = $this->createCase($individual);
307
308 $this->callAPISuccess('Case', 'create', array(
309 'id' => $caseObj->id,
310 'custom_' . $customFileFieldA['id'] => $fileA['id'],
311 'custom_' . $customFileFieldB['id'] => $fileB['id'],
312 ));
313
314 $reassignIndividual = $this->individualCreate();
315 $this->createLoggedInUser();
316 $newCase = CRM_Case_BAO_Case::mergeCases($reassignIndividual, $caseObj->id, $individual, NULL, TRUE);
317
318 $entityFiles = new CRM_Core_DAO_EntityFile();
319 $entityFiles->entity_id = $newCase[0];
320 $entityFiles->entity_table = $customGroup['table_name'];
321 $entityFiles->find();
322
323 $totalEntityFiles = 0;
324 while ($entityFiles->fetch()) {
325 $totalEntityFiles++;
326 }
327
328 $this->assertEquals(2, $totalEntityFiles, 'Two files should be attached with new case.');
329 }
330
331 /**
332 * FIXME: need to create an activity to run this test
333 * function testGetCases() {
334 * $cases = CRM_Case_BAO_Case::getCases(TRUE, 3);
335 * $this->assertEquals('Housing Support', $cases[1]['case_type']);
336 * $this->assertEquals(1, $cases[1]['case_type_id']);
337 * }
338 */
339 public function testGetCasesSummary() {
340 $cases = CRM_Case_BAO_Case::getCasesSummary();
341 $this->assertEquals(1, $cases['rows']['Housing Support']['Ongoing']['count']);
342 }
343
344 /**
345 * Test that getRelatedCases() returns the other case when you create a
346 * Link Cases activity on one of the cases.
347 */
348 public function testGetRelatedCases() {
349 $loggedInUser = $this->createLoggedInUser();
350 // create some cases
351 $client_id_1 = $this->individualCreate([], 0);
352 $caseObj_1 = $this->createCase($client_id_1, $loggedInUser);
353 $case_id_1 = $caseObj_1->id;
354 $client_id_2 = $this->individualCreate([], 1);
355 $caseObj_2 = $this->createCase($client_id_2, $loggedInUser);
356 $case_id_2 = $caseObj_2->id;
357
358 // Create link case activity. We could go thru the whole form processes
359 // but we really just want to test the BAO function so just need the
360 // activity to exist.
361 $result = $this->callAPISuccess('activity', 'create', [
362 'activity_type_id' => 'Link Cases',
363 'subject' => 'Test Link Cases',
364 'status_id' => 'Completed',
365 'source_contact_id' => $loggedInUser,
366 'target_contact_id' => $client_id_1,
367 'case_id' => $case_id_1,
368 ]);
369
370 // Put it in the format needed for endPostProcess
371 $activity = new StdClass();
372 $activity->id = $result['id'];
373 $params = [
374 'link_to_case_id' => $case_id_2,
375 ];
376 CRM_Case_Form_Activity_LinkCases::endPostProcess(NULL, $params, $activity);
377
378 // Get related cases for case 1
379 $cases = CRM_Case_BAO_Case::getRelatedCases($case_id_1);
380 // It should have case 2
381 $this->assertEquals($case_id_2, $cases[$case_id_2]['case_id']);
382
383 // Ditto but reverse the cases
384 $cases = CRM_Case_BAO_Case::getRelatedCases($case_id_2);
385 $this->assertEquals($case_id_1, $cases[$case_id_1]['case_id']);
386 }
387
388 /**
389 * Test various things after a case is closed.
390 *
391 * This annotation is not ideal, but without it there is some kind of
392 * messup that happens to quickform that persists between tests, e.g.
393 * it can't add maxfilesize validation rules.
394 * @runInSeparateProcess
395 * @preserveGlobalState disabled
396 */
397 public function testCaseClosure() {
398 $loggedInUser = $this->createLoggedInUser();
399 $client_id = $this->individualCreate();
400 $caseObj = $this->createCase($client_id, $loggedInUser);
401 $case_id = $caseObj->id;
402
403 // Get the case status option value for "Resolved" (name="Closed").
404 $closed_status = $this->callAPISuccess('OptionValue', 'getValue', [
405 'return' => 'value',
406 'option_group_id' => 'case_status',
407 'name' => 'Closed',
408 ]);
409 $this->assertNotEmpty($closed_status);
410
411 // Get the activity status option value for "Completed"
412 $completed_status = $this->callAPISuccess('OptionValue', 'getValue', [
413 'return' => 'value',
414 'option_group_id' => 'activity_status',
415 'name' => 'Completed',
416 ]);
417 $this->assertNotEmpty($completed_status);
418
419 // Get the value for the activity type id we need to create
420 $atype = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Change Case Status');
421
422 // Now it gets weird. There doesn't seem to be a good way to test this, so we simulate a form and the various bits that go with it.
423
424 // HTTP vars needed because that's how the form determines stuff
425 $oldMETHOD = empty($_SERVER['REQUEST_METHOD']) ? NULL : $_SERVER['REQUEST_METHOD'];
426 $oldGET = empty($_GET) ? [] : $_GET;
427 $oldREQUEST = empty($_REQUEST) ? [] : $_REQUEST;
428 $_SERVER['REQUEST_METHOD'] = 'GET';
429 $_GET['caseid'] = $case_id;
430 $_REQUEST['caseid'] = $case_id;
431 $_GET['cid'] = $client_id;
432 $_REQUEST['cid'] = $client_id;
433 $_GET['action'] = 'add';
434 $_REQUEST['action'] = 'add';
435 $_GET['reset'] = 1;
436 $_REQUEST['reset'] = 1;
437 $_GET['atype'] = $atype;
438 $_REQUEST['atype'] = $atype;
439
440 $form = new CRM_Case_Form_Activity();
441 $form->controller = new CRM_Core_Controller_Simple('CRM_Case_Form_Activity', 'Case Activity');
442 $form->_activityTypeId = $atype;
443 $form->_activityTypeName = 'Change Case Status';
444 $form->_activityTypeFile = 'ChangeCaseStatus';
445
446 $form->preProcess();
447 $form->buildQuickForm();
448 $form->setDefaultValues();
449
450 // Now submit the form. Store the date used so we can check it later.
451
452 $t = time();
453 $now_date = date('Y-m-d H:i:s', $t);
454 $now_date_date_only = date('Y-m-d', $t);
455 $actParams = [
456 'is_unittest' => TRUE,
457 'case_status_id' => $closed_status,
458 'activity_date_time' => $now_date,
459 'target_contact_id' => $client_id,
460 'source_contact_id' => $loggedInUser,
461 // yeah this is extra weird, but without it you get the wrong subject
462 'subject' => 'null',
463 ];
464
465 $form->postProcess($actParams);
466
467 // Ok now let's check some things
468
469 $result = $this->callAPISuccess('Case', 'get', [
470 'sequential' => 1,
471 'id' => $case_id,
472 ]);
473 $caseData = array_shift($result['values']);
474
475 $this->assertEquals($caseData['end_date'], $now_date_date_only);
476 $this->assertEquals($caseData['status_id'], $closed_status);
477
478 // now get the latest activity and check some things for it
479
480 $actId = max($caseData['activities']);
481 $this->assertNotEmpty($actId);
482
483 $result = $this->callAPISuccess('Activity', 'get', [
484 'sequential' => 1,
485 'id' => $actId,
486 ]);
487 $activity = array_shift($result['values']);
488
489 $this->assertEquals($activity['subject'], 'Case status changed from Ongoing to Resolved');
490 $this->assertEquals($activity['activity_date_time'], $now_date);
491 $this->assertEquals($activity['status_id'], $completed_status);
492
493 // Now replace old globals
494 if (is_null($oldMETHOD)) {
495 unset($_SERVER['REQUEST_METHOD']);
496 }
497 else {
498 $_SERVER['REQUEST_METHOD'] = $oldMETHOD;
499 }
500 $_GET = $oldGET;
501 $_REQUEST = $oldREQUEST;
502 }
503
504 /**
505 * Test getGlobalContacts
506 */
507 public function testGetGlobalContacts() {
508 //Add contact to case resource.
509 $caseResourceContactID = $this->individualCreate();
510 $this->callAPISuccess('GroupContact', 'create', [
511 'group_id' => "Case_Resources",
512 'contact_id' => $caseResourceContactID,
513 ]);
514
515 //No contact should be returned.
516 CRM_Core_Config::singleton()->userPermissionClass->permissions = [];
517 $groupInfo = [];
518 $groupContacts = CRM_Case_BAO_Case::getGlobalContacts($groupInfo);
519 $this->assertEquals(0, count($groupContacts));
520
521 //Verify if contact is returned correctly.
522 CRM_Core_Config::singleton()->userPermissionClass->permissions = [
523 'access CiviCRM',
524 'view all contacts',
525 ];
526 $groupInfo = [];
527 $groupContacts = CRM_Case_BAO_Case::getGlobalContacts($groupInfo);
528 $this->assertEquals(1, count($groupContacts));
529 $this->assertEquals($caseResourceContactID, key($groupContacts));
530 }
531
532 /**
533 * Test max_instances
534 */
535 public function testMaxInstances() {
536 $loggedInUser = $this->createLoggedInUser();
537 $client_id = $this->individualCreate();
538 $caseObj = $this->createCase($client_id, $loggedInUser);
539 $case_id = $caseObj->id;
540
541 // Sanity check to make sure we'll be testing what we think we're testing.
542 $this->assertEquals($caseObj->case_type_id, 1);
543
544 // Get the case type
545 $result = $this->callAPISuccess('CaseType', 'get', [
546 'sequential' => 1,
547 'id' => 1,
548 ]);
549 $caseType = array_shift($result['values']);
550 $activityTypeName = $caseType['definition']['activityTypes'][1]['name'];
551 // Sanity check to make sure we'll be testing what we think we're testing.
552 $this->assertEquals($activityTypeName, "Medical evaluation");
553
554 // Look up the activity type label - we need it later
555 $result = $this->callAPISuccess('OptionValue', 'get', [
556 'sequential' => 1,
557 'option_group_id' => 'activity_type',
558 'name' => $activityTypeName,
559 ]);
560 $optionValue = array_shift($result['values']);
561 $activityTypeLabel = $optionValue['label'];
562 $this->assertNotEmpty($activityTypeLabel);
563
564 // Locate the existing activity independently so we can check it
565 $result = $this->callAPISuccess('Activity', 'get', [
566 'sequential' => 1,
567 // this sometimes confuses me - pass in the name for the id
568 'activity_type_id' => $activityTypeName,
569 ]);
570 // There should be only one in the database at this point so this should be the id.
571 $activity_id = $result['id'];
572 $this->assertNotEmpty($activity_id);
573 $this->assertGreaterThan(0, $activity_id);
574 $activityArr = array_shift($result['values']);
575
576 // At the moment everything should be happy, although there's nothing to test because if max_instances has no value then nothing gets called, which is correct since it means unlimited. But we don't have a way to test that right now. For fun we could test max_instances=0 but that isn't the same as "not set". 0 would actually mean 0 are allowed, which is pointless, since then why would you even add the activity type to the config.
577
578 // Update max instances for the activity type
579 // We're not really checking that the tested code has retrieved the new case type definition, just that given some numbers as input it returns the right thing as output, so these lines are mostly symbolic at the moment.
580 $caseType['definition']['activityTypes'][1]['max_instances'] = 1;
581 $this->callAPISuccess('CaseType', 'create', $caseType);
582
583 // Now we should get a link back
584 $editUrl = CRM_Case_Form_Activity::checkMaxInstances(
585 $case_id,
586 $activityArr['activity_type_id'],
587 // max instances
588 1,
589 $loggedInUser,
590 $client_id,
591 // existing activity count
592 1
593 );
594 $this->assertNotNull($editUrl);
595
596 $expectedUrl = CRM_Utils_System::url(
597 'civicrm/case/activity',
598 "reset=1&cid={$client_id}&caseid={$case_id}&action=update&id={$activity_id}"
599 );
600 $this->assertEquals($editUrl, $expectedUrl);
601
602 // And also a bounce message is expected
603 $bounceMessage = CRM_Case_Form_Activity::getMaxInstancesBounceMessage(
604 $editUrl,
605 $activityTypeLabel,
606 // max instances,
607 1,
608 // existing activity count
609 1
610 );
611 $this->assertNotEmpty($bounceMessage);
612
613 // Now check with max_instances = 2
614 $caseType['definition']['activityTypes'][1]['max_instances'] = 2;
615 $this->callAPISuccess('CaseType', 'create', $caseType);
616
617 // So it should now be back to being happy
618 $editUrl = CRM_Case_Form_Activity::checkMaxInstances(
619 $case_id,
620 $activityArr['activity_type_id'],
621 // max instances
622 2,
623 $loggedInUser,
624 $client_id,
625 // existing activity count
626 1
627 );
628 $this->assertNull($editUrl);
629 $bounceMessage = CRM_Case_Form_Activity::getMaxInstancesBounceMessage(
630 $editUrl,
631 $activityTypeLabel,
632 // max instances,
633 2,
634 // existing activity count
635 1
636 );
637 $this->assertEmpty($bounceMessage);
638
639 // Add new activity check again
640 $newActivity = [
641 'case_id' => $case_id,
642 'activity_type_id' => $activityArr['activity_type_id'],
643 'status_id' => $activityArr['status_id'],
644 'subject' => "A different subject",
645 'activity_date_time' => date('Y-m-d H:i:s'),
646 'source_contact_id' => $loggedInUser,
647 'target_id' => $client_id,
648 ];
649 $this->callAPISuccess('Activity', 'create', $newActivity);
650
651 $editUrl = CRM_Case_Form_Activity::checkMaxInstances(
652 $case_id,
653 $activityArr['activity_type_id'],
654 // max instances
655 2,
656 $loggedInUser,
657 $client_id,
658 // existing activity count
659 2
660 );
661 // There should be no url here.
662 $this->assertNull($editUrl);
663
664 // But there should be a warning message still.
665 $bounceMessage = CRM_Case_Form_Activity::getMaxInstancesBounceMessage(
666 $editUrl,
667 $activityTypeLabel,
668 // max instances,
669 2,
670 // existing activity count
671 2
672 );
673 $this->assertNotEmpty($bounceMessage);
674 }
675
676 /**
677 * Test changing the label for the case manager role and then creating
678 * a case.
679 * At the time this test was written this test would fail, demonstrating
680 * one problem with name vs label.
681 */
682 public function testCreateCaseWithChangedManagerLabel() {
683 // We could just assume the relationship that gets created has
684 // relationship_type_id = 1, but let's create a case, see what the
685 // id is, then do our actual test.
686 $loggedInUser = $this->createLoggedInUser();
687 $client_id = $this->individualCreate();
688 $caseObj = $this->createCase($client_id, $loggedInUser);
689 $case_id = $caseObj->id;
690
691 // Going to assume the stock case type has what it currently has at the
692 // time of writing, which is the autocreated case manager relationship for
693 // the logged in user.
694 $getParams = [
695 'contact_id_b' => $loggedInUser,
696 'case_id' => $case_id,
697 ];
698 $result = $this->callAPISuccess('Relationship', 'get', $getParams);
699 // as noted above assume this is the only one
700 $relationship_type_id = $result['values'][$result['id']]['relationship_type_id'];
701
702 // Save the old labels first so we can put back at end of test.
703 $oldParams = [
704 'id' => $relationship_type_id,
705 ];
706 $oldValues = $this->callAPISuccess('RelationshipType', 'get', $oldParams);
707 // Now change the label of the relationship type.
708 $changeParams = [
709 'id' => $relationship_type_id,
710 'label_a_b' => 'Best ' . $oldValues['values'][$relationship_type_id]['label_a_b'],
711 'label_b_a' => 'Best ' . $oldValues['values'][$relationship_type_id]['label_b_a'],
712 ];
713 $this->callAPISuccess('RelationshipType', 'create', $changeParams);
714
715 // Now try creating another case.
716 $caseObj2 = $this->createCase($client_id, $loggedInUser);
717 $case_id2 = $caseObj2->id;
718
719 $checkParams = [
720 'contact_id_b' => $loggedInUser,
721 'case_id' => $case_id2,
722 ];
723 $result = $this->callAPISuccess('Relationship', 'get', $checkParams);
724 // Main thing is the above createCase call doesn't fail, but let's check
725 // the relationship type id is what we expect too while we're here.
726 // See note above about assuming this is the only relationship autocreated.
727 $this->assertEquals($relationship_type_id, $result['values'][$result['id']]['relationship_type_id']);
728
729 // Now put relationship type back to the way it was.
730 $changeParams = [
731 'id' => $relationship_type_id,
732 'label_a_b' => $oldValues['values'][$relationship_type_id]['label_a_b'],
733 'label_b_a' => $oldValues['values'][$relationship_type_id]['label_b_a'],
734 ];
735 $this->callAPISuccess('RelationshipType', 'create', $changeParams);
736 }
737
738 /**
739 * Test change case status with linked cases choosing the option to
740 * update the linked cases.
741 */
742 public function testChangeCaseStatusLinkedCases() {
743 $loggedInUser = $this->createLoggedInUser();
744 $clientId1 = $this->individualCreate();
745 $clientId2 = $this->individualCreate();
746 $case1 = $this->createCase($clientId1, $loggedInUser);
747 $case2 = $this->createCase($clientId2, $loggedInUser);
748 $linkActivity = $this->callAPISuccess('Activity', 'create', [
749 'case_id' => $case1->id,
750 'source_contact_id' => $loggedInUser,
751 'target_contact' => $clientId1,
752 'activity_type_id' => 'Link Cases',
753 'subject' => 'Test Link Cases',
754 'status_id' => 'Completed',
755 ]);
756
757 // Put it in the format needed for endPostProcess
758 $activity = new StdClass();
759 $activity->id = $linkActivity['id'];
760 $params = ['link_to_case_id' => $case2->id];
761 CRM_Case_Form_Activity_LinkCases::endPostProcess(NULL, $params, $activity);
762
763 // Get the option_value.value for case status Closed
764 $closedStatusResult = $this->callAPISuccess('OptionValue', 'get', [
765 'option_group_id' => 'case_status',
766 'name' => 'Closed',
767 'return' => ['value'],
768 ]);
769 $closedStatus = $closedStatusResult['values'][$closedStatusResult['id']]['value'];
770
771 // Go thru the motions to change case status
772 $form = new CRM_Case_Form_Activity_ChangeCaseStatus();
773 $form->_caseId = [$case1->id];
774 $form->_oldCaseStatus = [$case1->status_id];
775 $params = [
776 'id' => $case1->id,
777 'case_status_id' => $closedStatus,
778 'updateLinkedCases' => '1',
779 ];
780
781 CRM_Case_Form_Activity_ChangeCaseStatus::beginPostProcess($form, $params);
782 // Check that the second case is now also in the form member.
783 $this->assertEquals([$case1->id, $case2->id], $form->_caseId);
784
785 // We need to pass in an actual activity later
786 $result = $this->callAPISuccess('Activity', 'create', [
787 'case_id' => $case1->id,
788 'source_contact_id' => $loggedInUser,
789 'target_contact' => $clientId1,
790 'activity_type_id' => 'Change Case Status',
791 'subject' => 'Status changed',
792 'status_id' => 'Completed',
793 ]);
794 $changeStatusActivity = new CRM_Activity_DAO_Activity();
795 $changeStatusActivity->id = $result['id'];
796 $changeStatusActivity->find(TRUE);
797
798 $params = [
799 'case_id' => $case1->id,
800 'target_contact_id' => [$clientId1],
801 'case_status_id' => $closedStatus,
802 'activity_date_time' => $changeStatusActivity->activity_date_time,
803 ];
804
805 CRM_Case_Form_Activity_ChangeCaseStatus::endPostProcess($form, $params, $changeStatusActivity);
806
807 // @todo Check other case got closed.
808 /*
809 * We can't do this here because it doesn't happen until the parent
810 * activity does its thing.
811 $linkedCase = $this->callAPISuccess('Case', 'get', ['id' => $case2->id]);
812 $this->assertEquals($closedStatus, $linkedCase['values'][$linkedCase['id']]['status_id']);
813 $this->assertEquals(date('Y-m-d', strtotime($changeStatusActivity->activity_date_time)), $linkedCase['values'][$linkedCase['id']]['end_date']);
814 */
815 }
816
817 /**
818 * test getCaseActivityQuery
819 * @dataProvider caseActivityQueryProvider
820 * @param array $input
821 * @param array $expected
822 */
823 public function testGetCaseActivityQuery(array $input, array $expected): void {
824 $activity_type_map = array_flip(CRM_Activity_BAO_Activity::buildOptions('activity_type_id', 'validate'));
825
826 $loggedInUser = $this->createLoggedInUser();
827 $individual[1] = $this->individualCreate();
828 $caseObj[1] = $this->createCase($individual[1], $loggedInUser, [
829 // Unfortunately the query we're testing is not full-group-by compliant
830 // and does not have a well-defined sort order either. If we use the
831 // default casetype then there are two activities with the same date
832 // and sometimes you get one returned and sometimes the other. If the
833 // second case type timeline is altered in future it's possible the
834 // same problem could intermittently occur.
835 'case_type_id' => 2,
836 'case_type' => 'adult_day_care_referral',
837 ]);
838 $individual[2] = $this->individualCreate([], 1);
839
840 // create a second case with a different start date
841 $other_date = strtotime($input['other_date']);
842 $caseObj[2] = $this->createCase($individual[2], $loggedInUser, [
843 'case_type_id' => 2,
844 'case_type' => 'adult_day_care_referral',
845 'start_date' => date('Y-m-d', $other_date),
846 'start_date_time' => date('YmdHis', $other_date),
847 ]);
848
849 foreach (['upcoming', 'recent'] as $type) {
850 // See note above about the query being ill-defined, so this only works
851 // on mysql 5.7 and 8, similar to other tests in this file that call
852 // getCases which also uses this query.
853 $sql = CRM_Case_BAO_Case::getCaseActivityQuery($type, $loggedInUser);
854 $dao = CRM_Core_DAO::executeQuery($sql);
855 $activities = [];
856 $counter = 0;
857 while ($dao->fetch()) {
858 $activities[$counter] = [
859 'case_id' => $dao->case_id,
860 'case_subject' => $dao->case_subject,
861 'contact_id' => (int) $dao->contact_id,
862 'phone' => $dao->phone,
863 'contact_type' => $dao->contact_type,
864 'activity_type_id' => (int) $dao->activity_type_id,
865 'case_type_id' => (int) $dao->case_type_id,
866 'case_status_id' => (int) $dao->case_status_id,
867 // This is activity status
868 'status_id' => (int) $dao->status_id,
869 'case_start_date' => $dao->case_start_date,
870 'case_role' => $dao->case_role,
871 'activity_date_time' => $dao->activity_date_time,
872 ];
873
874 // Need to replace some placeholders since we don't know what they
875 // are at the time the dataprovider is evaluated.
876 $offset = $expected[$type][$counter]['which_case_offset'];
877 unset($expected[$type][$counter]['which_case_offset']);
878 $expected[$type][$counter]['case_id'] = $caseObj[$offset]->id;
879 $expected[$type][$counter]['contact_id'] = $individual[$offset];
880 $expected[$type][$counter]['activity_type_id'] = $activity_type_map[$expected[$type][$counter]['activity_type_id']];
881 $expected[$type][$counter]['case_start_date'] = $caseObj[$offset]->start_date;
882 // To avoid a millisecond rollover bug, where e.g. the dataprovider
883 // runs a whole second before this test, we make this relative to the
884 // case start date, which it is anyway in the timeline.
885 $expected[$type][$counter]['activity_date_time'] = date('Y-m-d H:i:s', strtotime($caseObj[$offset]->start_date . $expected[$type][$counter]['activity_date_time']));
886
887 $counter++;
888 }
889 $this->assertEquals($expected[$type], $activities);
890 }
891 }
892
893 /**
894 * dataprovider for testGetCaseActivityQuery
895 * @return array
896 */
897 public function caseActivityQueryProvider(): array {
898 return [
899 0 => [
900 'input' => [
901 'other_date' => '-1 day',
902 ],
903 'expected' => [
904 'upcoming' => [
905 0 => [
906 'which_case_offset' => 2,
907 'case_id' => 'REPLACE_ME',
908 'case_subject' => 'Case Subject',
909 'contact_id' => 'REPLACE_ME',
910 'phone' => NULL,
911 'contact_type' => 'Individual',
912 'activity_type_id' => 'Medical evaluation',
913 'case_type_id' => 2,
914 'case_status_id' => 1,
915 'status_id' => 1,
916 'case_start_date' => 'REPLACE_ME',
917 'case_role' => 'Senior Services Coordinator is',
918 'activity_date_time' => ' +3 day',
919 ],
920 1 => [
921 'which_case_offset' => 1,
922 // REPLACE_ME's will get replaced in the test since the values haven't been created yet at the time dataproviders get evaluated.
923 'case_id' => 'REPLACE_ME',
924 'case_subject' => 'Case Subject',
925 'contact_id' => 'REPLACE_ME',
926 'phone' => NULL,
927 'contact_type' => 'Individual',
928 'activity_type_id' => 'Medical evaluation',
929 'case_type_id' => 2,
930 'case_status_id' => 1,
931 'status_id' => 1,
932 'case_start_date' => 'REPLACE_ME',
933 'case_role' => 'Senior Services Coordinator is',
934 'activity_date_time' => ' +3 day',
935 ],
936 ],
937 'recent' => [
938 0 => [
939 'which_case_offset' => 2,
940 'case_id' => 'REPLACE_ME',
941 'case_subject' => 'Case Subject',
942 'contact_id' => 'REPLACE_ME',
943 'phone' => NULL,
944 'contact_type' => 'Individual',
945 'activity_type_id' => 'Open Case',
946 'case_type_id' => 2,
947 'case_status_id' => 1,
948 'status_id' => 2,
949 'case_start_date' => 'REPLACE_ME',
950 'case_role' => 'Senior Services Coordinator is',
951 // means no offset from case start date
952 'activity_date_time' => '',
953 ],
954 1 => [
955 'which_case_offset' => 1,
956 'case_id' => 'REPLACE_ME',
957 'case_subject' => 'Case Subject',
958 'contact_id' => 'REPLACE_ME',
959 'phone' => NULL,
960 'contact_type' => 'Individual',
961 'activity_type_id' => 'Open Case',
962 'case_type_id' => 2,
963 'case_status_id' => 1,
964 'status_id' => 2,
965 'case_start_date' => 'REPLACE_ME',
966 'case_role' => 'Senior Services Coordinator is',
967 // means no offset from case start date
968 'activity_date_time' => '',
969 ],
970 ],
971 ],
972 ],
973
974 1 => [
975 'input' => [
976 'other_date' => '-7 day',
977 ],
978 'expected' => [
979 'upcoming' => [
980 0 => [
981 'which_case_offset' => 2,
982 'case_id' => 'REPLACE_ME',
983 'case_subject' => 'Case Subject',
984 'contact_id' => 'REPLACE_ME',
985 'phone' => NULL,
986 'contact_type' => 'Individual',
987 'activity_type_id' => 'Medical evaluation',
988 'case_type_id' => 2,
989 'case_status_id' => 1,
990 'status_id' => 1,
991 'case_start_date' => 'REPLACE_ME',
992 'case_role' => 'Senior Services Coordinator is',
993 'activity_date_time' => ' +3 day',
994 ],
995 1 => [
996 'which_case_offset' => 1,
997 'case_id' => 'REPLACE_ME',
998 'case_subject' => 'Case Subject',
999 'contact_id' => 'REPLACE_ME',
1000 'phone' => NULL,
1001 'contact_type' => 'Individual',
1002 'activity_type_id' => 'Medical evaluation',
1003 'case_type_id' => 2,
1004 'case_status_id' => 1,
1005 'status_id' => 1,
1006 'case_start_date' => 'REPLACE_ME',
1007 'case_role' => 'Senior Services Coordinator is',
1008 'activity_date_time' => ' +3 day',
1009 ],
1010 ],
1011 'recent' => [
1012 0 => [
1013 'which_case_offset' => 2,
1014 'case_id' => 'REPLACE_ME',
1015 'case_subject' => 'Case Subject',
1016 'contact_id' => 'REPLACE_ME',
1017 'phone' => NULL,
1018 'contact_type' => 'Individual',
1019 'activity_type_id' => 'Open Case',
1020 'case_type_id' => 2,
1021 'case_status_id' => 1,
1022 'status_id' => 2,
1023 'case_start_date' => 'REPLACE_ME',
1024 'case_role' => 'Senior Services Coordinator is',
1025 // means no offset from case start date
1026 'activity_date_time' => '',
1027 ],
1028 1 => [
1029 'which_case_offset' => 1,
1030 'case_id' => 'REPLACE_ME',
1031 'case_subject' => 'Case Subject',
1032 'contact_id' => 'REPLACE_ME',
1033 'phone' => NULL,
1034 'contact_type' => 'Individual',
1035 'activity_type_id' => 'Open Case',
1036 'case_type_id' => 2,
1037 'case_status_id' => 1,
1038 'status_id' => 2,
1039 'case_start_date' => 'REPLACE_ME',
1040 'case_role' => 'Senior Services Coordinator is',
1041 // means no offset from case start date
1042 'activity_date_time' => '',
1043 ],
1044 ],
1045 ],
1046 ],
1047
1048 2 => [
1049 'input' => [
1050 'other_date' => '-14 day',
1051 ],
1052 'expected' => [
1053 'upcoming' => [
1054 0 => [
1055 'which_case_offset' => 2,
1056 'case_id' => 'REPLACE_ME',
1057 'case_subject' => 'Case Subject',
1058 'contact_id' => 'REPLACE_ME',
1059 'phone' => NULL,
1060 'contact_type' => 'Individual',
1061 'activity_type_id' => 'Medical evaluation',
1062 'case_type_id' => 2,
1063 'case_status_id' => 1,
1064 'status_id' => 1,
1065 'case_start_date' => 'REPLACE_ME',
1066 'case_role' => 'Senior Services Coordinator is',
1067 'activity_date_time' => ' +3 day',
1068 ],
1069 1 => [
1070 'which_case_offset' => 1,
1071 'case_id' => 'REPLACE_ME',
1072 'case_subject' => 'Case Subject',
1073 'contact_id' => 'REPLACE_ME',
1074 'phone' => NULL,
1075 'contact_type' => 'Individual',
1076 'activity_type_id' => 'Medical evaluation',
1077 'case_type_id' => 2,
1078 'case_status_id' => 1,
1079 'status_id' => 1,
1080 'case_start_date' => 'REPLACE_ME',
1081 'case_role' => 'Senior Services Coordinator is',
1082 'activity_date_time' => ' +3 day',
1083 ],
1084 ],
1085 'recent' => [
1086 0 => [
1087 'which_case_offset' => 1,
1088 'case_id' => 'REPLACE_ME',
1089 'case_subject' => 'Case Subject',
1090 'contact_id' => 'REPLACE_ME',
1091 'phone' => NULL,
1092 'contact_type' => 'Individual',
1093 'activity_type_id' => 'Open Case',
1094 'case_type_id' => 2,
1095 'case_status_id' => 1,
1096 'status_id' => 2,
1097 'case_start_date' => 'REPLACE_ME',
1098 'case_role' => 'Senior Services Coordinator is',
1099 // means no offset from case start date
1100 'activity_date_time' => '',
1101 ],
1102 ],
1103 ],
1104 ],
1105
1106 3 => [
1107 'input' => [
1108 'other_date' => '-21 day',
1109 ],
1110 'expected' => [
1111 'upcoming' => [
1112 0 => [
1113 'which_case_offset' => 2,
1114 'case_id' => 'REPLACE_ME',
1115 'case_subject' => 'Case Subject',
1116 'contact_id' => 'REPLACE_ME',
1117 'phone' => NULL,
1118 'contact_type' => 'Individual',
1119 'activity_type_id' => 'Medical evaluation',
1120 'case_type_id' => 2,
1121 'case_status_id' => 1,
1122 'status_id' => 1,
1123 'case_start_date' => 'REPLACE_ME',
1124 'case_role' => 'Senior Services Coordinator is',
1125 'activity_date_time' => ' +3 day',
1126 ],
1127 1 => [
1128 'which_case_offset' => 1,
1129 'case_id' => 'REPLACE_ME',
1130 'case_subject' => 'Case Subject',
1131 'contact_id' => 'REPLACE_ME',
1132 'phone' => NULL,
1133 'contact_type' => 'Individual',
1134 'activity_type_id' => 'Medical evaluation',
1135 'case_type_id' => 2,
1136 'case_status_id' => 1,
1137 'status_id' => 1,
1138 'case_start_date' => 'REPLACE_ME',
1139 'case_role' => 'Senior Services Coordinator is',
1140 'activity_date_time' => ' +3 day',
1141 ],
1142 ],
1143 'recent' => [
1144 0 => [
1145 'which_case_offset' => 1,
1146 'case_id' => 'REPLACE_ME',
1147 'case_subject' => 'Case Subject',
1148 'contact_id' => 'REPLACE_ME',
1149 'phone' => NULL,
1150 'contact_type' => 'Individual',
1151 'activity_type_id' => 'Open Case',
1152 'case_type_id' => 2,
1153 'case_status_id' => 1,
1154 'status_id' => 2,
1155 'case_start_date' => 'REPLACE_ME',
1156 'case_role' => 'Senior Services Coordinator is',
1157 // means no offset from case start date
1158 'activity_date_time' => '',
1159 ],
1160 ],
1161 ],
1162 ],
1163 ];
1164 }
1165
1166 /**
1167 * Test that if you only have "my cases" permission you can still view
1168 * Manage Case for **closed** cases of yours.
1169 */
1170 public function testCanViewClosedCaseAsNonAdmin() {
1171 $loggedInUser = $this->createLoggedInUser();
1172 CRM_Core_Config::singleton()->userPermissionClass->permissions = [
1173 'access CiviCRM',
1174 'view all contacts',
1175 'edit all contacts',
1176 'add cases',
1177 // this is one important part we're testing
1178 'access my cases and activities',
1179 ];
1180 $individual = $this->individualCreate();
1181 $caseObj = $this->createCase($individual, $loggedInUser);
1182 $caseId = $caseObj->id;
1183
1184 // This isn't everything needed to close a case but is good enough for
1185 // our purposes.
1186 $this->callAPISuccess('Case', 'create', [
1187 'id' => $caseId,
1188 'status_id' => 'Closed',
1189 ]);
1190
1191 // Manage Case goes thru this tab even when not visiting from the tab.
1192 $tab = new CRM_Case_Page_Tab();
1193 $tab->set('action', 'view');
1194 $tab->set('cid', $individual);
1195 $tab->set('id', $caseId);
1196 $tab->set('context', 'standalone');
1197 $tab->preProcess();
1198 // At this point it would have thrown PrematureExitException if we didn't have access.
1199 // Let's assert something while we're here. This is also what would have
1200 // failed, but by itself doesn't depend on permissions.
1201 $this->assertArrayHasKey($caseId, CRM_Case_BAO_Case::getCases(FALSE, ['type' => 'any']));
1202 }
1203
1204 /**
1205 * Test a high number of assigned case roles.
1206 */
1207 public function testGoingTo11() {
1208 $loggedInUser = $this->createLoggedInUser();
1209 $individual = $this->individualCreate();
1210 $caseObj = $this->createCase($individual, $loggedInUser);
1211 $caseId = $caseObj->id;
1212
1213 // Create lots of assigned roles
1214 for ($i = 1; $i <= 30; $i++) {
1215 // create a new type
1216 $relationship_type_id = $this->callAPISuccess('RelationshipType', 'create', [
1217 'name_a_b' => "has as Wizard level $i",
1218 'name_b_a' => "is Wizard level $i for",
1219 ])['id'];
1220
1221 // Now make a new person and give them the role
1222 $contact_id = $this->individualCreate([], 0, TRUE);
1223 $this->callAPISuccess('Relationship', 'create', [
1224 'case_id' => $caseId,
1225 'contact_id_a' => $individual,
1226 'contact_id_b' => $contact_id,
1227 'relationship_type_id' => $relationship_type_id,
1228 ]);
1229 }
1230
1231 // Note the stock case type adds a manager role for the logged in user so it's 31 not 30.
1232 $this->assertCount(31, CRM_Case_BAO_Case::getCaseRoles($individual, $caseId), 'Why not just make ten louder?');
1233 }
1234
1235 }