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 0000000..bdadf05 Binary files /dev/null and b/writable/uploads/sample_partner_grid_file.xls differ