diff --git a/.env.sample b/.env.sample index e8a13f85..3d57c40a 100755 --- a/.env.sample +++ b/.env.sample @@ -116,6 +116,21 @@ VIDAL_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_BASE_URL = diff --git a/app/Commands/BackfillClaimReport.php b/app/Commands/BackfillClaimReport.php deleted file mode 100644 index ee247d3d..00000000 --- a/app/Commands/BackfillClaimReport.php +++ /dev/null @@ -1,200 +0,0 @@ - '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> $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]; - } -} diff --git a/app/Commands/SyncClaimReportFromDump.php b/app/Commands/SyncClaimReportFromDump.php deleted file mode 100644 index bb4a581b..00000000 --- a/app/Commands/SyncClaimReportFromDump.php +++ /dev/null @@ -1,63 +0,0 @@ - '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; - } -} diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 1628b476..a059de1c 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -510,7 +510,7 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('all', 'ClaimReportDashboardController::all'); $routes->get('debug', 'ClaimReportDashboardController::debug'); $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) { @@ -607,6 +607,8 @@ $routes->group("rfq", ["filter" => "authMVC"], function ($routes) { $routes->post("create", "LeadsController::createRFQ"); $routes->post("savePolicyInfo", "LeadsController::savePolicyInfo"); $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("nonEB","LeadsController::rfqNonEB"); // 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('debug', 'ClaimReportDashboardController::debug'); $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); - $routes->match(['get', 'post'], 'sync', 'ClaimReportDashboardController::sync'); }); $routes->group('enrollment-collection-v1', static function ($routes) { diff --git a/app/Controllers/ClaimReportDashboardController.php b/app/Controllers/ClaimReportDashboardController.php index 7e277754..acbe8e8d 100644 --- a/app/Controllers/ClaimReportDashboardController.php +++ b/app/Controllers/ClaimReportDashboardController.php @@ -5,11 +5,12 @@ namespace App\Controllers; use App\Models\ClaimReportDashboardModel; use App\Models\ClaimsCollectionV2DashboardModel; use App\Models\ClaimDumpFileModel; -use App\Libraries\ClaimReportSyncService; 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 { @@ -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). */ @@ -183,95 +554,4 @@ class ClaimReportDashboardController extends BaseController ->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); - } } diff --git a/app/Controllers/DigitMotorController.php b/app/Controllers/DigitMotorController.php index 7035a7f1..343e499a 100644 --- a/app/Controllers/DigitMotorController.php +++ b/app/Controllers/DigitMotorController.php @@ -78,6 +78,8 @@ class DigitMotorController extends BaseController $data['title'] = 'Digit Motor Journey'; $data['quote'] = null; $data['quote_id'] = null; + $data['vehicle_master'] = null; + $data['policyholder'] = []; if ($quoteId) { $data['quote'] = $this->quoteModel->getDetail((int) $quoteId); @@ -85,6 +87,17 @@ class DigitMotorController extends BaseController if (!$data['quote']) { 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); @@ -347,6 +360,9 @@ class DigitMotorController extends BaseController 'message' => $e->getMessage(), 'digit_code' => $e->getDigitCode(), 'infra' => $e->isInfraError(), + 'data' => is_array($e->getResponseBody()) + ? ($e->getResponseBody()['_nhance_shell'] ?? null) + : null, ], min($http, 599)); } catch (\Throwable $e) { log_message('error', 'DigitMotor unexpected error: ' . $e->getMessage()); diff --git a/app/Controllers/ICICILombardController.php b/app/Controllers/ICICILombardController.php index 92b6c594..203952ee 100644 --- a/app/Controllers/ICICILombardController.php +++ b/app/Controllers/ICICILombardController.php @@ -238,14 +238,15 @@ class ICICILombardController extends AdminController ]; $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){ - return $this->response->setJSON([ + return [ 'status' => false, 'message' => 'Token generation failed.', 'data' => $response - ]); + ]; } // Debug removed: return token response to caller. diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index 1c1b154a..78ff86e1 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -38,6 +38,7 @@ use App\Helpers\MailHelper; use App\Helpers\ExcelMergeHelper; use App\Helpers\ExcelSanitizeHelper; use App\Libraries\GoogleSheetLib; +use App\Libraries\GmcQcrPdfService; 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); } + /** + * 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 { diff --git a/app/Database/digit_motor_tables.sql b/app/Database/digit_motor_tables.sql index 2bc7a99b..74e5675f 100644 --- a/app/Database/digit_motor_tables.sql +++ b/app/Database/digit_motor_tables.sql @@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS motor_quote ( end_date DATE DEFAULT NULL, pincode VARCHAR(6) NOT NULL, coverage_details JSON DEFAULT NULL, + policyholder_details JSON DEFAULT NULL, premium DECIMAL(12,2) DEFAULT NULL, idv DECIMAL(12,2) DEFAULT NULL, status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', diff --git a/app/Libraries/ClaimReportSyncService.php b/app/Libraries/ClaimReportSyncService.php deleted file mode 100644 index b787c876..00000000 --- a/app/Libraries/ClaimReportSyncService.php +++ /dev/null @@ -1,444 +0,0 @@ - - */ - 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, - * tpa_results: list>, - * logs: list - * } - */ - 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> $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 $phases - * @param list> $tpaResults - * @param list $logs - * @return array - */ - 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, - ]; - } -} diff --git a/app/Libraries/DigitMotor/DigitExecutorService.php b/app/Libraries/DigitMotor/DigitExecutorService.php index a9198297..3f6e19a3 100644 --- a/app/Libraries/DigitMotor/DigitExecutorService.php +++ b/app/Libraries/DigitMotor/DigitExecutorService.php @@ -41,17 +41,54 @@ class DigitExecutorService */ public function quickQuote(array $input): array { - $enquiryId = $input['enquiry_id'] ?? ('NH' . date('ymdHis') . random_int(100, 999)); - $payload = $this->buildQuickQuotePayload($input, $enquiryId); + $quoteIdIn = (int) ($input['quote_id'] ?? 0); + $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'); - $response = $this->api->post( - $this->config->executorPath, - $payload, - $this->config->integrationIds['quickQuote'], - $quoteId - ); + try { + $response = $this->api->post( + $this->config->executorPath, + $payload, + $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']); $idv = $this->pickNumber($response, ['idv', 'vehicleIDV', 'insuredDeclaredValue']); @@ -61,10 +98,13 @@ class DigitExecutorService $vehicleIdv = $response['vehicle']['vehicleIDV'] ?? $response['vehicleIDV'] ?? []; + // Re-quote resets create/KYC progress so journey stays consistent. $this->quoteModel->update($quoteId, [ - 'premium' => $premium, - 'idv' => $idv, - 'status' => 'QUOTED', + 'premium' => $premium, + 'idv' => $idv, + 'status' => 'QUOTED', + 'quote_number' => null, + 'application_id' => null, 'coverage_details' => json_encode($input['coverages'] ?? $payload['contract']['coverages'] ?? []), ]); @@ -123,6 +163,21 @@ class DigitExecutorService $input['vehicle_identification_number'] = $vin; $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); $response = $this->api->post( @@ -137,12 +192,13 @@ class DigitExecutorService $premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']) ?? $detail['premium']; $this->quoteModel->update($quoteId, [ - 'quote_number' => $quoteNumber, - 'application_id' => $applicationId, - 'premium' => $premium, - 'start_date' => $input['start_date'] ?? $detail['start_date'], - 'end_date' => $input['end_date'] ?? $detail['end_date'], - 'status' => 'CREATED', + 'quote_number' => $quoteNumber, + 'application_id' => $applicationId, + 'premium' => $premium, + 'start_date' => $input['start_date'] ?? $detail['start_date'], + 'end_date' => $input['end_date'] ?? $detail['end_date'], + 'status' => 'CREATED', + 'policyholder_details' => json_encode($policyholder), ]); return [ @@ -472,10 +528,18 @@ class DigitExecutorService 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 = [ - 'enquiry_id' => $enquiryId, + 'enquiry_id' => $existing['enquiry_id'] ?? $enquiryId, 'policy_holder_type' => $input['policy_holder_type'] ?? 'INDIVIDUAL', 'insurance_product_code' => (string) ($input['insurance_product_code'] ?? '20101'), 'sub_insurance_product_code' => (string) ($input['sub_insurance_product_code'] ?? 'PB'), @@ -487,9 +551,13 @@ class DigitExecutorService 'end_date' => $input['end_date'] ?? null, 'pincode' => (string) ($input['pincode'] ?? ''), '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) { $quoteId = (int) $existing['id']; $this->quoteModel->update($quoteId, $quoteData); diff --git a/app/Libraries/GmcQcrPdfService.php b/app/Libraries/GmcQcrPdfService.php new file mode 100644 index 00000000..22ecea63 --- /dev/null +++ b/app/Libraries/GmcQcrPdfService.php @@ -0,0 +1,582 @@ +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 2–7 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}> + */ + 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}> + */ + 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 = '
'; + } + + $nhance = is_file($nhanceLogo) + ? '' + : 'Nhance'; + + return ' + +
+
 
+ ' . $logoHtml . ' +
' . $this->escape($clientName) . '
+
Group Medical Insurance Quote
+ + +
+'; + } + + /** + * Render a coverage page (2-column: Particulars + value) matching original style. + * + * @param list $rows + */ + private function renderCoverageHtml(string $title, array $rows, string $nhanceLogo, bool $showTitle): string + { + $nhance = is_file($nhanceLogo) + ? '' + : 'Nhance'; + + $thead = 'ParticularsExpiring'; + + $tbody = ''; + $i = 0; + foreach ($rows as $row) { + $bg = ($i % 2 === 0) ? '#ffffff' : '#f7fafa'; + $tbody .= ''; + $tbody .= '' . $this->escape($row['particular']) . ''; + $tbody .= '' . $this->escape($row['value']) . ''; + $tbody .= ''; + $i++; + } + + $titleBlock = $showTitle + ? '
GMC ' . $this->escape(str_replace('GMC ', '', $title)) . '
' + : '
GMC ' . $this->escape(str_replace('GMC ', '', $title)) . ' (continued)
'; + + return ' + +
+ ' . $titleBlock . ' + ' . $thead . '' . $tbody . '
+
+ +'; + } + + /** + * @param list}> $sections + */ + private function renderPremiumHtml(array $sections, string $nhanceLogo): string + { + $nhance = is_file($nhanceLogo) + ? '' + : 'Nhance'; + + $body = ''; + foreach ($sections as $section) { + $body .= '
' . $this->escape($section['title']) . '
'; + $body .= ' + + + + + '; + $i = 0; + foreach ($section['rows'] as $row) { + $bg = ($i % 2 === 0) ? '#ffffff' : '#f7fafa'; + $body .= ' + + + + + '; + $i++; + } + $body .= '
Insurer NamePremiumGSTTotal Premium
' . $this->escape($row['insurer']) . '' . $this->escape($row['premium']) . '' . $this->escape($row['gst']) . '' . $this->escape($row['total']) . '
'; + } + + return ' + +
+
Quote Comparison
+ ' . $body . ' +
+ +'; + } +} diff --git a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php index db74db09..7e8851f3 100644 --- a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php @@ -115,18 +115,6 @@ class AbhiClaimImportService extends BaseTpaClaimImportService /** * 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 = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php index c501f486..533394b1 100644 --- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php @@ -330,11 +330,6 @@ abstract class BaseTpaClaimImportService 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 $this->db->transCommit(); @@ -1169,289 +1164,6 @@ abstract class BaseTpaClaimImportService ->where('is_active', 1) ->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 $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 $dumpRowIds - * @return list> - */ - 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> $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 diff --git a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php index 725ee2f5..496c61cf 100644 --- a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php @@ -177,19 +177,6 @@ class FhplClaimImportService extends BaseTpaClaimImportService /** * 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 = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php index 97096210..98a6d5d0 100644 --- a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php @@ -106,19 +106,6 @@ class IciciClaimImportService extends BaseTpaClaimImportService /** * 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 = [ 'PAID' => 11, 'SETTLED' => 11, diff --git a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php index b1d0aefa..beb08156 100644 --- a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php @@ -157,19 +157,6 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService /** * 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 = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php index 477922f3..0eb3d9d3 100644 --- a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php @@ -94,19 +94,6 @@ class RcareClaimImportService extends BaseTpaClaimImportService /** * 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 = [ 'CL Paid with Settlement Letter' => 11, 'Settled' => 11, diff --git a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php index 921ec6ba..44d2a6a5 100644 --- a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php @@ -350,19 +350,6 @@ class VidalClaimImportService extends BaseTpaClaimImportService /** * 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 = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Models/ClaimReportDashboardModel.php b/app/Models/ClaimReportDashboardModel.php index 6bee42b5..61f00511 100644 --- a/app/Models/ClaimReportDashboardModel.php +++ b/app/Models/ClaimReportDashboardModel.php @@ -3,7 +3,8 @@ 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. */ class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel @@ -13,7 +14,10 @@ class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel /** * 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 { @@ -21,11 +25,18 @@ class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel 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) { - return $this->claimsTableCache[$policyId] = $table; + if ($policyId <= 0) { + 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') @@ -34,23 +45,26 @@ class ClaimReportDashboardModel extends ClaimsCollectionV2DashboardModel ->get() ->getRowArray(); - $tpaId = (int) ($policy['tpa_id'] ?? 0); + $tpaId = (int) ($policy['tpa_id'] ?? 0); $dumpTable = $this->getTpaDumpTableMap()[$tpaId] ?? null; - if ($dumpTable === null || !$db->tableExists($dumpTable)) { - return $this->claimsTableCache[$policyId] = $table; + // Mapped dump TPA with existing dump table → claim_report only. + if ($dumpTable !== null && $db->tableExists($dumpTable)) { + return $this->claimsTableCache[$policyId] = 'claim_report'; } - $hasReportRows = $db->table('claim_report') - ->where('client_policy_id', $policyId) - ->where('is_active', 1) - ->countAllResults() > 0; + // Non-dump / missing dump table → env-gated ticket_master fallback. + return $this->claimsTableCache[$policyId] = $fallbackEnabled + ? 'ticket_master' + : 'claim_report'; + } - if ($hasReportRows) { - $table = 'claim_report'; - } - - return $this->claimsTableCache[$policyId] = $table; + protected function isTicketMasterFallbackEnabled(): bool + { + return filter_var( + env('CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER', false), + FILTER_VALIDATE_BOOLEAN + ); } /** diff --git a/app/Models/MotorQuoteModel.php b/app/Models/MotorQuoteModel.php index 90d52d55..3e817dcb 100644 --- a/app/Models/MotorQuoteModel.php +++ b/app/Models/MotorQuoteModel.php @@ -17,7 +17,7 @@ class MotorQuoteModel extends Model 'insurance_product_code', 'sub_insurance_product_code', 'previous_insurer_code', 'previous_policy_expiry_date', 'external_policy_number', 'is_ncb_transfer', 'start_date', 'end_date', 'pincode', - 'coverage_details', 'premium', 'idv', 'status', + 'coverage_details', 'policyholder_details', 'premium', 'idv', 'status', 'created_at', 'updated_at', ]; protected $useTimestamps = true; @@ -67,6 +67,12 @@ class MotorQuoteModel extends Model if (!empty($quote['coverage_details']) && is_string($quote['coverage_details'])) { $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; } diff --git a/app/Views/digit_motor/journey.php b/app/Views/digit_motor/journey.php index 9bbdae10..98aecdb6 100644 --- a/app/Views/digit_motor/journey.php +++ b/app/Views/digit_motor/journey.php @@ -6,6 +6,11 @@ $kyc = $quote['kyc'] ?? []; $payment = $quote['payment'] ?? []; $policy = $quote['policy'] ?? []; $status = $quote['status'] ?? 'DRAFT'; +$policyholder = $policyholder ?? ($quote['policyholder_details'] ?? []); +if (!is_array($policyholder)) { + $policyholder = []; +} +$vehicleMaster = $vehicle_master ?? null; $statusToStep = [ 'DRAFT' => 0, @@ -17,6 +22,18 @@ $statusToStep = [ ]; $initialStep = $statusToStep[$status] ?? 0; $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 +} ?>