Skip to content
15 changes: 12 additions & 3 deletions ProcessMaker/Repositories/CaseParticipatedRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,19 @@ public function create(CaseStarted $case, int $userId): void
/**
* Update the cases participated.
*
* Each participant row stores only the tasks assigned to that user.
*
* @param CaseStarted $case
* @return void
*/
public function update(CaseStarted $case): void
{
try {
CaseParticipated::where('case_number', $case->case_number)
->update($this->mapCaseToArray($case));
$participants = CaseParticipated::where('case_number', $case->case_number)->get();

foreach ($participants as $participant) {
$participant->update($this->mapCaseToArray($case, $participant->user_id));
}
} catch (\Exception $e) {
$this->logException($e);
}
Expand All @@ -58,6 +63,8 @@ public function update(CaseStarted $case): void
/**
* Maps properties of a `CaseStarted` object to an array, optionally including a user ID.
*
* When a userId is provided, tasks are scoped to that user's assigned tasks only.
*
* @param CaseStarted case Takes a `CaseStarted` object and parameter as input and returns an array with specific
* properties mapped from the `CaseStarted` object.
* @param int userId Takes an optional `userId` parameter.
Expand All @@ -80,7 +87,9 @@ private function mapCaseToArray(CaseStarted $case, int $userId = null): array
'processes' => $case->processes,
'requests' => $case->requests,
'request_tokens' => $case->request_tokens,
'tasks' => $case->tasks,
'tasks' => $userId !== null
? CaseUtils::filterTasksByUser($case->tasks, $userId)
: $case->tasks,
'participants' => $case->participants,
'initiated_at' => $case->initiated_at,
'completed_at' => $case->completed_at,
Expand Down
65 changes: 59 additions & 6 deletions ProcessMaker/Repositories/CaseSyncRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ public static function syncCases(array $requestIds): array
'keywords' => CaseUtils::getKeywords($dataKeywords),
],
);
// copy all the columns from cases_started to cases_participated, except for the user_id
// Copy case-level columns from cases_started to cases_participated, except
// user_id and tasks. Tasks are user-scoped and applied separately below.
$sql = "UPDATE cases_participated AS cp
INNER JOIN cases_started AS cs ON cp.case_number = cs.case_number
SET
Expand All @@ -78,7 +79,6 @@ public static function syncCases(array $requestIds): array
cp.processes = cs.processes,
cp.requests = cs.requests,
cp.request_tokens = cs.request_tokens,
cp.tasks = cs.tasks,
cp.participants = cs.participants,
cp.initiated_at = cs.initiated_at,
cp.completed_at = cs.completed_at,
Expand All @@ -87,6 +87,8 @@ public static function syncCases(array $requestIds): array
";
DB::statement($sql);

self::syncParticipatedTasks($instance->case_number, $caseStartedTasks);

$successes[] = $caseStarted->case_number;
} catch (\Exception $e) {
$errors[] = $instance->case_number . ' ' . $e->getMessage();
Expand Down Expand Up @@ -174,6 +176,7 @@ private static function processTokens($instance, &$caseParticipatedData, &$caseS
'process_id' => $token->process_id,
'element_type' => $token->element_type,
'status' => $token->status,
'user_id' => $token->user_id,
];

$caseParticipatedData['processes'] = CaseUtils::storeProcesses($caseParticipatedData['processes'], $processData);
Expand All @@ -184,7 +187,11 @@ private static function processTokens($instance, &$caseParticipatedData, &$caseS
$caseStartedRequestTokens = CaseUtils::storeRequestTokens($caseStartedRequestTokens, $token->getKey());
$caseStartedTasks = CaseUtils::storeTasks($caseStartedTasks, $taskData);

self::syncCasesParticipated($instance->case_number, $token->user_id, $caseParticipatedData);
self::syncCasesParticipated(
$instance->case_number,
$token->user_id,
self::participatedDataForUser($caseParticipatedData, $token->user_id)
);
}
}

Expand Down Expand Up @@ -228,6 +235,7 @@ private static function processChildRequests(
'process_id' => $spToken->process_id,
'element_type' => $spToken->element_type,
'status' => $spToken->status,
'user_id' => $spToken->user_id,
];

if (in_array($spToken->element_type, CaseUtils::ALLOWED_ELEMENT_TYPES)) {
Expand All @@ -239,7 +247,11 @@ private static function processChildRequests(
$caseStartedRequestTokens = CaseUtils::storeRequestTokens($caseStartedRequestTokens, $spToken->getKey());
$caseStartedTasks = CaseUtils::storeTasks($caseStartedTasks, $taskData);

self::syncCasesParticipated($instance->case_number, $spToken->user_id, $caseParticipatedData);
self::syncCasesParticipated(
$instance->case_number,
$spToken->user_id,
self::participatedDataForUser($caseParticipatedData, $spToken->user_id)
);
}
}
}
Expand All @@ -248,12 +260,17 @@ private static function processChildRequests(
/**
* Sync the cases participated.
*
* @param CaseStarted $caseStarted
* @param TokenInterface $token
* @param int|string $caseNumber
* @param int|null $userId
* @param array $data
* @return void
*/
private static function syncCasesParticipated($caseNumber, $userId, $data)
{
if (is_null($userId)) {
return;
}

CaseParticipated::updateOrCreate(
[
'case_number' => $caseNumber,
Expand All @@ -262,4 +279,40 @@ private static function syncCasesParticipated($caseNumber, $userId, $data)
$data,
);
}

/**
* Build participated case data with tasks scoped to a specific user.
*
* @param array $data
* @param int|null $userId
* @return array
*/
private static function participatedDataForUser(array $data, $userId): array
{
if (is_null($userId)) {
return $data;
}

$data['tasks'] = CaseUtils::filterTasksByUser($data['tasks'] ?? collect(), $userId);

return $data;
}

/**
* Apply user-scoped task lists to all participated rows for a case.
*
* @param int|string $caseNumber
* @param Collection $caseStartedTasks
* @return void
*/
private static function syncParticipatedTasks($caseNumber, Collection $caseStartedTasks): void
{
$participants = CaseParticipated::where('case_number', $caseNumber)->get();

foreach ($participants as $participant) {
$participant->update([
'tasks' => CaseUtils::filterTasksByUser($caseStartedTasks, $participant->user_id),
]);
}
}
}
16 changes: 11 additions & 5 deletions ProcessMaker/Repositories/CaseTaskRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,16 @@ public function findCaseByTaskId(int $caseNumber, string $taskId): ?object
*/
public function updateTaskStatusInCase(int $caseNumber, string $taskIndex, string $status): void
{
DB::table($this->table)
->where('case_number', $caseNumber)
->update([
'tasks' => DB::raw('JSON_SET(tasks, "' . $taskIndex . '.status", "' . $status . '")'),
]);
$query = DB::table($this->table)->where('case_number', $caseNumber);

// Participated rows store user-scoped task lists, so only update
// rows that contain this task
if ($this->table === 'cases_participated') {
$query->whereJsonContains('tasks', ['id' => (string) $this->task->id]);
}

$query->update([
'tasks' => DB::raw('JSON_SET(tasks, "' . $taskIndex . '.status", "' . $status . '")'),
]);
}
}
54 changes: 54 additions & 0 deletions ProcessMaker/Repositories/CaseUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Collection;
use ProcessMaker\Constants\CaseStatusConstants;
use ProcessMaker\Models\ProcessRequestToken;

class CaseUtils
{
Expand Down Expand Up @@ -31,6 +32,7 @@ class CaseUtils
'process_id' => 'process_id',
'element_type' => 'element_type',
'status' => 'status',
'user_id' => 'user_id',
];

const KEYWORD_FIELDS = [
Expand Down Expand Up @@ -142,6 +144,7 @@ public static function storeRequestTokens(Collection $requestTokens, ?int $token
* 'name' => string, // The name of the element
* 'process_id' => int // The unique identifier of the process
* 'element_type' => string // The type of the element
* 'user_id' => int|null // The assignee user id
* ]
* @return Collection
*/
Expand All @@ -160,6 +163,57 @@ public static function storeTasks(Collection $tasks, ?array $taskData = []): Col
return $tasks->unique('id')->values();
}

/**
* Filter tasks to only those assigned to the given user.
*
* Tasks missing user_id are resolved from process_request_tokens.
*
* @param Collection|array|null $tasks
* @param int $userId
* @return Collection
*/
public static function filterTasksByUser(Collection|array|null $tasks, int $userId): Collection
{
$tasks = collect($tasks);
$assigneeByTaskId = self::resolveMissingTaskAssignees($tasks);

return $tasks
->filter(function ($task) use ($userId, $assigneeByTaskId) {
$taskUserId = data_get($task, 'user_id');
if ($taskUserId === null) {
$taskUserId = $assigneeByTaskId[(string) data_get($task, 'id')] ?? null;
}

return (int) $taskUserId === $userId;
})
->values();
}

/**
* Resolve assignees for tasks that do not include user_id in the snapshot.
*
* @param Collection $tasks
* @return array<string, int|null>
*/
private static function resolveMissingTaskAssignees(Collection $tasks): array
{
$missingIds = $tasks
->filter(fn ($task) => data_get($task, 'user_id') === null && data_get($task, 'id') !== null)
->pluck('id')
->map(fn ($id) => (string) $id)
->unique()
->values();

if ($missingIds->isEmpty()) {
return [];
}

return ProcessRequestToken::whereIn('id', $missingIds)
->pluck('user_id', 'id')
->mapWithKeys(fn ($assigneeId, $taskId) => [(string) $taskId => $assigneeId])
->all();
}

/**
* Store participants.
*
Expand Down
30 changes: 24 additions & 6 deletions tests/Feature/Api/V1_1/CaseUtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public function test_store_tasks()
'process_id' => 1001,
'element_type' => 'task',
'status' => 'ACTIVE',
'user_id' => 42,
];
$result = CaseUtils::storeTasks($tasks, $taskData);
$this->assertEquals(1, $result->count());
Expand All @@ -69,9 +70,24 @@ public function test_store_tasks()
'name' => 'Task 1',
'process_id' => 1001,
'status' => 'ACTIVE',
'user_id' => 42,
], $result->first());
}

public function test_filter_tasks_by_user()
{
$tasks = new Collection([
['id' => '1', 'name' => 'Task A', 'user_id' => 10, 'status' => 'ACTIVE'],
['id' => '2', 'name' => 'Task B', 'user_id' => 20, 'status' => 'ACTIVE'],
['id' => '3', 'name' => 'Task C', 'user_id' => 10, 'status' => 'CLOSED'],
]);

$result = CaseUtils::filterTasksByUser($tasks, 10);

$this->assertEquals(2, $result->count());
$this->assertEquals(['1', '3'], $result->pluck('id')->all());
}

public function test_store_participants()
{
$participants = new Collection();
Expand Down Expand Up @@ -123,8 +139,8 @@ public function test_store_participants_with_empty_data()

public function test_extract_data_process()
{
$object = (object)[
'process' => (object)[
$object = (object) [
'process' => (object) [
'id' => 1,
'name' => 'Process 1',
],
Expand All @@ -138,11 +154,11 @@ public function test_extract_data_process()

public function test_extract_data_request()
{
$object = (object)[
'processRequest' => (object)[
$object = (object) [
'processRequest' => (object) [
'id' => 1,
'name' => 'Request 1',
'parentRequest' => (object)[
'parentRequest' => (object) [
'id' => 2,
],
],
Expand All @@ -157,13 +173,14 @@ public function test_extract_data_request()

public function test_extract_data_task()
{
$object = (object)[
$object = (object) [
'id' => 1,
'element_id' => 101,
'element_name' => 'Task 1',
'process_id' => 1001,
'element_type' => 'task',
'status' => 'ACTIVE',
'user_id' => 42,
];
$expected = [
'id' => 1,
Expand All @@ -172,6 +189,7 @@ public function test_extract_data_task()
'process_id' => 1001,
'element_type' => 'task',
'status' => 'ACTIVE',
'user_id' => 42,
];
$this->assertEquals($expected, CaseUtils::extractData($object, 'TASK'));
}
Expand Down
Loading
Loading