diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 1cff8157..22a29aeb 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -35,6 +35,7 @@ $routes->get("updateRenewalDataNotExistingClient", "ClientController::updateRene $routes->get("updateRenewalInsurerData", "ClientController::updateRenewalInsurerData"); $routes->get("sendMutipleToEmails", "MasterController::sendMutipleToEmails"); $routes->post("getCommission", "InsuranceCommissionController::initiateCommissionCalc",['filter' => 'CommissionApiFilter']); +$routes->get("importRules", "RuleImportController::upload"); // $routes->post("iAgreeForAddOn", "EmployeeRestController::iAgreeForAddOn"); // $routes->get("sendCroneRemainderMail", "DashboardController::sendCroneRemainderMail"); // $routes->post("employeeUpload", "EmployeeRestController::employeeUpload"); diff --git a/app/Config/Services.php b/app/Config/Services.php index fab85a50..5f982877 100755 --- a/app/Config/Services.php +++ b/app/Config/Services.php @@ -7,6 +7,7 @@ use App\Libraries\Slug; use App\Libraries\MyLogger; use App\Libraries\GmailAPI; use App\Libraries\MyGoogleDrive; +use App\Libraries\RuleImportService; use App\Libraries\DataServiceSqlite; use App\Controllers\Home; @@ -80,5 +81,14 @@ class Services extends BaseService return new MyGoogleDrive(); } + + public static function ruleImportService($getShared = true) + { + if ($getShared) { + return static::getSharedInstance('ruleImportService'); + } + + return new RuleImportService(); + } } diff --git a/app/Controllers/RuleImportController.php b/app/Controllers/RuleImportController.php new file mode 100644 index 00000000..a3162499 --- /dev/null +++ b/app/Controllers/RuleImportController.php @@ -0,0 +1,350 @@ +ruleImportService = \Config\Services::ruleImportService(); + } + + /** + * Upload endpoint for form (POST) + * Input form field: 'rules_file' + */ + public function uploadORI() + { + // echo 'hi'; + !dd($result = $this->ruleImportService->processUpload(['id' => 1,'file_name' => 'sample_commission.csv','insurer_id' => 5, 'department' => 'motor' ,'commission_month' => '2025-11-10']));die; + try { + $file = $this->request->getFile('rules_file'); + if (!$file || !$file->isValid()) { + return $this->response->setJSON(['status'=>'error','message'=>'No file uploaded or upload error']); + } + + // Move uploaded file to writable temp location + $tmpPath = WRITEPATH . 'uploads/' . $file->getRandomName(); + $file->move(WRITEPATH . 'uploads', $file->getName()); // keep original name inside uploads + $uploadedFullPath = $file->getTempName(); // Note: CI may store in tmp; we will use moved file path instead + $movedPath = WRITEPATH . 'uploads/' . $file->getName(); + + // Process file + $result = $this->ruleImportService->processUpload($movedPath, $file->getName()); + + // Return JSON with annotated file link if present + if (isset($result['annotated_file']) && $result['annotated_file']) { + $annotUrl = base_url('writable/uploads/annotated/' . basename($result['annotated_file'])); + $result['annotated_url'] = $annotUrl; + } + + return $this->response->setJSON($result); + + } catch (\Throwable $e) { + log_message('critical', 'RuleImportController::upload ' . $e->getMessage()); + return $this->response->setJSON(['status'=>'exception','message'=>$e->getMessage()]); + } + } + + public function upload() + { + // Make sure filesystem helpers available if you need them + helper(['filesystem']); + + // CommissionFilesModel – adjust namespace if different + $commissionFilesModel = new \App\Models\CommissionFilesModel(); + + try { + // --------------------------------------------------------- + // 1. Get uploaded file + // --------------------------------------------------------- + $file = $this->request->getFile('rules_file'); + if (!$file || !$file->isValid()) { + log_message('warning', 'RuleImportController::upload - No file or invalid upload.'); + return $this->respond([ + 'status' => 'error', + 'message' => 'No file uploaded or upload error.' + ], 400); + } + + // --------------------------------------------------------- + // 2. Read POST fields + // --------------------------------------------------------- + $insurerId = $this->request->getPost('insurer_id'); + $department = $this->request->getPost('department'); + $commissionMonth = $this->request->getPost('commission_month'); + $createdBy = $this->request->getPost('created_by'); + + if (empty($insurerId) || empty($department) || empty($commissionMonth) || empty($createdBy)) { + log_message('warning', 'RuleImportController::upload - Missing required POST data.', [ + 'insurer_id' => $insurerId, + 'department' => $department, + 'commission_month' => $commissionMonth, + 'created_by' => $createdBy, + ]); + return $this->respond([ + 'status' => 'error', + 'message' => 'Missing required fields: insurer_id, department, commission_month, created_by.' + ], 400); + } + + // Optional / default fields + $postedFileName = $this->request->getPost('file_name') ?: $file->getClientName(); + $fileStatus = $this->request->getPost('file_status') ?: 'pending'; + $isActive = $this->request->getPost('is_active') !== null ? (int)$this->request->getPost('is_active') : 1; + // rules_count is given by user but we will override it after processing on success + $postedRulesCount = $this->request->getPost('rules_count') !== null + ? (int)$this->request->getPost('rules_count') + : 0; + + // --------------------------------------------------------- + // 3. Move file to WRITEPATH/uploads/commission/files using user filename + // (no random name as per your requirement) + // --------------------------------------------------------- + $uploadDir = WRITEPATH . 'uploads/commission/files/'; + if (!is_dir($uploadDir)) { + if (!mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) { + throw new \RuntimeException("Failed to create upload directory: {$uploadDir}"); + } + } + + // sanitize user file name but keep it deterministic (no random, no timestamp) + $safeName = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $postedFileName); + $targetFileName = $safeName; + $movedFullPath = $uploadDir . $targetFileName; + + $file->move($uploadDir, $targetFileName); + if (!file_exists($movedFullPath)) { + log_message('error', "RuleImportController::upload - Failed to move uploaded file to {$movedFullPath}"); + return $this->respond([ + 'status' => 'error', + 'message' => 'Failed to store uploaded file.' + ], 500); + } + + log_message('info', "RuleImportController::upload - File moved to {$movedFullPath}"); + + // --------------------------------------------------------- + // 4. Insert commission_files row with status pending + // --------------------------------------------------------- + $insertData = [ + 'file_name' => $targetFileName, + 'insurer_id' => (int)$insurerId, + 'department' => $department, + 'commission_month'=> $commissionMonth, + 'rules_count' => 0, // will update on success + 'file_status' => $fileStatus, // 'pending' by default + 'is_active' => $isActive, + 'created_by' => (int)$createdBy, + 'created_at' => date('Y-m-d H:i:s'), + ]; + + $commissionFilesModel->insert($insertData); + $insertId = $commissionFilesModel->getInsertID(); + + if (empty($insertId)) { + log_message('error', 'RuleImportController::upload - Failed to insert commission_files record', ['data' => $insertData]); + return $this->respond([ + 'status' => 'error', + 'message' => 'Failed to record upload in database.' + ], 500); + } + + log_message('info', "RuleImportController::upload - commission_files inserted id={$insertId}", $insertData); + + // --------------------------------------------------------- + // 5. Call ruleImportService->processUpload with inserted file info + // As per your spec: + // $this->ruleImportService->processUpload([ + // 'id' => 1, + // 'file_name' => 'sample_commission.csv', + // 'insurer_id' => 5, + // 'department' => 'motor', + // 'commission_month' => '2025-11-10' + // ]) + // --------------------------------------------------------- + $payload = [ + 'id' => (int)$insertId, + 'file_name' => $targetFileName, + 'insurer_id' => (int)$insurerId, + 'department' => $department, + 'commission_month' => $commissionMonth, + ]; + + log_message('info', 'RuleImportController::upload - Calling ruleImportService->processUpload', ['payload' => $payload]); + + $result = $this->ruleImportService->processUpload($payload); + + if (!is_array($result) || !isset($result['status'])) { + log_message('error', 'RuleImportController::upload - Invalid service response', ['response' => $result]); + // update file status as failed + $commissionFilesModel->update($insertId, [ + 'file_status' => 'failed', + 'updated_by' => (int)$createdBy, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + return $this->respond([ + 'status' => 'error', + 'message' => 'Invalid response from import service.' + ], 500); + } + + // --------------------------------------------------------- + // 6. Handle SUCCESS + // - result['rules'] exists + // - result['errors'] empty + // - NO annotated_file + // - Save rules as JSON in WRITEPATH/uploads/commission/json/{insurer_id}_{department}.json + // --------------------------------------------------------- + if ($result['status'] === 'success') { + $rulesArray = isset($result['rules']) && is_array($result['rules']) ? $result['rules'] : []; + $rulesCount = count($rulesArray); + + // Save JSON to WRITEPATH . 'uploads/commission/json/{insurer_id}_{department}.json' + $jsonDir = WRITEPATH . 'uploads/commission/json/'; + if (!is_dir($jsonDir)) { + if (!mkdir($jsonDir, 0755, true) && !is_dir($jsonDir)) { + throw new \RuntimeException("Failed to create JSON output directory: {$jsonDir}"); + } + } + + $deptSlug = preg_replace('/[^a-zA-Z0-9_\-]/', '_', strtolower($department)); + $jsonName = (int)$insurerId . '_' . $deptSlug . '.json'; + $jsonPath = $jsonDir . $jsonName; + + $jsonData = json_encode($rulesArray, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($jsonData === false) { + log_message('error', 'RuleImportController::upload - json_encode failed for rules', [ + 'last_error' => json_last_error_msg() + ]); + // mark as failed since we cannot save rules + $commissionFilesModel->update($insertId, [ + 'file_status' => 'failed', + 'updated_by' => (int)$createdBy, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + return $this->respond([ + 'status' => 'error', + 'message' => 'Failed to encode rules as JSON.' + ], 500); + } + + if (file_put_contents($jsonPath, $jsonData) === false) { + log_message('error', 'RuleImportController::upload - Failed to write rules JSON file', ['json_path' => $jsonPath]); + $commissionFilesModel->update($insertId, [ + 'file_status' => 'failed', + 'updated_by' => (int)$createdBy, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + return $this->respond([ + 'status' => 'error', + 'message' => 'Failed to store rules JSON file.' + ], 500); + } + + log_message('info', 'RuleImportController::upload - Rules JSON written', [ + 'file_id' => $insertId, + 'json_path' => $jsonPath, + 'rules_cnt' => $rulesCount, + ]); + + // Update DB: status, rules_count, updated_by + $commissionFilesModel->update($insertId, [ + 'file_status' => 'processed', + 'rules_count' => $rulesCount, + 'updated_by' => (int)$createdBy, + 'updated_at' => date('Y-m-d H:i:s'), + // If you have a column for JSON path, uncomment: + // 'json_file_path' => $jsonPath, + ]); + + return $this->respond([ + 'status' => 'success', + 'message' => 'File processed successfully.', + 'file_id' => $insertId, + 'rules_count' => $rulesCount, + 'json_file' => $jsonPath, + 'service' => $result, // optional: return full service response if you want + ], 200); + } + + // --------------------------------------------------------- + // 7. Handle ERROR (validation failed etc.) + // - Do NOT save any rules JSON + // - Update file_status to validation_failed + // - Store annotated_file path if you have such a column + // --------------------------------------------------------- + if ($result['status'] === 'error') { + $annotatedPath = $result['annotated_file'] ?? null; + $errors = $result['errors'] ?? []; + + $updateData = [ + 'file_status' => 'validation_failed', + 'rules_count' => 0, // do not save rules + 'updated_by' => (int)$createdBy, + 'updated_at' => date('Y-m-d H:i:s'), + ]; + + // If you have a column for annotated file path, e.g. annotated_file_path + if ($annotatedPath) { + $updateData['annotated_file_path'] = $annotatedPath; + } + + $commissionFilesModel->update($insertId, $updateData); + + log_message('warning', "RuleImportController::upload - Validation failed for file_id={$insertId}", [ + 'errors' => $errors, + 'annotated_file' => $annotatedPath + ]); + + return $this->respond([ + 'status' => 'error', + 'message' => 'Validation failed. No rules saved.', + 'file_id' => $insertId, + 'errors' => $errors, + 'annotated_file' => $annotatedPath, + ], 422); + } + + // --------------------------------------------------------- + // 8. Unexpected status + // --------------------------------------------------------- + log_message('error', 'RuleImportController::upload - Unexpected result status from service', ['result' => $result]); + $commissionFilesModel->update($insertId, [ + 'file_status' => 'failed', + 'updated_by' => (int)$createdBy, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + return $this->respond([ + 'status' => 'error', + 'message' => 'Unexpected import service result.' + ], 500); + + } catch (\Throwable $ex) { + log_message('critical', 'RuleImportController::upload exception: ' . $ex->getMessage(), [ + 'trace' => $ex->getTraceAsString() + ]); + + // Try to update the commission_files record if insertId exists + if (isset($insertId) && !empty($insertId)) { + try { + $commissionFilesModel->update($insertId, [ + 'file_status' => 'failed', + 'updated_by' => isset($createdBy) ? (int)$createdBy : null, + 'updated_at' => date('Y-m-d H:i:s'), + 'notes' => 'Upload exception: ' . $ex->getMessage(), + ]); + } catch (\Throwable $e2) { + log_message('error', 'RuleImportController::upload - failed to update commission_files after exception: ' . $e2->getMessage()); + } + } + + return $this->respond([ + 'status' => 'exception', + 'message' => $ex->getMessage(), + ], 500); + } + } +} diff --git a/app/Libraries/RuleImportService.php b/app/Libraries/RuleImportService.php new file mode 100644 index 00000000..3e471f57 --- /dev/null +++ b/app/Libraries/RuleImportService.php @@ -0,0 +1,1373 @@ +initializeExpectedColumns(); + $this->initializeValidators(); + $this->setupAnnotatedDirectory(); + } + + /** + * Initialize expected column headers + */ + private function initializeExpectedColumns(): void + { + $this->expectedColumns = [ + self::COL_RULE_NAME, + self::COL_POLICY_BUSINESS_TYPE, + self::COL_POLICY_NAME, + self::COL_PREMIUM_TYPE, + self::COL_VEHICLE_TYPE, + self::COL_VEHICLE_SUB_TYPE, + self::COL_MAKE, + self::COL_MODEL, + self::COL_CC_MIN, + self::COL_CC_MAX, + self::COL_FUEL_TYPE, + self::COL_VEHICLE_AGE_MIN, + self::COL_VEHICLE_AGE_MAX, + self::COL_VEHICLE_WEIGHT_MIN, + self::COL_VEHICLE_WEIGHT_MAX, + self::COL_RTO_STATE, + self::COL_RTO_CITY, + self::COL_RENEWAL_TYPE, + self::COL_RENEWAL_SUB_TYPE, + self::COL_COMMISSION_TYPE, + self::COL_COMMISSION_VALUE, + self::COL_COMMISSION_PARAMS, + self::COL_NOTES + ]; + } + + /** + * Initialize column validators + */ + private function initializeValidators(): void + { + $this->columnValidators = [ + self::COL_CC_MIN => 'validateNumeric', + self::COL_CC_MAX => 'validateNumeric', + self::COL_VEHICLE_AGE_MIN => 'validateNumeric', + self::COL_VEHICLE_AGE_MAX => 'validateNumeric', + self::COL_VEHICLE_WEIGHT_MIN => 'validateNumeric', + self::COL_VEHICLE_WEIGHT_MAX => 'validateNumeric', + self::COL_FUEL_TYPE => 'validateCommaList', + self::COL_COMMISSION_TYPE => 'validateCommissionType', + self::COL_COMMISSION_VALUE => 'validateNumeric', + self::COL_COMMISSION_PARAMS => 'validateCompositeParams', + ]; + } + + /** + * Setup annotated directory for error files + */ + private function setupAnnotatedDirectory(): void + { + $this->annotatedDir = WRITEPATH . 'uploads/commission/files/'; + if (!is_dir($this->annotatedDir)) { + mkdir($this->annotatedDir, 0755, true); + } + } + + /** + * Process uploaded file + * + * @param string $tempFilePath Temporary file path + * @param string $originalName Original filename + * @return array Processing result + */ + public function processUpload(array $params): array + { + // dd($params); + $startTime = microtime(true); + + try { + $this->department = $params['department']; + $this->uploadedCommissionFileID = $params['id']; + $tempFilePath = WRITEPATH.'uploads/commission/files/'.$params['file_name']; + $originalName = $params['file_name']; + // Validate file extension + $extension = $this->getFileExtension($params['file_name']); + $this->validateFileExtension($extension); + + // Load spreadsheet + $spreadsheet = $this->loadSpreadsheet($tempFilePath, $extension); + $sheet = $spreadsheet->getActiveSheet(); + + // Extract and validate headers + $headerMap = $this->extractHeaders($sheet); + $this->validateHeaders($headerMap); + // dd($headerMap); + + // Process rows + $this->validationResult = new ValidationResult(); + $rules = $this->processRows($sheet, $headerMap); + + // Generate annotated file if errors exist + $annotatedPath = null; + if ($this->validationResult->hasErrors()) { + $annotatedPath = $this->createAnnotatedFile( + $spreadsheet, + $headerMap, + $this->validationResult->getErrors(), + $originalName + ); + } + + $duration = round(microtime(true) - $startTime, 2); + + return $this->buildSuccessResponse($rules, $annotatedPath, $duration); + + } catch (Exception $e) { + log_message('error', "RuleImportService failed: " . $e->getMessage()); + return $this->buildErrorResponse($e); + } + } + + /** + * Get file extension + */ + private function getFileExtension(string $filename): string + { + return strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + } + + /** + * Validate file extension + */ + private function validateFileExtension(string $extension): void + { + if (!in_array($extension, self::SUPPORTED_EXTENSIONS, true)) { + throw new RuntimeException( + "Unsupported file type: {$extension}. " . + "Supported types: " . implode(', ', self::SUPPORTED_EXTENSIONS) + ); + } + } + + /** + * Load spreadsheet based on file type + */ + private function loadSpreadsheet(string $filePath, string $extension): Spreadsheet + { + if ($extension === 'csv') { + $reader = IOFactory::createReader('Csv'); + $reader->setDelimiter(','); + $reader->setEnclosure('"'); + return $reader->load($filePath); + } + + return IOFactory::load($filePath); + } + + /** + * Extract header row and create column mapping + */ + private function extractHeaders($sheet): array + { + $highestCol = $sheet->getHighestColumn(); + $headerRowData = $sheet->rangeToArray("A1:{$highestCol}1", null, true, true, true); + $headerRow = array_values($headerRowData[1]); + + $headerMap = []; + foreach ($headerRow as $index => $header) { + $label = trim((string)$header); + if ($label !== '') { + $headerMap[$label] = $index + 1; + } + } + + return $headerMap; + } + + /** + * Validate all required headers are present + */ + private function validateHeaders(array $headerMap): void + { + $missingColumns = array_diff($this->expectedColumns, array_keys($headerMap)); + + if (!empty($missingColumns)) { + throw new RuntimeException( + "Missing required columns: " . implode(', ', $missingColumns) + ); + } + } + + /** + * Process all data rows + */ + private function processRows($sheet, array $headerMap): array + { + $rules = []; + $highestRow = $sheet->getHighestRow(); + + for ($rowNum = 2; $rowNum <= $highestRow; $rowNum++) { + $rowData = $this->extractRowData($sheet, $headerMap, $rowNum); + + // Skip empty rows + if ($this->isEmptyRow($rowData)) { + continue; + } + + // Validate row + $this->validateRow($rowData, $rowNum); + + // Convert to rule if no errors + if (!$this->validationResult->hasRowErrors($rowNum)) { + $rules[] = $this->convertRowToRule($rowData); + } + } + + return $rules; + } + + /** + * Extract data from a single row + // */ + // private function extractRowData($sheet, array $headerMap, int $rowNum): array + // { + // $rowData = []; + // foreach ($headerMap as $colName => $colIndex) { + // $cell = $sheet->getCellByColumnAndRow($colIndex, $rowNum); + // $rowData[$colName] = trim((string)$cell->getValue()); + // } + // return $rowData; + // } + + + /** + * Extract data from a single row + * + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet + * @param array $headerMap Column name => column index mapping + * @param int $rowNum Row number to extract + * @return array Associative array of column name => value + */ + private function extractRowData($sheet, array $headerMap, int $rowNum): array + { + $rowData = []; + + foreach ($headerMap as $colName => $colIndex) { + // Convert column index to letter (A, B, C, etc.) + $colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex); + + // Get cell value using coordinate (e.g., "A2", "B2") + $cellCoordinate = $colLetter . $rowNum; + $value = $sheet->getCell($cellCoordinate)->getValue(); + + // Handle different data types + if ($value instanceof \PhpOffice\PhpSpreadsheet\RichText\RichText) { + $value = $value->getPlainText(); + } + + $rowData[$colName] = trim((string)$value); + } + + return $rowData; + } + + /** + * Check if row is empty + */ + private function isEmptyRow(array $rowData): bool + { + foreach ($rowData as $value) { + if ($value !== '') { + return false; + } + } + return true; + } + + /** + * Validate a single row + */ + private function validateRow(array $rowData, int $rowNum): void + { + // Field-level validation + foreach ($this->columnValidators as $colName => $validatorMethod) { + $value = $rowData[$colName] ?? ''; + if (!$this->$validatorMethod($value)) { + $errorMessage = $this->buildValidationMessage($value, $colName, $validatorMethod); + $this->validationResult->addError($rowNum, $colName, $errorMessage); + } + } + + // Range validations + $this->validateRangePair($rowData, self::COL_CC_MIN, self::COL_CC_MAX, $rowNum); + $this->validateRangePair($rowData, self::COL_VEHICLE_AGE_MIN, self::COL_VEHICLE_AGE_MAX, $rowNum); + $this->validateRangePair($rowData, self::COL_VEHICLE_WEIGHT_MIN, self::COL_VEHICLE_WEIGHT_MAX, $rowNum); + + // Business rule validations + $this->validateBusinessRules($rowData, $rowNum); + } + + /** + * Validate range pairs (min <= max) + */ + private function validateRangePair( + array $rowData, + string $minCol, + string $maxCol, + int $rowNum + ): void { + $min = $rowData[$minCol] ?? ''; + $max = $rowData[$maxCol] ?? ''; + + if ($min !== '' && $max !== '' && is_numeric($min) && is_numeric($max)) { + if ((float)$min > (float)$max) { + $this->validationResult->addError( + $rowNum, + $minCol, + "{$min} : ERROR - {$minCol} cannot be greater than {$maxCol}" + ); + } + } + } + + /** + * Validate business logic rules + */ + private function validateBusinessRules(array $rowData, int $rowNum): void + { + $commissionType = strtolower($rowData[self::COL_COMMISSION_TYPE] ?? ''); + $commissionValue = $rowData[self::COL_COMMISSION_VALUE] ?? ''; + $commissionParams = $rowData[self::COL_COMMISSION_PARAMS] ?? ''; + + // Composite commission requires params + if ($commissionType === self::COMMISSION_COMPOSITE && empty($commissionParams)) { + $this->validationResult->addError( + $rowNum, + self::COL_COMMISSION_PARAMS, + "Required when commission type is composite" + ); + } + + // Flat commission requires value + if ($commissionType === self::COMMISSION_FLAT && empty($commissionValue)) { + $this->validationResult->addError( + $rowNum, + self::COL_COMMISSION_VALUE, + "Required when commission type is flat" + ); + } + + // Percentage commission requires value + if ($commissionType === self::COMMISSION_PERCENTAGE && empty($commissionValue)) { + $this->validationResult->addError( + $rowNum, + self::COL_COMMISSION_VALUE, + "Required when commission type is percentage" + ); + } + } + + // ==================== VALIDATORS ==================== + + /** + * Validate numeric values + */ + protected function validateNumeric(string $value): bool + { + if ($value === '') { + return true; + } + return filter_var($value, FILTER_VALIDATE_FLOAT) !== false; + } + + /** + * Validate comma-separated list + */ + protected function validateCommaList(string $value): bool + { + if ($value === '') { + return true; + } + return preg_match("/^[a-zA-Z0-9\s,\-]+$/", $value) === 1; + } + + /** + * Validate commission type + */ + protected function validateCommissionType(string $value): bool + { + if (trim($value) === '') { + return false; // Required field + } + return in_array(strtolower(trim($value)), self::COMMISSION_TYPES, true); + } + + /** + * Validate composite parameters (TP:OD:PA format) + */ + protected function validateCompositeParams(string $value): bool + { + if ($value === '') { + return true; + } + + // Format: number:number or number:number:number + if (!preg_match('/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)(?::(\d+(?:\.\d+)?))?$/', $value, $matches)) { + return false; + } + + // Validate percentage ranges (0-100) + $values = array_filter($matches, 'is_numeric'); + foreach ($values as $val) { + if ($val < 0 || $val > 100) { + return false; + } + } + + return true; + } + + /** + * Build validation error message + */ + protected function buildValidationMessage( + string $value, + string $colName, + string $validatorMethod + ): string { + $messages = [ + 'validateNumeric' => 'expecting numeric value only', + 'validateCommaList' => 'expecting comma-separated values', + 'validateCommissionType' => 'expecting one of: ' . implode(', ', self::COMMISSION_TYPES), + 'validateCompositeParams' => 'expecting format TP:OD:PA (e.g., 10:25:0)' + ]; + + $suffix = $messages[$validatorMethod] ?? 'invalid value'; + return empty($value) ? "ERROR - {$suffix}" : "{$value} : ERROR - {$suffix}"; + } + + // ==================== CONVERSION ==================== + + /** + * Convert row data to rule JSON structure + */ + protected function convertRowToRuleV1(array $rowData): array + { + $rule = [ + 'id' => $this->generateRuleId($rowData), + 'name' => $this->sanitize($rowData[self::COL_RULE_NAME] ?? ''), + 'policy_business_type' => $this->sanitize($rowData[self::COL_POLICY_BUSINESS_TYPE] ?? ''), + 'policy_name' => $this->sanitize($rowData[self::COL_POLICY_NAME] ?? ''), + 'premium_type' => $this->sanitize($rowData[self::COL_PREMIUM_TYPE] ?? ''), + 'vehicle' => $this->buildVehicleData($rowData), + 'fuel_type' => $this->parseFuelTypes($rowData[self::COL_FUEL_TYPE] ?? ''), + 'vehicle_age' => $this->buildRangeData( + self::COL_VEHICLE_AGE_MIN, + self::COL_VEHICLE_AGE_MAX, + $rowData + ), + 'vehicle_weight' => $this->buildRangeData( + self::COL_VEHICLE_WEIGHT_MIN, + self::COL_VEHICLE_WEIGHT_MAX, + $rowData + ), + 'rto' => $this->buildRtoData($rowData), + 'renewal_type' => $this->sanitize($rowData[self::COL_RENEWAL_TYPE] ?? ''), + 'commission' => $this->buildCommissionData($rowData), + 'notes' => $this->sanitize($rowData[self::COL_NOTES] ?? '') + ]; + + return $this->removeEmptyValues($rule); + } + + /** + * Generate unique rule ID + */ + private function generateRuleId(array $rowData): string + { + return 'rule_' . substr(md5(json_encode($rowData) . time()), 0, 13); + } + + /** + * Build vehicle data structure + */ + private function buildVehicleData(array $rowData): array + { + return [ + 'type' => $this->sanitize($rowData[self::COL_VEHICLE_TYPE] ?? ''), + 'sub_type' => $this->sanitize($rowData[self::COL_VEHICLE_SUB_TYPE] ?? ''), + 'make' => $this->sanitize($rowData[self::COL_MAKE] ?? ''), + 'model' => $this->sanitize($rowData[self::COL_MODEL] ?? ''), + 'cc' => [ + 'min' => $this->parseFloat($rowData[self::COL_CC_MIN] ?? ''), + 'max' => $this->parseFloat($rowData[self::COL_CC_MAX] ?? '') + ] + ]; + } + + /** + * Build range data (min/max) + */ + private function buildRangeData(string $minCol, string $maxCol, array $rowData): array + { + return [ + 'min' => $this->parseFloat($rowData[$minCol] ?? ''), + 'max' => $this->parseFloat($rowData[$maxCol] ?? '') + ]; + } + + /** + * Build RTO data structure + */ + private function buildRtoData(array $rowData): array + { + return [ + 'state' => $this->sanitize($rowData[self::COL_RTO_STATE] ?? ''), + 'city' => $this->sanitize($rowData[self::COL_RTO_CITY] ?? '') + ]; + } + + /** + * Build commission data structure + */ + private function buildCommissionData(array $rowData): array + { + $commissionType = strtolower($this->sanitize($rowData[self::COL_COMMISSION_TYPE] ?? '')); + + $commission = [ + 'type' => $commissionType, + 'value' => $this->parseFloat($rowData[self::COL_COMMISSION_VALUE] ?? '') + ]; + + if ($commissionType === self::COMMISSION_COMPOSITE) { + $commission['components'] = $this->parseCompositeParams( + $rowData[self::COL_COMMISSION_PARAMS] ?? '' + ); + } + + return $commission; + } + + /** + * Parse composite commission parameters + */ + private function parseCompositeParams(string $params): array + { + if (empty($params)) { + return []; + } + + $parts = explode(':', $params); + $components = []; + $premiumTypes = ['tp_premium', 'od_premium', 'pa_premium']; + + foreach ($premiumTypes as $index => $premiumType) { + if (isset($parts[$index]) && $parts[$index] !== '') { + $percentage = (float)$parts[$index]; + if ($percentage > 0) { + $components[] = [ + 'on' => $premiumType, + 'percentage' => $percentage + ]; + } + } + } + + return $components; + } + + /** + * Parse fuel types from comma-separated string + */ + private function parseFuelTypes(string $fuelTypeString): array + { + if (empty($fuelTypeString)) { + return []; + } + + return array_values(array_filter( + array_map('trim', explode(',', $fuelTypeString)), + fn($value) => $value !== '' + )); + } + + /** + * Sanitize string value + */ + private function sanitize(string $value): string + { + return trim($value); + } + + /** + * Parse float value + */ + private function parseFloat(string $value): ?float + { + if ($value === '' || !is_numeric($value)) { + return null; + } + return (float)$value; + } + + /** + * Remove null and empty values from array recursively + */ + private function removeEmptyValues(array $data): array + { + return array_filter($data, function($value) { + if (is_array($value)) { + $filtered = $this->removeEmptyValues($value); + return !empty($filtered); + } + return $value !== null && $value !== ''; + }); + } + + // ==================== ANNOTATION ==================== + + /** + * Create annotated file with errors + */ + // private function createAnnotatedFile( + // Spreadsheet $spreadsheet, + // array $headerMap, + // array $errors, + // string $originalName + // ): string { + // $sheet = $spreadsheet->getActiveSheet(); + + // // Add error column header + // $lastCol = $sheet->getHighestColumn(); + // $lastColIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($lastCol); + // $errorColIndex = $lastColIndex + 1; + + // $sheet->setCellValueByColumnAndRow($errorColIndex, 1, 'Validation Errors'); + + // // Annotate errors + // foreach ($errors as $rowNum => $columns) { + // $errorMessages = []; + + // foreach ($columns as $colName => $message) { + // $colIndex = $headerMap[$colName]; + // $cell = $sheet->getCellByColumnAndRow($colIndex, $rowNum); + + // // Set error value in original column + // $cell->setValueExplicit( + // $message, + // \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING + // ); + + // $errorMessages[] = "{$colName}: {$message}"; + // } + + // // Set combined error message + // $sheet->setCellValueByColumnAndRow( + // $errorColIndex, + // $rowNum, + // implode(' | ', $errorMessages) + // ); + // } + + // return $this->saveAnnotatedFile($spreadsheet, $originalName); + // } + + /** + * Create annotated file with errors (Enhanced with styling) + */ + private function createAnnotatedFile( + Spreadsheet $spreadsheet, + array $headerMap, + array $errors, + string $originalName + ): string { + $sheet = $spreadsheet->getActiveSheet(); + + // Add error column header + $lastCol = $sheet->getHighestColumn(); + $lastColIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($lastCol); + $errorColIndex = $lastColIndex + 1; + $errorColLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($errorColIndex); + + // Set header for validation errors column + $headerCoordinate = $errorColLetter . '1'; + $sheet->setCellValue($headerCoordinate, 'Validation Errors'); + + // Optional: Style the header + $sheet->getStyle($headerCoordinate)->applyFromArray([ + 'font' => ['bold' => true], + 'fill' => [ + 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, + 'startColor' => ['rgb' => 'FFD700'] + ] + ]); + + // Annotate errors + foreach ($errors as $rowNum => $columns) { + $errorMessages = []; + + foreach ($columns as $colName => $message) { + if (!isset($headerMap[$colName])) { + continue; // Skip if column not found + } + + $colIndex = $headerMap[$colName]; + $colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex); + $cellCoordinate = $colLetter . $rowNum; + + // Set error value in original column + $sheet->setCellValueExplicit( + $cellCoordinate, + $message, + \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING + ); + + // Optional: Highlight error cells in red + $sheet->getStyle($cellCoordinate)->applyFromArray([ + 'fill' => [ + 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, + 'startColor' => ['rgb' => 'FFB6C1'] // Light red + ], + 'font' => ['color' => ['rgb' => 'FF0000']] // Red text + ]); + + $errorMessages[] = "{$colName}: {$message}"; + } + + // Set combined error message in the validation errors column + $errorCellCoordinate = $errorColLetter . $rowNum; + $sheet->setCellValue($errorCellCoordinate, implode(' | ', $errorMessages)); + + // Optional: Style the summary error cell + $sheet->getStyle($errorCellCoordinate)->applyFromArray([ + 'fill' => [ + 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, + 'startColor' => ['rgb' => 'FFA500'] // Orange + ] + ]); + } + + // Auto-size the error column for better readability + $sheet->getColumnDimension($errorColLetter)->setAutoSize(true); + + return $this->saveAnnotatedFile($spreadsheet, $originalName); + } + + + + + + /** + * Save annotated spreadsheet + */ + private function saveAnnotatedFile(Spreadsheet $spreadsheet, string $originalName): string + { + $safeName = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $originalName); + $extension = $this->getFileExtension($originalName); + $timestamp = time(); + + $filename = "annotated_{$timestamp}_{$safeName}"; + $filename = "annotated_{$safeName}"; + $filepath = $this->annotatedDir . $filename; + + if ($extension === 'csv') { + $writer = IOFactory::createWriter($spreadsheet, 'Csv'); + $writer->setDelimiter(','); + $writer->setEnclosure('"'); + $writer->save($filepath); + } else { + $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); + if (!str_ends_with($filepath, '.xlsx')) { + $filepath .= '.xlsx'; + } + $writer->save($filepath); + } + + return $filepath; + } + + // ==================== RESPONSE BUILDERS ==================== + + /** + * Build success response + */ + private function buildSuccessResponse( + array $rules, + ?string $annotatedPath, + float $duration + ): array { + $hasErrors = $this->validationResult->hasErrors(); + + return [ + 'status' => $hasErrors ? 'error' : 'success', + 'rules' => $rules, + 'errors' => $hasErrors ? $this->validationResult->getErrors() : [], + 'annotated_file' => $annotatedPath, + 'statistics' => [ + 'total_rules' => count($rules), + 'error_count' => $this->validationResult->getErrorCount(), + 'duration_seconds' => $duration + ] + ]; + } + + /** + * Build error response + */ + private function buildErrorResponse(Exception $e): array + { + return [ + 'status' => 'exception', + 'message' => $e->getMessage(), + 'error_type' => get_class($e) + ]; + } + + + + /** + * Convert row data to rule engine JSON structure + * + * This function transforms Excel row data into a rule engine format with: + * - Conditions: Field-based criteria with operators (==, >=, <=, >, <, in) + * - Calculation: Commission calculation logic (percentage, flat/fixed, composite) + */ + protected function convertRowToRule(array $rowData): array + { + $rule = [ + 'id' => $this->generateRuleId($rowData), + 'name' => $this->sanitize($rowData[self::COL_RULE_NAME] ?? ''), + 'department' => $this->department, + 'file_id' => $this->uploadedCommissionFileID, + 'conditions' => $this->buildConditions($rowData), + 'calculation' => $this->buildCalculation($rowData) + ]; + + return $rule; + } + + /** + * Build conditions array from row data + * Converts Excel columns into rule engine conditions with operators + */ + private function buildConditions(array $rowData): array + { + $conditions = []; + + // Vehicle Type - exact match + if (!empty($rowData[self::COL_VEHICLE_TYPE])) { + $vehicleTypes = $this->parseCommaList($rowData[self::COL_VEHICLE_TYPE]); + + if (count($vehicleTypes) > 1) { + // Multiple values: use 'in' operator + $conditions[] = [ + 'field' => 'vehicle_type', + 'operator' => 'in', + 'value' => $vehicleTypes + ]; + } else { + // Single value: use '==' operator + $conditions[] = [ + 'field' => 'vehicle_type', + 'operator' => '==', + 'value' => $vehicleTypes[0] + ]; + } + } + + // Vehicle Sub Type + if (!empty($rowData[self::COL_VEHICLE_SUB_TYPE])) { + $conditions[] = [ + 'field' => 'vehicle_sub_type', + 'operator' => '==', + 'value' => $this->sanitize($rowData[self::COL_VEHICLE_SUB_TYPE]) + ]; + } + + // Make + if (!empty($rowData[self::COL_MAKE])) { + $makes = $this->parseCommaList($rowData[self::COL_MAKE]); + + if (count($makes) > 1) { + $conditions[] = [ + 'field' => 'make', + 'operator' => 'in', + 'value' => $makes + ]; + } else { + $conditions[] = [ + 'field' => 'make', + 'operator' => '==', + 'value' => $makes[0] + ]; + } + } + + // Model + if (!empty($rowData[self::COL_MODEL])) { + $models = $this->parseCommaList($rowData[self::COL_MODEL]); + + if (count($models) > 1) { + $conditions[] = [ + 'field' => 'model', + 'operator' => 'in', + 'value' => $models + ]; + } else { + $conditions[] = [ + 'field' => 'model', + 'operator' => '==', + 'value' => $models[0] + ]; + } + } + + // CC (Cubic Capacity) - Range handling + $ccMin = $rowData[self::COL_CC_MIN] ?? ''; + $ccMax = $rowData[self::COL_CC_MAX] ?? ''; + + if ($ccMin !== '' && $ccMax !== '') { + if ($ccMin === $ccMax) { + // Exact value + $conditions[] = [ + 'field' => 'cubic_capacity', + 'operator' => '==', + 'value' => (float)$ccMin + ]; + } else { + // Range: min and max + $conditions[] = [ + 'field' => 'cubic_capacity', + 'operator' => '>=', + 'value' => (float)$ccMin + ]; + $conditions[] = [ + 'field' => 'cubic_capacity', + 'operator' => '<=', + 'value' => (float)$ccMax + ]; + } + } elseif ($ccMin !== '') { + // Only minimum specified + $conditions[] = [ + 'field' => 'cubic_capacity', + 'operator' => '>=', + 'value' => (float)$ccMin + ]; + } elseif ($ccMax !== '') { + // Only maximum specified + $conditions[] = [ + 'field' => 'cubic_capacity', + 'operator' => '<=', + 'value' => (float)$ccMax + ]; + } + + // Fuel Type + if (!empty($rowData[self::COL_FUEL_TYPE])) { + $fuelTypes = $this->parseCommaList($rowData[self::COL_FUEL_TYPE]); + + if (count($fuelTypes) > 1) { + $conditions[] = [ + 'field' => 'fuel_type', + 'operator' => 'in', + 'value' => $fuelTypes + ]; + } else { + $conditions[] = [ + 'field' => 'fuel_type', + 'operator' => '==', + 'value' => $fuelTypes[0] + ]; + } + } + + // Vehicle Age - Range handling + $ageMin = $rowData[self::COL_VEHICLE_AGE_MIN] ?? ''; + $ageMax = $rowData[self::COL_VEHICLE_AGE_MAX] ?? ''; + + if ($ageMin !== '' && $ageMax !== '') { + if ($ageMin === $ageMax) { + $conditions[] = [ + 'field' => 'vehicle_age', + 'operator' => '==', + 'value' => (int)$ageMin + ]; + } else { + $conditions[] = [ + 'field' => 'vehicle_age', + 'operator' => '>=', + 'value' => (int)$ageMin + ]; + $conditions[] = [ + 'field' => 'vehicle_age', + 'operator' => '<=', + 'value' => (int)$ageMax + ]; + } + } elseif ($ageMin !== '') { + $conditions[] = [ + 'field' => 'vehicle_age', + 'operator' => '>=', + 'value' => (int)$ageMin + ]; + } elseif ($ageMax !== '') { + $conditions[] = [ + 'field' => 'vehicle_age', + 'operator' => '<=', + 'value' => (int)$ageMax + ]; + } + + // Vehicle Weight - Range handling + $weightMin = $rowData[self::COL_VEHICLE_WEIGHT_MIN] ?? ''; + $weightMax = $rowData[self::COL_VEHICLE_WEIGHT_MAX] ?? ''; + + if ($weightMin !== '' && $weightMax !== '') { + if ($weightMin === $weightMax) { + $conditions[] = [ + 'field' => 'vehicle_weight', + 'operator' => '==', + 'value' => (float)$weightMin + ]; + } else { + $conditions[] = [ + 'field' => 'vehicle_weight', + 'operator' => '>=', + 'value' => (float)$weightMin + ]; + $conditions[] = [ + 'field' => 'vehicle_weight', + 'operator' => '<=', + 'value' => (float)$weightMax + ]; + } + } elseif ($weightMin !== '') { + $conditions[] = [ + 'field' => 'vehicle_weight', + 'operator' => '>=', + 'value' => (float)$weightMin + ]; + } elseif ($weightMax !== '') { + $conditions[] = [ + 'field' => 'vehicle_weight', + 'operator' => '<=', + 'value' => (float)$weightMax + ]; + } + + // RTO State + if (!empty($rowData[self::COL_RTO_STATE])) { + $states = $this->parseCommaList($rowData[self::COL_RTO_STATE]); + + if (count($states) > 1) { + $conditions[] = [ + 'field' => 'rto_state', + 'operator' => 'in', + 'value' => $states + ]; + } else { + $conditions[] = [ + 'field' => 'rto_state', + 'operator' => '==', + 'value' => $states[0] + ]; + } + } + + // RTO City + if (!empty($rowData[self::COL_RTO_CITY])) { + $cities = $this->parseCommaList($rowData[self::COL_RTO_CITY]); + + if (count($cities) > 1) { + $conditions[] = [ + 'field' => 'rto_city', + 'operator' => 'in', + 'value' => $cities + ]; + } else { + $conditions[] = [ + 'field' => 'rto_city', + 'operator' => '==', + 'value' => $cities[0] + ]; + } + } + + // Policy Business Type + if (!empty($rowData[self::COL_POLICY_BUSINESS_TYPE])) { + $conditions[] = [ + 'field' => 'policy_business_type', + 'operator' => '==', + 'value' => $this->sanitize($rowData[self::COL_POLICY_BUSINESS_TYPE]) + ]; + } + + // Policy Name + if (!empty($rowData[self::COL_POLICY_NAME])) { + $conditions[] = [ + 'field' => 'policy_name', + 'operator' => '==', + 'value' => $this->sanitize($rowData[self::COL_POLICY_NAME]) + ]; + } + + // Premium Type (could be TP, OD, Comprehensive, etc.) + if (!empty($rowData[self::COL_PREMIUM_TYPE])) { + $premiumTypes = $this->parseCommaList($rowData[self::COL_PREMIUM_TYPE]); + + if (count($premiumTypes) > 1) { + $conditions[] = [ + 'field' => 'policy_type', + 'operator' => 'in', + 'value' => $premiumTypes + ]; + } else { + $conditions[] = [ + 'field' => 'policy_type', + 'operator' => '==', + 'value' => $premiumTypes[0] + ]; + } + } + + // Renewal Type + if (!empty($rowData[self::COL_RENEWAL_TYPE])) { + $renewalTypes = $this->parseCommaList($rowData[self::COL_RENEWAL_TYPE]); + + if (count($renewalTypes) > 1) { + $conditions[] = [ + 'field' => 'renewal_type', + 'operator' => 'in', + 'value' => $renewalTypes + ]; + } else { + $conditions[] = [ + 'field' => 'renewal_type', + 'operator' => '==', + 'value' => $renewalTypes[0] + ]; + } + } + + // Renewal Sub Type + if (!empty($rowData[self::COL_RENEWAL_SUB_TYPE])) { + $conditions[] = [ + 'field' => 'renewal_sub_type', + 'operator' => '==', + 'value' => $this->sanitize($rowData[self::COL_RENEWAL_SUB_TYPE]) + ]; + } + + return $conditions; + } + + /** + * Build calculation object based on commission type + */ + private function buildCalculation(array $rowData): array + { + $commissionType = strtolower(trim($rowData[self::COL_COMMISSION_TYPE] ?? '')); + $commissionValue = $rowData[self::COL_COMMISSION_VALUE] ?? ''; + $commissionParams = $rowData[self::COL_COMMISSION_PARAMS] ?? ''; + + switch ($commissionType) { + case 'composite': + return $this->buildCompositeCalculation($commissionParams); + + case 'percentage': + return $this->buildPercentageCalculation($commissionValue); + + case 'flat': + case 'fixed': + return $this->buildFixedCalculation($commissionValue); + + case 'tiered': + // For future implementation + return [ + 'type' => 'tiered', + 'tiers' => [] // To be implemented based on requirements + ]; + + default: + // Default to percentage if not specified + return $this->buildPercentageCalculation($commissionValue); + } + } + + /** + * Build composite calculation (multiple premium components) + */ + private function buildCompositeCalculation(string $params): array + { + $components = []; + + if (!empty($params)) { + $parts = explode(':', $params); + $premiumTypes = ['tp_premium', 'od_premium', 'pa_premium']; + + foreach ($premiumTypes as $index => $premiumType) { + if (isset($parts[$index]) && trim($parts[$index]) !== '') { + $percentage = (float)$parts[$index]; + if ($percentage > 0) { + $components[] = [ + 'percentage' => $percentage, + 'on' => $premiumType + ]; + } + } + } + } + + return [ + 'type' => 'composite', + 'components' => $components + ]; + } + + /** + * Build percentage calculation (single percentage on total/specific premium) + */ + private function buildPercentageCalculation(string $value): array + { + $percentage = !empty($value) ? (float)$value : 0; + + return [ + 'type' => 'percentage', + 'value' => $percentage, + 'on' => 'premium' // Default to total premium + ]; + } + + /** + * Build fixed/flat calculation (absolute amount) + */ + private function buildFixedCalculation(string $value): array + { + $amount = !empty($value) ? (float)$value : 0; + + return [ + 'type' => 'fixed', + 'value' => $amount, + 'on' => 'premium' // Default to total premium + ]; + } + + /** + * Parse comma-separated list into array + */ + private function parseCommaList(string $value): array + { + if (empty($value)) { + return []; + } + + return array_values(array_filter( + array_map('trim', explode(',', $value)), + fn($item) => $item !== '' + )); + } + +} + +/** + * ValidationResult - Helper class for managing validation errors + */ +class ValidationResult +{ + private array $errors = []; + + public function addError(int $row, string $column, string $message): void + { + $this->errors[$row][$column] = $message; + } + + public function hasErrors(): bool + { + return !empty($this->errors); + } + + public function hasRowErrors(int $row): bool + { + return isset($this->errors[$row]) && !empty($this->errors[$row]); + } + + public function getErrors(): array + { + return $this->errors; + } + + public function getErrorCount(): int + { + return array_sum(array_map('count', $this->errors)); + } + + public function getRowErrors(int $row): array + { + return $this->errors[$row] ?? []; + } +} \ No newline at end of file diff --git a/app/Models/CommissionFilesModel.php b/app/Models/CommissionFilesModel.php new file mode 100644 index 00000000..99a44a8b --- /dev/null +++ b/app/Models/CommissionFilesModel.php @@ -0,0 +1,67 @@ + 'required|min_length[1]|max_length[100]', + // 'insurer_id' => 'permit_empty|integer', + // 'department' => 'permit_empty|max_length[45]', + // 'commission_month' => 'permit_empty|valid_date', + // 'file_status' => 'permit_empty|max_length[10]', + // 'is_active' => 'permit_empty|in_list[0,1]' + // ]; + + protected $validationMessages = []; + protected $skipValidation = false; + + /** + * Get files with optional filters + */ + // public function getFiles($filters = []) + // { + // if (!empty($filters['insurer_id'])) { + // $this->where('insurer_id', $filters['insurer_id']); + // } + + // if (!empty($filters['department'])) { + // $this->where('department', $filters['department']); + // } + + // if (!empty($filters['file_status'])) { + // $this->where('file_status', $filters['file_status']); + // } + + // if (isset($filters['is_active'])) { + // $this->where('is_active', $filters['is_active']); + // } + + // return $this->orderBy('id', 'DESC')->findAll(); + // } +} diff --git a/app/Views/mail_template.php b/app/Views/mail_template.php index e4f6f20f..bead26fe 100644 --- a/app/Views/mail_template.php +++ b/app/Views/mail_template.php @@ -95,6 +95,7 @@ p{ background-image:url(''); background-size:contain; background-repeat:no-repeat; + background-position: center; ">