From 9f933095b04c744061b47e8d10affd64f5a912e1 Mon Sep 17 00:00:00 2001 From: velz Date: Thu, 19 Sep 2024 09:25:22 +0530 Subject: [PATCH] FEAT_INS_STATEMENT_UPLOAD_INV_PAYMENTS --- app/Config/Routes.php | 7 + .../PolicyTransactionController.php | 448 ++++++++++- app/Helpers/utility_helper.php | 4 +- app/Models/InsurerStatements.php | 30 + app/Models/InvPaymentDetailsModel.php | 24 + app/Models/PTCOShareDetailsModel.php | 41 + app/Views/insurer_statement_list.php | 708 ++++++++++++++++++ app/Views/layout/footer.php | 2 +- app/Views/layout/header.php | 3 + .../sample_excel/insurer_stament_sample.xlsx | Bin 0 -> 6594 bytes 10 files changed, 1264 insertions(+), 3 deletions(-) create mode 100644 app/Models/InsurerStatements.php create mode 100644 app/Models/InvPaymentDetailsModel.php create mode 100644 app/Views/insurer_statement_list.php create mode 100644 public/sample_excel/insurer_stament_sample.xlsx diff --git a/app/Config/Routes.php b/app/Config/Routes.php index eab9f909..6a7ac292 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -317,6 +317,13 @@ $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) { $routes->get("list", "PolicyTransactionController::reportBDS"); }); + $routes->group("statement", ["filter" => "authMVC"], function ($routes) { + $routes->get("list", "PolicyTransactionController::statementList"); + $routes->post("upload", "PolicyTransactionController::uploadInsurerStatement"); + $routes->get("getPaymentDetails/(:any)", "PolicyTransactionController::getInvoicePaymentDetails/$1"); + $routes->post("saveInvoicePaymentDetails", "PolicyTransactionController::saveInvoicePaymentDetails"); + }); + }); $routes->cli('cli/processjob', 'JobWorker::processJob'); diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 7c9c46e7..60daeb55 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -25,7 +25,9 @@ use App\Models\EmployeeModel; use App\Models\UserTeamsModel; use App\Models\UserModel; use App\Models\EmployeePolicyModel; - +use App\Models\InsurerStatements; +use App\Models\InvPaymentDetailsModel; +use Kint; class PolicyTransactionController extends BaseController { @@ -51,6 +53,9 @@ class PolicyTransactionController extends BaseController protected $userTeamsModel; protected $userModel; protected $employeePolicyModel; + protected $insurerStatements; + protected $invoiceStatus; + protected $invPaymentDetailsModel; public function __construct() { @@ -76,6 +81,15 @@ class PolicyTransactionController extends BaseController $this->userTeamsModel = new UserTeamsModel(); $this->userModel = new UserModel(); $this->employeePolicyModel = new EmployeePolicyModel(); + $this->insurerStatements = new InsurerStatements(); + $this->invPaymentDetailsModel = new InvPaymentDetailsModel(); + $this->invoiceStatus = [ + 'pending' => 'Pending', + 'generated' => 'Generated', + 'sent' => 'Sent', + 'payment_received' => 'Payment Received', + + ]; } // Policy Transaction Inception @@ -965,6 +979,438 @@ class PolicyTransactionController extends BaseController $this->loadLayout('report_bds_filter', $data); } + + //insurer statement list page + public function statementList() + { + // dd($this->updateInsurerStatement(['file_id' => 13])); + // $data['insurers'] = $this->insurerModel->where('is_active',1)->findAll(); + $data['invoice_status_array'] = $this->invoiceStatus; + $data['insurers'] = $this->insurerBranchModel ->getInsurerBranchesWithInsurerNames(); + + // dd( $data['insurers']); + $data['insurer_statement_list'] = $this->insurerStatements + ->select('insurer_statements.*, + insurers.name AS insurer_name,insurers.short_name,user_profiles.first_name,insurer_branch.branch_code + ') + ->join('insurers', 'insurer_statements.insurer_id = insurers.id') + ->join('insurer_branch', 'insurer_statements.branch_id = insurer_branch.id') + ->join('user_profiles', 'insurer_statements.created_by = user_profiles.id') + ->orderBy('insurer_statements.id', 'desc') + ->findAll(); + // dd( $data['insurer_statement_list']); + $this->loadLayout('insurer_statement_list', $data); + } + + public function uploadInsurerStatement() + { + + //validate uploaded file + $filename = ''; + $validated = $this->validate([ + 'statement' => [ + 'uploaded[statement]', + 'mime_in[statement,application/vnd.ms-excel,application/vnd,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet]', + 'max_size[statement,16384]', + ], + ]); + $this->createStatementFolder(); + if ($validated) + { + $avatar = $this->request->getFile('statement'); + if (!$avatar) { + $this->myLogger->logme("error", 'Statement File not found'); + return $this->respond(['dataStatus' => false, 'code' => 400, 'message' => 'File not found'], 400); + } + + $is_moved = $avatar->move(WRITEPATH . 'uploads/statements/'); + if ($is_moved) { + $filename = $avatar->getName(); + // Handle successful upload, e.g., log success or further processing + $this->myLogger->logme("error", 'Statement File moved successful'); + + } else { + $this->myLogger->logme("error", 'Statement File move failed'); + return $this->respond(['dataStatus' => false, 'code' => 500, 'message' => 'File move failed'], 500); + } + } else { + $this->myLogger->logme("error", 'Statement Upload failed Invalid file'); + return $this->respond(['dataStatus' => false, 'code' => 404, 'message' => 'Invalid file'], 404); + } + + //process post variable entry in file table + $loggedInUserID = get_session_userid(); + // dd($loggedInUserID); + // $loggedInUserID = 8; + + $insurer = $this->request->getPost('insurer'); + + $insurer_id = explode('-', $insurer)[0]; + $branch_id = explode('-', $insurer)[1]; + // print_r($insurer_id); + // print_r($branch_id); + // die(); + $month = $this->request->getPost('statement_month'); + $month = change_date_format($month,'Y-M-d','Y-m-d'); + // print_r($month);die; + + $file_id = $this->insurerStatements->insert(['insurer_id' => $insurer_id, 'branch_id' => $branch_id, 'file_name' => $filename,'month' => $month, 'created_by' => $loggedInUserID]); //here field policy_id have client_policy_id and not policy id from policy master + $this->myLogger->logme("error", '{file_id} statement uploaded success', ['file_id' => $file_id]); + + //validate file + $validation_result = $this->validateInsurerStatement(['file_id' => $file_id]); + //update file content to DB + if($validation_result['status'] ) + { + $this->updateInsurerStatement(['file_id' => $file_id]); + } + + if (!isset($file_id) || !$validation_result['status']) { + return $this->respond(['dataStatus' => false, 'code' => 404, 'message' => 'file not uploaded', 'error_data' => $validation_result['error_data'],'error_code' => $validation_result['error_code']], 200); + } + + return $this->respond(['dataStatus' => true, 'code' => 200, 'data' => 'file upload success'], 200); + } + + public function createStatementFolder() + { + $folderPath = WRITEPATH . 'uploads/statements/'; + + // Check if the folder doesn't exist + if (!file_exists($folderPath)) + { + // Create the folder + if (mkdir($folderPath, 0777, true)) { + $this->myLogger->logme('error','statement upload folder created successfully'); + + // Set permissions to a+rwx (read, write, execute for all) + chmod($folderPath, 0777); + $this->myLogger->logme('error','Permissions set to a+rwx.'); + } else { + // echo "Failed to create folder."; + $this->myLogger->logme('error','Failed to create statement upload folder.'); + } + } + else + { + $this->myLogger->logme('error','upload folder exists.'); + } + } + + public function validateInsurerStatement($params) + { + //get file info + $file_id = $params['file_id']; + $file = $this->insurerStatements->find($file_id); + // dd($file); + $date = new \DateTime($file['month']); + + $month = $date->format('m'); + $year = $date->format('Y'); + + $error_data = ['error_code' => '','error_data' => []]; + $status = 'success'; + $ret_status = true; + // dd($month.'-'.$year); + $return = []; + if(!isset($file)) + { + //file not found in DB + return array('status' => false, 'msg' => 'statement file not found in DB'); + } + $file_name_with_path = WRITEPATH."/uploads/statements/".$file['file_name']; + + //check physical file + if(!file_exists($file_name_with_path)) + { + //file not found update status and reason + $message = "Physcial file not found"; + // echo $message; + $this->myLogger->logme('error',($message . ' for statement file id ' . $file_id)); + $this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed','reason' => json_encode(['error_code' => 0,'error_data' => $message])])->update(); + return array('status' => false, 'error_code' => 0); //0 - Physcial file not found + } + + //get excel data to php array + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path); + $sheet = $spreadsheet->getActiveSheet(); + + $highestRowAndColumn = $sheet->getHighestRowAndColumn(); + // dd($highestRowAndColumn); + $excel_data = $sheet->rangeToArray('A1:' . $highestRowAndColumn['column'] . $highestRowAndColumn['row']); + unset($excel_data[0]); + // dd($excel_data); + //get no of line items and update in DB + $line_items = count($excel_data); + // get uploaded month transactions data + $source_data = $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactions(month:$month,year:$year,insurer_id:$file['insurer_id'],insurer_branch_id:$file['branch_id']); + // Kint::dump($source_data); + // Kint::dump($excel_data); + + // check policy no,insurer and etc in DB for this month + // if all good return true, otherwise return false with messssage + foreach ($excel_data as $excel_key => $excel_row) + { + $is_source_found = 0; + $excel_row[4] = trim($excel_row[4]); + $excel_row[5] = trim($excel_row[5]); + // Kint::dump(change_date_format($excel_row[4],'d-m-Y','Y-m-d')); + // echo $excel_row[1].'#'.$excel_row[2] .'#'.$excel_row[3].'#'.change_date_format($excel_row[4],'d-m-Y','Y-m-d').'#'.change_date_format($excel_row[5],'d-m-Y','Y-m-d').'
'; + foreach ($source_data as $source_key => $source_row) + { + // Kint::dump(change_date_format($excel_row[3],'d-m-Y','Y-m-d')); + if( (trim($excel_row[1]) == $source_row['policy_no']) && trim($excel_row[2]) == $source_row['endorsement_no'] && trim($excel_row[3]) == $source_row['client_name'] && change_date_format($excel_row[4],'d-m-Y','Y-m-d') == $source_row['policy_start_date'] && change_date_format($excel_row[5],'d-m-Y','Y-m-d') == $source_row['policy_end_date']) + { + $is_source_found = 1; + unset($source_data[$source_key]); + continue 2; + } + } + + if($is_source_found == 0) + { + // echo $excel_key.'-'.$excel_row[1] . '- not found
'; + $error_data['error_code'] = 1;//match not found + // $error_data['error_data'] = ($error_data['error_data'] ?? []); + $error_data['error_data'] = array_merge($error_data['error_data'],[$excel_row[0]]);//match not found + } + } + + if($error_data['error_code']) + { + $status = 'failed'; + $ret_status = false; + } + //update in DB + $this->insurerStatements->where('id', $file_id)->set(['line_items' => $line_items,'file_status' => $status,'reason' => json_encode($error_data)])->update(); + return array('status' => $ret_status, 'error_code' => $error_data['error_code'],'error_data' => $error_data['error_data']); + } + + + public function updateInsurerStatement($params) + { + //get file info + $file_id = $params['file_id']; + $file = $this->insurerStatements->find($file_id); + // dd($file); + $date = new \DateTime($file['month']); + + $month = $date->format('m'); + $year = $date->format('Y'); + + $error_data = ['error_code' => '','error_data' => []]; + $status = 'success'; + $ret_status = true; + // dd($month.'-'.$year); + $return = []; + if(!isset($file)) + { + //file not found in DB + return array('status' => false, 'msg' => 'statement file not found in DB'); + } + $file_name_with_path = WRITEPATH."/uploads/statements/".$file['file_name']; + + //check physical file + if(!file_exists($file_name_with_path)) + { + //file not found update status and reason + $message = "Physcial file not found"; + // echo $message; + $this->myLogger->logme('error',($message . ' for statement file id ' . $file_id)); + $this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed','reason' => json_encode(['error_code' => 0,'error_data' => $message])])->update(); + return array('status' => false, 'error_code' => 0); //0 - Physcial file not found + } + + //get excel data to php array + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path); + $sheet = $spreadsheet->getActiveSheet(); + + $highestRowAndColumn = $sheet->getHighestRowAndColumn(); + // dd($highestRowAndColumn); + $excel_data = $sheet->rangeToArray('A1:' . $highestRowAndColumn['column'] . $highestRowAndColumn['row']); + unset($excel_data[0]); + // dd($excel_data); + //get no of line items and update in DB + $line_items = count($excel_data); + // get uploaded month transactions data + $source_data = $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactions(month:$month,year:$year,insurer_id:$file['insurer_id'],insurer_branch_id:$file['branch_id']); + // Kint::dump($source_data);die; + // Kint::dump($excel_data); + + // check policy no,insurer and etc in DB for this month + // if all good return true, otherwise return false with messssage + $data_to_update = []; + foreach ($excel_data as $excel_key => $excel_row) + { + $is_source_found = 0; + $excel_row[4] = trim($excel_row[4]); + $excel_row[5] = trim($excel_row[5]); + + foreach ($source_data as $source_key => $source_row) + { + // Kint::dump(change_date_format($excel_row[3],'d-m-Y','Y-m-d')); + if( (trim($excel_row[1]) == $source_row['policy_no']) && trim($excel_row[2]) == $source_row['endorsement_no'] && trim($excel_row[3]) == $source_row['client_name'] && change_date_format($excel_row[4],'d-m-Y','Y-m-d') == $source_row['policy_start_date'] && change_date_format($excel_row[5],'d-m-Y','Y-m-d') == $source_row['policy_end_date']) + { + $is_source_found = 1; + + //calculate percentage first + $total_amt = 0; + + $actual_bp_per = trim($excel_row[9]); + $actual_bp_brokerage = trim($excel_row[12]); + $actual_bp_amt = trim($excel_row[6]); + + if(($actual_bp_brokerage && $actual_bp_brokerage != 0 && $actual_bp_brokerage != "")) + { + $total_amt += $actual_bp_brokerage; + //percentage reverse calculation + if($actual_bp_per == 0 || $actual_bp_per == "") + { + $actual_bp_per = round(($actual_bp_brokerage / $actual_bp_amt) * 100,2); + } + + } + else + { + $actual_bp_brokerage = $actual_bp_amt * ($actual_bp_per / 100); + $total_amt += $actual_bp_brokerage; + } + + $actual_tp_per = trim($excel_row[10]); + $actual_tp_brokerage = trim($excel_row[13]); + $actual_tp_amt = trim($excel_row[7]); + + if($actual_tp_brokerage && $actual_tp_brokerage != 0 && $actual_tp_brokerage != "") + { + $total_amt += $actual_tp_brokerage; + //percentage reverse calculation + if($actual_tp_per == 0 || $actual_tp_per == "") + { + $actual_tp_per = ($actual_tp_brokerage / $actual_tp_amt) * 100; + } + } + else + { + $actual_tp_brokerage = $actual_tp_amt * ($actual_tp_per / 100); + $total_amt += $actual_tp_brokerage; + } + + $actual_tep_per = trim($excel_row[11]); + $actual_tep_brokerage = trim($excel_row[14]); + $actual_tep_amt = trim($excel_row[8]); + + if($actual_tep_brokerage && $actual_tep_brokerage != 0 && $actual_tep_brokerage != "") + { + $total_amt += $actual_tep_brokerage; + //percentage reverse calculation + if($actual_tep_per == 0 || $actual_tep_per == "") + { + $actual_tep_per = ($actual_tep_brokerage / $actual_tep_amt) * 100; + } + } + else + { + $actual_tep_brokerage = $actual_tep_amt * ($actual_tep_per / 100); + $total_amt += $actual_tep_brokerage; + } + + //find variance + $variance_amt = $source_row['exp_amt'] - $total_amt; + + $data_to_update[] = ['id' => $source_row['id'],'actual_bp_amt' => $actual_bp_amt,'actual_tp_amt' => $actual_tp_amt,'actual_tep_amt' => $actual_tep_amt,'actual_bp_per' => $actual_bp_per,'actual_tp_per' => $actual_tp_per,'actual_tep_per' => $actual_tep_per,'variance' => $variance_amt,'actual_tep_brokerage_amt' => $actual_tep_brokerage,'actual_tp_brokerage_amt' => $actual_tp_brokerage,'actual_bp_brokerage_amt' => $actual_bp_brokerage,'reward' => trim($excel_row[15]),'statement_id' => $file_id]; + + unset($source_data[$source_key]); + continue 2; + } + } + } + $this->PTCOShareDetailsModel->updateBatch($data_to_update, 'id'); + // dd($data_to_update); + // if($error_data['error_code']) + // { + // $status = 'failed'; + // $ret_status = false; + // } + //update in DB + //$this->insurerStatements->where('id', $file_id)->set(['file_status' => $status,'reason' => json_encode($error_data)])->update(); + return array('status' => $ret_status, 'error_code' => $error_data['error_code'],'error_data' => $error_data['error_data']); + } + + public function getInvoicePaymentDetails() + { + $statement_id = $this->request->getUri()->getSegment(4); + + $inv_details = $this->insurerStatements->find($statement_id); + $inv_payment_details = $this->invPaymentDetailsModel + ->where('statement_id',$statement_id) + ->where('is_active',1) + ->get() + ->getResultArray(); + // print_r($inv_details); + $data = [ 'invoice_status' => $inv_details['invoice_status'], + 'invoice_no' => $inv_details['invoice_no'], + 'invoice_date' => $inv_details['invoice_date'] ]; + $data['payments'] = $inv_payment_details; + return $this->respond(['dataStatus' => true, 'code' => 200, 'data' => $data], 200); + + + } + + public function saveInvoicePaymentDetails() + { + $jsonData = $this->request->getJSON(); + $jsonData = (array)$jsonData; + // echo 'Hi'; + // print_r($jsonData);die(); + + $invoiceStatus = $jsonData['invoice_status']; + $hiddenStatementId = $jsonData['hidden_statement_id']; + $invoiceNo = $jsonData['invoice_no']; + $invoiceDate = $jsonData['invoice_date']; + + //Update statement table + $parentData = [ + 'invoice_status' => $invoiceStatus, + 'invoice_no' => $invoiceNo, + 'invoice_date' => $invoiceDate, + 'updated_by' => get_session_userid() + ]; + + + + // $this->insurerStatements->update($hiddenStatementId, $parentData); + + // Process child data + $invoiceAmounts = $jsonData['invoice_amount']; + $utrNos = $jsonData['utr_no']; + $paymentDates = $jsonData['payment_date']; + $pks = $jsonData['pk']; + + foreach ($invoiceAmounts as $index => $invoiceAmount) { + $pk = $pks[$index]; // Get the pk for the current record + $utrNo = $utrNos[$index]; + $paymentDate = $paymentDates[$index]; + + // Prepare data for insert/update + $childData = [ + 'inv_amt' => $invoiceAmount, + 'utr_no' => $utrNo, + 'received_date' => $paymentDate, + 'statement_id' => $hiddenStatementId, + 'id' => is_numeric($pk) ? (int)$pk : '', + ]; + if($pk){ $childData['updated_by'] = get_session_userid(); } + else { $childData['created_by'] = get_session_userid(); } + // print_r($childData); + // Insert or update + $this->invPaymentDetailsModel->save($childData); + print_r($this->invPaymentDetailsModel->errors()); + + } + + return $this->respond(['dataStatus' => true, 'code' => 200], 200); + //return $this->response->setJSON(['dataStatus' => 'true']); + } } \ No newline at end of file diff --git a/app/Helpers/utility_helper.php b/app/Helpers/utility_helper.php index 6c7c6c1a..a764697b 100644 --- a/app/Helpers/utility_helper.php +++ b/app/Helpers/utility_helper.php @@ -24,6 +24,7 @@ if (!function_exists('change_date_format')) { function change_date_format($data, $source_format, $output_format) { + $data = trim($data); // echo $data, $source_format, $output_format;return true; try { // Create DateTime object with the source format @@ -31,7 +32,8 @@ if (!function_exists('change_date_format')) { // Check if the DateTime object is created successfully if ($dateTime === false) { - throw new Exception('Invalid date or format'); + $errors = DateTime::getLastErrors(); + throw new Exception('Invalid date or format' . implode(', ', $errors['errors'])); } // Format the DateTime object with the output format $formattedDate = $dateTime->format($output_format); diff --git a/app/Models/InsurerStatements.php b/app/Models/InsurerStatements.php new file mode 100644 index 00000000..6b85ed0f --- /dev/null +++ b/app/Models/InsurerStatements.php @@ -0,0 +1,30 @@ +db->table('pt_co_share_details pt_co') + ->select(' + pt_co.id, + pt_co.pt_id, + pt_co.exp_amt, + pt.id AS policy_transaction_id, + pt.endorsement_no, + c.client_name, + pt.created_at, + pt_co.insurer_id, + pt_co.insurer_branch_id, + pt.policy_no, + pt.policy_issue_date, + pt.policy_start_date, + pt.policy_end_date, + pt.status, + pt_co.bp_amt, + pt_co.tp_amt, + pt_co.tep_amt, + pt_co.exp_amt + ') + ->join('policy_transaction pt', 'pt_co.pt_id = pt.id') + ->join('clients c', 'pt.client_id = c.id') + ->where('pt_co.is_active', 1) + ->where('MONTH(pt.created_at)', $month) + ->where('YEAR(pt.created_at)', $year) + ->where('pt_co.insurer_id', $insurer_id) + ->where('pt_co.insurer_branch_id', $insurer_branch_id) + // ->where('pt_co.exp_amt', 0.00) + // ->orWhere('pt_co.exp_amt is null') + ->get() + ->getResultArray(); + } } diff --git a/app/Views/insurer_statement_list.php b/app/Views/insurer_statement_list.php new file mode 100644 index 00000000..29d14a7b --- /dev/null +++ b/app/Views/insurer_statement_list.php @@ -0,0 +1,708 @@ + + + + +
+
+
+
+
+
+

Insurer Statement List

+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Insurer
Month
Statement
Line Items
User/Time
Action
'.$row['first_name'] ?> + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/Views/layout/footer.php b/app/Views/layout/footer.php index 27e232ec..22f12861 100644 --- a/app/Views/layout/footer.php +++ b/app/Views/layout/footer.php @@ -299,7 +299,7 @@ fetchMessages(); - setInterval(fetchMessages, 10000); // Fetch messages every ten seconds + setInterval(fetchMessages, 60000); // Fetch messages every sixty seconds }); $(document).ready(function() { diff --git a/app/Views/layout/header.php b/app/Views/layout/header.php index 03b42981..de039f2b 100644 --- a/app/Views/layout/header.php +++ b/app/Views/layout/header.php @@ -670,6 +670,9 @@
  • BDS
  • +
  • + Statement upload +
  • diff --git a/public/sample_excel/insurer_stament_sample.xlsx b/public/sample_excel/insurer_stament_sample.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0cdabdf1cc8ce0b6f56d6c603e928a1058a38cde GIT binary patch literal 6594 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 literal 0 HcmV?d00001