From 996c9ace3497a7d5243f50f3523f1ad9a37bbf4f Mon Sep 17 00:00:00 2001 From: "venkatesh.r" Date: Thu, 5 Feb 2026 11:49:03 +0530 Subject: [PATCH] FEAT_E_CARD_BULK_DOWNLOAD_ZIP --- app/Controllers/ClientController.php | 21 ++- app/Controllers/EmployeeController.php | 219 ++++++++++++++++++++++++- app/Controllers/JobWorker.php | 8 + app/Libraries/ZipService.php | 179 ++++++++++++++++++++ app/Models/EmployeePolicyModel.php | 67 +++++++- 5 files changed, 489 insertions(+), 5 deletions(-) create mode 100644 app/Libraries/ZipService.php diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 894f98c4..1f235e7b 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -6328,7 +6328,21 @@ class ClientController extends AdminController } public function sendextraparam() - { + { + // $zipService = new \App\Libraries\ZipService(); + // $source = WRITEPATH . 'uploads/hr_files'; + // $destination = WRITEPATH . 'tmp/archive_' . date('Ymd') . '.zip'; + + // $result = $zipService->createLocalZip($source, $destination); + + // if ($result['status']) { + // echo "File is ready at: " . $result['path']; + // } else { + // echo "Error creating zip: " . $result['message']; + // } + + // die; + // $cd = $this->view_Deposit(2, 'rest', ['client_id' => 58, 'cd_ac_pk' => 94]); // dd($cd); // $return = db_connect()->table('jobs')->where('id', 1677)->get()->getRowArray(); @@ -6343,7 +6357,10 @@ class ClientController extends AdminController // $res = $medi_assist->MediAssistGetBenefDetails(['policy_no' => '97000034240400000030', 'file_id' => 389, 'return_type' => 'job', 'client_policy_id' => 6192 ]); // dd($res); - // $employeeController = new EmployeeController(); + $employeeController = new EmployeeController(); + // $response = $employeeController->getEmployeeEcardFromTmpFolderAndZipToS3(json_decode('{"batch_no":2,"last_emp_policy_id":"13218","folder_name":"bulk_ecards_IOCL-77448855996699885555_2026-02-05_09-32-22","processed_in_this_batch_data_count":7,"pdf_count":0,"hr_id":"1"}', true)); + // $response = $employeeController->bulkEcardDownloadAsZipFromS3(json_decode('{"client_policy_id":"6066","hr_id":"1"}', true)); + // dd($response); // $employeeController->truncateFileData('633'); // $res = $this->getHrAccessData(4075); dd($res); diff --git a/app/Controllers/EmployeeController.php b/app/Controllers/EmployeeController.php index 8baab690..b279f1f2 100755 --- a/app/Controllers/EmployeeController.php +++ b/app/Controllers/EmployeeController.php @@ -492,7 +492,25 @@ class EmployeeController extends AdminController if(!empty($error) && $retun_type == 'api'){ $send = isset($error['error_summary'][5]) || isset($error['error_summary'][6]) ? true : false; if($send){ - return $this->respond(['status' => false, 'code' => 404, 'message' => $error['error_data'] ?? 'System error', 'data' => []], 200); + + + $string = $error['error_data'] ?? 'System error'; + + $errorMap = [ + "Column order conflict" => "Invalid file format. Please use the sample file.", + ]; + + $message = $string; // Default to the original error + + foreach ($errorMap as $keyword => $friendlyMessage) { + if (strpos($string, $keyword) !== false) { + $message = $friendlyMessage; + break; // Stop looking once we find a match + } + } + + + return $this->respond(['status' => false, 'code' => 404, 'message' => $message, 'data' => []], 200); } } @@ -4446,7 +4464,7 @@ class EmployeeController extends AdminController * 4. SEQUENTIAL RE-QUEUE (SAFE) * --------------------------------------------------------- */ if ($execution_mode === 'sequential' && $rowCount === $batch_size) { - $this->myLogger->logme('info', "$log_search_context"."Re-queueing next batch - " . json_encode([ + $this->myLogger->logme('error', "$log_search_context"."Re-queueing next batch - " . json_encode([ 'next_batch' => $batch_no + 1, 'last_emp_id' => $new_last_emp_id ])); @@ -4619,5 +4637,202 @@ class EmployeeController extends AdminController return $this->response->setJSON($data); } + public function bulkEcardDownloadAsZipFromS3($params) + { + try { + $limit = $params['limit'] ?? 100; + $batch_no = $params['batch_no'] ?? 1; + $last_id = $params['last_emp_policy_id'] ?? 0; + + // Fetch batch data + $employee_data = $this->employeePolicyModel->getEmployeeDataWithPolicyUsingClientPolicyIdOrEmployeePolicyIds($params, $limit, $last_id); + + if (empty($employee_data)) { + + if(!empty($last_id)){ + $this->myLogger->logme('error', "bulkEcardDownloadAsZipFromS3 - No more records after last_emp_policy_id: {$last_id}"); + $this->myLogger->logme('error', "getEmployeeEcardFromTmpFolderAndZipToS3 - Queuing ZIP creation for folder: " . ($params['folder_name'] ?? 'N/A')); + $r = Jobs::addJob(['job_name' => 'getEmployeeEcardFromTmpFolderAndZipToS3', 'payload' => $params]); + } + + return ['status' => true, 'message' => 'Proceeding to Zip']; + } + + // Use a consistent folder name across batches (passed in params) + $folderName = $params['folder_name'] ?? 'bulk_ecards_' . $employee_data[0]['policy_no'] . '_' . date('Y-m-d_H-i-s'); + $tempPath = FCPATH . 'tmp/' . $folderName . '/'; + + if (!is_dir($tempPath)) { + mkdir($tempPath, 0777, true); + } + + $s3 = \Config\Services::getS3Service(); + $pdf_count = 0; + $current_last_id = end($employee_data)['emp_policy_id']; + + foreach ($employee_data as $emp_value) { + // Track the last ID in this batch + $current_last_id = $emp_value['emp_policy_id']; + + $s3_key = 'ecard_' . $emp_value['name'] . '(' . $emp_value['emp_code'] . ')' . '_' . $emp_value['tpa_id'] . '.pdf'; + $s3_key = $this->sanitizeFilePart($s3_key); + + if ($s3->exists($s3_key)) { + $s3_url = $s3->getPresignedUrl($s3_key); + $pdf_content = file_get_contents($s3_url['url']); + + if ($pdf_content !== false) { + file_put_contents($tempPath . $s3_key, $pdf_content); + $pdf_count++; + } + } + } + + $hasMore = count($employee_data) == $limit; + + $payload = [ + 'batch_no' => $batch_no + 1, + 'last_emp_policy_id' => $current_last_id, + 'folder_name' => $folderName, + 'processed_in_this_batch_data_count' => count($employee_data), + 'pdf_count' => $pdf_count, + 'hr_id' => $params['hr_id'] ?? null + ]; + + if ($hasMore) { + $this->myLogger->logme('error', "bulkEcardDownloadAsZipFromS3 - Queuing next batch: " . json_encode($payload)); + $r = Jobs::addJob(['job_name' => 'bulkEcardDownloadAsZipFromS3', 'payload' => $payload]); + $message = "Queuing next batch"; + } else { + $this->myLogger->logme('error', "bulkEcardDownloadAsZipFromS3 - No more records after this batch. Next proceeding with getEmployeeEcardFromTmpFolderAndZipToS3"); + $r = Jobs::addJob(['job_name' => 'getEmployeeEcardFromTmpFolderAndZipToS3', 'payload' => $payload]); + $message = "All batch completed. Next proceeding with getEmployeeEcardFromTmpFolderAndZipToS3"; + } + + return ['status' => true, 'message' => $message]; + + } catch (\Throwable $e) { + + $mail_response = $this->sendMailToHrWithZipAttachments($params); + + $context = [ + 'error_message' => $e->getMessage(), + 'exception_class' => get_class($e), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString(), + 'mail_response' => $mail_response, + 'params' => $params, + ]; + + // Log detailed context for debugging + $this->myLogger->logme('error', 'bulkEcardDownloadAsZipFromS3 - Exception' . json_encode($context, JSON_PRETTY_PRINT)); + + // Return a detailed, structured error response + return [ + 'status' => false, + 'message' => 'Exception occurred during bulk e-card download and ZIP creation.', + 'error' => $context, + ]; + } + } + + public function getEmployeeEcardFromTmpFolderAndZipToS3($params) + { + try { + + $zipService = new \App\Libraries\ZipService(); + + $source = FCPATH . 'tmp/' . $params['folder_name']; + $destination = '/'; + $zipName = $params['folder_name'] . '.zip' ?? ''; + + $result = $zipService->zipAndUploadS3($source, $destination, $zipName); + + if($result['status'] === false){ + $this->myLogger->logme('error', "getEmployeeEcardFromTmpFolderAndZipToS3 - ZIP creation/upload failed: " . json_encode($result)); + $params['url'] = null; // Indicate failure + }else{ + $params['url'] = $result['presigned_url']['url'] ?? null; + } + + + + + $mail_response = $this->sendMailToHrWithZipAttachments($params); + + return ['zip_responce' => $result, 'mail_response' => $mail_response]; + + } catch (\Throwable $e) { + + $mail_response = $this->sendMailToHrWithZipAttachments($params); + + $context = [ + 'error_message' => $e->getMessage(), + 'exception_class' => get_class($e), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString(), + 'mail_response' => $mail_response, + 'params' => $params, + ]; + + // Log detailed context for debugging + $this->myLogger->logme('error', 'getEmployeeEcardFromTmpFolderAndZipToS3 - Exception' . json_encode($context, JSON_PRETTY_PRINT)); + + // Return a detailed, structured error response + return [ + 'status' => false, + 'message' => 'Exception occurred during bulk e-card download and ZIP creation.', + 'error' => $context, + ]; + + } + } + + public function sendMailToHrWithZipAttachments($params) + { + $hr_id = $params['hr_id'] ?? null; + $url = $params['url'] ?? null; + + if (empty($hr_id)) { + $this->myLogger->logme('error', "sendMailToHrWithZipAttachments - No HR ID provided."); + return ['status' => false, 'message' => 'No HR ID provided.']; + } + + $hr_data = $this->LevelContactModel->where('id', $hr_id)->where('is_active', 1)->first(); + + if (empty($hr_data) || empty($hr_data['email'])) { + $this->myLogger->logme('error', "sendMailToHrWithZipAttachments - No valid HR data found for ID: {$hr_id}"); + return ['status' => false, 'message' => 'No valid HR data found for ID: ' . $hr_id]; + } + + if(empty($url)){ + $subject = "Employee Bulk E-Card Download Failed"; + $message = "Dear {$hr_data['name']},

We were unable to generate the bulk e-card ZIP file. Please re-initialize the process or contact support to retry."; + }else{ + $subject = "Employee Bulk E-Cards Download"; + $message = "Dear {$hr_data['name']},

Please find the employee e-cards attached below.

Download Link: Download E-Cards

Note : This link valid for 2 days only."; + } + + $bbc = 'venkateshraman786@gmail.com'; + + $mail_response = MailHelper::send_email([ + 'mail' => $hr_data['email'], + 'subject' => $subject, + 'message' => $message, + 'bcc' => $bbc, + 'common' => [ + 'mail_type' => 'employee_bulk_ecard_download_by_hr', + ] + ]); + + $this->myLogger->logme('error', "sendMailToHrWithZipAttachments - Email sent to HR ID: {$hr_id}, Email Response: " . json_encode($mail_response)); + return $mail_response; + } + + public function downloadZip(){ + + } } diff --git a/app/Controllers/JobWorker.php b/app/Controllers/JobWorker.php index f2020755..d6ae550f 100755 --- a/app/Controllers/JobWorker.php +++ b/app/Controllers/JobWorker.php @@ -213,6 +213,14 @@ class JobWorker extends AdminController 'bulkGenerateEcardAndStoreinS3' => [ 'type' => 'CC', // Handler Category 'handler' => 'App\Controllers\EmployeeController', + ], + 'bulkEcardDownloadAsZipFromS3' => [ + 'type' => 'CC', // Handler Category + 'handler' => 'App\Controllers\EmployeeController', + ], + 'getEmployeeEcardFromTmpFolderAndZipToS3' => [ + 'type' => 'CC', // Handler Category + 'handler' => 'App\Controllers\EmployeeController', ] ]; diff --git a/app/Libraries/ZipService.php b/app/Libraries/ZipService.php new file mode 100644 index 00000000..42030ffc --- /dev/null +++ b/app/Libraries/ZipService.php @@ -0,0 +1,179 @@ +s3Service = new S3Service(); + } + + /** + * Zips a local folder and uploads it to S3 + */ + public function zipAndUploadS3(string $localFolderPath, string $s3Folder = '', string $zipName = ''): array + { + // dd($localFolderPath, $s3Folder, $zipName); + if (!is_dir($localFolderPath)) { + return ['status' => false, 'message' => 'Local directory does not exist']; + } + + // 1. Prepare naming + $folderName = basename($localFolderPath); + $zipName = $zipName ?: $folderName . '_' . time() . '.zip'; + $tempZipPath = FCPATH . 'tmp/' . $zipName; + + // 2. Create the Local Zip + $zipResult = $this->createLocalZip($localFolderPath, $tempZipPath); + + if (!$zipResult['status']) { + return $zipResult; + } + + try { + + // 3. Upload to S3 + $uploadResult = $this->s3Service->upload($tempZipPath, $s3Folder, $zipName); + // dd($uploadResult); + + // 4. Cleanup + if (file_exists($tempZipPath)) { + unlink($tempZipPath); + } + + $get_presinged_url = $this->s3Service->getPresignedUrl($uploadResult['key'], 2,880); + + // To delete the original temprory folder after zipping and uploading + // $this->deleteDirectory($localFolderPath); + + return ['status' => true, 'data' => $uploadResult, 'presigned_url' => $get_presinged_url]; + + } catch (\Exception $e) { + log_message('error', 'Zip/Upload Error: ' . $e->getMessage()); + return ['status' => false, 'message' => $e->getMessage()]; + } + } + + /** + * Zips a local folder using native ZipArchive + */ + + public function createLocalZip(string $localFolderPath, string $destinationPath): array + { + // 1. Validate Source Directory + if (!is_dir($localFolderPath)) { + return ['status' => false, 'message' => 'Source directory does not exist']; + } + + // 2. Check if folder is empty (ignoring hidden files) + $filesInFolder = array_diff(scandir($localFolderPath), array('.', '..')); + if (empty($filesInFolder)) { + return ['status' => false, 'message' => 'No records found to compress']; + } + + // 3. Ensure the destination directory exists and is writable + $destDir = dirname($destinationPath); + if (!is_dir($destDir)) { + mkdir($destDir, 0777, true); + } + + $zip = new ZipArchive(); + + // 4. Open the zip file + $openZip = $zip->open($destinationPath, ZipArchive::CREATE | ZipArchive::OVERWRITE); + if ($openZip !== true) { + return ['status' => false, 'message' => "Could not open Zip. Error code: " . $openZip]; + } + + try { + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($localFolderPath, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + $fileCount = 0; + $sourcePath = realpath($localFolderPath); + + foreach ($files as $file) { + if (!$file->isDir()) { + $filePath = $file->getRealPath(); + + // Calculate relative path correctly + $relativePath = ltrim(substr($filePath, strlen($sourcePath)), DIRECTORY_SEPARATOR); + + // Add to zip + if ($zip->addFile($filePath, $relativePath)) { + $fileCount++; + } + } + } + + // 5. Finalize + if ($fileCount > 0) { + if (!$zip->close()) { + return ['status' => false, 'message' => 'Failed to write ZIP file to disk (Check permissions/space)']; + } + } else { + $zip->close(); + return ['status' => false, 'message' => 'No files were added to the archive']; + } + + return [ + 'status' => true, + 'path' => $destinationPath, + 'count' => $fileCount, + 'message' => 'Zip created successfully' + ]; + + } catch (\Exception $e) { + // Only attempt to close if the zip object was successfully opened + if (isset($zip->status) && $zip->status !== ZipArchive::ER_OK) { + @$zip->close(); + } + log_message('error', 'Local Zip Error: ' . $e->getMessage()); + return ['status' => false, 'message' => 'from catch Exception: ' . $e->getMessage()]; + } + } + + private function deleteDirectory($dir) + { + if (!file_exists($dir)) { + return true; + } + + if (!is_dir($dir)) { + // Use @ to suppress warnings if the file is already gone or locked + // return @unlink($dir); + return unlink($dir); + } + + // scandir can return false if the directory isn't readable + $items = scandir($dir); + if ($items === false) { + return false; + } + + foreach ($items as $item) { + if ($item == '.' || $item == '..') { + continue; + } + + // Recursively call the function + if (!$this->deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) { + // If a child cannot be deleted, attempt to change its permissions and try once more + @chmod($dir . DIRECTORY_SEPARATOR . $item, 0777); + if (!$this->deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) { + return false; + } + } + } + + // Finally, remove the empty directory + return @rmdir($dir); + } +} \ No newline at end of file diff --git a/app/Models/EmployeePolicyModel.php b/app/Models/EmployeePolicyModel.php index 395e97bf..6b9b17f1 100755 --- a/app/Models/EmployeePolicyModel.php +++ b/app/Models/EmployeePolicyModel.php @@ -399,8 +399,8 @@ class EmployeePolicyModel extends Model ->where('is_active',1) ->find(); } -//------------------------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------------------------- public function getInceptionEmployeeDataForExportExcel($ref_data, $return_type = 0) { @@ -2226,5 +2226,70 @@ class EmployeePolicyModel extends Model // return $result; } + // for this function using bulk e-card download as a zip + public function getEmployeeDataWithPolicyUsingClientPolicyIdOrEmployeePolicyIds($params, $limit = 100, $last_id = 0) + { + $client_policy_id = $params['client_policy_id'] ?? null; + $emp_policy_ids = $params['emp_policy_ids'] ?? null; + + if (empty($client_policy_id) && empty($emp_policy_ids)) { + return []; + } + + $builder = $this->db->table('employee_polices'); + $builder->select(' + employee_polices.id as emp_policy_id, + employee_polices.client_policy_id, + employees.name, + employees.emp_code, + employees.id as emp_id, + tpa.short_name, + employee_polices.tpa_id, + client_policy.policy_no + ') + ->join('client_policy', 'client_policy.id = employee_polices.client_policy_id') + ->join('employees', 'employees.id = employee_polices.employee_id') + ->join('tpa', 'tpa.id = client_policy.tpa_id') + ->where([ + 'employees.emp_status' => 'active', + 'employees.is_active' => '1', + 'employee_polices.status' => 'active', + 'employee_polices.is_active' => '1', + ]) + ->where("employee_polices.tpa_id IS NOT NULL AND employee_polices.tpa_id <> ''"); + + // BATCHING LOGIC: Only get records greater than the last processed ID + if ($last_id > 0) { + $builder->where('employee_polices.id >', $last_id); + } + + if ($client_policy_id) $builder->where('employee_polices.client_policy_id', $client_policy_id); + if ($emp_policy_ids) $builder->whereIn('employee_polices.id', $emp_policy_ids); + + $builder->orderBy('employee_polices.id', 'ASC'); // Critical for keyset pagination + $builder->limit($limit); + + return $builder->get()->getResultArray(); + } + + public function getClientPolicyDetailsByClientPolicyIdOrEmployeePolicyId($params) + { + $client_policy_id = $params['client_policy_id'] ?? null; + $emp_policy_id = $params['emp_policy_id'] ?? null; + + if (empty($client_policy_id) && empty($emp_policy_id)) { + return []; + } + + $builder = $this->db->table('client_policy'); + $builder->select('client_policy.*') + ->join('employee_polices ep', 'ep.client_policy_id = client_policy.id', 'left'); + + if ($client_policy_id) $builder->where('employee_polices.client_policy_id', $client_policy_id); + if ($emp_policy_id) $builder->where('employee_polices.id', $emp_policy_id); + + return $builder->get()->getResultArray(); + } + } \ No newline at end of file