diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php index b65c1b0a06..b8cff1b634 100644 --- a/ProcessMaker/Models/ProcessRequestToken.php +++ b/ProcessMaker/Models/ProcessRequestToken.php @@ -801,7 +801,11 @@ public function valueAliasStatus($value, $expression, $callback = null, User $us } elseif ($user) { $query->whereIn('process_request_tokens.id', $user->availableSelfServiceTasksQuery()); } else { - $query->where('process_request_tokens.is_self_service', 1); + $query->where([ + 'process_request_tokens.is_self_service' => 1, + 'process_request_tokens.status' => 'ACTIVE', + 'process_request_tokens.user_id' => null, + ]); } } elseif (array_key_exists($value, $statusMap)) { $query->where('process_request_tokens.is_self_service', 0); @@ -1633,24 +1637,13 @@ public static function scopeWhereUserAssigned($query, $userId) $userColumn = 'user_id'; $query->where(function ($query) use ($userColumn, $userId) { $query->where($userColumn, $userId); - $query->orWhere(function ($query) use ($userColumn, $userId) { - $query->whereNull($userColumn); - $query->where('process_request_tokens.is_self_service', 1); - $user = User::find($userId); - $query->where(function ($query) use ($user) { - foreach ($user->groups as $group) { - $query->orWhereJsonContains( - 'process_request_tokens.self_service_groups', strval($group->getKey()) - ); // backwards compatibility - $query->orWhereJsonContains( - 'process_request_tokens.self_service_groups->groups', strval($group->getKey()) - ); - } - $query->orWhereJsonContains( - 'process_request_tokens.self_service_groups->users', strval($user->getKey()) - ); - }); - }); + $user = User::find($userId); + if ($user) { + $query->orWhereIn( + 'process_request_tokens.id', + $user->availableSelfServiceTasksQuery() + ); + } }); return $query; diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index 2ad7b59031..237d4a42a3 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -99,24 +99,13 @@ private function applyUserIdFilter($query, $column, $filterByFields, $fieldFilte $query->where(function ($query) use ($key, $column, $fieldFilter) { $userColumn = is_string($key) ? $key : $column; $query->where($userColumn, $fieldFilter); - $query->orWhere(function ($query) use ($userColumn, $fieldFilter) { - $query->whereNull($userColumn); - $query->where('process_request_tokens.is_self_service', 1); - $user = User::find($fieldFilter); - $query->where(function ($query) use ($user) { - foreach ($user->groups as $group) { - $query->orWhereJsonContains( - 'process_request_tokens.self_service_groups', strval($group->getKey()) - ); // backwards compatibility - $query->orWhereJsonContains( - 'process_request_tokens.self_service_groups->groups', strval($group->getKey()) - ); - } - $query->orWhereJsonContains( - 'process_request_tokens.self_service_groups->users', strval($user->getKey()) - ); - }); - }); + $user = User::find($fieldFilter); + if ($user) { + $query->orWhereIn( + 'process_request_tokens.id', + $user->availableSelfServiceTasksQuery() + ); + } }); } diff --git a/tests/Feature/Api/BoundaryEventsTest.php b/tests/Feature/Api/BoundaryEventsTest.php index 7a63b0dbd4..bf86ab6a43 100644 --- a/tests/Feature/Api/BoundaryEventsTest.php +++ b/tests/Feature/Api/BoundaryEventsTest.php @@ -444,4 +444,139 @@ public function testTimerBoundaryEventMultiInstance() $this->assertCount(1, $activeTokens); $this->assertEquals('Task 2', $activeTokens[0]->element_name); } + + /** + * Tests that an interrupting timer boundary event closes an unclaimed self-service task + * without changing its assignment state + * + * @group timer_events + */ + public function testTimerBoundaryEventClosesSelfServiceTask() + { + $now = TaskSchedulerManager::fakeToday('2018-05-01T00:00:00Z'); + $process = $this->createSelfServiceTimerProcess(true); + + $instance = $this->startProcess($process, 'node_1'); + + $selfServiceToken = $instance->tokens()->where('element_id', 'node_2')->firstOrFail(); + $this->assertEquals('ACTIVE', $selfServiceToken->status); + $this->assertEquals(1, $selfServiceToken->is_self_service); + $this->assertNull($selfServiceToken->user_id); + $this->assertContains( + $selfServiceToken->id, + $this->taskIdsForPmql($instance, 'status = "Self Service"') + ); + + $now->modify('+1 minute'); + TaskSchedulerManager::fakeToday($now); + $this->runScheduledTasks(); + + $selfServiceToken->refresh(); + $this->assertEquals('CLOSED', $selfServiceToken->status); + $this->assertEquals(1, $selfServiceToken->is_self_service); + $this->assertNull($selfServiceToken->user_id); + $this->assertNull($selfServiceToken->completed_at); + $this->assertEquals('COMPLETED', $instance->refresh()->status); + $this->assertNotContains( + $selfServiceToken->id, + $this->taskIdsForPmql($instance, 'status = "Self Service"') + ); + $this->assertContains( + $selfServiceToken->id, + $this->taskIdsForPmql($instance, 'is_self_service = 1') + ); + } + + /** + * Tests that a closed self-service task is excluded before the request completes + * + * @group timer_events + */ + public function testTimerBoundaryEventExcludesClosedSelfServiceTaskWhileRequestContinues() + { + $now = TaskSchedulerManager::fakeToday('2018-05-01T00:00:00Z'); + $process = $this->createSelfServiceTimerProcess(true, true); + + $instance = $this->startProcess($process, 'node_1'); + + $now->modify('+1 minute'); + TaskSchedulerManager::fakeToday($now); + $this->runScheduledTasks(); + + $selfServiceToken = $instance->tokens()->where('element_id', 'node_2')->firstOrFail(); + $this->assertEquals('CLOSED', $selfServiceToken->status); + $this->assertEquals(1, $selfServiceToken->is_self_service); + $this->assertNull($selfServiceToken->user_id); + $this->assertEquals('ACTIVE', $instance->refresh()->status); + $this->assertTrue( + $instance->tokens()->where('element_id', 'node_7')->where('status', 'ACTIVE')->exists() + ); + $this->assertNotContains( + $selfServiceToken->id, + $this->taskIdsForPmql($instance, 'status = "Self Service"') + ); + $this->assertContains( + $selfServiceToken->id, + $this->taskIdsForPmql($instance, 'is_self_service = 1') + ); + } + + /** + * Tests that a non-interrupting timer boundary event keeps the self-service task open + * + * @group timer_events + */ + public function testNonInterruptingTimerBoundaryEventKeepsSelfServiceTaskOpen() + { + $now = TaskSchedulerManager::fakeToday('2018-05-01T00:00:00Z'); + $process = $this->createSelfServiceTimerProcess(false); + + $instance = $this->startProcess($process, 'node_1'); + + $now->modify('+1 minute'); + TaskSchedulerManager::fakeToday($now); + $this->runScheduledTasks(); + + $selfServiceToken = $instance->tokens()->where('element_id', 'node_2')->firstOrFail(); + $this->assertEquals('ACTIVE', $selfServiceToken->status); + $this->assertEquals(1, $selfServiceToken->is_self_service); + $this->assertNull($selfServiceToken->user_id); + $this->assertContains( + $selfServiceToken->id, + $this->taskIdsForPmql($instance, 'status = "Self Service"') + ); + } + + private function createSelfServiceTimerProcess(bool $cancelActivity, bool $continueAfterBoundary = false) + { + $bpmn = file_get_contents(__DIR__ . '/processes/Timer_BoundaryEvent_SelfService.bpmn'); + $bpmn = str_replace('{selfServiceUserId}', $this->user->id, $bpmn); + if (!$cancelActivity) { + $bpmn = str_replace('cancelActivity="true"', 'cancelActivity="false"', $bpmn); + } + if ($continueAfterBoundary) { + $bpmn = str_replace( + '' . PHP_EOL + . ' node_8' . PHP_EOL + . ' ', + '' . PHP_EOL + . ' node_8' . PHP_EOL + . ' ', + $bpmn + ); + } + + return $this->createProcess($bpmn); + } + + private function taskIdsForPmql(ProcessRequest $instance, string $pmql): array + { + $response = $this->apiCall('GET', route('api.tasks.index', [ + 'process_request_id' => $instance->id, + 'pmql' => $pmql, + ])); + $response->assertStatus(200); + + return array_column($response->json('data'), 'id'); + } } diff --git a/tests/Feature/Api/TasksTest.php b/tests/Feature/Api/TasksTest.php index 94002630a0..84006f5e41 100644 --- a/tests/Feature/Api/TasksTest.php +++ b/tests/Feature/Api/TasksTest.php @@ -154,6 +154,58 @@ public function testGetListAssignedTasks() $this->assertEquals(2, count($response->json()['data'])); } + public function testUserFilterIncludesOnlyActiveSelfServiceTasksAndPreservesAssignedHistory() + { + $eligibleUser = User::factory()->create(); + $request = ProcessRequest::factory()->create(); + $selfServiceGroups = ['users' => [(string) $eligibleUser->id], 'groups' => []]; + + $activeSelfService = ProcessRequestToken::factory()->create([ + 'process_id' => $request->process_id, + 'process_request_id' => $request->id, + 'element_type' => 'task', + 'status' => 'ACTIVE', + 'user_id' => null, + 'is_self_service' => 1, + 'self_service_groups' => $selfServiceGroups, + ]); + $closedSelfService = ProcessRequestToken::factory()->create([ + 'process_id' => $request->process_id, + 'process_request_id' => $request->id, + 'element_type' => 'task', + 'status' => 'CLOSED', + 'user_id' => null, + 'is_self_service' => 1, + 'self_service_groups' => $selfServiceGroups, + ]); + $assignedClosedTask = ProcessRequestToken::factory()->create([ + 'process_id' => $request->process_id, + 'process_request_id' => $request->id, + 'element_type' => 'task', + 'status' => 'CLOSED', + 'user_id' => $eligibleUser->id, + 'is_self_service' => 0, + ]); + + $scopeQuery = ProcessRequestToken::query(); + ProcessRequestToken::scopeWhereUserAssigned($scopeQuery, $eligibleUser->id); + $scopeIds = $scopeQuery->pluck('id')->all(); + $this->assertContains($activeSelfService->id, $scopeIds); + $this->assertNotContains($closedSelfService->id, $scopeIds); + $this->assertContains($assignedClosedTask->id, $scopeIds); + + $response = $this->apiCall('GET', route('api.' . $this->resource . '.index', [ + 'user_id' => $eligibleUser->id, + 'per_page' => 100, + ])); + $response->assertStatus(200); + + $taskIds = array_column($response->json('data'), 'id'); + $this->assertContains($activeSelfService->id, $taskIds); + $this->assertNotContains($closedSelfService->id, $taskIds); + $this->assertContains($assignedClosedTask->id, $taskIds); + } + /** * Return all tasks if we are only requesting closed tasks and if * the user can view the request. diff --git a/tests/Feature/Api/processes/Timer_BoundaryEvent_SelfService.bpmn b/tests/Feature/Api/processes/Timer_BoundaryEvent_SelfService.bpmn new file mode 100644 index 0000000000..070926bc7f --- /dev/null +++ b/tests/Feature/Api/processes/Timer_BoundaryEvent_SelfService.bpmn @@ -0,0 +1,58 @@ + + + + + node_4 + + + node_4 + node_5 + + + node_5 + + + node_8 + + R4/2018-05-01T00:00:00Z/PT1M + + + + node_8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit/ProcessMaker/FilterTest.php b/tests/unit/ProcessMaker/FilterTest.php index 68ad23e729..d8e4c1f889 100644 --- a/tests/unit/ProcessMaker/FilterTest.php +++ b/tests/unit/ProcessMaker/FilterTest.php @@ -362,6 +362,23 @@ public function testTaskStatusSelfservice() $this->assertStringContainsString('json_contains(`self_service_groups`', $sql); } + public function testTaskStatusSelfServiceWithoutAuthenticatedUserRequiresActiveUnassignedTask() + { + Auth::shouldReceive('user')->andReturn(null); + + $sql = $this->filter([ + [ + 'subject' => ['type' => 'Status'], + 'operator' => '=', + 'value' => 'Self Service', + ], + ], ProcessRequestToken::class); + + $this->assertStringContainsString('`process_request_tokens`.`is_self_service` = 1', $sql); + $this->assertStringContainsString("`process_request_tokens`.`status` = 'ACTIVE'", $sql); + $this->assertStringContainsString('`process_request_tokens`.`user_id` is null', $sql); + } + public function testTaskStatusActive() { $user = User::factory()->create();