diff --git a/.gitignore b/.gitignore index adeb2bd5..91df762f 100755 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ writable/**/*.sqlite writable/tmp/* !writable/tmp/.gitkeep +writable/insurer_claim_form/* +!writable/insurer_claim_form/Claim_Form_IRDAI.pdf + vendor/ build/ composer.lock diff --git a/app/Config/Constants.php b/app/Config/Constants.php index c9ed0960..b9691662 100755 --- a/app/Config/Constants.php +++ b/app/Config/Constants.php @@ -133,3 +133,5 @@ define('UPLOAD_EXT_EXCEL', ['xls', 'xlsx', 'ods', 'csv']); define('UPLOAD_EXT_MAIL_ATTACHMENTS', ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx']); define('UPLOAD_EXT_NON_EB_RACK_RATE', ['pdf', 'xls', 'xlsx']); define('UPLOAD_EXT_ASSET_FILES', ['pdf', 'xls', 'xlsx', 'csv']); +define('UPLOAD_EXT_INSURER_CLAIM_FORM', ['pdf', 'doc', 'docx']); +define('INSURER_DEFAULT_CLAIM_FORM_FILE', 'Claim_Form_IRDAI.pdf'); diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 234b82a9..e18e3df7 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -257,6 +257,7 @@ $routes->group("/master", ["filter" => "authMVC"], function ($routes) { $routes->get("create", "MasterController::insurerOnboarding"); $routes->post("createpost", "MasterController::createInsurerGeneralInfo"); $routes->post("edit", "MasterController::editInsurerGeneralInfo"); + $routes->get("download-claim-form/(:num)", "MasterController::downloadInsurerClaimForm/$1"); $routes->get("list/(:any)", "MasterController::editInsurerOnboarding/$1"); $routes->group("branch", ["filter" => "authMVC"], function ($routes) { @@ -464,6 +465,8 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('insertSampleTpaApiData/(:any)', 'TestingController::insertSampleTpaApiData/$1'); $routes->get('listEmployeeCountByClientPolicy', 'TestingController::listEmployeeCountByClientPolicy'); $routes->get('testMediAssistWellness','TestingController::testMediAssistWellness'); + $routes->match(['get', 'post'], 'decryptVisitSso', 'ApiServiceController::decryptVisitSso'); // disabled for now do not remove this + $routes->get("getInsurerClaimFormDownloadUrl", "EmployeeRestController::getInsurerClaimFormDownloadUrl"); $routes->group('claims-collection-v2', static function ($routes) { $routes->get('preview', 'ClaimsCollectionV2DashboardController::preview'); @@ -750,6 +753,7 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->get("cdTransactionData", "EmployeeRestController::cdTransactionData"); $routes->match( ['get', 'post'], 'claimsSearch','EmployeeRestController::claimsSearch'); $routes->get("claimView", "EmployeeRestController::claimView"); + $routes->get("getInsurerClaimFormDownloadUrl", "EmployeeRestController::getInsurerClaimFormDownloadUrl"); $routes->get("exportCashDepositData", "EmployeeRestController::exportCashDepositData"); $routes->get("hrFileList", "EmployeeRestController::hrFileList"); $routes->get("hrFileUploadMasters", "EmployeeRestController::hrFileUploadMasters"); diff --git a/app/Controllers/ApiServiceController.php b/app/Controllers/ApiServiceController.php index 735cbfb0..de934916 100644 --- a/app/Controllers/ApiServiceController.php +++ b/app/Controllers/ApiServiceController.php @@ -580,6 +580,86 @@ class ApiServiceController extends BaseController } } + /** + * Decrypt Visit SSO userParams (reverse of Visit encryption in getWellnessUrl). + * Accepts encrypted string or full SSO URL via POST/GET as userParams or encrypted_sso. + */ + public function decryptVisitSso() + { + $encrypted = $this->request->getGet('encrypted_sso'); + + if (empty($encrypted)) { + return $this->response->setJSON([ + 'status' => false, + 'message' => 'userParams or encrypted_sso is required', + ]); + } + + $result = $this->decryptVisitSsoParams($encrypted); + + if ($result === false) { + return $this->response->setJSON([ + 'status' => false, + 'message' => 'Decryption failed. Check encrypted value and VISIT_SECRET_KEY / VISIT_IV.', + ]); + } + + return $this->response->setJSON([ + 'status' => true, + 'data' => $result['params'], + 'plainText' => $result['plainText'], + ]); + } + + /** + * Decrypt Visit SSO payload using the same key/IV as getWellnessUrl Visit branch. + * + * @param string $encrypted Base64URL userParams value or full SSO URL containing userParams= + * @return array{plainText: string, params: array}|false + */ + private function decryptVisitSsoParams(string $encrypted) + { + if (strpos($encrypted, 'userParams=') !== false) { + $query = []; + parse_str(parse_url($encrypted, PHP_URL_QUERY) ?? '', $query); + $encrypted = $query['userParams'] ?? ''; + } + + $encrypted = trim($encrypted); + if ($encrypted === '') { + return false; + } + + $base64 = strtr($encrypted, '-_', '+/'); + $base64 .= str_repeat('=', (4 - strlen($base64) % 4) % 4); + + $cipherRaw = base64_decode($base64, true); + if ($cipherRaw === false) { + return false; + } + + $derivedKey = hash('sha256', env('VISIT_SECRET_KEY'), true); + $plainText = openssl_decrypt( + $cipherRaw, + 'aes-256-cbc', + $derivedKey, + OPENSSL_RAW_DATA, + env('VISIT_IV') + ); + + if ($plainText === false) { + return false; + } + + $params = []; + parse_str($plainText, $params); + + return [ + 'plainText' => $plainText, + 'params' => $params, + ]; + } + /** * Manual TPA Claim Push - accepts claim_id via POST and delegates to pushClaims(). * Returns the response from pushClaims() as the API response. diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 90b426ce..b56714a0 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -1037,7 +1037,7 @@ class ClientController extends AdminController $editData['client_branch'] = $this->clientBranchModel->where(['client_id' => $id, 'is_active' => 1])->findAll(); $editData['client_relation'] = $this->clientRMModel->where(['client_id' => $id, 'is_active' => 1])->findAll(); $editData['client_branch']['role'] = get_role_id(); - $editData['lead_data'] = $this->leadsModel->getLeadForInsertClientList(null, $id, 'update'); + $editData['lead_data'] = $this->leadsModel->getLeadForInsertClientList([1,3,2], $id, 'update'); $clientPoliceData = $this->clientPolicyModel->getClientPolicyByClientId($id); foreach ($clientPoliceData as $key => $value) { diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index cc4767dc..dd4a261c 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -5941,4 +5941,196 @@ class EmployeeRestController extends AdminController return $this->respond(['status' => false, 'code' => 404, 'message' => 'Failed to sent mail.', 'result' => $result], 200); } + public function getInsurerClaimFormDownloadUrl() + { + $clientPolicyId = trim((string) ($this->request->getGet('client_policy_id') ?? '')); + + if ($clientPolicyId === '') { + $errorContext = [ + 'api' => 'getInsurerClaimFormDownloadUrl', + 'error' => 'client_policy_id is required', + 'client_policy_id' => null, + 'request_uri' => (string) current_url(), + 'request_method' => $this->request->getMethod(), + ]; + log_message('error', '[getInsurerClaimFormDownloadUrl] Missing required parameter | ' . json_encode($errorContext)); + $this->myLogger->logme('error', '[getInsurerClaimFormDownloadUrl] Missing required parameter | ' . json_encode($errorContext)); + + return $this->respond([ + 'status' => false, + 'code' => 400, + 'message' => 'client_policy_id is required', + 'data' => [], + ], 200); + } + + if (! ctype_digit($clientPolicyId) || (int) $clientPolicyId <= 0) { + $errorContext = [ + 'api' => 'getInsurerClaimFormDownloadUrl', + 'error' => 'Invalid client_policy_id', + 'client_policy_id' => $clientPolicyId, + 'request_uri' => (string) current_url(), + ]; + log_message('error', '[getInsurerClaimFormDownloadUrl] Invalid client_policy_id | ' . json_encode($errorContext)); + $this->myLogger->logme('error', '[getInsurerClaimFormDownloadUrl] Invalid client_policy_id | ' . json_encode($errorContext)); + + return $this->respond([ + 'status' => false, + 'code' => 400, + 'message' => 'client_policy_id must be a valid positive number', + 'data' => [], + ], 200); + } + + try { + $clientPolicy = $this->clientPolicyModel + ->select('client_policy.id, client_policy.insurer_id, client_policy.policy_no, client_policy.is_active, insurers.id as insurer_pk, insurers.name as insurer_name, insurers.short_name as insurer_short_name, insurers.is_active as insurer_is_active, insurers.insurer_claim_form, insurers.insurer_claim_form_original_name') + ->join('insurers', 'insurers.id = client_policy.insurer_id', 'left') + ->where('client_policy.id', (int) $clientPolicyId) + ->first(); + + if (empty($clientPolicy)) { + $errorContext = [ + 'api' => 'getInsurerClaimFormDownloadUrl', + 'error' => 'Client policy not found', + 'client_policy_id' => (int) $clientPolicyId, + 'request_uri' => (string) current_url(), + ]; + log_message('error', '[getInsurerClaimFormDownloadUrl] Client policy not found | ' . json_encode($errorContext)); + $this->myLogger->logme('error', '[getInsurerClaimFormDownloadUrl] Client policy not found | ' . json_encode($errorContext)); + + return $this->respond([ + 'status' => false, + 'code' => 404, + 'message' => 'Client policy not found for the given client_policy_id', + 'data' => [], + ], 200); + } + + if ((int) ($clientPolicy['is_active'] ?? 0) !== 1) { + $errorContext = [ + 'api' => 'getInsurerClaimFormDownloadUrl', + 'error' => 'Client policy is inactive', + 'client_policy_id' => (int) $clientPolicyId, + 'policy_no' => $clientPolicy['policy_no'] ?? null, + ]; + log_message('error', '[getInsurerClaimFormDownloadUrl] Inactive client policy | ' . json_encode($errorContext)); + $this->myLogger->logme('error', '[getInsurerClaimFormDownloadUrl] Inactive client policy | ' . json_encode($errorContext)); + + return $this->respond([ + 'status' => false, + 'code' => 404, + 'message' => 'Client policy is inactive', + 'data' => [], + ], 200); + } + + if (empty($clientPolicy['insurer_id'])) { + $errorContext = [ + 'api' => 'getInsurerClaimFormDownloadUrl', + 'error' => 'Insurer is not mapped to client policy', + 'client_policy_id' => (int) $clientPolicyId, + 'policy_no' => $clientPolicy['policy_no'] ?? null, + ]; + log_message('error', '[getInsurerClaimFormDownloadUrl] Insurer not mapped | ' . json_encode($errorContext)); + $this->myLogger->logme('error', '[getInsurerClaimFormDownloadUrl] Insurer not mapped | ' . json_encode($errorContext)); + + return $this->respond([ + 'status' => false, + 'code' => 404, + 'message' => 'Insurer is not mapped to this client policy', + 'data' => [], + ], 200); + } + + if ((int) ($clientPolicy['insurer_is_active'] ?? 0) !== 1) { + $errorContext = [ + 'api' => 'getInsurerClaimFormDownloadUrl', + 'error' => 'Insurer is inactive', + 'client_policy_id' => (int) $clientPolicyId, + 'insurer_id' => (int) $clientPolicy['insurer_id'], + 'insurer_name' => $clientPolicy['insurer_name'] ?? null, + ]; + log_message('error', '[getInsurerClaimFormDownloadUrl] Inactive insurer | ' . json_encode($errorContext)); + $this->myLogger->logme('error', '[getInsurerClaimFormDownloadUrl] Inactive insurer | ' . json_encode($errorContext)); + + return $this->respond([ + 'status' => false, + 'code' => 404, + 'message' => 'Insurer is inactive for this client policy', + 'data' => [], + ], 200); + } + + $insurerData = [ + 'id' => $clientPolicy['insurer_id'], + 'name' => $clientPolicy['insurer_name'] ?? '', + 'short_name' => $clientPolicy['insurer_short_name'] ?? '', + 'insurer_claim_form' => $clientPolicy['insurer_claim_form'] ?? null, + 'insurer_claim_form_original_name'=> $clientPolicy['insurer_claim_form_original_name'] ?? null, + ]; + + $claimFormAvailability = resolve_insurer_claim_form_file($insurerData); + + if (empty($claimFormAvailability['available'])) { + $errorContext = [ + 'api' => 'getInsurerClaimFormDownloadUrl', + 'error' => 'Insurer claim form file not found', + 'client_policy_id' => (int) $clientPolicyId, + 'insurer_id' => (int) $clientPolicy['insurer_id'], + 'insurer_name' => $clientPolicy['insurer_name'] ?? null, + 'insurer_short_name' => $clientPolicy['insurer_short_name'] ?? null, + 'stored_claim_form' => $clientPolicy['insurer_claim_form'] ?? null, + 'checked_paths' => $claimFormAvailability['checked_paths'] ?? [], + ]; + log_message('error', '[getInsurerClaimFormDownloadUrl] Claim form file not found | ' . json_encode($errorContext)); + $this->myLogger->logme('error', '[getInsurerClaimFormDownloadUrl] Claim form file not found | ' . json_encode($errorContext)); + + return $this->respond([ + 'status' => false, + 'code' => 404, + 'message' => 'Insurer claim form is not available for this client policy', + 'data' => [], + ], 200); + } + + $downloadUrl = base_url('claim-form-download/' . md5((string) $clientPolicy['insurer_id'])); + + log_message('info', '[getInsurerClaimFormDownloadUrl] Success | ' . json_encode([ + 'client_policy_id' => (int) $clientPolicyId, + 'insurer_id' => (int) $clientPolicy['insurer_id'], + 'file_source' => $claimFormAvailability['source'], + 'file_name' => $claimFormAvailability['file_name'], + 'download_url' => $downloadUrl, + ])); + + return $this->respond([ + 'status' => true, + 'code' => 200, + 'message' => 'Insurer claim form download URL fetched successfully', + 'data' => [ + 'download_url' => $downloadUrl, + ], + ], 200); + } catch (\Throwable $th) { + $errorContext = [ + 'api' => 'getInsurerClaimFormDownloadUrl', + 'error' => $th->getMessage(), + 'client_policy_id' => $clientPolicyId, + 'file' => $th->getFile(), + 'line' => $th->getLine(), + 'trace' => $th->getTraceAsString(), + ]; + log_message('error', '[getInsurerClaimFormDownloadUrl] Exception | ' . json_encode($errorContext)); + $this->myLogger->logme('error', '[getInsurerClaimFormDownloadUrl] Exception | ' . json_encode($errorContext)); + + return $this->respond([ + 'status' => false, + 'code' => 500, + 'message' => 'Unable to fetch insurer claim form download URL. Please try again later.', + 'data' => [], + ], 200); + } + } + } diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index ecc9ed74..bd343eee 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -4539,6 +4539,14 @@ class LeadsController extends BaseController return $this->respond(['status' => false, 'message' => 'Failed to create policy', 'data' => null], 200); } + if (empty($client_id) && ! empty($data['client_id'])) { + $client_id = $data['client_id']; + } + + if (empty($branch_id) && ! empty($data['client_branch_id'])) { + $branch_id = $data['client_branch_id']; + } + $result = $this->createClientPolicyWithLeadData($data, $client_id, $branch_id); // print_r($result); die; diff --git a/app/Controllers/MasterController.php b/app/Controllers/MasterController.php index 022c3a31..9d33863e 100755 --- a/app/Controllers/MasterController.php +++ b/app/Controllers/MasterController.php @@ -331,6 +331,13 @@ class MasterController extends AdminController 'ext_in' => 'Allowed file types: jpg, jpeg, png', ] ], + 'insurer_claim_form' => [ + 'rules' => 'if_exist|max_size[insurer_claim_form,5120]|ext_in[insurer_claim_form,pdf,doc,docx]', + 'errors' => [ + 'max_size' => 'Claim form file size should not exceed 5 MB', + 'ext_in' => 'Allowed file types: pdf, doc, docx', + ] + ], ]; // $rules = [ // 'name' => [ @@ -368,6 +375,14 @@ class MasterController extends AdminController $uploadFilePath = ROOTPATH . 'public/uploads/logo/'; $file_name = file_Upload($this->request->getFile('insurer_logo'), $uploadFilePath, UPLOAD_EXT_IMAGES); + + $claimFormUploadPath = WRITEPATH . 'insurer_claim_form/'; + if (!is_dir($claimFormUploadPath)) { + mkdir($claimFormUploadPath, 0777, true); + } + $insurerShortName = trim((string) ($this->request->getPost('short_name') ?? '')); + $claimFormUpload = file_Upload_random_name($this->request->getFile('insurer_claim_form'), $claimFormUploadPath, UPLOAD_EXT_INSURER_CLAIM_FORM, $insurerShortName); + $data = $this->request->getPost(); $sanitized_post_data = sanitizeInputArrayAdvanced($data); @@ -376,6 +391,10 @@ class MasterController extends AdminController $sanitized_post_data['is_multi_event'] = (!empty($sanitized_post_data['is_multi_event'])) ? 1 : 0; $sanitized_post_data['created_by'] = get_session_userid(); $sanitized_post_data['insurer_logo'] = $file_name; + if (!empty($claimFormUpload['stored_name'])) { + $sanitized_post_data['insurer_claim_form'] = $claimFormUpload['stored_name']; + $sanitized_post_data['insurer_claim_form_original_name'] = $claimFormUpload['original_name']; + } $insert = $this->insurerModel->insert($sanitized_post_data); @@ -573,6 +592,13 @@ class MasterController extends AdminController 'ext_in' => 'Only JPG, JPEG, and PNG files are allowed.', ] ], + 'insurer_claim_form' => [ + 'rules' => 'permit_empty|max_size[insurer_claim_form,5120]|ext_in[insurer_claim_form,pdf,doc,docx]', + 'errors' => [ + 'max_size' => 'Claim form file size should not exceed 5 MB', + 'ext_in' => 'Allowed file types: pdf, doc, docx', + ] + ], ]; if (!$this->validateData($postData, $rules)) { @@ -594,6 +620,18 @@ class MasterController extends AdminController $uploadFilePath = ROOTPATH . 'public/uploads/logo/'; $file_name = file_Upload($this->request->getFile('insurer_logo'), $uploadFilePath, UPLOAD_EXT_IMAGES); + + $claimFormUploadPath = WRITEPATH . 'insurer_claim_form/'; + if (!is_dir($claimFormUploadPath)) { + mkdir($claimFormUploadPath, 0777, true); + } + $insurerShortName = trim((string) ($this->request->getPost('short_name') ?? '')); + if ($insurerShortName === '' && !empty($this->request->getPost('PrimaryKey'))) { + $existingInsurerForShortName = $this->insurerModel->find($this->request->getPost('PrimaryKey')); + $insurerShortName = trim((string) ($existingInsurerForShortName['short_name'] ?? '')); + } + $claimFormUpload = file_Upload_random_name($this->request->getFile('insurer_claim_form'), $claimFormUploadPath, UPLOAD_EXT_INSURER_CLAIM_FORM, $insurerShortName); + $data = $this->request->getPost(); $sanitized_post_data = sanitizeInputArrayAdvanced($data); $id = $sanitized_post_data['PrimaryKey']; @@ -607,6 +645,18 @@ class MasterController extends AdminController $sanitized_post_data['insurer_logo'] = $file_name; } + if (!empty($claimFormUpload['stored_name'])) { + $existingInsurer = $this->insurerModel->find($id); + if (!empty($existingInsurer['insurer_claim_form'])) { + $oldClaimFormPath = $claimFormUploadPath . $existingInsurer['insurer_claim_form']; + if (is_file($oldClaimFormPath)) { + unlink($oldClaimFormPath); + } + } + $sanitized_post_data['insurer_claim_form'] = $claimFormUpload['stored_name']; + $sanitized_post_data['insurer_claim_form_original_name'] = $claimFormUpload['original_name']; + } + $update = $this->insurerModel->update($id,$sanitized_post_data); if($update){ @@ -616,6 +666,24 @@ class MasterController extends AdminController } } + public function downloadInsurerClaimForm($id = null) + { + $insurer = $this->insurerModel->where(['id' => (int) $id, 'is_active' => 1])->first(); + + if (empty($insurer)) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Claim form file not found'); + } + + $claimFormFile = resolve_insurer_claim_form_file($insurer); + + if (empty($claimFormFile['available']) || empty($claimFormFile['file_path'])) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Claim form file not found'); + } + + return $this->response->download($claimFormFile['file_path'], null) + ->setFileName($claimFormFile['file_name']); + } + public function editInsurerGet($id = null) { $this->myLogger->logme('error','Edit TPA Onboarding function called'); @@ -3084,6 +3152,7 @@ class MasterController extends AdminController 'files' => WRITEPATH . 'uploads/commission/files', 'rules' => WRITEPATH . 'uploads/commission/rules', 'claim_sample_forms' => ROOTPATH . 'public/claim_sample_forms/', + 'insurer_claim_form' => WRITEPATH . 'insurer_claim_form/', 'tmp' => ROOTPATH . 'public/tmp/', 'bds_dump_excel' => WRITEPATH . 'uploads/bds_dump_excel/', 'claims_mis' => WRITEPATH . 'uploads/claims_mis/', @@ -3942,204 +4011,10 @@ class MasterController extends AdminController */ public function cronCdLowBalanceAlert() { - log_message('error', 'cronCdLowBalanceAlert: started'); + $response = \App\Helpers\CdLowBalanceAlertHelper::runCron(); + $statusCode = ($response['status'] ?? false) ? 200 : 500; - try { - $cdMasters = $this->CDMasterModel->getCdMastersWithLowAlertThreshold(); - log_message('error', 'cronCdLowBalanceAlert: CD masters with alert threshold count = ' . count($cdMasters)); - - $lowBalanceAccounts = []; - - foreach ($cdMasters as $cdMaster) { - $cdAcPk = (int) ($cdMaster['id'] ?? 0); - $threshold = (float) ($cdMaster['cd_balance_low_alert_amount'] ?? 0); - - if ($cdAcPk <= 0 || $threshold <= 0) { - continue; - } - - $lastDeposit = $this->clientDepositModel - ->select('balance') - ->where('cd_ac_pk', $cdAcPk) - ->where('is_active', 1) - ->orderBy('id', 'DESC') - ->first(); - - if (! empty($lastDeposit)) { - $currentBalance = (float) ($lastDeposit['balance'] ?? 0); - } else { - $currentBalance = (float) ($cdMaster['opening_bal'] ?? 0); - } - - if ($currentBalance >= $threshold) { - continue; - } - - $lowBalanceAccounts[] = array_merge($cdMaster, [ - 'current_balance' => $currentBalance, - 'alert_amount' => $threshold, - ]); - } - - if (empty($lowBalanceAccounts)) { - $response = [ - 'status' => true, - 'message' => 'No CD accounts below the configured low-balance alert amount.', - 'low_balance_count' => 0, - 'account_manager_mails_sent' => 0, - 'data' => [], - 'errors' => [], - ]; - log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); - return $this->respond($response, 200); - } - - $accountManagerMailsSent = 0; - $mailErrors = []; - $data = []; - - foreach ($lowBalanceAccounts as $account) { - $result = $this->sendCdLowBalanceAccountManagerAlert($account); - $accountManagerMailsSent += (int) ($result['sent'] ?? 0); - if (! empty($result['errors'])) { - $mailErrors = array_merge($mailErrors, $result['errors']); - } - if (! empty($result['data'])) { - $data = array_merge($data, $result['data']); - } - } - - $response = [ - 'status' => true, - 'message' => 'CD low balance alert cron completed.', - 'low_balance_count' => count($lowBalanceAccounts), - 'account_manager_mails_sent' => $accountManagerMailsSent, - 'data' => $data, - 'errors' => $mailErrors, - ]; - - log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); - return $this->respond($response, 200); - } catch (\Throwable $e) { - $response = [ - 'status' => false, - 'message' => 'CD low balance alert cron failed.', - 'low_balance_count' => 0, - 'account_manager_mails_sent' => 0, - 'data' => [], - 'errors' => [$e->getMessage()], - ]; - log_message('error', 'cronCdLowBalanceAlert Exception: ' . $e->getMessage()); - log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); - return $this->respond($response, 500); - } - } - - private function sendCdLowBalanceAccountManagerAlert(array $account): array - { - $clientId = (int) ($account['client_id'] ?? 0); - $cdAcPk = (int) ($account['id'] ?? 0); - $cdAcNo = (string) ($account['cd_ac_no'] ?? ''); - - if ($clientId <= 0) { - log_message('error', 'cronCdLowBalanceAlert ACM mail: skipped, invalid client_id for cd_ac_pk=' . $cdAcPk); - return ['sent' => 0, 'errors' => [], 'data' => []]; - } - - $recipients = $this->getAccountManagerRecipients($clientId); - if (empty($recipients)) { - $error = "Client {$clientId}: No account manager found."; - log_message('error', 'cronCdLowBalanceAlert ACM mail: ' . $error); - return ['sent' => 0, 'errors' => [$error], 'data' => []]; - } - - $sent = 0; - $errors = []; - $data = []; - $subject = 'CD Low Balance Alert - ' . ($account['client_name'] ?? 'Client'); - - foreach ($recipients as $recipient) { - $acmEmail = (string) ($recipient['email'] ?? ''); - $acmName = (string) ($recipient['name'] ?? 'Account Manager'); - $message = $this->buildCdLowBalanceAccountManagerMailContent($account, $acmName); - - $mailResponse = MailHelper::send_email([ - 'mail' => $acmEmail, - 'subject' => $subject, - 'message' => $message, - 'common' => [ - 'mail_type' => 'account_manager_cd_low_balance_alert', - 'client_id' => $clientId, - ], - ]); - - $decoded = is_string($mailResponse) ? json_decode($mailResponse, true) : $mailResponse; - $mailStatus = is_array($decoded) && ($decoded['status'] ?? '') === 'success' ? 'success' : 'failed'; - - $data[] = [ - 'client_id' => $clientId, - 'cd_ac_pk' => $cdAcPk, - 'cd_ac_no' => $cdAcNo, - 'acm_name' => $acmName, - 'acm_email' => $acmEmail, - 'mail_status' => $mailStatus, - ]; - - if ($mailStatus === 'success') { - $sent++; - } else { - $errors[] = "Client {$clientId}: Failed to send alert to {$acmEmail}."; - } - } - - return ['sent' => $sent, 'errors' => $errors, 'data' => $data]; - } - - /** - * @return array - */ - private function getAccountManagerRecipients(int $clientId): array - { - $rows = $this->insurerRMModel - ->select('user_profiles.first_name, user_profiles.email') - ->join('user_profiles', 'user_profiles.id = client_rm.user_id') - ->where('client_rm.client_id', $clientId) - ->where('client_rm.level', 3) - ->where('client_rm.is_active', 1) - ->where('user_profiles.is_active', 1) - ->findAll(); - - $recipients = []; - $seenEmails = []; - - foreach ($rows as $row) { - $email = strtolower(trim((string) ($row['email'] ?? ''))); - if ($email === '' || isset($seenEmails[$email])) { - continue; - } - - $seenEmails[$email] = true; - $recipients[] = [ - 'name' => trim((string) ($row['first_name'] ?? 'Account Manager')) ?: 'Account Manager', - 'email' => (string) $row['email'], - ]; - } - - return $recipients; - } - - private function buildCdLowBalanceAccountManagerMailContent(array $account, string $recipientName): string - { - return '

Dear ' . esc($recipientName) . ',

' - . '

The CD account balance for ' . esc((string) ($account['client_name'] ?? '')) . ' has fallen below the configured alert threshold.

' - . '' - . '' - . '' - . '' - . '' - . '' - . '
Insurer' . esc((string) ($account['insurer_name'] ?? '')) . '
Insurer Branch' . esc((string) ($account['insurer_branch_name'] ?? '')) . '
CD Account No' . esc((string) ($account['cd_ac_no'] ?? '')) . '
Current Balance' . esc(number_format((float) ($account['current_balance'] ?? 0), 2, '.', '')) . '
Alert Amount' . esc(number_format((float) ($account['alert_amount'] ?? 0), 2, '.', '')) . '
' - . '

Please review the CD account and take the required action.

'; + return $this->respond($response, $statusCode); } diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php index b4171cb9..0e1b76a2 100644 --- a/app/Controllers/TicketController.php +++ b/app/Controllers/TicketController.php @@ -3405,25 +3405,26 @@ class TicketController extends BaseController } public function downloadClaimForm($insurer_id) - { + { $insurerModel = new InsurerModel(); $insurer_data = $insurerModel->where('MD5(id)', $insurer_id)->where('is_active', 1)->first(); - + if (!$insurer_data) { $data['message'] = 'The Physical File Not Found'; echo view('errors/404', $data); + return; } - - $fileName = $insurer_data['short_name'] . '.pdf'; - $filePath = ROOTPATH . 'public/claim_sample_forms/' . $fileName; - + + $claimFormFile = resolve_insurer_claim_form_file($insurer_data); + try { - if (file_exists($filePath)) { - return $this->response->download($filePath, null); - } else { - $data['message'] = 'The Physical File Not Found'; - echo view('errors/404', $data); + if (!empty($claimFormFile['available']) && !empty($claimFormFile['file_path'])) { + return $this->response->download($claimFormFile['file_path'], null) + ->setFileName($claimFormFile['file_name']); } + + $data['message'] = 'The Physical File Not Found'; + echo view('errors/404', $data); } catch (\Exception $e) { $this->myLogger->logme('error', $e->getMessage()); return $this->response->setStatusCode(500)->setBody('An error occurred while downloading the file.'); diff --git a/app/Database/insurers_add_claim_form_column.sql b/app/Database/insurers_add_claim_form_column.sql new file mode 100644 index 00000000..7ed6d458 --- /dev/null +++ b/app/Database/insurers_add_claim_form_column.sql @@ -0,0 +1,5 @@ +ALTER TABLE insurers + ADD COLUMN insurer_claim_form VARCHAR(255) NULL DEFAULT NULL AFTER insurer_logo; + +ALTER TABLE insurers + ADD COLUMN insurer_claim_form_original_name VARCHAR(255) NULL DEFAULT NULL AFTER insurer_claim_form; diff --git a/app/Helpers/CdLowBalanceAlertHelper.php b/app/Helpers/CdLowBalanceAlertHelper.php new file mode 100644 index 00000000..4a70d740 --- /dev/null +++ b/app/Helpers/CdLowBalanceAlertHelper.php @@ -0,0 +1,274 @@ + + */ + public static function runCron(): array + { + log_message('error', 'cronCdLowBalanceAlert: started'); + + try { + $cdMasterModel = new CDMasterModel(); + $cdMasters = $cdMasterModel->getCdMastersWithLowAlertThreshold(); + log_message('error', 'cronCdLowBalanceAlert: CD masters with alert threshold count = ' . count($cdMasters)); + + $depositModel = new ClientDepositModel(); + $lowBalanceAccounts = []; + + foreach ($cdMasters as $cdMaster) { + $cdAcPk = (int) ($cdMaster['id'] ?? 0); + $threshold = (float) ($cdMaster['cd_balance_low_alert_amount'] ?? 0); + + if ($cdAcPk <= 0 || $threshold <= 0) { + continue; + } + + $currentBalance = self::resolveCurrentBalance($depositModel, $cdAcPk, (float) ($cdMaster['opening_bal'] ?? 0)); + if ($currentBalance >= $threshold) { + continue; + } + + $lowBalanceAccounts[] = array_merge($cdMaster, [ + 'current_balance' => $currentBalance, + 'alert_amount' => $threshold, + ]); + } + + if (empty($lowBalanceAccounts)) { + $response = [ + 'status' => true, + 'message' => 'No CD accounts below the configured low-balance alert amount.', + 'low_balance_count' => 0, + 'account_manager_mails_sent' => 0, + 'data' => [], + 'errors' => [], + ]; + log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); + + return $response; + } + + $accountManagerMailsSent = 0; + $mailErrors = []; + $data = []; + + foreach ($lowBalanceAccounts as $account) { + $result = self::sendAccountManagerAlert($account); + $accountManagerMailsSent += (int) ($result['sent'] ?? 0); + if (! empty($result['errors'])) { + $mailErrors = array_merge($mailErrors, $result['errors']); + } + if (! empty($result['data'])) { + $data = array_merge($data, $result['data']); + } + } + + $response = [ + 'status' => true, + 'message' => 'CD low balance alert cron completed.', + 'low_balance_count' => count($lowBalanceAccounts), + 'account_manager_mails_sent' => $accountManagerMailsSent, + 'data' => $data, + 'errors' => $mailErrors, + ]; + log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); + + return $response; + } catch (\Throwable $e) { + $response = [ + 'status' => false, + 'message' => 'CD low balance alert cron failed.', + 'low_balance_count' => 0, + 'account_manager_mails_sent' => 0, + 'data' => [], + 'errors' => [$e->getMessage()], + ]; + log_message('error', 'cronCdLowBalanceAlert Exception: ' . $e->getMessage()); + log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); + + return $response; + } + } + + /** + * @return array|null + */ + private static function buildLowBalanceAccount(int $cdAcPk, float $currentBalance): ?array + { + $cdMasterModel = new CDMasterModel(); + $cdMaster = $cdMasterModel->getCdMasterWithLowAlertThresholdById($cdAcPk); + if ($cdMaster === null) { + return null; + } + + $threshold = (float) ($cdMaster['cd_balance_low_alert_amount'] ?? 0); + if ($threshold <= 0 || $currentBalance >= $threshold) { + return null; + } + + return array_merge($cdMaster, [ + 'current_balance' => $currentBalance, + 'alert_amount' => $threshold, + ]); + } + + private static function resolveCurrentBalance(ClientDepositModel $depositModel, int $cdAcPk, float $openingBalance): float + { + $lastDeposit = $depositModel + ->select('balance') + ->where('cd_ac_pk', $cdAcPk) + ->where('is_active', 1) + ->orderBy('id', 'DESC') + ->first(); + + if (! empty($lastDeposit)) { + return (float) ($lastDeposit['balance'] ?? 0); + } + + return $openingBalance; + } + + /** + * @param array $account + * @return array{sent: int, errors: array, data: array>} + */ + private static function sendAccountManagerAlert(array $account): array + { + $clientId = (int) ($account['client_id'] ?? 0); + $cdAcPk = (int) ($account['id'] ?? 0); + $cdAcNo = (string) ($account['cd_ac_no'] ?? ''); + + if ($clientId <= 0) { + log_message('error', 'cronCdLowBalanceAlert ACM mail: skipped, invalid client_id for cd_ac_pk=' . $cdAcPk); + + return ['sent' => 0, 'errors' => [], 'data' => []]; + } + + $recipients = self::getAccountManagerRecipients($clientId); + if (empty($recipients)) { + $error = "Client {$clientId}: No account manager found."; + log_message('error', 'cronCdLowBalanceAlert ACM mail: ' . $error); + + return ['sent' => 0, 'errors' => [$error], 'data' => []]; + } + + $sent = 0; + $errors = []; + $data = []; + $subject = 'CD Low Balance Alert - ' . ($account['client_name'] ?? 'Client'); + + foreach ($recipients as $recipient) { + $acmEmail = (string) ($recipient['email'] ?? ''); + $acmName = (string) ($recipient['name'] ?? 'Account Manager'); + $message = self::buildAccountManagerMailContent($account, $acmName); + + $mailResponse = MailHelper::send_email([ + 'mail' => $acmEmail, + 'subject' => $subject, + 'message' => $message, + 'common' => [ + 'mail_type' => 'account_manager_cd_low_balance_alert', + 'client_id' => $clientId, + ], + ]); + + $decoded = is_string($mailResponse) ? json_decode($mailResponse, true) : $mailResponse; + $mailStatus = is_array($decoded) && ($decoded['status'] ?? '') === 'success' ? 'success' : 'failed'; + + $data[] = [ + 'client_id' => $clientId, + 'cd_ac_pk' => $cdAcPk, + 'cd_ac_no' => $cdAcNo, + 'acm_name' => $acmName, + 'acm_email' => $acmEmail, + 'mail_status' => $mailStatus, + ]; + + if ($mailStatus === 'success') { + $sent++; + } else { + $errors[] = "Client {$clientId}: Failed to send alert to {$acmEmail}."; + } + } + + return ['sent' => $sent, 'errors' => $errors, 'data' => $data]; + } + + /** + * @return array + */ + private static function getAccountManagerRecipients(int $clientId): array + { + $rows = (new ClientRMModel()) + ->select('user_profiles.first_name, user_profiles.email') + ->join('user_profiles', 'user_profiles.id = client_rm.user_id') + ->where('client_rm.client_id', $clientId) + ->where('client_rm.level', 3) + ->where('client_rm.is_active', 1) + ->where('user_profiles.is_active', 1) + ->findAll(); + + $recipients = []; + $seenEmails = []; + + foreach ($rows as $row) { + $email = strtolower(trim((string) ($row['email'] ?? ''))); + if ($email === '' || isset($seenEmails[$email])) { + continue; + } + + $seenEmails[$email] = true; + $recipients[] = [ + 'name' => trim((string) ($row['first_name'] ?? 'Account Manager')) ?: 'Account Manager', + 'email' => (string) $row['email'], + ]; + } + + return $recipients; + } + + /** + * @param array $account + */ + private static function buildAccountManagerMailContent(array $account, string $recipientName): string + { + return '

Dear ' . esc($recipientName) . ',

' + . '

The CD account balance for ' . esc((string) ($account['client_name'] ?? '')) . ' has fallen below the configured alert threshold.

' + . '' + . '' + . '' + . '' + . '' + . '' + . '
Insurer' . esc((string) ($account['insurer_name'] ?? '')) . '
Insurer Branch' . esc((string) ($account['insurer_branch_name'] ?? '')) . '
CD Account No' . esc((string) ($account['cd_ac_no'] ?? '')) . '
Current Balance' . esc(number_format((float) ($account['current_balance'] ?? 0), 2, '.', '')) . '
Alert Amount' . esc(number_format((float) ($account['alert_amount'] ?? 0), 2, '.', '')) . '
' + . '

Please review the CD account and take the required action.

'; + } +} diff --git a/app/Helpers/DepositHelper.php b/app/Helpers/DepositHelper.php index cc8fbde0..db660b25 100755 --- a/app/Helpers/DepositHelper.php +++ b/app/Helpers/DepositHelper.php @@ -76,6 +76,11 @@ class DepositHelper // Return the response if ($insertId) { + if (($data['transaction_type'] ?? 'Credit') !== 'Credit') { + $cdAcPk = self::resolveCdAcPk($data); + CdLowBalanceAlertHelper::triggerAfterBalanceReduction($cdAcPk, $newBalance); + } + return [ 'success' => true, 'message' => 'Transaction saved successfully', @@ -126,6 +131,24 @@ class DepositHelper return $lastBalance; } + + private static function resolveCdAcPk(array $data): ?int + { + if (! empty($data['cd_ac_pk'])) { + return (int) $data['cd_ac_pk']; + } + + if (empty($data['cd_ac_no'])) { + return null; + } + + $cdMaster = (new CDMasterModel()) + ->where('cd_ac_no', $data['cd_ac_no']) + ->where('is_active', 1) + ->first(); + + return $cdMaster ? (int) $cdMaster['id'] : null; + } } ?> diff --git a/app/Helpers/utility_helper.php b/app/Helpers/utility_helper.php index a79b8b49..00955f17 100755 --- a/app/Helpers/utility_helper.php +++ b/app/Helpers/utility_helper.php @@ -99,6 +99,92 @@ if (! function_exists('file_Upload')) { } } +if (! function_exists('file_Upload_random_name')) { + function file_Upload_random_name($fileToUpload, $filepath, array $allowedExtensions = UPLOAD_ALLOWED_EXTENSIONS, string $namePrefix = ''): array + { + $empty = ['stored_name' => '', 'original_name' => '']; + + if ($fileToUpload === null || ! $fileToUpload->isValid() || $fileToUpload->hasMoved()) { + return $empty; + } + + if (! validate_upload_extension($fileToUpload, $allowedExtensions)) { + return $empty; + } + + $originalName = sanitize_upload_filename($fileToUpload->getClientName() ?: $fileToUpload->getName()); + $ext = strtolower($fileToUpload->getClientExtension() ?: pathinfo($originalName, PATHINFO_EXTENSION)); + $namePrefix = preg_replace('/[^a-zA-Z0-9\-_]/', '', $namePrefix); + $prefix = $namePrefix !== '' ? $namePrefix . '_' : ''; + $storedName = $prefix . time() . '_' . bin2hex(random_bytes(8)) . ($ext !== '' ? '.' . $ext : ''); + $fileToUpload->move($filepath, $storedName); + + return ['stored_name' => $storedName, 'original_name' => $originalName]; + } +} + +if (! function_exists('resolve_insurer_claim_form_file')) { + function resolve_insurer_claim_form_file(array $insurer): array + { + $checkedPaths = []; + $claimFormDir = WRITEPATH . 'insurer_claim_form/'; + + if (!empty($insurer['insurer_claim_form'])) { + $storedFileName = basename($insurer['insurer_claim_form']); + $uploadedPath = $claimFormDir . $storedFileName; + $checkedPaths[] = $uploadedPath; + + if (is_file($uploadedPath)) { + return [ + 'available' => true, + 'file_path' => $uploadedPath, + 'file_name' => !empty($insurer['insurer_claim_form_original_name']) + ? basename($insurer['insurer_claim_form_original_name']) + : $storedFileName, + 'source' => 'uploaded', + 'checked_paths' => $checkedPaths, + ]; + } + } + + $defaultPath = $claimFormDir . INSURER_DEFAULT_CLAIM_FORM_FILE; + $checkedPaths[] = $defaultPath; + + if (is_file($defaultPath)) { + return [ + 'available' => true, + 'file_path' => $defaultPath, + 'file_name' => INSURER_DEFAULT_CLAIM_FORM_FILE, + 'source' => 'irdai_default', + 'checked_paths' => $checkedPaths, + ]; + } + + if (!empty($insurer['short_name'])) { + $legacyPath = ROOTPATH . 'public/claim_sample_forms/' . $insurer['short_name'] . '.pdf'; + $checkedPaths[] = $legacyPath; + + if (is_file($legacyPath)) { + return [ + 'available' => true, + 'file_path' => $legacyPath, + 'file_name' => $insurer['short_name'] . '.pdf', + 'source' => 'legacy', + 'checked_paths' => $checkedPaths, + ]; + } + } + + return [ + 'available' => false, + 'file_path' => null, + 'file_name' => null, + 'source' => null, + 'checked_paths' => $checkedPaths, + ]; + } +} + if (! function_exists('file_Upload_for_lead')) { function file_Upload_for_lead($fileToUpload, $filepath, array $allowedExtensions = UPLOAD_ALLOWED_EXTENSIONS) { diff --git a/app/Models/CDMasterModel.php b/app/Models/CDMasterModel.php index d5df7b00..2b1e66db 100755 --- a/app/Models/CDMasterModel.php +++ b/app/Models/CDMasterModel.php @@ -118,6 +118,34 @@ class CDMasterModel extends Model * @return array> */ public function getCdMastersWithLowAlertThreshold(): array + { + return $this->buildCdMastersWithLowAlertThresholdQuery() + ->orderBy('cd_master.id', 'desc') + ->get() + ->getResultArray(); + } + + /** + * @return array|null + */ + public function getCdMasterWithLowAlertThresholdById(int $cdAcPk): ?array + { + if ($cdAcPk <= 0) { + return null; + } + + $row = $this->buildCdMastersWithLowAlertThresholdQuery() + ->where('cd_master.id', $cdAcPk) + ->get() + ->getRowArray(); + + return $row ?: null; + } + + /** + * @return \CodeIgniter\Database\BaseBuilder + */ + private function buildCdMastersWithLowAlertThresholdQuery() { return $this->db->table('cd_master') ->select('cd_master.*, clients.client_name, clients.short_name, insurers.name AS insurer_name, insurer_branch.branch_name AS insurer_branch_name') @@ -128,9 +156,6 @@ class CDMasterModel extends Model ->where('clients.is_active', 1) ->where('insurers.is_active', 1) ->where('cd_master.cd_balance_low_alert_amount IS NOT NULL', null, false) - ->where('cd_master.cd_balance_low_alert_amount >', 0) - ->orderBy('cd_master.id', 'desc') - ->get() - ->getResultArray(); + ->where('cd_master.cd_balance_low_alert_amount >', 0); } } diff --git a/app/Models/InsurerModel.php b/app/Models/InsurerModel.php index a08b3c9e..5b375e09 100755 --- a/app/Models/InsurerModel.php +++ b/app/Models/InsurerModel.php @@ -15,6 +15,8 @@ class InsurerModel extends Model "name", "short_name", "insurer_logo", + "insurer_claim_form", + "insurer_claim_form_original_name", "created_by", "updated_by", "is_active", diff --git a/app/Models/LeadsModel.php b/app/Models/LeadsModel.php index 5ebd7f13..1d20863a 100644 --- a/app/Models/LeadsModel.php +++ b/app/Models/LeadsModel.php @@ -190,7 +190,7 @@ class LeadsModel extends Model public function getLeadForInsertClientList($type = null, $client_id = null, $from = null) { $query = $this->db->table('leads') - ->select('leads.*, user_profiles.first_name as user_name, policy_type.allocg') + ->select('leads.*, user_profiles.first_name as user_name, policy_type.allocg, policy_type.policy_type as policy_type_name') ->join('user_profiles', 'leads.created_by = user_profiles.id') ->join('policy_type', 'leads.policy_type_id = policy_type.id', 'left') ->where('leads.is_active', 1) @@ -200,13 +200,24 @@ class LeadsModel extends Model $query->where("(leads.is_policy_created = '' OR leads.is_policy_created IS NULL)"); if ($client_id) { - $query->join('clients', 'clients.id = ' . (int) $client_id) - ->join('client_branch', 'client_branch.client_id = clients.id AND client_branch.is_active = 1') - ->where('leads.client_name = clients.client_name', null, false) - ->where('leads.client_short_name = clients.short_name', null, false) - ->where('leads.branch_name = client_branch.branch_name', null, false) - ->where('leads.branch_code = client_branch.branch_code', null, false) - ->groupBy('leads.id'); + $clientId = (int) $client_id; + $query->groupStart() + ->where('leads.client_id', $clientId) + ->orWhere( + "EXISTS ( + SELECT 1 + FROM clients c + INNER JOIN client_branch cb ON cb.client_id = c.id AND cb.is_active = 1 + WHERE c.id = {$clientId} + AND leads.client_name = c.client_name + AND leads.client_short_name = c.short_name + AND leads.branch_name = cb.branch_name + AND leads.branch_code = cb.branch_code + )", + null, + false + ) + ->groupEnd(); } } else { $query->groupStart() @@ -214,14 +225,16 @@ class LeadsModel extends Model ->orWhere("(leads.is_policy_created = '' OR leads.is_policy_created IS NULL)") ->groupEnd(); - if ($type) { - $query->whereIn('leads.lead_type', $type); - } + if ($client_id) { $query->where('leads.client_id', $client_id); } } + if ($type) { + $query->whereIn('leads.lead_type', $type); + } + $query->groupStart() ->where("policy_type.allocg != 'EB'") ->orWhere("(policy_type.allocg = 'EB' AND leads.proposel_data IS NOT NULL AND leads.proposel_data <> '')") diff --git a/app/Views/client_list.php b/app/Views/client_list.php index 88e26a8c..18b3a859 100755 --- a/app/Views/client_list.php +++ b/app/Views/client_list.php @@ -244,7 +244,7 @@ table.dataTable thead th { - + diff --git a/app/Views/client_policy.php b/app/Views/client_policy.php index 101ab426..5b54fbe1 100755 --- a/app/Views/client_policy.php +++ b/app/Views/client_policy.php @@ -396,7 +396,7 @@ input:checked + .slider_blue::before { - + diff --git a/app/Views/insurer_basic_info.php b/app/Views/insurer_basic_info.php index b3af15ed..5e010955 100755 --- a/app/Views/insurer_basic_info.php +++ b/app/Views/insurer_basic_info.php @@ -187,6 +187,39 @@ input:checked + .slider:before { + +
+ +
+
+ + +
+
+ Allowed: PDF, DOC, DOCX. Max size: 5MB. If no insurer form is uploaded, will be used. + + + +
+
+
@@ -240,10 +273,47 @@ input:checked + .slider:before { } }); + // Custom Parsley validator for insurer claim form file size + window.Parsley.addValidator('insurerClaimMaxFileSize', { + validateString: function(value, maxSize, parsleyInstance) { + if (!window.FileReader) { + return true; + } + var files = parsleyInstance.$element[0].files; + if (files.length == 0) { + return true; + } + return files[0].size <= maxSize * 1024; + }, + requirementType: 'integer', + messages: { + en: 'This file should not be larger than %s KB.' + } + }); + + // Custom Parsley validator for insurer claim form file extension + window.Parsley.addValidator('insurerClaimFileExtension', { + validateString: function(value, requirement, parsleyInstance) { + var file = parsleyInstance.$element[0].files[0]; + if (!file) { return true; } + var extensions = requirement.split(',').map(function(ext) { return ext.trim().toLowerCase(); }); + var fileExtension = file.name.split('.').pop().toLowerCase(); + return extensions.indexOf(fileExtension) !== -1; + }, + requirementType: 'string', + messages: { + en: 'Allowed extensions are: %s.' + } + }); + $('#insurer_logo').on('change', function() { PreviewImage(); }); + $('#insurer_claim_form').on('change', function() { + validateInsurerClaimFormFile(true); + }); + $("#insurer_general_form").submit(function(events) { events.preventDefault(); @@ -251,6 +321,10 @@ input:checked + .slider:before { if (!validateInsurerLogoFile(true)) { return; } + + if (!validateInsurerClaimFormFile(true)) { + return; + } // var isValid = $('#insurer_General_PrimaryKey').parsley().validate(); var isValid = $('#insurer_general_form').parsley().validate(); @@ -430,6 +504,45 @@ input:checked + .slider:before { $('#logo_error_container').empty(); return true; } + + function resetInsurerClaimFormInput() { + var fileInput = document.getElementById("insurer_claim_form"); + fileInput.value = ""; + $('#claim_form_error_container').empty(); + } + + function validateInsurerClaimFormFile(showToastr) { + var fileInput = document.getElementById("insurer_claim_form"); + if (!fileInput.files || !fileInput.files[0]) { + return true; + } + + var file = fileInput.files[0]; + var fileName = file.name.toLowerCase(); + var parts = fileName.split('.'); + var allowedExtensions = /(\.pdf|\.doc|\.docx)$/i; + + if (!allowedExtensions.exec(fileName) || parts.length > 2) { + $('#claim_form_error_container').html('
  • Only PDF, DOC, and DOCX files are allowed.
'); + if (showToastr) { + toastr.warning("Only PDF, DOC, and DOCX files are allowed.", "Invalid file type"); + } + resetInsurerClaimFormInput(); + return false; + } + + if ((file.size / 1024) > 5120) { + $('#claim_form_error_container').html('
  • File size should not exceed 5MB.
'); + if (showToastr) { + toastr.warning("File size should not exceed 5MB.", "Invalid file size"); + } + resetInsurerClaimFormInput(); + return false; + } + + $('#claim_form_error_container').empty(); + return true; + } //----------------------------------------------------------------------------------------------------- diff --git a/tests/smoke_cd_low_balance_alert.php b/tests/smoke_cd_low_balance_alert.php new file mode 100644 index 00000000..0c776240 --- /dev/null +++ b/tests/smoke_cd_low_balance_alert.php @@ -0,0 +1,196 @@ +systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development')); + +$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; +if (is_file($boot)) { + require_once $boot; +} + +use App\Helpers\CdLowBalanceAlertHelper; +use App\Helpers\DepositHelper; +use App\Models\CDMasterModel; +use App\Models\ClientDepositModel; +use App\Models\ClientRMModel; + +$pass = 0; +$fail = 0; +$results = []; + +function ok(string $label, bool $cond, string $detail = ''): void +{ + global $pass, $fail, $results; + if ($cond) { + $pass++; + $results[] = '[PASS] ' . $label . ($detail ? " — {$detail}" : ''); + } else { + $fail++; + $results[] = '[FAIL] ' . $label . ($detail ? " — {$detail}" : ''); + } +} + +function argValue(string $flag): ?string +{ + global $argv; + $idx = array_search($flag, $argv, true); + if ($idx === false || ! isset($argv[$idx + 1])) { + return null; + } + + return (string) $argv[$idx + 1]; +} + +function resolveCurrentBalance(ClientDepositModel $depositModel, int $cdAcPk, float $openingBalance): float +{ + $lastDeposit = $depositModel + ->select('balance') + ->where('cd_ac_pk', $cdAcPk) + ->where('is_active', 1) + ->orderBy('id', 'DESC') + ->first(); + + if (! empty($lastDeposit)) { + return (float) ($lastDeposit['balance'] ?? 0); + } + + return $openingBalance; +} + +$db = db_connect('default'); +$cdMasterModel = new CDMasterModel(); +$depositModel = new ClientDepositModel(); +$runCron = in_array('--cron', $argv, true); +$debitAmount = argValue('--debit'); +$cdAcPkArg = null; + +foreach (array_slice($argv, 1) as $arg) { + if (str_starts_with($arg, '--')) { + continue; + } + if (ctype_digit($arg)) { + $cdAcPkArg = (int) $arg; + break; + } +} + +echo PHP_EOL . '=== smoke_cd_low_balance_alert ===' . PHP_EOL; + +if ($runCron) { + $response = CdLowBalanceAlertHelper::runCron(); + ok('cron status', ($response['status'] ?? false) === true, (string) ($response['message'] ?? '')); + ok('cron response has data key', array_key_exists('data', $response)); + echo 'Cron response: ' . json_encode($response, JSON_PRETTY_PRINT) . PHP_EOL; +} else { + $configured = $cdMasterModel->getCdMastersWithLowAlertThreshold(); + ok('at least one CD master has alert threshold', count($configured) > 0, 'count=' . count($configured)); + + if ($cdAcPkArg === null && ! empty($configured)) { + $cdAcPkArg = (int) ($configured[0]['id'] ?? 0); + } + + ok('cd_ac_pk resolved', $cdAcPkArg !== null && $cdAcPkArg > 0, 'cd_ac_pk=' . ($cdAcPkArg ?? 'none')); + + if ($cdAcPkArg > 0) { + $cdMaster = $cdMasterModel->getCdMasterWithLowAlertThresholdById($cdAcPkArg); + ok('CD master found with alert threshold', $cdMaster !== null, 'cd_ac_pk=' . $cdAcPkArg); + + if ($cdMaster !== null) { + $threshold = (float) ($cdMaster['cd_balance_low_alert_amount'] ?? 0); + $currentBalance = resolveCurrentBalance($depositModel, $cdAcPkArg, (float) ($cdMaster['opening_bal'] ?? 0)); + $isLow = $currentBalance < $threshold; + + echo PHP_EOL . 'Account snapshot:' . PHP_EOL; + echo ' client_id : ' . ($cdMaster['client_id'] ?? '') . ' (' . ($cdMaster['client_name'] ?? '') . ')' . PHP_EOL; + echo ' cd_ac_pk : ' . $cdAcPkArg . PHP_EOL; + echo ' cd_ac_no : ' . ($cdMaster['cd_ac_no'] ?? '') . PHP_EOL; + echo ' balance : ' . number_format($currentBalance, 2) . PHP_EOL; + echo ' alert amt : ' . number_format($threshold, 2) . PHP_EOL; + echo ' below alert : ' . ($isLow ? 'yes' : 'no') . PHP_EOL; + + $acmRows = (new ClientRMModel()) + ->select('user_profiles.first_name, user_profiles.email') + ->join('user_profiles', 'user_profiles.id = client_rm.user_id') + ->where('client_rm.client_id', (int) $cdMaster['client_id']) + ->where('client_rm.level', 3) + ->where('client_rm.is_active', 1) + ->where('user_profiles.is_active', 1) + ->findAll(); + ok('account manager exists for client', count($acmRows) > 0, 'count=' . count($acmRows)); + + foreach ($acmRows as $row) { + echo ' ACM : ' . ($row['first_name'] ?? '') . ' <' . ($row['email'] ?? '') . '>' . PHP_EOL; + } + + if ($debitAmount !== null) { + $amount = (float) $debitAmount; + ok('debit amount > 0', $amount > 0, 'amount=' . $amount); + + $beforeBalance = $currentBalance; + $afterBalance = $beforeBalance - $amount; + + echo PHP_EOL . 'Posting test debit of ' . number_format($amount, 2) . ' ...' . PHP_EOL; + + $saveResult = DepositHelper::saveDeposit([ + 'amount' => $amount, + 'sub_type_id' => 4, + 'client_id' => (int) $cdMaster['client_id'], + 'client_policy_id' => 0, + 'endorsement_no' => 'SMOKE-TEST-' . date('YmdHis'), + 'cd_ac_no' => (string) ($cdMaster['cd_ac_no'] ?? ''), + 'insurer_id' => (int) ($cdMaster['insurer_id'] ?? 0), + 'unit' => 'SMOKE', + 'description' => 'Smoke test debit for CD low balance alert', + 'transaction_type' => 'Debit', + 'updated_by' => 1, + 'event_name' => 'smoke_test', + 'is_active' => 1, + 'cd_ac_pk' => $cdAcPkArg, + ], 1); + + ok('saveDeposit succeeded', ($saveResult['success'] ?? false) === true, json_encode($saveResult)); + + $newBalance = resolveCurrentBalance($depositModel, $cdAcPkArg, (float) ($cdMaster['opening_bal'] ?? 0)); + ok('balance reduced after debit', $newBalance < $beforeBalance, "before={$beforeBalance}, after={$newBalance}"); + + if ($afterBalance < $threshold) { + ok('balance now below alert threshold', $newBalance < $threshold); + echo 'Check writable/logs/log-' . date('Y-m-d') . '.log for CdLowBalanceAlertHelper triggerAfterBalanceReduction' . PHP_EOL; + echo 'ACM mail should be sent if mail is configured.' . PHP_EOL; + } else { + echo 'Balance still above alert threshold after debit; no mail expected.' . PHP_EOL; + echo 'Tip: use a larger --debit amount or lower cd_balance_low_alert_amount for this account.' . PHP_EOL; + } + } else { + echo PHP_EOL . 'Dry run only (no debit posted).' . PHP_EOL; + echo 'To test live debit + alert: php tests/smoke_cd_low_balance_alert.php ' . $cdAcPkArg . ' --debit 1' . PHP_EOL; + echo 'To test cron scan: php tests/smoke_cd_low_balance_alert.php --cron' . PHP_EOL; + } + } + } +} + +echo PHP_EOL . implode(PHP_EOL, $results) . PHP_EOL; +echo PHP_EOL . "Summary: {$pass} passed, {$fail} failed" . PHP_EOL; + +exit($fail > 0 ? 1 : 0); diff --git a/tests/smoke_get_insurer_claim_form_download_url.php b/tests/smoke_get_insurer_claim_form_download_url.php new file mode 100644 index 00000000..ac7b5a5d --- /dev/null +++ b/tests/smoke_get_insurer_claim_form_download_url.php @@ -0,0 +1,185 @@ +systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development')); + +$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; +if (is_file($boot)) { + require_once $boot; +} + +use App\Controllers\EmployeeRestController; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\URI; +use Config\Services; + +$pass = 0; +$fail = 0; +$results = []; + +function ok(string $label, bool $cond, string $detail = ''): void +{ + global $pass, $fail, $results; + if ($cond) { + $pass++; + $results[] = '[PASS] ' . $label . ($detail ? " — {$detail}" : ''); + } else { + $fail++; + $results[] = '[FAIL] ' . $label . ($detail ? " — {$detail}" : ''); + } +} + +function invokeApi(EmployeeRestController $controller, array $query): array +{ + $uri = new URI('http://localhost/nhance_v2/employeeRest/getInsurerClaimFormDownloadUrl'); + if ($query !== []) { + $uri->setQuery(http_build_query($query)); + } + + $request = new IncomingRequest( + config('App'), + $uri, + null, + new \CodeIgniter\HTTP\UserAgent() + ); + $request->setGlobal('get', $query); + + $response = Services::response(); + $logger = Services::logger(); + $controller->initController($request, $response, $logger); + + $response = $controller->getInsurerClaimFormDownloadUrl(); + $body = json_decode($response->getBody(), true); + + return is_array($body) ? $body : []; +} + +$db = db_connect('default'); +$controller = new EmployeeRestController(); + +$legacyPolicy = $db->query( + "SELECT cp.id AS client_policy_id, i.short_name + FROM client_policy cp + JOIN insurers i ON i.id = cp.insurer_id + WHERE cp.is_active = 1 + AND i.is_active = 1 + AND i.short_name = 'ICICIPRU' + LIMIT 1" +)->getRowArray(); + +$uploadedPolicy = $db->query( + "SELECT cp.id AS client_policy_id, i.short_name, i.insurer_claim_form + FROM client_policy cp + JOIN insurers i ON i.id = cp.insurer_id + WHERE cp.is_active = 1 + AND i.is_active = 1 + AND i.insurer_claim_form IS NOT NULL + AND i.insurer_claim_form <> '' + LIMIT 1" +)->getRowArray(); + +$inactivePolicy = $db->query( + 'SELECT id AS client_policy_id FROM client_policy WHERE is_active = 0 LIMIT 1' +)->getRowArray(); + +$missingParam = invokeApi($controller, []); +ok('Missing client_policy_id returns 400', ($missingParam['code'] ?? null) === 400, json_encode($missingParam)); + +$invalidParam = invokeApi($controller, ['client_policy_id' => 'abc']); +ok('Invalid client_policy_id returns 400', ($invalidParam['code'] ?? null) === 400, json_encode($invalidParam)); + +$notFound = invokeApi($controller, ['client_policy_id' => '999999999']); +ok('Unknown client_policy_id returns 404', ($notFound['code'] ?? null) === 404, json_encode($notFound)); + +if (!empty($inactivePolicy['client_policy_id'])) { + $inactive = invokeApi($controller, ['client_policy_id' => (string) $inactivePolicy['client_policy_id']]); + ok('Inactive client policy returns 404', ($inactive['code'] ?? null) === 404, json_encode($inactive)); +} + +if (!empty($legacyPolicy['client_policy_id'])) { + $legacyPath = ROOTPATH . 'public/claim_sample_forms/' . $legacyPolicy['short_name'] . '.pdf'; + $legacy = invokeApi($controller, ['client_policy_id' => (string) $legacyPolicy['client_policy_id']]); + $legacyResolved = resolve_insurer_claim_form_file(['short_name' => $legacyPolicy['short_name']]); + ok( + 'No uploaded form falls back to IRDAI default (ICICIPRU policy)', + ($legacy['status'] ?? false) === true + && ($legacy['code'] ?? null) === 200 + && !empty($legacy['data']['download_url']) + && count($legacy['data']) === 1 + && ($legacyResolved['source'] ?? '') === 'irdai_default', + 'legacy_file_exists=' . (is_file($legacyPath) ? 'yes' : 'no') . ' | ' . json_encode($legacy) + ); +} + +$irdaiDefaultPolicy = $db->query( + "SELECT cp.id AS client_policy_id, i.short_name + FROM client_policy cp + JOIN insurers i ON i.id = cp.insurer_id + WHERE cp.is_active = 1 + AND i.is_active = 1 + AND (i.insurer_claim_form IS NULL OR i.insurer_claim_form = '') + AND i.short_name NOT IN ('ICICIPRU', 'DIGITLIF', 'MAXLIFE', 'NIA', 'OIC', 'PIFL', 'RELIANCE') + LIMIT 1" +)->getRowArray(); + +if (!empty($irdaiDefaultPolicy['client_policy_id'])) { + $irdai = invokeApi($controller, ['client_policy_id' => (string) $irdaiDefaultPolicy['client_policy_id']]); + $irdaiResolved = resolve_insurer_claim_form_file(['short_name' => $irdaiDefaultPolicy['short_name']]); + ok( + 'IRDAI default claim form fallback', + ($irdai['status'] ?? false) === true + && ($irdai['code'] ?? null) === 200 + && !empty($irdai['data']['download_url']) + && count($irdai['data']) === 1 + && ($irdaiResolved['source'] ?? '') === 'irdai_default', + json_encode($irdai) + ); +} + +if (!empty($uploadedPolicy['client_policy_id'])) { + $uploadedPath = WRITEPATH . 'insurer_claim_form/' . basename((string) $uploadedPolicy['insurer_claim_form']); + $uploaded = invokeApi($controller, ['client_policy_id' => (string) $uploadedPolicy['client_policy_id']]); + $uploadedResolved = resolve_insurer_claim_form_file([ + 'insurer_claim_form' => $uploadedPolicy['insurer_claim_form'], + 'short_name' => $uploadedPolicy['short_name'] ?? '', + ]); + ok( + 'Uploaded claim form success', + ($uploaded['status'] ?? false) === true + && ($uploaded['code'] ?? null) === 200 + && !empty($uploaded['data']['download_url']) + && count($uploaded['data']) === 1 + && ($uploadedResolved['source'] ?? '') === 'uploaded', + 'file_exists=' . (is_file($uploadedPath) ? 'yes' : 'no') . ' | ' . json_encode($uploaded) + ); +} + +$cliPolicyId = $argv[1] ?? ($legacyPolicy['client_policy_id'] ?? null); +if ($cliPolicyId) { + $manual = invokeApi($controller, ['client_policy_id' => (string) $cliPolicyId]); + echo "\nManual test client_policy_id={$cliPolicyId}\n"; + echo json_encode($manual, JSON_PRETTY_PRINT) . "\n"; +} + +echo "\nSmoke test summary: {$pass} passed, {$fail} failed\n"; +foreach ($results as $line) { + echo $line . "\n"; +} + +exit($fail > 0 ? 1 : 0); diff --git a/writable/insurer_claim_form/Claim_Form_IRDAI.pdf b/writable/insurer_claim_form/Claim_Form_IRDAI.pdf new file mode 100644 index 00000000..95861048 Binary files /dev/null and b/writable/insurer_claim_form/Claim_Form_IRDAI.pdf differ