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); $this->incomingData = $params; 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 { $temp_rule_id = $this->generateRuleId($rowData); //rule_e07f52d4366a2_32_oct2025_3_com $conditions = $this->buildConditions($rowData); // $temp_rule_id .= '_'.$this->incomingData['id'].'_'. strtolower(date('MY')).'_'.(count($conditions)); $monthFormatted = strtoupper(date('M', strtotime($this->incomingData['commission_month']))) . date('Y', strtotime($this->incomingData['commission_month'])); $temp_rule_id .= '_'.$this->incomingData['id'].'_'.$monthFormatted; $calculation = $this->buildCalculation($rowData); // $temp_rule_id .= '_'.substr($calculation['type'], 0, 3); $rule = [ 'id' => $temp_rule_id, 'name' => $this->sanitize($rowData[self::COL_RULE_NAME] ?? ''), 'department' => $this->department, 'is_deleted' => false, 'file_id' => $this->uploadedCommissionFileID, 'conditions' => $conditions, 'calculation' => $calculation, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), 'created_by' => $this->incomingData['created_by'], 'updated_by' => $this->incomingData['created_by'], ]; 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' => 'weight', 'operator' => '==', 'value' => (float)$weightMin ]; } else { $conditions[] = [ 'field' => 'weight', 'operator' => '>=', 'value' => (float)$weightMin ]; $conditions[] = [ 'field' => 'weight', 'operator' => '<=', 'value' => (float)$weightMax ]; } } elseif ($weightMin !== '') { $conditions[] = [ 'field' => 'weight', 'operator' => '>=', 'value' => (float)$weightMin ]; } elseif ($weightMax !== '') { $conditions[] = [ 'field' => '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' => 'geo_rto_state', 'operator' => 'in', 'value' => $states ]; } else { $conditions[] = [ 'field' => 'geo_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' => 'geo_rto_city', 'operator' => 'in', 'value' => $cities ]; } else { $conditions[] = [ 'field' => 'geo_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' => 'product', '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] ?? []; } }