diff --git a/app/Commands/RecreateOrphanDumpTickets.php b/app/Commands/RecreateOrphanDumpTickets.php new file mode 100644 index 00000000..aa0d28e8 --- /dev/null +++ b/app/Commands/RecreateOrphanDumpTickets.php @@ -0,0 +1,319 @@ + 'TPA key: icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all)', + '--tpa-id' => 'Numeric TPA primary key (overrides --tpa)', + '--file-id' => 'Limit to one claim_dump_files.id', + '--include-pending' => 'Also process dump rows with ticket_id IS NULL (never linked)', + '--apply' => 'Clear orphan links and run Job 2 (default is dry-run list only)', + ]; + + private function tpaConfigs(): array + { + return [ + 'icici' => ['env' => 'ICICI_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_icici'], + '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'], + 'vidal' => ['env' => 'VIDAL_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_vidal'], + ]; + } + + public function run(array $params) + { + helper('utility_helper'); + + $db = db_connect(); + $apply = $this->hasFlag('apply'); + $includePending = $this->hasFlag('include-pending'); + $fileIdFilter = (int) $this->resolveOptionValue('file-id', '0'); + $configs = $this->tpaConfigs(); + $selected = $this->resolveSelectedTpas($configs); + + if ($selected === []) { + return; + } + + $summary = []; + + foreach ($selected as $tpaKey) { + $cfg = $configs[$tpaKey]; + $tpaId = (int) env($cfg['env']); + $table = $cfg['table']; + + CLI::newLine(); + CLI::write(str_repeat('=', 64), 'yellow'); + CLI::write(strtoupper($tpaKey) . " (tpa_id={$tpaId}, table={$table})", 'yellow'); + CLI::write(str_repeat('=', 64), 'yellow'); + + if (!$this->tableExists($db, $table)) { + CLI::error("Table {$table} missing — skipped"); + $summary[$tpaKey] = ['ok' => false, 'error' => 'table missing']; + continue; + } + + $orphans = $this->findOrphans($db, $table, $fileIdFilter); + CLI::write('Orphan dump rows (ticket_id missing in ticket_master): ' . count($orphans)); + + $pending = []; + if ($includePending) { + $pending = $this->findPending($db, $table, $fileIdFilter); + CLI::write('Pending dump rows (ticket_id IS NULL): ' . count($pending)); + } + + if ($orphans === [] && $pending === []) { + $summary[$tpaKey] = ['ok' => true, 'orphans' => 0, 'pending' => 0, 'files' => 0]; + continue; + } + + foreach (array_slice($orphans, 0, 20) as $row) { + CLI::write(sprintf( + ' [orphan] dump_id=%s file_id=%s ticket_id=%s', + $row['id'], + $row['file_id'], + $row['ticket_id'] + )); + } + if (count($orphans) > 20) { + CLI::write(' ... and ' . (count($orphans) - 20) . ' more orphans'); + } + + foreach (array_slice($pending, 0, 10) as $row) { + CLI::write(sprintf( + ' [pending] dump_id=%s file_id=%s', + $row['id'], + $row['file_id'] + )); + } + + $fileIds = array_values(array_unique(array_map( + 'intval', + array_merge(array_column($orphans, 'file_id'), array_column($pending, 'file_id')) + ))); + CLI::write('Affected file_ids: ' . implode(', ', $fileIds), 'cyan'); + + if (!$apply) { + CLI::write("Dry-run only. Apply with: php spark tpa:recreate-orphan-tickets --tpa={$tpaKey} --apply", 'yellow'); + $summary[$tpaKey] = [ + 'ok' => true, + 'orphans' => count($orphans), + 'pending' => count($pending), + 'files' => count($fileIds), + 'dry_run' => true, + ]; + continue; + } + + $fileResults = []; + foreach ($fileIds as $fileId) { + CLI::write("Processing file_id={$fileId} ...", 'light_red'); + try { + $service = TpaClaimsImportFactory::make($tpaId); + $result = $service->recreateOrphanDumpTickets($fileId); + CLI::write(' result: ' . json_encode($result)); + + $stillOrphan = $this->countOrphansForFile($db, $table, $fileId); + $linked = $db->table($table) + ->where('file_id', $fileId) + ->where('is_active', 1) + ->where('ticket_id IS NOT NULL', null, false) + ->countAllResults(); + + $ok = !empty($result['status']) && $stillOrphan === 0; + $fileResults[] = [ + 'file_id' => $fileId, + 'ok' => $ok, + 'still_orphan' => $stillOrphan, + 'linked' => $linked, + 'message' => $result['message'] ?? null, + ]; + + if (!$ok) { + CLI::error(" file_id={$fileId} still has {$stillOrphan} orphan(s) or Job2 failed"); + } else { + CLI::write(" file_id={$fileId} OK (linked={$linked})", 'green'); + } + + if (!empty($result['status'])) { + $db->table('claim_dump_files')->where('id', $fileId)->update([ + 'status' => 'success', + 'reason' => null, + ]); + } + } catch (\Throwable $e) { + CLI::error(" file_id={$fileId} ERROR: " . $e->getMessage()); + $fileResults[] = [ + 'file_id' => $fileId, + 'ok' => false, + 'error' => $e->getMessage(), + ]; + } + } + + $allOk = !in_array(false, array_column($fileResults, 'ok'), true); + $summary[$tpaKey] = [ + 'ok' => $allOk, + 'orphans' => count($orphans), + 'pending' => count($pending), + 'files' => count($fileIds), + 'results' => $fileResults, + ]; + } + + CLI::newLine(); + CLI::write(str_repeat('=', 64), 'cyan'); + CLI::write('SUMMARY', 'cyan'); + CLI::write(str_repeat('=', 64), 'cyan'); + foreach ($summary as $tpaKey => $row) { + $line = sprintf( + '%-12s %s orphans=%s pending=%s files=%s%s', + strtoupper($tpaKey), + !empty($row['ok']) ? 'OK' : 'FAIL', + $row['orphans'] ?? 0, + $row['pending'] ?? 0, + $row['files'] ?? 0, + !empty($row['dry_run']) ? ' (dry-run)' : (!empty($row['error']) ? ' | ' . $row['error'] : '') + ); + CLI::write($line, !empty($row['ok']) ? 'green' : 'red'); + } + + if (!$apply) { + CLI::newLine(); + CLI::write('No DB changes made. Re-run with --apply to clear orphans and insert tickets.', 'yellow'); + } + } + + /** + * Dump rows with ticket_id set but no active ticket_master row. + */ + private function findOrphans($db, string $table, int $fileIdFilter = 0): array + { + $sql = "SELECT d.id, d.file_id, d.ticket_id + FROM `{$table}` d + LEFT JOIN ticket_master tm ON tm.id = d.ticket_id AND tm.is_active = 1 + WHERE d.is_active = 1 + AND d.ticket_id IS NOT NULL + AND tm.id IS NULL"; + $binds = []; + if ($fileIdFilter > 0) { + $sql .= ' AND d.file_id = ?'; + $binds[] = $fileIdFilter; + } + $sql .= ' ORDER BY d.file_id ASC, d.id ASC'; + + return $db->query($sql, $binds)->getResultArray(); + } + + /** + * Dump rows never linked / rejected (eligible for Job 2 insert). + */ + private function findPending($db, string $table, int $fileIdFilter = 0): array + { + $builder = $db->table($table) + ->select('id, file_id, ticket_id') + ->where('is_active', 1) + ->where('ticket_id IS NULL', null, false) + ->where('master_reject_reason IS NULL', null, false); + if ($fileIdFilter > 0) { + $builder->where('file_id', $fileIdFilter); + } + + return $builder->orderBy('file_id', 'ASC')->orderBy('id', 'ASC')->get()->getResultArray(); + } + + private function countOrphansForFile($db, string $table, int $fileId): int + { + $sql = "SELECT COUNT(*) AS cnt + FROM `{$table}` d + LEFT JOIN ticket_master tm ON tm.id = d.ticket_id AND tm.is_active = 1 + WHERE d.is_active = 1 + AND d.file_id = ? + AND d.ticket_id IS NOT NULL + AND tm.id IS NULL"; + $row = $db->query($sql, [$fileId])->getRowArray(); + + return (int) ($row['cnt'] ?? 0); + } + + private function resolveSelectedTpas(array $configs): array + { + $tpaIdOpt = (int) $this->resolveOptionValue('tpa-id', '0'); + if ($tpaIdOpt > 0) { + foreach ($configs as $key => $cfg) { + if ((int) env($cfg['env']) === $tpaIdOpt) { + return [$key]; + } + } + CLI::error("No TPA config matches --tpa-id={$tpaIdOpt}"); + return []; + } + + $tpaOpt = strtolower((string) ($this->resolveOptionValue('tpa', '') ?: 'all')); + if ($tpaOpt === '' || $tpaOpt === 'all') { + return array_keys($configs); + } + if (!isset($configs[$tpaOpt])) { + CLI::error("Unknown --tpa={$tpaOpt}. Use: " . implode('|', array_keys($configs)) . '|all'); + return []; + } + + return [$tpaOpt]; + } + + private function tableExists($db, string $table): bool + { + return !empty($db->query('SHOW TABLES LIKE ' . $db->escape($table))->getResultArray()); + } + + private function hasFlag(string $name): bool + { + if (CLI::getOption($name) !== null) { + return true; + } + return in_array('--' . $name, $_SERVER['argv'] ?? [], true); + } + + private function resolveOptionValue(string $name, string $default): string + { + $opt = CLI::getOption($name); + if (is_string($opt) && $opt !== '') { + return $opt; + } + + $argv = $_SERVER['argv'] ?? []; + foreach ($argv as $i => $arg) { + if (preg_match('/^--' . preg_quote($name, '/') . '=(.+)$/', (string) $arg, $m)) { + return trim($m[1]); + } + if ($arg === '--' . $name && isset($argv[$i + 1]) && strpos((string) $argv[$i + 1], '--') !== 0) { + return (string) $argv[$i + 1]; + } + } + + return $default; + } +} diff --git a/app/Commands/TestTpaE2ePipeline.php b/app/Commands/TestTpaE2ePipeline.php new file mode 100644 index 00000000..51c4c5e6 --- /dev/null +++ b/app/Commands/TestTpaE2ePipeline.php @@ -0,0 +1,549 @@ + 'TPA key: icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all)', + '--tpa-id' => 'Numeric TPA primary key (overrides --tpa)', + '--apply' => 'Run Job 1 + Job 2 (default is generate Excel + file row only)', + '--truncate' => 'Soft-truncate the test file after a successful apply', + '--new-status' => 'Override dump status string written into Excel', + ]; + + /** + * Per-TPA Excel seed config. + * excel_fields: excel header => source key from ticket/policy context + */ + private function tpaConfigs(): array + { + return [ + 'icici' => [ + 'env' => 'ICICI_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_icici', + 'sheet' => null, + 'default_status'=> 'REJECTED', + 'status_header' => 'Updated_status', + 'excel_fields' => [ + 'POLICY_NO' => 'policy_no', + 'UHID' => 'tpa_no', + 'EMPLOYEE_MEMBER_ID' => 'emp_code', + 'RELATION' => 'relation', + 'CLAIMED_AMOUNT' => 'claim_amount', + 'DOA' => 'doa', + 'Updated_status' => 'status_value', + ], + ], + 'abhi' => [ + 'env' => 'ABHI_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_abhi', + 'sheet' => null, + 'default_status'=> 'Rejected', + 'status_header' => 'Claim Status', + 'excel_fields' => [ + 'Policy Number' => 'policy_no', + 'HEALTHCARD_ID' => 'tpa_no', + 'Member Code' => 'emp_code', + 'Relation' => 'relation', + 'Claimed Amount'=> 'claim_amount', + 'DOA' => 'doa', + 'Claim Status' => 'status_value', + ], + ], + 'mediassist' => [ + 'env' => 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_medi_assist', + 'sheet' => null, + 'default_status'=> 'Rejected', + 'status_header' => 'claim_status', + 'excel_fields' => [ + 'policy_no' => 'policy_no', + 'event_id' => 'tpa_no', + 'pribenef_employee_code' => 'emp_code', + 'benef_relation' => 'relation', + 'claim_amount' => 'claim_amount', + 'date_of_admission' => 'doa', + 'claim_status' => 'status_value', + ], + ], + 'fhpl' => [ + 'env' => 'FHPL_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_fhpl', + 'sheet' => 'Claims&Preauth', + 'default_status'=> 'Rejected', + 'status_header' => 'Current Claim Status', + 'excel_fields' => [ + 'Policy No' => 'policy_no', + 'UHIDNO' => 'tpa_no', + 'employeeid' => 'emp_code', + 'relationship' => 'relation', + 'claimamount' => 'claim_amount', + 'Admdate' => 'doa', + 'Current Claim Status' => 'status_value', + ], + ], + 'rcare' => [ + 'env' => 'R_CARE_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_reliance', + 'sheet' => 'CL', + 'default_status'=> 'Rejected', + 'status_header' => 'Final Status', + 'excel_fields' => [ + 'Policy Number' => 'policy_no', + 'UHID' => 'tpa_no', + 'Employee/Member Id' => 'emp_code', + 'Relation' => 'relation', + 'Claimed Amount' => 'claim_amount', + 'DOA/OPD Treatment From' => 'doa', + 'Final Status' => 'status_value', + ], + ], + 'vidal' => [ + 'env' => 'VIDAL_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_vidal', + 'sheet' => null, + 'default_status'=> 'Rejected', + 'status_header' => 'Claim Status', + 'excel_fields' => [ + 'Insurer Policy Number' => 'policy_no', + 'Primary Policy Holder Card ID' => 'tpa_no', + 'Employee Number' => 'emp_code', + 'Relation' => 'relation', + 'Claim Amount' => 'claim_amount', + 'Date of Admission' => 'doa', + 'Claim Status' => 'status_value', + ], + ], + ]; + } + + public function run(array $params) + { + helper('utility_helper'); + + $db = db_connect(); + $apply = $this->hasFlag('apply'); + $truncate = $this->hasFlag('truncate'); + $statusOverride = $this->resolveOptionValue('new-status', ''); + $configs = $this->tpaConfigs(); + + $selected = $this->resolveSelectedTpas($configs); + if ($selected === []) { + return; + } + + $summary = []; + foreach ($selected as $tpaKey) { + CLI::newLine(); + CLI::write(str_repeat('=', 64), 'yellow'); + CLI::write('E2E TPA: ' . strtoupper($tpaKey), 'yellow'); + CLI::write(str_repeat('=', 64), 'yellow'); + + try { + $summary[$tpaKey] = $this->runForTpa( + $db, + $tpaKey, + $configs[$tpaKey], + $apply, + $truncate, + $statusOverride + ); + } catch (\Throwable $e) { + CLI::error("[{$tpaKey}] " . $e->getMessage()); + $summary[$tpaKey] = [ + 'ok' => false, + 'error' => $e->getMessage(), + ]; + } + } + + CLI::newLine(); + CLI::write(str_repeat('=', 64), 'cyan'); + CLI::write('E2E SUMMARY', 'cyan'); + CLI::write(str_repeat('=', 64), 'cyan'); + foreach ($summary as $tpaKey => $row) { + $ok = !empty($row['ok']); + $line = sprintf( + '%-12s %s file_id=%s job1=%s job2=%s%s', + strtoupper($tpaKey), + $ok ? 'OK' : 'FAIL', + $row['file_id'] ?? '-', + $row['job1'] ?? '-', + $row['job2'] ?? '-', + !empty($row['error']) ? ' | ' . $row['error'] : '' + ); + CLI::write($line, $ok ? 'green' : 'red'); + } + + if (!$apply) { + CLI::newLine(); + CLI::write('Dry-run only (Excel + claim_dump_files created). Re-run with --apply to execute Job1+Job2.', 'yellow'); + } + } + + private function resolveSelectedTpas(array $configs): array + { + $tpaIdOpt = (int) $this->resolveOptionValue('tpa-id', '0'); + if ($tpaIdOpt > 0) { + foreach ($configs as $key => $cfg) { + if ((int) env($cfg['env']) === $tpaIdOpt) { + return [$key]; + } + } + CLI::error("No TPA config matches --tpa-id={$tpaIdOpt}"); + return []; + } + + $tpaOpt = strtolower((string) ($this->resolveOptionValue('tpa', '') ?: 'all')); + if ($tpaOpt === '' || $tpaOpt === 'all') { + return array_keys($configs); + } + if (!isset($configs[$tpaOpt])) { + CLI::error("Unknown --tpa={$tpaOpt}. Use: " . implode('|', array_keys($configs)) . '|all'); + return []; + } + + return [$tpaOpt]; + } + + private function runForTpa( + $db, + string $tpaKey, + array $cfg, + bool $apply, + bool $truncate, + string $statusOverride + ): array { + $tpaId = (int) env($cfg['env']); + CLI::write("{$cfg['env']} = {$tpaId}", 'cyan'); + CLI::write("table={$cfg['table']}, sheet=" . ($cfg['sheet'] ?? 'active'), 'cyan'); + + if (!$this->tableExists($db, $cfg['table'])) { + throw new \RuntimeException("Table {$cfg['table']} does not exist"); + } + + $context = $this->resolveSeedContext($db, $tpaId, $tpaKey, $cfg, $statusOverride); + CLI::write('Seed ticket: ' . json_encode([ + 'id' => $context['ticket']['id'] ?? null, + 'client_id' => $context['client_id'], + 'client_policy_id' => $context['client_policy_id'], + 'policy_no' => $context['policy_no'], + 'emp_code' => $context['emp_code'], + 'tpa_no' => $context['tpa_no'], + 'claim_amount' => $context['claim_amount'], + 'doa' => $context['doa'], + 'status_value' => $context['status_value'], + ])); + + $service = TpaClaimsImportFactory::make($tpaId); + $headers = $this->extractExcelHeaders($service); + if ($headers === []) { + throw new \RuntimeException('Could not read Excel headers from service mapping'); + } + + $rowValues = $this->buildExcelRow($headers, $cfg, $context); + $fileName = "{$tpaKey}_e2e_" . date('Ymd_His') . '.xlsx'; + $uploadDir = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0775, true); + } + $filePath = $uploadDir . $fileName; + $this->writeExcel($filePath, $headers, $rowValues, $cfg['sheet'] ?? null); + CLI::write("Excel written: {$filePath}", 'green'); + + $fileId = $this->createClaimDumpFile($db, [ + 'tpa_id' => $tpaId, + 'client_id' => $context['client_id'], + 'client_policy_id' => $context['client_policy_id'], + 'file_name' => $fileName, + 'status' => 'inprogress', + 'created_by' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'is_active' => 1, + ]); + CLI::write("claim_dump_files.id = {$fileId}", 'green'); + + $result = [ + 'ok' => true, + 'file_id' => $fileId, + 'file_name' => $fileName, + 'job1' => 'skipped', + 'job2' => 'skipped', + ]; + + if (!$apply) { + CLI::write("Dry-run ready. Apply with: php spark tpa:e2e-pipeline --tpa={$tpaKey} --apply", 'yellow'); + // Point user at existing file_id for apply-by-id later if needed + return $result; + } + + // Clear claim_dump_ref_id so link/update path can re-bind cleanly. + if (!empty($context['ticket']['id']) && !empty($context['ticket']['claim_dump_ref_id'])) { + $db->table('ticket_master') + ->where('id', $context['ticket']['id']) + ->update(['claim_dump_ref_id' => null]); + CLI::write('Cleared seed ticket.claim_dump_ref_id for retest.', 'yellow'); + } + + CLI::write('Running Job 1 (Excel → dump)...', 'light_red'); + $job1 = $service->runTpaClaimDumpInsert($filePath, $fileId); + CLI::write('Job1 result: ' . json_encode($job1)); + $result['job1'] = !empty($job1['status']) ? 'ok' : 'fail'; + if (empty($job1['status'])) { + $db->table('claim_dump_files')->where('id', $fileId)->update([ + 'status' => 'failed', + 'reason' => json_encode(['error_data' => $job1['message'] ?? 'Job1 failed']), + ]); + $result['ok'] = false; + $result['error'] = $job1['message'] ?? 'Job1 failed'; + return $result; + } + + $dumpCount = $db->table($cfg['table'])->where('file_id', $fileId)->where('is_active', 1)->countAllResults(); + CLI::write("Dump rows after Job1: {$dumpCount}", 'cyan'); + + CLI::write('Running Job 2 (dump → ticket_master)...', 'light_red'); + $job2 = $service->runTicketMasterInsert(['file_id' => $fileId]); + CLI::write('Job2 result: ' . json_encode($job2)); + $result['job2'] = !empty($job2['status']) ? 'ok' : 'fail'; + + if (!empty($job2['status'])) { + $db->table('claim_dump_files')->where('id', $fileId)->update([ + 'status' => 'success', + 'reason' => null, + ]); + } else { + $db->table('claim_dump_files')->where('id', $fileId)->update([ + 'status' => 'failed', + 'reason' => json_encode(['error_data' => $job2['message'] ?? 'Job2 failed']), + ]); + $result['ok'] = false; + $result['error'] = $job2['message'] ?? 'Job2 failed'; + return $result; + } + + $afterDump = $db->table($cfg['table']) + ->select('id, ticket_id, master_reject_reason, is_active') + ->where('file_id', $fileId) + ->get()->getResultArray(); + CLI::write('Dump AFTER Job2: ' . json_encode($afterDump, JSON_PRETTY_PRINT)); + + $linked = 0; + foreach ($afterDump as $drow) { + if (!empty($drow['ticket_id'])) { + $linked++; + } + } + $result['linked'] = $linked; + if ($linked === 0 && empty($afterDump[0]['master_reject_reason'])) { + $result['ok'] = false; + $result['error'] = 'Job2 reported success but dump ticket_id is still null'; + CLI::error($result['error']); + return $result; + } + + if ($truncate) { + CLI::write('Running soft truncate...', 'light_red'); + $trunc = $service->softTruncateClaimDump($fileId); + CLI::write('Truncate: ' . json_encode($trunc)); + $result['truncated'] = !empty($trunc['status']); + if (empty($trunc['status'])) { + $result['ok'] = false; + $result['error'] = $trunc['message'] ?? 'Truncate failed'; + } + } + + return $result; + } + + private function resolveSeedContext($db, int $tpaId, string $tpaKey, array $cfg, string $statusOverride): array + { + $ticket = $db->table('ticket_master') + ->select('id, client_id, client_policy_id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, tpa_id') + ->where('tpa_id', $tpaId) + ->where('is_active', 1) + ->where('emp_code IS NOT NULL', null, false) + ->where('tpa_no IS NOT NULL', null, false) + ->where('claim_amount IS NOT NULL', null, false) + ->where('doa IS NOT NULL', null, false) + ->orderBy('id', 'DESC') + ->get()->getRowArray(); + + if (empty($ticket)) { + CLI::write("[{$tpaKey}] No ticket for this tpa_id; falling back to any ticket with identity keys.", 'yellow'); + $ticket = $db->table('ticket_master') + ->select('id, client_id, client_policy_id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, tpa_id') + ->where('is_active', 1) + ->where('client_policy_id IS NOT NULL', null, false) + ->where('emp_code IS NOT NULL', null, false) + ->where('tpa_no IS NOT NULL', null, false) + ->where('claim_amount IS NOT NULL', null, false) + ->where('doa IS NOT NULL', null, false) + ->orderBy('id', 'DESC') + ->get()->getRowArray(); + } + + if (empty($ticket)) { + throw new \RuntimeException('No suitable ticket_master row found to seed Excel'); + } + + $policy = $db->table('client_policy') + ->select('id, client_id, policy_no') + ->where('id', $ticket['client_policy_id']) + ->get()->getRowArray(); + if (empty($policy) || trim((string) ($policy['policy_no'] ?? '')) === '') { + throw new \RuntimeException('client_policy / policy_no missing for seed ticket'); + } + + $statusValue = $statusOverride !== '' ? $statusOverride : $cfg['default_status']; + + return [ + 'ticket' => $ticket, + 'client_id' => (int) ($ticket['client_id'] ?? $policy['client_id']), + 'client_policy_id' => (int) $ticket['client_policy_id'], + 'policy_no' => trim((string) $policy['policy_no']), + 'emp_code' => (string) $ticket['emp_code'], + 'tpa_no' => (string) $ticket['tpa_no'], + 'claim_amount' => (string) $ticket['claim_amount'], + 'doa' => (string) $ticket['doa'], + 'relation' => 'SELF', + 'status_value' => $statusValue, + ]; + } + + private function extractExcelHeaders(object $service): array + { + $ref = new ReflectionClass($service); + if (!$ref->hasProperty('mapping')) { + return []; + } + $prop = $ref->getProperty('mapping'); + $prop->setAccessible(true); + $mapping = $prop->getValue($service); + if (!is_array($mapping)) { + return []; + } + + $headers = []; + foreach ($mapping as $map) { + $excel = $map['excel_column'] ?? null; + if (is_array($excel)) { + $name = trim((string) ($excel['col_name'] ?? '')); + } else { + $name = trim((string) $excel); + } + if ($name !== '') { + $headers[] = $name; + } + } + + return $headers; + } + + private function buildExcelRow(array $headers, array $cfg, array $context): array + { + $row = array_fill(0, count($headers), ''); + $headerIndex = array_flip($headers); + + foreach ($cfg['excel_fields'] as $excelHeader => $sourceKey) { + if (!isset($headerIndex[$excelHeader])) { + CLI::write("Warning: Excel header '{$excelHeader}' not in mapping; skipped.", 'yellow'); + continue; + } + $row[$headerIndex[$excelHeader]] = $context[$sourceKey] ?? ''; + } + + // Ensure status header is filled even if excel_fields key differs. + if (isset($headerIndex[$cfg['status_header']])) { + $row[$headerIndex[$cfg['status_header']]] = $context['status_value']; + } + + return $row; + } + + private function writeExcel(string $filePath, array $headers, array $rowValues, ?string $sheetName): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + if ($sheetName) { + $sheet->setTitle($sheetName); + } + + foreach ($headers as $i => $header) { + $col = $i + 1; + $sheet->setCellValue([$col, 1], $header); + $sheet->setCellValue([$col, 2], $rowValues[$i] ?? ''); + } + + $writer = new Xlsx($spreadsheet); + $writer->save($filePath); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } + + private function createClaimDumpFile($db, array $data): int + { + $cols = array_column($db->query('SHOW COLUMNS FROM claim_dump_files')->getResultArray(), 'Field'); + $data = array_intersect_key($data, array_flip($cols)); + $db->table('claim_dump_files')->insert($data); + $id = (int) $db->insertID(); + if ($id <= 0) { + throw new \RuntimeException('Failed to insert claim_dump_files'); + } + return $id; + } + + private function tableExists($db, string $table): bool + { + return !empty($db->query('SHOW TABLES LIKE ' . $db->escape($table))->getResultArray()); + } + + private function hasFlag(string $name): bool + { + if (CLI::getOption($name) !== null) { + return true; + } + return in_array('--' . $name, $_SERVER['argv'] ?? [], true); + } + + private function resolveOptionValue(string $name, string $default): string + { + $opt = CLI::getOption($name); + if (is_string($opt) && $opt !== '') { + return $opt; + } + + $argv = $_SERVER['argv'] ?? []; + foreach ($argv as $i => $arg) { + if (preg_match('/^--' . preg_quote($name, '/') . '=(.+)$/', (string) $arg, $m)) { + return trim($m[1]); + } + if ($arg === '--' . $name && isset($argv[$i + 1]) && strpos((string) $argv[$i + 1], '--') !== 0) { + return (string) $argv[$i + 1]; + } + } + + return $default; + } +} diff --git a/app/Commands/TestTpaStatusUpdate.php b/app/Commands/TestTpaStatusUpdate.php index 6dc61edc..8b9471c5 100644 --- a/app/Commands/TestTpaStatusUpdate.php +++ b/app/Commands/TestTpaStatusUpdate.php @@ -261,6 +261,24 @@ class TestTpaStatusUpdate extends BaseCommand $result = $service->runTicketMasterInsert(['file_id' => $currentFileId]); CLI::write('Result: ' . json_encode($result, JSON_PRETTY_PRINT)); + // Mirror TicketServiceController Job 2 success/fail file status update. + if (!empty($result['status'])) { + $db->table('claim_dump_files')->where('id', $currentFileId)->update([ + 'status' => 'success', + 'reason' => null, + ]); + CLI::write("claim_dump_files.status => success", 'green'); + } else { + $db->table('claim_dump_files')->where('id', $currentFileId)->update([ + 'status' => 'failed', + 'reason' => json_encode([ + 'error_summary' => [5 => 1], + 'error_data' => $result['message'] ?? 'Job 2 failed', + ]), + ]); + CLI::write("claim_dump_files.status => failed", 'red'); + } + $afterDump = $db->table($cfg['table']) ->select($this->dumpSelectColumns($cfg)) ->where('file_id', $currentFileId) @@ -489,6 +507,15 @@ class TestTpaStatusUpdate extends BaseCommand if (in_array($arg, ['--file_id', '--file-id'], true) && isset($argv[$i + 1])) { return (int) $argv[$i + 1]; } + // Bare numeric arg (e.g. `tpa:test-status --tpa=icici 70 --apply`) + if (is_numeric($arg) && (int) $arg > 0 && $i > 0 && strpos((string) $argv[$i - 1], 'spark') === false) { + $prev = (string) ($argv[$i - 1] ?? ''); + // Skip values belonging to options like --tpa=... or --new-status STATUS + if ($prev === '--tpa' || $prev === '--new-status' || $prev === '--file_id' || $prev === '--file-id') { + continue; + } + return (int) $arg; + } } return (int) (CLI::getOption('file_id') ?? CLI::getOption('file-id') ?? 0); diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 9989ee54..d2df5889 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -116,6 +116,8 @@ $routes->group("/user", ["filter" => "authMVC"], function ($routes) { $routes->group("/dashboard", ["filter" => "authMVC"], function ($routes) { $routes->get("view", "DashboardController::dashboard"); + $routes->get("claims-dash", "DashboardController::claimsDashFragment"); + $routes->get("leads-dash", "DashboardController::leadsDashFragment"); $routes->get('get-notification', 'DashboardController::getDashboardNotifications'); $routes->get('acknowledge-notification/(:segment)', 'DashboardController::acknowledgeMessage/$1'); $routes->get('get-pending-action', 'PendingActionsController::getPendingActions'); @@ -444,6 +446,9 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('getClaimDumpFileErrorData', 'TicketController::getClaimDumpFileErrorData'); $routes->get("claim_dump_excel_error/(:any)", "TicketController::getClaimDumpExcelFileErrors/$1"); $routes->get("download_claim_dump_file/(:any)", "TicketController::downloadClaimDumpFile/$1"); + $routes->match(['get', 'post'], 'truncateClaimDumpFile', 'TicketController::truncateClaimDumpFile'); + $routes->match(['get', 'post'], 'reprocessClaimDumpPending', 'TicketController::reprocessClaimDumpPending'); + $routes->match(['get', 'post'], 'getClaimDumpPendingRows', 'TicketController::getClaimDumpPendingRows'); $routes->post('uploadMultiFileFromRfq', 'LeadsController::uploadMultiFileFromRfq'); $routes->get('downloadMemberFile/(:any)', 'LeadsController::downloadMemberFile/$1'); $routes->get('downloadFullMemberDataExcelErrorFile/(:any)', 'LeadsController::downloadFullMemberDataExcelErrorFile/$1'); diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index b56714a0..d64146ee 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -2994,20 +2994,20 @@ class ClientController extends AdminController $base_policy = $sanitized_post_data['base_policy'] ?? null; - $insurerValue = (string) $sanitized_post_data['insurer'] ?? null; + $insurerValue = (string) ($sanitized_post_data['insurer'] ?? ''); list($insurerBranchId, $insurerId) = explode('-', $insurerValue); $sanitized_post_data['insurer_branch_id'] = $insurerBranchId; $sanitized_post_data['insurer_id'] = $insurerId; - $tpaValue = (string) $sanitized_post_data['tpa'] ?? null; + $tpaValue = (string) ($sanitized_post_data['tpa'] ?? ''); - if ($tpaValue === null || $tpaValue === '') { + if ($tpaValue === '') { $tpaBranchId = null; $tpaId = null; } else { - list($tpaBranchId, $tpaId) = explode('-', $tpaValue); + list($tpaBranchId, $tpaId) = array_pad(explode('-', $tpaValue, 2), 2, null); } @@ -3197,15 +3197,15 @@ class ClientController extends AdminController $base_policy = $sanitized_post_data['base_policy'] ?? null; - $insurerValue = (string) $sanitized_post_data['insurer']; + $insurerValue = (string) ($sanitized_post_data['insurer'] ?? ''); list($insurerBranchId, $insurerId) = explode('-', $insurerValue); $sanitized_post_data['insurer_branch_id'] = $insurerBranchId ?? null; $sanitized_post_data['insurer_id'] = $insurerId ?? null; - $tpaValue = (string) $sanitized_post_data['tpa'] ?? null; - if (!empty($tpaValue) || $tpaValue !== '') { - list($tpaBranchId, $tpaId) = explode('-', $tpaValue); + $tpaValue = (string) ($sanitized_post_data['tpa'] ?? ''); + if ($tpaValue !== '') { + list($tpaBranchId, $tpaId) = array_pad(explode('-', $tpaValue, 2), 2, null); } else { $tpaBranchId = null; $tpaId = null; diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index 3ff685d7..a7464980 100755 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -191,105 +191,101 @@ class DashboardController extends AdminController public function dashboard() { - $data = []; + $roleId = get_role_id(); + $teams = user_team(); - if (in_array(get_role_id(), [1, 2, 3, 5]) || (in_array(MANAGEMENT_TEAM_ID, user_team()) || in_array(FINANCE_TEAM_ID, user_team()) || in_array(BUSINESS_TEAM_ID, user_team()))) { + $showPending = in_array($roleId, [1, 2, 3, 5]); + $showClaims = ($roleId == STAFF_ROLE_ID && in_array(CLAIMS_TEAM_ID, $teams)) || in_array($roleId, [1, 5]); + $showLeads = ($roleId == STAFF_ROLE_ID && in_array(ENROLLMENT_TEAM_ID, $teams)) || in_array($roleId, [1, 5]); - $db = db_connect(); - $sql = "SELECT - clients.id AS client_id, - clients.client_name, - clients.short_name, - client_branch.id AS client_branch_id, - client_branch.branch_name, - client_branch.branch_code, - - COUNT(employees.id) AS total_employees, - SUM(CASE WHEN employees.emp_status = 'draft' THEN 1 ELSE 0 END) AS draft_count, - SUM(CASE WHEN employees.emp_status IN ('enrolled', 'active') THEN 1 ELSE 0 END) AS enrolled_count, - - SUM(CASE WHEN auth_history.user_id IS NOT NULL THEN 1 ELSE 0 END) AS logged_in_count, - SUM(CASE WHEN auth_history.user_id IS NULL THEN 1 ELSE 0 END) AS not_logged_in_count, - - CASE - WHEN EXISTS ( - SELECT 1 FROM client_policy - WHERE client_policy.client_branch_id = client_branch.id - AND client_policy.is_active = 1 - AND client_policy.open_for_enrollment = 1 - ) THEN 1 - ELSE 0 - END AS open_or_close_enrollment - - FROM clients - LEFT JOIN client_branch ON clients.id = client_branch.client_id - LEFT JOIN employees ON client_branch.id = employees.client_branch_id - LEFT JOIN ( - SELECT user_id, user_type - FROM auth_history - WHERE user_type = 'employee' - GROUP BY user_id - ) AS auth_history ON employees.id = auth_history.user_id - - WHERE employees.relationship = 'Self' - AND employees.emp_status IN ('draft', 'enrolled', 'active') - AND employees.is_active = 1 - AND clients.is_active = 1 - AND client_branch.is_active = 1 - - GROUP BY clients.id, client_branch.id"; - - $query = $db->query($sql); - $results = $query->getResultArray(); - - $pendingActionsController = new PendingActionsController; - $pendingActionsData = $pendingActionsController->getPendingActionsForDashBoard(); - - //BDS dashboard data - // $businessTeamData = $this->policyTransactionModel->getBusinessReportList(); - // $financeTeamData = $this->policyTransactionModel->getFinanceReportList(); - // $businessTeamStatusData = $this->data_construct_for_bds($businessTeamData); - // $financeTeamStatusData = $this->data_construct_for_bds($financeTeamData); - $businessTeamData = []; - $financeTeamData = []; - $businessTeamStatusData = []; - $financeTeamStatusData = []; - - $data['client_branch_emp_list'] = $results; - $session = \Config\Services::session(); - // $session->set('enrollment_data', json_encode($data)); - - $data['pendingActionsData'] = $pendingActionsData; - $data['businessTeamCount'] = count($businessTeamData) ?? 0; - $data['financeTeamCount'] = count($financeTeamData) ?? 0; - $data['businessTeamStatusData'] = $businessTeamStatusData; - $data['financeTeamStatusData'] = $financeTeamStatusData; - $data['policyStatus'] = $this->policyStatus; - $data['colorShades'] = $this->colorShades; + if ($showPending) { + $defaultPane = 'pending'; + } elseif ($showClaims) { + $defaultPane = 'claims'; + } elseif ($showLeads) { + $defaultPane = 'leads'; + } else { + $defaultPane = null; } - if ((get_role_id() == STAFF_ROLE_ID && in_array(CLAIMS_TEAM_ID,user_team())) || in_array(get_role_id(),[1,5])){ + + if ($showPending) { + $pendingActionsController = new PendingActionsController(); + $data['pendingActionsData'] = $pendingActionsController->getPendingActionsForDashBoard(); + $data['businessTeamCount'] = 0; + $data['financeTeamCount'] = 0; + $data['businessTeamStatusData'] = []; + $data['financeTeamStatusData'] = []; + $data['policyStatus'] = $this->policyStatus; + $data['colorShades'] = $this->colorShades; + } + + // Only the default tab loads with the page; other tabs fetch on click. + $data['lazy_load_claims'] = $showClaims && $defaultPane !== 'claims'; + $data['lazy_load_leads'] = $showLeads && $defaultPane !== 'leads'; + + if ($showClaims && !$data['lazy_load_claims']) { $data['claim_data'] = $this->getClaimData(); - $data['colorShades'] = $this->colorShades; - + $data['colorShades'] = $this->colorShades; } - if ((get_role_id() == STAFF_ROLE_ID && in_array(ENROLLMENT_TEAM_ID,user_team())) || in_array(get_role_id(),[1,5])){ + + if ($showLeads && !$data['lazy_load_leads']) { $data['lead_data'] = $this->leadModel->getDashData(); $data['bds_renewal'] = $this->policyTransactionModel->getBDSRenewalData(); - $data['colorShades'] = $this->colorShades; - + $data['colorShades'] = $this->colorShades; } - // dd(get_role_id(),user_team()); - // dd($data); $data['tab_name'] = 'Dashboard'; $data['page_name'] = 'Dashboard'; - echo view('layout/header', $data); + echo view('layout/header', $data); echo view('DashBoard', $data); echo view('layout/footer'); } + /** + * HTML fragment for Claims dashboard tab (loaded on tab click). + */ + public function claimsDashFragment() + { + $roleId = get_role_id(); + $teams = user_team(); + $allowed = ($roleId == STAFF_ROLE_ID && in_array(CLAIMS_TEAM_ID, $teams)) || in_array($roleId, [1, 5]); + if (!$allowed) { + return $this->response->setStatusCode(403)->setBody('Forbidden'); + } + + $data = [ + 'claim_data' => $this->getClaimData(), + 'colorShades' => $this->colorShades, + 'dash_claims_pane_active' => 'active show', + ]; + + return $this->response->setBody(view('claims_dash', $data)); + } + + /** + * HTML fragment for Leads / BDS Renewals dashboard tab (loaded on tab click). + */ + public function leadsDashFragment() + { + $roleId = get_role_id(); + $teams = user_team(); + $allowed = ($roleId == STAFF_ROLE_ID && in_array(ENROLLMENT_TEAM_ID, $teams)) || in_array($roleId, [1, 5]); + if (!$allowed) { + return $this->response->setStatusCode(403)->setBody('Forbidden'); + } + + $data = [ + 'lead_data' => $this->leadModel->getDashData(), + 'bds_renewal' => $this->policyTransactionModel->getBDSRenewalData(), + 'colorShades' => $this->colorShades, + 'dash_leads_pane_active' => 'active show', + ]; + + return $this->response->setBody(view('leads_dash', $data)); + } + public function getClaimData() { diff --git a/app/Controllers/PendingActionsController.php b/app/Controllers/PendingActionsController.php index bb8f2e92..0151f0c2 100755 --- a/app/Controllers/PendingActionsController.php +++ b/app/Controllers/PendingActionsController.php @@ -97,115 +97,292 @@ class PendingActionsController extends AdminController return $data; } - //for only COUNT + /** + * Dashboard counts only — avoids loading full pending-action rowsets. + * Cached briefly so repeated dashboard hits don't re-run heavy aggregations. + */ public function getPendingActionsForDashBoard() { - $tpa = $this->getPendingActionForTPAIDEmpty(); - $uhid = $this->getPendingActionForUHIDEmpty(); - $deletion = $this->getPendingActionForDeletion(); - $inception = $this->getPendingActionForInception(); - $ticketData = $this->getTicketsDataForDashBoard(); - $correction = $this->getPendingActionForCorrection(); - $si_enhancement = $this->getPendingActionForSIEnhancement(); - $PolicyRenewalData = $this->getPolicyRenewalDataForAllClient(); - - $uhid_export_count = []; - $uhid_not_export_count = []; - foreach ($uhid as $value) { - if($value['batch_export_count'] == 1){ - $uhid_export_count[] = $value['batch_export_count']; - }else{ - $uhid_not_export_count[] = $value['batch_export_count']; - } - } - $tpa_export_count = []; - $tpa_not_export_count = []; - foreach ($tpa as $value) { - if($value['batch_export_count'] == 1){ - $tpa_export_count[] = $value['batch_export_count']; - }else{ - $tpa_not_export_count[] = $value['batch_export_count']; - } - } - $deletion_export_count = []; - $deletion_not_export_count = []; - foreach ($deletion as $value) { - if($value['batch_export_count'] == 1){ - $deletion_export_count[] = $value['batch_export_count']; - }else{ - $deletion_not_export_count[] = $value['batch_export_count']; - } - } - $correction_export_count = []; - $correction_not_export_count = []; - foreach ($correction as $value) { - if($value['batch_export_count'] == 1){ - $correction_export_count[] = $value['batch_export_count']; - }else{ - $correction_not_export_count[] = $value['batch_export_count']; - } - } - $si_export_count = []; - $si_not_export_count = []; - foreach ($si_enhancement as $value) { - if($value['batch_export_count'] == 1){ - $si_export_count[] = $value['batch_export_count']; - }else{ - $si_not_export_count[] = $value['batch_export_count']; - } + $cache = \Config\Services::cache(); + $cacheKey = 'dashboard_pending_actions_counts_v2'; + $cached = $cache->get($cacheKey); + if (is_array($cached)) { + return $cached; } - // dd($uhid_export_count, $tpa_export_count ,$deletion_export_count, $correction_export_count, $si_export_count, $ticketData, $inception, $si_enhancement, $correction, $deletion, $tpa, $uhid, $PolicyRenewalData); - - $inception = count($inception); - $correction = count($correction); - $si_enhancement = count($si_enhancement); - $deletion = count($deletion); - $tpa = count($tpa); - $uhid = count($uhid); - $PolicyRenewalData = count($PolicyRenewalData); - $ticketData = count($ticketData); - - $uhid_export_count = count($uhid_export_count); - $tpa_export_count = count($tpa_export_count); - $deletion_export_count = count($deletion_export_count); - $correction_export_count = count($correction_export_count); - $si_export_count = count($si_export_count); - - $uhid_not_export_count = count($uhid_not_export_count); - $tpa_not_export_count = count($tpa_not_export_count); - $deletion_not_export_count = count($deletion_not_export_count); - $correction_not_export_count = count($correction_not_export_count); - $si_not_export_count = count($si_not_export_count); - - - // dd($inception, $si_enhancement, $correction, $deletion, $tpa, $uhid, $PolicyRenewalData); + $uhidCounts = $this->countPendingUhidForDashboard(); + $tpaCounts = $this->countPendingTpaForDashboard(); + $correctionCounts = $this->countPendingCorrectionForDashboard(); + $deletionCounts = $this->countPendingDeletionForDashboard(); + $siCounts = $this->countPendingSiForDashboard(); $data = [ - 'inception' => $inception, - 'correction' => $correction, - 'si_enhancement' => $si_enhancement, - 'deletion' => $deletion, - 'tpa' => $tpa, - 'uhid' => $uhid, - 'PolicyRenewalData' => $PolicyRenewalData, - 'ticketData' => $ticketData, - - 'uhid_export_count' => $uhid_export_count, - 'tpa_export_count' => $tpa_export_count, - 'deletion_export_count' => $deletion_export_count, - 'correction_export_count' => $correction_export_count, - 'si_export_count' => $si_export_count, - - 'uhid_not_export_count' => $uhid_not_export_count, - 'tpa_not_export_count' => $tpa_not_export_count, - 'deletion_not_export_count' => $deletion_not_export_count, - 'correction_not_export_count' => $correction_not_export_count, - 'si_not_export_count' => $si_not_export_count, + 'inception' => $this->countPendingInceptionForDashboard(), + 'correction' => $correctionCounts['total'], + 'si_enhancement' => $siCounts['total'], + 'deletion' => $deletionCounts['total'], + 'tpa' => $tpaCounts['total'], + 'uhid' => $uhidCounts['total'], + 'PolicyRenewalData' => $this->countPolicyRenewalForDashboard(), + 'ticketData' => $this->countTicketsForDashboard(), + 'uhid_export_count' => $uhidCounts['export_count'], + 'tpa_export_count' => $tpaCounts['export_count'], + 'deletion_export_count' => $deletionCounts['export_count'], + 'correction_export_count' => $correctionCounts['export_count'], + 'si_export_count' => $siCounts['export_count'], + 'uhid_not_export_count' => $uhidCounts['not_export_count'], + 'tpa_not_export_count' => $tpaCounts['not_export_count'], + 'deletion_not_export_count' => $deletionCounts['not_export_count'], + 'correction_not_export_count' => $correctionCounts['not_export_count'], + 'si_not_export_count' => $siCounts['not_export_count'], ]; + + $cache->save($cacheKey, $data, 60); + return $data; } + private function countPendingInceptionForDashboard(): int + { + // NOT EXISTS avoids scanning employee_polices for every open policy via LEFT JOIN + HAVING. + $sql = " + SELECT COUNT(*) AS cnt + FROM client_policy cp + INNER JOIN clients ON cp.client_id = clients.id + INNER JOIN client_branch cb ON cb.id = cp.client_branch_id + INNER JOIN policy_type ON cp.policy_type_id = policy_type.id + WHERE cp.is_active = 1 + AND cp.open_for_enrollment = 1 + AND cp.is_addon = 1 + AND cp.policy_status = 1 + AND cp.inception_type = 1 + AND cp.policy_type_id IN (1, 2, 3) + AND NOT EXISTS ( + SELECT 1 + FROM employee_polices ep + WHERE ep.client_policy_id = cp.id + AND ep.is_active = 1 + ) + "; + + return (int) ($this->db->query($sql)->getRowArray()['cnt'] ?? 0); + } + + private function countPendingUhidForDashboard(): array + { + // Aggregate batch_files once, then join — correlated COUNT per policy was acceptable but JOIN scales better. + $sql = " + SELECT + COUNT(*) AS total, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) = 1 THEN 1 ELSE 0 END) AS export_count, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) != 1 THEN 1 ELSE 0 END) AS not_export_count + FROM ( + SELECT ep.client_policy_id + FROM employee_polices ep + INNER JOIN client_policy cp + ON ep.client_policy_id = cp.id + AND cp.is_active = 1 + AND cp.policy_status = 1 + INNER JOIN clients c ON c.id = cp.client_id + INNER JOIN client_branch cb ON cb.id = cp.client_branch_id + INNER JOIN policy_type ON policy_type.id = cp.policy_type_id + WHERE ep.uhid IS NULL + AND ep.status = 'active' + AND ep.is_active = 1 + GROUP BY ep.client_policy_id + ) AS pending + LEFT JOIN ( + SELECT client_policy_id, COUNT(*) AS export_cnt + FROM batch_files + WHERE actions = 'export' + AND event_type = 'inception' + AND insurer_or_tpa = 'insurer' + GROUP BY client_policy_id + ) AS bf ON bf.client_policy_id = pending.client_policy_id + "; + + return $this->normalizeExportCounts($this->db->query($sql)->getRowArray()); + } + + private function countPendingTpaForDashboard(): array + { + $sql = " + SELECT + COUNT(*) AS total, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) = 1 THEN 1 ELSE 0 END) AS export_count, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) != 1 THEN 1 ELSE 0 END) AS not_export_count + FROM ( + SELECT employee_polices.client_policy_id + FROM employee_polices + INNER JOIN client_policy + ON employee_polices.client_policy_id = client_policy.id + AND client_policy.is_active = 1 + AND client_policy.policy_status = 1 + INNER JOIN clients ON clients.id = client_policy.client_id + INNER JOIN client_branch ON client_branch.id = client_policy.client_branch_id + INNER JOIN policy_type ON policy_type.id = client_policy.policy_type_id + WHERE employee_polices.tpa_id IS NULL + AND employee_polices.uhid IS NOT NULL + AND employee_polices.status = 'active' + AND employee_polices.is_active = 1 + GROUP BY employee_polices.client_policy_id + ) AS pending + LEFT JOIN ( + SELECT client_policy_id, COUNT(*) AS export_cnt + FROM batch_files + WHERE actions = 'export' + AND event_type = 'inception' + AND insurer_or_tpa = 'tpa' + GROUP BY client_policy_id + ) AS bf ON bf.client_policy_id = pending.client_policy_id + "; + + return $this->normalizeExportCounts($this->db->query($sql)->getRowArray()); + } + + /** + * Start from pending emp_endorsement rows — NOT from all clients with EXISTS + * (EXISTS over every client was hanging for minutes on staging ~500k policies). + */ + private function countPendingCorrectionForDashboard(): array + { + $sql = " + SELECT + COUNT(*) AS total, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) = 1 THEN 1 ELSE 0 END) AS export_count, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) != 1 THEN 1 ELSE 0 END) AS not_export_count + FROM ( + SELECT e.client_id + FROM emp_endorsement ee + INNER JOIN employees e + ON ee.pk = e.id + AND e.is_active = 1 + AND e.emp_status = 'active' + WHERE ee.actions = 'c' + AND ee.is_active = 1 + AND ee.status = 'pending' + AND ee.endorsement_id IS NULL + GROUP BY e.client_id + ) AS pending + LEFT JOIN ( + SELECT client_id, COUNT(*) AS export_cnt + FROM batch_files + WHERE actions = 'export' + AND event_type = 'correction' + AND insurer_or_tpa = 'tpa' + GROUP BY client_id + ) AS bf ON bf.client_id = pending.client_id + "; + + return $this->normalizeExportCounts($this->db->query($sql)->getRowArray()); + } + + private function countPendingDeletionForDashboard(): array + { + $sql = " + SELECT + COUNT(*) AS total, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) = 1 THEN 1 ELSE 0 END) AS export_count, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) != 1 THEN 1 ELSE 0 END) AS not_export_count + FROM ( + SELECT cp.client_id + FROM emp_endorsement ee + INNER JOIN employee_polices ep + ON ee.pk = ep.id + AND ep.is_active = 1 + AND ep.status = 'active' + INNER JOIN client_policy cp + ON ep.client_policy_id = cp.id + AND cp.is_active = 1 + AND cp.policy_status = 1 + WHERE ee.actions = 'd' + AND ee.is_active = 1 + AND ee.status = 'pending' + AND ee.endorsement_id IS NULL + AND ee.table_name = 'employee_polices' + GROUP BY cp.client_id + ) AS pending + LEFT JOIN ( + SELECT client_id, COUNT(*) AS export_cnt + FROM batch_files + WHERE actions = 'export' + AND event_type = 'deletion' + AND insurer_or_tpa = 'insurer' + GROUP BY client_id + ) AS bf ON bf.client_id = pending.client_id + "; + + return $this->normalizeExportCounts($this->db->query($sql)->getRowArray()); + } + + private function countPendingSiForDashboard(): array + { + $sql = " + SELECT + COUNT(*) AS total, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) = 1 THEN 1 ELSE 0 END) AS export_count, + SUM(CASE WHEN COALESCE(bf.export_cnt, 0) != 1 THEN 1 ELSE 0 END) AS not_export_count + FROM ( + SELECT cp.client_id + FROM emp_endorsement ee + INNER JOIN employee_polices ep + ON ee.pk = ep.id + AND ep.is_active = 1 + AND ep.status = 'active' + INNER JOIN client_policy cp + ON ep.client_policy_id = cp.id + AND cp.is_active = 1 + AND cp.policy_status = 1 + WHERE ee.actions = 'si' + AND ee.is_active = 1 + AND ee.status = 'pending' + AND ee.endorsement_id IS NULL + GROUP BY cp.client_id + ) AS pending + LEFT JOIN ( + SELECT client_id, COUNT(*) AS export_cnt + FROM batch_files + WHERE actions = 'export' + AND event_type = 'si_enhancement' + AND insurer_or_tpa = 'tpa' + GROUP BY client_id + ) AS bf ON bf.client_id = pending.client_id + "; + + return $this->normalizeExportCounts($this->db->query($sql)->getRowArray()); + } + + private function countPolicyRenewalForDashboard(): int + { + return (int) $this->clientPolicyModel + ->join('clients', 'clients.id = client_policy.client_id') + ->join('client_branch', 'client_branch.id = client_policy.client_branch_id') + ->where('client_policy.is_active', 1) + ->where('client_policy.policy_status', 0) + ->where('clients.is_active', 1) + ->where('client_branch.is_active', 1) + ->where('client_policy.policy_end_date < DATE_ADD(CURDATE(), INTERVAL 2 MONTH)', null, false) + ->countAllResults(); + } + + private function countTicketsForDashboard(): int + { + return (int) $this->db->table('hdz_tickets') + ->join('hdz_status', 'hdz_status.id = hdz_tickets.status') + ->where('hdz_status.active', 1) + ->where('hdz_tickets.status !=', 5) + ->countAllResults(); + } + + private function normalizeExportCounts(?array $row): array + { + return [ + 'total' => (int) ($row['total'] ?? 0), + 'export_count' => (int) ($row['export_count'] ?? 0), + 'not_export_count' => (int) ($row['not_export_count'] ?? 0), + ]; + } + diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php index 0e1b76a2..a23a8711 100644 --- a/app/Controllers/TicketController.php +++ b/app/Controllers/TicketController.php @@ -2527,14 +2527,44 @@ class TicketController extends BaseController public function removeTicket() { $ticket_id = $this->request->getGet('ticket_id'); - if(!empty($ticket_id)){ - $data['is_active'] = 0; + if (!empty($ticket_id)) { + $data['is_active'] = 0; $this->ticketMasterModel->where('id', $ticket_id)->set($data)->update(); + $this->clearDumpTicketLinksForTicket((int) $ticket_id); return $this->respond(['status' => true, 'code' => 200, 'message' => 'Claim removed successfully'], 200); - }else{ + } else { return $this->respond(['status' => false, 'code' => 404, 'message' => 'Failed to remove Claim'], 200); } - + } + + /** + * Clear TPA dump ticket_id links when a ticket is soft-deleted (avoids orphan dump refs). + */ + private function clearDumpTicketLinksForTicket(int $ticketId): void + { + if ($ticketId <= 0) { + return; + } + + $tables = [ + 'claims_dump_vidal', + 'claims_dump_abhi', + 'claims_dump_medi_assist', + 'claims_dump_fhpl', + 'claims_dump_reliance', + 'claims_dump_icici', + ]; + + $db = db_connect(); + foreach ($tables as $table) { + if (!$db->tableExists($table)) { + continue; + } + $db->table($table) + ->where('ticket_id', $ticketId) + ->set(['ticket_id' => null]) + ->update(); + } } public function getTpaClaimPushLogs() @@ -4317,6 +4347,7 @@ class TicketController extends BaseController claim_dump_files.id as file_id, claim_dump_files.file_name, claim_dump_files.status, + claim_dump_files.tpa_id, claim_dump_files.created_at, up.first_name as user_name, c.client_name, @@ -4329,6 +4360,10 @@ class TicketController extends BaseController ->orderBy('claim_dump_files.id', 'desc') ->findAll(); + $data['claim_dump_file_data'] = $this->attachPendingDumpTicketCounts( + $data['claim_dump_file_data'] ?? [] + ); + return $this->loadLayout('claim_dump_file_list', $data); }else{ @@ -4391,15 +4426,22 @@ class TicketController extends BaseController $file_id = $this->claimDumpFileModel->insert($insert_data); $this->myLogger->logme("error", 'claim_dumb_file_id : {file_id}, uploaded success', ['file_id' => $file_id]); + log_message('error', '[CLAIM_DUMP][UPLOAD_SAVED] file_id=' . $file_id + . ' file_name=' . $filename + . ' client_id=' . ($insert_data['client_id'] ?? 'null') + . ' client_policy_id=' . ($insert_data['client_policy_id'] ?? 'null') + . ' tpa_id=' . ($insert_data['tpa_id'] ?? 'null')); //after file upload success than call the file formate validation in service controller // $ticketServiceController = new TicketServiceController(); if(!empty($insert_data['tpa_id'])){ $r = Jobs::addJob(['job_name' => 'tpaClaimDumpImporter', 'payload' => ['file_id' => $file_id]]); + log_message('error', '[CLAIM_DUMP][UPLOAD_QUEUED_JOB1] file_id=' . $file_id . ' job=tpaClaimDumpImporter'); // $response = $ticketServiceController->claimDumpExcelFileFormatValidation(["file_id" => $file_id]); }else{ $r = Jobs::addJob(['job_name' => 'claimDumpExcelFileFormatValidation', 'payload' => ['file_id' => $file_id]]); + log_message('error', '[CLAIM_DUMP][UPLOAD_QUEUED_LEGACY] file_id=' . $file_id . ' job=claimDumpExcelFileFormatValidation'); // $response = $ticketServiceController->claimDumpExcelFileFormatValidation(["file_id" => $file_id]); } return $this->respond(['status' => true, 'code' => 200, 'message' => 'File uploaded successfully. File being validated'], 200); @@ -4472,6 +4514,414 @@ class TicketController extends BaseController } } + + /** + * Soft-delete a TPA claim dump upload: dump rows + tickets created by that file_id. + */ + public function truncateClaimDumpFile() + { + $fileId = (int) ($this->request->getGet('file_id') ?? $this->request->getPost('file_id') ?? 0); + log_message('error', '[CLAIM_DUMP][CTRL_TRUNCATE_START] file_id=' . $fileId); + + if ($fileId <= 0) { + log_message('error', '[CLAIM_DUMP][CTRL_TRUNCATE_INVALID] file_id=' . $fileId); + return $this->respond(['status' => false, 'code' => 400, 'message' => 'file_id is required'], 200); + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->where('is_active', 1)->first(); + if (empty($fileData)) { + log_message('error', '[CLAIM_DUMP][CTRL_TRUNCATE_NOT_FOUND] file_id=' . $fileId); + return $this->respond(['status' => false, 'code' => 404, 'message' => 'File not found or already truncated'], 200); + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + if ($tpaId <= 0) { + log_message('error', '[CLAIM_DUMP][CTRL_TRUNCATE_NO_TPA] file_id=' . $fileId); + return $this->respond(['status' => false, 'code' => 400, 'message' => 'Truncate is only supported for TPA claim dumps'], 200); + } + + try { + $handler = \App\Libraries\TpaClaimsImportFactory::make($tpaId); + $result = $handler->softTruncateClaimDump($fileId); + log_message( + !empty($result['status']) ? 'info' : 'error', + '[CLAIM_DUMP][CTRL_TRUNCATE_DONE] file_id=' . $fileId + . ' status=' . (!empty($result['status']) ? 'true' : 'false') + . ' message=' . ($result['message'] ?? '') + ); + + return $this->respond([ + 'status' => !empty($result['status']), + 'code' => !empty($result['status']) ? 200 : 400, + 'message' => $result['message'] ?? 'Truncate failed', + 'data' => $result, + ], 200); + } catch (\Throwable $th) { + $this->myLogger->logme('error', 'truncateClaimDumpFile: ' . $th->getMessage()); + log_message('error', '[CLAIM_DUMP][CTRL_TRUNCATE_EXCEPTION] file_id=' . $fileId . ' error=' . $th->getMessage()); + return $this->respond([ + 'status' => false, + 'code' => 500, + 'message' => 'Truncate failed: ' . $th->getMessage(), + ], 200); + } + } + /** + * List dump rows for a file where ticket_id is still NULL (not moved to ticket_master). + */ + public function getClaimDumpPendingRows() + { + $fileId = (int) ($this->request->getGet('file_id') ?? $this->request->getPost('file_id') ?? 0); + if ($fileId <= 0) { + return $this->respond(['status' => false, 'code' => 400, 'message' => 'file_id is required'], 200); + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->where('is_active', 1)->first(); + if (empty($fileData)) { + return $this->respond(['status' => false, 'code' => 404, 'message' => 'File not found'], 200); + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + $tableMap = $this->getTpaDumpTableMap(); + $tpaTable = $tableMap[$tpaId] ?? null; + if ($tpaTable === null) { + return $this->respond([ + 'status' => false, + 'code' => 400, + 'message' => 'Pending dump view is only supported for TPA claim dumps', + ], 200); + } + + $displayMap = $this->getPendingDumpDisplayColumns($tpaId); + $selectCols = array_values(array_unique(array_merge(['id'], array_keys($displayMap)))); + + $db = db_connect(); + if (!$db->tableExists($tpaTable)) { + return $this->respond(['status' => false, 'code' => 400, 'message' => 'Dump table not found'], 200); + } + + // Only select columns that exist on the table. + $existingCols = array_column($db->query('SHOW COLUMNS FROM `' . $tpaTable . '`')->getResultArray(), 'Field'); + $selectCols = array_values(array_intersect($selectCols, $existingCols)); + if ($selectCols === []) { + $selectCols = ['id']; + } + + $rows = $db->table($tpaTable) + ->select(implode(', ', $selectCols)) + ->where('file_id', $fileId) + ->where('is_active', 1) + ->where('ticket_id IS NULL', null, false) + ->orderBy('id', 'ASC') + ->limit(500) + ->get() + ->getResultArray(); + + $headers = []; + foreach ($selectCols as $col) { + $headers[] = [ + 'key' => $col, + 'label' => $displayMap[$col] ?? $col, + ]; + } + + return $this->respond([ + 'status' => true, + 'code' => 200, + 'message' => 'Pending dump rows loaded', + 'data' => [ + 'file_id' => $fileId, + 'file_name' => $fileData['file_name'] ?? '', + 'tpa_id' => $tpaId, + 'count' => count($rows), + 'headers' => $headers, + 'rows' => $rows, + ], + ], 200); + } + + /** + * Display columns for pending dump modal (db_column => label). + * + * @return array + */ + private function getPendingDumpDisplayColumns(int $tpaId): array + { + $byTpa = [ + (int) env('ICICI_PRIMARY_KEY_CONSTANT') => [ + 'id' => 'Dump ID', + 'employee_member_id' => 'Emp Code', + 'main_member_name' => 'Employee Name', + 'insured_name' => 'Insured Name', + 'uhid' => 'UHID / TPA No', + 'claimed_amount' => 'Claim Amount', + 'doa' => 'DOA', + 'updated_status' => 'Status', + 'relation' => 'Relation', + 'master_reject_reason' => 'Reject Reason', + ], + (int) env('ABHI_PRIMARY_KEY_CONSTANT') => [ + 'id' => 'Dump ID', + 'member_code' => 'Emp Code', + 'proposer_name' => 'Employee Name', + 'patient_name' => 'Insured Name', + 'healthcard_id' => 'UHID / TPA No', + 'claimed_amount' => 'Claim Amount', + 'doa' => 'DOA', + 'claim_status' => 'Status', + 'relation' => 'Relation', + 'master_reject_reason' => 'Reject Reason', + ], + (int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => [ + 'id' => 'Dump ID', + 'pribenef_employee_code' => 'Emp Code', + 'pribenef_name' => 'Employee Name', + 'benef_name' => 'Insured Name', + 'event_id' => 'UHID / TPA No', + 'claim_amount' => 'Claim Amount', + 'date_of_admission' => 'DOA', + 'claim_status' => 'Status', + 'benef_relation' => 'Relation', + 'master_reject_reason' => 'Reject Reason', + ], + (int) env('FHPL_PRIMARY_KEY_CONSTANT') => [ + 'id' => 'Dump ID', + 'employee_id' => 'Emp Code', + 'main_member_name' => 'Employee Name', + 'member_name' => 'Insured Name', + 'uhid_no' => 'UHID / TPA No', + 'claim_amount' => 'Claim Amount', + 'admission_date' => 'DOA', + 'current_claim_status' => 'Status', + 'relationship' => 'Relation', + 'master_reject_reason' => 'Reject Reason', + ], + (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => [ + 'id' => 'Dump ID', + 'employee_member_id' => 'Emp Code', + 'insured_name' => 'Employee Name', + 'patient_name' => 'Insured Name', + 'uhid' => 'UHID / TPA No', + 'claimed_amount' => 'Claim Amount', + 'doa_opd_treatment_from' => 'DOA', + 'final_status' => 'Status', + 'relation' => 'Relation', + 'master_reject_reason' => 'Reject Reason', + ], + (int) env('VIDAL_PRIMARY_KEY_CONSTANT') => [ + 'id' => 'Dump ID', + 'employee_number' => 'Emp Code', + 'primary_policy_holder_name' => 'Employee Name', + 'patient_name' => 'Insured Name', + 'primary_policy_holder_card_id' => 'UHID / TPA No', + 'claim_amount' => 'Claim Amount', + 'date_of_admission' => 'DOA', + 'claim_status' => 'Status', + 'relation' => 'Relation', + 'master_reject_reason' => 'Reject Reason', + ], + ]; + + return $byTpa[$tpaId] ?? [ + 'id' => 'Dump ID', + 'master_reject_reason' => 'Reject Reason', + ]; + } + + /** + * Re-run Job 2 for dump rows that never got a ticket_master link (ticket_id IS NULL). + */ + public function reprocessClaimDumpPending() + { + $fileId = (int) ($this->request->getGet('file_id') ?? $this->request->getPost('file_id') ?? 0); + log_message('error', '[CLAIM_DUMP][CTRL_REPROCESS_PENDING_START] file_id=' . $fileId); + + if ($fileId <= 0) { + return $this->respond(['status' => false, 'code' => 400, 'message' => 'file_id is required'], 200); + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->where('is_active', 1)->first(); + if (empty($fileData)) { + return $this->respond(['status' => false, 'code' => 404, 'message' => 'File not found'], 200); + } + + if (($fileData['status'] ?? '') === 'processing') { + return $this->respond([ + 'status' => false, + 'code' => 400, + 'message' => 'Cannot reprocess while import is processing', + ], 200); + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + if ($tpaId <= 0) { + return $this->respond([ + 'status' => false, + 'code' => 400, + 'message' => 'Reprocess is only supported for TPA claim dumps', + ], 200); + } + + $tableMap = $this->getTpaDumpTableMap(); + $tpaTable = $tableMap[$tpaId] ?? null; + if ($tpaTable === null) { + return $this->respond(['status' => false, 'code' => 400, 'message' => 'Unsupported TPA'], 200); + } + + try { + $db = db_connect(); + $pendingBefore = (int) $db->table($tpaTable) + ->where('file_id', $fileId) + ->where('is_active', 1) + ->where('ticket_id IS NULL', null, false) + ->countAllResults(); + + if ($pendingBefore <= 0) { + return $this->respond([ + 'status' => true, + 'code' => 200, + 'message' => 'No pending dump rows to process', + 'data' => ['pending_before' => 0], + ], 200); + } + + // Allow previously rejected unlinked rows to be tried again. + $db->table($tpaTable) + ->where('file_id', $fileId) + ->where('is_active', 1) + ->where('ticket_id IS NULL', null, false) + ->where('master_reject_reason IS NOT NULL', null, false) + ->set(['master_reject_reason' => null]) + ->update(); + + $handler = \App\Libraries\TpaClaimsImportFactory::make($tpaId); + $result = $handler->runTicketMasterInsert(['file_id' => $fileId]); + + $pendingAfter = (int) $db->table($tpaTable) + ->where('file_id', $fileId) + ->where('is_active', 1) + ->where('ticket_id IS NULL', null, false) + ->countAllResults(); + + if (!empty($result['status'])) { + $this->claimDumpFileModel->update($fileId, [ + 'status' => 'success', + 'reason' => null, + ]); + } + + log_message( + 'error', + '[CLAIM_DUMP][CTRL_REPROCESS_PENDING_DONE] file_id=' . $fileId + . ' pending_before=' . $pendingBefore + . ' pending_after=' . $pendingAfter + . ' status=' . (!empty($result['status']) ? 'true' : 'false') + . ' message=' . ($result['message'] ?? '') + ); + + $moved = max(0, $pendingBefore - $pendingAfter); + $message = !empty($result['status']) + ? "Processed pending dump rows. Moved {$moved} of {$pendingBefore} to ticket master." + : ($result['message'] ?? 'Reprocess failed'); + + if (!empty($result['status']) && $pendingAfter > 0) { + $message .= " {$pendingAfter} row(s) still pending (e.g. employee not found)."; + } + + return $this->respond([ + 'status' => !empty($result['status']), + 'code' => !empty($result['status']) ? 200 : 400, + 'message' => $message, + 'data' => [ + 'pending_before' => $pendingBefore, + 'pending_after' => $pendingAfter, + 'moved' => $moved, + 'job2' => $result, + ], + ], 200); + } catch (\Throwable $th) { + $this->myLogger->logme('error', 'reprocessClaimDumpPending: ' . $th->getMessage()); + log_message('error', '[CLAIM_DUMP][CTRL_REPROCESS_PENDING_EXCEPTION] file_id=' . $fileId . ' error=' . $th->getMessage()); + return $this->respond([ + 'status' => false, + 'code' => 500, + 'message' => 'Reprocess failed: ' . $th->getMessage(), + ], 200); + } + } + + /** + * @return array + */ + private function getTpaDumpTableMap(): array + { + return [ + (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', + ]; + } + + /** + * Attach pending dump row counts (ticket_id IS NULL) for list UI. + * + * @param list> $files + * @return list> + */ + private function attachPendingDumpTicketCounts(array $files): array + { + if ($files === []) { + return $files; + } + + $tableMap = $this->getTpaDumpTableMap(); + $byTpa = []; + foreach ($files as $file) { + $tpaId = (int) ($file['tpa_id'] ?? 0); + $fileId = (int) ($file['file_id'] ?? 0); + if ($tpaId > 0 && $fileId > 0 && isset($tableMap[$tpaId])) { + $byTpa[$tpaId][] = $fileId; + } + } + + $counts = []; + $db = db_connect(); + foreach ($byTpa as $tpaId => $fileIds) { + $fileIds = array_values(array_unique($fileIds)); + if ($fileIds === []) { + continue; + } + $table = $tableMap[$tpaId]; + if (!$db->tableExists($table)) { + continue; + } + + $rows = $db->table($table) + ->select('file_id, COUNT(*) AS pending_count', false) + ->where('is_active', 1) + ->where('ticket_id IS NULL', null, false) + ->whereIn('file_id', $fileIds) + ->groupBy('file_id') + ->get() + ->getResultArray(); + + foreach ($rows as $row) { + $counts[(int) $row['file_id']] = (int) $row['pending_count']; + } + } + + foreach ($files as &$file) { + $fileId = (int) ($file['file_id'] ?? 0); + $file['pending_ticket_count'] = $counts[$fileId] ?? 0; + } + unset($file); + + return $files; + } + // -------- END CLAIM DUMP UPLOAD ---------------------------------------------------------------------------------------------- public function saveIRDocsJson() { diff --git a/app/Controllers/TicketServiceController.php b/app/Controllers/TicketServiceController.php index ffa6f154..3dcc45f4 100644 --- a/app/Controllers/TicketServiceController.php +++ b/app/Controllers/TicketServiceController.php @@ -1847,12 +1847,13 @@ class TicketServiceController extends AdminController // -------------------------------------------------------------------------------------------------------------------------------- /** - * Resolve and validate Claim Dump file metadata and physical file for TPA imports. + * Resolve Claim Dump file metadata (and optionally the physical Excel) for TPA imports. * * @param int|null $fileId + * @param bool $requirePhysicalFile Job 1 needs the Excel; Job 2 only needs DB metadata. * @return array{status:bool,message?:string,fileData?:array,filePath?:string} */ - private function resolveTpaClaimDumpFile(?int $fileId): array + private function resolveTpaClaimDumpFile(?int $fileId, bool $requirePhysicalFile = true): array { if (empty($fileId)) { return [ @@ -1871,7 +1872,7 @@ class TicketServiceController extends AdminController $filePath = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR . $fileData['file_name']; - if (!is_file($filePath)) { + if ($requirePhysicalFile && !is_file($filePath)) { return [ 'status' => false, 'message' => 'Claim dump file not found', @@ -1881,7 +1882,7 @@ class TicketServiceController extends AdminController return [ 'status' => true, 'fileData' => $fileData, - 'filePath' => $filePath, + 'filePath' => is_file($filePath) ? $filePath : null, ]; } @@ -1894,30 +1895,63 @@ class TicketServiceController extends AdminController public function tpaClaimDumpImporter(array $params) { $file_id = isset($params['file_id']) ? (int) $params['file_id'] : null; + log_message('error', '[CLAIM_DUMP][CTRL_JOB1_START] file_id=' . ($file_id ?? 'null')); try { $resolved = $this->resolveTpaClaimDumpFile($file_id); if ($resolved['status'] === false) { + $message = $resolved['message'] ?? 'Unable to resolve claim dump file'; + log_message('error', '[CLAIM_DUMP][CTRL_JOB1_RESOLVE_FAILED] file_id=' . ($file_id ?? 'null') . ' message=' . $message); + if (!empty($file_id)) { + $this->markAsFailed($file_id, $message); + } + return [ 'status' => false, - 'message' => $resolved['message'] ?? 'Unable to resolve claim dump file', + 'message' => $message, ]; } $fileData = $resolved['fileData']; $filePath = $resolved['filePath']; + log_message('error', '[CLAIM_DUMP][CTRL_JOB1_RESOLVED] file_id=' . $file_id . ' tpa_id=' . ($fileData['tpa_id'] ?? 'null') . ' file=' . ($fileData['file_name'] ?? '')); + + // Prevent concurrent Job 1 runs for the same file. + $db = \Config\Database::connect(); + $db->table('claim_dump_files') + ->where('id', $file_id) + ->where('status', 'inprogress') + ->update([ + 'status' => 'processing', + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + if ($db->affectedRows() === 0) { + log_message('error', '[CLAIM_DUMP][CTRL_JOB1_LOCK_SKIP] file_id=' . $file_id); + return [ + 'status' => true, + 'message' => 'Claim dump import already in progress or completed for this file.', + ]; + } + log_message('error', '[CLAIM_DUMP][CTRL_JOB1_LOCK_ACQUIRED] file_id=' . $file_id); $handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0); $result = $handler->runTpaClaimDumpInsert($filePath, $file_id); if (!empty($result['status']) && $result['status'] === true) { + // Ready for Job 2 (release Job 1 lock back to inprogress). + $this->claimDumpFileModel->update($file_id, [ + 'status' => 'inprogress', + ]); Jobs::addJob([ 'job_name' => 'tpaClaimDumpToTicketMasterImporters', 'payload' => ['file_id' => $file_id], ]); + log_message('error', '[CLAIM_DUMP][CTRL_JOB1_SUCCESS_QUEUED_JOB2] file_id=' . $file_id . ' message=' . ($result['message'] ?? '') . ' record_count=' . ($result['record_count'] ?? 0)); } else { // FORCE FAIL LOGIC + log_message('error', '[CLAIM_DUMP][CTRL_JOB1_FAILED] file_id=' . $file_id . ' message=' . ($result['message'] ?? 'unknown')); $this->markAsFailed( $file_id, $result['message'] ?? 'System error contact admin', @@ -1928,6 +1962,7 @@ class TicketServiceController extends AdminController return $result; } catch (\Throwable $th) { $this->myLogger->logme("error", 'TPA_CLAIM_IMPORTER_JOB : ' . $th->getMessage()); + log_message('error', '[CLAIM_DUMP][CTRL_JOB1_EXCEPTION] file_id=' . ($file_id ?? 'null') . ' error=' . $th->getMessage()); if (!empty($file_id)) { $this->markAsFailed($file_id, 'System error contact admin'); @@ -1968,30 +2003,60 @@ class TicketServiceController extends AdminController { $file_id = isset($params['file_id']) ? (int) $params['file_id'] : null; $fileData = null; + log_message('error', '[CLAIM_DUMP][CTRL_JOB2_START] file_id=' . ($file_id ?? 'null')); try { - $resolved = $this->resolveTpaClaimDumpFile($file_id); + // Job 2 reads staging tables only — physical Excel is not required. + $resolved = $this->resolveTpaClaimDumpFile($file_id, false); if ($resolved['status'] === false) { + $message = $resolved['message'] ?? 'Unable to resolve claim dump file'; + log_message('error', '[CLAIM_DUMP][CTRL_JOB2_RESOLVE_FAILED] file_id=' . ($file_id ?? 'null') . ' message=' . $message); + if (!empty($file_id)) { + $this->markAsFailed($file_id, $message); + } + return [ 'status' => false, - 'message' => $resolved['message'] ?? 'Unable to resolve claim dump file', + 'message' => $message, ]; } $fileData = $resolved['fileData']; + log_message('error', '[CLAIM_DUMP][CTRL_JOB2_RESOLVED] file_id=' . $file_id . ' tpa_id=' . ($fileData['tpa_id'] ?? 'null')); + + // Atomic lock: skip if another worker is already processing this file. + $db = \Config\Database::connect(); + $db->table('claim_dump_files') + ->where('id', $file_id) + ->where('status !=', 'processing') + ->update([ + 'status' => 'processing', + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + if ($db->affectedRows() === 0) { + log_message('error', '[CLAIM_DUMP][CTRL_JOB2_LOCK_SKIP] file_id=' . $file_id); + return [ + 'status' => true, + 'message' => 'Ticket master import already in progress for this file.', + ]; + } + log_message('error', '[CLAIM_DUMP][CTRL_JOB2_LOCK_ACQUIRED] file_id=' . $file_id); $handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0); $result = $handler->runTicketMasterInsert($params); if (!empty($result['status']) && $result['status'] === true) { - // Success: Update the status to success + // Success (including rejection-only): dump staging is kept for error export / retry. $this->claimDumpFileModel->update($file_id, [ 'status' => 'success', 'reason' => null, ]); + log_message('error', '[CLAIM_DUMP][CTRL_JOB2_SUCCESS] file_id=' . $file_id . ' message=' . ($result['message'] ?? '')); } else { - // Logic failure: The runTicketMasterInsert returned status false + // Job 2 failed but Job 1 dump rows are preserved for retry. + log_message('error', '[CLAIM_DUMP][CTRL_JOB2_FAILED] file_id=' . $file_id . ' message=' . ($result['message'] ?? 'unknown')); $this->markAsFailed( $file_id, $result['message'] ?? 'System error contact admin', @@ -2003,6 +2068,7 @@ class TicketServiceController extends AdminController } catch (\Throwable $th) { // Log the full error $this->myLogger->logme("error", 'TICKET_MASTER_CLAIM_IMPORTER_JOB :' . $th->getMessage() . ' at line ' . $th->getLine()); + log_message('error', '[CLAIM_DUMP][CTRL_JOB2_EXCEPTION] file_id=' . ($file_id ?? 'null') . ' error=' . $th->getMessage() . ' line=' . $th->getLine()); // CRITICAL: Even if the code crashes, try to mark the file as failed if ($file_id) { diff --git a/app/Database/bds_report_performance_indexes.sql b/app/Database/bds_report_performance_indexes.sql new file mode 100644 index 00000000..a3e1291a --- /dev/null +++ b/app/Database/bds_report_performance_indexes.sql @@ -0,0 +1,55 @@ +-- BDS report performance indexes +-- Run manually on the application database (e.g. nhance_live). +-- Safe to re-run: each statement checks information_schema before creating. + +-- policy_transaction: default 90-day filter + active flag +SET @idx := ( + SELECT COUNT(1) FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'policy_transaction' + AND index_name = 'idx_pt_active_created' +); +SET @sql := IF(@idx = 0, + 'CREATE INDEX idx_pt_active_created ON policy_transaction (is_active, created_at)', + 'SELECT ''idx_pt_active_created already exists'' AS info' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- pt_co_share_details: join from policy_transaction +SET @idx := ( + SELECT COUNT(1) FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'pt_co_share_details' + AND index_name = 'idx_pcsd_pt_active' +); +SET @sql := IF(@idx = 0, + 'CREATE INDEX idx_pcsd_pt_active ON pt_co_share_details (pt_id, is_active)', + 'SELECT ''idx_pcsd_pt_active already exists'' AS info' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- co_share_stmt_details: billed/reward aggregate joins +SET @idx := ( + SELECT COUNT(1) FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'co_share_stmt_details' + AND index_name = 'idx_cssd_coshare_stmt_active' +); +SET @sql := IF(@idx = 0, + 'CREATE INDEX idx_cssd_coshare_stmt_active ON co_share_stmt_details (co_share_id, statement_id, is_active)', + 'SELECT ''idx_cssd_coshare_stmt_active already exists'' AS info' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- insurer_statements: month + invoice filters used by aggregates +SET @idx := ( + SELECT COUNT(1) FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'insurer_statements' + AND index_name = 'idx_insq_active_month_invoice' +); +SET @sql := IF(@idx = 0, + 'CREATE INDEX idx_insq_active_month_invoice ON insurer_statements (is_active, month, invoice_status)', + 'SELECT ''idx_insq_active_month_invoice already exists'' AS info' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php index 08da325e..94ef8876 100644 --- a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php @@ -108,10 +108,8 @@ class AbhiClaimImportService extends BaseTpaClaimImportService 'rejection_category' => 'denial_reason', // Misc - 'diagnosis' => 'claim_description', 'healthcard_id' => 'tpa_no', 'claim_type' => 'tpa_claim_type', - 'diagnosis' => 'tpa_ailments', ]; @@ -211,7 +209,7 @@ class AbhiClaimImportService extends BaseTpaClaimImportService } $builder = $this->db->table('claims_dump_abhi'); - return $builder->updateBatch($data, 'id'); + return $builder->updateBatch($data, 'id') !== false; } @@ -235,7 +233,7 @@ class AbhiClaimImportService extends BaseTpaClaimImportService $dbColumn = $map['db_column']; $value = $row[$excelColumn] ?? null; - $item[$dbColumn] = trim($value); + $item[$dbColumn] = $value === null ? null : trim((string) $value); } foreach ($this->dateColumns as $key => $value) { @@ -296,10 +294,20 @@ class AbhiClaimImportService extends BaseTpaClaimImportService 'doa' => change_date_format($row['doa'] ?? '') ?? null, 'emp_code' => $row['member_code'] ?? null, 'claim_amount' => $row['claimed_amount'] ?? null, - 'tpa_no' => $row['healthcard_id'] ?? null + 'tpa_no' => $row['healthcard_id'] ?? null, + 'client_id' => $file_data['client_id'] ?? null, + 'client_policy_id' => $file_data['client_policy_id'] ?? null, ]; - $newStatusId = $this->checkStatusMapping($this->statusMapping, $row['claim_status'] ?? '') ?? 61; + $newStatusId = $this->resolveClaimStatusId( + $this->statusMapping, + $row['claim_status'] ?? '', + (int) $row['id'], + $rejecetd_reason + ); + if ($newStatusId === null) { + continue; + } $existingTicket = $this->getExistingTicketMasterClaim($params); if ($this->handleExistingTicketStatusUpdate( @@ -328,8 +336,8 @@ class AbhiClaimImportService extends BaseTpaClaimImportService $item['emp_id'] = $employee_data['id'] ?? null; $item['emp_name'] = $employee_data['name'] ?? null; $item['emp_code'] = $employee_data['emp_code'] ?? null; - $item['emp_mail'] = $employee_data['email_corporate'] ?? null; - $item['emp_mobile'] = $employee_data['mobile'] ?? null; + $item['emp_mail'] = $employee_data['emp_mail'] ?? null; + $item['emp_mobile'] = $employee_data['emp_mobile'] ?? null; if (!empty($employee_data['insured_emp_id'])) { $item['insured_emp_id'] = $employee_data['insured_emp_id'] ?? null; $item['insured_name'] = $employee_data['insured_name'] ?? null; @@ -355,7 +363,10 @@ class AbhiClaimImportService extends BaseTpaClaimImportService $item['claim_dump_ref_id'] = $row['id']; $item['claim_dump_date'] = $file_data['claim_dump_date'] ?? null; $item['created_by'] = $file_data['created_by'] ?? null; - $item['claim_type'] = isset($row['mainclaim_prepost_type']) && !empty($row['mainclaim_prepost_type']) && strtolower($row['mainclaim_prepost_type']) == 'normal' ? 1 : 3; + $item['claim_description'] = $row['diagnosis'] ?? null; + $item['tpa_ailments'] = $row['diagnosis'] ?? null; + $claimTypeRaw = strtolower(trim((string) ($row['claim_type'] ?? ''))); + $item['claim_type'] = (str_contains($claimTypeRaw, 'pre') || str_contains($claimTypeRaw, 'post')) ? 3 : 1; $item['priority'] = 1; $item['mode_of_intimation'] = 5; $item['ticket_type_id'] = 1; diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php index d5df02fc..9f8f5b4c 100644 --- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php @@ -44,60 +44,154 @@ abstract class BaseTpaClaimImportService ]; } + /** + * Structured claim-dump process logging. Search logs with: [CLAIM_DUMP] + * Uses error level so entries appear under production logger threshold. + */ + protected function logClaimDump(string $level, string $step, array $context = []): void + { + $parts = []; + foreach ($context as $key => $value) { + if (is_bool($value)) { + $parts[] = $key . '=' . ($value ? 'true' : 'false'); + } elseif (is_scalar($value) || $value === null) { + $parts[] = $key . '=' . ($value === null ? 'null' : $value); + } else { + $parts[] = $key . '=' . json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + } + + $suffix = $parts === [] ? '' : ' ' . implode(' ', $parts); + // Keep severity in the message; always write as error for production visibility. + log_message('error', '[CLAIM_DUMP][' . strtoupper($level) . '][' . $step . ']' . $suffix); + } + /** * First JOB for insert TPA wise Bulk Upload */ public function runTpaClaimDumpInsert(string $filePath, int $fileId): array { + $this->logClaimDump('info', 'JOB1_START', [ + 'file_id' => $fileId, + 'file_path' => $filePath, + ]); + // 1. Start Transaction $this->db->transBegin(); try { $fileData = $this->claimDumpFileModel->where('id', $fileId)->first(); + if (empty($fileData)) { + $this->logClaimDump('error', 'JOB1_FILE_NOT_FOUND', ['file_id' => $fileId]); + return $this->failTpaClaimDumpInsert($fileId, 'Claim dump file record not found'); + } $client_policy_data = $this->clientPolicyModel->where('id', $fileData['client_policy_id'])->first(); + if (empty($client_policy_data)) { + $this->logClaimDump('error', 'JOB1_POLICY_NOT_FOUND', [ + 'file_id' => $fileId, + 'client_policy_id' => $fileData['client_policy_id'] ?? null, + ]); + return $this->failTpaClaimDumpInsert($fileId, 'Client policy not found'); + } + + $this->logClaimDump('info', 'JOB1_FILE_LOADED', [ + 'file_id' => $fileId, + 'tpa_id' => $fileData['tpa_id'] ?? null, + 'client_id' => $fileData['client_id'] ?? null, + 'client_policy_id' => $fileData['client_policy_id'] ?? null, + 'policy_no' => $client_policy_data['policy_no'] ?? null, + ]); // Determine sheet name logic... if (env('FHPL_PRIMARY_KEY_CONSTANT') == $fileData['tpa_id']) { $rows = $this->readExcelBySheetName($filePath, 'Claims&Preauth'); + $this->logClaimDump('info', 'JOB1_EXCEL_READ', ['file_id' => $fileId, 'sheet' => 'Claims&Preauth', 'rows' => count($rows)]); } else if (env('R_CARE_PRIMARY_KEY_CONSTANT') == $fileData['tpa_id']) { $rows = $this->readExcelBySheetName($filePath, 'CL'); + $this->logClaimDump('info', 'JOB1_EXCEL_READ', ['file_id' => $fileId, 'sheet' => 'CL', 'rows' => count($rows)]); } else { $rows = $this->readExcel($filePath); + $this->logClaimDump('info', 'JOB1_EXCEL_READ', ['file_id' => $fileId, 'sheet' => 'active', 'rows' => count($rows)]); } if (empty($rows)) { + $this->logClaimDump('error', 'JOB1_EMPTY_EXCEL', ['file_id' => $fileId]); return $this->failTpaClaimDumpInsert($fileId, 'Excel file contains no data or wrong file upload'); } + if ($this->fileHasStagingRows($fileId)) { + $this->db->transRollback(); + $this->logClaimDump('info', 'JOB1_STAGING_EXISTS_SKIP', ['file_id' => $fileId]); + return [ + 'status' => true, + 'message' => 'Claim dump staging already exists for this file; skipped re-insert.', + ]; + } + if(isset($this->policyNumberMapping[$fileData['tpa_id']]) && !empty($this->policyNumberMapping[$fileData['tpa_id']])){ $policy_number_column = $this->policyNumberMapping[$fileData['tpa_id']]; }else{ $policy_number_column = 'policy_no'; } - - if($client_policy_data['policy_no'] != ($rows[0][$policy_number_column] ?? '')){ - return $this->failTpaClaimDumpInsert($fileId, 'Policy number mismatch in the file and in the system'); + $expectedPolicy = trim((string) ($client_policy_data['policy_no'] ?? '')); + foreach ($rows as $index => $row) { + $filePolicy = trim((string) ($row[$policy_number_column] ?? '')); + if ($filePolicy === '' || $filePolicy !== $expectedPolicy) { + $rowNumber = $index + 2; // header is row 1 + $this->logClaimDump('error', 'JOB1_POLICY_MISMATCH', [ + 'file_id' => $fileId, + 'excel_row' => $rowNumber, + 'expected' => $expectedPolicy, + 'found' => $filePolicy, + 'column' => $policy_number_column, + ]); + return $this->failTpaClaimDumpInsert( + $fileId, + "Policy number mismatch at Excel row {$rowNumber}. Expected '{$expectedPolicy}', found '{$filePolicy}'." + ); + } } + $this->logClaimDump('info', 'JOB1_POLICY_VALIDATED', [ + 'file_id' => $fileId, + 'policy_no' => $expectedPolicy, + 'rows_checked' => count($rows), + ]); $tpaInsertData = $this->mapTPAData($rows, $fileId); + $this->logClaimDump('info', 'JOB1_MAPPED', [ + 'file_id' => $fileId, + 'mapped_count' => count($tpaInsertData), + ]); if (empty($tpaInsertData)) { + $this->logClaimDump('error', 'JOB1_NO_MAPPED_ROWS', ['file_id' => $fileId]); return $this->failTpaClaimDumpInsert($fileId, 'These records already exist in the system.'); } $return_res = $this->bulkInsertTPATable($tpaInsertData); if ($return_res !== true) { + $this->logClaimDump('error', 'JOB1_BULK_INSERT_FAILED', ['file_id' => $fileId]); return $this->failTpaClaimDumpInsert($fileId, 'TPA Import bulk insert failed'); } // 2. Commit if everything is fine $this->db->transCommit(); + $this->logClaimDump('info', 'JOB1_SUCCESS', [ + 'file_id' => $fileId, + 'record_count' => count($tpaInsertData), + ]); return ['status' => true, 'message' => 'File uploaded successfully', 'record_count' => count($tpaInsertData)]; } catch (\Throwable $e) { + $this->logClaimDump('error', 'JOB1_EXCEPTION', [ + 'file_id' => $fileId, + 'error' => $e->getMessage(), + 'line' => $e->getLine(), + 'file' => $e->getFile(), + ]); return $this->failTpaClaimDumpInsert($fileId, 'System error : ' . $e->getMessage()); } } @@ -107,29 +201,57 @@ abstract class BaseTpaClaimImportService */ public function runTicketMasterInsert(array $params): array { + $file_id = (int) ($params['file_id'] ?? 0); + $this->logClaimDump('info', 'JOB2_START', ['file_id' => $file_id]); + + // Outside the Job 2 transaction so a later rollback cannot restore orphan links. + $orphansCleared = $this->clearOrphanDumpTicketLinks($file_id); + if ($orphansCleared > 0) { + $this->logClaimDump('info', 'JOB2_ORPHANS_CLEARED', [ + 'file_id' => $file_id, + 'count' => $orphansCleared, + ]); + } + // 1. Start manual transaction $this->db->transBegin(); try { - $file_id = $params['file_id']; $ticketMasterData = $this->mapClaimMasterData($file_id); + $this->logClaimDump('info', 'JOB2_MAPPED', [ + 'file_id' => $file_id, + 'mapping_status' => !empty($ticketMasterData['status']), + 'inserts' => count($ticketMasterData['mapped_array'] ?? []), + 'status_updates' => count($ticketMasterData['status_update_array'] ?? []), + 'rejects_or_links' => count($ticketMasterData['rejected_reason_array'] ?? []), + 'already_processed' => !empty($ticketMasterData['already_processed']), + 'message' => $ticketMasterData['message'] ?? null, + ]); // Check if mapping failed if (!$ticketMasterData['status']) { // Already-processed dump rows (ticket_id linked) must not be deleted as a "failure". if (!empty($ticketMasterData['already_processed'])) { $this->db->transCommit(); + $this->logClaimDump('info', 'JOB2_ALREADY_PROCESSED', ['file_id' => $file_id]); return [ 'status' => true, 'message' => $ticketMasterData['message'] ?? 'Claim dump already processed for this file.', ]; } - $this->rollbackAndCleanupClaimDumpData($file_id); + // Job 1 staging is already committed — roll back Job 2 only; keep dump for retry/export. + $this->db->transRollback(); + $this->logClaimDump('error', 'JOB2_MAPPING_FAILED', [ + 'file_id' => $file_id, + 'message' => $ticketMasterData['message'] ?? 'mapping failed', + ]); return $ticketMasterData; } - $message = ''; + $message = $orphansCleared > 0 + ? "Cleared {$orphansCleared} orphan dump ticket_id link(s). " + : ''; $hasExecutedTask = false; $hasInserts = !empty($ticketMasterData['mapped_array']); $hasExistingTicketUpdates = !empty($ticketMasterData['status_update_array']); @@ -138,6 +260,10 @@ abstract class BaseTpaClaimImportService // Process Mapped Data if ($hasInserts) { + $this->logClaimDump('info', 'JOB2_INSERT_TICKETS_START', [ + 'file_id' => $file_id, + 'count' => count($ticketMasterData['mapped_array']), + ]); $insert_res = $this->importClaimMaster($ticketMasterData['mapped_array']); if (!$insert_res) { return $this->failTicketMasterInsert($file_id, 'Ticket Master Claim bulk insert failed'); @@ -146,26 +272,45 @@ abstract class BaseTpaClaimImportService if (!$this->updateTicketIdInTPATable($file_id)) { return $this->failTicketMasterInsert($file_id, 'Updating ticket_id in TPA table failed'); } + if (!$this->recordHistoryForNewDumpTickets($ticketMasterData['mapped_array'], $file_id)) { + return $this->failTicketMasterInsert($file_id, 'Recording ticket history for dump claims failed'); + } $message .= 'Ticket Master Claim bulk insert success. '; $hasExecutedTask = true; + $this->logClaimDump('info', 'JOB2_INSERT_TICKETS_DONE', ['file_id' => $file_id]); } // Update existing tickets: status and/or missing claim_dump_ref_id if ($hasExistingTicketUpdates) { + $this->logClaimDump('info', 'JOB2_UPDATE_EXISTING_START', [ + 'file_id' => $file_id, + 'count' => count($ticketMasterData['status_update_array']), + ]); $update_status_res = $this->updateExistingTicketStatuses($ticketMasterData['status_update_array']); if (!$update_status_res) { return $this->failTicketMasterInsert($file_id, 'Updating existing tickets failed'); } + if (!$this->recordHistoryForExistingTicketUpdates($ticketMasterData['status_update_array'])) { + return $this->failTicketMasterInsert($file_id, 'Recording ticket history for status updates failed'); + } $message .= 'Existing ticket updated successfully. '; $hasExecutedTask = true; + $this->logClaimDump('info', 'JOB2_UPDATE_EXISTING_DONE', ['file_id' => $file_id]); } - // Process Rejected Reasons (also writes ticket_id onto TPA dump rows for existing tickets) + // Process dump link / reject updates (ticket_id and/or master_reject_reason) if ($hasRejectedReasons) { - $update_res = $this->updateTicketMasterRejectedReasonInTPATable($ticketMasterData['rejected_reason_array']); - if (!$update_res) { + $this->logClaimDump('info', 'JOB2_DUMP_LINK_REJECT_START', [ + 'file_id' => $file_id, + 'count' => count($ticketMasterData['rejected_reason_array']), + 'has_links' => $hasExistingTicketLinks, + ]); + $update_res = $this->updateTicketMasterRejectedReasonInTPATable( + $this->normalizeDumpLinkOrRejectUpdates($ticketMasterData['rejected_reason_array']) + ); + if ($update_res === false) { return $this->failTicketMasterInsert($file_id, 'Updating rejected reasons failed'); } @@ -177,6 +322,7 @@ abstract class BaseTpaClaimImportService $message .= 'Ticket Master Claim rejected reason updated successfully. '; } $hasExecutedTask = true; + $this->logClaimDump('info', 'JOB2_DUMP_LINK_REJECT_DONE', ['file_id' => $file_id]); } // If nothing was processed but no error occurred @@ -187,15 +333,29 @@ abstract class BaseTpaClaimImportService // 2. Commit the transaction $this->db->transCommit(); - $status = $hasInserts || $hasExistingTicketUpdates || $hasExistingTicketLinks; - if (!$status) { - $this->cleanupClaimDumpData($file_id); - } + // Rejection-only runs are still a completed Job 2 — keep dump rows for error export. + $status = $hasInserts || $hasExistingTicketUpdates || $hasExistingTicketLinks || $hasRejectedReasons; + + $this->logClaimDump('info', 'JOB2_SUCCESS', [ + 'file_id' => $file_id, + 'status' => $status, + 'has_inserts' => $hasInserts, + 'has_existing_updates' => $hasExistingTicketUpdates, + 'has_links' => $hasExistingTicketLinks, + 'has_rejects' => $hasRejectedReasons, + 'message' => trim($message), + ]); return ['status' => $status, 'message' => trim($message)]; } catch (\Throwable $th) { $fileId = (int) ($params['file_id'] ?? 0); + $this->logClaimDump('error', 'JOB2_EXCEPTION', [ + 'file_id' => $fileId, + 'error' => $th->getMessage(), + 'line' => $th->getLine(), + 'file' => $th->getFile(), + ]); return $this->failTicketMasterInsert( $fileId, 'System error during Ticket Master Insert: ' . $th->getMessage() @@ -204,7 +364,220 @@ abstract class BaseTpaClaimImportService } /** - * Remove TPA staging rows and ticket_master rows created for a failed claim dump upload. + * Clear dump ticket_id (and reject reason) when the linked ticket is missing or inactive. + * Makes orphan rows pending again so Job 2 can recreate tickets. + * + * @return int Number of dump rows reset + */ + protected function clearOrphanDumpTicketLinks(int $fileId): int + { + if ($fileId <= 0) { + return 0; + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->first(); + if (empty($fileData)) { + return 0; + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + $tpaTable = $this->tpaTableMapping[$tpaId] ?? null; + + if ($tpaTable === null || !in_array($tpaTable, $this->tpaTableMapping, true)) { + return 0; + } + + $sql = "UPDATE `{$tpaTable}` d + LEFT JOIN ticket_master tm ON tm.id = d.ticket_id AND tm.is_active = 1 + SET d.ticket_id = NULL, + d.master_reject_reason = NULL + WHERE d.file_id = ? + AND d.is_active = 1 + AND d.ticket_id IS NOT NULL + AND tm.id IS NULL"; + + $this->db->query($sql, [$fileId]); + + return $this->db->affectedRows(); + } + + /** + * Clear orphan dump ticket_id links (ticket missing/inactive), then run Job 2 + * so those dump rows are inserted into ticket_master again. + */ + public function recreateOrphanDumpTickets(int $fileId): array + { + $this->logClaimDump('info', 'RECREATE_ORPHANS_START', ['file_id' => $fileId]); + + if ($fileId <= 0) { + return ['status' => false, 'message' => 'Invalid file id']; + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->first(); + if (empty($fileData)) { + return ['status' => false, 'message' => 'claim_dump_files row not found']; + } + + $cleared = $this->clearOrphanDumpTicketLinks($fileId); + $this->logClaimDump('info', 'RECREATE_ORPHANS_CLEARED', [ + 'file_id' => $fileId, + 'cleared' => $cleared, + ]); + + // Job 2 also clears orphans; running insert/link for now-pending dump rows. + $result = $this->runTicketMasterInsert(['file_id' => $fileId]); + $result['orphans_cleared'] = $cleared; + + $this->logClaimDump( + !empty($result['status']) ? 'info' : 'error', + 'RECREATE_ORPHANS_DONE', + [ + 'file_id' => $fileId, + 'cleared' => $cleared, + 'status' => !empty($result['status']), + 'message' => $result['message'] ?? null, + ] + ); + + return $result; + } + + /** + * Soft-delete dump staging + tickets created by this upload (admin truncate / undo). + * Does not deactivate existing tickets that were only linked from this dump (different file_id). + */ + public function softTruncateClaimDump(int $fileId): array + { + $this->logClaimDump('info', 'TRUNCATE_START', ['file_id' => $fileId]); + + if ($fileId <= 0) { + $this->logClaimDump('error', 'TRUNCATE_INVALID_FILE_ID', ['file_id' => $fileId]); + return ['status' => false, 'message' => 'Invalid file id']; + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->where('is_active', 1)->first(); + if (empty($fileData)) { + $this->logClaimDump('error', 'TRUNCATE_FILE_NOT_FOUND', ['file_id' => $fileId]); + return ['status' => false, 'message' => 'File not found or already truncated']; + } + + if (($fileData['status'] ?? '') === 'processing') { + $this->logClaimDump('error', 'TRUNCATE_BLOCKED_PROCESSING', ['file_id' => $fileId]); + return ['status' => false, 'message' => 'Cannot truncate while import is processing']; + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + $tpaTable = $this->tpaTableMapping[$tpaId] ?? null; + if ($tpaTable === null || !in_array($tpaTable, $this->tpaTableMapping, true)) { + $this->logClaimDump('error', 'TRUNCATE_UNSUPPORTED_TPA', [ + 'file_id' => $fileId, + 'tpa_id' => $tpaId, + ]); + return ['status' => false, 'message' => 'Unsupported TPA for truncate']; + } + + $this->db->transBegin(); + + try { + $ticketIds = $this->db->table('ticket_master') + ->select('id, claim_status_id, created_by') + ->where('file_id', $fileId) + ->where('is_active', 1) + ->get() + ->getResultArray(); + + $this->logClaimDump('info', 'TRUNCATE_TICKETS_FOUND', [ + 'file_id' => $fileId, + 'tpa_table' => $tpaTable, + 'ticket_count' => count($ticketIds), + ]); + + // Soft-deactivate dump rows only — keep ticket_id for audit/traceability. + $this->db->table($tpaTable) + ->where('file_id', $fileId) + ->where('is_active', 1) + ->set(['is_active' => 0]) + ->update(); + $dumpAffected = $this->db->affectedRows(); + + $this->db->table('ticket_master') + ->where('file_id', $fileId) + ->where('is_active', 1) + ->set(['is_active' => 0]) + ->update(); + $ticketAffected = $this->db->affectedRows(); + + // Avoid Model callbacks that call get_session_userid() (breaks CLI / after output). + $actorId = null; + try { + if (function_exists('get_session_userid')) { + $actorId = get_session_userid(); + } + } catch (\Throwable $e) { + $actorId = null; + } + + if ($ticketIds !== []) { + $historyRows = []; + $now = date('Y-m-d H:i:s'); + foreach ($ticketIds as $ticket) { + $historyRows[] = [ + 'ticket_id' => $ticket['id'], + 'field_name' => 'claim_status_id', + 'display_name' => 'Claim Removed (TPA Dump Truncate)', + 'old_value' => $ticket['claim_status_id'], + 'new_value' => null, + 'created_by' => $ticket['created_by'] ?? $actorId, + 'created_at' => $now, + 'is_active' => 1, + ]; + } + if ($this->db->table('ticket_history')->insertBatch($historyRows) === false) { + throw new RuntimeException('Failed to write truncate ticket history'); + } + } + + $this->db->table('claim_dump_files') + ->where('id', $fileId) + ->update([ + 'is_active' => 0, + 'status' => 'truncated', + 'updated_by' => $actorId, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + if ($this->db->transStatus() === false) { + $this->db->transRollback(); + $this->logClaimDump('error', 'TRUNCATE_DB_FAILED', ['file_id' => $fileId]); + return ['status' => false, 'message' => 'Truncate failed']; + } + + $this->db->transCommit(); + + $this->logClaimDump('info', 'TRUNCATE_SUCCESS', [ + 'file_id' => $fileId, + 'dump_rows_deactivated' => $dumpAffected, + 'tickets_deactivated' => $ticketAffected, + ]); + + return [ + 'status' => true, + 'message' => 'Claim dump truncated successfully', + 'tickets_deactivated' => count($ticketIds), + ]; + } catch (\Throwable $e) { + $this->db->transRollback(); + $this->logClaimDump('error', 'TRUNCATE_EXCEPTION', [ + 'file_id' => $fileId, + 'error' => $e->getMessage(), + ]); + return ['status' => false, 'message' => 'Truncate failed: ' . $e->getMessage()]; + } + } + + /** + * Soft-delete TPA staging rows and ticket_master rows for a failed Job 1 dump upload. + * Never touches tickets unless the TPA dump table is resolved (avoids orphan ticket_id links). */ protected function cleanupClaimDumpData(int $fileId): void { @@ -214,34 +587,74 @@ abstract class BaseTpaClaimImportService $fileData = $this->claimDumpFileModel->where('id', $fileId)->first(); if (empty($fileData)) { + $this->logClaimDump('warning', 'CLEANUP_FILE_NOT_FOUND', ['file_id' => $fileId]); return; } $tpaId = (int) ($fileData['tpa_id'] ?? 0); $tpaTable = $this->tpaTableMapping[$tpaId] ?? null; - if ($tpaTable !== null) { - $this->db->table($tpaTable)->where('file_id', $fileId)->delete(); + if ($tpaTable === null) { + $this->logClaimDump('error', 'CLEANUP_UNKNOWN_TPA', [ + 'file_id' => $fileId, + 'tpa_id' => $tpaId, + ]); + return; } - $this->db->table('ticket_master')->where('file_id', $fileId)->delete(); + $this->db->table($tpaTable) + ->where('file_id', $fileId) + ->where('is_active', 1) + ->set([ + 'is_active' => 0, + 'ticket_id' => null, + ]) + ->update(); + $dumpAffected = $this->db->affectedRows(); + + $this->db->table('ticket_master') + ->where('file_id', $fileId) + ->where('is_active', 1) + ->set(['is_active' => 0]) + ->update(); + $ticketAffected = $this->db->affectedRows(); + + $this->logClaimDump('info', 'CLEANUP_DONE', [ + 'file_id' => $fileId, + 'tpa_table' => $tpaTable, + 'dump_rows_deactivated' => $dumpAffected, + 'tickets_deactivated' => $ticketAffected, + ]); } protected function rollbackAndCleanupClaimDumpData(int $fileId): void { + $this->logClaimDump('warning', 'JOB1_ROLLBACK_AND_CLEANUP', ['file_id' => $fileId]); $this->db->transRollback(); $this->cleanupClaimDumpData($fileId); } protected function failTpaClaimDumpInsert(int $fileId, string $message): array { + $this->logClaimDump('error', 'JOB1_FAIL', [ + 'file_id' => $fileId, + 'message' => $message, + ]); $this->rollbackAndCleanupClaimDumpData($fileId); return ['status' => false, 'message' => $message]; } + /** + * Job 2 failure: roll back this transaction only. + * Do not delete committed Job 1 dump rows so the file can be retried. + */ protected function failTicketMasterInsert(int $fileId, string $message): array { - $this->rollbackAndCleanupClaimDumpData($fileId); + $this->logClaimDump('error', 'JOB2_FAIL', [ + 'file_id' => $fileId, + 'message' => $message, + ]); + $this->db->transRollback(); return ['status' => false, 'message' => $message]; } @@ -275,8 +688,8 @@ abstract class BaseTpaClaimImportService foreach ($rows as $row) { - if(check_row_is_empty_or_null($row)){ - break; + if (check_row_is_empty_or_null($row)) { + continue; } $item = []; @@ -354,17 +767,26 @@ abstract class BaseTpaClaimImportService /** * Fetch existing ticket_master record matching claim identity keys. + * Scoped by client_id / client_policy_id when provided to avoid cross-tenant matches. */ protected function getExistingTicketMasterClaim(array $param): ?array { $ticketMaster = new TicketMasterModel(); - $ticket = $ticketMaster + $builder = $ticketMaster ->where('doa', $param['doa']) ->where('tpa_no', $param['tpa_no']) ->where('claim_amount', $param['claim_amount']) ->where('emp_code', $param['emp_code']) - ->where('is_active', 1) - ->first(); + ->where('is_active', 1); + + if (!empty($param['client_id'])) { + $builder->where('client_id', $param['client_id']); + } + if (!empty($param['client_policy_id'])) { + $builder->where('client_policy_id', $param['client_policy_id']); + } + + $ticket = $builder->orderBy('id', 'DESC')->first(); return $ticket ?: null; } @@ -378,8 +800,98 @@ abstract class BaseTpaClaimImportService return true; } + $payload = []; + foreach ($statusUpdates as $update) { + $row = $update; + unset($row['old_claim_status_id']); + $payload[] = $row; + } + $ticketMasterModel = new TicketMasterModel(); - return $ticketMasterModel->updateBatch($statusUpdates, 'id') !== false; + return $ticketMasterModel->updateBatch($payload, 'id') !== false; + } + + /** + * Write ticket_history for newly created dump tickets (by claim_dump_ref_id + file_id). + */ + protected function recordHistoryForNewDumpTickets(array $mappedTickets, int $fileId): bool + { + if (empty($mappedTickets) || $fileId <= 0) { + return true; + } + + $refIds = []; + foreach ($mappedTickets as $row) { + if (!empty($row['claim_dump_ref_id'])) { + $refIds[] = (int) $row['claim_dump_ref_id']; + } + } + $refIds = array_values(array_unique($refIds)); + if ($refIds === []) { + return true; + } + + $tickets = $this->db->table('ticket_master') + ->select('id, claim_status_id, created_by') + ->where('file_id', $fileId) + ->whereIn('claim_dump_ref_id', $refIds) + ->where('is_active', 1) + ->get() + ->getResultArray(); + + if ($tickets === []) { + return true; + } + + $historyRows = []; + foreach ($tickets as $ticket) { + $historyRows[] = [ + 'ticket_id' => $ticket['id'], + 'field_name' => 'claim_status_id', + 'display_name' => 'Claim Created (TPA Dump)', + 'old_value' => null, + 'new_value' => $ticket['claim_status_id'], + 'created_by' => $ticket['created_by'] ?? null, + 'is_active' => 1, + ]; + } + + $historyModel = new \App\Models\TicketHistoryModel(); + return $historyModel->insertBatch($historyRows) !== false; + } + + /** + * Write ticket_history when dump import updates an existing ticket's claim_status_id. + */ + protected function recordHistoryForExistingTicketUpdates(array $statusUpdates): bool + { + if (empty($statusUpdates)) { + return true; + } + + $historyRows = []; + foreach ($statusUpdates as $update) { + if (empty($update['id']) || !array_key_exists('claim_status_id', $update)) { + continue; + } + + $historyRows[] = [ + 'ticket_id' => $update['id'], + 'field_name' => 'claim_status_id', + 'display_name' => 'Claim Status Updated (TPA Dump)', + 'old_value' => $update['old_claim_status_id'] ?? null, + 'new_value' => $update['claim_status_id'], + 'created_by' => $update['updated_by'] ?? null, + 'is_active' => 1, + ]; + } + + if ($historyRows === []) { + return true; + } + + $historyModel = new \App\Models\TicketHistoryModel(); + return $historyModel->insertBatch($historyRows) !== false; } /** @@ -396,12 +908,38 @@ abstract class BaseTpaClaimImportService return false; } + /** + * Ensure link and reject dump updates share the same columns for updateBatch. + */ + protected function normalizeDumpLinkOrRejectUpdates(array $rows): array + { + $normalized = []; + + foreach ($rows as $row) { + if (empty($row['id'])) { + continue; + } + + $normalized[] = [ + 'id' => $row['id'], + 'ticket_id' => $row['ticket_id'] ?? null, + 'master_reject_reason' => array_key_exists('master_reject_reason', $row) + ? $row['master_reject_reason'] + : null, + ]; + } + + return $normalized; + } + /** * If ticket already exists: * - update claim_status_id when changed * - set claim_dump_ref_id on ticket_master when null * - set ticket_id on TPA dump row when null (via rejected_reason_array) * + * Link updates do not write master_reject_reason (that field is for true rejects only). + * * Returns true when the row was handled as an existing ticket (caller should continue). */ protected function handleExistingTicketStatusUpdate( @@ -421,33 +959,24 @@ abstract class BaseTpaClaimImportService $claimDumpRefIdMissing = empty($existingTicket['claim_dump_ref_id']); $ticketUpdate = ['id' => $ticketId]; - $reasons = []; if ($statusChanged) { $ticketUpdate['claim_status_id'] = $newStatusId; - $reasons[] = 'status updated'; + $ticketUpdate['old_claim_status_id'] = $currentStatusId; } if ($claimDumpRefIdMissing) { $ticketUpdate['claim_dump_ref_id'] = $dumpRowId; - $reasons[] = 'claim_dump_ref_id linked'; } if (count($ticketUpdate) > 1) { $statusUpdateArray[] = $ticketUpdate; } - if ($statusChanged || $claimDumpRefIdMissing) { - $reason = 'Existing claim ' . implode(' and ', $reasons) . '.'; - } else { - $reason = 'This claim already exists in our system.'; - } - // Dump rows reaching here already have ticket_id NULL; link them to the existing ticket. $rejectedReasonArray[] = [ - 'id' => $dumpRowId, - 'master_reject_reason' => $reason, - 'ticket_id' => $ticketId, + 'id' => $dumpRowId, + 'ticket_id' => $ticketId, ]; return true; @@ -481,7 +1010,8 @@ abstract class BaseTpaClaimImportService /** * Response when no pending dump rows are left for ticket_master mapping. - * Marks already-processed files so cleanup does not delete linked dump rows. + * Orphan ticket_id links are cleared before mapping (see clearOrphanDumpTicketLinks). + * Remaining linked/rejected rows are treated as already processed (not a wipe failure). */ protected function emptyClaimMasterMappingResponse(string $table, int $fileId): array { @@ -506,19 +1036,36 @@ abstract class BaseTpaClaimImportService } /** - * Dublicate check in the TPA specific table records + * Resolve employee (self) and insured member for a claim dump row. */ - public function getEmployeeDetails(int $client_id, int $client_policy_id, string $emp_code, string $relation): array - { + public function getEmployeeDetails(int $client_id, int $client_policy_id, ?string $emp_code, ?string $relation = null): array + { + if ($emp_code === null || trim($emp_code) === '') { + return []; + } + + $emp_code = trim($emp_code); + $relation = $relation !== null ? strtolower(trim($relation)) : ''; $client_policy_data = $this->clientPolicyModel->where('id', $client_policy_id)->first(); - if(!empty($client_policy_data)){ - if(!empty($client_policy_data['base_policy'])){ + if (!empty($client_policy_data)) { + if (!empty($client_policy_data['base_policy'])) { $client_policy_id = $client_policy_data['base_policy']; } } - + $EmployeeModel = new EmployeeModel(); + + // When relation is unknown, still resolve the employee (self); insured join is skipped. + $insuredJoin = "insured.emp_code = employees.emp_code + AND insured.client_id = employees.client_id"; + if ($relation !== '') { + $insuredJoin .= " AND LOWER(insured.relationship) = " . $EmployeeModel->db->escape($relation); + } else { + // No usable relation — do not match any insured row. + $insuredJoin .= " AND 1 = 0"; + } + $employeeData = $EmployeeModel ->select([ 'employees.id AS id', @@ -537,9 +1084,7 @@ abstract class BaseTpaClaimImportService ) ->join( 'employees AS insured', - "insured.emp_code = employees.emp_code - AND insured.client_id = employees.client_id - AND LOWER(insured.relationship) = " . $EmployeeModel->db->escape(strtolower($relation)), + $insuredJoin, 'left' ) ->where('employees.is_active', 1) @@ -556,13 +1101,75 @@ abstract class BaseTpaClaimImportService public function checkStatusMapping($statusArray, $statusString) { + $statusString = trim((string) $statusString); + if ($statusString === '') { + return null; + } + foreach ($statusArray as $key => $value) { - if (strtolower($statusString) == strtolower($key) || strtolower($statusString) == strtolower(trim($key))) { + if (strtolower($statusString) === strtolower(trim((string) $key))) { return $value; } } - - return 61; + + return null; + } + + /** + * Map dump status text to claim_status_id, or quarantine the dump row when unknown/empty. + */ + protected function resolveClaimStatusId( + array $statusMapping, + ?string $statusString, + int $dumpRowId, + array &$rejectedReasonArray + ): ?int { + $statusString = trim((string) ($statusString ?? '')); + + if ($statusString === '') { + $rejectedReasonArray[] = [ + 'id' => $dumpRowId, + 'master_reject_reason' => 'Claim status is empty in the dump file.', + ]; + return null; + } + + $statusId = $this->checkStatusMapping($statusMapping, $statusString); + if ($statusId === null) { + $rejectedReasonArray[] = [ + 'id' => $dumpRowId, + 'master_reject_reason' => 'Unknown claim status: ' . $statusString, + ]; + return null; + } + + return (int) $statusId; + } + + /** + * True when this file already has staging rows in its TPA dump table. + */ + protected function fileHasStagingRows(int $fileId): bool + { + if ($fileId <= 0) { + return false; + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->first(); + if (empty($fileData)) { + return false; + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + $tpaTable = $this->tpaTableMapping[$tpaId] ?? null; + if ($tpaTable === null || !in_array($tpaTable, $this->tpaTableMapping, true)) { + return false; + } + + return $this->db->table($tpaTable) + ->where('file_id', $fileId) + ->where('is_active', 1) + ->countAllResults() > 0; } /** diff --git a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php index e436be0a..577b7045 100644 --- a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php @@ -155,7 +155,6 @@ class FhplClaimImportService extends BaseTpaClaimImportService // Amounts 'claim_amount' => 'claim_amount', - 'settled_amount' => 'settled_amount', 'disallowed_amount' => 'denial_reason', 'coverage_amount' => 'si_amt', @@ -167,14 +166,12 @@ class FhplClaimImportService extends BaseTpaClaimImportService 'provider_pincode' => 'hospital_pin_code', // Remarks / Description - 'diagnosis' => 'claim_description', 'rejection_remarks' => 'return_remark', // Payment 'cheque_no' => 'utr_details', 'uhid_no' => 'tpa_no', 'claim_type' => 'tpa_claim_type', - 'diagnosis' => 'tpa_ailments', ]; @@ -277,7 +274,7 @@ class FhplClaimImportService extends BaseTpaClaimImportService } $builder = $this->db->table('claims_dump_fhpl'); - return $builder->updateBatch($data, 'id'); + return $builder->updateBatch($data, 'id') !== false; } @@ -301,11 +298,12 @@ class FhplClaimImportService extends BaseTpaClaimImportService $dbColumn = $map['db_column']; $value = $row[$excelColumn] ?? null; - $item[$dbColumn] = trim($value); + $item[$dbColumn] = $value === null ? null : trim((string) $value); } foreach ($this->dateColumns as $key => $value) { - $item[$value] = change_date_format($item[$value] ?? null, 'd-M-y', 'Y-m-d'); + // Auto-detect source format (Excel may be Y-m-d or d-M-y). + $item[$value] = change_date_format($item[$value] ?? null); } $item['file_id'] = $file_id ?? null; @@ -362,10 +360,20 @@ class FhplClaimImportService extends BaseTpaClaimImportService 'doa' => change_date_format($row['admission_date'] ?? '') ?? null, 'emp_code' => $row['employee_id'] ?? null, 'claim_amount' => $row['claim_amount'] ?? null, - 'tpa_no' => $row['uhid_no'] ?? null + 'tpa_no' => $row['uhid_no'] ?? null, + 'client_id' => $file_data['client_id'] ?? null, + 'client_policy_id' => $file_data['client_policy_id'] ?? null, ]; - $newStatusId = $this->checkStatusMapping($this->statusMapping, $row['current_claim_status'] ?? '') ?? 61; + $newStatusId = $this->resolveClaimStatusId( + $this->statusMapping, + $row['current_claim_status'] ?? '', + (int) $row['id'], + $rejecetd_reason + ); + if ($newStatusId === null) { + continue; + } $existingTicket = $this->getExistingTicketMasterClaim($params); if ($this->handleExistingTicketStatusUpdate( @@ -394,8 +402,8 @@ class FhplClaimImportService extends BaseTpaClaimImportService $item['emp_id'] = $employee_data['id'] ?? null; $item['emp_name'] = $employee_data['name'] ?? null; $item['emp_code'] = $employee_data['emp_code'] ?? null; - $item['emp_mail'] = $employee_data['email_corporate'] ?? null; - $item['emp_mobile'] = $employee_data['mobile'] ?? null; + $item['emp_mail'] = $employee_data['emp_mail'] ?? null; + $item['emp_mobile'] = $employee_data['emp_mobile'] ?? null; if (!empty($employee_data['insured_emp_id'])) { $item['insured_emp_id'] = $employee_data['insured_emp_id'] ?? null; $item['insured_name'] = $employee_data['insured_name'] ?? null; @@ -421,6 +429,8 @@ class FhplClaimImportService extends BaseTpaClaimImportService $item['claim_dump_ref_id'] = $row['id']; $item['claim_dump_date'] = $file_data['claim_dump_date'] ?? null; $item['created_by'] = $file_data['created_by'] ?? null; + $item['claim_description'] = $row['diagnosis'] ?? null; + $item['tpa_ailments'] = $row['diagnosis'] ?? null; $item['claim_type'] = 1; $item['priority'] = 1; $item['mode_of_intimation'] = 5; diff --git a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php index b4d61ebd..2e74334c 100644 --- a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php @@ -85,7 +85,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService // Claim 'claim_number' => 'claim_number', 'claimed_amount' => 'claim_amount', - 'claim_status' => 'tpa_claim_status', + 'updated_status' => 'tpa_claim_status', // Dates 'doa' => 'doa', @@ -192,7 +192,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService } $builder = $this->db->table('claims_dump_icici'); - return $builder->updateBatch($data, 'id'); + return $builder->updateBatch($data, 'id') !== false; } @@ -277,10 +277,20 @@ class IciciClaimImportService extends BaseTpaClaimImportService 'doa' => change_date_format($row['doa'] ?? '') ?? null, 'emp_code' => $row['employee_member_id'] ?? null, 'claim_amount' => $row['claimed_amount'] ?? null, - 'tpa_no' => $row['uhid'] ?? null + 'tpa_no' => $row['uhid'] ?? null, + 'client_id' => $file_data['client_id'] ?? null, + 'client_policy_id' => $file_data['client_policy_id'] ?? null, ]; - $newStatusId = $this->checkStatusMapping($this->statusMapping, $row['updated_status'] ?? '') ?? 61; + $newStatusId = $this->resolveClaimStatusId( + $this->statusMapping, + $row['updated_status'] ?? '', + (int) $row['id'], + $rejecetd_reason + ); + if ($newStatusId === null) { + continue; + } $existingTicket = $this->getExistingTicketMasterClaim($params); if ($this->handleExistingTicketStatusUpdate( @@ -309,8 +319,8 @@ class IciciClaimImportService extends BaseTpaClaimImportService $item['emp_id'] = $employee_data['id'] ?? null; $item['emp_name'] = $employee_data['name'] ?? null; $item['emp_code'] = $employee_data['emp_code'] ?? null; - $item['emp_mail'] = $employee_data['email_corporate'] ?? null; - $item['emp_mobile'] = $employee_data['mobile'] ?? null; + $item['emp_mail'] = $employee_data['emp_mail'] ?? null; + $item['emp_mobile'] = $employee_data['emp_mobile'] ?? null; if (!empty($employee_data['insured_emp_id'])) { $item['insured_emp_id'] = $employee_data['insured_emp_id'] ?? null; $item['insured_name'] = $employee_data['insured_name'] ?? null; diff --git a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php index e2533924..978c92c5 100644 --- a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php @@ -119,7 +119,7 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService // Policy / Claim identifiers 'claim_id' => 'claim_number', - 'event_id' => 'tpa_no', + 'event_id' => 'tpa_claim_id', 'claim_pre_auths' => 'tpa_claim_push_reference_no', // Claim type & status @@ -229,7 +229,7 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService } $builder = $this->db->table('claims_dump_medi_assist'); - return $builder->updateBatch($data, 'id'); + return $builder->updateBatch($data, 'id') !== false; } @@ -306,14 +306,33 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService foreach ($tpaClaimDumpData as $row) { + $tpaNo = trim((string) ($row['benef_insurer_id'] ?? '')); + if ($tpaNo === '') { + $tpaNo = trim((string) ($row['benef_maid'] ?? '')); + } + if ($tpaNo === '') { + $tpaNo = trim((string) ($row['event_id'] ?? '')); + } + $tpaNo = $tpaNo !== '' ? $tpaNo : null; + $params = [ 'doa' => change_date_format($row['date_of_admission'] ?? '') ?? null, 'emp_code' => $row['pribenef_employee_code'] ?? null, 'claim_amount' => $row['claim_amount'] ?? null, - 'tpa_no' => $row['event_id'] ?? null + 'tpa_no' => $tpaNo, + 'client_id' => $file_data['client_id'] ?? null, + 'client_policy_id' => $file_data['client_policy_id'] ?? null, ]; - $newStatusId = $this->checkStatusMapping($this->statusMapping, $row['claim_status'] ?? '') ?? 61; + $newStatusId = $this->resolveClaimStatusId( + $this->statusMapping, + $row['claim_status'] ?? '', + (int) $row['id'], + $rejecetd_reason + ); + if ($newStatusId === null) { + continue; + } $existingTicket = $this->getExistingTicketMasterClaim($params); if ($this->handleExistingTicketStatusUpdate( @@ -342,8 +361,8 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService $item['emp_id'] = $employee_data['id'] ?? null; $item['emp_name'] = $employee_data['name'] ?? null; $item['emp_code'] = $employee_data['emp_code'] ?? null; - $item['emp_mail'] = $employee_data['email_corporate'] ?? null; - $item['emp_mobile'] = $employee_data['mobile'] ?? null; + $item['emp_mail'] = $employee_data['emp_mail'] ?? null; + $item['emp_mobile'] = $employee_data['emp_mobile'] ?? null; if (!empty($employee_data['insured_emp_id'])) { $item['insured_emp_id'] = $employee_data['insured_emp_id'] ?? null; $item['insured_name'] = $employee_data['insured_name'] ?? null; @@ -369,6 +388,7 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService $item['claim_status_id'] = $newStatusId; $item['claim_dump_date'] = $file_data['claim_dump_date'] ?? null; $item['created_by'] = $file_data['created_by'] ?? null; + $item['tpa_no'] = $tpaNo; $item['claim_type'] = 1; $item['priority'] = 1; $item['mode_of_intimation'] = 5; diff --git a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php index 646a63e9..7a07bd75 100644 --- a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php @@ -83,16 +83,13 @@ class RcareClaimImportService extends BaseTpaClaimImportService 'hospital_state' => 'hospital_state', 'hospital_district' => 'hospital_city', - 'diagnosis' => 'claim_description', - 'diagnosis' => 'tpa_ailments', - - // Payment 'cheque_neft_number' => 'utr_details', 'cheque_neft_date' => 'settled_date', // References 'cl_inward_no' => 'claim_number', + 'uhid' => 'tpa_no', ]; protected $statusMapping = [ @@ -178,7 +175,7 @@ class RcareClaimImportService extends BaseTpaClaimImportService } $builder = $this->db->table('claims_dump_reliance'); - return $builder->updateBatch($data, 'id'); + return $builder->updateBatch($data, 'id') !== false; } @@ -262,10 +259,20 @@ class RcareClaimImportService extends BaseTpaClaimImportService 'doa' => change_date_format($row['doa_opd_treatment_from'] ?? '') ?? null, 'emp_code' => $row['employee_member_id'] ?? null, 'claim_amount' => $row['claimed_amount'] ?? null, - 'tpa_no' => $row['uhid'] ?? null + 'tpa_no' => $row['uhid'] ?? null, + 'client_id' => $file_data['client_id'] ?? null, + 'client_policy_id' => $file_data['client_policy_id'] ?? null, ]; - $newStatusId = $this->checkStatusMapping($this->statusMapping, $row['final_status'] ?? '') ?? 61; + $newStatusId = $this->resolveClaimStatusId( + $this->statusMapping, + $row['final_status'] ?? '', + (int) $row['id'], + $rejecetd_reason + ); + if ($newStatusId === null) { + continue; + } $existingTicket = $this->getExistingTicketMasterClaim($params); if ($this->handleExistingTicketStatusUpdate( @@ -294,8 +301,8 @@ class RcareClaimImportService extends BaseTpaClaimImportService $item['emp_id'] = $employee_data['id'] ?? null; $item['emp_name'] = $employee_data['name'] ?? null; $item['emp_code'] = $employee_data['emp_code'] ?? null; - $item['emp_mail'] = $employee_data['email_corporate'] ?? null; - $item['emp_mobile'] = $employee_data['mobile'] ?? null; + $item['emp_mail'] = $employee_data['emp_mail'] ?? null; + $item['emp_mobile'] = $employee_data['emp_mobile'] ?? null; if (!empty($employee_data['insured_emp_id'])) { $item['insured_emp_id'] = $employee_data['insured_emp_id'] ?? null; $item['insured_name'] = $employee_data['insured_name'] ?? null; @@ -321,6 +328,8 @@ class RcareClaimImportService extends BaseTpaClaimImportService $item['created_by'] = $file_data['created_by'] ?? null; $item['claim_dump_ref_id'] = $row['id']; $item['claim_dump_date'] = $file_data['claim_dump_date'] ?? null; + $item['claim_description'] = $row['diagnosis'] ?? null; + $item['tpa_ailments'] = $row['diagnosis'] ?? null; $item['claim_type'] = 1; $item['priority'] = 1; $item['mode_of_intimation'] = 5; diff --git a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php index fd628cc9..5c5762c2 100644 --- a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php @@ -423,7 +423,7 @@ class VidalClaimImportService extends BaseTpaClaimImportService } $builder = $this->db->table('claims_dump_vidal'); - return $builder->updateBatch($data, 'id'); + return $builder->updateBatch($data, 'id') !== false; } @@ -509,10 +509,20 @@ class VidalClaimImportService extends BaseTpaClaimImportService 'doa' => change_date_format($row['date_of_admission'] ?? '') ?? null, 'emp_code' => $row['employee_number'] ?? null, 'claim_amount' => $row['claim_amount'] ?? null, - 'tpa_no' => $row['primary_policy_holder_card_id'] ?? null + 'tpa_no' => $row['primary_policy_holder_card_id'] ?? null, + 'client_id' => $file_data['client_id'] ?? null, + 'client_policy_id' => $file_data['client_policy_id'] ?? null, ]; - $newStatusId = $this->checkStatusMapping($this->statusMapping, $row['claim_status'] ?? '') ?? 61; + $newStatusId = $this->resolveClaimStatusId( + $this->statusMapping, + $row['claim_status'] ?? '', + (int) $row['id'], + $rejecetd_reason + ); + if ($newStatusId === null) { + continue; + } $existingTicket = $this->getExistingTicketMasterClaim($params); if ($this->handleExistingTicketStatusUpdate( @@ -540,9 +550,9 @@ class VidalClaimImportService extends BaseTpaClaimImportService if (!empty($employee_data)) { $item['emp_id'] = $employee_data['id'] ?? null; $item['emp_name'] = $employee_data['name'] ?? null; - $item['emp_mail'] = $employee_data['email_corporate'] ?? null; + $item['emp_mail'] = $employee_data['emp_mail'] ?? null; $item['emp_code'] = $employee_data['emp_code'] ?? null; - $item['emp_mobile'] = $employee_data['mobile'] ?? null; + $item['emp_mobile'] = $employee_data['emp_mobile'] ?? null; $item['insured_emp_id'] = $employee_data['insured_emp_id'] ?? null; $item['insured_name'] = $employee_data['insured_name'] ?? null; } else { @@ -759,14 +769,14 @@ class VidalClaimImportService extends BaseTpaClaimImportService } - public function convertRelation(string $relation, string $gender): ?string + public function convertRelation(?string $relation, ?string $gender = null): ?string { if (empty($relation)) { return null; } $relation = strtolower($relation); - $gender = strtolower($gender); + $gender = strtolower((string) ($gender ?? '')); if ($relation == 'self') { return $relation; diff --git a/app/Models/PolicyTransactionModel.php b/app/Models/PolicyTransactionModel.php index 4f051a66..38ca5936 100644 --- a/app/Models/PolicyTransactionModel.php +++ b/app/Models/PolicyTransactionModel.php @@ -2186,42 +2186,44 @@ // Increase GROUP_CONCAT limit $this->db->query("SET SESSION group_concat_max_len = 1000000;"); - $builder = $this->db->table('policy_transaction pt'); + // NOT EXISTS avoids a self-join that balloons on large policy_transaction tables. + $sql = " + SELECT + SUM(CASE WHEN pt.policy_end_date < CURDATE() THEN 1 ELSE 0 END) AS Expired, + SUM(CASE WHEN pt.policy_end_date BETWEEN CURDATE() AND (CURDATE() + INTERVAL 1 MONTH) THEN 1 ELSE 0 END) AS `Renewal Pending`, + GROUP_CONCAT(CASE WHEN pt.policy_end_date < CURDATE() THEN pt.id ELSE NULL END) AS Expired_ids, + GROUP_CONCAT(CASE WHEN pt.policy_end_date BETWEEN CURDATE() AND (CURDATE() + INTERVAL 1 MONTH) THEN pt.id ELSE NULL END) AS Renewal_Pending_ids + FROM policy_transaction pt + WHERE pt.policy_end_date IS NOT NULL + AND pt.is_active = 1 + AND ( + pt.policy_end_date < CURDATE() + OR pt.policy_end_date BETWEEN CURDATE() AND (CURDATE() + INTERVAL 1 MONTH) + ) + AND NOT EXISTS ( + SELECT 1 + FROM policy_transaction renewed + WHERE renewed.source_client_policy_id = pt.client_policy_id + AND renewed.client_id = pt.client_id + AND renewed.client_branch_id = pt.client_branch_id + ) + "; - $builder->select([ - 'SUM(CASE WHEN pt.policy_end_date < CURDATE() THEN 1 ELSE 0 END) AS Expired', - 'SUM(CASE WHEN pt.policy_end_date BETWEEN CURDATE() AND (CURDATE() + INTERVAL 1 MONTH) THEN 1 ELSE 0 END) AS `Renewal Pending`', - 'GROUP_CONCAT(CASE WHEN pt.policy_end_date < CURDATE() THEN pt.id ELSE NULL END) AS Expired_ids', - 'GROUP_CONCAT(CASE WHEN pt.policy_end_date BETWEEN CURDATE() AND (CURDATE() + INTERVAL 1 MONTH) THEN pt.id ELSE NULL END) AS Renewal_Pending_ids' - ]); - $builder->where("pt.policy_end_date IS NOT NULL", null, false); - $builder->where('pt.is_active', 1); + $result = $this->db->query($sql)->getResultArray(); + $row = $result[0] ?? [ + 'Expired' => 0, + 'Renewal Pending' => 0, + 'Expired_ids' => null, + 'Renewal_Pending_ids' => null, + ]; - // Self join to check if a policy has been renewed - $builder->join('policy_transaction renewed', 'renewed.source_client_policy_id = pt.client_policy_id and renewed.client_id = pt.client_id and renewed.client_branch_id = pt.client_branch_id', 'left'); + $total = (int) ($row['Expired'] ?? 0) + (int) ($row['Renewal Pending'] ?? 0); - // Filter for expired or expiring policies - $builder->where("(pt.policy_end_date < CURDATE() OR pt.policy_end_date BETWEEN CURDATE() AND (CURDATE() + INTERVAL 1 MONTH))", null, false); + $row['Expired_ids'] = $row['Expired_ids'] ?: []; + $row['Renewal Pending_ids'] = $row['Renewal_Pending_ids'] ?: []; + $row['total'] = $total; - $builder->where('renewed.id IS NULL', null, false); - $query = $builder->get(); - $result = $query->getResultArray(); - - $total = 0; - foreach ($result[0] as $key => $value) { - if ($key !== 'Expired_ids' && $key !== 'Renewal_Pending_ids') { - $total += $value; - } - } - - // Ensure ID fields are arrays (not null) - $result[0]['Expired_ids'] = $result[0]['Expired_ids'] ? $result[0]['Expired_ids'] : []; - $result[0]['Renewal Pending_ids'] = $result[0]['Renewal_Pending_ids'] ? $result[0]['Renewal_Pending_ids'] : []; - - // Add total count - $result[0]['total'] = $total; - - return $result[0]; + return $row; } public function reportBDSNew( @@ -3423,31 +3425,30 @@ } $main_conditions = ''; - function addCondition(&$main_conditions, &$whereAdded, $condition) - { + $addCondition = static function (&$main_conditions, &$whereAdded, $condition) { if (!$whereAdded) { $main_conditions .= " WHERE $condition "; $whereAdded = true; } else { $main_conditions .= " AND $condition "; } - } + }; // 4. Common filters if ($client_id != 0) { - addCondition($main_conditions, $whereAdded, "client_id = $client_id"); + $addCondition($main_conditions, $whereAdded, "client_id = $client_id"); } if ($insurer_id != 0) { - addCondition($main_conditions, $whereAdded, "insurer_id = $insurer_id"); + $addCondition($main_conditions, $whereAdded, "insurer_id = $insurer_id"); } if ($client_branch_id != 0) { - addCondition($main_conditions, $whereAdded, "client_branch_id = $client_branch_id"); + $addCondition($main_conditions, $whereAdded, "client_branch_id = $client_branch_id"); } if ($insurer_branch_id != 0) { - addCondition($main_conditions, $whereAdded, "insurer_branch_id = $insurer_branch_id"); + $addCondition($main_conditions, $whereAdded, "insurer_branch_id = $insurer_branch_id"); } $sql = " @@ -3590,19 +3591,15 @@ LEFT JOIN pt_co_share_details pcsd ON pt.id = pcsd.pt_id JOIN clients c ON c.id = pt.client_id LEFT JOIN client_branch cb ON pt.client_branch_id = cb.id - LEFT JOIN client_policy cp ON pt.client_policy_id = cp.id - LEFT JOIN user_profiles up ON pt.created_by = up.id LEFT JOIN vehicle v ON pt.vehicle_id = v.id LEFT JOIN policy_type ptype ON pt.policy_type_id = ptype.id LEFT JOIN insurers ins ON pcsd.insurer_id = ins.id LEFT JOIN insurer_branch ib ON pcsd.insurer_branch_id = ib.id LEFT JOIN tpa ON pt.tpa_id = tpa.id - LEFT JOIN tpa_branch tb ON pt.tpa_branch_id = tb.id LEFT JOIN user_profiles su ON pt.sales_generated_by = su.id LEFT JOIN user_profiles se ON pt.serviced_by = se.id LEFT JOIN user_profiles created_user ON pt.created_by = created_user.id LEFT JOIN nhance_branch ON pt.issuer_branch = nhance_branch.id - LEFT JOIN nhance_branch service_branch ON pt.service_person_branch_id = service_branch.id LEFT JOIN user_profiles salse_manager ON pt.salse_person_manager_id = salse_manager.id LEFT JOIN user_profiles service_manager ON pt.service_person_manager_id = service_manager.id @@ -3698,21 +3695,7 @@ 0 AS reward, - COALESCE(( - SELECT SUM( - COALESCE(cs.actual_bp_brokerage_amt,0) + - COALESCE(cs.actual_tp_brokerage_amt,0) + - COALESCE(cs.actual_tep_brokerage_amt,0) - ) - FROM co_share_stmt_details cs - JOIN insurer_statements i ON cs.statement_id = i.id - WHERE cs.co_share_id = pcsd.id - AND i.invoice_status IS NOT NULL - AND i.month = insq.month - AND i.is_active = 1 - AND cs.is_active = 1 - GROUP BY pt.policy_no, i.month, pcsd.insurer_id - ),0) AS billed_amt, + COALESCE(MAX(bds_billed.billed_amt), 0) AS billed_amt, CASE WHEN pcsd.co_share_type IN (0,1) THEN pt.policy_no @@ -3774,27 +3757,40 @@ LEFT JOIN pt_co_share_details pcsd ON pt.id = pcsd.pt_id LEFT JOIN co_share_stmt_details cssd ON pcsd.id = cssd.co_share_id LEFT JOIN insurer_statements insq ON cssd.statement_id = insq.id + INNER JOIN ( + SELECT + cs.co_share_id, + i.month, + SUM( + COALESCE(cs.actual_bp_brokerage_amt, 0) + + COALESCE(cs.actual_tp_brokerage_amt, 0) + + COALESCE(cs.actual_tep_brokerage_amt, 0) + ) AS billed_amt + FROM co_share_stmt_details cs + JOIN insurer_statements i ON cs.statement_id = i.id + WHERE i.invoice_status IS NOT NULL + AND i.is_active = 1 + AND cs.is_active = 1 + GROUP BY cs.co_share_id, i.month + HAVING billed_amt <> 0 + ) bds_billed ON bds_billed.co_share_id = pcsd.id + AND bds_billed.month = insq.month JOIN clients c ON c.id = pt.client_id LEFT JOIN client_branch cb ON pt.client_branch_id = cb.id - LEFT JOIN client_policy cp ON pt.client_policy_id = cp.id - LEFT JOIN user_profiles up ON pt.created_by = up.id LEFT JOIN vehicle v ON pt.vehicle_id = v.id LEFT JOIN policy_type ptype ON pt.policy_type_id = ptype.id LEFT JOIN insurers ins ON pcsd.insurer_id = ins.id LEFT JOIN insurer_branch ib ON pcsd.insurer_branch_id = ib.id LEFT JOIN tpa ON pt.tpa_id = tpa.id - LEFT JOIN tpa_branch tb ON pt.tpa_branch_id = tb.id LEFT JOIN user_profiles su ON pt.sales_generated_by = su.id LEFT JOIN user_profiles se ON pt.serviced_by = se.id LEFT JOIN user_profiles created_user ON pt.created_by = created_user.id LEFT JOIN nhance_branch ON pt.issuer_branch = nhance_branch.id - LEFT JOIN nhance_branch service_branch ON pt.service_person_branch_id = service_branch.id LEFT JOIN user_profiles salse_manager ON pt.salse_person_manager_id = salse_manager.id LEFT JOIN user_profiles service_manager ON pt.service_person_manager_id = service_manager.id LEFT JOIN cd_master ON pt.cd_ac_pk = cd_master.id - WHERE pt.is_active = 1 AND pcsd.is_active = 1 AND cssd.is_active = 1 @@ -3885,16 +3881,7 @@ 0 AS total_irda_amt_2, - COALESCE(( - SELECT SUM(cs.reward) - FROM co_share_stmt_details cs - JOIN insurer_statements i ON cs.statement_id = i.id - WHERE cs.co_share_id = pcsd.id - AND i.month = insq.month - AND i.is_active = 1 - AND cs.is_active = 1 - GROUP BY pt.policy_no, i.month, pcsd.insurer_id - ),0) AS reward, + COALESCE(bds_reward.reward, 0) AS reward, 0 AS billed_amt, @@ -3958,21 +3945,30 @@ LEFT JOIN pt_co_share_details pcsd ON pt.id = pcsd.pt_id LEFT JOIN co_share_stmt_details cssd ON pcsd.id = cssd.co_share_id LEFT JOIN insurer_statements insq ON cssd.statement_id = insq.id + INNER JOIN ( + SELECT + cs.co_share_id, + i.month, + SUM(cs.reward) AS reward + FROM co_share_stmt_details cs + JOIN insurer_statements i ON cs.statement_id = i.id + WHERE i.is_active = 1 + AND cs.is_active = 1 + GROUP BY cs.co_share_id, i.month + HAVING reward <> 0 + ) bds_reward ON bds_reward.co_share_id = pcsd.id + AND bds_reward.month = insq.month JOIN clients c ON c.id = pt.client_id LEFT JOIN client_branch cb ON pt.client_branch_id = cb.id - LEFT JOIN client_policy cp ON pt.client_policy_id = cp.id - LEFT JOIN user_profiles up ON pt.created_by = up.id LEFT JOIN vehicle v ON pt.vehicle_id = v.id LEFT JOIN policy_type ptype ON pt.policy_type_id = ptype.id LEFT JOIN insurers ins ON pcsd.insurer_id = ins.id LEFT JOIN insurer_branch ib ON pcsd.insurer_branch_id = ib.id LEFT JOIN tpa ON pt.tpa_id = tpa.id - LEFT JOIN tpa_branch tb ON pt.tpa_branch_id = tb.id LEFT JOIN user_profiles su ON pt.sales_generated_by = su.id LEFT JOIN user_profiles se ON pt.serviced_by = se.id LEFT JOIN user_profiles created_user ON pt.created_by = created_user.id LEFT JOIN nhance_branch ON pt.issuer_branch = nhance_branch.id - LEFT JOIN nhance_branch service_branch ON pt.service_person_branch_id = service_branch.id LEFT JOIN user_profiles salse_manager ON pt.salse_person_manager_id = salse_manager.id LEFT JOIN user_profiles service_manager ON pt.service_person_manager_id = service_manager.id @@ -4102,17 +4098,19 @@ /** * Cached processed BDS report list for a given filter set. + * Returns ['rows' => array, 'totals' => array]. */ public function getCachedBDSReportList(array $filters): array { - $cacheKey = 'bds_report_v1_' . md5(json_encode($filters)); + $cacheKey = 'bds_report_v2_' . md5(json_encode($filters)); $cache = \Config\Services::cache(); $cached = $cache->get($cacheKey); - if (is_array($cached)) { + if (is_array($cached) && isset($cached['rows']) && isset($cached['totals'])) { return $cached; } + // Legacy cache entries stored raw row arrays — ignore and rebuild. $processed = $this->getBDSReportList( $filters['start_date'] ?? 0, $filters['end_date'] ?? 0, @@ -4128,9 +4126,14 @@ $filters['where'] ?? [] ); - $cache->save($cacheKey, $processed, 300); + $payload = [ + 'rows' => $processed, + 'totals' => $this->calculateBDSReportTotals($processed), + ]; - return $processed; + $cache->save($cacheKey, $payload, 300); + + return $payload; } /** @@ -4138,11 +4141,14 @@ */ public function getBDSReportListDataTable(int $draw, int $start, int $length, string $searchValue, array $filters): array { - $allRows = $this->getCachedBDSReportList($filters); + $cached = $this->getCachedBDSReportList($filters); + $allRows = $cached['rows']; $recordsTotal = count($allRows); + $totals = $cached['totals']; if ($searchValue !== '') { $allRows = $this->filterBDSReportRowsBySearch($allRows, $searchValue); + $totals = $this->calculateBDSReportTotals($allRows); } $recordsFiltered = count($allRows); @@ -4160,7 +4166,7 @@ 'recordsTotal' => $recordsTotal, 'recordsFiltered' => $recordsFiltered, 'data' => $pageRows, - 'totals' => $this->calculateBDSReportTotals($allRows), + 'totals' => $totals, ]; } diff --git a/app/Models/TicketMasterModel.php b/app/Models/TicketMasterModel.php index f6ea661d..07bdd813 100644 --- a/app/Models/TicketMasterModel.php +++ b/app/Models/TicketMasterModel.php @@ -93,6 +93,10 @@ class TicketMasterModel extends Model 'policy_transaction_id', 'tpa_claim_type', 'tpa_ailments', + 'tpa_claim_status', + 'tpa_claim_id', + 'settled_amount', + 'tpa_claim_push_reference_no', 'claim_dump_ref_id', 'last_updated_by', 'tpa_shortfall_no', @@ -874,25 +878,37 @@ class TicketMasterModel extends Model public function getDashData($claim_statuses, $limit) { $finalResults = []; + if (empty($claim_statuses)) { + return $finalResults; + } - foreach ($claim_statuses as $typeId => $statuses) { - // Get all active, non-null claim status tickets for this type - $builder = $this->db->table('ticket_master tm') - ->select('tm.id, tm.ticket_type_id, tcs.claim_status, th.last_claim_status_change') - ->join('ticket_claim_status tcs', 'tm.claim_status_id = tcs.id', 'left') - ->join( - '(SELECT ticket_id, MAX(created_at) AS last_claim_status_change + $typeIds = array_map('intval', array_keys($claim_statuses)); + + // One history aggregate + one ticket pull for all types (was 4 full scans). + $builder = $this->db->table('ticket_master tm') + ->select('tm.id, tm.ticket_type_id, tcs.claim_status, th.last_claim_status_change') + ->join('ticket_claim_status tcs', 'tm.claim_status_id = tcs.id', 'left') + ->join( + '(SELECT ticket_id, MAX(created_at) AS last_claim_status_change FROM ticket_history WHERE field_name = \'claim_status_id\' GROUP BY ticket_id) th', - 'tm.id = th.ticket_id', - 'left' - ) - ->where('tm.ticket_type_id', $typeId) - ->where('tm.is_active', 1) - ->where('tm.claim_status_id IS NOT NULL'); + 'tm.id = th.ticket_id', + 'left' + ) + ->whereIn('tm.ticket_type_id', $typeIds) + ->where('tm.is_active', 1) + ->where('tm.claim_status_id IS NOT NULL'); - $results = $builder->get()->getResultArray(); + $allRows = $builder->get()->getResultArray(); + + $rowsByType = []; + foreach ($allRows as $row) { + $rowsByType[$row['ticket_type_id']][] = $row; + } + + foreach ($claim_statuses as $typeId => $statuses) { + $results = $rowsByType[$typeId] ?? []; $summary = [ 'ticket_type_id' => $typeId, @@ -908,7 +924,6 @@ class TicketMasterModel extends Model $threshold = date('Y-m-d H:i:s', strtotime("-{$dateLimit} days")); $ticketIds = []; - // dd($results); foreach ($results as $row) { if ( $row['claim_status'] === $status && @@ -924,17 +939,14 @@ class TicketMasterModel extends Model } $total++; - } } - // Convert ticket IDs array to a comma-separated string $summary[$alias . '_ids'] = implode(',', $ticketIds); } $summary['total'] = $total; - // Add approved but not settled IDs and count $approvedNotSettled = $this->getNotSettledbutApprovedCount($typeId); $summary['approved_not_settled'] = $approvedNotSettled['count']; $summary['approved_not_settled_ids'] = $approvedNotSettled['ticket_ids']; @@ -942,7 +954,6 @@ class TicketMasterModel extends Model $finalResults[$typeId] = $summary; } - // dd($finalResults); return $finalResults; } diff --git a/app/Views/DashBoard.php b/app/Views/DashBoard.php index 561e8098..88e0a6c0 100755 --- a/app/Views/DashBoard.php +++ b/app/Views/DashBoard.php @@ -259,7 +259,7 @@ // $isActive = get_role_id() == STAFF_ROLE_ID && in_array(ENROLLMENT_TEAM_ID,user_team()) ? 'active show' : ''; ?> -->