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();