Repository: ProcessMaker/processmaker
Pull request #8256: FOUR-23482 S2: BE - Populate the STAGE and PROGRESS columns in run ca…
Author: login: gproly
Labels: name: merge-epic
Created: 2025-05-21T12:19:48Z | Merged: 2025-05-23T14:19:07Z

## Description

The following tables needs to update with the stageId and stageName defined in process_request_tokens

process_request_tokens.stage_id 

process_request_tokens.stage_name 

https://processmaker.atlassian.net/browse/FOUR-23482

ci:deploy
ci:modeler:epic/FOUR-22600


Diff:
diff --git a/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php b/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php
index b55c89698b..45d6c7d2c7 100644
--- a/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php
+++ b/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php
@@ -53,7 +53,8 @@ public function index(Request $request, Process $process = null)
         $userConfiguration = (new UserConfigurationController())->index()['ui_configuration'];
 
         $defaultSavedSearch = $this->getDefaultSavedSearchId();
+        $defaultColumns = [];
 
-        return view('processes-catalogue.index', compact('process', 'currentUser', 'manager', 'userConfiguration', 'defaultSavedSearch'));
+        return view('processes-catalogue.index', compact('process', 'currentUser', 'manager', 'userConfiguration', 'defaultSavedSearch', 'defaultColumns'));
     }
 }
diff --git a/ProcessMaker/Listeners/BpmnSubscriber.php b/ProcessMaker/Listeners/BpmnSubscriber.php
index 39994964b3..c7f0f7f810 100644
--- a/ProcessMaker/Listeners/BpmnSubscriber.php
+++ b/ProcessMaker/Listeners/BpmnSubscriber.php
@@ -279,14 +279,6 @@ public function updateDataWithFlowTransition($transition, $flow, $instance)
 
         // Exit if no variable or expression is set
         $config = json_decode($flow->getProperties()['config'], true);
-        // Check if stage is configured to update the intance
-        $instance->stage_id = null;
-        $instance->stage_name = null;
-        if (!empty($config['stageName']) && !empty($config['stageId'])) {
-            $instance->setProperty('stage_name', $config['stageName']);
-            $instance->setProperty('stage_id', $config['stageId'] ?? null);
-        }
-        // Check if update_data is empty
         if (empty($config['update_data'])
             || empty($config['update_data']['variable'])
             || empty($config['update_data']['expression'])
@@ -332,7 +324,6 @@ public function onTerminateEndEvent($event)
     public function subscribe($events)
     {
         $events->listen(TransitionInterface::EVENT_CONDITIONED_TRANSITION, static::class . '@updateDataWithFlowTransition');
-        $events->listen(TransitionInterface::EVENT_AFTER_TRANSIT, static::class . '@updateDataWithFlowTransition');
 
         $events->listen(ProcessInterface::EVENT_PROCESS_INSTANCE_CREATED, static::class . '@onProcessCreated');
         $events->listen(ProcessInterface::EVENT_PROCESS_INSTANCE_COMPLETED, static::class . '@onProcessCompleted');
diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php
index c4a6f5d98e..0f052e1819 100644
--- a/ProcessMaker/Models/ProcessRequestToken.php
+++ b/ProcessMaker/Models/ProcessRequestToken.php
@@ -4,6 +4,7 @@
 
 use Carbon\Carbon;
 use DB;
+use DOMXPath;
 use Exception;
 use Illuminate\Auth\Access\AuthorizationException;
 use Illuminate\Support\Arr;
@@ -21,6 +22,7 @@
 use ProcessMaker\Nayra\Contracts\Bpmn\MultiInstanceLoopCharacteristicsInterface;
 use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface;
 use ProcessMaker\Nayra\Managers\WorkflowManagerDefault;
+use ProcessMaker\Nayra\Storage\BpmnDocument;
 use ProcessMaker\Notifications\ActivityActivatedNotification;
 use ProcessMaker\Notifications\TaskReassignmentNotification;
 use ProcessMaker\Query\Expression;
@@ -1046,10 +1048,52 @@ function ($key) use ($allowed) {
             ARRAY_FILTER_USE_KEY
         );
 
-        // Set stage properties in record
-        $instance = $this->getInstance();
-        $this->stage_id = $instance ? $instance->getProperty('stage_name', null) : null;
-        $this->stage_name = $instance ? $instance->getProperty('stage_id', null) : null;
+        $this->setStagePropertiesInRecord();
+    }
+
+    /**
+     * Extracts and assigns stage properties from the BPMN sequence flow configuration.
+     *
+     * This method loads the BPMN XML from the process associated with the current instance
+     * and uses XPath to locate any sequenceFlow elements that target the current element ID.
+     * It iterates over the matching flows and attempts to extract the `pm:config` attribute
+     * from each, which is expected to be a JSON-encoded string.
+     *
+     * If a configuration is found with a defined `stage.id`, the method assigns the `stage_id`
+     * and `stage_name` properties of the current object accordingly.
+     *
+     * Typical use case: setting the stage metadata (ID and name) during execution flow
+     * resolution in a BPMN process.
+     *
+     * @return void
+     */
+    public function setStagePropertiesInRecord()
+    {
+        try {
+            $instance = $this->getInstance();
+            $bpmnDocument = new BpmnDocument();
+            $bpmnDocument->loadXml($instance->process->bpmn);
+            $xpath = new DOMXPath($bpmnDocument);
+            $xpath->registerNamespace('bpmn', BpmnDocument::BPMN_MODEL);
+            $sequenceFlows = $xpath->query("//bpmn:sequenceFlow[@targetRef='$this->element_id']");
+            $config = null;
+            foreach ($sequenceFlows as $flow) {
+                $id = $flow->getAttribute('id');
+                $incomingFlow = $bpmnDocument->findElementById($id);
+                if (!$incomingFlow) {
+                    continue;
+                }
+                $pmConfig = $incomingFlow->getAttribute('pm:config');
+                $config = json_decode($pmConfig ?? '');
+                if (isset($config->stage) && isset($config->stage->id)) {
+                    break;
+                }
+            }
+            $this->stage_id = isset($config->stage) && isset($config->stage->id) ? $config->stage->id : null;
+            $this->stage_name = isset($config->stage) && isset($config->stage->name) ? $config->stage->name : null;
+        } catch (Exception $e) {
+            Log::debug($e->getMessage());
+        }
     }
 
     public function loadTokenProperties()
diff --git a/ProcessMaker/Repositories/CaseParticipatedRepository.php b/ProcessMaker/Repositories/CaseParticipatedRepository.php
index 48a3b19f77..022d3316bf 100644
--- a/ProcessMaker/Repositories/CaseParticipatedRepository.php
+++ b/ProcessMaker/Repositories/CaseParticipatedRepository.php
@@ -79,6 +79,9 @@ private function mapCaseToArray(CaseStarted $case, int $userId = null): array
             'initiated_at' => $case->initiated_at,
             'completed_at' => $case->completed_at,
             'keywords' => $case->keywords,
+            'last_stage_id' => $case->last_stage_id,
+            'last_stage_name' => $case->last_stage_name,
+            'progress' => $case->progress ?? 0,
         ];
 
         if ($userId !== null) {
diff --git a/ProcessMaker/Repositories/CaseRepository.php b/ProcessMaker/Repositories/CaseRepository.php
index ea9db90c5e..e767bd1007 100644
--- a/ProcessMaker/Repositories/CaseRepository.php
+++ b/ProcessMaker/Repositories/CaseRepository.php
@@ -68,8 +68,9 @@ public function create(ExecutionInstanceInterface $instance): void
                 'initiated_at' => $instance->initiated_at,
                 'completed_at' => null,
                 'keywords' => CaseUtils::getKeywords($dataKeywords),
-                // TO_DO: save the last stage id $this->case->last_stage_id
-                // TO_DO: save the last stage id $this->case->last_stage_name
+                'last_stage_id' => $instance->last_stage_id,
+                'last_stage_name' => $instance->last_stage_name,
+                'progress' => 0,
             ]);
         } catch (\Exception $e) {
             Log::error('CaseException: ' . $e->getMessage());
@@ -100,8 +101,9 @@ public function update(ExecutionInstanceInterface $instance, TokenInterface $tok
             $this->case->request_tokens = CaseUtils::storeRequestTokens($this->case->request_tokens, $token->getKey());
             $this->case->tasks = CaseUtils::storeTasks($this->case->tasks, $taskData);
             $this->case->keywords = CaseUtils::getKeywords($dataKeywords);
-            // TO_DO: save the last stage id $this->case->last_stage_id
-            // TO_DO: save the last stage id $this->case->last_stage_name
+            $this->case->last_stage_id = $token->stage_id;
+            $this->case->last_stage_name = $token->stage_name;
+            $this->case->progress = calculateProgressById($token->stage_id, $instance?->process?->stages);
 
             $this->updateParticipants($token);
 
diff --git a/ProcessMaker/Repositories/TokenRepository.php b/ProcessMaker/Repositories/TokenRepository.php
index 127cb8a07a..39a92f0bce 100644
--- a/ProcessMaker/Repositories/TokenRepository.php
+++ b/ProcessMaker/Repositories/TokenRepository.php
@@ -169,9 +169,10 @@ public function persistActivityActivated(ActivityInterface $activity, TokenInter
         $token->saveOrFail();
         $token->setId($token->getKey());
         $request = $token->getInstance();
+        $request->last_stage_id = $token->stage_id;
+        $request->last_stage_name = $token->stage_name;
+        $request->progress = calculateProgressById($token->stage_id, $request?->process?->stages);
         $request->notifyProcessUpdated('ACTIVITY_ACTIVATED', $token);
-        // TO_DO: save the last stage id $this->request->last_stage_id
-        // TO_DO: save the last stage id $this->request->last_stage_name
 
         CaseUpdate::dispatchSync($request, $token);
 
diff --git a/ProcessMaker/Traits/ProcessMapTrait.php b/ProcessMaker/Traits/ProcessMapTrait.php
index ff08a9e530..8bdfaa10c2 100644
--- a/ProcessMaker/Traits/ProcessMapTrait.php
+++ b/ProcessMaker/Traits/ProcessMapTrait.php
@@ -175,13 +175,12 @@ public static function getStagesByProcessId(int $processId): ?array
         if ($process) {
             $stages = $process->stages;
 
-            // If stages are stored as JSON, decode them
-            if ($stages) {
-                return json_decode($stages, true); // Decode JSON to an associative array
+            if (!is_null($stages)) {
+                return $stages;
             }
         }
 
-        // Return null if the process does not exist or has no stages
+        // Return empty array if the process does not exist or has no stages
         return [];
     }
 }
diff --git a/ProcessMaker/Traits/ProcessTrait.php b/ProcessMaker/Traits/ProcessTrait.php
index 6da4908351..dbffc87502 100644
--- a/ProcessMaker/Traits/ProcessTrait.php
+++ b/ProcessMaker/Traits/ProcessTrait.php
@@ -236,7 +236,7 @@ public function getCounts()
     public function getStagesSummary($stages = null)
     {
         if (!empty($stages)) {
-            $allStages = json_decode($stages, true);
+            $allStages = $stages;
         } else {
             return [];
         }
diff --git a/helpers.php b/helpers.php
index 18d71f72e0..3108611e7e 100644
--- a/helpers.php
+++ b/helpers.php
@@ -276,3 +276,50 @@ function shouldShow($element)
         }
     }
 }
+
+if (!function_exists('calculateProgressById')) {
+    /**
+     * Calculates the progress percentage of a stage based on its order
+     * within the total number of stages.
+     *
+     * This function searches for the stage with the given ID in the provided list
+     * of stages. Each stage must contain at least 'id' and 'order' keys.
+     * If the stage is found, it calculates the progress as:
+     *     (stage_order / total_stages) * 100
+     *
+     * The result is rounded to two decimal places.
+     *
+     * If the list of stages is `null` or empty, or if the stage with the given ID
+     * is not found, the function returns 0.0.
+     *
+     * @param int|null $id The ID of the stage to calculate progress for.
+     * @param array|null $stages An array of stages, where each stage is an associative array
+     *                           containing at least the keys 'id' and 'order'.
+     *                           This parameter can be null.
+     *
+     * @return float The progress percentage (0.0 to 100.0), rounded to two decimal places.
+     */
+    function calculateProgressById(int|null $id, array|null $stages = [])
+    {
+        if (is_null($stages)) {
+            return 0.0;
+        }
+        $totalStages = count($stages);
+        if ($totalStages === 0) {
+            return 0.0;
+        }
+        $currentStageOrder = 0;
+        foreach ($stages as $stage) {
+            if ($stage['id'] === $id) {
+                $currentStageOrder = $stage['order'];
+                break;
+            }
+        }
+        if ($currentStageOrder === 0) {
+            return 0.0;
+        }
+        $progress = ($currentStageOrder / $totalStages) * 100;
+
+        return round($progress, 2);
+    }
+}
diff --git a/resources/js/processes/modeler/components/inspector/StageItem.vue b/resources/js/processes/modeler/components/inspector/StageItem.vue
index 807b351c07..ea7d3ff8fd 100644
--- a/resources/js/processes/modeler/components/inspector/StageItem.vue
+++ b/resources/js/processes/modeler/components/inspector/StageItem.vue
@@ -5,13 +5,13 @@
     <div class="tw-flex-1">
       <template v-if="editing">
         <input
-          v-model="localLabel"
+          v-model="localName"
           class="tw-w-full tw-border tw-rounded px-2"
           @keyup.enter="onKeyupEnter"
         />
       </template>
       <template v-else>
-        <span :class="{ 'font-bold stage-item-selected': selected }">{{ label }}</span>
+        <span :class="{ 'font-bold stage-item-selected': selected }">{{ name }}</span>
       </template>
     </div>
     <i v-if="selected" class="fas fp-check-circle-blue stage-item-color" @click="onClickSelected"></i>
@@ -31,17 +31,17 @@ import { ref, watch } from 'vue';
 const props = defineProps({
   id: Number,
   order: Number,
-  label: String,
+  name: String,
   selected: Boolean
 });
 
 const emit = defineEmits(['onRemove', 'onUpdate', 'onClickCheckbox', 'onClickSelected']);
 const editing = ref(false);
-const localLabel = ref(props.label);
+const localName = ref(props.name);
 const isChecked = ref(false);
 
 const onKeyupEnter = () => {
-  emit('onUpdate', localLabel.value);
+  emit('onUpdate', localName.value);
   editing.value = false;
 };
 
@@ -53,8 +53,8 @@ const onClickSelected = () => {
   emit('onClickSelected');
 };
 
-watch(() => props.label, (val) => {
-  localLabel.value = val;
+watch(() => props.name, (val) => {
+  localName.value = val;
 });
 
 watch(() => props.selected, (val) => {
diff --git a/resources/js/processes/modeler/components/inspector/StageList.vue b/resources/js/processes/modeler/components/inspector/StageList.vue
index 86d2574946..37fa4bf3c7 100644
--- a/resources/js/processes/modeler/components/inspector/StageList.vue
+++ b/resources/js/processes/modeler/components/inspector/StageList.vue
@@ -11,7 +11,7 @@
         :key="index"
         :id="item.id"
         :order="item.order"
-        :label="item.label"
+        :name="item.name"
         :selected="item.selected"
         @onUpdate="onUpdate(index, $event)"
         @onRemove="onRemove(index)"
@@ -26,7 +26,7 @@
       <input
         v-model="newStage"
         class="tw-flex-1 tw-border tw-rounded px-2"
-        :placeholder="$t('Enter label')"
+        :placeholder="$t('Enter name')"
         @keyup.enter="onKeyupEnter"
       />
     </div>
@@ -46,6 +46,7 @@
 import { ref, computed, watch, onMounted } from 'vue';
 import draggable from 'vuedraggable';
 import StageItem from './StageItem.vue';
+import i18next from 'i18next';
 
 const props = defineProps({
   initialStages: {
@@ -59,7 +60,6 @@ const newStage = ref('');
 const stages = ref([...props.initialStages]);
 const totalStages = computed(() => stages.value.length);
 const disableButton = computed(() => totalStages.value >= 8);
-const $t = window.ProcessMaker.i18n.t;
 
 const onClickAdd = () => {
   if (disableButton.value) {
@@ -69,14 +69,22 @@ const onClickAdd = () => {
   newStage.value = '';
 };
 
+const generateUniqueId = () => {
+  const timestamp = Math.floor(Date.now() / 1000);
+  const random = Math.floor(Math.random() * 1000);
+  const paddedRandom = random.toString().padStart(3, '0');
+  const combined = `${timestamp}${paddedRandom}`.slice(0, 10);
+  return Number(combined);
+}
+
 const onKeyupEnter = () => {
   if (newStage.value.trim()) {
-    const uniqueId = Number(`${Date.now()}${Math.floor(Math.random() * 900 + 100)}`);
+    const uniqueId = generateUniqueId();
     const order = stages.value.length + 1;
     stages.value.push({
       id: uniqueId,
       order: order,
-      label: newStage.value,
+      name: newStage.value,
       selected: false
     });
     newStage.value = '';
@@ -86,17 +94,17 @@ const onKeyupEnter = () => {
   }
 };
 
-const onUpdate = (index, newLabel) => {
-  const oldLabel = stages.value[index].label;
-  stages.value[index].label = newLabel;
-  emit('onUpdate', stages.value, index, newLabel, oldLabel);
+const onUpdate = (index, newName) => {
+  const oldName = stages.value[index].name;
+  stages.value[index].name = newName;
+  emit('onUpdate', stages.value, index, newName, oldName);
   emit('onChange', stages.value);
 };
 
 const onRemove = (index) => {
   ProcessMaker.confirmModal(
-    $t("Caution!"),
-    $t("Are you sure you want to delete the stage?"),
+    i18next.t("Caution!"),
+    i18next.t("Are you sure you want to delete the stage?"),
     "",
     () => {
       const removed = stages.value.splice(index, 1);
diff --git a/resources/js/processes/modeler/components/inspector/StageManager.vue b/resources/js/processes/modeler/components/inspector/StageManager.vue
index 1c67008617..d029c76001 100644
--- a/resources/js/processes/modeler/components/inspector/StageManager.vue
+++ b/resources/js/processes/modeler/components/inspector/StageManager.vue
@@ -94,7 +94,7 @@ const updateStagesForAllFlowConfigs = (stages) => {
       let config = getConfigFromDefinition(link.component.node.definition);
       if (config?.stage?.id === stage.id) {
         config.stage.order = stage.order;
-        config.stage.label = stage.label;
+        config.stage.name = stage.name;
         Vue.set(link.component.node.definition, 'config', JSON.stringify(config));
       }
     }
@@ -119,7 +119,7 @@ const applyStageToFlow = (stage) => {
   config.stage = {
     id: stage.id,
     order: stage.order,
-    label: stage.label
+    name: stage.name
   };
   let definition = getDefinition();
   Vue.set(definition, 'config', JSON.stringify(config));
diff --git a/tests/Feature/Api/ProcessControllerTest.php b/tests/Feature/Api/ProcessControllerTest.php
index 915423c313..141efba453 100644
--- a/tests/Feature/Api/ProcessControllerTest.php
+++ b/tests/Feature/Api/ProcessControllerTest.php
@@ -202,8 +202,8 @@ public function testSaveAndRetrieveStages()
     public function test_can_get_process_stages()
     {
         $stages = [
-            ['id' => 1, 'order' => 1, 'label' => 'Start', 'selected' => false],
-            ['id' => 2, 'order' => 2, 'label' => 'Review', 'selected' => true],
+            ['id' => 1, 'order' => 1, 'name' => 'Start', 'selected' => false],
+            ['id' => 2, 'order' => 2, 'name' => 'Review', 'selected' => true],
         ];
 
         $process = Process::factory()->create([
@@ -234,8 +234,8 @@ public function test_can_save_process_stages()
         $process = Process::factory()->create();
 
         $newStages = [
-            ['id' => 1, 'order' => 1, 'label' => 'New Stage 1', 'selected' => true],
-            ['id' => 2, 'order' => 2, 'label' => 'New Stage 2', 'selected' => false],
+            ['id' => 1, 'order' => 1, 'name' => 'New Stage 1', 'selected' => true],
+            ['id' => 2, 'order' => 2, 'name' => 'New Stage 2', 'selected' => false],
         ];
 
         $response = $this->actingAs($this->user, 'api')
diff --git a/tests/Feature/Api/ProcessLaunchpadTest.php b/tests/Feature/Api/ProcessLaunchpadTest.php
index 001937db6d..ecaa7e9674 100644
--- a/tests/Feature/Api/ProcessLaunchpadTest.php
+++ b/tests/Feature/Api/ProcessLaunchpadTest.php
@@ -116,13 +116,13 @@ public function testGetStagesCountPerProcessWithStagesAndNoRequests()
     {
         // Create stages for the process
         $stages = [
-            ['id' => 1, 'name' => 'Stage A'],
-            ['id' => 2, 'name' => 'Stage B'],
+            ['id' => 1, 'name' => 'Stage A', 'order' => 1],
+            ['id' => 2, 'name' => 'Stage B', 'order' => 2],
         ];
         $user = Auth::user();
         // Create a process with stages
         $process = Process::factory()->create([
-            'stages' => json_encode($stages),
+            'stages' => $stages,
         ]);
         ProcessLaunchpad::factory()->create([
             'process_id' => $process->id,
@@ -130,7 +130,7 @@ public function testGetStagesCountPerProcessWithStagesAndNoRequests()
 
         // No ProcessRequest records are created for this test
 
-        $stageSummary = $process->getStagesSummary(json_encode($stages));
+        $stageSummary = $process->getStagesSummary($stages);
         $expectedStagesSummary = [
             ['id' => 1, 'name' => 'Stage A', 'count' => 0, 'percentage' => 0],
             ['id' => 2, 'name' => 'Stage B', 'count' => 0, 'percentage' => 0],
@@ -152,7 +152,7 @@ public function testGetStagesCountPerProcess()
         ];
         // Create a process
         $process = Process::factory()->create([
-            'stages' => json_encode($stages),
+            'stages' => $stages,
         ]);
         ProcessLaunchpad::factory()->create([
             'process_id' => $process->id,
@@ -176,7 +176,7 @@ public function testGetStagesCountPerProcess()
 
         // Call the API GET
         $response = $this->apiCall('GET', self::API_TEST_URL . '/' . $process->id);
-        $stageSummary = $process->getStagesSummary(json_encode($stages));
+        $stageSummary = $process->getStagesSummary($stages);
         // Validate the header status code
         $response->assertStatus(200);
         $this->assertNotEmpty($response);
diff --git a/tests/Model/ProcessRequestTokenTest.php b/tests/Model/ProcessRequestTokenTest.php
new file mode 100644
index 0000000000..ff8388a5fc
--- /dev/null
+++ b/tests/Model/ProcessRequestTokenTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Tests\Model;
+
+use DOMXPath;
+use ProcessMaker\Models\ProcessRequestToken;
+use ProcessMaker\Nayra\Storage\BpmnDocument;
+use stdClass;
+use Tests\TestCase;
+
+class ProcessRequestTokenTest extends TestCase
+{
+    public function testSetStagePropertiesInRecord()
+    {
+        // Create a partial mock of the token
+        $token = $this->getMockBuilder(ProcessRequestToken::class)
+            ->onlyMethods(['getInstance'])
+            ->getMock();
+
+        // Fake BPMN XML with a sequenceFlow and pm:config
+        $bpmnXml = <<<'XML'
+                    <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:pm="http://processmaker.com/BPMN/Configuration">
+                        <bpmn:sequenceFlow id="flow_1" targetRef="element_123" pm:config='{"stage": {"id": 7, "name": "Review"}}'/>
+                    </bpmn:definitions>
+                    XML;
+
+        // Simulate getInstance returning a process with BPMN XML
+        $instance = new stdClass();
+        $instance->process = new stdClass();
+        $instance->process->bpmn = $bpmnXml;
+
+        $token->method('getInstance')->willReturn($instance);
+        $token->element_id = 'element_123';
+
+        // Act
+        $token->setStagePropertiesInRecord();
+
+        // Assert
+        $this->assertEquals(7, $token->stage_id);
+        $this->assertEquals('Review', $token->stage_name);
+    }
+}


Reviews:
user: login: pmPaulis
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:17:13Z
user: login: gproly
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T21:09:53Z
user: login: pmPaulis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T22:43:08Z

Review comments:
diff_hunk: @@ -58,17 +58,23 @@ public function getStageCase($case_number)
      */
     public function getStagesSummary($requestId)
     {
-        // TO_DO: get all values processes.stages
-        $allStages = [
-            ['id' => 1, 'name' => 'Stage A'],
-            ['id' => 2, 'name' => 'Stage B'],
-            ['id' => 3, 'name' => 'Stage C'],
-        ];
-        // TO_DO: Create a query to get ProcessRequestToken::where('process_request_id', $requestId)->select('stage_id', 'stage_name', 'status', 'completed_at')->get();
-        $allCurrentStages = [
-            ['stage_id' => 1, 'stage_name' => 'Stage A', 'status' => 'CLOSED', 'completed_at' => '2025-04-23 21:22:34'],
-            ['stage_id' => 2, 'stage_name' => 'Stage B', 'status' => 'ACTIVE', 'completed_at' => 'null'],
-        ];
+        $processId = ProcessRequest::find($requestId);
path: ProcessMaker/Http/Controllers/Api/CaseController.php
user: login: pmPaulis
body: @gproly please revert this changes, I updated this in other ticket, this will cause conflicts
created_at: 2025-05-22T19:15:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,17 +58,23 @@ public function getStageCase($case_number)
      */
     public function getStagesSummary($requestId)
     {
-        // TO_DO: get all values processes.stages
-        $allStages = [
-            ['id' => 1, 'name' => 'Stage A'],
-            ['id' => 2, 'name' => 'Stage B'],
-            ['id' => 3, 'name' => 'Stage C'],
-        ];
-        // TO_DO: Create a query to get ProcessRequestToken::where('process_request_id', $requestId)->select('stage_id', 'stage_name', 'status', 'completed_at')->get();
-        $allCurrentStages = [
-            ['stage_id' => 1, 'stage_name' => 'Stage A', 'status' => 'CLOSED', 'completed_at' => '2025-04-23 21:22:34'],
-            ['stage_id' => 2, 'stage_name' => 'Stage B', 'status' => 'ACTIVE', 'completed_at' => 'null'],
-        ];
+        $processId = ProcessRequest::find($requestId);
path: ProcessMaker/Http/Controllers/Api/CaseController.php
user: login: gproly
body: Applied
created_at: 2025-05-22T21:09:53Z
author_association: CONTRIBUTOR

PR comments:
user: login: vladyrichter
created_at: 2025-05-21T14:06:16Z
body: QA server K8S was successfully deployed https://ci-e0b1ab1440.engk8s.processmaker.net
author_association: NONE
user: login: vladyrichter
created_at: 2025-05-22T18:51:14Z
body: QA server K8S was successfully deployed https://ci-e0b1ab1440.engk8s.processmaker.net
author_association: NONE
user: login: pmPaulis
created_at: 2025-05-22T19:06:44Z
body: @gproly 
The test are failing
```
TypeError: calculateProgressById(): Argument #1 ($id) must be of type int, null given, called in /opt/processmaker/ProcessMaker/Repositories/CaseRepository.php on line 106
/opt/processmaker/helpers.php:302
/opt/processmaker/ProcessMaker/Repositories/CaseRepository.php:106
/opt/processmaker/ProcessMaker/Jobs/CaseUpdate.php:31
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:36
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Container/Util.php:43
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:95
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:35
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Container/Container.php:696
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php:126
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:170
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:127
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php:130
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Support/Testing/Fakes/BusFake.php:675
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php:126
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:170
/opt/processmaker/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:127
```
Regards,
author_association: CONTRIBUTOR
user: login: vladyrichter
created_at: 2025-05-22T22:15:40Z
body: QA server K8S was successfully deployed https://ci-e0b1ab1440.engk8s.processmaker.net
author_association: NONE
user: login: vladyrichter
created_at: 2025-05-22T22:46:16Z
body: QA server K8S was successfully deployed https://ci-e0b1ab1440.engk8s.processmaker.net
author_association: NONE
user: login: vladyrichter
created_at: 2025-05-22T23:26:13Z
body: QA server K8S was successfully deployed https://ci-e0b1ab1440.engk8s.processmaker.net
author_association: NONE
user: login: vladyrichter
created_at: 2025-05-23T00:24:54Z
body: QA server K8S was successfully deployed https://ci-e0b1ab1440.engk8s.processmaker.net
author_association: NONE
user: login: pmPaulis
created_at: 2025-05-23T00:57:17Z
body: @gproly please check the issue with the unit test related with `completeTask():` is not working:
```
Unit Test Tests\Feature\EditDataTest::completeTask(): Argument #1 ($task) must be of type ProcessMaker\Models\ProcessRequestToken, null given, called in /opt/processmaker/tests/Feature/EditDataTest.php on line 178
... 
✘ Register intermediate timer events
   │ TypeError: Tests\Feature\Api\IntermediateTimerEventTest::completeTask(): Argument #1 ($token) must be of type ProcessMaker\Models\ProcessRequestToken, null given, called in /opt/processmaker/tests/Feature/Api/IntermediateTimerEventTest.php on line 73
```
author_association: CONTRIBUTOR
user: login: vladyrichter
created_at: 2025-05-23T03:05:15Z
body: QA server K8S was successfully deployed https://ci-e0b1ab1440.engk8s.processmaker.net
author_association: NONE
user: login: processmaker-sonarqube[bot]
created_at: 2025-05-23T03:26:06Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarqube.processmaker.net/dashboard?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&pullRequest=8256) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.processmaker.net/project/issues?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&pullRequest=8256&resolved=false) [0 New issues](https://sonarqube.processmaker.net/project/issues?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&pullRequest=8256&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://sonarqube.processmaker.net/project/issues?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&fixedInPullRequest=8256) [0 Fixed issues](https://sonarqube.processmaker.net/project/issues?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&fixedInPullRequest=8256)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://sonarqube.processmaker.net/project/issues?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&pullRequest=8256&issueStatuses=ACCEPTED) [0 Accepted issues](https://sonarqube.processmaker.net/project/issues?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&pullRequest=8256&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.processmaker.net/security_hotspots?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&pullRequest=8256) [0 Security Hotspots](https://sonarqube.processmaker.net/security_hotspots?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&pullRequest=8256)  
![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/no-data-16px.png '') No data about Coverage  
![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/no-data-16px.png '') No data about Duplication  
  
[See analysis details on SonarQube](https://sonarqube.processmaker.net/dashboard?id=ProcessMaker_processmaker_AYkzkfKexYvY_isvKt07&pullRequest=8256)


author_association: NONE