MERGE_TEST_BUG_FIXES

This commit is contained in:
Ubuntu 2026-07-30 14:21:44 +05:30
commit 005cef7faa
28 changed files with 1424 additions and 1640 deletions

View File

@ -116,6 +116,21 @@ VIDAL_PRIMARY_KEY_CONSTANT =
MEDI_ASSIST_PRIMARY_KEY_CONSTANT = MEDI_ASSIST_PRIMARY_KEY_CONSTANT =
#--------------------------------------------------------------------
# TPA PRIMARY KEY CONSTANTS LIVE
#--------------------------------------------------------------------
MEDI_ASSIST_PRIMARY_KEY_CONSTANT_LIVE =
ICICI_PRIMARY_KEY_CONSTANT_LIVE =
ABHI_PRIMARY_KEY_CONSTANT_LIVE =
R_CARE_PRIMARY_KEY_CONSTANT_LIVE =
FHPL_PRIMARY_KEY_CONSTANT_LIVE =
VIDAL_PRIMARY_KEY_CONSTANT_LIVE =
VOLO_PRIMARY_KEY_CONSTANT_LIVE =
# When true, claims-collection-report falls back to ticket_master only if the
# policy TPA has no mapped dump table (or dump table missing). Dump TPAs always use claim_report.
CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER = false
FHPL_TOKEN_URL = FHPL_TOKEN_URL =
FHPL_BASE_URL = FHPL_BASE_URL =

View File

@ -1,200 +0,0 @@
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Seed claim_report from existing dump-sourced ticket_master rows.
*
* Usage:
* php spark claim:backfill-report
* php spark claim:backfill-report --policy=4687
* php spark claim:backfill-report --limit=5000
*/
class BackfillClaimReport extends BaseCommand
{
protected $group = 'Claims';
protected $name = 'claim:backfill-report';
protected $description = 'Backfill claim_report from ticket_master dump claims';
protected $usage = 'claim:backfill-report [--policy=ID] [--limit=N]';
protected $options = [
'--policy' => 'Optional client_policy_id to limit backfill',
'--limit' => 'Batch size (default 1000)',
];
public function run(array $params)
{
$db = db_connect();
if (!$db->tableExists('claim_report')) {
CLI::error('Table claim_report does not exist. Run migrations first.');
return EXIT_ERROR;
}
if (!$db->tableExists('ticket_master')) {
CLI::error('Table ticket_master does not exist.');
return EXIT_ERROR;
}
$policyId = (int) (CLI::getOption('policy') ?? 0);
$limit = (int) (CLI::getOption('limit') ?? 1000);
if ($limit <= 0) {
$limit = 1000;
}
$offset = 0;
$totalInserted = 0;
$totalUpdated = 0;
$totalSkipped = 0;
$now = date('Y-m-d H:i:s');
CLI::write('Backfilling claim_report from ticket_master (dump-sourced claims)...', 'yellow');
while (true) {
$builder = $db->table('ticket_master')
->where('is_active', 1)
->groupStart()
->where('claim_dump_ref_id IS NOT NULL', null, false)
->orWhere('file_id IS NOT NULL', null, false)
->groupEnd()
->where("claim_number IS NOT NULL AND TRIM(claim_number) != ''", null, false)
->orderBy('id', 'ASC')
->limit($limit, $offset);
if ($policyId > 0) {
$builder->where('client_policy_id', $policyId);
}
$tickets = $builder->get()->getResultArray();
if ($tickets === []) {
break;
}
$rows = [];
foreach ($tickets as $tm) {
$claimNumber = trim((string) ($tm['claim_number'] ?? ''));
$clientPolicyId = (int) ($tm['client_policy_id'] ?? 0);
if ($claimNumber === '' || $clientPolicyId <= 0) {
$totalSkipped++;
continue;
}
$rows[] = [
'tpa_id' => $tm['tpa_id'] ?? null,
'client_id' => $tm['client_id'] ?? null,
'client_policy_id' => $clientPolicyId,
'file_id' => $tm['file_id'] ?? null,
'ticket_id' => $tm['id'] ?? null,
'source_table' => null,
'source_row_id' => $tm['claim_dump_ref_id'] ?? null,
'claim_number' => $claimNumber,
'emp_code' => $tm['emp_code'] ?? null,
'tpa_no' => $tm['tpa_no'] ?? null,
'emp_id' => $tm['emp_id'] ?? null,
'insured_emp_id' => $tm['insured_emp_id'] ?? null,
'claim_amount' => $tm['claim_amount'] ?? null,
'approved_amount' => $tm['approved_amount'] ?? null,
'incurred_amount' => $tm['approved_amount'] ?? $tm['claim_amount'] ?? null,
'si_amt' => $tm['si_amt'] ?? null,
'tpa_claim_status' => $tm['tpa_claim_status'] ?? null,
'claim_status_id' => $tm['claim_status_id'] ?? null,
'tpa_claim_type' => $tm['tpa_claim_type'] ?? null,
'tpa_ailments' => $tm['tpa_ailments'] ?? null,
'doa' => $tm['doa'] ?? null,
'dod' => $tm['dod'] ?? null,
'date_of_intimat' => $tm['date_of_intimat'] ?? null,
'settled_date' => $tm['settled_date'] ?? null,
'approved_date' => $tm['approved_date'] ?? null,
'claim_dump_date' => $tm['claim_dump_date'] ?? null,
'hospital_name' => $tm['hospital_name'] ?? null,
'hospital_city' => $tm['hospital_city'] ?? null,
'hospital_state' => $tm['hospital_state'] ?? null,
'hospital_pin_code'=> $tm['hospital_pin_code'] ?? null,
'hospital_address' => $tm['hospital_address'] ?? null,
'gender' => null,
'age' => null,
'relation' => $tm['relationship'] ?? null,
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
];
}
if ($rows !== []) {
$result = $this->upsertRows($db, $rows);
if ($result === false) {
CLI::error('Upsert failed at offset ' . $offset);
return EXIT_ERROR;
}
$totalInserted += $result['inserted'];
$totalUpdated += $result['updated'];
}
$offset += count($tickets);
CLI::write("Processed {$offset} ticket_master rows...", 'green');
if (count($tickets) < $limit) {
break;
}
}
CLI::write("Done. inserted≈{$totalInserted}, updated≈{$totalUpdated}, skipped={$totalSkipped}", 'green');
return EXIT_SUCCESS;
}
/**
* @param list<array<string, mixed>> $rows
* @return array{inserted:int,updated:int}|false
*/
private function upsertRows($db, array $rows)
{
$columns = [
'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id',
'source_table', 'source_row_id', 'claim_number', 'emp_code', 'tpa_no',
'emp_id', 'insured_emp_id', 'claim_amount', 'approved_amount', 'incurred_amount',
'si_amt', 'tpa_claim_status', 'claim_status_id', 'tpa_claim_type', 'tpa_ailments',
'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'claim_dump_date',
'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address',
'gender', 'age', 'relation', 'is_active', 'created_at', 'updated_at',
];
$updateCols = array_values(array_diff($columns, ['client_policy_id', 'claim_number', 'created_at']));
$inserted = 0;
$updated = 0;
foreach (array_chunk($rows, 100) as $chunk) {
$placeholders = [];
$binds = [];
foreach ($chunk as $row) {
$rowPlaceholders = [];
foreach ($columns as $col) {
$rowPlaceholders[] = '?';
$binds[] = $row[$col] ?? null;
}
$placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')';
}
$updates = [];
foreach ($updateCols as $col) {
$updates[] = '`' . $col . '` = VALUES(`' . $col . '`)';
}
$sql = 'INSERT INTO `claim_report` (`' . implode('`, `', $columns) . '`) VALUES '
. implode(', ', $placeholders)
. ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
if ($db->query($sql, $binds) === false) {
return false;
}
$affected = $db->affectedRows();
// MySQL: 1 = insert, 2 = update existing
$updated += (int) floor($affected / 2);
$inserted += max(0, $affected - (2 * (int) floor($affected / 2)));
}
return ['inserted' => $inserted, 'updated' => $updated];
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace App\Commands;
use App\Libraries\ClaimReportSyncService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Copy linked TPA dump (+ optional ticket_master) claims into claim_report.
*
* Usage:
* php spark claim:sync-report
* php spark claim:sync-report --phase1
* php spark claim:sync-report --policy=12 --phase1
* php spark claim:sync-report --tpa=mediassist --policy=12
* php spark claim:sync-report --ticket-only
*/
class SyncClaimReportFromDump extends BaseCommand
{
protected $group = 'Claims';
protected $name = 'claim:sync-report';
protected $description = 'Sync claim_report from TPA dump tables + ticket_master';
protected $usage = 'claim:sync-report [--policy=ID] [--tpa=NAME|all] [--limit=N] [--phase1] [--ticket-only]';
protected $options = [
'--policy' => 'Optional client_policy_id',
'--tpa' => 'icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all)',
'--limit' => 'Batch size per dump query (default 500)',
'--phase1' => 'TPA dump tables only (skip ticket_master)',
'--ticket-only' => 'Skip dump tables; copy only from ticket_master',
];
public function run(array $params)
{
$policyId = (int) ($params['policy'] ?? CLI::getOption('policy') ?? 0);
$limit = (int) ($params['limit'] ?? CLI::getOption('limit') ?? 500);
$ticketOnly = array_key_exists('ticket-only', $params) || CLI::getOption('ticket-only') !== null;
$phase1Only = array_key_exists('phase1', $params) || CLI::getOption('phase1') !== null;
$tpaOpt = strtolower(trim((string) ($params['tpa'] ?? CLI::getOption('tpa') ?? 'all')));
if ($limit <= 0) {
$limit = 500;
}
$result = (new ClaimReportSyncService())->sync($policyId, $tpaOpt, $limit, $ticketOnly, $phase1Only);
foreach ($result['logs'] as $line) {
$color = 'white';
if (str_starts_with($line, '[FAIL]')) {
$color = 'red';
} elseif (str_starts_with($line, '[DONE]') || str_starts_with($line, 'Done.')) {
$color = 'green';
} elseif (str_starts_with($line, '[SKIP]') || str_starts_with($line, 'Phase') || str_starts_with($line, 'Skipping')) {
$color = 'yellow';
} elseif (str_starts_with($line, ' ')) {
$color = 'green';
}
CLI::write($line, $color);
}
return ! empty($result['status']) ? EXIT_SUCCESS : EXIT_ERROR;
}
}

View File

@ -510,7 +510,7 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) {
$routes->get('all', 'ClaimReportDashboardController::all'); $routes->get('all', 'ClaimReportDashboardController::all');
$routes->get('debug', 'ClaimReportDashboardController::debug'); $routes->get('debug', 'ClaimReportDashboardController::debug');
$routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1');
$routes->match(['get', 'post'], 'sync', 'ClaimReportDashboardController::sync'); $routes->get('sync', 'ClaimReportDashboardController::sync');
}); });
$routes->group('enrollment-collection-v1', static function ($routes) { $routes->group('enrollment-collection-v1', static function ($routes) {
@ -607,6 +607,8 @@ $routes->group("rfq", ["filter" => "authMVC"], function ($routes) {
$routes->post("create", "LeadsController::createRFQ"); $routes->post("create", "LeadsController::createRFQ");
$routes->post("savePolicyInfo", "LeadsController::savePolicyInfo"); $routes->post("savePolicyInfo", "LeadsController::savePolicyInfo");
$routes->post("createQCR", "LeadsController::createQCR"); $routes->post("createQCR", "LeadsController::createQCR");
$routes->post("generateGmcQcrPdf", "LeadsController::generateGmcQcrPdf");
$routes->get("downloadGmcQcrPdf/(:num)", "LeadsController::downloadGmcQcrPdf/$1");
$routes->get("list/(:any)", "LeadsController::viewRFQ/$1"); $routes->get("list/(:any)", "LeadsController::viewRFQ/$1");
$routes->get("nonEB","LeadsController::rfqNonEB"); $routes->get("nonEB","LeadsController::rfqNonEB");
// Non-EB dedicated RFQ/QCR endpoints (do not alter existing ones) // Non-EB dedicated RFQ/QCR endpoints (do not alter existing ones)
@ -865,7 +867,6 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel
$routes->get('all', 'ClaimReportDashboardController::all'); $routes->get('all', 'ClaimReportDashboardController::all');
$routes->get('debug', 'ClaimReportDashboardController::debug'); $routes->get('debug', 'ClaimReportDashboardController::debug');
$routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1');
$routes->match(['get', 'post'], 'sync', 'ClaimReportDashboardController::sync');
}); });
$routes->group('enrollment-collection-v1', static function ($routes) { $routes->group('enrollment-collection-v1', static function ($routes) {

View File

@ -5,11 +5,12 @@ namespace App\Controllers;
use App\Models\ClaimReportDashboardModel; use App\Models\ClaimReportDashboardModel;
use App\Models\ClaimsCollectionV2DashboardModel; use App\Models\ClaimsCollectionV2DashboardModel;
use App\Models\ClaimDumpFileModel; use App\Models\ClaimDumpFileModel;
use App\Libraries\ClaimReportSyncService;
use CodeIgniter\API\ResponseTrait; use CodeIgniter\API\ResponseTrait;
/** /**
* Claims Collection dashboard API using claim_report (falls back to ticket_master). * Claims Collection dashboard API using claim_report.
* ticket_master fallback is env-gated (CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER)
* and only applies when the policy TPA has no dump table.
*/ */
class ClaimReportDashboardController extends BaseController class ClaimReportDashboardController extends BaseController
{ {
@ -153,6 +154,376 @@ class ClaimReportDashboardController extends BaseController
]); ]);
} }
/**
* Sync TPA dump tables claim_report.
* Only rows where ticket_id IS NOT NULL and is_active = 1 are copied.
* Idempotent: existing (client_policy_id, claim_number) rows are skipped.
* No request parameters required.
*
* GET /util/claims-collection-report/sync
*/
public function sync()
{
@set_time_limit(0);
@ini_set('max_execution_time', '0');
$db = db_connect();
if (!$db->tableExists('claim_report')) {
return $this->respond([
'status' => false,
'message' => 'Table claim_report does not exist. Run migrations first.',
], 500);
}
// env_key => [dump_table, claim_number_col, dump_col => claim_report_col mapping]
$tpaTableMap = [
'VIDAL_PRIMARY_KEY_CONSTANT' => [
'table' => 'claims_dump_vidal',
'claim_col' => 'insurer_claim_number',
'mapping' => [
'insurer_claim_number' => 'claim_number',
'employee_number' => 'emp_code',
'date_of_admission' => 'doa',
'date_of_discharge' => 'dod',
'claim_amount' => 'claim_amount',
'approved_amount' => 'approved_amount',
'sum_insured' => 'si_amt',
'claim_status' => 'tpa_claim_status',
'hospital_name' => 'hospital_name',
'hospital_address' => 'hospital_address',
'hospital_city' => 'hospital_city',
'hospital_state' => 'hospital_state',
'hospital_pincode' => 'hospital_pin_code',
'type_of_claim' => 'tpa_claim_type',
'diagnosis' => 'tpa_ailments',
'tpa_claim_number' => 'tpa_no',
],
],
'ABHI_PRIMARY_KEY_CONSTANT' => [
'table' => 'claims_dump_abhi',
'claim_col' => 'abhi_claim_no',
'mapping' => [
'abhi_claim_no' => 'claim_number',
'member_code' => 'emp_code',
'doa' => 'doa',
'dod' => 'dod',
'intimation_date' => 'date_of_intimat',
'claim_status' => 'tpa_claim_status',
'claimed_amount' => 'claim_amount',
'hospital_name' => 'hospital_name',
'hospital_city' => 'hospital_city',
'hospital_state' => 'hospital_state',
'settled_date' => 'settled_date',
'healthcard_id' => 'tpa_no',
'claim_type' => 'tpa_claim_type',
'diagnosis' => 'tpa_ailments',
'abhi_amount_less_coins_current_month' => 'approved_amount',
'patient_age' => 'age',
'gender' => 'gender',
'relation' => 'relation',
],
],
'MEDI_ASSIST_PRIMARY_KEY_CONSTANT' => [
'table' => 'claims_dump_medi_assist',
'claim_col' => 'claim_id',
'mapping' => [
'claim_id' => 'claim_number',
'pribenef_employee_code' => 'emp_code',
'date_of_admission' => 'doa',
'date_of_discharge' => 'dod',
'intimation_date' => 'date_of_intimat',
'settled_date' => 'settled_date',
'processed_date' => 'approved_date',
'claim_status' => 'tpa_claim_status',
'claim_amount' => 'claim_amount',
'claim_approved_amount' => 'approved_amount',
'hospital_name' => 'hospital_name',
'hospital_address' => 'hospital_address',
'hospital_city' => 'hospital_city',
'hospital_state' => 'hospital_state',
'hospital_pincode' => 'hospital_pin_code',
'claim_type' => 'tpa_claim_type',
'primary_ailment_name' => 'tpa_ailments',
'benef_sum_insured' => 'si_amt',
'benef_gender' => 'gender',
'benef_age' => 'age',
'benef_relation' => 'relation',
'incurred_amount' => 'incurred_amount',
],
],
'FHPL_PRIMARY_KEY_CONSTANT' => [
'table' => 'claims_dump_fhpl',
'claim_col' => 'claim_id',
'mapping' => [
'claim_id' => 'claim_number',
'employee_id' => 'emp_code',
'admission_date' => 'doa',
'discharge_date' => 'dod',
'claim_received_date' => 'date_of_intimat',
'claim_passed_date' => 'approved_date',
'settled_date' => 'settled_date',
'current_claim_status' => 'tpa_claim_status',
'claim_amount' => 'claim_amount',
'settled_amount' => 'approved_amount',
'incurred_amount' => 'incurred_amount',
'coverage_amount' => 'si_amt',
'provider_name' => 'hospital_name',
'provider_address' => 'hospital_address',
'provider_state' => 'hospital_state',
'provider_place' => 'hospital_city',
'provider_pincode' => 'hospital_pin_code',
'claim_type' => 'tpa_claim_type',
'diagnosis' => 'tpa_ailments',
'uhid_no' => 'tpa_no',
'gender' => 'gender',
'years' => 'age',
'relationship' => 'relation',
],
],
'R_CARE_PRIMARY_KEY_CONSTANT' => [
'table' => 'claims_dump_reliance',
'claim_col' => 'cl_inward_no',
'mapping' => [
'cl_inward_no' => 'claim_number',
'employee_member_id' => 'emp_code',
'doa_opd_treatment_from' => 'doa',
'dod_opd_treatment_to' => 'dod',
'approved_date' => 'approved_date',
'cheque_neft_date' => 'settled_date',
'claimed_amount' => 'claim_amount',
'net_sanction_amount' => 'approved_amount',
'final_status' => 'tpa_claim_status',
'hospital_name' => 'hospital_name',
'hospital_state' => 'hospital_state',
'hospital_district' => 'hospital_city',
'uhid' => 'tpa_no',
'diagnosis' => 'tpa_ailments',
'member_reimbursement_cl_type' => 'tpa_claim_type',
'gender' => 'gender',
'age' => 'age',
'relation' => 'relation',
'sum_insured' => 'si_amt',
],
],
'ICICI_PRIMARY_KEY_CONSTANT' => [
'table' => 'claims_dump_icici',
'claim_col' => 'claim_number',
'mapping' => [
'claim_number' => 'claim_number',
'employee_member_id' => 'emp_code',
'doa' => 'doa',
'dod' => 'dod',
'payment_date' => 'settled_date',
'claimed_amount' => 'claim_amount',
'net_sanct_amt' => 'approved_amount',
'updated_status' => 'tpa_claim_status',
'hospital_name' => 'hospital_name',
'hospital_city' => 'hospital_city',
'hospital_state' => 'hospital_state',
'type_of_claim' => 'tpa_claim_type',
'diagnosis' => 'tpa_ailments',
'uhid' => 'tpa_no',
'sum_insured' => 'si_amt',
'gender' => 'gender',
'age' => 'age',
'relation' => 'relation',
],
],
];
// Build tpa_id → config map from env
$tpaMap = [];
foreach ($tpaTableMap as $envKey => $cfg) {
$tpaId = (int) env($envKey);
if ($tpaId > 0) {
$tpaMap[$tpaId] = $cfg;
}
}
$totalFound = 0;
$totalInserted = 0;
$totalSkipped = 0;
$totalFailed = 0;
$tpaResults = [];
$now = date('Y-m-d H:i:s');
foreach ($tpaMap as $tpaId => $cfg) {
$dumpTable = $cfg['table'];
$claimCol = $cfg['claim_col'];
$mapping = $cfg['mapping'];
if (!$db->tableExists($dumpTable)) {
$tpaResults[] = [
'tpa_id' => $tpaId,
'table' => $dumpTable,
'status' => 'skipped',
'detail' => 'Dump table does not exist.',
'found' => 0,
'inserted' => 0,
'skipped' => 0,
'failed' => 0,
];
continue;
}
// Fetch all active dump rows with a ticket_id (all columns)
$dumpRows = $db->table($dumpTable)
->where('is_active', 1)
->where('ticket_id IS NOT NULL', null, false)
->get()
->getResultArray();
// Pre-load all related ticket_master rows in one query
$ticketIds = array_filter(array_column($dumpRows, 'ticket_id'));
$ticketsById = [];
if (!empty($ticketIds)) {
$ticketRows = $db->table('ticket_master')
->whereIn('id', array_values(array_unique($ticketIds)))
->get()
->getResultArray();
foreach ($ticketRows as $tm) {
$ticketsById[(int) $tm['id']] = $tm;
}
}
$found = count($dumpRows);
$inserted = 0;
$skipped = 0;
$failed = 0;
$totalFound += $found;
foreach ($dumpRows as $row) {
$claimNumber = trim((string) ($row[$claimCol] ?? ''));
$clientPolicyId = (int) ($row['client_policy_id'] ?? 0);
if ($claimNumber === '' || $clientPolicyId <= 0) {
$skipped++;
$totalSkipped++;
continue;
}
// Fetch linked ticket_master row for emp_id / insured_emp_id
$ticket = $ticketsById[(int) ($row['ticket_id'] ?? 0)] ?? null;
// Check if already exists in claim_report
$existing = $db->table('claim_report')
->where('client_policy_id', $clientPolicyId)
->where('claim_number', $claimNumber)
->get()
->getRowArray();
// Base row metadata from dump; employee/system IDs always from ticket_master
$reportRow = [
'tpa_id' => $tpaId,
'client_id' => $row['client_id'] ?? ($ticket['client_id'] ?? null),
'file_id' => $row['file_id'] ?? ($ticket['file_id'] ?? null),
'ticket_id' => $row['ticket_id'],
'source_table' => $dumpTable,
'source_row_id' => $row['id'],
'is_active' => 1,
'updated_at' => $now,
'emp_id' => $ticket['emp_id'] ?? null,
'insured_emp_id' => $ticket['insured_emp_id'] ?? null,
'claim_status_id' => $ticket['claim_status_id'] ?? null,
];
// 1) Fill ALL claim_report data fields from ticket_master first
$ticketCols = [
'emp_code', 'tpa_no',
'gender', 'age', 'relation',
'claim_amount', 'approved_amount', 'si_amt', 'incurred_amount',
'tpa_claim_status', 'tpa_claim_type', 'tpa_ailments',
'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date',
'hospital_name', 'hospital_city', 'hospital_state',
'hospital_pin_code', 'hospital_address',
'claim_dump_date',
];
if ($ticket) {
foreach ($ticketCols as $col) {
$ticketVal = $ticket[$col] ?? null;
// ticket_master uses "relationship"; claim_report uses "relation"
if ($col === 'relation' && ($ticketVal === null || $ticketVal === '')) {
$ticketVal = $ticket['relationship'] ?? null;
}
if ($ticketVal !== null && $ticketVal !== '') {
$reportRow[$col] = $ticketVal;
}
}
}
// 2) Dump table fills only fields still null/empty after ticket_master
foreach ($mapping as $dumpCol => $reportCol) {
$current = $reportRow[$reportCol] ?? null;
if ($current !== null && $current !== '') {
continue;
}
if (array_key_exists($dumpCol, $row) && $row[$dumpCol] !== null && $row[$dumpCol] !== '') {
$reportRow[$reportCol] = $row[$dumpCol];
}
}
// incurred_amount final fallback
if (empty($reportRow['incurred_amount'])) {
$reportRow['incurred_amount'] = $reportRow['approved_amount'] ?? $reportRow['claim_amount'] ?? null;
}
if ($existing) {
// Update existing row with full data
$ok = $db->table('claim_report')
->where('client_policy_id', $clientPolicyId)
->where('claim_number', $claimNumber)
->update($reportRow);
if ($ok) {
$skipped++;
$totalSkipped++;
} else {
$failed++;
$totalFailed++;
}
continue;
}
// New insert
$reportRow['client_policy_id'] = $clientPolicyId;
$reportRow['claim_number'] = $claimNumber;
$reportRow['created_at'] = $now;
$ok = $db->table('claim_report')->insert($reportRow);
if ($ok) {
$inserted++;
$totalInserted++;
} else {
$failed++;
$totalFailed++;
}
}
$tpaResults[] = [
'tpa_id' => $tpaId,
'table' => $dumpTable,
'status' => 'done',
'found' => $found,
'inserted' => $inserted,
'skipped' => $skipped,
'failed' => $failed,
];
}
return $this->respond([
'status' => $totalFailed === 0,
'message' => $totalFailed === 0 ? 'Sync completed successfully.' : 'Sync completed with some failures.',
'summary' => [
'total_found' => $totalFound,
'total_inserted' => $totalInserted,
'total_skipped' => $totalSkipped,
'total_failed' => $totalFailed,
],
'tpa_results' => $tpaResults,
]);
}
/** /**
* Admin check only: raw JSON on screen (no dashboard UI). * Admin check only: raw JSON on screen (no dashboard UI).
*/ */
@ -183,95 +554,4 @@ class ClaimReportDashboardController extends BaseController
->setBody($body); ->setBody($body);
} }
/**
* Run claim report sync via URL (authMVC / JWT).
* Uses ClaimReportSyncService directly (no spark/CLI).
*
* Query params (all optional):
* client_policy / client_policy_id
* tpa=icici|abhi|mediassist|fhpl|rcare|vidal|all
* limit=500
* phase1=true TPA dump tables only (skip ticket_master)
* ticket_only=1 ticket_master only (ignored if phase1=true)
*/
public function sync()
{
@set_time_limit(0);
@ini_set('max_execution_time', '0');
$policyId = $this->resolvePolicyId();
$tpa = strtolower(trim((string) ($this->request->getGet('tpa') ?? $this->request->getPost('tpa') ?? 'all')));
$limit = (int) ($this->request->getGet('limit') ?? $this->request->getPost('limit') ?? 500);
$ticketOnly = $this->isTruthyParam('ticket_only');
$phase1Only = $this->isTruthyParam('phase1');
$allowedTpa = ['all', 'vidal', 'abhi', 'mediassist', 'fhpl', 'rcare', 'icici'];
if ($tpa === '' || ! in_array($tpa, $allowedTpa, true)) {
return $this->respond([
'status' => false,
'message' => 'Invalid tpa. Allowed: ' . implode(', ', $allowedTpa),
], 422);
}
if ($limit <= 0) {
$limit = 500;
}
if ($limit > 5000) {
$limit = 5000;
}
$startedAt = date('Y-m-d H:i:s');
try {
$result = (new ClaimReportSyncService())->sync(
$policyId,
$tpa,
$limit,
$ticketOnly,
$phase1Only
);
} catch (\Throwable $e) {
return $this->respond([
'status' => false,
'message' => 'Sync failed: ' . $e->getMessage(),
'policy_id' => $policyId > 0 ? $policyId : null,
'tpa' => $tpa,
'limit' => $limit,
'phase1' => $phase1Only,
'ticket_only' => $ticketOnly,
'started_at' => $startedAt,
], 500);
}
$ok = ! empty($result['status']);
return $this->respond([
'status' => $ok,
'message' => $result['message'] ?? ($ok ? 'Sync completed.' : 'Sync failed.'),
'policy_id' => $policyId > 0 ? $policyId : null,
'tpa' => $tpa,
'limit' => $limit,
'phase1' => $phase1Only,
'ticket_only' => $ticketOnly && ! $phase1Only,
'started_at' => $startedAt,
'finished_at' => date('Y-m-d H:i:s'),
'summary' => $result['summary'] ?? null,
'phases' => $result['phases'] ?? [],
'tpa_results' => $result['tpa_results'] ?? [],
'logs' => $result['logs'] ?? [],
], $ok ? 200 : 500);
}
/**
* True when GET/POST param is 1/true/yes (case-insensitive).
*/
protected function isTruthyParam(string $name): bool
{
$raw = $this->request->getGet($name) ?? $this->request->getPost($name);
if ($raw === null) {
return false;
}
return in_array(strtolower(trim((string) $raw)), ['1', 'true', 'yes'], true);
}
} }

View File

@ -78,6 +78,8 @@ class DigitMotorController extends BaseController
$data['title'] = 'Digit Motor Journey'; $data['title'] = 'Digit Motor Journey';
$data['quote'] = null; $data['quote'] = null;
$data['quote_id'] = null; $data['quote_id'] = null;
$data['vehicle_master'] = null;
$data['policyholder'] = [];
if ($quoteId) { if ($quoteId) {
$data['quote'] = $this->quoteModel->getDetail((int) $quoteId); $data['quote'] = $this->quoteModel->getDetail((int) $quoteId);
@ -85,6 +87,17 @@ class DigitMotorController extends BaseController
if (!$data['quote']) { if (!$data['quote']) {
return redirect()->to(base_url('digit-motor/list'))->with('error', 'Quote not found.'); return redirect()->to(base_url('digit-motor/list'))->with('error', 'Quote not found.');
} }
$data['policyholder'] = is_array($data['quote']['policyholder_details'] ?? null)
? $data['quote']['policyholder_details']
: [];
$vcode = $data['quote']['vehicle']['vehicle_maincode'] ?? '';
if ($vcode !== '') {
try {
$data['vehicle_master'] = $this->vehicleMaster->findActive((string) $vcode);
} catch (\Throwable $e) {
$data['vehicle_master'] = null;
}
}
} }
return $this->loadLayout('digit_motor/journey', $data); return $this->loadLayout('digit_motor/journey', $data);
@ -347,6 +360,9 @@ class DigitMotorController extends BaseController
'message' => $e->getMessage(), 'message' => $e->getMessage(),
'digit_code' => $e->getDigitCode(), 'digit_code' => $e->getDigitCode(),
'infra' => $e->isInfraError(), 'infra' => $e->isInfraError(),
'data' => is_array($e->getResponseBody())
? ($e->getResponseBody()['_nhance_shell'] ?? null)
: null,
], min($http, 599)); ], min($http, 599));
} catch (\Throwable $e) { } catch (\Throwable $e) {
log_message('error', 'DigitMotor unexpected error: ' . $e->getMessage()); log_message('error', 'DigitMotor unexpected error: ' . $e->getMessage());

View File

@ -238,14 +238,15 @@ class ICICILombardController extends AdminController
]; ];
$response = call_third_party_api($url, $method, $headers, $body); $response = call_third_party_api($url, $method, $headers, $body);
log_message('error', 'ICICI - generateAuthToken API URL: ' . json_encode(["url" => $url, "method" => $method, "headers" => $headers, "body" => $body], JSON_PRETTY_PRINT));
log_message('error', 'ICICI - generateAuthToken API response: ' . json_encode($response, JSON_PRETTY_PRINT));
if($response['status'] != true){ if($response['status'] != true){
return $this->response->setJSON([ return [
'status' => false, 'status' => false,
'message' => 'Token generation failed.', 'message' => 'Token generation failed.',
'data' => $response 'data' => $response
]); ];
} }
// Debug removed: return token response to caller. // Debug removed: return token response to caller.

View File

@ -38,6 +38,7 @@ use App\Helpers\MailHelper;
use App\Helpers\ExcelMergeHelper; use App\Helpers\ExcelMergeHelper;
use App\Helpers\ExcelSanitizeHelper; use App\Helpers\ExcelSanitizeHelper;
use App\Libraries\GoogleSheetLib; use App\Libraries\GoogleSheetLib;
use App\Libraries\GmcQcrPdfService;
class LeadsController extends BaseController class LeadsController extends BaseController
{ {
@ -1840,6 +1841,59 @@ class LeadsController extends BaseController
return $this->respond(['status' => false, 'id' => $result, 'message' => "Failed to create QCR", 'data' => $data], 200); return $this->respond(['status' => false, 'id' => $result, 'message' => "Failed to create QCR", 'data' => $data], 200);
} }
/**
* Generate GMC Quote Comparison Report PDF from saved QCR data.
* POST /rfq/generateGmcQcrPdf body: lead_id
*/
public function generateGmcQcrPdf()
{
$leadId = (int) ($this->request->getPost('lead_id') ?? $this->request->getGet('lead_id') ?? 0);
if ($leadId <= 0) {
return $this->respond(['status' => false, 'message' => 'lead_id is required'], 200);
}
$proposals = $this->request->getPost('proposals');
if (! is_array($proposals)) {
$proposals = null;
}
try {
$service = new GmcQcrPdfService($this->RFQModel, $this->leadsModel, $this->clientModel);
$result = $service->generate($leadId, $proposals);
return $this->respond($result, 200);
} catch (\Throwable $e) {
$this->myLogger->logme('error', 'generateGmcQcrPdf | ' . $e->getMessage());
return $this->respond(['status' => false, 'message' => 'Failed to generate PDF: ' . $e->getMessage()], 200);
}
}
/**
* Download last generated GMC QCR PDF.
* GET /rfq/downloadGmcQcrPdf/(:num)
*/
public function downloadGmcQcrPdf($leadId = null)
{
$leadId = (int) ($leadId ?? $this->request->getGet('lead_id') ?? 0);
if ($leadId <= 0) {
return $this->respond(['status' => false, 'message' => 'lead_id is required'], 400);
}
try {
$service = new GmcQcrPdfService($this->RFQModel, $this->leadsModel, $this->clientModel);
$result = $service->resolveDownload($leadId);
if (! ($result['status'] ?? false)) {
return $this->respond($result, 404);
}
return $this->response
->download($result['file_path'], null)
->setFileName($result['download_name']);
} catch (\Throwable $e) {
$this->myLogger->logme('error', 'downloadGmcQcrPdf | ' . $e->getMessage());
return $this->respond(['status' => false, 'message' => 'Failed to download PDF: ' . $e->getMessage()], 500);
}
}
public function reorderProposalsByInsurerTotal(array $data): array public function reorderProposalsByInsurerTotal(array $data): array
{ {

View File

@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS motor_quote (
end_date DATE DEFAULT NULL, end_date DATE DEFAULT NULL,
pincode VARCHAR(6) NOT NULL, pincode VARCHAR(6) NOT NULL,
coverage_details JSON DEFAULT NULL, coverage_details JSON DEFAULT NULL,
policyholder_details JSON DEFAULT NULL,
premium DECIMAL(12,2) DEFAULT NULL, premium DECIMAL(12,2) DEFAULT NULL,
idv DECIMAL(12,2) DEFAULT NULL, idv DECIMAL(12,2) DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',

View File

@ -1,444 +0,0 @@
<?php
namespace App\Libraries;
use App\Libraries\TpaClaimsImportFactory;
use InvalidArgumentException;
/**
* Sync claim_report from TPA dump tables (+ optional ticket_master).
* Safe to call from HTTP or spark (no CLI constants required).
*/
class ClaimReportSyncService
{
/**
* @return array<string, array{env:string,table:string}>
*/
public function tpaConfigs(): array
{
return [
'vidal' => [
'env' => 'VIDAL_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_vidal',
],
'abhi' => [
'env' => 'ABHI_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_abhi',
],
'mediassist' => [
'env' => 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_medi_assist',
],
'fhpl' => [
'env' => 'FHPL_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_fhpl',
],
'rcare' => [
'env' => 'R_CARE_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_reliance',
],
'icici' => [
'env' => 'ICICI_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_icici',
],
];
}
/**
* @return array{
* status: bool,
* message: string,
* summary: array{mapped:int,skipped:int,errors:int,message:?string},
* phases: list<string>,
* tpa_results: list<array<string,mixed>>,
* logs: list<string>
* }
*/
public function sync(
int $policyId = 0,
string $tpa = 'all',
int $limit = 500,
bool $ticketOnly = false,
bool $phase1Only = false
): array {
$logs = [];
$phases = [];
$tpaResults = [];
$db = db_connect();
$log = static function (string $line) use (&$logs): void {
$logs[] = $line;
};
if (!$db->tableExists('claim_report')) {
$msg = 'Table claim_report does not exist. Run: php spark migrate';
$log($msg);
return $this->result(false, $msg, 0, 0, 1, $phases, $tpaResults, $logs);
}
// phase1Only and ticketOnly are mutually exclusive; phase1 wins.
if ($phase1Only) {
$ticketOnly = false;
}
$tpa = strtolower(trim($tpa));
if ($tpa === '') {
$tpa = 'all';
}
if ($limit <= 0) {
$limit = 500;
}
$totalMapped = 0;
$totalSkipped = 0;
$errors = 0;
if (!$ticketOnly) {
$phase1 = 'Phase 1: sync from TPA dump tables (linked ticket_id rows)...';
$phases[] = $phase1;
$log($phase1);
$configs = $this->tpaConfigs();
if ($tpa !== 'all') {
if (!isset($configs[$tpa])) {
$msg = 'Unknown tpa=' . $tpa . '. Use: ' . implode('|', array_keys($configs)) . '|all';
$log($msg);
return $this->result(false, $msg, 0, 0, 1, $phases, $tpaResults, $logs);
}
$configs = [$tpa => $configs[$tpa]];
}
foreach ($configs as $key => $cfg) {
$tpaId = (int) env($cfg['env']);
$table = $cfg['table'];
if ($tpaId <= 0) {
$detail = "{$key}: env {$cfg['env']} not set";
$log('[SKIP] ' . $detail);
$tpaResults[] = ['tpa' => $key, 'mapped' => 0, 'status' => 'skipped', 'detail' => $detail];
continue;
}
if (!$db->tableExists($table)) {
$detail = "{$key}: table {$table} missing";
$log('[SKIP] ' . $detail);
$tpaResults[] = ['tpa' => $key, 'mapped' => 0, 'status' => 'skipped', 'detail' => $detail];
continue;
}
try {
$service = TpaClaimsImportFactory::make($tpaId);
} catch (InvalidArgumentException $e) {
$detail = $key . ': ' . $e->getMessage();
$log('[SKIP] ' . $detail);
$tpaResults[] = ['tpa' => $key, 'mapped' => 0, 'status' => 'skipped', 'detail' => $detail];
continue;
}
$offset = 0;
$tpaMapped = 0;
while (true) {
$builder = $db->table($table)
->select('id, file_id, ticket_id, client_policy_id')
->where('is_active', 1)
->where('ticket_id IS NOT NULL', null, false)
->orderBy('id', 'ASC')
->limit($limit, $offset);
if ($policyId > 0) {
$builder->where('client_policy_id', $policyId);
}
$dumpRows = $builder->get()->getResultArray();
if ($dumpRows === []) {
break;
}
$byFile = [];
foreach ($dumpRows as $row) {
$fileId = (int) ($row['file_id'] ?? 0);
$dumpId = (int) ($row['id'] ?? 0);
if ($fileId <= 0 || $dumpId <= 0) {
$totalSkipped++;
continue;
}
$byFile[$fileId][] = $dumpId;
}
foreach ($byFile as $fileId => $dumpIds) {
$result = $service->backfillClaimReportByDumpIds((int) $fileId, $dumpIds);
if (!$result['status']) {
$log("[FAIL] {$key} file_id={$fileId} upsert failed");
$errors++;
continue;
}
$tpaMapped += (int) $result['count'];
$totalMapped += (int) $result['count'];
}
$offset += count($dumpRows);
$log(" {$key}: processed {$offset} dump rows (mapped so far {$tpaMapped})");
if (count($dumpRows) < $limit) {
break;
}
}
$done = "[DONE] {$key} mapped≈{$tpaMapped}";
$log($done);
$tpaResults[] = [
'tpa' => $key,
'mapped' => $tpaMapped,
'status' => 'done',
'detail' => $done,
];
}
} else {
$phase = 'Skipping dump tables (ticket-only).';
$phases[] = $phase;
$log($phase);
}
if ($phase1Only) {
$skip = 'Phase 2 skipped (phase1=true — TPA dump tables only).';
$phases[] = $skip;
$log($skip);
$doneMsg = "Done. dump mapped≈{$totalMapped}, skipped={$totalSkipped}, errors={$errors}";
$log($doneMsg);
return $this->result(
$errors === 0,
$errors === 0 ? 'Phase 1 sync completed.' : 'Phase 1 sync completed with errors.',
$totalMapped,
$totalSkipped,
$errors,
$phases,
$tpaResults,
$logs,
$doneMsg
);
}
$phase2 = 'Phase 2: fill gaps from ticket_master dump-sourced claims...';
$phases[] = $phase2;
$log($phase2);
$tmResult = $this->syncFromTicketMaster($db, $policyId, $limit, $log);
if ($tmResult === false) {
return $this->result(false, 'ticket_master sync failed', $totalMapped, $totalSkipped, $errors + 1, $phases, $tpaResults, $logs);
}
$totalMapped += $tmResult['mapped'];
$totalSkipped += $tmResult['skipped'];
$doneMsg = "Done. dump+ticket mapped≈{$totalMapped}, skipped={$totalSkipped}, errors={$errors}";
$log($doneMsg);
return $this->result(
$errors === 0,
$errors === 0 ? 'Sync completed.' : 'Sync completed with errors. See summary.',
$totalMapped,
$totalSkipped,
$errors,
$phases,
$tpaResults,
$logs,
$doneMsg
);
}
/**
* @param callable(string):void $log
* @return array{mapped:int,skipped:int}|false
*/
private function syncFromTicketMaster($db, int $policyId, int $limit, callable $log)
{
if (!$db->tableExists('ticket_master')) {
$log('ticket_master missing');
return false;
}
$offset = 0;
$mapped = 0;
$skipped = 0;
$now = date('Y-m-d H:i:s');
while (true) {
$builder = $db->table('ticket_master')
->where('is_active', 1)
->groupStart()
->where('claim_dump_ref_id IS NOT NULL', null, false)
->orWhere('file_id IS NOT NULL', null, false)
->groupEnd()
->where("claim_number IS NOT NULL AND TRIM(claim_number) != ''", null, false)
->orderBy('id', 'ASC')
->limit($limit, $offset);
if ($policyId > 0) {
$builder->where('client_policy_id', $policyId);
}
$tickets = $builder->get()->getResultArray();
if ($tickets === []) {
break;
}
$rows = [];
foreach ($tickets as $tm) {
$claimNumber = trim((string) ($tm['claim_number'] ?? ''));
$clientPolicyId = (int) ($tm['client_policy_id'] ?? 0);
if ($claimNumber === '' || $clientPolicyId <= 0) {
$skipped++;
continue;
}
$sourceTable = null;
$tpaId = (int) ($tm['tpa_id'] ?? 0);
foreach ($this->tpaConfigs() as $cfg) {
if ($tpaId === (int) env($cfg['env'])) {
$sourceTable = $cfg['table'];
break;
}
}
$rows[] = [
'tpa_id' => $tm['tpa_id'] ?? null,
'client_id' => $tm['client_id'] ?? null,
'client_policy_id' => $clientPolicyId,
'file_id' => $tm['file_id'] ?? null,
'ticket_id' => $tm['id'] ?? null,
'source_table' => $sourceTable,
'source_row_id' => $tm['claim_dump_ref_id'] ?? null,
'claim_number' => $claimNumber,
'emp_code' => $tm['emp_code'] ?? null,
'tpa_no' => $tm['tpa_no'] ?? null,
'emp_id' => $tm['emp_id'] ?? null,
'insured_emp_id' => $tm['insured_emp_id'] ?? null,
'claim_amount' => $tm['claim_amount'] ?? null,
'approved_amount' => $tm['approved_amount'] ?? null,
'incurred_amount' => $tm['approved_amount'] ?? $tm['claim_amount'] ?? null,
'si_amt' => $tm['si_amt'] ?? null,
'tpa_claim_status' => $tm['tpa_claim_status'] ?? null,
'claim_status_id' => $tm['claim_status_id'] ?? null,
'tpa_claim_type' => $tm['tpa_claim_type'] ?? null,
'tpa_ailments' => $tm['tpa_ailments'] ?? null,
'doa' => $tm['doa'] ?? null,
'dod' => $tm['dod'] ?? null,
'date_of_intimat' => $tm['date_of_intimat'] ?? null,
'settled_date' => $tm['settled_date'] ?? null,
'approved_date' => $tm['approved_date'] ?? null,
'claim_dump_date' => $tm['claim_dump_date'] ?? null,
'hospital_name' => $tm['hospital_name'] ?? null,
'hospital_city' => $tm['hospital_city'] ?? null,
'hospital_state' => $tm['hospital_state'] ?? null,
'hospital_pin_code' => $tm['hospital_pin_code'] ?? null,
'hospital_address' => $tm['hospital_address'] ?? null,
'gender' => null,
'age' => null,
'relation' => $tm['relationship'] ?? null,
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
];
}
if ($rows !== [] && $this->upsertRows($db, $rows) === false) {
$log('ticket_master upsert failed at offset ' . $offset);
return false;
}
$mapped += count($rows);
$offset += count($tickets);
$log(" ticket_master: processed {$offset} rows (mapped {$mapped})");
if (count($tickets) < $limit) {
break;
}
}
return ['mapped' => $mapped, 'skipped' => $skipped];
}
/**
* @param list<array<string, mixed>> $rows
*/
private function upsertRows($db, array $rows): bool
{
$columns = [
'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id',
'source_table', 'source_row_id', 'claim_number', 'emp_code', 'tpa_no',
'emp_id', 'insured_emp_id', 'claim_amount', 'approved_amount', 'incurred_amount',
'si_amt', 'tpa_claim_status', 'claim_status_id', 'tpa_claim_type', 'tpa_ailments',
'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'claim_dump_date',
'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address',
'gender', 'age', 'relation', 'is_active', 'created_at', 'updated_at',
];
$updateCols = array_values(array_diff($columns, ['client_policy_id', 'claim_number', 'created_at']));
foreach (array_chunk($rows, 100) as $chunk) {
$placeholders = [];
$binds = [];
foreach ($chunk as $row) {
$rowPlaceholders = [];
foreach ($columns as $col) {
$rowPlaceholders[] = '?';
$binds[] = $row[$col] ?? null;
}
$placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')';
}
$updates = [];
foreach ($updateCols as $col) {
if (in_array($col, ['gender', 'age', 'relation', 'approved_amount', 'incurred_amount', 'tpa_ailments', 'tpa_claim_type', 'source_table', 'source_row_id'], true)) {
$updates[] = '`' . $col . '` = COALESCE(VALUES(`' . $col . '`), `' . $col . '`)';
} else {
$updates[] = '`' . $col . '` = VALUES(`' . $col . '`)';
}
}
$sql = 'INSERT INTO `claim_report` (`' . implode('`, `', $columns) . '`) VALUES '
. implode(', ', $placeholders)
. ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
if ($db->query($sql, $binds) === false) {
return false;
}
}
return true;
}
/**
* @param list<string> $phases
* @param list<array<string,mixed>> $tpaResults
* @param list<string> $logs
* @return array<string, mixed>
*/
private function result(
bool $status,
string $message,
int $mapped,
int $skipped,
int $errors,
array $phases,
array $tpaResults,
array $logs,
?string $summaryMessage = null
): array {
return [
'status' => $status,
'message' => $message,
'summary' => [
'mapped' => $mapped,
'skipped' => $skipped,
'errors' => $errors,
'message' => $summaryMessage,
],
'phases' => $phases,
'tpa_results' => $tpaResults,
'logs' => $logs,
];
}
}

View File

@ -41,17 +41,54 @@ class DigitExecutorService
*/ */
public function quickQuote(array $input): array public function quickQuote(array $input): array
{ {
$enquiryId = $input['enquiry_id'] ?? ('NH' . date('ymdHis') . random_int(100, 999)); $quoteIdIn = (int) ($input['quote_id'] ?? 0);
$payload = $this->buildQuickQuotePayload($input, $enquiryId); $existing = $quoteIdIn > 0 ? $this->quoteModel->find($quoteIdIn) : null;
// Prefer existing enquiry when updating; only mint a new one for brand-new journeys.
$enquiryId = null;
if ($existing && !empty($existing['enquiry_id'])) {
$enquiryId = (string) $existing['enquiry_id'];
} elseif (!empty($input['enquiry_id']) && $input['enquiry_id'] !== '— new —') {
$enquiryId = (string) $input['enquiry_id'];
if (!$existing) {
$existing = $this->quoteModel->where('enquiry_id', $enquiryId)->first();
}
}
if ($enquiryId === null || $enquiryId === '') {
$enquiryId = 'NH' . date('ymdHis') . random_int(100, 999);
}
$input['quote_id'] = $existing ? (int) $existing['id'] : $quoteIdIn;
$input['enquiry_id'] = $enquiryId;
$payload = $this->buildQuickQuotePayload($input, $enquiryId);
$quoteId = $this->persistQuoteShell($input, $enquiryId, 'DRAFT'); $quoteId = $this->persistQuoteShell($input, $enquiryId, 'DRAFT');
$response = $this->api->post( try {
$this->config->executorPath, $response = $this->api->post(
$payload, $this->config->executorPath,
$this->config->integrationIds['quickQuote'], $payload,
$quoteId $this->config->integrationIds['quickQuote'],
); $quoteId
);
} catch (DigitApiException $e) {
// Keep shell continuity on Digit failures so retries do not spawn duplicates.
throw new DigitApiException(
$e->getMessage(),
$e->getDigitCode(),
$e->getHttpStatus(),
[
'_nhance_shell' => [
'quote_id' => $quoteId,
'enquiry_id' => $enquiryId,
],
'digit' => $e->getResponseBody(),
],
$e,
$e->getRequestUrl(),
$e->getRequestBody()
);
}
$premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']); $premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']);
$idv = $this->pickNumber($response, ['idv', 'vehicleIDV', 'insuredDeclaredValue']); $idv = $this->pickNumber($response, ['idv', 'vehicleIDV', 'insuredDeclaredValue']);
@ -61,10 +98,13 @@ class DigitExecutorService
$vehicleIdv = $response['vehicle']['vehicleIDV'] ?? $response['vehicleIDV'] ?? []; $vehicleIdv = $response['vehicle']['vehicleIDV'] ?? $response['vehicleIDV'] ?? [];
// Re-quote resets create/KYC progress so journey stays consistent.
$this->quoteModel->update($quoteId, [ $this->quoteModel->update($quoteId, [
'premium' => $premium, 'premium' => $premium,
'idv' => $idv, 'idv' => $idv,
'status' => 'QUOTED', 'status' => 'QUOTED',
'quote_number' => null,
'application_id' => null,
'coverage_details' => json_encode($input['coverages'] ?? $payload['contract']['coverages'] ?? []), 'coverage_details' => json_encode($input['coverages'] ?? $payload['contract']['coverages'] ?? []),
]); ]);
@ -123,6 +163,21 @@ class DigitExecutorService
$input['vehicle_identification_number'] = $vin; $input['vehicle_identification_number'] = $vin;
$input['engine_number'] = $engine; $input['engine_number'] = $engine;
$policyholder = [
'first_name' => $input['first_name'] ?? null,
'last_name' => $input['last_name'] ?? null,
'mobile' => $input['mobile'] ?? null,
'email' => $input['email'] ?? null,
'pan' => $input['pan'] ?? null,
'dob' => $input['dob'] ?? null,
'address' => $input['address'] ?? null,
];
// Persist locally before Digit call so Back/reload keeps form data even if API fails.
$this->quoteModel->update($quoteId, [
'policyholder_details' => json_encode($policyholder),
'coverage_details' => json_encode($input['coverages'] ?? ($detail['coverage_details'] ?? [])),
]);
$payload = $this->buildCreateQuotePayload($detail, $input); $payload = $this->buildCreateQuotePayload($detail, $input);
$response = $this->api->post( $response = $this->api->post(
@ -137,12 +192,13 @@ class DigitExecutorService
$premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']) ?? $detail['premium']; $premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']) ?? $detail['premium'];
$this->quoteModel->update($quoteId, [ $this->quoteModel->update($quoteId, [
'quote_number' => $quoteNumber, 'quote_number' => $quoteNumber,
'application_id' => $applicationId, 'application_id' => $applicationId,
'premium' => $premium, 'premium' => $premium,
'start_date' => $input['start_date'] ?? $detail['start_date'], 'start_date' => $input['start_date'] ?? $detail['start_date'],
'end_date' => $input['end_date'] ?? $detail['end_date'], 'end_date' => $input['end_date'] ?? $detail['end_date'],
'status' => 'CREATED', 'status' => 'CREATED',
'policyholder_details' => json_encode($policyholder),
]); ]);
return [ return [
@ -472,10 +528,18 @@ class DigitExecutorService
protected function persistQuoteShell(array $input, string $enquiryId, string $status): int protected function persistQuoteShell(array $input, string $enquiryId, string $status): int
{ {
$existing = $this->quoteModel->where('enquiry_id', $enquiryId)->first(); $quoteIdIn = (int) ($input['quote_id'] ?? 0);
$existing = null;
if ($quoteIdIn > 0) {
$existing = $this->quoteModel->find($quoteIdIn);
}
if (!$existing && $enquiryId !== '') {
$existing = $this->quoteModel->where('enquiry_id', $enquiryId)->first();
}
$quoteData = [ $quoteData = [
'enquiry_id' => $enquiryId, 'enquiry_id' => $existing['enquiry_id'] ?? $enquiryId,
'policy_holder_type' => $input['policy_holder_type'] ?? 'INDIVIDUAL', 'policy_holder_type' => $input['policy_holder_type'] ?? 'INDIVIDUAL',
'insurance_product_code' => (string) ($input['insurance_product_code'] ?? '20101'), 'insurance_product_code' => (string) ($input['insurance_product_code'] ?? '20101'),
'sub_insurance_product_code' => (string) ($input['sub_insurance_product_code'] ?? 'PB'), 'sub_insurance_product_code' => (string) ($input['sub_insurance_product_code'] ?? 'PB'),
@ -487,9 +551,13 @@ class DigitExecutorService
'end_date' => $input['end_date'] ?? null, 'end_date' => $input['end_date'] ?? null,
'pincode' => (string) ($input['pincode'] ?? ''), 'pincode' => (string) ($input['pincode'] ?? ''),
'coverage_details' => json_encode($input['coverages'] ?? []), 'coverage_details' => json_encode($input['coverages'] ?? []),
'status' => $status,
]; ];
// Only stamp DRAFT on brand-new rows; existing rows keep status until QQ success.
if (!$existing) {
$quoteData['status'] = $status;
}
if ($existing) { if ($existing) {
$quoteId = (int) $existing['id']; $quoteId = (int) $existing['id'];
$this->quoteModel->update($quoteId, $quoteData); $this->quoteModel->update($quoteId, $quoteData);

View File

@ -0,0 +1,582 @@
<?php
namespace App\Libraries;
use App\Models\ClientModel;
use App\Models\LeadsModel;
use App\Models\RFQModel;
use Mpdf\Mpdf;
use Mpdf\Output\Destination;
/**
* Builds the GMC Quote Comparison Report PDF:
* cover (client) + static front + dynamic QCR pages + static back.
*/
class GmcQcrPdfService
{
public const GMC_POLICY_TYPE_IDS = [2, 3, 4, 5];
private const PAGE_W_MM = 338.6667; // 960 pt
private const PAGE_H_MM = 190.5; // 540 pt
private const ROWS_PER_PAGE = 18;
private const COLS_PER_PAGE = 5;
private RFQModel $rfqModel;
private LeadsModel $leadsModel;
private ClientModel $clientModel;
public function __construct(?RFQModel $rfqModel = null, ?LeadsModel $leadsModel = null, ?ClientModel $clientModel = null)
{
$this->rfqModel = $rfqModel ?? new RFQModel();
$this->leadsModel = $leadsModel ?? new LeadsModel();
$this->clientModel = $clientModel ?? new ClientModel();
}
public static function isGmc(int $policyTypeId): bool
{
return in_array($policyTypeId, self::GMC_POLICY_TYPE_IDS, true);
}
/**
* @return array{status:bool,message:string,file_name?:string,file_path?:string,download_name?:string}
*/
/**
* @param int $leadId
* @param array|null $selectedProposals If provided, only include these proposal names
*/
public function generate(int $leadId, ?array $selectedProposals = null): array
{
$lead = $this->leadsModel
->select('leads.*, policy_type.policy_type, policy_type.long_name')
->join('policy_type', 'leads.policy_type_id = policy_type.id', 'left')
->where('leads.id', $leadId)
->where('leads.is_active', 1)
->first();
if (! $lead) {
return ['status' => false, 'message' => 'Opportunity not found'];
}
if (! self::isGmc((int) ($lead['policy_type_id'] ?? 0))) {
return ['status' => false, 'message' => 'GMC QCR PDF is only available for GMC policies'];
}
// Primary: explicit QCR row (type=2)
$rfq = $this->rfqModel
->where('lead_id', $leadId)
->where('type', 2)
->where('is_active', 1)
->orderBy('id', 'desc')
->first();
// Fallback: some older leads/pages show latest active RFQ row on QCR view.
// If explicit QCR is missing, use latest active saved table JSON.
if (! $rfq || empty($rfq['json'])) {
$rfq = $this->rfqModel
->where('lead_id', $leadId)
->where('is_active', 1)
->orderBy('id', 'desc')
->first();
}
if (! $rfq || empty($rfq['json'])) {
return ['status' => false, 'message' => 'No saved proposal table data found. Please save this page first.'];
}
$qcrJson = json_decode($rfq['json'], true);
if (! is_array($qcrJson) || empty($qcrJson['table_data'])) {
return ['status' => false, 'message' => 'QCR data is invalid or empty'];
}
$clientName = trim((string) ($lead['client_name'] ?? 'Client'));
$logoPath = $this->resolveClientLogoPath((int) ($lead['client_id'] ?? 0));
$nhanceLogo = ROOTPATH . 'public/assets/images/Nhance-Logo-Final.png';
$coveragePages = $this->buildCoveragePages($qcrJson, $selectedProposals);
$premiumSections = $this->buildPremiumSections($qcrJson, $selectedProposals);
if (empty($coveragePages) && empty($premiumSections)) {
return ['status' => false, 'message' => 'No QCR-enabled proposals found to include in the PDF'];
}
$frontPdf = ROOTPATH . 'public/assets/qcr_gmc/static_front.pdf';
$backPdf = ROOTPATH . 'public/assets/qcr_gmc/static_back.pdf';
if (! is_file($frontPdf) || ! is_file($backPdf)) {
return ['status' => false, 'message' => 'Static GMC QCR template PDFs are missing'];
}
$tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'gmc_qcr_mpdf';
if (! is_dir($tempDir)) {
@mkdir($tempDir, 0775, true);
}
$mpdfNested = $tempDir . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfNested)) {
@mkdir($mpdfNested, 0775, true);
}
$outDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'qcr_pdf' . DIRECTORY_SEPARATOR;
if (! is_dir($outDir)) {
@mkdir($outDir, 0775, true);
}
$safeClient = preg_replace('/[^A-Za-z0-9_\- ]+/', '', $clientName) ?: 'Client';
$safeClient = trim(preg_replace('/\s+/', ' ', $safeClient));
$downloadName = $safeClient . ' GMC Quote comparison.-' . date('d-F-Y') . '.pdf';
$storedName = 'gmc_qcr_' . $leadId . '_' . time() . '_' . bin2hex(random_bytes(3)) . '.pdf';
$outPath = $outDir . $storedName;
try {
$mpdf = new Mpdf([
'tempDir' => $tempDir,
'mode' => 'utf-8',
'format' => [self::PAGE_W_MM, self::PAGE_H_MM],
'orientation' => 'P',
'margin_left' => 0,
'margin_right' => 0,
'margin_top' => 0,
'margin_bottom'=> 0,
]);
// 1) Cover
$mpdf->AddPageByArray($this->pageArray());
$mpdf->WriteHTML($this->renderCoverHtml($clientName, $logoPath, $nhanceLogo));
// 2) Static front (pages 27 from master)
$this->importPdfPages($mpdf, $frontPdf, $tempDir);
// 3) Dynamic coverage pages — one set per proposal (Quote Asked only)
foreach ($coveragePages as $page) {
$rowChunks = array_chunk($page['rows'], self::ROWS_PER_PAGE);
foreach ($rowChunks as $chunkIdx => $chunk) {
$mpdf->AddPageByArray($this->pageArray());
$mpdf->WriteHTML($this->renderCoverageHtml(
$page['title'],
$chunk,
$nhanceLogo,
$chunkIdx === 0
));
}
}
// 4) Quote Comparison — insurers with premium data
if (! empty($premiumSections)) {
$mpdf->AddPageByArray($this->pageArray());
$mpdf->WriteHTML($this->renderPremiumHtml($premiumSections, $nhanceLogo));
}
// 5) Static back (last 3)
$this->importPdfPages($mpdf, $backPdf, $tempDir);
$mpdf->Output($outPath, Destination::FILE);
} catch (\Throwable $e) {
log_message('error', 'GmcQcrPdfService::generate failed | lead_id=' . $leadId . ' | ' . $e->getMessage());
return ['status' => false, 'message' => 'PDF generation failed: ' . $e->getMessage()];
}
if (! is_file($outPath)) {
return ['status' => false, 'message' => 'PDF file was not created'];
}
$this->persistGeneratedPath($lead, $storedName, $downloadName);
return [
'status' => true,
'message' => 'GMC QCR PDF generated successfully',
'file_name' => $storedName,
'file_path' => $outPath,
'download_name' => $downloadName,
];
}
/**
* @return array{status:bool,message:string,file_path?:string,download_name?:string}
*/
public function resolveDownload(int $leadId): array
{
$lead = $this->leadsModel->where('id', $leadId)->where('is_active', 1)->first();
if (! $lead) {
return ['status' => false, 'message' => 'Opportunity not found'];
}
if (! self::isGmc((int) ($lead['policy_type_id'] ?? 0))) {
return ['status' => false, 'message' => 'GMC QCR PDF is only available for GMC policies'];
}
$misc = [];
if (! empty($lead['misc'])) {
$decoded = json_decode($lead['misc'], true);
if (is_array($decoded)) {
$misc = $decoded;
}
}
$storedName = $misc['gmc_qcr_pdf'] ?? null;
$downloadName = $misc['gmc_qcr_pdf_download_name'] ?? null;
if (! $storedName) {
return ['status' => false, 'message' => 'No generated PDF found. Please click Generate first.'];
}
$path = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'qcr_pdf' . DIRECTORY_SEPARATOR . $storedName;
if (! is_file($path)) {
return ['status' => false, 'message' => 'Generated PDF file is missing. Please Generate again.'];
}
if (! $downloadName) {
$clientName = trim((string) ($lead['client_name'] ?? 'Client'));
$downloadName = $clientName . ' GMC Quote comparison.-' . date('d-F-Y') . '.pdf';
}
return [
'status' => true,
'message' => 'OK',
'file_path' => $path,
'download_name' => $downloadName,
];
}
private function pageArray(): array
{
return [
'orientation' => 'P',
'sheet-size' => [self::PAGE_W_MM, self::PAGE_H_MM],
'margin-left' => 0,
'margin-right' => 0,
'margin-top' => 0,
'margin-bottom'=> 0,
];
}
private function resolveClientLogoPath(int $clientId): ?string
{
if ($clientId <= 0) {
return null;
}
$client = $this->clientModel->select('client_logo')->where('id', $clientId)->where('is_active', 1)->first();
if (empty($client['client_logo'])) {
return null;
}
$path = ROOTPATH . 'public/uploads/logo/' . $client['client_logo'];
return is_file($path) ? $path : null;
}
/**
* Build one coverage page set per QCR-enabled proposal.
* Each page shows only the "Quote Asked" column (2-col table like original).
*
* @return list<array{title:string,rows:list<array{particular:string,value:string}>}>
*/
private function buildCoveragePages(array $qcrJson, ?array $selectedProposals = null): array
{
$proposalMeta = $qcrJson['proposal_data']['over_all_column_data'] ?? [];
$headers = $qcrJson['table_data']['headers'] ?? [];
$dataRows = $qcrJson['table_data']['data'] ?? [];
// Collect QCR-enabled proposals that have a Quote Asked column
$proposals = [];
foreach ($headers as $header) {
$parent = (string) ($header['parentHeader'] ?? '');
if (in_array($parent, ['Sno', 'Item Key', 'Particulars', 'Action', ''], true)) {
continue;
}
$meta = $proposalMeta[$parent] ?? null;
$proposalQcr = $meta === null ? 1 : (int) ($meta['qcr'] ?? 0);
if ($proposalQcr !== 1) {
continue;
}
// Filter by user-selected proposals if provided
if ($selectedProposals !== null && ! in_array($parent, $selectedProposals, true)) {
continue;
}
$subs = $header['subHeaders'] ?? [];
if (in_array('Quote Asked', $subs, true) && ! isset($proposals[$parent])) {
$proposals[$parent] = true;
}
}
$pages = [];
foreach (array_keys($proposals) as $proposalName) {
$rows = [];
foreach ($dataRows as $row) {
$actionQcr = 1;
$particular = '';
$value = '';
foreach (($row['data'] ?? []) as $cell) {
$parent = (string) ($cell['parentth'] ?? '');
$sub = (string) ($cell['subth'] ?? '');
$raw = $cell['value'] ?? $cell['input_value'] ?? '';
if (is_array($raw)) {
if (isset($raw['qcr'])) {
$actionQcr = (int) $raw['qcr'];
}
$raw = '';
}
$text = trim(html_entity_decode(strip_tags((string) $raw)));
if ($parent === 'Particulars') {
$particular = $text;
} elseif ($parent === $proposalName && $sub === 'Quote Asked') {
$value = $text;
} elseif ($parent === 'Action' && is_array($cell['value'] ?? $cell['input_value'] ?? null)) {
$actionQcr = (int) (($cell['value'] ?? $cell['input_value'] ?? [])['qcr'] ?? 1);
}
}
if ($actionQcr !== 1 || $particular === '') {
continue;
}
$rows[] = ['particular' => $particular, 'value' => $value];
}
if (! empty($rows)) {
$pages[] = [
'title' => 'GMC Policy Coverage ' . $proposalName,
'rows' => $rows,
];
}
}
return $pages;
}
/**
* Build Quote Comparison sections from premium_data insurers only (not Quote Asked).
*
* @return list<array{title:string,rows:list<array{insurer:string,premium:string,gst:string,total:string}>}>
*/
private function buildPremiumSections(array $qcrJson, ?array $selectedProposals = null): array
{
$premiumData = $qcrJson['premium_data']['data'] ?? [];
$proposalMeta = $qcrJson['proposal_data']['over_all_column_data'] ?? [];
if (! is_array($premiumData) || empty($premiumData)) {
return [];
}
$sections = [];
foreach ($proposalMeta as $proposalName => $meta) {
if ((int) ($meta['qcr'] ?? 0) !== 1) {
continue;
}
if ($selectedProposals !== null && ! in_array($proposalName, $selectedProposals, true)) {
continue;
}
$insMap = $premiumData[$proposalName] ?? [];
if (! is_array($insMap)) {
continue;
}
$sectionRows = [];
foreach ($insMap as $insurerName => $metrics) {
if ($insurerName === 'Quote Asked' || $insurerName === '' || ! is_array($metrics)) {
continue;
}
$premium = trim((string) ($metrics['Premium'] ?? ''));
$gst = trim((string) ($metrics['GST Amount (₹)'] ?? $metrics['GST Amount'] ?? ''));
$total = trim((string) ($metrics['Total'] ?? ''));
if ($premium === '' && $total === '') {
continue;
}
if ($premium === 'Premium') {
continue;
}
$sectionRows[] = [
'insurer' => $insurerName,
'premium' => $premium,
'gst' => $gst,
'total' => $total,
];
}
if (! empty($sectionRows)) {
$sections[] = [
'title' => 'GMC Quote ' . $proposalName,
'rows' => $sectionRows,
];
}
}
return $sections;
}
private function importPdfPages(Mpdf $mpdf, string $pdfPath, string $tempDir): void
{
helper('merge_pdf');
try {
merge_ticket_pdf_add_pdf_pages($mpdf, $pdfPath, $tempDir);
} catch (\Throwable $e) {
// Fallback: import page-by-page with explicit sheet size
$pageCount = $mpdf->setSourceFile($pdfPath);
for ($p = 1; $p <= $pageCount; $p++) {
$tplId = $mpdf->importPage($p);
$mpdf->AddPageByArray($this->pageArray());
$mpdf->useTemplate($tplId, 0, 0, self::PAGE_W_MM, self::PAGE_H_MM, false);
}
}
}
private function persistGeneratedPath(array $lead, string $storedName, string $downloadName): void
{
$misc = [];
if (! empty($lead['misc'])) {
$decoded = json_decode($lead['misc'], true);
if (is_array($decoded)) {
$misc = $decoded;
}
}
// Remove previous file if present
if (! empty($misc['gmc_qcr_pdf'])) {
$old = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'qcr_pdf' . DIRECTORY_SEPARATOR . $misc['gmc_qcr_pdf'];
if (is_file($old) && $misc['gmc_qcr_pdf'] !== $storedName) {
@unlink($old);
}
}
$misc['gmc_qcr_pdf'] = $storedName;
$misc['gmc_qcr_pdf_download_name'] = $downloadName;
$misc['gmc_qcr_pdf_generated_at'] = date('Y-m-d H:i:s');
$this->leadsModel->update((int) $lead['id'], [
'misc' => json_encode($misc),
]);
}
private function escape(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
private function renderCoverHtml(string $clientName, ?string $logoPath, string $nhanceLogo): string
{
$logoHtml = '';
if ($logoPath) {
$logoHtml = '<div style="margin-bottom:8mm;"><img src="' . $this->escape($logoPath) . '" style="max-height:55mm;max-width:100mm;" /></div>';
}
$nhance = is_file($nhanceLogo)
? '<img src="' . $this->escape($nhanceLogo) . '" style="height:10mm;" />'
: '<span style="color:#0aa3a0;font-size:14pt;font-weight:bold;">Nhance</span>';
return '
<html><head><style>
@page { margin: 0; }
body { margin:0; padding:0; font-family: freeserif, DejaVu Serif, serif; }
.wrap { width:100%; height:190mm; position:relative; text-align:center; }
.spacer { height:48mm; }
.client { color:#0b7f7c; font-size:16pt; margin-bottom:6mm; font-family: freeserif, DejaVu Serif, serif; }
.title { font-size:24pt; color:#1a2744; font-family: freeserif, DejaVu Serif, serif; }
.title em { color:#0b7f7c; font-style:italic; }
.footer-logo { position:absolute; left:12mm; bottom:8mm; }
.footer-bar { position:absolute; right:0; bottom:0; width:28mm; height:8mm; background:#0aa3a0; }
</style></head><body>
<div class="wrap">
<div class="spacer">&nbsp;</div>
' . $logoHtml . '
<div class="client">' . $this->escape($clientName) . '</div>
<div class="title"><strong>Group</strong> <em>Medical Insurance</em> <strong>Quote</strong></div>
<div class="footer-logo">' . $nhance . '</div>
<div class="footer-bar"></div>
</div>
</body></html>';
}
/**
* Render a coverage page (2-column: Particulars + value) matching original style.
*
* @param list<array{particular:string,value:string}> $rows
*/
private function renderCoverageHtml(string $title, array $rows, string $nhanceLogo, bool $showTitle): string
{
$nhance = is_file($nhanceLogo)
? '<img src="' . $this->escape($nhanceLogo) . '" style="height:8mm;" />'
: '<span style="color:#0aa3a0;font-size:11pt;font-weight:bold;">Nhance</span>';
$thead = '<tr><th style="width:55%;">Particulars</th><th style="width:45%;">Expiring</th></tr>';
$tbody = '';
$i = 0;
foreach ($rows as $row) {
$bg = ($i % 2 === 0) ? '#ffffff' : '#f7fafa';
$tbody .= '<tr style="background:' . $bg . ';">';
$tbody .= '<td class="part">' . $this->escape($row['particular']) . '</td>';
$tbody .= '<td>' . $this->escape($row['value']) . '</td>';
$tbody .= '</tr>';
$i++;
}
$titleBlock = $showTitle
? '<div class="title"><span class="bar"></span><strong>GMC</strong> <em>' . $this->escape(str_replace('GMC ', '', $title)) . '</em></div>'
: '<div class="title-sm"><strong>GMC</strong> <em>' . $this->escape(str_replace('GMC ', '', $title)) . '</em> <span style="color:#666;font-size:9pt;">(continued)</span></div>';
return '
<html><head><style>
body { margin:0; padding:0; font-family: dejavusans, sans-serif; color:#222; }
.page { padding:8mm 14mm 14mm 14mm; }
.title { font-family: freeserif, DejaVu Serif, serif; color:#1a2744; font-size:20pt; margin:0 0 5mm 0; padding-left:5mm; border-left:3mm solid #0aa3a0; }
.title em { color:#0aa3a0; font-style:italic; }
.title-sm { font-family: freeserif, DejaVu Serif, serif; color:#1a2744; font-size:16pt; margin:0 0 4mm 0; padding-left:5mm; border-left:3mm solid #0aa3a0; }
.title-sm em { color:#0aa3a0; font-style:italic; }
table { width:100%; border-collapse:collapse; table-layout:fixed; margin-top:2mm; }
th { background:#1a7a6e; color:#fff; font-size:9pt; padding:2.8mm 3mm; text-align:center; vertical-align:middle; font-family: dejavusans, sans-serif; }
td { font-size:8pt; padding:2.2mm 3mm; border-bottom:0.15mm solid #e0eded; text-align:center; vertical-align:middle; word-wrap:break-word; }
td.part { text-align:center; color:#333; }
.footer { position:absolute; left:12mm; bottom:5mm; }
</style></head><body>
<div class="page">
' . $titleBlock . '
<table><thead>' . $thead . '</thead><tbody>' . $tbody . '</tbody></table>
</div>
<div class="footer">' . $nhance . '</div>
</body></html>';
}
/**
* @param list<array{title:string,rows:list<array{insurer:string,premium:string,gst:string,total:string}>}> $sections
*/
private function renderPremiumHtml(array $sections, string $nhanceLogo): string
{
$nhance = is_file($nhanceLogo)
? '<img src="' . $this->escape($nhanceLogo) . '" style="height:8mm;" />'
: '<span style="color:#0aa3a0;font-size:11pt;font-weight:bold;">Nhance</span>';
$body = '';
foreach ($sections as $section) {
$body .= '<div class="section-title">' . $this->escape($section['title']) . '</div>';
$body .= '<table><thead><tr>
<th style="width:40%;">Insurer Name</th>
<th style="width:20%;">Premium</th>
<th style="width:20%;">GST</th>
<th style="width:20%;">Total Premium</th>
</tr></thead><tbody>';
$i = 0;
foreach ($section['rows'] as $row) {
$bg = ($i % 2 === 0) ? '#ffffff' : '#f7fafa';
$body .= '<tr style="background:' . $bg . ';">
<td class="left">' . $this->escape($row['insurer']) . '</td>
<td>' . $this->escape($row['premium']) . '</td>
<td>' . $this->escape($row['gst']) . '</td>
<td>' . $this->escape($row['total']) . '</td>
</tr>';
$i++;
}
$body .= '</tbody></table>';
}
return '
<html><head><style>
body { margin:0; padding:0; font-family: dejavusans, sans-serif; color:#222; }
.page { padding:8mm 14mm 14mm 14mm; }
.title { font-family: freeserif, DejaVu Serif, serif; color:#1a2744; font-size:20pt; margin:0 0 6mm 0; border-left:3mm solid #0aa3a0; padding-left:5mm; }
.title em { color:#0aa3a0; font-style:italic; }
.section-title { font-size:10pt; font-weight:bold; margin:5mm 0 2mm 0; font-family: dejavusans, sans-serif; }
table { width:100%; border-collapse:collapse; margin-bottom:4mm; }
th { background:#1a7a6e; color:#fff; font-size:9pt; padding:2.8mm 3mm; text-align:center; font-family: dejavusans, sans-serif; }
td { font-size:8.5pt; padding:2.5mm 3mm; border-bottom:0.15mm solid #e0eded; text-align:center; }
td.left { text-align:left; }
.footer { position:absolute; left:12mm; bottom:5mm; }
</style></head><body>
<div class="page">
<div class="title"><strong>Quote</strong> <em>Comparison</em></div>
' . $body . '
</div>
<div class="footer">' . $nhance . '</div>
</body></html>';
}
}

View File

@ -115,18 +115,6 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
/** /**
* Extra dump claim_report fields beyond ticketMasterMapping. * Extra dump claim_report fields beyond ticketMasterMapping.
* report_column => dump_column
*/
protected $claimReportEnrichment = [
'approved_amount' => 'abhi_amount_less_coins_current_month',
'incurred_amount' => 'claimed_amount',
'tpa_ailments' => 'diagnosis',
'tpa_claim_type' => 'claim_type',
'gender' => 'gender',
'age' => 'patient_age',
'relation' => 'relation',
];
protected $statusMapping = [ protected $statusMapping = [
'Settled' => 11, 'Settled' => 11,
'Rejected' => 8, 'Rejected' => 8,

View File

@ -330,11 +330,6 @@ abstract class BaseTpaClaimImportService
return $this->failTicketMasterInsert($file_id, 'No data found to process.'); return $this->failTicketMasterInsert($file_id, 'No data found to process.');
} }
// Upsert normalized analytics rows into claim_report (same transaction)
if (!$this->syncClaimReportForJob2($file_id, $ticketMasterData)) {
return $this->failTicketMasterInsert($file_id, 'Claim report upsert failed');
}
// 2. Commit the transaction // 2. Commit the transaction
$this->db->transCommit(); $this->db->transCommit();
@ -1170,289 +1165,6 @@ abstract class BaseTpaClaimImportService
->countAllResults() > 0; ->countAllResults() > 0;
} }
/**
* Collect dump row IDs processed in Job 2 and upsert claim_report rows.
*/
protected function syncClaimReportForJob2(int $fileId, array $ticketMasterData): bool
{
$dumpIds = [];
foreach ($ticketMasterData['mapped_array'] ?? [] as $row) {
if (!empty($row['claim_dump_ref_id'])) {
$dumpIds[] = (int) $row['claim_dump_ref_id'];
}
}
foreach ($ticketMasterData['status_update_array'] ?? [] as $row) {
if (!empty($row['claim_dump_ref_id'])) {
$dumpIds[] = (int) $row['claim_dump_ref_id'];
}
}
foreach ($ticketMasterData['rejected_reason_array'] ?? [] as $row) {
// Linked existing tickets: ticket_id set, no reject reason
if (!empty($row['id']) && !empty($row['ticket_id']) && empty($row['master_reject_reason'])) {
$dumpIds[] = (int) $row['id'];
}
}
$dumpIds = array_values(array_unique(array_filter($dumpIds)));
if ($dumpIds === []) {
return true;
}
$rows = $this->mapClaimReportData($fileId, $dumpIds);
$this->logClaimDump('info', 'CLAIM_REPORT_UPSERT', [
'file_id' => $fileId,
'dump_ids' => count($dumpIds),
'report_rows' => count($rows),
]);
return $this->upsertClaimReport($rows);
}
/**
* Public backfill entry: map dump row IDs for a file into claim_report.
*
* @param list<int> $dumpRowIds
* @return array{status:bool,count:int}
*/
public function backfillClaimReportByDumpIds(int $fileId, array $dumpRowIds): array
{
$dumpRowIds = array_values(array_unique(array_filter(array_map('intval', $dumpRowIds))));
if ($fileId <= 0 || $dumpRowIds === []) {
return ['status' => true, 'count' => 0];
}
$rows = $this->mapClaimReportData($fileId, $dumpRowIds);
$ok = $this->upsertClaimReport($rows);
return ['status' => $ok, 'count' => count($rows)];
}
/**
* Map dump rows (+ linked ticket_master) into claim_report-shaped rows.
*
* @param list<int> $dumpRowIds
* @return list<array<string, mixed>>
*/
protected function mapClaimReportData(int $fileId, array $dumpRowIds): array
{
if ($fileId <= 0 || $dumpRowIds === []) {
return [];
}
$fileData = $this->claimDumpFileModel->where('id', $fileId)->first();
if (empty($fileData)) {
return [];
}
$tpaId = (int) ($fileData['tpa_id'] ?? 0);
$tpaTable = $this->tpaTableMapping[$tpaId] ?? null;
if ($tpaTable === null || !$this->db->tableExists($tpaTable)) {
return [];
}
$dumpRows = $this->db->table($tpaTable)
->whereIn('id', $dumpRowIds)
->where('is_active', 1)
->get()
->getResultArray();
if ($dumpRows === []) {
return [];
}
$ticketIds = [];
$refIds = [];
foreach ($dumpRows as $dump) {
if (!empty($dump['ticket_id'])) {
$ticketIds[] = (int) $dump['ticket_id'];
}
$refIds[] = (int) $dump['id'];
}
$ticketsById = [];
$ticketsByRef = [];
if ($ticketIds !== []) {
$ticketRows = $this->db->table('ticket_master')
->whereIn('id', array_values(array_unique($ticketIds)))
->get()
->getResultArray();
foreach ($ticketRows as $ticket) {
$ticketsById[(int) $ticket['id']] = $ticket;
}
}
if ($refIds !== []) {
$ticketRows = $this->db->table('ticket_master')
->where('file_id', $fileId)
->whereIn('claim_dump_ref_id', array_values(array_unique($refIds)))
->get()
->getResultArray();
foreach ($ticketRows as $ticket) {
$ticketsByRef[(int) $ticket['claim_dump_ref_id']] = $ticket;
$ticketsById[(int) $ticket['id']] = $ticket;
}
}
$mapping = property_exists($this, 'ticketMasterMapping') ? ($this->ticketMasterMapping ?? []) : [];
$enrichment = property_exists($this, 'claimReportEnrichment') ? ($this->claimReportEnrichment ?? []) : [];
$reportFieldsFromTicketMap = [
'claim_number', 'emp_code', 'tpa_no', 'claim_amount', 'approved_amount', 'si_amt',
'tpa_claim_status', 'tpa_claim_type', 'tpa_ailments',
'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'registration_date',
'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address',
'denial_reason', 'denial_date', 'utr_details', 'return_remark',
];
$out = [];
foreach ($dumpRows as $dump) {
$dumpId = (int) ($dump['id'] ?? 0);
$ticket = null;
if (!empty($dump['ticket_id']) && isset($ticketsById[(int) $dump['ticket_id']])) {
$ticket = $ticketsById[(int) $dump['ticket_id']];
} elseif (isset($ticketsByRef[$dumpId])) {
$ticket = $ticketsByRef[$dumpId];
}
// Skip dump rows that never became / linked to a ticket
if (empty($ticket) && empty($dump['ticket_id'])) {
continue;
}
$item = [
'tpa_id' => $tpaId ?: ($ticket['tpa_id'] ?? null),
'client_id' => $dump['client_id'] ?? $fileData['client_id'] ?? ($ticket['client_id'] ?? null),
'client_policy_id' => (int) ($dump['client_policy_id'] ?? $fileData['client_policy_id'] ?? ($ticket['client_policy_id'] ?? 0)),
'file_id' => $fileId,
'ticket_id' => $ticket['id'] ?? ($dump['ticket_id'] ?? null),
'source_table' => $tpaTable,
'source_row_id' => $dumpId,
'claim_dump_date' => $fileData['claim_dump_date'] ?? ($ticket['claim_dump_date'] ?? null),
'is_active' => 1,
];
foreach ($mapping as $dumpCol => $ticketCol) {
if (!in_array($ticketCol, $reportFieldsFromTicketMap, true)) {
continue;
}
// Map ticket-shaped columns that exist on claim_report
$reportCol = $ticketCol === 'registration_date' ? 'date_of_intimat' : $ticketCol;
if (!array_key_exists($reportCol, $item) || $item[$reportCol] === null) {
$item[$reportCol] = array_key_exists($dumpCol, $dump) ? $dump[$dumpCol] : null;
}
}
foreach ($enrichment as $reportCol => $dumpCol) {
if ($dumpCol === null || $dumpCol === '') {
continue;
}
$value = $dump[$dumpCol] ?? null;
if ($value !== null && $value !== '') {
$item[$reportCol] = $value;
}
}
if ($ticket) {
foreach (['emp_id', 'insured_emp_id', 'claim_status_id', 'emp_code', 'claim_number', 'claim_amount', 'approved_amount', 'tpa_claim_type', 'tpa_ailments', 'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address', 'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'si_amt', 'tpa_claim_status', 'tpa_no'] as $col) {
if ((!isset($item[$col]) || $item[$col] === null || $item[$col] === '') && isset($ticket[$col]) && $ticket[$col] !== null && $ticket[$col] !== '') {
$item[$col] = $ticket[$col];
}
}
if (empty($item['ticket_id'])) {
$item['ticket_id'] = $ticket['id'];
}
}
if (empty($item['incurred_amount'])) {
$item['incurred_amount'] = $item['approved_amount'] ?? $item['claim_amount'] ?? null;
}
$claimNumber = trim((string) ($item['claim_number'] ?? ''));
if ($claimNumber === '' || (int) ($item['client_policy_id'] ?? 0) <= 0) {
continue;
}
$item['claim_number'] = $claimNumber;
// Drop fields that are not on claim_report
unset($item['denial_reason'], $item['denial_date'], $item['utr_details'], $item['return_remark'], $item['registration_date']);
$out[] = $item;
}
return $out;
}
/**
* Insert or update claim_report by UNIQUE(client_policy_id, claim_number).
*
* @param list<array<string, mixed>> $rows
*/
protected function upsertClaimReport(array $rows): bool
{
if ($rows === []) {
return true;
}
if (!$this->db->tableExists('claim_report')) {
$this->logClaimDump('warning', 'CLAIM_REPORT_TABLE_MISSING', []);
return true;
}
$columns = [
'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id',
'source_table', 'source_row_id', 'claim_number', 'emp_code', 'tpa_no',
'emp_id', 'insured_emp_id', 'claim_amount', 'approved_amount', 'incurred_amount',
'si_amt', 'tpa_claim_status', 'claim_status_id', 'tpa_claim_type', 'tpa_ailments',
'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'claim_dump_date',
'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address',
'gender', 'age', 'relation', 'is_active', 'created_at', 'updated_at',
];
$updateCols = array_values(array_diff($columns, ['client_policy_id', 'claim_number', 'created_at']));
$now = date('Y-m-d H:i:s');
foreach (array_chunk($rows, 100) as $chunk) {
$placeholders = [];
$binds = [];
foreach ($chunk as $row) {
$rowPlaceholders = [];
foreach ($columns as $col) {
$rowPlaceholders[] = '?';
if ($col === 'created_at' || $col === 'updated_at') {
$binds[] = $now;
} elseif ($col === 'is_active') {
$binds[] = isset($row[$col]) ? (int) $row[$col] : 1;
} else {
$binds[] = $row[$col] ?? null;
}
}
$placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')';
}
$updates = [];
foreach ($updateCols as $col) {
if ($col === 'updated_at') {
$updates[] = '`updated_at` = VALUES(`updated_at`)';
} else {
$updates[] = '`' . $col . '` = VALUES(`' . $col . '`)';
}
}
$sql = 'INSERT INTO `claim_report` (`' . implode('`, `', $columns) . '`) VALUES '
. implode(', ', $placeholders)
. ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
if ($this->db->query($sql, $binds) === false) {
return false;
}
}
return true;
}
/** /**
* Map Excel rows to TPA table structure * Map Excel rows to TPA table structure
*/ */

View File

@ -177,19 +177,6 @@ class FhplClaimImportService extends BaseTpaClaimImportService
/** /**
* Extra dump claim_report fields beyond ticketMasterMapping. * Extra dump claim_report fields beyond ticketMasterMapping.
* report_column => dump_column
*/
protected $claimReportEnrichment = [
'approved_amount' => 'settled_amount',
'incurred_amount' => 'incurred_amount',
'tpa_ailments' => 'diagnosis',
'tpa_claim_type' => 'claim_type',
'gender' => 'gender',
'age' => 'years',
'relation' => 'relationship',
'si_amt' => 'coverage_amount',
];
protected $statusMapping = [ protected $statusMapping = [
'Settled' => 11, 'Settled' => 11,
'Rejected' => 8, 'Rejected' => 8,

View File

@ -106,19 +106,6 @@ class IciciClaimImportService extends BaseTpaClaimImportService
/** /**
* Extra dump claim_report fields beyond ticketMasterMapping. * Extra dump claim_report fields beyond ticketMasterMapping.
* report_column => dump_column
*/
protected $claimReportEnrichment = [
'approved_amount' => 'net_sanct_amt',
'incurred_amount' => 'claimed_amount',
'tpa_ailments' => 'diagnosis',
'tpa_claim_type' => 'type_of_claim',
'gender' => 'gender',
'age' => 'age',
'relation' => 'relation',
'si_amt' => 'sum_insured',
];
protected $statusMapping = [ protected $statusMapping = [
'PAID' => 11, 'PAID' => 11,
'SETTLED' => 11, 'SETTLED' => 11,

View File

@ -157,19 +157,6 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
/** /**
* Extra dump claim_report fields beyond ticketMasterMapping. * Extra dump claim_report fields beyond ticketMasterMapping.
* report_column => dump_column
*/
protected $claimReportEnrichment = [
'approved_amount' => 'claim_approved_amount',
'incurred_amount' => 'incurred_amount',
'tpa_ailments' => 'primary_ailment_name',
'tpa_claim_type' => 'claim_type',
'gender' => 'benef_gender',
'age' => 'benef_age',
'relation' => 'benef_relation',
'si_amt' => 'benef_sum_insured',
];
protected $statusMapping = [ protected $statusMapping = [
'Settled' => 11, 'Settled' => 11,
'Rejected' => 8, 'Rejected' => 8,

View File

@ -94,19 +94,6 @@ class RcareClaimImportService extends BaseTpaClaimImportService
/** /**
* Extra dump claim_report fields beyond ticketMasterMapping. * Extra dump claim_report fields beyond ticketMasterMapping.
* report_column => dump_column
*/
protected $claimReportEnrichment = [
'approved_amount' => 'net_sanction_amount',
'incurred_amount' => 'claimed_amount',
'tpa_ailments' => 'diagnosis',
'tpa_claim_type' => 'member_reimbursement_cl_type',
'gender' => 'gender',
'age' => 'age',
'relation' => 'relation',
'si_amt' => 'sum_insured',
];
protected $statusMapping = [ protected $statusMapping = [
'CL Paid with Settlement Letter' => 11, 'CL Paid with Settlement Letter' => 11,
'Settled' => 11, 'Settled' => 11,

View File

@ -350,19 +350,6 @@ class VidalClaimImportService extends BaseTpaClaimImportService
/** /**
* Extra dump claim_report fields beyond ticketMasterMapping. * Extra dump claim_report fields beyond ticketMasterMapping.
* report_column => dump_column
*/
protected $claimReportEnrichment = [
'approved_amount' => 'approved_amount',
'incurred_amount' => 'total_incurred_amount',
'tpa_ailments' => 'diagnosis',
'tpa_claim_type' => 'type_of_claim',
'gender' => 'gender',
'age' => 'age',
'relation' => 'relation',
'si_amt' => 'sum_insured',
];
protected $statusMapping = [ protected $statusMapping = [
'Settled' => 11, 'Settled' => 11,
'Rejected' => 8, 'Rejected' => 8,

View File

@ -3,7 +3,8 @@
namespace App\Models; namespace App\Models;
/** /**
* Claims Collection dashboard backed by claim_report with ticket_master fallback. * Claims Collection dashboard backed by claim_report.
* Falls back to ticket_master only for non-dump TPAs when env flag is true.
* Reuses V2 KPI SQL; claim fact tables are rewritten at query time. * Reuses V2 KPI SQL; claim fact tables are rewritten at query time.
*/ */
class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel
@ -13,7 +14,10 @@ class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel
/** /**
* Resolve claim fact table for a policy. * Resolve claim fact table for a policy.
* Uses claim_report when TPA dump table exists and claim_report has rows; else ticket_master. *
* - TPA dump table exists always claim_report (even if empty; never ticket_master).
* - Dump table missing / TPA unmapped ticket_master only when
* CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER=true; otherwise claim_report.
*/ */
public function resolveClaimsTable(int $policyId): string public function resolveClaimsTable(int $policyId): string
{ {
@ -21,11 +25,18 @@ class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel
return $this->claimsTableCache[$policyId]; return $this->claimsTableCache[$policyId];
} }
$table = 'ticket_master'; $db = \Config\Database::connect($this->DBGroup);
$db = \Config\Database::connect($this->DBGroup); $fallbackEnabled = $this->isTicketMasterFallbackEnabled();
if (!$db->tableExists('claim_report') || $policyId <= 0) { if ($policyId <= 0) {
return $this->claimsTableCache[$policyId] = $table; return $this->claimsTableCache[$policyId] = $fallbackEnabled
? 'ticket_master'
: 'claim_report';
}
// Table missing — cannot query claim_report; ticket_master is the only option.
if (!$db->tableExists('claim_report')) {
return $this->claimsTableCache[$policyId] = 'ticket_master';
} }
$policy = $db->table('client_policy') $policy = $db->table('client_policy')
@ -34,23 +45,26 @@ class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel
->get() ->get()
->getRowArray(); ->getRowArray();
$tpaId = (int) ($policy['tpa_id'] ?? 0); $tpaId = (int) ($policy['tpa_id'] ?? 0);
$dumpTable = $this->getTpaDumpTableMap()[$tpaId] ?? null; $dumpTable = $this->getTpaDumpTableMap()[$tpaId] ?? null;
if ($dumpTable === null || !$db->tableExists($dumpTable)) { // Mapped dump TPA with existing dump table → claim_report only.
return $this->claimsTableCache[$policyId] = $table; if ($dumpTable !== null && $db->tableExists($dumpTable)) {
return $this->claimsTableCache[$policyId] = 'claim_report';
} }
$hasReportRows = $db->table('claim_report') // Non-dump / missing dump table → env-gated ticket_master fallback.
->where('client_policy_id', $policyId) return $this->claimsTableCache[$policyId] = $fallbackEnabled
->where('is_active', 1) ? 'ticket_master'
->countAllResults() > 0; : 'claim_report';
}
if ($hasReportRows) { protected function isTicketMasterFallbackEnabled(): bool
$table = 'claim_report'; {
} return filter_var(
env('CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER', false),
return $this->claimsTableCache[$policyId] = $table; FILTER_VALIDATE_BOOLEAN
);
} }
/** /**

View File

@ -17,7 +17,7 @@ class MotorQuoteModel extends Model
'insurance_product_code', 'sub_insurance_product_code', 'insurance_product_code', 'sub_insurance_product_code',
'previous_insurer_code', 'previous_policy_expiry_date', 'external_policy_number', 'previous_insurer_code', 'previous_policy_expiry_date', 'external_policy_number',
'is_ncb_transfer', 'start_date', 'end_date', 'pincode', 'is_ncb_transfer', 'start_date', 'end_date', 'pincode',
'coverage_details', 'premium', 'idv', 'status', 'coverage_details', 'policyholder_details', 'premium', 'idv', 'status',
'created_at', 'updated_at', 'created_at', 'updated_at',
]; ];
protected $useTimestamps = true; protected $useTimestamps = true;
@ -67,6 +67,12 @@ class MotorQuoteModel extends Model
if (!empty($quote['coverage_details']) && is_string($quote['coverage_details'])) { if (!empty($quote['coverage_details']) && is_string($quote['coverage_details'])) {
$quote['coverage_details'] = json_decode($quote['coverage_details'], true); $quote['coverage_details'] = json_decode($quote['coverage_details'], true);
} }
if (!empty($quote['policyholder_details']) && is_string($quote['policyholder_details'])) {
$quote['policyholder_details'] = json_decode($quote['policyholder_details'], true);
}
if (!is_array($quote['policyholder_details'] ?? null)) {
$quote['policyholder_details'] = [];
}
return $quote; return $quote;
} }

View File

@ -6,6 +6,11 @@ $kyc = $quote['kyc'] ?? [];
$payment = $quote['payment'] ?? []; $payment = $quote['payment'] ?? [];
$policy = $quote['policy'] ?? []; $policy = $quote['policy'] ?? [];
$status = $quote['status'] ?? 'DRAFT'; $status = $quote['status'] ?? 'DRAFT';
$policyholder = $policyholder ?? ($quote['policyholder_details'] ?? []);
if (!is_array($policyholder)) {
$policyholder = [];
}
$vehicleMaster = $vehicle_master ?? null;
$statusToStep = [ $statusToStep = [
'DRAFT' => 0, 'DRAFT' => 0,
@ -17,6 +22,18 @@ $statusToStep = [
]; ];
$initialStep = $statusToStep[$status] ?? 0; $initialStep = $statusToStep[$status] ?? 0;
$coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_details'] : []; $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_details'] : [];
// Normalize addon flags from either UI-shaped or Digit-shaped coverage JSON
$addonOn = [
'personal_accident' => !empty($coverage['personal_accident']) || !empty($coverage['personalAccident']['selection']),
'zero_dep' => !empty($coverage['zero_dep']) || !empty($coverage['addons']['partsDepreciation']['selection']),
'engine_protect' => !empty($coverage['engine_protect']) || !empty($coverage['addons']['engineProtection']['selection']),
'rsa' => !empty($coverage['rsa']) || !empty($coverage['addons']['roadSideAssistance']['selection']),
'consumables' => !empty($coverage['consumables']) || !empty($coverage['addons']['consumables']['selection']),
'key_protect' => !empty($coverage['key_protect']) || !empty($coverage['addons']['keyAndLockProtect']['selection']),
];
if ($coverage === []) {
$addonOn['personal_accident'] = true; // default matches chip markup
}
?> ?>
<style> <style>
:root{ :root{
@ -105,8 +122,10 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
.dm-section:first-of-type{margin-top:0;padding-top:0;border-top:none;} .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{display:grid;grid-template-columns:repeat(2,1fr);gap:14px 16px;}
.dm-grid.g3{grid-template-columns:repeat(3,1fr);} .dm-grid.g3{grid-template-columns:repeat(3,1fr);}
.dm-grid.g4{grid-template-columns:repeat(4,1fr);}
.dm-field{display:flex;flex-direction:column;gap:6px;} .dm-field{display:flex;flex-direction:column;gap:6px;}
.dm-field.full{grid-column:1/-1;} .dm-field.full{grid-column:1/-1;}
.dm-field.span2{grid-column:span 2;}
.dm-field label{font-size:12px;color:var(--dm-muted);font-weight:500;margin:0;} .dm-field label{font-size:12px;color:var(--dm-muted);font-weight:500;margin:0;}
.dm-field label .req{color:var(--dm-red);margin-left:2px;font-weight:700;} .dm-field label .req{color:var(--dm-red);margin-left:2px;font-weight:700;}
.dm-field input,.dm-field select{ .dm-field input,.dm-field select{
@ -188,7 +207,7 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
.dm-suggest .meta{color:var(--dm-muted);font-size:11px;} .dm-suggest .meta{color:var(--dm-muted);font-size:11px;}
.dm-vcode-tag{font-family:monospace;font-size:12px;color:var(--dm-teal-dark);margin-top:4px;} .dm-vcode-tag{font-family:monospace;font-size:12px;color:var(--dm-teal-dark);margin-top:4px;}
@media(max-width:768px){ @media(max-width:768px){
.dm-grid,.dm-grid.g3,.dm-quote-cards{grid-template-columns:1fr;} .dm-grid,.dm-grid.g3,.dm-grid.g4,.dm-quote-cards{grid-template-columns:1fr;}
.dm-stop{width:52px;} .dm-stop{width:52px;}
.dm-stop-label{font-size:10px;} .dm-stop-label{font-size:10px;}
.dm-road{left:22px;right:22px;} .dm-road{left:22px;right:22px;}
@ -227,7 +246,7 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</div> </div>
<div class="dm-section">Vehicle</div> <div class="dm-section">Vehicle</div>
<div class="dm-grid g3"> <div class="dm-grid g4">
<div class="dm-field"><label>Registration number <span class="req">*</span></label><input id="f_reg" value="<?= esc($vehicle['license_plate_number'] ?? '') ?>" placeholder="GJ04DA8726" data-required="1" data-label="Registration number"><div class="dm-err">Required</div></div> <div class="dm-field"><label>Registration number <span class="req">*</span></label><input id="f_reg" value="<?= esc($vehicle['license_plate_number'] ?? '') ?>" placeholder="GJ04DA8726" data-required="1" data-label="Registration number"><div class="dm-err">Required</div></div>
<div class="dm-field"> <div class="dm-field">
<label>Make <span class="req">*</span></label> <label>Make <span class="req">*</span></label>
@ -268,7 +287,7 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</div> </div>
<div class="dm-section">Previous policy</div> <div class="dm-section">Previous policy</div>
<div class="dm-grid g3"> <div class="dm-grid g4">
<div class="dm-field"><label>Previous insurer</label> <div class="dm-field"><label>Previous insurer</label>
<select id="f_prev_ins"><option value=""> none / unknown </option></select> <select id="f_prev_ins"><option value=""> none / unknown </option></select>
</div> </div>
@ -289,12 +308,12 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
<div class="dm-section">Coverage add-ons</div> <div class="dm-section">Coverage add-ons</div>
<div class="dm-chips" id="addonChips"> <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<?= !empty($addonOn['personal_accident']) ? ' 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<?= !empty($addonOn['zero_dep']) ? ' on' : '' ?>" 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<?= !empty($addonOn['engine_protect']) ? ' on' : '' ?>" 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<?= !empty($addonOn['rsa']) ? ' on' : '' ?>" 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<?= !empty($addonOn['consumables']) ? ' on' : '' ?>" 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 class="dm-chip<?= !empty($addonOn['key_protect']) ? ' on' : '' ?>" data-key="key_protect" onclick="this.classList.toggle('on')">Key & lock</div>
</div> </div>
<div class="dm-actions"> <div class="dm-actions">
@ -330,14 +349,14 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</div> </div>
<div class="dm-section">Policyholder</div> <div class="dm-section">Policyholder</div>
<div class="dm-grid"> <div class="dm-grid g4">
<div class="dm-field"><label>First name <span class="req">*</span></label><input id="f_fname" value="" data-required="1" data-label="First name"><div class="dm-err">Required</div></div> <div class="dm-field"><label>First name <span class="req">*</span></label><input id="f_fname" value="<?= esc($policyholder['first_name'] ?? '') ?>" data-required="1" data-label="First name"><div class="dm-err">Required</div></div>
<div class="dm-field"><label>Last name</label><input id="f_lname" value=""></div> <div class="dm-field"><label>Last name</label><input id="f_lname" value="<?= esc($policyholder['last_name'] ?? '') ?>"></div>
<div class="dm-field"><label>Mobile <span class="req">*</span></label><input id="f_mobile" value="" data-required="1" data-label="Mobile" data-len="10"><div class="dm-err">Required (10 digits)</div></div> <div class="dm-field"><label>Mobile <span class="req">*</span></label><input id="f_mobile" value="<?= esc($policyholder['mobile'] ?? '') ?>" data-required="1" data-label="Mobile" data-len="10"><div class="dm-err">Required (10 digits)</div></div>
<div class="dm-field"><label>Email <span class="req">*</span></label><input id="f_email" type="email" value="" data-required="1" data-label="Email"><div class="dm-err">Required</div></div> <div class="dm-field"><label>Email <span class="req">*</span></label><input id="f_email" type="email" value="<?= esc($policyholder['email'] ?? '') ?>" data-required="1" data-label="Email"><div class="dm-err">Required</div></div>
<div class="dm-field"><label>PAN <span class="req">*</span></label><input id="f_pan" value="" data-required="1" data-label="PAN"><div class="dm-err">Required</div></div> <div class="dm-field"><label>PAN <span class="req">*</span></label><input id="f_pan" value="<?= esc($policyholder['pan'] ?? '') ?>" data-required="1" data-label="PAN"><div class="dm-err">Required</div></div>
<div class="dm-field"><label>Date of birth <span class="req">*</span></label><input type="date" id="f_dob" value="" data-required="1" data-label="Date of birth"><div class="dm-err">Required</div></div> <div class="dm-field"><label>Date of birth <span class="req">*</span></label><input type="date" id="f_dob" value="<?= esc($policyholder['dob'] ?? '') ?>" data-required="1" data-label="Date of birth"><div class="dm-err">Required</div></div>
<div class="dm-field full"><label>Address <span class="req">*</span></label><input id="f_address" value="" data-required="1" data-label="Address"><div class="dm-err">Required</div></div> <div class="dm-field span2"><label>Address <span class="req">*</span></label><input id="f_address" value="<?= esc($policyholder['address'] ?? '') ?>" data-required="1" data-label="Address"><div class="dm-err">Required</div></div>
</div> </div>
<div class="dm-actions"> <div class="dm-actions">
@ -470,11 +489,16 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
<script> <script>
window.DM = { window.DM = {
quoteId: <?= $quoteId ? (int)$quoteId : 'null' ?>, quoteId: <?= $quoteId ? (int)$quoteId : 'null' ?>,
enquiryId: <?= json_encode($quote['enquiry_id'] ?? null) ?>,
base: '<?= rtrim(base_url('digit-motor'), '/') ?>', base: '<?= rtrim(base_url('digit-motor'), '/') ?>',
step: <?= (int)$initialStep ?>, step: <?= (int)$initialStep ?>,
selectedProduct: '<?= esc($quote['insurance_product_code'] ?? '20102') ?>', selectedProduct: '<?= esc($quote['insurance_product_code'] ?? '20102') ?>',
selectedInsurer: '<?= esc($quote['previous_insurer_code'] ?? '') ?>', selectedInsurer: '<?= esc($quote['previous_insurer_code'] ?? '') ?>',
selectedVehicleCode: '<?= esc($vehicle['vehicle_maincode'] ?? '') ?>' selectedVehicleCode: '<?= esc($vehicle['vehicle_maincode'] ?? '') ?>',
vehicleMake: <?= json_encode($vehicleMaster['make'] ?? '') ?>,
vehicleModel: <?= json_encode($vehicleMaster['model'] ?? '') ?>,
vehicleVariant: <?= json_encode($vehicleMaster['variant'] ?? '') ?>,
busy: false
}; };
const dmSteps = ["Quote","Create","KYC","Pay","Policy"]; const dmSteps = ["Quote","Create","KYC","Pay","Policy"];
@ -495,8 +519,23 @@ let dmSearchTimer = null;
dmGo(window.DM.step, true); dmGo(window.DM.step, true);
dmLoadMasters(); dmLoadMasters();
dmBindMasterUi(); dmBindMasterUi();
dmHydrateVehicle();
})(); })();
function dmHydrateVehicle(){
const make = window.DM.vehicleMake || '';
const model = window.DM.vehicleModel || '';
const code = window.DM.selectedVehicleCode || '';
if (!code && !make) return;
if (make) {
$('#f_make').val(make);
dmLoadModels(make, model, code);
} else if (code) {
$('#f_vcode').val(code);
$('#f_vcode_tag').text('Code ' + code);
}
}
function dmFillSelect(sel, items, valueKey, labelFn, selected, emptyLabel){ function dmFillSelect(sel, items, valueKey, labelFn, selected, emptyLabel){
const $s = $(sel); const $s = $(sel);
$s.empty(); $s.empty();
@ -694,7 +733,18 @@ function dmAlert(msg, ok){
} }
function dmBusy(on){ function dmBusy(on){
document.getElementById('dmPanel').classList.toggle('dm-loading', !!on); window.DM.busy = !!on;
const panel = document.getElementById('dmPanel');
if (panel) panel.classList.toggle('dm-loading', !!on);
// NHANCE global page loader (layout header)
if (on) {
$('.loader').show();
$('.loader-mask').fadeIn();
} else {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
}
} }
function dmAddonFlags(){ function dmAddonFlags(){
@ -777,6 +827,8 @@ function dmValidateRequired(fieldIds){
} }
function dmQuickQuote(){ function dmQuickQuote(){
if (window.DM.busy) return;
const claimChip = document.querySelector('.dm-chip[data-group="claim"].on'); const claimChip = document.querySelector('.dm-chip[data-group="claim"].on');
const missing = dmValidateRequired([ const missing = dmValidateRequired([
'f_reg', 'f_make', 'f_model', 'f_variant', 'f_vcode', 'f_reg', 'f_make', 'f_model', 'f_variant', 'f_vcode',
@ -789,7 +841,13 @@ function dmQuickQuote(){
return; return;
} }
const tagEnquiry = ($('#enquiryIdTag').text() || '').trim();
const enquiryId = window.DM.enquiryId
|| (tagEnquiry && tagEnquiry !== '— new —' ? tagEnquiry : null);
const body = { const body = {
quote_id: window.DM.quoteId || null,
enquiry_id: enquiryId,
license_plate_number: $('#f_reg').val().trim(), license_plate_number: $('#f_reg').val().trim(),
vehicle_maincode: $('#f_vcode').val().trim(), vehicle_maincode: $('#f_vcode').val().trim(),
registration_date: $('#f_reg_date').val(), registration_date: $('#f_reg_date').val(),
@ -805,8 +863,7 @@ function dmQuickQuote(){
previous_ncb: $('#f_prev_ncb').val() || 'ZERO', previous_ncb: $('#f_prev_ncb').val() || 'ZERO',
previous_policy_type: $('#f_prev_ptype').val() || '', previous_policy_type: $('#f_prev_ptype').val() || '',
is_claim_in_last_year: claimChip ? claimChip.dataset.val === '1' : false, is_claim_in_last_year: claimChip ? claimChip.dataset.val === '1' : false,
coverages: dmAddonFlags(), coverages: dmAddonFlags()
enquiry_id: window.DM.quoteId ? ($('#enquiryIdTag').text().trim() !== '— new —' ? $('#enquiryIdTag').text().trim() : null) : null
}; };
if (body.start_date){ if (body.start_date){
@ -821,6 +878,7 @@ function dmQuickQuote(){
.done(function(res){ .done(function(res){
if (!res.status){ dmAlert(res.message || 'Quick quote failed'); return; } if (!res.status){ dmAlert(res.message || 'Quick quote failed'); return; }
window.DM.quoteId = res.data.quote_id; window.DM.quoteId = res.data.quote_id;
window.DM.enquiryId = res.data.enquiry_id;
$('#enquiryIdTag').text(res.data.enquiry_id); $('#enquiryIdTag').text(res.data.enquiry_id);
const prem = res.data.premium != null ? Number(res.data.premium) : null; const prem = res.data.premium != null ? Number(res.data.premium) : null;
const idv = res.data.idv != null ? Number(res.data.idv) : null; const idv = res.data.idv != null ? Number(res.data.idv) : null;
@ -840,12 +898,21 @@ function dmQuickQuote(){
const parts = Object.keys(errs).map(function(k){ return errs[k]; }); const parts = Object.keys(errs).map(function(k){ return errs[k]; });
if (parts.length) msg = parts.join(' | '); if (parts.length) msg = parts.join(' | ');
} }
// If shell was created, keep continuity for retry
if (xhr.responseJSON && xhr.responseJSON.data) {
if (xhr.responseJSON.data.quote_id) window.DM.quoteId = xhr.responseJSON.data.quote_id;
if (xhr.responseJSON.data.enquiry_id) {
window.DM.enquiryId = xhr.responseJSON.data.enquiry_id;
$('#enquiryIdTag').text(xhr.responseJSON.data.enquiry_id);
}
}
dmAlert(msg); dmAlert(msg);
}) })
.always(function(){ dmBusy(false); }); .always(function(){ dmBusy(false); });
} }
function dmCreateQuote(){ function dmCreateQuote(){
if (window.DM.busy) return;
if (!window.DM.quoteId){ dmAlert('Run quick quote first.'); return; } if (!window.DM.quoteId){ dmAlert('Run quick quote first.'); return; }
// Keep QQ + CQ chassis/engine in sync when editing on step 2 // Keep QQ + CQ chassis/engine in sync when editing on step 2

View File

@ -73,6 +73,8 @@ function openFilterNav(){ document.getElementById('digit-filter-sidebar').style.
function closeFilterNav(){ document.getElementById('digit-filter-sidebar').style.width = '0'; } function closeFilterNav(){ document.getElementById('digit-filter-sidebar').style.width = '0'; }
function applyFilters(){ function applyFilters(){
$('.loader').show();
$('.loader-mask').fadeIn();
$.ajax({ $.ajax({
url: '<?= base_url('digit-motor/list') ?>', url: '<?= base_url('digit-motor/list') ?>',
type: 'POST', type: 'POST',
@ -92,6 +94,10 @@ function applyFilters(){
error: function(){ error: function(){
if (window.toastr) toastr.error('Failed to load filtered list.', 'Error'); if (window.toastr) toastr.error('Failed to load filtered list.', 'Error');
else alert('Failed to load filtered list.'); else alert('Failed to load filtered list.');
},
complete: function(){
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
} }
}); });
} }

View File

@ -535,6 +535,24 @@
<button id="submitPlacement" class="btn btn-primary" onclick="checkTheTableDataChanged(6)">Placement</button> <button id="submitPlacement" class="btn btn-primary" onclick="checkTheTableDataChanged(6)">Placement</button>
<button id="viewDemography" class="btn btn-primary" onclick="viewDemography()">Demography</button> <button id="viewDemography" class="btn btn-primary" onclick="viewDemography()">Demography</button>
<button id="viewDocs" class="btn btn-primary" onclick="viewDocs()">Docs</button> <button id="viewDocs" class="btn btn-primary" onclick="viewDocs()">Docs</button>
<?php if (in_array((int) ($lead_data['policy_type_id'] ?? 0), [2, 3, 4, 5], true)) { ?>
<div class="btn-group" id="pptBtnGroup" style="display:none;">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
PPT
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#" onclick="startPptGeneration(); return false;">Generate</a>
<a class="dropdown-item" href="#" onclick="downloadGmcQcrPdf(); return false;">Download</a>
</div>
</div>
<?php } ?>
<!-- Floating Generate PDF button (hidden until proposal selection mode) -->
<div id="pptFloatingBar" style="display:none; position:fixed; bottom:20px; left:50%; transform:translateX(-50%); z-index:9999; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.25); padding:10px 20px; text-align:center;">
<span style="font-size:13px; color:#555; margin-right:10px;">Select proposals to include in PDF, then click:</span>
<button type="button" class="btn btn-success" onclick="confirmGeneratePdf()">Generate PDF</button>
<button type="button" class="btn btn-secondary ml-2" onclick="cancelPptSelection()">Cancel</button>
</div>
<input type="hidden" id="lead_id" name="lead_id" value="<?= isset($lead_id) ? $lead_id : '' ?>"> <input type="hidden" id="lead_id" name="lead_id" value="<?= isset($lead_id) ? $lead_id : '' ?>">
<input type="hidden" id="rfq_primaryKey" name="rfq_primaryKey" value="<?= isset($rfq_data['id']) ? $rfq_data['id'] : '' ?>"> <input type="hidden" id="rfq_primaryKey" name="rfq_primaryKey" value="<?= isset($rfq_data['id']) ? $rfq_data['id'] : '' ?>">
<input type="hidden" id="qcr_count" value="<?= isset($qcr_count) ? $qcr_count : 0 ?>"> <input type="hidden" id="qcr_count" value="<?= isset($qcr_count) ? $qcr_count : 0 ?>">
@ -1328,6 +1346,7 @@
$('#submitData').text('Save RFQ'); $('#submitData').text('Save RFQ');
$('#submitMail').text('Send Insurer Mail'); $('#submitMail').text('Send Insurer Mail');
$('#submitPlacement').hide(); $('#submitPlacement').hide();
$('#pptBtnGroup').hide();
document.getElementById("second_table")?.remove(); document.getElementById("second_table")?.remove();
if(data){ if(data){
@ -1353,6 +1372,7 @@
$('#second_table').show(); $('#second_table').show();
$('#submitPlacement').show(); $('#submitPlacement').show();
$('#submitQCRData').hide(); $('#submitQCRData').hide();
$('#pptBtnGroup').show();
} }
if(RFQ_or_QCR == 1 && insurer_count > 0){ if(RFQ_or_QCR == 1 && insurer_count > 0){
@ -7942,6 +7962,141 @@ function appendMultiFileData(data) {
} }
var pptSelectionMode = false;
function startPptGeneration() {
pptSelectionMode = true;
// Add checkboxes to each proposal header (top-level th in first row of rfqTable)
let headerRow = document.querySelector('#rfqTable thead tr');
if (!headerRow) return;
let ths = headerRow.querySelectorAll('th');
ths.forEach(function(th, idx) {
// Skip Sno, Item Key, Particulars, Action columns
let keySpan = th.querySelector('.key_name');
let displaySpan = th.querySelector('.display_name');
if (!keySpan && !displaySpan) return;
let keyName = (keySpan ? keySpan.innerText.trim() : '');
let displayName = (displaySpan ? displaySpan.innerText.trim() : '');
if (!keyName || keyName === 'Sno') return;
if (keyName.toLowerCase() === 'action') return;
// Already has checkbox?
if (th.querySelector('.ppt-select-cb')) return;
let cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = true;
cb.className = 'ppt-select-cb';
cb.dataset.proposalKey = keyName;
cb.style.cssText = 'margin-left:6px; transform:scale(1.3); vertical-align:middle; cursor:pointer;';
cb.title = 'Include in PDF';
th.appendChild(cb);
});
$('#pptFloatingBar').fadeIn();
toastr.info('Select the proposals you want to include in the PDF', 'PPT Generation');
}
function cancelPptSelection() {
pptSelectionMode = false;
document.querySelectorAll('.ppt-select-cb').forEach(function(cb) { cb.remove(); });
$('#pptFloatingBar').fadeOut();
}
function confirmGeneratePdf() {
let selected = [];
document.querySelectorAll('.ppt-select-cb:checked').forEach(function(cb) {
selected.push(cb.dataset.proposalKey);
});
if (selected.length === 0) {
toastr.warning('Please select at least one proposal', 'Warning');
return;
}
// Clean up selection UI
cancelPptSelection();
let lead_id = $('#lead_id').val();
if (!lead_id) {
toastr.error('Lead ID is required', 'Error');
return;
}
$('.loader').fadeIn();
$('.loader-mask').fadeIn();
$.ajax({
url: '<?= base_url('rfq/generateGmcQcrPdf') ?>',
type: 'POST',
data: { lead_id: lead_id, proposals: selected },
dataType: 'json',
success: function (res) {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
if (res && res.status) {
toastr.success(res.message || 'PDF generated successfully', 'Success');
} else {
toastr.error((res && res.message) ? res.message : 'Failed to generate PDF', 'Error');
}
},
error: function (xhr) {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
let msg = 'Failed to generate PDF';
try {
const parsed = JSON.parse(xhr.responseText);
if (parsed && parsed.message) msg = parsed.message;
} catch (e) {}
toastr.error(msg, 'Error');
}
});
}
function downloadGmcQcrPdf() {
let lead_id = $('#lead_id').val();
if (!lead_id) {
toastr.error('Lead ID is required', 'Error');
return;
}
const url = '<?= base_url('rfq/downloadGmcQcrPdf/') ?>' + lead_id;
$('.loader').fadeIn();
$('.loader-mask').fadeIn();
fetch(url, { credentials: 'same-origin' })
.then(async function (response) {
const contentType = response.headers.get('Content-Type') || '';
if (!response.ok || contentType.indexOf('application/json') !== -1) {
let message = 'No generated PDF found. Please click Generate first.';
try {
const data = await response.json();
if (data && data.message) message = data.message;
} catch (e) {}
throw new Error(message);
}
const disposition = response.headers.get('Content-Disposition') || '';
let filename = 'GMC Quote comparison.pdf';
const match = disposition.match(/filename=\"?([^\";]+)\"?/i);
if (match && match[1]) filename = match[1];
const blob = await response.blob();
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(link.href);
})
.catch(function (err) {
toastr.error(err.message || 'Failed to download PDF', 'Error');
})
.finally(function () {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
});
}
function toggleButtons(status) { function toggleButtons(status) {
$("#save_placement_btn").show(); $("#save_placement_btn").show();

Binary file not shown.

Binary file not shown.

View File

@ -1,374 +0,0 @@
<?php
/**
* Smoke test: claim_report table + Claim Report dashboard.
*
* Run:
* php tests/smoke_claim_report_dashboard.php [policy_id]
* php tests/smoke_claim_report_dashboard.php 4687
*
* Checks:
* - claim_report table / schema
* - claims_source resolver (claim_report vs ticket_master)
* - KPI model + controller (mirror of V2)
* - routes under util/ and employeeRest/
* - optional HTTP auth-gate reachability
*/
declare(strict_types=1);
ob_start();
define('FCPATH', __DIR__ . '/../public/');
chdir(FCPATH);
require FCPATH . '../app/Config/Paths.php';
$paths = new Config\Paths();
require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
require_once SYSTEMPATH . 'Config/DotEnv.php';
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development'));
$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
if (is_file($boot)) {
require_once $boot;
}
helper('url');
use App\Controllers\ClaimReportDashboardController;
use App\Models\ClaimReportDashboardModel;
use App\Models\ClaimsCollectionV2DashboardModel;
use Config\Services;
$policyId = isset($argv[1]) ? (int) $argv[1] : 0;
$pass = 0;
$fail = 0;
$results = [];
function ok(string $label, bool $cond, string $detail = ''): void
{
global $pass, $fail, $results;
if ($cond) {
$pass++;
$results[] = '[PASS] ' . $label . ($detail ? "{$detail}" : '');
} else {
$fail++;
$results[] = '[FAIL] ' . $label . ($detail ? "{$detail}" : '');
}
}
function info(string $line): void
{
global $results;
$results[] = $line;
}
$db = \Config\Database::connect();
// Default: prefer a policy that already has claim_report or ticket_master rows.
if ($policyId <= 0) {
$pick = null;
if ($db->tableExists('claim_report')) {
$pick = $db->query(
'SELECT client_policy_id AS id FROM claim_report WHERE is_active = 1 AND client_policy_id > 0 ORDER BY id DESC LIMIT 1'
)->getRowArray();
}
if (empty($pick)) {
$pick = $db->query(
"SELECT client_policy_id AS id FROM ticket_master
WHERE is_active = 1 AND client_policy_id > 0
AND (claim_dump_ref_id IS NOT NULL OR file_id IS NOT NULL)
ORDER BY id DESC LIMIT 1"
)->getRowArray();
}
if (empty($pick)) {
$pick = $db->query('SELECT id FROM client_policy WHERE is_active = 1 ORDER BY id DESC LIMIT 1')->getRowArray();
}
$policyId = (int) ($pick['id'] ?? 0);
}
$model = new ClaimReportDashboardModel();
$kpiMap = ClaimsCollectionV2DashboardModel::KPI_MAP;
info('=== Schema ===');
$hasTable = $db->tableExists('claim_report');
ok('claim_report table exists', $hasTable);
if ($hasTable) {
$fields = $db->getFieldNames('claim_report');
$required = [
'id', 'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id',
'source_table', 'source_row_id', 'claim_number', 'claim_amount', 'approved_amount',
'incurred_amount', 'tpa_claim_type', 'tpa_ailments', 'claim_status_id',
'hospital_name', 'doa', 'dod', 'claim_dump_date', 'is_active',
];
$missing = array_values(array_diff($required, $fields));
ok('claim_report required columns', $missing === [], $missing === [] ? count($fields) . ' cols' : 'missing: ' . implode(', ', $missing));
$indexes = $db->query('SHOW INDEX FROM `claim_report`')->getResultArray();
$indexNames = array_unique(array_column($indexes, 'Key_name'));
ok('unique key uq_claim_report_policy_claim', in_array('uq_claim_report_policy_claim', $indexNames, true));
}
info('');
info('=== Source resolver (policy_id=' . $policyId . ') ===');
$policy = $policyId > 0
? $db->table('client_policy')->select('id, tpa_id, policy_no')->where('id', $policyId)->get()->getRowArray()
: null;
if (empty($policy)) {
info('[WARN] policy id ' . $policyId . ' not found — resolver/KPI checks will use empty data');
ok('policy id resolved for test', $policyId > 0, 'policy_id=' . $policyId);
} else {
ok('policy exists', true, 'tpa_id=' . ($policy['tpa_id'] ?? 'null') . ' policy_no=' . ($policy['policy_no'] ?? ''));
}
$tpaTableMap = [
(int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'claims_dump_vidal',
(int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'claims_dump_abhi',
(int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'claims_dump_medi_assist',
(int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'claims_dump_fhpl',
(int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'claims_dump_reliance',
(int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'claims_dump_icici',
];
$tpaId = (int) ($policy['tpa_id'] ?? 0);
$dumpTable = $tpaTableMap[$tpaId] ?? null;
$dumpExists = $dumpTable !== null && $db->tableExists($dumpTable);
$reportCount = $hasTable
? (int) $db->table('claim_report')->where('client_policy_id', $policyId)->where('is_active', 1)->countAllResults()
: 0;
$tmCount = (int) $db->table('ticket_master')->where('client_policy_id', $policyId)->where('is_active', 1)->countAllResults();
info('[INFO] dump_table=' . ($dumpTable ?? 'none') . ' exists=' . ($dumpExists ? 'yes' : 'no'));
info('[INFO] claim_report rows=' . $reportCount . ' ticket_master rows=' . $tmCount);
$expectedSource = ($dumpExists && $reportCount > 0) ? 'claim_report' : 'ticket_master';
$actualSource = $model->resolveClaimsTable($policyId);
ok('resolveClaimsTable matches expectation', $actualSource === $expectedSource, "expected={$expectedSource} actual={$actualSource}");
info('');
info('=== KPI model ===');
ok('KPI_MAP count', count($kpiMap) === 36, (string) count($kpiMap));
ok('id 207 maps to incurred_ratio', ($kpiMap[207] ?? '') === 'incurred_ratio');
try {
$rows = $model->policy_exposure_summary($policyId);
ok('model policy_exposure_summary', is_array($rows), 'rows=' . count($rows));
} catch (Throwable $e) {
ok('model policy_exposure_summary', false, $e->getMessage());
}
try {
$rows = $model->getKpi('incurred_ratio', $policyId);
ok('model getKpi(incurred_ratio)', is_array($rows), 'rows=' . count($rows));
} catch (Throwable $e) {
ok('model getKpi(incurred_ratio)', false, $e->getMessage());
}
try {
$rows = $model->getKpi('total_claims', $policyId);
ok('model getKpi(total_claims)', is_array($rows), 'rows=' . count($rows));
} catch (Throwable $e) {
ok('model getKpi(total_claims)', false, $e->getMessage());
}
try {
$rows = $model->getKpi('claim_amount_by_gender', $policyId);
ok('model getKpi(claim_amount_by_gender)', is_array($rows), 'rows=' . count($rows));
} catch (Throwable $e) {
ok('model getKpi(claim_amount_by_gender)', false, $e->getMessage());
}
$sampleKpis = [
'policy_exposure_summary',
'premium_as_on_date',
'total_claims',
'incurred_amount',
'incurred_ratio',
'claim_amount_by_gender',
'top_5_hospitals_by_incurred_amount',
'cashless_claim_amt',
'top_10_ailments_by_claim_count',
];
$samplePass = 0;
foreach ($sampleKpis as $method) {
try {
$rows = $model->getKpi($method, $policyId);
if (is_array($rows)) {
$samplePass++;
}
} catch (Throwable $e) {
info('[WARN] sample KPI ' . $method . ': ' . $e->getMessage());
}
}
ok('sample claim KPIs runnable', $samplePass === count($sampleKpis), "{$samplePass}/" . count($sampleKpis));
try {
$all = $model->getAllKpis($policyId);
$metaSource = $all['_meta']['claims_source'] ?? null;
unset($all['_meta']);
ok('model getAllKpis', count($all) === 36, 'kpis=' . count($all));
ok('getAllKpis _meta.claims_source', $metaSource === $actualSource, (string) $metaSource);
} catch (Throwable $e) {
// Older MySQL without CTE support can fail on age_band (WITH ...); not claim_report-specific.
info('[WARN] getAllKpis: ' . $e->getMessage());
info('[SKIP] model getAllKpis — CTE/MySQL limitation; sample KPIs already verified');
ok('resolver still available after getAllKpis skip', $actualSource !== '', 'source=' . $actualSource);
}
info('');
info('=== Controller ===');
$request = Services::request(null, false);
$response = Services::response();
$request->setGlobal('get', ['client_policy' => (string) $policyId]);
$controller = new ClaimReportDashboardController();
$controller->initController($request, $response, service('logger'));
$slugResp = json_decode($controller->kpi('incurred_ratio')->getJSON(), true);
ok(
'controller kpi by slug',
($slugResp['status'] ?? false) === true
&& ($slugResp['kpi'] ?? '') === 'incurred_ratio'
&& ($slugResp['claims_source'] ?? '') === $actualSource,
'source=' . ($slugResp['claims_source'] ?? 'null')
);
$idResp = json_decode($controller->kpi('207')->getJSON(), true);
ok('controller kpi by id 207', ($idResp['status'] ?? false) === true && ($idResp['kpi_id'] ?? 0) === 207);
$badResp = json_decode($controller->kpi('not_a_kpi')->getJSON(), true);
ok('controller unknown kpi 404', ($badResp['status'] ?? true) === false);
$missingPolicyReq = Services::request(null, false);
$missingPolicyReq->setGlobal('get', []);
$missingCtrl = new ClaimReportDashboardController();
$missingCtrl->initController($missingPolicyReq, Services::response(), service('logger'));
$missingResp = json_decode($missingCtrl->all()->getJSON(), true);
ok('controller all requires policy', ($missingResp['status'] ?? true) === false);
try {
$allResp = json_decode($controller->all()->getJSON(), true);
ok(
'controller all KPIs',
($allResp['status'] ?? false) === true
&& count($allResp['data'] ?? []) === 36
&& ($allResp['claims_source'] ?? '') === $actualSource,
'source=' . ($allResp['claims_source'] ?? 'null') . ' kpis=' . count($allResp['data'] ?? [])
);
} catch (Throwable $e) {
info('[WARN] controller all: ' . $e->getMessage());
info('[SKIP] controller all KPIs — CTE/MySQL limitation on age_band');
ok('controller single-kpi path still healthy', ($slugResp['status'] ?? false) === true);
}
try {
$debugOut = $controller->debug($policyId);
$debugBody = is_string($debugOut) ? $debugOut : $debugOut->getBody();
$debugJson = json_decode($debugBody, true);
ok(
'controller debug JSON',
($debugJson['status'] ?? false) === true
&& isset($debugJson['data'])
&& ($debugJson['claims_source'] ?? '') === $actualSource
);
} catch (Throwable $e) {
info('[WARN] controller debug: ' . $e->getMessage());
info('[SKIP] controller debug — CTE/MySQL limitation on age_band');
ok('controller debug skipped safely', true);
}
$previewOut = $controller->preview($policyId);
$previewHtml = is_string($previewOut) ? $previewOut : $previewOut->getBody();
ok(
'controller preview HTML',
str_contains($previewHtml, 'kpi-grid')
&& str_contains($previewHtml, 'claims-collection-report')
);
info('');
info('=== Routes (static check of Routes.php) ===');
$routesFile = APPPATH . 'Config/Routes.php';
$routesSrc = is_file($routesFile) ? (string) file_get_contents($routesFile) : '';
ok('Routes.php readable', $routesSrc !== '');
ok(
'Routes.php defines claims-collection-report group',
str_contains($routesSrc, "group('claims-collection-report'")
);
ok(
'Routes.php wires ClaimReportDashboardController',
substr_count($routesSrc, 'ClaimReportDashboardController::') >= 6,
'refs=' . substr_count($routesSrc, 'ClaimReportDashboardController::')
);
ok(
'Routes.php has util + employeeRest groups for report',
substr_count($routesSrc, "group('claims-collection-report'") >= 2,
'groups=' . substr_count($routesSrc, "group('claims-collection-report'")
);
info('');
info('=== Spot-check vs V2 (same policy) ===');
try {
$v2 = new ClaimsCollectionV2DashboardModel();
$v2Rows = $v2->getKpi('total_claims', $policyId);
$crRows = $model->getKpi('total_claims', $policyId);
ok('total_claims both return arrays', is_array($v2Rows) && is_array($crRows), 'v2=' . count($v2Rows) . ' report=' . count($crRows));
// When source is ticket_master, totals should match V2 closely.
if ($actualSource === 'ticket_master' && $v2Rows !== [] && $crRows !== []) {
$v2Val = json_encode($v2Rows[0] ?? []);
$crVal = json_encode($crRows[0] ?? []);
ok('total_claims matches V2 when source=ticket_master', $v2Val === $crVal, $crVal ?: 'empty');
} else {
info('[INFO] skip strict V2 equality (source=' . $actualSource . ')');
ok('total_claims callable on both models', true, 'skipped equality');
}
} catch (Throwable $e) {
ok('spot-check vs V2', false, $e->getMessage());
}
$baseUrl = rtrim((string) env('app.baseURL', ''), '/');
if ($baseUrl !== '') {
info('');
info('=== HTTP auth gate checks (no session/token) ===');
$urls = [
'MVC preview' => $baseUrl . '/util/claims-collection-report/preview?client_policy=' . $policyId,
'MVC kpi slug' => $baseUrl . '/util/claims-collection-report/kpi/incurred_ratio?client_policy=' . $policyId,
'MVC kpi id' => $baseUrl . '/util/claims-collection-report/kpi/207?client_policy=' . $policyId,
'MVC all' => $baseUrl . '/util/claims-collection-report/all?client_policy=' . $policyId,
'MVC debug' => $baseUrl . '/util/claims-collection-report/debug?client_policy=' . $policyId,
'JWT kpi slug' => $baseUrl . '/employeeRest/claims-collection-report/kpi/incurred_ratio?client_policy=' . $policyId,
'JWT debug' => $baseUrl . '/employeeRest/claims-collection-report/debug?client_policy=' . $policyId,
];
foreach ($urls as $label => $url) {
$ctx = stream_context_create(['http' => ['ignore_errors' => true, 'timeout' => 10]]);
$body = @file_get_contents($url, false, $ctx);
$code = 0;
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
$code = (int) $m[1];
}
$blocked = in_array($code, [401, 403, 302, 303], true);
$reachable = $code >= 200 && $code < 500;
ok("HTTP {$label} (" . ($code ?: 'no connection') . ')', $blocked || $reachable, $url);
}
} else {
info('[SKIP] HTTP checks — app.baseURL not set in .env');
}
info('');
info('=== Manual follow-ups ===');
info('[HINT] Backfill: php spark claim:backfill-report --policy=' . $policyId);
info('[HINT] After Job 2 / backfill, re-run this script and expect claims_source=claim_report when dump table exists.');
ob_end_clean();
echo '=== Claim Report dashboard smoke test (policy_id=' . $policyId . ') ===' . PHP_EOL . PHP_EOL;
echo implode(PHP_EOL, $results) . PHP_EOL;
echo PHP_EOL . "=== Summary: {$pass} passed, {$fail} failed ===" . PHP_EOL;
exit($fail > 0 ? 1 : 0);

View File

@ -1,36 +0,0 @@
<?php
/**
* Sync claim_report from TPA dump tables + ticket_master.
*
* Run:
* php tests/sync_claim_report_from_dump.php
* php tests/sync_claim_report_from_dump.php --policy=12
* php tests/sync_claim_report_from_dump.php --tpa=mediassist --policy=12
* php tests/sync_claim_report_from_dump.php --ticket-only
* php tests/sync_claim_report_from_dump.php --limit=500
*
* Prefer spark (same logic):
* php spark claim:sync-report --policy=12
*/
declare(strict_types=1);
$args = array_slice($argv, 1);
$sparkArgs = ['claim:sync-report'];
foreach ($args as $arg) {
if (str_starts_with($arg, '--')) {
$sparkArgs[] = $arg;
} elseif (ctype_digit($arg)) {
$sparkArgs[] = '--policy=' . $arg;
}
}
$root = dirname(__DIR__);
chdir($root);
$cmd = 'php spark ' . implode(' ', array_map('escapeshellarg', $sparkArgs));
echo "Running: {$cmd}" . PHP_EOL . PHP_EOL;
passthru($cmd, $exitCode);
exit($exitCode);