Repository: epicollect5/epicollect5-server
Pull request #10: Fixed bugs and refactoring for better semantic
Author: login: mirko77
Created: 2025-03-06T12:06:59Z | Merged: 2025-03-07T10:58:24Z



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

- **New Features**
	- Added support for TXT file exports, allowing error messages and file responses to be delivered in a clear, plain text format.
	- Enhanced the archive download process to ensure all essential data is consistently included in exported files.
	- Introduced a new service provider for handling TXT file responses.

- **Bug Fixes**
	- Improved error logging for better clarity during file export operations.

- **Documentation**
	- Updated documentation comments across various methods to clarify functionality and improve maintainability.
	- Enhanced documentation for methods related to downloading and exporting entries, ensuring users understand the expected parameters and behaviors.

These updates result in more reliable and transparent data exports, providing end-users with a smoother and more predictable download experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/Http/Controllers/Api/Entries/DownloadController.php b/app/Http/Controllers/Api/Entries/DownloadController.php
index f3d2c4cc..913c9274 100644
--- a/app/Http/Controllers/Api/Entries/DownloadController.php
+++ b/app/Http/Controllers/Api/Entries/DownloadController.php
@@ -7,6 +7,7 @@
 use Cookie;
 use ec5\Http\Validation\Entries\Download\RuleDownload;
 use ec5\Libraries\Utilities\Common;
+use ec5\Models\User\User;
 use ec5\Services\Entries\EntriesDownloadService;
 use ec5\Services\Entries\EntriesViewService;
 use ec5\Services\Mapping\DataMappingService;
@@ -63,21 +64,19 @@ public function index(Request $request, RuleDownload $ruleDownload, EntriesViewS
     /**
      * Sends an archive file as a download and deletes it after sending, or returns an error if the file is missing.
      *
-     * This method checks if the file at the specified path exists. If it does, it queues a media cookie based on the
-     * provided timestamp and initiates a download response that deletes the file once sent. If the file cannot be found,
+     * This method checks if the file exists at the specified path. If it does, it queues a download entries cookie based on
+     * the provided timestamp and returns a download response that deletes the file after sending. If the file cannot be found,
      * it returns an error response with a designated error code.
      *
      * @param string $filepath The path to the archive file.
-     * @param string $filename The name that will be used for the downloaded file.
-     * @param mixed|null $timestamp Optional timestamp used for generating a media cookie and error response.
-     *
-     * @return \Illuminate\Http\Response The download response or an error response.
+     * @param string $filename The name to be used for the downloaded file.
+     * @param ?string $timestamp Optional timestamp used for generating the download entries cookie and error response.
      */
-    private function sendArchive($filepath, $filename, $timestamp = null)
+    private function sendArchive(string $filepath, string $filename, ?string $timestamp = null)
     {
         if (file_exists($filepath)) {
-            $mediaCookie = Common::getMediaCookie($timestamp);
-            Cookie::queue($mediaCookie);
+            $downloadEntriesCookie = Common::getDownloadEntriesCookie($timestamp);
+            Cookie::queue($downloadEntriesCookie);
             return response()->download($filepath, $filename)->deleteFileAfterSend(true);
         } else {
             return Common::errorResponseAsFile($timestamp, 'ec5_364');
@@ -96,10 +95,10 @@ private function sendArchive($filepath, $filename, $timestamp = null)
      *
      * @param string $projectDir Directory where the archive is to be stored.
      * @param array $params Parameters for archive creation (e.g., output format).
-     * @param mixed $timestamp A timestamp used for file naming and error response consistency.
+     * @param ?string $timestamp A timestamp used for file naming and error response consistency.
      * @return mixed The response from sending the archive file or an error response.
      */
-    private function createArchive(string $projectDir, array $params, $timestamp)
+    private function createArchive(string $projectDir, array $params, ?string $timestamp)
     {
         $lockKey = 'download-entries-archive-' . $this->requestedUser()->id;
 
@@ -133,10 +132,10 @@ private function createArchive(string $projectDir, array $params, $timestamp)
      * currently requested project, and then adds the user's ID to ensure that archive files are stored
      * in a unique directory per user. This helps prevent conflicts during concurrent downloads.
      *
-     * @param object $user The authenticated user object with an accessible 'id' property.
+     * @param User $user The authenticated user object with an accessible 'id' property.
      * @return string The complete file system path for the user's archive directory.
      */
-    private function getArchivePath($user)
+    private function getArchivePath(User $user)
     {
         // Setup storage
         $storage = Storage::disk('entries_zip');
diff --git a/app/Http/Controllers/Api/Entries/DownloadSubsetController.php b/app/Http/Controllers/Api/Entries/DownloadSubsetController.php
index 1ae5a52b..e38bbe34 100644
--- a/app/Http/Controllers/Api/Entries/DownloadSubsetController.php
+++ b/app/Http/Controllers/Api/Entries/DownloadSubsetController.php
@@ -10,6 +10,7 @@
 use ec5\Services\Entries\EntriesViewService;
 use ec5\Services\Mapping\DataMappingService;
 use ec5\Traits\Requests\RequestAttributes;
+use Illuminate\Database\Query\Builder;
 use Illuminate\Http\Request;
 use League\Csv\Writer;
 use Log;
@@ -31,6 +32,15 @@ public function __construct(DataMappingService $dataMappingService)
         $this->dataMappingService = $dataMappingService;
     }
 
+    /**
+     * Processes a request to download a subset of entries.
+     *
+     * This method validates and sanitizes the incoming query parameters and download rules, including verifying a timestamp
+     * from the download entries cookie. It checks the project mapping and map index, initializes the data mapping service, and
+     * retrieves the appropriate set of entries (either branch entries or form entries) based on provided parameters. It then
+     * generates a ZIP archive containing the subset of entries, queues a download tracking cookie, and returns a response to
+     * stream the ZIP file. In cases of validation or processing errors, an appropriate error response is returned.
+     */
     public function subset(Request $request, RuleDownloadSubset $ruleDownloadSubset, EntriesViewService $entriesViewService)
     {
         // Check the mapping is valid
@@ -100,8 +110,8 @@ public function subset(Request $request, RuleDownloadSubset $ruleDownloadSubset,
             return Response::apiErrorCode(400, $this->errors);
             //todo should I delete any leftovers here?
         }
-        $mediaCookie = Common::getMediaCookie($timestamp);
-        Cookie::queue($mediaCookie);
+        $downloadEntriesCookie = Common::getDownloadEntriesCookie($timestamp);
+        Cookie::queue($downloadEntriesCookie);
 
         return response()->download($filepath, $filename)->deleteFileAfterSend(true);
     }
@@ -109,18 +119,18 @@ public function subset(Request $request, RuleDownloadSubset $ruleDownloadSubset,
     /**
      * Creates a ZIP archive containing a CSV export of entries.
      *
-     * This method generates unique temporary CSV and ZIP filenames, writes the CSV file with 
-     * header and mapped entry rows (processed in chunks from the provided query) to a temporary 
-     * location, compresses the CSV into a ZIP archive, and deletes the temporary CSV file. If 
-     * an error occurs during the CSV writing process, the error is logged and an error code is 
+     * This method generates unique temporary CSV and ZIP filenames, writes the CSV file with
+     * header and mapped entry rows (processed in chunks from the provided query) to a temporary
+     * location, compresses the CSV into a ZIP archive, and deletes the temporary CSV file. If
+     * an error occurs during the CSV writing process, the error is logged and an error code is
      * recorded.
      *
-     * @param mixed $query Query object used to retrieve entries in chunks.
+     * @param Builder $query Query object used to retrieve entries in chunks.
      * @param string $filename Reference filename used to derive the CSV file name within the archive.
      *
      * @return string Full path to the generated ZIP archive.
      */
-    private function createSubsetArchive($query, $filename): string
+    private function createSubsetArchive(Builder $query, string $filename): string
     {
         $exportChunk = config('epicollect.limits.entries_export_chunk');
         $projectRef = $this->requestedProject()->ref;
diff --git a/app/Http/Controllers/Api/Entries/DownloadTemplateController.php b/app/Http/Controllers/Api/Entries/DownloadTemplateController.php
index f049d59b..9ee32292 100644
--- a/app/Http/Controllers/Api/Entries/DownloadTemplateController.php
+++ b/app/Http/Controllers/Api/Entries/DownloadTemplateController.php
@@ -26,6 +26,14 @@ class DownloadTemplateController
     use RequestAttributes;
 
     /**
+     * Processes a CSV template file download request.
+     *
+     * Extracts project and template mapping data, validates the request parameters, and generates a CSV file containing
+     * the template headers. If a branch reference is provided, the method generates a branch-specific template; otherwise,
+     * it creates a hierarchy template. A download cookie is queued using a timestamp from the query string. The method
+     * aborts with a 404 error if the timestamp is missing or invalid, and returns a 400 error response if validation fails
+     * or a specified branch reference is not found.
+     *
      * @throws BindingResolutionException
      */
     public function sendTemplateFileCSV(Request $request, RuleDownloadTemplate $ruleUploadTemplate)
@@ -107,8 +115,8 @@ public function sendTemplateFileCSV(Request $request, RuleDownloadTemplate $rule
             $mapTos
         );
 
-        $mediaCookie = Common::getMediaCookie($timestamp);
-        Cookie::queue($mediaCookie);
+        $downloadEntriesCookie = Common::getDownloadEntriesCookie($timestamp);
+        Cookie::queue($downloadEntriesCookie);
 
         $content = implode(',', $csvHeaders);
         //return a csv file with the proper column headers
diff --git a/app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php b/app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php
index 683d413c..b9850299 100644
--- a/app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php
+++ b/app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php
@@ -11,6 +11,7 @@
 use ec5\Traits\Requests\RequestAttributes;
 use ec5\Traits\Response\ResponseTools;
 use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Database\Query\Builder;
 
 abstract class ViewEntriesControllerBase
 {
@@ -31,7 +32,7 @@ public function __construct(
     /**
      * @param array $params
      * @param array $columns
-     * @return \Illuminate\Database\Query\Builder
+     * @return Builder
      */
     protected function runQueryHierarchy(array $params, array $columns)
     {
@@ -67,7 +68,7 @@ protected function runQueryHierarchy(array $params, array $columns)
     /**
      * @param array $params
      * @param array $columns
-     * @return \Illuminate\Database\Query\Builder
+     * @return Builder
      */
     protected function runQueryBranch(array $params, array $columns)
     {
diff --git a/app/Http/Controllers/Api/Entries/View/ViewEntriesDataController.php b/app/Http/Controllers/Api/Entries/View/ViewEntriesDataController.php
index c570bc29..d1f0e160 100644
--- a/app/Http/Controllers/Api/Entries/View/ViewEntriesDataController.php
+++ b/app/Http/Controllers/Api/Entries/View/ViewEntriesDataController.php
@@ -6,6 +6,7 @@
 use Exception;
 use Illuminate\Contracts\Foundation\Application;
 use Illuminate\Contracts\Routing\ResponseFactory;
+use Illuminate\Database\Query\Builder;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Support\Facades\Response;
 use Log;
@@ -288,14 +289,14 @@ private function sendBranchEntriesCSV(array $params)
      * If an error occurs while writing any entry, the function logs the error and returns an API
      * error response with a 400 status code.
      *
-     * @param mixed $query The query object used to retrieve and paginate entry data.
+     * @param Builder $query The query object used to retrieve and paginate entry data.
      * @param array $params Array of CSV export parameters including:
      *                      - 'headers' (string): Set to 'true' to output the CSV header row.
      *                      - 'per_page' (int): Number of entries per page for pagination.
      *
      * @return mixed A CSV stream response on success, or an API error response if an error occurs.
      */
-    private function sendCSVResponse($query, $params)
+    private function sendCSVResponse(Builder $query, array $params)
     {
         $access = $this->requestedProject()->access;
         // Open the output stream
@@ -332,6 +333,6 @@ private function sendCSVResponse($query, $params)
             Log::error(__METHOD__ . ' failed.', ['exception' => $e->getMessage()]);
             return Response::apiErrorCode(400, ['entries-export-csv' => ['ec5_232']]);
         }
-        return Response::toCSVStream($data);
+        return Response::toCSVStream();
     }
 }
diff --git a/app/Libraries/Utilities/Common.php b/app/Libraries/Utilities/Common.php
index aab38b8c..1d903f4a 100644
--- a/app/Libraries/Utilities/Common.php
+++ b/app/Libraries/Utilities/Common.php
@@ -4,8 +4,10 @@
 
 use Cookie;
 use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
 use Illuminate\Support\Facades\Response;
 use Illuminate\Support\Str;
+use Random\RandomException;
 use Symfony\Component\DomCrawler\Crawler;
 
 class Common
@@ -209,7 +211,19 @@ public static function configWithParams($key, $params = [])
     }
 
     //used to generate random Android device ID in tests
-    public static function generateRandomHex($length = 16)
+
+    /**
+     * Generates a random hexadecimal string.
+     *
+     * Calculates the required number of random bytes based on the desired length, converts them to a hexadecimal string,
+     * and truncates the result to exactly match the specified length.
+     *
+     * @param int $length Desired length of the resulting hexadecimal string. Defaults to 16.
+     * @return string A random hexadecimal string of the specified length.
+     *
+     * @throws RandomException If random byte generation fails.
+     */
+    public static function generateRandomHex(int $length = 16): string
     {
         // Calculate the number of bytes needed
         $byteLength = (int)ceil($length / 2);
@@ -230,8 +244,16 @@ public static function getMonthName($monthNumber): string
     }
 
     //Use a cookie to signal the download has completed and hide overlay.
-    //no need to be secured, just a timestamp
-    public static function getMediaCookie($value)
+    /**
+     * Creates a cookie for download entries using the specified value.
+     *
+     * The cookie is configured to expire at the end of the browser session and is available across the entire site.
+     * It is not marked as secure, allowing JavaScript access, and uses the 'Lax' SameSite policy.
+     *
+     * @param mixed $value The value to assign to the cookie, typically a timestamp.
+     * @return \Symfony\Component\HttpFoundation\Cookie The generated cookie instance.
+     */
+    public static function getDownloadEntriesCookie(mixed $value): \Symfony\Component\HttpFoundation\Cookie
     {
         $cookieName = config('epicollect.setup.cookies.download_entries');
         return Cookie::make(
@@ -257,6 +279,7 @@ public static function getMediaCookie($value)
      * a default version ('0.0.0').
      *
      * @return string The Epicollect5 version number, or '0.0.0' if unavailable.
+     * @throws GuzzleException
      */
     public static function getCGPSEpicollectVersion(): string
     {
@@ -287,29 +310,24 @@ public static function getCGPSEpicollectVersion(): string
     }
 
     /**
-     * Generates an error response as a downloadable CSV file.
-     *
-     * This method is used during file downloads to ensure that any error is returned as a CSV file, keeping the user on the dataviewer page.
-     * It sets a media cookie based on the provided timestamp, retrieves the error message using the supplied error code from the configuration,
-     * and returns the error content formatted as a CSV file with the filename "epicollect5-error.txt".
+     * Generates an error response as a downloadable TXT file.
      *
-     * @param mixed $timestamp The timestamp used to generate a unique media cookie for the file download.
-     * @param mixed $code The error code that corresponds to the error message found in the configuration.
+     * This function sets a download entries cookie using the provided timestamp to signal the completion of a download on the frontend. It then retrieves the error message corresponding to the given error code from the configuration—defaulting to a generic message if no match is found—and returns the error content formatted as a TXT file.
      *
-     * @return \Illuminate\Http\Response The CSV file response containing the error details.
+     * @param string $timestamp A unique identifier used to generate the download entries cookie.
+     * @param string $code The error code used to retrieve the corresponding error message from the configuration.
      */
-    public static function errorResponseAsFile($timestamp, $code)
+    public static function errorResponseAsFile(string $timestamp, string $code)
     {
-        // imp: this happens only when users are downloading the file, so send error as file
-        // to keep the user on the dataviewer page.
-        // because on the front end this is requested using window.location
-        $mediaCookie = Common::getMediaCookie($timestamp);
-        Cookie::queue($mediaCookie);
+        //media cookie is needed to hide the loader on the front end
+        //when the response is received
+        $downloadEntriesCookie = Common::getDownloadEntriesCookie($timestamp);
+        Cookie::queue($downloadEntriesCookie);
         $filename = 'epicollect5-error.txt';
         $content = config('epicollect.codes.'.$code);
         if (empty($content)) {
             $content = 'An unexpected error occurred.';
         }
-        return Response::toCSVFile($content, $filename);
+        return Response::toTXTFile($content, $filename);
     }
 }
diff --git a/app/Models/Entries/BranchEntry.php b/app/Models/Entries/BranchEntry.php
index 52323604..f747baee 100644
--- a/app/Models/Entries/BranchEntry.php
+++ b/app/Models/Entries/BranchEntry.php
@@ -74,7 +74,6 @@ public function getEntry($projectId, $options, array $columns = array('*')): Bui
         return $this->sortAndFilterEntries($q, $options);
     }
 
-
     /**
      * Retrieve branch entries by branch reference.
      *
@@ -110,20 +109,22 @@ public function getBranchEntriesByBranchRef(int $projectId, array $params, $colu
     }
 
     /**
-     * Retrieves a query builder for branch entries matching specific criteria.
+     * Retrieves branch entries for archive downloads
+     *
+     * This method builds a query on the branch entries table (as defined in configuration)
+     * filtered by the provided project ID, form reference, and branch reference.
+     * It ensures that the 'id' column is always included in the selection.
      *
-     * This static method builds a query on the branch entries table (as defined in configuration)
-     * filtered by the provided project ID, form reference, and branch reference. It ensures that
-     * the 'id' column is always included in the selection.
+     *  No sorting as this is for downloading archive only
      *
-     * @param mixed $projectId The identifier of the project.
+     * @param int $projectId The identifier of the project.
      * @param array $params Associative array with keys:
      *                      - 'form_ref': The form reference used for filtering.
      *                      - 'branch_ref': The branch (owner input) reference for filtering.
      * @param array $columns The list of columns to select; defaults to all columns.
-     * @return \Illuminate\Database\Query\Builder The query builder instance for the branch entries.
+     * @return Builder The query builder instance for the branch entries.
      */
-    public static function getBranchEntriesByBranchRefOP($projectId, $params, $columns = array('*')): Builder
+    public function getBranchEntriesByBranchRefForArchive(int $projectId, array $params, array $columns = array('*')): Builder
     {
         // Ensure 'id' is included in the columns
         if (!in_array('id', $columns)) {
diff --git a/app/Models/Entries/Entry.php b/app/Models/Entries/Entry.php
index 17f9d968..31943884 100644
--- a/app/Models/Entries/Entry.php
+++ b/app/Models/Entries/Entry.php
@@ -56,7 +56,7 @@ class Entry extends Model
      * 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 int $projectId Identifier of the project.
      * @param array $options Array containing query parameters:
      *                       - 'form_ref': Form reference for filtering.
      *                       - 'uuid': Unique identifier of the entry.
@@ -64,9 +64,8 @@ class Entry extends Model
      *                       - '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
+    public function getEntry(int $projectId, array $options, array $columns = array('*')): Builder
     {
         $q = DB::table($this->table)->select($columns)
             ->where('project_id', '=', $projectId)
@@ -143,44 +142,17 @@ public function getChildEntriesUuids($projectId, array $uuids, $uuid): array
         return $uuids;
     }
 
-
-    /**
-     * @param $projectId
-     * @param $options
-     * @param array $columns
-     * @return Builder
-     */
-    public function getEntries($projectId, $options, array $columns = array('*')): Builder
-    {
-        $q = DB::table($this->table)
-            ->where('project_id', '=', $projectId)
-            ->where('form_ref', '=', $options['form_ref'])
-            ->where(function ($query) use ($options) {
-                // If we have a user ID
-                if (!empty($options['user_id'])) {
-                    $query->where('user_id', '=', $options['user_id']);
-                }
-            })
-            ->select($columns);
-
-        if (!empty($options['input_ref'])) {
-            $q = $this->createFilterOptions($q, $options);
-        }
-
-        return $this->sortAndFilterEntries($q, $options);
-    }
-
     /**
      * Retrieves entries for the specified project and form.
      *
      * Constructs a query on the entries table, filtering by the project identifier and form reference.
      * Optionally filters by user ID if provided, and applies additional sorting and filtering via the sortAndFilterEntries method.
      *
-     * @param mixed $projectId The project identifier.
+     * @param int $projectId The project identifier.
      * @param array $params An array containing query parameters; must include a 'form_ref' key and may include a 'user_id' key.
      * @param array $columns The list of columns to retrieve; defaults to ['*']
      */
-    public function getEntriesByForm($projectId, $params, $columns = array('*')): Builder
+    public function getEntriesByForm(int $projectId, array $params, array $columns = array('*')): Builder
     {
         $q = DB::table(config('epicollect.tables.entries'))
             ->where('project_id', '=', $projectId)
@@ -197,18 +169,19 @@ public function getEntriesByForm($projectId, $params, $columns = array('*')): Bu
     }
 
     /**
-     * Retrieves entries for a specific form, ensuring the 'id' column is always selected.
+     * Retrieves entries for a specific form for archive downloads
      *
-     * This method constructs a query on the entries table using the given project identifier and form reference
-     * from the parameters. It guarantees that the list of selected columns includes 'id', appending it if absent.
+     * Constructs and returns a query builder for fetching entries from the entries table filtered by the specified
+     * project identifier and form reference provided in the parameters. This method ensures that the 'id' column is
+     * included in the list of selected columns by appending it if absent.
      *
-     * @param mixed $projectId The identifier for the project.
-     * @param array $params Array of parameters that must include a 'form_ref' key for filtering entries by form.
-     * @param array $columns Optional list of columns to select; defaults to all columns.
+     * No sorting as this is for downloading archive only
      *
-     * @return \Illuminate\Database\Query\Builder Query builder instance for retrieving the filtered entries.
+     * @param int   $projectId The identifier of the project.
+     * @param array $params    An array of parameters that must include a 'form_ref' key for filtering entries by form.
+     * @param array $columns   Optional list of columns to select; defaults to all columns.
      */
-    public static function getEntriesByFormOP($projectId, $params, $columns = array('*')): Builder
+    public function getEntriesByFormForArchive(int $projectId, array $params, array $columns = array('*')): Builder
     {
         // Ensure 'id' is included in the columns
         if (!in_array('id', $columns)) {
diff --git a/app/Providers/Macros/Response/ToTXTFileMacro.php b/app/Providers/Macros/Response/ToTXTFileMacro.php
new file mode 100644
index 00000000..214429a3
--- /dev/null
+++ b/app/Providers/Macros/Response/ToTXTFileMacro.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace ec5\Providers\Macros\Response;
+
+use Illuminate\Support\Facades\Response;
+use Illuminate\Support\ServiceProvider;
+
+class ToTXTFileMacro extends ServiceProvider
+{
+    /**
+     * Registers the "toTXTFile" macro on the Response facade.
+     *
+     * This macro provides a convenient way to generate a downloadable plain text file response.
+     * It assembles headers to set the content type as UTF-8 encoded plain text and configures the
+     * Content-Disposition header to prompt a file download with the specified filename.
+     */
+    public function boot(): void
+    {
+        Response::macro('toTXTFile', function (string $content, string $filename) {
+            $headers = [
+                'Content-Type' => 'text/plain; charset=utf-8',
+                'Content-Disposition' => 'attachment; filename="' . $filename . '"',
+            ];
+            return Response::make($content, 200, $headers);
+        });
+    }
+}
diff --git a/app/Services/Entries/EntriesDownloadService.php b/app/Services/Entries/EntriesDownloadService.php
index 05b896e5..03ecf2f4 100644
--- a/app/Services/Entries/EntriesDownloadService.php
+++ b/app/Services/Entries/EntriesDownloadService.php
@@ -50,7 +50,7 @@ public function __construct(DataMappingService $dataMappingService)
      *
      * @return bool True if the archive is successfully created; false otherwise.
      */
-    public function createArchive(ProjectDTO $project, $projectDir, $params): bool
+    public function createArchive(ProjectDTO $project, string $projectDir, array $params): bool
     {
         $this->totalDuration = 0;
         $this->totalEntries = 0;
@@ -75,13 +75,13 @@ public function createArchive(ProjectDTO $project, $projectDir, $params): bool
             // Set the mapping
             $this->dataMappingService->init($this->project, $format, config('epicollect.strings.form'), $form['ref'], null, $mapIndex);
 
-            $columns = ['title', 'entry_data', 'branch_counts', 'child_counts', 'user_id', 'uploaded_at'];
+            $columns = ['id','title', 'entry_data', 'branch_counts', 'child_counts', 'user_id', 'uploaded_at'];
 
             // Log the start of the query for form entries
             $startTime = microtime(true);
             Log::info("Starting query for form entries: {$form['ref']}");
             // Get the query for these entries
-             $query = (new Entry())->getEntriesByForm($this->project->getId(), $params, $columns);
+            $query = (new Entry())->getEntriesByFormForArchive($this->project->getId(), $params, $columns);
             // Log the end of the query and calculate duration
             $endTime = microtime(true);
             $duration = round($endTime - $startTime, 4);
@@ -117,14 +117,13 @@ public function createArchive(ProjectDTO $project, $projectDir, $params): bool
                     $mapIndex
                 );
 
-                $columns = ['uuid', 'title', 'entry_data', 'user_id', 'uploaded_at'];
-
+                $columns = ['id','uuid', 'title', 'entry_data', 'user_id', 'uploaded_at'];
 
                 // Log the start of the query for branch entries
                 $startTime = microtime(true);
                 Log::info("Starting query for branch entries: {$branch['ref']}");
                 // Get the query for these branch entries
-                $query = (new BranchEntry())->getBranchEntriesByBranchRef(
+                $query = (new BranchEntry())->getBranchEntriesByBranchRefForArchive(
                     $this->project->getId(),
                     $params,
                     $columns
@@ -191,7 +190,7 @@ private function buildZipArchive($projectDir, $projectSlug, $format): void
      *
      * @return bool True if the file was written successfully; false otherwise.
      */
-    public function writeToFile(Builder $query, $projectDir, $fileName, $format): bool
+    public function writeToFile(Builder $query, string $projectDir, string $fileName, string $format): bool
     {
         // Make directory(recursive)  if it doesn't already exist,
         // directory will be like 2a58ddf888a04268b1545553dda88f28/123
@@ -226,7 +225,7 @@ public function writeToFile(Builder $query, $projectDir, $fileName, $format): bo
      *
      * @return bool True if the CSV file was written successfully, false on failure.
      */
-    public function writeCSV(Builder $query, $outputFile): bool
+    public function writeCSV(Builder $query, string $outputFile): bool
     {
         $startTime = microtime(true); // Start time
         $memoryUsageStart = memory_get_usage(); // Initial memory usage
@@ -328,7 +327,7 @@ public function writeCSV(Builder $query, $outputFile): bool
      * @param string $outputFile The path where the JSON file will be created.
      * @return bool True if the JSON data was written successfully; false if an error occurred.
      */
-    public function writeJSON(Builder $query, $outputFile): bool
+    public function writeJSON(Builder $query, string $outputFile): bool
     {
         $startTime = microtime(true); // Start time
         $memoryUsageStart = memory_get_usage(); // Initial memory usage
diff --git a/app/Services/Mapping/DataMappingService.php b/app/Services/Mapping/DataMappingService.php
index 655b2534..613e5dc3 100644
--- a/app/Services/Mapping/DataMappingService.php
+++ b/app/Services/Mapping/DataMappingService.php
@@ -47,15 +47,15 @@ public function __construct()
      * references alongside the mapping index.
      *
      * @param ProjectDTO $project The project data transfer object containing definitions and forms.
-     * @param mixed $format The desired output format (e.g., CSV or JSON).
-     * @param mixed $type The mapping type (e.g., form or branch).
-     * @param mixed $formRef The reference identifier for the main form.
-     * @param mixed $branchRef The reference identifier for the branch, if applicable.
+     * @param string $format The desired output format (e.g., CSV or JSON).
+     * @param string $type The mapping type (e.g., form or branch).
+     * @param string $formRef The reference identifier for the main form.
+     * @param mixed $branchRef The reference identifier for the branch, if applicable. can be null.
      * @param mixed $mapIndex The index to select the specific mapping configuration.
      *
      * @return void
      */
-    public function init(ProjectDTO $project, $format, $type, $formRef, $branchRef, $mapIndex): void
+    public function init(ProjectDTO $project, string $format, string $type, string $formRef, mixed $branchRef, mixed $mapIndex): void
     {
         $this->project = $project;
         $this->forms = $project->getProjectDefinition()->getData()['project']['forms'];
@@ -141,12 +141,12 @@ public function getUUIDHeadersCSV(): array
      * @param mixed $userId Identifier for the user; used to retrieve the creator's email when required.
      * @param string $title Title or label for the CSV entry.
      * @param string $uploaded_at MySQL date string representing the upload time, which is converted to ISO 8601 format.
-     * @param bool $access Project access flag; if true, indicates a private project and triggers inclusion of the creator's email.
-     * @param string $branchCountsString Optional JSON string representing branch entry counts; defaults to an empty string.
+     * @param string $access private or public
+     * @param ?string $branchCountsString Optional JSON string representing branch entry counts; defaults to an empty string.
      *
      * @return array CSV row as an array of mapped values.
      */
-    public function getMappedEntryCSV($JSONEntryString, $userId, $title, $uploaded_at, $access, $branchCountsString = ''): array
+    public function getMappedEntryCSV(string $JSONEntryString, mixed $userId, string $title, string $uploaded_at, string $access, ?string $branchCountsString = ''): array
     {
         $output = [];
         try {
@@ -248,7 +248,7 @@ private function getUUIDHeadersForm($uuid, $relationships): array
      *
      * @return array An array where the first element is the owner entry UUID and the second is the branch UUID.
      */
-    private function getUUIDHeadersBranch($uuid, $relationships): array
+    private function getUUIDHeadersBranch(string $uuid, array $relationships): array
     {
         $out = [];
         //imp: order matters so don't change, otherwise need to change other places where header is ...
@@ -266,15 +266,15 @@ private function getUUIDHeadersBranch($uuid, $relationships): array
      * specialized parsing for branch, location, checkbox, searchsingle, and searchmultiple inputs.
      *
      * @param string $JSONEntryString A JSON-encoded string containing entry data and relationships.
-     * @param int|string $userId The identifier used to retrieve the creator's email if needed.
+     * @param mixed $userId The identifier used to retrieve the creator's email if needed.
      * @param string $title The title to include in the output.
      * @param string $uploaded_at The MySQL-formatted upload timestamp to be converted to ISO 8601 format.
-     * @param mixed $access An indicator determining whether to add the creator's email for private projects.
-     * @param string $branchCountsString (Optional) A JSON-encoded string with branch count information.
+     * @param string $access private or public
+     * @param ?string $branchCountsString (Optional) A JSON-encoded string with branch count information.
      * @return false|array|string A JSON-encoded string of the mapped entry, an array if the entry data is missing,
      *                             or an empty string on JSON decoding failure.
      */
-    public function getMappedEntryJSON($JSONEntryString, $userId, $title, $uploaded_at, $access, $branchCountsString = ''): false|array|string
+    public function getMappedEntryJSON(string $JSONEntryString, mixed $userId, string $title, string $uploaded_at, string $access, ?string $branchCountsString = ''): false|array|string
     {
         $output = [];
         try {
@@ -430,7 +430,25 @@ private function setupMapping($formRef, $branchRef, $mapIndex): void
         $this->map = $mapping;
     }
 
-    private function parseAnswer($type, $answer, $input)
+    /**
+     * Parses and converts an answer based on its input type.
+     *
+     * This method transforms a raw answer into a formatted value suitable for output. It handles various input types including:
+     * - Selection inputs (e.g., radio, dropdown, search, and checkbox) by mapping answers through a possible answer mapping.
+     * - Location data in CSV and JSON formats by converting latitude and longitude into UTM coordinates using a GPointConverter.
+     * - Date and time values by formatting them according to a configured PHP datetime format.
+     * - Media inputs (photo, video, audio) by generating an appropriate media URL.
+     * - Numeric values by casting to integer or float.
+     *
+     * In cases where the expected location data is missing or invalid, the method returns fallback empty values while logging relevant information.
+     *
+     * @param mixed $type   The type identifier of the input (e.g., 'radio', 'csv-location', 'date', etc.).
+     * @param mixed $answer The raw answer to convert; can be a string, array, or associative array with specific keys.
+     * @param array $input  Metadata and configuration for the input, used for mapping and formatting.
+     *
+     * @return mixed The parsed answer, which may vary in type (string, array, integer, or float) depending on the input type.
+     */
+    private function parseAnswer(string $type, mixed $answer, array $input): mixed
     {
         $parsedAnswer = '';
         $converter = new GPointConverter();
@@ -457,6 +475,8 @@ private function parseAnswer($type, $answer, $input)
                     }
                 }
 
+
+
                 $parsedAnswer = count($temp) === 0 ? '' : implode(', ', $temp);
                 break;
             case 'json-searchsingle':
@@ -503,8 +523,14 @@ private function parseAnswer($type, $answer, $input)
                         ];
                     }
                 } catch (Throwable $e) {
-                    Log::debug(__METHOD__ . ' failed | csv-location', ['exception' => $e->getMessage()]);
-                    //we get here when there is not an answer?
+                    Log::info(
+                        __METHOD__ . ' failed | csv-location, probably empty location',
+                        [
+                            'exception' => $e->getMessage(),
+                            '$answer' => $answer
+                        ]
+                    );
+                    //we get here when there is not an answer
                     $parsedAnswer = [
                         '',
                         '',
@@ -675,7 +701,7 @@ public function getBranchInputsFlattened($forms, $formRef, $branchInputRef): arr
      * @param string $mysqlDate The MySQL date string to convert.
      * @return string The ISO 8601 formatted date string.
      */
-    private function convertMYSQLDateToISO($mysqlDate): string
+    private function convertMYSQLDateToISO(string $mysqlDate): string
     {
         return Carbon::parse($mysqlDate)->format('Y-m-d\TH:i:s.000\Z');
     }
@@ -689,10 +715,10 @@ private function convertMYSQLDateToISO($mysqlDate): string
      *
      * @param string $access The project's access level, used to determine whether to add the creator's email.
      * @param array  &$output Reference to the output array that will include the 'created_by' field if applicable.
-     * @param int|string|null $userId The identifier of the user whose email should be retrieved.
+     * @param mixed $userId The identifier of the user whose email should be retrieved.
      *
      */
-    private function addCreatedByIfNeeded($access, array &$output, $userId): void
+    private function addCreatedByIfNeeded(string $access, array &$output, mixed $userId): void
     {
         if ($access === config('epicollect.strings.project_access.private')) {
             //userId can be  0 when the entry was uploaded when the project was public without logging in.
diff --git a/config/app.php b/config/app.php
index d6d6cde8..9c2a6355 100644
--- a/config/app.php
+++ b/config/app.php
@@ -183,6 +183,7 @@
         ec5\Providers\Macros\Response\ApiDataMacro::class,
         ec5\Providers\Macros\Response\ToCSVStreamMacro::class,
         ec5\Providers\Macros\Response\ToCSVFileMacro::class,
+        ec5\Providers\Macros\Response\ToTXTFileMacro::class,
         ec5\Providers\Macros\Response\ToJSONFileMacro::class,
         ec5\Providers\Macros\Response\ToMediaStreamMacro::class,
         //Rate Limiter
diff --git a/tests/Http/Controllers/Api/Entries/DownloadControllerTest.php b/tests/Http/Controllers/Api/Entries/DownloadControllerTest.php
index 5c9c9cc3..50453751 100644
--- a/tests/Http/Controllers/Api/Entries/DownloadControllerTest.php
+++ b/tests/Http/Controllers/Api/Entries/DownloadControllerTest.php
@@ -2,19 +2,25 @@
 
 namespace Tests\Http\Controllers\Api\Entries;
 
+use Auth;
 use Carbon\Carbon;
+use ec5\Libraries\Generators\EntryGenerator;
 use ec5\Libraries\Generators\ProjectDefinitionGenerator;
+use ec5\Models\Entries\Entry;
 use ec5\Models\Project\Project;
 use ec5\Models\Project\ProjectRole;
 use ec5\Models\Project\ProjectStats;
 use ec5\Models\Project\ProjectStructure;
 use ec5\Models\User\User;
+use ec5\Services\Mapping\ProjectMappingService;
+use ec5\Services\Project\ProjectExtraService;
 use ec5\Traits\Assertions;
 use Exception;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
 use Illuminate\Support\Str;
 use Storage;
 use Tests\TestCase;
+use Throwable;
 use ZipArchive;
 
 class DownloadControllerTest extends TestCase
@@ -54,7 +60,7 @@ public function test_should_fails_if_user_not_logged_in()
             ]
         );
 
-        $response = $this->get('api/internal/download-entries/' . $project->slug)
+        $this->get('api/internal/download-entries/' . $project->slug)
             ->assertStatus(400)
             ->assertJsonStructure([
                 'errors' => [
@@ -132,6 +138,7 @@ public function test_download_json_public()
         }
         $this->assertEquals($zipName, $extractedFilename);
         // Get the response content as a file
+        /** @noinspection PhpUndefinedMethodInspection */
         $responseContent = $response->getFile();
         // Get the downloaded file's path
         $filePath = $responseContent->getPathname();
@@ -195,6 +202,7 @@ public function test_download_json_private()
         }
         $this->assertEquals($zipName, $extractedFilename);
         // Get the response content as a file
+        /** @noinspection PhpUndefinedMethodInspection */
         $responseContent = $response->getFile();
         // Get the downloaded file's path
         $filePath = $responseContent->getPathname();
@@ -258,6 +266,7 @@ public function test_download_csv_public()
         }
         $this->assertEquals($zipName, $extractedFilename);
         // Get the response content as a file
+        /** @noinspection PhpUndefinedMethodInspection */
         $responseContent = $response->getFile();
         // Get the downloaded file's path
         $filePath = $responseContent->getPathname();
@@ -267,15 +276,26 @@ public function test_download_csv_public()
         Storage::delete($filePath);
     }
 
+    /**
+     * @throws Throwable
+     */
     public function test_download_csv_private()
     {
         //create user
         $format = 'csv';
         $user = factory(User::class)->create();
-        //create project
+        $projectName = config('testing.WEB_UPLOAD_CONTROLLER_PROJECT.name');
+        $projectSlug = config('testing.WEB_UPLOAD_CONTROLLER_PROJECT.slug');
+        //create a project with custom project definition
+        $projectDefinition = ProjectDefinitionGenerator::createProject(1);
+        $projectDefinition['data']['project']['name'] = $projectName;
+        $projectDefinition['data']['project']['slug'] = $projectSlug;
         $project = factory(Project::class)->create(
             [
                 'created_by' => $user->id,
+                'name' => array_get($projectDefinition, 'data.project.name'),
+                'slug' => array_get($projectDefinition, 'data.project.slug'),
+                'ref' => array_get($projectDefinition, 'data.project.ref'),
                 'access' => config('epicollect.strings.project_access.private')
             ]
         );
@@ -287,9 +307,18 @@ public function test_download_csv_private()
             'role' => config('epicollect.strings.project_roles.creator')
         ]);
 
+        //create project structures
+        $projectExtraService = new ProjectExtraService();
+        $projectExtra = $projectExtraService->generateExtraStructure($projectDefinition['data']);
+        $projectMappingService = new ProjectMappingService();
+        $projectMapping = [$projectMappingService->createEC5AUTOMapping($projectExtra)];
+
         factory(ProjectStructure::class)->create(
             [
-                'project_id' => $project->id
+                'project_id' => $project->id,
+                'project_definition' => json_encode($projectDefinition['data']),
+                'project_extra' => json_encode($projectExtra),
+                'project_mapping' => json_encode($projectMapping)
             ]
         );
 
@@ -299,6 +328,35 @@ public function test_download_csv_private()
                 'total_entries' => 0
             ]
         );
+
+        //generate entries by creator
+        $entryGenerator = new EntryGenerator($projectDefinition);
+        $formRef = array_get($projectDefinition, 'data.project.forms.0.ref');
+        $entryPayloads = [];
+        for ($i = 0; $i < 10; $i++) {
+            Auth::login($user);
+            $entryPayloads[$i] = $entryGenerator->createParentEntryPayload($formRef);
+            $entryRowBundle = $entryGenerator->createParentEntryRow(
+                $user,
+                $project,
+                config('epicollect.strings.project_roles.creator'),
+                $projectDefinition,
+                $entryPayloads[$i]
+            );
+            Auth::logout();
+
+            $this->assertEntryRowAgainstPayload(
+                $entryRowBundle,
+                $entryPayloads[$i]
+            );
+        }
+
+        //assert rows are created
+        $this->assertCount(
+            10,
+            Entry::where('project_id', $project->id)->get()
+        );
+
         $cookies = [config('epicollect.setup.cookies.download_entries') => Carbon::now()->timestamp];
         $params = [
             config('epicollect.setup.cookies.download_entries') => Carbon::now()->timestamp,
@@ -307,7 +365,7 @@ public function test_download_csv_private()
         $response = $this->actingAs($user)->call('GET', 'api/internal/download-entries/' . $project->slug, $params, $cookies);
 
         $response->assertStatus(200);
-        // Assert that the returned file is a zip file
+        // Assert that the returned file is a zip file. If .txt, that is an error response as a file
         $this->assertTrue($response->headers->get('Content-Type') === 'application/zip');
         //assert filename
         $zipName = $project->slug . '-' . $params['format'] . '.zip';
@@ -321,11 +379,23 @@ public function test_download_csv_private()
         }
         $this->assertEquals($zipName, $extractedFilename);
         // Get the response content as a file
+        /** @noinspection PhpUndefinedMethodInspection */
         $responseContent = $response->getFile();
         // Get the downloaded file's path
         $filePath = $responseContent->getPathname();
 
-        $this->assertZipContent($filePath, $format, 1, 0);
+
+        $numOfForms = sizeof($projectDefinition['data']['project']['forms']);
+        $numOfBranches = 0;
+        foreach ($projectDefinition['data']['project']['forms'] as $form) {
+            foreach ($form['inputs'] as $input) {
+                if ($input['type'] === 'branch') {
+                    $numOfBranches++;
+                }
+            }
+        }
+
+        $this->assertZipContent($filePath, $format, $numOfForms, $numOfBranches);
 
         Storage::delete($filePath);
     }
@@ -418,6 +488,7 @@ public function test_download_csv_private_multiple_forms_and_branches()
         }
         $this->assertEquals($zipName, $extractedFilename);
         // Get the response content as a file
+        /** @noinspection PhpUndefinedMethodInspection */
         $responseContent = $response->getFile();
         // Get the downloaded file's path
         $filePath = $responseContent->getPathname();
@@ -515,6 +586,7 @@ public function test_download_json_private_multiple_forms_and_branches()
         }
         $this->assertEquals($zipName, $extractedFilename);
         // Get the response content as a file
+        /** @noinspection PhpUndefinedMethodInspection */
         $responseContent = $response->getFile();
         // Get the downloaded file's path
         $filePath = $responseContent->getPathname();
@@ -614,28 +686,28 @@ public function test_should_abort_if_timestamp_missing()
             ]
         );
 
-        $response = $this->actingAs($user)->get('api/internal/download-entries/' . $project->slug)
-            ->assertStatus(400)
-            ->assertJsonStructure([
-                'errors' => [
-                    '*' => [
-                        'code',
-                        'title',
-                        'source',
-                    ]
-                ]
-            ])
-            ->assertExactJson(
-                [
-                    "errors" => [
-                        [
-                            "code" => "ec5_29",
-                            "title" => "Value invalid.",
-                            "source" => "download-entries"
-                        ],
-                    ]
-                ]
-            );
+        $this->actingAs($user)->get('api/internal/download-entries/' . $project->slug)
+             ->assertStatus(400)
+             ->assertJsonStructure([
+                 'errors' => [
+                     '*' => [
+                         'code',
+                         'title',
+                         'source',
+                     ]
+                 ]
+             ])
+             ->assertExactJson(
+                 [
+                     "errors" => [
+                         [
+                             "code" => "ec5_29",
+                             "title" => "Value invalid.",
+                             "source" => "download-entries"
+                         ],
+                     ]
+                 ]
+             );
     }
 
     public function test_should_abort_if_timestamp_malformed()
@@ -739,7 +811,10 @@ private function assertZipContent($filePath, $extension, $filesCountForm, $files
 
         // Close the zip file
         $zip->close();
-        // Assert that at least one JSON file was found in the zip
+        // Assert that at least one file was found in the zip
         $this->assertTrue($fileFound);
+
+        //assert the file is not an error file
+
     }
 }
diff --git a/tests/Http/Controllers/Api/Entries/View/External/ExportRoutes/EntriesExportPrivateCreatorTest.php b/tests/Http/Controllers/Api/Entries/View/External/ExportRoutes/EntriesExportPrivateCreatorTest.php
index 67134f83..faa67ee3 100644
--- a/tests/Http/Controllers/Api/Entries/View/External/ExportRoutes/EntriesExportPrivateCreatorTest.php
+++ b/tests/Http/Controllers/Api/Entries/View/External/ExportRoutes/EntriesExportPrivateCreatorTest.php
@@ -187,7 +187,7 @@ public function test_getting_OAuth2_token()
                 $projectDefinition,
                 $entryPayloads[$i]
             );
-            Auth::login($user);
+            Auth::logout();
 
             $this->assertEntryRowAgainstPayload(
                 $entryRowBundle,


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

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

<details>
<summary>app/Http/Controllers/Api/Entries/DownloadController.php (1)</summary><blockquote>

`63-73`: _🧹 Nitpick (assertive)_

**Update method documentation to match implementation.**

The documentation still mentions "queues a media cookie" while the implementation now uses a download entries cookie.

```diff
 * Sends an archive file as a download and deletes it after sending, or returns an error if the file is missing.
 *
 *
- * This method checks if the specified path exists. If it does, it queues a media cookie based on the
+ * This method checks if the specified path exists. If it does, it queues a download entries cookie based on the
 * provided timestamp and initiates a download response that deletes the file once sent. If the file cannot be found,
 * it returns an error response with a designated error code.
```

</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 d7d80942e6f6e7d76301c843470f9d7c58450a34 and d5524d498009b9ef0ac3ed60469b46a7762ac8a5.

</details>

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

* `app/Http/Controllers/Api/Entries/DownloadController.php` (1 hunks)
* `app/Http/Controllers/Api/Entries/DownloadSubsetController.php` (1 hunks)
* `app/Http/Controllers/Api/Entries/DownloadTemplateController.php` (1 hunks)
* `app/Libraries/Utilities/Common.php` (5 hunks)
* `app/Models/Entries/BranchEntry.php` (1 hunks)
* `app/Models/Entries/Entry.php` (1 hunks)
* `app/Providers/Macros/Response/ToTXTFileMacro.php` (1 hunks)
* `app/Services/Entries/EntriesDownloadService.php` (4 hunks)
* `app/Services/Mapping/DataMappingService.php` (4 hunks)
* `config/app.php` (1 hunks)
* `tests/Http/Controllers/Api/Entries/DownloadControllerTest.php` (7 hunks)
* `tests/Http/Controllers/Api/Entries/View/External/ExportRoutes/EntriesExportPrivateCreatorTest.php` (1 hunks)

</details>

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

<details>
<summary>config/app.php (1)</summary>

`186-186`: **Service provider registration looks good**

The registration of the `ToTXTFileMacro` service provider alongside other response macro providers follows the established pattern.

</details>
<details>
<summary>app/Http/Controllers/Api/Entries/DownloadTemplateController.php (1)</summary>

`110-111`: **Cookie naming improved for better semantics**

Changing from media cookie to download entries cookie provides clearer semantic meaning for the functionality and better aligns with the actual purpose of the cookie.

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

`103-104`: **Cookie naming improved for better semantics**

Consistent with changes in other controllers, changing from media cookie to download entries cookie provides better semantic clarity.

---

`112-115`: **Documentation formatting improved**

The method documentation formatting changes improve readability and maintain consistent style.

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

`205-211`: **Method renaming and type declaration improvements.**

The changes to this method are well-documented and improve code clarity. The method has been renamed from `getEntriesByForm` to `getEntriesByFormForArchive` to better reflect its purpose, and type declarations have been added to the parameters.

---

`218-222`: **Optimized implementation for archive downloads.**

The implementation has been streamlined by removing user filtering and sorting operations, which will improve performance during bulk exports. This is a good performance optimization.

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

`115-128`: **Method renaming and improved type declarations.**

The changes to this method mirror those in the Entry class, providing consistency across the codebase. The method has been renamed to better reflect its purpose, and type declarations have been added to parameters.

---

`130-140`: **Performance optimization for archive downloads.**

Similar to the Entry class, this implementation has been optimized by removing user filtering and sorting, which is appropriate for the archive download use case.

</details>
<details>
<summary>app/Http/Controllers/Api/Entries/DownloadController.php (1)</summary>

`77-78`: **Updated cookie handling for downloads.**

The code now correctly uses `getDownloadEntriesCookie` instead of the previous media cookie method, which is semantically more appropriate for entry downloads.

</details>
<details>
<summary>tests/Http/Controllers/Api/Entries/View/External/ExportRoutes/EntriesExportPrivateCreatorTest.php (1)</summary>

`190-190`: **Changed authentication behavior in test.**

This change modifies the test to ensure entries can be exported without requiring the user to be logged in, which is more appropriate for token-based authentication tests.

</details>
<details>
<summary>tests/Http/Controllers/Api/Entries/DownloadControllerTest.php (6)</summary>

`280-292`: **Good project setup enhancement using config values**

The test now properly uses configuration values for project name and slug instead of hardcoded values, and correctly initializes the project with a custom definition. This makes the test more maintainable and realistic.

---

`303-315`: **Improved test fixture with detailed project structures**

Good addition of project structure generation using the proper services. This creates a more realistic test environment by including extras and mappings that match the project definition.

---

`325-351`: **Well-implemented entry generation logic**

The test now creates actual entries with proper authentication cycles, making it more thorough. The assertion validating that entries are properly stored is a good practice.

---

`361-361`: **Helpful comment about error response format**

This comment clarifies the expected behavior when encountering errors, making the test more maintainable.

---

`380-390`: **Improved test validation with dynamic form/branch counting**

The test now dynamically calculates the expected number of forms and branches based on the project definition, making the test more robust against changes in the test fixtures.

---

`807-808`: **Good assertion for error response detection**

This assertion verifies that the response is not an error file, which helps ensure that the test is passing for the right reasons.

</details>
<details>
<summary>app/Services/Mapping/DataMappingService.php (4)</summary>

`460-461`: **Added spacing for better code readability**

These added blank lines improve the code's readability by separating logical sections.

---

`508-514`: **Enhanced error logging with context information**

The log level has been appropriately changed from debug to info, and now includes the answer variable in the context. This will provide better diagnostic information when location parsing fails.

---

`686-686`: **Added appropriate type declaration for parameter**

Adding the string type declaration to the $mysqlDate parameter enforces type safety and improves code clarity.

---

`703-703`: **Added appropriate type declaration for parameter**

Adding the string type declaration to the $access parameter enforces type safety and improves code clarity.

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

`53-53`: **Improved method signature with type declarations**

Type declarations for $projectDir and $params parameters add type safety and improve code readability.

---

`78-78`: **Added 'id' column to the query columns array**

Including the 'id' column first in the columns array is a good practice when working with database queries, especially when using Laravel's lazyById loading mechanism.

---

`84-84`: **Method name change to be more semantically correct**

Renaming the method from `getEntriesByForm` to `getEntriesByFormForArchive` better reflects the specific purpose of this method.

---

`120-120`: **Added 'id' column to the branch query columns array**

Consistent with the change on line 78, including 'id' in the columns for branch entries is good practice.

---

`126-126`: **Method name change to be more semantically correct**

Renaming the method from `getBranchEntriesByBranchRef` to `getBranchEntriesByBranchRefForArchive` aligns with the naming convention established for form entries and better reflects the method's purpose.

---

`228-228`: **Improved method signature with type declaration**

Adding the string type declaration for the $outputFile parameter enhances type safety.

</details>
<details>
<summary>app/Libraries/Utilities/Common.php (5)</summary>

`215-218`: **Added proper exception documentation**

Adding the `@throws RandomException` annotation and return type declaration provides better information about potential exceptions that could be thrown and improves type safety.

---

`240-240`: **Method renamed for better semantic meaning**

Renaming from `getMediaCookie` to `getDownloadEntriesCookie` clearly reflects the specific purpose of this method, improving code readability and maintainability.

---

`266-266`: **Added exception documentation**

Adding the `@throws GuzzleException` annotation properly documents that this method may throw a GuzzleException.

---

`297-302`: **Updated documentation to reflect actual behavior**

The documentation now correctly describes the method as generating a TXT file rather than a CSV file, which is consistent with the implementation change on line 317.

---

`306-318`: **Improved error response implementation**

The method now:
1. Uses type declarations for parameters
2. Uses the newly named `getDownloadEntriesCookie` method
3. Returns a TXT file instead of a CSV file, which is a better format for error messages

This is a good enhancement that maintains consistency with the renamed cookie method and provides a more appropriate format for error messages.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T12:09:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<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 d5524d498009b9ef0ac3ed60469b46a7762ac8a5 and 754a667cb5dfe8ca917a58154933133017059df2.

</details>

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

* `app/Providers/Macros/Response/ToTXTFileMacro.php` (1 hunks)

</details>

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

<details>
<summary>app/Providers/Macros/Response/ToTXTFileMacro.php (3)</summary>

`1-20`: **Well-implemented response macro for TXT files.**

The implementation follows Laravel's service provider and macro conventions correctly. The response is properly configured with the appropriate content type and disposition headers for a text file download.

---

`15-15`: **Ensure filename sanitization before using this macro.**

The filename is included directly in the Content-Disposition header. Make sure to sanitize filenames at the controller level before passing them to this macro to prevent potential header injection issues.

---

`14-14`: **Header capitalization is correct.**

The 'Content-Type' header follows standard HTTP capitalization conventions, as suggested in a previous review.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:33:40Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

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

<details>
<summary>app/Providers/Macros/Response/ToTXTFileMacro.php (1)</summary><blockquote>

`19-25`: _🧹 Nitpick (assertive)_

**Consider adding type hints to macro parameters.**

For better type safety and code documentation, consider adding parameter type hints:

```diff
-        Response::macro('toTXTFile', function ($content, $filename) {
+        Response::macro('toTXTFile', function (string $content, string $filename) {
```

</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 754a667cb5dfe8ca917a58154933133017059df2 and a5c66c5c342cc97496f4eccbf56b5f08e3b91ac5.

</details>

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

* `app/Http/Controllers/Api/Entries/DownloadController.php` (1 hunks)
* `app/Http/Controllers/Api/Entries/DownloadSubsetController.php` (2 hunks)
* `app/Http/Controllers/Api/Entries/DownloadTemplateController.php` (2 hunks)
* `app/Libraries/Utilities/Common.php` (5 hunks)
* `app/Models/Entries/Entry.php` (1 hunks)
* `app/Providers/Macros/Response/ToTXTFileMacro.php` (1 hunks)
* `app/Services/Mapping/DataMappingService.php` (5 hunks)

</details>

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

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

`28-35`: **Good addition of comprehensive docblock.**

The detailed docblock clearly explains the method's purpose and behavior, covering parameters, exceptions, and expected outcomes. This significantly improves code documentation and maintainability.

---

`118-119`: **Consistent semantic improvement in cookie handling.**

Changed from retrieving media cookie to download entries cookie, which better reflects the actual function purpose. This change aligns with similar updates across other controllers, ensuring consistent terminology throughout the codebase.

</details>
<details>
<summary>app/Providers/Macros/Response/ToTXTFileMacro.php (1)</summary>

`1-27`: **New TXT file response macro implemented correctly.**

The implementation follows the established pattern for response macros in the application. The macro properly sets content type and disposition headers for text file downloads.

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

`34-42`: **Well-documented method with clear behavior explanation.**

The added docblock thoroughly explains the method's functionality, including validation, sanitization, and response handling. This significantly enhances code maintainability.

---

`112-113`: **Consistent semantic improvement in cookie handling.**

Changed from retrieving media cookie to download entries cookie, matching the same pattern used in other controllers. This unified approach to cookie naming improves code clarity and consistency.

---

`121-125`: **Improved documentation for createSubsetArchive method.**

The enhanced docblock provides better explanation of the method's purpose and functionality, making future maintenance easier.

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

`200-212`: **Improved method documentation with clear purpose.**

The updated docblock clearly specifies that this method is specifically for retrieving archive entries, providing better context for its usage.

---

`213-213`: **Better method naming and type safety improvements.**

The method has been renamed from `getEntriesByFormOP` to `getEntriesByFormForArchive`, better reflecting its specific purpose. Additionally, explicit type hints (`int`, `array`) have been added to parameters, improving type safety.

</details>
<details>
<summary>app/Http/Controllers/Api/Entries/DownloadController.php (3)</summary>

`66-67`: **Doc comment aligns well with the cookie-handling changes.**  
The updated description accurately reflects that the method now queues a download entries cookie when the file exists.

---

`71-72`: **Parameter documentation improvements look good.**  
Documenting the optional nature and purpose of the `$timestamp` parameter clarifies usage for future maintainers.

---

`77-78`: **Cookie method call is consistent with the new naming convention.**  
Replacing `getMediaCookie` with `getDownloadEntriesCookie` matches the broader cookie-handling updates, and queueing the cookie is correctly placed before initiating the download response.

</details>
<details>
<summary>app/Libraries/Utilities/Common.php (5)</summary>

`7-10`: **Imports for exceptions are appropriate.**  
Adding `GuzzleHttp\Exception\GuzzleException` and `Random\RandomException` confirms that the methods within this class may throw or handle these new exceptions, matching your updated docblocks.

---

`213-225`: **Improved documentation for random hex generation.**  
Specifying the thrown `RandomException` and reinforcing the exact length constraint helps clarify the method's contract.

---

`246-255`: **New `getDownloadEntriesCookie()` doc is clear and thorough.**  
Explaining the cookie’s scope (end of session) and security properties ensures maintainers understand its usage and implications.

---

`312-319`: **Enhanced docblock for errorResponseAsFile.**  
Noting that it now returns a *.txt file is helpful for clarity on the format and for setting expectations in dependent code.

---

`321-327`: **Cookie queue and TXT file response look correct.**  
Ensuring that a download entries cookie is always queued before returning the TXT response aligns with the front-end’s requirement to detect download completion. The final TXT output is a straightforward approach for conveying errors.  



Also applies to: 332-332

</details>
<details>
<summary>app/Services/Mapping/DataMappingService.php (4)</summary>

`433-445`: **Docblock clarifies the parsing logic well.**  
Detailing how each input type is transformed (including location, selection, and media types) will aid new contributors in understanding the method’s responsibilities.

---

`526-534`: **Refined logging strategy.**  
Switching from debug to info logging for empty CSV-location data is sensible for better visibility in production logs. The message and context (exception + answer) should help diagnose issues.

---

`704-704`: **Adding a return type for convertMYSQLDateToISO encourages clearer contracts.**  
Specifying `: string` ensures that this helper method has a predictable output, improving overall type safety.

---

`721-721`: **Expanding type declarations for addCreatedByIfNeeded.**  
Defining `$access` as a string aligns with the existing usage, and returning `void` clarifies that it mutates `$output` by reference without returning a separate value.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:47:46Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 9**

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

<details>
<summary>app/Services/Mapping/DataMappingService.php (1)</summary><blockquote>

`478-479`: _🧹 Nitpick (assertive)_

**Minor whitespace addition.**

The addition of blank lines improves code readability while not affecting functionality.

</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 a5c66c5c342cc97496f4eccbf56b5f08e3b91ac5 and 23763701cf752ff2ff515f841141db45db415e5d.

</details>

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

* `app/Http/Controllers/Api/Entries/DownloadController.php` (4 hunks)
* `app/Http/Controllers/Api/Entries/DownloadSubsetController.php` (3 hunks)
* `app/Libraries/Utilities/Common.php` (5 hunks)
* `app/Models/Entries/BranchEntry.php` (1 hunks)
* `app/Models/Entries/Entry.php` (3 hunks)
* `app/Providers/Macros/Response/ToTXTFileMacro.php` (1 hunks)
* `app/Services/Entries/EntriesDownloadService.php` (6 hunks)
* `app/Services/Mapping/DataMappingService.php` (9 hunks)
* `tests/Http/Controllers/Api/Entries/DownloadControllerTest.php` (14 hunks)

</details>

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

<details>
<summary>app/Libraries/Utilities/Common.php (3)</summary>

`7-10`: **Good addition of exception imports**  
These imports precisely match the exception handling needs for random bytes and Guzzle operations.

---

`215-226`: **Comprehensive doc block and typehint**  
Providing detailed documentation and enforcing `int` return type ensures clear usage and robust error handling.

---

`312-331`: **TXT response for errors**  
Switching to a TXT file for error responses aligns with the updated macro and clarifies the format for end users. Implementation looks consistent.

</details>
<details>
<summary>app/Providers/Macros/Response/ToTXTFileMacro.php (2)</summary>

`1-8`: **Service provider structure**  
Clean approach for adding macros and organizing response logic under a dedicated provider.

---

`9-25`: **Macro clarity**  
The `toTXTFile` macro correctly sets headers for text files and uses parameter type hints for clarity.

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

`127-139`: **Archive retrieval method**  
Excluding both sorting and user-specific filtering is appropriate for bulk archiving scenarios. Implementation appears correct.

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

`13-13`: **Confirm the correct Builder import.**

The code is importing `Illuminate\Database\Query\Builder`, but if the models return an Eloquent builder (commonly `Illuminate\Database\Eloquent\Builder`), you may need to update this import. Otherwise, if the method truly returns a query builder instance, this is fine.

---

`35-43`: **Well-documented method purpose.**

Adding this detailed docblock strengthens clarity on how subset downloads are validated and handled.

---

`113-114`: **Cookie replacement aligns with new logic.**

Switching from `getMediaCookie()` to `getDownloadEntriesCookie()` is consistent with the updated download cookie management. No issues found.

---

`122-125`: **Clear explanation of the CSV and ZIP creation steps.**

These docblock clarifications help maintainers understand the archiving process and potential error states.

---

`133-133`: **Improved type safety with Builder parameter.**

Specifying `Builder $query` in the method signature improves clarity and enforces type safety. Just ensure it matches the actual return type of your query methods.

</details>
<details>
<summary>tests/Http/Controllers/Api/Entries/DownloadControllerTest.php (7)</summary>

`5-16`: **New import statements recognized.**

These imports (e.g., `Auth`, `EntryGenerator`, `Entry`, `ProjectMappingService`, `ProjectExtraService`) appear valid and align with the test's extended functionality.

---

`279-281`: **Docblock reflecting thrown exceptions.**

Declaring `@throws Throwable` ensures test consumers are aware it may throw. This improves test self-documentation.

---

`287-289`: **Config usage for project name/slug.**

Loading `WEB_UPLOAD_CONTROLLER_PROJECT` from config is convenient but please confirm that these config keys always exist, or handle them if missing.

---

`296-298`: **Legacy helper usage.**

Using `array_get()` is fine if your Laravel version supports it. Otherwise, consider `Arr::get()`.

---

`310-315`: **Project structures generation.**

Creating extra structures and mappings using `ProjectExtraService` and `ProjectMappingService` is an elegant approach for robust test setup. No issues found.

---

`318-322`: **Storing expanded definitions.**

Attaching `project_definition`, `project_extra`, and `project_mapping` ensures each test project’s structure is well-defined. Looks good.

---

`689-710`: **Validating missing timestamp scenario.**

This test aligns with the controller’s logic, returning HTTP 400 when no timestamp query/cookie is present.

</details>
<details>
<summary>app/Http/Controllers/Api/Entries/DownloadController.php (5)</summary>

`10-10`: **User model import.**

Including `ec5\Models\User\User` clarifies type hints in your methods. This is a clean improvement.

---

`68-69`: **Docblock clarifies cookie usage.**

Stating that a download entries cookie is queued ensures maintainers grasp the shift in cookie handling.

---

`73-74`: **Extending docblock for optional timestamp.**

Documenting `$timestamp` clarifies how it affects cookie creation and error responses.

---

`78-79`: **Consistent cookie usage.**

Replacing media cookie logic with `getDownloadEntriesCookie()` ensures uniform handling across controllers.

---

`135-138`: **Refined type-hinting for $user.**

`private function getArchivePath(User $user)` clarifies the user context. This enforces that only authenticated user references are passed.

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

`59-68`: **Good type safety enhancement in method signature.**

The addition of type declarations for `$projectId` (int) and `$options` (array) in the `getEntry` method improves code clarity and type safety. The documentation has also been appropriately updated to reflect these changes.

---

`151-155`: **Good type safety enhancement in method signature.**

The addition of type declarations for `$projectId` (int), `$params` (array), and `$columns` (array) in the `getEntriesByForm` method improves code clarity and type safety. The documentation has also been updated to reflect these changes.

---

`171-195`: **Well-designed specialized method for archive operations.**

The new `getEntriesByFormForArchive` method is a good optimization for archive downloads. It ensures the 'id' column is always included and removes user filtering and sorting for better performance during bulk exports.


The implementation is clean and the documentation clearly explains its purpose and behavior.

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

`53-53`: **Good type safety enhancement in method signature.**

The addition of type declarations for `$projectDir` (string) and `$params` (array) in the `createArchive` method improves code clarity and type safety.

---

`78-78`: **Proper adjustment for archive-specific data retrieval.**

Adding 'id' as the first column and switching to the specialized `getEntriesByFormForArchive` method aligns with the optimizations made for archive operations. This ensures consistent ID-based retrieval for all entries.



Also applies to: 84-84

---

`120-120`: **Consistent implementation for branch entries.**

Similar to the form entries changes, this update ensures 'id' is included in branch entries and uses the specialized archive method `getBranchEntriesByBranchRefForArchive`. This maintains consistency in how data is retrieved for archives.



Also applies to: 126-130

---

`193-193`: **Good type safety enhancement in method signature.**

The addition of type declarations for `$projectDir` (string) and `$fileName` (string) in the `writeToFile` method improves code clarity and type safety.

---

`228-228`: **Good type safety enhancement in method signature.**

The addition of the type declaration for `$outputFile` (string) in the `writeCSV` method improves code clarity and type safety.

---

`330-330`: **Good type safety enhancement in method signature.**

The addition of the type declaration for `$outputFile` (string) in the `writeJSON` method improves code clarity and type safety.

</details>
<details>
<summary>app/Services/Mapping/DataMappingService.php (8)</summary>

`51-58`: **Good type safety enhancement in method signature.**

The addition of type declarations for `$format` (string), `$type` (string), and `$formRef` (string) in the `init` method improves code clarity and type safety. Using `mixed` for `$branchRef` is appropriate as it can be null.

---

`149-149`: **Good type safety enhancement in method signature.**

The addition of type declarations for parameters in the `getMappedEntryCSV` method improves code clarity and type safety. Using `mixed` for `$userId` is appropriate as it can be of different types.

---

`251-252`: **Good type safety enhancement in method signature.**

The addition of type declarations for `$uuid` (string) and `$relationships` (array) in the `getUUIDHeadersBranch` method improves code clarity and type safety.

---

`277-277`: **Good type safety enhancement in method signature.**

The addition of type declarations for parameters in the `getMappedEntryJSON` method improves code clarity and type safety. Using `mixed` for `$userId` is appropriate as it can be of different types.

---

`433-451`: **Excellent documentation enhancement.**

The added detailed DocBlock for the `parseAnswer` method significantly improves code documentation by clearly explaining the method's purpose, parameters, and return value. The type declarations in the method signature also enhance type safety.

---

`526-532`: **Improved error logging with better context.**

The enhancement to the error logging in the `parseAnswer` method, including the `$answer` variable in the log context, provides more information for debugging. Changing the log level from `debug` to `info` ensures better visibility of these messages.

---

`704-705`: **Good type safety enhancement in method signature.**

The addition of the type declaration for `$mysqlDate` (string) in the `convertMYSQLDateToISO` method improves code clarity and type safety.

---

`721-722`: **Good type safety enhancement in method signature.**

The addition of type declarations for `$access` (string) in the `addCreatedByIfNeeded` method improves code clarity and type safety. Using `mixed` for `$userId` is appropriate as it can be of different types.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:49:43Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<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 c1430ff9efa01e5ae866178528e45a25d861ab92 and 1547d99f2a8958f6f95033ad9552871b5f03282c.

</details>

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

* `app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php` (3 hunks)
* `app/Http/Controllers/Api/Entries/View/ViewEntriesDataController.php` (3 hunks)

</details>

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

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

`14-14`: **Import Statement for Builder Added**

The new import for `Illuminate\Database\Query\Builder` is a clear and beneficial update. It improves readability in the method annotations by using an alias instead of the fully-qualified name.

---

`71-71`: **Consistent Return Type Annotation for `runQueryBranch`**

Similarly, updating the docblock for `runQueryBranch` to `@return Builder` improves consistency and readability. The changes ensure that both methods communicate the correct return type to developers and static analysis tools.

</details>
<details>
<summary>app/Http/Controllers/Api/Entries/View/ViewEntriesDataController.php (4)</summary>

`9-9`: **Good addition of Builder import**

Adding the explicit import for `Illuminate\Database\Query\Builder` is appropriate to support the type-hinting changes in the `sendCSVResponse` method.

---

`292-293`: **Improved type documentation**

Updating the parameter type from `mixed` to `Builder` in the documentation provides better clarity about the expected parameter type, which aligns with the method signature change.

---

`299-300`: **Enhanced type safety with explicit parameter types**

Adding explicit type declarations (`Builder $query, array $params`) improves type safety and makes the method's requirements clear to callers.

---

`336-336`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify the removal of `$data` parameter in `toCSVStream()`**

The `$data` file handle is no longer passed to `Response::toCSVStream()`. Ensure that the `toCSVStream()` method has been refactored elsewhere to properly handle the output buffer without requiring the file handle parameter.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the implementation of Response::toCSVStream() method
# to verify it can work without the file handle parameter

# Look for the Response facade or class with toCSVStream method
rg "function toCSVStream" --type php
```

Length of output: 38

---



</details>

**Action Required: Confirm the Refactoring of `toCSVStream()`**

The recent verification did not locate any implementation of `toCSVStream()` in the codebase. Please manually verify that the `Response::toCSVStream()` method has been refactored to handle the output buffer without requiring a `$data` parameter, and that its usage in the controller is correct.

- Verify that the updated `toCSVStream()` method exists in the `Response` class (or its respective implementation file) and that it no longer accepts a `$data` parameter.
- Confirm that any dependencies or caller functions have been adjusted to accommodate this refactor.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T10:57:27Z

Review comments:
diff_hunk: @@ -0,0 +1,20 @@
+<?php
+
+namespace ec5\Providers\Macros\Response;
+
+use Illuminate\Support\Facades\Response;
+use Illuminate\Support\ServiceProvider;
+
+class ToTXTFileMacro extends ServiceProvider
+{
+    public function boot(): void
+    {
+        Response::macro('toTXTFile', function ($content, $filename) {
+            $headers = [
+                'Content-type' => 'text/plain; charset=utf-8',
+                'Content-Disposition' => 'attachment; filename="' . $filename . '"',
+            ];
+            return Response::make($content, 200, $headers);
+        });
+    }
+}
path: app/Providers/Macros/Response/ToTXTFileMacro.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**New TXT file response macro implemented correctly**

The implementation follows the established pattern for response macros in the application. The macro properly sets content type and disposition headers for text file downloads.


One suggestion worth considering:

```diff
-                'Content-type' => 'text/plain; charset=utf-8',
+                'Content-Type' => 'text/plain; charset=utf-8',
```

The capitalization of "Content-Type" is more commonly used in HTTP standards, though both forms work in practice.



<!-- 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
<?php

namespace ec5\Providers\Macros\Response;

use Illuminate\Support\Facades\Response;
use Illuminate\Support\ServiceProvider;

class ToTXTFileMacro extends ServiceProvider
{
    public function boot(): void
    {
        Response::macro('toTXTFile', function ($content, $filename) {
            $headers = [
                'Content-Type' => 'text/plain; charset=utf-8',
                'Content-Disposition' => 'attachment; filename="' . $filename . '"',
            ];
            return Response::make($content, 200, $headers);
        });
    }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T12:09:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+<?php
+
+namespace ec5\Providers\Macros\Response;
+
+use Illuminate\Support\Facades\Response;
+use Illuminate\Support\ServiceProvider;
+
+class ToTXTFileMacro extends ServiceProvider
+{
+    public function boot(): void
+    {
+        Response::macro('toTXTFile', function ($content, $filename) {
+            $headers = [
+                'Content-Type' => 'text/plain; charset=utf-8',
+                'Content-Disposition' => 'attachment; filename="' . $filename . '"',
+            ];
+            return Response::make($content, 200, $headers);
+        });
path: app/Providers/Macros/Response/ToTXTFileMacro.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider adding type hints to macro parameters.**

For better type safety and code documentation, consider adding parameter type hints:

```diff
-        Response::macro('toTXTFile', function ($content, $filename) {
+        Response::macro('toTXTFile', function (string $content, string $filename) {
```



<!-- 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
        Response::macro('toTXTFile', function (string $content, string $filename) {
            $headers = [
                'Content-Type' => 'text/plain; charset=utf-8',
                'Content-Disposition' => 'attachment; filename="' . $filename . '"',
            ];
            return Response::make($content, 200, $headers);
        });
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T13:33:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -457,6 +475,8 @@ private function parseAnswer($type, $answer, $input)
                     }
                 }
 
+
+
path: app/Services/Mapping/DataMappingService.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Minor whitespace addition.**  
Though it doesn’t affect functionality, the extra blank lines can be retained if they improve readability or removed if the team prefers a tighter format.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T13:47:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -230,8 +244,16 @@ public static function getMonthName($monthNumber): string
     }
 
     //Use a cookie to signal the download has completed and hide overlay.
-    //no need to be secured, just a timestamp
-    public static function getMediaCookie($value)
+    /**
+     * Creates a cookie for download entries using the specified value.
+     *
+     * The cookie is configured to expire at the end of the browser session and is available across the entire site.
+     * It is not marked as secure, allowing JavaScript access, and uses the 'Lax' SameSite policy.
+     *
+     * @param mixed $value The value to assign to the cookie, typically a timestamp.
+     * @return \Symfony\Component\HttpFoundation\Cookie The generated cookie instance.
+     */
+    public static function getDownloadEntriesCookie(mixed $value): \Symfony\Component\HttpFoundation\Cookie
path: app/Libraries/Utilities/Common.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider enabling secure cookie over HTTPS**  
For added security, enable the secure flag when the application is served over HTTPS.  

```diff
- false,           // Secure flag: should be true if using HTTPS
+ true,            // Secure flag: only sent over HTTPS in production
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,20 +109,22 @@ public function getBranchEntriesByBranchRef(int $projectId, array $params, $colu
     }
 
     /**
-     * Retrieves a query builder for branch entries matching specific criteria.
+     * Retrieves branch entries fpr archive downloads
+     *
+     * This method builds a query on the branch entries table (as defined in configuration)
+     * filtered by the provided project ID, form reference, and branch reference.
+     * It ensures tha the 'id' column is always included in the selection.
      *
-     * This static method builds a query on the branch entries table (as defined in configuration)
-     * filtered by the provided project ID, form reference, and branch reference. It ensures that
-     * the 'id' column is always included in the selection.
+     *  No sorting as this is for downloading archive only
      *
-     * @param mixed $projectId The identifier of the project.
+     * @param int $projectId The identifier of the project.
      * @param array $params Associative array with keys:
      *                      - 'form_ref': The form reference used for filtering.
      *                      - 'branch_ref': The branch (owner input) reference for filtering.
      * @param array $columns The list of columns to select; defaults to all columns.
-     * @return \Illuminate\Database\Query\Builder The query builder instance for the branch entries.
+     * @return Builder The query builder instance for the branch entries.
path: app/Models/Entries/BranchEntry.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Minor docblock typos**  
Correct “fpr” → “for” and “tha” → “that” for improved readability.  

```diff
-     * Retrieves branch entries fpr archive downloads
+     * Retrieves branch entries for archive downloads

-  * It ensures tha the 'id' column is always included
+  * It ensures that the 'id' column is always included
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,7 +60,7 @@ public function test_should_fails_if_user_not_logged_in()
             ]
         );
 
-        $response = $this->get('api/internal/download-entries/' . $project->slug)
+        $this->get('api/internal/download-entries/' . $project->slug)
path: tests/Http/Controllers/Api/Entries/DownloadControllerTest.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**HTTP 400 for unauthenticated user.**

This test enforces a 400 status code if the user is not logged in while attempting to download entries. Although 401 or 403 are more common for unauthenticated requests, this may be intentional per project requirements.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -132,6 +138,7 @@ public function test_download_json_public()
         }
         $this->assertEquals($zipName, $extractedFilename);
         // Get the response content as a file
+        /** @noinspection PhpUndefinedMethodInspection */
path: tests/Http/Controllers/Api/Entries/DownloadControllerTest.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**IDE-specific suppression comment.**

`/** @noinspection PhpUndefinedMethodInspection */` is fine but can be handled through IDE settings or ignored in commit if this is purely local.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,6 +202,7 @@ public function test_download_json_private()
         }
         $this->assertEquals($zipName, $extractedFilename);
         // Get the response content as a file
+        /** @noinspection PhpUndefinedMethodInspection */
path: tests/Http/Controllers/Api/Entries/DownloadControllerTest.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Repeated IDE suppression note.**

Same observation as line 141 regarding `@noinspection`. Consider removing or moving IDE-specific comments to local settings.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -299,6 +328,35 @@ public function test_download_csv_private()
                 'total_entries' => 0
             ]
         );
+
+        //generate entries by creator
+        $entryGenerator = new EntryGenerator($projectDefinition);
+        $formRef = array_get($projectDefinition, 'data.project.forms.0.ref');
+        $entryPayloads = [];
+        for ($i = 0; $i < 10; $i++) {
+            Auth::login($user);
+            $entryPayloads[$i] = $entryGenerator->createParentEntryPayload($formRef);
+            $entryRowBundle = $entryGenerator->createParentEntryRow(
+                $user,
+                $project,
+                config('epicollect.strings.project_roles.creator'),
+                $projectDefinition,
+                $entryPayloads[$i]
+            );
+            Auth::logout();
+
+            $this->assertEntryRowAgainstPayload(
+                $entryRowBundle,
+                $entryPayloads[$i]
+            );
+        }
+
+        //assert rows are created
+        $this->assertCount(
+            10,
+            Entry::where('project_id', $project->id)->get()
+        );
path: tests/Http/Controllers/Api/Entries/DownloadControllerTest.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Loop-based entry creation.**

Repeated login/logout within the loop might introduce slight performance overhead. If not necessary for test logic, consider logging in once before the loop.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -739,7 +811,10 @@ private function assertZipContent($filePath, $extension, $filesCountForm, $files
 
         // Close the zip file
         $zip->close();
-        // Assert that at least one JSON file was found in the zip
+        // Assert that at least one file was found in the zip
         $this->assertTrue($fileFound);
+
+        //assert the file is not an error file
path: tests/Http/Controllers/Api/Entries/DownloadControllerTest.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Confirming non-error ZIP content.**

The assertion ensures the file is found in the ZIP. Consider also checking for a possible `.txt` error file if your system sometimes returns text content for errors.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,21 +64,19 @@ public function index(Request $request, RuleDownload $ruleDownload, EntriesViewS
     /**
      * Sends an archive file as a download and deletes it after sending, or returns an error if the file is missing.
      *
-     * This method checks if the file at the specified path exists. If it does, it queues a media cookie based on the
-     * provided timestamp and initiates a download response that deletes the file once sent. If the file cannot be found,
+     * This method checks if the file exists at the specified path. If it does, it queues a download entries cookie based on
+     * the provided timestamp and returns a download response that deletes the file after sending. If the file cannot be found,
      * it returns an error response with a designated error code.
      *
      * @param string $filepath The path to the archive file.
-     * @param string $filename The name that will be used for the downloaded file.
-     * @param mixed|null $timestamp Optional timestamp used for generating a media cookie and error response.
-     *
-     * @return \Illuminate\Http\Response The download response or an error response.
+     * @param string $filename The name to be used for the downloaded file.
+     * @param mixed $timestamp Optional timestamp used for generating the download entries cookie and error response.
      */
-    private function sendArchive($filepath, $filename, $timestamp = null)
+    private function sendArchive(string $filepath, string $filename, mixed $timestamp = null)
path: app/Http/Controllers/Api/Entries/DownloadController.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Method signature updated with mixed parameter.**

While `mixed $timestamp` offers flexibility, specifying an int|string union might reflect the 13-digit requirement more precisely.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -99,7 +98,7 @@ private function sendArchive($filepath, $filename, $timestamp = null)
      * @param mixed $timestamp A timestamp used for file naming and error response consistency.
      * @return mixed The response from sending the archive file or an error response.
      */
-    private function createArchive(string $projectDir, array $params, $timestamp)
+    private function createArchive(string $projectDir, array $params, mixed $timestamp)
path: app/Http/Controllers/Api/Entries/DownloadController.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Method signature updated with mixed param.**

Similar to `sendArchive()`, consider if `$timestamp` can be narrowed to a numeric or string type.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-06T17:49:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,7 +32,7 @@ public function __construct(
     /**
      * @param array $params
      * @param array $columns
-     * @return \Illuminate\Database\Query\Builder
+     * @return Builder
path: app/Http/Controllers/Api/Entries/View/ViewEntriesControllerBase.php
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Updated Docblock Return Type for `runQueryHierarchy`**

The docblock now specifies `@return Builder`, which aligns with the added import and clarifies the method’s expected return type. Consider, if your project permits, enforcing this through a return type declaration for enhanced type safety.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-07T10:57:26Z
author_association: CONTRIBUTOR

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

## Walkthrough
This pull request updates cookie handling across multiple controllers by replacing calls to retrieve a media cookie with calls to retrieve a download entries cookie. The changes also include method renames and updates to type declarations in utility and model classes, with documentation adjustments to reflect the new behavior. Additionally, the file download service enforces stricter type checks, and error responses have been modified to return TXT content. A new service provider for TXT file responses has been added, and relevant test cases have been enhanced, including a change in authentication behavior in one test.

## Changes

| File(s) | Change Summary |
| --- | --- |
| `app/.../DownloadController.php`, `DownloadSubsetController.php`, `DownloadTemplateController.php` | Replaced `getMediaCookie` with `getDownloadEntriesCookie` in cookie handling; updated documentation comments accordingly. |
| `app/.../Common.php` | Renamed `getMediaCookie` to `getDownloadEntriesCookie`; added return type for `generateRandomHex`; updated `errorResponseAsFile` to return a string with TXT content type; updated docblock for `getCGPSEpicollectVersion` to indicate potential `GuzzleException`. |
| `app/.../BranchEntry.php`, `Entry.php` | Renamed data retrieval methods to their `ForArchive` variants; changed method visibility from static to public; added explicit parameter types and updated documentation comments. |
| `app/.../ToTXTFileMacro.php` | Added a new service provider that registers the `toTXTFile` macro for generating TXT file responses. |
| `app/.../EntriesDownloadService.php` | Enforced stricter type checking in method signatures (`createArchive` and `writeCSV`); prepended the `'id'` column to CSV output; updated calls to use the `ForArchive` data retrieval methods. |
| `app/.../DataMappingService.php` | Updated logging level in `parseAnswer` (from debug to info) and included the answer context; added type declarations to methods `convertMYSQLDateToISO` and `addCreatedByIfNeeded`; adjusted whitespace for readability. |
| `config/app.php` | Added a new provider entry for `ToTXTFileMacro`. |
| `tests/.../DownloadControllerTest.php` | Enhanced project creation and entry generation logic; updated assertions to verify the archive download content structure (forms and branches) and entry count. |
| `tests/.../EntriesExportPrivateCreatorTest.php` | Modified authentication in the test by replacing `Auth::login($user)` with `Auth::logout()`, affecting export behavior under unauthenticated conditions. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as User
    participant DC as DownloadController
    participant C as Common
    participant FS as FileSystem

    U->>DC: Request archive download
    DC->>C: getDownloadEntriesCookie(timestamp)
    DC->>FS: Check if file exists
    alt File exists
        DC->>U: Return file download with cookie
    else File missing
        DC->>C: errorResponseAsFile(timestamp, code)
        DC->>U: Return error response (TXT)
    end
```

## Possibly related PRs
- **epicollect5/epicollect5-server#4**: The changes in the main PR regarding the `sendArchive` method in the `DownloadController` are related to the adjustments made in the retrieved PR, which also modifies the `sendArchive` method to streamline error handling and cookie management.
- **epicollect5/epicollect5-server#8**: The changes in the main PR regarding the `sendArchive` method in the `DownloadController` and the updates to cookie handling are related to the modifications in the `DownloadSubsetController`, which also involve changing cookie management from media to download entries, indicating a direct connection at the code level.

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

---



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

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=epicollect5/epicollect5-server&utm_content=10):

- 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: coderabbitai[bot]
created_at: 2025-03-06T12:18:15Z
body: > [!NOTE]
> Generated docstrings for this pull request at https://github.com/epicollect5/epicollect5-server/pull/11
author_association: CONTRIBUTOR
user: login: mirko77
created_at: 2025-03-07T10:46:38Z
body: @coderabbitai review last commits
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-03-07T10:47:00Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
@mirko77 I'll review the latest commits in this PR. Based on the summary I can see this includes several refactoring changes across multiple files in the codebase.

<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR