MERGE_TEST_BUG_FIXES
This commit is contained in:
commit
19afd681b1
19
.env.sample
19
.env.sample
@ -190,6 +190,25 @@ ICICI_GRANT_TYPE =
|
||||
|
||||
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_EXECUTOR_PATH = /OneAPI/v1/executor
|
||||
DIGIT_MOTOR_USERNAME =
|
||||
DIGIT_MOTOR_PASSWORD =
|
||||
DIGIT_MOTOR_ENVIRONMENT = staging
|
||||
DIGIT_MOTOR_TIMEOUT = 30
|
||||
DIGIT_MOTOR_TOKEN_LEEWAY_SEC = 60
|
||||
DIGIT_MOTOR_PDF_AUTH_KEY =
|
||||
DIGIT_MOTOR_IID_QUICK_QUOTE = 29266-0100
|
||||
DIGIT_MOTOR_IID_CREATE_QUOTE = 29268-0100
|
||||
DIGIT_MOTOR_IID_KYC = 29269-0100
|
||||
DIGIT_MOTOR_IID_PAYMENT = 29270-0100
|
||||
DIGIT_MOTOR_IID_POLICY_STATUS = 29271-0100
|
||||
DIGIT_MOTOR_IID_POLICY_PDF = 29272-0100
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# File storage (S3 uploads)
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
@ -127,6 +127,12 @@ class Acl
|
||||
'teams' => []
|
||||
],
|
||||
|
||||
// ===================== DIGIT MOTOR =====================
|
||||
'#^/digit-motor#' => [
|
||||
'roles' => [ HEAD_ROLE_ID, ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, STAFF_ROLE_ID],
|
||||
'teams' => [FINANCE_TEAM_ID, POS_TEAM_ID, BUSINESS_TEAM_ID, MANAGEMENT_TEAM_ID]
|
||||
],
|
||||
|
||||
// ===================== POLICY TRANSACTION / BDS =====================
|
||||
'#^/policy_tranction#' => [
|
||||
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
|
||||
|
||||
45
app/Config/DigitMotor.php
Normal file
45
app/Config/DigitMotor.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class DigitMotor extends BaseConfig
|
||||
{
|
||||
public string $baseUrl;
|
||||
public string $authPath;
|
||||
public string $executorPath;
|
||||
public string $username;
|
||||
public string $password;
|
||||
public string $environment;
|
||||
public int $timeout;
|
||||
public int $tokenLeewaySec;
|
||||
/** Partner key required by Digit policy PDF headerParam.Authorization */
|
||||
public string $pdfAuthKey;
|
||||
public array $integrationIds;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
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->executorPath = (string) env('DIGIT_MOTOR_EXECUTOR_PATH', '/OneAPI/v1/executor');
|
||||
$this->username = (string) env('DIGIT_MOTOR_USERNAME', '');
|
||||
$this->password = (string) env('DIGIT_MOTOR_PASSWORD', '');
|
||||
$this->environment = (string) env('DIGIT_MOTOR_ENVIRONMENT', 'staging');
|
||||
$this->timeout = (int) env('DIGIT_MOTOR_TIMEOUT', 30);
|
||||
$this->tokenLeewaySec = (int) env('DIGIT_MOTOR_TOKEN_LEEWAY_SEC', 60);
|
||||
$this->pdfAuthKey = (string) env('DIGIT_MOTOR_PDF_AUTH_KEY', '');
|
||||
|
||||
// Integration IDs from NHANCE Digit API kit (CURL / Bruno export)
|
||||
$this->integrationIds = [
|
||||
'quickQuote' => (string) env('DIGIT_MOTOR_IID_QUICK_QUOTE', '29266-0100'),
|
||||
'createQuote' => (string) env('DIGIT_MOTOR_IID_CREATE_QUOTE', '29268-0100'),
|
||||
'kycStatus' => (string) env('DIGIT_MOTOR_IID_KYC', '29269-0100'),
|
||||
'payment' => (string) env('DIGIT_MOTOR_IID_PAYMENT', '29270-0100'),
|
||||
'policyStatus' => (string) env('DIGIT_MOTOR_IID_POLICY_STATUS', '29271-0100'),
|
||||
'policyPdf' => (string) env('DIGIT_MOTOR_IID_POLICY_PDF', '29272-0100'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1201,6 +1201,24 @@ $routes->group('expense', ["filter" => "authMVC", 'namespace' => 'App\Controller
|
||||
$routes->get('client-policies', 'ExpenseController::clientPolicies');
|
||||
});
|
||||
|
||||
// Digit Motor Module
|
||||
$routes->group('digit-motor', ['filter' => 'authMVC', 'namespace' => 'App\Controllers'], static function ($routes) {
|
||||
$routes->match(['get', 'post'], 'list', 'DigitMotorController::list');
|
||||
$routes->get('journey', 'DigitMotorController::journey');
|
||||
$routes->get('journey/(:num)', 'DigitMotorController::journey/$1');
|
||||
$routes->get('quotes/(:num)', 'DigitMotorController::detail/$1');
|
||||
|
||||
$routes->post('quotes/quick', 'DigitMotorController::quickQuote');
|
||||
$routes->post('quotes/create', 'DigitMotorController::createQuote');
|
||||
$routes->post('quotes/(:num)/create', 'DigitMotorController::createQuote/$1');
|
||||
$routes->get('kyc/(:num)', 'DigitMotorController::kycStatus/$1');
|
||||
$routes->post('payments/(:num)/link', 'DigitMotorController::paymentLink/$1');
|
||||
$routes->get('policies/(:num)/status', 'DigitMotorController::policyStatus/$1');
|
||||
$routes->get('policies/(:num)/pdf', 'DigitMotorController::policyPdf/$1');
|
||||
|
||||
$routes->get('payment/callback/(:segment)/(:num)', 'DigitMotorController::paymentCallback/$1/$2');
|
||||
});
|
||||
|
||||
$routes->get('docs', 'Docs\DocsController::index');
|
||||
$routes->get('docs/(:segment)', 'Docs\DocsController::page/$1');
|
||||
|
||||
|
||||
226
app/Controllers/DigitMotorController.php
Normal file
226
app/Controllers/DigitMotorController.php
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\DigitMotor\DigitApiException;
|
||||
use App\Libraries\DigitMotor\DigitExecutorService;
|
||||
use App\Models\MotorQuoteModel;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
class DigitMotorController extends BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected MotorQuoteModel $quoteModel;
|
||||
protected DigitExecutorService $executor;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
set_session_context('Digit Motor Controller');
|
||||
$this->quoteModel = new MotorQuoteModel();
|
||||
$this->executor = new DigitExecutorService();
|
||||
}
|
||||
|
||||
// ===================== LIST =====================
|
||||
|
||||
public function list()
|
||||
{
|
||||
if (strtolower($this->request->getMethod()) === 'post') {
|
||||
$filters = [
|
||||
'status' => $this->request->getPost('status'),
|
||||
'enquiry_id' => $this->request->getPost('enquiry_id'),
|
||||
'quote_number' => $this->request->getPost('quote_number'),
|
||||
'license_plate' => $this->request->getPost('license_plate'),
|
||||
];
|
||||
$data['quotes'] = $this->quoteModel->getListWithVehicle($filters);
|
||||
return view('digit_motor/list_table', $data);
|
||||
}
|
||||
|
||||
$data['quotes'] = $this->quoteModel->getListWithVehicle();
|
||||
$data['statuses'] = [
|
||||
'DRAFT' => 'Draft',
|
||||
'QUOTED' => 'Quoted',
|
||||
'CREATED' => 'Created',
|
||||
'KYC_DONE' => 'KYC Done',
|
||||
'PAID' => 'Paid',
|
||||
'EFFECTIVE' => 'Effective',
|
||||
'FAILED' => 'Failed',
|
||||
];
|
||||
$data['title'] = 'Digit Motor Quotes';
|
||||
return $this->loadLayout('digit_motor/list', $data);
|
||||
}
|
||||
|
||||
// ===================== JOURNEY UI =====================
|
||||
|
||||
public function journey($quoteId = null)
|
||||
{
|
||||
$data['title'] = 'Digit Motor Journey';
|
||||
$data['quote'] = null;
|
||||
$data['quote_id'] = null;
|
||||
|
||||
if ($quoteId) {
|
||||
$data['quote'] = $this->quoteModel->getDetail((int) $quoteId);
|
||||
$data['quote_id'] = (int) $quoteId;
|
||||
if (!$data['quote']) {
|
||||
return redirect()->to(base_url('digit-motor/list'))->with('error', 'Quote not found.');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->loadLayout('digit_motor/journey', $data);
|
||||
}
|
||||
|
||||
public function detail($quoteId)
|
||||
{
|
||||
$detail = $this->quoteModel->getDetail((int) $quoteId);
|
||||
if (!$detail) {
|
||||
return $this->respond(['status' => false, 'message' => 'Quote not found.'], 404);
|
||||
}
|
||||
return $this->respond(['status' => true, 'data' => $detail]);
|
||||
}
|
||||
|
||||
// ===================== API ACTIONS =====================
|
||||
|
||||
public function quickQuote()
|
||||
{
|
||||
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]',
|
||||
];
|
||||
if (!$this->validateDigitInput($input, $rules)) {
|
||||
return $this->respond([
|
||||
'status' => false,
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$result = $this->executor->quickQuote($input);
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'message' => 'Quick quote generated.',
|
||||
'data' => $result,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function createQuote($quoteId = null)
|
||||
{
|
||||
return $this->runAction(function () use ($quoteId) {
|
||||
$input = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||
$quoteId = (int) ($quoteId ?: ($input['quote_id'] ?? 0));
|
||||
if ($quoteId <= 0) {
|
||||
return $this->respond(['status' => false, 'message' => 'quote_id is required.'], 422);
|
||||
}
|
||||
|
||||
$result = $this->executor->createQuote($quoteId, $input);
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'message' => 'Quote created.',
|
||||
'data' => $result,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function kycStatus($quoteId)
|
||||
{
|
||||
return $this->runAction(function () use ($quoteId) {
|
||||
$result = $this->executor->kycStatus((int) $quoteId);
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'message' => 'KYC status fetched.',
|
||||
'data' => $result,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function paymentLink($quoteId)
|
||||
{
|
||||
return $this->runAction(function () use ($quoteId) {
|
||||
$input = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||
$result = $this->executor->paymentLink((int) $quoteId, $input ?: []);
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'message' => 'Payment link generated.',
|
||||
'data' => $result,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function policyStatus($quoteId)
|
||||
{
|
||||
return $this->runAction(function () use ($quoteId) {
|
||||
$result = $this->executor->policyStatus((int) $quoteId);
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'message' => 'Policy status fetched.',
|
||||
'data' => $result,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function policyPdf($quoteId)
|
||||
{
|
||||
return $this->runAction(function () use ($quoteId) {
|
||||
$result = $this->executor->policyPdf((int) $quoteId);
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'message' => 'Policy PDF fetched.',
|
||||
'data' => $result,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function paymentCallback($outcome, $quoteId)
|
||||
{
|
||||
$quoteId = (int) $quoteId;
|
||||
if ($outcome === 'success' && $quoteId > 0) {
|
||||
try {
|
||||
$this->executor->policyStatus($quoteId);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'DigitMotor payment callback policyStatus: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to(base_url('digit-motor/journey/' . $quoteId))
|
||||
->with($outcome === 'success' ? 'success' : 'error', $outcome === 'success'
|
||||
? 'Payment return received. Refreshing policy status.'
|
||||
: 'Payment was cancelled or failed.');
|
||||
}
|
||||
|
||||
protected function runAction(callable $fn)
|
||||
{
|
||||
try {
|
||||
return $fn();
|
||||
} catch (DigitApiException $e) {
|
||||
log_message('error', 'DigitMotor API error: ' . $e->getMessage() . ' code=' . $e->getDigitCode());
|
||||
$http = $e->getHttpStatus() >= 400 ? $e->getHttpStatus() : 502;
|
||||
if ($e->isInfraError()) {
|
||||
$http = 502;
|
||||
}
|
||||
return $this->respond([
|
||||
'status' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'digit_code' => $e->getDigitCode(),
|
||||
'infra' => $e->isInfraError(),
|
||||
], min($http, 599));
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'DigitMotor unexpected error: ' . $e->getMessage());
|
||||
return $this->respond([
|
||||
'status' => false,
|
||||
'message' => 'Something went wrong. Please try again.',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateDigitInput(array $data, array $rules): bool
|
||||
{
|
||||
$this->validator = \Config\Services::validation();
|
||||
$this->validator->setRules($rules);
|
||||
return $this->validator->run($data);
|
||||
}
|
||||
}
|
||||
125
app/Database/digit_motor_demo_seed.sql
Normal file
125
app/Database/digit_motor_demo_seed.sql
Normal file
@ -0,0 +1,125 @@
|
||||
-- Dummy Digit Motor full-flow seed (matches current live schema column lengths)
|
||||
|
||||
DELETE k FROM motor_kyc k
|
||||
INNER JOIN motor_quote q ON q.id = k.quote_id
|
||||
WHERE q.enquiry_id = 'DEMO_FULL_FLOW_01';
|
||||
|
||||
DELETE p FROM motor_payment p
|
||||
INNER JOIN motor_quote q ON q.id = p.quote_id
|
||||
WHERE q.enquiry_id = 'DEMO_FULL_FLOW_01';
|
||||
|
||||
DELETE pol FROM motor_policy pol
|
||||
INNER JOIN motor_quote q ON q.id = pol.quote_id
|
||||
WHERE q.enquiry_id = 'DEMO_FULL_FLOW_01';
|
||||
|
||||
DELETE v FROM motor_vehicle v
|
||||
INNER JOIN motor_quote q ON q.id = v.quote_id
|
||||
WHERE q.enquiry_id = 'DEMO_FULL_FLOW_01';
|
||||
|
||||
DELETE FROM motor_quote WHERE enquiry_id = 'DEMO_FULL_FLOW_01';
|
||||
|
||||
INSERT INTO motor_quote (
|
||||
enquiry_id, quote_number, application_id, policy_holder_type,
|
||||
insurance_product_code, sub_insurance_product_code,
|
||||
previous_insurer_code, previous_policy_expiry_date, external_policy_number,
|
||||
is_ncb_transfer, start_date, end_date, pincode,
|
||||
coverage_details, premium, idv, status, created_at, updated_at
|
||||
) VALUES (
|
||||
'DEMO_FULL_FLOW_01',
|
||||
'D700690360',
|
||||
'V2F7789D887C44F1E8D87C1A7D50874F61F25EA9335D73AC40FBE994E1FFB696',
|
||||
'INDIVIDUAL',
|
||||
'20101',
|
||||
'PB',
|
||||
190,
|
||||
'2026-05-04',
|
||||
'PREV190DEMO01',
|
||||
0,
|
||||
'2026-04-17',
|
||||
'2027-04-16',
|
||||
'560068',
|
||||
JSON_OBJECT(
|
||||
'personal_accident', true,
|
||||
'zero_dep', true,
|
||||
'engine_protect', false,
|
||||
'rsa', true,
|
||||
'consumables', true,
|
||||
'key_protect', true,
|
||||
'tppd', true
|
||||
),
|
||||
13301.21,
|
||||
284500.00,
|
||||
'EFFECTIVE',
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
|
||||
SET @qid = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO motor_vehicle (
|
||||
quote_id, is_vehicle_new, vehicle_maincode, license_plate_number,
|
||||
vehicle_identification_number, engine_number, manufacture_date, registration_date,
|
||||
idv, default_idv, minimum_idv, maximum_idv
|
||||
) VALUES (
|
||||
@qid, 0, '1113811407', 'GJ04DA8726',
|
||||
'MAKGM651CJ4306951', 'L15Z15337915', '2014-01-10', '2014-01-10',
|
||||
284500.00, 280000.00, 255000.00, 310000.00
|
||||
);
|
||||
|
||||
INSERT INTO motor_kyc (
|
||||
quote_id, kyc_id, kyc_verification_status, reference_id, link,
|
||||
mismatch_type, id_verification_doc_type, address_verification_doc_type,
|
||||
mode, checked_at
|
||||
) VALUES (
|
||||
@qid,
|
||||
'KYC-DEMO-10042',
|
||||
'DONE',
|
||||
'RF29268010042',
|
||||
'https://example.com/kyc/ovd/demo',
|
||||
NULL,
|
||||
'Aadhaar',
|
||||
'Aadhaar',
|
||||
'O',
|
||||
NOW()
|
||||
);
|
||||
|
||||
INSERT INTO motor_payment (
|
||||
quote_id, application_id, digit_payment_id, request_reference,
|
||||
payment_mode, cancel_return_url, success_return_url, dispatcher_response,
|
||||
premium, payment_status, created_at
|
||||
) VALUES (
|
||||
@qid,
|
||||
'V2F7789D887C44F1E8D87C1A7D50874F61F25EA9335D73AC40FBE994E1FFB696',
|
||||
'DPY29268010042',
|
||||
'REQ-DEMO-10042',
|
||||
'EB',
|
||||
'http://localhost/nhance/digit-motor/payment/callback/cancel/0',
|
||||
'http://localhost/nhance/digit-motor/payment/callback/success/0',
|
||||
'https://example.com/digit/payment/demo-link',
|
||||
13301.21,
|
||||
'PAID',
|
||||
NOW()
|
||||
);
|
||||
|
||||
UPDATE motor_payment
|
||||
SET
|
||||
cancel_return_url = CONCAT('http://localhost/nhance/digit-motor/payment/callback/cancel/', @qid),
|
||||
success_return_url = CONCAT('http://localhost/nhance/digit-motor/payment/callback/success/', @qid)
|
||||
WHERE quote_id = @qid;
|
||||
|
||||
INSERT INTO motor_policy (
|
||||
quote_id, policy_number, policy_status, schedule_path, proposal_path,
|
||||
response_code, response_message, updated_at
|
||||
) VALUES (
|
||||
@qid,
|
||||
'D700690360',
|
||||
'EFFECTIVE',
|
||||
'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
|
||||
'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
|
||||
'0',
|
||||
'Policy is effective',
|
||||
NOW()
|
||||
);
|
||||
|
||||
SELECT @qid AS demo_quote_id, enquiry_id, quote_number, status
|
||||
FROM motor_quote WHERE id = @qid;
|
||||
122
app/Database/digit_motor_tables.sql
Normal file
122
app/Database/digit_motor_tables.sql
Normal file
@ -0,0 +1,122 @@
|
||||
-- Digit Motor Module tables
|
||||
-- Run once against the NHANCE database.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS motor_token (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
environment VARCHAR(20) NOT NULL DEFAULT 'staging',
|
||||
access_token TEXT NOT NULL,
|
||||
refresh_token TEXT,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_motor_token_env (environment)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
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,
|
||||
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',
|
||||
previous_insurer_code SMALLINT DEFAULT NULL,
|
||||
previous_policy_expiry_date DATE DEFAULT NULL,
|
||||
external_policy_number VARCHAR(32) DEFAULT NULL,
|
||||
is_ncb_transfer TINYINT(1) DEFAULT 0,
|
||||
start_date DATE DEFAULT NULL,
|
||||
end_date DATE DEFAULT NULL,
|
||||
pincode VARCHAR(6) NOT NULL,
|
||||
coverage_details JSON DEFAULT NULL,
|
||||
premium DECIMAL(12,2) DEFAULT NULL,
|
||||
idv DECIMAL(12,2) DEFAULT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
|
||||
-- DRAFT -> QUOTED -> CREATED -> KYC_DONE -> PAID -> EFFECTIVE / FAILED
|
||||
created_by INT DEFAULT NULL,
|
||||
updated_by INT DEFAULT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_motor_quote_enquiry (enquiry_id),
|
||||
KEY idx_motor_quote_status (status),
|
||||
KEY idx_motor_quote_quote_number (quote_number)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS motor_vehicle (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
quote_id BIGINT NOT NULL,
|
||||
is_vehicle_new TINYINT(1) NOT NULL DEFAULT 0,
|
||||
vehicle_maincode VARCHAR(30) NOT NULL,
|
||||
license_plate_number VARCHAR(12) NOT NULL,
|
||||
vehicle_identification_number VARCHAR(30) DEFAULT NULL,
|
||||
engine_number VARCHAR(30) DEFAULT NULL,
|
||||
manufacture_date DATE NOT NULL,
|
||||
registration_date DATE NOT NULL,
|
||||
registration_authority VARCHAR(10) DEFAULT NULL,
|
||||
idv DECIMAL(12,2) DEFAULT NULL,
|
||||
default_idv DECIMAL(12,2) DEFAULT NULL,
|
||||
minimum_idv DECIMAL(12,2) DEFAULT NULL,
|
||||
maximum_idv DECIMAL(12,2) DEFAULT NULL,
|
||||
UNIQUE KEY uq_motor_vehicle_quote (quote_id),
|
||||
CONSTRAINT fk_motor_vehicle_quote FOREIGN KEY (quote_id) REFERENCES motor_quote(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS motor_kyc (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
quote_id BIGINT NOT NULL,
|
||||
kyc_id VARCHAR(64) DEFAULT NULL,
|
||||
kyc_verification_status VARCHAR(20) DEFAULT NULL,
|
||||
reference_id VARCHAR(64) DEFAULT NULL,
|
||||
link VARCHAR(512) DEFAULT NULL,
|
||||
mismatch_type VARCHAR(40) DEFAULT NULL,
|
||||
id_verification_doc_type VARCHAR(40) DEFAULT NULL,
|
||||
address_verification_doc_type VARCHAR(40) DEFAULT NULL,
|
||||
mode CHAR(1) DEFAULT 'O',
|
||||
checked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_motor_kyc_quote (quote_id),
|
||||
CONSTRAINT fk_motor_kyc_quote FOREIGN KEY (quote_id) REFERENCES motor_quote(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS motor_payment (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
quote_id BIGINT NOT NULL,
|
||||
application_id VARCHAR(128) NOT NULL,
|
||||
digit_payment_id VARCHAR(64) DEFAULT NULL,
|
||||
request_reference VARCHAR(64) DEFAULT NULL,
|
||||
payment_mode VARCHAR(5) DEFAULT 'EB',
|
||||
cancel_return_url VARCHAR(512) DEFAULT NULL,
|
||||
success_return_url VARCHAR(512) DEFAULT NULL,
|
||||
dispatcher_response VARCHAR(512) DEFAULT NULL,
|
||||
premium DECIMAL(12,2) DEFAULT NULL,
|
||||
payment_status VARCHAR(20) DEFAULT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_motor_payment_quote (quote_id),
|
||||
CONSTRAINT fk_motor_payment_quote FOREIGN KEY (quote_id) REFERENCES motor_quote(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS motor_policy (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
quote_id BIGINT NOT NULL,
|
||||
policy_number VARCHAR(32) DEFAULT NULL,
|
||||
policy_status VARCHAR(20) DEFAULT NULL,
|
||||
schedule_path VARCHAR(512) DEFAULT NULL,
|
||||
proposal_path VARCHAR(512) DEFAULT NULL,
|
||||
response_code VARCHAR(10) DEFAULT NULL,
|
||||
response_message VARCHAR(255) DEFAULT NULL,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_motor_policy_quote (quote_id),
|
||||
CONSTRAINT fk_motor_policy_quote FOREIGN KEY (quote_id) REFERENCES motor_quote(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS motor_api_log (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
quote_id BIGINT DEFAULT NULL,
|
||||
integration_id VARCHAR(20) DEFAULT NULL,
|
||||
endpoint VARCHAR(120) DEFAULT NULL,
|
||||
request_body JSON DEFAULT NULL,
|
||||
response_body JSON DEFAULT NULL,
|
||||
http_status SMALLINT DEFAULT NULL,
|
||||
error_code VARCHAR(10) DEFAULT NULL,
|
||||
duration_ms INT DEFAULT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_motor_api_log_quote (quote_id),
|
||||
KEY idx_motor_api_log_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
177
app/Libraries/DigitMotor/DigitApiClient.php
Normal file
177
app/Libraries/DigitMotor/DigitApiClient.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\DigitMotor;
|
||||
|
||||
use App\Models\MotorApiLogModel;
|
||||
use Config\DigitMotor as DigitMotorConfig;
|
||||
|
||||
/**
|
||||
* Thin HTTP client for Digit OneAPI executor calls.
|
||||
*/
|
||||
class DigitApiClient
|
||||
{
|
||||
protected DigitMotorConfig $config;
|
||||
protected DigitAuthClient $authClient;
|
||||
protected MotorApiLogModel $logModel;
|
||||
|
||||
public function __construct(?DigitMotorConfig $config = null, ?DigitAuthClient $authClient = null)
|
||||
{
|
||||
$this->config = $config ?? config('DigitMotor');
|
||||
$this->authClient = $authClient ?? new DigitAuthClient($this->config);
|
||||
$this->logModel = new MotorApiLogModel();
|
||||
helper('api');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST to Digit executor (or arbitrary path under baseUrl).
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function post(string $path, array $payload, ?string $integrationId = null, ?int $quoteId = null): array
|
||||
{
|
||||
return $this->request('POST', $path, $payload, $integrationId, $quoteId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
protected function request(
|
||||
string $method,
|
||||
string $path,
|
||||
array $payload,
|
||||
?string $integrationId,
|
||||
?int $quoteId,
|
||||
bool $isRetry
|
||||
): array {
|
||||
$token = $this->authClient->getToken($isRetry);
|
||||
$url = $this->config->baseUrl . $path;
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $token,
|
||||
];
|
||||
if ($integrationId) {
|
||||
$headers[] = 'integrationId: ' . $integrationId;
|
||||
}
|
||||
|
||||
$started = microtime(true);
|
||||
$response = call_third_party_api($url, $method, $headers, $payload);
|
||||
$durationMs = (int) round((microtime(true) - $started) * 1000);
|
||||
|
||||
$httpCode = (int) ($response['code'] ?? 0);
|
||||
$data = $response['data'] ?? [];
|
||||
if (is_string($data)) {
|
||||
$decoded = json_decode($data, true);
|
||||
$data = is_array($decoded) ? $decoded : ['raw' => $data];
|
||||
}
|
||||
|
||||
$digitCode = $this->extractDigitCode($data, $httpCode);
|
||||
|
||||
$this->writeLog($quoteId, $integrationId, $path, $payload, $data, $httpCode, $digitCode, $durationMs);
|
||||
|
||||
// Stale token — refresh once and retry
|
||||
if (!$isRetry && ($httpCode === 401 || (string) $digitCode === '401')) {
|
||||
$this->authClient->refreshToken();
|
||||
return $this->request($method, $path, $payload, $integrationId, $quoteId, true);
|
||||
}
|
||||
|
||||
if ($this->isHardFail($httpCode, $digitCode, $response['status'] ?? false, $data)) {
|
||||
$message = $this->extractMessage($data, $httpCode);
|
||||
throw new DigitApiException($message, $digitCode, $httpCode, $data);
|
||||
}
|
||||
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
protected function isHardFail(int $httpCode, $digitCode, bool $httpOk, $data): bool
|
||||
{
|
||||
$code = (string) $digitCode;
|
||||
if (in_array($code, ['403', '999', '400', '500'], true)) {
|
||||
return true;
|
||||
}
|
||||
if ($httpCode >= 400) {
|
||||
return true;
|
||||
}
|
||||
if (!$httpOk) {
|
||||
return true;
|
||||
}
|
||||
// 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;
|
||||
if ($errCode !== null && (string) $errCode !== '0' && strtoupper((string) $errCode) !== 'SUCCESS') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function extractDigitCode($data, int $httpCode)
|
||||
{
|
||||
if (!is_array($data)) {
|
||||
return $httpCode ?: null;
|
||||
}
|
||||
return $data['error']['code']
|
||||
?? $data['code']
|
||||
?? $data['errorCode']
|
||||
?? $data['responseCode']
|
||||
?? ($httpCode >= 400 ? (string) $httpCode : null);
|
||||
}
|
||||
|
||||
protected function extractMessage($data, int $httpCode): string
|
||||
{
|
||||
if (!is_array($data)) {
|
||||
return 'Digit API request failed (HTTP ' . $httpCode . ').';
|
||||
}
|
||||
$msg = $data['error']['message']
|
||||
?? $data['message']
|
||||
?? $data['errorMessage']
|
||||
?? $data['responseMessage']
|
||||
?? null;
|
||||
if (is_array($msg)) {
|
||||
$msg = json_encode($msg);
|
||||
}
|
||||
return $msg ?: 'Digit API request failed (HTTP ' . $httpCode . ').';
|
||||
}
|
||||
|
||||
protected function writeLog(
|
||||
?int $quoteId,
|
||||
?string $integrationId,
|
||||
string $endpoint,
|
||||
array $requestBody,
|
||||
$responseBody,
|
||||
int $httpStatus,
|
||||
$errorCode,
|
||||
int $durationMs
|
||||
): void {
|
||||
try {
|
||||
$this->logModel->insert([
|
||||
'quote_id' => $quoteId,
|
||||
'integration_id' => $integrationId,
|
||||
'endpoint' => $endpoint,
|
||||
'request_body' => json_encode($this->redact($requestBody)),
|
||||
'response_body' => json_encode($this->redact(is_array($responseBody) ? $responseBody : ['raw' => $responseBody])),
|
||||
'http_status' => $httpStatus,
|
||||
'error_code' => $errorCode !== null ? (string) $errorCode : null,
|
||||
'duration_ms' => $durationMs,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'DigitMotor API log write failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function redact(array $data): array
|
||||
{
|
||||
$sensitive = ['password', 'access_token', 'refresh_token', 'Authorization', 'authorization'];
|
||||
$out = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array((string) $key, $sensitive, true)) {
|
||||
$out[$key] = '***REDACTED***';
|
||||
} elseif (is_array($value)) {
|
||||
$out[$key] = $this->redact($value);
|
||||
} else {
|
||||
$out[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
48
app/Libraries/DigitMotor/DigitApiException.php
Normal file
48
app/Libraries/DigitMotor/DigitApiException.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\DigitMotor;
|
||||
|
||||
use Exception;
|
||||
|
||||
class DigitApiException extends Exception
|
||||
{
|
||||
protected $digitCode;
|
||||
protected $digitMessage;
|
||||
protected $httpStatus;
|
||||
protected $responseBody;
|
||||
|
||||
public function __construct(
|
||||
string $message,
|
||||
$digitCode = null,
|
||||
int $httpStatus = 0,
|
||||
$responseBody = null,
|
||||
?Exception $previous = null
|
||||
) {
|
||||
parent::__construct($message, 0, $previous);
|
||||
$this->digitCode = $digitCode;
|
||||
$this->digitMessage = $message;
|
||||
$this->httpStatus = $httpStatus;
|
||||
$this->responseBody = $responseBody;
|
||||
}
|
||||
|
||||
public function getDigitCode()
|
||||
{
|
||||
return $this->digitCode;
|
||||
}
|
||||
|
||||
public function getHttpStatus(): int
|
||||
{
|
||||
return $this->httpStatus;
|
||||
}
|
||||
|
||||
public function getResponseBody()
|
||||
{
|
||||
return $this->responseBody;
|
||||
}
|
||||
|
||||
public function isInfraError(): bool
|
||||
{
|
||||
$code = (string) $this->digitCode;
|
||||
return in_array($code, ['403', '999'], true) || in_array($this->httpStatus, [403], true);
|
||||
}
|
||||
}
|
||||
110
app/Libraries/DigitMotor/DigitAuthClient.php
Normal file
110
app/Libraries/DigitMotor/DigitAuthClient.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\DigitMotor;
|
||||
|
||||
use App\Models\MotorTokenModel;
|
||||
use Config\DigitMotor as DigitMotorConfig;
|
||||
|
||||
/**
|
||||
* Owns Digit OneAPI token lifecycle (cache + refresh).
|
||||
*/
|
||||
class DigitAuthClient
|
||||
{
|
||||
protected DigitMotorConfig $config;
|
||||
protected MotorTokenModel $tokenModel;
|
||||
|
||||
public function __construct(?DigitMotorConfig $config = null)
|
||||
{
|
||||
$this->config = $config ?? config('DigitMotor');
|
||||
$this->tokenModel = new MotorTokenModel();
|
||||
helper('api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a valid Bearer access token, refreshing from Digit when needed.
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function getToken(bool $forceRefresh = false): string
|
||||
{
|
||||
if (!$forceRefresh) {
|
||||
$cached = $this->tokenModel->getByEnvironment($this->config->environment);
|
||||
if ($cached && !empty($cached['access_token']) && !empty($cached['expires_at'])) {
|
||||
$expiresAt = strtotime($cached['expires_at']);
|
||||
if ($expiresAt !== false && ($expiresAt - $this->config->tokenLeewaySec) > time()) {
|
||||
return $cached['access_token'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fetchAndStoreToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh (e.g. after 401).
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function refreshToken(): string
|
||||
{
|
||||
return $this->getToken(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
protected function fetchAndStoreToken(): string
|
||||
{
|
||||
if ($this->config->username === '' || $this->config->password === '') {
|
||||
throw new DigitApiException(
|
||||
'Digit Motor credentials are not configured. Set DIGIT_MOTOR_USERNAME and DIGIT_MOTOR_PASSWORD in .env.',
|
||||
'CONFIG',
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
$url = $this->config->baseUrl . $this->config->authPath;
|
||||
$headers = ['Content-Type: application/json'];
|
||||
$body = [
|
||||
'username' => $this->config->username,
|
||||
'password' => $this->config->password,
|
||||
];
|
||||
|
||||
$response = call_third_party_api($url, 'POST', $headers, $body);
|
||||
$httpCode = (int) ($response['code'] ?? 0);
|
||||
$data = $response['data'] ?? [];
|
||||
|
||||
if (is_string($data)) {
|
||||
$decoded = json_decode($data, true);
|
||||
$data = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
$accessToken = $data['access_token']
|
||||
?? $data['accessToken']
|
||||
?? $data['token']
|
||||
?? null;
|
||||
|
||||
if (empty($accessToken) || empty($response['status'])) {
|
||||
$msg = $data['message'] ?? $data['error'] ?? 'Digit token generation failed.';
|
||||
throw new DigitApiException(
|
||||
is_string($msg) ? $msg : 'Digit token generation failed.',
|
||||
$data['code'] ?? (string) $httpCode,
|
||||
$httpCode,
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
$expiresIn = (int) ($data['expires_in'] ?? $data['expiresIn'] ?? 900);
|
||||
$refreshToken = $data['refresh_token'] ?? $data['refreshToken'] ?? null;
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + max(60, $expiresIn));
|
||||
|
||||
$this->tokenModel->upsertToken(
|
||||
$this->config->environment,
|
||||
$accessToken,
|
||||
$refreshToken,
|
||||
$expiresAt
|
||||
);
|
||||
|
||||
return $accessToken;
|
||||
}
|
||||
}
|
||||
743
app/Libraries/DigitMotor/DigitExecutorService.php
Normal file
743
app/Libraries/DigitMotor/DigitExecutorService.php
Normal file
@ -0,0 +1,743 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\DigitMotor;
|
||||
|
||||
use App\Models\MotorKycModel;
|
||||
use App\Models\MotorPaymentModel;
|
||||
use App\Models\MotorPolicyModel;
|
||||
use App\Models\MotorQuoteModel;
|
||||
use App\Models\MotorVehicleModel;
|
||||
use Config\DigitMotor as DigitMotorConfig;
|
||||
|
||||
/**
|
||||
* Business actions against Digit OneAPI executor services.
|
||||
*/
|
||||
class DigitExecutorService
|
||||
{
|
||||
protected DigitMotorConfig $config;
|
||||
protected DigitApiClient $api;
|
||||
protected MotorQuoteModel $quoteModel;
|
||||
protected MotorVehicleModel $vehicleModel;
|
||||
protected MotorKycModel $kycModel;
|
||||
protected MotorPaymentModel $paymentModel;
|
||||
protected MotorPolicyModel $policyModel;
|
||||
|
||||
public function __construct(?DigitMotorConfig $config = null, ?DigitApiClient $api = null)
|
||||
{
|
||||
$this->config = $config ?? config('DigitMotor');
|
||||
$this->api = $api ?? new DigitApiClient($this->config);
|
||||
$this->quoteModel = new MotorQuoteModel();
|
||||
$this->vehicleModel = new MotorVehicleModel();
|
||||
$this->kycModel = new MotorKycModel();
|
||||
$this->paymentModel = new MotorPaymentModel();
|
||||
$this->policyModel = new MotorPolicyModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create/update local quote + call motorQuickQuote.
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function quickQuote(array $input): array
|
||||
{
|
||||
$enquiryId = $input['enquiry_id'] ?? ('NH' . date('ymdHis') . random_int(100, 999));
|
||||
$payload = $this->buildQuickQuotePayload($input, $enquiryId);
|
||||
|
||||
$quoteId = $this->persistQuoteShell($input, $enquiryId, 'DRAFT');
|
||||
|
||||
$response = $this->api->post(
|
||||
$this->config->executorPath,
|
||||
$payload,
|
||||
$this->config->integrationIds['quickQuote'],
|
||||
$quoteId
|
||||
);
|
||||
|
||||
$premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']);
|
||||
$idv = $this->pickNumber($response, ['idv', 'vehicleIDV', 'insuredDeclaredValue']);
|
||||
if ($idv === null && isset($response['vehicle']['vehicleIDV']['idv'])) {
|
||||
$idv = (float) $response['vehicle']['vehicleIDV']['idv'];
|
||||
}
|
||||
|
||||
$vehicleIdv = $response['vehicle']['vehicleIDV'] ?? $response['vehicleIDV'] ?? [];
|
||||
|
||||
$this->quoteModel->update($quoteId, [
|
||||
'premium' => $premium,
|
||||
'idv' => $idv,
|
||||
'status' => 'QUOTED',
|
||||
'coverage_details' => json_encode($input['coverages'] ?? $payload['contract']['coverages'] ?? []),
|
||||
]);
|
||||
|
||||
$vehicle = $this->vehicleModel->where('quote_id', $quoteId)->first();
|
||||
if ($vehicle) {
|
||||
$this->vehicleModel->update($vehicle['id'], [
|
||||
'idv' => $idv ?? ($vehicleIdv['idv'] ?? null),
|
||||
'default_idv' => $vehicleIdv['defaultIdv'] ?? $vehicleIdv['defaultIDV'] ?? null,
|
||||
'minimum_idv' => $vehicleIdv['minimumIdv'] ?? $vehicleIdv['minIdv'] ?? null,
|
||||
'maximum_idv' => $vehicleIdv['maximumIdv'] ?? $vehicleIdv['maxIdv'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'quote_id' => $quoteId,
|
||||
'enquiry_id' => $enquiryId,
|
||||
'premium' => $premium,
|
||||
'idv' => $idv,
|
||||
'status' => 'QUOTED',
|
||||
'response' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Call motorCreateQuote for an existing quoted row.
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function createQuote(int $quoteId, array $input = []): array
|
||||
{
|
||||
$detail = $this->quoteModel->getDetail($quoteId);
|
||||
if (!$detail) {
|
||||
throw new DigitApiException('Quote not found.', '404', 404);
|
||||
}
|
||||
|
||||
$payload = $this->buildCreateQuotePayload($detail, $input);
|
||||
|
||||
$response = $this->api->post(
|
||||
$this->config->executorPath,
|
||||
$payload,
|
||||
$this->config->integrationIds['createQuote'],
|
||||
$quoteId
|
||||
);
|
||||
|
||||
$quoteNumber = $response['quoteNumber'] ?? $response['policyNumber'] ?? ($response['contract']['quoteNumber'] ?? null);
|
||||
$applicationId = $response['applicationId'] ?? $response['policyId'] ?? ($response['contract']['applicationId'] ?? null);
|
||||
$premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']) ?? $detail['premium'];
|
||||
|
||||
$this->quoteModel->update($quoteId, [
|
||||
'quote_number' => $quoteNumber,
|
||||
'application_id' => $applicationId,
|
||||
'premium' => $premium,
|
||||
'start_date' => $input['start_date'] ?? $detail['start_date'],
|
||||
'end_date' => $input['end_date'] ?? $detail['end_date'],
|
||||
'status' => 'CREATED',
|
||||
]);
|
||||
|
||||
return [
|
||||
'quote_id' => $quoteId,
|
||||
'quote_number' => $quoteNumber,
|
||||
'application_id' => $applicationId,
|
||||
'premium' => $premium,
|
||||
'status' => 'CREATED',
|
||||
'response' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll KYC status and upsert motor_kyc.
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function kycStatus(int $quoteId): array
|
||||
{
|
||||
$detail = $this->quoteModel->getDetail($quoteId);
|
||||
if (!$detail || empty($detail['quote_number'])) {
|
||||
throw new DigitApiException('Quote number is required for KYC status. Create quote first.', '400', 400);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'queryParam' => [
|
||||
'policyNumber' => $detail['quote_number'],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->api->post(
|
||||
$this->config->executorPath,
|
||||
$payload,
|
||||
$this->config->integrationIds['kycStatus'],
|
||||
$quoteId
|
||||
);
|
||||
|
||||
$status = $response['kycVerificationStatus']
|
||||
?? $response['kycStatus']
|
||||
?? ($response['kyc']['kycVerificationStatus'] ?? null);
|
||||
|
||||
$row = [
|
||||
'quote_id' => $quoteId,
|
||||
'kyc_id' => $response['kycId'] ?? ($response['kyc']['kycId'] ?? null),
|
||||
'kyc_verification_status' => $status,
|
||||
'reference_id' => $response['referenceId'] ?? ($response['kyc']['referenceId'] ?? null),
|
||||
'link' => $response['link'] ?? ($response['kyc']['link'] ?? null),
|
||||
'mismatch_type' => $response['mismatchType'] ?? null,
|
||||
'id_verification_doc_type' => $response['idVerificationDocType'] ?? null,
|
||||
'address_verification_doc_type' => $response['addressVerificationDocType'] ?? null,
|
||||
'mode' => $response['mode'] ?? 'O',
|
||||
'checked_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$existing = $this->kycModel->where('quote_id', $quoteId)->orderBy('id', 'DESC')->first();
|
||||
if ($existing) {
|
||||
$this->kycModel->update($existing['id'], $row);
|
||||
$row['id'] = $existing['id'];
|
||||
} else {
|
||||
$row['id'] = $this->kycModel->insert($row);
|
||||
}
|
||||
|
||||
$normalized = strtoupper((string) $status);
|
||||
if (in_array($normalized, ['DONE', 'VERIFIED', 'SUCCESS', 'COMPLETED'], true)) {
|
||||
$this->quoteModel->update($quoteId, ['status' => 'KYC_DONE']);
|
||||
}
|
||||
|
||||
return [
|
||||
'quote_id' => $quoteId,
|
||||
'kyc' => $row,
|
||||
'status' => $status,
|
||||
'response' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate payment link.
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function paymentLink(int $quoteId, array $input = []): array
|
||||
{
|
||||
$detail = $this->quoteModel->getDetail($quoteId);
|
||||
if (!$detail || empty($detail['application_id'])) {
|
||||
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, '.', ''));
|
||||
|
||||
$successUrl = $input['success_return_url']
|
||||
?? base_url('digit-motor/payment/callback/success/' . $quoteId);
|
||||
$cancelUrl = $input['cancel_return_url']
|
||||
?? base_url('digit-motor/payment/callback/cancel/' . $quoteId);
|
||||
|
||||
$payload = [
|
||||
'premiumAmount' => $premiumAmount,
|
||||
'paymentMode' => $input['payment_mode'] ?? 'EB',
|
||||
'successReturnUrl' => $successUrl,
|
||||
'cancelReturnUrl' => $cancelUrl,
|
||||
'applicationId' => $detail['application_id'],
|
||||
];
|
||||
|
||||
$response = $this->api->post(
|
||||
$this->config->executorPath,
|
||||
$payload,
|
||||
$this->config->integrationIds['payment'],
|
||||
$quoteId
|
||||
);
|
||||
|
||||
$dispatcher = $response['dispatcherResponse']
|
||||
?? $response['paymentLink']
|
||||
?? $response['redirectUrl']
|
||||
?? null;
|
||||
|
||||
$row = [
|
||||
'quote_id' => $quoteId,
|
||||
'application_id' => $detail['application_id'],
|
||||
'digit_payment_id' => $response['digitPaymentId'] ?? $response['paymentId'] ?? null,
|
||||
'request_reference' => $response['requestReference'] ?? null,
|
||||
'payment_mode' => $payload['paymentMode'],
|
||||
'cancel_return_url' => $cancelUrl,
|
||||
'success_return_url' => $successUrl,
|
||||
'dispatcher_response' => $dispatcher,
|
||||
'premium' => $detail['premium'],
|
||||
'payment_status' => 'LINK_GENERATED',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$row['id'] = $this->paymentModel->insert($row);
|
||||
|
||||
return [
|
||||
'quote_id' => $quoteId,
|
||||
'payment' => $row,
|
||||
'dispatcher_response'=> $dispatcher,
|
||||
'response' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll policy status.
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function policyStatus(int $quoteId): array
|
||||
{
|
||||
$detail = $this->quoteModel->getDetail($quoteId);
|
||||
if (!$detail || empty($detail['quote_number'])) {
|
||||
throw new DigitApiException('Quote/policy number is required for policy status.', '400', 400);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'queryParam' => [
|
||||
'policyNumber' => $detail['quote_number'],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->api->post(
|
||||
$this->config->executorPath,
|
||||
$payload,
|
||||
$this->config->integrationIds['policyStatus'],
|
||||
$quoteId
|
||||
);
|
||||
|
||||
$policyStatus = $response['policyStatus']
|
||||
?? $response['status']
|
||||
?? ($response['policy']['policyStatus'] ?? null);
|
||||
$policyNumber = $response['policyNumber'] ?? $detail['quote_number'];
|
||||
|
||||
$row = [
|
||||
'quote_id' => $quoteId,
|
||||
'policy_number' => $policyNumber,
|
||||
'policy_status' => $policyStatus,
|
||||
'response_code' => isset($response['responseCode']) ? (string) $response['responseCode'] : null,
|
||||
'response_message' => $response['responseMessage'] ?? null,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$existing = $this->policyModel->where('quote_id', $quoteId)->first();
|
||||
if ($existing) {
|
||||
$this->policyModel->update($existing['id'], $row);
|
||||
$row['id'] = $existing['id'];
|
||||
$row['schedule_path'] = $existing['schedule_path'];
|
||||
$row['proposal_path'] = $existing['proposal_path'];
|
||||
} else {
|
||||
$row['id'] = $this->policyModel->insert($row);
|
||||
}
|
||||
|
||||
$normalized = strtoupper((string) $policyStatus);
|
||||
if (in_array($normalized, ['EFFECTIVE', 'COMPLETE', 'COMPLETED', 'ACTIVE'], true)) {
|
||||
$this->quoteModel->update($quoteId, ['status' => 'EFFECTIVE']);
|
||||
if (!empty($detail['payment'])) {
|
||||
$this->paymentModel->update($detail['payment']['id'], ['payment_status' => 'PAID']);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'quote_id' => $quoteId,
|
||||
'policy' => $row,
|
||||
'status' => $policyStatus,
|
||||
'response' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch policy PDF paths (only after policy is effective-ish).
|
||||
*
|
||||
* @throws DigitApiException
|
||||
*/
|
||||
public function policyPdf(int $quoteId): array
|
||||
{
|
||||
$detail = $this->quoteModel->getDetail($quoteId);
|
||||
if (!$detail) {
|
||||
throw new DigitApiException('Quote not found.', '404', 404);
|
||||
}
|
||||
|
||||
$policyId = $detail['application_id']
|
||||
?? ($detail['policy']['policy_number'] ?? null);
|
||||
if (empty($policyId)) {
|
||||
throw new DigitApiException('Application/policy ID is required for PDF.', '400', 400);
|
||||
}
|
||||
|
||||
$policyStatus = strtoupper((string) ($detail['policy']['policy_status'] ?? $detail['status'] ?? ''));
|
||||
if (!in_array($policyStatus, ['EFFECTIVE', 'COMPLETE', 'COMPLETED', 'ACTIVE', 'PAID'], true)
|
||||
&& !in_array(strtoupper((string) $detail['status']), ['EFFECTIVE', 'PAID'], true)) {
|
||||
throw new DigitApiException(
|
||||
'Policy PDF is available only after the policy is effective. Current status: ' . ($detail['status'] ?? 'unknown'),
|
||||
'400',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'policyId' => $policyId,
|
||||
'headerParam' => [
|
||||
'Authorization' => $this->config->pdfAuthKey,
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->api->post(
|
||||
$this->config->executorPath,
|
||||
$payload,
|
||||
$this->config->integrationIds['policyPdf'],
|
||||
$quoteId
|
||||
);
|
||||
|
||||
$schedulePath = $response['schedulePath'] ?? $response['policySchedulePath'] ?? ($response['schedule'] ?? null);
|
||||
$proposalPath = $response['proposalPath'] ?? $response['proposalFormPath'] ?? ($response['proposal'] ?? null);
|
||||
|
||||
$row = [
|
||||
'quote_id' => $quoteId,
|
||||
'policy_number' => $detail['quote_number'],
|
||||
'policy_status' => $detail['policy']['policy_status'] ?? $detail['status'],
|
||||
'schedule_path' => $schedulePath,
|
||||
'proposal_path' => $proposalPath,
|
||||
'response_code' => isset($response['responseCode']) ? (string) $response['responseCode'] : null,
|
||||
'response_message' => $response['responseMessage'] ?? null,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$existing = $this->policyModel->where('quote_id', $quoteId)->first();
|
||||
if ($existing) {
|
||||
$this->policyModel->update($existing['id'], $row);
|
||||
$row['id'] = $existing['id'];
|
||||
} else {
|
||||
$row['id'] = $this->policyModel->insert($row);
|
||||
}
|
||||
|
||||
return [
|
||||
'quote_id' => $quoteId,
|
||||
'policy' => $row,
|
||||
'response' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
protected function persistQuoteShell(array $input, string $enquiryId, string $status): int
|
||||
{
|
||||
$existing = $this->quoteModel->where('enquiry_id', $enquiryId)->first();
|
||||
|
||||
$quoteData = [
|
||||
'enquiry_id' => $enquiryId,
|
||||
'policy_holder_type' => $input['policy_holder_type'] ?? 'INDIVIDUAL',
|
||||
'insurance_product_code' => (string) ($input['insurance_product_code'] ?? '20101'),
|
||||
'sub_insurance_product_code' => (string) ($input['sub_insurance_product_code'] ?? 'PB'),
|
||||
'previous_insurer_code' => $input['previous_insurer_code'] ?? null,
|
||||
'previous_policy_expiry_date'=> $input['previous_policy_expiry_date'] ?? null,
|
||||
'external_policy_number' => $input['previous_policy_number'] ?? null,
|
||||
'is_ncb_transfer' => !empty($input['is_ncb_transfer']) ? 1 : 0,
|
||||
'start_date' => $input['start_date'] ?? null,
|
||||
'end_date' => $input['end_date'] ?? null,
|
||||
'pincode' => (string) ($input['pincode'] ?? ''),
|
||||
'coverage_details' => json_encode($input['coverages'] ?? []),
|
||||
'status' => $status,
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$quoteId = (int) $existing['id'];
|
||||
$this->quoteModel->update($quoteId, $quoteData);
|
||||
$vehicle = $this->vehicleModel->where('quote_id', $quoteId)->first();
|
||||
$vehicleData = $this->vehicleRowFromInput($input, $quoteId);
|
||||
if ($vehicle) {
|
||||
$this->vehicleModel->update($vehicle['id'], $vehicleData);
|
||||
} else {
|
||||
$this->vehicleModel->insert($vehicleData);
|
||||
}
|
||||
return $quoteId;
|
||||
}
|
||||
|
||||
$quoteId = (int) $this->quoteModel->insert($quoteData);
|
||||
$this->vehicleModel->insert($this->vehicleRowFromInput($input, $quoteId));
|
||||
return $quoteId;
|
||||
}
|
||||
|
||||
protected function vehicleRowFromInput(array $input, int $quoteId): array
|
||||
{
|
||||
$plate = strtoupper(preg_replace('/\s+/', '', (string) ($input['license_plate_number'] ?? '')));
|
||||
$authority = $input['registration_authority'] ?? null;
|
||||
if (!$authority && strlen($plate) >= 4) {
|
||||
$authority = substr($plate, 0, 4);
|
||||
}
|
||||
|
||||
return [
|
||||
'quote_id' => $quoteId,
|
||||
'is_vehicle_new' => !empty($input['is_vehicle_new']) ? 1 : 0,
|
||||
'vehicle_maincode' => (string) ($input['vehicle_maincode'] ?? ''),
|
||||
'license_plate_number' => $plate,
|
||||
'vehicle_identification_number' => $input['vehicle_identification_number'] ?? null,
|
||||
'engine_number' => $input['engine_number'] ?? null,
|
||||
'manufacture_date' => $input['manufacture_date'] ?? date('Y-m-d'),
|
||||
'registration_date' => $input['registration_date'] ?? date('Y-m-d'),
|
||||
'registration_authority' => $authority,
|
||||
'idv' => $input['idv'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildQuickQuotePayload(array $input, string $enquiryId): array
|
||||
{
|
||||
$plate = strtoupper(preg_replace('/\s+/', '', (string) ($input['license_plate_number'] ?? '')));
|
||||
$authority = $input['registration_authority'] ?? (strlen($plate) >= 4 ? substr($plate, 0, 4) : '');
|
||||
$coverages = $this->defaultCoverages($input['coverages'] ?? []);
|
||||
|
||||
return [
|
||||
'vehicle' => [
|
||||
'isVehicleNew' => !empty($input['is_vehicle_new']),
|
||||
'vehicleIDV' => [
|
||||
'idv' => (string) ($input['idv'] ?? '0'),
|
||||
],
|
||||
'vehicleMaincode' => (string) ($input['vehicle_maincode'] ?? ''),
|
||||
'licensePlateNumber' => $plate,
|
||||
'registrationAuthority' => $authority,
|
||||
'vehicleIdentificationNumber' => $input['vehicle_identification_number'] ?? '',
|
||||
'engineNumber' => $input['engine_number'] ?? '',
|
||||
'manufactureDate' => $input['manufacture_date'] ?? null,
|
||||
'registrationDate' => $input['registration_date'] ?? null,
|
||||
'fastTagNumber' => null,
|
||||
'ownershipSerialNumber' => 0,
|
||||
],
|
||||
'previousInsurer' => [
|
||||
'isPreviousInsurerKnown' => !empty($input['previous_insurer_code']),
|
||||
'previousInsurerCode' => isset($input['previous_insurer_code']) ? (string) $input['previous_insurer_code'] : '',
|
||||
'previousPolicyExpiryDate' => $input['previous_policy_expiry_date'] ?? null,
|
||||
'previousPolicyNumber' => $input['previous_policy_number'] ?? '',
|
||||
'isClaimInLastYear' => !empty($input['is_claim_in_last_year']),
|
||||
'previousNoClaimBonus' => $input['previous_ncb'] ?? 'ZERO',
|
||||
'previousPolicyType' => $input['previous_policy_type'] ?? '',
|
||||
],
|
||||
'contract' => [
|
||||
'insuranceProductCode' => (int) ($input['insurance_product_code'] ?? 20102),
|
||||
'startDate' => $input['start_date'] ?? null,
|
||||
'endDate' => $input['end_date'] ?? null,
|
||||
'policyTerm' => (int) ($input['policy_term'] ?? 1),
|
||||
'coverages' => $coverages,
|
||||
'subInsuranceProductCode' => $input['sub_insurance_product_code'] ?? 'PB',
|
||||
'policyHolderType' => $input['policy_holder_type'] ?? 'INDIVIDUAL',
|
||||
'externalPolicyNumber' => $input['external_policy_number'] ?? '',
|
||||
],
|
||||
'pinCode' => (int) ($input['pincode'] ?? 0),
|
||||
'enquiryId' => $enquiryId,
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildCreateQuotePayload(array $detail, array $input): array
|
||||
{
|
||||
$vehicle = $detail['vehicle'] ?? [];
|
||||
$coverages = $input['coverages']
|
||||
?? (is_array($detail['coverage_details']) ? $detail['coverage_details'] : $this->defaultCoverages([]));
|
||||
|
||||
$plate = $vehicle['license_plate_number'] ?? '';
|
||||
$authority = $vehicle['registration_authority'] ?? (strlen($plate) >= 4 ? substr($plate, 0, 4) : '');
|
||||
|
||||
$startDate = $input['start_date'] ?? $detail['start_date'];
|
||||
$endDate = $input['end_date'] ?? $detail['end_date'];
|
||||
|
||||
$payload = [
|
||||
'enquiryId' => $detail['enquiry_id'],
|
||||
'contract' => [
|
||||
'insuranceProductCode' => (string) ($detail['insurance_product_code'] ?? '20101'),
|
||||
'subInsuranceProductCode' => (string) ($detail['sub_insurance_product_code'] ?? 'PB'),
|
||||
'startDate' => $startDate,
|
||||
'endDate' => $endDate,
|
||||
'policyHolderType' => $detail['policy_holder_type'] ?? 'INDIVIDUAL',
|
||||
'externalPolicyNumber' => $detail['external_policy_number'],
|
||||
'isNCBTransfer' => !empty($detail['is_ncb_transfer']) ? true : null,
|
||||
'coverages' => $coverages,
|
||||
],
|
||||
'vehicle' => [
|
||||
'isVehicleNew' => !empty($vehicle['is_vehicle_new']) ? 'false' : 'false',
|
||||
'vehicleMaincode' => $vehicle['vehicle_maincode'] ?? '',
|
||||
'licensePlateNumber' => $plate,
|
||||
'vehicleIdentificationNumber' => $vehicle['vehicle_identification_number'] ?? '',
|
||||
'registrationAuthority' => $authority,
|
||||
'engineNumber' => $vehicle['engine_number'] ?? '',
|
||||
'manufactureDate' => $vehicle['manufacture_date'] ?? null,
|
||||
'registrationDate' => $vehicle['registration_date'] ?? null,
|
||||
'vehicleIDV' => [
|
||||
'idv' => (float) ($vehicle['idv'] ?? $detail['idv'] ?? 0),
|
||||
],
|
||||
'usageType' => null,
|
||||
'permitType' => null,
|
||||
'motorType' => null,
|
||||
],
|
||||
'hypothecation' => [
|
||||
'isHypothecation' => false,
|
||||
'hypothecationAgency' => '',
|
||||
'hypothecationCIty' => '',
|
||||
],
|
||||
'previousInsurer' => [
|
||||
'isPreviousInsurerKnown' => !empty($detail['previous_insurer_code']),
|
||||
'previousInsurerCode' => isset($detail['previous_insurer_code']) ? (string) $detail['previous_insurer_code'] : '',
|
||||
'previousPolicyNumber' => $detail['external_policy_number'] ?? '',
|
||||
'previousPolicyExpiryDate' => $detail['previous_policy_expiry_date'] ?? null,
|
||||
'isClaimInLastYear' => $input['is_claim_in_last_year'] ?? 'false',
|
||||
'originalPreviousPolicyType' => $input['previous_policy_type'] ?? '1OD_1TP',
|
||||
'previousPolicyType' => $input['previous_policy_type'] ?? '1OD_1TP',
|
||||
'previousNoClaimBonus' => $input['previous_ncb'] ?? 'ZERO',
|
||||
'currentThirdPartyPolicy' => null,
|
||||
],
|
||||
'pospInfo' => [
|
||||
'isPOSP' => 'false',
|
||||
'pospName' => null,
|
||||
'pospUniqueNumber' => null,
|
||||
'pospLocation' => '',
|
||||
'pospPanNumber' => null,
|
||||
'pospAadhaarNumber' => null,
|
||||
'pospContactNumber' => null,
|
||||
],
|
||||
'persons' => $input['persons'] ?? [$this->defaultPerson($input, $detail)],
|
||||
'dealer' => [
|
||||
'dealerName' => '',
|
||||
'city' => '',
|
||||
'deliveryDate' => null,
|
||||
],
|
||||
'motorQuestions' => [
|
||||
'furtherAgreement' => '',
|
||||
'selfInspection' => false,
|
||||
'financer' => '',
|
||||
],
|
||||
'motorBreakIn' => [
|
||||
'isBreakin' => false,
|
||||
'breakinExcess' => null,
|
||||
'breakinComments' => null,
|
||||
'isPreInspectionWaived' => false,
|
||||
'isPreInspectionCompleted' => null,
|
||||
'isDocumentUploaded' => null,
|
||||
],
|
||||
'kyc' => $input['kyc'] ?? [
|
||||
'isKYCDone' => !empty($input['kyc_id']),
|
||||
'ckycReferenceDocId' => $input['ckyc_doc_id'] ?? 'D07',
|
||||
'ckycReferenceNumber' => $input['ckyc_reference'] ?? ($input['pan'] ?? ''),
|
||||
'dateOfBirth' => $input['dob'] ?? null,
|
||||
'photo' => '',
|
||||
],
|
||||
'nominee' => $input['nominee'] ?? [
|
||||
'firstName' => $input['nominee_first_name'] ?? 'NOMINEE',
|
||||
'middleName' => '',
|
||||
'lastName' => $input['nominee_last_name'] ?? '',
|
||||
'dateOfBirth' => $input['nominee_dob'] ?? '1990-01-01',
|
||||
'relation' => $input['nominee_relation'] ?? 'OTHER',
|
||||
'personType' => 'INDIVIDUAL',
|
||||
],
|
||||
];
|
||||
|
||||
// Fix isVehicleNew flag properly
|
||||
$payload['vehicle']['isVehicleNew'] = !empty($vehicle['is_vehicle_new']) ? 'true' : 'false';
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
protected function defaultPerson(array $input, array $detail): array
|
||||
{
|
||||
$pincode = $detail['pincode'] ?? ($input['pincode'] ?? '');
|
||||
return [
|
||||
'personType' => 'INDIVIDUAL',
|
||||
'addresses' => [[
|
||||
'addressType' => 'PRIMARY_RESIDENCE',
|
||||
'flatNumber' => null,
|
||||
'streetNumber' => null,
|
||||
'street' => $input['address'] ?? '',
|
||||
'district' => '',
|
||||
'state' => (string) ($input['state_code'] ?? ''),
|
||||
'city' => $input['city'] ?? '',
|
||||
'country' => 'IN',
|
||||
'pincode' => (string) $pincode,
|
||||
]],
|
||||
'communications' => [
|
||||
[
|
||||
'communicationType' => 'MOBILE',
|
||||
'communicationId' => $input['mobile'] ?? '9999999999',
|
||||
'isPrefferedCommunication' => true,
|
||||
],
|
||||
[
|
||||
'communicationType' => 'EMAIL',
|
||||
'communicationId' => $input['email'] ?? 'noreply@nhance.local',
|
||||
'isPrefferedCommunication' => true,
|
||||
],
|
||||
],
|
||||
'identificationDocuments' => [
|
||||
[
|
||||
'issuingPlace' => 'IN',
|
||||
'documentType' => 'PAN_CARD',
|
||||
'documentId' => $input['pan'] ?? '',
|
||||
],
|
||||
],
|
||||
'isPolicyHolder' => true,
|
||||
'isVehicleOwner' => true,
|
||||
'firstName' => $input['first_name'] ?? 'CUSTOMER',
|
||||
'middleName' => null,
|
||||
'lastName' => $input['last_name'] ?? '',
|
||||
'dateOfBirth' => $input['dob'] ?? '1990-01-01',
|
||||
'gender' => $input['gender'] ?? 'MALE',
|
||||
'isDriver' => true,
|
||||
'isInsuredPerson' => true,
|
||||
];
|
||||
}
|
||||
|
||||
protected function defaultCoverages(array $overrides): array
|
||||
{
|
||||
$base = [
|
||||
'isIMT23' => false,
|
||||
'personalAccident' => [
|
||||
'selection' => !empty($overrides['personal_accident'] ?? true),
|
||||
'insuredAmount' => 1500000,
|
||||
'coverTerm' => 1,
|
||||
],
|
||||
'addons' => [
|
||||
'personalBelonging' => ['selection' => !empty($overrides['personal_belonging'])],
|
||||
'returnToInvoice' => ['selection' => !empty($overrides['return_to_invoice'])],
|
||||
'rimProtection' => ['selection' => !empty($overrides['rim_protection'])],
|
||||
'consumables' => ['selection' => !empty($overrides['consumables'])],
|
||||
'partsDepreciation' => [
|
||||
'selection' => !empty($overrides['zero_dep'] ?? $overrides['parts_depreciation']),
|
||||
'claimsCovered' => $overrides['claims_covered'] ?? 'TWO',
|
||||
],
|
||||
'engineProtection' => ['selection' => !empty($overrides['engine_protect'])],
|
||||
'tyreProtection' => ['selection' => !empty($overrides['tyre_protect'])],
|
||||
'roadSideAssistance' => ['selection' => !empty($overrides['rsa'] ?? $overrides['roadside_assistance'])],
|
||||
'keyAndLockProtect' => ['selection' => !empty($overrides['key_protect'])],
|
||||
],
|
||||
'accessories' => [
|
||||
'electrical' => ['selection' => false, 'insuredAmount' => 0],
|
||||
'nonElectrical' => ['selection' => false, 'insuredAmount' => 0],
|
||||
'cng' => ['selection' => false, 'insuredAmount' => 0],
|
||||
],
|
||||
'voluntaryDeductible' => $overrides['voluntary_deductible'] ?? 'ZERO',
|
||||
'isGeoExt' => null,
|
||||
'legalLiability' => [
|
||||
'nonFarePaxLL' => ['selection' => false, 'insuredCount' => 1],
|
||||
'unnamedPaxLL' => ['selection' => false, 'insuredCount' => 0],
|
||||
'workersCompensationLL' => ['selection' => false, 'insuredCount' => 1],
|
||||
'paidDriverLL' => ['selection' => true, 'insuredCount' => 1],
|
||||
'employeesLL' => ['selection' => false, 'insuredCount' => 1],
|
||||
'cleanersLL' => ['selection' => false, 'insuredCount' => 1],
|
||||
],
|
||||
'thirdPartyLiability' => [
|
||||
'isTPPD' => array_key_exists('tppd', $overrides) ? (bool) $overrides['tppd'] : true,
|
||||
],
|
||||
'unnamedPA' => [
|
||||
'unnamedPaidDriver' => ['selection' => false, 'insuredAmount' => 100000, 'insuredCount' => 1],
|
||||
'unnamedPax' => ['selection' => false, 'insuredAmount' => 0, 'insuredCount' => 4],
|
||||
'unnamedConductor' => ['selection' => false, 'insuredAmount' => 100000, 'insuredCount' => 1],
|
||||
'unnamedHirer' => ['selection' => false, 'insuredAmount' => 100000, 'insuredCount' => 1],
|
||||
'unnamedPillionRider'=> ['selection' => false, 'insuredAmount' => 100000, 'insuredCount' => 1],
|
||||
'unnamedCleaner' => ['selection' => false, 'insuredAmount' => 100000, 'insuredCount' => 1],
|
||||
],
|
||||
'isOverturningExclusionIMT47' => false,
|
||||
'isTheftAndConversionRiskIMT43' => false,
|
||||
'ownDamage' => [
|
||||
'discount' => [
|
||||
'userSpecialDiscountPercent' => 0,
|
||||
'discounts' => [],
|
||||
],
|
||||
'surcharge' => [
|
||||
'loadings' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// If caller passed a Digit-shaped coverages object already, prefer it
|
||||
if (isset($overrides['addons']) || isset($overrides['thirdPartyLiability'])) {
|
||||
return array_replace_recursive($base, $overrides);
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
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 (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];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
20
app/Models/MotorApiLogModel.php
Normal file
20
app/Models/MotorApiLogModel.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorApiLogModel extends Model
|
||||
{
|
||||
protected $table = 'motor_api_log';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'quote_id', 'integration_id', 'endpoint', 'request_body', 'response_body',
|
||||
'http_status', 'error_code', 'duration_ms', 'created_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
21
app/Models/MotorKycModel.php
Normal file
21
app/Models/MotorKycModel.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorKycModel extends Model
|
||||
{
|
||||
protected $table = 'motor_kyc';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'quote_id', 'kyc_id', 'kyc_verification_status', 'reference_id', 'link',
|
||||
'mismatch_type', 'id_verification_doc_type', 'address_verification_doc_type',
|
||||
'mode', 'checked_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
21
app/Models/MotorPaymentModel.php
Normal file
21
app/Models/MotorPaymentModel.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorPaymentModel extends Model
|
||||
{
|
||||
protected $table = 'motor_payment';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'quote_id', 'application_id', 'digit_payment_id', 'request_reference',
|
||||
'payment_mode', 'cancel_return_url', 'success_return_url',
|
||||
'dispatcher_response', 'premium', 'payment_status', 'created_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
20
app/Models/MotorPolicyModel.php
Normal file
20
app/Models/MotorPolicyModel.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorPolicyModel extends Model
|
||||
{
|
||||
protected $table = 'motor_policy';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'quote_id', 'policy_number', 'policy_status', 'schedule_path',
|
||||
'proposal_path', 'response_code', 'response_message', 'updated_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
92
app/Models/MotorQuoteModel.php
Normal file
92
app/Models/MotorQuoteModel.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorQuoteModel extends Model
|
||||
{
|
||||
protected $table = 'motor_quote';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'enquiry_id', 'quote_number', 'application_id', 'policy_holder_type',
|
||||
'insurance_product_code', 'sub_insurance_product_code',
|
||||
'previous_insurer_code', 'previous_policy_expiry_date', 'external_policy_number',
|
||||
'is_ncb_transfer', 'start_date', 'end_date', 'pincode',
|
||||
'coverage_details', 'premium', 'idv', 'status',
|
||||
'created_by', 'updated_by', 'created_at', 'updated_at',
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = ['setCreatedBy'];
|
||||
protected $beforeUpdate = ['setUpdatedBy'];
|
||||
|
||||
protected function setCreatedBy(array $data): array
|
||||
{
|
||||
if (function_exists('get_session_userid')) {
|
||||
$data['data']['created_by'] = get_session_userid();
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function setUpdatedBy(array $data): array
|
||||
{
|
||||
if (function_exists('get_session_userid')) {
|
||||
$data['data']['updated_by'] = get_session_userid();
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getListWithVehicle(array $filters = []): array
|
||||
{
|
||||
$builder = $this->db->table('motor_quote q')
|
||||
->select('q.*, v.license_plate_number, v.vehicle_maincode, v.registration_date')
|
||||
->join('motor_vehicle v', 'v.quote_id = q.id', 'left')
|
||||
->orderBy('q.id', 'DESC');
|
||||
|
||||
if (!empty($filters['status'])) {
|
||||
$builder->where('q.status', $filters['status']);
|
||||
}
|
||||
if (!empty($filters['enquiry_id'])) {
|
||||
$builder->like('q.enquiry_id', $filters['enquiry_id']);
|
||||
}
|
||||
if (!empty($filters['quote_number'])) {
|
||||
$builder->like('q.quote_number', $filters['quote_number']);
|
||||
}
|
||||
if (!empty($filters['license_plate'])) {
|
||||
$builder->like('v.license_plate_number', $filters['license_plate']);
|
||||
}
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getDetail(int $quoteId): ?array
|
||||
{
|
||||
$quote = $this->find($quoteId);
|
||||
if (!$quote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$vehicleModel = new MotorVehicleModel();
|
||||
$kycModel = new MotorKycModel();
|
||||
$paymentModel = new MotorPaymentModel();
|
||||
$policyModel = new MotorPolicyModel();
|
||||
|
||||
$quote['vehicle'] = $vehicleModel->where('quote_id', $quoteId)->first();
|
||||
$quote['kyc'] = $kycModel->where('quote_id', $quoteId)->orderBy('id', 'DESC')->first();
|
||||
$quote['payment'] = $paymentModel->where('quote_id', $quoteId)->orderBy('id', 'DESC')->first();
|
||||
$quote['policy'] = $policyModel->where('quote_id', $quoteId)->first();
|
||||
|
||||
if (!empty($quote['coverage_details']) && is_string($quote['coverage_details'])) {
|
||||
$quote['coverage_details'] = json_decode($quote['coverage_details'], true);
|
||||
}
|
||||
|
||||
return $quote;
|
||||
}
|
||||
}
|
||||
41
app/Models/MotorTokenModel.php
Normal file
41
app/Models/MotorTokenModel.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorTokenModel extends Model
|
||||
{
|
||||
protected $table = 'motor_token';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'environment', 'access_token', 'refresh_token', 'expires_at', 'created_at',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
|
||||
public function getByEnvironment(string $environment): ?array
|
||||
{
|
||||
return $this->where('environment', $environment)->first();
|
||||
}
|
||||
|
||||
public function upsertToken(string $environment, string $accessToken, ?string $refreshToken, string $expiresAt): void
|
||||
{
|
||||
$existing = $this->getByEnvironment($environment);
|
||||
$row = [
|
||||
'environment' => $environment,
|
||||
'access_token' => $accessToken,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_at' => $expiresAt,
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$this->update($existing['id'], $row);
|
||||
} else {
|
||||
$this->insert($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
app/Models/MotorVehicleModel.php
Normal file
22
app/Models/MotorVehicleModel.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class MotorVehicleModel extends Model
|
||||
{
|
||||
protected $table = 'motor_vehicle';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'quote_id', 'is_vehicle_new', 'vehicle_maincode', 'license_plate_number',
|
||||
'vehicle_identification_number', 'engine_number', 'manufacture_date',
|
||||
'registration_date', 'registration_authority',
|
||||
'idv', 'default_idv', 'minimum_idv', 'maximum_idv',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
672
app/Views/digit_motor/journey.php
Normal file
672
app/Views/digit_motor/journey.php
Normal file
@ -0,0 +1,672 @@
|
||||
<?php
|
||||
$quote = $quote ?? null;
|
||||
$quoteId = $quote_id ?? ($quote['id'] ?? null);
|
||||
$vehicle = $quote['vehicle'] ?? [];
|
||||
$kyc = $quote['kyc'] ?? [];
|
||||
$payment = $quote['payment'] ?? [];
|
||||
$policy = $quote['policy'] ?? [];
|
||||
$status = $quote['status'] ?? 'DRAFT';
|
||||
|
||||
$statusToStep = [
|
||||
'DRAFT' => 0,
|
||||
'QUOTED' => 1,
|
||||
'CREATED' => 2,
|
||||
'KYC_DONE' => 3,
|
||||
'PAID' => 4,
|
||||
'EFFECTIVE' => 4,
|
||||
];
|
||||
$initialStep = $statusToStep[$status] ?? 0;
|
||||
$coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_details'] : [];
|
||||
?>
|
||||
<style>
|
||||
:root{
|
||||
--dm-teal:#02a8b5;
|
||||
--dm-teal-dark:#028a95;
|
||||
--dm-teal-soft:#e7f6f7;
|
||||
--dm-ink:#1B1D23;
|
||||
--dm-muted:#6B6A63;
|
||||
--dm-line:#DEDBD2;
|
||||
--dm-paper:#F7F5F0;
|
||||
--dm-green:#1F7A4D;
|
||||
--dm-green-bg:#E4F2EA;
|
||||
--dm-steel:#35577A;
|
||||
--dm-steel-bg:#E7EEF4;
|
||||
--dm-red:#C43D3D;
|
||||
--dm-red-bg:#FBEAEA;
|
||||
}
|
||||
.dm-page{width:100%;padding:24px 8px 24px;}
|
||||
.dm-toolbar{
|
||||
display:flex;justify-content:space-between;align-items:center;
|
||||
margin:12px 0 16px;padding-top:8px;gap:12px;flex-wrap:wrap;
|
||||
position:relative;z-index:2;
|
||||
}
|
||||
.dm-back{
|
||||
display:inline-flex;align-items:center;gap:6px;
|
||||
color:var(--dm-teal-dark)!important;font-weight:600;font-size:14px;text-decoration:none!important;
|
||||
}
|
||||
.dm-back:hover{color:var(--dm-teal)!important;}
|
||||
.dm-enquiry{font-size:12px;color:var(--dm-muted);}
|
||||
.dm-enquiry strong{
|
||||
display:inline-block;margin-left:6px;font-family:monospace;font-size:13px;color:var(--dm-teal-dark);font-weight:600;
|
||||
}
|
||||
|
||||
/* Progress stepper — compact */
|
||||
.dm-route-wrap{
|
||||
background:linear-gradient(135deg,#026a72 0%,#02a8b5 100%);
|
||||
padding:12px 20px 14px;border-radius:10px;margin-bottom:14px;
|
||||
}
|
||||
.dm-route{position:relative;height:48px;width:100%;}
|
||||
.dm-road{
|
||||
position:absolute;top:14px;left:28px;right:28px;height:3px;
|
||||
background:rgba(255,255,255,.22);border-radius:2px;z-index:1;
|
||||
}
|
||||
.dm-fill{
|
||||
position:absolute;top:14px;left:28px;height:3px;
|
||||
background:#fff;border-radius:2px;width:0;z-index:1;
|
||||
transition:width .45s ease, left .45s ease;
|
||||
}
|
||||
.dm-stops{
|
||||
position:absolute;top:0;left:0;right:0;bottom:0;
|
||||
display:flex;justify-content:space-between;align-items:flex-start;z-index:2;
|
||||
}
|
||||
.dm-stop{flex:0 0 auto;width:64px;text-align:center;cursor:pointer;}
|
||||
.dm-dot{
|
||||
width:22px;height:22px;border-radius:50%;
|
||||
background:#026a72;border:2px solid rgba(255,255,255,.55);
|
||||
margin:4px auto 0;display:flex;align-items:center;justify-content:center;
|
||||
color:rgba(255,255,255,.7);font-size:11px;transition:all .2s;position:relative;z-index:3;
|
||||
}
|
||||
.dm-stop.done .dm-dot{background:#fff;border-color:#fff;color:var(--dm-teal-dark);}
|
||||
.dm-stop.active .dm-dot{
|
||||
background:#fff;border-color:#fff;color:var(--dm-teal-dark);
|
||||
box-shadow:0 0 0 3px rgba(255,255,255,.28);
|
||||
}
|
||||
.dm-stop-label{
|
||||
margin-top:4px;font-size:11px;color:rgba(255,255,255,.7);white-space:nowrap;font-weight:500;line-height:1.2;
|
||||
}
|
||||
.dm-stop.active .dm-stop-label,.dm-stop.done .dm-stop-label{color:#fff;font-weight:600;}
|
||||
|
||||
.dm-panel{
|
||||
background:#fff;border:1px solid var(--dm-line);border-radius:12px;padding:28px 28px 24px;position:relative;
|
||||
box-shadow:0 4px 16px rgba(2,168,181,.06);
|
||||
}
|
||||
.dm-panel-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:20px;}
|
||||
.dm-eyebrow{font-size:11px;color:var(--dm-teal-dark);text-transform:uppercase;letter-spacing:.08em;font-weight:600;margin-bottom:6px;}
|
||||
.dm-title{font-size:20px;font-weight:600;color:var(--dm-ink);}
|
||||
.dm-desc{font-size:13px;color:var(--dm-muted);margin-top:6px;max-width:640px;line-height:1.5;}
|
||||
.dm-index{font-family:monospace;font-size:32px;color:#d9ecee;font-weight:500;}
|
||||
.dm-step{display:none;}
|
||||
.dm-step.active{display:block;}
|
||||
|
||||
.dm-section{
|
||||
font-size:11px;color:var(--dm-muted);text-transform:uppercase;letter-spacing:.06em;font-weight:600;
|
||||
margin:22px 0 12px;padding-top:16px;border-top:1px solid var(--dm-line);
|
||||
}
|
||||
.dm-section:first-of-type{margin-top:0;padding-top:0;border-top:none;}
|
||||
.dm-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px 16px;}
|
||||
.dm-grid.g3{grid-template-columns:repeat(3,1fr);}
|
||||
.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 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:focus,.dm-field select:focus{border-color:var(--dm-teal);background:#fff;}
|
||||
|
||||
.dm-chips{display:flex;gap:8px;flex-wrap:wrap;}
|
||||
.dm-chip{
|
||||
padding:7px 14px;border:1px solid var(--dm-line);border-radius:20px;font-size:12.5px;cursor:pointer;
|
||||
color:var(--dm-muted);user-select:none;background:#fff;transition:all .15s;
|
||||
}
|
||||
.dm-chip.on{background:var(--dm-teal);border-color:var(--dm-teal);color:#fff;}
|
||||
|
||||
.dm-actions{display:flex;justify-content:space-between;align-items:center;margin-top:26px;padding-top:18px;border-top:1px solid var(--dm-line);}
|
||||
.dm-btn{
|
||||
height:40px;padding:0 18px;border-radius:8px;font-size:13.5px;font-weight:500;cursor:pointer;
|
||||
border:1px solid var(--dm-line);background:#fff;color:var(--dm-ink);display:inline-flex;align-items:center;gap:6px;
|
||||
}
|
||||
.dm-btn.primary{background:var(--dm-teal);border-color:var(--dm-teal);color:#fff;}
|
||||
.dm-btn.primary:hover{background:var(--dm-teal-dark);}
|
||||
.dm-btn.ghost{border-color:transparent;color:var(--dm-muted);}
|
||||
.dm-btn:disabled{opacity:.45;cursor:not-allowed;}
|
||||
|
||||
.dm-quote-cards{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
||||
.dm-quote-card{border:1px solid var(--dm-line);border-radius:12px;padding:18px;cursor:pointer;position:relative;}
|
||||
.dm-quote-card.selected{border:2px solid var(--dm-teal);padding:17px;background:var(--dm-teal-soft);}
|
||||
.dm-quote-card .tag{font-size:11px;color:var(--dm-muted);text-transform:uppercase;letter-spacing:.05em;}
|
||||
.dm-quote-card .amount{font-size:24px;font-weight:600;margin:8px 0 2px;color:var(--dm-ink);}
|
||||
.dm-quote-card .idv{font-size:12px;color:var(--dm-muted);}
|
||||
.dm-check{
|
||||
position:absolute;top:14px;right:14px;width:20px;height:20px;border-radius:50%;
|
||||
background:var(--dm-teal);color:#fff;display:none;align-items:center;justify-content:center;font-size:11px;
|
||||
}
|
||||
.dm-quote-card.selected .dm-check{display:flex;}
|
||||
|
||||
.dm-status{border:1px solid var(--dm-line);border-radius:12px;padding:18px;display:flex;align-items:center;gap:14px;}
|
||||
.dm-icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;}
|
||||
.dm-icon.ok{background:var(--dm-green-bg);color:var(--dm-green);}
|
||||
.dm-icon.wait{background:var(--dm-steel-bg);color:var(--dm-steel);}
|
||||
.dm-icon.warn{background:var(--dm-red-bg);color:var(--dm-red);}
|
||||
.dm-st-title{font-weight:600;font-size:14.5px;}
|
||||
.dm-st-sub{font-size:12.5px;color:var(--dm-muted);margin-top:3px;}
|
||||
.dm-pill{display:inline-block;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600;}
|
||||
.dm-pill.ok{background:var(--dm-green-bg);color:var(--dm-green);}
|
||||
.dm-pill.wait{background:var(--dm-steel-bg);color:var(--dm-steel);}
|
||||
.dm-ref{width:100%;border-collapse:collapse;margin-top:16px;}
|
||||
.dm-ref tr{border-top:1px solid var(--dm-line);}
|
||||
.dm-ref tr:first-child{border-top:none;}
|
||||
.dm-ref td{padding:10px 0;font-size:13px;}
|
||||
.dm-ref td:first-child{color:var(--dm-muted);width:44%;}
|
||||
.dm-ref td:last-child{text-align:right;font-family:monospace;}
|
||||
.dm-doc{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--dm-line);border-radius:10px;padding:12px 14px;margin-top:10px;}
|
||||
.dm-alert{display:none;margin-bottom:14px;padding:10px 14px;border-radius:8px;font-size:13px;}
|
||||
.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;}
|
||||
@media(max-width:768px){
|
||||
.dm-grid,.dm-grid.g3,.dm-quote-cards{grid-template-columns:1fr;}
|
||||
.dm-stop{width:52px;}
|
||||
.dm-stop-label{font-size:10px;}
|
||||
.dm-road{left:22px;right:22px;}
|
||||
.dm-route{height:44px;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid dm-page">
|
||||
<div class="dm-toolbar">
|
||||
<a href="<?= base_url('digit-motor/list') ?>" class="dm-back">
|
||||
<i class="mdi mdi-arrow-left"></i> Back to list
|
||||
</a>
|
||||
<div class="dm-enquiry">Enquiry ID <strong id="enquiryIdTag"><?= esc($quote['enquiry_id'] ?? '— new —') ?></strong></div>
|
||||
</div>
|
||||
|
||||
<div class="dm-route-wrap">
|
||||
<div class="dm-route" id="dmRoute">
|
||||
<div class="dm-road"></div>
|
||||
<div class="dm-fill" id="dmFill"></div>
|
||||
<div class="dm-stops" id="dmStops"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-panel" id="dmPanel">
|
||||
<div id="dmAlert" class="dm-alert"></div>
|
||||
|
||||
<!-- Step 0: Quick Quote -->
|
||||
<div class="dm-step active" data-step="0">
|
||||
<div class="dm-panel-head">
|
||||
<div>
|
||||
<div class="dm-eyebrow">Step 1 of 5 · quick quote</div>
|
||||
<div class="dm-title">Vehicle and coverage details</div>
|
||||
<div class="dm-desc">Enter the vehicle to fetch an indicative premium before creating the policy quote.</div>
|
||||
</div>
|
||||
<div class="dm-index">01</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>Prior policy expiry</label><input type="date" id="f_prev_exp" value="<?= esc($quote['previous_policy_expiry_date'] ?? '') ?>"></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>
|
||||
<div class="dm-chip on" data-group="claim" data-val="0" onclick="dmToggleExclusive(this)">No</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-section">Coverage add-ons</div>
|
||||
<div class="dm-chips" id="addonChips">
|
||||
<div class="dm-chip on" data-key="personal_accident" onclick="this.classList.toggle('on')">Personal accident</div>
|
||||
<div class="dm-chip" data-key="zero_dep" onclick="this.classList.toggle('on')">Zero depreciation</div>
|
||||
<div class="dm-chip" data-key="engine_protect" onclick="this.classList.toggle('on')">Engine protect</div>
|
||||
<div class="dm-chip" data-key="rsa" onclick="this.classList.toggle('on')">Roadside assistance</div>
|
||||
<div class="dm-chip" data-key="consumables" onclick="this.classList.toggle('on')">Consumables</div>
|
||||
<div class="dm-chip" data-key="key_protect" onclick="this.classList.toggle('on')">Key & lock</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-actions">
|
||||
<span style="font-size:11.5px;color:var(--dm-muted);font-family:monospace;">POST · motorQuickQuote</span>
|
||||
<button class="dm-btn primary" type="button" onclick="dmQuickQuote()">Get quote <i class="mdi mdi-arrow-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Create Quote -->
|
||||
<div class="dm-step" data-step="1">
|
||||
<div class="dm-panel-head">
|
||||
<div>
|
||||
<div class="dm-eyebrow">Step 2 of 5 · create quote</div>
|
||||
<div class="dm-title">Confirm premium plan</div>
|
||||
<div class="dm-desc">Review the quick-quote premium, add policyholder details, then create the Digit quote.</div>
|
||||
</div>
|
||||
<div class="dm-index">02</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-quote-cards">
|
||||
<div class="dm-quote-card selected" id="planCard">
|
||||
<div class="dm-check"><i class="mdi mdi-check"></i></div>
|
||||
<div class="tag">Quoted premium</div>
|
||||
<div class="amount" id="planPremium">₹<?= isset($quote['premium']) ? number_format((float)$quote['premium'], 0) : '—' ?></div>
|
||||
<div class="idv" id="planIdv">IDV ₹<?= isset($quote['idv']) ? number_format((float)$quote['idv'], 0) : '—' ?></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>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>
|
||||
|
||||
<div class="dm-actions">
|
||||
<button class="dm-btn ghost" type="button" onclick="dmGo(0)"><i class="mdi mdi-arrow-left"></i> Back</button>
|
||||
<button class="dm-btn primary" type="button" onclick="dmCreateQuote()">Create quote <i class="mdi mdi-arrow-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: KYC -->
|
||||
<div class="dm-step" data-step="2">
|
||||
<div class="dm-panel-head">
|
||||
<div>
|
||||
<div class="dm-eyebrow">Step 3 of 5 · KYC status</div>
|
||||
<div class="dm-title">Identity verification</div>
|
||||
<div class="dm-desc">Poll KYC status before the payment link can be issued.</div>
|
||||
</div>
|
||||
<div class="dm-index">03</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-status" id="kycStatusCard">
|
||||
<div class="dm-icon wait"><i class="mdi mdi-shield-check"></i></div>
|
||||
<div>
|
||||
<div class="dm-st-title" id="kycTitle"><?= esc($kyc['kyc_verification_status'] ?? 'Not checked') ?></div>
|
||||
<div class="dm-st-sub" id="kycSub">Click refresh to fetch latest KYC status from Digit.</div>
|
||||
</div>
|
||||
<div class="ml-auto"><span class="dm-pill wait" id="kycPill"><?= esc($kyc['kyc_verification_status'] ?? 'Pending') ?></span></div>
|
||||
</div>
|
||||
|
||||
<table class="dm-ref">
|
||||
<tr><td>Quote number</td><td id="refQuoteNo"><?= esc($quote['quote_number'] ?? '—') ?></td></tr>
|
||||
<tr><td>Application ID</td><td id="refAppId"><?= esc($quote['application_id'] ?? '—') ?></td></tr>
|
||||
<tr><td>KYC reference</td><td id="refKycRef"><?= esc($kyc['reference_id'] ?? '—') ?></td></tr>
|
||||
<tr><td>OVD link</td><td id="refKycLink"><?= !empty($kyc['link']) ? '<a href="'.esc($kyc['link']).'" target="_blank">Open</a>' : '—' ?></td></tr>
|
||||
</table>
|
||||
|
||||
<div class="dm-actions">
|
||||
<button class="dm-btn ghost" type="button" onclick="dmGo(1)"><i class="mdi mdi-arrow-left"></i> Back</button>
|
||||
<div>
|
||||
<button class="dm-btn" type="button" onclick="dmKycStatus()"><i class="mdi mdi-refresh"></i> Refresh KYC</button>
|
||||
<button class="dm-btn primary" type="button" onclick="dmGo(3)">Proceed to payment <i class="mdi mdi-arrow-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Payment -->
|
||||
<div class="dm-step" data-step="3">
|
||||
<div class="dm-panel-head">
|
||||
<div>
|
||||
<div class="dm-eyebrow">Step 4 of 5 · payment</div>
|
||||
<div class="dm-title">Collect premium</div>
|
||||
<div class="dm-desc">Generates a Digit UI payment link for the customer to complete the transaction.</div>
|
||||
</div>
|
||||
<div class="dm-index">04</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-grid">
|
||||
<div class="dm-field"><label>Premium amount</label><input id="payPremium" readonly value="<?= isset($quote['premium']) ? 'INR '.number_format((float)$quote['premium'], 2, '.', '') : '' ?>"></div>
|
||||
<div class="dm-field"><label>Payment mode</label><input value="EB · Digit UI" readonly></div>
|
||||
</div>
|
||||
|
||||
<div class="dm-status" style="margin-top:18px;" id="payStatusCard">
|
||||
<div class="dm-icon wait"><i class="mdi mdi-link-variant"></i></div>
|
||||
<div>
|
||||
<div class="dm-st-title" id="payTitle"><?= !empty($payment['dispatcher_response']) ? 'Payment link ready' : 'No payment link yet' ?></div>
|
||||
<div class="dm-st-sub" id="paySub"><?= esc($payment['digit_payment_id'] ?? 'Generate a link to continue') ?></div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<?php if (!empty($payment['dispatcher_response'])): ?>
|
||||
<a class="dm-btn" id="payOpenBtn" href="<?= esc($payment['dispatcher_response']) ?>" target="_blank">Open link <i class="mdi mdi-open-in-new"></i></a>
|
||||
<?php else: ?>
|
||||
<a class="dm-btn" id="payOpenBtn" href="#" style="display:none;" target="_blank">Open link</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-actions">
|
||||
<button class="dm-btn ghost" type="button" onclick="dmGo(2)"><i class="mdi mdi-arrow-left"></i> Back</button>
|
||||
<div>
|
||||
<button class="dm-btn" type="button" onclick="dmPaymentLink()"><i class="mdi mdi-link-variant"></i> Generate link</button>
|
||||
<button class="dm-btn primary" type="button" onclick="dmPolicyStatus(); dmGo(4);">Check policy status <i class="mdi mdi-arrow-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Policy -->
|
||||
<div class="dm-step" data-step="4">
|
||||
<div class="dm-panel-head">
|
||||
<div>
|
||||
<div class="dm-eyebrow">Step 5 of 5 · policy</div>
|
||||
<div class="dm-title">Policy status & documents</div>
|
||||
<div class="dm-desc">Confirm payment outcome, then download schedule and proposal PDFs.</div>
|
||||
</div>
|
||||
<div class="dm-index">05</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-status" id="policyStatusCard">
|
||||
<div class="dm-icon <?= in_array(strtoupper($policy['policy_status'] ?? $status), ['EFFECTIVE','COMPLETE','COMPLETED']) ? 'ok' : 'wait' ?>">
|
||||
<i class="mdi mdi-file-check"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="dm-st-title" id="polTitle">
|
||||
<?= esc(($policy['policy_number'] ?? $quote['quote_number'] ?? 'Policy') . ' · ' . ($policy['policy_status'] ?? $status)) ?>
|
||||
</div>
|
||||
<div class="dm-st-sub" id="polSub">Refresh status after customer payment, then fetch PDFs.</div>
|
||||
</div>
|
||||
<div class="ml-auto"><span class="dm-pill <?= in_array(strtoupper($policy['policy_status'] ?? ''), ['EFFECTIVE','COMPLETE']) ? 'ok' : 'wait' ?>" id="polPill"><?= esc($policy['policy_status'] ?? $status) ?></span></div>
|
||||
</div>
|
||||
|
||||
<div class="dm-section">Documents</div>
|
||||
<div class="dm-doc">
|
||||
<div><i class="mdi mdi-file-pdf-box text-danger"></i> Policy schedule</div>
|
||||
<a class="dm-btn" id="dlSchedule" href="<?= esc($policy['schedule_path'] ?? '#') ?>" target="_blank" <?= empty($policy['schedule_path']) ? 'style="pointer-events:none;opacity:.4"' : '' ?>>Download</a>
|
||||
</div>
|
||||
<div class="dm-doc">
|
||||
<div><i class="mdi mdi-file-pdf-box text-danger"></i> Proposal form</div>
|
||||
<a class="dm-btn" id="dlProposal" href="<?= esc($policy['proposal_path'] ?? '#') ?>" target="_blank" <?= empty($policy['proposal_path']) ? 'style="pointer-events:none;opacity:.4"' : '' ?>>Download</a>
|
||||
</div>
|
||||
|
||||
<div class="dm-actions">
|
||||
<button class="dm-btn ghost" type="button" onclick="dmGo(3)"><i class="mdi mdi-arrow-left"></i> Back</button>
|
||||
<div>
|
||||
<button class="dm-btn" type="button" onclick="dmPolicyStatus()"><i class="mdi mdi-refresh"></i> Refresh status</button>
|
||||
<button class="dm-btn primary" type="button" onclick="dmPolicyPdf()"><i class="mdi mdi-download"></i> Fetch PDFs</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.DM = {
|
||||
quoteId: <?= $quoteId ? (int)$quoteId : 'null' ?>,
|
||||
base: '<?= rtrim(base_url('digit-motor'), '/') ?>',
|
||||
step: <?= (int)$initialStep ?>
|
||||
};
|
||||
|
||||
const dmSteps = ["Quote","Create","KYC","Pay","Policy"];
|
||||
const dmIcons = ["mdi-car","mdi-file-plus","mdi-shield-check","mdi-credit-card","mdi-file-check"];
|
||||
|
||||
(function initRoute(){
|
||||
const stops = document.getElementById('dmStops');
|
||||
dmSteps.forEach((label,i)=>{
|
||||
const el = document.createElement('div');
|
||||
el.className = 'dm-stop';
|
||||
el.dataset.i = i;
|
||||
el.innerHTML = '<div class="dm-dot"><i class="mdi '+dmIcons[i]+'"></i></div><div class="dm-stop-label">'+label+'</div>';
|
||||
el.onclick = ()=> dmGo(i);
|
||||
stops.appendChild(el);
|
||||
});
|
||||
dmGo(window.DM.step, true);
|
||||
})();
|
||||
|
||||
function dmGo(i, silent){
|
||||
window.DM.step = i;
|
||||
document.querySelectorAll('.dm-step').forEach(v=> v.classList.toggle('active', Number(v.dataset.step)===i));
|
||||
document.querySelectorAll('.dm-stop').forEach(s=>{
|
||||
const n = Number(s.dataset.i);
|
||||
s.classList.toggle('done', n < i);
|
||||
s.classList.toggle('active', n === i);
|
||||
});
|
||||
// Progress fill between stop centers — dots sit above the line (no crossover)
|
||||
const stops = document.querySelectorAll('.dm-stop');
|
||||
const fill = document.getElementById('dmFill');
|
||||
if (fill && stops.length > 1){
|
||||
const first = stops[0].offsetLeft + stops[0].offsetWidth / 2;
|
||||
const current = stops[i].offsetLeft + stops[i].offsetWidth / 2;
|
||||
fill.style.left = first + 'px';
|
||||
fill.style.width = Math.max(0, current - first) + 'px';
|
||||
fill.style.maxWidth = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function dmToggleExclusive(el){
|
||||
const g = el.dataset.group;
|
||||
document.querySelectorAll('.dm-chip[data-group="'+g+'"]').forEach(c=> c.classList.remove('on'));
|
||||
el.classList.add('on');
|
||||
}
|
||||
|
||||
function dmAlert(msg, ok){
|
||||
if (window.toastr) {
|
||||
if (ok) {
|
||||
toastr.success(msg, 'Success');
|
||||
} else {
|
||||
toastr.error(msg, 'Error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Fallback if toastr is unavailable
|
||||
const el = document.getElementById('dmAlert');
|
||||
if (!el) { alert(msg); return; }
|
||||
el.className = 'dm-alert ' + (ok ? 'ok' : 'error');
|
||||
el.textContent = msg;
|
||||
if (ok) setTimeout(function(){ el.className='dm-alert'; el.textContent=''; }, 4000);
|
||||
}
|
||||
|
||||
function dmBusy(on){
|
||||
document.getElementById('dmPanel').classList.toggle('dm-loading', !!on);
|
||||
}
|
||||
|
||||
function dmAddonFlags(){
|
||||
const flags = {};
|
||||
document.querySelectorAll('#addonChips .dm-chip').forEach(c=>{
|
||||
flags[c.dataset.key] = c.classList.contains('on');
|
||||
});
|
||||
return flags;
|
||||
}
|
||||
|
||||
function dmPost(url, body){
|
||||
return $.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify(body || {})
|
||||
});
|
||||
}
|
||||
|
||||
function dmGet(url){
|
||||
return $.ajax({ url: url, type: 'GET', dataType: 'json' });
|
||||
}
|
||||
|
||||
function dmQuickQuote(){
|
||||
const claimChip = document.querySelector('.dm-chip[data-group="claim"].on');
|
||||
const body = {
|
||||
license_plate_number: $('#f_reg').val().trim(),
|
||||
vehicle_maincode: $('#f_vcode').val().trim(),
|
||||
registration_date: $('#f_reg_date').val(),
|
||||
manufacture_date: $('#f_mfg_date').val(),
|
||||
vehicle_identification_number: $('#f_vin').val().trim(),
|
||||
engine_number: $('#f_engine').val().trim(),
|
||||
pincode: $('#f_pin').val().trim(),
|
||||
insurance_product_code: $('#f_product').val(),
|
||||
start_date: $('#f_start').val() || null,
|
||||
end_date: null,
|
||||
previous_insurer_code: $('#f_prev_ins').val() || null,
|
||||
previous_policy_expiry_date: $('#f_prev_exp').val() || null,
|
||||
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
|
||||
};
|
||||
|
||||
if (body.start_date){
|
||||
const d = new Date(body.start_date);
|
||||
d.setFullYear(d.getFullYear()+1);
|
||||
d.setDate(d.getDate()-1);
|
||||
body.end_date = d.toISOString().slice(0,10);
|
||||
}
|
||||
|
||||
dmBusy(true);
|
||||
dmPost(window.DM.base + '/quotes/quick', body)
|
||||
.done(function(res){
|
||||
if (!res.status){ dmAlert(res.message || 'Quick quote failed'); return; }
|
||||
window.DM.quoteId = res.data.quote_id;
|
||||
$('#enquiryIdTag').text(res.data.enquiry_id);
|
||||
const prem = res.data.premium != null ? Number(res.data.premium) : null;
|
||||
const idv = res.data.idv != null ? Number(res.data.idv) : null;
|
||||
$('#planPremium').text(prem != null ? '₹' + prem.toLocaleString('en-IN', {maximumFractionDigits:0}) : '—');
|
||||
$('#planIdv').text(idv != null ? 'IDV ₹' + idv.toLocaleString('en-IN', {maximumFractionDigits:0}) : 'IDV —');
|
||||
$('#payPremium').val(prem != null ? 'INR ' + prem.toFixed(2) : '');
|
||||
dmAlert('Quick quote generated.', true);
|
||||
history.replaceState(null, '', window.DM.base + '/journey/' + window.DM.quoteId);
|
||||
dmGo(1);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message) || 'Quick quote failed.';
|
||||
dmAlert(msg);
|
||||
})
|
||||
.always(function(){ dmBusy(false); });
|
||||
}
|
||||
|
||||
function dmCreateQuote(){
|
||||
if (!window.DM.quoteId){ dmAlert('Run quick quote first.'); return; }
|
||||
const body = {
|
||||
first_name: $('#f_fname').val().trim(),
|
||||
last_name: $('#f_lname').val().trim(),
|
||||
mobile: $('#f_mobile').val().trim(),
|
||||
email: $('#f_email').val().trim(),
|
||||
pan: $('#f_pan').val().trim(),
|
||||
dob: $('#f_dob').val(),
|
||||
address: $('#f_address').val().trim(),
|
||||
start_date: $('#f_start').val() || null,
|
||||
coverages: dmAddonFlags()
|
||||
};
|
||||
if (body.start_date){
|
||||
const d = new Date(body.start_date);
|
||||
d.setFullYear(d.getFullYear()+1);
|
||||
d.setDate(d.getDate()-1);
|
||||
body.end_date = d.toISOString().slice(0,10);
|
||||
}
|
||||
|
||||
dmBusy(true);
|
||||
dmPost(window.DM.base + '/quotes/' + window.DM.quoteId + '/create', body)
|
||||
.done(function(res){
|
||||
if (!res.status){ dmAlert(res.message || 'Create quote failed'); return; }
|
||||
$('#refQuoteNo').text(res.data.quote_number || '—');
|
||||
$('#refAppId').text(res.data.application_id || '—');
|
||||
if (res.data.premium != null){
|
||||
$('#payPremium').val('INR ' + Number(res.data.premium).toFixed(2));
|
||||
$('#planPremium').text('₹' + Number(res.data.premium).toLocaleString('en-IN', {maximumFractionDigits:0}));
|
||||
}
|
||||
dmAlert('Quote created.', true);
|
||||
dmGo(2);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
dmAlert((xhr.responseJSON && xhr.responseJSON.message) || 'Create quote failed.');
|
||||
})
|
||||
.always(function(){ dmBusy(false); });
|
||||
}
|
||||
|
||||
function dmKycStatus(){
|
||||
if (!window.DM.quoteId){ dmAlert('No quote loaded.'); return; }
|
||||
dmBusy(true);
|
||||
dmGet(window.DM.base + '/kyc/' + window.DM.quoteId)
|
||||
.done(function(res){
|
||||
if (!res.status){ dmAlert(res.message || 'KYC fetch failed'); return; }
|
||||
const k = res.data.kyc || {};
|
||||
const st = (res.data.status || k.kyc_verification_status || 'UNKNOWN').toString();
|
||||
$('#kycTitle').text(st);
|
||||
$('#kycPill').text(st).removeClass('ok wait').addClass(/done|verif|success|complete/i.test(st) ? 'ok' : 'wait');
|
||||
$('#refKycRef').text(k.reference_id || '—');
|
||||
$('#refKycLink').html(k.link ? '<a href="'+k.link+'" target="_blank">Open</a>' : '—');
|
||||
$('#kycSub').text('Last checked just now.');
|
||||
dmAlert('KYC status updated.', true);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
dmAlert((xhr.responseJSON && xhr.responseJSON.message) || 'KYC fetch failed.');
|
||||
})
|
||||
.always(function(){ dmBusy(false); });
|
||||
}
|
||||
|
||||
function dmPaymentLink(){
|
||||
if (!window.DM.quoteId){ dmAlert('No quote loaded.'); return; }
|
||||
dmBusy(true);
|
||||
dmPost(window.DM.base + '/payments/' + window.DM.quoteId + '/link', {})
|
||||
.done(function(res){
|
||||
if (!res.status){ dmAlert(res.message || 'Payment link failed'); return; }
|
||||
const link = res.data.dispatcher_response;
|
||||
const pay = res.data.payment || {};
|
||||
$('#payTitle').text(link ? 'Payment link ready' : 'Link missing in response');
|
||||
$('#paySub').text(pay.digit_payment_id || 'digit payment');
|
||||
if (link){
|
||||
$('#payOpenBtn').attr('href', link).show();
|
||||
}
|
||||
dmAlert('Payment link generated.', true);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
dmAlert((xhr.responseJSON && xhr.responseJSON.message) || 'Payment link failed.');
|
||||
})
|
||||
.always(function(){ dmBusy(false); });
|
||||
}
|
||||
|
||||
function dmPolicyStatus(){
|
||||
if (!window.DM.quoteId){ dmAlert('No quote loaded.'); return; }
|
||||
dmBusy(true);
|
||||
dmGet(window.DM.base + '/policies/' + window.DM.quoteId + '/status')
|
||||
.done(function(res){
|
||||
if (!res.status){ dmAlert(res.message || 'Policy status failed'); return; }
|
||||
const p = res.data.policy || {};
|
||||
const st = (res.data.status || p.policy_status || 'UNKNOWN').toString();
|
||||
$('#polTitle').text((p.policy_number || 'Policy') + ' · ' + st);
|
||||
$('#polPill').text(st).removeClass('ok wait').addClass(/effective|complete|active/i.test(st) ? 'ok' : 'wait');
|
||||
dmAlert('Policy status updated.', true);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
dmAlert((xhr.responseJSON && xhr.responseJSON.message) || 'Policy status failed.');
|
||||
})
|
||||
.always(function(){ dmBusy(false); });
|
||||
}
|
||||
|
||||
function dmPolicyPdf(){
|
||||
if (!window.DM.quoteId){ dmAlert('No quote loaded.'); return; }
|
||||
dmBusy(true);
|
||||
dmGet(window.DM.base + '/policies/' + window.DM.quoteId + '/pdf')
|
||||
.done(function(res){
|
||||
if (!res.status){ dmAlert(res.message || 'PDF fetch failed'); return; }
|
||||
const p = res.data.policy || {};
|
||||
if (p.schedule_path){
|
||||
$('#dlSchedule').attr('href', p.schedule_path).css({pointerEvents:'auto',opacity:1});
|
||||
}
|
||||
if (p.proposal_path){
|
||||
$('#dlProposal').attr('href', p.proposal_path).css({pointerEvents:'auto',opacity:1});
|
||||
}
|
||||
dmAlert('PDF links updated.', true);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
dmAlert((xhr.responseJSON && xhr.responseJSON.message) || 'PDF fetch failed.');
|
||||
})
|
||||
.always(function(){ dmBusy(false); });
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function(){ dmGo(window.DM.step, true); });
|
||||
</script>
|
||||
103
app/Views/digit_motor/list.php
Normal file
103
app/Views/digit_motor/list.php
Normal file
@ -0,0 +1,103 @@
|
||||
<style>
|
||||
.digit-motor-list .page-title { color: #02a8b5; font-weight: 600; }
|
||||
.digit-motor-list .col-12 { max-width: 98% !important; }
|
||||
.digit-motor-list .table th,
|
||||
.digit-motor-list .table td { padding: 8px; }
|
||||
.digit-motor-list table.dataTable tbody td { padding: 4px 8px !important; }
|
||||
.digit-motor-list .dataTables_filter { position: absolute; }
|
||||
.digit-motor-list .dataTables_length label { height: 21px !important; }
|
||||
.digit-motor-list .status-pill {
|
||||
display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 600;
|
||||
}
|
||||
.digit-motor-list .st-QUOTED { background: #e7f6f7; color: #028a95; }
|
||||
.digit-motor-list .st-CREATED { background: #e8eef5; color: #35577A; }
|
||||
.digit-motor-list .st-KYC_DONE { background: #e4f2ea; color: #1F7A4D; }
|
||||
.digit-motor-list .st-EFFECTIVE { background: #e4f2ea; color: #1F7A4D; }
|
||||
.digit-motor-list .st-DRAFT,
|
||||
.digit-motor-list .st-FAILED { background: #f5f5f5; color: #666; }
|
||||
.digit-motor-list .st-PAID { background: #fff4e0; color: #8A5B00; }
|
||||
#digit-motor-datatable tbody tr { cursor: pointer; }
|
||||
#digit-motor-datatable tbody tr:hover { background-color: #e0e0e0; }
|
||||
</style>
|
||||
|
||||
<div class="container-fluid digit-motor-list">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="page-title-box">
|
||||
<h4 class="page-title">Digit Motor Quotes</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="digit_motor_list_div">
|
||||
<?= view('digit_motor/list_table', ['quotes' => $quotes ?? []]) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="digit-filter-sidebar" class="filter-sidebar" style="height:100%;width:0;position:fixed;z-index:1001;top:0;right:0;background:#f8f9fa;overflow-x:hidden;transition:0.4s;box-shadow:-2px 0 5px rgba(0,0,0,0.1);display:flex;flex-direction:column;">
|
||||
<div style="padding:15px 20px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #dee2e6;">
|
||||
<h4 class="m-0">Filter</h4>
|
||||
<a href="javascript:void(0)" onclick="closeFilterNav()" style="font-size:28px;text-decoration:none;color:#6c757d;">×</a>
|
||||
</div>
|
||||
<div style="padding:20px;flex-grow:1;overflow-y:auto;">
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select class="form-control" id="filter_status">
|
||||
<option value="">All</option>
|
||||
<?php if (!empty($statuses)): foreach ($statuses as $k => $v): ?>
|
||||
<option value="<?= esc($k) ?>"><?= esc($v) ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Enquiry ID</label>
|
||||
<input type="text" class="form-control" id="filter_enquiry_id">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Quote number</label>
|
||||
<input type="text" class="form-control" id="filter_quote_number">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Registration number</label>
|
||||
<input type="text" class="form-control" id="filter_license_plate">
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:15px 20px;display:flex;justify-content:flex-end;gap:10px;border-top:1px solid #dee2e6;">
|
||||
<button type="button" class="btn btn-secondary" onclick="resetFilters()">Clear</button>
|
||||
<button type="button" class="btn btn-primary" onclick="applyFilters()">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openFilterNav(){ document.getElementById('digit-filter-sidebar').style.width = '350px'; }
|
||||
function closeFilterNav(){ document.getElementById('digit-filter-sidebar').style.width = '0'; }
|
||||
|
||||
function applyFilters(){
|
||||
$.ajax({
|
||||
url: '<?= base_url('digit-motor/list') ?>',
|
||||
type: 'POST',
|
||||
data: {
|
||||
status: $('#filter_status').val(),
|
||||
enquiry_id: $('#filter_enquiry_id').val(),
|
||||
quote_number: $('#filter_quote_number').val(),
|
||||
license_plate: $('#filter_license_plate').val()
|
||||
},
|
||||
success: function(html){
|
||||
if ($.fn.DataTable && $.fn.DataTable.isDataTable('#digit-motor-datatable')) {
|
||||
$('#digit-motor-datatable').DataTable().destroy();
|
||||
}
|
||||
$('#digit_motor_list_div').html(html);
|
||||
closeFilterNav();
|
||||
},
|
||||
error: function(){
|
||||
if (window.toastr) toastr.error('Failed to load filtered list.', 'Error');
|
||||
else alert('Failed to load filtered list.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetFilters(){
|
||||
$('#filter_status,#filter_enquiry_id,#filter_quote_number,#filter_license_plate').val('');
|
||||
applyFilters();
|
||||
}
|
||||
</script>
|
||||
120
app/Views/digit_motor/list_table.php
Normal file
120
app/Views/digit_motor/list_table.php
Normal file
@ -0,0 +1,120 @@
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="digit-motor-datatable">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Enquiry ID </th>
|
||||
<th>Reg. No. </th>
|
||||
<th>Quote No. </th>
|
||||
<th>Premium </th>
|
||||
<th>IDV </th>
|
||||
<th>Status </th>
|
||||
<th>Created </th>
|
||||
<th>Action </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($quotes)): foreach ($quotes as $q): ?>
|
||||
<tr data-id="<?= (int) $q['id'] ?>" data-href="<?= base_url('digit-motor/journey/' . $q['id']) ?>">
|
||||
<td><?= esc($q['enquiry_id'] ?? '-') ?></td>
|
||||
<td><?= esc($q['license_plate_number'] ?? '-') ?></td>
|
||||
<td><?= esc($q['quote_number'] ?? '-') ?></td>
|
||||
<td><?= isset($q['premium']) && $q['premium'] !== null ? '₹' . number_format((float) $q['premium'], 2) : '-' ?></td>
|
||||
<td><?= isset($q['idv']) && $q['idv'] !== null ? '₹' . number_format((float) $q['idv'], 0) : '-' ?></td>
|
||||
<td>
|
||||
<?php $st = $q['status'] ?? 'DRAFT'; ?>
|
||||
<span class="status-pill st-<?= esc($st) ?>"><?= esc($st) ?></span>
|
||||
</td>
|
||||
<td><?= !empty($q['created_at']) ? date('d M Y H:i', strtotime($q['created_at'])) : '-' ?></td>
|
||||
<td>
|
||||
<div class="btn-group dropdown">
|
||||
<a href="javascript:void(0);" class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown">
|
||||
<i class="mdi mdi-dots-horizontal"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="<?= base_url('digit-motor/journey/' . $q['id']) ?>">
|
||||
<i class="mdi mdi-eye-outline mr-2 text-muted font-18 vertical-middle"></i>Open journey
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var table = $('#digit-motor-datatable');
|
||||
if (!table.length || !$.fn.DataTable) return;
|
||||
if ($.fn.DataTable.isDataTable(table)) {
|
||||
table.DataTable().destroy();
|
||||
}
|
||||
|
||||
table.DataTable({
|
||||
scrollX: true,
|
||||
dom: "<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" +
|
||||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row align-items-center'<'col-5 text-start'i><'col-7 d-flex justify-content-end align-items-center'<'me-2'l>p>>",
|
||||
lengthMenu: [[10, 20, 50, 100], [10, 20, 50, 100]],
|
||||
buttons: [
|
||||
{
|
||||
text: '<i class="mdi mdi-filter"></i><span class="btn-custom"> Filter </span>',
|
||||
className: 'btn app-btn-primary mr-2',
|
||||
action: function() { openFilterNav(); }
|
||||
},
|
||||
{
|
||||
text: '<i class="mdi mdi-plus"></i><span class="btn-custom"> Add </span>',
|
||||
className: 'btn app-btn-primary mr-2',
|
||||
action: function() { window.location.href = '<?= base_url('digit-motor/journey') ?>'; }
|
||||
},
|
||||
{
|
||||
extend: 'collection',
|
||||
text: '<span class="btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>',
|
||||
className: 'btn app-btn-secondary',
|
||||
buttons: [
|
||||
{
|
||||
extend: 'csv',
|
||||
text: '<i class="mdi mdi-file-delimited"></i> CSV',
|
||||
className: 'app-btn-primary',
|
||||
title: 'Digit-Motor-Quotes'
|
||||
},
|
||||
{
|
||||
extend: 'excel',
|
||||
text: '<i class="mdi mdi-file-excel"></i> EXCEL',
|
||||
title: 'Digit-Motor-Quotes',
|
||||
sheetName: 'Quotes',
|
||||
exportOptions: { orthogonal: 'sort' },
|
||||
className: 'app-btn-primary'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
language: {
|
||||
search: '<div class="datatable-search-wrapper" style="position:relative;display:inline-block;">_INPUT_<i class="mdi mdi-magnify datatable-search-icon" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#666;"></i></div>',
|
||||
searchPlaceholder: 'Search',
|
||||
emptyTable: '<div class="text-center text-muted">No Data found</div>'
|
||||
},
|
||||
paging: true,
|
||||
pageLength: 10,
|
||||
ordering: true,
|
||||
order: [[6, 'desc']],
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: [7] }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
$(document).off('click.digitMotorRow').on('click.digitMotorRow', '#digit-motor-datatable tbody tr', function(e) {
|
||||
if ($(e.target).closest('td').index() === $(this).children('td').length - 1) return;
|
||||
if ($(e.target).closest('a,button,.dropdown-menu').length) return;
|
||||
var href = $(this).data('href');
|
||||
if (href) window.location.href = href;
|
||||
});
|
||||
</script>
|
||||
@ -2350,6 +2350,12 @@ body[data-sidebar-size="condensed"] .footer {
|
||||
<span> Motor Policy Bulk Upload</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/digit-motor/list') ?>">
|
||||
<i class="ri-roadster-line"></i>
|
||||
<span> Digit Motor</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<?php } ?>
|
||||
<?php if (in_array(get_role_id(), [1,5])) { ?>
|
||||
|
||||
15
db.md
15
db.md
@ -339,3 +339,18 @@ END$$
|
||||
|
||||
DELIMITER ;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Digit Motor Module - Database Tables
|
||||
|
||||
Apply: `app/Database/digit_motor_tables.sql`
|
||||
|
||||
Tables:
|
||||
- `motor_token` — cached Digit OneAPI bearer tokens
|
||||
- `motor_quote` — quote header (quick + create quote)
|
||||
- `motor_vehicle` — vehicle 1:1 with quote
|
||||
- `motor_kyc` — KYC poll results
|
||||
- `motor_payment` — payment link records
|
||||
- `motor_policy` — policy status + PDF paths
|
||||
- `motor_api_log` — redacted request/response audit log
|
||||
|
||||
Loading…
Reference in New Issue
Block a user