MERGE_TEST_BUG_FIXES
This commit is contained in:
commit
647f7470bc
@ -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
|
||||
|
||||
103
app/Commands/DigitMotorImportMasters.php
Normal file
103
app/Commands/DigitMotorImportMasters.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Libraries\DigitMotor\DigitMasterImportService;
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
|
||||
/**
|
||||
* Create Digit motor master tables and import kit Excel files.
|
||||
*
|
||||
* Usage:
|
||||
* php spark digit-motor:import-masters --create-tables
|
||||
* php spark digit-motor:import-masters --path="/path/to/Masters"
|
||||
* php spark digit-motor:import-masters --only=products,previous_insurers,ncb
|
||||
* php spark digit-motor:import-masters --skip-vehicles
|
||||
*/
|
||||
class DigitMotorImportMasters extends BaseCommand
|
||||
{
|
||||
protected $group = 'DigitMotor';
|
||||
protected $name = 'digit-motor:import-masters';
|
||||
protected $description = 'Create motor_master_* tables and import Digit Masters Excel files.';
|
||||
protected $usage = 'digit-motor:import-masters [--path] [--create-tables] [--only] [--skip-vehicles]';
|
||||
protected $options = [
|
||||
'--path' => '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;
|
||||
}
|
||||
}
|
||||
@ -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', '');
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
152
app/Database/digit_motor_master_tables.sql
Normal file
152
app/Database/digit_motor_master_tables.sql
Normal file
@ -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;
|
||||
@ -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',
|
||||
|
||||
@ -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 . ').';
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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***',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
633
app/Libraries/DigitMotor/DigitMasterImportService.php
Normal file
633
app/Libraries/DigitMotor/DigitMasterImportService.php
Normal file
@ -0,0 +1,633 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\DigitMotor;
|
||||
|
||||
use App\Models\MotorMasterAddonAgeLimitModel;
|
||||
use App\Models\MotorMasterDocTypeModel;
|
||||
use App\Models\MotorMasterImportLogModel;
|
||||
use App\Models\MotorMasterNcbModel;
|
||||
use App\Models\MotorMasterNomineeRelationModel;
|
||||
use App\Models\MotorMasterPincodeModel;
|
||||
use App\Models\MotorMasterPreviousInsurerModel;
|
||||
use App\Models\MotorMasterPreviousPolicyTypeModel;
|
||||
use App\Models\MotorMasterProductModel;
|
||||
use App\Models\MotorMasterRtoModel;
|
||||
use App\Models\MotorMasterStateModel;
|
||||
use App\Models\MotorMasterSubProductModel;
|
||||
use App\Models\MotorMasterVehicleModel;
|
||||
use App\Models\MotorMasterVoluntaryDeductibleModel;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\IReadFilter;
|
||||
|
||||
/**
|
||||
* Imports Digit kit Masters Excel files into motor_master_* tables.
|
||||
*/
|
||||
class DigitMasterImportService
|
||||
{
|
||||
protected string $mastersPath;
|
||||
protected $db;
|
||||
protected MotorMasterImportLogModel $logModel;
|
||||
|
||||
/** @var callable|null */
|
||||
protected $progressCallback;
|
||||
|
||||
public function __construct(?string $mastersPath = null)
|
||||
{
|
||||
$this->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<string, array{rows:int,status:string,message?:string}>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
17
app/Models/MotorMasterAddonAgeLimitModel.php
Normal file
17
app/Models/MotorMasterAddonAgeLimitModel.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterAddonAgeLimitModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_addon_age_limit';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'addon_name', 'age_limit_4w', 'age_limit_2w', 'is_active', 'imported_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
16
app/Models/MotorMasterDocTypeModel.php
Normal file
16
app/Models/MotorMasterDocTypeModel.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterDocTypeModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_doc_type';
|
||||
protected $primaryKey = 'doc_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['doc_code', 'doc_type', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
17
app/Models/MotorMasterImportLogModel.php
Normal file
17
app/Models/MotorMasterImportLogModel.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterImportLogModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_import_log';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'master_key', 'source_file', 'rows_upserted', 'status', 'message', 'imported_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
21
app/Models/MotorMasterNcbModel.php
Normal file
21
app/Models/MotorMasterNcbModel.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterNcbModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_ncb';
|
||||
protected $primaryKey = 'ncb_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['ncb_code', 'sort_order', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function activeList(): array
|
||||
{
|
||||
return $this->where('is_active', 1)->orderBy('sort_order')->orderBy('ncb_code')->findAll();
|
||||
}
|
||||
}
|
||||
16
app/Models/MotorMasterNomineeRelationModel.php
Normal file
16
app/Models/MotorMasterNomineeRelationModel.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterNomineeRelationModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_nominee_relation';
|
||||
protected $primaryKey = 'relation_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['relation_code', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
23
app/Models/MotorMasterPincodeModel.php
Normal file
23
app/Models/MotorMasterPincodeModel.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterPincodeModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_pincode';
|
||||
protected $primaryKey = 'pincode';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'pincode', 'city', 'district', 'street', 'taluk', 'state_code', 'segment', 'is_active', 'imported_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function findActive(string $pincode): ?array
|
||||
{
|
||||
return $this->where('pincode', $pincode)->where('is_active', 1)->first();
|
||||
}
|
||||
}
|
||||
21
app/Models/MotorMasterPreviousInsurerModel.php
Normal file
21
app/Models/MotorMasterPreviousInsurerModel.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterPreviousInsurerModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_previous_insurer';
|
||||
protected $primaryKey = 'insurer_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['insurer_code', 'insurer_name', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function activeList(): array
|
||||
{
|
||||
return $this->where('is_active', 1)->orderBy('insurer_name')->findAll();
|
||||
}
|
||||
}
|
||||
21
app/Models/MotorMasterPreviousPolicyTypeModel.php
Normal file
21
app/Models/MotorMasterPreviousPolicyTypeModel.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterPreviousPolicyTypeModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_previous_policy_type';
|
||||
protected $primaryKey = 'policy_type_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['policy_type_code', 'description', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function activeList(): array
|
||||
{
|
||||
return $this->where('is_active', 1)->orderBy('policy_type_code')->findAll();
|
||||
}
|
||||
}
|
||||
25
app/Models/MotorMasterProductModel.php
Normal file
25
app/Models/MotorMasterProductModel.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterProductModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_product';
|
||||
protected $primaryKey = 'product_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['product_code', 'product_name', 'vehicle_class', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function activeList(?string $vehicleClass = null): array
|
||||
{
|
||||
$builder = $this->where('is_active', 1);
|
||||
if ($vehicleClass) {
|
||||
$builder->where('vehicle_class', $vehicleClass);
|
||||
}
|
||||
return $builder->orderBy('product_code')->findAll();
|
||||
}
|
||||
}
|
||||
16
app/Models/MotorMasterRtoModel.php
Normal file
16
app/Models/MotorMasterRtoModel.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterRtoModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_rto';
|
||||
protected $primaryKey = 'rto_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['rto_code', 'city_state', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
16
app/Models/MotorMasterStateModel.php
Normal file
16
app/Models/MotorMasterStateModel.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterStateModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_state';
|
||||
protected $primaryKey = 'state_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['state_code', 'state_name', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
17
app/Models/MotorMasterSubProductModel.php
Normal file
17
app/Models/MotorMasterSubProductModel.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterSubProductModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_sub_product';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'business_type', 'product_label', 'sub_product_code', 'is_active', 'imported_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
101
app/Models/MotorMasterVehicleModel.php
Normal file
101
app/Models/MotorMasterVehicleModel.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterVehicleModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_vehicle';
|
||||
protected $primaryKey = 'vehicle_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'vehicle_code', 'make', 'model', 'variant', 'body_type', 'seating_capacity',
|
||||
'power', 'cubic_capacity', 'gross_vehicle_weight', 'fuel_type', 'no_of_wheels',
|
||||
'abs', 'air_bags', 'length_m', 'ex_showroom_price', 'price_year',
|
||||
'production_status', 'manufacturing', 'vehicle_type', 'is_active', 'imported_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function findActive(string $vehicleCode): ?array
|
||||
{
|
||||
return $this->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();
|
||||
}
|
||||
}
|
||||
21
app/Models/MotorMasterVoluntaryDeductibleModel.php
Normal file
21
app/Models/MotorMasterVoluntaryDeductibleModel.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorMasterVoluntaryDeductibleModel extends Model
|
||||
{
|
||||
protected $table = 'motor_master_voluntary_deductible';
|
||||
protected $primaryKey = 'deductible_code';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = ['deductible_code', 'sort_order', 'is_active', 'imported_at'];
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function activeList(): array
|
||||
{
|
||||
return $this->where('is_active', 1)->orderBy('sort_order')->orderBy('deductible_code')->findAll();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
<div class="dm-section">Vehicle</div>
|
||||
<div class="dm-grid g3">
|
||||
<div class="dm-field"><label>Registration number</label><input id="f_reg" value="<?= esc($vehicle['license_plate_number'] ?? '') ?>" placeholder="GJ04DA8726"></div>
|
||||
<div class="dm-field"><label>Vehicle code</label><input id="f_vcode" value="<?= esc($vehicle['vehicle_maincode'] ?? '') ?>" placeholder="1113811407"></div>
|
||||
<div class="dm-field"><label>Registration date</label><input type="date" id="f_reg_date" value="<?= esc($vehicle['registration_date'] ?? '') ?>"></div>
|
||||
<div class="dm-field"><label>Manufacture date</label><input type="date" id="f_mfg_date" value="<?= esc($vehicle['manufacture_date'] ?? '') ?>"></div>
|
||||
<div class="dm-field"><label>Chassis / VIN</label><input id="f_vin" value="<?= esc($vehicle['vehicle_identification_number'] ?? '') ?>"></div>
|
||||
<div class="dm-field"><label>Engine number</label><input id="f_engine" value="<?= esc($vehicle['engine_number'] ?? '') ?>"></div>
|
||||
<div class="dm-field"><label>Pincode</label><input id="f_pin" value="<?= esc($quote['pincode'] ?? '') ?>" maxlength="6"></div>
|
||||
<div class="dm-field"><label>Product code</label>
|
||||
<select id="f_product">
|
||||
<option value="20101" <?= ($quote['insurance_product_code'] ?? '') == '20101' ? 'selected' : '' ?>>20101 · Private Car Package</option>
|
||||
<option value="20102" <?= ($quote['insurance_product_code'] ?? '20102') == '20102' ? 'selected' : '' ?>>20102 · Private Car</option>
|
||||
</select>
|
||||
<div class="dm-field"><label>Registration number <span class="req">*</span></label><input id="f_reg" value="<?= esc($vehicle['license_plate_number'] ?? '') ?>" placeholder="GJ04DA8726" data-required="1" data-label="Registration number"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field">
|
||||
<label>Make <span class="req">*</span></label>
|
||||
<div class="dm-suggest-wrap">
|
||||
<input id="f_make" placeholder="Search make..." autocomplete="off" data-required="1" data-label="Make">
|
||||
<div class="dm-suggest" id="sug_make"></div>
|
||||
</div>
|
||||
<div class="dm-err">Required</div>
|
||||
</div>
|
||||
<div class="dm-field">
|
||||
<label>Model <span class="req">*</span></label>
|
||||
<select id="f_model" disabled data-required="1" data-label="Model"><option value="">Select make first</option></select>
|
||||
<div class="dm-err">Required</div>
|
||||
</div>
|
||||
<div class="dm-field">
|
||||
<label>Variant / vehicle code <span class="req">*</span></label>
|
||||
<select id="f_variant" disabled data-required="1" data-label="Variant / vehicle code"><option value="">Select model first</option></select>
|
||||
<div class="dm-vcode-tag" id="f_vcode_tag"><?= esc($vehicle['vehicle_maincode'] ?? '') ?></div>
|
||||
<input type="hidden" id="f_vcode" value="<?= esc($vehicle['vehicle_maincode'] ?? '') ?>" data-required="1" data-label="Vehicle code">
|
||||
<div class="dm-err">Required</div>
|
||||
</div>
|
||||
<div class="dm-field"><label>Or search vehicle code</label>
|
||||
<div class="dm-suggest-wrap">
|
||||
<input id="f_vsearch" placeholder="Type code / make / model..." autocomplete="off">
|
||||
<div class="dm-suggest" id="sug_vsearch"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dm-field"><label>Registration date <span class="req">*</span></label><input type="date" id="f_reg_date" value="<?= esc($vehicle['registration_date'] ?? '') ?>" data-required="1" data-label="Registration date"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field"><label>Manufacture date <span class="req">*</span></label><input type="date" id="f_mfg_date" value="<?= esc($vehicle['manufacture_date'] ?? '') ?>" data-required="1" data-label="Manufacture date"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field"><label>Chassis / VIN <span class="req">*</span></label><input id="f_vin" value="<?= esc($vehicle['vehicle_identification_number'] ?? '') ?>" data-required="1" data-label="Chassis / VIN" minlength="5" placeholder="e.g. MAKGM651CJ4306951"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field"><label>Engine number <span class="req">*</span></label><input id="f_engine" value="<?= esc($vehicle['engine_number'] ?? '') ?>" data-required="1" data-label="Engine number" minlength="5" placeholder="e.g. L15Z15337915"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field"><label>Pincode <span class="req">*</span></label><input id="f_pin" value="<?= esc($quote['pincode'] ?? '') ?>" maxlength="6" data-required="1" data-label="Pincode" data-len="6"><div class="dm-vcode-tag" id="f_pin_tag"></div><div class="dm-err">Required (6 digits)</div></div>
|
||||
<div class="dm-field"><label>Product code <span class="req">*</span></label>
|
||||
<select id="f_product" data-required="1" data-label="Product code"><option value="">Loading products...</option></select>
|
||||
<div class="dm-err">Required</div>
|
||||
</div>
|
||||
<div class="dm-field"><label>Policy start</label><input type="date" id="f_start" value="<?= esc($quote['start_date'] ?? '') ?>"></div>
|
||||
</div>
|
||||
|
||||
<div class="dm-section">Previous policy</div>
|
||||
<div class="dm-grid g3">
|
||||
<div class="dm-field"><label>Previous insurer code</label><input id="f_prev_ins" value="<?= esc($quote['previous_insurer_code'] ?? '') ?>" placeholder="190"></div>
|
||||
<div class="dm-field"><label>Previous insurer</label>
|
||||
<select id="f_prev_ins"><option value="">— none / unknown —</option></select>
|
||||
</div>
|
||||
<div class="dm-field"><label>Prior policy expiry</label><input type="date" id="f_prev_exp" value="<?= esc($quote['previous_policy_expiry_date'] ?? '') ?>"></div>
|
||||
<div class="dm-field"><label>Previous NCB</label>
|
||||
<select id="f_prev_ncb"><option value="ZERO">ZERO</option></select>
|
||||
</div>
|
||||
<div class="dm-field"><label>Previous policy type</label>
|
||||
<select id="f_prev_ptype"><option value="">— optional —</option></select>
|
||||
</div>
|
||||
<div class="dm-field"><label>Claim in last year</label>
|
||||
<div class="dm-chips" style="height:38px;align-items:center;">
|
||||
<div class="dm-chip" data-group="claim" data-val="1" onclick="dmToggleExclusive(this)">Yes</div>
|
||||
@ -269,15 +323,21 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-section">Vehicle identifiers</div>
|
||||
<div class="dm-grid">
|
||||
<div class="dm-field"><label>Chassis / VIN <span class="req">*</span></label><input id="f_cq_vin" value="<?= esc($vehicle['vehicle_identification_number'] ?? '') ?>" data-required="1" data-label="Chassis / VIN" minlength="5" placeholder="e.g. MAKGM651CJ4306951"><div class="dm-err">Required for create quote</div></div>
|
||||
<div class="dm-field"><label>Engine number <span class="req">*</span></label><input id="f_cq_engine" value="<?= esc($vehicle['engine_number'] ?? '') ?>" data-required="1" data-label="Engine number" minlength="5" placeholder="e.g. L15Z15337915"><div class="dm-err">Required for create quote</div></div>
|
||||
</div>
|
||||
|
||||
<div class="dm-section">Policyholder</div>
|
||||
<div class="dm-grid">
|
||||
<div class="dm-field"><label>First name</label><input id="f_fname" value=""></div>
|
||||
<div class="dm-field"><label>First name <span class="req">*</span></label><input id="f_fname" value="" data-required="1" data-label="First name"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field"><label>Last name</label><input id="f_lname" value=""></div>
|
||||
<div class="dm-field"><label>Mobile</label><input id="f_mobile" value=""></div>
|
||||
<div class="dm-field"><label>Email</label><input id="f_email" value=""></div>
|
||||
<div class="dm-field"><label>PAN</label><input id="f_pan" value=""></div>
|
||||
<div class="dm-field"><label>Date of birth</label><input type="date" id="f_dob" value=""></div>
|
||||
<div class="dm-field full"><label>Address</label><input id="f_address" value=""></div>
|
||||
<div class="dm-field"><label>Mobile <span class="req">*</span></label><input id="f_mobile" value="" data-required="1" data-label="Mobile" data-len="10"><div class="dm-err">Required (10 digits)</div></div>
|
||||
<div class="dm-field"><label>Email <span class="req">*</span></label><input id="f_email" type="email" value="" data-required="1" data-label="Email"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field"><label>PAN <span class="req">*</span></label><input id="f_pan" value="" data-required="1" data-label="PAN"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field"><label>Date of birth <span class="req">*</span></label><input type="date" id="f_dob" value="" data-required="1" data-label="Date of birth"><div class="dm-err">Required</div></div>
|
||||
<div class="dm-field full"><label>Address <span class="req">*</span></label><input id="f_address" value="" data-required="1" data-label="Address"><div class="dm-err">Required</div></div>
|
||||
</div>
|
||||
|
||||
<div class="dm-actions">
|
||||
@ -411,11 +471,16 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
|
||||
window.DM = {
|
||||
quoteId: <?= $quoteId ? (int)$quoteId : 'null' ?>,
|
||||
base: '<?= rtrim(base_url('digit-motor'), '/') ?>',
|
||||
step: <?= (int)$initialStep ?>
|
||||
step: <?= (int)$initialStep ?>,
|
||||
selectedProduct: '<?= esc($quote['insurance_product_code'] ?? '20102') ?>',
|
||||
selectedInsurer: '<?= esc($quote['previous_insurer_code'] ?? '') ?>',
|
||||
selectedVehicleCode: '<?= esc($vehicle['vehicle_maincode'] ?? '') ?>'
|
||||
};
|
||||
|
||||
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($('<option>').val('').text(emptyLabel));
|
||||
}
|
||||
(items || []).forEach(function(item){
|
||||
const val = typeof item === 'string' ? item : item[valueKey];
|
||||
const label = typeof labelFn === 'function' ? labelFn(item) : val;
|
||||
const opt = $('<option>').val(val).text(label);
|
||||
if (selected != null && String(selected) === String(val)) opt.prop('selected', true);
|
||||
$s.append(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function dmLoadMasters(){
|
||||
dmGet(window.DM.base + '/masters/bootstrap')
|
||||
.done(function(res){
|
||||
if (!res.status){
|
||||
dmAlert(res.message || 'Masters not loaded');
|
||||
return;
|
||||
}
|
||||
const d = res.data || {};
|
||||
dmFillSelect('#f_product', d.products || [], 'product_code', function(p){
|
||||
return p.product_code + ' · ' + p.product_name;
|
||||
}, window.DM.selectedProduct, null);
|
||||
|
||||
dmFillSelect('#f_prev_ins', d.previous_insurers || [], 'insurer_code', function(p){
|
||||
return p.insurer_code + ' · ' + p.insurer_name;
|
||||
}, window.DM.selectedInsurer, '— none / unknown —');
|
||||
|
||||
dmFillSelect('#f_prev_ncb', d.ncb || [], 'ncb_code', function(p){ return p.ncb_code; }, 'ZERO', null);
|
||||
|
||||
dmFillSelect('#f_prev_ptype', d.previous_policy_types || [], 'policy_type_code', function(p){
|
||||
return p.policy_type_code + (p.description ? ' · ' + p.description : '');
|
||||
}, '', '— optional —');
|
||||
})
|
||||
.fail(function(){
|
||||
dmAlert('Could not load Digit masters. Import with: php spark digit-motor:import-masters --create-tables');
|
||||
});
|
||||
}
|
||||
|
||||
function dmBindMasterUi(){
|
||||
$('#f_make').on('input', function(){
|
||||
const q = $(this).val().trim();
|
||||
clearTimeout(dmMakeTimer);
|
||||
if (q.length < 1){ $('#sug_make').removeClass('open').empty(); return; }
|
||||
dmMakeTimer = setTimeout(function(){
|
||||
dmGet(window.DM.base + '/masters/vehicles/makes?q=' + encodeURIComponent(q))
|
||||
.done(function(res){
|
||||
const box = $('#sug_make').empty();
|
||||
(res.data || []).slice(0, 40).forEach(function(make){
|
||||
$('<button type="button">').text(make).on('click', function(){
|
||||
$('#f_make').val(make);
|
||||
box.removeClass('open').empty();
|
||||
dmLoadModels(make);
|
||||
}).appendTo(box);
|
||||
});
|
||||
box.toggleClass('open', (res.data || []).length > 0);
|
||||
});
|
||||
}, 250);
|
||||
});
|
||||
|
||||
$('#f_model').on('change', function(){
|
||||
const make = $('#f_make').val().trim();
|
||||
const model = $(this).val();
|
||||
if (make && model) dmLoadVariants(make, model);
|
||||
});
|
||||
|
||||
$('#f_variant').on('change', function(){
|
||||
const code = $(this).val();
|
||||
dmSetVehicleCode(code);
|
||||
});
|
||||
|
||||
$('#f_vsearch').on('input', function(){
|
||||
const q = $(this).val().trim();
|
||||
clearTimeout(dmSearchTimer);
|
||||
if (q.length < 2){ $('#sug_vsearch').removeClass('open').empty(); return; }
|
||||
dmSearchTimer = setTimeout(function(){
|
||||
dmGet(window.DM.base + '/masters/vehicles/search?q=' + encodeURIComponent(q))
|
||||
.done(function(res){
|
||||
const box = $('#sug_vsearch').empty();
|
||||
(res.data || []).forEach(function(v){
|
||||
const label = (v.make || '') + ' ' + (v.model || '') + ' ' + (v.variant || '');
|
||||
$('<button type="button">').html(
|
||||
'<div>'+label.trim()+'</div><div class="meta">'+v.vehicle_code+(v.fuel_type ? ' · '+v.fuel_type : '')+'</div>'
|
||||
).on('click', function(){
|
||||
$('#f_make').val(v.make || '');
|
||||
dmSetVehicleCode(v.vehicle_code);
|
||||
$('#f_vsearch').val(label.trim());
|
||||
box.removeClass('open').empty();
|
||||
dmLoadModels(v.make || '', v.model || '', v.vehicle_code);
|
||||
}).appendTo(box);
|
||||
});
|
||||
box.toggleClass('open', (res.data || []).length > 0);
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
$('#f_pin').on('blur', function(){
|
||||
const pin = $(this).val().trim();
|
||||
if (pin.length !== 6){ $('#f_pin_tag').text(''); return; }
|
||||
dmGet(window.DM.base + '/masters/pincode/' + pin)
|
||||
.done(function(res){
|
||||
if (res.status && res.data){
|
||||
$('#f_pin_tag').text((res.data.city || '') + (res.data.district ? ', ' + res.data.district : ''));
|
||||
} else {
|
||||
$('#f_pin_tag').text('Pincode not in Digit master');
|
||||
}
|
||||
})
|
||||
.fail(function(){ $('#f_pin_tag').text('Pincode not in Digit master'); });
|
||||
});
|
||||
|
||||
$(document).on('click', function(e){
|
||||
if (!$(e.target).closest('.dm-suggest-wrap').length){
|
||||
$('.dm-suggest').removeClass('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function dmSetVehicleCode(code){
|
||||
$('#f_vcode').val(code || '');
|
||||
$('#f_vcode_tag').text(code || '');
|
||||
window.DM.selectedVehicleCode = code || '';
|
||||
}
|
||||
|
||||
function dmLoadModels(make, selectedModel, selectedCode){
|
||||
const $model = $('#f_model').prop('disabled', true).empty().append($('<option>').val('').text('Loading...'));
|
||||
$('#f_variant').prop('disabled', true).empty().append($('<option>').val('').text('Select model first'));
|
||||
dmGet(window.DM.base + '/masters/vehicles/models?make=' + encodeURIComponent(make))
|
||||
.done(function(res){
|
||||
dmFillSelect('#f_model', res.data || [], null, null, selectedModel || '', 'Select model');
|
||||
$model.prop('disabled', false);
|
||||
if (selectedModel) dmLoadVariants(make, selectedModel, selectedCode);
|
||||
});
|
||||
}
|
||||
|
||||
function dmLoadVariants(make, model, selectedCode){
|
||||
const $v = $('#f_variant').prop('disabled', true).empty().append($('<option>').val('').text('Loading...'));
|
||||
dmGet(window.DM.base + '/masters/vehicles/variants?make=' + encodeURIComponent(make) + '&model=' + encodeURIComponent(model))
|
||||
.done(function(res){
|
||||
$v.empty().append($('<option>').val('').text('Select variant'));
|
||||
(res.data || []).forEach(function(v){
|
||||
const label = (v.variant || 'STD') + ' · ' + v.vehicle_code + (v.fuel_type ? ' · ' + v.fuel_type : '');
|
||||
const opt = $('<option>').val(v.vehicle_code).text(label);
|
||||
if (selectedCode && String(selectedCode) === String(v.vehicle_code)) opt.prop('selected', true);
|
||||
$v.append(opt);
|
||||
});
|
||||
$v.prop('disabled', false);
|
||||
if (selectedCode) dmSetVehicleCode(selectedCode);
|
||||
});
|
||||
}
|
||||
|
||||
function dmGo(i, silent){
|
||||
window.DM.step = i;
|
||||
document.querySelectorAll('.dm-step').forEach(v=> v.classList.toggle('active', Number(v.dataset.step)===i));
|
||||
@ -499,8 +719,76 @@ function dmGet(url){
|
||||
return $.ajax({ url: url, type: 'GET', dataType: 'json' });
|
||||
}
|
||||
|
||||
function dmClearFieldErrors($scope){
|
||||
$scope = $scope || $(document);
|
||||
$scope.find('.dm-field').removeClass('has-error');
|
||||
$scope.find('.dm-invalid').removeClass('dm-invalid');
|
||||
}
|
||||
|
||||
function dmMarkError($el, msg){
|
||||
const $field = $el.closest('.dm-field');
|
||||
$field.addClass('has-error');
|
||||
$el.addClass('dm-invalid');
|
||||
if (msg) $field.find('.dm-err').text(msg);
|
||||
}
|
||||
|
||||
function dmValidateRequired(fieldIds){
|
||||
dmClearFieldErrors();
|
||||
const missing = [];
|
||||
fieldIds.forEach(function(id){
|
||||
const $el = $('#' + id);
|
||||
if (!$el.length) return;
|
||||
let val = ($el.val() || '').toString().trim();
|
||||
const label = $el.data('label') || id;
|
||||
const needLen = parseInt($el.data('len'), 10) || 0;
|
||||
|
||||
// Variant may be empty if vehicle was picked via search — accept hidden f_vcode
|
||||
if (id === 'f_variant' && $('#f_vcode').val()) {
|
||||
return;
|
||||
}
|
||||
if (id === 'f_make' && $('#f_vcode').val()) {
|
||||
return;
|
||||
}
|
||||
if (id === 'f_model' && $('#f_vcode').val()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
dmMarkError($el, label + ' is required');
|
||||
missing.push(label);
|
||||
return;
|
||||
}
|
||||
if (needLen && val.replace(/\D/g,'').length < needLen && id === 'f_mobile') {
|
||||
dmMarkError($el, 'Enter a valid 10-digit mobile');
|
||||
missing.push(label);
|
||||
return;
|
||||
}
|
||||
if (needLen && id === 'f_pin' && val.length !== needLen) {
|
||||
dmMarkError($el, 'Pincode must be 6 digits');
|
||||
missing.push(label);
|
||||
return;
|
||||
}
|
||||
if (id === 'f_reg' && val.length < 4) {
|
||||
dmMarkError($el, 'Registration number is too short');
|
||||
missing.push(label);
|
||||
}
|
||||
});
|
||||
return missing;
|
||||
}
|
||||
|
||||
function dmQuickQuote(){
|
||||
const claimChip = document.querySelector('.dm-chip[data-group="claim"].on');
|
||||
const missing = dmValidateRequired([
|
||||
'f_reg', 'f_make', 'f_model', 'f_variant', 'f_vcode',
|
||||
'f_reg_date', 'f_mfg_date', 'f_vin', 'f_engine', 'f_pin', 'f_product'
|
||||
]);
|
||||
if (missing.length){
|
||||
dmAlert('Please fill required fields: ' + missing.join(', '));
|
||||
const first = document.querySelector('.dm-field.has-error');
|
||||
if (first) first.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
license_plate_number: $('#f_reg').val().trim(),
|
||||
vehicle_maincode: $('#f_vcode').val().trim(),
|
||||
@ -514,6 +802,8 @@ function dmQuickQuote(){
|
||||
end_date: null,
|
||||
previous_insurer_code: $('#f_prev_ins').val() || null,
|
||||
previous_policy_expiry_date: $('#f_prev_exp').val() || null,
|
||||
previous_ncb: $('#f_prev_ncb').val() || 'ZERO',
|
||||
previous_policy_type: $('#f_prev_ptype').val() || '',
|
||||
is_claim_in_last_year: claimChip ? claimChip.dataset.val === '1' : false,
|
||||
coverages: dmAddonFlags(),
|
||||
enquiry_id: window.DM.quoteId ? ($('#enquiryIdTag').text().trim() !== '— new —' ? $('#enquiryIdTag').text().trim() : null) : null
|
||||
@ -539,10 +829,17 @@ function dmQuickQuote(){
|
||||
$('#payPremium').val(prem != null ? 'INR ' + prem.toFixed(2) : '');
|
||||
dmAlert('Quick quote generated.', true);
|
||||
history.replaceState(null, '', window.DM.base + '/journey/' + window.DM.quoteId);
|
||||
$('#f_cq_vin').val(body.vehicle_identification_number || '');
|
||||
$('#f_cq_engine').val(body.engine_number || '');
|
||||
dmGo(1);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message) || 'Quick quote failed.';
|
||||
let msg = (xhr.responseJSON && xhr.responseJSON.message) || 'Quick quote failed.';
|
||||
const errs = xhr.responseJSON && xhr.responseJSON.errors;
|
||||
if (errs && typeof errs === 'object') {
|
||||
const parts = Object.keys(errs).map(function(k){ return errs[k]; });
|
||||
if (parts.length) msg = parts.join(' | ');
|
||||
}
|
||||
dmAlert(msg);
|
||||
})
|
||||
.always(function(){ dmBusy(false); });
|
||||
@ -550,6 +847,19 @@ function dmQuickQuote(){
|
||||
|
||||
function dmCreateQuote(){
|
||||
if (!window.DM.quoteId){ dmAlert('Run quick quote first.'); return; }
|
||||
|
||||
// Keep QQ + CQ chassis/engine in sync when editing on step 2
|
||||
if ($('#f_cq_vin').val()) $('#f_vin').val($('#f_cq_vin').val().trim().toUpperCase());
|
||||
if ($('#f_cq_engine').val()) $('#f_engine').val($('#f_cq_engine').val().trim().toUpperCase());
|
||||
|
||||
const missing = dmValidateRequired(['f_cq_vin', 'f_cq_engine', 'f_fname', 'f_mobile', 'f_email', 'f_pan', 'f_dob', 'f_address']);
|
||||
if (missing.length){
|
||||
dmAlert('Please fill required fields: ' + missing.join(', '));
|
||||
const first = document.querySelector('.dm-field.has-error');
|
||||
if (first) first.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
first_name: $('#f_fname').val().trim(),
|
||||
last_name: $('#f_lname').val().trim(),
|
||||
@ -558,6 +868,8 @@ function dmCreateQuote(){
|
||||
pan: $('#f_pan').val().trim(),
|
||||
dob: $('#f_dob').val(),
|
||||
address: $('#f_address').val().trim(),
|
||||
vehicle_identification_number: $('#f_cq_vin').val().trim().toUpperCase(),
|
||||
engine_number: $('#f_cq_engine').val().trim().toUpperCase(),
|
||||
start_date: $('#f_start').val() || null,
|
||||
coverages: dmAddonFlags()
|
||||
};
|
||||
@ -610,8 +922,12 @@ function dmKycStatus(){
|
||||
|
||||
function dmPaymentLink(){
|
||||
if (!window.DM.quoteId){ dmAlert('No quote loaded.'); return; }
|
||||
const body = {
|
||||
payment_mode: $('#payMode').val() || 'EB',
|
||||
premium_amount: ($('#payPremium').val() || '').trim() || null
|
||||
};
|
||||
dmBusy(true);
|
||||
dmPost(window.DM.base + '/payments/' + window.DM.quoteId + '/link', {})
|
||||
dmPost(window.DM.base + '/payments/' + window.DM.quoteId + '/link', body)
|
||||
.done(function(res){
|
||||
if (!res.status){ dmAlert(res.message || 'Payment link failed'); return; }
|
||||
const link = res.data.dispatcher_response;
|
||||
@ -669,4 +985,12 @@ function dmPolicyPdf(){
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function(){ dmGo(window.DM.step, true); });
|
||||
|
||||
$(document).on('input change', '.dm-field input, .dm-field select', function(){
|
||||
const $field = $(this).closest('.dm-field');
|
||||
if (($(this).val() || '').toString().trim()) {
|
||||
$field.removeClass('has-error');
|
||||
$(this).removeClass('dm-invalid');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user