From b1ce7306872da80048824e77e76329b464cff2f8 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Mon, 9 Mar 2026 13:57:30 +0530 Subject: [PATCH 01/18] FIX_Endorsement file --- app/Config/Routes.php | 6 +- app/Controllers/EndorsementController.php | 123 +++++++++++++++++++++- 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 388edbe..87957c5 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -28,7 +28,7 @@ $routes->group('api', ['filter' => 'appSignature'], function ($routes) { //api's with token - $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], function ($routes) { +$routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], function ($routes) { //logout $routes->get('auth/logout', 'StaffAuthController::logout'); @@ -141,7 +141,9 @@ $routes->group('api', ['filter' => 'appSignature'], function ($routes) { $routes->post('endorsement/createEndorsement', 'EndorsementController::createEndorsement'); $routes->post('endorsement/updateEndorsement', 'EndorsementController::updateEndorsement'); $routes->get('endorsement/downloadEndorsementCompletionFile', 'EndorsementController::downloadEndorsementCompletionFile'); - + $routes->post('endorsement/uploadEndorsementFile', 'EndorsementController::uploadEndorsementFile'); + $routes->get('endorsement/deleteEndorsement', 'EndorsementController::deleteEndorsement'); + //dashboard $routes->get('dashboard/managerDashboard', 'DashboardController::managerDashboard'); $routes->get('dashboard/handlerDashboard', 'DashboardController::handlerDashboard'); diff --git a/app/Controllers/EndorsementController.php b/app/Controllers/EndorsementController.php index 92f2594..f3dcb27 100644 --- a/app/Controllers/EndorsementController.php +++ b/app/Controllers/EndorsementController.php @@ -408,6 +408,30 @@ class EndorsementController extends ResourceController } } + public function deleteEndorsement() + { + try { + + $id = $this->request->getGet('id'); + + + // check if EndorsementModel exists + $file = $this->EndorsementModel->find((int)$id); + if (!$file) { + return $this->respond(['status' => 'failed','code' => 200, 'data' => 'Endorsement not found'], 200); + } + + // update status + $this->EndorsementModel->update($id, ['is_active' => 0]); + + return $this->respond([ + 'status' => 'success', 'code' => 200,'data' => "Endorsement Deleted"], 200); + + } catch (\Exception $e) { + return $this->respond(['status' => 'failed','code' => 500,'data' => $e->getMessage()], 500); + } + } + // public function createEndorsement() // { @@ -556,6 +580,7 @@ class EndorsementController extends ResourceController { try { $id = $this->request->getGet('id'); + $type = $this->request->getGet('type'); // 'completion' or 'original' if (!$id) { return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'ID is required'], 200); @@ -568,7 +593,23 @@ class EndorsementController extends ResourceController return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File not found'], 200); } - $filePath = WRITEPATH . 'uploads/endorsement/' . $fileRecord['endorsement_file_name']; + // $filePath = WRITEPATH . 'uploads/endorsement/' . $fileRecord['endorsement_file_name']; + + // ✅ If type is null/empty → original (default) + // ✅ If type = 'completion' → endorsement_pdf/ subfolder + if (!$type || $type === 'original') { + $fileName = $fileRecord['endorsement_file_name'] ?? null; + $subPath = ''; + } else if ($type === 'completion') { + $fileName = $fileRecord['endorsement_completion_file'] ?? null; + $subPath = 'endorsement_pdf/'; + } + + if (!$fileName) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'No file uploaded yet'], 200); + } + + $filePath = WRITEPATH . 'uploads/endorsement/' . $subPath . $fileName; if (!file_exists($filePath)) { return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File missing on server'], 200); @@ -623,11 +664,87 @@ class EndorsementController extends ResourceController } } + public function uploadEndorsementFile() + { + try { + $data = $this->request->getPost(); + // ✅ Validate required POST fields first + if (empty($data['id']) || empty($data['updated_by'])) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'Missing required fields'], 400); + } + $uploadPath = WRITEPATH . 'uploads/endorsement/'; + $allowedTypes = ['application/pdf']; + $pdfPath = $uploadPath . 'endorsement_pdf/'; + $endorsementPdf = $this->request->getFile('endorsement_completion_file'); - + // ✅ Check file existence and validity BEFORE accessing its properties + if (!$endorsementPdf || !$endorsementPdf->isValid()) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'No valid file received'], 400); + } + + // ✅ MIME validation only after confirming file exists + if (!in_array($endorsementPdf->getMimeType(), $allowedTypes)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'Only PDF files are allowed'], 400); + } + + // ✅ Fetch existing record to get old file name BEFORE uploading + $existingRecord = $this->EndorsementModel->find($data['id']); + $oldFileName = $existingRecord['endorsement_completion_file'] ?? null; + + // ✅ Ensure upload directory exists + if (!is_dir($pdfPath)) { + mkdir($pdfPath, 0777, true); + } + + // ✅ Upload new file + $pdfFileName = time() . '_' . $endorsementPdf->getRandomName(); + $endorsementPdf->move($pdfPath, $pdfFileName); + + // ✅ Guard: ensure file was actually saved on disk + if (!$pdfFileName || !file_exists($pdfPath . $pdfFileName)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'File upload failed'], 400); + } + + // ✅ Update DB with new file name + $updateData = [ + 'endorsement_completion_file' => $pdfFileName, + 'updated_by' => $data['updated_by'], + ]; + + $updated = $this->EndorsementModel->update($data['id'], $updateData); + + // ✅ DB update failed → delete the newly uploaded file to avoid orphan files + if (!$updated) { + if (file_exists($pdfPath . $pdfFileName)) { + unlink($pdfPath . $pdfFileName); + } + log_message('error', 'DB update failed for endorsement ID: ' . $data['id'] . '. New file removed.'); + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => 'DB update failed, file not saved'], 500); + } + + // ✅ DB success → NOW safe to delete old file + if ($oldFileName) { + $oldFilePath = $pdfPath . $oldFileName; + if (file_exists($oldFilePath)) { + if (!unlink($oldFilePath)) { + log_message('warning', 'DB updated but failed to delete old file: ' . $oldFilePath); + } else { + log_message('info', 'Old file deleted after successful DB update: ' . $oldFilePath); + } + } + } + + log_message('info', 'Endorsement PDF uploaded successfully. ID: ' . $data['id'] . ', File: ' . $pdfFileName); + + return $this->respond(['status' => 'success', 'code' => 200, 'data' => $data['id']], 200); + + } catch (\Exception $e) { + log_message('error', 'uploadEndorsementFile error: ' . $e->getMessage()); + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } - } From 4872cd17c1562ce329021f731ee87fa6cdb49dc7 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Tue, 10 Mar 2026 17:34:43 +0530 Subject: [PATCH 02/18] FIX_UPDATEAPI_ENDOSEMENTPATHAPI --- app/Controllers/EndorsementController.php | 90 +++++++++++++++++++---- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/app/Controllers/EndorsementController.php b/app/Controllers/EndorsementController.php index f3dcb27..a9738c5 100644 --- a/app/Controllers/EndorsementController.php +++ b/app/Controllers/EndorsementController.php @@ -268,19 +268,32 @@ class EndorsementController extends ResourceController if (!$endorsement) { return $this->respond([ 'status' => 'failed', 'code' => 404,'data' => 'Endorsement not found' ], 404); } // FILE UPLOAD - $uploadedCompletionFile = $endorsement['endorsement_file_name']; + $uploadedOriginalCompletionFile = $endorsement['endorsement_file_name']; - $uploadFile = $this->request->getFile('endorsement_file_name'); + $uploadOriginalFile = $this->request->getFile('endorsement_file_name'); - if ($uploadFile && $uploadFile->isValid()) { + if ($uploadOriginalFile && $uploadOriginalFile->isValid()) { - $uploadPath = WRITEPATH . 'uploads/endorsement/'; - if (!is_dir($uploadPath)) { - mkdir($uploadPath, 0777, true); + $uploadOriginalPath = WRITEPATH . 'uploads/endorsement/'; + if (!is_dir($uploadOriginalPath)) { + mkdir($uploadOriginalPath, 0777, true); } - $uploadedCompletionFile = time() . '_' . $uploadFile->getRandomName(); - $uploadFile->move($uploadPath, $uploadedCompletionFile); + $uploadedOriginalCompletionFile = time() . '_' . $uploadOriginalFile->getRandomName(); + $uploadOriginalFile->move($uploadOriginalPath, $uploadedOriginalCompletionFile); // ✅ fixed variable + } + + // FILE REVISED UPLOAD + $uploadedRevisedCompletionFile = $endorsement['endorsement_completion_file']; + $uploadRevisedFile = $this->request->getFile('endorsement_completion_file'); + + if ($uploadRevisedFile && $uploadRevisedFile->isValid()) { + $uploadRevisedPath = WRITEPATH . 'uploads/endorsement/endorsement_pdf/'; + if (!is_dir($uploadRevisedPath)) { + mkdir($uploadRevisedPath, 0777, true); + } + $uploadedRevisedCompletionFile = time() . '_' . $uploadRevisedFile->getRandomName(); + $uploadRevisedFile->move($uploadRevisedPath, $uploadedRevisedCompletionFile); } //COMMON UPDATE FIELDS @@ -320,10 +333,10 @@ class EndorsementController extends ResourceController $updateData['pending_days'] = $reqData['pending_days']; } - - - // Always update file (either old or new) - $updateData['endorsement_file_name'] = $uploadedCompletionFile; + + // Always update both files (new or existing) + $updateData['endorsement_file_name'] = $uploadedOriginalCompletionFile; + $updateData['endorsement_completion_file'] = $uploadedRevisedCompletionFile; // ✅ fixed variable // External Policy Editable Fields if ($endorsement['policy_from'] === 'External') { @@ -623,7 +636,7 @@ class EndorsementController extends ResourceController } } - public function EndorsementFilePath() + public function EndorsementFilePathGOWTHAM() { try { $endorsementId = $this->request->getGet('endorsement_id'); @@ -664,6 +677,57 @@ class EndorsementController extends ResourceController } } + public function EndorsementFilePath() + { + try { + $endorsementId = $this->request->getGet('endorsement_id'); + + if (!$endorsementId) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'endorsement_id is required'], 200); + } + + // Fetch record from DB + $fileRecord = $this->EndorsementModel->where('is_active', 1)->find((int)$endorsementId); + + if (!$fileRecord) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File not found in database'], 200); + } + + // ✅ STEP 1: Check completion file first + $completionFile = $fileRecord['endorsement_completion_file'] ?? null; + $originalFile = $fileRecord['endorsement_file_name'] ?? null; + + if (!empty($completionFile)) { + // ✅ Use completion file path + $fileName = $completionFile; + $filePath = WRITEPATH . 'uploads/endorsement/endorsement_pdf/' . $fileName; + } elseif (!empty($originalFile)) { + // ✅ Fallback to original file path + $fileName = $originalFile; + $filePath = WRITEPATH . 'uploads/endorsement/' . $fileName; + } else { + // ❌ Neither file exists in DB + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'No file uploaded yet'], 200); + } + + // ✅ STEP 2: Check file exists on disk + if (!file_exists($filePath)) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File missing on server'], 200); + } + + // ✅ STEP 3: Stream file to browser / Flutter + $mime = mime_content_type($filePath); + + return $this->response + ->setHeader('Content-Type', $mime) + ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') + ->setBody(file_get_contents($filePath)); + + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'message' => $e->getMessage()], 500); + } + } + public function uploadEndorsementFile() { try { From 0e77e8344cd47ac276ac85b114992787f9d2a8c6 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Tue, 17 Mar 2026 09:16:17 +0530 Subject: [PATCH 03/18] FIX_export_endorsement_xl --- app/Controllers/ExcelExportController.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/Controllers/ExcelExportController.php b/app/Controllers/ExcelExportController.php index 7b6eec4..d838dc5 100644 --- a/app/Controllers/ExcelExportController.php +++ b/app/Controllers/ExcelExportController.php @@ -1547,10 +1547,11 @@ class ExcelExportController extends ResourceController 'Insurer', 'Broker', 'Contact Person', + 'Partner Code And Name', 'Remarks', 'Is Financial', 'Endorsement Premium Amount', - 'Commission Amount' + 'Commission Amount', ]; $finalData = []; @@ -2480,10 +2481,11 @@ class ExcelExportController extends ResourceController I.name AS insurer_name, pb.name AS broker_name, per.contact_person, + CONCAT(pa.agent_code, ' - ', pa.name) AS agent_details per.endorsement_description, per.financia_or_non_financial, per.endorsement_premium, - per.commission_amount + per.commission_amount, From 9e98aa9a69637fd8b2d57e84d773151d95ffbb53 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Wed, 18 Mar 2026 12:54:59 +0530 Subject: [PATCH 04/18] FIX_Endrosement_list_partner name --- app/Controllers/EndorsementController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Controllers/EndorsementController.php b/app/Controllers/EndorsementController.php index a9738c5..656f513 100644 --- a/app/Controllers/EndorsementController.php +++ b/app/Controllers/EndorsementController.php @@ -51,6 +51,8 @@ class EndorsementController extends ResourceController i.short_name as insurer_short_name, ib.branch_name as insurer_branch, c.client_name, c.phone as client_phone, c.email as client_email, + partner_endorsement_request.agent_id, + pa.agent_code as agent_code, pa.name as agent_name, pp.enquiry_id, etm.endorsement_type as endorsement_type_value, From d98a50c5c2859b358d4249bdb376087a6bbb1ea9 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Wed, 18 Mar 2026 14:38:57 +0530 Subject: [PATCH 05/18] FIX_date_issue_fixes --- app/Controllers/EndorsementController.php | 12 ++++++++++++ app/Controllers/ExcelExportController.php | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/Controllers/EndorsementController.php b/app/Controllers/EndorsementController.php index 656f513..6dfcd8b 100644 --- a/app/Controllers/EndorsementController.php +++ b/app/Controllers/EndorsementController.php @@ -47,6 +47,18 @@ class EndorsementController extends ResourceController $builder = $this->EndorsementModel ->select('partner_endorsement_request.*, + CASE + WHEN partner_endorsement_request.policy_start_date = "0000-00-00" + OR YEAR(partner_endorsement_request.policy_start_date) < 2000 + THEN NULL + ELSE DATE_FORMAT(partner_endorsement_request.policy_start_date, "%Y-%m-%d") + END as policy_start_date, + CASE + WHEN partner_endorsement_request.policy_end_date = "0000-00-00" + OR YEAR(partner_endorsement_request.policy_end_date) < 2000 + THEN NULL + ELSE DATE_FORMAT(partner_endorsement_request.policy_end_date, "%Y-%m-%d") + END as policy_end_date, i.name as insurer_name, i.short_name as insurer_short_name, ib.branch_name as insurer_branch, diff --git a/app/Controllers/ExcelExportController.php b/app/Controllers/ExcelExportController.php index d838dc5..fe3b438 100644 --- a/app/Controllers/ExcelExportController.php +++ b/app/Controllers/ExcelExportController.php @@ -1546,8 +1546,8 @@ class ExcelExportController extends ResourceController 'Insured Name', 'Insurer', 'Broker', - 'Contact Person', 'Partner Code And Name', + 'Contact Person', 'Remarks', 'Is Financial', 'Endorsement Premium Amount', @@ -2480,12 +2480,12 @@ class ExcelExportController extends ResourceController per.insured_name, I.name AS insurer_name, pb.name AS broker_name, + CONCAT(pa.agent_code, ' - ', pa.name) AS agent_details, per.contact_person, - CONCAT(pa.agent_code, ' - ', pa.name) AS agent_details per.endorsement_description, per.financia_or_non_financial, per.endorsement_premium, - per.commission_amount, + per.commission_amount From d9f533ac740d19b9e99e0c981382f81e85241628 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Mon, 23 Mar 2026 16:29:48 +0530 Subject: [PATCH 06/18] FIX_Changes and Additional Requirements1 --- app/Config/Routes.php | 19 +- app/Controllers/DashboardController.php | 408 ++++++++++++++++++++++ app/Controllers/EndorsementController.php | 10 +- app/Controllers/EnquiryController.php | 6 +- app/Controllers/InvoiceController.php | 29 +- app/Helpers/common_helper.php | 3 +- 6 files changed, 468 insertions(+), 7 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 87957c5..8e974d1 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -178,6 +178,23 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('reports/endorsement-excel', 'ExcelExportController::downloadExcelEndorsement'); + + // DASHBOARD Season 6 — Partner Portal + + // GET /partner/{id}/details + $routes->get('partner/(:num)/details', 'DashboardController::partnerDetails/$1'); + + // GET /partner/{id}/policies + $routes->get('partner/(:num)/policies', 'DashboardController::partnerPolicies/$1'); + + // GET /partner/{id}/renewals?days=N + $routes->get('partner/(:num)/renewals', 'DashboardController::partnerRenewals/$1'); + + // GET /partner/{id}/earnings + $routes->get('partner/(:num)/earnings', 'DashboardController::partnerEarnings/$1'); + + + //invoice @@ -203,7 +220,7 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('audit/history', 'MasterController::getHistory'); - }); +}); // common diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index 941b7ee..e4a42a9 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -1172,5 +1172,413 @@ class DashboardController extends ResourceController return array_values($final); } +// ───────────────────────────────────────────────────────────────────────────── +// DashboardController.php — Partner Portal API methods +// Routes: +// GET partner/(:num)/details → partnerDetails($id) +// GET partner/(:num)/policies → partnerPolicies($id) +// GET partner/(:num)/renewals → partnerRenewals($id) ?days=20 +// GET partner/(:num)/earnings → partnerEarnings($id) +// ───────────────────────────────────────────────────────────────────────────── + +// ══════════════════════════════════════════════════════════════════════════════ +// GET partner/{id}/details +// agent_id = partner_agent.id +// Joins: partner_policy (agent_id), partner_enquiry (agent_id), +// partner_endorsement_request (agent_id) +// ══════════════════════════════════════════════════════════════════════════════ +public function partnerDetails($id) +{ + $ref = []; + + try { + if (empty($id)) { + return $this->respond([ + 'status' => 'error', + 'code' => 400, + 'data' => [], + 'message' => 'Missing required parameter: id', + ], 200); + } + + // ── 1. Agent profile (partner_agent.id = $id) + $agent = $this->db->table('partner_agent pa') + ->select(' + pa.id, + pa.name AS agent_name, + pa.agent_code, + pa.mobile, + pa.email, + pa.is_active, + ps.name AS manager_name + ') + ->join('partner_staff ps', 'ps.id = pa.manager_id', 'left') + ->where('pa.id', $id) + ->get()->getRowArray(); + + if (empty($agent)) { + throw new \RuntimeException('Partner not found.', 404); + } + + // -- 2. Policy counts + premium + commission + // mapped_policies = ALL policies under this agent (226) + // issued_policies = policy_number IS NOT NULL AND is_active = 1 (217) + // pending_policies = policy_number IS NULL (9 — raised but not yet issued) + // total_premium / commission = from issued policies only + $policyStats = $this->db->table('partner_policy pp') + ->select(' + COUNT(pp.id) AS mapped_policies, + SUM(CASE WHEN pp.policy_number IS NOT NULL AND pp.is_active = 1 AND pp.premium_amount IS NOT NULL THEN 1 ELSE 0 END) AS issued_policies, + SUM(CASE WHEN pp.policy_number IS NULL THEN 1 ELSE 0 END) AS pending_policies, + COALESCE(SUM(CASE WHEN pp.policy_number IS NOT NULL THEN pp.premium_amount ELSE 0 END), 0) AS total_premium, + COALESCE(SUM(CASE WHEN pp.policy_number IS NOT NULL THEN pp.premium_amount * 0.15 ELSE 0 END), 0) AS commission_earned + ') + ->where('pp.agent_id', $id) + ->get()->getRowArray(); + + // ── 3. Enquiry counts + // enquiry_status enum: To be assigned | Assigned | In progress | Completed + $enquiryStats = $this->db->table('partner_enquiry pe') + ->select(' + COUNT(pe.id) AS enquiry_total, + SUM(CASE WHEN pe.enquiry_status = "Completed" THEN 1 ELSE 0 END) AS enquiry_completed, + SUM(CASE WHEN pe.enquiry_status != "Completed" THEN 1 ELSE 0 END) AS enquiry_pending + ') + ->where('pe.agent_id', $id) + ->where('pe.is_active', 1) + ->get()->getRowArray(); + + // ── 4. Endorsement counts + // status is varchar(20) — adjust "Completed" to match your actual values + $endorseStats = $this->db->table('partner_endorsement_request per') + ->select(' + COUNT(per.id) AS endorsement_total, + SUM(CASE WHEN per.status = "Completed" THEN 1 ELSE 0 END) AS endorsement_done, + SUM(CASE WHEN per.status != "Completed" THEN 1 ELSE 0 END) AS endorsement_pending + ') + ->where('per.agent_id', $id) + ->where('per.is_active', 1) + ->get()->getRowArray(); + + // ── Merge everything + $data = array_merge( + $agent, + [ + 'status' => $agent['is_active'] ? 'Active' : 'Inactive', + 'commission_rate' => 15, + ], + $policyStats ?? [], + $enquiryStats ?? [], + $endorseStats ?? [], + ); + + $ref['message'] = 'Partner details retrieved successfully.'; + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $data, + 'ref' => $ref, + ], 200); + + } catch (\Throwable $e) { + + if ($e instanceof \RuntimeException && $e->getCode() === 404) { + $ref['message'] = 'No Data Found'; + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [], + 'message' => 'No Data Found', + 'ref' => $ref, + ], 200); + } + + $isDbError = $e instanceof \CodeIgniter\Database\Exceptions\DatabaseException + || $e instanceof \mysqli_sql_exception + || $e instanceof \PDOException; + + if ($isDbError) { + $ref['debug_info'] = $e->getFile() . ' / LN : ' . $e->getLine(); + $ref['message'] = 'Database Error Occurred.'; + } else { + $ref['message'] = 'An unexpected error occurred: ' . $e->getMessage(); + } + + return $this->respond([ + 'status' => 'error', + 'code' => 500, + 'data' => [], + 'ref' => $ref, + ], 200); + } } + + +// ══════════════════════════════════════════════════════════════════════════════ +// GET partner/{id}/policies +// partner_policy.agent_id = $id +// holder_name → pp.insured_name (the actual insured person, NOT agent) +// product → pp.product (varchar 50), falls back to pp.vehicle_type +// policy_no → pp.policy_number +// ══════════════════════════════════════════════════════════════════════════════ +public function partnerPolicies($id) +{ + $ref = []; + + try { + if (empty($id)) { + return $this->respond([ + 'status' => 'error', + 'code' => 400, + 'data' => [], + 'message' => 'Missing required parameter: id', + ], 200); + } + + $results = $this->db->table('partner_policy pp') + ->select(' + pp.policy_number AS policy_no, + pp.insured_name AS holder_name, + COALESCE(NULLIF(pp.product, ""), pp.vehicle_type) AS product, + pp.premium_amount AS premium, + DATE_FORMAT(pp.issued_date, "%d-%m-%Y") AS issued_date, + DATE_FORMAT(pp.end_date, "%d-%m-%Y") AS expiry_date + ') + ->where('pp.agent_id', $id) + ->where('pp.policy_number IS NOT NULL') + ->where('pp.premium_amount IS NOT NULL') + ->where('pp.is_active', 1) + ->orderBy('pp.issued_date', 'DESC') + ->get()->getResultArray(); + + $ref['total_records'] = count($results); + + if (empty($results)) { + throw new \RuntimeException('No policies found for this partner.', 404); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $results, + 'ref' => $ref, + ], 200); + + } catch (\Throwable $e) { + + if ($e instanceof \RuntimeException && $e->getCode() === 404) { + $ref['message'] = 'No Data Found'; + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [], + 'message' => 'No Data Found', + 'ref' => $ref, + ], 200); + } + + $isDbError = $e instanceof \CodeIgniter\Database\Exceptions\DatabaseException + || $e instanceof \mysqli_sql_exception + || $e instanceof \PDOException; + + if ($isDbError) { + $ref['debug_info'] = $e->getFile() . ' / LN : ' . $e->getLine(); + $ref['message'] = 'Database Error Occurred.'; + } else { + $ref['message'] = 'An unexpected error occurred: ' . $e->getMessage(); + } + + return $this->respond([ + 'status' => 'error', + 'code' => 500, + 'data' => [], + 'ref' => $ref, + ], 200); + } +} + + +// ══════════════════════════════════════════════════════════════════════════════ +// GET partner/{id}/renewals?days=20 +// partner_policy.agent_id = $id +// holder_name → pp.insured_name +// premium → pp.premium_amount +// ══════════════════════════════════════════════════════════════════════════════ +public function partnerRenewals($id) +{ + $ref = []; + $days = (int) ($this->request->getGet('days') ?? 20); + + try { + if (empty($id)) { + return $this->respond([ + 'status' => 'error', + 'code' => 400, + 'data' => [], + 'message' => 'Missing required parameter: id', + ], 200); + } + + $results = $this->db->table('partner_policy pp') + ->select(' + pp.policy_number AS policy_no, + pp.insured_name AS holder_name, + pp.premium_amount AS premium, + DATE_FORMAT(pp.end_date, "%d-%m-%Y") AS end_date, + DATEDIFF(pp.end_date, CURDATE()) AS days_left + ') + ->where('pp.agent_id', $id) + ->where('pp.is_active', 1) + ->where('pp.policy_number IS NOT NULL') + ->where('pp.premium_amount IS NOT NULL') + ->where('pp.end_date >= CURDATE()', null, false) + ->where("pp.end_date <= DATE_ADD(CURDATE(), INTERVAL {$days} DAY)", null, false) + ->orderBy('pp.end_date', 'ASC') + ->get()->getResultArray(); + + $ref['days_filter'] = $days; + $ref['total_records'] = count($results); + + if (empty($results)) { + throw new \RuntimeException('No renewals due within ' . $days . ' days.', 404); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $results, + 'ref' => $ref, + ], 200); + + } catch (\Throwable $e) { + + if ($e instanceof \RuntimeException && $e->getCode() === 404) { + $ref['message'] = 'No Data Found'; + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [], + 'message' => 'No Data Found', + 'ref' => $ref, + ], 200); + } + + $isDbError = $e instanceof \CodeIgniter\Database\Exceptions\DatabaseException + || $e instanceof \mysqli_sql_exception + || $e instanceof \PDOException; + + if ($isDbError) { + $ref['debug_info'] = $e->getFile() . ' / LN : ' . $e->getLine(); + $ref['message'] = 'Database Error Occurred.'; + } else { + $ref['message'] = 'An unexpected error occurred: ' . $e->getMessage(); + } + + return $this->respond([ + 'status' => 'error', + 'code' => 500, + 'data' => [], + 'ref' => $ref, + ], 200); + } +} + + +// ══════════════════════════════════════════════════════════════════════════════ +// GET partner/{id}/earnings +// partner_policy.agent_id = $id +// Groups by issued_date month → month_key (YYYY-MM), month_label (Month YYYY) +// paid = MAX(is_data_accuracy_checked) — 1 if all policies in month are checked +// No month_key param → returns ALL months (FY filtering done client-side in Dart) +// ══════════════════════════════════════════════════════════════════════════════ +public function partnerEarnings($id) +{ + $ref = []; + $monthKey = $this->request->getGet('month_key') ?? null; + + try { + if (empty($id)) { + return $this->respond([ + 'status' => 'error', + 'code' => 400, + 'data' => [], + 'message' => 'Missing required parameter: id', + ], 200); + } + + $builder = $this->db->table('partner_policy pp'); + + $builder->select(" + DATE_FORMAT(pp.issued_date, '%Y-%m') AS month_key, + DATE_FORMAT(pp.issued_date, '%M %Y') AS month_label, + COUNT(pp.id) AS policies, + COALESCE(SUM(pp.premium_amount), 0) AS premium, + COALESCE(SUM(pp.premium_amount * 0.15), 0) AS commission, + COALESCE(SUM(pp.premium_amount * 0.15 * 0.10), 0) AS tds, + COALESCE(SUM(pp.premium_amount * 0.15 * 0.90), 0) AS net_payout, + MAX(pp.is_data_accuracy_checked) AS paid + "); + + $builder->where('pp.agent_id', $id); + $builder->where('pp.is_active', 1); + $builder->where('pp.policy_number IS NOT NULL'); + $builder->where('pp.premium_amount IS NOT NULL'); + + if (!empty($monthKey)) { + $builder->where("DATE_FORMAT(pp.issued_date, '%Y-%m')", $monthKey); + } + + $builder->groupBy("DATE_FORMAT(pp.issued_date, '%Y-%m')"); + $builder->orderBy('month_key', 'ASC'); + + $results = $builder->get()->getResultArray(); + + $ref['month_filter'] = $monthKey ?? 'all'; + $ref['total_records'] = count($results); + + if (empty($results)) { + throw new \RuntimeException('No earning data found for this partner.', 404); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $results, + 'ref' => $ref, + ], 200); + + } catch (\Throwable $e) { + + if ($e instanceof \RuntimeException && $e->getCode() === 404) { + $ref['message'] = 'No Data Found'; + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [], + 'message' => 'No Data Found', + 'ref' => $ref, + ], 200); + } + + $isDbError = $e instanceof \CodeIgniter\Database\Exceptions\DatabaseException + || $e instanceof \mysqli_sql_exception + || $e instanceof \PDOException; + + if ($isDbError) { + $ref['debug_info'] = $e->getFile() . ' / LN : ' . $e->getLine(); + $ref['message'] = 'Database Error Occurred.'; + } else { + $ref['message'] = 'An unexpected error occurred: ' . $e->getMessage(); + } + + return $this->respond([ + 'status' => 'error', + 'code' => 500, + 'data' => [], + 'ref' => $ref, + ], 200); + } + } +} \ No newline at end of file diff --git a/app/Controllers/EndorsementController.php b/app/Controllers/EndorsementController.php index 6dfcd8b..a3d16de 100644 --- a/app/Controllers/EndorsementController.php +++ b/app/Controllers/EndorsementController.php @@ -139,8 +139,14 @@ class EndorsementController extends ResourceController foreach ($data as $key => $row) { $data[$key]['created_at'] = date('d-m-Y h:i A', strtotime($row['created_at'])); $data[$key]['updated_at'] = date('d-m-Y h:i A', strtotime($row['updated_at'])); - $data[$key]['policy_start_date'] = date('d-m-Y', strtotime($row['policy_start_date'])); - $data[$key]['policy_end_date'] = date('d-m-Y', strtotime($row['policy_end_date'])); + // ✅ NULL-safe date formatting + $data[$key]['policy_start_date'] = (!empty($row['policy_start_date']) && $row['policy_start_date'] !== '0000-00-00') + ? date('d-m-Y', strtotime($row['policy_start_date'])) + : null; + + $data[$key]['policy_end_date'] = (!empty($row['policy_end_date']) && $row['policy_end_date'] !== '0000-00-00') + ? date('d-m-Y', strtotime($row['policy_end_date'])) + : null; } return $this->respond(['status' => 'success','code' => 200,'data' => $data], 200); diff --git a/app/Controllers/EnquiryController.php b/app/Controllers/EnquiryController.php index 22c2454..fcd535a 100644 --- a/app/Controllers/EnquiryController.php +++ b/app/Controllers/EnquiryController.php @@ -186,11 +186,13 @@ class EnquiryController extends ResourceController } //Get enquiry details - $enquiry = $this->enquiryModel->select('partner_enquiry.* , I.name as insurer_name , I.short_name , VT.vehicle_type as vehicle_type ,PB.name as broker_name ,PA.name as agent_name,PA.agent_code') + $enquiry = $this->enquiryModel->select('partner_enquiry.* , I.name as insurer_name , I.short_name , VT.vehicle_type as vehicle_type ,PB.name as broker_name ,PA.name as agent_name,PA.agent_code,PPMM.value as payment_mode_value') ->join('insurers I', 'I.id = partner_enquiry.insurer_id', 'left') ->join('vehicle_type VT', 'VT.id = partner_enquiry.vehicle_type_id', 'left') ->join('partner_brokers PB', 'PB.id = partner_enquiry.broker_id', 'left') ->join('partner_agent PA', 'PA.id = partner_enquiry.agent_id', 'left') + ->join('partner_quotation PQ', 'PQ.enquiry_id = partner_enquiry.id', 'left') + ->join('partner_payment_mode_master PPMM', 'PPMM.id = PQ.payment_mode_id', 'left') ->where('partner_enquiry.id', $enquiry_id) ->where('partner_enquiry.is_active', 1) ->first(); @@ -834,7 +836,7 @@ class EnquiryController extends ResourceController try { $data = $this->request->getJSON(true); - print_r($data);die; + // print_r($data);die; if (!isset($data['id'])) { return $this->respond(['status' => 'failed', 'code' => 200, 'error' => 'ID Required'], 200); diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index 0f9d45d..5fb26df 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -41,7 +41,33 @@ class InvoiceController extends ResourceController pos.name as pos_name, FORMAT(partner_invoice.invoice_amount, 2, "en_IN") AS invoice_amount_indian_format, DATE_FORMAT(partner_invoice.invoice_date, "%d-%m-%Y") AS invoice_date_ui_format, - ') + ( + SELECT GROUP_CONCAT(pa.name ORDER BY pa.id SEPARATOR ", ") + FROM partner_agent pa + WHERE JSON_SEARCH(partner_invoice.agent_id, "one", CAST(pa.id AS CHAR)) IS NOT NULL + ) AS partner_names, + ( + SELECT GROUP_CONCAT(piu.utr_no ORDER BY piu.id SEPARATOR ", ") + FROM partner_invoice_utr piu + WHERE piu.invoice_id = partner_invoice.id + AND piu.is_active = 1 + ) AS utr_numbers, + partner_invoice.invoice_amount AS invoiced_amount, + COALESCE(( + SELECT SUM(piu.amount) + FROM partner_invoice_utr piu + WHERE piu.invoice_id = partner_invoice.id + AND piu.is_active = 1 + ), 0.00) AS payout_amount, + ( + partner_invoice.invoice_amount - COALESCE(( + SELECT SUM(piu.amount) + FROM partner_invoice_utr piu + WHERE piu.invoice_id = partner_invoice.id + AND piu.is_active = 1 + ), 0.00) + ) AS balance_amount', + false) ->join('partner_brokers PB', 'PB.id = partner_invoice.broker_id', 'left') ->join('partner_pos pos', 'pos.id = partner_invoice.pos_id', 'left') ->where('partner_invoice.is_active', 1) @@ -193,6 +219,7 @@ class InvoiceController extends ResourceController private function generateInvoiceNo($pos_id) { + $pos_id = !empty($pos_id) ? $pos_id : 0; $year = date('Y'); $prefix = "NIIB/$pos_id/$year/"; diff --git a/app/Helpers/common_helper.php b/app/Helpers/common_helper.php index 7f3ae14..6d84858 100644 --- a/app/Helpers/common_helper.php +++ b/app/Helpers/common_helper.php @@ -181,8 +181,9 @@ if (!function_exists('createBDS')) { if (!function_exists('format_date_for_database')) { - function format_date_for_database(string $date): ?string + function format_date_for_database(?string $date): ?string { + if (empty($date)) return null; // ✅ handle null/empty $dt = DateTime::createFromFormat('d-m-Y', $date); if ($dt) { return $dt->format('Y-m-d'); From ce7033654a77642f8f99c2735ab88eeaa1c39ec7 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Tue, 24 Mar 2026 17:57:34 +0530 Subject: [PATCH 07/18] FIX_Changes and Additional Requirements 3,4,5 --- app/Config/Routes.php | 9 + app/Controllers/AgentController.php | 719 +++++++++++++++++- app/Controllers/InvoiceController.php | 518 ++++++++++++- app/Models/AgentIncentiveFileModel.php | 4 +- app/Models/PartnerAccountHistoryModel.php | 39 + app/Models/PartnerGridDetailsModel.php | 187 +++++ writable/uploads/sample_partner_grid_file.xls | Bin 0 -> 7680 bytes 7 files changed, 1460 insertions(+), 16 deletions(-) create mode 100644 app/Models/PartnerAccountHistoryModel.php create mode 100644 app/Models/PartnerGridDetailsModel.php create mode 100644 writable/uploads/sample_partner_grid_file.xls diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 8e974d1..c1aed35 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -73,6 +73,10 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->post('agent/uploadAgentIncentiveFile', 'AgentController::uploadAgentIncentiveFile'); $routes->get('agent/deleteAgentIncentiveFile', 'AgentController::deleteAgentIncentiveFile'); $routes->get('agent/downloadAgentIncentiveFile', 'AgentController::downloadAgentIncentiveFile'); + $routes->get('agent/downloadSamplePartnerGridExcel', 'AgentController::downloadSamplePartnerGridExcel'); + $routes->get('agent/monthlyCommissionGridFilters', 'AgentController::monthlyCommissionGridFilters'); + $routes->get('agent/monthlyCommissionGridList', 'AgentController::monthlyCommissionGridList'); + $routes->get('agent/downloadMonthlyCommissionGrid', 'AgentController::downloadMonthlyCommissionGrid'); //Staff @@ -201,9 +205,14 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('invoice/list', 'InvoiceController::invoiceList'); $routes->get('invoice/details', 'InvoiceController::findInvoiceWithItems'); $routes->post('invoice/create-or-update', 'InvoiceController::createOrUpdateInvoice'); + $routes->post('invoice/add-payment', 'InvoiceController::addInvoicePayment'); $routes->get('invoice/delete', 'InvoiceController::deleteInvoice'); $routes->post('invoice/commission-rate-list', 'InvoiceController::getCommissionRateList'); $routes->get('invoice/getAgentUnusedCommissionList', 'InvoiceController::getAgentUnusedCommissionList'); + $routes->post('invoice/bulk-upload-commission', 'InvoiceController::bulkUploadCommission'); + $routes->post('invoice/bulk-upload-commission/proceed', 'InvoiceController::bulkUploadCommissionProceed'); + $routes->post('invoice/bulk-upload-commission', 'InvoiceController::bulkUploadCommission'); + $routes->post('invoice/bulk-upload-commission/proceed', 'InvoiceController::bulkUploadCommissionProceed'); // SALES EXECUTIVE diff --git a/app/Controllers/AgentController.php b/app/Controllers/AgentController.php index 47cd921..e71be0d 100644 --- a/app/Controllers/AgentController.php +++ b/app/Controllers/AgentController.php @@ -5,16 +5,41 @@ use CodeIgniter\RESTful\ResourceController; use App\Controllers\BaseController; use App\Models\AgentModel; use App\Models\AgentIncentiveFileModel; +use App\Models\PartnerGridDetailsModel; +use PhpOffice\PhpSpreadsheet\IOFactory; class AgentController extends ResourceController { protected $AgentModel; protected $AgentIncentiveFileModel; + protected $PartnerGridDetailsModel; public function __construct() { + helper('jwt_helper'); $this->AgentModel = new AgentModel(); $this->AgentIncentiveFileModel = new AgentIncentiveFileModel(); + $this->PartnerGridDetailsModel = new PartnerGridDetailsModel(); + } + + private function getAuthenticatedUserData(): ?object + { + $header = $this->request->getHeaderLine('Authorization'); + if (!$header || !preg_match('/Bearer\s(\S+)/', $header, $matches)) { + return null; + } + + $decodedToken = validateJWT($matches[1]); + if (!$decodedToken || !isset($decodedToken['data'])) { + return null; + } + + return $decodedToken['data']; + } + + private function canManagePartnerGrid(?string $role): bool + { + return in_array((string) $role, ['1', '4'], true); } // List of all agents @@ -267,10 +292,30 @@ class AgentController extends ResourceController public function agentIncentiveFileList() { try{ + $authUser = $this->getAuthenticatedUserData(); + if (!$authUser) { + return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); + } - $agent_id = $this->request->getGet('agent_id'); + $roleId = (string) ($authUser->role_id ?? 'agent'); + $agentIdFromRequest = $this->request->getGet('agent_id'); + $fileType = trim((string) ($this->request->getGet('file_type') ?? '')); - $data = $this->AgentIncentiveFileModel->where('agent_id',$agent_id)->where('is_active',1)->findAll(); + // Partner can only view own files; manager/accounts can view all. + $agentId = $this->canManagePartnerGrid($roleId) ? $agentIdFromRequest : ($authUser->id ?? null); + if (empty($agentId)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'agent_id is required'], 200); + } + + $builder = $this->AgentIncentiveFileModel + ->where('agent_id', (int) $agentId) + ->where('is_active', 1); + + if ($fileType !== '') { + $builder->where('file_type', $fileType); + } + + $data = $builder->orderBy('id', 'DESC')->findAll(); return $this->respond(['status' => 'success', 'code' => 200, 'data' => $data], 200); @@ -285,13 +330,39 @@ class AgentController extends ResourceController { try{ $data = $this->request->getPost(); - - //duplicate check - $duplicateData = $this->AgentIncentiveFileModel->where('agent_id',$data['agent_id'])->where('incentive_month',$data['incentive_month'])->first(); - if(!empty($duplicateData)){ - return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'Duplicate Entry.'], 200); + $authUser = $this->getAuthenticatedUserData(); + if (!$authUser) { + return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); } + $roleId = (string) ($authUser->role_id ?? 'agent'); + $isGridUpload = isset($data['file_type']) && $data['file_type'] === 'grid'; + + if ($isGridUpload && !$this->canManagePartnerGrid($roleId)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 403, + 'data' => 'Only Manager and Accounts can upload/edit partner grid files' + ], 403); + } + + if ($isGridUpload) { + $this->uploadGridFile($data); + } else { + + $duplicateData = $this->AgentIncentiveFileModel + ->where('agent_id', $data['agent_id']) + ->where('incentive_month', $data['incentive_month']) + ->first(); + + if (!empty($duplicateData)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 200, + 'data' => 'Duplicate Entry.' + ], 200); + } + } // handle file uploads $incentiveFile = $this->request->getFile('incentive_file_name'); @@ -311,6 +382,7 @@ class AgentController extends ResourceController 'agent_id' => $data['agent_id'], 'incentive_month' => $data['incentive_month'], 'incentive_file_name' => $incentiveFileName, + 'file_type' => $data['file_type'] ?? 'incentive', 'created_by' => $data['created_by'] ?? null ]; @@ -323,10 +395,292 @@ class AgentController extends ResourceController } } + // ───────────────────────────────────────── + // Grid Functionality + // ───────────────────────────────────────── + public function uploadGridFile($data){ + $gridFile = $this->request->getFile('incentive_file_name'); + + if (!$gridFile || !$gridFile->isValid()) { + throw new \RuntimeException('Valid grid file is required'); + } + + $extension = strtolower((string) $gridFile->getExtension()); + if (!in_array($extension, ['xlsx', 'xls', 'csv'], true)) { + throw new \RuntimeException('Only xlsx, xls, csv grid files are allowed'); + } + + $spreadsheet = IOFactory::load($gridFile->getTempName()); + $rows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); + + if (empty($rows)) { + throw new \RuntimeException('Grid file is empty'); + } + + $normalize = static function ($value): string { + $value = strtolower(trim((string) $value)); + return preg_replace('/[^a-z0-9]+/', '', $value) ?? ''; + }; + $toNumber = static function ($value): float { + if (is_numeric($value)) { + return (float) $value; + } + if (preg_match('/-?\d+(\.\d+)?/', (string) $value, $matches)) { + return (float) $matches[0]; + } + return 0.0; + }; + $toNullable = static function ($value): ?string { + $value = trim((string) $value); + return $value === '' ? null : $value; + }; + $formatNumber = static function (float $value): string { + return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.'); + }; + $parsePremium = static function ($value) use ($formatNumber): ?string { + $source = trim((string) $value); + if ($source === '') { + return null; + } + if (preg_match('/-?\d+(?:\.\d+)?/', $source, $matches) !== 1) { + return null; + } + return $formatNumber((float) $matches[0]); + }; + + // Fixed format: first row is header and data starts from row 2. + $headerRowIndex = 0; + $headerMap = []; + $headerRow = $rows[$headerRowIndex] ?? []; + foreach ($headerRow as $colIndex => $cell) { + $normalizedHeader = $normalize($cell); + if ($normalizedHeader === 'type') { + $headerMap['vehicle_type_id'] = $colIndex; + } elseif ($normalizedHeader === 'insurer') { + $headerMap['insurer_id'] = $colIndex; + } elseif ($normalizedHeader === 'rto') { + $headerMap['rto_id'] = $colIndex; + } elseif ($normalizedHeader === 'segment') { + $headerMap['segment_id'] = $colIndex; + } elseif ($normalizedHeader === 'comp') { + $headerMap['comp'] = $colIndex; + } elseif ($normalizedHeader === 'tp') { + $headerMap['tp'] = $colIndex; + } elseif ($normalizedHeader === 'fuel') { + $headerMap['fuel'] = $colIndex; + } elseif ($normalizedHeader === 'remarks') { + $headerMap['remarks'] = $colIndex; + } + } + + if (!isset($headerMap['insurer_id'], $headerMap['rto_id'], $headerMap['segment_id'], $headerMap['vehicle_type_id'])) { + throw new \RuntimeException('Invalid grid header. Required: TYPE, INSURER, RTO, SEGMENT'); + } + + $defaultPartnerId = !empty($data['agent_id']) ? (int) $data['agent_id'] : null; + $createdBy = $data['created_by'] ?? null; + + $db = \Config\Database::connect(); + + $vehicleTypeMaster = $db->table('vehicle_type')->select('id, vehicle_type')->where('is_active', 1)->get()->getResultArray(); + + $insurerMaster = $db->table('insurers')->select('id, name, short_name')->where('is_active', 1)->get()->getResultArray(); + + $rtoMaster = $db->table('rto_master')->select('id, rto_code, rto_name')->where('is_active', 1)->get()->getResultArray(); + + $partnerMaster = $db->table('partner_agent')->select('id, name, agent_code, retention_rate')->where('is_active', 1)->get()->getResultArray(); + + + $insurerMap = []; + + foreach ($insurerMaster as $item) { + $insurerMap[$normalize($item['name'])] = (int) $item['id']; + $insurerMap[$normalize($item['short_name'])] = (int) $item['id']; + $insurerMap[(string) $item['id']] = (int) $item['id']; + } + + $rtoMap = []; + foreach ($rtoMaster as $item) { + $rtoMap[$normalize($item['rto_code'])] = (int) $item['id']; + $rtoMap[$normalize($item['rto_name'])] = (int) $item['id']; + $rtoMap[(string) $item['id']] = (int) $item['id']; + } + + $partnerMap = []; + $partnerRetentionMap = []; + foreach ($partnerMaster as $item) { + $id = (int) $item['id']; + $partnerMap[$normalize($item['name'])] = $id; + $partnerMap[$normalize($item['agent_code'])] = $id; + $partnerMap[(string) $id] = $id; + $partnerRetentionMap[$id] = $toNumber($item['retention_rate'] ?? 0); + } + + $insertCount = 0; + $updateCount = 0; + $skipStats = [ + 'empty_row' => 0, + 'missing_required_columns' => 0, + 'master_mapping_failed' => 0, + 'partner_not_found' => 0, + ]; + $skipSamples = []; + + for ($i = $headerRowIndex + 1, $count = count($rows); $i < $count; $i++) { + $row = $rows[$i]; + $hasAnyData = false; + + foreach ($row as $cellValue) { + if (trim((string) $cellValue) !== '') { + $hasAnyData = true; + break; + } + } + + if (!$hasAnyData) { + $skipStats['empty_row']++; + continue; + } + + $insurerRaw = trim((string) ($row[$headerMap['insurer_id']] ?? '')); + $rtoRaw = trim((string) ($row[$headerMap['rto_id']] ?? '')); + $segmentRaw = trim((string) ($row[$headerMap['segment_id']] ?? '')); + $vehicleTypeRaw = trim((string) ($row[$headerMap['vehicle_type_id']] ?? $segmentRaw)); + + if ($insurerRaw === '' || $rtoRaw === '' || $segmentRaw === '' || $vehicleTypeRaw === '') { + $skipStats['missing_required_columns']++; + if (count($skipSamples) < 15) { + $skipSamples[] = [ + 'row' => $i + 1, + 'reason' => 'missing_required_columns', + 'insurer' => $insurerRaw, + 'rto' => $rtoRaw, + 'segment' => $segmentRaw, + 'vehicle_type' => $vehicleTypeRaw, + ]; + } + continue; + } + + + $insurerId = $insurerMap[$normalize($insurerRaw)] ?? null; + if ($insurerId === null && ctype_digit($insurerRaw) && isset($insurerMap[$insurerRaw])) { + $insurerId = $insurerMap[$insurerRaw]; + } + + $rtoId = $rtoMap[$normalize($rtoRaw)] ?? null; + if ($rtoId === null && ctype_digit($rtoRaw) && isset($rtoMap[$rtoRaw])) { + $rtoId = $rtoMap[$rtoRaw]; + } + + if (empty($insurerId) || empty($rtoId)) { + $skipStats['master_mapping_failed']++; + if (count($skipSamples) < 15) { + $skipSamples[] = [ + 'row' => $i + 1, + 'reason' => 'master_mapping_failed', + 'insurer' => $insurerRaw, + 'rto' => $rtoRaw, + 'segment' => $segmentRaw, + 'vehicle_type' => $vehicleTypeRaw, + ]; + } + continue; + } + + $comp = trim((string) ($row[$headerMap['comp']] ?? '')); + $tp = trim((string) ($row[$headerMap['tp']] ?? '')); + $remarks = trim((string) ($row[$headerMap['remarks']] ?? '')); + $fuelRaw = trim((string) ($row[$headerMap['fuel']] ?? '')); + $fuel = $toNullable($fuelRaw); + $partnerRaw = trim((string) ($row[$headerMap['partner_id']] ?? '')); + + $partnerId = null; + if ($partnerRaw !== '') { + $partnerId = $partnerMap[$normalize($partnerRaw)] ?? null; + if ($partnerId === null && ctype_digit($partnerRaw) && isset($partnerMap[$partnerRaw])) { + $partnerId = $partnerMap[$partnerRaw]; + } + } + + if (empty($partnerId) && !empty($defaultPartnerId)) { + $partnerId = $defaultPartnerId; + } + + if (empty($partnerId)) { + $skipStats['partner_not_found']++; + if (count($skipSamples) < 15) { + $skipSamples[] = [ + 'row' => $i + 1, + 'reason' => 'partner_not_found', + 'partner' => $partnerRaw, + ]; + } + continue; + } + + $retentionRate = $partnerRetentionMap[(int) $partnerId] ?? 0.0; + $compSanitized = $parsePremium($comp); + $tpSanitized = $parsePremium($tp); + $partnerComp = $compSanitized !== null ? $formatNumber($retentionRate + (float) $compSanitized) : null; + $partnerTp = $tpSanitized !== null ? $formatNumber($retentionRate + (float) $tpSanitized) : null; + + $recordData = [ + 'vehicle_type_id' => $vehicleTypeRaw, + 'insurer_id' => (string) $insurerId, + 'rto_id' => (string) $rtoId, + 'segment_id' => $segmentRaw, + 'comp' => $compSanitized, + 'tp' => $tpSanitized, + 'fuel' => $fuel, + 'remarks' => $toNullable($remarks), + 'partner_id' => (string) $partnerId, + 'partner_comp' => $partnerComp, + 'partner_tp' => $partnerTp, + ]; + + $existing = $this->PartnerGridDetailsModel + ->where('vehicle_type_id', $vehicleTypeRaw) + ->where('insurer_id', (string) $insurerId) + ->where('rto_id', (string) $rtoId) + ->where('segment_id', $segmentRaw) + ->where('partner_id', (string) $partnerId) + ->first(); + + if ($existing) { + $recordData['updated_by'] = $createdBy; + $this->PartnerGridDetailsModel->update((int) $existing['id'], $recordData); + $updateCount++; + } else { + $recordData['created_by'] = $createdBy; + $this->PartnerGridDetailsModel->insert($recordData); + $insertCount++; + } + } + + if ($insertCount === 0 && $updateCount === 0) { + $debugData = [ + 'message' => 'No valid rows found in grid file', + 'header_row' => $headerRowIndex + 1, + 'detected_headers' => array_keys($headerMap), + 'skip_stats' => $skipStats, + 'skip_samples' => $skipSamples, + ]; + throw new \RuntimeException(json_encode($debugData, JSON_UNESCAPED_SLASHES)); + } + + return true; + } + // Delete agent incentive file public function deleteAgentIncentiveFile() { try { + $authUser = $this->getAuthenticatedUserData(); + if (!$authUser) { + return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); + } + $roleId = (string) ($authUser->role_id ?? 'agent'); $id = $this->request->getGet('id'); @@ -337,6 +691,14 @@ class AgentController extends ResourceController return $this->respond(['status' => 'failed','code' => 200, 'data' => 'File not found'], 200); } + if (($file['file_type'] ?? '') === 'grid' && !$this->canManagePartnerGrid($roleId)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 403, + 'data' => 'Only Manager and Accounts can edit/delete partner grid files' + ], 403); + } + // update status $this->AgentIncentiveFileModel->update($id, ['is_active' => 0]); @@ -352,6 +714,11 @@ class AgentController extends ResourceController public function downloadAgentIncentiveFile() { try { + $authUser = $this->getAuthenticatedUserData(); + if (!$authUser) { + return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); + } + $roleId = (string) ($authUser->role_id ?? 'agent'); $id = $this->request->getGet('id'); if (!$id) { @@ -365,6 +732,10 @@ class AgentController extends ResourceController return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File not found'], 200); } + if (!$this->canManagePartnerGrid($roleId) && (int) $fileRecord['agent_id'] !== (int) ($authUser->id ?? 0)) { + return $this->respond(['status' => 'failed', 'code' => 403, 'data' => 'Access denied'], 403); + } + $filePath = WRITEPATH . 'uploads/agent/incentive_file/' . $fileRecord['incentive_file_name']; if (!file_exists($filePath)) { @@ -379,10 +750,344 @@ class AgentController extends ResourceController } } + // Download sample partner grid excel + public function downloadSamplePartnerGridExcel() + { + try { + $filePath = WRITEPATH . 'uploads/sample_partner_grid_file.xlsx'; + if (!file_exists($filePath)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'Sample partner grid file not found on server', + ], 200); + } + return $this->response->download($filePath, null); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ], 500); + } + } + // Monthly commission grid filters + public function monthlyCommissionGridFilters() + { + try { + $authUser = $this->getAuthenticatedUserData(); + if (!$authUser) { + return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); + } + $db = \Config\Database::connect(); + $rtoMaster = $db->table('rto_master') + ->select('id, rto_code, rto_name') + ->where('is_active', 1) + ->orderBy('rto_code', 'ASC') + ->get() + ->getResultArray(); + $segmentMaster = $db->table('vehicle_type') + ->select('id, vehicle_type') + ->where('is_active', 1) + ->orderBy('vehicle_type', 'ASC') + ->get() + ->getResultArray(); + + $planMaster = $db->table('partner_insurance_plan_type_master') + ->select('id, insurance_plan_type') + ->where('is_active', 1) + ->orderBy('insurance_plan_type', 'ASC') + ->get() + ->getResultArray(); + + $partnerMaster = $db->table('partner_agent') + ->select('id, name, agent_code, retention_rate') + ->where('is_active', 1) + ->orderBy('name', 'ASC') + ->get() + ->getResultArray(); + + $monthRows = $db->table('partner_grid_details') + ->select("DATE_FORMAT(created_at, '%Y-%m') AS month_key") + ->where('created_at IS NOT NULL', null, false) + ->groupBy("DATE_FORMAT(created_at, '%Y-%m')") + ->orderBy('month_key', 'DESC') + ->get() + ->getResultArray(); + + $months = array_values(array_filter(array_map(static function ($row) { + return $row['month_key'] ?? null; + }, $monthRows))); + + $latestMonth = !empty($months) ? $months[0] : date('Y-m'); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'rto_master' => $rtoMaster, + 'segment_master' => $segmentMaster, + 'plan_master' => $planMaster, + 'partner_master' => $partnerMaster, + 'months' => $months, + 'default_month' => $latestMonth, + ], + ], 200); + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } + + // Monthly commission grid list + public function monthlyCommissionGridList() + { + try { + $authUser = $this->getAuthenticatedUserData(); + if (!$authUser) { + return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); + } + + $partnerId = (int) ($this->request->getGet('partner_id') ?? 0); + $rtoId = (int) ($this->request->getGet('rto_id') ?? 0); + $segmentId = (int) ($this->request->getGet('segment_id') ?? 0); + $planType = trim((string) ($this->request->getGet('insurance_plan_type') ?? '')); + $month = trim((string) ($this->request->getGet('month') ?? '')); + + if ($partnerId <= 0 || $rtoId <= 0 || $segmentId <= 0 || $planType === '') { + return $this->respond([ + 'status' => 'failed', + 'code' => 200, + 'data' => 'partner_id, rto_id, segment_id and insurance_plan_type are required', + ], 200); + } + + $normalizedPlan = strtolower($planType); + + $db = \Config\Database::connect(); + $latestRow = $db->table('partner_grid_details') + ->select("DATE_FORMAT(created_at, '%Y-%m') AS month_key") + ->where('created_at IS NOT NULL', null, false) + ->orderBy('created_at', 'DESC') + ->get(1) + ->getRowArray(); + + $effectiveMonth = $month !== '' ? $month : (($latestRow['month_key'] ?? '') ?: date('Y-m')); + + $rows = $db->table('partner_grid_details pgd') + ->select(" + pgd.id, + pgd.partner_id, + pa.name AS partner_name, + pa.agent_code, + COALESCE(pa.retention_rate, 0) AS retention_rate, + pgd.rto_id, + rm.rto_code, + rm.rto_name, + pgd.segment, + pgd.vehicle_type_id, + vt.vehicle_type AS vehicle_type_name, + pgd.comp, + pgd.tp, + pgd.fuel, + pgd.remarks, + DATE_FORMAT(pgd.created_at, '%Y-%m') AS month_key + ") + ->join('partner_agent pa', 'pa.id = pgd.partner_id', 'left') + ->join('rto_master rm', 'rm.id = pgd.rto_id', 'left') + ->join('vehicle_type vt', 'vt.id = pgd.vehicle_type_id', 'left') + ->where('pgd.partner_id', $partnerId) + ->where('pgd.rto_id', $rtoId) + ->where('pgd.segment_id', $segmentId) + ->where("DATE_FORMAT(pgd.created_at, '%Y-%m') =", $effectiveMonth) + ->orderBy('pgd.id', 'DESC') + ->get() + ->getResultArray(); + + $toPercent = static function ($value): float { + if ($value === null) { + return 0.0; + } + if (is_numeric($value)) { + return (float) $value; + } + if (preg_match('/-?\d+(\.\d+)?/', (string) $value, $matches)) { + return (float) $matches[0]; + } + return 0.0; + }; + + $result = []; + foreach ($rows as $row) { + $retention = $toPercent($row['retention_rate'] ?? 0); + $compRate = $toPercent($row['comp'] ?? 0); + $tpRate = $toPercent($row['tp'] ?? 0); + + if (strpos($normalizedPlan, 'third') !== false || $normalizedPlan === 'tp') { + $gridRate = $tpRate; + } else { + // Comprehensive + Own Damage use COMP column. + $gridRate = $compRate; + } + + $netRate = $gridRate - $retention; + + $row['insurance_plan_type'] = $planType; + $row['month'] = $effectiveMonth; + $row['grid_rate_percentage'] = round($gridRate, 2); + $row['retention_rate_percentage'] = round($retention, 2); + $row['final_commission_percentage'] = round($netRate, 2); + $result[] = $row; + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $result, + 'meta' => [ + 'month' => $effectiveMonth, + 'insurance_plan_type' => $planType, + ], + ], 200); + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } + + // Monthly commission grid download + public function downloadMonthlyCommissionGrid() + { + try { + $authUser = $this->getAuthenticatedUserData(); + if (!$authUser) { + return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); + } + + $partnerId = (int) ($this->request->getGet('partner_id') ?? 0); + $rtoId = (int) ($this->request->getGet('rto_id') ?? 0); + $segmentId = (int) ($this->request->getGet('segment_id') ?? 0); + $planType = trim((string) ($this->request->getGet('insurance_plan_type') ?? '')); + $month = trim((string) ($this->request->getGet('month') ?? '')); + + if ($partnerId <= 0 || $rtoId <= 0 || $segmentId <= 0 || $planType === '') { + return $this->respond([ + 'status' => 'failed', + 'code' => 200, + 'data' => 'partner_id, rto_id, segment_id and insurance_plan_type are required', + ], 200); + } + + $normalizedPlan = strtolower($planType); + $db = \Config\Database::connect(); + $latestRow = $db->table('partner_grid_details') + ->select("DATE_FORMAT(created_at, '%Y-%m') AS month_key") + ->where('created_at IS NOT NULL', null, false) + ->orderBy('created_at', 'DESC') + ->get(1) + ->getRowArray(); + $effectiveMonth = $month !== '' ? $month : (($latestRow['month_key'] ?? '') ?: date('Y-m')); + + $sourceRows = $db->table('partner_grid_details pgd') + ->select(" + pa.name AS partner_name, + pa.agent_code, + COALESCE(pa.retention_rate, 0) AS retention_rate, + rm.rto_code, + rm.rto_name, + vt.vehicle_type AS segment_name, + pgd.comp, + pgd.tp + ") + ->join('partner_agent pa', 'pa.id = pgd.partner_id', 'left') + ->join('rto_master rm', 'rm.id = pgd.rto_id', 'left') + ->join('vehicle_type vt', 'vt.id = pgd.segment_id', 'left') + ->where('pgd.partner_id', $partnerId) + ->where('pgd.rto_id', $rtoId) + ->where('pgd.segment_id', $segmentId) + ->where("DATE_FORMAT(pgd.created_at, '%Y-%m') =", $effectiveMonth) + ->orderBy('pgd.id', 'DESC') + ->get() + ->getResultArray(); + + $toPercent = static function ($value): float { + if ($value === null) { + return 0.0; + } + if (is_numeric($value)) { + return (float) $value; + } + if (preg_match('/-?\d+(\.\d+)?/', (string) $value, $matches)) { + return (float) $matches[0]; + } + return 0.0; + }; + + $rows = []; + foreach ($sourceRows as $row) { + $retention = $toPercent($row['retention_rate'] ?? 0); + $compRate = $toPercent($row['comp'] ?? 0); + $tpRate = $toPercent($row['tp'] ?? 0); + $gridRate = (strpos($normalizedPlan, 'third') !== false || $normalizedPlan === 'tp') ? $tpRate : $compRate; + + $row['month'] = $effectiveMonth; + $row['insurance_plan_type'] = $planType; + $row['grid_rate_percentage'] = round($gridRate, 2); + $row['retention_rate_percentage'] = round($retention, 2); + $row['final_commission_percentage'] = round($gridRate - $retention, 2); + $rows[] = $row; + } + + if (empty($rows)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'No data found for the selected filters', + ], 200); + } + + $safePlan = preg_replace('/[^a-zA-Z0-9]+/', '_', strtolower($planType)) ?: 'plan'; + $fileName = "monthly_commission_{$effectiveMonth}_{$safePlan}.csv"; + $tmpFile = WRITEPATH . 'uploads/temp/' . $fileName; + if (!is_dir(dirname($tmpFile))) { + mkdir(dirname($tmpFile), 0777, true); + } + + $fp = fopen($tmpFile, 'w'); + fputcsv($fp, [ + 'Month', + 'Partner', + 'Partner Code', + 'RTO', + 'Segment', + 'Insurance Plan', + 'Grid %', + 'Retention %', + 'Final Commission %', + ]); + + foreach ($rows as $row) { + fputcsv($fp, [ + $row['month'] ?? $effectiveMonth, + $row['partner_name'] ?? '', + $row['agent_code'] ?? '', + trim((string) (($row['rto_code'] ?? '') . ' - ' . ($row['rto_name'] ?? '')), ' -'), + $row['segment_name'] ?? '', + $row['insurance_plan_type'] ?? $planType, + $row['grid_rate_percentage'] ?? 0, + $row['retention_rate_percentage'] ?? 0, + $row['final_commission_percentage'] ?? 0, + ]); + } + + fclose($fp); + return $this->response->download($tmpFile, null)->setFileName($fileName); + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } } diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index 5fb26df..c275563 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -8,6 +8,7 @@ use App\Models\QuotationModel; use App\Models\InvoiceModel; use App\Models\InvoiceItemModel; use App\Models\InvoiceUtrModel; +use App\Models\PartnerAccountHistoryModel; use CodeIgniter\Database\Exceptions\DataException; class InvoiceController extends ResourceController @@ -18,6 +19,7 @@ class InvoiceController extends ResourceController protected $InvoiceModel; protected $InvoiceItemModel; protected $InvoiceUtrModel; + protected $PartnerAccountHistoryModel; protected $db; public function __construct() @@ -28,6 +30,7 @@ class InvoiceController extends ResourceController $this->InvoiceModel = new InvoiceModel(); $this->InvoiceItemModel = new InvoiceItemModel(); $this->InvoiceUtrModel = new InvoiceUtrModel(); + $this->PartnerAccountHistoryModel = new PartnerAccountHistoryModel(); $this->db = \Config\Database::connect(); } @@ -53,19 +56,37 @@ class InvoiceController extends ResourceController AND piu.is_active = 1 ) AS utr_numbers, partner_invoice.invoice_amount AS invoiced_amount, - COALESCE(( - SELECT SUM(piu.amount) - FROM partner_invoice_utr piu - WHERE piu.invoice_id = partner_invoice.id - AND piu.is_active = 1 - ), 0.00) AS payout_amount, ( - partner_invoice.invoice_amount - COALESCE(( + COALESCE(( + SELECT SUM(pah.paid_amount) + FROM partner_account_history pah + WHERE pah.invoice_id = partner_invoice.id + AND pah.is_active = 1 + ), 0.00) + + + COALESCE(( SELECT SUM(piu.amount) FROM partner_invoice_utr piu WHERE piu.invoice_id = partner_invoice.id AND piu.is_active = 1 ), 0.00) + ) AS payout_amount, + ( + partner_invoice.invoice_amount - ( + COALESCE(( + SELECT SUM(pah.paid_amount) + FROM partner_account_history pah + WHERE pah.invoice_id = partner_invoice.id + AND pah.is_active = 1 + ), 0.00) + + + COALESCE(( + SELECT SUM(piu.amount) + FROM partner_invoice_utr piu + WHERE piu.invoice_id = partner_invoice.id + AND piu.is_active = 1 + ), 0.00) + ) ) AS balance_amount', false) ->join('partner_brokers PB', 'PB.id = partner_invoice.broker_id', 'left') @@ -104,6 +125,29 @@ class InvoiceController extends ResourceController ->where('partner_invoice_items.is_active', 1) ->findAll(); + $paymentHistory = $this->PartnerAccountHistoryModel + ->where('invoice_id', $id) + ->where('is_active', 1) + ->orderBy('paid_date', 'DESC') + ->orderBy('id', 'DESC') + ->findAll(); + + $paidAmount = 0.00; + foreach ($paymentHistory as $payment) { + $paidAmount += (float) ($payment['paid_amount'] ?? 0); + } + + $utrPaidAmount = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $id) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + + $totalPaidAmount = $paidAmount + $utrPaidAmount; + $invoiceAmount = (float) ($invoice['invoice_amount'] ?? 0); + $balanceAmount = max($invoiceAmount - $totalPaidAmount, 0); return $this->respond([ @@ -111,7 +155,10 @@ class InvoiceController extends ResourceController 'code' => 200, 'data' => [ 'invoice' => $invoice, - 'items' => $items + 'items' => $items, + 'payment_history' => $paymentHistory, + 'paid_amount' => $totalPaidAmount, + 'balance_amount' => $balanceAmount ] ], 200); @@ -415,6 +462,461 @@ class InvoiceController extends ResourceController } } + public function addInvoicePayment() + { + $this->db->transBegin(); + + try { + $input = $this->request->getJSON(true); + + $invoiceId = (int)($input['invoice_id'] ?? 0); + $paidAmount = (float)($input['paid_amount'] ?? 0); + $paidDateRaw = $input['paid_date'] ?? null; + + if ($invoiceId <= 0 || $paidAmount <= 0 || empty($paidDateRaw)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'invoice_id, paid_amount and paid_date are required' + ], 400); + } + + $invoice = $this->InvoiceModel + ->where('id', $invoiceId) + ->where('is_active', 1) + ->first(); + + if (empty($invoice)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message'=> 'Invoice not found' + ], 404); + } + + $paidDateTs = strtotime($paidDateRaw); + if ($paidDateTs === false) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'Invalid paid_date format' + ], 400); + } + $paidDate = date('Y-m-d', $paidDateTs); + + $historyPaidAmount = (float) ( + $this->PartnerAccountHistoryModel + ->selectSum('paid_amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + + $utrPaidAmount = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + + $currentTotalPaid = $historyPaidAmount + $utrPaidAmount; + $invoiceAmount = (float) ($invoice['invoice_amount'] ?? 0); + $remainingBalance = max($invoiceAmount - $currentTotalPaid, 0); + + if ($paidAmount > $remainingBalance) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'Paid amount exceeds invoice balance' + ], 400); + } + + $historyData = [ + 'invoice_id' => $invoiceId, + 'paid_amount' => $paidAmount, + 'paid_date' => $paidDate, + 'is_active' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'created_by' => (int)($input['created_by'] ?? 0), + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => (int)($input['updated_by'] ?? 0), + ]; + + $historyId = $this->PartnerAccountHistoryModel->insert($historyData); + + if ($historyId === false) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> 'Failed to record payment', + 'error' => $this->PartnerAccountHistoryModel->errors() + ], 500); + } + + $latestTotalPaid = $currentTotalPaid + $paidAmount; + $newBalance = max($invoiceAmount - $latestTotalPaid, 0); + $payoutStatus = $newBalance == 0.0 ? 2 : 1; + + $this->InvoiceModel->update($invoiceId, [ + 'payout_status' => $payoutStatus, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => (int)($input['updated_by'] ?? 0), + ]); + + if ($this->db->transStatus() === false) { + $this->db->transRollback(); + throw new DataException('Transaction failed'); + } + + $this->db->transCommit(); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'history_id' => $historyId, + 'invoice_id' => $invoiceId, + 'paid_amount' => $latestTotalPaid, + 'balance_amount' => $newBalance + ] + ], 200); + } catch (\Exception $e) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> $e->getMessage() + ], 500); + } + } + + public function bulkUploadCommission() + { + $this->db->transBegin(); + try { + $input = $this->request->getJSON(true); + $rows = $input['rows'] ?? []; + $updatedBy = (int)($input['updated_by'] ?? 0); + + if (empty($rows) || !is_array($rows)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'rows is required', + ], 400); + } + + $mismatchRows = []; + $validRows = []; + $skippedRows = 0; + + foreach ($rows as $row) { + $policyNo = trim((string)($row['policy_number'] ?? '')); + $invoiceNo = trim((string)($row['invoice_no'] ?? '')); + $commissionAmount = (float)($row['commission_amount'] ?? 0); + $lineNo = (int)($row['line_no'] ?? 0); + + if ($policyNo === '' || $invoiceNo === '') { + $skippedRows++; + continue; + } + + $record = $this->db->table('partner_invoice_items pii') + ->select(' + pii.id as item_id, + pii.invoice_id, + pii.policy_id, + pii.policy_no, + pi.invoice_no, + pi.agent_id as invoice_agent_json, + pe.agent_id as policy_agent_id + ') + ->join('partner_invoice pi', 'pi.id = pii.invoice_id', 'inner') + ->join('partner_policy pp', 'pp.id = pii.policy_id', 'left') + ->join('partner_enquiry pe', 'pe.id = pp.enquiry_id', 'left') + ->where('pii.is_active', 1) + ->where('pi.is_active', 1) + ->where('pi.invoice_no', $invoiceNo) + ->where('pii.policy_no', $policyNo) + ->get() + ->getRowArray(); + + if (empty($record)) { + $skippedRows++; + continue; + } + + $invoiceAgentIds = $this->extractAgentIdsFromInvoice($record['invoice_agent_json'] ?? ''); + $policyAgentId = isset($record['policy_agent_id']) ? (int)$record['policy_agent_id'] : 0; + $targetAgentId = !empty($invoiceAgentIds) ? (int)$invoiceAgentIds[0] : 0; + $isMismatch = $policyAgentId > 0 && !in_array($policyAgentId, $invoiceAgentIds, true); + + if ($isMismatch) { + $mismatchRows[] = [ + 'line_no' => $lineNo, + 'item_id' => (int)$record['item_id'], + 'invoice_id' => (int)$record['invoice_id'], + 'policy_id' => (int)$record['policy_id'], + 'policy_number' => $policyNo, + 'invoice_no' => $invoiceNo, + 'commission_amount' => $commissionAmount, + 'policy_agent_id' => $policyAgentId, + 'target_agent_id' => $targetAgentId, + ]; + continue; + } + + $validRows[] = [ + 'item_id' => (int)$record['item_id'], + 'invoice_id' => (int)$record['invoice_id'], + 'policy_id' => (int)$record['policy_id'], + 'commission_amount' => $commissionAmount, + ]; + } + + if (!empty($mismatchRows)) { + $this->ensureBulkUploadStagingTable(); + $token = $this->generateProceedToken(); + + $this->db->table('invoice_bulk_upload_staging')->insert([ + 'proceed_token' => $token, + 'payload_json' => json_encode([ + 'rows' => $rows, + 'updated_by' => $updatedBy, + ]), + 'mismatch_json' => json_encode($mismatchRows), + 'created_at' => date('Y-m-d H:i:s'), + 'created_by' => $updatedBy, + 'is_active' => 1, + ]); + + $this->db->transRollback(); + return $this->respond([ + 'status' => 'partner_mismatch', + 'code' => 200, + 'message' => 'Partner mismatch detected', + 'mismatch_count' => count($mismatchRows), + 'mismatches' => $mismatchRows, + 'proceed_token' => $token, + ], 200); + } + + $updatedCount = $this->applyBulkCommissionRows($validRows, $updatedBy); + + if ($this->db->transStatus() === false) { + $this->db->transRollback(); + throw new DataException('Transaction failed'); + } + + $this->db->transCommit(); + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'message' => 'Bulk upload completed', + 'updated_count' => $updatedCount, + 'skipped_count' => $skippedRows, + ], 200); + } catch (\Exception $e) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message' => $e->getMessage(), + ], 500); + } + } + + public function bulkUploadCommissionProceed() + { + $this->db->transBegin(); + try { + $input = $this->request->getJSON(true); + $proceedToken = trim((string)($input['proceed_token'] ?? '')); + $forceReassign = (int)($input['force_reassign_partner'] ?? 0); + $updatedBy = (int)($input['updated_by'] ?? 0); + + if ($proceedToken === '' || $forceReassign !== 1) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'proceed_token and force_reassign_partner=1 are required', + ], 400); + } + + $this->ensureBulkUploadStagingTable(); + $staging = $this->db->table('invoice_bulk_upload_staging') + ->where('proceed_token', $proceedToken) + ->where('is_active', 1) + ->get() + ->getRowArray(); + + if (empty($staging)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message' => 'Invalid or expired proceed token', + ], 404); + } + + $payload = json_decode($staging['payload_json'] ?? '{}', true); + $rows = $payload['rows'] ?? []; + $mismatchRows = json_decode($staging['mismatch_json'] ?? '[]', true); + + foreach ($mismatchRows as $mismatch) { + $policyId = (int)($mismatch['policy_id'] ?? 0); + $targetAgentId = (int)($mismatch['target_agent_id'] ?? 0); + if ($policyId <= 0 || $targetAgentId <= 0) { + continue; + } + + $policy = $this->PolicyModel->select('enquiry_id')->where('id', $policyId)->first(); + if (!empty($policy['enquiry_id'])) { + $this->EnquiryModel->update((int)$policy['enquiry_id'], [ + 'agent_id' => $targetAgentId, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => $updatedBy, + ]); + } + } + + $finalRows = []; + foreach ($rows as $row) { + $policyNo = trim((string)($row['policy_number'] ?? '')); + $invoiceNo = trim((string)($row['invoice_no'] ?? '')); + $commissionAmount = (float)($row['commission_amount'] ?? 0); + if ($policyNo === '' || $invoiceNo === '') { + continue; + } + + $record = $this->db->table('partner_invoice_items pii') + ->select('pii.id as item_id, pii.invoice_id, pii.policy_id') + ->join('partner_invoice pi', 'pi.id = pii.invoice_id', 'inner') + ->where('pii.is_active', 1) + ->where('pi.is_active', 1) + ->where('pi.invoice_no', $invoiceNo) + ->where('pii.policy_no', $policyNo) + ->get() + ->getRowArray(); + + if (empty($record)) { + continue; + } + + $finalRows[] = [ + 'item_id' => (int)$record['item_id'], + 'invoice_id' => (int)$record['invoice_id'], + 'policy_id' => (int)$record['policy_id'], + 'commission_amount' => $commissionAmount, + ]; + } + + $updatedCount = $this->applyBulkCommissionRows($finalRows, $updatedBy); + + $this->db->table('invoice_bulk_upload_staging') + ->where('id', (int)$staging['id']) + ->update([ + 'is_active' => 0, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => $updatedBy, + ]); + + if ($this->db->transStatus() === false) { + $this->db->transRollback(); + throw new DataException('Transaction failed'); + } + + $this->db->transCommit(); + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'message' => 'Bulk upload completed with partner reassignment', + 'updated_count' => $updatedCount, + ], 200); + } catch (\Exception $e) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message' => $e->getMessage(), + ], 500); + } + } + + private function applyBulkCommissionRows(array $rows, int $updatedBy): int + { + $updatedCount = 0; + foreach ($rows as $row) { + $itemId = (int)($row['item_id'] ?? 0); + $invoiceId = (int)($row['invoice_id'] ?? 0); + $policyId = (int)($row['policy_id'] ?? 0); + $commissionAmount = (float)($row['commission_amount'] ?? 0); + if ($itemId <= 0) { + continue; + } + + $affected = $this->db->table('partner_invoice_items') + ->where('id', $itemId) + ->where('invoice_id', $invoiceId) + ->where('policy_id', $policyId) + ->where('is_active', 1) + ->update([ + 'commission_amount' => $commissionAmount, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => $updatedBy, + ]); + + if ($affected) { + $updatedCount++; + } + } + + return $updatedCount; + } + + private function extractAgentIdsFromInvoice($agentJson): array + { + if (is_array($agentJson)) { + return array_values(array_map('intval', $agentJson)); + } + if ($agentJson === null || $agentJson === '') { + return []; + } + + $decoded = json_decode((string)$agentJson, true); + if (is_array($decoded)) { + return array_values(array_map('intval', $decoded)); + } + + if (is_numeric($agentJson)) { + return [(int)$agentJson]; + } + + return []; + } + + private function generateProceedToken(): string + { + return bin2hex(random_bytes(16)); + } + + private function ensureBulkUploadStagingTable(): void + { + $sql = "CREATE TABLE IF NOT EXISTS invoice_bulk_upload_staging ( + id INT AUTO_INCREMENT PRIMARY KEY, + proceed_token VARCHAR(64) NOT NULL UNIQUE, + payload_json LONGTEXT NULL, + mismatch_json LONGTEXT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NULL, + created_by INT NULL, + updated_at DATETIME NULL, + updated_by INT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"; + $this->db->query($sql); + } + // public function getCommissionRateList() // { diff --git a/app/Models/AgentIncentiveFileModel.php b/app/Models/AgentIncentiveFileModel.php index 735cf33..0d56149 100644 --- a/app/Models/AgentIncentiveFileModel.php +++ b/app/Models/AgentIncentiveFileModel.php @@ -15,6 +15,7 @@ class AgentIncentiveFileModel extends Model 'agent_id', 'incentive_month', 'incentive_file_name', + 'file_type', 'is_active', 'created_by', 'created_on', @@ -34,6 +35,7 @@ class AgentIncentiveFileModel extends Model protected $validationRules = [ 'agent_id' => 'required|integer', 'incentive_month' => 'required|valid_date', - 'incentive_file_name'=> 'required|string|max_length[150]' + 'incentive_file_name'=> 'required|string|max_length[150]', + 'file_type' => 'permit_empty|max_length[50]' ]; } diff --git a/app/Models/PartnerAccountHistoryModel.php b/app/Models/PartnerAccountHistoryModel.php new file mode 100644 index 0000000..4d55bf4 --- /dev/null +++ b/app/Models/PartnerAccountHistoryModel.php @@ -0,0 +1,39 @@ + 'required|integer', + 'paid_amount' => 'required|decimal', + 'paid_date' => 'required|valid_date', + ]; + + protected $validationMessages = []; + protected $skipValidation = false; +} diff --git a/app/Models/PartnerGridDetailsModel.php b/app/Models/PartnerGridDetailsModel.php new file mode 100644 index 0000000..873f2fb --- /dev/null +++ b/app/Models/PartnerGridDetailsModel.php @@ -0,0 +1,187 @@ + 'permit_empty|max_length[50]', + 'insurer_id' => 'required|max_length[50]', + 'rto_id' => 'required|max_length[50]', + 'segment_id' => 'required|max_length[100]', + 'comp' => 'permit_empty|max_length[50]', + 'tp' => 'permit_empty|max_length[50]', + 'remarks' => 'permit_empty|max_length[255]', + 'partner_id' => 'required|max_length[50]', + 'fuel' => 'permit_empty|max_length[50]', + 'partner_comp' => 'permit_empty|max_length[20]', + 'partner_tp' => 'permit_empty|max_length[20]', + 'created_by' => 'permit_empty|integer', + 'updated_by' => 'permit_empty|integer', + ]; + + protected $validationMessages = [ + 'insurer_id' => [ + 'required' => 'Insurer is required.', + 'max_length' => 'Insurer name must not exceed 50 characters.', + ], + 'rto_id' => [ + 'required' => 'RTO is required.', + 'max_length' => 'RTO code must not exceed 50 characters.', + ], + 'segment_id' => [ + 'required' => 'Segment is required.', + 'max_length' => 'Segment must not exceed 100 characters.', + ], + 'partner_id' => [ + 'required' => 'Partner is required.', + 'max_length' => 'Partner ID must not exceed 50 characters.', + ], + ]; + + protected $skipValidation = false; + + // ───────────────────────────────────────── + // Custom Methods + // ───────────────────────────────────────── + + /** + * Get all active partner grid records + */ + public function getAllRecords() + { + return $this->orderBy('id', 'ASC')->findAll(); + } + + /** + * Get records by Insurer ID + */ + public function getByInsurer(string $insurerId) + { + return $this->where('insurer_id', $insurerId)->findAll(); + } + + /** + * Get records by RTO ID + */ + public function getByRTO(string $rtoId) + { + return $this->where('rto_id', $rtoId)->findAll(); + } + + /** + * Get records by Segment ID + */ + public function getBySegment(string $segmentId) + { + return $this->where('segment_id', $segmentId)->findAll(); + } + + /** + * Get records by Partner ID + */ + public function getByPartner(string $partnerId) + { + return $this->where('partner_id', $partnerId)->findAll(); + } + + /** + * Get records by Insurer and RTO + */ + public function getByInsurerAndRTO(string $insurerId, string $rtoId) + { + return $this->where('insurer_id', $insurerId) + ->where('rto_id', $rtoId) + ->findAll(); + } + + /** + * Search with multiple filters + */ + public function search(array $filters = []) + { + $builder = $this->builder(); + + if (!empty($filters['insurer_id'])) { + $builder->where('insurer_id', $filters['insurer_id']); + } + if (!empty($filters['rto_id'])) { + $builder->where('rto_id', $filters['rto_id']); + } + if (!empty($filters['segment_id'])) { + $builder->where('segment_id', $filters['segment_id']); + } + if (!empty($filters['partner_id'])) { + $builder->where('partner_id', $filters['partner_id']); + } + + return $builder->get()->getResultArray(); + } + + /** + * Insert with created_by + */ + public function insertRecord(array $data, int $userId) + { + $data['created_by'] = $userId; + $data['updated_by'] = $userId; + return $this->insert($data); + } + + /** + * Update with updated_by + */ + public function updateRecord(int $id, array $data, int $userId) + { + $data['updated_by'] = $userId; + return $this->update($id, $data); + } + + /** + * Delete a record by ID + */ + public function deleteRecord(int $id) + { + return $this->delete($id); + } +} \ No newline at end of file diff --git a/writable/uploads/sample_partner_grid_file.xls b/writable/uploads/sample_partner_grid_file.xls new file mode 100644 index 0000000000000000000000000000000000000000..bdadf0534c3caf622137e3b0033b039fb861073a GIT binary patch literal 7680 zcmeHMYiwLc6`s4#yG@#GoQKn<4HLI@?0DDs5vK_y&2H8=%_i&J)%qH5p$HsjlZHBR z)451ML;35I(O!rd3k_!y1ARZmyWVFZ&H1#!;fARCkb-2qw!VwpR=_+!+?+p6AR1S)D8zgeY~ zqfQ~7QD4CfFF$O8#RQvQPR*ddSWfwymAQ@YkHF2wiuu9ur~jrO4})&D{^|R<{?~xk zg0Kasb)YuTde8>YM$jfuJ7_cLF3{beEuecq_ky;9J^|98ZJ-X&eW3e64}dyB+d*BR zZqN=;4=4pngL*-q1oeUXK?9&c&`!`0Xcy=~&~DHk5c~U(7c>1Z()&C<>&5;E{(q$s zHfAvvRTu7joi1N||GTh=pGGc;UHlQU$JA-sMs8wiQvbWce$ipVqHSJ;k?uvxQZGQd zM*T#jugZ6?SRV>yl;(W%MDo6s5#67YZ-;!>;A^R$OP$||q#t(q8p=1m_HDFJQPj%9 z|D8}1Y6^DMl8XYjF1J_J1@~2U&a%{6%LVF5Ra0k_Rz<{5BBp0P30+P>7gNJlE3YAa zTL#9~$ZB<-yZ$4!Q?;3ismrpO&#S*TG?Z+Hi<)u{m&o5=o}ArWto%}Pwz^pP|6Tcc zjHzb#ywDuy9y1O!7YR*y_Y(5d67n5O$Pv-j@zMfEyFJHV4CiJT7v0X|n*4 zWArp~v=fj&AA%>DQw_$+xv<3R=3)}J%|#?$KNpvH!(6oSMOm@WV$~n;{L|>;wc2d| zx8RX1{4QBGNGbO@OZ`cGmitn@BD%%uqCVhcnL$RoZPK7xRp-9-OPq3T^|Xa^u6+@H zL)jU%^$L60xEErB0(9vN|p zE(+vIBV{1FWuy;`IR#`^oRMth@F>pG?s!R$A9S38Q^9F_teAJ5VHB<$!XCOmf7sDo zx}@Eb9vte^xt#8{l`1-}?(f}G#ZFdq#*hw={JM~CeXA1?50cfI2g>k66yNbgA)om*1>&-FDx>=4{u2gcJiifhZcV`vj z&q`i~W;c;raSGXdG1pXQsJBma#kdUi4plJ)rNS^;Jve+IH{aF}6*!d5m&YplP-*xe zV{eFX)ye1Z!}UgD9cXZ8??APIi_iUP7mD%VGih%GA>jocbQnUYuFNi)Y}rLu!|sm4 z9=Y$V6B|sz(mwpbmEVjUDewD&$misaCGDsOxH4d%Pb242WQWw~k?%Pvy8+5;>sRZ2K7^8Y1oFc;lwJwsN3i7Y+3c5(;_C6+Ej}N^;pLreJ|D++_T9YCWAdzT zt}?s|@epgSwpiat*&!}^rc?A0D7#iT9?dIBIIR zI5_G^j%{y(7ER8=@t=B`ybw4p62pbT@iD=05pY~xzmQg<*l{)k!{o)0$Lj_;>gywL zN%;u08y_Kr-n7dV#ndAL=bQ*3^u|XBL4O~CW8fo%1%Vd9R>Ca^w99t6_?SE*a4R7~ zxCJ3B2)r7xB*)c9;P&PBA<}{nX+fafx65&i2$2?qh#=4x5rK2%BSZy(mobwU9Hpor z@RDZoh`{ZV2wYeGC`AQ<*Ep8s{QC$oLE!byR#*l_CX8P_M8fjnIALvpQ()VoBUs2i`x0)BuWmU^s1n|e6Zre5LHs|-Il zW-+{o>;!stFWXJBEP#^uMIy&>YC2A2FJQs)T1}3gAP_9v&cl!~J85&7D|J!6>Hy7U zJ?NkM;K##|MHk2(gN%-Yjgx3jtV>;&k|XA-`7WoOPooguY8j$xRGq~;6JMJ6_MgXl(=nM7 zN@npcH4Vwqq07VPHnH6P??u@kc}+&uFc2n1hnoBYu`+#je}w*LfIaKs-->9Z;~#u~ I|4-xp3+2;~M*si- literal 0 HcmV?d00001 From 48a76a54ea5d089bded47a83e6a6efe8537b5126 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Wed, 25 Mar 2026 09:53:38 +0530 Subject: [PATCH 08/18] FIX_Changes and Additional Requirements 2 --- app/Controllers/AgentController.php | 44 +++++++++++++++----------- app/Models/PartnerGridDetailsModel.php | 31 +++++++++++++----- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/app/Controllers/AgentController.php b/app/Controllers/AgentController.php index e71be0d..4d99710 100644 --- a/app/Controllers/AgentController.php +++ b/app/Controllers/AgentController.php @@ -461,7 +461,7 @@ class AgentController extends ResourceController } elseif ($normalizedHeader === 'rto') { $headerMap['rto_id'] = $colIndex; } elseif ($normalizedHeader === 'segment') { - $headerMap['segment_id'] = $colIndex; + $headerMap['segment'] = $colIndex; } elseif ($normalizedHeader === 'comp') { $headerMap['comp'] = $colIndex; } elseif ($normalizedHeader === 'tp') { @@ -473,7 +473,7 @@ class AgentController extends ResourceController } } - if (!isset($headerMap['insurer_id'], $headerMap['rto_id'], $headerMap['segment_id'], $headerMap['vehicle_type_id'])) { + if (!isset($headerMap['insurer_id'], $headerMap['rto_id'], $headerMap['segment'], $headerMap['vehicle_type_id'])) { throw new \RuntimeException('Invalid grid header. Required: TYPE, INSURER, RTO, SEGMENT'); } @@ -544,7 +544,7 @@ class AgentController extends ResourceController $insurerRaw = trim((string) ($row[$headerMap['insurer_id']] ?? '')); $rtoRaw = trim((string) ($row[$headerMap['rto_id']] ?? '')); - $segmentRaw = trim((string) ($row[$headerMap['segment_id']] ?? '')); + $segmentRaw = trim((string) ($row[$headerMap['segment']] ?? '')); $vehicleTypeRaw = trim((string) ($row[$headerMap['vehicle_type_id']] ?? $segmentRaw)); if ($insurerRaw === '' || $rtoRaw === '' || $segmentRaw === '' || $vehicleTypeRaw === '') { @@ -629,7 +629,7 @@ class AgentController extends ResourceController 'vehicle_type_id' => $vehicleTypeRaw, 'insurer_id' => (string) $insurerId, 'rto_id' => (string) $rtoId, - 'segment_id' => $segmentRaw, + 'segment' => $segmentRaw, 'comp' => $compSanitized, 'tp' => $tpSanitized, 'fuel' => $fuel, @@ -643,7 +643,7 @@ class AgentController extends ResourceController ->where('vehicle_type_id', $vehicleTypeRaw) ->where('insurer_id', (string) $insurerId) ->where('rto_id', (string) $rtoId) - ->where('segment_id', $segmentRaw) + ->where('segment', $segmentRaw) ->where('partner_id', (string) $partnerId) ->first(); @@ -792,7 +792,7 @@ class AgentController extends ResourceController ->get() ->getResultArray(); - $segmentMaster = $db->table('vehicle_type') + $vehicleTypeMaster = $db->table('vehicle_type') ->select('id, vehicle_type') ->where('is_active', 1) ->orderBy('vehicle_type', 'ASC') @@ -832,7 +832,7 @@ class AgentController extends ResourceController 'code' => 200, 'data' => [ 'rto_master' => $rtoMaster, - 'segment_master' => $segmentMaster, + 'vehicle_type_master' => $vehicleTypeMaster, 'plan_master' => $planMaster, 'partner_master' => $partnerMaster, 'months' => $months, @@ -855,15 +855,17 @@ class AgentController extends ResourceController $partnerId = (int) ($this->request->getGet('partner_id') ?? 0); $rtoId = (int) ($this->request->getGet('rto_id') ?? 0); - $segmentId = (int) ($this->request->getGet('segment_id') ?? 0); + $vehicleTypeId = (int) ($this->request->getGet('vehicle_type_id') ?? 0); $planType = trim((string) ($this->request->getGet('insurance_plan_type') ?? '')); $month = trim((string) ($this->request->getGet('month') ?? '')); + $segment = trim((string) ($this->request->getGet('segment') ?? '')); - if ($partnerId <= 0 || $rtoId <= 0 || $segmentId <= 0 || $planType === '') { + + if ($partnerId <= 0 || $rtoId <= 0 || $vehicleTypeId <= 0 || $planType === '' || $segment === '') { return $this->respond([ 'status' => 'failed', 'code' => 200, - 'data' => 'partner_id, rto_id, segment_id and insurance_plan_type are required', + 'data' => 'partner_id, rto_id, vehicle_type_id, segment and insurance_plan_type are required', ], 200); } @@ -903,7 +905,8 @@ class AgentController extends ResourceController ->join('vehicle_type vt', 'vt.id = pgd.vehicle_type_id', 'left') ->where('pgd.partner_id', $partnerId) ->where('pgd.rto_id', $rtoId) - ->where('pgd.segment_id', $segmentId) + ->where('pgd.segment', $segment) + ->where('pgd.vehicle_type_id', $vehicleTypeId) ->where("DATE_FORMAT(pgd.created_at, '%Y-%m') =", $effectiveMonth) ->orderBy('pgd.id', 'DESC') ->get() @@ -970,15 +973,16 @@ class AgentController extends ResourceController $partnerId = (int) ($this->request->getGet('partner_id') ?? 0); $rtoId = (int) ($this->request->getGet('rto_id') ?? 0); - $segmentId = (int) ($this->request->getGet('segment_id') ?? 0); + $segment = (int) ($this->request->getGet('segment') ?? 0); + $vehicleTypeId = (int) ($this->request->getGet('vehicle_type_id') ?? 0); $planType = trim((string) ($this->request->getGet('insurance_plan_type') ?? '')); $month = trim((string) ($this->request->getGet('month') ?? '')); - if ($partnerId <= 0 || $rtoId <= 0 || $segmentId <= 0 || $planType === '') { + if ($partnerId <= 0 || $rtoId <= 0 || $vehicleTypeId <= 0 || $planType === '' || $segment === '') { return $this->respond([ 'status' => 'failed', 'code' => 200, - 'data' => 'partner_id, rto_id, segment_id and insurance_plan_type are required', + 'data' => 'partner_id, rto_id, vehicle_type_id, segment and insurance_plan_type are required', ], 200); } @@ -999,16 +1003,18 @@ class AgentController extends ResourceController COALESCE(pa.retention_rate, 0) AS retention_rate, rm.rto_code, rm.rto_name, - vt.vehicle_type AS segment_name, + vt.vehicle_type AS vehicle_type_name, + pdg.segment, pgd.comp, pgd.tp ") ->join('partner_agent pa', 'pa.id = pgd.partner_id', 'left') ->join('rto_master rm', 'rm.id = pgd.rto_id', 'left') - ->join('vehicle_type vt', 'vt.id = pgd.segment_id', 'left') + ->join('vehicle_type vt', 'vt.id = pgd.vehicle_type_id', 'left') ->where('pgd.partner_id', $partnerId) ->where('pgd.rto_id', $rtoId) - ->where('pgd.segment_id', $segmentId) + ->where('pgd.segment', $segment) + ->where('pgd.vehicle_type_id', $vehicleTypeId) ->where("DATE_FORMAT(pgd.created_at, '%Y-%m') =", $effectiveMonth) ->orderBy('pgd.id', 'DESC') ->get() @@ -1063,6 +1069,7 @@ class AgentController extends ResourceController 'Partner', 'Partner Code', 'RTO', + 'Type', 'Segment', 'Insurance Plan', 'Grid %', @@ -1076,7 +1083,8 @@ class AgentController extends ResourceController $row['partner_name'] ?? '', $row['agent_code'] ?? '', trim((string) (($row['rto_code'] ?? '') . ' - ' . ($row['rto_name'] ?? '')), ' -'), - $row['segment_name'] ?? '', + $row['vehicle_type_name'] ?? '', + $row['segment'] ?? '', $row['insurance_plan_type'] ?? $planType, $row['grid_rate_percentage'] ?? 0, $row['retention_rate_percentage'] ?? 0, diff --git a/app/Models/PartnerGridDetailsModel.php b/app/Models/PartnerGridDetailsModel.php index 873f2fb..9f6086b 100644 --- a/app/Models/PartnerGridDetailsModel.php +++ b/app/Models/PartnerGridDetailsModel.php @@ -22,7 +22,7 @@ class PartnerGridDetailsModel extends Model 'vehicle_type_id', 'insurer_id', 'rto_id', - 'segment_id', + 'segment', 'comp', 'tp', 'fuel', @@ -48,7 +48,7 @@ class PartnerGridDetailsModel extends Model 'vehicle_type_id' => 'permit_empty|max_length[50]', 'insurer_id' => 'required|max_length[50]', 'rto_id' => 'required|max_length[50]', - 'segment_id' => 'required|max_length[100]', + 'segment' => 'required|max_length[100]', 'comp' => 'permit_empty|max_length[50]', 'tp' => 'permit_empty|max_length[50]', 'remarks' => 'permit_empty|max_length[255]', @@ -69,7 +69,11 @@ class PartnerGridDetailsModel extends Model 'required' => 'RTO is required.', 'max_length' => 'RTO code must not exceed 50 characters.', ], - 'segment_id' => [ + 'vehicle_type_id' => [ + 'required' => 'Type is required.', + 'max_length' => 'Type must not exceed 50 characters.', + ], + 'segment' => [ 'required' => 'Segment is required.', 'max_length' => 'Segment must not exceed 100 characters.', ], @@ -110,11 +114,19 @@ class PartnerGridDetailsModel extends Model } /** - * Get records by Segment ID + * Get records by Segment */ - public function getBySegment(string $segmentId) + public function getBySegment(string $segment) { - return $this->where('segment_id', $segmentId)->findAll(); + return $this->where('segment', $segment)->findAll(); + } + + /** + * Get records by Vehicle Type ID + */ + public function getByType(string $vehicleTypeId) + { + return $this->where('vehicle_type_id', $vehicleTypeId)->findAll(); } /** @@ -148,8 +160,11 @@ class PartnerGridDetailsModel extends Model if (!empty($filters['rto_id'])) { $builder->where('rto_id', $filters['rto_id']); } - if (!empty($filters['segment_id'])) { - $builder->where('segment_id', $filters['segment_id']); + if (!empty($filters['vehicle_type_id'])) { + $builder->where('vehicle_type_id', $filters['vehicle_type_id']); + } + if (!empty($filters['segment'])) { + $builder->where('segment', $filters['segment']); } if (!empty($filters['partner_id'])) { $builder->where('partner_id', $filters['partner_id']); From 0fb92252dcb3b4c8238a08464ebe7f5eca4070c5 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Wed, 25 Mar 2026 15:41:05 +0530 Subject: [PATCH 09/18] FIX_Changes and Additional Requirements ( GRID XL Backend ) --- app/Config/Routes.php | 22 +- app/Controllers/AgentController.php | 742 +---------------- app/Controllers/AgentIncentiveController.php | 757 ++++++++++++++++++ app/Models/PartnerGridDetailsModel.php | 202 ----- .../PartnerInsurancePayoutGridModel.php | 129 +++ 5 files changed, 906 insertions(+), 946 deletions(-) create mode 100644 app/Controllers/AgentIncentiveController.php delete mode 100644 app/Models/PartnerGridDetailsModel.php create mode 100644 app/Models/PartnerInsurancePayoutGridModel.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index c1aed35..b860eea 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -73,10 +73,12 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->post('agent/uploadAgentIncentiveFile', 'AgentController::uploadAgentIncentiveFile'); $routes->get('agent/deleteAgentIncentiveFile', 'AgentController::deleteAgentIncentiveFile'); $routes->get('agent/downloadAgentIncentiveFile', 'AgentController::downloadAgentIncentiveFile'); - $routes->get('agent/downloadSamplePartnerGridExcel', 'AgentController::downloadSamplePartnerGridExcel'); - $routes->get('agent/monthlyCommissionGridFilters', 'AgentController::monthlyCommissionGridFilters'); - $routes->get('agent/monthlyCommissionGridList', 'AgentController::monthlyCommissionGridList'); - $routes->get('agent/downloadMonthlyCommissionGrid', 'AgentController::downloadMonthlyCommissionGrid'); + + $routes->post('agent/uploadGrid', 'AgentIncentiveController::uploadPayoutGridFile'); + $routes->get('agent/payoutGrid', 'AgentIncentiveController::getPayoutGrid'); + $routes->get('agent/downloadGrid', 'AgentIncentiveController::downloadGridInExcel'); + $routes->get('agent/loadGrid', 'AgentIncentiveController::loadGrid'); + $routes->get('agent/downloadExistGrid', 'AgentIncentiveController::downloadLastestGrid'); //Staff @@ -714,15 +716,3 @@ $routes->get("processjob", "JobWorker::processJob"); - - - - - - - - - - - - diff --git a/app/Controllers/AgentController.php b/app/Controllers/AgentController.php index 4d99710..5ad4302 100644 --- a/app/Controllers/AgentController.php +++ b/app/Controllers/AgentController.php @@ -5,41 +5,16 @@ use CodeIgniter\RESTful\ResourceController; use App\Controllers\BaseController; use App\Models\AgentModel; use App\Models\AgentIncentiveFileModel; -use App\Models\PartnerGridDetailsModel; -use PhpOffice\PhpSpreadsheet\IOFactory; class AgentController extends ResourceController { protected $AgentModel; protected $AgentIncentiveFileModel; - protected $PartnerGridDetailsModel; public function __construct() { - helper('jwt_helper'); $this->AgentModel = new AgentModel(); $this->AgentIncentiveFileModel = new AgentIncentiveFileModel(); - $this->PartnerGridDetailsModel = new PartnerGridDetailsModel(); - } - - private function getAuthenticatedUserData(): ?object - { - $header = $this->request->getHeaderLine('Authorization'); - if (!$header || !preg_match('/Bearer\s(\S+)/', $header, $matches)) { - return null; - } - - $decodedToken = validateJWT($matches[1]); - if (!$decodedToken || !isset($decodedToken['data'])) { - return null; - } - - return $decodedToken['data']; - } - - private function canManagePartnerGrid(?string $role): bool - { - return in_array((string) $role, ['1', '4'], true); } // List of all agents @@ -292,30 +267,12 @@ class AgentController extends ResourceController public function agentIncentiveFileList() { try{ - $authUser = $this->getAuthenticatedUserData(); - if (!$authUser) { - return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); - } - $roleId = (string) ($authUser->role_id ?? 'agent'); - $agentIdFromRequest = $this->request->getGet('agent_id'); - $fileType = trim((string) ($this->request->getGet('file_type') ?? '')); + $agent_id = $this->request->getGet('agent_id'); + $type = $this->request->getGet('type'); - // Partner can only view own files; manager/accounts can view all. - $agentId = $this->canManagePartnerGrid($roleId) ? $agentIdFromRequest : ($authUser->id ?? null); - if (empty($agentId)) { - return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'agent_id is required'], 200); - } - - $builder = $this->AgentIncentiveFileModel - ->where('agent_id', (int) $agentId) - ->where('is_active', 1); - - if ($fileType !== '') { - $builder->where('file_type', $fileType); - } - - $data = $builder->orderBy('id', 'DESC')->findAll(); + $fileType = (!empty($type) && $type === 'grid') ? 'grid' : 'incentive'; + $data = $this->AgentIncentiveFileModel->where('agent_id',$agent_id)->where('is_active',1)->where('file_type', $fileType)->findAll(); return $this->respond(['status' => 'success', 'code' => 200, 'data' => $data], 200); @@ -330,39 +287,13 @@ class AgentController extends ResourceController { try{ $data = $this->request->getPost(); - $authUser = $this->getAuthenticatedUserData(); - if (!$authUser) { - return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); + + //duplicate check + $duplicateData = $this->AgentIncentiveFileModel->where('agent_id',$data['agent_id'])->where('incentive_month',$data['incentive_month'])->where('file_type','incentive')->first(); + if(!empty($duplicateData)){ + return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'Duplicate Entry.'], 200); } - $roleId = (string) ($authUser->role_id ?? 'agent'); - $isGridUpload = isset($data['file_type']) && $data['file_type'] === 'grid'; - - if ($isGridUpload && !$this->canManagePartnerGrid($roleId)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 403, - 'data' => 'Only Manager and Accounts can upload/edit partner grid files' - ], 403); - } - - if ($isGridUpload) { - $this->uploadGridFile($data); - } else { - - $duplicateData = $this->AgentIncentiveFileModel - ->where('agent_id', $data['agent_id']) - ->where('incentive_month', $data['incentive_month']) - ->first(); - - if (!empty($duplicateData)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 200, - 'data' => 'Duplicate Entry.' - ], 200); - } - } // handle file uploads $incentiveFile = $this->request->getFile('incentive_file_name'); @@ -382,7 +313,7 @@ class AgentController extends ResourceController 'agent_id' => $data['agent_id'], 'incentive_month' => $data['incentive_month'], 'incentive_file_name' => $incentiveFileName, - 'file_type' => $data['file_type'] ?? 'incentive', + 'file_type' => 'incentive', 'created_by' => $data['created_by'] ?? null ]; @@ -395,310 +326,20 @@ class AgentController extends ResourceController } } - // ───────────────────────────────────────── - // Grid Functionality - // ───────────────────────────────────────── - public function uploadGridFile($data){ - $gridFile = $this->request->getFile('incentive_file_name'); - - if (!$gridFile || !$gridFile->isValid()) { - throw new \RuntimeException('Valid grid file is required'); - } - - $extension = strtolower((string) $gridFile->getExtension()); - if (!in_array($extension, ['xlsx', 'xls', 'csv'], true)) { - throw new \RuntimeException('Only xlsx, xls, csv grid files are allowed'); - } - - $spreadsheet = IOFactory::load($gridFile->getTempName()); - $rows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); - - if (empty($rows)) { - throw new \RuntimeException('Grid file is empty'); - } - - $normalize = static function ($value): string { - $value = strtolower(trim((string) $value)); - return preg_replace('/[^a-z0-9]+/', '', $value) ?? ''; - }; - $toNumber = static function ($value): float { - if (is_numeric($value)) { - return (float) $value; - } - if (preg_match('/-?\d+(\.\d+)?/', (string) $value, $matches)) { - return (float) $matches[0]; - } - return 0.0; - }; - $toNullable = static function ($value): ?string { - $value = trim((string) $value); - return $value === '' ? null : $value; - }; - $formatNumber = static function (float $value): string { - return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.'); - }; - $parsePremium = static function ($value) use ($formatNumber): ?string { - $source = trim((string) $value); - if ($source === '') { - return null; - } - if (preg_match('/-?\d+(?:\.\d+)?/', $source, $matches) !== 1) { - return null; - } - return $formatNumber((float) $matches[0]); - }; - - // Fixed format: first row is header and data starts from row 2. - $headerRowIndex = 0; - $headerMap = []; - $headerRow = $rows[$headerRowIndex] ?? []; - foreach ($headerRow as $colIndex => $cell) { - $normalizedHeader = $normalize($cell); - if ($normalizedHeader === 'type') { - $headerMap['vehicle_type_id'] = $colIndex; - } elseif ($normalizedHeader === 'insurer') { - $headerMap['insurer_id'] = $colIndex; - } elseif ($normalizedHeader === 'rto') { - $headerMap['rto_id'] = $colIndex; - } elseif ($normalizedHeader === 'segment') { - $headerMap['segment'] = $colIndex; - } elseif ($normalizedHeader === 'comp') { - $headerMap['comp'] = $colIndex; - } elseif ($normalizedHeader === 'tp') { - $headerMap['tp'] = $colIndex; - } elseif ($normalizedHeader === 'fuel') { - $headerMap['fuel'] = $colIndex; - } elseif ($normalizedHeader === 'remarks') { - $headerMap['remarks'] = $colIndex; - } - } - - if (!isset($headerMap['insurer_id'], $headerMap['rto_id'], $headerMap['segment'], $headerMap['vehicle_type_id'])) { - throw new \RuntimeException('Invalid grid header. Required: TYPE, INSURER, RTO, SEGMENT'); - } - - $defaultPartnerId = !empty($data['agent_id']) ? (int) $data['agent_id'] : null; - $createdBy = $data['created_by'] ?? null; - - $db = \Config\Database::connect(); - - $vehicleTypeMaster = $db->table('vehicle_type')->select('id, vehicle_type')->where('is_active', 1)->get()->getResultArray(); - - $insurerMaster = $db->table('insurers')->select('id, name, short_name')->where('is_active', 1)->get()->getResultArray(); - - $rtoMaster = $db->table('rto_master')->select('id, rto_code, rto_name')->where('is_active', 1)->get()->getResultArray(); - - $partnerMaster = $db->table('partner_agent')->select('id, name, agent_code, retention_rate')->where('is_active', 1)->get()->getResultArray(); - - - $insurerMap = []; - - foreach ($insurerMaster as $item) { - $insurerMap[$normalize($item['name'])] = (int) $item['id']; - $insurerMap[$normalize($item['short_name'])] = (int) $item['id']; - $insurerMap[(string) $item['id']] = (int) $item['id']; - } - - $rtoMap = []; - foreach ($rtoMaster as $item) { - $rtoMap[$normalize($item['rto_code'])] = (int) $item['id']; - $rtoMap[$normalize($item['rto_name'])] = (int) $item['id']; - $rtoMap[(string) $item['id']] = (int) $item['id']; - } - - $partnerMap = []; - $partnerRetentionMap = []; - foreach ($partnerMaster as $item) { - $id = (int) $item['id']; - $partnerMap[$normalize($item['name'])] = $id; - $partnerMap[$normalize($item['agent_code'])] = $id; - $partnerMap[(string) $id] = $id; - $partnerRetentionMap[$id] = $toNumber($item['retention_rate'] ?? 0); - } - - $insertCount = 0; - $updateCount = 0; - $skipStats = [ - 'empty_row' => 0, - 'missing_required_columns' => 0, - 'master_mapping_failed' => 0, - 'partner_not_found' => 0, - ]; - $skipSamples = []; - - for ($i = $headerRowIndex + 1, $count = count($rows); $i < $count; $i++) { - $row = $rows[$i]; - $hasAnyData = false; - - foreach ($row as $cellValue) { - if (trim((string) $cellValue) !== '') { - $hasAnyData = true; - break; - } - } - - if (!$hasAnyData) { - $skipStats['empty_row']++; - continue; - } - - $insurerRaw = trim((string) ($row[$headerMap['insurer_id']] ?? '')); - $rtoRaw = trim((string) ($row[$headerMap['rto_id']] ?? '')); - $segmentRaw = trim((string) ($row[$headerMap['segment']] ?? '')); - $vehicleTypeRaw = trim((string) ($row[$headerMap['vehicle_type_id']] ?? $segmentRaw)); - - if ($insurerRaw === '' || $rtoRaw === '' || $segmentRaw === '' || $vehicleTypeRaw === '') { - $skipStats['missing_required_columns']++; - if (count($skipSamples) < 15) { - $skipSamples[] = [ - 'row' => $i + 1, - 'reason' => 'missing_required_columns', - 'insurer' => $insurerRaw, - 'rto' => $rtoRaw, - 'segment' => $segmentRaw, - 'vehicle_type' => $vehicleTypeRaw, - ]; - } - continue; - } - - - $insurerId = $insurerMap[$normalize($insurerRaw)] ?? null; - if ($insurerId === null && ctype_digit($insurerRaw) && isset($insurerMap[$insurerRaw])) { - $insurerId = $insurerMap[$insurerRaw]; - } - - $rtoId = $rtoMap[$normalize($rtoRaw)] ?? null; - if ($rtoId === null && ctype_digit($rtoRaw) && isset($rtoMap[$rtoRaw])) { - $rtoId = $rtoMap[$rtoRaw]; - } - - if (empty($insurerId) || empty($rtoId)) { - $skipStats['master_mapping_failed']++; - if (count($skipSamples) < 15) { - $skipSamples[] = [ - 'row' => $i + 1, - 'reason' => 'master_mapping_failed', - 'insurer' => $insurerRaw, - 'rto' => $rtoRaw, - 'segment' => $segmentRaw, - 'vehicle_type' => $vehicleTypeRaw, - ]; - } - continue; - } - - $comp = trim((string) ($row[$headerMap['comp']] ?? '')); - $tp = trim((string) ($row[$headerMap['tp']] ?? '')); - $remarks = trim((string) ($row[$headerMap['remarks']] ?? '')); - $fuelRaw = trim((string) ($row[$headerMap['fuel']] ?? '')); - $fuel = $toNullable($fuelRaw); - $partnerRaw = trim((string) ($row[$headerMap['partner_id']] ?? '')); - - $partnerId = null; - if ($partnerRaw !== '') { - $partnerId = $partnerMap[$normalize($partnerRaw)] ?? null; - if ($partnerId === null && ctype_digit($partnerRaw) && isset($partnerMap[$partnerRaw])) { - $partnerId = $partnerMap[$partnerRaw]; - } - } - - if (empty($partnerId) && !empty($defaultPartnerId)) { - $partnerId = $defaultPartnerId; - } - - if (empty($partnerId)) { - $skipStats['partner_not_found']++; - if (count($skipSamples) < 15) { - $skipSamples[] = [ - 'row' => $i + 1, - 'reason' => 'partner_not_found', - 'partner' => $partnerRaw, - ]; - } - continue; - } - - $retentionRate = $partnerRetentionMap[(int) $partnerId] ?? 0.0; - $compSanitized = $parsePremium($comp); - $tpSanitized = $parsePremium($tp); - $partnerComp = $compSanitized !== null ? $formatNumber($retentionRate + (float) $compSanitized) : null; - $partnerTp = $tpSanitized !== null ? $formatNumber($retentionRate + (float) $tpSanitized) : null; - - $recordData = [ - 'vehicle_type_id' => $vehicleTypeRaw, - 'insurer_id' => (string) $insurerId, - 'rto_id' => (string) $rtoId, - 'segment' => $segmentRaw, - 'comp' => $compSanitized, - 'tp' => $tpSanitized, - 'fuel' => $fuel, - 'remarks' => $toNullable($remarks), - 'partner_id' => (string) $partnerId, - 'partner_comp' => $partnerComp, - 'partner_tp' => $partnerTp, - ]; - - $existing = $this->PartnerGridDetailsModel - ->where('vehicle_type_id', $vehicleTypeRaw) - ->where('insurer_id', (string) $insurerId) - ->where('rto_id', (string) $rtoId) - ->where('segment', $segmentRaw) - ->where('partner_id', (string) $partnerId) - ->first(); - - if ($existing) { - $recordData['updated_by'] = $createdBy; - $this->PartnerGridDetailsModel->update((int) $existing['id'], $recordData); - $updateCount++; - } else { - $recordData['created_by'] = $createdBy; - $this->PartnerGridDetailsModel->insert($recordData); - $insertCount++; - } - } - - if ($insertCount === 0 && $updateCount === 0) { - $debugData = [ - 'message' => 'No valid rows found in grid file', - 'header_row' => $headerRowIndex + 1, - 'detected_headers' => array_keys($headerMap), - 'skip_stats' => $skipStats, - 'skip_samples' => $skipSamples, - ]; - throw new \RuntimeException(json_encode($debugData, JSON_UNESCAPED_SLASHES)); - } - - return true; - } - // Delete agent incentive file public function deleteAgentIncentiveFile() { try { - $authUser = $this->getAuthenticatedUserData(); - if (!$authUser) { - return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); - } - $roleId = (string) ($authUser->role_id ?? 'agent'); $id = $this->request->getGet('id'); // check if agent exists - $file = $this->AgentIncentiveFileModel->find((int)$id); + $file = $this->AgentIncentiveFileModel->where('file_type','incentive')->find((int)$id); if (!$file) { return $this->respond(['status' => 'failed','code' => 200, 'data' => 'File not found'], 200); } - if (($file['file_type'] ?? '') === 'grid' && !$this->canManagePartnerGrid($roleId)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 403, - 'data' => 'Only Manager and Accounts can edit/delete partner grid files' - ], 403); - } - // update status $this->AgentIncentiveFileModel->update($id, ['is_active' => 0]); @@ -714,28 +355,21 @@ class AgentController extends ResourceController public function downloadAgentIncentiveFile() { try { - $authUser = $this->getAuthenticatedUserData(); - if (!$authUser) { - return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); - } - $roleId = (string) ($authUser->role_id ?? 'agent'); $id = $this->request->getGet('id'); + $type = $this->request->getGet('type'); // 'incentive' or 'grid' if (!$id) { return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'ID is required'], 200); } + $fileType = (!empty($type) && $type === 'grid') ? 'grid' : 'incentive'; // Fetch record from DB - $fileRecord = $this->AgentIncentiveFileModel->where('is_active',1)->find((int)$id); + $fileRecord = $this->AgentIncentiveFileModel->where('is_active',1)->where('file_type',$fileType)->find((int)$id); if (!$fileRecord) { return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File not found'], 200); } - if (!$this->canManagePartnerGrid($roleId) && (int) $fileRecord['agent_id'] !== (int) ($authUser->id ?? 0)) { - return $this->respond(['status' => 'failed', 'code' => 403, 'data' => 'Access denied'], 403); - } - $filePath = WRITEPATH . 'uploads/agent/incentive_file/' . $fileRecord['incentive_file_name']; if (!file_exists($filePath)) { @@ -750,352 +384,4 @@ class AgentController extends ResourceController } } - // Download sample partner grid excel - public function downloadSamplePartnerGridExcel() - { - try { - $filePath = WRITEPATH . 'uploads/sample_partner_grid_file.xlsx'; - - if (!file_exists($filePath)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 404, - 'data' => 'Sample partner grid file not found on server', - ], 200); - } - - return $this->response->download($filePath, null); - } catch (\Exception $e) { - return $this->respond([ - 'status' => 'failed', - 'code' => 500, - 'data' => $e->getMessage(), - ], 500); - } - } - - // Monthly commission grid filters - public function monthlyCommissionGridFilters() - { - try { - $authUser = $this->getAuthenticatedUserData(); - if (!$authUser) { - return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); - } - - $db = \Config\Database::connect(); - - $rtoMaster = $db->table('rto_master') - ->select('id, rto_code, rto_name') - ->where('is_active', 1) - ->orderBy('rto_code', 'ASC') - ->get() - ->getResultArray(); - - $vehicleTypeMaster = $db->table('vehicle_type') - ->select('id, vehicle_type') - ->where('is_active', 1) - ->orderBy('vehicle_type', 'ASC') - ->get() - ->getResultArray(); - - $planMaster = $db->table('partner_insurance_plan_type_master') - ->select('id, insurance_plan_type') - ->where('is_active', 1) - ->orderBy('insurance_plan_type', 'ASC') - ->get() - ->getResultArray(); - - $partnerMaster = $db->table('partner_agent') - ->select('id, name, agent_code, retention_rate') - ->where('is_active', 1) - ->orderBy('name', 'ASC') - ->get() - ->getResultArray(); - - $monthRows = $db->table('partner_grid_details') - ->select("DATE_FORMAT(created_at, '%Y-%m') AS month_key") - ->where('created_at IS NOT NULL', null, false) - ->groupBy("DATE_FORMAT(created_at, '%Y-%m')") - ->orderBy('month_key', 'DESC') - ->get() - ->getResultArray(); - - $months = array_values(array_filter(array_map(static function ($row) { - return $row['month_key'] ?? null; - }, $monthRows))); - - $latestMonth = !empty($months) ? $months[0] : date('Y-m'); - - return $this->respond([ - 'status' => 'success', - 'code' => 200, - 'data' => [ - 'rto_master' => $rtoMaster, - 'vehicle_type_master' => $vehicleTypeMaster, - 'plan_master' => $planMaster, - 'partner_master' => $partnerMaster, - 'months' => $months, - 'default_month' => $latestMonth, - ], - ], 200); - } catch (\Exception $e) { - return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); - } - } - - // Monthly commission grid list - public function monthlyCommissionGridList() - { - try { - $authUser = $this->getAuthenticatedUserData(); - if (!$authUser) { - return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); - } - - $partnerId = (int) ($this->request->getGet('partner_id') ?? 0); - $rtoId = (int) ($this->request->getGet('rto_id') ?? 0); - $vehicleTypeId = (int) ($this->request->getGet('vehicle_type_id') ?? 0); - $planType = trim((string) ($this->request->getGet('insurance_plan_type') ?? '')); - $month = trim((string) ($this->request->getGet('month') ?? '')); - $segment = trim((string) ($this->request->getGet('segment') ?? '')); - - - if ($partnerId <= 0 || $rtoId <= 0 || $vehicleTypeId <= 0 || $planType === '' || $segment === '') { - return $this->respond([ - 'status' => 'failed', - 'code' => 200, - 'data' => 'partner_id, rto_id, vehicle_type_id, segment and insurance_plan_type are required', - ], 200); - } - - $normalizedPlan = strtolower($planType); - - $db = \Config\Database::connect(); - $latestRow = $db->table('partner_grid_details') - ->select("DATE_FORMAT(created_at, '%Y-%m') AS month_key") - ->where('created_at IS NOT NULL', null, false) - ->orderBy('created_at', 'DESC') - ->get(1) - ->getRowArray(); - - $effectiveMonth = $month !== '' ? $month : (($latestRow['month_key'] ?? '') ?: date('Y-m')); - - $rows = $db->table('partner_grid_details pgd') - ->select(" - pgd.id, - pgd.partner_id, - pa.name AS partner_name, - pa.agent_code, - COALESCE(pa.retention_rate, 0) AS retention_rate, - pgd.rto_id, - rm.rto_code, - rm.rto_name, - pgd.segment, - pgd.vehicle_type_id, - vt.vehicle_type AS vehicle_type_name, - pgd.comp, - pgd.tp, - pgd.fuel, - pgd.remarks, - DATE_FORMAT(pgd.created_at, '%Y-%m') AS month_key - ") - ->join('partner_agent pa', 'pa.id = pgd.partner_id', 'left') - ->join('rto_master rm', 'rm.id = pgd.rto_id', 'left') - ->join('vehicle_type vt', 'vt.id = pgd.vehicle_type_id', 'left') - ->where('pgd.partner_id', $partnerId) - ->where('pgd.rto_id', $rtoId) - ->where('pgd.segment', $segment) - ->where('pgd.vehicle_type_id', $vehicleTypeId) - ->where("DATE_FORMAT(pgd.created_at, '%Y-%m') =", $effectiveMonth) - ->orderBy('pgd.id', 'DESC') - ->get() - ->getResultArray(); - - $toPercent = static function ($value): float { - if ($value === null) { - return 0.0; - } - if (is_numeric($value)) { - return (float) $value; - } - if (preg_match('/-?\d+(\.\d+)?/', (string) $value, $matches)) { - return (float) $matches[0]; - } - return 0.0; - }; - - $result = []; - foreach ($rows as $row) { - $retention = $toPercent($row['retention_rate'] ?? 0); - $compRate = $toPercent($row['comp'] ?? 0); - $tpRate = $toPercent($row['tp'] ?? 0); - - if (strpos($normalizedPlan, 'third') !== false || $normalizedPlan === 'tp') { - $gridRate = $tpRate; - } else { - // Comprehensive + Own Damage use COMP column. - $gridRate = $compRate; - } - - $netRate = $gridRate - $retention; - - $row['insurance_plan_type'] = $planType; - $row['month'] = $effectiveMonth; - $row['grid_rate_percentage'] = round($gridRate, 2); - $row['retention_rate_percentage'] = round($retention, 2); - $row['final_commission_percentage'] = round($netRate, 2); - $result[] = $row; - } - - return $this->respond([ - 'status' => 'success', - 'code' => 200, - 'data' => $result, - 'meta' => [ - 'month' => $effectiveMonth, - 'insurance_plan_type' => $planType, - ], - ], 200); - } catch (\Exception $e) { - return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); - } - } - - // Monthly commission grid download - public function downloadMonthlyCommissionGrid() - { - try { - $authUser = $this->getAuthenticatedUserData(); - if (!$authUser) { - return $this->respond(['status' => 'failed', 'code' => 401, 'data' => 'Unauthorized'], 401); - } - - $partnerId = (int) ($this->request->getGet('partner_id') ?? 0); - $rtoId = (int) ($this->request->getGet('rto_id') ?? 0); - $segment = (int) ($this->request->getGet('segment') ?? 0); - $vehicleTypeId = (int) ($this->request->getGet('vehicle_type_id') ?? 0); - $planType = trim((string) ($this->request->getGet('insurance_plan_type') ?? '')); - $month = trim((string) ($this->request->getGet('month') ?? '')); - - if ($partnerId <= 0 || $rtoId <= 0 || $vehicleTypeId <= 0 || $planType === '' || $segment === '') { - return $this->respond([ - 'status' => 'failed', - 'code' => 200, - 'data' => 'partner_id, rto_id, vehicle_type_id, segment and insurance_plan_type are required', - ], 200); - } - - $normalizedPlan = strtolower($planType); - $db = \Config\Database::connect(); - $latestRow = $db->table('partner_grid_details') - ->select("DATE_FORMAT(created_at, '%Y-%m') AS month_key") - ->where('created_at IS NOT NULL', null, false) - ->orderBy('created_at', 'DESC') - ->get(1) - ->getRowArray(); - $effectiveMonth = $month !== '' ? $month : (($latestRow['month_key'] ?? '') ?: date('Y-m')); - - $sourceRows = $db->table('partner_grid_details pgd') - ->select(" - pa.name AS partner_name, - pa.agent_code, - COALESCE(pa.retention_rate, 0) AS retention_rate, - rm.rto_code, - rm.rto_name, - vt.vehicle_type AS vehicle_type_name, - pdg.segment, - pgd.comp, - pgd.tp - ") - ->join('partner_agent pa', 'pa.id = pgd.partner_id', 'left') - ->join('rto_master rm', 'rm.id = pgd.rto_id', 'left') - ->join('vehicle_type vt', 'vt.id = pgd.vehicle_type_id', 'left') - ->where('pgd.partner_id', $partnerId) - ->where('pgd.rto_id', $rtoId) - ->where('pgd.segment', $segment) - ->where('pgd.vehicle_type_id', $vehicleTypeId) - ->where("DATE_FORMAT(pgd.created_at, '%Y-%m') =", $effectiveMonth) - ->orderBy('pgd.id', 'DESC') - ->get() - ->getResultArray(); - - $toPercent = static function ($value): float { - if ($value === null) { - return 0.0; - } - if (is_numeric($value)) { - return (float) $value; - } - if (preg_match('/-?\d+(\.\d+)?/', (string) $value, $matches)) { - return (float) $matches[0]; - } - return 0.0; - }; - - $rows = []; - foreach ($sourceRows as $row) { - $retention = $toPercent($row['retention_rate'] ?? 0); - $compRate = $toPercent($row['comp'] ?? 0); - $tpRate = $toPercent($row['tp'] ?? 0); - $gridRate = (strpos($normalizedPlan, 'third') !== false || $normalizedPlan === 'tp') ? $tpRate : $compRate; - - $row['month'] = $effectiveMonth; - $row['insurance_plan_type'] = $planType; - $row['grid_rate_percentage'] = round($gridRate, 2); - $row['retention_rate_percentage'] = round($retention, 2); - $row['final_commission_percentage'] = round($gridRate - $retention, 2); - $rows[] = $row; - } - - if (empty($rows)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 404, - 'data' => 'No data found for the selected filters', - ], 200); - } - - $safePlan = preg_replace('/[^a-zA-Z0-9]+/', '_', strtolower($planType)) ?: 'plan'; - $fileName = "monthly_commission_{$effectiveMonth}_{$safePlan}.csv"; - $tmpFile = WRITEPATH . 'uploads/temp/' . $fileName; - if (!is_dir(dirname($tmpFile))) { - mkdir(dirname($tmpFile), 0777, true); - } - - $fp = fopen($tmpFile, 'w'); - fputcsv($fp, [ - 'Month', - 'Partner', - 'Partner Code', - 'RTO', - 'Type', - 'Segment', - 'Insurance Plan', - 'Grid %', - 'Retention %', - 'Final Commission %', - ]); - - foreach ($rows as $row) { - fputcsv($fp, [ - $row['month'] ?? $effectiveMonth, - $row['partner_name'] ?? '', - $row['agent_code'] ?? '', - trim((string) (($row['rto_code'] ?? '') . ' - ' . ($row['rto_name'] ?? '')), ' -'), - $row['vehicle_type_name'] ?? '', - $row['segment'] ?? '', - $row['insurance_plan_type'] ?? $planType, - $row['grid_rate_percentage'] ?? 0, - $row['retention_rate_percentage'] ?? 0, - $row['final_commission_percentage'] ?? 0, - ]); - } - - fclose($fp); - return $this->response->download($tmpFile, null)->setFileName($fileName); - } catch (\Exception $e) { - return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); - } - } } diff --git a/app/Controllers/AgentIncentiveController.php b/app/Controllers/AgentIncentiveController.php new file mode 100644 index 0000000..3cf31bb --- /dev/null +++ b/app/Controllers/AgentIncentiveController.php @@ -0,0 +1,757 @@ +AgentIncentiveFileModel = new AgentIncentiveFileModel(); + $this->PayoutGridModel = new PartnerInsurancePayoutGridModel(); + $this->PartnerAgentModel = new AgentModel(); + } + + + // ------------------------------------------------------------------------- + // Upload Grid File (file_type = 'grid') + // Parses Excel and UPSERTs rows into partner_insurance_payout_grid + // ------------------------------------------------------------------------- + public function uploadPayoutGridFile() + { + try { + /* ==================================================================== + * STEP 1 — Validate POST input + * ==================================================================== */ + $data = $this->request->getPost(); + $agentId = $data['agent_id'] ?? null; + $month = $data['incentive_month'] ?? null; + $createdBy = $data['created_by'] ?? null; + + if (empty($agentId) || empty($month)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'agent_id and incentive_month are required.', + ], 200); + } + + /* ==================================================================== + * STEP 2 — Duplicate check on partner_agent_incentive_file + * ==================================================================== */ + $duplicate = $this->AgentIncentiveFileModel + ->where('agent_id', $agentId) + ->where('incentive_month', $month) + ->where('file_type', 'grid') + ->first(); + + if (!empty($duplicate)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 200, + 'data' => 'Duplicate Entry.', + ], 200); + } + + /* ==================================================================== + * STEP 3 — Fetch retention_rate from partner_agent using agent_id + * + * retention_rate is stored as decimal(4,2) e.g. 2.50 + * Used later to calculate partner_comp / partner_tp / partner_od + * by subtracting from broker_excel values. + * If agent not found or retention_rate is NULL → partner fields = null + * ==================================================================== */ + $agent = $this->PartnerAgentModel->find($agentId); + $retentionRate = (!empty($agent) && $agent['retention_rate'] !== null) + ? (float) $agent['retention_rate'] + : null; + + /* ==================================================================== + * STEP 4 — Validate uploaded file (extension check) + * ==================================================================== */ + $gridFile = $this->request->getFile('incentive_file_name'); + + if (!$gridFile || !$gridFile->isValid()) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'No valid file uploaded.', + ], 200); + } + + $extension = strtolower($gridFile->getClientExtension()); + if (!in_array($extension, ['xlsx', 'xls', 'csv'], true)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'Only xlsx, xls, csv grid files are allowed.', + ], 200); + } + + /* ==================================================================== + * STEP 5 — Move file to upload directory + * ==================================================================== */ + $uploadPath = WRITEPATH . 'uploads/agent/incentive_file/'; + if (!is_dir($uploadPath)) { + mkdir($uploadPath, 0777, true); + } + + $gridFileName = time() . '_' . $gridFile->getRandomName(); + $gridFile->move($uploadPath, $gridFileName); + + /* ==================================================================== + * STEP 6 — Insert record into partner_agent_incentive_file + * (same as uploadAgentIncentiveFile but file_type = 'grid') + * ==================================================================== */ + $this->AgentIncentiveFileModel->insert([ + 'agent_id' => $agentId, + 'incentive_month' => $month, + 'incentive_file_name' => $gridFileName, + 'file_type' => 'grid', + 'is_active' => 1, + 'created_by' => $createdBy, + ]); + + /* ==================================================================== + * STEP 7 — Parse Excel sheet into a flat array of rows + * ==================================================================== */ + $spreadsheet = IOFactory::load($uploadPath . $gridFileName); + $rows = $spreadsheet->getActiveSheet() + ->toArray(null, true, true, false); + + if (empty($rows)) { + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'inserted' => 0, + 'updated' => 0, + 'message' => 'File saved but grid sheet is empty — no rows processed.', + ], + ], 200); + } + + /* ==================================================================== + * STEP 8 — Helper: normalise a cell value + * Strips spaces & non-alphanumeric chars, returns lowercase. + * Used to safely compare header/section values. + * ==================================================================== */ + $normalize = static function ($value): string { + $value = strtolower(trim((string) $value)); + return preg_replace('/[^a-z0-9]+/', '', $value) ?? ''; + }; + + /* ==================================================================== + * STEP 9 — Helper: extract numeric value from broker_excel cell + * + * Broker excel cells come in mixed formats: + * "22" → 22.0 (plain number) + * "4.8" → 4.8 + * "5.5X" → 5.5 (strip trailing X) + * "NET 1.9X" → 1.9 (strip prefix text + X) + * "OD 1.5X" → 1.5 (strip OD prefix + X) + * "1.5 X" → 1.5 (space before X) + * "OD25+TP10" → 35.0 (compound format, splits and sums up) + * "OD 20+TP 15"→ 35.0 (compound format, splits and sums up) + * "" / null → null + * + * Returns float|null + * ==================================================================== */ + $extractNumeric = static function ($value): ?float { + $str = strtolower(trim((string) $value)); + + // Blank or nan → null + if ($str === '' || $str === 'nan') { + return null; + } + + // Compound values like "OD25+TP10" or "OD 20+TP 15" + if (str_contains($str, '+')) { + $sum = 0.0; + $hasValid = false; + foreach (explode('+', $str) as $part) { + $cleanPart = preg_replace('/^(net|od|tp)\s*/i', '', trim($part)); + $cleanPart = rtrim(trim($cleanPart), 'xX '); + if (is_numeric($cleanPart)) { + $sum += (float) $cleanPart; + $hasValid = true; + } + } + return $hasValid ? $sum : null; + } + + // Strip known text prefixes: "net", "od", "tp", spaces + $str = preg_replace('/^(net|od|tp)\s*/i', '', $str); + + // Strip trailing "x" or "X" and any surrounding spaces + $str = rtrim(trim($str), 'xX '); + + // Now try to parse as float + if (is_numeric($str)) { + return (float) $str; + } + + return null; + }; + + /* ==================================================================== + * STEP 10 — Helper: calculate partner rate + * + * Formula: partner_value = broker_excel_value - retention_rate + * + * Rules: + * - If broker_excel_value is null → return null + * - If retention_rate is null → return null + * - Result rounded to 2 decimal places + * - Stored as string to match varchar column type + * ==================================================================== */ + $calcPartnerRate = static function ( + ?string $brokerExcelRaw, + ?float $retentionRate, + callable $extractNumeric + ): ?string { + // Either side missing → cannot compute partner rate + if ($brokerExcelRaw === null || $retentionRate === null) { + return null; + } + + $brokerValue = $extractNumeric($brokerExcelRaw); + + // Could not parse a clean number from the broker value + if ($brokerValue === null) { + return null; + } + + // partner rate = broker excel value − retention rate + $partnerValue = round($brokerValue - $retentionRate, 2); + + return (string) $partnerValue; + }; + + /* ==================================================================== + * STEP 11 — Walk rows: detect section headers → column headers → data + * + * Excel has two column layouts depending on section: + * + * Layout A — TWO WHEELER / PCV / GCV / LCV / HCV / BUS / TAXI etc. + * [0] INSURER | [1] RTO | [2] SEGMENT | [3] COMP | [4] TP + * [5] REMARKS | [6] BROKER | [7] BROKER_EXCEL_COMP | [8] BROKER_EXCEL_TP + * + * Layout B — PRIVATE CAR-TP / fuel-separated sections + * [0] INSURER | [1] RTO | [2] SEGMENT | [3] TP | [4] FUEL + * [5] REMARKS | [6] BROKER | [7] BROKER_EXCEL_TP + * ==================================================================== */ + $currentVehicleType = null; + $layoutHasComp = true; // true = Layout A, false = Layout B + $insertedCount = 0; + $updatedCount = 0; + + foreach ($rows as $row) { + + // Pad row to 9 columns so every index access is always safe + while (count($row) < 9) { + $row[] = null; + } + + $col0 = trim((string) ($row[0] ?? '')); + $col1 = trim((string) ($row[1] ?? '')); + $col2 = trim((string) ($row[2] ?? '')); + $col3 = trim((string) ($row[3] ?? '')); + $col4 = trim((string) ($row[4] ?? '')); + + // Consider col1 empty when it is blank or the string "nan" + $isCol1Empty = ($col1 === '' || strtolower($col1) === 'nan'); + + /* ------------------------------------------------------------------ + * Row type A — Date row (very first row of the sheet) → skip + * ------------------------------------------------------------------ */ + if ($col0 !== '' && $isCol1Empty && strtotime($col0) !== false) { + continue; + } + + /* ------------------------------------------------------------------ + * Row type B — Section-header row + * Condition: col0 has text, col1 is empty, col0 is NOT "INSURER" + * Action: set currentVehicleType, reset layout flag + * ------------------------------------------------------------------ */ + if ( + $col0 !== '' + && $isCol1Empty + && strtoupper($col0) !== 'INSURER' + && $normalize($col0) !== 'nan' + ) { + $currentVehicleType = strtoupper($col0); + $layoutHasComp = true; // will be re-detected from next header row + continue; + } + + /* ------------------------------------------------------------------ + * Row type C — Column-header row (col0 == "INSURER") + * Detect layout from col[4]: + * FUEL / PETROL / DIESEL / TP → Layout B (no comp column) + * anything else → Layout A (has comp column) + * ------------------------------------------------------------------ */ + if (strtoupper($col0) === 'INSURER') { + $col4Upper = strtoupper($col4); + $layoutHasComp = !( + str_contains($col4Upper, 'FUEL') + || str_contains($col4Upper, 'PETROL') + || str_contains($col4Upper, 'DIESEL') + || $col4Upper === 'TP' + ); + continue; + } + + /* ------------------------------------------------------------------ + * Row type D — Completely blank row → skip + * ------------------------------------------------------------------ */ + if ($col0 === '' && $col1 === '' && $col2 === '') { + continue; + } + + /* ------------------------------------------------------------------ + * Row type E — Data row before any section header was seen → skip + * ------------------------------------------------------------------ */ + if ($currentVehicleType === null) { + continue; + } + + /* ================================================================== + * MAP COLUMNS TO FIELDS based on detected layout + * ================================================================== */ + if ($layoutHasComp) { + /* ---------------------------------------------------------------- + * LAYOUT A (COMP + TP both present) + * col[3] = COMP (broker rate for comprehensive) + * col[4] = TP (broker rate for third-party) + * col[7] = BROKER_EXCEL_COMP + * col[8] = BROKER_EXCEL_TP + * ---------------------------------------------------------------- */ + $insurer = $col0 !== '' ? strtoupper($col0) : null; + $rto = $col1 !== '' ? strtoupper($col1) : null; + $segment = $col2 !== '' ? strtoupper($col2) : null; + $comp = $col3 !== '' ? strtoupper($col3) : null; + $tp = $col4 !== '' ? strtoupper($col4) : null; + $fuel = null; + $remarks = trim((string) ($row[5] ?? '')) ?: null; + $brokerName = trim((string) ($row[6] ?? '')) ?: null; + $brokerExcelComp = trim((string) ($row[7] ?? '')) ?: null; + $brokerExcelTp = trim((string) ($row[8] ?? '')) ?: null; + $brokerExcelOd = null; + $od = null; + + /* partner_comp = broker_excel_comp − retention_rate + partner_tp = broker_excel_tp − retention_rate + partner_od = null (no OD broker value in Layout A) + Any of these will be null if broker_excel or retention_rate is null */ + $partnerComp = $calcPartnerRate($brokerExcelComp, $retentionRate, $extractNumeric); + $partnerTp = $calcPartnerRate($brokerExcelTp, $retentionRate, $extractNumeric); + $partnerOd = null; + + } else { + /* ---------------------------------------------------------------- + * LAYOUT B (TP only, with FUEL column) + * col[3] = TP (broker rate for third-party) + * col[4] = FUEL type + * col[7] = BROKER_EXCEL_TP + * No COMP or OD broker excel column in this layout + * ---------------------------------------------------------------- */ + $insurer = $col0 !== '' ? strtoupper($col0) : null; + $rto = $col1 !== '' ? strtoupper($col1) : null; + $segment = $col2 !== '' ? strtoupper($col2) : null; + $comp = null; + $tp = $col3 !== '' ? strtoupper($col3) : null; + $fuel = $col4 !== '' ? strtoupper($col4) : null; + $remarks = trim((string) ($row[5] ?? '')) ?: null; + $brokerName = trim((string) ($row[6] ?? '')) ?: null; + $brokerExcelComp = null; + $brokerExcelTp = trim((string) ($row[7] ?? '')) ?: null; + $brokerExcelOd = null; + $od = null; + + /* partner_comp = null (no comp in Layout B) + partner_tp = broker_excel_tp − retention_rate + partner_od = null (no OD broker value in Layout B) */ + $partnerComp = null; + $partnerTp = $calcPartnerRate($brokerExcelTp, $retentionRate, $extractNumeric); + $partnerOd = null; + } + + // Skip rows that have no essential identifiers + if (empty($insurer) || empty($segment)) { + continue; + } + + /* ------------------------------------------------------------------ + * OD-only row detection + * Some rows encode OD rate inside the comp column (e.g. "OD 1.5X") + * when tp is empty. Promote comp → od and clear comp. + * Recalculate partner_od from broker_excel_comp in this case. + * ------------------------------------------------------------------ */ + if ($comp !== null && stripos($comp, 'OD') === 0 && $tp === null) { + $od = $comp; + $comp = null; + $brokerExcelOd = $brokerExcelComp; // broker excel comp was OD value + $brokerExcelComp = null; + + // partner_od = broker_excel_od − retention_rate + $partnerOd = $calcPartnerRate($brokerExcelOd, $retentionRate, $extractNumeric); + $partnerComp = null; + } + + /* ================================================================== + * UPSERT into partner_insurance_payout_grid + * Natural key: vehicle_type + insurer + rto + segment + * UPDATE if record exists, INSERT otherwise. + * ================================================================== */ + $existing = $this->PayoutGridModel + ->where('vehicle_type', $currentVehicleType) + ->where('insurer', $insurer) + ->where('rto', $rto ?? '') + ->where('segment', $segment) + ->first(); + + $gridRow = [ + 'vehicle_type' => $currentVehicleType, + 'fuel' => $fuel, + 'insurer' => $insurer, + 'rto' => $rto, + 'broker_name' => $brokerName, + 'segment' => $segment, + 'comp' => $comp, + 'tp' => $tp, + 'od' => $od, + 'remarks' => $remarks, + 'broker_excel_comp' => $brokerExcelComp, + 'broker_excel_tp' => $brokerExcelTp, + 'broker_excel_od' => $brokerExcelOd, + // partner_* = broker_excel_* − retention_rate + // null when either broker value or retention_rate is missing + 'partner_comp' => $partnerComp, + 'partner_tp' => $partnerTp, + 'partner_od' => $partnerOd, + ]; + + if (!empty($existing)) { + // Record already exists → UPDATE, stamp updated_by + $gridRow['updated_by'] = $createdBy; + $this->PayoutGridModel->update($existing['id'], $gridRow); + $updatedCount++; + } else { + // New record → INSERT, stamp created_by + $gridRow['created_by'] = $createdBy; + $this->PayoutGridModel->insert($gridRow); + $insertedCount++; + } + } + + /* ==================================================================== + * STEP 12 — Return summary response + * ==================================================================== */ + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'file' => $gridFileName, + 'inserted' => $insertedCount, + 'updated' => $updatedCount, + 'retention_rate' => $retentionRate, + 'message' => "Grid processed: {$insertedCount} inserted, {$updatedCount} updated.", + ], + ], 200); + + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ], 500); + } + } + + // ------------------------------------------------------------------------- + // List all grid records (optional utility endpoint) + // ------------------------------------------------------------------------- + // GET /agent/payoutGrid?role=Manager means all list + // GET /agent/payoutGrid?role=Manager&insurer=HDFC&vehicle_type=TWO%20WHEELER&plan_type=comp + // GET /agent/payoutGrid?role=Manager&insurer=HDFC&rto=TN&segment=BIKE&plan_type=comp + // GET /agent/payoutGrid?role=Manager&insurer=HDFC&segment=BIKE&vehicle_type=TWO%20WHEELER&plan_type=comp + public function getPayoutGrid() + { + try { + $request = $this->request; + $role = $request->getGet('role'); + + // 1. Check if Role is provided + if (!$role) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'Role is required to fetch payout data.' + ], 400); + } + + // 2. Extract Filters + $insurer = $request->getGet('insurer'); + $rto = $request->getGet('rto'); + $segment = $request->getGet('segment'); + $vehicle_type = $request->getGet('vehicle_type'); + $plan_type = $request->getGet('plan_type'); + + // 3. Build the Grid Query + $builder = $this->PayoutGridModel->builder(); + $builder->orderBy('vehicle_type', 'ASC')->orderBy('insurer', 'ASC'); + + // Apply filters only for authorized roles + if (in_array($role, ['Manager', 'Accounts'])) { + if (!empty($insurer)) $builder->where('insurer', $insurer); + if (!empty($rto)) $builder->where('rto', $rto); + if (!empty($segment)) $builder->where('segment', $segment); + if (!empty($vehicle_type)) $builder->where('vehicle_type', $vehicle_type); + + // Handle dynamic column selection (comp/tp/od) + if (!empty($plan_type) && in_array($plan_type, ['comp', 'tp', 'od'])) { + $builder->select("id, insurer, vehicle_type, segment, rto, $plan_type as payout"); + } else { + // Default selection if no plan_type or invalid plan_type + $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od'); + } + } + + $gridResults = $builder->get()->getResult(); + + // 4. Combine Grid Results with Dropdown Meta-data + $responseData = [ + 'grid' => $gridResults, + 'rtos' => $this->getUniqueColumnValues('rto'), + 'segments' => $this->getUniqueColumnValues('segment'), + 'vehicle_types' => $this->getUniqueColumnValues('vehicle_type'), + 'insurers' => $this->getUniqueColumnValues('insurer'), + 'plan_types' => [ + ['value' => 'comp', 'label' => 'Comprehensive'], + ['value' => 'tp', 'label' => 'Third Party'], + ['value' => 'od', 'label' => 'Own Damage'] + ] + ]; + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $responseData + ], 200); + + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage() + ], 500); + } + } + + /** + * Helper function to fetch unique non-empty values for a column + */ + private function getUniqueColumnValues($column) + { + $results = $this->PayoutGridModel->select($column) + ->distinct() + ->where("$column IS NOT NULL") + ->where("$column !=", '') + ->orderBy($column, 'ASC') + ->findAll(); + + return array_column($results, $column); + } + + + // ------------------------------------------------------------------------- + // 2. Download Filtered Grid in Excel + // Route: $routes->get('agent/downloadGrid', 'AgentIncentiveController::downloadGridInExcel'); + // Purpose: Applies the exact same filters as getPayoutGrid(), but instead + // of returning JSON, it generates and downloads an Excel file. + // ------------------------------------------------------------------------- + public function downloadGridInExcel() + { + try { + $request = $this->request; + + // Get GET parameters for filtering + $role = $request->getGet('role'); + $insurer = $request->getGet('insurer'); + $rto = $request->getGet('rto'); + $segment = $request->getGet('segment'); + $vehicle_type = $request->getGet('vehicle_type'); + $plan_type = $request->getGet('plan_type'); // comp, tp, od + + $builder = $this->PayoutGridModel->builder(); + $builder->orderBy('vehicle_type', 'ASC')->orderBy('insurer', 'ASC'); + + // Apply filters if the user is a Manager or Accounts role + if (in_array($role, ['Manager', 'Accounts'])) { + if (!empty($insurer)) $builder->where('insurer', $insurer); + if (!empty($rto)) $builder->where('rto', $rto); + if (!empty($segment)) $builder->where('segment', $segment); + if (!empty($vehicle_type)) $builder->where('vehicle_type', $vehicle_type); + } + + // Fetch the filtered records + $records = $builder->get()->getResultArray(); + + if (empty($records)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'No records found to export based on your filters.' + ], 404); + } + + // --- Start Excel Generation --- + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // Setup Header Row + $sheet->setCellValue('A1', 'Vehicle Type'); + $sheet->setCellValue('B1', 'Insurer'); + $sheet->setCellValue('C1', 'RTO'); + $sheet->setCellValue('D1', 'Segment'); + $sheet->setCellValue('E1', 'Broker Name'); // Changed from 'ID' for clarity + + // Set the dynamic column header based on the selected plan type + if ($plan_type === 'comp') { + $sheet->setCellValue('F1', 'Comp'); + $sheet->setCellValue('G1', 'Comp'); + } elseif ($plan_type === 'tp') { + $sheet->setCellValue('F1', 'TP'); + $sheet->setCellValue('G1', 'TP'); + } elseif ($plan_type === 'od') { + $sheet->setCellValue('F1', 'OD'); + $sheet->setCellValue('G1', 'OD'); + } else { + // If no specific plan type is selected, show all + $sheet->setCellValue('F1', 'Comp'); + $sheet->setCellValue('G1', 'TP'); + $sheet->setCellValue('H1', 'OD'); + + $sheet->setCellValue('I1', 'Comp'); + $sheet->setCellValue('J1', 'TP'); + $sheet->setCellValue('K1', 'OD'); + } + + // Populate Excel Rows + $rowNumber = 2; // Start on row 2 (row 1 is headers) + foreach ($records as $row) { + $sheet->setCellValue('A' . $rowNumber, $row['vehicle_type']); + $sheet->setCellValue('B' . $rowNumber, $row['insurer']); + $sheet->setCellValue('C' . $rowNumber, $row['rto']); + $sheet->setCellValue('D' . $rowNumber, $row['segment']); + $sheet->setCellValue('E' . $rowNumber, $row['broker_name']); + + // Output dynamic columns based on selected plan type + if ($plan_type === 'comp') { + $sheet->setCellValue('F' . $rowNumber, $row['comp']); + $sheet->setCellValue('G' . $rowNumber, $row['partner_comp']); + } elseif ($plan_type === 'tp') { + $sheet->setCellValue('F' . $rowNumber, $row['tp']); + $sheet->setCellValue('G' . $rowNumber, $row['partner_tp']); + } elseif ($plan_type === 'od') { + $sheet->setCellValue('F' . $rowNumber, $row['od']); + $sheet->setCellValue('G' . $rowNumber, $row['partner_od']); + } else { + $sheet->setCellValue('F' . $rowNumber, $row['comp']); + $sheet->setCellValue('G' . $rowNumber, $row['tp']); + $sheet->setCellValue('H' . $rowNumber, $row['od']); + $sheet->setCellValue('I' . $rowNumber, $row['partner_comp']); + $sheet->setCellValue('J' . $rowNumber, $row['partner_tp']); + $sheet->setCellValue('K' . $rowNumber, $row['partner_od']); + } + $rowNumber++; + } + + // Auto-size columns for better readability + foreach (range('A', $sheet->getHighestColumn()) as $columnID) { + $sheet->getColumnDimension($columnID)->setAutoSize(true); + } + + // Write the file to a temporary location + $writer = new Xlsx($spreadsheet); + $fileName = 'Filtered_Payout_Grid_' . date('Y-m-d_H-i') . '.xlsx'; + $tempFile = tempnam(sys_get_temp_dir(), 'grid_export'); + $writer->save($tempFile); + + // Return the file as a direct download response + return $this->response->download($tempFile, null)->setFileName($fileName); + + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => 'Failed to generate Excel file: ' . $e->getMessage() + ], 500); + } + } + + // ------------------------------------------------------------------------- + // 3. Download the Originally Uploaded Reference Grid + // Route: $routes->get('agent/downloadExistGrid', 'AgentIncentiveController::downloadLastestGrid'); + // Purpose: Finds the most recently uploaded physical Excel file (where file_type='grid') + // from the partner_agent_incentive_file table and initiates a download. + // ------------------------------------------------------------------------- + public function downloadLastestGrid() + { + try { + // Find the latest file entry in the database where file_type is 'grid' + $latestFileRecord = $this->AgentIncentiveFileModel + ->where('file_type', 'grid') + ->orderBy('created_on', 'DESC') + ->first(); + + if (empty($latestFileRecord)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'No uploaded reference grid file found in the database.' + ], 404); + } + + // Construct the exact file path where it was saved during upload + $fileName = $latestFileRecord['incentive_file_name']; + $filePath = WRITEPATH . 'uploads/agent/incentive_file/' . $fileName; + + // Check if the physical file actually exists on the server + if (!file_exists($filePath)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'The file record exists, but the physical file is missing from the server.' + ], 404); + } + + // Initiate the download of the physical file + return $this->response->download($filePath, null)->setFileName('Reference_Grid_' . $fileName); + + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => 'Error attempting to download file: ' . $e->getMessage() + ], 500); + } + } + +} diff --git a/app/Models/PartnerGridDetailsModel.php b/app/Models/PartnerGridDetailsModel.php deleted file mode 100644 index 9f6086b..0000000 --- a/app/Models/PartnerGridDetailsModel.php +++ /dev/null @@ -1,202 +0,0 @@ - 'permit_empty|max_length[50]', - 'insurer_id' => 'required|max_length[50]', - 'rto_id' => 'required|max_length[50]', - 'segment' => 'required|max_length[100]', - 'comp' => 'permit_empty|max_length[50]', - 'tp' => 'permit_empty|max_length[50]', - 'remarks' => 'permit_empty|max_length[255]', - 'partner_id' => 'required|max_length[50]', - 'fuel' => 'permit_empty|max_length[50]', - 'partner_comp' => 'permit_empty|max_length[20]', - 'partner_tp' => 'permit_empty|max_length[20]', - 'created_by' => 'permit_empty|integer', - 'updated_by' => 'permit_empty|integer', - ]; - - protected $validationMessages = [ - 'insurer_id' => [ - 'required' => 'Insurer is required.', - 'max_length' => 'Insurer name must not exceed 50 characters.', - ], - 'rto_id' => [ - 'required' => 'RTO is required.', - 'max_length' => 'RTO code must not exceed 50 characters.', - ], - 'vehicle_type_id' => [ - 'required' => 'Type is required.', - 'max_length' => 'Type must not exceed 50 characters.', - ], - 'segment' => [ - 'required' => 'Segment is required.', - 'max_length' => 'Segment must not exceed 100 characters.', - ], - 'partner_id' => [ - 'required' => 'Partner is required.', - 'max_length' => 'Partner ID must not exceed 50 characters.', - ], - ]; - - protected $skipValidation = false; - - // ───────────────────────────────────────── - // Custom Methods - // ───────────────────────────────────────── - - /** - * Get all active partner grid records - */ - public function getAllRecords() - { - return $this->orderBy('id', 'ASC')->findAll(); - } - - /** - * Get records by Insurer ID - */ - public function getByInsurer(string $insurerId) - { - return $this->where('insurer_id', $insurerId)->findAll(); - } - - /** - * Get records by RTO ID - */ - public function getByRTO(string $rtoId) - { - return $this->where('rto_id', $rtoId)->findAll(); - } - - /** - * Get records by Segment - */ - public function getBySegment(string $segment) - { - return $this->where('segment', $segment)->findAll(); - } - - /** - * Get records by Vehicle Type ID - */ - public function getByType(string $vehicleTypeId) - { - return $this->where('vehicle_type_id', $vehicleTypeId)->findAll(); - } - - /** - * Get records by Partner ID - */ - public function getByPartner(string $partnerId) - { - return $this->where('partner_id', $partnerId)->findAll(); - } - - /** - * Get records by Insurer and RTO - */ - public function getByInsurerAndRTO(string $insurerId, string $rtoId) - { - return $this->where('insurer_id', $insurerId) - ->where('rto_id', $rtoId) - ->findAll(); - } - - /** - * Search with multiple filters - */ - public function search(array $filters = []) - { - $builder = $this->builder(); - - if (!empty($filters['insurer_id'])) { - $builder->where('insurer_id', $filters['insurer_id']); - } - if (!empty($filters['rto_id'])) { - $builder->where('rto_id', $filters['rto_id']); - } - if (!empty($filters['vehicle_type_id'])) { - $builder->where('vehicle_type_id', $filters['vehicle_type_id']); - } - if (!empty($filters['segment'])) { - $builder->where('segment', $filters['segment']); - } - if (!empty($filters['partner_id'])) { - $builder->where('partner_id', $filters['partner_id']); - } - - return $builder->get()->getResultArray(); - } - - /** - * Insert with created_by - */ - public function insertRecord(array $data, int $userId) - { - $data['created_by'] = $userId; - $data['updated_by'] = $userId; - return $this->insert($data); - } - - /** - * Update with updated_by - */ - public function updateRecord(int $id, array $data, int $userId) - { - $data['updated_by'] = $userId; - return $this->update($id, $data); - } - - /** - * Delete a record by ID - */ - public function deleteRecord(int $id) - { - return $this->delete($id); - } -} \ No newline at end of file diff --git a/app/Models/PartnerInsurancePayoutGridModel.php b/app/Models/PartnerInsurancePayoutGridModel.php new file mode 100644 index 0000000..69ad700 --- /dev/null +++ b/app/Models/PartnerInsurancePayoutGridModel.php @@ -0,0 +1,129 @@ + 'required|max_length[100]', + 'insurer' => 'required|max_length[45]', + 'segment' => 'required|max_length[100]', + ]; + + protected $validationMessages = [ + 'vehicle_type' => ['required' => 'Vehicle type is required.'], + 'insurer' => ['required' => 'Insurer is required.'], + 'segment' => ['required' => 'Segment is required.'], + ]; + + protected $skipValidation = false; + + // ------------------------------------------------------------------------- + // Find by the natural UPSERT key + // ------------------------------------------------------------------------- + public function findByUpsertKey( + string $vehicleType, + string $insurer, + ?string $rto, + string $segment + ): ?array { + return $this + ->where('vehicle_type', $vehicleType) + ->where('insurer', $insurer) + ->where('rto', $rto ?? '') + ->where('segment', $segment) + ->first(); + } + + // ------------------------------------------------------------------------- + // Bulk UPSERT helper + // Accepts an array of grid rows and inserts or updates each one. + // Returns ['inserted' => int, 'updated' => int] + // ------------------------------------------------------------------------- + public function bulkUpsert(array $gridRows, ?int $userId = null): array + { + $inserted = 0; + $updated = 0; + + foreach ($gridRows as $row) { + $existing = $this->findByUpsertKey( + $row['vehicle_type'], + $row['insurer'], + $row['rto'] ?? null, + $row['segment'] + ); + + $row['updated_by'] = $userId; + + if (!empty($existing)) { + $this->update($existing['id'], $row); + $updated++; + } else { + $row['created_by'] = $userId; + $this->insert($row); + $inserted++; + } + } + + return ['inserted' => $inserted, 'updated' => $updated]; + } + + // ------------------------------------------------------------------------- + // Fetch grid filtered by vehicle_type + // ------------------------------------------------------------------------- + public function getByVehicleType(string $vehicleType): array + { + return $this + ->where('vehicle_type', $vehicleType) + ->orderBy('insurer', 'ASC') + ->findAll(); + } + + // ------------------------------------------------------------------------- + // Fetch grid filtered by insurer + rto + // ------------------------------------------------------------------------- + public function getByInsurerAndRto(string $insurer, string $rto): array + { + return $this + ->where('insurer', $insurer) + ->where('rto', $rto) + ->orderBy('vehicle_type', 'ASC') + ->findAll(); + } +} From b792d4c0583f11ece803ddec31e3b91cef538297 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Thu, 26 Mar 2026 09:20:46 +0530 Subject: [PATCH 10/18] FIX_Changes and Additional Requirements ( GRID Updated API ) --- app/Config/Routes.php | 1 + app/Controllers/AgentIncentiveController.php | 90 ++++++++++++++++++- .../PartnerInsurancePayoutGridModel.php | 8 ++ 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index b860eea..f2b1d15 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -75,6 +75,7 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('agent/downloadAgentIncentiveFile', 'AgentController::downloadAgentIncentiveFile'); $routes->post('agent/uploadGrid', 'AgentIncentiveController::uploadPayoutGridFile'); + $routes->post('agent/updateGrid', 'AgentIncentiveController::updateGrid'); $routes->get('agent/payoutGrid', 'AgentIncentiveController::getPayoutGrid'); $routes->get('agent/downloadGrid', 'AgentIncentiveController::downloadGridInExcel'); $routes->get('agent/loadGrid', 'AgentIncentiveController::loadGrid'); diff --git a/app/Controllers/AgentIncentiveController.php b/app/Controllers/AgentIncentiveController.php index 3cf31bb..f987e98 100644 --- a/app/Controllers/AgentIncentiveController.php +++ b/app/Controllers/AgentIncentiveController.php @@ -526,10 +526,12 @@ class AgentIncentiveController extends ResourceController // Handle dynamic column selection (comp/tp/od) if (!empty($plan_type) && in_array($plan_type, ['comp', 'tp', 'od'])) { - $builder->select("id, insurer, vehicle_type, segment, rto, $plan_type as payout"); + $builder->select("id, insurer, vehicle_type, segment, rto, $plan_type as payout,fuel,broker_name,comp,tp,od,remarks,broker_excel_comp, + broker_excel_tp,broker_excel_od,partner_comp,partner_tp,partner_od,created_by,created_at,updated_by,updated_at"); } else { // Default selection if no plan_type or invalid plan_type - $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od'); + $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel,broker_name,comp,tp,od,remarks,broker_excel_comp, + broker_excel_tp,broker_excel_od,partner_comp,partner_tp,partner_od,created_by,created_at,updated_by,updated_at'); } } @@ -579,6 +581,90 @@ class AgentIncentiveController extends ResourceController return array_column($results, $column); } + // ------------------------------------------------------------------------- + // Update grid row by id + // Route: POST /agent/updateGrid + // ------------------------------------------------------------------------- + public function updateGrid() + { + try { + $jsonData = (array) ($this->request->getJSON(true) ?? []); + $postData = $this->request->getPost() ?? []; + $data = !empty($jsonData) ? $jsonData : $postData; + + $id = isset($data['id']) ? (int) $data['id'] : 0; + if ($id <= 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'id is required.', + ], 200); + } + + $existing = $this->PayoutGridModel->find($id); + if (empty($existing)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'Grid record not found.', + ], 200); + } + + $updateData = [ + 'vehicle_type' => array_key_exists('vehicle_type', $data) ? $data['vehicle_type'] : null, + 'fuel' => array_key_exists('fuel', $data) ? $data['fuel'] : null, + 'rto' => array_key_exists('rto', $data) ? $data['rto'] : null, + 'segment' => array_key_exists('segment', $data) ? $data['segment'] : null, + 'broker_name' => array_key_exists('broker_name', $data) ? $data['broker_name'] : null, + 'comp' => array_key_exists('comp', $data) ? $data['comp'] : null, + 'tp' => array_key_exists('tp', $data) ? $data['tp'] : null, + 'od' => array_key_exists('od', $data) ? $data['od'] : null, + 'broker_excel_comp' => array_key_exists('broker_excel_comp', $data) ? $data['broker_excel_comp'] : null, + 'broker_excel_tp' => array_key_exists('broker_excel_tp', $data) ? $data['broker_excel_tp'] : null, + 'broker_excel_od' => array_key_exists('broker_excel_od', $data) ? $data['broker_excel_od'] : null, + 'partner_comp' => array_key_exists('partner_comp', $data) ? $data['partner_comp'] : null, + 'partner_tp' => array_key_exists('partner_tp', $data) ? $data['partner_tp'] : null, + 'partner_od' => array_key_exists('partner_od', $data) ? $data['partner_od'] : null, + 'remarks' => array_key_exists('remarks', $data) ? $data['remarks'] : null, + ]; + + $hasAnyField = false; + foreach (array_keys($updateData) as $field) { + if (array_key_exists($field, $data)) { + $hasAnyField = true; + break; + } + } + + if (!$hasAnyField) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'No update fields provided.', + ], 200); + } + + if (array_key_exists('updated_by', $data)) { + $updateData['updated_by'] = $data['updated_by']; + } + + $this->PayoutGridModel->updateGridById($id, $updateData); + $updated = $this->PayoutGridModel->find($id); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $updated, + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ], 500); + } + } + // ------------------------------------------------------------------------- // 2. Download Filtered Grid in Excel diff --git a/app/Models/PartnerInsurancePayoutGridModel.php b/app/Models/PartnerInsurancePayoutGridModel.php index 69ad700..983635b 100644 --- a/app/Models/PartnerInsurancePayoutGridModel.php +++ b/app/Models/PartnerInsurancePayoutGridModel.php @@ -126,4 +126,12 @@ class PartnerInsurancePayoutGridModel extends Model ->orderBy('vehicle_type', 'ASC') ->findAll(); } + + // ------------------------------------------------------------------------- + // Update a grid row by id + // ------------------------------------------------------------------------- + public function updateGridById(int $id, array $data): bool + { + return $this->update($id, $data); + } } From a5a9004a5d00491d9b725896c018a2d5607bd54f Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 27 Mar 2026 10:08:31 +0530 Subject: [PATCH 11/18] FIX_Endorsement file --- app/Config/Routes.php | 10 +- app/Controllers/AgentIncentiveController.php | 593 ++++++------------ app/Controllers/EndorsementController.php | 1 + app/Models/AgentIncentiveFileModel.php | 8 +- .../PartnerInsurancePayoutGridModel.php | 67 +- 5 files changed, 194 insertions(+), 485 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index f2b1d15..2ddf684 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -74,13 +74,9 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('agent/deleteAgentIncentiveFile', 'AgentController::deleteAgentIncentiveFile'); $routes->get('agent/downloadAgentIncentiveFile', 'AgentController::downloadAgentIncentiveFile'); - $routes->post('agent/uploadGrid', 'AgentIncentiveController::uploadPayoutGridFile'); - $routes->post('agent/updateGrid', 'AgentIncentiveController::updateGrid'); - $routes->get('agent/payoutGrid', 'AgentIncentiveController::getPayoutGrid'); - $routes->get('agent/downloadGrid', 'AgentIncentiveController::downloadGridInExcel'); - $routes->get('agent/loadGrid', 'AgentIncentiveController::loadGrid'); - $routes->get('agent/downloadExistGrid', 'AgentIncentiveController::downloadLastestGrid'); - + $routes->get('grid', 'AgentIncentiveController::getGridData'); + $routes->post('grid/upload', 'AgentIncentiveController::uploadGridFile'); + $routes->get('grid/fileList', 'AgentIncentiveController::gridFileList'); //Staff $routes->get('staff/staffList', 'StaffController::staffList'); diff --git a/app/Controllers/AgentIncentiveController.php b/app/Controllers/AgentIncentiveController.php index f987e98..90697df 100644 --- a/app/Controllers/AgentIncentiveController.php +++ b/app/Controllers/AgentIncentiveController.php @@ -27,65 +27,87 @@ class AgentIncentiveController extends ResourceController $this->PartnerAgentModel = new AgentModel(); } + // GET /agent/gridFileList + public function gridFileList() + { + try { + $builder = $this->AgentIncentiveFileModel->builder(); + + // 1. Explicitly select and format dates in the SQL layer (Faster + Indian Format) + $builder->select(" + paif.id, + paif.agent_id, + paif.incentive_month, + DATE_FORMAT(paif.incentive_month, '%d-%m-%Y') as vaild_from, + paif.incentive_file_name, + ps.name as created_by_name, + paif.created_on, + DATE_FORMAT(paif.created_on, '%d-%m-%Y %h:%i %p') as created_date + "); + + $builder->from('partner_agent_incentive_file paif'); + + // 2. The Join + $builder->join('partner_staff ps', 'ps.id = paif.created_by', 'left'); + + // 3. Filters + $builder->where('paif.is_active', 1); + $builder->where('paif.file_type', 'grid'); + + // 4. THE FIX: Group by the primary ID to stop the "5 rows" duplication + $builder->groupBy('paif.id'); + + // 5. Order + $builder->orderBy('paif.id', 'DESC'); + + $query = $builder->get(); + $result = $query->getResult(); + + // Use $this->respond to maintain consistency with your other API methods + return $this->respond([ + 'status' => 'success', // Changed to 'success' to match your other methods + 'code' => 200, + 'data' => $result + ], 200); + + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage() + ], 500); + } + } // ------------------------------------------------------------------------- // Upload Grid File (file_type = 'grid') // Parses Excel and UPSERTs rows into partner_insurance_payout_grid // ------------------------------------------------------------------------- - public function uploadPayoutGridFile() + public function uploadGridFile() { try { /* ==================================================================== - * STEP 1 — Validate POST input + * STEP 1 — Validate POST input (Agent ID removed) * ==================================================================== */ $data = $this->request->getPost(); - $agentId = $data['agent_id'] ?? null; - $month = $data['incentive_month'] ?? null; - $createdBy = $data['created_by'] ?? null; + $month = $data['incentive_month'] ?? null; + $createdBy = $data['created_by'] ?? null; - if (empty($agentId) || empty($month)) { + /* ==================================================================== + * STEP 2 — Validation now only checks for month + * ==================================================================== */ + if (empty($month)) { return $this->respond([ 'status' => 'failed', 'code' => 400, - 'data' => 'agent_id and incentive_month are required.', + 'data' => 'Incentive month is required.', ], 200); } /* ==================================================================== - * STEP 2 — Duplicate check on partner_agent_incentive_file - * ==================================================================== */ - $duplicate = $this->AgentIncentiveFileModel - ->where('agent_id', $agentId) - ->where('incentive_month', $month) - ->where('file_type', 'grid') - ->first(); - - if (!empty($duplicate)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 200, - 'data' => 'Duplicate Entry.', - ], 200); - } - - /* ==================================================================== - * STEP 3 — Fetch retention_rate from partner_agent using agent_id - * - * retention_rate is stored as decimal(4,2) e.g. 2.50 - * Used later to calculate partner_comp / partner_tp / partner_od - * by subtracting from broker_excel values. - * If agent not found or retention_rate is NULL → partner fields = null - * ==================================================================== */ - $agent = $this->PartnerAgentModel->find($agentId); - $retentionRate = (!empty($agent) && $agent['retention_rate'] !== null) - ? (float) $agent['retention_rate'] - : null; - - /* ==================================================================== - * STEP 4 — Validate uploaded file (extension check) + * STEP 3 — Validate uploaded file * ==================================================================== */ $gridFile = $this->request->getFile('incentive_file_name'); - if (!$gridFile || !$gridFile->isValid()) { return $this->respond([ 'status' => 'failed', @@ -94,339 +116,132 @@ class AgentIncentiveController extends ResourceController ], 200); } - $extension = strtolower($gridFile->getClientExtension()); - if (!in_array($extension, ['xlsx', 'xls', 'csv'], true)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 400, - 'data' => 'Only xlsx, xls, csv grid files are allowed.', - ], 200); - } - /* ==================================================================== - * STEP 5 — Move file to upload directory + * STEP 4 — Move file * ==================================================================== */ $uploadPath = WRITEPATH . 'uploads/agent/incentive_file/'; - if (!is_dir($uploadPath)) { - mkdir($uploadPath, 0777, true); - } + if (!is_dir($uploadPath)) mkdir($uploadPath, 0777, true); $gridFileName = time() . '_' . $gridFile->getRandomName(); $gridFile->move($uploadPath, $gridFileName); /* ==================================================================== - * STEP 6 — Insert record into partner_agent_incentive_file - * (same as uploadAgentIncentiveFile but file_type = 'grid') + * STEP 5 — Save File record & GET THE ID * ==================================================================== */ - $this->AgentIncentiveFileModel->insert([ - 'agent_id' => $agentId, + $fileData = [ 'incentive_month' => $month, 'incentive_file_name' => $gridFileName, 'file_type' => 'grid', 'is_active' => 1, 'created_by' => $createdBy, - ]); - - /* ==================================================================== - * STEP 7 — Parse Excel sheet into a flat array of rows - * ==================================================================== */ - $spreadsheet = IOFactory::load($uploadPath . $gridFileName); - $rows = $spreadsheet->getActiveSheet() - ->toArray(null, true, true, false); - - if (empty($rows)) { + ]; + + // insert() with returnID enabled returns the inserted primary key in CI4. + $fileId = $this->AgentIncentiveFileModel->insert($fileData, true); + if (empty($fileId) || (int)$fileId <= 0) { + $modelErrors = $this->AgentIncentiveFileModel->errors(); return $this->respond([ - 'status' => 'success', - 'code' => 200, - 'data' => [ - 'inserted' => 0, - 'updated' => 0, - 'message' => 'File saved but grid sheet is empty — no rows processed.', - ], + 'status' => 'failed', + 'code' => 400, + 'data' => !empty($modelErrors) ? $modelErrors : 'Unable to create incentive file record.', ], 200); } /* ==================================================================== - * STEP 8 — Helper: normalise a cell value - * Strips spaces & non-alphanumeric chars, returns lowercase. - * Used to safely compare header/section values. + * STEP 6 — Parse Excel * ==================================================================== */ - $normalize = static function ($value): string { - $value = strtolower(trim((string) $value)); - return preg_replace('/[^a-z0-9]+/', '', $value) ?? ''; - }; + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($uploadPath . $gridFileName); + $rows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); + + if (empty($rows)) { + return $this->respond(['status' => 'success', 'code' => 200, 'data' => 'File empty.'], 200); + } /* ==================================================================== - * STEP 9 — Helper: extract numeric value from broker_excel cell - * - * Broker excel cells come in mixed formats: - * "22" → 22.0 (plain number) - * "4.8" → 4.8 - * "5.5X" → 5.5 (strip trailing X) - * "NET 1.9X" → 1.9 (strip prefix text + X) - * "OD 1.5X" → 1.5 (strip OD prefix + X) - * "1.5 X" → 1.5 (space before X) - * "OD25+TP10" → 35.0 (compound format, splits and sums up) - * "OD 20+TP 15"→ 35.0 (compound format, splits and sums up) - * "" / null → null - * - * Returns float|null + * STEP 7 — Clean Numeric Helper (Modified to store only numbers) * ==================================================================== */ $extractNumeric = static function ($value): ?float { $str = strtolower(trim((string) $value)); + if ($str === '' || $str === 'nan') return null; - // Blank or nan → null - if ($str === '' || $str === 'nan') { - return null; - } - - // Compound values like "OD25+TP10" or "OD 20+TP 15" if (str_contains($str, '+')) { - $sum = 0.0; - $hasValid = false; + $sum = 0.0; $hasValid = false; foreach (explode('+', $str) as $part) { - $cleanPart = preg_replace('/^(net|od|tp)\s*/i', '', trim($part)); - $cleanPart = rtrim(trim($cleanPart), 'xX '); - if (is_numeric($cleanPart)) { - $sum += (float) $cleanPart; - $hasValid = true; - } + $cleanPart = preg_replace('/[^0-9.]/', '', $part); + if (is_numeric($cleanPart)) { $sum += (float) $cleanPart; $hasValid = true; } } return $hasValid ? $sum : null; } - - // Strip known text prefixes: "net", "od", "tp", spaces - $str = preg_replace('/^(net|od|tp)\s*/i', '', $str); - - // Strip trailing "x" or "X" and any surrounding spaces - $str = rtrim(trim($str), 'xX '); - - // Now try to parse as float - if (is_numeric($str)) { - return (float) $str; - } - - return null; + // Strip everything except numbers and decimals + $str = preg_replace('/[^0-9.]/', '', $str); + return is_numeric($str) ? (float) $str : null; }; /* ==================================================================== - * STEP 10 — Helper: calculate partner rate - * - * Formula: partner_value = broker_excel_value - retention_rate - * - * Rules: - * - If broker_excel_value is null → return null - * - If retention_rate is null → return null - * - Result rounded to 2 decimal places - * - Stored as string to match varchar column type - * ==================================================================== */ - $calcPartnerRate = static function ( - ?string $brokerExcelRaw, - ?float $retentionRate, - callable $extractNumeric - ): ?string { - // Either side missing → cannot compute partner rate - if ($brokerExcelRaw === null || $retentionRate === null) { - return null; - } - - $brokerValue = $extractNumeric($brokerExcelRaw); - - // Could not parse a clean number from the broker value - if ($brokerValue === null) { - return null; - } - - // partner rate = broker excel value − retention rate - $partnerValue = round($brokerValue - $retentionRate, 2); - - return (string) $partnerValue; - }; - - /* ==================================================================== - * STEP 11 — Walk rows: detect section headers → column headers → data - * - * Excel has two column layouts depending on section: - * - * Layout A — TWO WHEELER / PCV / GCV / LCV / HCV / BUS / TAXI etc. - * [0] INSURER | [1] RTO | [2] SEGMENT | [3] COMP | [4] TP - * [5] REMARKS | [6] BROKER | [7] BROKER_EXCEL_COMP | [8] BROKER_EXCEL_TP - * - * Layout B — PRIVATE CAR-TP / fuel-separated sections - * [0] INSURER | [1] RTO | [2] SEGMENT | [3] TP | [4] FUEL - * [5] REMARKS | [6] BROKER | [7] BROKER_EXCEL_TP + * STEP 8 — Process Rows * ==================================================================== */ $currentVehicleType = null; - $layoutHasComp = true; // true = Layout A, false = Layout B + $layoutHasComp = true; $insertedCount = 0; - $updatedCount = 0; foreach ($rows as $row) { + while (count($row) < 9) $row[] = null; - // Pad row to 9 columns so every index access is always safe - while (count($row) < 9) { - $row[] = null; - } + $col0 = trim((string) $row[0]); + $col1 = trim((string) $row[1]); - $col0 = trim((string) ($row[0] ?? '')); - $col1 = trim((string) ($row[1] ?? '')); - $col2 = trim((string) ($row[2] ?? '')); - $col3 = trim((string) ($row[3] ?? '')); - $col4 = trim((string) ($row[4] ?? '')); - - // Consider col1 empty when it is blank or the string "nan" - $isCol1Empty = ($col1 === '' || strtolower($col1) === 'nan'); - - /* ------------------------------------------------------------------ - * Row type A — Date row (very first row of the sheet) → skip - * ------------------------------------------------------------------ */ - if ($col0 !== '' && $isCol1Empty && strtotime($col0) !== false) { - continue; - } - - /* ------------------------------------------------------------------ - * Row type B — Section-header row - * Condition: col0 has text, col1 is empty, col0 is NOT "INSURER" - * Action: set currentVehicleType, reset layout flag - * ------------------------------------------------------------------ */ - if ( - $col0 !== '' - && $isCol1Empty - && strtoupper($col0) !== 'INSURER' - && $normalize($col0) !== 'nan' - ) { + // Section Header Detection + if ($col0 !== '' && ($col1 === '' || strtolower($col1) === 'nan') && strtoupper($col0) !== 'INSURER') { $currentVehicleType = strtoupper($col0); - $layoutHasComp = true; // will be re-detected from next header row continue; } - /* ------------------------------------------------------------------ - * Row type C — Column-header row (col0 == "INSURER") - * Detect layout from col[4]: - * FUEL / PETROL / DIESEL / TP → Layout B (no comp column) - * anything else → Layout A (has comp column) - * ------------------------------------------------------------------ */ + // Column Header Detection (Layout A vs B) if (strtoupper($col0) === 'INSURER') { - $col4Upper = strtoupper($col4); - $layoutHasComp = !( - str_contains($col4Upper, 'FUEL') - || str_contains($col4Upper, 'PETROL') - || str_contains($col4Upper, 'DIESEL') - || $col4Upper === 'TP' - ); + $col4Upper = strtoupper((string)$row[4]); + $layoutHasComp = !preg_match('/(FUEL|PETROL|DIESEL|TP)/', $col4Upper); continue; } - /* ------------------------------------------------------------------ - * Row type D — Completely blank row → skip - * ------------------------------------------------------------------ */ - if ($col0 === '' && $col1 === '' && $col2 === '') { - continue; - } + if (empty($col0) || $currentVehicleType === null) continue; - /* ------------------------------------------------------------------ - * Row type E — Data row before any section header was seen → skip - * ------------------------------------------------------------------ */ - if ($currentVehicleType === null) { - continue; - } - - /* ================================================================== - * MAP COLUMNS TO FIELDS based on detected layout - * ================================================================== */ + // Mapping with Clean Numbers if ($layoutHasComp) { - /* ---------------------------------------------------------------- - * LAYOUT A (COMP + TP both present) - * col[3] = COMP (broker rate for comprehensive) - * col[4] = TP (broker rate for third-party) - * col[7] = BROKER_EXCEL_COMP - * col[8] = BROKER_EXCEL_TP - * ---------------------------------------------------------------- */ - $insurer = $col0 !== '' ? strtoupper($col0) : null; - $rto = $col1 !== '' ? strtoupper($col1) : null; - $segment = $col2 !== '' ? strtoupper($col2) : null; - $comp = $col3 !== '' ? strtoupper($col3) : null; - $tp = $col4 !== '' ? strtoupper($col4) : null; + $insurer = strtoupper($col0); + $rto = strtoupper((string)$row[1]); + $segment = strtoupper((string)$row[2]); + // We extract numeric values only for the storage + $comp = $extractNumeric($row[3]); + $tp = $extractNumeric($row[4]); + $od = null; $fuel = null; - $remarks = trim((string) ($row[5] ?? '')) ?: null; - $brokerName = trim((string) ($row[6] ?? '')) ?: null; - $brokerExcelComp = trim((string) ($row[7] ?? '')) ?: null; - $brokerExcelTp = trim((string) ($row[8] ?? '')) ?: null; + $brokerName = trim((string)$row[6]); + $brokerExcelComp = $extractNumeric($row[7]); + $brokerExcelTp = $extractNumeric($row[8]); $brokerExcelOd = null; - $od = null; - - /* partner_comp = broker_excel_comp − retention_rate - partner_tp = broker_excel_tp − retention_rate - partner_od = null (no OD broker value in Layout A) - Any of these will be null if broker_excel or retention_rate is null */ - $partnerComp = $calcPartnerRate($brokerExcelComp, $retentionRate, $extractNumeric); - $partnerTp = $calcPartnerRate($brokerExcelTp, $retentionRate, $extractNumeric); - $partnerOd = null; - + } else { - /* ---------------------------------------------------------------- - * LAYOUT B (TP only, with FUEL column) - * col[3] = TP (broker rate for third-party) - * col[4] = FUEL type - * col[7] = BROKER_EXCEL_TP - * No COMP or OD broker excel column in this layout - * ---------------------------------------------------------------- */ - $insurer = $col0 !== '' ? strtoupper($col0) : null; - $rto = $col1 !== '' ? strtoupper($col1) : null; - $segment = $col2 !== '' ? strtoupper($col2) : null; - $comp = null; - $tp = $col3 !== '' ? strtoupper($col3) : null; - $fuel = $col4 !== '' ? strtoupper($col4) : null; - $remarks = trim((string) ($row[5] ?? '')) ?: null; - $brokerName = trim((string) ($row[6] ?? '')) ?: null; - $brokerExcelComp = null; - $brokerExcelTp = trim((string) ($row[7] ?? '')) ?: null; - $brokerExcelOd = null; + $insurer = strtoupper($col0); + $rto = strtoupper((string)$row[1]); + $segment = strtoupper((string)$row[2]); + $comp = $extractNumeric($row[3]); + $tp = null; $od = null; - - /* partner_comp = null (no comp in Layout B) - partner_tp = broker_excel_tp − retention_rate - partner_od = null (no OD broker value in Layout B) */ - $partnerComp = null; - $partnerTp = $calcPartnerRate($brokerExcelTp, $retentionRate, $extractNumeric); - $partnerOd = null; - } - - // Skip rows that have no essential identifiers - if (empty($insurer) || empty($segment)) { - continue; - } - - /* ------------------------------------------------------------------ - * OD-only row detection - * Some rows encode OD rate inside the comp column (e.g. "OD 1.5X") - * when tp is empty. Promote comp → od and clear comp. - * Recalculate partner_od from broker_excel_comp in this case. - * ------------------------------------------------------------------ */ - if ($comp !== null && stripos($comp, 'OD') === 0 && $tp === null) { - $od = $comp; - $comp = null; - $brokerExcelOd = $brokerExcelComp; // broker excel comp was OD value + $fuel = strtoupper((string)$row[4]); + $brokerName = trim((string)$row[6]); $brokerExcelComp = null; - - // partner_od = broker_excel_od − retention_rate - $partnerOd = $calcPartnerRate($brokerExcelOd, $retentionRate, $extractNumeric); - $partnerComp = null; + $brokerExcelTp = $extractNumeric($row[7]); + $brokerExcelOd = null; } - /* ================================================================== - * UPSERT into partner_insurance_payout_grid - * Natural key: vehicle_type + insurer + rto + segment - * UPDATE if record exists, INSERT otherwise. - * ================================================================== */ - $existing = $this->PayoutGridModel - ->where('vehicle_type', $currentVehicleType) - ->where('insurer', $insurer) - ->where('rto', $rto ?? '') - ->where('segment', $segment) - ->first(); + // OD logic (if COMP contains an OD value) + if ($comp !== null && stripos((string)$row[3], 'OD') !== false && $tp === null) { + $od = $comp; $comp = null; + $brokerExcelOd = $brokerExcelComp; $brokerExcelComp = null; + } $gridRow = [ + 'partner_agent_incentive_file_id' => $fileId, // STORE THE FILE ID HERE 'vehicle_type' => $currentVehicleType, 'fuel' => $fuel, 'insurer' => $insurer, @@ -436,51 +251,31 @@ class AgentIncentiveController extends ResourceController 'comp' => $comp, 'tp' => $tp, 'od' => $od, - 'remarks' => $remarks, - 'broker_excel_comp' => $brokerExcelComp, - 'broker_excel_tp' => $brokerExcelTp, - 'broker_excel_od' => $brokerExcelOd, - // partner_* = broker_excel_* − retention_rate - // null when either broker value or retention_rate is missing - 'partner_comp' => $partnerComp, - 'partner_tp' => $partnerTp, - 'partner_od' => $partnerOd, + 'remarks' => trim((string)($row[5] ?? '')), + 'broker_comp' => $brokerExcelComp, + 'broker_tp' => $brokerExcelTp, + 'broker_od' => $brokerExcelOd, ]; - if (!empty($existing)) { - // Record already exists → UPDATE, stamp updated_by - $gridRow['updated_by'] = $createdBy; - $this->PayoutGridModel->update($existing['id'], $gridRow); - $updatedCount++; - } else { - // New record → INSERT, stamp created_by - $gridRow['created_by'] = $createdBy; - $this->PayoutGridModel->insert($gridRow); - $insertedCount++; - } + + $gridRow['created_by'] = $createdBy; + $this->PayoutGridModel->insert($gridRow); + $insertedCount++; + } - /* ==================================================================== - * STEP 12 — Return summary response - * ==================================================================== */ return $this->respond([ 'status' => 'success', 'code' => 200, 'data' => [ - 'file' => $gridFileName, - 'inserted' => $insertedCount, - 'updated' => $updatedCount, - 'retention_rate' => $retentionRate, - 'message' => "Grid processed: {$insertedCount} inserted, {$updatedCount} updated.", + 'file_id' => $fileId, + 'inserted' => $insertedCount, + 'message' => "Processed: {$insertedCount} new", ], ], 200); } catch (\Exception $e) { - return $this->respond([ - 'status' => 'failed', - 'code' => 500, - 'data' => $e->getMessage(), - ], 500); + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); } } @@ -491,11 +286,12 @@ class AgentIncentiveController extends ResourceController // GET /agent/payoutGrid?role=Manager&insurer=HDFC&vehicle_type=TWO%20WHEELER&plan_type=comp // GET /agent/payoutGrid?role=Manager&insurer=HDFC&rto=TN&segment=BIKE&plan_type=comp // GET /agent/payoutGrid?role=Manager&insurer=HDFC&segment=BIKE&vehicle_type=TWO%20WHEELER&plan_type=comp - public function getPayoutGrid() + public function getGridData() { try { $request = $this->request; $role = $request->getGet('role'); + $file_id = $request->getGet('file_id'); // 1. Check if Role is provided if (!$role) { @@ -526,12 +322,39 @@ class AgentIncentiveController extends ResourceController // Handle dynamic column selection (comp/tp/od) if (!empty($plan_type) && in_array($plan_type, ['comp', 'tp', 'od'])) { - $builder->select("id, insurer, vehicle_type, segment, rto, $plan_type as payout,fuel,broker_name,comp,tp,od,remarks,broker_excel_comp, - broker_excel_tp,broker_excel_od,partner_comp,partner_tp,partner_od,created_by,created_at,updated_by,updated_at"); + $builder->select("id, insurer, vehicle_type, segment, rto, $plan_type as payout,fuel,broker_name,comp,tp,od,remarks,broker_comp, + broker_tp,broker_od,created_by,created_at,updated_by,updated_at"); } else { // Default selection if no plan_type or invalid plan_type - $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel,broker_name,comp,tp,od,remarks,broker_excel_comp, - broker_excel_tp,broker_excel_od,partner_comp,partner_tp,partner_od,created_by,created_at,updated_by,updated_at'); + $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel,broker_name,comp,tp,od,remarks,broker_comp, + broker_tp,broker_od,created_by,created_at,updated_by,updated_at'); + } + } + + if ($file_id) { + $builder->where('partner_agent_incentive_file_id', $file_id); + } else { + $maxRow = $this->PayoutGridModel + ->selectMax('partner_agent_incentive_file_id') + ->first(); + + // Model may return array or object based on global returnType. + $maxFileId = null; + if (is_array($maxRow)) { + $maxFileId = $maxRow['partner_agent_incentive_file_id'] ?? null; + } elseif (is_object($maxRow)) { + $maxFileId = $maxRow->partner_agent_incentive_file_id ?? null; + } + + if (!empty($maxFileId)) { + $builder->where('partner_agent_incentive_file_id', $maxFileId); + } else { + // No data case + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'No data found' + ], 404); } } @@ -619,12 +442,10 @@ class AgentIncentiveController extends ResourceController 'comp' => array_key_exists('comp', $data) ? $data['comp'] : null, 'tp' => array_key_exists('tp', $data) ? $data['tp'] : null, 'od' => array_key_exists('od', $data) ? $data['od'] : null, - 'broker_excel_comp' => array_key_exists('broker_excel_comp', $data) ? $data['broker_excel_comp'] : null, - 'broker_excel_tp' => array_key_exists('broker_excel_tp', $data) ? $data['broker_excel_tp'] : null, - 'broker_excel_od' => array_key_exists('broker_excel_od', $data) ? $data['broker_excel_od'] : null, - 'partner_comp' => array_key_exists('partner_comp', $data) ? $data['partner_comp'] : null, - 'partner_tp' => array_key_exists('partner_tp', $data) ? $data['partner_tp'] : null, - 'partner_od' => array_key_exists('partner_od', $data) ? $data['partner_od'] : null, + 'broker_comp' => array_key_exists('broker_comp', $data) ? $data['broker_comp'] : null, + 'broker_tp' => array_key_exists('broker_tp', $data) ? $data['broker_tp'] : null, + 'broker_od' => array_key_exists('broker_od', $data) ? $data['broker_od'] : null, + 'remarks' => array_key_exists('remarks', $data) ? $data['remarks'] : null, ]; @@ -669,7 +490,7 @@ class AgentIncentiveController extends ResourceController // ------------------------------------------------------------------------- // 2. Download Filtered Grid in Excel // Route: $routes->get('agent/downloadGrid', 'AgentIncentiveController::downloadGridInExcel'); - // Purpose: Applies the exact same filters as getPayoutGrid(), but instead + // Purpose: Applies the exact same filters as getGridData/(:num)(), but instead // of returning JSON, it generates and downloads an Excel file. // ------------------------------------------------------------------------- public function downloadGridInExcel() @@ -751,20 +572,18 @@ class AgentIncentiveController extends ResourceController // Output dynamic columns based on selected plan type if ($plan_type === 'comp') { $sheet->setCellValue('F' . $rowNumber, $row['comp']); - $sheet->setCellValue('G' . $rowNumber, $row['partner_comp']); + } elseif ($plan_type === 'tp') { $sheet->setCellValue('F' . $rowNumber, $row['tp']); - $sheet->setCellValue('G' . $rowNumber, $row['partner_tp']); + } elseif ($plan_type === 'od') { $sheet->setCellValue('F' . $rowNumber, $row['od']); - $sheet->setCellValue('G' . $rowNumber, $row['partner_od']); + } else { $sheet->setCellValue('F' . $rowNumber, $row['comp']); $sheet->setCellValue('G' . $rowNumber, $row['tp']); $sheet->setCellValue('H' . $rowNumber, $row['od']); - $sheet->setCellValue('I' . $rowNumber, $row['partner_comp']); - $sheet->setCellValue('J' . $rowNumber, $row['partner_tp']); - $sheet->setCellValue('K' . $rowNumber, $row['partner_od']); + } $rowNumber++; } @@ -792,52 +611,4 @@ class AgentIncentiveController extends ResourceController } } - // ------------------------------------------------------------------------- - // 3. Download the Originally Uploaded Reference Grid - // Route: $routes->get('agent/downloadExistGrid', 'AgentIncentiveController::downloadLastestGrid'); - // Purpose: Finds the most recently uploaded physical Excel file (where file_type='grid') - // from the partner_agent_incentive_file table and initiates a download. - // ------------------------------------------------------------------------- - public function downloadLastestGrid() - { - try { - // Find the latest file entry in the database where file_type is 'grid' - $latestFileRecord = $this->AgentIncentiveFileModel - ->where('file_type', 'grid') - ->orderBy('created_on', 'DESC') - ->first(); - - if (empty($latestFileRecord)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 404, - 'data' => 'No uploaded reference grid file found in the database.' - ], 404); - } - - // Construct the exact file path where it was saved during upload - $fileName = $latestFileRecord['incentive_file_name']; - $filePath = WRITEPATH . 'uploads/agent/incentive_file/' . $fileName; - - // Check if the physical file actually exists on the server - if (!file_exists($filePath)) { - return $this->respond([ - 'status' => 'failed', - 'code' => 404, - 'data' => 'The file record exists, but the physical file is missing from the server.' - ], 404); - } - - // Initiate the download of the physical file - return $this->response->download($filePath, null)->setFileName('Reference_Grid_' . $fileName); - - } catch (\Exception $e) { - return $this->respond([ - 'status' => 'failed', - 'code' => 500, - 'data' => 'Error attempting to download file: ' . $e->getMessage() - ], 500); - } - } - } diff --git a/app/Controllers/EndorsementController.php b/app/Controllers/EndorsementController.php index a3d16de..a788f7d 100644 --- a/app/Controllers/EndorsementController.php +++ b/app/Controllers/EndorsementController.php @@ -405,6 +405,7 @@ class EndorsementController extends ResourceController if (!empty($staff) && $staff['role_id'] == 4) { $updateData['is_data_accuracy_checked'] = 1; + $updateData['status'] = "Closed" ; } } diff --git a/app/Models/AgentIncentiveFileModel.php b/app/Models/AgentIncentiveFileModel.php index 0d56149..9a86903 100644 --- a/app/Models/AgentIncentiveFileModel.php +++ b/app/Models/AgentIncentiveFileModel.php @@ -33,9 +33,9 @@ class AgentIncentiveFileModel extends Model // Validation rules (optional, add as per your need) protected $validationRules = [ - 'agent_id' => 'required|integer', - 'incentive_month' => 'required|valid_date', - 'incentive_file_name'=> 'required|string|max_length[150]', - 'file_type' => 'permit_empty|max_length[50]' + 'agent_id' => 'permit_empty|integer', + 'incentive_month' => 'required|valid_date', + 'incentive_file_name' => 'required|string|max_length[150]', + 'file_type' => 'permit_empty|max_length[50]' ]; } diff --git a/app/Models/PartnerInsurancePayoutGridModel.php b/app/Models/PartnerInsurancePayoutGridModel.php index 983635b..bd7598c 100644 --- a/app/Models/PartnerInsurancePayoutGridModel.php +++ b/app/Models/PartnerInsurancePayoutGridModel.php @@ -24,12 +24,10 @@ class PartnerInsurancePayoutGridModel extends Model 'tp', 'od', 'remarks', - 'broker_excel_comp', - 'broker_excel_tp', - 'broker_excel_od', - 'partner_comp', - 'partner_tp', - 'partner_od', + 'broker_comp', + 'broker_tp', + 'broker_od', + 'partner_agent_incentive_file_id', 'created_by', 'updated_by', ]; @@ -54,56 +52,6 @@ class PartnerInsurancePayoutGridModel extends Model protected $skipValidation = false; - // ------------------------------------------------------------------------- - // Find by the natural UPSERT key - // ------------------------------------------------------------------------- - public function findByUpsertKey( - string $vehicleType, - string $insurer, - ?string $rto, - string $segment - ): ?array { - return $this - ->where('vehicle_type', $vehicleType) - ->where('insurer', $insurer) - ->where('rto', $rto ?? '') - ->where('segment', $segment) - ->first(); - } - - // ------------------------------------------------------------------------- - // Bulk UPSERT helper - // Accepts an array of grid rows and inserts or updates each one. - // Returns ['inserted' => int, 'updated' => int] - // ------------------------------------------------------------------------- - public function bulkUpsert(array $gridRows, ?int $userId = null): array - { - $inserted = 0; - $updated = 0; - - foreach ($gridRows as $row) { - $existing = $this->findByUpsertKey( - $row['vehicle_type'], - $row['insurer'], - $row['rto'] ?? null, - $row['segment'] - ); - - $row['updated_by'] = $userId; - - if (!empty($existing)) { - $this->update($existing['id'], $row); - $updated++; - } else { - $row['created_by'] = $userId; - $this->insert($row); - $inserted++; - } - } - - return ['inserted' => $inserted, 'updated' => $updated]; - } - // ------------------------------------------------------------------------- // Fetch grid filtered by vehicle_type // ------------------------------------------------------------------------- @@ -127,11 +75,4 @@ class PartnerInsurancePayoutGridModel extends Model ->findAll(); } - // ------------------------------------------------------------------------- - // Update a grid row by id - // ------------------------------------------------------------------------- - public function updateGridById(int $id, array $data): bool - { - return $this->update($id, $data); - } } From 08e296e3fda9680f674edca680fad67b26de70a5 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 27 Mar 2026 18:33:45 +0530 Subject: [PATCH 12/18] FIX_Changes and Additional Requirements ( Report BUGS ) --- app/Config/Routes.php | 7 +- app/Controllers/EndorsementController.php | 34 ++++- app/Controllers/ExcelExportController.php | 175 ++++++++++++++++++++++ app/Controllers/InvoiceController.php | 112 +++++++++++++- app/Controllers/PolicyController.php | 76 ++++++++++ app/Models/PolicyModel.php | 1 + 6 files changed, 395 insertions(+), 10 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 2ddf684..2e68cac 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -126,6 +126,7 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('policy/findPolicy', 'PolicyController::findPolicy'); $routes->post('policy/createPolicy', 'PolicyController::createPolicy'); $routes->post('policy/updatePolicy', 'PolicyController::updatePolicy'); + $routes->post('policy/updatePolicyCommission', 'PolicyController::updatePolicyCommission'); $routes->get('policy/downloadPolicyFile', 'PolicyController::downloadPolicyFile'); $routes->post('policy/uploadPolicyFile', 'PolicyController::uploadPolicyFile'); $routes->get("policy/searchThePolicies", "PolicyController::searchThePolicies"); @@ -196,8 +197,8 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f // GET /partner/{id}/earnings $routes->get('partner/(:num)/earnings', 'DashboardController::partnerEarnings/$1'); - - + // DASHBOARD Season 7 — Partner Portal + $routes->get('grid/download', 'ExcelExportController::downloadExcelGrid'); //invoice @@ -210,8 +211,6 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('invoice/getAgentUnusedCommissionList', 'InvoiceController::getAgentUnusedCommissionList'); $routes->post('invoice/bulk-upload-commission', 'InvoiceController::bulkUploadCommission'); $routes->post('invoice/bulk-upload-commission/proceed', 'InvoiceController::bulkUploadCommissionProceed'); - $routes->post('invoice/bulk-upload-commission', 'InvoiceController::bulkUploadCommission'); - $routes->post('invoice/bulk-upload-commission/proceed', 'InvoiceController::bulkUploadCommissionProceed'); // SALES EXECUTIVE diff --git a/app/Controllers/EndorsementController.php b/app/Controllers/EndorsementController.php index a788f7d..5a36221 100644 --- a/app/Controllers/EndorsementController.php +++ b/app/Controllers/EndorsementController.php @@ -316,11 +316,30 @@ class EndorsementController extends ResourceController $uploadRevisedFile->move($uploadRevisedPath, $uploadedRevisedCompletionFile); } - //COMMON UPDATE FIELDS + /* + * COMMON UPDATE FIELDS + * Keep update mapping explicit so fields sent by frontend are not silently ignored. + */ $updateData = []; // Update only if provided (avoid null overwrite) + if (isset($reqData['policy_from'])) { + $updateData['policy_from'] = $reqData['policy_from']; + } + + if (isset($reqData['policy_number'])) { + $updateData['policy_number'] = $reqData['policy_number']; + } + + if (isset($reqData['manager_id'])) { + $updateData['manager_id'] = $reqData['manager_id']; + } + + if (isset($reqData['agent_id'])) { + $updateData['agent_id'] = $reqData['agent_id']; + } + if (isset($reqData['endorsement_type'])) { $updateData['endorsement_type'] = $reqData['endorsement_type']; } @@ -349,6 +368,10 @@ class EndorsementController extends ResourceController $updateData['endorsement_premium'] = $reqData['endorsement_premium']; } + if (isset($reqData['commission_amount'])) { + $updateData['commission_amount'] = $reqData['commission_amount']; + } + if (isset($reqData['pending_days'])) { $updateData['pending_days'] = $reqData['pending_days']; } @@ -358,8 +381,13 @@ class EndorsementController extends ResourceController $updateData['endorsement_file_name'] = $uploadedOriginalCompletionFile; $updateData['endorsement_completion_file'] = $uploadedRevisedCompletionFile; // ✅ fixed variable - // External Policy Editable Fields - if ($endorsement['policy_from'] === 'External') { + /* + * External Policy Editable Fields + * Use incoming policy_from first (if provided), fallback to stored value. + * This prevents missing updates when stored value casing differs (external/External). + */ + $effectivePolicyFrom = $reqData['policy_from'] ?? $endorsement['policy_from'] ?? ''; + if (strcasecmp(trim((string)$effectivePolicyFrom), 'External') === 0) { if (isset($reqData['insurer_id'])) { $updateData['insurer_id'] = $reqData['insurer_id'] ?? null; diff --git a/app/Controllers/ExcelExportController.php b/app/Controllers/ExcelExportController.php index fe3b438..f8b3860 100644 --- a/app/Controllers/ExcelExportController.php +++ b/app/Controllers/ExcelExportController.php @@ -2550,4 +2550,179 @@ class ExcelExportController extends ResourceController } } + // grid/download?role=Manager means all list + // grid/download?role=Manager&insurer=HDFC&vehicle_type=TWO%20WHEELER&plan_type=comp + // grid/download?role=Manager&insurer=HDFC&rto=TN&segment=BIKE&plan_type=comp + // grid/download?role=Manager&insurer=HDFC&segment=BIKE&vehicle_type=TWO%20WHEELER&plan_type=comp + public function downloadExcelGrid() + { + try { + $request = $this->request; + + // ✅ Inputs + $role = trim((string) ($request->getGet('role') ?? '')); + $fileId = trim((string) ($request->getGet('file_id') ?? '')); + $insurer = trim((string) ($request->getGet('insurer') ?? '')); + $rto = trim((string) ($request->getGet('rto') ?? '')); + $segment = trim((string) ($request->getGet('segment') ?? '')); + $vehicleType = trim((string) ($request->getGet('vehicle_type') ?? '')); + $planType = strtolower(trim((string) ($request->getGet('plan_type') ?? ''))); + $search = trim((string) ($request->getGet('search') ?? '')); + $loggedId = trim((string) ($request->getGet('logged_id') ?? '')); + + if ($role === '') { + return $this->response->setJSON([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'Role is required to export payout grid.', + ]); + } + + $isAgent = strtolower($role) === 'agent'; + + // ✅ Get retention rate (only for Agent) + $retentionRate = 0; + if ($isAgent && $loggedId !== '') { + $agent = $this->db->table('partner_agent') + ->select('retention_rate') + ->where('id', $loggedId) + ->get() + ->getRowArray(); + + $retentionRate = isset($agent['retention_rate']) ? (float)$agent['retention_rate'] : 0; + } + + // ✅ Main query + $builder = $this->db->table('partner_insurance_payout_grid'); + + if ($fileId !== '') { + $builder->where('partner_agent_incentive_file_id', $fileId); + } else { + $maxFileId = $this->db->table('partner_insurance_payout_grid') + ->selectMax('partner_agent_incentive_file_id') + ->get() + ->getRow() + ->partner_agent_incentive_file_id ?? null; + + if (!empty($maxFileId)) { + $builder->where('partner_agent_incentive_file_id', $maxFileId); + } + } + + // ✅ Filters + if ($insurer !== '') $builder->where('insurer', $insurer); + if ($rto !== '') $builder->where('rto', $rto); + if ($segment !== '') $builder->where('segment', $segment); + if ($vehicleType !== '') $builder->where('vehicle_type', $vehicleType); + + if (in_array($planType, ['comp', 'tp', 'od'])) { + $builder->where("$planType IS NOT NULL", null, false) + ->where("$planType !=", '') + ->where("$planType !=", '0'); + } + + // ✅ Search + if ($search !== '') { + $builder->groupStart() + ->like('insurer', $search) + ->orLike('vehicle_type', $search) + ->orLike('segment', $search) + ->orLike('rto', $search) + ->orLike('remarks', $search) + ->groupEnd(); + } + + // ✅ Fetch rows + $rows = $builder + ->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, remarks') + ->orderBy('id', 'DESC') + ->get() + ->getResultArray(); + + // ========================= + // ✅ HEADERS + // ========================= + $headers = ['S.No', 'Insurer', 'Vehicle Type', 'Segment', 'RTO']; + + if ($isAgent) { + if ($planType === 'comp') { + $headers[] = 'Comp'; + } elseif ($planType === 'tp') { + $headers[] = 'TP'; + } elseif ($planType === 'od') { + $headers[] = 'OD'; + } + } else { + // ✅ Manager / Accounts + $headers[] = 'Comp'; + $headers[] = 'TP'; + $headers[] = 'OD'; + } + + $headers[] = 'Remarks'; + + // ========================= + // ✅ DATA + // ========================= + $data = []; + $sNo = 1; + + foreach ($rows as $row) { + + $comp = isset($row['comp']) ? (float)$row['comp'] : 0; + $tp = isset($row['tp']) ? (float)$row['tp'] : 0; + $od = isset($row['od']) ? (float)$row['od'] : 0; + + // ✅ Apply retention only for Agent + if ($isAgent) { + $comp -= $retentionRate; + $tp -= $retentionRate; + $od -= $retentionRate; + } + + $line = [ + $sNo++, + $row['insurer'] ?? '-', + $row['vehicle_type'] ?? '-', + $row['segment'] ?? '-', + $row['rto'] ?? '-', + ]; + + // ✅ Dynamic columns (Agent only) + if ($isAgent) { + if ($planType === 'comp') { + $line[] = $comp; + } elseif ($planType === 'tp') { + $line[] = $tp; + } elseif ($planType === 'od') { + $line[] = $od; + } + } else { + // ✅ Manager / Accounts → always all + $line[] = $comp; + $line[] = $tp; + $line[] = $od; + } + + $line[] = $row['remarks'] ?? '-'; + + $data[] = $line; + } + + // ✅ Export + $fileName = "grid_" . date('Ymd_His') . ".xlsx"; + + return $this->streamExcelFile($headers, $data, 'Payout Grid', $fileName, false); + + } catch (\Throwable $e) { + return $this->response->setJSON([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ]); + } + } + + + } diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index c275563..beb89d6 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -9,7 +9,9 @@ use App\Models\InvoiceModel; use App\Models\InvoiceItemModel; use App\Models\InvoiceUtrModel; use App\Models\PartnerAccountHistoryModel; +// use App\Models\AgentIncentiveFileModel; use CodeIgniter\Database\Exceptions\DataException; +// use PhpOffice\PhpSpreadsheet\IOFactory; class InvoiceController extends ResourceController { @@ -20,6 +22,7 @@ class InvoiceController extends ResourceController protected $InvoiceItemModel; protected $InvoiceUtrModel; protected $PartnerAccountHistoryModel; + // protected $AgentIncentiveFileModel; protected $db; public function __construct() @@ -31,6 +34,7 @@ class InvoiceController extends ResourceController $this->InvoiceItemModel = new InvoiceItemModel(); $this->InvoiceUtrModel = new InvoiceUtrModel(); $this->PartnerAccountHistoryModel = new PartnerAccountHistoryModel(); + // $this->AgentIncentiveFileModel = new AgentIncentiveFileModel(); $this->db = \Config\Database::connect(); } @@ -193,7 +197,7 @@ class InvoiceController extends ResourceController if (!$invoiceId) { // Generate invoice number only for CREATE - $invoiceData['invoice_no'] = $this->generateInvoiceNo($input['pos_id']); + $invoiceData['invoice_no'] = $this->generateInvoiceNo($input['pos_id'] ?? 0); $invoiceData['created_at'] = date('Y-m-d H:i:s'); $invoiceData['created_by'] = $input['created_by'] ?? 0; @@ -266,9 +270,8 @@ class InvoiceController extends ResourceController private function generateInvoiceNo($pos_id) { - $pos_id = !empty($pos_id) ? $pos_id : 0; $year = date('Y'); - $prefix = "NIIB/$pos_id/$year/"; + $prefix = !empty($pos_id) & $pos_id != 0 ? "NIIB/$pos_id/$year/" : "MIG/$year/"; // Get last invoice of current year $lastInvoice = $this->InvoiceModel @@ -592,6 +595,109 @@ class InvoiceController extends ResourceController } } + // public function bulkUploadCommission() + // { + // $this->db->transBegin(); + // try { + // $input = $this->request->getJSON(true); + // if (!is_array($input)) { + // $input = []; + // } + + // $post = $this->request->getPost(); + // if (!is_array($post)) { + // $post = []; + // } + + // $rows = $input['rows'] ?? []; + // $updatedBy = (int)($input['updated_by'] ?? ($post['created_by'] ?? 0)); + + // // Multipart flow: read excel on backend and store upload metadata. + // $file = $this->request->getFile('file_name'); + // if ($file && $file->isValid()) { + // $month = date('Y-m-d'); + // $uploadPath = WRITEPATH . 'uploads/agent/incentive_file/'; + // if (!is_dir($uploadPath)) { + // mkdir($uploadPath, 0777, true); + // } + + // $storedFileName = time() . '_' . $file->getRandomName(); + // $file->move($uploadPath, $storedFileName); + + // $this->AgentIncentiveFileModel->insert([ + // 'incentive_month' => $month, + // 'incentive_file_name' => $storedFileName, + // 'file_type' => 'invoice', + // 'is_active' => 1, + // 'created_by' => $updatedBy > 0 ? $updatedBy : null, + // ], true); + + // $spreadsheet = IOFactory::load($uploadPath . $storedFileName); + // $excelRows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); + + // if (empty($excelRows)) { + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'message' => 'Uploaded file is empty', + // ], 400); + // } + + // $headerRow = $excelRows[0] ?? []; + // $headerIndex = []; + // foreach ($headerRow as $idx => $headerValue) { + // $normalized = preg_replace('/[^a-z0-9]/', '', strtolower(trim((string)$headerValue))); + // if (!empty($normalized)) { + // $headerIndex[$normalized] = (int)$idx; + // } + // } + + // $policyIdx = $headerIndex['policynumber'] ?? null; + // $invoiceIdx = $headerIndex['invoicenumber'] ?? null; + // $commissionIdx = $headerIndex['commission'] ?? ($headerIndex['commissionamount'] ?? null); + + // if ($policyIdx === null || $commissionIdx === null) { + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'message' => 'Expected columns: Policy Number, Invoice Number, Commission or Commission Amount', + // ], 400); + // } + + // $fallbackInvoiceNo = trim((string)($post['invoice_number'] ?? '')); + // $rows = []; + // foreach ($excelRows as $index => $row) { + // if ($index === 0) { + // continue; + // } + + // $policyNo = trim((string)($row[$policyIdx] ?? '')); + // $invoiceNoFromFile = $invoiceIdx !== null ? trim((string)($row[$invoiceIdx] ?? '')) : ''; + // $invoiceNo = $invoiceNoFromFile !== '' ? $invoiceNoFromFile : $fallbackInvoiceNo; + // $commissionRaw = trim((string)($row[$commissionIdx] ?? '')); + + // if ($policyNo === '' && $invoiceNo === '' && $commissionRaw === '') { + // continue; + // } + + // $commission = (float)str_replace(',', '', $commissionRaw); + // if ($policyNo === '' || $invoiceNo === '' || !is_numeric(str_replace(',', '', $commissionRaw))) { + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'message' => 'Invalid data at line ' . ($index + 1), + // ], 400); + // } + + // $rows[] = [ + // 'line_no' => $index + 1, + // 'policy_number' => $policyNo, + // 'invoice_no' => $invoiceNo, + // 'commission_amount' => $commission, + // ]; + // } + // } + public function bulkUploadCommission() { $this->db->transBegin(); diff --git a/app/Controllers/PolicyController.php b/app/Controllers/PolicyController.php index 855ade8..ee4cac3 100644 --- a/app/Controllers/PolicyController.php +++ b/app/Controllers/PolicyController.php @@ -411,6 +411,82 @@ class PolicyController extends ResourceController } } + public function updatePolicyCommission() + { + try { + $data = $this->request->getJSON(true); + + if (empty($data['id'])) { + return $this->respond([ + 'status' => 'failed', + 'code' => 422, + 'data' => 'Policy id is required', + ], 422); + } + + if (!isset($data['commission_amount']) || $data['commission_amount'] === '') { + return $this->respond([ + 'status' => 'failed', + 'code' => 422, + 'data' => 'commission_amount is required', + ], 422); + } + + $policyId = (int) $data['id']; + $policy = $this->PolicyModel->find($policyId); + if (!$policy) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'Policy not found', + ], 404); + } + + $commission = (float) $data['commission_amount']; + if ($commission < 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 422, + 'data' => 'commission_amount must be positive', + ], 422); + } + + /* + * Commission-only update endpoint. + * Keeps the existing policy workflow unchanged and updates only required fields. + */ + $updateData = [ + 'commission_amount' => number_format($commission, 2, '.', ''), + 'updated_by' => $data['updated_by'] ?? null, + 'updated_on' => date('Y-m-d H:i:s'), + ]; + + if (!$this->PolicyModel->update($policyId, $updateData)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 422, + 'data' => $this->PolicyModel->errors(), + ], 422); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'policy_id' => (string) $policyId, + 'commission_amount' => $updateData['commission_amount'], + 'message' => 'Commission updated successfully', + ], + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ], 500); + } + } + public function uploadPolicyFile() { try { diff --git a/app/Models/PolicyModel.php b/app/Models/PolicyModel.php index b53c047..7dc61e5 100644 --- a/app/Models/PolicyModel.php +++ b/app/Models/PolicyModel.php @@ -30,6 +30,7 @@ class PolicyModel extends Model 'pt_oc_share_details_id', 'created_by', 'updated_by', + 'updated_on', // newly added 'tp', From 914a068e3305c56f9df7a1cb84ca03223a99b4c4 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 27 Mar 2026 20:05:32 +0530 Subject: [PATCH 13/18] FIX_POS --- app/Config/Routes.php | 3 + app/Controllers/InvoiceController.php | 279 ++++++++++++++++---------- 2 files changed, 176 insertions(+), 106 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 2e68cac..e69e7d7 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -206,11 +206,14 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('invoice/details', 'InvoiceController::findInvoiceWithItems'); $routes->post('invoice/create-or-update', 'InvoiceController::createOrUpdateInvoice'); $routes->post('invoice/add-payment', 'InvoiceController::addInvoicePayment'); + $routes->get('invoice/add-payment-history', 'InvoiceController::addPaymentHistory'); $routes->get('invoice/delete', 'InvoiceController::deleteInvoice'); $routes->post('invoice/commission-rate-list', 'InvoiceController::getCommissionRateList'); $routes->get('invoice/getAgentUnusedCommissionList', 'InvoiceController::getAgentUnusedCommissionList'); $routes->post('invoice/bulk-upload-commission', 'InvoiceController::bulkUploadCommission'); $routes->post('invoice/bulk-upload-commission/proceed', 'InvoiceController::bulkUploadCommissionProceed'); + $routes->post('invoice/bulk-upload-commission', 'InvoiceController::bulkUploadCommission'); + $routes->post('invoice/bulk-upload-commission/proceed', 'InvoiceController::bulkUploadCommissionProceed'); // SALES EXECUTIVE diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index beb89d6..2ec020c 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -9,9 +9,9 @@ use App\Models\InvoiceModel; use App\Models\InvoiceItemModel; use App\Models\InvoiceUtrModel; use App\Models\PartnerAccountHistoryModel; -// use App\Models\AgentIncentiveFileModel; +use App\Models\AgentIncentiveFileModel; use CodeIgniter\Database\Exceptions\DataException; -// use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\IOFactory; class InvoiceController extends ResourceController { @@ -22,7 +22,7 @@ class InvoiceController extends ResourceController protected $InvoiceItemModel; protected $InvoiceUtrModel; protected $PartnerAccountHistoryModel; - // protected $AgentIncentiveFileModel; + protected $AgentIncentiveFileModel; protected $db; public function __construct() @@ -34,7 +34,7 @@ class InvoiceController extends ResourceController $this->InvoiceItemModel = new InvoiceItemModel(); $this->InvoiceUtrModel = new InvoiceUtrModel(); $this->PartnerAccountHistoryModel = new PartnerAccountHistoryModel(); - // $this->AgentIncentiveFileModel = new AgentIncentiveFileModel(); + $this->AgentIncentiveFileModel = new AgentIncentiveFileModel(); $this->db = \Config\Database::connect(); } @@ -197,7 +197,7 @@ class InvoiceController extends ResourceController if (!$invoiceId) { // Generate invoice number only for CREATE - $invoiceData['invoice_no'] = $this->generateInvoiceNo($input['pos_id'] ?? 0); + $invoiceData['invoice_no'] = $this->generateInvoiceNo($input['pos_id']); $invoiceData['created_at'] = date('Y-m-d H:i:s'); $invoiceData['created_by'] = $input['created_by'] ?? 0; @@ -270,8 +270,9 @@ class InvoiceController extends ResourceController private function generateInvoiceNo($pos_id) { + $pos_id = !empty($pos_id) ? $pos_id : 0; $year = date('Y'); - $prefix = !empty($pos_id) & $pos_id != 0 ? "NIIB/$pos_id/$year/" : "MIG/$year/"; + $prefix = "NIIB/$pos_id/$year/"; // Get last invoice of current year $lastInvoice = $this->InvoiceModel @@ -595,116 +596,181 @@ class InvoiceController extends ResourceController } } - // public function bulkUploadCommission() - // { - // $this->db->transBegin(); - // try { - // $input = $this->request->getJSON(true); - // if (!is_array($input)) { - // $input = []; - // } + public function addPaymentHistory() + { + try { + $invoiceId = (int)($this->request->getGet('invoice_id') ?? 0); + if ($invoiceId <= 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'invoice_id is required' + ], 400); + } - // $post = $this->request->getPost(); - // if (!is_array($post)) { - // $post = []; - // } + $invoice = $this->InvoiceModel + ->select('id, invoice_no, invoice_amount') + ->where('id', $invoiceId) + ->where('is_active', 1) + ->first(); - // $rows = $input['rows'] ?? []; - // $updatedBy = (int)($input['updated_by'] ?? ($post['created_by'] ?? 0)); + if (empty($invoice)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message'=> 'Invoice not found' + ], 404); + } - // // Multipart flow: read excel on backend and store upload metadata. - // $file = $this->request->getFile('file_name'); - // if ($file && $file->isValid()) { - // $month = date('Y-m-d'); - // $uploadPath = WRITEPATH . 'uploads/agent/incentive_file/'; - // if (!is_dir($uploadPath)) { - // mkdir($uploadPath, 0777, true); - // } + $history = $this->db->table('partner_account_history pah') + ->select(' + pah.id, + pah.invoice_id, + pi.invoice_no, + pi.invoice_amount AS total_amount, + pah.paid_amount, + pah.paid_date, + pah.created_at, + COALESCE(ps.name, "-") AS createdby_name, + ( + pi.invoice_amount - COALESCE(( + SELECT SUM(pah2.paid_amount) + FROM partner_account_history pah2 + WHERE pah2.invoice_id = pah.invoice_id + AND pah2.is_active = 1 + AND pah2.id <= pah.id + ), 0) + ) AS balance_amount + ', false) + ->join('partner_invoice pi', 'pi.id = pah.invoice_id', 'inner') + ->join('partner_staff ps', 'ps.id = pah.created_by', 'left') + ->where('pah.invoice_id', $invoiceId) + ->where('pah.is_active', 1) + ->orderBy('pah.id', 'DESC') + ->get() + ->getResultArray(); - // $storedFileName = time() . '_' . $file->getRandomName(); - // $file->move($uploadPath, $storedFileName); - - // $this->AgentIncentiveFileModel->insert([ - // 'incentive_month' => $month, - // 'incentive_file_name' => $storedFileName, - // 'file_type' => 'invoice', - // 'is_active' => 1, - // 'created_by' => $updatedBy > 0 ? $updatedBy : null, - // ], true); - - // $spreadsheet = IOFactory::load($uploadPath . $storedFileName); - // $excelRows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); - - // if (empty($excelRows)) { - // return $this->respond([ - // 'status' => 'failed', - // 'code' => 400, - // 'message' => 'Uploaded file is empty', - // ], 400); - // } - - // $headerRow = $excelRows[0] ?? []; - // $headerIndex = []; - // foreach ($headerRow as $idx => $headerValue) { - // $normalized = preg_replace('/[^a-z0-9]/', '', strtolower(trim((string)$headerValue))); - // if (!empty($normalized)) { - // $headerIndex[$normalized] = (int)$idx; - // } - // } - - // $policyIdx = $headerIndex['policynumber'] ?? null; - // $invoiceIdx = $headerIndex['invoicenumber'] ?? null; - // $commissionIdx = $headerIndex['commission'] ?? ($headerIndex['commissionamount'] ?? null); - - // if ($policyIdx === null || $commissionIdx === null) { - // return $this->respond([ - // 'status' => 'failed', - // 'code' => 400, - // 'message' => 'Expected columns: Policy Number, Invoice Number, Commission or Commission Amount', - // ], 400); - // } - - // $fallbackInvoiceNo = trim((string)($post['invoice_number'] ?? '')); - // $rows = []; - // foreach ($excelRows as $index => $row) { - // if ($index === 0) { - // continue; - // } - - // $policyNo = trim((string)($row[$policyIdx] ?? '')); - // $invoiceNoFromFile = $invoiceIdx !== null ? trim((string)($row[$invoiceIdx] ?? '')) : ''; - // $invoiceNo = $invoiceNoFromFile !== '' ? $invoiceNoFromFile : $fallbackInvoiceNo; - // $commissionRaw = trim((string)($row[$commissionIdx] ?? '')); - - // if ($policyNo === '' && $invoiceNo === '' && $commissionRaw === '') { - // continue; - // } - - // $commission = (float)str_replace(',', '', $commissionRaw); - // if ($policyNo === '' || $invoiceNo === '' || !is_numeric(str_replace(',', '', $commissionRaw))) { - // return $this->respond([ - // 'status' => 'failed', - // 'code' => 400, - // 'message' => 'Invalid data at line ' . ($index + 1), - // ], 400); - // } - - // $rows[] = [ - // 'line_no' => $index + 1, - // 'policy_number' => $policyNo, - // 'invoice_no' => $invoiceNo, - // 'commission_amount' => $commission, - // ]; - // } - // } + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $history, + 'invoice'=> [ + 'invoice_id' => (int)$invoice['id'], + 'invoice_no' => $invoice['invoice_no'], + 'total_amount' => (float)$invoice['invoice_amount'], + ], + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> $e->getMessage() + ], 500); + } + } public function bulkUploadCommission() { $this->db->transBegin(); try { $input = $this->request->getJSON(true); + if (!is_array($input)) { + $input = []; + } + + $post = $this->request->getPost(); + if (!is_array($post)) { + $post = []; + } + $rows = $input['rows'] ?? []; - $updatedBy = (int)($input['updated_by'] ?? 0); + $updatedBy = (int)($input['updated_by'] ?? ($post['created_by'] ?? 0)); + + // Multipart flow: read excel on backend and store upload metadata. + $file = $this->request->getFile('file_name'); + if ($file && $file->isValid()) { + $month = date('Y-m-d'); + $uploadPath = WRITEPATH . 'uploads/agent/incentive_file/'; + if (!is_dir($uploadPath)) { + mkdir($uploadPath, 0777, true); + } + + $storedFileName = time() . '_' . $file->getRandomName(); + $file->move($uploadPath, $storedFileName); + + $this->AgentIncentiveFileModel->insert([ + 'incentive_month' => $month, + 'incentive_file_name' => $storedFileName, + 'file_type' => 'invoice', + 'is_active' => 1, + 'created_by' => $updatedBy > 0 ? $updatedBy : null, + ], true); + + $spreadsheet = IOFactory::load($uploadPath . $storedFileName); + $excelRows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); + + if (empty($excelRows)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'Uploaded file is empty', + ], 400); + } + + $headerRow = $excelRows[0] ?? []; + $headerIndex = []; + foreach ($headerRow as $idx => $headerValue) { + $normalized = preg_replace('/[^a-z0-9]/', '', strtolower(trim((string)$headerValue))); + if (!empty($normalized)) { + $headerIndex[$normalized] = (int)$idx; + } + } + + $policyIdx = $headerIndex['policynumber'] ?? null; + $invoiceIdx = $headerIndex['invoicenumber'] ?? null; + $commissionIdx = $headerIndex['commission'] ?? ($headerIndex['commissionamount'] ?? null); + + if ($policyIdx === null || $commissionIdx === null) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'Expected columns: Policy Number, Invoice Number, Commission or Commission Amount', + ], 400); + } + + $fallbackInvoiceNo = trim((string)($post['invoice_number'] ?? '')); + $rows = []; + foreach ($excelRows as $index => $row) { + if ($index === 0) { + continue; + } + + $policyNo = trim((string)($row[$policyIdx] ?? '')); + $invoiceNoFromFile = $invoiceIdx !== null ? trim((string)($row[$invoiceIdx] ?? '')) : ''; + $invoiceNo = $invoiceNoFromFile !== '' ? $invoiceNoFromFile : $fallbackInvoiceNo; + $commissionRaw = trim((string)($row[$commissionIdx] ?? '')); + + if ($policyNo === '' && $invoiceNo === '' && $commissionRaw === '') { + continue; + } + + $commission = (float)str_replace(',', '', $commissionRaw); + if ($policyNo === '' || $invoiceNo === '' || !is_numeric(str_replace(',', '', $commissionRaw))) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'Invalid data at line ' . ($index + 1), + ], 400); + } + + $rows[] = [ + 'line_no' => $index + 1, + 'policy_number' => $policyNo, + 'invoice_no' => $invoiceNo, + 'commission_amount' => $commission, + ]; + } + } if (empty($rows) || !is_array($rows)) { return $this->respond([ @@ -798,7 +864,8 @@ class InvoiceController extends ResourceController 'is_active' => 1, ]); - $this->db->transRollback(); + // Keep staged token/payload so proceed API can continue later. + $this->db->transCommit(); return $this->respond([ 'status' => 'partner_mismatch', 'code' => 200, From 2abe868d6f13696b2943f6c67018e79eaef29db8 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 27 Mar 2026 20:34:47 +0530 Subject: [PATCH 14/18] FIX_EmptyCommit --- app/Controllers/AgentController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Controllers/AgentController.php b/app/Controllers/AgentController.php index 5ad4302..29f6658 100644 --- a/app/Controllers/AgentController.php +++ b/app/Controllers/AgentController.php @@ -384,4 +384,6 @@ class AgentController extends ResourceController } } + //Empty Commit + } From 874476dd74a064725f8a6a8b99a93fac6fb9fbdb Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Mon, 30 Mar 2026 11:15:04 +0530 Subject: [PATCH 15/18] FIX_comm.amount >= 0 --- app/Controllers/InvoiceController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index 2ec020c..1725700 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -432,7 +432,7 @@ class InvoiceController extends ResourceController ->join('partner_invoice_items pii', 'pii.policy_id = partner_policy.id', 'left') ->join('partner_invoice pi', 'pi.id = pii.invoice_id AND pi.is_active = 1', 'left') ->where('partner_policy.is_active', 1) - ->where('partner_policy.commission_amount >', 0) + ->where('partner_policy.commission_amount >=', 0) ->where('partner_policy.is_data_accuracy_checked', 1) ->where('partner_policy.manager_id', $manager_id) ->where('pii.policy_id IS NULL') // ❗ unused commission From 8ba7fc6ccb216898e225c4401fd297d9f8184f63 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Mon, 30 Mar 2026 14:28:22 +0530 Subject: [PATCH 16/18] FIX_Comm.amount 2 --- app/Controllers/InvoiceController.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index 1725700..2262a25 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -178,12 +178,16 @@ class InvoiceController extends ResourceController try { $input = $this->request->getJSON(true); + // Normalize pos_id (client may send null/0/non-numeric like "w") + $posIdRaw = $input['pos_id'] ?? 0; + $posId = is_numeric($posIdRaw) ? (int)$posIdRaw : 0; + // Basic invoice data $invoiceData = [ 'invoice_amount' => $input['invoice_amount'] ?? 0, 'broker_id' => $input['broker_id'] ?? 0, 'agent_id' => json_encode($input['agent_id'] ?? []), - 'pos_id' => $input['pos_id'] ?? 0, + 'pos_id' => $posId, 'till_date' => $input['till_date'] ? date('Y-m-d', strtotime($input['till_date'])) : null, 'invoice_date' => $input['invoice_date'] ? date('Y-m-d', strtotime($input['invoice_date'])) : null, 'payout_status' => 1, @@ -197,7 +201,7 @@ class InvoiceController extends ResourceController if (!$invoiceId) { // Generate invoice number only for CREATE - $invoiceData['invoice_no'] = $this->generateInvoiceNo($input['pos_id']); + $invoiceData['invoice_no'] = $this->generateInvoiceNo($posId); $invoiceData['created_at'] = date('Y-m-d H:i:s'); $invoiceData['created_by'] = $input['created_by'] ?? 0; @@ -272,7 +276,12 @@ class InvoiceController extends ResourceController { $pos_id = !empty($pos_id) ? $pos_id : 0; $year = date('Y'); - $prefix = "NIIB/$pos_id/$year/"; + + // If pos_id is missing/invalid (0/null/"w"), generate invoice as MIG/Year/NNNNN + // Otherwise generate as NIIB/{pos_id}/Year/NNNNN + $prefix = ($pos_id > 0) + ? "NIIB/{$pos_id}/{$year}/" + : "MIG/{$year}/"; // Get last invoice of current year $lastInvoice = $this->InvoiceModel From 1407f7c224beccc47d457dfb88f6c39d5f45fbee Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Mon, 30 Mar 2026 19:07:57 +0530 Subject: [PATCH 17/18] FIX_UTR Number --- app/Controllers/InvoiceController.php | 73 ++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index 2262a25..9ead9c0 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -197,6 +197,7 @@ class InvoiceController extends ResourceController ]; $invoiceId = $input['id'] ?? null; + $isCreate = empty($invoiceId); if (!$invoiceId) { @@ -252,6 +253,70 @@ class InvoiceController extends ResourceController } } + + + /* + * UTR save (Tempa) + */ + if ($isCreate && !empty($input['utrs']) && is_array($input['utrs'])) { + + $seenUtrs = []; + + foreach ($input['utrs'] as $utrRow) { + + $utrNo = trim((string)($utrRow['utr_no'] ?? '')); + + + if ($utrNo === '') continue; + + + // if (in_array($utrNo, $seenUtrs, true)) { + // $this->db->transRollback(); + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'message'=> "Duplicate UTR in request: {$utrNo}" + // ], 400); + // } + + $seenUtrs[] = $utrNo; + + // $exists = $this->InvoiceUtrModel->where('utr_no', $utrNo)->where('is_active', 1)->first(); + + // if ($exists) { + // $this->db->transRollback(); + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'message'=> "UTR already exists: {$utrNo}" + // ], 400); + // } + + // ✅ Format values + // $utrDate = !empty($utrRow['utr_date']) + // ? date('Y-m-d', strtotime($utrRow['utr_date'])) + // : null; + + // $amount = isset($utrRow['amount']) + // ? (float)$utrRow['amount'] + // : 0; + + $result_utr = $this->InvoiceUtrModel->insert([ + 'invoice_id' => $invoiceId, + 'utr_no' => $utrNo, + 'utr_date' => null, + 'amount' => 0, + 'is_active' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'created_by' => $input['created_by'] ?? 0 + ]); + + // if (!$result_utr) { + // print_r($this->InvoiceUtrModel->errors()); + // die; + // } + } + } if ($this->db->transStatus() === false) { $this->db->transRollback(); @@ -340,7 +405,13 @@ class InvoiceController extends ResourceController // Build the main query $query = $this->PolicyModel - ->select('partner_policy.policy_number as policy_no, partner_policy.id as policy_id, partner_policy.issued_date, partner_policy.commission_amount, partner_policy.insured_name as customer_name, pe.agent_id ,pa.name as agent_name,pa.agent_code, partner_policy.premium_amount, pi.id as invoice_id, pi.invoice_no') + ->select('partner_policy.policy_number as policy_no, partner_policy.id as policy_id, partner_policy.issued_date, partner_policy.commission_amount, partner_policy.insured_name as customer_name, pe.agent_id ,pa.name as agent_name,pa.agent_code, partner_policy.premium_amount, pi.id as invoice_id, pi.invoice_no, + COALESCE(( + SELECT GROUP_CONCAT(piu.utr_no ORDER BY piu.id SEPARATOR ", ") + FROM partner_invoice_utr piu + WHERE piu.invoice_id = pi.id + AND piu.is_active = 1 + ), "") as utr_no', false) ->join( 'partner_enquiry pe', 'pe.id = partner_policy.enquiry_id AND pe.broker_id = ' . (int)$input['broker_id'], From d6452b7467e3405aa3be2af634a74f901f4945cb Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Tue, 31 Mar 2026 18:25:01 +0530 Subject: [PATCH 18/18] FIX_UTR Backend Code Changes --- app/Config/Routes.php | 9 +- app/Controllers/InvoiceController.php | 302 ++++++++++++++++------ app/Models/PartnerAccountHistoryModel.php | 39 --- 3 files changed, 237 insertions(+), 113 deletions(-) delete mode 100644 app/Models/PartnerAccountHistoryModel.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e69e7d7..59f115f 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -205,8 +205,13 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->get('invoice/list', 'InvoiceController::invoiceList'); $routes->get('invoice/details', 'InvoiceController::findInvoiceWithItems'); $routes->post('invoice/create-or-update', 'InvoiceController::createOrUpdateInvoice'); - $routes->post('invoice/add-payment', 'InvoiceController::addInvoicePayment'); - $routes->get('invoice/add-payment-history', 'InvoiceController::addPaymentHistory'); + $routes->get('invoice/utrDetails', 'InvoiceController::utrDetails'); + $routes->post('invoice/add-payment', 'InvoiceController::addInvoicePayment'); // tempo + $routes->post('invoice/updateUtrDetails', 'InvoiceController::updateUtrDetails'); + $routes->get('invoice/add-payment-history', 'InvoiceController::addPaymentHistory'); // tempo + $routes->post('invoice/add-payment-history', 'InvoiceController::addPaymentHistory'); // tempo + $routes->get('invoice/listUtrDetails', 'InvoiceController::listUtrDetails'); + $routes->post('invoice/listUtrDetails', 'InvoiceController::listUtrDetails'); $routes->get('invoice/delete', 'InvoiceController::deleteInvoice'); $routes->post('invoice/commission-rate-list', 'InvoiceController::getCommissionRateList'); $routes->get('invoice/getAgentUnusedCommissionList', 'InvoiceController::getAgentUnusedCommissionList'); diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index 9ead9c0..3325e7d 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -8,7 +8,6 @@ use App\Models\QuotationModel; use App\Models\InvoiceModel; use App\Models\InvoiceItemModel; use App\Models\InvoiceUtrModel; -use App\Models\PartnerAccountHistoryModel; use App\Models\AgentIncentiveFileModel; use CodeIgniter\Database\Exceptions\DataException; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -21,7 +20,6 @@ class InvoiceController extends ResourceController protected $InvoiceModel; protected $InvoiceItemModel; protected $InvoiceUtrModel; - protected $PartnerAccountHistoryModel; protected $AgentIncentiveFileModel; protected $db; @@ -33,7 +31,6 @@ class InvoiceController extends ResourceController $this->InvoiceModel = new InvoiceModel(); $this->InvoiceItemModel = new InvoiceItemModel(); $this->InvoiceUtrModel = new InvoiceUtrModel(); - $this->PartnerAccountHistoryModel = new PartnerAccountHistoryModel(); $this->AgentIncentiveFileModel = new AgentIncentiveFileModel(); $this->db = \Config\Database::connect(); } @@ -61,13 +58,6 @@ class InvoiceController extends ResourceController ) AS utr_numbers, partner_invoice.invoice_amount AS invoiced_amount, ( - COALESCE(( - SELECT SUM(pah.paid_amount) - FROM partner_account_history pah - WHERE pah.invoice_id = partner_invoice.id - AND pah.is_active = 1 - ), 0.00) - + COALESCE(( SELECT SUM(piu.amount) FROM partner_invoice_utr piu @@ -77,13 +67,6 @@ class InvoiceController extends ResourceController ) AS payout_amount, ( partner_invoice.invoice_amount - ( - COALESCE(( - SELECT SUM(pah.paid_amount) - FROM partner_account_history pah - WHERE pah.invoice_id = partner_invoice.id - AND pah.is_active = 1 - ), 0.00) - + COALESCE(( SELECT SUM(piu.amount) FROM partner_invoice_utr piu @@ -129,18 +112,14 @@ class InvoiceController extends ResourceController ->where('partner_invoice_items.is_active', 1) ->findAll(); - $paymentHistory = $this->PartnerAccountHistoryModel + $paymentHistory = $this->InvoiceUtrModel + ->select('id, invoice_id, amount as paid_amount, utr_no, utr_date as paid_date, created_at, created_by') ->where('invoice_id', $id) ->where('is_active', 1) - ->orderBy('paid_date', 'DESC') + ->where('amount >', 0) ->orderBy('id', 'DESC') ->findAll(); - $paidAmount = 0.00; - foreach ($paymentHistory as $payment) { - $paidAmount += (float) ($payment['paid_amount'] ?? 0); - } - $utrPaidAmount = (float) ( $this->InvoiceUtrModel ->selectSum('amount', 'total') @@ -149,7 +128,7 @@ class InvoiceController extends ResourceController ->first()['total'] ?? 0 ); - $totalPaidAmount = $paidAmount + $utrPaidAmount; + $totalPaidAmount = $utrPaidAmount; $invoiceAmount = (float) ($invoice['invoice_amount'] ?? 0); $balanceAmount = max($invoiceAmount - $totalPaidAmount, 0); @@ -546,7 +525,7 @@ class InvoiceController extends ResourceController } } - public function addInvoicePayment() + public function updateUtrDetails() { $this->db->transBegin(); @@ -556,8 +535,10 @@ class InvoiceController extends ResourceController $invoiceId = (int)($input['invoice_id'] ?? 0); $paidAmount = (float)($input['paid_amount'] ?? 0); $paidDateRaw = $input['paid_date'] ?? null; + $utrId = (int)($input['utr_id'] ?? 0); + $utrNoInput = trim((string)($input['utr_no'] ?? '')); - if ($invoiceId <= 0 || $paidAmount <= 0 || empty($paidDateRaw)) { + if ($invoiceId <= 0 || $paidAmount < 0 || empty($paidDateRaw)) { return $this->respond([ 'status' => 'failed', 'code' => 400, @@ -588,14 +569,6 @@ class InvoiceController extends ResourceController } $paidDate = date('Y-m-d', $paidDateTs); - $historyPaidAmount = (float) ( - $this->PartnerAccountHistoryModel - ->selectSum('paid_amount', 'total') - ->where('invoice_id', $invoiceId) - ->where('is_active', 1) - ->first()['total'] ?? 0 - ); - $utrPaidAmount = (float) ( $this->InvoiceUtrModel ->selectSum('amount', 'total') @@ -604,11 +577,96 @@ class InvoiceController extends ResourceController ->first()['total'] ?? 0 ); - $currentTotalPaid = $historyPaidAmount + $utrPaidAmount; + $currentTotalPaid = $utrPaidAmount; $invoiceAmount = (float) ($invoice['invoice_amount'] ?? 0); $remainingBalance = max($invoiceAmount - $currentTotalPaid, 0); - if ($paidAmount > $remainingBalance) { + /* + * If utr_id is empty/0 from request: + * 1) create a fresh UTR row with given utr_no (or generated fallback), + * 2) keep amount = 0 first, + * 3) then continue existing update flow using the new utr_id. + */ + if ($utrId <= 0) { + $utrNo = $utrNoInput !== '' ? $utrNoInput : ('UTR' . date('YmdHis')); + while ( + !empty( + $this->InvoiceUtrModel + ->where('invoice_id', $invoiceId) + ->where('utr_no', $utrNo) + ->where('is_active', 1) + ->first() + ) + ) { + usleep(200000); + $utrNo = 'UTR' . date('YmdHis'); + } + + $insertedUtrId = $this->InvoiceUtrModel->insert([ + 'invoice_id' => $invoiceId, + 'utr_no' => $utrNo, + 'utr_date' => null, + 'amount' => 0, + 'is_active' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'created_by' => (int)($input['created_by'] ?? 0), + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => (int)($input['updated_by'] ?? 0), + ]); + + if ($insertedUtrId === false) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> 'Failed to create UTR', + 'error' => $this->InvoiceUtrModel->errors() + ], 500); + } + + $utrId = (int)$insertedUtrId; + } + + $utrQuery = $this->InvoiceUtrModel + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->where('id', $utrId); + $utrRow = $utrQuery->first(); + + if (empty($utrRow)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message'=> 'Selected UTR not found for this invoice' + ], 404); + } + + $existingUtrAmount = (float)($utrRow['amount'] ?? 0); + if ($paidAmount > ($remainingBalance + $existingUtrAmount)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'Paid amount exceeds invoice balance' + ], 400); + } + if ($paidAmount < $existingUtrAmount) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'Paid amount cannot be less than existing paid amount' + ], 400); + } + + $otherUtrPaidAmount = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->where('id !=', (int)$utrRow['id']) + ->first()['total'] ?? 0 + ); + $maxAllowedForSelectedUtr = max($invoiceAmount - $otherUtrPaidAmount, 0); + if ($paidAmount > $maxAllowedForSelectedUtr) { return $this->respond([ 'status' => 'failed', 'code' => 400, @@ -616,30 +674,24 @@ class InvoiceController extends ResourceController ], 400); } - $historyData = [ - 'invoice_id' => $invoiceId, - 'paid_amount' => $paidAmount, - 'paid_date' => $paidDate, - 'is_active' => 1, - 'created_at' => date('Y-m-d H:i:s'), - 'created_by' => (int)($input['created_by'] ?? 0), - 'updated_at' => date('Y-m-d H:i:s'), - 'updated_by' => (int)($input['updated_by'] ?? 0), - ]; + $updated = $this->InvoiceUtrModel->update((int)$utrRow['id'], [ + 'amount' => $paidAmount, + 'utr_date' => $paidDate, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => (int)($input['updated_by'] ?? 0), + ]); - $historyId = $this->PartnerAccountHistoryModel->insert($historyData); - - if ($historyId === false) { + if ($updated === false) { $this->db->transRollback(); return $this->respond([ 'status' => 'failed', 'code' => 500, - 'message'=> 'Failed to record payment', - 'error' => $this->PartnerAccountHistoryModel->errors() + 'message'=> 'Failed to update UTR payment', + 'error' => $this->InvoiceUtrModel->errors() ], 500); } - $latestTotalPaid = $currentTotalPaid + $paidAmount; + $latestTotalPaid = ($currentTotalPaid - $existingUtrAmount) + $paidAmount; $newBalance = max($invoiceAmount - $latestTotalPaid, 0); $payoutStatus = $newBalance == 0.0 ? 2 : 1; @@ -660,7 +712,8 @@ class InvoiceController extends ResourceController 'status' => 'success', 'code' => 200, 'data' => [ - 'history_id' => $historyId, + 'utr_id' => (int)$utrRow['id'], + 'utr_no' => (string)$utrRow['utr_no'], 'invoice_id' => $invoiceId, 'paid_amount' => $latestTotalPaid, 'balance_amount' => $newBalance @@ -676,7 +729,12 @@ class InvoiceController extends ResourceController } } - public function addPaymentHistory() + public function addInvoicePayment() + { + return $this->updateUtrDetails(); + } + + public function listUtrDetails() { try { $invoiceId = (int)($this->request->getGet('invoice_id') ?? 0); @@ -702,42 +760,142 @@ class InvoiceController extends ResourceController ], 404); } - $history = $this->db->table('partner_account_history pah') + $history = $this->db->table('partner_invoice_utr piu') ->select(' - pah.id, - pah.invoice_id, + piu.id, + piu.invoice_id, pi.invoice_no, pi.invoice_amount AS total_amount, - pah.paid_amount, - pah.paid_date, - pah.created_at, - COALESCE(ps.name, "-") AS createdby_name, + piu.amount AS paid_amount, + piu.utr_no, + piu.utr_date AS paid_date, + piu.created_at, + COALESCE(psu.name, psc.name, "-") AS createdby_name, ( pi.invoice_amount - COALESCE(( - SELECT SUM(pah2.paid_amount) - FROM partner_account_history pah2 - WHERE pah2.invoice_id = pah.invoice_id - AND pah2.is_active = 1 - AND pah2.id <= pah.id + SELECT SUM(piu2.amount) + FROM partner_invoice_utr piu2 + WHERE piu2.invoice_id = piu.invoice_id + AND piu2.is_active = 1 + AND piu2.amount > 0 + AND piu2.id <= piu.id ), 0) ) AS balance_amount ', false) - ->join('partner_invoice pi', 'pi.id = pah.invoice_id', 'inner') - ->join('partner_staff ps', 'ps.id = pah.created_by', 'left') - ->where('pah.invoice_id', $invoiceId) - ->where('pah.is_active', 1) - ->orderBy('pah.id', 'DESC') + ->join('partner_invoice pi', 'pi.id = piu.invoice_id', 'inner') + ->join('partner_staff psc', 'psc.id = piu.created_by', 'left') + ->join('partner_staff psu', 'psu.id = piu.updated_by', 'left') + ->where('piu.invoice_id', $invoiceId) + ->where('piu.is_active', 1) + ->where('piu.amount >', 0) + ->orderBy('piu.id', 'DESC') ->get() ->getResultArray(); + // Return unique UTR list for this invoice (remove duplicates by utr_no). + // Keep only unpaid UTR rows (amount 0 or null). + $availableUtrs = $this->db->table('partner_invoice_utr') + ->select('MIN(id) as id, utr_no') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->where('(amount IS NULL OR amount <= 0)', null, false) + ->where('TRIM(COALESCE(utr_no, "")) !=', '') + ->groupBy('utr_no') + ->orderBy('MIN(id)', 'ASC', false) + ->get() + ->getResultArray(); + + $paidToDate = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + $balanceAmount = max(((float)$invoice['invoice_amount']) - $paidToDate, 0); + return $this->respond([ 'status' => 'success', 'code' => 200, 'data' => $history, + 'available_utrs' => $availableUtrs, 'invoice'=> [ 'invoice_id' => (int)$invoice['id'], 'invoice_no' => $invoice['invoice_no'], 'total_amount' => (float)$invoice['invoice_amount'], + 'paid_to_date' => $paidToDate, + 'balance_amount' => $balanceAmount, + ], + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> $e->getMessage() + ], 500); + } + } + + public function addPaymentHistory() + { + return $this->listUtrDetails(); + } + + public function utrDetails() + { + try { + $invoiceId = (int)($this->request->getGet('invoice_id') ?? 0); + if ($invoiceId <= 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'invoice_id is required' + ], 400); + } + + $invoice = $this->InvoiceModel + ->select('id, invoice_no, invoice_amount') + ->where('id', $invoiceId) + ->where('is_active', 1) + ->first(); + + if (empty($invoice)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message'=> 'Invoice not found' + ], 404); + } + + $utrDetails = $this->InvoiceUtrModel + ->select('id, invoice_id, amount as paid_amount, utr_no, utr_date') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->orderBy('id', 'ASC') + ->findAll(); + + $paidToDate = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + + $balanceAmount = max(((float)$invoice['invoice_amount']) - $paidToDate, 0); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'utrs_details' => $utrDetails, + 'invoice' => [ + 'invoice_id' => (int)$invoice['id'], + 'invoice_no' => $invoice['invoice_no'], + 'total_amount' => (float)$invoice['invoice_amount'], + 'paid_to_date' => $paidToDate, + 'balance_amount' => $balanceAmount, + ], ], ], 200); } catch (\Exception $e) { diff --git a/app/Models/PartnerAccountHistoryModel.php b/app/Models/PartnerAccountHistoryModel.php deleted file mode 100644 index 4d55bf4..0000000 --- a/app/Models/PartnerAccountHistoryModel.php +++ /dev/null @@ -1,39 +0,0 @@ - 'required|integer', - 'paid_amount' => 'required|decimal', - 'paid_date' => 'required|valid_date', - ]; - - protected $validationMessages = []; - protected $skipValidation = false; -}