MERGE_TEST_BUG_FIXES

This commit is contained in:
Ubuntu 2026-07-23 09:17:33 +05:30
commit 647f7470bc
27 changed files with 2013 additions and 53 deletions

View File

@ -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

View 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;
}
}

View File

@ -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', '');

View File

@ -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');

View File

@ -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;
}
}

View 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;

View File

@ -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',

View File

@ -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 . ').';
}

View File

@ -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);
}
}

View File

@ -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***',
]
);
}

View File

@ -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;
}
}

View 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;
}
}

View 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;
}

View 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;
}

View 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;
}

View 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();
}
}

View 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;
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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;
}

View 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;
}

View 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;
}

View 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();
}
}

View 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();
}
}

View File

@ -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>