1394 lines
44 KiB
PHP
1394 lines
44 KiB
PHP
<?php
|
|
// declare(strict_types=1);
|
|
|
|
namespace App\Libraries;
|
|
|
|
use CodeIgniter\CLI\CLI;
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
use RuntimeException;
|
|
use Exception;
|
|
|
|
/**
|
|
* RuleImportService - Refactored Version
|
|
*
|
|
* Handles motor policy rule imports from Excel/CSV with:
|
|
* - Type-safe validation
|
|
* - Extensible architecture
|
|
* - Clear separation of concerns
|
|
* - Comprehensive error handling
|
|
*/
|
|
class RuleImportService
|
|
{
|
|
// File format constants
|
|
private const SUPPORTED_EXTENSIONS = ['xlsx', 'xls', 'csv'];
|
|
|
|
// Commission type constants
|
|
private const COMMISSION_PERCENTAGE = 'percentage';
|
|
private const COMMISSION_FLAT = 'flat';
|
|
private const COMMISSION_COMPOSITE = 'composite';
|
|
private const COMMISSION_TIERED = 'tiered';
|
|
private const COMMISSION_TYPES = [
|
|
self::COMMISSION_PERCENTAGE,
|
|
self::COMMISSION_FLAT,
|
|
self::COMMISSION_COMPOSITE,
|
|
self::COMMISSION_TIERED
|
|
];
|
|
|
|
// Column name constants
|
|
private const COL_RULE_NAME = 'Rule Name';
|
|
private const COL_POLICY_BUSINESS_TYPE = 'Policy Business Type';
|
|
private const COL_POLICY_NAME = 'Policy Name';
|
|
private const COL_PREMIUM_TYPE = 'Premium Type';
|
|
private const COL_VEHICLE_TYPE = 'Vehicle Type';
|
|
private const COL_VEHICLE_SUB_TYPE = 'Vehicle Sub Type';
|
|
private const COL_MAKE = 'Make';
|
|
private const COL_MODEL = 'Model';
|
|
private const COL_CC_MIN = 'CC Min';
|
|
private const COL_CC_MAX = 'CC Max';
|
|
private const COL_FUEL_TYPE = 'Fuel Type';
|
|
private const COL_VEHICLE_AGE_MIN = 'Vehicle Age Min';
|
|
private const COL_VEHICLE_AGE_MAX = 'Vehicle Age Max';
|
|
private const COL_VEHICLE_WEIGHT_MIN = 'Vehicle Weight Min';
|
|
private const COL_VEHICLE_WEIGHT_MAX = 'Vehicle Weight Max';
|
|
private const COL_RTO_STATE = 'RTO State';
|
|
private const COL_RTO_CITY = 'RTO City';
|
|
private const COL_RENEWAL_TYPE = 'Renewal Type';
|
|
private const COL_RENEWAL_SUB_TYPE = 'Renewal Sub Type';
|
|
private const COL_COMMISSION_TYPE = 'Commission Type';
|
|
private const COL_COMMISSION_VALUE = 'Commission Value';
|
|
private const COL_COMMISSION_PARAMS = 'Commission Params(TP:OD:PA)';
|
|
private const COL_NOTES = 'Notes';
|
|
|
|
protected array $expectedColumns;
|
|
protected array $columnValidators;
|
|
protected array $incomingData;
|
|
protected string $annotatedDir;
|
|
protected string $department;
|
|
protected string $uploadedCommissionFileID;
|
|
|
|
private ValidationResult $validationResult;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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] ?? [];
|
|
}
|
|
} |