Repository: epicollect5/epicollect5-server
Pull request #8: Fixed branch locations query, added missing method
Author: login: mirko77
Created: 2025-03-01T20:33:51Z | Merged: 2025-03-03T17:42:21Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new method for retrieving geographical data with improved user role filtering for enhanced API responses.

- **Bug Fixes**
  - Removed outdated geographical data retrieval method to streamline functionality.

- **Tests**
  - Expanded test coverage for location-based endpoints, including branch entries, and improved error handling for more robust validations.
  - Updated tests to utilize instance methods instead of static calls, enhancing clarity and maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/Http/Controllers/Api/Entries/DownloadSubsetController.php b/app/Http/Controllers/Api/Entries/DownloadSubsetController.php
index 340875ef..c22679f1 100644
--- a/app/Http/Controllers/Api/Entries/DownloadSubsetController.php
+++ b/app/Http/Controllers/Api/Entries/DownloadSubsetController.php
@@ -82,7 +82,7 @@ public function subset(Request $request, RuleDownloadSubset $ruleDownloadSubset,
         if ($params['branch_ref'] !== '') {
             $columns = ['uuid', 'title', 'entry_data', 'user_id', 'uploaded_at'];
             // Get the query for these branch entries
-            $query = BranchEntry::getBranchEntriesByBranchRef(
+            $query = (new BranchEntry())->getBranchEntriesByBranchRef(
                 $this->requestedProject()->getId(),
                 $params,
                 $columns
@@ -91,7 +91,7 @@ public function subset(Request $request, RuleDownloadSubset $ruleDownloadSubset,
             //hierarchy
             $columns = ['title', 'entry_data', 'branch_counts', 'child_counts', 'user_id', 'uploaded_at'];
             // Get the query for these entries
-            $query = Entry::getEntriesByForm($this->requestedProject()->getId(), $params, $columns);
+            $query = (new Entry())->getEntriesByForm($this->requestedProject()->getId(), $params, $columns);
         }
 
         $filepath = $this->createSubsetArchive($query, $filename);
diff --git a/app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php b/app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php
index 7e1815ff..683d413c 100644
--- a/app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php
+++ b/app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php
@@ -54,7 +54,7 @@ protected function runQueryHierarchy(array $params, array $columns)
                 );
             } else {
                 // All Form Entries
-                $query = Entry::getEntriesByForm(
+                $query = $entryModel->getEntriesByForm(
                     $this->requestedProject()->getId(),
                     $params,
                     $columns
@@ -90,7 +90,7 @@ protected function runQueryBranch(array $params, array $columns)
                 );
             } else {
                 // All Branch Entries for Branch Ref
-                $query = $branchEntryModel::getBranchEntriesByBranchRef(
+                $query = $branchEntryModel->getBranchEntriesByBranchRef(
                     $this->requestedProject()->getId(),
                     $params,
                     $columns
diff --git a/app/Models/Entries/BranchEntry.php b/app/Models/Entries/BranchEntry.php
index 23422522..ae4b579f 100644
--- a/app/Models/Entries/BranchEntry.php
+++ b/app/Models/Entries/BranchEntry.php
@@ -71,10 +71,10 @@ public function getEntry($projectId, $options, array $columns = array('*')): Bui
                 }
             });
 
-        return self::sortAndFilterEntries($q, $options);
+        return $this->sortAndFilterEntries($q, $options);
     }
 
-    public static function getBranchEntriesByBranchRef($projectId, $params, $columns = array('*')): Builder
+    public function getBranchEntriesByBranchRef($projectId, $params, $columns = array('*')): Builder
     {
         $q = DB::table(config('epicollect.tables.branch_entries'))
             ->where('project_id', '=', $projectId)
@@ -88,7 +88,7 @@ public static function getBranchEntriesByBranchRef($projectId, $params, $columns
             })
             ->select($columns);
 
-        return self::sortAndFilterEntries($q, $params);
+        return $this->sortAndFilterEntries($q, $params);
     }
 
     /**
diff --git a/app/Models/Entries/Entry.php b/app/Models/Entries/Entry.php
index 34dd644f..07d19140 100644
--- a/app/Models/Entries/Entry.php
+++ b/app/Models/Entries/Entry.php
@@ -3,7 +3,6 @@
 namespace ec5\Models\Entries;
 
 use DB;
-use ec5\Services\Entries\EntriesViewService;
 use ec5\Traits\Eloquent\Entries;
 use ec5\Traits\Models\SerializeDates;
 use Illuminate\Database\Eloquent\Model;
@@ -50,10 +49,22 @@ class Entry extends Model
     ];
 
     /**
-     * @param $projectId
-     * @param $options
-     * @param array $columns
-     * @return Builder
+     * Retrieves a query builder for a specific entry using the given project ID and options.
+     *
+     * This method builds a database query to select an entry from the entries table by matching the
+     * project identifier with a specified form reference and UUID. It optionally applies additional filters
+     * for user and parent entry identifiers if provided in the options. The query is then processed with
+     * sorting and filtering adjustments.
+     *
+     * @param mixed $projectId Identifier of the project.
+     * @param array $options Array containing query parameters:
+     *                       - 'form_ref': Form reference for filtering.
+     *                       - 'uuid': Unique identifier of the entry.
+     *                       - 'user_id' (optional): Identifier of the user for additional filtering.
+     *                       - 'parent_entry_uuid' (optional): UUID of the parent entry for additional filtering.
+     * @param array $columns List of columns to select; defaults to all columns.
+     *
+     * @return \Illuminate\Database\Query\Builder Query builder instance for the entry.
      */
     public function getEntry($projectId, $options, array $columns = array('*')): Builder
     {
@@ -73,33 +84,7 @@ public function getEntry($projectId, $options, array $columns = array('*')): Bui
                 }
             });
 
-        return self::sortAndFilterEntries($q, $options);
-    }
-
-    /**
-     * @param $projectId
-     * @param $params
-     * @return Builder
-     */
-    public function getGeoJsonData($projectId, $params): Builder
-    {
-        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
-        $whereSql = 'project_id = ?';
-
-        //get all location data
-        $q = DB::table($this->table)
-            ->whereRaw($whereSql, [$projectId])
-            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
-
-        //filter by user (imp: applied to COLLECTOR ROLE ONLY)
-        /**
-         * @see EntriesViewService::getSanitizedQueryParams
-         */
-        if (!empty($params['user_id'])) {
-            $q->where('user_id', '=', $params['user_id']);
-        }
-
-        return self::sortAndFilterEntries($q, $params);
+        return $this->sortAndFilterEntries($q, $options);
     }
 
     /**
@@ -126,7 +111,7 @@ public function getChildEntriesForParent($projectId, $options, array $columns =
         if (!empty($options['input_ref'])) {
             $q = $this->createFilterOptions($q, $options);
         }
-        return self::sortAndFilterEntries($q, $options);
+        return $this->sortAndFilterEntries($q, $options);
     }
 
     /**
@@ -185,7 +170,7 @@ public function getEntries($projectId, $options, array $columns = array('*')): B
         return $this->sortAndFilterEntries($q, $options);
     }
 
-    public static function getEntriesByForm($projectId, $params, $columns = array('*')): Builder
+    public function getEntriesByForm($projectId, $params, $columns = array('*')): Builder
     {
         $q = DB::table(config('epicollect.tables.entries'))
             ->where('project_id', '=', $projectId)
@@ -198,7 +183,7 @@ public static function getEntriesByForm($projectId, $params, $columns = array('*
             })
             ->select($columns);
 
-        return self::sortAndFilterEntries($q, $params);
+        return $this->sortAndFilterEntries($q, $params);
     }
 
     /**
diff --git a/app/Services/Entries/EntriesDownloadService.php b/app/Services/Entries/EntriesDownloadService.php
index 806fa743..547c1cf2 100644
--- a/app/Services/Entries/EntriesDownloadService.php
+++ b/app/Services/Entries/EntriesDownloadService.php
@@ -56,7 +56,7 @@ public function createArchive(ProjectDTO $project, $projectDir, $params): bool
 
             $columns = ['title', 'entry_data', 'branch_counts', 'child_counts', 'user_id', 'uploaded_at'];
             // Get the query for these entries
-            $query = Entry::getEntriesByForm($this->project->getId(), $params, $columns);
+            $query = (new Entry())->getEntriesByForm($this->project->getId(), $params, $columns);
             // Write to file
             if (!$this->writeToFile($query, $projectDir, $fileName, $format)) {
                 return false;
@@ -88,7 +88,7 @@ public function createArchive(ProjectDTO $project, $projectDir, $params): bool
                 $columns = ['uuid', 'title', 'entry_data', 'user_id', 'uploaded_at'];
 
                 // Get the query for these branch entries
-                $query = BranchEntry::getBranchEntriesByBranchRef(
+                $query = (new BranchEntry())->getBranchEntriesByBranchRef(
                     $this->project->getId(),
                     $params,
                     $columns
@@ -230,12 +230,12 @@ function (Collection $entries) use (&$count, $total, $file) {
                             $count++;
                             // Write row to file
                             fwrite($file, $this->dataMappingService->getMappedEntryJSON(
-                                    $entry->entry_data,
-                                    $entry->user_id,
-                                    $entry->title,
-                                    $entry->uploaded_at,
-                                    $entry->branch_counts ?? null
-                                ) . $append);
+                                $entry->entry_data,
+                                $entry->user_id,
+                                $entry->title,
+                                $entry->uploaded_at,
+                                $entry->branch_counts ?? null
+                            ) . $append);
                         }
                     }
                 );
diff --git a/app/Traits/Eloquent/Entries.php b/app/Traits/Eloquent/Entries.php
index 38b43d02..64240420 100644
--- a/app/Traits/Eloquent/Entries.php
+++ b/app/Traits/Eloquent/Entries.php
@@ -11,6 +11,66 @@
 
 trait Entries
 {
+    /**
+     * Retrieves GeoJSON data for entries associated with a specific project.
+     *
+     * Constructs a query to extract GeoJSON data using the provided project ID and JSON key reference (via 'input_ref').
+     * If a 'user_id' is supplied in the parameters, the results are further filtered to include only entries corresponding
+     * to that user. The query is subsequently modified with additional sorting and filtering by calling sortAndFilterEntries.
+     *
+     * @param mixed $projectId The identifier of the project.
+     * @param array $params   Query parameters, including:
+     *                        - 'input_ref': The JSON key used to extract data from the geo_json_data field.
+     *                        - 'user_id' (optional): Filters entries by user ID if provided.
+     *
+     * @return \Illuminate\Database\Query\Builder The query builder instance configured for retrieving GeoJSON data.
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return $this->sortAndFilterEntries($q, $params);
+    }
+
     /**
      * Search for entries based on answers
      *
@@ -33,7 +93,7 @@ protected function createFilterOptions(Builder $q, $options): Builder
         return $q;
     }
 
-    public static function sortAndFilterEntries(Builder $q, $filters): Builder
+    public function sortAndFilterEntries(Builder $q, $filters): Builder
     {
         // Filtering
         if (!empty($filters['filter_by'])) {
diff --git a/tests/Http/Controllers/Api/Entries/View/Internal/ViewEntriesLocationsControllerTest.php b/tests/Http/Controllers/Api/Entries/View/Internal/ViewEntriesLocationsControllerTest.php
index 4fafdfbb..c38ffb22 100644
--- a/tests/Http/Controllers/Api/Entries/View/Internal/ViewEntriesLocationsControllerTest.php
+++ b/tests/Http/Controllers/Api/Entries/View/Internal/ViewEntriesLocationsControllerTest.php
@@ -5,6 +5,7 @@
 use Auth;
 use ec5\Libraries\Utilities\Common;
 use ec5\Libraries\Utilities\Generators;
+use ec5\Models\Entries\BranchEntry;
 use ec5\Models\Entries\Entry;
 use ec5\Models\Project\ProjectRole;
 use ec5\Models\User\User;
@@ -12,11 +13,16 @@
 use Exception;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
 use Tests\Http\Controllers\Api\Entries\View\ViewEntriesBaseControllerTest;
+use Throwable;
 
 class ViewEntriesLocationsControllerTest extends ViewEntriesBaseControllerTest
 {
-    use DatabaseTransactions, Assertions;
+    use DatabaseTransactions;
+    use Assertions;
 
+    /**
+     * @throws Throwable
+     */
     public function test_parent_entry_row_stored_to_db()
     {
         $formRef = array_get($this->projectDefinition, 'data.project.forms.0.ref');
@@ -35,6 +41,9 @@ public function test_parent_entry_row_stored_to_db()
         );
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_multiple_parent_entry_rows_stored_to_db()
     {
         $count = rand(50, 100);
@@ -43,6 +52,10 @@ public function test_multiple_parent_entry_rows_stored_to_db()
         }
     }
 
+
+    /**
+     * @throws Throwable
+     */
     public function test_location_endpoint_input_ref_missing()
     {
         //generate entries
@@ -86,11 +99,14 @@ public function test_location_endpoint_input_ref_missing()
                     ]
                 ]
             ]);
-        } catch (\Throwable $e) {
+        } catch (Throwable $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_location_endpoint_input_ref_not_existing()
     {
         //generate entries
@@ -135,11 +151,14 @@ public function test_location_endpoint_input_ref_not_existing()
                     ]
                 ]
             ]);
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_location_endpoint_form_ref_not_existing()
     {
         //generate entries
@@ -185,11 +204,14 @@ public function test_location_endpoint_form_ref_not_existing()
                     ]
                 ]
             ]);
-        } catch (\Throwable $e) {
+        } catch (Throwable $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_entries_locations_endpoint_default_to_first_form()
     {
         //generate entries
@@ -238,15 +260,21 @@ public function test_entries_locations_endpoint_default_to_first_form()
 
             $this->assertEquals(1, sizeof($geoJson['features']));
             $this->assertEquals($entriesSavedUuids[0], $geoJson['features'][0]['id']);
+            $this->assertEquals($entriesSavedTitles[0], $geoJson['features'][0]['properties']['title']);
+
             $this->assertGeoJsonData(
                 $locationInputRefs[0],
                 $entryPayloads[0]['data']['entry'],
-                $geoJson['features'][0]);
-        } catch (\Exception $e) {
+                $geoJson['features'][0]
+            );
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_entries_locations_endpoint_form_0_multiple_entries()
     {
         //generate entries
@@ -302,11 +330,14 @@ public function test_entries_locations_endpoint_form_0_multiple_entries()
                 $this->assertContains($feature['properties']['uuid'], $entriesSavedUuids);
                 $this->assertContains($feature['properties']['title'], $entriesSavedTitles);
             }
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_entries_locations_endpoint_form_0_single_entry()
     {
         //generate entries
@@ -362,18 +393,138 @@ public function test_entries_locations_endpoint_form_0_single_entry()
             //Actual: 2024-02-07
             $this->assertEquals(
                 date('Y-m-d', strtotime($entryPayloads[0]['data']['entry']['created_at'])),
-                $geoJson['features'][0]['properties']['created_at']);
+                $geoJson['features'][0]['properties']['created_at']
+            );
 
 
             $this->assertGeoJsonData(
                 $locationInputRefs[0],
                 $entryPayloads[0]['data']['entry'],
-                $geoJson['features'][0]);
-        } catch (\Exception $e) {
+                $geoJson['features'][0]
+            );
+        } catch (Exception $e) {
+            $this->logTestError($e, $response);
+        }
+    }
+
+    /**
+     * @throws Throwable
+     */
+    public function test_branch_entries_locations_endpoint_form_0_single_entry()
+    {
+        //generate entries
+        $formRef = array_get($this->projectDefinition, 'data.project.forms.0.ref');
+        $entryPayloads = [];
+        for ($i = 0; $i < 1; $i++) {
+            $entryPayloads[$i] = $this->entryGenerator->createParentEntryPayload($formRef);
+            $entryRowBundle = $this->entryGenerator->createParentEntryRow(
+                $this->user,
+                $this->project,
+                $this->role,
+                $this->projectDefinition,
+                $entryPayloads[$i]
+            );
+
+            $this->assertEntryRowAgainstPayload(
+                $entryRowBundle,
+                $entryPayloads[$i]
+            );
+        }
+
+        //get branch inputs
+        $inputs = array_get($this->projectDefinition, 'data.project.forms.0.inputs');
+        //get the first branch
+        $branchRef = '';
+        $branchIndex = 0;
+        foreach ($inputs as $index => $input) {
+            if ($input['type'] === config('epicollect.strings.inputs_type.branch')) {
+                $branchIndex = $index;
+                $branchRef = $input['ref'];
+                break;
+            }
+        }
+
+        $branchEntryPayloads = [];
+        for ($i = 0; $i < 1; $i++) {
+            $branchEntryPayloads[$i] = $this->entryGenerator->createBranchEntryPayload(
+                $formRef,
+                $inputs[$branchIndex]['branch'],
+                $entryPayloads[0]['data']['id'],
+                $branchRef
+            );
+            $entryRowBundle = $this->entryGenerator->createBranchEntryRow(
+                $this->user,
+                $this->project,
+                $this->role,
+                $this->projectDefinition,
+                $branchEntryPayloads[$i]
+            );
+
+            $this->assertEntryRowAgainstPayload(
+                $entryRowBundle,
+                $branchEntryPayloads[$i]
+            );
+        }
+
+        //assert rows are created
+        $this->assertCount(
+            1,
+            Entry::where('uuid', $entryPayloads[0]['data']['id'])->get()
+        );
+
+        $this->assertCount(
+            1,
+            BranchEntry::where(
+                'owner_entry_id',
+                Entry::where('uuid', $entryPayloads[0]['data']['id'])->pluck('id')
+            )->get()
+        );
+
+        $branchEntriesSavedUuids = BranchEntry::where('project_id', $this->project->id)->pluck('uuid')->toArray();
+        $branchEntriesSavedTitles = BranchEntry::where('project_id', $this->project->id)->pluck('title')->toArray();
+
+        //get location inputs for the branch entry (only one for this test)
+        $locationBranchInputRefs = Common::getBranchLocationInputRefs($this->projectDefinition, 0, $branchRef);
+
+        //assert response passing the location input_ref and first form ref and the branch_ref
+        //imp: input_ref is the branch input of type location, branch_ref is the branch owning that input
+        $queryString = '?form_ref=' . $formRef . '&input_ref='.$locationBranchInputRefs[0].'&branch_ref=' .$branchRef ;
+        $response = [];
+        try {
+            $response[] = $this->actingAs($this->user)
+                ->get('api/internal/entries-locations/' . $this->project->slug . $queryString);
+            $response[0]->assertStatus(200);
+            $this->assertEntriesLocationsResponse($response[0]);
+
+            $json = json_decode($response[0]->getContent(), true);
+            $geoJson = $json['data']['geojson'];
+
+            $this->assertEquals(1, sizeof($geoJson['features']));
+            $this->assertEquals($branchEntriesSavedUuids[0], $geoJson['features'][0]['id']);
+            $this->assertEquals($branchEntriesSavedUuids[0], $geoJson['features'][0]['properties']['uuid']);
+            $this->assertEquals($branchEntriesSavedTitles[0], $geoJson['features'][0]['properties']['title']);
+
+            //Uploaded: 2024-02-07T15:56:10.000Z
+            //Actual: 2024-02-07
+            $this->assertEquals(
+                date('Y-m-d', strtotime($branchEntryPayloads[0]['data']['branch_entry']['created_at'])),
+                $geoJson['features'][0]['properties']['created_at']
+            );
+
+
+            $this->assertGeoJsonData(
+                $locationBranchInputRefs[0],
+                $branchEntryPayloads[0]['data']['branch_entry'],
+                $geoJson['features'][0]
+            );
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_creator_should_view_all_entries()
     {
         $formRef = array_get($this->projectDefinition, 'data.project.forms.0.ref');
@@ -445,11 +596,14 @@ public function test_creator_should_view_all_entries()
             //should be able to get all the entries ($this->user)
             $this->assertCount($numOfEntries * 2, $geoJson['features']);
 
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_manager_should_view_all_entries()
     {
         $formRef = array_get($this->projectDefinition, 'data.project.forms.0.ref');
@@ -521,11 +675,14 @@ public function test_manager_should_view_all_entries()
             //should be able to get all the entries ($this->user)
             $this->assertCount($numOfEntries * 2, $geoJson['features']);
 
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_curator_should_view_all_entries()
     {
         $formRef = array_get($this->projectDefinition, 'data.project.forms.0.ref');
@@ -597,11 +754,14 @@ public function test_curator_should_view_all_entries()
             //should be able to get all the entries ($this->user)
             $this->assertCount($numOfEntries * 2, $geoJson['features']);
 
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_viewer_should_view_all_entries()
     {
         $formRef = array_get($this->projectDefinition, 'data.project.forms.0.ref');
@@ -673,11 +833,14 @@ public function test_viewer_should_view_all_entries()
             //should be able to get all the entries ($this->user)
             $this->assertCount($numOfEntries * 2, $geoJson['features']);
 
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_collector_can_view_only_own_entries()
     {
         $formRef = array_get($this->projectDefinition, 'data.project.forms.0.ref');
@@ -787,9 +950,9 @@ public function test_collector_can_view_only_own_entries()
             foreach ($geoJson['features'] as $feature) {
                 $this->assertTrue(in_array($feature['id'], $collectorEntriesUuids));
             }
-        } catch (\Exception $e) {
+        } catch (Exception $e) {
             $this->logTestError($e, $response);
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/tests/Models/Eloquent/BranchEntryTest.php b/tests/Models/Eloquent/BranchEntryTest.php
index 4ade8bdc..5fea54e2 100644
--- a/tests/Models/Eloquent/BranchEntryTest.php
+++ b/tests/Models/Eloquent/BranchEntryTest.php
@@ -15,18 +15,19 @@ class BranchEntryTest extends TestCase
 {
     use DatabaseTransactions;
 
-    protected $faker;
-    protected $user;
-    protected $project;
-    protected $entry;
-    protected $formRef;
-    protected $branchInputRef;
-    protected $superadmin;
+    protected User $user;
+    protected Project $project;
+    protected Entry $entry;
+    protected string $formRef;
+    protected string $branchInputRef;
+    protected User $superadmin;
+    protected BranchEntry $branchEntryModel;
 
     public function setUp(): void
     {
         parent::setUp();
 
+        $this->branchEntryModel = new BranchEntry();
         $user = factory(User::class)->create();
         $superadmin = User::where('email', config('epicollect.setup.super_admin_user.email'))->first();
         $project = factory(Project::class)->create([
@@ -52,7 +53,7 @@ public function setUp(): void
             'title' => 'Ciao'
         ]);
 
-        $this->assertEquals(1, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(1, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $project->id,
             [
                 'form_ref' => $formRef,
@@ -61,7 +62,7 @@ public function setUp(): void
             []
         )->count());
 
-        $this->assertEquals(1, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(1, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $project->id,
             [
                 'form_ref' => $formRef,
@@ -71,7 +72,7 @@ public function setUp(): void
             []
         )->count());
 
-        $this->assertEquals(0, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(0, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $project->id,
             [
                 'form_ref' => $formRef,
@@ -110,7 +111,7 @@ public function test_should_get_branch_entries_by_form_and_branch_input_ref()
             ]);
         }
 
-        $this->assertEquals($numOfEntries, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals($numOfEntries, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -137,7 +138,7 @@ public function test_should_get_branch_entries_by_form_and_title()
             ]);
         }
 
-        $this->assertEquals(1, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(1, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -147,7 +148,7 @@ public function test_should_get_branch_entries_by_form_and_title()
             []
         )->count());
 
-        $this->assertEquals($numOfEntries, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals($numOfEntries, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -157,7 +158,7 @@ public function test_should_get_branch_entries_by_form_and_title()
             []
         )->count());
 
-        $this->assertEquals(0, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(0, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -188,7 +189,7 @@ public function test_should_get_branch_entries_by_user()
             ]);
         }
 
-        $this->assertEquals(0, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(0, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -198,7 +199,7 @@ public function test_should_get_branch_entries_by_user()
             []
         )->count());
 
-        $this->assertEquals(0, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(0, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -208,7 +209,7 @@ public function test_should_get_branch_entries_by_user()
             []
         )->count());
 
-        $this->assertEquals($numOfEntries, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals($numOfEntries, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -241,7 +242,7 @@ public function test_should_filter_entries_by_created_at()
             $year++;
         }
 
-        $this->assertEquals($numOfEntries, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals($numOfEntries, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -254,7 +255,7 @@ public function test_should_filter_entries_by_created_at()
         //filter by year (one by one)
         $year = 2016;
         for ($i = 0; $i < $numOfEntries; $i++) {
-            $this->assertEquals(1, BranchEntry::getBranchEntriesByBranchRef(
+            $this->assertEquals(1, $this->branchEntryModel->getBranchEntriesByBranchRef(
                 $this->project->id,
                 [
                     'form_ref' => $this->formRef,
@@ -271,7 +272,7 @@ public function test_should_filter_entries_by_created_at()
         //filter by year (between)
         $yearFrom = 2020;
         $yearTo = 2022;
-        $this->assertEquals(3, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(3, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -285,7 +286,7 @@ public function test_should_filter_entries_by_created_at()
 
         //remove entries so far
         BranchEntry::where('project_id', $this->project->id)->delete();
-        $this->assertEquals(0, BranchEntry::getBranchEntriesByBranchRef(
+        $this->assertEquals(0, $this->branchEntryModel->getBranchEntriesByBranchRef(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -314,7 +315,7 @@ public function test_should_filter_entries_by_created_at()
 
         //assert one entry per month
         for ($month = 1; $month <= 12; $month++) {
-            $this->assertEquals(1, BranchEntry::getBranchEntriesByBranchRef(
+            $this->assertEquals(1, $this->branchEntryModel->getBranchEntriesByBranchRef(
                 $this->project->id,
                 [
                     'form_ref' => $this->formRef,
@@ -348,7 +349,7 @@ public function test_should_sort_entries_by_natural_sorting_numeric()
         }
 
         // Fetch entries with natural sorting applied ASC
-        $entries = BranchEntry::getBranchEntriesByBranchRef($this->project->id, [
+        $entries = $this->branchEntryModel->getBranchEntriesByBranchRef($this->project->id, [
             'form_ref' => $this->formRef,
             'branch_ref' => $this->branchInputRef,
             'sort_by' => 'title',
@@ -361,7 +362,7 @@ public function test_should_sort_entries_by_natural_sorting_numeric()
         }
 
         // Fetch entries with natural sorting applied ASC
-        $entries = BranchEntry::getBranchEntriesByBranchRef($this->project->id, [
+        $entries = $this->branchEntryModel->getBranchEntriesByBranchRef($this->project->id, [
             'form_ref' => $this->formRef,
             'branch_ref' => $this->branchInputRef,
             'sort_by' => 'title',
@@ -374,7 +375,7 @@ public function test_should_sort_entries_by_natural_sorting_numeric()
         }
 
         // Fetch entries with natural sorting applied ASC
-        $entries = BranchEntry::getBranchEntriesByBranchRef($this->project->id, [
+        $entries = $this->branchEntryModel->getBranchEntriesByBranchRef($this->project->id, [
             'form_ref' => $this->formRef,
             'branch_ref' => $this->branchInputRef,
             'sort_by' => 'title',
@@ -409,7 +410,7 @@ public function test_should_sort_entries_by_natural_sorting_accented_chars()
         }
 
         // Fetch entries with natural sorting applied
-        $entries = BranchEntry::getBranchEntriesByBranchRef($this->project->id, [
+        $entries = $this->branchEntryModel->getBranchEntriesByBranchRef($this->project->id, [
             'form_ref' => $this->formRef,
             'branch_ref' => $this->branchInputRef,
             'sort_by' => 'title',
@@ -429,7 +430,7 @@ public function test_should_sort_entries_by_natural_sorting_accented_chars()
         }
 
         // Fetch entries with natural sorting applied
-        $entries = BranchEntry::getBranchEntriesByBranchRef($this->project->id, [
+        $entries = $this->branchEntryModel->getBranchEntriesByBranchRef($this->project->id, [
             'form_ref' => $this->formRef,
             'branch_ref' => $this->branchInputRef,
             'sort_by' => 'title',
@@ -441,4 +442,4 @@ public function test_should_sort_entries_by_natural_sorting_accented_chars()
             $this->assertEquals(array_reverse($expectedOrder)[$key], $entry->title);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/Models/Eloquent/EntryTest.php b/tests/Models/Eloquent/EntryTest.php
index fdb8551b..e9743349 100644
--- a/tests/Models/Eloquent/EntryTest.php
+++ b/tests/Models/Eloquent/EntryTest.php
@@ -14,16 +14,17 @@ class EntryTest extends TestCase
 {
     use DatabaseTransactions;
 
-    protected $faker;
-    protected $user;
-    protected $project;
-    protected $formRef;
-    protected $superadmin;
+    protected User $user;
+    protected Project $project;
+    protected string $formRef;
+    protected User $superadmin;
+    protected Entry $entryModel;
 
-    public function setUp():void
+    public function setUp(): void
     {
         parent::setUp();
 
+        $this->entryModel = new Entry();
         $user = factory(User::class)->create();
         $superadmin = User::where('email', config('epicollect.setup.super_admin_user.email'))->first();
         $project = factory(Project::class)->create([
@@ -38,13 +39,13 @@ public function setUp():void
             'title' => 'Ciao'
         ]);
 
-        $this->assertEquals(1, Entry::getEntriesByForm(
+        $this->assertEquals(1, $this->entryModel->getEntriesByForm(
             $project->id,
             ['form_ref' => $formRef],
             []
         )->count());
 
-        $this->assertEquals(1, Entry::getEntriesByForm(
+        $this->assertEquals(1, $this->entryModel->getEntriesByForm(
             $project->id,
             [
                 'form_ref' => $formRef,
@@ -53,7 +54,7 @@ public function setUp():void
             []
         )->count());
 
-        $this->assertEquals(0, Entry::getEntriesByForm(
+        $this->assertEquals(0, $this->entryModel->getEntriesByForm(
             $project->id,
             [
                 'form_ref' => $formRef,
@@ -85,7 +86,7 @@ public function test_should_get_entries_by_form()
             ]);
         }
 
-        $this->assertEquals($numOfEntries, Entry::getEntriesByForm(
+        $this->assertEquals($numOfEntries, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef
@@ -108,7 +109,7 @@ public function test_should_get_entries_by_form_and_title()
             ]);
         }
 
-        $this->assertEquals(1, Entry::getEntriesByForm(
+        $this->assertEquals(1, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -117,7 +118,7 @@ public function test_should_get_entries_by_form_and_title()
             []
         )->count());
 
-        $this->assertEquals($numOfEntries, Entry::getEntriesByForm(
+        $this->assertEquals($numOfEntries, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -126,7 +127,7 @@ public function test_should_get_entries_by_form_and_title()
             []
         )->count());
 
-        $this->assertEquals(0, Entry::getEntriesByForm(
+        $this->assertEquals(0, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -153,7 +154,7 @@ public function test_should_get_entries_by_user()
             ]);
         }
 
-        $this->assertEquals(0, Entry::getEntriesByForm(
+        $this->assertEquals(0, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -162,7 +163,7 @@ public function test_should_get_entries_by_user()
             []
         )->count());
 
-        $this->assertEquals(0, Entry::getEntriesByForm(
+        $this->assertEquals(0, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -171,7 +172,7 @@ public function test_should_get_entries_by_user()
             []
         )->count());
 
-        $this->assertEquals($numOfEntries, Entry::getEntriesByForm(
+        $this->assertEquals($numOfEntries, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -200,7 +201,7 @@ public function test_should_filter_entries_by_created_at()
             $year++;
         }
 
-        $this->assertEquals($numOfEntries, Entry::getEntriesByForm(
+        $this->assertEquals($numOfEntries, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -212,7 +213,7 @@ public function test_should_filter_entries_by_created_at()
         //filter by year (one by one)
         $year = 2016;
         for ($i = 0; $i < $numOfEntries; $i++) {
-            $this->assertEquals(1, Entry::getEntriesByForm(
+            $this->assertEquals(1, $this->entryModel->getEntriesByForm(
                 $this->project->id,
                 [
                     'form_ref' => $this->formRef,
@@ -228,7 +229,7 @@ public function test_should_filter_entries_by_created_at()
         //filter by year (between)
         $yearFrom = 2020;
         $yearTo = 2022;
-        $this->assertEquals(3, Entry::getEntriesByForm(
+        $this->assertEquals(3, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -241,7 +242,7 @@ public function test_should_filter_entries_by_created_at()
 
         //remove entries so far
         Entry::where('project_id', $this->project->id)->delete();
-        $this->assertEquals(0, Entry::getEntriesByForm(
+        $this->assertEquals(0, $this->entryModel->getEntriesByForm(
             $this->project->id,
             [
                 'form_ref' => $this->formRef,
@@ -266,7 +267,7 @@ public function test_should_filter_entries_by_created_at()
 
         //assert one entry per month
         for ($month = 1; $month <= 12; $month++) {
-            $this->assertEquals(1, Entry::getEntriesByForm(
+            $this->assertEquals(1, $this->entryModel->getEntriesByForm(
                 $this->project->id,
                 [
                     'form_ref' => $this->formRef,
@@ -296,7 +297,7 @@ public function test_should_sort_entries_by_natural_sorting_numeric()
         }
 
         // Fetch entries with natural sorting applied ASC
-        $entries = Entry::getEntriesByForm($this->project->id, [
+        $entries = $this->entryModel->getEntriesByForm($this->project->id, [
             'form_ref' => $this->formRef,
             'sort_by' => 'title',
             'sort_order' => 'ASC'
@@ -308,7 +309,7 @@ public function test_should_sort_entries_by_natural_sorting_numeric()
         }
 
         // Fetch entries with natural sorting applied ASC
-        $entries = Entry::getEntriesByForm($this->project->id, [
+        $entries = $this->entryModel->getEntriesByForm($this->project->id, [
             'form_ref' => $this->formRef,
             'sort_by' => 'title',
             //   'sort_order' => 'DESC' //default
@@ -320,7 +321,7 @@ public function test_should_sort_entries_by_natural_sorting_numeric()
         }
 
         // Fetch entries with natural sorting applied ASC
-        $entries = Entry::getEntriesByForm($this->project->id, [
+        $entries = $this->entryModel->getEntriesByForm($this->project->id, [
             'form_ref' => $this->formRef,
             'sort_by' => 'title',
             'sort_order' => 'DESC' //default
@@ -346,7 +347,7 @@ public function test_should_sort_entries_by_natural_sorting_accented_chars()
         }
 
         // Fetch entries with natural sorting applied
-        $entries = Entry::getEntriesByForm($this->project->id, [
+        $entries = $this->entryModel->getEntriesByForm($this->project->id, [
             'form_ref' => $this->formRef,
             'sort_by' => 'title',
             'sort_order' => 'ASC'
@@ -365,7 +366,7 @@ public function test_should_sort_entries_by_natural_sorting_accented_chars()
         }
 
         // Fetch entries with natural sorting applied
-        $entries = Entry::getEntriesByForm($this->project->id, [
+        $entries = $this->entryModel->getEntriesByForm($this->project->id, [
             'form_ref' => $this->formRef,
             'sort_by' => 'title',
             'sort_order' => 'DESC'
@@ -377,7 +378,7 @@ public function test_should_sort_entries_by_natural_sorting_accented_chars()
         }
 
         // Fetch entries with natural sorting applied
-        $entries = Entry::getEntriesByForm($this->project->id, [
+        $entries = $this->entryModel->getEntriesByForm($this->project->id, [
             'form_ref' => $this->formRef,
             'sort_by' => 'title'
         ])->get();
@@ -388,4 +389,4 @@ public function test_should_sort_entries_by_natural_sorting_accented_chars()
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index b0d75066..4559d290 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -43,6 +43,18 @@ public function createApplication(): Application
         return $app;
     }
 
+    /**
+     * Logs detailed error information for a failing test.
+     *
+     * This method outputs the exception message along with any available expected and actual values
+     * obtained from a comparison failure. It further processes the provided response to display
+     * additional context—such as a JSON representation of response details—if applicable. Finally,
+     * it marks the test as failed with a formatted error message that includes the relative file path.
+     *
+     * @param Exception $e The exception encountered during the test.
+     * @param mixed     $response Additional context for the error, which may include a test response
+     *                            object with diagnostic details.
+     */
     public function logTestError(Exception $e, $response): void
     {
         $expected = '';
@@ -56,7 +68,7 @@ public function logTestError(Exception $e, $response): void
             $actual = print_r($e->getComparisonFailure()->getActual(), true) . PHP_EOL;
         }
 
-        echo 'Expected: ', $expected ?? PHP_EOL;
+        echo 'Expected: ', $expected;
         echo 'Actual: ' . $actual ?? PHP_EOL;
 
         // Ensure $response is an array or a Countable object before using sizeof()
@@ -84,14 +96,25 @@ public function logTestError(Exception $e, $response): void
 
         // Mark the test as failed with expected and actual values
         $filePath = str_replace(base_path(), '', $e->getFile());
-        $stackTrace = $e->getTraceAsString();
-        $stackTraceLines = explode("\n", $stackTrace ?? '');
 
         // Log error for failed assertion
-        $this->fail("Error in {$filePath}:\n\n{$e->getMessage()}");
+        $this->fail("Error in $filePath:\n\n{$e->getMessage()}");
     }
 
-    //clear database manually as we are not using database transactions
+    /**
+     * Clears test-related database records manually.
+     *
+     * This method deletes entries from various tables to remove test artifacts. It targets users whose emails match
+     * specific patterns, as well as records related to a provided user, project, or client ID. The deletions include
+     * test users, associated user providers, OAuth clients, projects, project roles, project structures, stats, entries,
+     * branch entries, and OAuth tokens.
+     *
+     * @param array $params Associative array that may include:
+     *   - 'user': (optional) A user object whose records in users, user providers, and OAuth clients will be deleted.
+     *   - 'project': (optional) A project object whose records in projects, project roles, project structures, stats,
+     *                entries, branch entries, and OAuth client projects will be deleted.
+     *   - 'client_id': (optional) A client ID used to delete corresponding OAuth access tokens.
+     */
     public function clearDatabase($params): void
     {
         $user = $params['user'] ?? null;


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 91ef47fca1d7eb1af74580c5875a21684556a898 and b3e817a2b2995e9c3ded1dd77a3c86f5e302a3f9.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `app/Models/Entries/Entry.php` (0 hunks)
* `app/Traits/Eloquent/Entries.php` (1 hunks)
* `tests/Http/Controllers/Api/Entries/View/Internal/ViewEntriesLocationsControllerTest.php` (14 hunks)
* `tests/TestCase.php` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* app/Models/Entries/Entry.php

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>app/Traits/Eloquent/Entries.php (1)</summary>

`14-39`: **Implementation of getGeoJsonData looks good**

This method successfully implements the functionality to retrieve location data for entries or branch entries. The implementation follows good practices with proper parameter binding, clear comments, and effective reuse of existing functionality through the `sortAndFilterEntries` method.

</details>
<details>
<summary>tests/Http/Controllers/Api/Entries/View/Internal/ViewEntriesLocationsControllerTest.php (2)</summary>

`8-16`: **Good import organization and import simplification**

The imports are well organized, with the addition of the BranchEntry class needed for the new test and changing the fully qualified `\Throwable` to the imported `Throwable`.

---

`410-522`: **Branch entries location endpoint test is well implemented**

The new test method thoroughly validates the functionality of retrieving location data for branch entries. It follows a consistent pattern with other tests, properly creates test data, validates the data in the database, and thoroughly checks the API response.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-01T20:35:57Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b3e817a2b2995e9c3ded1dd77a3c86f5e302a3f9 and b29ff8be3cc0315eea0b386bdf07d3e232541ad1.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `app/Traits/Eloquent/Entries.php` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>app/Traits/Eloquent/Entries.php (3)</summary>

`50-53`: **Consider handling potential null or corrupt geo_json_data.**

The current implementation assumes that the geo_json_data column always contains well-formed JSON data for the requested input reference. 



Consider adding a filter to exclude entries with null or corrupted geo_json_data:

```diff
        $q = DB::table($this->table)
            ->whereRaw($whereSql, [$projectId])
+           ->whereRaw('JSON_EXTRACT(geo_json_data, ?) IS NOT NULL', ['$."' . $params['input_ref'] . '"'])
            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
```

---

`59-61`: **Well-implemented user filtering for COLLECTOR ROLE.**

The conditional filtering by user_id is well-documented and implemented correctly. The comment explicitly notes this applies only to COLLECTOR ROLE which is helpful context.

---

`25-49`: **Documentation includes good examples of the expected data structure.**

The detailed documentation with an example of the expected JSON structure is excellent. It helps developers understand the data format being manipulated.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:22:24Z
user: login: mirko77
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:26:13Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:26:45Z
user: login: mirko77
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:27:13Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:27:40Z
user: login: mirko77
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:37:39Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:38:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>tests/Models/Eloquent/BranchEntryTest.php (1)</summary><blockquote>

`364-370`: _🧹 Nitpick (assertive)_

**Inconsistent comment for the sort order.**

The comment on line 369 mentions that DESC is the default sort order, but the code doesn't specify any sort order in this test case, suggesting it might be using ASC (or whatever the default in the implementation is). Consider updating the comment or explicitly setting the sort order for clarity.

```diff
- //   'sort_order' => 'DESC' //default
+ //   'sort_order' => 'ASC' //explicitly using ASC
```

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>app/Traits/Eloquent/Entries.php (1)</summary><blockquote>

`28-28`: _🧹 Nitpick (assertive)_

**Define more specific parameter types.**

The method signature could be improved by specifying more precise types for the parameters.



```diff
-    public function getGeoJsonData($projectId, $params): Builder
+    public function getGeoJsonData(string $projectId, array $params): Builder
```

</blockquote></details>
<details>
<summary>tests/TestCase.php (1)</summary><blockquote>

`71-71`: _🧹 Nitpick (assertive)_

**Be consistent with string concatenation style**

Line 71 now uses a comma for string concatenation while line 72 still uses the concatenation operator (`.`). Consider using the same style throughout for better code consistency.



```diff
-echo 'Expected: ', $expected;
+echo 'Expected: ' . $expected;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b29ff8be3cc0315eea0b386bdf07d3e232541ad1 and 4664eb80f23754adb89b06c22fd821829cf35816.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `app/Http/Controllers/Api/Entries/DownloadSubsetController.php` (2 hunks)
* `app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php` (2 hunks)
* `app/Models/Entries/BranchEntry.php` (2 hunks)
* `app/Models/Entries/Entry.php` (5 hunks)
* `app/Services/Entries/EntriesDownloadService.php` (3 hunks)
* `app/Traits/Eloquent/Entries.php` (2 hunks)
* `tests/Models/Eloquent/BranchEntryTest.php` (22 hunks)
* `tests/Models/Eloquent/EntryTest.php` (22 hunks)
* `tests/TestCase.php` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>app/Traits/Eloquent/Entries.php (1)</summary>

```
Learnt from: mirko77
PR: epicollect5/epicollect5-server#8
File: app/Traits/Eloquent/Entries.php:14-64
Timestamp: 2025-03-03T11:27:39.248Z
Learning: In the Epicollect5 codebase, when the `input_ref` parameter is missing in the `getGeoJsonData` method of the Entries trait, the SQL query will return an empty dataset rather than failing with an error.
```

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (31)</summary><blockquote>

<details>
<summary>app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php (2)</summary>

`57-57`: **Method call converted from static to instance method.**

The change from a static call to an instance method call is correct and aligns with the updated method signature in the Entry model. This change ensures proper object-oriented design by working with instances rather than static methods.

---

`93-93`: **Method call converted from static to instance method.**

The change from a static call to an instance method call is correct and aligns with the updated method signature in the BranchEntry model. This change is consistent with the similar update made in the runQueryHierarchy method.

</details>
<details>
<summary>app/Traits/Eloquent/Entries.php (3)</summary>

`14-72`: **Well-implemented GeoJSON data retrieval method.**

This implementation properly moves the GeoJSON data retrieval functionality to the Entries trait, making it available for both Entry and BranchEntry models. The method is thoroughly documented with clear explanations of parameters and examples of the data structure.


As I learned from your previous feedback, if the `input_ref` parameter is missing, the SQL query will return an empty dataset rather than failing with an error, which is the expected behavior.

---

`71-71`: **Method call updated to use instance method.**

The call to `sortAndFilterEntries` has been correctly updated to use `$this->` instead of `self::`, which resolves potential issues with PHP's late static binding in traits.

---

`96-96`: **Method visibility and static modifier updated.**

The `sortAndFilterEntries` method has been changed from static to an instance method, which aligns with PHP 8.1+ best practices for traits and improves object-oriented design.

</details>
<details>
<summary>app/Services/Entries/EntriesDownloadService.php (2)</summary>

`59-59`: **Static method call converted to instance method call.**

The change from a static call to an instance method call is correct and aligns with the updated method signature in the Entry model. The instantiation pattern using `(new Entry())` is consistent with other similar changes in the codebase.

---

`91-95`: **Static method call converted to instance method call.**

The change from a static call to an instance method call is correct and aligns with the updated method signature in the BranchEntry model. The instantiation pattern using `(new BranchEntry())` is consistent with other similar changes in the codebase.

</details>
<details>
<summary>app/Http/Controllers/Api/Entries/DownloadSubsetController.php (2)</summary>

`85-89`: **Static method call converted to instance method call.**

The change from a static call to an instance method call is correct and aligns with the updated method signature in the BranchEntry model. The instantiation pattern using `(new BranchEntry())` is consistent with similar changes in other files.

---

`94-94`: **Static method call converted to instance method call.**

The change from a static call to an instance method call is correct and aligns with the updated method signature in the Entry model. The instantiation pattern using `(new Entry())` is consistent with similar changes in other files.

</details>
<details>
<summary>app/Models/Entries/Entry.php (4)</summary>

`51-68`: **Great docblock addition!**

The detailed PHPDoc for the `getEntry` method significantly improves code clarity by thoroughly documenting parameters, return type, and function behavior.

---

`87-87`: **Method call updated correctly from static to instance**

The method call has been properly updated to use instance method syntax to align with the changes in the Entries trait.

---

`173-173`: **Appropriate method signature change from static to instance**

Good change - converting the static method to an instance method improves the object-oriented design and makes the code more testable.

---

`186-186`: **Method call correctly updated from static to instance**

The method call has been properly updated to use instance method syntax.

</details>
<details>
<summary>app/Models/Entries/BranchEntry.php (3)</summary>

`74-74`: **Method call updated correctly from static to instance**

The call to `sortAndFilterEntries` has been updated appropriately to use instance method syntax.

---

`77-77`: **Good method signature change from static to instance**

Converting the static method to an instance method improves the code's object-oriented design and consistency with the rest of the codebase.

---

`91-91`: **Method call correctly updated from static to instance**

The method call has been properly updated to use instance method syntax.

</details>
<details>
<summary>tests/TestCase.php (3)</summary>

`46-57`: **Excellent documentation addition**

The comprehensive PHPDoc for the `logTestError` method clearly explains its purpose, behavior, and parameters.

---

`101-101`: **String formatting change looks good**

The change to using double quotes for variable interpolation is a good practice and makes the code more readable.

---

`104-117`: **Comprehensive docblock addition**

Great work on adding a detailed PHPDoc for the `clearDatabase` method, explaining its purpose and parameters thoroughly.

</details>
<details>
<summary>tests/Models/Eloquent/EntryTest.php (8)</summary>

`17-21`: **Type hints improve code clarity**

Adding PHP type hints to class properties enhances code readability and helps catch type-related errors early.

---

`23-27`: **Good setup with instance initialization**

The setup method now properly initializes the Entry model instance, which is necessary for calling instance methods.

---

`42-46`: **Properly updated method calls from static to instance**

All the static method calls have been correctly updated to use the instance method via `$this->entryModel->getEntriesByForm()`.



Also applies to: 48-55, 57-64

---

`89-95`: **Test assertions correctly updated to use instance methods**

The test assertions have been properly updated to call instance methods on the Entry model.



Also applies to: 112-119, 121-129, 130-137

---

`157-164`: **User filtering tests updated correctly**

The tests for user filtering now properly use instance methods on the Entry model.



Also applies to: 166-173, 175-182

---

`204-211`: **Date filtering tests updated appropriately**

All date filtering tests have been correctly updated to use instance methods instead of static methods.



Also applies to: 216-226, 232-241, 245-251

---

`270-279`: **Sorting tests properly updated**

The sorting tests now correctly use instance methods via `$this->entryModel`.



Also applies to: 300-304, 312-316, 324-328

---

`350-354`: **Accent sorting tests updated correctly**

The tests for sorting entries with accented characters have been properly updated to use instance methods.



Also applies to: 369-373, 381-384

</details>
<details>
<summary>tests/Models/Eloquent/BranchEntryTest.php (4)</summary>

`18-24`: **Good type declaration additions for properties.**

Adding explicit type declarations enhances code safety by enabling static type checking, improving IDE autocompletion, and making the code more self-documenting.

---

`24-24`: **Appropriate addition of the BranchEntry model instance property.**

This property addition aligns with the refactoring from static to instance method usage, which is a good object-oriented practice.

---

`30-30`: **Good initialization of the BranchEntry model in setUp.**

Initializing the model in the setUp method ensures it's available for all test methods, supporting the move from static to instance methods.

---

`56-63`: **Consistent refactoring from static to instance method calls.**

The change from static `BranchEntry::getBranchEntriesByBranchRef` to instance `$this->branchEntryModel->getBranchEntriesByBranchRef` calls is applied consistently throughout the test file. This improves code maintainability and follows better OOP practices by avoiding static method dependencies.



Also applies to: 65-73, 75-83, 114-121, 141-149, 151-159, 161-169, 192-200, 202-210, 212-220, 245-253, 258-268, 275-285, 289-296, 318-328, 352-357, 365-370, 378-383, 413-418, 433-438

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T15:55:51Z

Review comments:
diff_hunk: @@ -56,7 +56,7 @@ public function logTestError(Exception $e, $response): void
             $actual = print_r($e->getComparisonFailure()->getActual(), true) . PHP_EOL;
         }
 
-        echo 'Expected: ', $expected ?? PHP_EOL;
+        echo 'Expected: ', $expected;
path: tests/TestCase.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Be consistent with string concatenation style**

Line 59 now uses a comma for string concatenation while line 60 still uses the concatenation operator (`.`). Consider using the same style throughout for better code consistency.


```diff
-echo 'Expected: ', $expected;
+echo 'Expected: ' . $expected;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
echo 'Expected: ' . $expected;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T20:35:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,11 +99,14 @@ public function test_location_endpoint_input_ref_missing()
                     ]
                 ]
             ]);
-        } catch (\Throwable $e) {
+        } catch (Throwable $e) {
             $this->logTestError($e, $response);
path: tests/Http/Controllers/Api/Entries/View/Internal/ViewEntriesLocationsControllerTest.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider standardizing exception handling across test methods**

There's inconsistency in exception types being caught. Some methods catch `Throwable` while others catch `Exception`. Since all methods are now documented with `@throws Throwable`, consider standardizing to catch `Throwable` consistently across the entire class.


```diff
-} catch (Exception $e) {
+} catch (Throwable $e) {
```


Also applies to: 154-156, 207-209, 270-272, 333-335, 405-407, 520-522, 599-601, 678-680, 757-759, 836-838, 953-955

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-01T20:35:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
path: app/Traits/Eloquent/Entries.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Define more specific parameter types.**

The method signature could be improved by specifying more precise types for the parameters.



```diff
-    public function getGeoJsonData($projectId, $params): Builder
+    public function getGeoJsonData(string $projectId, array $params): Builder
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
     */
    public function getGeoJsonData(string $projectId, array $params): Builder
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T11:22:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return self::sortAndFilterEntries($q, $params);
path: app/Traits/Eloquent/Entries.php
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify the trait context in self::sortAndFilterEntries call.**

The use of `self::` to call a static method from within a trait might cause issues if the trait is used in classes where this method isn't available.

---

🏁 Script executed:

```shell
#!/bin/bash

# Check if sortAndFilterEntries is defined as static in all classes using this trait
echo "Looking for classes that use the Entries trait..."
classes=$(fd -t f -e php | xargs grep -l "use.*\\\\Traits\\\\Eloquent\\\\Entries" | grep -v "Entries.php")

echo "Found classes: $classes"

for class in $classes; do
  echo "Checking if class $class implements sortAndFilterEntries..."
  # Check if the method is implemented in the class
  if grep -q "function sortAndFilterEntries" $class; then
    echo "Method found in $class"
  else
    echo "Method NOT found in $class! This could cause issues with the self:: call."
  fi
done
```

Length of output: 2812

---



</details>

**Action Required: Ensure `sortAndFilterEntries` is Defined or Called Correctly**

The verification shows that none of the classes using the `Entries` trait (listed below) implement the `sortAndFilterEntries` method. This confirms that calling `self::sortAndFilterEntries(...)` in the trait may lead to runtime issues.

- **Affected Files:**
  - `app/Models/Entries/BranchEntry.php`
  - `app/Models/Entries/Entry.php`
  - `app/Services/Entries/CreateEntryService.php`
  - `app/Services/Entries/EntriesUniquenessService.php`

**Recommendations:**
- **Option 1:** Define a default static implementation of `sortAndFilterEntries` within the trait.
- **Option 2:** Change the call from `self::sortAndFilterEntries(...)` to `static::sortAndFilterEntries(...)` if you expect each concrete class to override it. In that case, ensure that the method is implemented in all target classes.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-03T11:22:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return self::sortAndFilterEntries($q, $params);
+    }
path: app/Traits/Eloquent/Entries.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Well-structured implementation of getGeoJsonData method.**

This implementation properly moves the GeoJSON data retrieval functionality from the Entry model to the Entries trait, which is a good architectural decision for better code organization. The docstring and implementation are clear, with good parameter handling and SQL construction.




Consider adding validation for the required `input_ref` parameter. If this parameter is missing, the SQL query would fail. Add a check like:

```diff
    public function getGeoJsonData($projectId, $params): Builder
    {
+       if (empty($params['input_ref'])) {
+           throw new \InvalidArgumentException('Input reference is required');
+       }
        
        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
        $whereSql = 'project_id = ?';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    /** Get GeoJSON data for entries or branch entries
     *
     * @param $projectId
     * @param $params
     * @return Builder
     */
    public function getGeoJsonData($projectId, $params): Builder
    {
        if (empty($params['input_ref'])) {
            throw new \InvalidArgumentException('Input reference is required');
        }
        
        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
        $whereSql = 'project_id = ?';

        /**
         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
         *
         * Example of data:
         *
         * {
         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
         *     "type": "Feature",
         *     "geometry": {
         *       "type": "Point",
         *       "coordinates": [10.565543, 45.458595]
         *     },
         *     "properties": {
         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
         *       "accuracy": 4,
         *       "created_at": "2025-03-03",
         *       "possible_answers": []
         *     }
         *   },
         *   {...}
         * }
         *
         */
        $q = DB::table($this->table)
            ->whereRaw($whereSql, [$projectId])
            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);

        /**
         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
         *
         * @see EntriesViewService::getSanitizedQueryParams
         */
        if (!empty($params['user_id'])) {
            $q->where('user_id', '=', $params['user_id']);
        }

        return self::sortAndFilterEntries($q, $params);
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-03T11:22:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return self::sortAndFilterEntries($q, $params);
path: app/Traits/Eloquent/Entries.php
user: login: mirko77
body: @coderabbitai elaborate option 1
created_at: 2025-03-03T11:26:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return self::sortAndFilterEntries($q, $params);
path: app/Traits/Eloquent/Entries.php
user: login: coderabbitai[bot]
body: @mirko77 Let me elaborate on Option 1 in detail.

## Option 1: Define a default static implementation of `sortAndFilterEntries` within the trait

Looking at your code, I notice that `sortAndFilterEntries` is already defined as a static method in the `Entries` trait:

```php
public static function sortAndFilterEntries(Builder $q, $filters): Builder
{
    // Implementation details...
}
```

However, there's an issue with how it's being called in the new `getGeoJsonData` method:

```php
return self::sortAndFilterEntries($q, $params);
```

### The Problem

When using `self::` to call a static method from within a trait, PHP looks for that method in the class that's using the trait, not in the trait itself. Since none of the classes using this trait implement `sortAndFilterEntries` (as shown by the verification script), this call could lead to runtime errors.

### The Solution

Change the call from:
```php
return self::sortAndFilterEntries($q, $params);
```

To:
```php
return static::sortAndFilterEntries($q, $params);
```

Using `static::` instead of `self::` will ensure proper late static binding, making PHP look for the method in the current class context (which includes the trait). This is the recommended approach for calling static methods within traits.

This change maintains the static nature of the method while ensuring it will be properly resolved at runtime, regardless of whether the implementing classes override the method or not.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-03T11:26:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return self::sortAndFilterEntries($q, $params);
+    }
path: app/Traits/Eloquent/Entries.php
user: login: mirko77
body: @coderabbitai the query would not fail, it would return an empty dataset.
created_at: 2025-03-03T11:27:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return self::sortAndFilterEntries($q, $params);
+    }
path: app/Traits/Eloquent/Entries.php
user: login: coderabbitai[bot]
body: @mirko77 Thank you for the correction. You're right, and I apologize for my misunderstanding. If the `input_ref` parameter is missing, the SQL query would not fail but instead return an empty dataset as you mentioned.

The JSON_EXTRACT function in MySQL/MariaDB would simply return NULL when trying to extract from a non-existent path, which would result in an empty dataset rather than a SQL error.

Thanks for clarifying how this works in your codebase. I'll keep this behavior in mind for future reviews.



ʕ•ᴥ•ʔ

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: mirko77
PR: epicollect5/epicollect5-server#8
File: app/Traits/Eloquent/Entries.php:14-64
Timestamp: 2025-03-03T11:27:39.049Z
Learning: In the Epicollect5 codebase, when the `input_ref` parameter is missing in the `getGeoJsonData` method of the Entries trait, the SQL query will return an empty dataset rather than failing with an error.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-03T11:27:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return self::sortAndFilterEntries($q, $params);
path: app/Traits/Eloquent/Entries.php
user: login: mirko77
body: @coderabbitai

As of PHP 8.1.0, calling a static method, or accessing a static property directly on a trait is deprecated. Static methods and properties should only be accessed on a class using the trait.

how do we solve this?
created_at: 2025-03-03T11:37:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,58 @@
 
 trait Entries
 {
+    /** Get GeoJSON data for entries or branch entries
+     *
+     * @param $projectId
+     * @param $params
+     * @return Builder
+     */
+    public function getGeoJsonData($projectId, $params): Builder
+    {
+        $selectSql = 'JSON_EXTRACT(geo_json_data, ?) as geo_json_data ';
+        $whereSql = 'project_id = ?';
+
+        /**
+         * Get the GeoJSON data for a location question by passing the input reference (input_ref) of that question.
+         *
+         * Example of data:
+         *
+         * {
+         *   "913e1f06a69e4718b3625bb3518e4672_67c58cb5c6e96_67c58cc7c0912_67c58cd0c0913": {
+         *     "id": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *     "type": "Feature",
+         *     "geometry": {
+         *       "type": "Point",
+         *       "coordinates": [10.565543, 45.458595]
+         *     },
+         *     "properties": {
+         *       "uuid": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "title": "653c25d0-f81f-11ef-adfb-0b667d454f81",
+         *       "accuracy": 4,
+         *       "created_at": "2025-03-03",
+         *       "possible_answers": []
+         *     }
+         *   },
+         *   {...}
+         * }
+         *
+         */
+        $q = DB::table($this->table)
+            ->whereRaw($whereSql, [$projectId])
+            ->selectRaw($selectSql, ['$."' . $params['input_ref'] . '"']);
+
+        /**
+         * filter by user (imp: applied to COLLECTOR ROLE ONLY)
+         *
+         * @see EntriesViewService::getSanitizedQueryParams
+         */
+        if (!empty($params['user_id'])) {
+            $q->where('user_id', '=', $params['user_id']);
+        }
+
+        return self::sortAndFilterEntries($q, $params);
path: app/Traits/Eloquent/Entries.php
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@mirko77 You've raised an important point about the PHP 8.1 deprecation of calling static methods directly on traits. You're right that in PHP 8.1+, this is considered deprecated practice.

Since we need to address this issue with the `sortAndFilterEntries` static method in the `Entries` trait, here are the best solutions:

### Option 1: Convert to a non-static method
Change the method definition from `public static function` to just `public function` and update the call:

```php
// In the trait
public function sortAndFilterEntries(Builder $q, $filters): Builder
{
    // Implementation stays the same
}

// And in getGeoJsonData, change:
return $this->sortAndFilterEntries($q, $params);
```

### Option 2: Use a helper class for static functionality
Create a dedicated helper class for the static functionality:

```php
<?php

namespace ec5\Helpers;

use Illuminate\Database\Query\Builder;

class EntriesHelper
{
    public static function sortAndFilterEntries(Builder $q, $filters): Builder
    {
        // Copy implementation from trait
        // Filtering
        if (!empty($filters['filter_by'])) {
            // ... rest of the implementation
        }
        
        return $q;
    }
}
```

Then update your trait to use this helper:

```php
use ec5\Helpers\EntriesHelper;

// In getGeoJsonData:
return EntriesHelper::sortAndFilterEntries($q, $params);
```

### Option 3: Always call through a class
If you want to keep the static method in the trait, ensure it's only called through classes that use the trait:

```php
// In the Entry model:
public static function getSortedAndFilteredEntries(Builder $q, $filters): Builder
{
    return static::sortAndFilterEntries($q, $filters);
}

// In the trait:
return Entry::getSortedAndFilteredEntries($q, $params);
```

I recommend Option 1 as it's the simplest approach and aligns with the instance-based nature of the trait. This would resolve the deprecation warning while maintaining the existing functionality.

Would you like me to provide more details about any of these options?

---

🏁 Script executed:
```
#!/bin/bash

# Check how the sortAndFilterEntries method is being used in the codebase
echo "Checking usage of sortAndFilterEntries in the codebase:"
rg "sortAndFilterEntries" --type php -A 3 -B 3

# Get PHP version from composer.json
if [ -f "composer.json" ]; then
  echo -e "\nPHP version from composer.json:"
  cat composer.json | grep -A 5 "require"
fi

# Check what classes use the Entries trait
echo -e "\nClasses using the Entries trait:"
rg "use.*\\\\Traits\\\\Eloquent\\\\Entries" --type php
```

Length of output: 4528

---



</details>

@mirko77 Thanks for bringing up this important point about PHP 8.1+ deprecation. Looking at your codebase, I can see this is indeed a critical issue since you're running PHP ≥8.3 according to your composer.json.

Based on examining how `sortAndFilterEntries` is currently used across your project, I notice there's a mixed pattern of both static (`self::`) and instance (`$this->`) calls to this method in multiple files. Let me propose the best approach to fix this:

## Solution: Create a dedicated helper class

Since the method is used both statically and as an instance method across different files, the cleanest solution is to extract it to a dedicated helper class:

```php
<?php

namespace ec5\Helpers;

use Carbon\Carbon;
use Illuminate\Database\Query\Builder;

class EntriesHelper
{
    public static function sortAndFilterEntries(Builder $q, $filters): Builder
    {
        // Filtering
        if (!empty($filters['filter_by'])) {
            // Filter between
            if (!empty($filters['filter_from']) && !empty($filters['filter_to'])) {
                //create artificial dates using Carbon modifiers
                //to include the full range of entries
                $from = Carbon::parse($filters['filter_from'])->startOfDay();
                $to = Carbon::parse($filters['filter_to'])->endOfDay();
                $q->whereBetween($filters['filter_by'], [$from, $to]);
            } // Filter from
            else {
                if (!empty($filters['filter_from'])) {
                    $q->where($filters['filter_by'], '>=', $filters['filter_from']);
                } // Filter to
                else {
                    if (!empty($filters['filter_to'])) {
                        $q->where($filters['filter_by'], '<=', $filters['filter_to']);
                    }
                }
            }
        }
        //filter by title
        if (!empty($filters['title'])) {
            $q->where('title', 'LIKE', '%' . $filters['title'] . '%');
        }
        // Sorting
        if (!empty($filters['sort_by']) && !empty($filters['sort_order'])) {
            if ($filters['sort_by'] === 'title') {
                //handle the natural sort on alphanumeric titles -> t.ly/tl5X
                $q->orderByRaw('LENGTH(' . $filters['sort_by'] . ') ' . $filters['sort_order'] . ' , ' . $filters['sort_by'] . ' ' . $filters['sort_order']);
            } else {
                $q->orderBy($filters['sort_by'], $filters['sort_order']);
            }
        } else {
            //default sorting, most recent first
            $q->orderBy('created_at', 'DESC');
        }
        return $q;
    }
}
```

Then modify your trait to use this helper:

```php
// In the getGeoJsonData method of the Entries trait
return \ec5\Helpers\EntriesHelper::sortAndFilterEntries($q, $params);
```

You'll also need to update all other places where the method is called:

1. In Entry.php:
```php
// Change from:
return self::sortAndFilterEntries($q, $options);
// To:
return \ec5\Helpers\EntriesHelper::sortAndFilterEntries($q, $options);

// And change from:
return $this->sortAndFilterEntries($q, $options);
// To:
return \ec5\Helpers\EntriesHelper::sortAndFilterEntries($q, $options);
```

2. In BranchEntry.php (same pattern)

This approach offers several advantages:
- Avoids the PHP 8.1+ deprecation warning
- Centralizes the filtering/sorting logic in one place
- Makes it easier to maintain and update the functionality
- Keeps the API consistent across the codebase

Would you like me to suggest an alternative approach, or would you prefer I elaborate on any aspects of this solution?

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-03T11:38:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -230,12 +230,12 @@ function (Collection $entries) use (&$count, $total, $file) {
                             $count++;
                             // Write row to file
                             fwrite($file, $this->dataMappingService->getMappedEntryJSON(
-                                    $entry->entry_data,
-                                    $entry->user_id,
-                                    $entry->title,
-                                    $entry->uploaded_at,
-                                    $entry->branch_counts ?? null
-                                ) . $append);
+                                $entry->entry_data,
+                                $entry->user_id,
+                                $entry->title,
+                                $entry->uploaded_at,
+                                $entry->branch_counts ?? null
+                            ) . $append);
path: app/Services/Entries/EntriesDownloadService.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Minor formatting adjustment for improved readability.**

The indentation of parameters passed to the `getMappedEntryJSON` method has been adjusted for better readability. This is a minor but positive change to improve code clarity.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T15:55:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,18 +15,19 @@ class BranchEntryTest extends TestCase
 {
     use DatabaseTransactions;
 
-    protected $faker;
-    protected $user;
-    protected $project;
-    protected $entry;
-    protected $formRef;
-    protected $branchInputRef;
-    protected $superadmin;
+    protected User $user;
+    protected Project $project;
+    protected Entry $entry;
+    protected string $formRef;
+    protected string $branchInputRef;
+    protected User $superadmin;
+    protected BranchEntry $branchEntryModel;
path: tests/Models/Eloquent/BranchEntryTest.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider adding documentation for class properties.**

Since the PR objectives mention a request for docstring generation, consider adding PHPDoc comments for these class properties to improve code documentation and provide context about their usage.

```diff
+ /**
+  * @var User The user associated with the test
+  */
 protected User $user;
+ /**
+  * @var Project The project used in the test
+  */
 protected Project $project;
+ /**
+  * @var Entry The entry used for branch entry creation
+  */
 protected Entry $entry;
+ /**
+  * @var string The form reference
+  */
 protected string $formRef;
+ /**
+  * @var string The branch input reference
+  */
 protected string $branchInputRef;
+ /**
+  * @var User The superadmin user
+  */
 protected User $superadmin;
+ /**
+  * @var BranchEntry The branch entry model instance
+  */
 protected BranchEntry $branchEntryModel;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    /**
     * @var User The user associated with the test
     */
    protected User $user;
    /**
     * @var Project The project used in the test
     */
    protected Project $project;
    /**
     * @var Entry The entry used for branch entry creation
     */
    protected Entry $entry;
    /**
     * @var string The form reference
     */
    protected string $formRef;
    /**
     * @var string The branch input reference
     */
    protected string $branchInputRef;
    /**
     * @var User The superadmin user
     */
    protected User $superadmin;
    /**
     * @var BranchEntry The branch entry model instance
     */
    protected BranchEntry $branchEntryModel;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T15:55:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-01T20:34:05Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
The changes remove the `getGeoJsonData` method from the `Entry` model and add an equivalent method to the `Entries` trait. This method builds a SQL query to retrieve GeoJSON data with a conditional user filter and applies sorting and filtering. Additionally, several test methods in the location controller test file have been updated with refined error handling and additional test cases for branch entries, while the `TestCase` class’s error logging output formatting has been adjusted.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `app/Models/…/Entry.php` | Removed the `getGeoJsonData` method from the `Entry` model, eliminating the logic for constructing the GeoJSON query. Updated `getEntriesByForm` from static to instance method. |
| `app/Traits/Eloquent/Entries.php` | Added the `getGeoJsonData` method to build and execute a SQL query that retrieves GeoJSON data with role-specific filtering and applies sorting via `sortAndFilterEntries`. Changed `sortAndFilterEntries` from static to public instance method. |
| `tests/Http/Controllers/…/ViewEntriesLocationsControllerTest.php` | Updated several test methods to use `Throwable` for error catching, added new tests (e.g., branch entries), and enhanced assertions for response titles. |
| `tests/TestCase.php` | Modified the `logTestError` method to change output formatting by removing the null coalescing operator and simplifying the failure message output. Updated `clearDatabase` method documentation. |
| `app/Http/Controllers/…/DownloadSubsetController.php` | Changed static method calls for `BranchEntry` and `Entry` to instance method calls. |
| `app/Http/Controllers/…/ViewEntriesControllerBase.php` | Updated static method calls for `Entry` and `BranchEntry` to instance method calls. |
| `app/Models/…/BranchEntry.php` | Changed `getEntry` and `getBranchEntriesByBranchRef` from static to public instance methods. |
| `app/Services/Entries/EntriesDownloadService.php` | Changed static method calls for `Entry` and `BranchEntry` to instance method calls. |
| `tests/Models/Eloquent/…/BranchEntryTest.php` | Introduced a new property for `BranchEntry` instance and refactored method calls to use instance methods. Updated property declarations for type safety. |
| `tests/Models/Eloquent/…/EntryTest.php` | Updated property declarations to typed properties and refactored method calls to use an instance of `Entry`. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Caller
    participant EntriesTrait
    participant Database

    Caller->>EntriesTrait: getGeoJsonData(projectId, params)
    EntriesTrait->>EntriesTrait: Build SQL query (with conditional user filter)
    EntriesTrait->>Database: Execute query for geo_json_data
    Database-->>EntriesTrait: Return query results
    EntriesTrait->>EntriesTrait: sortAndFilterEntries() is called
    EntriesTrait-->>Caller: Return Builder instance
```

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` or `@crai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: mirko77
created_at: 2025-03-01T22:04:56Z
body: @coderabbitai generate docstrings
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-03-01T22:05:01Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Docstrings generation started.

</details>
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-03-01T22:06:21Z
body: > [!NOTE]
> Generated docstrings for this pull request at https://github.com/epicollect5/epicollect5-server/pull/9
author_association: CONTRIBUTOR