gwm : go digit

This commit is contained in:
Gowtham M 2026-07-21 09:32:13 +05:30
parent 8fa027ce79
commit 1582202798
23 changed files with 2792 additions and 0 deletions

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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;">&times;</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>

View 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&nbsp;</th>
<th>Reg. No.&nbsp;</th>
<th>Quote No.&nbsp;</th>
<th>Premium&nbsp;</th>
<th>IDV&nbsp;</th>
<th>Status&nbsp;</th>
<th>Created&nbsp;</th>
<th>Action&nbsp;</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>

View File

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

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