From 0fb92252dcb3b4c8238a08464ebe7f5eca4070c5 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Wed, 25 Mar 2026 15:41:05 +0530 Subject: [PATCH] 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(); + } +}