diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 6718cc1..5a6df97 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -261,6 +261,7 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->group('test', [ ], function ($routes) { $routes->get("policyCommissionCalculationAndGeminiPolicyRead", "PolicyController::policyCommissionCalculationAndGeminiPolicyRead"); + $routes->get("policyRagRead", "PolicyRagController::policyRagRead"); }); diff --git a/app/Controllers/JobWorker.php b/app/Controllers/JobWorker.php index c339475..09c8857 100755 --- a/app/Controllers/JobWorker.php +++ b/app/Controllers/JobWorker.php @@ -18,7 +18,7 @@ class JobWorker extends BaseController [ 'readFileAndCalculateCommission' => [ 'type' => 'CC', // Handler Category (Possible values: HC, CC, HF) - 'handler' => 'App\Controllers\PolicyController', + 'handler' => 'App\Controllers\PolicyRagController', ], ]; @@ -39,29 +39,32 @@ class JobWorker extends BaseController public static function processJobs(array $jobdata = []) { - // echo 'listen';//die(); + $db = \Config\Database::connect(); + + $runningJob = $db->query( + "SELECT id FROM partner_jobs WHERE status=? LIMIT 1", + [self::STATUS_RUNNING] + )->getRow(); + + if ($runningJob !== null) { + SELF::streamOutput("Another job is already running (id: {$runningJob->id}). Skipping.\n"); + return false; + } + $query = " SELECT id, name, payload, uuid FROM partner_jobs WHERE status=? - ORDER BY created_dt ASC"; - $where_condition = [self::STATUS_QUEUED]; - $db = \Config\Database::connect(); - $jobs = $db->query($query, $where_condition)->getResult(); + ORDER BY created_dt ASC + LIMIT 1"; + $jobs = $db->query($query, [self::STATUS_QUEUED])->getResult(); - if(count($jobs)) - { - // echo count($jobs); - // print_r($jobs);die; - foreach($jobs as $key => $job) - { - // echo $job->id.' - '.$job->name; - //sleep(1); - - SELF::processjob(['id' => $job->id,'uuid' => $job->uuid]); - // usleep( 500000 ); - } + if (count($jobs)) { + $job = $jobs[0]; + return SELF::processJob(['id' => $job->id, 'uuid' => $job->uuid]); } + + return false; } /** * process jobs @@ -72,27 +75,42 @@ class JobWorker extends BaseController // print_r($jobdata); // echo 'listen'; // die(); - $query = " + + $db = \Config\Database::connect(); + $db->transStart(); + + // Do not start a new job while another job is already running. + $runningJob = $db->query( + "SELECT id FROM partner_jobs WHERE status=? LIMIT 1", + [self::STATUS_RUNNING] + )->getRow(); + + if ($runningJob !== null) { + $db->transComplete(); + SELF::streamOutput("Another job is already running (id: {$runningJob->id}). Skipping.\n"); + return false; + } + + if (isset($jobdata) && count($jobdata)) { + $query = " + SELECT id, name, payload, uuid + FROM partner_jobs + WHERE status=? AND id=? AND uuid=? + ORDER BY created_dt ASC + LIMIT 1 FOR UPDATE"; + $where_condition = [self::STATUS_QUEUED, $jobdata['id'], $jobdata['uuid']]; + } else { + $query = " SELECT id, name, payload, uuid FROM partner_jobs WHERE status=? ORDER BY created_dt ASC LIMIT 1 FOR UPDATE"; - $where_condition = [self::STATUS_QUEUED]; - - if(isset($jobdata) && count($jobdata)) - { - $query = " - SELECT id, name, payload, uuid - FROM partner_jobs - WHERE status=? AND id=? AND uuid=? - ORDER BY created_dt ASC - LIMIT 1 FOR UPDATE"; - $where_condition = [self::STATUS_QUEUED,$jobdata['id'],$jobdata['uuid']]; + $where_condition = [self::STATUS_QUEUED]; } - - $db = \Config\Database::connect(); + $job = $db->query($query, $where_condition)->getResult(); + $db->transComplete(); // print_r($job);die(); if ($job !== []) { diff --git a/app/Controllers/PolicyRagController.php b/app/Controllers/PolicyRagController.php new file mode 100644 index 0000000..fe413ba --- /dev/null +++ b/app/Controllers/PolicyRagController.php @@ -0,0 +1,540 @@ + 'Two Wheeler', + '2 wheeler' => 'Two Wheeler', + '2w' => 'Two Wheeler', + 'motorcycle' => 'Two Wheeler', + 'bike' => 'Two Wheeler', + 'scooter' => 'Two Wheeler', + 'moped' => 'Two Wheeler', + 'four wheeler' => 'Four Wheeler', + '4 wheeler' => 'Four Wheeler', + '4w' => 'Four Wheeler', + 'private car' => 'Four Wheeler', + 'car' => 'Four Wheeler', + 'jeep' => 'Four Wheeler', + 'goods vehicle' => 'Goods Vehicle', + 'good vehicle' => 'Goods Vehicle', + 'goods carrier' => 'Goods Vehicle', + 'goods carrying vehicle' => 'Goods Vehicle', + 'cargo vehicle' => 'Goods Vehicle', + 'lorry' => 'Goods Vehicle', + 'truck' => 'Goods Vehicle', + 'transport vehicle goods'=> 'Goods Vehicle', + 'commercial vehicle' => 'Commercial Vehicle', + 'commercial car' => 'Commercial Vehicle', + 'taxi' => 'Commercial Vehicle', + 'cab' => 'Commercial Vehicle', + 'hire vehicle' => 'Commercial Vehicle', + 'passenger commercial' => 'Commercial Vehicle', + 'bus' => 'Bus', + 'passenger bus' => 'Bus', + 'school bus' => 'Bus', + 'private bus' => 'Bus', + 'stage carriage' => 'Bus', + 'tractor' => 'Tractor', + 'agricultural tractor' => 'Tractor', + 'farm tractor' => 'Tractor', + ]; + + protected array $fuelType = [ + 'Petrol' => 'Petrol', + 'Diesel' => 'Diesel', + 'CNG' => 'CNG', + 'LPG' => 'LPG', + ]; + + public function __construct() + { + $this->PolicyModel = new PolicyModel(); + } + + /** + * Job handler: read policy PDF via RAG API, update DB, then calculate commission. + */ + public function readFileAndCalculateCommission(array $data): array + { + $policyId = $data['policy_id']; + + log_message('debug', 'PolicyRag readFileAndCalculateCommission started for policy_id: ' . $policyId); + + $readDoc = $this->readPolicyDocViaRag($policyId); + $calculateCommission = ['status' => 'failed']; + + if ($readDoc['status'] === 'success') { + $extracted = $readDoc['data']; + $policyData = $this->mapExtractedDataToPolicyFields($extracted); + + log_message('debug', 'Policy RAG Update Payload: ' . json_encode($policyData)); + + $updateStatus = $this->PolicyModel->update($policyId, $policyData); + + if ($updateStatus) { + log_message('debug', 'Policy updated successfully via RAG for ID: ' . $policyId); + $policyController = new PolicyController(); + $calculateCommission = $policyController->calculateCommission($policyId); + } else { + log_message('error', 'Failed to update policy via RAG for ID: ' . $policyId); + } + } else { + log_message('error', 'readPolicyDocViaRag returned failure: ' . json_encode($readDoc)); + } + + return [ + 'readPolicyDocViaRag' => $readDoc['status'], + 'calculateCommission' => $calculateCommission['status'] ?? 'failed', + ]; + } + + /** + * Test endpoint: ?type=read&policy_id=123 + */ + public function policyRagRead() + { + $type = $this->request->getGet('type'); + $policyId = $this->request->getGet('policy_id'); + + if ($type === 'read') { + $readDoc = $this->readPolicyDocViaRag($policyId); + + if ($readDoc['status'] === 'success') { + $policyData = $this->mapExtractedDataToPolicyFields($readDoc['data']); + $this->PolicyModel->update($policyId, $policyData); + } + + return $this->respond(['status' => $readDoc['status'], 'data' => $readDoc], 200); + } + + if ($type === 'commission') { + $policyController = new PolicyController(); + return $policyController->calculateCommission($policyId, true); + } + + return $this->respond(['status' => 'failed', 'message' => 'Invalid type. Use type=read or type=commission'], 200); + } + + /** + * Upload PDF to RAG, poll until indexed, extract JSON via chat, then delete RAG file. + */ + public function readPolicyDocViaRag($policyId = null) + { + $fileId = null; + + try { + $record = $this->PolicyModel->find((int) $policyId); + + if (!$record) { + return $this->formatReadResponse('failed', 'Policy not found'); + } + + $uploadedPath = WRITEPATH . 'uploads/policy/policy_pdf/'; + $pdfFilePath = $uploadedPath . $record['policy_pdf_file_name']; + + if (!file_exists($pdfFilePath)) { + log_message('info', "RAG read: PDF not found at {$pdfFilePath}"); + return $this->formatReadResponse('failed', "File not found at {$pdfFilePath}"); + } + + $uploadResult = $this->uploadFileToRag($pdfFilePath); + if ($uploadResult['status'] !== 'success') { + return $this->formatReadResponse('failed', $uploadResult['message']); + } + + $fileId = $uploadResult['data']['file_id'] ?? null; + if (empty($fileId)) { + return $this->formatReadResponse('failed', 'RAG upload did not return file_id'); + } + + $updated = $this->PolicyModel->update((int) $policyId, ['rag_file_id' => $fileId]); + if ($updated === false) { + log_message('error', "Failed to save rag_file_id={$fileId} for policy_id={$policyId}"); + } + log_message('info', "RAG file uploaded. file_id={$fileId}, policy_id={$policyId}"); + + $indexResult = $this->waitForFileIndexed($fileId); + if ($indexResult['status'] !== 'success') { + return $this->formatReadResponse('failed', $indexResult['message']); + } + + $chatResult = $this->chatWithRag($fileId, $this->getPolicyReadPrompt()); + if ($chatResult['status'] !== 'success') { + return $this->formatReadResponse('failed', $chatResult['message']); + } + + $answer = $chatResult['data']['answer'] ?? ''; + // dd($answer); + $finalData = $this->extractJsonFromText($answer); + + if ($finalData === null) { + return $this->formatReadResponse('failed', 'Could not extract valid JSON from RAG answer'); + } + + log_message('info', 'RAG policy doc read successful for policy_id=' . $policyId); + return $this->formatReadResponse('success', 'Doc read success', $finalData); + } catch (\Throwable $e) { + log_message('error', 'PolicyRag readPolicyDocViaRag error: ' . $e->getMessage()); + return $this->formatReadResponse('failed', 'Error: ' . $e->getMessage()); + } finally { + if (!empty($fileId)) { + $deleteResult = $this->deleteRagFile($fileId); + if ($deleteResult['status'] !== 'success') { + log_message('error', 'RAG file delete failed for file_id=' . $fileId . ': ' . ($deleteResult['message'] ?? '')); + } + } + } + } + + private function getPolicyReadPrompt(): string + { + $sampleJson = json_encode([ + 'policy_number' => '', + 'issued_date' => '', + 'start_date' => '', + 'end_date' => '', + 'broker_name' => '', + 'insured_name' => '', + 'reg_no' => '', + 'rto_state_code' => '', + 'rto_city_code' => '', + 'weight' => '', + 'fuel_type' => '', + 'date_of_registration' => '', + 'year_of_manufacture' => '', + 'engine_no' => '', + 'chassis_no' => '', + 'make' => '', + 'model' => '', + 'cubic_capacity' => '', + 'vehicle_type' => '', + 'tp' => 0, + 'od' => 0, + 'pa' => 0, + 'cgst' => 0, + 'sgst' => 0, + 'igst' => 0, + 'premium_amount' => 0, + ], JSON_UNESCAPED_SLASHES); + + return 'Read the motor policy document and return only valid JSON using this exact structure (no explanation): ' + . $sampleJson + . '. Rules: vehicle_type must be one of Two Wheeler, Four Wheeler, Goods Vehicle, Bus, Tractor, Commercial Vehicle. ' + . 'rto_state_code is first 2 characters of reg_no. rto_city_code is 3rd and 4th characters of reg_no. ' + . 'All dates must be in YYYY-MM-DD format. TP premium must not include PA amount.'; + } + + private function mapExtractedDataToPolicyFields(array $data): array + { + $data = $this->normalizeExtractedData($data); + + $vehicleType = $data['vehicle_type'] ?? null; + $fuelType = $data['fuel_type'] ?? null; + + if ($vehicleType !== null && $vehicleType !== '') { + $vehicleType = $this->postProcessExtractedData((string) $vehicleType, $this->tier1VehicleTypeMap); + } else { + $vehicleType = 'UN_IDEN_DOC'; + } + + if ($fuelType !== null && $fuelType !== '') { + $fuelType = $this->postProcessExtractedData((string) $fuelType, $this->fuelType); + } else { + $fuelType = 'UN_IDEN_DOC'; + } + + return [ + 'policy_number' => $this->nullIfEmpty($data['policy_number'] ?? null), + 'issued_date' => $this->nullIfEmpty($data['issued_date'] ?? null), + 'start_date' => $this->nullIfEmpty($data['start_date'] ?? null), + 'end_date' => $this->nullIfEmpty($data['end_date'] ?? null), + 'broker_name' => $this->nullIfEmpty($data['broker_name'] ?? null), + 'tp' => $this->nullIfEmpty($data['tp'] ?? null), + 'od' => $this->nullIfEmpty($data['od'] ?? null), + 'pa' => $this->nullIfEmpty($data['pa'] ?? null), + 'cgst' => $this->nullIfEmpty($data['cgst'] ?? 0), + 'sgst' => $this->nullIfEmpty($data['sgst'] ?? 0), + 'igst' => $this->nullIfEmpty($data['igst'] ?? 0), + 'premium_amount' => $this->nullIfEmpty($data['premium_amount'] ?? null), + 'rto_state_code' => $this->nullIfEmpty($data['rto_state_code'] ?? null), + 'rto_city_code' => $this->nullIfEmpty($data['rto_city_code'] ?? null), + 'weight' => $this->nullIfEmpty($data['weight'] ?? null), + 'fuel_type' => $fuelType, + 'date_of_registration' => $this->nullIfEmpty($data['date_of_registration'] ?? null), + 'year_of_manufacture' => $this->nullIfEmpty($data['year_of_manufacture'] ?? null), + 'engine_no' => $this->nullIfEmpty($data['engine_no'] ?? null), + 'chassis_no' => $this->nullIfEmpty($data['chassis_no'] ?? null), + 'make' => $this->nullIfEmpty($data['make'] ?? null), + 'model' => $this->nullIfEmpty($data['model'] ?? null), + 'cubic_capacity' => $this->nullIfEmpty($data['cubic_capacity'] ?? null), + 'product' => $vehicleType, + 'rc_no' => $this->nullIfEmpty($data['reg_no'] ?? null), + 'insured_name' => $this->nullIfEmpty($data['insured_name'] ?? null), + ]; + } + + private function normalizeExtractedData(array $data): array + { + if (!isset($data['policy']) && !isset($data['vehicle']) && !isset($data['premium'])) { + return $data; + } + + return [ + 'policy_number' => $data['policy']['policy_number'] ?? null, + 'issued_date' => $data['policy']['issue_date'] ?? null, + 'start_date' => $data['policy']['period']['start'] ?? null, + 'end_date' => $data['policy']['period']['end'] ?? null, + 'broker_name' => $data['policy']['intermediary_name'] ?? null, + 'insured_name' => $data['insured']['name'] ?? null, + 'reg_no' => $data['vehicle']['reg_no'] ?? null, + 'rto_state_code' => $data['vehicle']['rto_state_code'] ?? null, + 'rto_city_code' => $data['vehicle']['rto_city_code'] ?? null, + 'weight' => $data['vehicle']['weight'] ?? null, + 'fuel_type' => $data['vehicle']['fuel_type'] ?? null, + 'date_of_registration' => $data['vehicle']['date_of_registration'] ?? null, + 'year_of_manufacture' => $data['vehicle']['year_of_manufacture'] ?? null, + 'engine_no' => $data['vehicle']['engine_no'] ?? null, + 'chassis_no' => $data['vehicle']['chassis_no'] ?? null, + 'make' => $data['vehicle']['make'] ?? null, + 'model' => $data['vehicle']['model'] ?? null, + 'cubic_capacity' => $data['vehicle']['cubic_capacity'] ?? null, + 'vehicle_type' => $data['vehicle']['vehicle_type'] ?? null, + 'tp' => $data['premium']['tp'] ?? null, + 'od' => $data['premium']['od'] ?? null, + 'pa' => $data['premium']['pa'] ?? null, + 'cgst' => $data['premium']['taxes']['cgst'] ?? 0, + 'sgst' => $data['premium']['taxes']['sgst'] ?? 0, + 'igst' => $data['premium']['taxes']['igst'] ?? 0, + 'premium_amount' => $data['premium']['total'] ?? null, + ]; + } + + private function nullIfEmpty($value) + { + if ($value === '' || $value === null) { + return null; + } + + return $value; + } + + private function postProcessExtractedData(string $incoming, array $masterArray): string + { + $value = strtolower(trim($incoming)); + $value = preg_replace('/\s+/', ' ', $value); + + return $masterArray[$value] ?? 'UN_IDEN_TYPE'; + } + + private function ragApiBaseUrl(): string + { + $baseUrl = rtrim((string) getenv('RAG_API_BASE_URL'), '/'); + if ($baseUrl === '') { + throw new \RuntimeException('RAG_API_BASE_URL is not configured'); + } + + return $baseUrl; + } + + private function uploadFileToRag(string $pdfFilePath): array + { + $url = $this->ragApiBaseUrl() . '/api/files/upload'; + $cfile = new \CURLFile($pdfFilePath, 'application/pdf', basename($pdfFilePath)); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, [ + 'uploaded_file' => $cfile, + 'auto_detect' => 'true', + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + $curlError = curl_error($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($curlError) { + log_message('error', 'RAG upload cURL error: ' . $curlError); + return ['status' => 'failed', 'message' => 'RAG upload cURL error: ' . $curlError]; + } + + if ($httpCode < 200 || $httpCode >= 300) { + log_message('error', "RAG upload HTTP {$httpCode}: " . substr((string) $response, 0, 500)); + return ['status' => 'failed', 'message' => "RAG upload failed with HTTP {$httpCode}"]; + } + + $responseData = json_decode((string) $response, true); + if (!is_array($responseData) || empty($responseData['success'])) { + return ['status' => 'failed', 'message' => 'RAG upload returned invalid response']; + } + + return ['status' => 'success', 'data' => $responseData]; + } + + private function getFileStatus(string $fileId): array + { + $url = $this->ragApiBaseUrl() . '/api/files/status?file_id=' . urlencode($fileId); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + $curlError = curl_error($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($curlError) { + return ['status' => 'failed', 'message' => 'RAG status cURL error: ' . $curlError]; + } + + if ($httpCode < 200 || $httpCode >= 300) { + return ['status' => 'failed', 'message' => "RAG status check failed with HTTP {$httpCode}"]; + } + + $responseData = json_decode((string) $response, true); + if (!is_array($responseData) || empty($responseData['success'])) { + return ['status' => 'failed', 'message' => 'RAG status returned invalid response']; + } + + return ['status' => 'success', 'data' => $responseData]; + } + + private function waitForFileIndexed(string $fileId): array + { + $interval = (int) (getenv('RAG_API_STATUS_POLL_INTERVAL') ?: 10); + $maxAttempts = (int) (getenv('RAG_API_STATUS_MAX_ATTEMPTS') ?: 60); + + for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { + $statusResult = $this->getFileStatus($fileId); + if ($statusResult['status'] !== 'success') { + return $statusResult; + } + + $fileStatus = strtolower((string) ($statusResult['data']['status'] ?? '')); + log_message('info', "RAG file status poll attempt {$attempt}/{$maxAttempts}: file_id={$fileId}, status={$fileStatus}"); + + if ($fileStatus === 'indexed') { + return $statusResult; + } + + if (in_array($fileStatus, ['failed', 'error'], true)) { + return ['status' => 'failed', 'message' => 'RAG file indexing failed with status: ' . $fileStatus]; + } + + if ($attempt < $maxAttempts) { + sleep($interval); + } + } + + return ['status' => 'failed', 'message' => 'RAG file indexing timed out']; + } + + private function chatWithRag(string $fileId, string $question): array + { + $url = $this->ragApiBaseUrl() . '/api/chat'; + + $payload = json_encode([ + 'question' => $question, + 'file_id' => $fileId, + ]); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + $curlError = curl_error($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($curlError) { + return ['status' => 'failed', 'message' => 'RAG chat cURL error: ' . $curlError]; + } + + if ($httpCode < 200 || $httpCode >= 300) { + log_message('error', "RAG chat HTTP {$httpCode}: " . substr((string) $response, 0, 500)); + return ['status' => 'failed', 'message' => "RAG chat failed with HTTP {$httpCode}"]; + } + + $responseData = json_decode((string) $response, true); + if (!is_array($responseData) || empty($responseData['success'])) { + return ['status' => 'failed', 'message' => 'RAG chat returned invalid response']; + } + + return ['status' => 'success', 'data' => $responseData]; + } + + private function deleteRagFile(string $fileId): array + { + $url = $this->ragApiBaseUrl() . '/api/files/id/' . urlencode($fileId); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + $curlError = curl_error($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($curlError) { + return ['status' => 'failed', 'message' => 'RAG delete cURL error: ' . $curlError]; + } + + if ($httpCode < 200 || $httpCode >= 300) { + log_message('error', "RAG delete HTTP {$httpCode}: " . substr((string) $response, 0, 500)); + return ['status' => 'failed', 'message' => "RAG delete failed with HTTP {$httpCode}"]; + } + + $responseData = json_decode((string) $response, true); + if (!is_array($responseData) || empty($responseData['success'])) { + return ['status' => 'failed', 'message' => 'RAG delete returned invalid response']; + } + + log_message('info', 'RAG file deleted: file_id=' . $fileId); + return ['status' => 'success', 'data' => $responseData]; + } + + private function extractJsonFromText(string $text): ?array + { + $jsonString = ''; + + if (preg_match('/```json\s*(.*?)\s*```/s', $text, $matches)) { + $jsonString = trim($matches[1]); + } else { + $start = strpos($text, '{'); + $end = strrpos($text, '}'); + if ($start !== false && $end !== false && $end > $start) { + $jsonString = substr($text, $start, $end - $start + 1); + } + } + + if ($jsonString === '') { + return null; + } + + $decoded = json_decode($jsonString, true); + return is_array($decoded) ? $decoded : null; + } + + private function formatReadResponse(string $status, string $message, ?array $data = null): array + { + $result = ['status' => $status, 'message' => $message]; + if ($data !== null) { + $result['data'] = $data; + } + + return $result; + } +} diff --git a/app/Models/PolicyModel.php b/app/Models/PolicyModel.php index 7dc61e5..5e73831 100644 --- a/app/Models/PolicyModel.php +++ b/app/Models/PolicyModel.php @@ -59,7 +59,8 @@ class PolicyModel extends Model 'commission_from_insurer', 'agent_retention_rate', 'manager_retention_rate', - 'is_invoice_generated' + 'is_invoice_generated', + 'rag_file_id' ]; protected $useTimestamps = false;