From b97716ecb66f6560228f72b0eac170c7fce193b9 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Thu, 23 Jul 2026 09:15:18 +0530 Subject: [PATCH] GWM : go digit test fixes --- .env.sample | 3 +- app/Commands/DigitMotorImportMasters.php | 103 +++ app/Config/DigitMotor.php | 2 +- app/Config/Routes.php | 9 + app/Controllers/DigitMotorController.php | 204 +++++- app/Database/digit_motor_master_tables.sql | 152 +++++ app/Database/digit_motor_tables.sql | 4 +- app/Libraries/DigitMotor/DigitApiClient.php | 44 +- .../DigitMotor/DigitApiException.php | 42 +- app/Libraries/DigitMotor/DigitAuthClient.php | 27 +- .../DigitMotor/DigitExecutorService.php | 127 +++- .../DigitMotor/DigitMasterImportService.php | 633 ++++++++++++++++++ app/Models/MotorMasterAddonAgeLimitModel.php | 17 + app/Models/MotorMasterDocTypeModel.php | 16 + app/Models/MotorMasterImportLogModel.php | 17 + app/Models/MotorMasterNcbModel.php | 21 + .../MotorMasterNomineeRelationModel.php | 16 + app/Models/MotorMasterPincodeModel.php | 23 + .../MotorMasterPreviousInsurerModel.php | 21 + .../MotorMasterPreviousPolicyTypeModel.php | 21 + app/Models/MotorMasterProductModel.php | 25 + app/Models/MotorMasterRtoModel.php | 16 + app/Models/MotorMasterStateModel.php | 16 + app/Models/MotorMasterSubProductModel.php | 17 + app/Models/MotorMasterVehicleModel.php | 101 +++ .../MotorMasterVoluntaryDeductibleModel.php | 21 + app/Views/digit_motor/journey.php | 368 +++++++++- 27 files changed, 2013 insertions(+), 53 deletions(-) create mode 100644 app/Commands/DigitMotorImportMasters.php create mode 100644 app/Database/digit_motor_master_tables.sql create mode 100644 app/Libraries/DigitMotor/DigitMasterImportService.php create mode 100644 app/Models/MotorMasterAddonAgeLimitModel.php create mode 100644 app/Models/MotorMasterDocTypeModel.php create mode 100644 app/Models/MotorMasterImportLogModel.php create mode 100644 app/Models/MotorMasterNcbModel.php create mode 100644 app/Models/MotorMasterNomineeRelationModel.php create mode 100644 app/Models/MotorMasterPincodeModel.php create mode 100644 app/Models/MotorMasterPreviousInsurerModel.php create mode 100644 app/Models/MotorMasterPreviousPolicyTypeModel.php create mode 100644 app/Models/MotorMasterProductModel.php create mode 100644 app/Models/MotorMasterRtoModel.php create mode 100644 app/Models/MotorMasterStateModel.php create mode 100644 app/Models/MotorMasterSubProductModel.php create mode 100644 app/Models/MotorMasterVehicleModel.php create mode 100644 app/Models/MotorMasterVoluntaryDeductibleModel.php diff --git a/.env.sample b/.env.sample index e781e450..e8a13f85 100755 --- a/.env.sample +++ b/.env.sample @@ -194,7 +194,7 @@ ICICI_PRIMARY_KEY_CONSTANT = # Digit Motor (OneAPI) #-------------------------------------------------------------------- DIGIT_MOTOR_BASE_URL = https://preprod-oneapi.godigit.com -DIGIT_MOTOR_AUTH_PATH = /OneAPI/digit/generateAuthKey +DIGIT_MOTOR_AUTH_PATH = /OneAPI/v1/auth DIGIT_MOTOR_EXECUTOR_PATH = /OneAPI/v1/executor DIGIT_MOTOR_USERNAME = DIGIT_MOTOR_PASSWORD = @@ -202,6 +202,7 @@ DIGIT_MOTOR_ENVIRONMENT = staging DIGIT_MOTOR_TIMEOUT = 30 DIGIT_MOTOR_TOKEN_LEEWAY_SEC = 60 DIGIT_MOTOR_PDF_AUTH_KEY = +DIGIT_MOTOR_MASTERS_PATH = DIGIT_MOTOR_IID_QUICK_QUOTE = 29266-0100 DIGIT_MOTOR_IID_CREATE_QUOTE = 29268-0100 DIGIT_MOTOR_IID_KYC = 29269-0100 diff --git a/app/Commands/DigitMotorImportMasters.php b/app/Commands/DigitMotorImportMasters.php new file mode 100644 index 00000000..5b8e2caf --- /dev/null +++ b/app/Commands/DigitMotorImportMasters.php @@ -0,0 +1,103 @@ + 'Path to Digit Masters folder (xlsx files).', + '--create-tables' => 'Run digit_motor_master_tables.sql first.', + '--only' => 'Comma-separated master keys to import.', + '--skip-vehicles' => 'Skip large vehicle master import.', + ]; + + public function run(array $params) + { + $defaultPath = '/home/smart/Downloads/Digit API Integration_API kit (1)/Masters'; + $path = CLI::getOption('path') ?: env('DIGIT_MOTOR_MASTERS_PATH', $defaultPath); + $path = rtrim((string) $path, '/'); + + if (!is_dir($path)) { + CLI::error('Masters path not found: ' . $path); + return EXIT_ERROR; + } + + $service = new DigitMasterImportService($path); + $service->setProgressCallback(static function (string $msg) { + CLI::write($msg, 'yellow'); + }); + + if (CLI::getOption('create-tables') !== null) { + $sql = ROOTPATH . 'app/Database/digit_motor_master_tables.sql'; + CLI::write('Creating tables from ' . $sql, 'green'); + try { + $service->createTables($sql); + CLI::write('Tables created / verified.', 'green'); + } catch (\Throwable $e) { + CLI::error('Create tables failed: ' . $e->getMessage()); + return EXIT_ERROR; + } + } + + $onlyOpt = CLI::getOption('only'); + // CI may return true for `--only=vehicles` when value parsing fails — also check argv + if ($onlyOpt === true || $onlyOpt === null || $onlyOpt === '') { + foreach ($_SERVER['argv'] ?? [] as $arg) { + if (str_starts_with((string) $arg, '--only=')) { + $onlyOpt = substr((string) $arg, 7); + break; + } + } + } + $only = []; + if (is_string($onlyOpt) && $onlyOpt !== '' && $onlyOpt !== '1') { + $only = array_filter(array_map('trim', explode(',', $onlyOpt))); + } + + if (CLI::getOption('skip-vehicles') !== null) { + if (!$only) { + $only = [ + 'products', 'previous_insurers', 'ncb', 'voluntary_deductible', + 'previous_policy_type', 'doc_types', 'nominee_relations', 'states', + 'sub_products', 'addon_age_limits', 'pincodes', 'rtos', + ]; + } else { + $only = array_values(array_diff($only, ['vehicles'])); + } + } + + CLI::write('Importing from: ' . $path, 'green'); + $results = $service->importAll($only); + + CLI::newLine(); + CLI::write(str_pad('Master', 28) . str_pad('Rows', 10) . 'Status', 'white'); + CLI::write(str_repeat('-', 55)); + foreach ($results as $key => $res) { + $line = str_pad($key, 28) . str_pad((string) $res['rows'], 10) . $res['status']; + if (!empty($res['message'])) { + $line .= ' — ' . $res['message']; + } + CLI::write($line, $res['status'] === 'OK' ? 'green' : 'red'); + } + + $failed = array_filter($results, static fn ($r) => $r['status'] !== 'OK'); + return $failed ? EXIT_ERROR : EXIT_SUCCESS; + } +} diff --git a/app/Config/DigitMotor.php b/app/Config/DigitMotor.php index 1c39594a..3ef3bb5b 100644 --- a/app/Config/DigitMotor.php +++ b/app/Config/DigitMotor.php @@ -23,7 +23,7 @@ class DigitMotor extends BaseConfig parent::__construct(); $this->baseUrl = rtrim((string) env('DIGIT_MOTOR_BASE_URL', 'https://preprod-oneapi.godigit.com'), '/'); - $this->authPath = (string) env('DIGIT_MOTOR_AUTH_PATH', '/OneAPI/digit/generateAuthKey'); + $this->authPath = (string) env('DIGIT_MOTOR_AUTH_PATH', '/OneAPI/v1/auth'); $this->executorPath = (string) env('DIGIT_MOTOR_EXECUTOR_PATH', '/OneAPI/v1/executor'); $this->username = (string) env('DIGIT_MOTOR_USERNAME', ''); $this->password = (string) env('DIGIT_MOTOR_PASSWORD', ''); diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 3e18c556..e1868f0f 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1209,6 +1209,15 @@ $routes->group('digit-motor', ['filter' => 'authMVC', 'namespace' => 'App\Contro $routes->get('journey/(:num)', 'DigitMotorController::journey/$1'); $routes->get('quotes/(:num)', 'DigitMotorController::detail/$1'); + // Digit master lookups + $routes->get('masters/bootstrap', 'DigitMotorController::mastersBootstrap'); + $routes->get('masters/vehicles/search', 'DigitMotorController::masterVehicleSearch'); + $routes->get('masters/vehicles/makes', 'DigitMotorController::masterVehicleMakes'); + $routes->get('masters/vehicles/models', 'DigitMotorController::masterVehicleModels'); + $routes->get('masters/vehicles/variants', 'DigitMotorController::masterVehicleVariants'); + $routes->get('masters/pincode/(:num)', 'DigitMotorController::masterPincode/$1'); + $routes->get('masters/pincode', 'DigitMotorController::masterPincode'); + $routes->post('quotes/quick', 'DigitMotorController::quickQuote'); $routes->post('quotes/create', 'DigitMotorController::createQuote'); $routes->post('quotes/(:num)/create', 'DigitMotorController::createQuote/$1'); diff --git a/app/Controllers/DigitMotorController.php b/app/Controllers/DigitMotorController.php index 54a7e164..7035a7f1 100644 --- a/app/Controllers/DigitMotorController.php +++ b/app/Controllers/DigitMotorController.php @@ -4,6 +4,13 @@ namespace App\Controllers; use App\Libraries\DigitMotor\DigitApiException; use App\Libraries\DigitMotor\DigitExecutorService; +use App\Models\MotorMasterNcbModel; +use App\Models\MotorMasterPincodeModel; +use App\Models\MotorMasterPreviousInsurerModel; +use App\Models\MotorMasterPreviousPolicyTypeModel; +use App\Models\MotorMasterProductModel; +use App\Models\MotorMasterVehicleModel; +use App\Models\MotorMasterVoluntaryDeductibleModel; use App\Models\MotorQuoteModel; use CodeIgniter\API\ResponseTrait; @@ -13,12 +20,26 @@ class DigitMotorController extends BaseController protected MotorQuoteModel $quoteModel; protected DigitExecutorService $executor; + protected MotorMasterVehicleModel $vehicleMaster; + protected MotorMasterProductModel $productMaster; + protected MotorMasterPreviousInsurerModel $insurerMaster; + protected MotorMasterNcbModel $ncbMaster; + protected MotorMasterPreviousPolicyTypeModel $prevPolicyTypeMaster; + protected MotorMasterVoluntaryDeductibleModel $deductibleMaster; + protected MotorMasterPincodeModel $pincodeMaster; public function __construct() { set_session_context('Digit Motor Controller'); - $this->quoteModel = new MotorQuoteModel(); - $this->executor = new DigitExecutorService(); + $this->quoteModel = new MotorQuoteModel(); + $this->executor = new DigitExecutorService(); + $this->vehicleMaster = new MotorMasterVehicleModel(); + $this->productMaster = new MotorMasterProductModel(); + $this->insurerMaster = new MotorMasterPreviousInsurerModel(); + $this->ncbMaster = new MotorMasterNcbModel(); + $this->prevPolicyTypeMaster = new MotorMasterPreviousPolicyTypeModel(); + $this->deductibleMaster = new MotorMasterVoluntaryDeductibleModel(); + $this->pincodeMaster = new MotorMasterPincodeModel(); } // ===================== LIST ===================== @@ -78,6 +99,92 @@ class DigitMotorController extends BaseController return $this->respond(['status' => true, 'data' => $detail]); } + // ===================== MASTER LOOKUPS ===================== + + public function mastersBootstrap() + { + try { + return $this->respond([ + 'status' => true, + 'data' => [ + 'products' => $this->productMaster->activeList(), + 'previous_insurers' => $this->insurerMaster->activeList(), + 'ncb' => $this->ncbMaster->activeList(), + 'previous_policy_types' => $this->prevPolicyTypeMaster->activeList(), + 'voluntary_deductibles' => $this->deductibleMaster->activeList(), + ], + ]); + } catch (\Throwable $e) { + log_message('error', 'DigitMotor mastersBootstrap: ' . $e->getMessage()); + return $this->respond([ + 'status' => false, + 'message' => 'Master tables not ready. Run: php spark digit-motor:import-masters --create-tables', + ], 503); + } + } + + public function masterVehicleSearch() + { + $q = trim((string) ($this->request->getGet('q') ?? '')); + if (strlen($q) < 2) { + return $this->respond(['status' => true, 'data' => []]); + } + + return $this->respond([ + 'status' => true, + 'data' => $this->vehicleMaster->search($q, 40), + ]); + } + + public function masterVehicleMakes() + { + $q = trim((string) ($this->request->getGet('q') ?? '')); + return $this->respond([ + 'status' => true, + 'data' => $this->vehicleMaster->distinctMakes($q ?: null, 120), + ]); + } + + public function masterVehicleModels() + { + $make = trim((string) ($this->request->getGet('make') ?? '')); + if ($make === '') { + return $this->respond(['status' => false, 'message' => 'make is required.'], 422); + } + $q = trim((string) ($this->request->getGet('q') ?? '')); + return $this->respond([ + 'status' => true, + 'data' => $this->vehicleMaster->distinctModels($make, $q ?: null), + ]); + } + + public function masterVehicleVariants() + { + $make = trim((string) ($this->request->getGet('make') ?? '')); + $model = trim((string) ($this->request->getGet('model') ?? '')); + if ($make === '' || $model === '') { + return $this->respond(['status' => false, 'message' => 'make and model are required.'], 422); + } + return $this->respond([ + 'status' => true, + 'data' => $this->vehicleMaster->variants($make, $model), + ]); + } + + public function masterPincode($pincode = null) + { + $pincode = preg_replace('/\D+/', '', (string) ($pincode ?: $this->request->getGet('pincode'))); + if (strlen($pincode) !== 6) { + return $this->respond(['status' => false, 'message' => 'Valid 6-digit pincode required.'], 422); + } + $row = $this->pincodeMaster->findActive($pincode); + return $this->respond([ + 'status' => (bool) $row, + 'data' => $row, + 'message'=> $row ? null : 'Pincode not found in Digit master.', + ], $row ? 200 : 404); + } + // ===================== API ACTIONS ===================== public function quickQuote() @@ -85,17 +192,30 @@ class DigitMotorController extends BaseController return $this->runAction(function () { $input = $this->request->getJSON(true) ?: $this->request->getPost(); $rules = [ - 'license_plate_number' => 'required|min_length[4]', - 'vehicle_maincode' => 'required', - 'registration_date' => 'required', - 'manufacture_date' => 'required', - 'pincode' => 'required|exact_length[6]', + 'license_plate_number' => 'required|min_length[4]', + 'vehicle_maincode' => 'required', + 'registration_date' => 'required', + 'manufacture_date' => 'required', + 'vehicle_identification_number' => 'required|min_length[5]', + 'engine_number' => 'required|min_length[5]', + 'pincode' => 'required|exact_length[6]', + 'insurance_product_code' => 'required', ]; if (!$this->validateDigitInput($input, $rules)) { + $errors = $this->validator->getErrors(); + $first = reset($errors); return $this->respond([ 'status' => false, - 'message' => 'Validation failed.', - 'errors' => $this->validator->getErrors(), + 'message' => is_string($first) ? $first : 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $masterError = $this->validateQuickQuoteMasters($input); + if ($masterError !== null) { + return $this->respond([ + 'status' => false, + 'message' => $masterError, ], 422); } @@ -117,6 +237,26 @@ class DigitMotorController extends BaseController return $this->respond(['status' => false, 'message' => 'quote_id is required.'], 422); } + $rules = [ + 'first_name' => 'required', + 'mobile' => 'required|exact_length[10]', + 'email' => 'required|valid_email', + 'pan' => 'required', + 'dob' => 'required', + 'address' => 'required', + 'vehicle_identification_number' => 'required|min_length[5]', + 'engine_number' => 'required|min_length[5]', + ]; + if (!$this->validateDigitInput($input, $rules)) { + $errors = $this->validator->getErrors(); + $first = reset($errors); + return $this->respond([ + 'status' => false, + 'message' => is_string($first) ? $first : 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $result = $this->executor->createQuote($quoteId, $input); return $this->respond([ 'status' => true, @@ -197,7 +337,7 @@ class DigitMotorController extends BaseController try { return $fn(); } catch (DigitApiException $e) { - log_message('error', 'DigitMotor API error: ' . $e->getMessage() . ' code=' . $e->getDigitCode()); + log_message('error', $e->toLogMessage()); $http = $e->getHttpStatus() >= 400 ? $e->getHttpStatus() : 502; if ($e->isInfraError()) { $http = 502; @@ -223,4 +363,48 @@ class DigitMotorController extends BaseController $this->validator->setRules($rules); return $this->validator->run($data); } + + /** + * Ensure vehicleMaincode / pincode / product exist in Digit masters before API call. + */ + protected function validateQuickQuoteMasters(array $input): ?string + { + try { + $code = trim((string) ($input['vehicle_maincode'] ?? '')); + if ($code === '' || !$this->vehicleMaster->findActive($code)) { + return 'Vehicle code "' . $code . '" not found in Digit vehicle master. Select Make → Model → Variant.'; + } + + $pin = preg_replace('/\D+/', '', (string) ($input['pincode'] ?? '')); + if (strlen($pin) !== 6 || !$this->pincodeMaster->findActive($pin)) { + return 'Pincode "' . ($input['pincode'] ?? '') . '" not found in Digit pin master (may not be serviceable).'; + } + + $product = trim((string) ($input['insurance_product_code'] ?? '')); + if ($product !== '') { + $prod = $this->productMaster->where('product_code', $product)->where('is_active', 1)->first(); + if (!$prod) { + return 'Product code "' . $product . '" is not a valid Digit motor product.'; + } + } + + $insurer = trim((string) ($input['previous_insurer_code'] ?? '')); + if ($insurer !== '') { + $row = $this->insurerMaster->where('insurer_code', str_pad($insurer, 3, '0', STR_PAD_LEFT)) + ->where('is_active', 1)->first(); + if (!$row) { + // also try raw code + $row = $this->insurerMaster->where('insurer_code', $insurer)->where('is_active', 1)->first(); + } + if (!$row) { + return 'Previous insurer code "' . $insurer . '" not found in Digit previous-insurer master.'; + } + } + } catch (\Throwable $e) { + log_message('error', 'DigitMotor master validation skipped: ' . $e->getMessage()); + // If masters table missing, don't block — Digit will still validate + } + + return null; + } } diff --git a/app/Database/digit_motor_master_tables.sql b/app/Database/digit_motor_master_tables.sql new file mode 100644 index 00000000..523139f4 --- /dev/null +++ b/app/Database/digit_motor_master_tables.sql @@ -0,0 +1,152 @@ +-- Digit Motor master / lookup tables +-- Source: Digit API Integration kit → Masters/ +-- Run once (or via: php spark digit-motor:import-masters --create-tables) + +CREATE TABLE IF NOT EXISTS motor_master_vehicle ( + vehicle_code VARCHAR(30) NOT NULL, + make VARCHAR(80) NOT NULL, + model VARCHAR(120) NOT NULL, + variant VARCHAR(120) DEFAULT NULL, + body_type VARCHAR(60) DEFAULT NULL, + seating_capacity SMALLINT DEFAULT NULL, + power DECIMAL(10,2) DEFAULT NULL, + cubic_capacity DECIMAL(10,2) DEFAULT NULL, + gross_vehicle_weight DECIMAL(12,2) DEFAULT NULL, + fuel_type VARCHAR(40) DEFAULT NULL, + no_of_wheels TINYINT DEFAULT NULL, + abs CHAR(1) DEFAULT NULL, + air_bags SMALLINT DEFAULT NULL, + length_m DECIMAL(10,3) DEFAULT NULL, + ex_showroom_price DECIMAL(14,2) DEFAULT NULL, + price_year SMALLINT DEFAULT NULL, + production_status VARCHAR(60) DEFAULT NULL, + manufacturing VARCHAR(40) DEFAULT NULL, + vehicle_type VARCHAR(40) DEFAULT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (vehicle_code), + KEY idx_mmv_make (make), + KEY idx_mmv_make_model (make, model), + KEY idx_mmv_make_model_variant (make, model, variant), + KEY idx_mmv_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_previous_insurer ( + insurer_code VARCHAR(10) NOT NULL, + insurer_name VARCHAR(180) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (insurer_code), + KEY idx_mmpi_name (insurer_name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_product ( + product_code VARCHAR(10) NOT NULL, + product_name VARCHAR(120) NOT NULL, + vehicle_class VARCHAR(10) DEFAULT NULL COMMENT '2W / 4W / CV', + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (product_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_sub_product ( + id INT PRIMARY KEY AUTO_INCREMENT, + business_type VARCHAR(20) NOT NULL COMMENT 'NEW / ROLLOVER', + product_label VARCHAR(120) NOT NULL, + sub_product_code VARCHAR(20) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_mmsp (business_type, product_label, sub_product_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_pincode ( + pincode VARCHAR(6) NOT NULL, + city VARCHAR(120) DEFAULT NULL, + district VARCHAR(120) DEFAULT NULL, + street VARCHAR(180) DEFAULT NULL, + taluk VARCHAR(120) DEFAULT NULL, + state_code VARCHAR(10) DEFAULT NULL, + segment VARCHAR(40) DEFAULT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (pincode), + KEY idx_mmp_city (city), + KEY idx_mmp_state (state_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_rto ( + rto_code VARCHAR(10) NOT NULL, + city_state VARCHAR(180) DEFAULT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (rto_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_ncb ( + ncb_code VARCHAR(30) NOT NULL, + sort_order SMALLINT NOT NULL DEFAULT 0, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (ncb_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_previous_policy_type ( + policy_type_code VARCHAR(20) NOT NULL, + description VARCHAR(120) DEFAULT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (policy_type_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_voluntary_deductible ( + deductible_code VARCHAR(40) NOT NULL, + sort_order SMALLINT NOT NULL DEFAULT 0, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (deductible_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_doc_type ( + doc_code VARCHAR(10) NOT NULL, + doc_type VARCHAR(60) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (doc_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_nominee_relation ( + relation_code VARCHAR(40) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (relation_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_state ( + state_code VARCHAR(10) NOT NULL, + state_name VARCHAR(120) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (state_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_addon_age_limit ( + id INT PRIMARY KEY AUTO_INCREMENT, + addon_name VARCHAR(120) NOT NULL, + age_limit_4w VARCHAR(120) DEFAULT NULL, + age_limit_2w VARCHAR(255) DEFAULT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_mmaal_addon (addon_name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS motor_master_import_log ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + master_key VARCHAR(60) NOT NULL, + source_file VARCHAR(255) DEFAULT NULL, + rows_upserted INT NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'OK', + message TEXT DEFAULT NULL, + imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_mmil_master (master_key), + KEY idx_mmil_imported (imported_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/app/Database/digit_motor_tables.sql b/app/Database/digit_motor_tables.sql index 30b6aadd..2bc7a99b 100644 --- a/app/Database/digit_motor_tables.sql +++ b/app/Database/digit_motor_tables.sql @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS motor_quote ( id BIGINT PRIMARY KEY AUTO_INCREMENT, enquiry_id VARCHAR(64) NOT NULL, quote_number VARCHAR(32) DEFAULT NULL, - application_id VARCHAR(128) DEFAULT NULL, + application_id VARCHAR(255) DEFAULT NULL, policy_holder_type VARCHAR(20) NOT NULL DEFAULT 'INDIVIDUAL', insurance_product_code VARCHAR(10) NOT NULL, sub_insurance_product_code VARCHAR(10) NOT NULL DEFAULT 'PB', @@ -78,7 +78,7 @@ CREATE TABLE IF NOT EXISTS motor_kyc ( CREATE TABLE IF NOT EXISTS motor_payment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, quote_id BIGINT NOT NULL, - application_id VARCHAR(128) NOT NULL, + application_id VARCHAR(255) NOT NULL, digit_payment_id VARCHAR(64) DEFAULT NULL, request_reference VARCHAR(64) DEFAULT NULL, payment_mode VARCHAR(5) DEFAULT 'EB', diff --git a/app/Libraries/DigitMotor/DigitApiClient.php b/app/Libraries/DigitMotor/DigitApiClient.php index 9cba0f99..3ad4f639 100644 --- a/app/Libraries/DigitMotor/DigitApiClient.php +++ b/app/Libraries/DigitMotor/DigitApiClient.php @@ -77,7 +77,15 @@ class DigitApiClient if ($this->isHardFail($httpCode, $digitCode, $response['status'] ?? false, $data)) { $message = $this->extractMessage($data, $httpCode); - throw new DigitApiException($message, $digitCode, $httpCode, $data); + throw new DigitApiException( + $message, + $digitCode, + $httpCode, + $data, + null, + $url, + $this->redact($payload) + ); } return is_array($data) ? $data : []; @@ -97,10 +105,18 @@ class DigitApiClient } // Digit sometimes returns 200 with error object if (is_array($data) && isset($data['error']) && !isset($data['grossPremium']) && !isset($data['premium']) && !isset($data['quoteNumber'])) { - $errCode = $data['error']['code'] ?? $data['code'] ?? null; + $errCode = $data['error']['errorCode'] + ?? $data['error']['code'] + ?? $data['code'] + ?? $data['statusCode'] + ?? null; if ($errCode !== null && (string) $errCode !== '0' && strtoupper((string) $errCode) !== 'SUCCESS') { return true; } + // validationMessages without a success payload + if (!empty($data['error']['validationMessages'])) { + return true; + } } return false; } @@ -110,10 +126,12 @@ class DigitApiClient if (!is_array($data)) { return $httpCode ?: null; } - return $data['error']['code'] + return $data['error']['errorCode'] + ?? $data['error']['code'] ?? $data['code'] ?? $data['errorCode'] ?? $data['responseCode'] + ?? $data['statusCode'] ?? ($httpCode >= 400 ? (string) $httpCode : null); } @@ -122,13 +140,31 @@ class DigitApiClient if (!is_array($data)) { return 'Digit API request failed (HTTP ' . $httpCode . ').'; } + + // Digit often returns validationMessages[] instead of a single message + $validation = $data['error']['validationMessages'] ?? $data['validationMessages'] ?? null; + if (is_array($validation) && $validation) { + $parts = []; + foreach ($validation as $item) { + if (is_string($item) && trim($item) !== '') { + $parts[] = trim($item); + } elseif (is_array($item)) { + $parts[] = trim((string) ($item['message'] ?? json_encode($item))); + } + } + if ($parts) { + return implode(' | ', $parts); + } + } + $msg = $data['error']['message'] ?? $data['message'] ?? $data['errorMessage'] ?? $data['responseMessage'] + ?? $data['statusMessage'] ?? null; if (is_array($msg)) { - $msg = json_encode($msg); + $msg = implode(' | ', array_map('strval', $msg)); } return $msg ?: 'Digit API request failed (HTTP ' . $httpCode . ').'; } diff --git a/app/Libraries/DigitMotor/DigitApiException.php b/app/Libraries/DigitMotor/DigitApiException.php index 7bb67eef..4c04f2f9 100644 --- a/app/Libraries/DigitMotor/DigitApiException.php +++ b/app/Libraries/DigitMotor/DigitApiException.php @@ -10,19 +10,25 @@ class DigitApiException extends Exception protected $digitMessage; protected $httpStatus; protected $responseBody; + protected $requestUrl; + protected $requestBody; public function __construct( string $message, $digitCode = null, int $httpStatus = 0, $responseBody = null, - ?Exception $previous = null + ?Exception $previous = null, + ?string $requestUrl = null, + $requestBody = null ) { parent::__construct($message, 0, $previous); $this->digitCode = $digitCode; $this->digitMessage = $message; $this->httpStatus = $httpStatus; $this->responseBody = $responseBody; + $this->requestUrl = $requestUrl; + $this->requestBody = $requestBody; } public function getDigitCode() @@ -40,9 +46,43 @@ class DigitApiException extends Exception return $this->responseBody; } + public function getRequestUrl(): ?string + { + return $this->requestUrl; + } + + public function getRequestBody() + { + return $this->requestBody; + } + public function isInfraError(): bool { $code = (string) $this->digitCode; return in_array($code, ['403', '999'], true) || in_array($this->httpStatus, [403], true); } + + /** + * Compact log line with url / request / response for debugging. + */ + public function toLogMessage(): string + { + $encode = static function ($value): string { + if ($value === null) { + return 'null'; + } + if (is_string($value)) { + return $value; + } + $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + return $json !== false ? $json : '[unencodable]'; + }; + + return 'DigitMotor API error: ' . $this->getMessage() + . ' | code=' . $this->digitCode + . ' | http=' . $this->httpStatus + . ' | url=' . ($this->requestUrl ?? 'n/a') + . ' | request=' . $encode($this->requestBody) + . ' | response=' . $encode($this->responseBody); + } } diff --git a/app/Libraries/DigitMotor/DigitAuthClient.php b/app/Libraries/DigitMotor/DigitAuthClient.php index cb632afe..928ab220 100644 --- a/app/Libraries/DigitMotor/DigitAuthClient.php +++ b/app/Libraries/DigitMotor/DigitAuthClient.php @@ -85,12 +85,33 @@ class DigitAuthClient ?? null; if (empty($accessToken) || empty($response['status'])) { - $msg = $data['message'] ?? $data['error'] ?? 'Digit token generation failed.'; + $msg = null; + if (is_array($data)) { + $msg = $data['message'] ?? $data['error'] ?? $data['error_description'] ?? null; + if (is_array($msg)) { + $msg = json_encode($msg); + } + } + if (!$msg && is_string($response['data'] ?? null) && stripos((string) $response['data'], '502') !== false) { + $msg = 'Digit auth gateway is down (HTTP 502). Retry in a few minutes.'; + } + if (!$msg && $httpCode >= 500) { + $msg = 'Digit auth service unavailable (HTTP ' . $httpCode . ').'; + } + if (!$msg) { + $msg = 'Digit token generation failed.'; + } throw new DigitApiException( - is_string($msg) ? $msg : 'Digit token generation failed.', + $msg, $data['code'] ?? (string) $httpCode, $httpCode, - $data + is_array($data) ? $data : ['raw' => $response['data'] ?? null], + null, + $url, + [ + 'username' => $this->config->username, + 'password' => '***REDACTED***', + ] ); } diff --git a/app/Libraries/DigitMotor/DigitExecutorService.php b/app/Libraries/DigitMotor/DigitExecutorService.php index 2db6cf5c..a9198297 100644 --- a/app/Libraries/DigitMotor/DigitExecutorService.php +++ b/app/Libraries/DigitMotor/DigitExecutorService.php @@ -3,6 +3,7 @@ namespace App\Libraries\DigitMotor; use App\Models\MotorKycModel; +use App\Models\MotorApiLogModel; use App\Models\MotorPaymentModel; use App\Models\MotorPolicyModel; use App\Models\MotorQuoteModel; @@ -99,6 +100,29 @@ class DigitExecutorService throw new DigitApiException('Quote not found.', '404', 404); } + // Digit Create Quote rejects empty / invalid engine + chassis (VIN). + $vin = strtoupper(trim((string) ($input['vehicle_identification_number'] ?? ($detail['vehicle']['vehicle_identification_number'] ?? '')))); + $engine = strtoupper(trim((string) ($input['engine_number'] ?? ($detail['vehicle']['engine_number'] ?? '')))); + if ($vin === '' || strlen($vin) < 5) { + throw new DigitApiException('Please enter a valid chassis / VIN number.', '400', 422); + } + if ($engine === '' || strlen($engine) < 5) { + throw new DigitApiException('Please enter a valid engine number.', '400', 422); + } + + $vehicle = $this->vehicleModel->where('quote_id', $quoteId)->first(); + if ($vehicle) { + $this->vehicleModel->update($vehicle['id'], [ + 'vehicle_identification_number' => $vin, + 'engine_number' => $engine, + ]); + $detail['vehicle']['vehicle_identification_number'] = $vin; + $detail['vehicle']['engine_number'] = $engine; + } + + $input['vehicle_identification_number'] = $vin; + $input['engine_number'] = $engine; + $payload = $this->buildCreateQuotePayload($detail, $input); $response = $this->api->post( @@ -206,8 +230,23 @@ class DigitExecutorService throw new DigitApiException('Application ID is required for payment. Create quote first.', '400', 400); } - $premiumAmount = $input['premium_amount'] - ?? ('INR ' . number_format((float) ($detail['premium'] ?? 0), 2, '.', '')); + // Prefer UI override, then stored premium; Digit expects "INR 1234.56" + $premiumValue = null; + if (!empty($input['premium_amount'])) { + $premiumValue = $this->parseMoneyAmount($input['premium_amount']); + } + if ($premiumValue === null) { + $premiumValue = $this->parseMoneyAmount($detail['premium'] ?? null); + } + if ($premiumValue === null || $premiumValue <= 0) { + throw new DigitApiException( + 'Premium amount is missing or zero. Re-run Create Quote before generating payment link.', + '400', + 422 + ); + } + + $premiumAmount = 'INR ' . number_format($premiumValue, 2, '.', ''); $successUrl = $input['success_return_url'] ?? base_url('digit-motor/payment/callback/success/' . $quoteId); @@ -222,6 +261,12 @@ class DigitExecutorService 'applicationId' => $detail['application_id'], ]; + // When Digit's ABS lookup fails, payment API asks for create-quote payload as absContractDetails. + $cqResponse = $this->latestCreateQuoteResponse($quoteId); + if (is_array($cqResponse) && $cqResponse !== []) { + $payload['absContractDetails'] = $cqResponse; + } + $response = $this->api->post( $this->config->executorPath, $payload, @@ -243,13 +288,18 @@ class DigitExecutorService 'cancel_return_url' => $cancelUrl, 'success_return_url' => $successUrl, 'dispatcher_response' => $dispatcher, - 'premium' => $detail['premium'], + 'premium' => $premiumValue, 'payment_status' => 'LINK_GENERATED', 'created_at' => date('Y-m-d H:i:s'), ]; $row['id'] = $this->paymentModel->insert($row); + // Keep quote premium in sync if it was previously null/zero + if (empty($detail['premium']) || (float) $detail['premium'] <= 0) { + $this->quoteModel->update($quoteId, ['premium' => $premiumValue]); + } + return [ 'quote_id' => $quoteId, 'payment' => $row, @@ -258,6 +308,32 @@ class DigitExecutorService ]; } + /** + * Last successful Create Quote response for this quote (used as absContractDetails). + */ + protected function latestCreateQuoteResponse(int $quoteId): ?array + { + $logModel = new MotorApiLogModel(); + $row = $logModel + ->where('quote_id', $quoteId) + ->where('integration_id', $this->config->integrationIds['createQuote']) + ->where('http_status', 200) + ->orderBy('id', 'DESC') + ->first(); + + if (!$row || empty($row['response_body'])) { + return null; + } + + $body = $row['response_body']; + if (is_string($body)) { + $decoded = json_decode($body, true); + return is_array($decoded) ? $decoded : null; + } + + return is_array($body) ? $body : null; + } + /** * Poll policy status. * @@ -523,9 +599,11 @@ class DigitExecutorService 'isVehicleNew' => !empty($vehicle['is_vehicle_new']) ? 'false' : 'false', 'vehicleMaincode' => $vehicle['vehicle_maincode'] ?? '', 'licensePlateNumber' => $plate, - 'vehicleIdentificationNumber' => $vehicle['vehicle_identification_number'] ?? '', + 'vehicleIdentificationNumber' => $input['vehicle_identification_number'] + ?? ($vehicle['vehicle_identification_number'] ?? ''), 'registrationAuthority' => $authority, - 'engineNumber' => $vehicle['engine_number'] ?? '', + 'engineNumber' => $input['engine_number'] + ?? ($vehicle['engine_number'] ?? ''), 'manufactureDate' => $vehicle['manufacture_date'] ?? null, 'registrationDate' => $vehicle['registration_date'] ?? null, 'vehicleIDV' => [ @@ -722,17 +800,48 @@ class DigitExecutorService protected function pickNumber(array $response, array $keys): ?float { foreach ($keys as $key) { - if (isset($response[$key]) && is_numeric($response[$key])) { - return (float) $response[$key]; + if (!array_key_exists($key, $response)) { + continue; + } + $parsed = $this->parseMoneyAmount($response[$key]); + if ($parsed !== null) { + return $parsed; } } if (isset($response['premiumBreakUp']) && is_array($response['premiumBreakUp'])) { foreach (['grossPremium', 'totalPremium', 'netPremium'] as $k) { - if (isset($response['premiumBreakUp'][$k]) && is_numeric($response['premiumBreakUp'][$k])) { - return (float) $response['premiumBreakUp'][$k]; + if (!array_key_exists($k, $response['premiumBreakUp'])) { + continue; + } + $parsed = $this->parseMoneyAmount($response['premiumBreakUp'][$k]); + if ($parsed !== null) { + return $parsed; } } } return null; } + + /** + * Digit returns money as numbers or strings like "INR 4030.88". + */ + protected function parseMoneyAmount($value): ?float + { + if ($value === null || $value === '') { + return null; + } + if (is_int($value) || is_float($value)) { + return (float) $value; + } + if (!is_string($value)) { + return null; + } + if (is_numeric($value)) { + return (float) $value; + } + if (preg_match('/([0-9]+(?:\.[0-9]+)?)/', $value, $m)) { + return (float) $m[1]; + } + return null; + } } diff --git a/app/Libraries/DigitMotor/DigitMasterImportService.php b/app/Libraries/DigitMotor/DigitMasterImportService.php new file mode 100644 index 00000000..649521b3 --- /dev/null +++ b/app/Libraries/DigitMotor/DigitMasterImportService.php @@ -0,0 +1,633 @@ +mastersPath = rtrim($mastersPath ?: '', '/'); + $this->db = db_connect(); + $this->logModel = new MotorMasterImportLogModel(); + } + + public function setProgressCallback(?callable $cb): self + { + $this->progressCallback = $cb; + return $this; + } + + public function createTables(string $sqlFile): void + { + if (!is_file($sqlFile)) { + throw new \RuntimeException('SQL file not found: ' . $sqlFile); + } + $sql = file_get_contents($sqlFile); + foreach (array_filter(array_map('trim', explode(';', $sql))) as $stmt) { + if ($stmt === '' || str_starts_with($stmt, '--')) { + continue; + } + // skip pure comment blocks + $clean = preg_replace('/^--.*$/m', '', $stmt); + $clean = trim($clean ?? ''); + if ($clean === '') { + continue; + } + $this->db->query($clean); + } + } + + /** + * @return array + */ + public function importAll(array $only = []): array + { + $map = [ + 'products' => fn () => $this->importProducts(), + 'previous_insurers' => fn () => $this->importPreviousInsurers(), + 'ncb' => fn () => $this->importNcb(), + 'voluntary_deductible'=> fn () => $this->importVoluntaryDeductible(), + 'previous_policy_type'=> fn () => $this->importPreviousPolicyType(), + 'doc_types' => fn () => $this->importDocTypes(), + 'nominee_relations' => fn () => $this->importNomineeRelations(), + 'states' => fn () => $this->importStates(), + 'sub_products' => fn () => $this->importSubProducts(), + 'addon_age_limits' => fn () => $this->importAddonAgeLimits(), + 'pincodes' => fn () => $this->importPincodes(), + 'rtos' => fn () => $this->importRtos(), + 'vehicles' => fn () => $this->importVehicles(), + ]; + + if ($only) { + $map = array_intersect_key($map, array_flip($only)); + } + + $results = []; + foreach ($map as $key => $fn) { + $this->progress('Importing ' . $key . '...'); + try { + $rows = $fn(); + $results[$key] = ['rows' => $rows, 'status' => 'OK']; + $this->logImport($key, $rows, 'OK'); + $this->progress($key . ': ' . $rows . ' rows'); + } catch (\Throwable $e) { + $results[$key] = ['rows' => 0, 'status' => 'FAILED', 'message' => $e->getMessage()]; + $this->logImport($key, 0, 'FAILED', $e->getMessage()); + $this->progress($key . ' FAILED: ' . $e->getMessage()); + } + } + + return $results; + } + + protected function progress(string $message): void + { + if ($this->progressCallback) { + ($this->progressCallback)($message); + } + } + + protected function logImport(string $key, int $rows, string $status, ?string $message = null): void + { + try { + $this->logModel->insert([ + 'master_key' => $key, + 'source_file' => $this->mastersPath, + 'rows_upserted' => $rows, + 'status' => $status, + 'message' => $message, + 'imported_at' => date('Y-m-d H:i:s'), + ]); + } catch (\Throwable $e) { + // ignore log failures + } + } + + protected function findFile(array $candidates): string + { + foreach ($candidates as $name) { + $path = $this->mastersPath . '/' . $name; + if (is_file($path)) { + return $path; + } + } + throw new \RuntimeException('Master file not found. Tried: ' . implode(', ', $candidates)); + } + + protected function loadSheet(string $path, string $sheetName = null, int $maxRows = 0): array + { + $reader = IOFactory::createReaderForFile($path); + $reader->setReadDataOnly(true); + if (method_exists($reader, 'setReadEmptyCells')) { + $reader->setReadEmptyCells(false); + } + + if ($sheetName) { + $reader->setLoadSheetsOnly([$sheetName]); + } + + $spreadsheet = $reader->load($path); + $sheet = $sheetName + ? $spreadsheet->getSheetByName($sheetName) + : $spreadsheet->getActiveSheet(); + + if (!$sheet) { + $sheet = $spreadsheet->getSheet(0); + } + + $rows = $sheet->toArray(null, true, true, false); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + + if ($maxRows > 0 && count($rows) > $maxRows) { + $rows = array_slice($rows, 0, $maxRows); + } + + return $rows; + } + + protected function upsertBatch(string $table, array $rows, array $updateCols): int + { + if (!$rows) { + return 0; + } + + $count = 0; + foreach (array_chunk($rows, 500) as $chunk) { + $this->db->table($table)->upsertBatch($chunk); + $count += count($chunk); + } + return $count; + } + + protected function cell($row, int $idx): string + { + $v = $row[$idx] ?? ''; + if ($v === null) { + return ''; + } + return trim((string) $v); + } + + public function importPreviousInsurers(): int + { + $path = $this->findFile(['Motor Prevoius insurer List.xlsx', 'Motor Previous insurer List.xlsx']); + $rows = $this->loadSheet($path); + $now = date('Y-m-d H:i:s'); + $out = []; + + foreach ($rows as $i => $row) { + if ($i === 0) { + continue; + } + $code = $this->cell($row, 0); + $name = $this->cell($row, 1); + if ($code === '' || !preg_match('/^\d+$/', $code) || $name === '' || strcasecmp($name, 'Name') === 0) { + continue; + } + $out[] = [ + 'insurer_code' => str_pad($code, 3, '0', STR_PAD_LEFT), + 'insurer_name' => $name, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_previous_insurer', $out, ['insurer_name', 'is_active', 'imported_at']); + } + + public function importProducts(): int + { + // Seed from known Digit product matrix (also present in Product Code Description workbook headers) + $now = date('Y-m-d H:i:s'); + $products = [ + ['20201', '2W Comprehensive', '2W'], + ['20202', '2W TP only', '2W'], + ['20203', '2W SAOD', '2W'], + ['20101', '4W Comprehensive', '4W'], + ['20102', '4W TP only', '4W'], + ['20103', '4W SAOD', '4W'], + ['20301', 'CV Comprehensive', 'CV'], + ['20302', 'CV TP only', 'CV'], + ]; + + $out = []; + foreach ($products as [$code, $name, $class]) { + $out[] = [ + 'product_code' => $code, + 'product_name' => $name, + 'vehicle_class' => $class, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_product', $out, ['product_name', 'vehicle_class', 'is_active', 'imported_at']); + } + + public function importNcb(): int + { + $path = $this->findFile(['NCB Master.xlsx']); + $rows = $this->loadSheet($path); + $now = date('Y-m-d H:i:s'); + $out = []; + $order = 0; + + foreach ($rows as $i => $row) { + $code = strtoupper($this->cell($row, 0)); + if ($code === '' || $code === 'NCB') { + continue; + } + $out[] = [ + 'ncb_code' => $code, + 'sort_order' => $order++, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_ncb', $out, ['sort_order', 'is_active', 'imported_at']); + } + + public function importVoluntaryDeductible(): int + { + $path = $this->findFile(['Volunatry Deductible Master.xlsx', 'Voluntary Deductible Master.xlsx']); + $rows = $this->loadSheet($path); + $now = date('Y-m-d H:i:s'); + $out = []; + $order = 0; + + foreach ($rows as $row) { + $code = strtoupper($this->cell($row, 0)); + if ($code === '' || str_contains($code, 'VOLUNTARY')) { + continue; + } + $out[] = [ + 'deductible_code' => $code, + 'sort_order' => $order++, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_voluntary_deductible', $out, ['sort_order', 'is_active', 'imported_at']); + } + + public function importPreviousPolicyType(): int + { + $path = $this->findFile(['Previous_Policy_Type.xlsx']); + $rows = $this->loadSheet($path, 'Description'); + $now = date('Y-m-d H:i:s'); + $out = []; + + foreach ($rows as $i => $row) { + $code = strtoupper(trim($this->cell($row, 0))); + $desc = $this->cell($row, 1); + if ($i === 0 || $code === '' || $code === 'DOMNAME') { + continue; + } + $out[] = [ + 'policy_type_code' => $code, + 'description' => $desc, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_previous_policy_type', $out, ['description', 'is_active', 'imported_at']); + } + + public function importDocTypes(): int + { + $path = $this->findFile(['Doc Type Master.xlsx', 'Doc Type Master1.xlsx']); + $rows = $this->loadSheet($path, 'KYC doc Type'); + $now = date('Y-m-d H:i:s'); + $out = []; + + foreach ($rows as $i => $row) { + $code = strtoupper($this->cell($row, 0)); + $type = strtoupper($this->cell($row, 1)); + if ($i === 0 || $code === '' || $code === 'DOCUMENT_NUMBER') { + continue; + } + $out[] = [ + 'doc_code' => $code, + 'doc_type' => $type, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_doc_type', $out, ['doc_type', 'is_active', 'imported_at']); + } + + public function importNomineeRelations(): int + { + $path = $this->findFile(['Nominee Master.xlsx']); + $rows = $this->loadSheet($path); + $now = date('Y-m-d H:i:s'); + $out = []; + + foreach ($rows as $row) { + $code = strtoupper(trim($this->cell($row, 0))); + if ($code === '' || str_contains($code, 'NOMINEE')) { + continue; + } + $out[] = [ + 'relation_code' => $code, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_nominee_relation', $out, ['is_active', 'imported_at']); + } + + public function importStates(): int + { + $path = $this->findFile(['State Code Master (1).xlsx', 'State Code Master.xlsx']); + $rows = $this->loadSheet($path); + $now = date('Y-m-d H:i:s'); + $out = []; + + foreach ($rows as $i => $row) { + $code = preg_replace('/\s+/', '', $this->cell($row, 0)); + $name = trim($this->cell($row, 1)); + if ($i === 0 || $code === '' || stripos($code, 'State') !== false) { + continue; + } + $out[] = [ + 'state_code' => $code, + 'state_name' => $name, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_state', $out, ['state_name', 'is_active', 'imported_at']); + } + + public function importSubProducts(): int + { + $path = $this->findFile(['Motor Product Code Description (1).xlsx', 'Motor Product Code Description.xlsx']); + $rows = $this->loadSheet($path, 'SubinsuranceProductcode'); + $now = date('Y-m-d H:i:s'); + $out = []; + $businessType = 'NEW'; + + foreach ($rows as $i => $row) { + $col0 = $this->cell($row, 0); + $label = $this->cell($row, 1); + $code = $this->cell($row, 2); + + if ($col0 !== '') { + if (stripos($col0, 'rollover') !== false || stripos($col0, 'renewal') !== false) { + $businessType = 'ROLLOVER'; + } elseif (stripos($col0, 'new') !== false) { + $businessType = 'NEW'; + } + } + + if ($label === '' || $code === '' || stripos($label, 'Product') === 0) { + continue; + } + + // code cell may contain notes like "51 (51- 5 yr...)" + if (preg_match('/^([A-Za-z0-9]+)/', $code, $m)) { + $code = $m[1]; + } + + $out[] = [ + 'business_type' => $businessType, + 'product_label' => $label, + 'sub_product_code' => $code, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + // unique by business+label+code + $uniq = []; + foreach ($out as $row) { + $k = $row['business_type'] . '|' . $row['product_label'] . '|' . $row['sub_product_code']; + $uniq[$k] = $row; + } + + if (!$uniq) { + return 0; + } + + $this->db->table('motor_master_sub_product')->truncate(); + $this->db->table('motor_master_sub_product')->insertBatch(array_values($uniq)); + return count($uniq); + } + + public function importAddonAgeLimits(): int + { + $path = $this->findFile(['Motor Product Code Description (1).xlsx', 'Motor Product Code Description.xlsx']); + $rows = $this->loadSheet($path, 'Addons age limit'); + $now = date('Y-m-d H:i:s'); + $out = []; + + foreach ($rows as $i => $row) { + $name = $this->cell($row, 0); + if ($i === 0 || $name === '' || stripos($name, 'Add On') === 0) { + continue; + } + $out[] = [ + 'addon_name' => $name, + 'age_limit_4w' => $this->cell($row, 1) ?: null, + 'age_limit_2w' => $this->cell($row, 2) ?: null, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + if (!$out) { + return 0; + } + + $this->db->table('motor_master_addon_age_limit')->truncate(); + $this->db->table('motor_master_addon_age_limit')->insertBatch($out); + return count($out); + } + + public function importPincodes(): int + { + $path = $this->findFile(['Pin Code and RTO Master_2023.xlsx']); + $rows = $this->loadSheet($path, 'PIN CODE'); + $now = date('Y-m-d H:i:s'); + $out = []; + $seen = []; + + foreach ($rows as $i => $row) { + $pin = preg_replace('/\D+/', '', $this->cell($row, 0)); + if ($i === 0 || strlen($pin) !== 6 || isset($seen[$pin])) { + continue; + } + $seen[$pin] = true; + $out[] = [ + 'pincode' => $pin, + 'city' => $this->cell($row, 1) ?: null, + 'district' => $this->cell($row, 2) ?: null, + 'street' => $this->cell($row, 3) ?: null, + 'taluk' => $this->cell($row, 4) ?: null, + 'segment' => $this->cell($row, 5) ?: null, + 'state_code' => null, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_pincode', $out, [ + 'city', 'district', 'street', 'taluk', 'segment', 'is_active', 'imported_at', + ]); + } + + public function importRtos(): int + { + $path = $this->findFile(['Pin Code and RTO Master_2023.xlsx']); + $rows = $this->loadSheet($path, 'RTO MASTER'); + $now = date('Y-m-d H:i:s'); + $out = []; + $seen = []; + + foreach ($rows as $i => $row) { + $city = $this->cell($row, 0); + $rto = strtoupper(preg_replace('/\s+/', '', $this->cell($row, 1))); + if ($i === 0 || $rto === '' || $rto === 'RTO' || isset($seen[$rto])) { + continue; + } + $seen[$rto] = true; + $out[] = [ + 'rto_code' => $rto, + 'city_state' => $city ?: null, + 'is_active' => 1, + 'imported_at' => $now, + ]; + } + + return $this->upsertBatch('motor_master_rto', $out, ['city_state', 'is_active', 'imported_at']); + } + + public function importVehicles(): int + { + $path = $this->findFile(['Vehicle Master_New (2).xlsx', 'Vehicle Master_New.xlsx', 'Vehicle Master.xlsx']); + $this->progress('Loading vehicle master workbook (large file)...'); + + $reader = IOFactory::createReaderForFile($path); + $reader->setReadDataOnly(true); + if (method_exists($reader, 'setReadEmptyCells')) { + $reader->setReadEmptyCells(false); + } + + $spreadsheet = $reader->load($path); + $sheet = $spreadsheet->getSheet(0); + $highestRow = (int) $sheet->getHighestDataRow(); + $this->progress('Vehicle master rows to scan: ' . $highestRow); + + $now = date('Y-m-d H:i:s'); + $batch = []; + $count = 0; + $chunkSize = 500; + + for ($r = 2; $r <= $highestRow; $r++) { + $code = trim((string) $sheet->getCell('A' . $r)->getValue()); + if ($code === '') { + continue; + } + + $batch[] = [ + 'vehicle_code' => $code, + 'make' => trim((string) $sheet->getCell('B' . $r)->getValue()), + 'model' => trim((string) $sheet->getCell('C' . $r)->getValue()), + 'variant' => trim((string) $sheet->getCell('D' . $r)->getValue()) ?: null, + 'body_type' => trim((string) $sheet->getCell('E' . $r)->getValue()) ?: null, + 'seating_capacity' => $this->toInt($sheet->getCell('F' . $r)->getValue()), + 'power' => $this->toFloat($sheet->getCell('G' . $r)->getValue()), + 'cubic_capacity' => $this->toFloat($sheet->getCell('H' . $r)->getValue()), + 'gross_vehicle_weight' => $this->toFloat($sheet->getCell('I' . $r)->getValue()), + 'fuel_type' => trim((string) $sheet->getCell('J' . $r)->getValue()) ?: null, + 'no_of_wheels' => $this->toInt($sheet->getCell('K' . $r)->getValue()), + 'abs' => substr(trim((string) $sheet->getCell('L' . $r)->getValue()), 0, 1) ?: null, + 'air_bags' => $this->toInt($sheet->getCell('M' . $r)->getValue()), + 'length_m' => $this->toFloat($sheet->getCell('N' . $r)->getValue()), + 'ex_showroom_price' => $this->toFloat($sheet->getCell('O' . $r)->getValue()), + 'price_year' => $this->toInt($sheet->getCell('P' . $r)->getValue()), + 'production_status' => trim((string) $sheet->getCell('Q' . $r)->getValue()) ?: null, + 'manufacturing' => trim((string) $sheet->getCell('R' . $r)->getValue()) ?: null, + 'vehicle_type' => trim((string) $sheet->getCell('S' . $r)->getValue()) ?: null, + 'is_active' => 1, + 'imported_at' => $now, + ]; + + if (count($batch) >= $chunkSize) { + $this->db->table('motor_master_vehicle')->upsertBatch($batch); + $count += count($batch); + $batch = []; + if ($count % 5000 === 0) { + $this->progress('Vehicles upserted: ' . $count); + } + } + } + + if ($batch) { + $this->db->table('motor_master_vehicle')->upsertBatch($batch); + $count += count($batch); + } + + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + + return $count; + } + + protected function toInt($v): ?int + { + if ($v === null || $v === '') { + return null; + } + if (!is_numeric($v)) { + return null; + } + return (int) $v; + } + + protected function toFloat($v): ?float + { + if ($v === null || $v === '') { + return null; + } + if (!is_numeric($v)) { + return null; + } + return (float) $v; + } +} diff --git a/app/Models/MotorMasterAddonAgeLimitModel.php b/app/Models/MotorMasterAddonAgeLimitModel.php new file mode 100644 index 00000000..22802b84 --- /dev/null +++ b/app/Models/MotorMasterAddonAgeLimitModel.php @@ -0,0 +1,17 @@ +where('is_active', 1)->orderBy('sort_order')->orderBy('ncb_code')->findAll(); + } +} diff --git a/app/Models/MotorMasterNomineeRelationModel.php b/app/Models/MotorMasterNomineeRelationModel.php new file mode 100644 index 00000000..4e015540 --- /dev/null +++ b/app/Models/MotorMasterNomineeRelationModel.php @@ -0,0 +1,16 @@ +where('pincode', $pincode)->where('is_active', 1)->first(); + } +} diff --git a/app/Models/MotorMasterPreviousInsurerModel.php b/app/Models/MotorMasterPreviousInsurerModel.php new file mode 100644 index 00000000..f9b45dbb --- /dev/null +++ b/app/Models/MotorMasterPreviousInsurerModel.php @@ -0,0 +1,21 @@ +where('is_active', 1)->orderBy('insurer_name')->findAll(); + } +} diff --git a/app/Models/MotorMasterPreviousPolicyTypeModel.php b/app/Models/MotorMasterPreviousPolicyTypeModel.php new file mode 100644 index 00000000..b2272203 --- /dev/null +++ b/app/Models/MotorMasterPreviousPolicyTypeModel.php @@ -0,0 +1,21 @@ +where('is_active', 1)->orderBy('policy_type_code')->findAll(); + } +} diff --git a/app/Models/MotorMasterProductModel.php b/app/Models/MotorMasterProductModel.php new file mode 100644 index 00000000..e5789004 --- /dev/null +++ b/app/Models/MotorMasterProductModel.php @@ -0,0 +1,25 @@ +where('is_active', 1); + if ($vehicleClass) { + $builder->where('vehicle_class', $vehicleClass); + } + return $builder->orderBy('product_code')->findAll(); + } +} diff --git a/app/Models/MotorMasterRtoModel.php b/app/Models/MotorMasterRtoModel.php new file mode 100644 index 00000000..06075f3a --- /dev/null +++ b/app/Models/MotorMasterRtoModel.php @@ -0,0 +1,16 @@ +where('vehicle_code', $vehicleCode)->where('is_active', 1)->first(); + } + + public function search(string $q, int $limit = 30): array + { + $q = trim($q); + if ($q === '') { + return []; + } + + $builder = $this->builder()->where('is_active', 1); + + if (preg_match('/^\d{6,}$/', $q)) { + $builder->like('vehicle_code', $q, 'after'); + } else { + $builder->groupStart() + ->like('make', $q) + ->orLike('model', $q) + ->orLike('variant', $q) + ->orLike('vehicle_code', $q) + ->groupEnd(); + } + + return $builder + ->orderBy('make') + ->orderBy('model') + ->orderBy('variant') + ->limit($limit) + ->get() + ->getResultArray(); + } + + public function distinctMakes(?string $q = null, int $limit = 100): array + { + $builder = $this->builder() + ->select('make') + ->where('is_active', 1) + ->groupBy('make') + ->orderBy('make') + ->limit($limit); + + if ($q) { + $builder->like('make', $q); + } + + return array_column($builder->get()->getResultArray(), 'make'); + } + + public function distinctModels(string $make, ?string $q = null, int $limit = 200): array + { + $builder = $this->builder() + ->select('model') + ->where('is_active', 1) + ->where('make', $make) + ->groupBy('model') + ->orderBy('model') + ->limit($limit); + + if ($q) { + $builder->like('model', $q); + } + + return array_column($builder->get()->getResultArray(), 'model'); + } + + public function variants(string $make, string $model, int $limit = 200): array + { + return $this->builder() + ->select('vehicle_code, variant, body_type, fuel_type, seating_capacity, cubic_capacity, production_status') + ->where('is_active', 1) + ->where('make', $make) + ->where('model', $model) + ->orderBy('variant') + ->limit($limit) + ->get() + ->getResultArray(); + } +} diff --git a/app/Models/MotorMasterVoluntaryDeductibleModel.php b/app/Models/MotorMasterVoluntaryDeductibleModel.php new file mode 100644 index 00000000..d8b4283b --- /dev/null +++ b/app/Models/MotorMasterVoluntaryDeductibleModel.php @@ -0,0 +1,21 @@ +where('is_active', 1)->orderBy('sort_order')->orderBy('deductible_code')->findAll(); + } +} diff --git a/app/Views/digit_motor/journey.php b/app/Views/digit_motor/journey.php index bcd81562..9bbdae10 100644 --- a/app/Views/digit_motor/journey.php +++ b/app/Views/digit_motor/journey.php @@ -108,10 +108,19 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta .dm-field{display:flex;flex-direction:column;gap:6px;} .dm-field.full{grid-column:1/-1;} .dm-field label{font-size:12px;color:var(--dm-muted);font-weight:500;margin:0;} +.dm-field label .req{color:var(--dm-red);margin-left:2px;font-weight:700;} .dm-field input,.dm-field select{ height:38px;border:1px solid var(--dm-line);border-radius:8px;padding:0 12px;font-size:13.5px; background:var(--dm-paper);outline:none;width:100%; } +.dm-field input.dm-invalid,.dm-field select.dm-invalid{ + border-color:var(--dm-red)!important;background:#fff5f5; +} +.dm-field .dm-err{ + display:none;font-size:11px;color:var(--dm-red);margin-top:2px; +} +.dm-field.has-error .dm-err{display:block;} + .dm-field input:focus,.dm-field select:focus{border-color:var(--dm-teal);background:#fff;} .dm-chips{display:flex;gap:8px;flex-wrap:wrap;} @@ -164,6 +173,20 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta .dm-alert.error{display:block;background:var(--dm-red-bg);color:var(--dm-red);} .dm-alert.ok{display:block;background:var(--dm-green-bg);color:var(--dm-green);} .dm-loading{opacity:.55;pointer-events:none;} +.dm-suggest-wrap{position:relative;} +.dm-suggest{ + position:absolute;left:0;right:0;top:100%;z-index:20;margin-top:4px; + background:#fff;border:1px solid var(--dm-line);border-radius:8px; + max-height:220px;overflow:auto;box-shadow:0 8px 20px rgba(0,0,0,.08);display:none; +} +.dm-suggest.open{display:block;} +.dm-suggest button{ + display:block;width:100%;text-align:left;border:0;background:#fff;padding:8px 12px; + font-size:12.5px;color:var(--dm-ink);cursor:pointer;border-bottom:1px solid var(--dm-line); +} +.dm-suggest button:hover{background:var(--dm-teal-soft);} +.dm-suggest .meta{color:var(--dm-muted);font-size:11px;} +.dm-vcode-tag{font-family:monospace;font-size:12px;color:var(--dm-teal-dark);margin-top:4px;} @media(max-width:768px){ .dm-grid,.dm-grid.g3,.dm-quote-cards{grid-template-columns:1fr;} .dm-stop{width:52px;} @@ -205,26 +228,57 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
Vehicle
-
-
-
-
-
-
-
-
- +
Required
+
+ +
+ +
+
+
Required
+
+
+ + +
Required
+
+
+ + +
+ +
Required
+
+
+
+ +
+
+
+
Required
+
Required
+
Required
+
Required
+
Required (6 digits)
+
+ +
Required
Previous policy
-
+
+ +
+
+ +
+
+ +
Yes
@@ -269,15 +323,21 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
+
Vehicle identifiers
+
+
Required for create quote
+
Required for create quote
+
+
Policyholder
-
+
Required
-
-
-
-
-
+
Required (10 digits)
+
Required
+
Required
+
Required
+
Required
@@ -411,11 +471,16 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta window.DM = { quoteId: , base: '', - step: + step: , + selectedProduct: '', + selectedInsurer: '', + selectedVehicleCode: '' }; const dmSteps = ["Quote","Create","KYC","Pay","Policy"]; const dmIcons = ["mdi-car","mdi-file-plus","mdi-shield-check","mdi-credit-card","mdi-file-check"]; +let dmMakeTimer = null; +let dmSearchTimer = null; (function initRoute(){ const stops = document.getElementById('dmStops'); @@ -428,8 +493,163 @@ const dmIcons = ["mdi-car","mdi-file-plus","mdi-shield-check","mdi-credit-card", stops.appendChild(el); }); dmGo(window.DM.step, true); + dmLoadMasters(); + dmBindMasterUi(); })(); +function dmFillSelect(sel, items, valueKey, labelFn, selected, emptyLabel){ + const $s = $(sel); + $s.empty(); + if (emptyLabel != null) { + $s.append($('