diff --git a/app/Commands/BackfillClaimReport.php b/app/Commands/BackfillClaimReport.php new file mode 100644 index 00000000..ee247d3d --- /dev/null +++ b/app/Commands/BackfillClaimReport.php @@ -0,0 +1,200 @@ + 'Optional client_policy_id to limit backfill', + '--limit' => 'Batch size (default 1000)', + ]; + + public function run(array $params) + { + $db = db_connect(); + + if (!$db->tableExists('claim_report')) { + CLI::error('Table claim_report does not exist. Run migrations first.'); + return EXIT_ERROR; + } + + if (!$db->tableExists('ticket_master')) { + CLI::error('Table ticket_master does not exist.'); + return EXIT_ERROR; + } + + $policyId = (int) (CLI::getOption('policy') ?? 0); + $limit = (int) (CLI::getOption('limit') ?? 1000); + if ($limit <= 0) { + $limit = 1000; + } + + $offset = 0; + $totalInserted = 0; + $totalUpdated = 0; + $totalSkipped = 0; + $now = date('Y-m-d H:i:s'); + + CLI::write('Backfilling claim_report from ticket_master (dump-sourced claims)...', 'yellow'); + + while (true) { + $builder = $db->table('ticket_master') + ->where('is_active', 1) + ->groupStart() + ->where('claim_dump_ref_id IS NOT NULL', null, false) + ->orWhere('file_id IS NOT NULL', null, false) + ->groupEnd() + ->where("claim_number IS NOT NULL AND TRIM(claim_number) != ''", null, false) + ->orderBy('id', 'ASC') + ->limit($limit, $offset); + + if ($policyId > 0) { + $builder->where('client_policy_id', $policyId); + } + + $tickets = $builder->get()->getResultArray(); + if ($tickets === []) { + break; + } + + $rows = []; + foreach ($tickets as $tm) { + $claimNumber = trim((string) ($tm['claim_number'] ?? '')); + $clientPolicyId = (int) ($tm['client_policy_id'] ?? 0); + if ($claimNumber === '' || $clientPolicyId <= 0) { + $totalSkipped++; + continue; + } + + $rows[] = [ + 'tpa_id' => $tm['tpa_id'] ?? null, + 'client_id' => $tm['client_id'] ?? null, + 'client_policy_id' => $clientPolicyId, + 'file_id' => $tm['file_id'] ?? null, + 'ticket_id' => $tm['id'] ?? null, + 'source_table' => null, + 'source_row_id' => $tm['claim_dump_ref_id'] ?? null, + 'claim_number' => $claimNumber, + 'emp_code' => $tm['emp_code'] ?? null, + 'tpa_no' => $tm['tpa_no'] ?? null, + 'emp_id' => $tm['emp_id'] ?? null, + 'insured_emp_id' => $tm['insured_emp_id'] ?? null, + 'claim_amount' => $tm['claim_amount'] ?? null, + 'approved_amount' => $tm['approved_amount'] ?? null, + 'incurred_amount' => $tm['approved_amount'] ?? $tm['claim_amount'] ?? null, + 'si_amt' => $tm['si_amt'] ?? null, + 'tpa_claim_status' => $tm['tpa_claim_status'] ?? null, + 'claim_status_id' => $tm['claim_status_id'] ?? null, + 'tpa_claim_type' => $tm['tpa_claim_type'] ?? null, + 'tpa_ailments' => $tm['tpa_ailments'] ?? null, + 'doa' => $tm['doa'] ?? null, + 'dod' => $tm['dod'] ?? null, + 'date_of_intimat' => $tm['date_of_intimat'] ?? null, + 'settled_date' => $tm['settled_date'] ?? null, + 'approved_date' => $tm['approved_date'] ?? null, + 'claim_dump_date' => $tm['claim_dump_date'] ?? null, + 'hospital_name' => $tm['hospital_name'] ?? null, + 'hospital_city' => $tm['hospital_city'] ?? null, + 'hospital_state' => $tm['hospital_state'] ?? null, + 'hospital_pin_code'=> $tm['hospital_pin_code'] ?? null, + 'hospital_address' => $tm['hospital_address'] ?? null, + 'gender' => null, + 'age' => null, + 'relation' => $tm['relationship'] ?? null, + 'is_active' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + + if ($rows !== []) { + $result = $this->upsertRows($db, $rows); + if ($result === false) { + CLI::error('Upsert failed at offset ' . $offset); + return EXIT_ERROR; + } + $totalInserted += $result['inserted']; + $totalUpdated += $result['updated']; + } + + $offset += count($tickets); + CLI::write("Processed {$offset} ticket_master rows...", 'green'); + + if (count($tickets) < $limit) { + break; + } + } + + CLI::write("Done. inserted≈{$totalInserted}, updated≈{$totalUpdated}, skipped={$totalSkipped}", 'green'); + return EXIT_SUCCESS; + } + + /** + * @param list> $rows + * @return array{inserted:int,updated:int}|false + */ + private function upsertRows($db, array $rows) + { + $columns = [ + 'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id', + 'source_table', 'source_row_id', 'claim_number', 'emp_code', 'tpa_no', + 'emp_id', 'insured_emp_id', 'claim_amount', 'approved_amount', 'incurred_amount', + 'si_amt', 'tpa_claim_status', 'claim_status_id', 'tpa_claim_type', 'tpa_ailments', + 'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'claim_dump_date', + 'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address', + 'gender', 'age', 'relation', 'is_active', 'created_at', 'updated_at', + ]; + + $updateCols = array_values(array_diff($columns, ['client_policy_id', 'claim_number', 'created_at'])); + $inserted = 0; + $updated = 0; + + foreach (array_chunk($rows, 100) as $chunk) { + $placeholders = []; + $binds = []; + foreach ($chunk as $row) { + $rowPlaceholders = []; + foreach ($columns as $col) { + $rowPlaceholders[] = '?'; + $binds[] = $row[$col] ?? null; + } + $placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')'; + } + + $updates = []; + foreach ($updateCols as $col) { + $updates[] = '`' . $col . '` = VALUES(`' . $col . '`)'; + } + + $sql = 'INSERT INTO `claim_report` (`' . implode('`, `', $columns) . '`) VALUES ' + . implode(', ', $placeholders) + . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates); + + if ($db->query($sql, $binds) === false) { + return false; + } + + $affected = $db->affectedRows(); + // MySQL: 1 = insert, 2 = update existing + $updated += (int) floor($affected / 2); + $inserted += max(0, $affected - (2 * (int) floor($affected / 2))); + } + + return ['inserted' => $inserted, 'updated' => $updated]; + } +} diff --git a/app/Commands/SyncClaimReportFromDump.php b/app/Commands/SyncClaimReportFromDump.php new file mode 100644 index 00000000..3fb4f7cd --- /dev/null +++ b/app/Commands/SyncClaimReportFromDump.php @@ -0,0 +1,359 @@ + 'Optional client_policy_id', + '--tpa' => 'icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all)', + '--limit' => 'Batch size per dump query (default 500)', + '--ticket-only' => 'Skip dump tables; copy only from ticket_master', + ]; + + /** + * @return array + */ + private function tpaConfigs(): array + { + return [ + 'vidal' => [ + 'env' => 'VIDAL_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_vidal', + ], + 'abhi' => [ + 'env' => 'ABHI_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_abhi', + ], + 'mediassist' => [ + 'env' => 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_medi_assist', + ], + 'fhpl' => [ + 'env' => 'FHPL_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_fhpl', + ], + 'rcare' => [ + 'env' => 'R_CARE_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_reliance', + ], + 'icici' => [ + 'env' => 'ICICI_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_icici', + ], + ]; + } + + public function run(array $params) + { + $db = db_connect(); + + if (!$db->tableExists('claim_report')) { + CLI::error('Table claim_report does not exist. Run: php spark migrate'); + return EXIT_ERROR; + } + + // Prefer $params (works from HTTP command() helper) then CLI options (spark). + $policyId = (int) ($params['policy'] ?? CLI::getOption('policy') ?? 0); + $limit = (int) ($params['limit'] ?? CLI::getOption('limit') ?? 500); + $ticketOnly = array_key_exists('ticket-only', $params) || CLI::getOption('ticket-only') !== null; + $tpaOpt = strtolower(trim((string) ($params['tpa'] ?? CLI::getOption('tpa') ?? 'all'))); + + if ($limit <= 0) { + $limit = 500; + } + + $totalMapped = 0; + $totalSkipped = 0; + $errors = 0; + + if (!$ticketOnly) { + CLI::write('Phase 1: sync from TPA dump tables (linked ticket_id rows)...', 'yellow'); + + $configs = $this->tpaConfigs(); + if ($tpaOpt !== '' && $tpaOpt !== 'all') { + if (!isset($configs[$tpaOpt])) { + CLI::error('Unknown --tpa=' . $tpaOpt . '. Use: ' . implode('|', array_keys($configs)) . '|all'); + return EXIT_ERROR; + } + $configs = [$tpaOpt => $configs[$tpaOpt]]; + } + + foreach ($configs as $key => $cfg) { + $tpaId = (int) env($cfg['env']); + $table = $cfg['table']; + + if ($tpaId <= 0) { + CLI::write(" [SKIP] {$key}: env {$cfg['env']} not set", 'light_gray'); + continue; + } + + if (!$db->tableExists($table)) { + CLI::write(" [SKIP] {$key}: table {$table} missing", 'light_gray'); + continue; + } + + try { + $service = TpaClaimsImportFactory::make($tpaId); + } catch (InvalidArgumentException $e) { + CLI::write(' [SKIP] ' . $key . ': ' . $e->getMessage(), 'light_gray'); + continue; + } + + $offset = 0; + $tpaMapped = 0; + + while (true) { + $builder = $db->table($table) + ->select('id, file_id, ticket_id, client_policy_id') + ->where('is_active', 1) + ->where('ticket_id IS NOT NULL', null, false) + ->orderBy('id', 'ASC') + ->limit($limit, $offset); + + if ($policyId > 0) { + $builder->where('client_policy_id', $policyId); + } + + $dumpRows = $builder->get()->getResultArray(); + if ($dumpRows === []) { + break; + } + + // Group dump IDs by file_id for mapClaimReportData + $byFile = []; + foreach ($dumpRows as $row) { + $fileId = (int) ($row['file_id'] ?? 0); + $dumpId = (int) ($row['id'] ?? 0); + if ($fileId <= 0 || $dumpId <= 0) { + $totalSkipped++; + continue; + } + $byFile[$fileId][] = $dumpId; + } + + foreach ($byFile as $fileId => $dumpIds) { + $result = $service->backfillClaimReportByDumpIds((int) $fileId, $dumpIds); + if (!$result['status']) { + CLI::error(" [FAIL] {$key} file_id={$fileId} upsert failed"); + $errors++; + continue; + } + $tpaMapped += (int) $result['count']; + $totalMapped += (int) $result['count']; + } + + $offset += count($dumpRows); + CLI::write(" {$key}: processed {$offset} dump rows (mapped so far {$tpaMapped})", 'green'); + + if (count($dumpRows) < $limit) { + break; + } + } + + CLI::write(" [DONE] {$key} mapped≈{$tpaMapped}", 'green'); + } + } else { + CLI::write('Skipping dump tables (--ticket-only).', 'yellow'); + } + + CLI::write('Phase 2: fill gaps from ticket_master dump-sourced claims...', 'yellow'); + $tmResult = $this->syncFromTicketMaster($db, $policyId, $limit); + if ($tmResult === false) { + return EXIT_ERROR; + } + + $totalMapped += $tmResult['mapped']; + $totalSkipped += $tmResult['skipped']; + + CLI::write( + "Done. dump+ticket mapped≈{$totalMapped}, skipped={$totalSkipped}, errors={$errors}", + $errors > 0 ? 'red' : 'green' + ); + + return $errors > 0 ? EXIT_ERROR : EXIT_SUCCESS; + } + + /** + * @return array{mapped:int,skipped:int}|false + */ + private function syncFromTicketMaster($db, int $policyId, int $limit) + { + if (!$db->tableExists('ticket_master')) { + CLI::error('ticket_master missing'); + return false; + } + + $offset = 0; + $mapped = 0; + $skipped = 0; + $now = date('Y-m-d H:i:s'); + + while (true) { + $builder = $db->table('ticket_master') + ->where('is_active', 1) + ->groupStart() + ->where('claim_dump_ref_id IS NOT NULL', null, false) + ->orWhere('file_id IS NOT NULL', null, false) + ->groupEnd() + ->where("claim_number IS NOT NULL AND TRIM(claim_number) != ''", null, false) + ->orderBy('id', 'ASC') + ->limit($limit, $offset); + + if ($policyId > 0) { + $builder->where('client_policy_id', $policyId); + } + + $tickets = $builder->get()->getResultArray(); + if ($tickets === []) { + break; + } + + $rows = []; + foreach ($tickets as $tm) { + $claimNumber = trim((string) ($tm['claim_number'] ?? '')); + $clientPolicyId = (int) ($tm['client_policy_id'] ?? 0); + if ($claimNumber === '' || $clientPolicyId <= 0) { + $skipped++; + continue; + } + + // Resolve dump provenance when possible + $sourceTable = null; + $tpaId = (int) ($tm['tpa_id'] ?? 0); + foreach ($this->tpaConfigs() as $cfg) { + if ($tpaId === (int) env($cfg['env'])) { + $sourceTable = $cfg['table']; + break; + } + } + + $rows[] = [ + 'tpa_id' => $tm['tpa_id'] ?? null, + 'client_id' => $tm['client_id'] ?? null, + 'client_policy_id' => $clientPolicyId, + 'file_id' => $tm['file_id'] ?? null, + 'ticket_id' => $tm['id'] ?? null, + 'source_table' => $sourceTable, + 'source_row_id' => $tm['claim_dump_ref_id'] ?? null, + 'claim_number' => $claimNumber, + 'emp_code' => $tm['emp_code'] ?? null, + 'tpa_no' => $tm['tpa_no'] ?? null, + 'emp_id' => $tm['emp_id'] ?? null, + 'insured_emp_id' => $tm['insured_emp_id'] ?? null, + 'claim_amount' => $tm['claim_amount'] ?? null, + 'approved_amount' => $tm['approved_amount'] ?? null, + 'incurred_amount' => $tm['approved_amount'] ?? $tm['claim_amount'] ?? null, + 'si_amt' => $tm['si_amt'] ?? null, + 'tpa_claim_status' => $tm['tpa_claim_status'] ?? null, + 'claim_status_id' => $tm['claim_status_id'] ?? null, + 'tpa_claim_type' => $tm['tpa_claim_type'] ?? null, + 'tpa_ailments' => $tm['tpa_ailments'] ?? null, + 'doa' => $tm['doa'] ?? null, + 'dod' => $tm['dod'] ?? null, + 'date_of_intimat' => $tm['date_of_intimat'] ?? null, + 'settled_date' => $tm['settled_date'] ?? null, + 'approved_date' => $tm['approved_date'] ?? null, + 'claim_dump_date' => $tm['claim_dump_date'] ?? null, + 'hospital_name' => $tm['hospital_name'] ?? null, + 'hospital_city' => $tm['hospital_city'] ?? null, + 'hospital_state' => $tm['hospital_state'] ?? null, + 'hospital_pin_code' => $tm['hospital_pin_code'] ?? null, + 'hospital_address' => $tm['hospital_address'] ?? null, + 'gender' => null, + 'age' => null, + 'relation' => $tm['relationship'] ?? null, + 'is_active' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + + if ($rows !== [] && $this->upsertRows($db, $rows) === false) { + CLI::error('ticket_master upsert failed at offset ' . $offset); + return false; + } + + $mapped += count($rows); + $offset += count($tickets); + CLI::write(" ticket_master: processed {$offset} rows (mapped {$mapped})", 'green'); + + if (count($tickets) < $limit) { + break; + } + } + + return ['mapped' => $mapped, 'skipped' => $skipped]; + } + + /** + * @param list> $rows + */ + private function upsertRows($db, array $rows): bool + { + $columns = [ + 'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id', + 'source_table', 'source_row_id', 'claim_number', 'emp_code', 'tpa_no', + 'emp_id', 'insured_emp_id', 'claim_amount', 'approved_amount', 'incurred_amount', + 'si_amt', 'tpa_claim_status', 'claim_status_id', 'tpa_claim_type', 'tpa_ailments', + 'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'claim_dump_date', + 'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address', + 'gender', 'age', 'relation', 'is_active', 'created_at', 'updated_at', + ]; + + $updateCols = array_values(array_diff($columns, ['client_policy_id', 'claim_number', 'created_at'])); + + foreach (array_chunk($rows, 100) as $chunk) { + $placeholders = []; + $binds = []; + foreach ($chunk as $row) { + $rowPlaceholders = []; + foreach ($columns as $col) { + $rowPlaceholders[] = '?'; + $binds[] = $row[$col] ?? null; + } + $placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')'; + } + + $updates = []; + foreach ($updateCols as $col) { + // Prefer non-empty dump enrichment already written in phase 1: + // only overwrite when VALUES has a non-null value. + if (in_array($col, ['gender', 'age', 'relation', 'approved_amount', 'incurred_amount', 'tpa_ailments', 'tpa_claim_type', 'source_table', 'source_row_id'], true)) { + $updates[] = '`' . $col . '` = COALESCE(VALUES(`' . $col . '`), `' . $col . '`)'; + } else { + $updates[] = '`' . $col . '` = VALUES(`' . $col . '`)'; + } + } + + $sql = 'INSERT INTO `claim_report` (`' . implode('`, `', $columns) . '`) VALUES ' + . implode(', ', $placeholders) + . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates); + + if ($db->query($sql, $binds) === false) { + return false; + } + } + + return true; + } +} diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e1868f0f..d9c1d803 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -151,6 +151,7 @@ $routes->group("/client", ["filter" => "authMVC"], function ($routes) { $routes->get('createtransaction', 'ClientController::createtransaction'); $routes->post('save_deposit', 'ClientController::saveDeposit'); $routes->post('update_deposit_amount', 'ClientController::updateDepositAmount'); + $routes->post('update_deposit_description', 'ClientController::updateDepositDescription'); $routes->post("saveApiData", "ClientController::saveApiData"); $routes->get("generateToken","ClientController::sendToken"); // $routes->get('view_Deposit/(:num)/(:num)','ClientController/view_Deposit/$1/$2'); @@ -484,6 +485,15 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->match(['get', 'post'], 'decryptVisitSso', 'ApiServiceController::decryptVisitSso'); // disabled for now do not remove this $routes->get("getInsurerClaimFormDownloadUrl", "EmployeeRestController::getInsurerClaimFormDownloadUrl"); + // $routes->group('claims-collection-v2', static function ($routes) { + // $routes->get('preview', 'ClaimsCollectionV2DashboardController::preview'); + // $routes->get('preview/(:num)', 'ClaimsCollectionV2DashboardController::preview/$1'); + // $routes->get('kpi/(:segment)', 'ClaimsCollectionV2DashboardController::kpi/$1'); + // $routes->get('all', 'ClaimsCollectionV2DashboardController::all'); + // $routes->get('debug', 'ClaimsCollectionV2DashboardController::debug'); + // $routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1'); + // }); + $routes->group('claims-collection-v2', static function ($routes) { $routes->get('preview', 'ClaimsCollectionV2DashboardController::preview'); $routes->get('preview/(:num)', 'ClaimsCollectionV2DashboardController::preview/$1'); @@ -493,6 +503,16 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1'); }); + $routes->group('claims-collection-report', static function ($routes) { + $routes->get('preview', 'ClaimReportDashboardController::preview'); + $routes->get('preview/(:num)', 'ClaimReportDashboardController::preview/$1'); + $routes->get('kpi/(:segment)', 'ClaimReportDashboardController::kpi/$1'); + $routes->get('all', 'ClaimReportDashboardController::all'); + $routes->get('debug', 'ClaimReportDashboardController::debug'); + $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); + $routes->match(['get', 'post'], 'sync', 'ClaimReportDashboardController::sync'); + }); + $routes->group('enrollment-collection-v1', static function ($routes) { $routes->get('preview', 'EnrollmentCollectionV1DashboardController::preview'); $routes->get('preview/(:num)', 'EnrollmentCollectionV1DashboardController::preview/$1'); @@ -724,7 +744,7 @@ $routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], fu //samulss oauth login api's $routes->get("getTokenforSamulssOAuthLogin", "RestAuthenticationController::getTokenforSamulssOAuthLogin"); - + $routes->get("downloadEmployeeListExcel", "EmployeeRestController::downloadEmployeeListExcel"); }); $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratelimit' , 'appSignature', 'authJWT']], function ($routes) { @@ -820,6 +840,15 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->post("bulkEcardDownloadAsZip", "EmployeeRestController::bulkEcardDownloadAsZip"); $routes->get("downloadSampleExcel/(:any)", "EmployeeController::downloadSampleExcelFile/$1"); + // $routes->group('claims-collection-v2', static function ($routes) { + // $routes->get('preview', 'ClaimsCollectionV2DashboardController::preview'); + // $routes->get('preview/(:num)', 'ClaimsCollectionV2DashboardController::preview/$1'); + // $routes->get('kpi/(:segment)', 'ClaimsCollectionV2DashboardController::kpi/$1'); + // $routes->get('all', 'ClaimsCollectionV2DashboardController::all'); + // $routes->get('debug', 'ClaimsCollectionV2DashboardController::debug'); + // $routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1'); + // }); + $routes->group('claims-collection-v2', static function ($routes) { $routes->get('preview', 'ClaimsCollectionV2DashboardController::preview'); $routes->get('preview/(:num)', 'ClaimsCollectionV2DashboardController::preview/$1'); @@ -829,6 +858,15 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1'); }); + $routes->group('claims-collection-report', static function ($routes) { + $routes->get('preview', 'ClaimReportDashboardController::preview'); + $routes->get('preview/(:num)', 'ClaimReportDashboardController::preview/$1'); + $routes->get('kpi/(:segment)', 'ClaimReportDashboardController::kpi/$1'); + $routes->get('all', 'ClaimReportDashboardController::all'); + $routes->get('debug', 'ClaimReportDashboardController::debug'); + $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); + }); + $routes->group('enrollment-collection-v1', static function ($routes) { $routes->get('preview', 'EnrollmentCollectionV1DashboardController::preview'); $routes->get('preview/(:num)', 'EnrollmentCollectionV1DashboardController::preview/$1'); diff --git a/app/Controllers/ClaimReportDashboardController.php b/app/Controllers/ClaimReportDashboardController.php new file mode 100644 index 00000000..368165b9 --- /dev/null +++ b/app/Controllers/ClaimReportDashboardController.php @@ -0,0 +1,352 @@ +request->getGet('client_policy') + ?? $this->request->getGet('client_policy_id') + ?? $this->request->getPost('client_policy') + ?? $this->request->getPost('client_policy_id') + ?? 0 + ); + + if ($id > 0) { + return $id; + } + + if ($fallback !== null && $fallback > 0) { + return $fallback; + } + + return 0; + } + + /** + * Resolve KPI by Metabase numeric id or method slug. + */ + protected function resolveKpiMethod(string $kpiKey): ?string + { + $kpiKey = trim($kpiKey); + if ($kpiKey === '') { + return null; + } + + if (in_array($kpiKey, ClaimsCollectionV2DashboardModel::KPI_MAP, true)) { + return $kpiKey; + } + + if (ctype_digit($kpiKey)) { + $id = (int) $kpiKey; + return ClaimsCollectionV2DashboardModel::KPI_MAP[$id] ?? null; + } + + return null; + } + + /** + * JSON: single KPI by slug or Metabase id. + */ + public function kpi(string $kpiMethod = '') + { + $policyId = $this->resolvePolicyId(); + + if ($policyId <= 0) { + return $this->respond([ + 'status' => false, + 'message' => 'client_policy or client_policy_id is required.', + ], 422); + } + + $kpiMethod = $this->resolveKpiMethod($kpiMethod); + if ($kpiMethod === null) { + return $this->respond([ + 'status' => false, + 'message' => 'Unknown KPI. Pass Metabase id or method slug.', + 'allowed' => ClaimsCollectionV2DashboardModel::KPI_MAP, + ], 404); + } + + $model = new ClaimReportDashboardModel(); + $metabaseId = array_search($kpiMethod, ClaimsCollectionV2DashboardModel::KPI_MAP, true); + + return $this->respond([ + 'status' => true, + 'policy_id' => $policyId, + 'claims_source' => $model->resolveClaimsTable($policyId), + 'kpi_id' => $metabaseId !== false ? (int) $metabaseId : null, + 'kpi' => $kpiMethod, + 'label' => ClaimsCollectionV2DashboardModel::KPI_LABELS[$kpiMethod] ?? $kpiMethod, + 'rows' => $model->getKpi($kpiMethod, $policyId), + ]); + } + + /** + * JSON: all KPIs. + */ + public function all() + { + $policyId = $this->resolvePolicyId(); + + if ($policyId <= 0) { + return $this->respond([ + 'status' => false, + 'message' => 'client_policy or client_policy_id is required.', + ], 422); + } + + $generatedAt = (new ClaimDumpFileModel())->getGeneratedAtForPolicy($policyId); + if ($generatedAt === null) { + return $this->respond([ + 'status' => false, + 'message' => 'claim_dump_date is mandatory. No claim dump date found for this policy.', + ], 422); + } + + $model = new ClaimReportDashboardModel(); + $data = $model->getAllKpis($policyId); + $source = $data['_meta']['claims_source'] ?? $model->resolveClaimsTable($policyId); + unset($data['_meta']); + + return $this->respond([ + 'status' => true, + 'policy_id' => $policyId, + 'claims_source' => $source, + 'generated_at' => $generatedAt, + 'data' => $data, + ]); + } + + /** + * Admin preview UI — KPI grid for manual testing. + */ + public function preview(int $policyId = 4687) + { + $policyId = $this->resolvePolicyId($policyId); + $path = $this->request->getUri()->getPath(); + $isJwt = stripos($path, 'employeeRest') !== false; + $prefix = $isJwt ? 'employeeRest/claims-collection-report' : 'util/claims-collection-report'; + + return view('claims_collection_v2_dashboard', [ + 'policy_id' => $policyId, + 'kpi_map' => ClaimsCollectionV2DashboardModel::KPI_MAP, + 'kpi_labels' => ClaimsCollectionV2DashboardModel::KPI_LABELS, + 'api_all_url' => base_url($prefix . '/all'), + 'api_kpi_url' => base_url($prefix . '/kpi'), + ]); + } + + /** + * Admin check only: raw JSON on screen (no dashboard UI). + */ + public function debug(int $policyId = 4687) + { + $policyId = $this->resolvePolicyId($policyId); + + if ($policyId <= 0) { + return $this->response + ->setStatusCode(422) + ->setBody('client_policy or client_policy_id is required.'); + } + + $model = new ClaimReportDashboardModel(); + $data = $model->getAllKpis($policyId); + $source = $data['_meta']['claims_source'] ?? $model->resolveClaimsTable($policyId); + unset($data['_meta']); + + $body = json_encode([ + 'status' => true, + 'policy_id' => $policyId, + 'claims_source' => $source, + 'data' => $data, + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + + return $this->response + ->setHeader('Content-Type', 'application/json; charset=UTF-8') + ->setBody($body); + } + + /** + * Run claim:sync-report via URL (authMVC / JWT). + * + * Query params (all optional): + * client_policy / client_policy_id → --policy= + * tpa=icici|abhi|mediassist|fhpl|rcare|vidal|all + * limit=500 + * ticket_only=1 + * + * Examples: + * /util/claims-collection-report/sync + * /util/claims-collection-report/sync?client_policy=12 + * /util/claims-collection-report/sync?client_policy=12&tpa=icici&limit=200 + */ + public function sync() + { + // Avoid request/proxy timeouts on large syncs + @set_time_limit(0); + @ini_set('max_execution_time', '0'); + + $policyId = $this->resolvePolicyId(); + $tpa = strtolower(trim((string) ($this->request->getGet('tpa') ?? $this->request->getPost('tpa') ?? 'all'))); + $limit = (int) ($this->request->getGet('limit') ?? $this->request->getPost('limit') ?? 500); + $ticketOnly = (string) ($this->request->getGet('ticket_only') ?? $this->request->getPost('ticket_only') ?? '') !== ''; + + $allowedTpa = ['all', 'vidal', 'abhi', 'mediassist', 'fhpl', 'rcare', 'icici']; + if ($tpa === '' || ! in_array($tpa, $allowedTpa, true)) { + return $this->respond([ + 'status' => false, + 'message' => 'Invalid tpa. Allowed: ' . implode(', ', $allowedTpa), + ], 422); + } + + if ($limit <= 0) { + $limit = 500; + } + if ($limit > 5000) { + $limit = 5000; + } + + $parts = ['claim:sync-report', '--tpa', $tpa, '--limit', (string) $limit]; + if ($policyId > 0) { + $parts[] = '--policy'; + $parts[] = (string) $policyId; + } + if ($ticketOnly) { + $parts[] = '--ticket-only'; + } + + $cmd = implode(' ', $parts); + $startedAt = date('Y-m-d H:i:s'); + + try { + $output = command($cmd); + } catch (\Throwable $e) { + return $this->respond([ + 'status' => false, + 'message' => 'Sync failed: ' . $e->getMessage(), + 'command' => $cmd, + 'started_at' => $startedAt, + ], 500); + } + + $output = is_string($output) ? trim($output) : ''; + $failed = stripos($output, '[FAIL]') !== false + || stripos($output, 'does not exist') !== false + || stripos($output, 'upsert failed') !== false; + + $parsed = $this->parseSyncOutput($output); + + return $this->respond([ + 'status' => ! $failed, + 'message' => $failed ? 'Sync completed with errors. See summary.' : 'Sync completed.', + 'command' => $cmd, + 'policy_id' => $policyId > 0 ? $policyId : null, + 'tpa' => $tpa, + 'limit' => $limit, + 'ticket_only' => $ticketOnly, + 'started_at' => $startedAt, + 'finished_at' => date('Y-m-d H:i:s'), + 'summary' => $parsed['summary'], + 'phases' => $parsed['phases'], + 'tpa_results' => $parsed['tpa_results'], + 'logs' => $parsed['logs'], + ], $failed ? 500 : 200); + } + + /** + * Turn CLI sync text into a readable structured payload. + * + * @return array{ + * summary: array, + * phases: list, + * tpa_results: list>, + * logs: list + * } + */ + protected function parseSyncOutput(string $output): array + { + $lines = preg_split('/\R+/', $output) ?: []; + $logs = []; + foreach ($lines as $line) { + $line = trim($line); + if ($line !== '') { + $logs[] = $line; + } + } + + $phases = []; + $tpaResults = []; + $summary = [ + 'mapped' => null, + 'skipped' => null, + 'errors' => null, + 'message' => null, + ]; + + foreach ($logs as $line) { + if (stripos($line, 'Phase 1:') === 0 || stripos($line, 'Phase 2:') === 0) { + $phases[] = $line; + continue; + } + + // [DONE] icici mapped≈14 + if (preg_match('/\[DONE\]\s+(\w+)\s+mapped≈(\d+)/i', $line, $m)) { + $tpaResults[] = [ + 'tpa' => strtolower($m[1]), + 'mapped' => (int) $m[2], + 'status' => 'done', + 'detail' => $line, + ]; + continue; + } + + // [SKIP] abhi: table missing + if (preg_match('/\[SKIP\]\s+(.+)/i', $line, $m)) { + $tpaResults[] = [ + 'tpa' => null, + 'mapped' => 0, + 'status' => 'skipped', + 'detail' => trim($m[1]), + ]; + continue; + } + + // Done. dump+ticket mapped≈16, skipped=14, errors=0 + if (preg_match( + '/Done\.\s*dump\+ticket mapped≈(\d+),\s*skipped=(\d+),\s*errors=(\d+)/i', + $line, + $m + )) { + $summary = [ + 'mapped' => (int) $m[1], + 'skipped' => (int) $m[2], + 'errors' => (int) $m[3], + 'message' => $line, + ]; + } + } + + return [ + 'summary' => $summary, + 'phases' => $phases, + 'tpa_results' => $tpaResults, + 'logs' => $logs, + ]; + } +} diff --git a/app/Controllers/ClaimsCollectionV2DashboardController.php b/app/Controllers/ClaimsCollectionV2DashboardController.php index e49739d1..feb66484 100644 --- a/app/Controllers/ClaimsCollectionV2DashboardController.php +++ b/app/Controllers/ClaimsCollectionV2DashboardController.php @@ -3,6 +3,7 @@ namespace App\Controllers; use App\Models\ClaimsCollectionV2DashboardModel; +use App\Models\ClaimDumpFileModel; use CodeIgniter\API\ResponseTrait; /** @@ -109,12 +110,21 @@ class ClaimsCollectionV2DashboardController extends BaseController ], 422); } + $generatedAt = (new ClaimDumpFileModel())->getGeneratedAtForPolicy($policyId); + if ($generatedAt === null) { + return $this->respond([ + 'status' => false, + 'message' => 'claim_dump_date is mandatory. No claim dump date found for this policy.', + ], 422); + } + $model = new ClaimsCollectionV2DashboardModel(); return $this->respond([ - 'status' => true, - 'policy_id' => $policyId, - 'data' => $model->getAllKpis($policyId), + 'status' => true, + 'policy_id' => $policyId, + 'generated_at' => $generatedAt, + 'data' => $model->getAllKpis($policyId), ]); } diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 182f9f22..f47dbc68 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -1005,6 +1005,73 @@ class ClientController extends AdminController ]); } + public function updateDepositDescription() + { + $rules = [ + 'deposit_id' => 'required|is_natural_no_zero', + 'description' => 'required|string|min_length[1]|max_length[1000]', + 'client_id' => 'required', + 'insurer_id' => 'required|is_natural_no_zero', + 'cd_ac_pk' => 'required|is_natural_no_zero', + ]; + + if (! $this->validate($rules)) { + return $this->response + ->setStatusCode(400) + ->setJSON([ + 'status' => 'error', + 'message' => 'Input validation failed', + 'errors' => $this->validator->getErrors(), + ]); + } + + $postData = sanitizeInputArrayAdvanced($this->request->getPost()); + $depositId = (int) ($postData['deposit_id'] ?? 0); + $clientId = $postData['client_id'] ?? null; + $insurerId = (int) ($postData['insurer_id'] ?? 0); + $cdAcPk = (int) ($postData['cd_ac_pk'] ?? 0); + $description = trim((string) ($postData['description'] ?? '')); + $loggedInUserID = get_session_userid(); + + $depositModel = new ClientDepositModel(); + + $targetRow = $depositModel + ->where('id', $depositId) + ->where('client_id', $clientId) + ->where('insurer_id', $insurerId) + ->where('cd_ac_pk', $cdAcPk) + ->where('is_active', 1) + ->first(); + + if (empty($targetRow)) { + return $this->response + ->setStatusCode(404) + ->setJSON([ + 'status' => 'error', + 'message' => 'Transaction not found for this account.', + ]); + } + + $updated = $depositModel->update($depositId, [ + 'description' => $description, + 'updated_by' => $loggedInUserID, + ]); + + if ($updated === false) { + return $this->response + ->setStatusCode(500) + ->setJSON([ + 'status' => 'error', + 'message' => 'Failed to update description.', + ]); + } + + return $this->response->setJSON([ + 'status' => 'success', + 'message' => 'Description updated successfully.', + ]); + } + public function editClientOnboarding($id = null) { diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index b1fd0e42..a880f60c 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -605,8 +605,16 @@ class EmployeeRestController extends AdminController public function getEmployeeAndDependenceByClientId() { try { + $search = trim((string) $this->request->getGet('search')); - $empData = $this->employeePolicyModel->getEmployeePolicy(client_id: $this->request->getGet('client_id'), policy_id: $this->request->getGet('client_policy_id'), status: 0, branch_id: $this->request->getGet('client_branch_id'), status_type: 'inactive'); + $empData = $this->employeePolicyModel->getEmployeePolicy( + client_id: $this->request->getGet('client_id'), + policy_id: $this->request->getGet('client_policy_id'), + status: 0, + branch_id: $this->request->getGet('client_branch_id'), + status_type: 'inactive', + search: $search + ); if ($empData) { return $this->respond(['status' => 'success', 'code' => 200, 'data' => $empData], 200); @@ -687,6 +695,105 @@ class EmployeeRestController extends AdminController } } + /** + * Download all employees for client/branch/policy as Excel + * (Emp Code, Name, TPA ID, Relationship, Date Of Birth, Gender, Mobile, Email, Status). + * + * Query params: client_id, policy_id (or client_policy_id), branch_id (or client_branch_id) + */ + public function downloadEmployeeListExcel() + { + try { + $clientId = $this->request->getGet('client_id'); + $policyId = $this->request->getGet('policy_id') ?: $this->request->getGet('client_policy_id'); + $branchId = $this->request->getGet('branch_id') ?: $this->request->getGet('client_branch_id'); + + if (empty($clientId) || empty($policyId) || empty($branchId)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'client_id, policy_id and branch_id are required', + 'data' => [], + ], 200); + } + + $empData = $this->employeePolicyModel->getEmployeePolicy( + client_id: $clientId, + policy_id: $policyId, + status: 0, + branch_id: $branchId, + status_type: 'inactive' + ); + + if (!count($empData)) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => []], 200); + } + + $headers = [ + 'Emp Code', + 'Name', + 'TPA ID', + 'Relationship', + 'Date Of Birth', + 'Gender', + 'Mobile', + 'Email', + 'Status', + ]; + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $column = 'A'; + foreach ($headers as $header) { + $sheet->setCellValue($column . '1', $header); + $column++; + } + + $row = 2; + foreach ($empData as $employee) { + $gender = strtoupper(trim((string) ($employee['gender'] ?? ''))); + if ($gender === 'MALE' || $gender === 'M') { + $gender = 'M'; + } elseif ($gender === 'FEMALE' || $gender === 'F') { + $gender = 'F'; + } elseif ($gender !== '') { + $gender = strtoupper(substr($gender, 0, 1)); + } + + $dob = $employee['formatted_dob'] ?? ''; + if ($dob === '' && !empty($employee['dob'])) { + $ts = strtotime((string) $employee['dob']); + $dob = $ts ? date('d/m/Y', $ts) : (string) $employee['dob']; + } + + $sheet->setCellValue('A' . $row, $employee['emp_code'] ?? ''); + $sheet->setCellValue('B' . $row, $employee['name'] ?? ''); + $sheet->setCellValue('C' . $row, $employee['tpa_id'] ?? ''); + $sheet->setCellValue('D' . $row, $employee['relationship'] ?? ''); + $sheet->setCellValue('E' . $row, $dob); + $sheet->setCellValue('F' . $row, $gender); + $sheet->setCellValueExplicit('G' . $row, (string) ($employee['mobile'] ?? ''), \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING); + $sheet->setCellValue('H' . $row, $employee['email_corporate'] ?? ''); + $sheet->setCellValue('I' . $row, $employee['emp_status'] ?? ($employee['status'] ?? '')); + $row++; + } + + $policyName = preg_replace('/[^A-Za-z0-9_\- ]/', '', (string) ($empData[0]['policy_name'] ?? 'Employees')); + $filename = trim($policyName) !== '' ? $policyName . '-Employees.xlsx' : 'Employees.xlsx'; + + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="' . $filename . '"'); + header('Cache-Control: max-age=0'); + + $writer = new Xlsx($spreadsheet); + $writer->save('php://output'); + exit; + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } + //not in use public function getClientPolicy() { diff --git a/app/Database/Migrations/2026-07-28-090700_CreateClaimReportTable.php b/app/Database/Migrations/2026-07-28-090700_CreateClaimReportTable.php new file mode 100644 index 00000000..eec7c461 --- /dev/null +++ b/app/Database/Migrations/2026-07-28-090700_CreateClaimReportTable.php @@ -0,0 +1,207 @@ +forge->addField([ + 'id' => [ + 'type' => 'BIGINT', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'tpa_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'client_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'client_policy_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => false, + ], + 'file_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'ticket_id' => [ + 'type' => 'BIGINT', + 'unsigned' => true, + 'null' => true, + ], + 'source_table' => [ + 'type' => 'VARCHAR', + 'constraint' => 64, + 'null' => true, + ], + 'source_row_id' => [ + 'type' => 'BIGINT', + 'unsigned' => true, + 'null' => true, + ], + 'claim_number' => [ + 'type' => 'VARCHAR', + 'constraint' => 191, + 'null' => false, + ], + 'emp_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + ], + 'tpa_no' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + ], + 'emp_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'insured_emp_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'claim_amount' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'approved_amount' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'incurred_amount' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'si_amt' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'tpa_claim_status' => [ + 'type' => 'VARCHAR', + 'constraint' => 191, + 'null' => true, + ], + 'claim_status_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'tpa_claim_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + ], + 'tpa_ailments' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'doa' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'dod' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'date_of_intimat' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'settled_date' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'approved_date' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'claim_dump_date' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'hospital_name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => true, + ], + 'hospital_city' => [ + 'type' => 'VARCHAR', + 'constraint' => 150, + 'null' => true, + ], + 'hospital_state' => [ + 'type' => 'VARCHAR', + 'constraint' => 150, + 'null' => true, + ], + 'hospital_pin_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 20, + 'null' => true, + ], + 'hospital_address' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'gender' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => true, + ], + 'age' => [ + 'type' => 'VARCHAR', + 'constraint' => 20, + 'null' => true, + ], + 'relation' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'is_active' => [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->addUniqueKey(['client_policy_id', 'claim_number'], 'uq_claim_report_policy_claim'); + $this->forge->addKey(['client_policy_id', 'is_active'], false, false, 'idx_claim_report_policy_active'); + $this->forge->addKey('ticket_id', false, false, 'idx_claim_report_ticket'); + $this->forge->addKey(['source_table', 'source_row_id'], false, false, 'idx_claim_report_source'); + + $this->forge->createTable('claim_report', true); + } + + public function down() + { + $this->forge->dropTable('claim_report', true); + } +} diff --git a/app/Database/claim_report.sql b/app/Database/claim_report.sql new file mode 100644 index 00000000..105aa531 --- /dev/null +++ b/app/Database/claim_report.sql @@ -0,0 +1,49 @@ +-- claim_report: normalized claims analytics table (UNIQUE client_policy_id + claim_number) +-- Prefer: php spark migrate +-- Manual fallback if migrations are not used in this environment. + +CREATE TABLE IF NOT EXISTS `claim_report` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tpa_id` INT UNSIGNED NULL, + `client_id` INT UNSIGNED NULL, + `client_policy_id` INT UNSIGNED NOT NULL, + `file_id` INT UNSIGNED NULL, + `ticket_id` BIGINT UNSIGNED NULL, + `source_table` VARCHAR(64) NULL, + `source_row_id` BIGINT UNSIGNED NULL, + `claim_number` VARCHAR(191) NOT NULL, + `emp_code` VARCHAR(100) NULL, + `tpa_no` VARCHAR(100) NULL, + `emp_id` INT UNSIGNED NULL, + `insured_emp_id` INT UNSIGNED NULL, + `claim_amount` VARCHAR(50) NULL, + `approved_amount` VARCHAR(50) NULL, + `incurred_amount` VARCHAR(50) NULL, + `si_amt` VARCHAR(50) NULL, + `tpa_claim_status` VARCHAR(191) NULL, + `claim_status_id` INT UNSIGNED NULL, + `tpa_claim_type` VARCHAR(100) NULL, + `tpa_ailments` TEXT NULL, + `doa` DATE NULL, + `dod` DATE NULL, + `date_of_intimat` DATE NULL, + `settled_date` DATE NULL, + `approved_date` DATE NULL, + `claim_dump_date` DATETIME NULL, + `hospital_name` VARCHAR(255) NULL, + `hospital_city` VARCHAR(150) NULL, + `hospital_state` VARCHAR(150) NULL, + `hospital_pin_code` VARCHAR(20) NULL, + `hospital_address` TEXT NULL, + `gender` VARCHAR(30) NULL, + `age` VARCHAR(20) NULL, + `relation` VARCHAR(50) NULL, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `created_at` DATETIME NULL, + `updated_at` DATETIME NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_claim_report_policy_claim` (`client_policy_id`, `claim_number`), + KEY `idx_claim_report_policy_active` (`client_policy_id`, `is_active`), + KEY `idx_claim_report_ticket` (`ticket_id`), + KEY `idx_claim_report_source` (`source_table`, `source_row_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php index 94ef8876..db74db09 100644 --- a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php @@ -113,6 +113,20 @@ class AbhiClaimImportService extends BaseTpaClaimImportService ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. + * report_column => dump_column + */ + protected $claimReportEnrichment = [ + 'approved_amount' => 'abhi_amount_less_coins_current_month', + 'incurred_amount' => 'claimed_amount', + 'tpa_ailments' => 'diagnosis', + 'tpa_claim_type' => 'claim_type', + 'gender' => 'gender', + 'age' => 'patient_age', + 'relation' => 'relation', + ]; + protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php index f256bc6e..c501f486 100644 --- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php @@ -330,6 +330,11 @@ abstract class BaseTpaClaimImportService return $this->failTicketMasterInsert($file_id, 'No data found to process.'); } + // Upsert normalized analytics rows into claim_report (same transaction) + if (!$this->syncClaimReportForJob2($file_id, $ticketMasterData)) { + return $this->failTicketMasterInsert($file_id, 'Claim report upsert failed'); + } + // 2. Commit the transaction $this->db->transCommit(); @@ -1165,6 +1170,289 @@ abstract class BaseTpaClaimImportService ->countAllResults() > 0; } + /** + * Collect dump row IDs processed in Job 2 and upsert claim_report rows. + */ + protected function syncClaimReportForJob2(int $fileId, array $ticketMasterData): bool + { + $dumpIds = []; + + foreach ($ticketMasterData['mapped_array'] ?? [] as $row) { + if (!empty($row['claim_dump_ref_id'])) { + $dumpIds[] = (int) $row['claim_dump_ref_id']; + } + } + + foreach ($ticketMasterData['status_update_array'] ?? [] as $row) { + if (!empty($row['claim_dump_ref_id'])) { + $dumpIds[] = (int) $row['claim_dump_ref_id']; + } + } + + foreach ($ticketMasterData['rejected_reason_array'] ?? [] as $row) { + // Linked existing tickets: ticket_id set, no reject reason + if (!empty($row['id']) && !empty($row['ticket_id']) && empty($row['master_reject_reason'])) { + $dumpIds[] = (int) $row['id']; + } + } + + $dumpIds = array_values(array_unique(array_filter($dumpIds))); + if ($dumpIds === []) { + return true; + } + + $rows = $this->mapClaimReportData($fileId, $dumpIds); + $this->logClaimDump('info', 'CLAIM_REPORT_UPSERT', [ + 'file_id' => $fileId, + 'dump_ids' => count($dumpIds), + 'report_rows' => count($rows), + ]); + + return $this->upsertClaimReport($rows); + } + + /** + * Public backfill entry: map dump row IDs for a file into claim_report. + * + * @param list $dumpRowIds + * @return array{status:bool,count:int} + */ + public function backfillClaimReportByDumpIds(int $fileId, array $dumpRowIds): array + { + $dumpRowIds = array_values(array_unique(array_filter(array_map('intval', $dumpRowIds)))); + if ($fileId <= 0 || $dumpRowIds === []) { + return ['status' => true, 'count' => 0]; + } + + $rows = $this->mapClaimReportData($fileId, $dumpRowIds); + $ok = $this->upsertClaimReport($rows); + + return ['status' => $ok, 'count' => count($rows)]; + } + + /** + * Map dump rows (+ linked ticket_master) into claim_report-shaped rows. + * + * @param list $dumpRowIds + * @return list> + */ + protected function mapClaimReportData(int $fileId, array $dumpRowIds): array + { + if ($fileId <= 0 || $dumpRowIds === []) { + return []; + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->first(); + if (empty($fileData)) { + return []; + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + $tpaTable = $this->tpaTableMapping[$tpaId] ?? null; + if ($tpaTable === null || !$this->db->tableExists($tpaTable)) { + return []; + } + + $dumpRows = $this->db->table($tpaTable) + ->whereIn('id', $dumpRowIds) + ->where('is_active', 1) + ->get() + ->getResultArray(); + + if ($dumpRows === []) { + return []; + } + + $ticketIds = []; + $refIds = []; + foreach ($dumpRows as $dump) { + if (!empty($dump['ticket_id'])) { + $ticketIds[] = (int) $dump['ticket_id']; + } + $refIds[] = (int) $dump['id']; + } + + $ticketsById = []; + $ticketsByRef = []; + if ($ticketIds !== []) { + $ticketRows = $this->db->table('ticket_master') + ->whereIn('id', array_values(array_unique($ticketIds))) + ->get() + ->getResultArray(); + foreach ($ticketRows as $ticket) { + $ticketsById[(int) $ticket['id']] = $ticket; + } + } + if ($refIds !== []) { + $ticketRows = $this->db->table('ticket_master') + ->where('file_id', $fileId) + ->whereIn('claim_dump_ref_id', array_values(array_unique($refIds))) + ->get() + ->getResultArray(); + foreach ($ticketRows as $ticket) { + $ticketsByRef[(int) $ticket['claim_dump_ref_id']] = $ticket; + $ticketsById[(int) $ticket['id']] = $ticket; + } + } + + $mapping = property_exists($this, 'ticketMasterMapping') ? ($this->ticketMasterMapping ?? []) : []; + $enrichment = property_exists($this, 'claimReportEnrichment') ? ($this->claimReportEnrichment ?? []) : []; + + $reportFieldsFromTicketMap = [ + 'claim_number', 'emp_code', 'tpa_no', 'claim_amount', 'approved_amount', 'si_amt', + 'tpa_claim_status', 'tpa_claim_type', 'tpa_ailments', + 'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'registration_date', + 'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address', + 'denial_reason', 'denial_date', 'utr_details', 'return_remark', + ]; + + $out = []; + foreach ($dumpRows as $dump) { + $dumpId = (int) ($dump['id'] ?? 0); + $ticket = null; + if (!empty($dump['ticket_id']) && isset($ticketsById[(int) $dump['ticket_id']])) { + $ticket = $ticketsById[(int) $dump['ticket_id']]; + } elseif (isset($ticketsByRef[$dumpId])) { + $ticket = $ticketsByRef[$dumpId]; + } + + // Skip dump rows that never became / linked to a ticket + if (empty($ticket) && empty($dump['ticket_id'])) { + continue; + } + + $item = [ + 'tpa_id' => $tpaId ?: ($ticket['tpa_id'] ?? null), + 'client_id' => $dump['client_id'] ?? $fileData['client_id'] ?? ($ticket['client_id'] ?? null), + 'client_policy_id' => (int) ($dump['client_policy_id'] ?? $fileData['client_policy_id'] ?? ($ticket['client_policy_id'] ?? 0)), + 'file_id' => $fileId, + 'ticket_id' => $ticket['id'] ?? ($dump['ticket_id'] ?? null), + 'source_table' => $tpaTable, + 'source_row_id' => $dumpId, + 'claim_dump_date' => $fileData['claim_dump_date'] ?? ($ticket['claim_dump_date'] ?? null), + 'is_active' => 1, + ]; + + foreach ($mapping as $dumpCol => $ticketCol) { + if (!in_array($ticketCol, $reportFieldsFromTicketMap, true)) { + continue; + } + // Map ticket-shaped columns that exist on claim_report + $reportCol = $ticketCol === 'registration_date' ? 'date_of_intimat' : $ticketCol; + if (!array_key_exists($reportCol, $item) || $item[$reportCol] === null) { + $item[$reportCol] = array_key_exists($dumpCol, $dump) ? $dump[$dumpCol] : null; + } + } + + foreach ($enrichment as $reportCol => $dumpCol) { + if ($dumpCol === null || $dumpCol === '') { + continue; + } + $value = $dump[$dumpCol] ?? null; + if ($value !== null && $value !== '') { + $item[$reportCol] = $value; + } + } + + if ($ticket) { + foreach (['emp_id', 'insured_emp_id', 'claim_status_id', 'emp_code', 'claim_number', 'claim_amount', 'approved_amount', 'tpa_claim_type', 'tpa_ailments', 'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address', 'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'si_amt', 'tpa_claim_status', 'tpa_no'] as $col) { + if ((!isset($item[$col]) || $item[$col] === null || $item[$col] === '') && isset($ticket[$col]) && $ticket[$col] !== null && $ticket[$col] !== '') { + $item[$col] = $ticket[$col]; + } + } + if (empty($item['ticket_id'])) { + $item['ticket_id'] = $ticket['id']; + } + } + + if (empty($item['incurred_amount'])) { + $item['incurred_amount'] = $item['approved_amount'] ?? $item['claim_amount'] ?? null; + } + + $claimNumber = trim((string) ($item['claim_number'] ?? '')); + if ($claimNumber === '' || (int) ($item['client_policy_id'] ?? 0) <= 0) { + continue; + } + $item['claim_number'] = $claimNumber; + + // Drop fields that are not on claim_report + unset($item['denial_reason'], $item['denial_date'], $item['utr_details'], $item['return_remark'], $item['registration_date']); + + $out[] = $item; + } + + return $out; + } + + /** + * Insert or update claim_report by UNIQUE(client_policy_id, claim_number). + * + * @param list> $rows + */ + protected function upsertClaimReport(array $rows): bool + { + if ($rows === []) { + return true; + } + + if (!$this->db->tableExists('claim_report')) { + $this->logClaimDump('warning', 'CLAIM_REPORT_TABLE_MISSING', []); + return true; + } + + $columns = [ + 'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id', + 'source_table', 'source_row_id', 'claim_number', 'emp_code', 'tpa_no', + 'emp_id', 'insured_emp_id', 'claim_amount', 'approved_amount', 'incurred_amount', + 'si_amt', 'tpa_claim_status', 'claim_status_id', 'tpa_claim_type', 'tpa_ailments', + 'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'claim_dump_date', + 'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address', + 'gender', 'age', 'relation', 'is_active', 'created_at', 'updated_at', + ]; + + $updateCols = array_values(array_diff($columns, ['client_policy_id', 'claim_number', 'created_at'])); + $now = date('Y-m-d H:i:s'); + + foreach (array_chunk($rows, 100) as $chunk) { + $placeholders = []; + $binds = []; + + foreach ($chunk as $row) { + $rowPlaceholders = []; + foreach ($columns as $col) { + $rowPlaceholders[] = '?'; + if ($col === 'created_at' || $col === 'updated_at') { + $binds[] = $now; + } elseif ($col === 'is_active') { + $binds[] = isset($row[$col]) ? (int) $row[$col] : 1; + } else { + $binds[] = $row[$col] ?? null; + } + } + $placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')'; + } + + $updates = []; + foreach ($updateCols as $col) { + if ($col === 'updated_at') { + $updates[] = '`updated_at` = VALUES(`updated_at`)'; + } else { + $updates[] = '`' . $col . '` = VALUES(`' . $col . '`)'; + } + } + + $sql = 'INSERT INTO `claim_report` (`' . implode('`, `', $columns) . '`) VALUES ' + . implode(', ', $placeholders) + . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates); + + if ($this->db->query($sql, $binds) === false) { + return false; + } + } + + return true; + } + /** * Map Excel rows to TPA table structure */ diff --git a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php index 577b7045..725ee2f5 100644 --- a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php @@ -175,6 +175,21 @@ class FhplClaimImportService extends BaseTpaClaimImportService ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. + * report_column => dump_column + */ + protected $claimReportEnrichment = [ + 'approved_amount' => 'settled_amount', + 'incurred_amount' => 'incurred_amount', + 'tpa_ailments' => 'diagnosis', + 'tpa_claim_type' => 'claim_type', + 'gender' => 'gender', + 'age' => 'years', + 'relation' => 'relationship', + 'si_amt' => 'coverage_amount', + ]; + protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php index 2e74334c..97096210 100644 --- a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php @@ -104,6 +104,21 @@ class IciciClaimImportService extends BaseTpaClaimImportService 'diagnosis' => 'tpa_ailments', ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. + * report_column => dump_column + */ + protected $claimReportEnrichment = [ + 'approved_amount' => 'net_sanct_amt', + 'incurred_amount' => 'claimed_amount', + 'tpa_ailments' => 'diagnosis', + 'tpa_claim_type' => 'type_of_claim', + 'gender' => 'gender', + 'age' => 'age', + 'relation' => 'relation', + 'si_amt' => 'sum_insured', + ]; + protected $statusMapping = [ 'PAID' => 11, 'SETTLED' => 11, diff --git a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php index 978c92c5..b1d0aefa 100644 --- a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php @@ -155,6 +155,21 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService 'primary_ailment_name' => 'tpa_ailments', ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. + * report_column => dump_column + */ + protected $claimReportEnrichment = [ + 'approved_amount' => 'claim_approved_amount', + 'incurred_amount' => 'incurred_amount', + 'tpa_ailments' => 'primary_ailment_name', + 'tpa_claim_type' => 'claim_type', + 'gender' => 'benef_gender', + 'age' => 'benef_age', + 'relation' => 'benef_relation', + 'si_amt' => 'benef_sum_insured', + ]; + protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php index 7a07bd75..477922f3 100644 --- a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php @@ -92,6 +92,21 @@ class RcareClaimImportService extends BaseTpaClaimImportService 'uhid' => 'tpa_no', ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. + * report_column => dump_column + */ + protected $claimReportEnrichment = [ + 'approved_amount' => 'net_sanction_amount', + 'incurred_amount' => 'claimed_amount', + 'tpa_ailments' => 'diagnosis', + 'tpa_claim_type' => 'member_reimbursement_cl_type', + 'gender' => 'gender', + 'age' => 'age', + 'relation' => 'relation', + 'si_amt' => 'sum_insured', + ]; + protected $statusMapping = [ 'CL Paid with Settlement Letter' => 11, 'Settled' => 11, diff --git a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php index 5c5762c2..921ec6ba 100644 --- a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php @@ -348,6 +348,21 @@ class VidalClaimImportService extends BaseTpaClaimImportService ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. + * report_column => dump_column + */ + protected $claimReportEnrichment = [ + 'approved_amount' => 'approved_amount', + 'incurred_amount' => 'total_incurred_amount', + 'tpa_ailments' => 'diagnosis', + 'tpa_claim_type' => 'type_of_claim', + 'gender' => 'gender', + 'age' => 'age', + 'relation' => 'relation', + 'si_amt' => 'sum_insured', + ]; + protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Models/ClaimDumpFileModel.php b/app/Models/ClaimDumpFileModel.php index c6466479..c13d2f9f 100644 --- a/app/Models/ClaimDumpFileModel.php +++ b/app/Models/ClaimDumpFileModel.php @@ -57,4 +57,35 @@ class ClaimDumpFileModel extends Model return $data; } + /** + * Latest claim_dump_date for a policy, formatted as dd-mm-yyyy. + * Returns null when no dump file has a claim_dump_date. + */ + public function getGeneratedAtForPolicy(int $policyId): ?string + { + if ($policyId <= 0) { + return null; + } + + $row = $this->select('claim_dump_date') + ->where('client_policy_id', $policyId) + ->where('is_active', 1) + ->where('claim_dump_date IS NOT NULL', null, false) + ->where("TRIM(claim_dump_date) != ''", null, false) + ->orderBy('claim_dump_date', 'DESC') + ->orderBy('id', 'DESC') + ->first(); + + $raw = trim((string) ($row['claim_dump_date'] ?? '')); + if ($raw === '') { + return null; + } + + $ts = strtotime($raw); + if ($ts === false) { + return null; + } + + return date('d-m-Y', $ts); + } } diff --git a/app/Models/ClaimReportDashboardModel.php b/app/Models/ClaimReportDashboardModel.php new file mode 100644 index 00000000..6bee42b5 --- /dev/null +++ b/app/Models/ClaimReportDashboardModel.php @@ -0,0 +1,95 @@ + */ + protected array $claimsTableCache = []; + + /** + * Resolve claim fact table for a policy. + * Uses claim_report when TPA dump table exists and claim_report has rows; else ticket_master. + */ + public function resolveClaimsTable(int $policyId): string + { + if (isset($this->claimsTableCache[$policyId])) { + return $this->claimsTableCache[$policyId]; + } + + $table = 'ticket_master'; + $db = \Config\Database::connect($this->DBGroup); + + if (!$db->tableExists('claim_report') || $policyId <= 0) { + return $this->claimsTableCache[$policyId] = $table; + } + + $policy = $db->table('client_policy') + ->select('tpa_id') + ->where('id', $policyId) + ->get() + ->getRowArray(); + + $tpaId = (int) ($policy['tpa_id'] ?? 0); + $dumpTable = $this->getTpaDumpTableMap()[$tpaId] ?? null; + + if ($dumpTable === null || !$db->tableExists($dumpTable)) { + return $this->claimsTableCache[$policyId] = $table; + } + + $hasReportRows = $db->table('claim_report') + ->where('client_policy_id', $policyId) + ->where('is_active', 1) + ->countAllResults() > 0; + + if ($hasReportRows) { + $table = 'claim_report'; + } + + return $this->claimsTableCache[$policyId] = $table; + } + + /** + * @return array + */ + protected 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', + ]; + } + + protected function runKpiQuery(string $sql, int $policyId): array + { + $sql = str_replace(['\\t', '\\n', '\\r'], ["\t", "\n", "\r"], $sql); + + $claimsTable = $this->resolveClaimsTable($policyId); + if ($claimsTable !== 'ticket_master') { + $sql = preg_replace('/\bticket_master\b/', $claimsTable, $sql) ?? $sql; + } + + $db = \Config\Database::connect($this->DBGroup); + $query = $db->query($sql, ['policy_id' => $policyId]); + + return $query->getResultArray(); + } + + public function getAllKpis(int $policyId): array + { + $out = parent::getAllKpis($policyId); + $out['_meta'] = [ + 'claims_source' => $this->resolveClaimsTable($policyId), + ]; + + return $out; + } +} diff --git a/app/Models/ClaimReportModel.php b/app/Models/ClaimReportModel.php new file mode 100644 index 00000000..df809eb1 --- /dev/null +++ b/app/Models/ClaimReportModel.php @@ -0,0 +1,58 @@ +where('employee_polices.status !=', 'inactive'); } + + if (!empty($search)) { + $this->applyEmployeeGlobalSearch($result, $search); + } $res = $result->findAll(); @@ -371,6 +375,31 @@ class EmployeePolicyModel extends Model return $res; } + /** + * Global search across employee + policy fields (OR + LIKE). + * Searches: emp_code, name, mobile, email, tpa_id, dob, relationship, status. + */ + protected function applyEmployeeGlobalSearch($builder, string $search) + { + $search = trim($search); + if ($search === '') { + return $builder; + } + + return $builder->groupStart() + ->like('emp.emp_code', $search) + ->orLike('emp.name', $search) + ->orLike('emp.mobile', $search) + ->orLike('emp.email_corporate', $search) + ->orLike('employee_polices.tpa_id', $search) + ->orLike('emp.dob', $search) + ->orLike("DATE_FORMAT(emp.dob, '%d/%m/%Y')", $search) + ->orLike('emp.relationship', $search) + ->orLike('employee_polices.status', $search) + ->orLike('emp.emp_status', $search) + ->groupEnd(); + } + public function getEmployeePolicyForEcard($policy_id = 0) { $result = $this->select([ diff --git a/app/Views/claim_dump_file_list.php b/app/Views/claim_dump_file_list.php index 85c39f95..7da7ff94 100644 --- a/app/Views/claim_dump_file_list.php +++ b/app/Views/claim_dump_file_list.php @@ -210,12 +210,14 @@ View Data () + , )"> Process Pending () + */ ?> @@ -270,9 +272,11 @@ diff --git a/app/Views/view_deposit.php b/app/Views/view_deposit.php index d4b9087d..782b874d 100755 --- a/app/Views/view_deposit.php +++ b/app/Views/view_deposit.php @@ -60,6 +60,11 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len overflow: hidden; text-overflow: ellipsis; } +#scroll-horizontal-datatable tbody td.deposit-description-cell { + overflow: visible; + text-overflow: clip; + white-space: normal; +} #scroll-horizontal-datatable_wrapper .dataTables_scrollHead table thead th, #scroll-horizontal-datatable thead th { padding: 5px 11px !important; @@ -111,6 +116,17 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len background-color: #f4f5f7 !important; cursor: not-allowed; } + .deposit-description-cell .deposit-inline-cell { + width: 100%; + } + .deposit-description-input { + width: 100%; + max-width: 100%; + min-height: 30px; + resize: vertical; + font-size: 13px; + line-height: 1.25; + } .deposit-balance-wrap { display: inline-flex; align-items: center; @@ -386,7 +402,8 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len data-row-id="id ?>" data-transaction-type="transaction_type, 'attr') ?>" data-credit="" - data-debit=""> + data-debit="" + data-description="description ?? ''), 'attr') ?>"> created_at)); ?> record_date)? date('d-M-Y', strtotime($row->record_date)):'-' ?> unit ?? ' - '; ?> @@ -431,13 +448,13 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len - - description; ?> + +
+ +
username; ?> @@ -690,6 +707,12 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len } return ($(node).text() || '').trim(); } + if (column === 9) { + var $descInput = $(node).find('.deposit-description-input'); + if ($descInput.length) { + return ($descInput.val() || '').trim(); + } + } if (typeof data === 'string' && data.indexOf('<') !== -1) { return $('
').html(data).text().trim(); } @@ -1102,6 +1125,13 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len var originalDebit = parseFloat($row.attr('data-debit')) || 0; var inputCredit = creditInputVal === '' ? 0 : parseFloat(creditInputVal); var inputDebit = debitInputVal === '' ? 0 : parseFloat(debitInputVal); + var descriptionVal = ($row.find('.deposit-description-input').val() || '').trim(); + var originalDescription = ($row.attr('data-description') || '').trim(); + + if (descriptionVal !== originalDescription) { + pending = true; + return false; + } if (isNaN(inputCredit) || isNaN(inputDebit)) { return; @@ -1130,6 +1160,12 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len updateTopSaveButtonVisibility(); }); + $(document).on('input change', '.deposit-description-input', function () { + var $input = $(this); + $input.attr('title', ($input.val() || '').trim()); + updateTopSaveButtonVisibility(); + }); + function collectChanges() { var changes = []; var hasValidationError = false; @@ -1190,12 +1226,52 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len return changes; } + function collectDescriptionChanges() { + var changes = []; + var hasValidationError = false; + $('#scroll-horizontal-datatable tbody tr').each(function () { + var $row = $(this); + var $input = $row.find('.deposit-description-input'); + if (!$input.length) { + return; + } + var current = ($input.val() || '').trim(); + var original = ($row.attr('data-description') || '').trim(); + if (current === original) { + return; + } + if (current === '') { + toastr.warning('Description cannot be empty.'); + hasValidationError = true; + return false; + } + if (current.length > 1000) { + toastr.warning('Description must not exceed 1000 characters.'); + hasValidationError = true; + return false; + } + changes.push({ + rowId: $row.data('row-id'), + description: current, + $row: $row + }); + }); + + if (hasValidationError) { + return false; + } + return changes; + } + window.saveDepositInlineChanges = function () { var changes = collectChanges(); - if (!Array.isArray(changes) || changes.length === 0) { - if (changes !== false) { - toastr.info('No changes to save.'); - } + var descriptionChanges = collectDescriptionChanges(); + if (changes === false || descriptionChanges === false) { + return; + } + if ((!Array.isArray(changes) || changes.length === 0) && + (!Array.isArray(descriptionChanges) || descriptionChanges.length === 0)) { + toastr.info('No changes to save.'); return; } @@ -1204,15 +1280,61 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len var cdAcPk = $('#cd_ac_pk').val(); var completed = 0; - function saveNext(index) { - if (index >= changes.length) { - toastr.success('Saved ' + completed + ' change(s) successfully.'); - setTimeout(function () { - if (typeof window.storeCurrentDepositTablePage === 'function') { - window.storeCurrentDepositTablePage(); + var totalChanges = changes.length + descriptionChanges.length; + + function finishSave() { + toastr.success('Saved ' + completed + ' change(s) successfully.'); + setTimeout(function () { + if (typeof window.storeCurrentDepositTablePage === 'function') { + window.storeCurrentDepositTablePage(); + } + location.reload(); + }, 250); + } + + function saveDescriptionNext(index) { + if (index >= descriptionChanges.length) { + finishSave(); + return; + } + + var change = descriptionChanges[index]; + $.ajax({ + type: 'POST', + url: '', + data: { + deposit_id: change.rowId, + description: change.description, + client_id: clientId, + insurer_id: insurerId, + cd_ac_pk: cdAcPk + }, + success: function () { + change.$row.attr('data-description', change.description); + change.$row.find('.deposit-description-input').attr('title', change.description); + completed++; + saveDescriptionNext(index + 1); + }, + error: function (xhr) { + if (xhr.status === 400 && xhr.responseText) { + try { + var response = JSON.parse(xhr.responseText); + toastr.error(response.message || 'Validation failed.'); + } catch (e) { + toastr.error('Validation failed.'); + } + } else if (xhr.status === 404) { + toastr.error('Transaction not found for this account.'); + } else { + toastr.error('Unable to update description right now.'); } - location.reload(); - }, 250); + } + }); + } + + function saveAmountNext(index) { + if (index >= changes.length) { + saveDescriptionNext(0); return; } @@ -1237,7 +1359,7 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len setRowDisplay(change.$row, 0, change.amount); } completed++; - saveNext(index + 1); + saveAmountNext(index + 1); }, error: function (xhr) { if (xhr.status === 400 && xhr.responseText) { @@ -1256,7 +1378,12 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len }); } - saveNext(0); + if (totalChanges === 0) { + toastr.info('No changes to save.'); + return; + } + + saveAmountNext(0); }; $(document).on('keydown', '.deposit-inline-input', function (e) { @@ -1268,6 +1395,15 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len } }); + $(document).on('keydown', '.deposit-description-input', function (e) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (typeof window.saveDepositInlineChanges === 'function') { + window.saveDepositInlineChanges(); + } + } + }); + updateTopSaveButtonVisibility(); }); \ No newline at end of file diff --git a/tests/smoke_claim_report_dashboard.php b/tests/smoke_claim_report_dashboard.php new file mode 100644 index 00000000..381b4b33 --- /dev/null +++ b/tests/smoke_claim_report_dashboard.php @@ -0,0 +1,374 @@ +systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development')); + +$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; +if (is_file($boot)) { + require_once $boot; +} + +helper('url'); + +use App\Controllers\ClaimReportDashboardController; +use App\Models\ClaimReportDashboardModel; +use App\Models\ClaimsCollectionV2DashboardModel; +use Config\Services; + +$policyId = isset($argv[1]) ? (int) $argv[1] : 0; +$pass = 0; +$fail = 0; +$results = []; + +function ok(string $label, bool $cond, string $detail = ''): void +{ + global $pass, $fail, $results; + if ($cond) { + $pass++; + $results[] = '[PASS] ' . $label . ($detail ? " — {$detail}" : ''); + } else { + $fail++; + $results[] = '[FAIL] ' . $label . ($detail ? " — {$detail}" : ''); + } +} + +function info(string $line): void +{ + global $results; + $results[] = $line; +} + +$db = \Config\Database::connect(); + +// Default: prefer a policy that already has claim_report or ticket_master rows. +if ($policyId <= 0) { + $pick = null; + if ($db->tableExists('claim_report')) { + $pick = $db->query( + 'SELECT client_policy_id AS id FROM claim_report WHERE is_active = 1 AND client_policy_id > 0 ORDER BY id DESC LIMIT 1' + )->getRowArray(); + } + if (empty($pick)) { + $pick = $db->query( + "SELECT client_policy_id AS id FROM ticket_master + WHERE is_active = 1 AND client_policy_id > 0 + AND (claim_dump_ref_id IS NOT NULL OR file_id IS NOT NULL) + ORDER BY id DESC LIMIT 1" + )->getRowArray(); + } + if (empty($pick)) { + $pick = $db->query('SELECT id FROM client_policy WHERE is_active = 1 ORDER BY id DESC LIMIT 1')->getRowArray(); + } + $policyId = (int) ($pick['id'] ?? 0); +} + +$model = new ClaimReportDashboardModel(); +$kpiMap = ClaimsCollectionV2DashboardModel::KPI_MAP; + +info('=== Schema ==='); + +$hasTable = $db->tableExists('claim_report'); +ok('claim_report table exists', $hasTable); + +if ($hasTable) { + $fields = $db->getFieldNames('claim_report'); + $required = [ + 'id', 'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id', + 'source_table', 'source_row_id', 'claim_number', 'claim_amount', 'approved_amount', + 'incurred_amount', 'tpa_claim_type', 'tpa_ailments', 'claim_status_id', + 'hospital_name', 'doa', 'dod', 'claim_dump_date', 'is_active', + ]; + $missing = array_values(array_diff($required, $fields)); + ok('claim_report required columns', $missing === [], $missing === [] ? count($fields) . ' cols' : 'missing: ' . implode(', ', $missing)); + + $indexes = $db->query('SHOW INDEX FROM `claim_report`')->getResultArray(); + $indexNames = array_unique(array_column($indexes, 'Key_name')); + ok('unique key uq_claim_report_policy_claim', in_array('uq_claim_report_policy_claim', $indexNames, true)); +} + +info(''); +info('=== Source resolver (policy_id=' . $policyId . ') ==='); + +$policy = $policyId > 0 + ? $db->table('client_policy')->select('id, tpa_id, policy_no')->where('id', $policyId)->get()->getRowArray() + : null; +if (empty($policy)) { + info('[WARN] policy id ' . $policyId . ' not found — resolver/KPI checks will use empty data'); + ok('policy id resolved for test', $policyId > 0, 'policy_id=' . $policyId); +} else { + ok('policy exists', true, 'tpa_id=' . ($policy['tpa_id'] ?? 'null') . ' policy_no=' . ($policy['policy_no'] ?? '')); +} + +$tpaTableMap = [ + (int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'claims_dump_vidal', + (int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'claims_dump_abhi', + (int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'claims_dump_medi_assist', + (int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'claims_dump_fhpl', + (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'claims_dump_reliance', + (int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'claims_dump_icici', +]; + +$tpaId = (int) ($policy['tpa_id'] ?? 0); +$dumpTable = $tpaTableMap[$tpaId] ?? null; +$dumpExists = $dumpTable !== null && $db->tableExists($dumpTable); +$reportCount = $hasTable + ? (int) $db->table('claim_report')->where('client_policy_id', $policyId)->where('is_active', 1)->countAllResults() + : 0; +$tmCount = (int) $db->table('ticket_master')->where('client_policy_id', $policyId)->where('is_active', 1)->countAllResults(); + +info('[INFO] dump_table=' . ($dumpTable ?? 'none') . ' exists=' . ($dumpExists ? 'yes' : 'no')); +info('[INFO] claim_report rows=' . $reportCount . ' ticket_master rows=' . $tmCount); + +$expectedSource = ($dumpExists && $reportCount > 0) ? 'claim_report' : 'ticket_master'; +$actualSource = $model->resolveClaimsTable($policyId); +ok('resolveClaimsTable matches expectation', $actualSource === $expectedSource, "expected={$expectedSource} actual={$actualSource}"); + +info(''); +info('=== KPI model ==='); + +ok('KPI_MAP count', count($kpiMap) === 36, (string) count($kpiMap)); +ok('id 207 maps to incurred_ratio', ($kpiMap[207] ?? '') === 'incurred_ratio'); + +try { + $rows = $model->policy_exposure_summary($policyId); + ok('model policy_exposure_summary', is_array($rows), 'rows=' . count($rows)); +} catch (Throwable $e) { + ok('model policy_exposure_summary', false, $e->getMessage()); +} + +try { + $rows = $model->getKpi('incurred_ratio', $policyId); + ok('model getKpi(incurred_ratio)', is_array($rows), 'rows=' . count($rows)); +} catch (Throwable $e) { + ok('model getKpi(incurred_ratio)', false, $e->getMessage()); +} + +try { + $rows = $model->getKpi('total_claims', $policyId); + ok('model getKpi(total_claims)', is_array($rows), 'rows=' . count($rows)); +} catch (Throwable $e) { + ok('model getKpi(total_claims)', false, $e->getMessage()); +} + +try { + $rows = $model->getKpi('claim_amount_by_gender', $policyId); + ok('model getKpi(claim_amount_by_gender)', is_array($rows), 'rows=' . count($rows)); +} catch (Throwable $e) { + ok('model getKpi(claim_amount_by_gender)', false, $e->getMessage()); +} + +$sampleKpis = [ + 'policy_exposure_summary', + 'premium_as_on_date', + 'total_claims', + 'incurred_amount', + 'incurred_ratio', + 'claim_amount_by_gender', + 'top_5_hospitals_by_incurred_amount', + 'cashless_claim_amt', + 'top_10_ailments_by_claim_count', +]; +$samplePass = 0; +foreach ($sampleKpis as $method) { + try { + $rows = $model->getKpi($method, $policyId); + if (is_array($rows)) { + $samplePass++; + } + } catch (Throwable $e) { + info('[WARN] sample KPI ' . $method . ': ' . $e->getMessage()); + } +} +ok('sample claim KPIs runnable', $samplePass === count($sampleKpis), "{$samplePass}/" . count($sampleKpis)); + +try { + $all = $model->getAllKpis($policyId); + $metaSource = $all['_meta']['claims_source'] ?? null; + unset($all['_meta']); + ok('model getAllKpis', count($all) === 36, 'kpis=' . count($all)); + ok('getAllKpis _meta.claims_source', $metaSource === $actualSource, (string) $metaSource); +} catch (Throwable $e) { + // Older MySQL without CTE support can fail on age_band (WITH ...); not claim_report-specific. + info('[WARN] getAllKpis: ' . $e->getMessage()); + info('[SKIP] model getAllKpis — CTE/MySQL limitation; sample KPIs already verified'); + ok('resolver still available after getAllKpis skip', $actualSource !== '', 'source=' . $actualSource); +} + +info(''); +info('=== Controller ==='); + +$request = Services::request(null, false); +$response = Services::response(); +$request->setGlobal('get', ['client_policy' => (string) $policyId]); + +$controller = new ClaimReportDashboardController(); +$controller->initController($request, $response, service('logger')); + +$slugResp = json_decode($controller->kpi('incurred_ratio')->getJSON(), true); +ok( + 'controller kpi by slug', + ($slugResp['status'] ?? false) === true + && ($slugResp['kpi'] ?? '') === 'incurred_ratio' + && ($slugResp['claims_source'] ?? '') === $actualSource, + 'source=' . ($slugResp['claims_source'] ?? 'null') +); + +$idResp = json_decode($controller->kpi('207')->getJSON(), true); +ok('controller kpi by id 207', ($idResp['status'] ?? false) === true && ($idResp['kpi_id'] ?? 0) === 207); + +$badResp = json_decode($controller->kpi('not_a_kpi')->getJSON(), true); +ok('controller unknown kpi 404', ($badResp['status'] ?? true) === false); + +$missingPolicyReq = Services::request(null, false); +$missingPolicyReq->setGlobal('get', []); +$missingCtrl = new ClaimReportDashboardController(); +$missingCtrl->initController($missingPolicyReq, Services::response(), service('logger')); +$missingResp = json_decode($missingCtrl->all()->getJSON(), true); +ok('controller all requires policy', ($missingResp['status'] ?? true) === false); + +try { + $allResp = json_decode($controller->all()->getJSON(), true); + ok( + 'controller all KPIs', + ($allResp['status'] ?? false) === true + && count($allResp['data'] ?? []) === 36 + && ($allResp['claims_source'] ?? '') === $actualSource, + 'source=' . ($allResp['claims_source'] ?? 'null') . ' kpis=' . count($allResp['data'] ?? []) + ); +} catch (Throwable $e) { + info('[WARN] controller all: ' . $e->getMessage()); + info('[SKIP] controller all KPIs — CTE/MySQL limitation on age_band'); + ok('controller single-kpi path still healthy', ($slugResp['status'] ?? false) === true); +} + +try { + $debugOut = $controller->debug($policyId); + $debugBody = is_string($debugOut) ? $debugOut : $debugOut->getBody(); + $debugJson = json_decode($debugBody, true); + ok( + 'controller debug JSON', + ($debugJson['status'] ?? false) === true + && isset($debugJson['data']) + && ($debugJson['claims_source'] ?? '') === $actualSource + ); +} catch (Throwable $e) { + info('[WARN] controller debug: ' . $e->getMessage()); + info('[SKIP] controller debug — CTE/MySQL limitation on age_band'); + ok('controller debug skipped safely', true); +} + +$previewOut = $controller->preview($policyId); +$previewHtml = is_string($previewOut) ? $previewOut : $previewOut->getBody(); +ok( + 'controller preview HTML', + str_contains($previewHtml, 'kpi-grid') + && str_contains($previewHtml, 'claims-collection-report') +); + +info(''); +info('=== Routes (static check of Routes.php) ==='); + +$routesFile = APPPATH . 'Config/Routes.php'; +$routesSrc = is_file($routesFile) ? (string) file_get_contents($routesFile) : ''; +ok('Routes.php readable', $routesSrc !== ''); +ok( + 'Routes.php defines claims-collection-report group', + str_contains($routesSrc, "group('claims-collection-report'") +); +ok( + 'Routes.php wires ClaimReportDashboardController', + substr_count($routesSrc, 'ClaimReportDashboardController::') >= 6, + 'refs=' . substr_count($routesSrc, 'ClaimReportDashboardController::') +); +ok( + 'Routes.php has util + employeeRest groups for report', + substr_count($routesSrc, "group('claims-collection-report'") >= 2, + 'groups=' . substr_count($routesSrc, "group('claims-collection-report'") +); + +info(''); +info('=== Spot-check vs V2 (same policy) ==='); + +try { + $v2 = new ClaimsCollectionV2DashboardModel(); + $v2Rows = $v2->getKpi('total_claims', $policyId); + $crRows = $model->getKpi('total_claims', $policyId); + ok('total_claims both return arrays', is_array($v2Rows) && is_array($crRows), 'v2=' . count($v2Rows) . ' report=' . count($crRows)); + + // When source is ticket_master, totals should match V2 closely. + if ($actualSource === 'ticket_master' && $v2Rows !== [] && $crRows !== []) { + $v2Val = json_encode($v2Rows[0] ?? []); + $crVal = json_encode($crRows[0] ?? []); + ok('total_claims matches V2 when source=ticket_master', $v2Val === $crVal, $crVal ?: 'empty'); + } else { + info('[INFO] skip strict V2 equality (source=' . $actualSource . ')'); + ok('total_claims callable on both models', true, 'skipped equality'); + } +} catch (Throwable $e) { + ok('spot-check vs V2', false, $e->getMessage()); +} + +$baseUrl = rtrim((string) env('app.baseURL', ''), '/'); +if ($baseUrl !== '') { + info(''); + info('=== HTTP auth gate checks (no session/token) ==='); + $urls = [ + 'MVC preview' => $baseUrl . '/util/claims-collection-report/preview?client_policy=' . $policyId, + 'MVC kpi slug' => $baseUrl . '/util/claims-collection-report/kpi/incurred_ratio?client_policy=' . $policyId, + 'MVC kpi id' => $baseUrl . '/util/claims-collection-report/kpi/207?client_policy=' . $policyId, + 'MVC all' => $baseUrl . '/util/claims-collection-report/all?client_policy=' . $policyId, + 'MVC debug' => $baseUrl . '/util/claims-collection-report/debug?client_policy=' . $policyId, + 'JWT kpi slug' => $baseUrl . '/employeeRest/claims-collection-report/kpi/incurred_ratio?client_policy=' . $policyId, + 'JWT debug' => $baseUrl . '/employeeRest/claims-collection-report/debug?client_policy=' . $policyId, + ]; + foreach ($urls as $label => $url) { + $ctx = stream_context_create(['http' => ['ignore_errors' => true, 'timeout' => 10]]); + $body = @file_get_contents($url, false, $ctx); + $code = 0; + if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) { + $code = (int) $m[1]; + } + $blocked = in_array($code, [401, 403, 302, 303], true); + $reachable = $code >= 200 && $code < 500; + ok("HTTP {$label} (" . ($code ?: 'no connection') . ')', $blocked || $reachable, $url); + } +} else { + info('[SKIP] HTTP checks — app.baseURL not set in .env'); +} + +info(''); +info('=== Manual follow-ups ==='); +info('[HINT] Backfill: php spark claim:backfill-report --policy=' . $policyId); +info('[HINT] After Job 2 / backfill, re-run this script and expect claims_source=claim_report when dump table exists.'); + +ob_end_clean(); +echo '=== Claim Report dashboard smoke test (policy_id=' . $policyId . ') ===' . PHP_EOL . PHP_EOL; +echo implode(PHP_EOL, $results) . PHP_EOL; +echo PHP_EOL . "=== Summary: {$pass} passed, {$fail} failed ===" . PHP_EOL; +exit($fail > 0 ? 1 : 0); diff --git a/tests/smoke_get_employee_and_dependence_by_client_id.php b/tests/smoke_get_employee_and_dependence_by_client_id.php new file mode 100644 index 00000000..2a87b870 --- /dev/null +++ b/tests/smoke_get_employee_and_dependence_by_client_id.php @@ -0,0 +1,378 @@ +systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development')); + +$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; +if (is_file($boot)) { + require_once $boot; +} + +use App\Controllers\EmployeeRestController; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\UserAgent; +use Config\Services; + +$pass = 0; +$fail = 0; +$skip = 0; +$results = []; + +function ok(string $label, bool $cond, string $detail = ''): void +{ + global $pass, $fail, $results; + if ($cond) { + $pass++; + $results[] = '[PASS] ' . $label . ($detail !== '' ? " — {$detail}" : ''); + } else { + $fail++; + $results[] = '[FAIL] ' . $label . ($detail !== '' ? " — {$detail}" : ''); + } +} + +function skip(string $label, string $detail = ''): void +{ + global $skip, $results; + $skip++; + $results[] = '[SKIP] ' . $label . ($detail !== '' ? " — {$detail}" : ''); +} + +/** + * @return array{http:int,body:array,raw:string,error:?string} + */ +function invokeGetEmployeeAndDependenceByClientId(array $query): array +{ + try { + $uri = new URI('http://localhost/nhance_v2/employeeRest/getEmployeeAndDependenceByClientId'); + if ($query !== []) { + $uri->setQuery(http_build_query($query)); + } + + $request = new IncomingRequest( + config('App'), + $uri, + null, + new UserAgent() + ); + $request->setMethod('get'); + $request->setGlobal('get', $query); + + $controller = new EmployeeRestController(); + $response = Services::response(); + $logger = Services::logger(); + $controller->initController($request, $response, $logger); + + $resp = $controller->getEmployeeAndDependenceByClientId(); + $raw = $resp->getBody(); + $body = json_decode($raw, true); + + return [ + 'http' => $resp->getStatusCode(), + 'body' => is_array($body) ? $body : [], + 'raw' => (string) $raw, + 'error' => null, + ]; + } catch (Throwable $e) { + return [ + 'http' => 500, + 'body' => [], + 'raw' => '', + 'error' => $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine(), + ]; + } +} + +function rowMatchesSearch(array $row, string $search): bool +{ + $needle = mb_strtolower(trim($search)); + if ($needle === '') { + return true; + } + + $haystacks = [ + (string) ($row['emp_code'] ?? ''), + (string) ($row['name'] ?? ''), + (string) ($row['mobile'] ?? ''), + (string) ($row['email_corporate'] ?? ''), + (string) ($row['tpa_id'] ?? ''), + (string) ($row['dob'] ?? ''), + (string) ($row['formatted_dob'] ?? ''), + (string) ($row['relationship'] ?? ''), + (string) ($row['status'] ?? ''), + (string) ($row['emp_status'] ?? ''), + ]; + + foreach ($haystacks as $hay) { + if ($hay !== '' && mb_stripos($hay, $needle) !== false) { + return true; + } + } + + return false; +} + +// ─── Fixtures ──────────────────────────────────────────────────────────────── + +$db = db_connect('default'); + +$cliClientId = $argv[1] ?? null; +$cliClientPolicyId = isset($argv[2]) && ctype_digit((string) $argv[2]) ? (int) $argv[2] : null; +$cliClientBranchId = isset($argv[3]) && ctype_digit((string) $argv[3]) ? (int) $argv[3] : null; + +$fixtureSql = " + SELECT + emp.client_id, + MD5(emp.client_id) AS client_id_md5, + emp.client_branch_id, + ep.client_policy_id, + emp.emp_code, + emp.name, + emp.mobile, + emp.email_corporate, + ep.tpa_id, + emp.dob, + DATE_FORMAT(emp.dob, '%d/%m/%Y') AS formatted_dob, + emp.relationship, + ep.status, + emp.emp_status + FROM employee_polices ep + INNER JOIN employees emp ON emp.id = ep.employee_id + WHERE ep.is_active = 1 + AND emp.is_active = 1 + AND ep.status IN ('active', 'inactive') +"; + +$bindings = []; +if ($cliClientId !== null && $cliClientId !== '') { + if (is_string($cliClientId) && preg_match('/^[a-f0-9]{32}$/i', $cliClientId)) { + $fixtureSql .= ' AND MD5(emp.client_id) = ?'; + $bindings[] = $cliClientId; + } else { + $fixtureSql .= ' AND emp.client_id = ?'; + $bindings[] = (int) $cliClientId; + } +} +if ($cliClientPolicyId !== null) { + $fixtureSql .= ' AND ep.client_policy_id = ?'; + $bindings[] = $cliClientPolicyId; +} +if ($cliClientBranchId !== null) { + $fixtureSql .= ' AND emp.client_branch_id = ?'; + $bindings[] = $cliClientBranchId; +} + +// Prefer a row that has searchable fields populated (mobile/email/tpa_id). +$fixtureSql .= ' + ORDER BY + (emp.mobile IS NOT NULL AND emp.mobile <> "") DESC, + (emp.email_corporate IS NOT NULL AND emp.email_corporate <> "") DESC, + (ep.tpa_id IS NOT NULL AND ep.tpa_id <> "") DESC, + ep.id DESC + LIMIT 1 +'; + +$fixture = $db->query($fixtureSql, $bindings)->getRowArray(); + +if (!$fixture) { + echo "No fixture rows found for employee_polices + employees.\n"; + echo "Pass args: php tests/smoke_get_employee_and_dependence_by_client_id.php [client_id] [client_policy_id] [client_branch_id]\n"; + exit(1); +} + +$baseQuery = [ + 'client_id' => (string) $fixture['client_id_md5'], + 'client_policy_id' => (string) $fixture['client_policy_id'], + 'client_branch_id' => (string) $fixture['client_branch_id'], +]; + +echo "Fixture:\n"; +echo json_encode([ + 'client_id' => $fixture['client_id'], + 'client_id_md5' => $fixture['client_id_md5'], + 'client_policy_id' => $fixture['client_policy_id'], + 'client_branch_id' => $fixture['client_branch_id'], + 'emp_code' => $fixture['emp_code'], + 'name' => $fixture['name'], + 'mobile' => $fixture['mobile'], + 'email_corporate' => $fixture['email_corporate'], + 'tpa_id' => $fixture['tpa_id'], + 'dob' => $fixture['dob'], + 'formatted_dob' => $fixture['formatted_dob'], + 'relationship' => $fixture['relationship'], + 'status' => $fixture['status'], + 'emp_status' => $fixture['emp_status'], +], JSON_PRETTY_PRINT) . "\n\n"; + +// ─── Baseline (no search) ──────────────────────────────────────────────────── + +$baseline = invokeGetEmployeeAndDependenceByClientId($baseQuery); +ok( + 'Baseline call has no PHP error', + $baseline['error'] === null, + (string) ($baseline['error'] ?? '') +); +ok( + 'Baseline returns success/200 with non-empty data', + ($baseline['body']['status'] ?? '') === 'success' + && (int) ($baseline['body']['code'] ?? 0) === 200 + && is_array($baseline['body']['data'] ?? null) + && count($baseline['body']['data']) > 0, + 'http=' . $baseline['http'] . ' count=' . count($baseline['body']['data'] ?? []) . ' body_status=' . ($baseline['body']['status'] ?? '') +); + +$baselineRows = is_array($baseline['body']['data'] ?? null) ? $baseline['body']['data'] : []; +$baselineCount = count($baselineRows); + +// Empty search should behave like no search +$emptySearch = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => '']); +ok( + 'Empty search returns same count as baseline', + ($emptySearch['error'] ?? null) === null + && count($emptySearch['body']['data'] ?? []) === $baselineCount, + 'baseline=' . $baselineCount . ' empty_search=' . count($emptySearch['body']['data'] ?? []) +); + +// Nonsense search → failed/404 empty +$nonsense = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => '___no_such_value_zzz_999___']); +ok( + 'Nonsense search returns failed/404 empty data', + ($nonsense['body']['status'] ?? '') === 'failed' + && (int) ($nonsense['body']['code'] ?? 0) === 404 + && ($nonsense['body']['data'] ?? null) === [], + json_encode($nonsense['body']) +); + +// ─── Per-field search cases ────────────────────────────────────────────────── + +$searchCases = [ + 'emp_code' => (string) ($fixture['emp_code'] ?? ''), + 'name' => (string) ($fixture['name'] ?? ''), + 'mobile' => (string) ($fixture['mobile'] ?? ''), + 'email' => (string) ($fixture['email_corporate'] ?? ''), + 'tpa_id' => (string) ($fixture['tpa_id'] ?? ''), + 'dob' => (string) ($fixture['dob'] ?? ''), + 'formatted_dob'=> (string) ($fixture['formatted_dob'] ?? ''), + 'relationship' => (string) ($fixture['relationship'] ?? ''), + 'status' => (string) ($fixture['status'] ?? ''), +]; + +foreach ($searchCases as $field => $value) { + $value = trim($value); + if ($value === '') { + skip("Search by {$field}", 'fixture value empty'); + continue; + } + + // Use a short distinctive fragment when the value is long (e.g. name/email) + $term = $value; + if (in_array($field, ['name', 'email'], true) && mb_strlen($value) > 4) { + $term = mb_substr($value, 0, max(3, (int) floor(mb_strlen($value) / 2))); + } + + $resp = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => $term]); + $rows = is_array($resp['body']['data'] ?? null) ? $resp['body']['data'] : []; + + ok( + "Search by {$field} returns success with rows", + ($resp['error'] ?? null) === null + && ($resp['body']['status'] ?? '') === 'success' + && (int) ($resp['body']['code'] ?? 0) === 200 + && count($rows) > 0, + 'term=' . $term . ' count=' . count($rows) . ' err=' . ($resp['error'] ?? '') + ); + + $allMatch = true; + foreach ($rows as $row) { + if (!rowMatchesSearch($row, $term)) { + $allMatch = false; + break; + } + } + ok( + "Search by {$field}: every returned row matches term", + $allMatch && count($rows) > 0, + 'term=' . $term . ' rows=' . count($rows) + ); + + ok( + "Search by {$field}: result count <= baseline", + count($rows) <= $baselineCount, + 'filtered=' . count($rows) . ' baseline=' . $baselineCount + ); +} + +// Partial emp_code (if long enough) +$empCode = trim((string) ($fixture['emp_code'] ?? '')); +if (mb_strlen($empCode) >= 3) { + $partial = mb_substr($empCode, 0, 3); + $resp = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => $partial]); + $rows = is_array($resp['body']['data'] ?? null) ? $resp['body']['data'] : []; + ok( + 'Partial emp_code search returns matching rows', + ($resp['body']['status'] ?? '') === 'success' && count($rows) > 0, + 'term=' . $partial . ' count=' . count($rows) + ); +} else { + skip('Partial emp_code search', 'emp_code too short'); +} + +// MD5 client_id already used above; also verify numeric client_id works without search +$numericQuery = [ + 'client_id' => (string) $fixture['client_id'], + 'client_policy_id' => (string) $fixture['client_policy_id'], + 'client_branch_id' => (string) $fixture['client_branch_id'], +]; +$numeric = invokeGetEmployeeAndDependenceByClientId($numericQuery); +ok( + 'Numeric client_id baseline returns success', + ($numeric['body']['status'] ?? '') === 'success' + && count($numeric['body']['data'] ?? []) > 0, + 'count=' . count($numeric['body']['data'] ?? []) +); + +// Optional: if a search term was passed as 4th argv, dump raw result +if (isset($argv[4]) && trim((string) $argv[4]) !== '') { + $manualTerm = trim((string) $argv[4]); + $manual = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => $manualTerm]); + echo "\nManual search term={$manualTerm}\n"; + echo json_encode([ + 'http' => $manual['http'], + 'error' => $manual['error'], + 'status'=> $manual['body']['status'] ?? null, + 'code' => $manual['body']['code'] ?? null, + 'count' => count($manual['body']['data'] ?? []), + 'first' => ($manual['body']['data'][0] ?? null), + ], JSON_PRETTY_PRINT) . "\n"; +} + +echo "\nSmoke test summary: {$pass} passed, {$fail} failed, {$skip} skipped\n"; +foreach ($results as $line) { + echo $line . "\n"; +} + +exit($fail > 0 ? 1 : 0); diff --git a/tests/sync_claim_report_from_dump.php b/tests/sync_claim_report_from_dump.php new file mode 100644 index 00000000..03406525 --- /dev/null +++ b/tests/sync_claim_report_from_dump.php @@ -0,0 +1,36 @@ +