diff --git a/.env.sample b/.env.sample
index 49343c62..50244384 100755
--- a/.env.sample
+++ b/.env.sample
@@ -129,4 +129,7 @@ HEALTH_INDIA_TOKEN_URL =
HEALTH_INDIA_USERNAME =
HEALTH_INDIA_PASSWORD =
-HEALTH_INDIA_PRIMARY_KEY_CONSTANT =
\ No newline at end of file
+HEALTH_INDIA_PRIMARY_KEY_CONSTANT =
+
+# BDS Daily Report Emails Configuration
+bds.dailyReportEmails =
diff --git a/app/Config/Acl.php b/app/Config/Acl.php
index 2960595f..b4d85eac 100644
--- a/app/Config/Acl.php
+++ b/app/Config/Acl.php
@@ -32,6 +32,8 @@ class Acl
'#^/metaTpaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/sales/dashboard#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
'#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
+ '#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
+
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 37e7f840..cf5fe44e 100755
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -444,11 +444,17 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) {
$routes->get('proceedExcelFileDataValidation', 'EmployeeController::proceedExcelFileDataValidation');
$routes->get('checkTpaApiEnable', 'EmployeeRestController::checkTpaApiEnable');
$routes->get('generateDemographyDataTable', 'LeadsController::generateDemographyDataTable');
- $routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend');
+ $routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend');
+ $routes->get('getTpaClaimDumpErrorData/(:any)', 'TicketServiceController::getTpaClaimDumpErrorData/$1');
+ $routes->get('croneDailyActivityReport', 'DashboardController::croneDailyActivityReport');
});
$routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
$routes->cli("cli/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
+$routes->cli("cli/cronDailyBDSReport", "PolicyTransactionController::cronDailyBDSReport");
+$routes->cli('cli/croneDailyActivityReport', 'DashboardController::croneDailyActivityReport');
+
+
$routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) {
@@ -496,6 +502,7 @@ $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) {
//$routes->post('failedStatement',"PolicyTransactionController::failedStatementList");
});
+ $routes->get("cronDailyBDSReport", "PolicyTransactionController::cronDailyBDSReport");
});
$routes->group("leads", ["filter" => "authMVC"], function ($routes) {
@@ -1011,4 +1018,13 @@ $routes->group('sales', function($routes) {
$routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard');
});
+// Expence Module Route Group
+$routes->group('expense', ["filter" => "authMVC", 'namespace' => 'App\Controllers'], static function($routes) {
+ $routes->get('/', 'ExpenseController::index');
+ $routes->post('save', 'ExpenseController::save');
+ $routes->get('get/(:num)', 'ExpenseController::getExpense/$1');
+ $routes->post('delete/(:num)', 'ExpenseController::delete/$1');
+ $routes->get('client-policies', 'ExpenseController::clientPolicies');
+});
+
diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php
index 24c0f2ec..ee3a8098 100755
--- a/app/Controllers/ClientController.php
+++ b/app/Controllers/ClientController.php
@@ -6969,16 +6969,23 @@ class ClientController extends AdminController
// $response = $ticketServiceController->getClaimExcelErrorData(["file_id" => 41]);
// $response = $ticketServiceController->claimDumpOnBoardProcess(["file_id" => 17]);
// $response = $ticketServiceController->extractExcelData("claims_dump_form_client.xlsx");
+
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 51]); //abhi
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 50]); //fhpl
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 53]); //icici
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 54]); //mediassist
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 52]); //reliance
- // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 48]); //vidal
- // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 48]); //vidal
- // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 54]); //mediassist
+ // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 48]); //vidal`
+ // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 57]); //mediassist
- // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 4]); //abhi
+
+ // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 51]); //abhi
+ // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 50]); //fhpl
+ // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 53]); //icici
+ // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 54]); //mediassist
+ // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 52]); //reliance
+ // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 48]); //vidal
+ // $response = $ticketServiceController->getTpaClaimDumpErrorData(["file_id" => 48]);
// dd($response);
// ---------- TICKET CONTROLLER --------------------------------------------------------------------------------
diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php
index 1dab325b..b4d224fe 100755
--- a/app/Controllers/DashboardController.php
+++ b/app/Controllers/DashboardController.php
@@ -7,6 +7,7 @@ use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
use App\Helpers\sendMailNotification;
+use App\Helpers\MailHelper;
use CodeIgniter\API\ResponseTrait;
@@ -24,6 +25,11 @@ use App\Controllers\EmpDataServiceController;
use App\Models\TicketMasterModel;
use App\Models\LeadsModel;
use App\Models\TicketClaimStatusModel;
+use App\Models\FileModel;
+use App\Models\BatchFileModel;
+use App\Models\SalesActivityModel;
+use App\Models\SalesActualLeadModel;
+
class DashboardController extends AdminController
@@ -43,6 +49,13 @@ class DashboardController extends AdminController
protected $policyStatus;
protected $colorShades;
protected $claimDashLimit;
+ protected $filesModel;
+ protected $batchFilesModel;
+ protected $leadsModel;
+ protected $ticketMasterModel;
+ protected $ticketClaimStatusModel;
+ protected $salesActivityModel;
+ protected $salesActualModel;
protected $myLogger;
@@ -59,6 +72,13 @@ class DashboardController extends AdminController
$this->ticketModel = new TicketMasterModel();
$this->leadModel = new LeadsModel();
$this->ticketStatusModel = new TicketClaimStatusModel();
+ $this->filesModel = new FileModel();
+ $this->batchFilesModel = new BatchFileModel();
+ $this->leadsModel = new LeadsModel();
+ $this->ticketMasterModel = new TicketMasterModel();
+ $this->ticketClaimStatusModel = new TicketClaimStatusModel();
+ $this->salesActivityModel = new SalesActivityModel();
+ $this->salesActualModel = new SalesActualLeadModel();
$this->myLogger = \Config\Services::mylogger();
@@ -787,4 +807,298 @@ class DashboardController extends AdminController
$data['ticket_type_id'] = $ticketTypeId;
return $this->respond(['status' => "success", "data" => $data], 200);
}
+
+ public function croneDailyActivityReport()
+ {
+
+ // $today = date('Y-m-d');
+ $today = date('Y-m-d', strtotime('-1 day'));
+
+ // Inception & endorsement counts (file uploads)
+ $total_inception_count = $this->filesModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('action', 'inception')
+ ->where('status', 'success')
+ ->countAllResults();
+
+ $total_endorsement_count = $this->filesModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('action !=', 'inception')
+ ->where('status', 'success')
+ ->countAllResults();
+
+ // TPA & Insurer batch file counts
+ $total_tpa_incetion_count = $this->batchFilesModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('event_type', 'inception')
+ ->where('insurer_or_tpa', 'tpa')
+ ->where('status', 'success')
+ ->countAllResults();
+
+ $total_tpa_endorsement_count = $this->batchFilesModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('event_type !=', 'inception')
+ ->where('insurer_or_tpa', 'tpa')
+ ->where('status', 'success')
+ ->countAllResults();
+
+ $total_insurer_incetion_count = $this->batchFilesModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('event_type', 'inception')
+ ->where('insurer_or_tpa', 'insurer')
+ ->where('status', 'success')
+ ->countAllResults();
+
+ $total_insurer_endorsement_count = $this->batchFilesModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('event_type !=', 'inception')
+ ->where('insurer_or_tpa', 'insurer')
+ ->where('status', 'success')
+ ->countAllResults();
+
+ // Claim counts
+ $total_claim_count = $this->ticketMasterModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->countAllResults();
+
+ $total_gmc_status_wise_claim_count = $this->ticketMasterModel
+ ->select('tcs.claim_status, count(*) as count')
+ ->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left')
+ ->where('ticket_master.is_active', 1)
+ ->where('ticket_master.ticket_type_id', 1)
+ // ->where('DATE(ticket_master.created_at)', $today)
+ ->where('tcs.is_active', 1)
+ ->groupBy('tcs.claim_status')
+ ->findAll();
+
+ $total_gpa_status_wise_claim_count = $this->ticketMasterModel
+ ->select('tcs.claim_status, count(*) as count')
+ ->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left')
+ ->where('ticket_master.is_active', 1)
+ ->where('ticket_master.ticket_type_id', 2)
+ // ->where('DATE(ticket_master.created_at)', $today)
+ ->where('tcs.is_active', 1)
+ ->groupBy('tcs.claim_status')
+ ->findAll();
+
+ $total_edli_status_wise_claim_count = $this->ticketMasterModel
+ ->select('tcs.claim_status, count(*) as count')
+ ->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left')
+ ->where('ticket_master.is_active', 1)
+ ->where('ticket_master.ticket_type_id', 3)
+ // ->where('DATE(ticket_master.created_at)', $today)
+ ->where('tcs.is_active', 1)
+ ->groupBy('tcs.claim_status')
+ ->findAll();
+
+ $total_gtli_status_wise_claim_count = $this->ticketMasterModel
+ ->select('tcs.claim_status, count(*) as count')
+ ->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left')
+ ->where('ticket_master.is_active', 1)
+ ->where('ticket_master.ticket_type_id', 4)
+ // ->where('DATE(ticket_master.created_at)', $today)
+ ->where('tcs.is_active', 1)
+ ->groupBy('tcs.claim_status')
+ ->findAll();
+
+ // Sales / lead counts
+ $total_opportunity_count = $this->leadsModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->countAllResults();
+
+ $total_rfq_created_count = $this->leadsModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('status', 'rfq_created')
+ ->countAllResults();
+
+ $total_rfq_insurer_send_count = $this->leadsModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('status', 'rfq_sent')
+ ->countAllResults();
+
+ $total_qcr_created_count = $this->leadsModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('status', 'qcr_created')
+ ->countAllResults();
+
+ $total_qcr_client_send_count = $this->leadsModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('status', 'qcr_sent')
+ ->countAllResults();
+
+ $total_placement_count = $this->leadsModel
+ ->where('is_active', 1)
+ ->where('DATE(created_at)', $today)
+ ->where('status', 'won')
+ ->countAllResults();
+
+ $total_activity_count = $this->salesActivityModel
+ ->where('DATE(created_at)', $today)
+ ->countAllResults();
+
+ $total_lead_count = $this->salesActualModel
+ ->where('DATE(created_at)', $today)
+ ->countAllResults();
+
+ $total_bds_count = $this->policyTransactionModel
+ ->select('pt_co_share_details.*')
+ ->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id')
+ ->where('DATE(policy_transaction.created_at)', $today)
+ ->where('pt_co_share_details.is_active', 1)
+ ->where('policy_transaction.is_active', 1)
+ ->countAllResults();
+
+ $total_bds_policy_wise_count = $this->policyTransactionModel
+ ->select('pt_co_share_details.*')
+ ->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id')
+ ->where('DATE(policy_transaction.created_at)', $today)
+ ->where('pt_co_share_details.is_active', 1)
+ ->where('policy_transaction.action_type', 'inception')
+ ->where('policy_transaction.is_active', 1)
+ ->countAllResults();
+
+
+ $total_bds_endorsement_wise_count = $this->policyTransactionModel
+ ->select('pt_co_share_details.*')
+ ->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id')
+ ->where('DATE(policy_transaction.created_at)', $today)
+ ->where('pt_co_share_details.is_active', 1)
+ ->where('policy_transaction.action_type !=', 'inception')
+ ->where('policy_transaction.is_active', 1)
+ ->countAllResults();
+
+ $total_bds_policy_type_wise_count = $this->policyTransactionModel
+ ->select('policy_type.policy_type, count(*) as count')
+ ->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id')
+ ->join('policy_type', 'policy_transaction.policy_type_id = policy_type.id')
+ ->where('DATE(policy_transaction.created_at)', $today)
+ ->where('pt_co_share_details.is_active', 1)
+ ->where('policy_transaction.is_active', 1)
+ ->groupBy('policy_transaction.policy_type_id')
+ ->findAll();
+
+ // Connect Pre DB for Employee Enrollment Count
+
+ $db2 = \Config\Database::connect('preDB');
+
+ $builder = $db2->table('employees');
+ $total_employee_draft_count = $builder->join('employee_polices', 'employees.id = employee_polices.employee_id')
+ ->select('employee_polices.status, count(*) as count')
+ ->where('DATE(employee_polices.created_at)', $today)
+ ->where('employee_polices.is_active', 1)
+ ->where('employees.is_active', 1)
+ ->whereIn('employee_polices.status', ['draft'])
+ ->groupBy('employee_polices.status')
+ ->countAllResults();
+
+
+ $builder1 = $db2->table('employees');
+ $total_employee_enrolled_count = $builder1->join('employee_polices', 'employees.id = employee_polices.employee_id')
+ ->select('employee_polices.status, count(*) as count')
+ ->where('DATE(employee_polices.created_at)', $today)
+ ->where('employee_polices.is_active', 1)
+ ->where('employees.is_active', 1)
+ ->whereIn('employee_polices.status', ['enrolled'])
+ ->groupBy('employee_polices.status')
+ ->countAllResults();
+
+ $builder2 = $db2->table('files');
+ $total_open_for_enrollemnt_policy_count = $builder2
+ ->select('COUNT(*) as count')
+ ->where('enrollment_open_date <=', $today)
+ ->where('enrollment_close_date >=', $today)
+ ->where('is_active', 1)
+ ->where('status', 'success')
+ ->countAllResults();
+
+ $data = [
+ 'total_inception_count' => $total_inception_count,
+ 'total_endorsement_count' => $total_endorsement_count,
+ 'total_tpa_incetion_count' => $total_tpa_incetion_count,
+ 'total_tpa_endorsement_count' => $total_tpa_endorsement_count,
+ 'total_insurer_incetion_count' => $total_insurer_incetion_count,
+ 'total_insurer_endorsement_count' => $total_insurer_endorsement_count,
+ 'total_claim_count' => $total_claim_count,
+ 'total_gmc_status_wise_claim_count' => $total_gmc_status_wise_claim_count,
+ 'total_gpa_status_wise_claim_count' => $total_gpa_status_wise_claim_count,
+ 'total_edli_status_wise_claim_count' => $total_edli_status_wise_claim_count,
+ 'total_gtli_status_wise_claim_count' => $total_gtli_status_wise_claim_count,
+ 'total_opportunity_count' => $total_opportunity_count,
+ 'total_rfq_created_count' => $total_rfq_created_count,
+ 'total_rfq_insurer_send_count' => $total_rfq_insurer_send_count,
+ 'total_qcr_created_count' => $total_qcr_created_count,
+ 'total_qcr_client_send_count' => $total_qcr_client_send_count,
+ 'total_placement_count' => $total_placement_count,
+ 'total_activity_count' => $total_activity_count,
+ 'total_lead_count' => $total_lead_count,
+ 'total_bds_count' => $total_bds_count,
+ 'total_bds_policy_wise_count' => $total_bds_policy_wise_count,
+ 'total_bds_endorsement_wise_count' => $total_bds_endorsement_wise_count,
+ 'total_bds_policy_type_wise_count' => $total_bds_policy_type_wise_count,
+ 'total_employee_draft_count' => $total_employee_draft_count,
+ 'total_employee_enrolled_count' => $total_employee_enrolled_count,
+ 'total_open_for_enrollemnt_policy_count' => $total_open_for_enrollemnt_policy_count,
+ ];
+
+ // dd($data);
+
+ $today = date('d-m-Y', strtotime($today));
+ $data['today'] = $today;
+
+ // Render the HTML email using the dedicated view
+ $message = view('daily_report_email_template', $data);
+ // return $message;
+
+ // Recipients: prefer dedicated env, fallback to BDS report emails if not set
+ $emailList = getenv('activity.dailyReportEmails') ?: getenv('bds.dailyReportEmails') ?: '';
+ $recipientEmails = array_filter(array_map('trim', explode(',', $emailList)));
+
+ if (empty($recipientEmails)) {
+ $this->myLogger->logme('error', 'croneDailyActivityReport: No recipients configured (set activity.dailyReportEmails in .env).');
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'Daily activity data prepared but no recipients configured.',
+ 'data' => $data,
+ ], 200);
+ }
+
+ $subject = "Daily Activity Report - {$today}";
+
+ $res = MailHelper::send_email([
+ 'mail' => $recipientEmails,
+ 'subject' => $subject,
+ 'message' => $message,
+ ]);
+
+ $resDecoded = is_string($res) ? json_decode($res, true) : $res;
+
+ if (isset($resDecoded['status']) && $resDecoded['status'] === 'success') {
+ $this->myLogger->logme('error', 'croneDailyActivityReport: Report email sent to ' . count($recipientEmails) . ' recipients');
+ return $this->respond([
+ 'status' => true,
+ 'message' => 'Daily activity report emailed successfully.',
+ 'recipients' => count($recipientEmails),
+ ], 200);
+ }
+
+ $this->myLogger->logme('error', 'croneDailyActivityReport: Email send failed - ' . json_encode($resDecoded));
+ return $this->respond([
+ 'status' => false,
+ 'message' => 'Daily activity data prepared but email send failed.',
+ 'data' => $data,
+ ], 500);
+ }
+
}
diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php
index 3a0bea5b..df5dcfab 100755
--- a/app/Controllers/EmployeeRestController.php
+++ b/app/Controllers/EmployeeRestController.php
@@ -2418,6 +2418,11 @@ class EmployeeRestController extends AdminController
["ticket_type" => "4", "type_name" => "GTLI"],
];
+ $claim_type = $this->ticketController->claimType;
+ unset($claim_type[1][2]);
+ unset($claim_type[1][4]);
+ $data['claim_type'] = $claim_type;
+
return $this->respond(['status' => (count($data) ? 'success' : 'failed'), 'code' => (count($data) ? 200 : 404), 'data' => $data], 200);
}
@@ -2581,61 +2586,90 @@ class EmployeeRestController extends AdminController
$required_docs = $this->ticketMaster->select('required_docs')->where('id', $ticket_id)->first();
$data['required_docs'] = json_decode($required_docs['required_docs'] ?? '{}', true) ?? [];
- // $ticketData = $data['ticket_data'];
- // $ticketHistory = $data['ticket_history'];
- // print_r($ticketHistory); die;
+ // print_rr($data['ticket_history']); die;
+
+ $filteredArray = array_filter($data['ticket_history'], function($item) {
+ return $item['field_name'] == 'claim_status_id';
+ });
+
+ $filteredArray = array_reverse(array_values($filteredArray));
$currentClaimStatus = $this->claimStatusModel->select("claim_status")->where('id', $data['claims_data']['claim_status_id'])->where('is_active', 1)->first();
$ticketClaimStatus = $this->claimStatusModel->select('claim_status, display_name')->where('display_name is not null')->where('ticket_type', $data['claims_data']['ticket_type_id'])->where('is_active', 1)->findAll();
$status_list = array_column($ticketClaimStatus, 'display_name', 'claim_status');
- // print_r($currentClaimStatus); die;
+ // dd($filteredArray, $status_list); die;
- $data['ticket_data'] = array_fill_keys(array_keys($data['ticket_data']), []);
+ $filtered_history = [];
+ $counter = 0;
+
+ foreach ($filteredArray as $key => $value) {
+ foreach ($status_list as $status => $display_name) {
+ if ($value['new_value'] == $status) {
- // Loop through ticket_data
- foreach ($data['ticket_data'] as $status => &$fields) {
- // Search ticket_history for matching old_status_value
- foreach ($data['ticket_history'] as $history) {
+ if($display_name == 'Under Process'){
+ $unique_key = $display_name . str_repeat("\u{200B}", $counter++);
+ }else{
+ $unique_key = $display_name;
+ }
- if ($history['old_status_value'] === $status) {
- // Attach modified_by and created_at
- // $fields['modified_by'] = $history['modified_by'];
- $fields['modified_by'] = "";
- $fields['modified_at'] = date('d-m-Y h:i A', strtotime($history['created_at']));
- // Break after first match (assuming latest entry is enough)
- break;
+ $filtered_history[$unique_key] = [
+ 'modified_by' => "",
+ 'modified_at' => date('d-m-Y h:i A', strtotime($value['created_at'])),
+ ];
}
}
}
+ // dd($filteredArray, $status_list, $filtered_history); die;
- $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_by'] = "";
- $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_at'] = $formatted = date('d-m-Y h:i A', strtotime($data['claims_data']['updated_at']));
- $new_ticket_data = [];
- foreach ($data['ticket_data'] as $oldKey => $value) {
+ // $data['ticket_data'] = array_fill_keys(array_keys($data['ticket_data']), []);
- // Only process if key exists in status_list
- if (! isset($status_list[$oldKey])) {
- continue; // skip and do NOT add to new array
- }
+ // // Loop through ticket_data
+ // foreach ($data['ticket_data'] as $status => &$fields) {
+ // // Search ticket_history for matching old_status_value
+ // foreach ($data['ticket_history'] as $history) {
- // Get new key based on mapping
- $newKey = $status_list[$oldKey];
+ // if ($history['old_status_value'] === $status) {
+ // // Attach modified_by and created_at
+ // // $fields['modified_by'] = $history['modified_by'];
+ // $fields['modified_by'] = "";
+ // $fields['modified_at'] = date('d-m-Y h:i A', strtotime($history['created_at']));
+ // // Break after first match (assuming latest entry is enough)
+ // break;
+ // }
+ // }
+ // }
- // Avoid duplicates
- if (! isset($new_ticket_data[$newKey])) {
- $new_ticket_data[$newKey] = $value;
- }
- }
+ // $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_by'] = "";
+ // $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_at'] = $formatted = date('d-m-Y h:i A', strtotime($data['claims_data']['updated_at']));
- uasort($new_ticket_data, function ($a, $b) {
- $timeA = \DateTime::createFromFormat('d-m-Y h:i A', $a['modified_at']);
- $timeB = \DateTime::createFromFormat('d-m-Y h:i A', $b['modified_at']);
- return $timeA <=> $timeB; // Ascending
- });
+ // $new_ticket_data = [];
- $data['ticket_data'] = $new_ticket_data;
+ // foreach ($data['ticket_data'] as $oldKey => $value) {
+
+ // // Only process if key exists in status_list
+ // if (! isset($status_list[$oldKey])) {
+ // continue; // skip and do NOT add to new array
+ // }
+
+ // // Get new key based on mapping
+ // $newKey = $status_list[$oldKey];
+
+ // // Avoid duplicates
+ // if (! isset($new_ticket_data[$newKey])) {
+ // $new_ticket_data[$newKey] = $value;
+ // }
+ // }
+
+ // uasort($new_ticket_data, function ($a, $b) {
+ // $timeA = \DateTime::createFromFormat('d-m-Y h:i A', $a['modified_at']);
+ // $timeB = \DateTime::createFromFormat('d-m-Y h:i A', $b['modified_at']);
+ // return $timeA <=> $timeB; // Ascending
+ // });
+
+ // $data['ticket_data'] = $new_ticket_data;
+ $data['ticket_data'] = $filtered_history;
$ticketMesssageModel = new TicketMessageModel();
$ticket_message = $ticketMesssageModel
@@ -5451,7 +5485,7 @@ class EmployeeRestController extends AdminController
'dashboard' => $database_id,
],
'exp' => time() + (10 * 60),
- 'params' => (object) [],
+ 'params' => (object) ['client_policy' => $policy_id ], // MUST be object for Metabase
];
$token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256');
diff --git a/app/Controllers/ExpenseController.php b/app/Controllers/ExpenseController.php
new file mode 100644
index 00000000..c734284d
--- /dev/null
+++ b/app/Controllers/ExpenseController.php
@@ -0,0 +1,412 @@
+myLogger = \Config\Services::mylogger();
+ $this->expenseModel = new ExpenseModel();
+ $this->clientModel = new ClientModel();
+ $this->clientPolicyModel = new ClientPolicyModel();
+ }
+
+ /**
+ * Web list + form view
+ */
+ public function index()
+ {
+ try {
+ $data['tab_name'] = 'Expense';
+ $data['page_name'] = 'Expense';
+ $descriptionPattern = '/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/u';
+
+ // Raw GET filters
+ $rawFilters = $this->request->getGet() ?? [];
+ $rawFilters = is_array($rawFilters) ? $rawFilters : [];
+
+ // Sanitize input array using existing helper
+ $sanitized = sanitizeInputArrayAdvanced($rawFilters);
+
+ $filters = [
+ 'client_id' => trim($sanitized['client_id'] ?? ''),
+ 'client_policy_id' => trim($sanitized['client_policy_id'] ?? ''),
+ 'approved_by' => trim($sanitized['approved_by'] ?? ''),
+ 'description' => trim($sanitized['description'] ?? ''),
+ 'amount' => trim($sanitized['amount'] ?? ''),
+ 'expense_date' => trim($sanitized['expense_date'] ?? ''),
+ ];
+
+ $validationErrors = [];
+
+ // Basic type / format validation for filters
+ if ($filters['client_id'] !== '' && ! ctype_digit($filters['client_id'])) {
+ $validationErrors[] = 'Invalid client selected for search.';
+ $filters['client_id'] = '';
+ }
+
+ if ($filters['client_policy_id'] !== '' && ! ctype_digit($filters['client_policy_id'])) {
+ $validationErrors[] = 'Invalid policy selected for search.';
+ $filters['client_policy_id'] = '';
+ }
+
+ if ($filters['approved_by'] !== '' && ! ctype_digit($filters['approved_by'])) {
+ $validationErrors[] = 'Invalid approver selected for search.';
+ $filters['approved_by'] = '';
+ }
+
+ if ($filters['description'] !== '' && ! preg_match($descriptionPattern, $filters['description'])) {
+ $validationErrors[] = 'Description filter contains invalid characters.';
+ $filters['description'] = '';
+ }
+
+ if ($filters['amount'] !== '') {
+ if (! is_numeric($filters['amount']) || (float) $filters['amount'] < 0) {
+ $validationErrors[] = 'Amount filter must be a non-negative number.';
+ $filters['amount'] = '';
+ }
+ }
+
+ if ($filters['expense_date'] !== '') {
+ $dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']);
+ $errors = $dt ? \DateTime::getLastErrors() : ['warning_count' => 1, 'error_count' => 1];
+ if (! $dt || ! empty($errors['warning_count']) || ! empty($errors['error_count'])) {
+ $validationErrors[] = 'Expense Date filter must be in DD-MM-YYYY format.';
+ $filters['expense_date'] = '';
+ }
+ }
+
+ $data['filters'] = $filters;
+ $data['validation_errors'] = $validationErrors;
+
+ // Clients for dropdown
+ $data['clients'] = $this->clientModel
+ ->select('id, client_name, short_name')
+ ->where('is_active', 1)
+ ->orderBy('client_name', 'ASC')
+ ->findAll();
+
+ // Approved by (users) dropdown
+ $db = db_connect();
+ $data['approved_users'] = $db->table('user_profiles')
+ ->select('id, first_name')
+ ->where('is_active', 1)
+ ->whereIn('id', [7, 8])
+ ->orderBy('first_name', 'ASC')
+ ->get()
+ ->getResultArray();
+
+ // Policies for filter dropdown (when client filter is selected)
+ $data['policies_for_filter'] = [];
+ if ($filters['client_id'] !== '') {
+ $data['policies_for_filter'] = $this->clientPolicyModel
+ ->select('id, policy_no')
+ ->where('client_id', (int) $filters['client_id'])
+ ->where('is_active', 1)
+ ->orderBy('id', 'ASC')
+ ->findAll();
+ }
+
+ // Existing expenses with optional filters
+ $builder = $this->expenseModel
+ ->select('
+ expenses.*,
+ clients.client_name,
+ clients.short_name,
+ client_policy.policy_no,
+ user_profiles.first_name AS approved_by_name
+ ')
+ ->join('clients', 'clients.id = expenses.client_id')
+ ->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left')
+ ->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left')
+ ->where('expenses.is_active', 1);
+
+ if ($filters['client_id'] !== '') {
+ $builder->where('expenses.client_id', (int) $filters['client_id']);
+ }
+
+ if ($filters['client_policy_id'] !== '') {
+ $builder->where('expenses.client_policy_id', (int) $filters['client_policy_id']);
+ }
+
+ if ($filters['approved_by'] !== '') {
+ $builder->where('expenses.approved_by', (int) $filters['approved_by']);
+ }
+
+ if ($filters['description'] !== '') {
+ $builder->like('expenses.description', (string) $filters['description']);
+ }
+
+ if ($filters['amount'] !== '') {
+ $builder->where('expenses.amount', (float) $filters['amount']);
+ }
+
+ if ($filters['expense_date'] !== '') {
+ $dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']);
+ if ($dt) {
+ $builder->where('expenses.expense_date', $dt->format('Y-m-d'));
+ }
+ }
+
+ $data['expenses'] = $builder
+ ->orderBy('expenses.id', 'DESC')
+ ->findAll();
+
+ return $this->loadLayout('expense_list', $data);
+ } catch (\Throwable $e) {
+ return handle_exception($e, $this->myLogger, $this->response);
+ }
+ }
+
+ /**
+ * Create / update expense (AJAX)
+ */
+ public function save()
+ {
+ try {
+ if ($this->request->getMethod() !== 'post') {
+ return $this->response
+ ->setStatusCode(405)
+ ->setJSON([
+ 'status' => false,
+ 'message' => 'Invalid request method',
+ ]);
+ }
+
+ $rawData = $this->request->getPost();
+ $data = sanitizeInputArrayAdvanced($rawData);
+
+ $id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : null;
+
+ $rules = [
+ 'client_id' => [
+ 'rules' => 'required|is_natural_no_zero',
+ 'errors' => [
+ 'required' => 'Client is required',
+ ],
+ ],
+ 'client_policy_id' => [
+ 'rules' => 'required|is_natural_no_zero',
+ 'errors' => [
+ 'required' => 'Policy is required',
+ ],
+ ],
+ 'description' => [
+ 'rules' => 'required|string|min_length[1]|max_length[2000]|regex_match[/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/]',
+ 'errors' => [
+ 'required' => 'Description is required',
+ 'min_length' => 'Description cannot be empty',
+ 'regex_match' => 'Description contains invalid characters.',
+ ],
+ ],
+ 'expense_date' => [
+ 'rules' => 'required|valid_date[d-m-Y]',
+ 'errors' => [
+ 'required' => 'Expense Date is required',
+ 'valid_date' => 'Expense Date must be in DD-MM-YYYY format',
+ ],
+ ],
+ 'approved_by' => [
+ 'rules' => 'required|is_natural_no_zero',
+ 'errors' => [
+ 'required' => 'Approved By is required',
+ ],
+ ],
+ 'amount' => [
+ 'rules' => 'required|numeric|greater_than_equal_to[0]',
+ 'errors' => [
+ 'required' => 'Amount is required',
+ 'numeric' => 'Amount must be numeric',
+ 'greater_than_equal_to' => 'Amount cannot be negative',
+ ],
+ ],
+ ];
+
+ if (! $this->validate($rules)) {
+ return $this->response->setStatusCode(400)->setJSON([
+ 'status' => false,
+ 'message' => 'Input validation failed',
+ 'errors' => $this->validator->getErrors(),
+ ]);
+ }
+
+ $payload = [
+ 'client_id' => (int) ($data['client_id'] ?? 0),
+ 'client_policy_id' => (int) ($data['client_policy_id'] ?? 0),
+ 'description' => $data['description'] ?? null,
+ 'approved_by' => (int) ($data['approved_by'] ?? 0),
+ 'amount' => $data['amount'] ?? null,
+ 'is_active' => 1,
+ ];
+
+ $expenseDate = $data['expense_date'] ?? null;
+ if (! empty($expenseDate)) {
+ $dt = \DateTime::createFromFormat('d-m-Y', $expenseDate);
+ $payload['expense_date'] = $dt ? $dt->format('Y-m-d') : null;
+ } else {
+ $payload['expense_date'] = null;
+ }
+
+ if ($id === null) {
+ $insertId = $this->expenseModel->insert($payload, true);
+ $success = ! empty($insertId);
+ $id = $insertId;
+ $message = $success
+ ? 'Expense created successfully'
+ : 'Unable to create expense. Please try again.';
+ } else {
+ $success = $this->expenseModel->update($id, $payload);
+ $message = $success
+ ? 'Expense updated successfully'
+ : 'Unable to update expense. Please try again.';
+ }
+
+ return $this->response
+ ->setStatusCode($success ? 200 : 400)
+ ->setJSON([
+ 'status' => (bool) $success,
+ 'message' => $message,
+ 'id' => $id,
+ ]);
+ } catch (\Throwable $e) {
+ return handle_exception($e, $this->myLogger, $this->response);
+ }
+ }
+
+ /**
+ * Get single expense (AJAX)
+ */
+ public function getExpense($id = null)
+ {
+ try {
+ $id = (int) $id;
+
+ if (empty($id)) {
+ return $this->response->setStatusCode(400)->setJSON([
+ 'status' => false,
+ 'message' => 'Invalid expense id',
+ ]);
+ }
+
+ $expense = $this->expenseModel
+ ->select('
+ expenses.*,
+ clients.client_name,
+ clients.short_name,
+ client_policy.policy_no,
+ user_profiles.first_name AS approved_by_name
+ ')
+ ->join('clients', 'clients.id = expenses.client_id', 'left')
+ ->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left')
+ ->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left')
+ ->where('expenses.id', $id)
+ ->where('expenses.is_active', 1)
+ ->first();
+
+ if (empty($expense)) {
+ return $this->response->setStatusCode(404)->setJSON([
+ 'status' => false,
+ 'message' => 'Expense not found',
+ ]);
+ }
+
+ return $this->response->setStatusCode(200)->setJSON([
+ 'status' => true,
+ 'data' => $expense,
+ ]);
+ } catch (\Throwable $e) {
+ return handle_exception($e, $this->myLogger, $this->response);
+ }
+ }
+
+ /**
+ * Soft delete expense (AJAX)
+ */
+ public function delete($id = null)
+ {
+ try {
+ if ($this->request->getMethod() !== 'post') {
+ return $this->response
+ ->setStatusCode(405)
+ ->setJSON([
+ 'status' => false,
+ 'message' => 'Invalid request method',
+ ]);
+ }
+
+ $id = (int) $id;
+
+ if (empty($id)) {
+ return $this->response->setStatusCode(400)->setJSON([
+ 'status' => false,
+ 'message' => 'Invalid expense id',
+ ]);
+ }
+
+ $payload = [
+ 'is_active' => 0,
+ ];
+
+ $success = $this->expenseModel->update($id, $payload);
+
+ return $this->response
+ ->setStatusCode($success ? 200 : 400)
+ ->setJSON([
+ 'status' => (bool) $success,
+ 'message' => $success
+ ? 'Expense deleted successfully'
+ : 'Unable to delete expense. Please try again.',
+ ]);
+ } catch (\Throwable $e) {
+ return handle_exception($e, $this->myLogger, $this->response);
+ }
+ }
+
+ /**
+ * Get policies by client for dropdown (AJAX)
+ */
+ public function clientPolicies()
+ {
+ try {
+ $clientId = (int) ($this->request->getGet('client_id') ?? 0);
+
+ if (empty($clientId)) {
+ return $this->response->setStatusCode(400)->setJSON([
+ 'status' => false,
+ 'message' => 'Client is required',
+ ]);
+ }
+
+ $policies = $this->clientPolicyModel
+ ->select('client_policy.id, client_policy.policy_no, policy_type.policy_type')
+ ->join('policy_type', 'policy_type.id = client_policy.policy_type_id AND policy_type.is_active = 1')
+ ->where('client_policy.client_id', $clientId)
+ ->where('client_policy.is_active', 1)
+ ->orderBy('client_policy.id', 'ASC')
+ ->findAll();
+
+ return $this->response->setStatusCode(200)->setJSON([
+ 'status' => true,
+ 'data' => $policies,
+ ]);
+ } catch (\Throwable $e) {
+ return handle_exception($e, $this->myLogger, $this->response);
+ }
+ }
+}
+
diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php
index 59c115fe..44ed315b 100644
--- a/app/Controllers/PolicyTransactionController.php
+++ b/app/Controllers/PolicyTransactionController.php
@@ -6098,8 +6098,189 @@ class PolicyTransactionController extends BaseController
return array('status' => false, 'message' => 'file_id is required');
}
- $this->policyTransactionModel->select()->findAll();
+ $data['is_active'] = 0;
+ $this->policyTransactionModel->where('file_id', $file_id)->set($data)->update();
+ $this->PTCOShareDetailsModel->where('file_id', $file_id)->set($data)->update();
+ return $this->respond(['status' => true, 'code' => 200, 'message' => 'BDS bulk upload data truncated successfully'], 200);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Cron job: Daily BDS Report
+ * Fetches today's BDS entries, generates Excel (matching report_bds.php column order), and emails to recipients from .env.
+ * Recipients: comma-separated emails in bds.reportEmails
+ * File stored temporarily in writable/tmp/ and deleted after sending.
+ */
+ public function cronDailyBDSReport()
+ {
+
+ helper('excel_import_export_helper');
+
+ $filePath = null;
+ try {
+
+ $today = date('Y-m-d', strtotime('-1 day'));
+ $reportList = $this->policyTransactionModel->getBDSReportList($today, $today,0,0,0,'created_at',0,0,0,0,0,[]);
+
+ if (empty($reportList)) {
+ $this->myLogger->logme('error', "cronDailyBDSReport: No BDS records for {$today}");
+ return $this->respond(['status' => 'success', 'message' => 'No BDS records for today. No report sent.'], 200);
+ }
+
+ // Column headers exactly matching report_bds.php order (including display:none columns)
+ $headers = [
+ 'S. No', 'User', 'Month', 'Business Type', 'Client Type', 'Insured Name', 'Transaction Type',
+ 'Policy Type', 'BAP Group', 'Vehicle Number', 'Policy No', 'Endorsement No', 'Insurer Branch',
+ 'Endorsement Effective Date', 'Policy Effective Date', 'Policy Expiry Date', 'Reference', 'Remarks',
+ 'BP Premium', 'TP Premium', 'Premium (without GST)', 'Total Premium', 'Agreed BP %', 'Agreed TP %',
+ 'Rewards', 'Agreed Amount', 'Invoiced Amount', 'Outstanding Amount',
+ 'Salse Person', 'Service Person', 'Salse Person Branch', 'Installment', 'Data Received Date', 'Renewal Date',
+ 'Co-Premium', 'Remuneration Pay By Leader', 'Salse Person Manager', 'Service Person Manager',
+ 'Service Person Branch', 'Rollover Date', 'Policyholder Name', 'Insured (Same as Proposer)',
+ 'Follower Policy No', 'Co-Share %', 'Non Commissional Premium Amount', 'CGST', 'SGST', 'IGST',
+ 'Stamp Duty', 'Standard BP %', 'Standard TP %', 'Actual BP Amount', 'Actual TP Amount',
+ 'Actual BP %', 'Actual TP %', 'Actual BP Remuneration Amount', 'Actual TP Remuneration Amount',
+ 'CD Account No'
+ ];
+
+ $excelData = [];
+ foreach ($reportList as $idx => $row) {
+ $totalIrda = (float)($row['total_irda_amt'] ?? 0);
+ $billedAmt = (float)($row['billed_amt'] ?? 0);
+ $unbilledAmt = $totalIrda - $billedAmt;
+ if ($totalIrda == 0) {
+ $unbilledAmt = abs($unbilledAmt);
+ }
+ $unbilledAmt = ($unbilledAmt == 0 && $billedAmt == 0) ? $totalIrda : $unbilledAmt;
+
+ $hasIrda = ($totalIrda != 0);
+
+ $excelData[] = [
+ $idx + 1,
+ $row['user_name'] ?? 'N/A',
+ $row['policy_issue_month'] ?? 'N/A',
+ $row['revenue_type'] ?? 'N/A',
+ $row['client_type'] ?? 'N/A',
+ $row['client_name'] ?? 'N/A',
+ $row['action_type'] ?? 'N/A',
+ $row['policy_type'] ?? 'N/A',
+ $row['bap'] ?? 'N/A',
+ $row['vehicle_no'] ?? 'N/A',
+ $row['policy_no'] ?? 'N/A',
+ $row['endorsement_no'] ?? 'N/A',
+ $row['insurer_branch_name'] ?? 'N/A',
+ !empty($row['endorse_eff_date']) ? change_date_format($row['endorse_eff_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ !empty($row['policy_start_date']) ? change_date_format($row['policy_start_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ !empty($row['policy_end_date']) ? change_date_format($row['policy_end_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ $row['ref'] ?? 'N/A',
+ $row['remarks'] ?? 'N/A',
+ $hasIrda ? ($row['bp_amt'] ?? '0.00') : '0.00',
+ $hasIrda ? ($row['tp_or_ter'] ?? '0.00') : '0.00',
+ $hasIrda ? ($row['premium_wo_gst'] ?? '0.00') : '0.00',
+ $hasIrda ? ($row['total_premium'] ?? '0.00') : '0.00',
+ $hasIrda ? ($row['agreed_bp_per'] ?? '0.00') . '%' : '0.00%',
+ $hasIrda ? ($row['agreed_tp_or_ter_per'] ?? '0.00') . '%' : '0.00%',
+ $row['reward'] ?? '0.00',
+ $row['total_irda_amt'] ?? '0.00',
+ !empty($row['billed_amt']) ? $row['billed_amt'] : '0.00',
+ number_format((float)$unbilledAmt, 2, '.', ''),
+ $row['salse_person_name'] ?? 'N/A',
+ $row['service_person_name'] ?? 'N/A',
+ $row['nhance_branch'] ?? 'N/A',
+ $row['installment'] ?? 'N/A',
+ !empty($row['data_received_date']) ? change_date_format($row['data_received_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ !empty($row['renewal_date']) ? change_date_format($row['renewal_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ $row['co_share'] ?? 'No',
+ $row['bro_payable_by'] ?? 'No',
+ $row['salse_manager_name'] ?? 'N/A',
+ $row['service_manager_name'] ?? 'N/A',
+ $row['service_branch'] ?? 'N/A',
+ !empty($row['rollover_date']) ? change_date_format($row['rollover_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ $row['policy_holder_name'] ?? 'N/A',
+ $row['same_as_proposer'] ?? 'No',
+ $row['follower_policy_no'] ?? 'N/A',
+ number_format((float)($row['co_share_per'] ?? 0), 2),
+ number_format((float)($row['non_comm_per_amt'] ?? 0), 2),
+ number_format((float)($row['bp_cgst'] ?? 0), 2),
+ number_format((float)($row['bp_sgst'] ?? 0), 2),
+ number_format((float)($row['bp_igst'] ?? 0), 2),
+ number_format((float)($row['stamp_duty'] ?? 0), 2),
+ number_format((float)($row['standerd_bp_per'] ?? 0), 2),
+ number_format((float)($row['standerd_tp_per'] ?? 0), 2),
+ number_format((float)($row['actual_bp_amt'] ?? 0), 2),
+ number_format((float)($row['actual_tp_amt'] ?? 0), 2),
+ number_format((float)($row['actual_bp_per'] ?? 0), 2),
+ number_format((float)($row['actual_tp_per'] ?? 0), 2),
+ number_format((float)($row['actual_tep_brokerage_amt'] ?? 0), 2),
+ number_format((float)($row['actual_tp_brokerage_amt'] ?? 0), 2),
+ $row['cd_ac_no'] ?? 'N/A'
+ ];
+ }
+
+ $today = date('d-m-Y', strtotime($today));
+ $fileName = 'BDS_Daily_Report_' . $today . '.xlsx';
+ $tmpDir = WRITEPATH . 'tmp' . DIRECTORY_SEPARATOR;
+ if (!is_dir($tmpDir)) {
+ mkdir($tmpDir, 0755, true);
+ }
+ $filePath = $tmpDir . $fileName;
+
+ $generated = generate_excel($headers, $excelData, $filePath);
+ if (!$generated) {
+ $this->myLogger->logme('error', 'cronDailyBDSReport: Excel generation failed');
+ return $this->respond(['status' => false, 'message' => 'Excel generation failed'], 500);
+ }
+
+ $emailList = getenv('bds.dailyReportEmails') ?: '';
+ $recipientEmails = array_filter(array_map('trim', explode(',', $emailList)));
+ if (empty($recipientEmails)) {
+ @unlink($filePath);
+ $this->myLogger->logme('error', 'cronDailyBDSReport: No recipients in bds.dailyReportEmails. Report generated but not sent.');
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'Report generated. No recipients configured (set bds.dailyReportEmails in .env).',
+ 'file' => $fileName
+ ], 200);
+ }
+
+ $subject = "BDS Report Up to - {$today}";
+ $message = "
Please find attached the BDS report up to {$today}.
";
+ $message .= "Total records: " . count($reportList) . "
";
+
+ $attachments = [
+ ['filePath' => $filePath, 'fileName' => $fileName]
+ ];
+
+ $res = MailHelper::send_email([
+ 'mail' => $recipientEmails,
+ 'subject' => $subject,
+ 'message' => $message,
+ 'attachments' => $attachments
+ ]);
+
+ @unlink($filePath);
+
+ $resDecoded = is_string($res) ? json_decode($res, true) : $res;
+ if (isset($resDecoded['status']) && $resDecoded['status'] === 'success') {
+ $this->myLogger->logme('error', "cronDailyBDSReport: Report sent to " . count($recipientEmails) . " recipients");
+ return $this->respond([
+ 'status' => true,
+ 'message' => 'Report generated and emailed successfully.',
+ 'recipients' => count($recipientEmails)
+ ], 200);
+ }
+
+ $this->myLogger->logme('error', 'cronDailyBDSReport: Email send failed - ' . json_encode($resDecoded));
+ return $this->respond(['status' => false, 'message' => 'Report generated but email send failed'], 500);
+ } catch (Exception $e) {
+ if ($filePath && is_file($filePath)) {
+ @unlink($filePath);
+ }
+ $this->myLogger->logme('error', 'cronDailyBDSReport Exception: ' . $e->getMessage());
+ return $this->respond(['status' => false, 'message' => $e->getMessage()], 500);
+ }
}
}
diff --git a/app/Controllers/TestingController.php b/app/Controllers/TestingController.php
index daae4293..c83fe172 100644
--- a/app/Controllers/TestingController.php
+++ b/app/Controllers/TestingController.php
@@ -1097,10 +1097,12 @@ class TestingController extends BaseController
'dashboard' => $database_id
],
'exp' => time() + (10 * 60), // 10 minutes
- 'params' => (object)[]
+ 'params' => (object) ['client_policy' => $policy_id ], // MUST be object for Metabase
+
];
$token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256');
+ // dd($token);
if ($this->request->getGet('api') == 1) {
return $this->respond([
diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php
index 01109672..7c70ec06 100644
--- a/app/Controllers/TicketController.php
+++ b/app/Controllers/TicketController.php
@@ -1173,9 +1173,13 @@ class TicketController extends BaseController
'regex_match' => 'Policy Number can only contain letters, numbers, spaces, hyphens(-) underscores(_), and slashes(/).'
]
],
- 'tpa_no' => ['label' => 'TPA ID','rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9]+$/]','errors' => [
- 'regex_match' => 'TPA ID can only contain letters and numbers.'
- ]],
+ 'tpa_no' => [
+ 'label' => 'TPA ID',
+ 'rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/]+$/]',
+ 'errors' => [
+ 'regex_match' => 'TPA ID can only contain letters, numbers, and /.'
+ ]
+ ],
'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]',
'errors' => [
'required' => 'Mobile number is required',
@@ -1522,9 +1526,13 @@ class TicketController extends BaseController
'regex_match' => 'Policy Number can only contain letters, numbers, spaces, hyphens(-) underscores(_), and slashes(/).'
]
],
- 'tpa_no' => ['label' => 'TPA ID','rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9]+$/]','errors' => [
- 'regex_match' => 'TPA ID can only contain letters and numbers.'
- ]],
+ 'tpa_no' => [
+ 'label' => 'TPA ID',
+ 'rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/]+$/]',
+ 'errors' => [
+ 'regex_match' => 'TPA ID can only contain letters, numbers, and /.'
+ ]
+ ],
'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]',
'errors' => [
'required' => 'Mobile number is required',
diff --git a/app/Controllers/TicketServiceController.php b/app/Controllers/TicketServiceController.php
index f95f547d..8e96a780 100644
--- a/app/Controllers/TicketServiceController.php
+++ b/app/Controllers/TicketServiceController.php
@@ -30,8 +30,9 @@ use App\Models\TicketMasterModel;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
-class TicketServiceController extends BaseController
+class TicketServiceController extends AdminController
{
use ResponseTrait;
@@ -1840,39 +1841,86 @@ class TicketServiceController extends BaseController
{
}
-
-
+
+
// --------------------------------------------------------------------------------------------------------------------------------
-
- public function tpaClaimDumpImporter($params)
+ /**
+ * Resolve and validate Claim Dump file metadata and physical file for TPA imports.
+ *
+ * @param int|null $fileId
+ * @return array{status:bool,message?:string,fileData?:array,filePath?:string}
+ */
+ private function resolveTpaClaimDumpFile(?int $fileId): array
{
- $file_id = $params['file_id'] ?? null; // Move outside try to ensure catch can see it
+ if (empty($fileId)) {
+ return [
+ 'status' => false,
+ 'message' => 'File ID is missing',
+ ];
+ }
+
+ $fileData = $this->claimDumpFileModel->find((int) $fileId);
+ if (!$fileData) {
+ return [
+ 'status' => false,
+ 'message' => 'Invalid file ID. No file data found',
+ ];
+ }
+
+ $filePath = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR . $fileData['file_name'];
+
+ if (!is_file($filePath)) {
+ return [
+ 'status' => false,
+ 'message' => 'Claim dump file not found',
+ ];
+ }
+
+ return [
+ 'status' => true,
+ 'fileData' => $fileData,
+ 'filePath' => $filePath,
+ ];
+ }
+
+ /**
+ * First TPA-wise job – parse Excel and push data into TPA staging table.
+ *
+ * @param array $params
+ * @return array
+ */
+ public function tpaClaimDumpImporter(array $params)
+ {
+ $file_id = isset($params['file_id']) ? (int) $params['file_id'] : null;
try {
- $file_path = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR;
+ $resolved = $this->resolveTpaClaimDumpFile($file_id);
- if (!$file_id) {
- return ['status' => false, 'message' => 'File ID is missing'];
+ if ($resolved['status'] === false) {
+ return [
+ 'status' => false,
+ 'message' => $resolved['message'] ?? 'Unable to resolve claim dump file',
+ ];
}
- $fileData = $this->claimDumpFileModel->find((int)$file_id);
- if (!$fileData) {
- return ['status' => false, 'message' => 'Invalid file ID. No file data found'];
- }
-
- $file_full_path = $file_path . $fileData['file_name'];
- if (!is_file($file_full_path)) {
- return ['status' => false, 'message' => 'Claim dump file not found'];
- }
+ $fileData = $resolved['fileData'];
+ $filePath = $resolved['filePath'];
$handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0);
- $result = $handler->runTpaClaimDumpInsert($file_full_path, $file_id);
+ $result = $handler->runTpaClaimDumpInsert($filePath, $file_id);
if (!empty($result['status']) && $result['status'] === true) {
- Jobs::addJob(['job_name' => 'tpaClaimDumpToTicketMasterImporters', 'payload' => ['file_id' => $file_id]]);
+ Jobs::addJob([
+ 'job_name' => 'tpaClaimDumpToTicketMasterImporters',
+ 'payload' => ['file_id' => $file_id],
+ ]);
} else {
// FORCE FAIL LOGIC
- $this->markAsFailed($file_id, $result['message'] ?? 'System error contact admin', $fileData['created_by'] ?? null);
+ $this->markAsFailed(
+ $file_id,
+ $result['message'] ?? 'System error contact admin',
+ $fileData['created_by'] ?? null
+ );
}
return $result;
@@ -1884,9 +1932,9 @@ class TicketServiceController extends BaseController
}
return [
- 'status' => false,
- 'message' => 'TPA Claim dump import failed',
- 'error_data' => $th->getMessage()
+ 'status' => false,
+ 'message' => 'TPA Claim dump import failed',
+ 'error_data' => $th->getMessage(),
];
}
}
@@ -1908,30 +1956,37 @@ class TicketServiceController extends BaseController
return $this->claimDumpFileModel->update($file_id, $data);
}
- public function tpaClaimDumpToTicketMasterImporters($params)
+ /**
+ * Second TPA-wise job – move data from TPA staging into ticket_master.
+ *
+ * @param array $params
+ * @return array
+ */
+ public function tpaClaimDumpToTicketMasterImporters(array $params)
{
- $file_id = $params['file_id'] ?? null;
+ $file_id = isset($params['file_id']) ? (int) $params['file_id'] : null;
$fileData = null;
try {
- if (!$file_id) {
- return ['status' => false, 'message' => 'File ID is missing'];
+ $resolved = $this->resolveTpaClaimDumpFile($file_id);
+
+ if ($resolved['status'] === false) {
+ return [
+ 'status' => false,
+ 'message' => $resolved['message'] ?? 'Unable to resolve claim dump file',
+ ];
}
- $fileData = $this->claimDumpFileModel->where('id', $file_id)->first();
-
- if (!$fileData) {
- return ['status' => false, 'message' => 'Invalid file ID. No file data found to import'];
- }
+ $fileData = $resolved['fileData'];
$handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0);
- $result = $handler->runTicketMasterInsert($params);
+ $result = $handler->runTicketMasterInsert($params);
if (!empty($result['status']) && $result['status'] === true) {
// Success: Update the status to success
$this->claimDumpFileModel->update($file_id, [
'status' => 'success',
- 'reason' => null
+ 'reason' => null,
]);
} else {
// Logic failure: The runTicketMasterInsert returned status false
@@ -1957,8 +2012,8 @@ class TicketServiceController extends BaseController
'message' => $th->getMessage(),
'error_data' => [
'line' => $th->getLine(),
- 'file' => $th->getFile()
- ]
+ 'file' => $th->getFile(),
+ ],
];
}
}
@@ -2011,5 +2066,176 @@ class TicketServiceController extends BaseController
return $data;
}
-
+ public function getTpaClaimDumpErrorData($file_id)
+ {
+ $file_id = (int) $file_id;
+
+ // Ensure we always have a Response object, even if controller was instantiated manually
+ $response = $this->response ?? service('response');
+
+ if ($file_id <= 0) {
+ return $response
+ ->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
+ ->setJSON(['status' => false, 'message' => 'Invalid file id']);
+ }
+
+ $fileData = $this->claimDumpFileModel->find($file_id);
+
+ if (!$fileData) {
+ return $response
+ ->setStatusCode(ResponseInterface::HTTP_NOT_FOUND)
+ ->setJSON(['status' => false, 'message' => 'File record not found']);
+ }
+
+ $tpaId = (int) ($fileData['tpa_id'] ?? 0);
+
+ if ($tpaId <= 0) {
+ return $response
+ ->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
+ ->setJSON(['status' => false, 'message' => 'TPA not linked with this file']);
+ }
+
+ // Resolve TPA staging table based on configured TPA IDs
+ $tableName = match ($tpaId) {
+ (int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'claims_dump_vidal',
+ (int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'claims_dump_abhi',
+ (int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'claims_dump_medi_assist',
+ (int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'claims_dump_fhpl',
+ (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'claims_dump_reliance',
+ (int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'claims_dump_icici',
+ default => null,
+ };
+
+ if ($tableName === null) {
+ return $response
+ ->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
+ ->setJSON(['status' => false, 'message' => 'Unsupported TPA for error dump export']);
+ }
+
+ $db = \Config\Database::connect();
+ $builder = $db->table($tableName);
+
+ // Fetch only records belonging to this file and having a rejection reason
+ $rows = $builder
+ ->where('file_id', $file_id)
+ ->where('is_active', 1)
+ ->where('master_reject_reason IS NOT NULL', null, false)
+ ->get()
+ ->getResultArray();
+
+ if (empty($rows)) {
+ // No error records – return a small Excel file with just a message
+ $spreadsheet = new Spreadsheet();
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->setTitle('Errors');
+ $sheet->setCellValue('A1', 'Message');
+ $sheet->setCellValue('A2', 'No rejected records found for this file.');
+
+ $writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
+ ob_start();
+ $writer->save('php://output');
+ $excelOutput = ob_get_clean();
+
+ $filename = 'tpa_claim_dump_errors_' . $file_id . '.xlsx';
+
+ return $response
+ ->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
+ ->setHeader('Cache-Control', 'max-age=0')
+ ->setBody($excelOutput);
+ }
+
+ // Columns that must NOT be included in the Excel export
+ $excludedColumns = [
+ 'id',
+ 'client_id',
+ 'client_policy_id',
+ 'ticket_id',
+ 'file_id',
+ 'created_at',
+ 'updated_at',
+ 'created_by',
+ 'updated_by',
+ 'is_active',
+ ];
+
+ $firstRow = $rows[0];
+
+ // Build header mapping and track the "Rejected Reason" column index
+ $dbColumnOrder = [];
+ $displayHeaderLabels = [];
+ $rejectedReasonColIdx = null; // 1-based index for Excel column
+
+ foreach ($firstRow as $columnName => $_) {
+ if (in_array($columnName, $excludedColumns, true)) {
+ continue;
+ }
+
+ $dbColumnOrder[] = $columnName;
+
+ if ($columnName === 'master_reject_reason' || $columnName === 'master_rejection_reason_key') {
+ $displayHeaderLabels[] = 'Rejected Reason';
+ $rejectedReasonColIdx = count($displayHeaderLabels); // current column index (1-based)
+ } else {
+ $displayHeaderLabels[] = $columnName;
+ }
+ }
+
+ $spreadsheet = new Spreadsheet();
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->setTitle('Errors');
+
+ // Header row
+ $rowIndex = 1;
+ foreach ($displayHeaderLabels as $colIndex => $headerLabel) {
+ $columnLetter = Coordinate::stringFromColumnIndex($colIndex + 1);
+ $cellAddress = $columnLetter . $rowIndex;
+ $sheet->setCellValue($cellAddress, $headerLabel);
+ }
+
+ // Data rows
+ $rowIndex = 2;
+ foreach ($rows as $row) {
+ foreach ($dbColumnOrder as $i => $columnName) {
+ $colIndex = $i + 1;
+ $columnLetter = Coordinate::stringFromColumnIndex($colIndex);
+ $cellAddress = $columnLetter . $rowIndex;
+ $value = $row[$columnName] ?? null;
+
+ $sheet->setCellValue($cellAddress, $value);
+
+ // Highlight the "Rejected Reason" values
+ if ($rejectedReasonColIdx !== null && $colIndex === $rejectedReasonColIdx) {
+ $sheet->getStyle($cellAddress)
+ ->getFill()
+ ->setFillType(Fill::FILL_SOLID)
+ ->getStartColor()
+ ->setARGB('FFFFF4B2'); // light yellow
+ }
+ }
+
+ $rowIndex++;
+ }
+
+ // Autosize columns for better readability
+ $highestColumnIndex = count($displayHeaderLabels);
+ for ($col = 1; $col <= $highestColumnIndex; $col++) {
+ $columnLetter = Coordinate::stringFromColumnIndex($col);
+ $sheet->getColumnDimension($columnLetter)->setAutoSize(true);
+ }
+
+ $writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
+ ob_start();
+ $writer->save('php://output');
+ $excelOutput = ob_get_clean();
+
+ $filename = 'tpa_claim_dump_errors_' . $file_id . '.xlsx';
+
+ return $response
+ ->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
+ ->setHeader('Cache-Control', 'max-age=0')
+ ->setBody($excelOutput);
+ }
+
}
diff --git a/app/Libraries/MyGoogleDrive.php b/app/Libraries/MyGoogleDrive.php
index 566098eb..2fea0f9e 100644
--- a/app/Libraries/MyGoogleDrive.php
+++ b/app/Libraries/MyGoogleDrive.php
@@ -16,7 +16,7 @@ class MyGoogleDrive
{
$this->myLogger = \Config\Services::mylogger();
$this->client = new Google_Client();
- $this->client->setAuthConfig(ROOTPATH . 'nhance-app-google-drive.json'); // App credentials
+ $this->client->setAuthConfig(ROOTPATH . 'nhance-ee8d1-e3c5269b1ec7.json'); // App credentials
// putenv('GOOGLE_APPLICATION_CREDENTIALS=' . ROOTPATH . 'nhance-app-google-drive.json');
$this->client->useApplicationDefaultCredentials();
diff --git a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php
index ffb4b970..103989bf 100644
--- a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php
+++ b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php
@@ -119,6 +119,19 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
'Settled' => 11,
'Rejected' => 8,
'Cancelled' => 13,
+ 'Approved' => 9,
+ 'Closed' => 12,
+ 'Query' => 4,
+ 'Required Information' => 4,
+ 'In-Progress' => 5,
+ 'Paid' => 11,
+ 'Denied' => 8,
+ 'Cancelled' => 13,
+ 'Processed' => 61,
+ 'Information Awaited' => 4,
+ 'Denied Letter Sent' => 66,
+ 'Cashless Document Awaited' => 3,
+ 'RI Cancelled' => 13,
];
protected $dateColumns = [
@@ -164,14 +177,30 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
}
- public function updateTicketIdInTPATable(): bool
+ public function updateTicketIdInTPATable(int $fileId): bool
{
- if (empty($data)) {
- return false;
+ $rows = $this->db->table('claims_dump_abhi cd')
+ ->select('cd.id, tm.id AS ticket_id')
+ ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
+ ->where('cd.is_active', 1)
+ ->where('cd.file_id', $fileId)
+ ->where('cd.ticket_id IS NULL')
+ ->get()
+ ->getResultArray();
+
+ if (empty($rows)) {
+ return true;
}
- $builder = $this->db->table('claims_dump_abhi');
- return $builder->updateBatch($data, 'id');
+ $updateData = [];
+ foreach ($rows as $row) {
+ $updateData[] = [
+ 'id' => $row['id'],
+ 'ticket_id' => $row['ticket_id'],
+ ];
+ }
+
+ return $this->db->table('claims_dump_abhi')->updateBatch($updateData, 'id') !== false;
}
@@ -329,7 +358,7 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
- $item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
+ $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']);
$item['file_id'] = $file_id;
$item['claim_dump_ref_id'] = $row['id'];
$item['created_by'] = $file_data['created_by'] ?? null;
diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php
index 3a6a5777..3ccbcadd 100644
--- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php
+++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php
@@ -9,17 +9,30 @@ use App\Models\TicketMasterModel;
use App\Models\EmployeeModel;
use App\Models\ClaimDumpFileModel;
use App\Models\ClaimsDumpFhplModel;
+use App\Models\ClientPolicyModel;
+
use RuntimeException;
abstract class BaseTpaClaimImportService
{
protected BaseConnection $db;
protected $claimDumpFileModel;
+ protected $clientPolicyModel;
+ protected $policyNumberMapping;
public function __construct()
{
$this->db = db_connect();
$this->claimDumpFileModel = new ClaimDumpFileModel();
+ $this->clientPolicyModel = new ClientPolicyModel();
+ $this->policyNumberMapping = [
+ (int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'Insurer Policy Number',
+ (int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'Policy Number',
+ (int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'policy_no',
+ (int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'Policy No',
+ (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'Policy Number',
+ (int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'POLICY_NO',
+ ];
}
/**
@@ -33,6 +46,8 @@ abstract class BaseTpaClaimImportService
try {
$fileData = $this->claimDumpFileModel->where('id', $fileId)->first();
+ $client_policy_data = $this->clientPolicyModel->where('id', $fileData['client_policy_id'])->first();
+
// Determine sheet name logic...
if (env('FHPL_PRIMARY_KEY_CONSTANT') == $fileData['tpa_id']) {
$rows = $this->readExcelBySheetName($filePath, 'Claims&Preauth');
@@ -47,6 +62,18 @@ abstract class BaseTpaClaimImportService
return ['status' => false, 'message' => 'Excel file contains no data or wrong file upload'];
}
+ if(isset($this->policyNumberMapping[$fileData['tpa_id']]) && !empty($this->policyNumberMapping[$fileData['tpa_id']])){
+ $policy_number_column = $this->policyNumberMapping[$fileData['tpa_id']];
+ }else{
+ $policy_number_column = 'policy_no';
+ }
+
+
+ if($client_policy_data['policy_no'] != ($rows[0][$policy_number_column] ?? '')){
+ $this->db->transRollback();
+ return ['status' => false, 'message' => 'Policy number mismatch in the file and in the system'];
+ }
+
$tpaInsertData = $this->mapTPAData($rows, $fileId);
if (empty($tpaInsertData)) {
@@ -81,7 +108,7 @@ abstract class BaseTpaClaimImportService
$this->db->transBegin();
try {
- $file_id = $params['file_id'];
+ $file_id = $params['file_id'];
$ticketMasterData = $this->mapClaimMasterData($file_id);
// Check if mapping failed
@@ -101,6 +128,12 @@ abstract class BaseTpaClaimImportService
$this->db->transRollback();
return ['status' => false, 'message' => 'Ticket Master Claim bulk insert failed'];
}
+ // Map newly created ticket IDs back to the TPA staging table
+ if (!$this->updateTicketIdInTPATable($file_id)) {
+ $this->db->transRollback();
+ return ['status' => false, 'message' => 'Updating ticket_id in TPA table failed'];
+ }
+
$message .= 'Ticket Master Claim bulk insert success. ';
$hasExecutedTask = true;
}else{
@@ -328,6 +361,16 @@ abstract class BaseTpaClaimImportService
return $employeeData ?? [];
}
+ public function checkStatusMapping($statusArray, $statusString)
+ {
+ foreach ($statusArray as $key => $value) {
+ if (strtolower($statusString) == strtolower($key) || strtolower($statusString) == strtolower(trim($key))) {
+ return $value;
+ }
+ }
+ return null;
+ }
+
/**
* Map Excel rows to TPA table structure
*/
@@ -349,9 +392,9 @@ abstract class BaseTpaClaimImportService
abstract protected function importClaimMaster(array $data): bool;
/**
- * Update TPA table with ticket_master primary key
+ * Update TPA table with ticket_master primary key for a given file.
*/
- abstract protected function updateTicketIdInTPATable(): bool;
+ abstract protected function updateTicketIdInTPATable(int $fileId): bool;
/**
* Update TPA table with ticket_master insert rejected reason
diff --git a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php
index 324fd3a7..7123230c 100644
--- a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php
+++ b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php
@@ -181,6 +181,13 @@ class FhplClaimImportService extends BaseTpaClaimImportService
protected $statusMapping = [
'Settled' => 11,
'Rejected' => 8,
+ 'Paid' => 11,
+ 'Approved' => 9,
+ 'Closed' => 12,
+ 'Under Process' => 5,
+ 'Query' => 4,
+ 'Required Information' => 4,
+ 'In-Progress' => 5,
];
protected $dateColumns = [
@@ -235,14 +242,31 @@ class FhplClaimImportService extends BaseTpaClaimImportService
}
- public function updateTicketIdInTPATable(): bool
+ public function updateTicketIdInTPATable(int $fileId): bool
{
- if (empty($data)) {
- return false;
+ // Join ticket_master with FHPL staging on file_id + claim_dump_ref_id -> id
+ $rows = $this->db->table('claims_dump_fhpl cd')
+ ->select('cd.id, tm.id AS ticket_id')
+ ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
+ ->where('cd.is_active', 1)
+ ->where('cd.file_id', $fileId)
+ ->where('cd.ticket_id IS NULL')
+ ->get()
+ ->getResultArray();
+
+ if (empty($rows)) {
+ return true;
}
-
- $builder = $this->db->table('claims_dump_fhpl');
- return $builder->updateBatch($data, 'id');
+
+ $updateData = [];
+ foreach ($rows as $row) {
+ $updateData[] = [
+ 'id' => $row['id'],
+ 'ticket_id' => $row['ticket_id'],
+ ];
+ }
+
+ return $this->db->table('claims_dump_fhpl')->updateBatch($updateData, 'id') !== false;
}
@@ -400,7 +424,7 @@ class FhplClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
- $item['claim_status_id'] = $statusMapping[$row['current_claim_status']] ?? 61;
+ $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['current_claim_status']) ?? 61;
$item['file_id'] = $file_id;
$item['claim_dump_ref_id'] = $row['id'];
$item['created_by'] = $file_data['created_by'] ?? null;
diff --git a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php
index 132986a2..6e055139 100644
--- a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php
+++ b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php
@@ -106,7 +106,13 @@ class IciciClaimImportService extends BaseTpaClaimImportService
protected $statusMapping = [
'PAID' => 11,
+ 'SETTLED' => 11,
'REJECTED' => 8,
+ 'APPROVED' => 9,
+ 'CLOSED' => 12,
+ 'QUERY' => 4,
+ 'REQUIRED INFORMATION' => 4,
+ 'IN-PROGRESS' => 5,
];
protected $dateColumns = [
@@ -152,14 +158,30 @@ class IciciClaimImportService extends BaseTpaClaimImportService
}
- public function updateTicketIdInTPATable(): bool
+ public function updateTicketIdInTPATable(int $fileId): bool
{
- if (empty($data)) {
- return false;
+ $rows = $this->db->table('claims_dump_icici cd')
+ ->select('cd.id, tm.id AS ticket_id')
+ ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
+ ->where('cd.is_active', 1)
+ ->where('cd.file_id', $fileId)
+ ->where('cd.ticket_id IS NULL')
+ ->get()
+ ->getResultArray();
+
+ if (empty($rows)) {
+ return true;
}
-
- $builder = $this->db->table('claims_dump_icici');
- return $builder->upsertBatch($data, 'id');
+
+ $updateData = [];
+ foreach ($rows as $row) {
+ $updateData[] = [
+ 'id' => $row['id'],
+ 'ticket_id' => $row['ticket_id'],
+ ];
+ }
+
+ return $this->db->table('claims_dump_icici')->updateBatch($updateData, 'id') !== false;
}
@@ -287,7 +309,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService
$item['tpa_id'] = $client_policy_data['tpa_id'] ?? null;
$item['acm_id'] = $client_policy_data['acm_id'] ?? null;
$item['policy_no'] = $client_policy_data['policy_no'] ?? null;
- $item['relationship'] = $this->convertRelation($row['relation_group'] ?? '');
+ $item['relationship'] = $this->convertRelation($row['relation'] ?? '');
$employee_data = $this->getEmployeeDetails($file_data['client_id'], $file_data['client_policy_id'], $row['employee_member_id'], $item['relationship']);
@@ -317,7 +339,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
- $item['claim_status_id'] = $statusMapping[$row['updated_status']] ?? 61;
+ $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['updated_status']) ?? 61;
$item['claim_dump_ref_id'] = $row['id'];
$item['file_id'] = $file_id;
$item['created_by'] = $file_data['created_by'] ?? null;
diff --git a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php
index d052c9f5..4a2952fb 100644
--- a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php
+++ b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php
@@ -158,12 +158,15 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
protected $statusMapping = [
'Settled' => 11,
'Rejected' => 8,
+ 'Paid' => 11,
'Denied' => 8,
'Cancelled' => 13,
'Processed' => 61,
'Information Awaited' => 4,
'Denied Letter Sent' => 66,
'Cashless Document Awaited' => 3,
+ 'Approved' => 9,
+ 'Closed' => 12,
];
/**
@@ -192,16 +195,30 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
}
- public function updateTicketIdInTPATable(): bool
+ public function updateTicketIdInTPATable(int $fileId): bool
{
- if (empty($data)) {
- return false;
- }
-
- $builder = $this->db->table('claims_dump_medi_assist');
- $builder->insertBatch($data);
+ $rows = $this->db->table('claims_dump_medi_assist cd')
+ ->select('cd.id, tm.id AS ticket_id')
+ ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
+ ->where('cd.is_active', 1)
+ ->where('cd.file_id', $fileId)
+ ->where('cd.ticket_id IS NULL')
+ ->get()
+ ->getResultArray();
- return true;
+ if (empty($rows)) {
+ return true;
+ }
+
+ $updateData = [];
+ foreach ($rows as $row) {
+ $updateData[] = [
+ 'id' => $row['id'],
+ 'ticket_id' => $row['ticket_id'],
+ ];
+ }
+
+ return $this->db->table('claims_dump_medi_assist')->updateBatch($updateData, 'id') !== false;
}
@@ -356,8 +373,8 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
// Meta fields
$item['claim_dump_ref_id'] = $row['id'];
- $item['file_id'] = $file_id;
- $item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
+ $item['file_id'] = $file_id;
+ $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']) ?? 61;
$item['created_by'] = $file_data['created_by'] ?? null;
$item['claim_type'] = 1;
$item['priority'] = 1;
diff --git a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php
index 770caadd..afe95c4f 100644
--- a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php
+++ b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php
@@ -96,14 +96,15 @@ class RcareClaimImportService extends BaseTpaClaimImportService
];
protected $statusMapping = [
+ 'CL Paid with Settlement Letter' => 11,
'Settled' => 11,
+ 'Paid' => 11,
'Rejected' => 8,
- 'Denied' => 8,
- 'Cancelled' => 13,
- 'Processed' => 61,
- 'Information Awaited' => 4,
- 'Denied Letter Sent' => 66,
- 'Cashless Document Awaited' => 3,
+ 'Approved' => 9,
+ 'Closed' => 12,
+ 'CL Rejected' => 8,
+ 'CL Approved' => 9,
+ 'AL Closed' => 12,
];
protected $dateColumns = [
@@ -143,14 +144,30 @@ class RcareClaimImportService extends BaseTpaClaimImportService
}
- public function updateTicketIdInTPATable(): bool
+ public function updateTicketIdInTPATable(int $fileId): bool
{
- if (empty($data)) {
- return false;
+ $rows = $this->db->table('claims_dump_reliance cd')
+ ->select('cd.id, tm.id AS ticket_id')
+ ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
+ ->where('cd.is_active', 1)
+ ->where('cd.file_id', $fileId)
+ ->where('cd.ticket_id IS NULL')
+ ->get()
+ ->getResultArray();
+
+ if (empty($rows)) {
+ return true;
}
-
- $builder = $this->db->table('claims_dump_reliance');
- return $builder->updateBatch($data, 'id');
+
+ $updateData = [];
+ foreach ($rows as $row) {
+ $updateData[] = [
+ 'id' => $row['id'],
+ 'ticket_id' => $row['ticket_id'],
+ ];
+ }
+
+ return $this->db->table('claims_dump_reliance')->updateBatch($updateData, 'id') !== false;
}
@@ -307,7 +324,7 @@ class RcareClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
- $item['claim_status_id'] = $statusMapping[$row['final_status']] ?? 61;
+ $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['final_status']) ?? 61;
$item['file_id'] = $file_id;
$item['created_by'] = $file_data['created_by'] ?? null;
$item['claim_dump_ref_id'] = $row['id'];
diff --git a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php
index c90d580b..70a1340c 100644
--- a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php
+++ b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php
@@ -349,10 +349,17 @@ class VidalClaimImportService extends BaseTpaClaimImportService
];
protected $statusMapping = [
- 'CL Paid with Settlement Letter' => 11,
- 'CL Rejected' => 8,
- 'CL Approved' => 9,
- 'AL Closed' => 12,
+ 'Settled' => 11,
+ 'Rejected' => 8,
+ 'Paid' => 11,
+ 'Denied' => 8,
+ 'Cancelled' => 13,
+ 'Processed' => 61,
+ 'Information Awaited' => 4,
+ 'Denied Letter Sent' => 66,
+ 'Cashless Document Awaited' => 3,
+ 'Approved' => 9,
+ 'Closed' => 12,
];
@@ -382,14 +389,30 @@ class VidalClaimImportService extends BaseTpaClaimImportService
}
- public function updateTicketIdInTPATable(): bool
+ public function updateTicketIdInTPATable(int $fileId): bool
{
- if (empty($data)) {
- return false;
+ $rows = $this->db->table('claims_dump_vidal cd')
+ ->select('cd.id, tm.id AS ticket_id')
+ ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
+ ->where('cd.is_active', 1)
+ ->where('cd.file_id', $fileId)
+ ->where('cd.ticket_id IS NULL')
+ ->get()
+ ->getResultArray();
+
+ if (empty($rows)) {
+ return true;
}
-
- $builder = $this->db->table('claims_dump_vidal');
- return $builder->updateBatch($data, 'id');
+
+ $updateData = [];
+ foreach ($rows as $row) {
+ $updateData[] = [
+ 'id' => $row['id'],
+ 'ticket_id' => $row['ticket_id'],
+ ];
+ }
+
+ return $this->db->table('claims_dump_vidal')->updateBatch($updateData, 'id') !== false;
}
@@ -542,7 +565,7 @@ class VidalClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
- $item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
+ $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']) ?? 61;
$item['file_id'] = $file_id;
$item['claim_dump_ref_id'] = $row['id'];
$item['created_by'] = $file_data['created_by'] ?? null;
diff --git a/app/Models/ExpenseModel.php b/app/Models/ExpenseModel.php
new file mode 100644
index 00000000..7d4a37c1
--- /dev/null
+++ b/app/Models/ExpenseModel.php
@@ -0,0 +1,73 @@
+= '$startDate' ";
$conditions .= " AND pcsd.pt_policy_issue_date <= '$endDate' ";
+ } else if ($date_type === "created_at") {
+ $conditions .= " AND pt.created_at <= '$endDate' ";
} else {
$conditions .= " AND pt.$date_type >= '$startDate' ";
$conditions .= " AND pt.$date_type <= '$endDate' ";
diff --git a/app/Views/claim_dump_file_list.php b/app/Views/claim_dump_file_list.php
index 1330cded..9ba0451b 100644
--- a/app/Views/claim_dump_file_list.php
+++ b/app/Views/claim_dump_file_list.php
@@ -24,6 +24,66 @@
.dataTables_length label {height: 21px !important;}
.readonly-select { background-color: #f3f3f3 !important; cursor: not-allowed; pointer-events: none; }
+
+.custom-tooltip {
+ position: relative;
+ display: inline-block;
+ cursor: pointer;
+}
+
+.info-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ background-color: #007bff;
+ color: #fff;
+ border-radius: 50%;
+ font-size: 14px;
+ font-style: italic;
+ font-weight: bold;
+ font-family: Georgia, serif;
+ line-height: 1;
+ vertical-align: middle;
+}
+
+.tooltiptext {
+ visibility: hidden;
+ opacity: 0;
+ position: fixed; /* fixed instead of absolute to escape modal overflow */
+ background: #333;
+ color: #fff;
+ padding: 10px 14px;
+ border-radius: 6px;
+ white-space: normal; /* allow text wrapping */
+ width: 320px; /* fixed width for multiline */
+ font-size: 12px;
+ font-weight: normal;
+ font-style: normal;
+ line-height: 1.6;
+ z-index: 99999;
+ transition: opacity 0.2s ease;
+ pointer-events: none;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ top: 20px !important;
+ left: 155px !important;
+}
+
+.tooltiptext::after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: 20px;
+ border: 6px solid transparent;
+ border-top-color: #333;
+}
+
+.custom-tooltip:hover .tooltiptext {
+ visibility: visible;
+ opacity: 1;
+}
+
@@ -151,7 +221,16 @@
@@ -219,6 +298,20 @@
let policyListByClient = null;
let client_list = null;
+ $(function () {
+ $('[data-toggle="tooltip"]').tooltip();
+ });
+
+ document.querySelectorAll('.custom-tooltip').forEach(function(tooltip) {
+ tooltip.addEventListener('mouseenter', function() {
+ var icon = this.querySelector('.info-icon');
+ var tip = this.querySelector('.tooltiptext');
+ var rect = icon.getBoundingClientRect();
+ tip.style.left = rect.left + 'px';
+ tip.style.top = (rect.top - tip.offsetHeight - 10) + 'px';
+ });
+ });
+
$(document).ready(function() {
getClientAndBranchAndPolicy();
@@ -232,10 +325,17 @@
let client_id = $('#client_id').val() ?? null;
let client_policy_id = $('#client_policy_id').val() ?? null;
+ let tpa_id = $('#tpa_id').val() ?? null;
if(client_id){
+
if(!client_policy_id){
- toastr.warning('Please select the client policy', 'Warning');
+ toastr.warning('Please select the client policy', 'WARNING');
+ return;
+ }
+
+ if(!tpa_id){
+ toastr.warning('TPA is required', 'WARNING');
return;
}
}
@@ -251,9 +351,15 @@
'application/vnd.oasis.opendocument.spreadsheet'
];
+ if (!fileInput.files.length) {
+ toastr.warning('Please select an Excel file', 'WARNING');
+ return false;
+ }
+
var selectedFile = fileInput.files[0];
+
if (!allowedTypes.includes(selectedFile.type)) {
- alert('Please upload a valid Excel file (.xlsx, .xls, .ods)');
+ toastr.warning('Please upload a valid Excel file (.xlsx, .xls, .ods)', 'WARNING');
return false;
}
@@ -500,6 +606,70 @@
}
+ function downloadTpaErrorFile(file_id) {
+ if (!file_id) {
+ toastr.error('Invalid file reference for error download', 'Error');
+ return;
+ }
+
+ var url = '= base_url("util/getTpaClaimDumpErrorData"); ?>/' + file_id;
+
+ $.ajax({
+ url: url,
+ type: 'GET',
+ xhrFields: { responseType: 'blob' },
+ success: function (data, status, xhr) {
+ var disposition = xhr.getResponseHeader('Content-Disposition') || '';
+ var contentType = xhr.getResponseHeader('Content-Type') || '';
+
+ // If server sent JSON (error), parse it and show via Toastr
+ if (contentType.indexOf('application/json') !== -1) {
+ var reader = new FileReader();
+ reader.onload = function () {
+ try {
+ var json = JSON.parse(reader.result);
+ var msg = json.message || 'Unable to download error file.';
+ toastr.error(msg, 'Error');
+ } catch (e) {
+ toastr.error('Unable to download error file.', 'Error');
+ }
+ };
+ reader.readAsText(data);
+ return;
+ }
+
+ // Otherwise assume it is Excel and trigger a download
+ var filename = 'tpa_claim_dump_errors_' + file_id + '.xlsx';
+ var match = /filename="?([^"]+)"?/.exec(disposition);
+ if (match && match[1]) {
+ filename = match[1];
+ }
+
+ var blob = new Blob([data], {
+ type: contentType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ });
+ var link = document.createElement('a');
+ link.href = window.URL.createObjectURL(blob);
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ toastr.success('Error file download started', 'Success');
+ },
+ error: function (xhr) {
+ var msg = 'Unable to download error file.';
+ try {
+ var json = JSON.parse(xhr.responseText);
+ if (json.message) {
+ msg = json.message;
+ }
+ } catch (e) {}
+ toastr.error(msg, 'Error');
+ }
+ });
+ }
+
$('.close').click(function(){
$('#modal_body').empty()
let html = `
`
diff --git a/app/Views/daily_report_email_template.php b/app/Views/daily_report_email_template.php
new file mode 100644
index 00000000..0ba7c8ff
--- /dev/null
+++ b/app/Views/daily_report_email_template.php
@@ -0,0 +1,347 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Date: = $today ?>
+
+
+
Inception & Endorsement (File Upload)
+
+
+
Total Inception
+
= $total_inception_count ?>
+
Employee file uploads marked as inception
+
+
+
Total Endorsement
+
= $total_endorsement_count ?>
+
All non‑inception successful uploads
+
+
+
+
+
Employee Enrollment
+
+
+
Draft / Enrolled
+
= $total_employee_draft_count ?? 0 ?> / = $total_employee_enrolled_count ?? 0 ?>
+
Employees in draft / enrolled status for the day
+
+
+
Open for Enrollment Policies
+
= $total_open_for_enrollemnt_policy_count ?? 0 ?>
+
Policies currently open for enrollment
+
+
+
+
+
TPA & Insurer Batch Summary
+
+
+
TPA (Inception / Endorsement)
+
= $total_tpa_incetion_count ?> / = $total_tpa_endorsement_count ?>
+
Successful TPA events
+
+
+
Insurer (Inception / Endorsement)
+
= $total_insurer_incetion_count ?> / = $total_insurer_endorsement_count ?>
+
Successful insurer events
+
+
+
+
+
Sales Funnel & Leads
+
+
+
Opportunities & Won
+
= $total_opportunity_count ?> / = $total_placement_count ?>
+
Total opportunities created vs converted
+
+
+
RFQ (Created / Sent to Insurer)
+
= $total_rfq_created_count ?> / = $total_rfq_insurer_send_count ?>
+
Movement from opportunity to RFQ
+
+
+
QCR (Created / Sent to Client)
+
= $total_qcr_created_count ?> / = $total_qcr_client_send_count ?>
+
Quotes prepared and shared
+
+
+
Total Leads & Activities
+
= $total_lead_count ?> / = $total_activity_count ?>
+
Lead entries and logged touchpoints
+
+
+
+
+
BDS Policy Transactions
+
+
+
Total BDS Transactions
+
= $total_bds_count ?>
+
All policy transactions processed
+
+
+
BDS (Policy / Endorsement)
+
= $total_bds_policy_wise_count ?> / = $total_bds_endorsement_wise_count ?>
+
Split of inceptions vs endorsements
+
+
+
+
+
Claims
+
+
+
Total Claims Registered for the day
+
= $total_claim_count ?>
+
+
+
+
+
+
BDS by Policy Type
+
+
+
+
+ | Policy Type |
+ Count |
+
+
+
+
+
+
+ |
+ = esc($row['policy_type']) ?>
+ |
+ = $row['count'] ?> |
+
+
+
+
+ |
+ No BDS activity recorded for today.
+ |
+
+
+
+
+
+
+
+
Claim Status Breakdown
+
+ The total claims received so far are listed ticket type-wise.
+
+
+ $total_gmc_status_wise_claim_count ?? [],
+ 'GPA Claims' => $total_gpa_status_wise_claim_count ?? [],
+ 'EDLI Claims' => $total_edli_status_wise_claim_count ?? [],
+ 'GTLI Claims' => $total_gtli_status_wise_claim_count ?? [],
+ ];
+ $hasAnyClaimData = false;
+ ?>
+
+ $rows): ?>
+
+
+
= esc($sectionTitle) ?>
+
+
+
+
+ | Status |
+ Count |
+
+
+
+
+
+ |
+ = esc($row['claim_status']) ?>
+ |
+ = $row['count'] ?> |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/expense_list.php b/app/Views/expense_list.php
new file mode 100644
index 00000000..2dedb059
--- /dev/null
+++ b/app/Views/expense_list.php
@@ -0,0 +1,586 @@
+
+
+
+
+
+
+
+
+
+
+
+ | S.No |
+ Client |
+ Policy No |
+ Description |
+ Approved By |
+ Amount |
+ Expense Date |
+ Action |
+
+
+
+
+ $row) : ?>
+
+ | = $index + 1; ?> |
+
+ = esc($row['client_name'] ?? ''); ?>
+ = ! empty($row['short_name']) ? ' (' . esc($row['short_name']) . ')' : ''; ?>
+ |
+ = esc($row['policy_no'] ?? ''); ?> |
+ = esc($row['description'] ?? ''); ?> |
+ = esc($row['approved_by_name'] ?? ''); ?> |
+ = number_format((float) ($row['amount'] ?? 0), 2); ?> |
+
+
+ = date('d-m-Y', strtotime($row['expense_date'])); ?>
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/layout/header.php b/app/Views/layout/header.php
index 365caf67..0086c4f5 100755
--- a/app/Views/layout/header.php
+++ b/app/Views/layout/header.php
@@ -1876,6 +1876,17 @@
+
+
+
+
+
/assets/images/expense_icon.png" alt="Logo" height="24">
+ Expense
+
+
+
+
diff --git a/public/assets/images/expense_icon.png b/public/assets/images/expense_icon.png
new file mode 100644
index 00000000..d1cba8e8
Binary files /dev/null and b/public/assets/images/expense_icon.png differ
diff --git a/tests/unit/ExpenseControllerTest.php b/tests/unit/ExpenseControllerTest.php
new file mode 100644
index 00000000..84ea3b29
--- /dev/null
+++ b/tests/unit/ExpenseControllerTest.php
@@ -0,0 +1,399 @@
+value = $value;
+ }
+
+ public static function fromValue($value): self
+ {
+ return new self($value);
+ }
+
+ public function map($callback): self
+ {
+ if ($this->value !== null) {
+ $this->value = $callback($this->value);
+ }
+
+ return $this;
+ }
+
+ public function getOrCall($callback)
+ {
+ return $this->value !== null ? $this->value : $callback();
+ }
+
+ public function getOrThrow($exception)
+ {
+ if ($this->value === null) {
+ throw $exception;
+ }
+
+ return $this->value;
+ }
+ }
+}
+
+namespace App\Filters {
+ use CodeIgniter\Filters\FilterInterface;
+ use CodeIgniter\HTTP\RequestInterface;
+ use CodeIgniter\HTTP\ResponseInterface;
+
+ // Test stubs for application filters to bypass logging, ACL, and security checks during tests.
+
+ class AclFilter implements FilterInterface
+ {
+ public function before(RequestInterface $request, $arguments = null) { return null; }
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
+ }
+
+ class HttpRequestLog implements FilterInterface
+ {
+ public function before(RequestInterface $request, $arguments = null) { return null; }
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
+ }
+
+ class SecurityInputFilter implements FilterInterface
+ {
+ public function before(RequestInterface $request, $arguments = null) { return null; }
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
+ }
+
+ class GlobalPostFileUploadGuard implements FilterInterface
+ {
+ public function before(RequestInterface $request, $arguments = null) { return null; }
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
+ }
+
+ class Cors implements FilterInterface
+ {
+ public function before(RequestInterface $request, $arguments = null) { return null; }
+ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
+ }
+}
+
+namespace Dotenv\Repository {
+ interface RepositoryInterface
+ {
+ public function get($name);
+
+ public function set($name, $value);
+ }
+
+ class RepositoryBuilder
+ {
+ public static function createWithDefaultAdapters(): self
+ {
+ return new self();
+ }
+
+ public function addAdapter($adapter): self
+ {
+ // No-op for testing.
+ return $this;
+ }
+
+ public function immutable(): self
+ {
+ return $this;
+ }
+
+ public function make(): RepositoryInterface
+ {
+ return new class() implements RepositoryInterface {
+ public function get($name)
+ {
+ $value = getenv($name);
+
+ return $value === false ? null : $value;
+ }
+
+ public function set($name, $value)
+ {
+ putenv("$name=$value");
+ }
+ };
+ }
+ }
+}
+
+namespace Dotenv\Repository\Adapter {
+ class PutenvAdapter
+ {
+ // Stub class used only to satisfy Illuminate\Support\Env references.
+ }
+}
+
+namespace Tests\unit {
+
+use CodeIgniter\Test\CIUnitTestCase;
+use CodeIgniter\Test\FeatureTestTrait;
+
+class ExpenseControllerTest extends CIUnitTestCase
+{
+ use FeatureTestTrait;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ // Override routes for testing to avoid filters/middleware.
+ $this->withRoutes([
+ ['get', 'expense', 'ExpenseController::index'],
+ ['post', 'expense/save', 'ExpenseController::save'],
+ ['get', 'expense/get/(:num)', 'ExpenseController::getExpense/$1'],
+ ['post', 'expense/delete/(:num)','ExpenseController::delete/$1'],
+ ['get', 'expense/client-policies', 'ExpenseController::clientPolicies'],
+ ]);
+
+ // Clean up any test data from previous runs
+ $db = db_connect('default');
+ $db->table('expenses')->like('description', 'CI4_TEST_', 'after')->delete();
+ }
+
+ /**
+ * Helper to insert an expense row directly into the database.
+ */
+ private function createExpenseRecord(array $overrides = []): int
+ {
+ $db = db_connect('default');
+
+ $data = array_merge([
+ 'client_id' => 1,
+ 'client_policy_id' => 1,
+ 'description' => 'CI4_TEST_' . uniqid('', true),
+ 'approved_by' => 1,
+ 'amount' => 100.00,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'is_active' => 1,
+ ], $overrides);
+
+ $db->table('expenses')->insert($data);
+
+ return (int) $db->insertID();
+ }
+
+ public function testCreateExpenseSuccess(): void
+ {
+ $payload = [
+ 'client_id' => '1',
+ 'client_policy_id' => '1',
+ 'approved_by' => '1',
+ 'description' => 'CI4_TEST_Valid Description 123-ABC',
+ 'expense_date' => '12-01-2026',
+ 'amount' => '250.75',
+ ];
+
+ $result = $this->post('expense/save', $payload);
+
+ $result->assertStatus(200);
+
+ $json = json_decode($result->getJSON(), true);
+
+ $this->assertIsArray($json);
+ $this->assertTrue($json['status'] ?? false);
+ $this->assertNotEmpty($json['id'] ?? null);
+
+ $insertedId = (int) $json['id'];
+
+ $db = db_connect('default');
+ $row = $db->table('expenses')
+ ->where('description', $payload['description'])
+ ->orderBy('id', 'DESC')
+ ->get()
+ ->getRowArray();
+
+ $this->assertNotEmpty($row);
+ $this->assertSame(1, (int) $row['is_active']);
+ $this->assertSame($payload['description'], $row['description']);
+ $this->assertEquals(250.75, (float) $row['amount']);
+ }
+
+ public function testCreateExpenseValidationFailureMissingFields(): void
+ {
+ $payload = [
+ // all required fields missing / empty
+ ];
+
+ $result = $this->post('expense/save', $payload);
+
+ $result->assertStatus(400);
+
+ $json = json_decode($result->getJSON(), true);
+
+ $this->assertFalse($json['status'] ?? true);
+ $this->assertSame('Input validation failed', $json['message'] ?? '');
+ $this->assertArrayHasKey('client_id', $json['errors'] ?? []);
+ $this->assertArrayHasKey('client_policy_id', $json['errors'] ?? []);
+ $this->assertArrayHasKey('approved_by', $json['errors'] ?? []);
+ $this->assertArrayHasKey('description', $json['errors'] ?? []);
+ $this->assertArrayHasKey('amount', $json['errors'] ?? []);
+ $this->assertArrayHasKey('expense_date', $json['errors'] ?? []);
+ }
+
+ public function testCreateExpenseValidationFailureInvalidDescription(): void
+ {
+ $payload = [
+ 'client_id' => '1',
+ 'client_policy_id' => '1',
+ 'approved_by' => '1',
+ 'description' => 'Invalid @ Description <>', // invalid characters
+ 'expense_date' => '12-01-2026',
+ 'amount' => '100.00',
+ ];
+
+ $result = $this->post('expense/save', $payload);
+
+ $result->assertStatus(400);
+
+ $json = json_decode($result->getJSON(), true);
+
+ $this->assertFalse($json['status'] ?? true);
+ $this->assertArrayHasKey('description', $json['errors'] ?? []);
+ $this->assertStringContainsString(
+ 'invalid characters',
+ strtolower($json['errors']['description'] ?? '')
+ );
+ }
+
+ public function testUpdateExpenseSuccess(): void
+ {
+ $id = $this->createExpenseRecord([
+ 'description' => 'CI4_TEST_Original Description',
+ 'amount' => 50.00,
+ ]);
+
+ $payload = [
+ 'id' => (string) $id,
+ 'client_id' => '1',
+ 'client_policy_id'=> '1',
+ 'approved_by' => '1',
+ 'description' => 'Updated Description 456',
+ 'expense_date' => '13-01-2026',
+ 'amount' => '75.50',
+ ];
+
+ $result = $this->post('expense/save', $payload);
+
+ $result->assertStatus(200);
+
+ $json = json_decode($result->getJSON(), true);
+
+ $this->assertTrue($json['status'] ?? false);
+ $this->assertSame($id, (int) ($json['id'] ?? 0));
+
+ $db = db_connect('default');
+ $row = $db->table('expenses')->where('id', $id)->get()->getRowArray();
+
+ $this->assertNotEmpty($row);
+ $this->assertSame('Updated Description 456', $row['description']);
+ $this->assertEquals(75.50, (float) $row['amount']);
+ }
+
+ public function testUpdateExpenseValidationFailureInvalidAmount(): void
+ {
+ $id = $this->createExpenseRecord();
+
+ $payload = [
+ 'id' => (string) $id,
+ 'client_id' => '1',
+ 'client_policy_id'=> '1',
+ 'approved_by' => '1',
+ 'description' => 'Another Valid Description',
+ 'expense_date' => '14-01-2026',
+ 'amount' => '-10', // invalid negative
+ ];
+
+ $result = $this->post('expense/save', $payload);
+
+ $result->assertStatus(400);
+
+ $json = json_decode($result->getJSON(), true);
+
+ $this->assertFalse($json['status'] ?? true);
+ $this->assertArrayHasKey('amount', $json['errors'] ?? []);
+ $this->assertStringContainsString(
+ 'cannot be negative',
+ strtolower($json['errors']['amount'] ?? '')
+ );
+ }
+
+ public function testDeleteExpenseSoftDeleteSuccess(): void
+ {
+ $id = $this->createExpenseRecord();
+
+ $result = $this->post('expense/delete/' . $id);
+
+ $result->assertStatus(200);
+
+ $json = json_decode($result->getJSON(), true);
+
+ $this->assertTrue($json['status'] ?? false);
+
+ $db = db_connect();
+ $row = $db->table('expenses')->where('id', $id)->get()->getRowArray();
+
+ $this->assertNotEmpty($row);
+ $this->assertSame(0, (int) $row['is_active']);
+ }
+
+ public function testDeleteExpenseInvalidId(): void
+ {
+ $result = $this->post('expense/delete/0');
+
+ $result->assertStatus(400);
+
+ $json = json_decode($result->getJSON(), true);
+
+ $this->assertFalse($json['status'] ?? true);
+ $this->assertSame('Invalid expense id', $json['message'] ?? '');
+ }
+
+ public function testSearchFilterByClientAndDescription(): void
+ {
+ // Create two distinct expenses
+ $this->createExpenseRecord([
+ 'client_id' => 10,
+ 'description' => 'FilterMatch Description',
+ ]);
+
+ $this->createExpenseRecord([
+ 'client_id' => 20,
+ 'description' => 'Other Description',
+ ]);
+
+ $result = $this->get('expense?client_id=10&description=FilterMatch');
+
+ $result->assertStatus(200);
+
+ $body = $result->getBody();
+
+ $this->assertStringContainsString('FilterMatch Description', $body);
+ $this->assertStringNotContainsString('Other Description', $body);
+ }
+
+ public function testSearchFilterValidationInvalidCharacters(): void
+ {
+ $result = $this->get('expense?description=');
+
+ $result->assertStatus(200);
+
+ $body = $result->getBody();
+
+ $this->assertStringContainsString('Description filter contains invalid characters.', $body);
+ }
+}
+
+}
+
+
diff --git a/tests/unit/PolicyTransactionControllerTest.php b/tests/unit/PolicyTransactionControllerTest.php
new file mode 100644
index 00000000..50abd143
--- /dev/null
+++ b/tests/unit/PolicyTransactionControllerTest.php
@@ -0,0 +1,278 @@
+initController($request, $response, $logger);
+
+ return $controller;
+ }
+
+ /**
+ * Helper to inject a stubbed PolicyTransactionModel into the controller.
+ *
+ * @param PolicyTransactionController $controller
+ * @param array $stubData
+ */
+ protected function injectBdsStubModel(PolicyTransactionController $controller, array $stubData): void
+ {
+ $stubModel = new class($stubData)
+ {
+ private array $data;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function getBDSReportList(
+ $start_date = 0,
+ $end_date = 0,
+ $client_id = 0,
+ $insurer_id = 0,
+ $policy_type_id = 0,
+ $date_type = 0,
+ $issuer = 0,
+ $client_branch_id = 0,
+ $insurer_branch_id = 0,
+ $client_policy_id = 0,
+ $user_id = 0,
+ $where = [],
+ ) {
+ return $this->data;
+ }
+ };
+
+ $refClass = new \ReflectionClass($controller);
+ $prop = $refClass->getProperty('policyTransactionModel');
+ $prop->setAccessible(true);
+ $prop->setValue($controller, $stubModel);
+ }
+
+ public function testCronDailyBDSReportReturnsUnauthorizedWithoutKey(): void
+ {
+ putenv('cron.secretKey=unit-test-secret');
+
+ $_GET = [];
+
+ $controller = $this->makeController();
+
+ $response = $controller->cronDailyBDSReport();
+
+ $this->assertSame(401, $response->getStatusCode());
+
+ $body = json_decode($response->getBody(), true);
+ $this->assertIsArray($body);
+ $this->assertSame('error', $body['status'] ?? null);
+ }
+
+ public function testCronDailyBDSReportNoRecordsReturnsSuccessMessage(): void
+ {
+ putenv('cron.secretKey=unit-test-secret');
+ putenv('bds.reportEmails='); // no recipients for this test
+
+ $_GET['key'] = 'unit-test-secret';
+
+ $controller = $this->makeController();
+
+ $this->injectBdsStubModel($controller, []);
+
+ $response = $controller->cronDailyBDSReport();
+
+ $this->assertSame(200, $response->getStatusCode());
+
+ $body = json_decode($response->getBody(), true);
+ $this->assertIsArray($body);
+ $this->assertSame('success', $body['status'] ?? null);
+ $this->assertStringContainsString(
+ 'No BDS records for today',
+ $body['message'] ?? ''
+ );
+ }
+
+ /**
+ * Integration-style test: generate an Excel file for today's BDS data
+ * and store it in the user's Downloads folder as BDS_DAILY_REPORT_TEST_CASE.xlsx.
+ *
+ * This reuses the same transformation logic as cronDailyBDSReport.
+ */
+ public function testGenerateDailyBdsReportExcelToDownloads(): void
+ {
+ helper(['excel_import_export_helper', 'utility_helper']);
+
+ // Sample BDS row to generate a test Excel (no DB dependency)
+ $today = date('Y-m-d');
+ $reportList = [
+ [
+ 'user_name' => 'Test User',
+ 'policy_issue_month' => 'Mar 2026',
+ 'revenue_type' => 'Fresh',
+ 'client_type' => 'Group',
+ 'client_name' => 'Test Client',
+ 'action_type' => 'Policy',
+ 'policy_type' => 'Health',
+ 'bap' => 'BAP-GRP',
+ 'vehicle_no' => 'TN01AB1234',
+ 'policy_no' => 'P-TEST-001',
+ 'endorsement_no' => 'E-TEST-001',
+ 'insurer_branch_name' => 'Chennai Branch',
+ 'endorse_eff_date' => $today,
+ 'policy_start_date' => $today,
+ 'policy_end_date' => $today,
+ 'ref' => 'REF-001',
+ 'remarks' => 'Test remark',
+ 'bp_amt' => 1000,
+ 'tp_or_ter' => 500,
+ 'premium_wo_gst' => 1500,
+ 'total_premium' => 1770,
+ 'agreed_bp_per' => 10,
+ 'agreed_tp_or_ter_per' => 5,
+ 'reward' => 100,
+ 'total_irda_amt' => 800,
+ 'billed_amt' => 300,
+ 'salse_person_name' => 'Sales Person',
+ 'service_person_name' => 'Service Person',
+ 'nhance_branch' => 'Nhance Chennai',
+ 'installment' => '1',
+ 'data_received_date' => $today,
+ 'renewal_date' => $today,
+ 'co_share' => 'No',
+ 'bro_payable_by' => 'No',
+ 'salse_manager_name' => 'Sales Manager',
+ 'service_manager_name' => 'Service Manager',
+ 'service_branch' => 'Service Branch',
+ 'rollover_date' => $today,
+ 'policy_holder_name' => 'Holder Name',
+ 'same_as_proposer' => 'Yes',
+ 'follower_policy_no' => 'F-TEST-001',
+ 'co_share_per' => 0,
+ 'non_comm_per_amt' => 0,
+ 'bp_cgst' => 0,
+ 'bp_sgst' => 0,
+ 'bp_igst' => 0,
+ 'stamp_duty' => 0,
+ 'standerd_bp_per' => 0,
+ 'standerd_tp_per' => 0,
+ 'actual_bp_amt' => 0,
+ 'actual_tp_amt' => 0,
+ 'actual_bp_per' => 0,
+ 'actual_tp_per' => 0,
+ 'actual_tep_brokerage_amt' => 0,
+ 'actual_tp_brokerage_amt' => 0,
+ 'cd_ac_no' => 'CD-001',
+ ],
+ ];
+
+ $headers = [
+ 'S. No', 'User', 'Month', 'Business Type', 'Client Type', 'Insured Name', 'Transaction Type',
+ 'Policy Type', 'BAP Group', 'Vehicle Number', 'Policy No', 'Endorsement No', 'Insurer Branch',
+ 'Endorsement Effective Date', 'Policy Effective Date', 'Policy Expiry Date', 'Reference', 'Remarks',
+ 'BP Premium', 'TP Premium', 'Premium (without GST)', 'Total Premium', 'Agreed BP %', 'Agreed TP %',
+ 'Rewards', 'Agreed Amount', 'Invoiced Amount', 'Outstanding Amount',
+ 'Salse Person', 'Service Person', 'Salse Person Branch', 'Installment', 'Data Received Date', 'Renewal Date',
+ 'Co-Premium', 'Remuneration Pay By Leader', 'Salse Person Manager', 'Service Person Manager',
+ 'Service Person Branch', 'Rollover Date', 'Policyholder Name', 'Insured (Same as Proposer)',
+ 'Follower Policy No', 'Co-Share %', 'Non Commissional Premium Amount', 'CGST', 'SGST', 'IGST',
+ 'Stamp Duty', 'Standard BP %', 'Standard TP %', 'Actual BP Amount', 'Actual TP Amount',
+ 'Actual BP %', 'Actual TP %', 'Actual BP Remuneration Amount', 'Actual TP Remuneration Amount',
+ 'CD Account No'
+ ];
+
+ $excelData = [];
+ foreach ($reportList as $idx => $row) {
+ $totalIrda = (float)($row['total_irda_amt'] ?? 0);
+ $billedAmt = (float)($row['billed_amt'] ?? 0);
+ $unbilledAmt = $totalIrda - $billedAmt;
+ if ($totalIrda == 0) {
+ $unbilledAmt = abs($unbilledAmt);
+ }
+ $unbilledAmt = ($unbilledAmt == 0 && $billedAmt == 0) ? $totalIrda : $unbilledAmt;
+
+ $hasIrda = ($totalIrda != 0);
+
+ $excelData[] = [
+ $idx + 1,
+ $row['user_name'] ?? 'N/A',
+ $row['policy_issue_month'] ?? 'N/A',
+ $row['revenue_type'] ?? 'N/A',
+ $row['client_type'] ?? 'N/A',
+ $row['client_name'] ?? 'N/A',
+ $row['action_type'] ?? 'N/A',
+ $row['policy_type'] ?? 'N/A',
+ $row['bap'] ?? 'N/A',
+ $row['vehicle_no'] ?? 'N/A',
+ $row['policy_no'] ?? 'N/A',
+ $row['endorsement_no'] ?? 'N/A',
+ $row['insurer_branch_name'] ?? 'N/A',
+ !empty($row['endorse_eff_date']) ? change_date_format($row['endorse_eff_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ !empty($row['policy_start_date']) ? change_date_format($row['policy_start_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ !empty($row['policy_end_date']) ? change_date_format($row['policy_end_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ $row['ref'] ?? 'N/A',
+ $row['remarks'] ?? 'N/A',
+ $hasIrda ? ($row['bp_amt'] ?? '0.00') : '0.00',
+ $hasIrda ? ($row['tp_or_ter'] ?? '0.00') : '0.00',
+ $hasIrda ? ($row['premium_wo_gst'] ?? '0.00') : '0.00',
+ $hasIrda ? ($row['total_premium'] ?? '0.00') : '0.00',
+ $hasIrda ? ($row['agreed_bp_per'] ?? '0.00') . '%' : '0.00%',
+ $hasIrda ? ($row['agreed_tp_or_ter_per'] ?? '0.00') . '%' : '0.00%',
+ $row['reward'] ?? '0.00',
+ $row['total_irda_amt'] ?? '0.00',
+ !empty($row['billed_amt']) ? $row['billed_amt'] : '0.00',
+ number_format((float)$unbilledAmt, 2, '.', ''),
+ $row['salse_person_name'] ?? 'N/A',
+ $row['service_person_name'] ?? 'N/A',
+ $row['nhance_branch'] ?? 'N/A',
+ $row['installment'] ?? 'N/A',
+ !empty($row['data_received_date']) ? change_date_format($row['data_received_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ !empty($row['renewal_date']) ? change_date_format($row['renewal_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ $row['co_share'] ?? 'No',
+ $row['bro_payable_by'] ?? 'No',
+ $row['salse_manager_name'] ?? 'N/A',
+ $row['service_manager_name'] ?? 'N/A',
+ $row['service_branch'] ?? 'N/A',
+ !empty($row['rollover_date']) ? change_date_format($row['rollover_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
+ $row['policy_holder_name'] ?? 'N/A',
+ $row['same_as_proposer'] ?? 'No',
+ $row['follower_policy_no'] ?? 'N/A',
+ number_format((float)($row['co_share_per'] ?? 0), 2),
+ number_format((float)($row['non_comm_per_amt'] ?? 0), 2),
+ number_format((float)($row['bp_cgst'] ?? 0), 2),
+ number_format((float)($row['bp_sgst'] ?? 0), 2),
+ number_format((float)($row['bp_igst'] ?? 0), 2),
+ number_format((float)($row['stamp_duty'] ?? 0), 2),
+ number_format((float)($row['standerd_bp_per'] ?? 0), 2),
+ number_format((float)($row['standerd_tp_per'] ?? 0), 2),
+ number_format((float)($row['actual_bp_amt'] ?? 0), 2),
+ number_format((float)($row['actual_tp_amt'] ?? 0), 2),
+ number_format((float)($row['actual_bp_per'] ?? 0), 2),
+ number_format((float)($row['actual_tp_per'] ?? 0), 2),
+ number_format((float)($row['actual_tep_brokerage_amt'] ?? 0), 2),
+ number_format((float)($row['actual_tp_brokerage_amt'] ?? 0), 2),
+ $row['cd_ac_no'] ?? 'N/A'
+ ];
+ }
+
+ $filePath = '/home/venkat/Downloads/BDS_DAILY_REPORT_TEST_CASE.xlsx';
+
+ $generated = generate_excel($headers, $excelData, $filePath);
+
+ $this->assertTrue($generated, 'Failed to generate BDS_DAILY_REPORT_TEST_CASE.xlsx in Downloads');
+ }
+}
+