diff --git a/app/Commands/SyncClaimReportFromDump.php b/app/Commands/SyncClaimReportFromDump.php index 3fb4f7cd..bb4a581b 100644 --- a/app/Commands/SyncClaimReportFromDump.php +++ b/app/Commands/SyncClaimReportFromDump.php @@ -2,19 +2,18 @@ namespace App\Commands; -use App\Libraries\TpaClaimsImportFactory; +use App\Libraries\ClaimReportSyncService; use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; -use InvalidArgumentException; /** - * Copy linked TPA dump + ticket_master claims into claim_report. + * Copy linked TPA dump (+ optional ticket_master) claims into claim_report. * * Usage: * php spark claim:sync-report - * php spark claim:sync-report --policy=12 + * php spark claim:sync-report --phase1 + * php spark claim:sync-report --policy=12 --phase1 * php spark claim:sync-report --tpa=mediassist --policy=12 - * php spark claim:sync-report --limit=500 * php spark claim:sync-report --ticket-only */ class SyncClaimReportFromDump extends BaseCommand @@ -22,338 +21,43 @@ class SyncClaimReportFromDump extends BaseCommand protected $group = 'Claims'; protected $name = 'claim:sync-report'; protected $description = 'Sync claim_report from TPA dump tables + ticket_master'; - protected $usage = 'claim:sync-report [--policy=ID] [--tpa=NAME|all] [--limit=N] [--ticket-only]'; + protected $usage = 'claim:sync-report [--policy=ID] [--tpa=NAME|all] [--limit=N] [--phase1] [--ticket-only]'; protected $options = [ '--policy' => 'Optional client_policy_id', '--tpa' => 'icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all)', '--limit' => 'Batch size per dump query (default 500)', + '--phase1' => 'TPA dump tables only (skip ticket_master)', '--ticket-only' => 'Skip dump tables; copy only from ticket_master', ]; - /** - * @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; + $phase1Only = array_key_exists('phase1', $params) || CLI::getOption('phase1') !== null; $tpaOpt = strtolower(trim((string) ($params['tpa'] ?? CLI::getOption('tpa') ?? 'all'))); if ($limit <= 0) { $limit = 500; } - $totalMapped = 0; - $totalSkipped = 0; - $errors = 0; + $result = (new ClaimReportSyncService())->sync($policyId, $tpaOpt, $limit, $ticketOnly, $phase1Only); - 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 ($result['logs'] as $line) { + $color = 'white'; + if (str_starts_with($line, '[FAIL]')) { + $color = 'red'; + } elseif (str_starts_with($line, '[DONE]') || str_starts_with($line, 'Done.')) { + $color = 'green'; + } elseif (str_starts_with($line, '[SKIP]') || str_starts_with($line, 'Phase') || str_starts_with($line, 'Skipping')) { + $color = 'yellow'; + } elseif (str_starts_with($line, ' ')) { + $color = 'green'; } - - 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($line, $color); } - 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; + return ! empty($result['status']) ? EXIT_SUCCESS : EXIT_ERROR; } } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d9c1d803..1628b476 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -865,6 +865,7 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->get('all', 'ClaimReportDashboardController::all'); $routes->get('debug', 'ClaimReportDashboardController::debug'); $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); + $routes->match(['get', 'post'], 'sync', 'ClaimReportDashboardController::sync'); }); $routes->group('enrollment-collection-v1', static function ($routes) { diff --git a/app/Controllers/ApiServiceController.php b/app/Controllers/ApiServiceController.php index de934916..0a716086 100644 --- a/app/Controllers/ApiServiceController.php +++ b/app/Controllers/ApiServiceController.php @@ -898,6 +898,7 @@ class ApiServiceController extends BaseController ->where('client_policy.tpa_id', $tpa_id) ->where('batch_files.is_active', 1) ->where('batch_files.event_type', 'api') + ->where('batch_files.client_policy_id', $policy_id) ->where('batch_files.icici_status_flag !=', 'COMPLETED') ->countAllResults(); diff --git a/app/Controllers/ClaimReportDashboardController.php b/app/Controllers/ClaimReportDashboardController.php index 5a7ad1fb..7e277754 100644 --- a/app/Controllers/ClaimReportDashboardController.php +++ b/app/Controllers/ClaimReportDashboardController.php @@ -5,6 +5,7 @@ namespace App\Controllers; use App\Models\ClaimReportDashboardModel; use App\Models\ClaimsCollectionV2DashboardModel; use App\Models\ClaimDumpFileModel; +use App\Libraries\ClaimReportSyncService; use CodeIgniter\API\ResponseTrait; /** @@ -183,29 +184,26 @@ class ClaimReportDashboardController extends BaseController } /** - * Run claim:sync-report via URL (authMVC / JWT). + * Run claim report sync via URL (authMVC / JWT). + * Uses ClaimReportSyncService directly (no spark/CLI). * * Query params (all optional): - * client_policy / client_policy_id → --policy= + * client_policy / client_policy_id * 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 + * phase1=true → TPA dump tables only (skip ticket_master) + * ticket_only=1 → ticket_master only (ignored if phase1=true) */ 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') ?? '') !== ''; + $ticketOnly = $this->isTruthyParam('ticket_only'); + $phase1Only = $this->isTruthyParam('phase1'); $allowedTpa = ['all', 'vidal', 'abhi', 'mediassist', 'fhpl', 'rcare', 'icici']; if ($tpa === '' || ! in_array($tpa, $allowedTpa, true)) { @@ -222,131 +220,58 @@ class ClaimReportDashboardController extends BaseController $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); + $result = (new ClaimReportSyncService())->sync( + $policyId, + $tpa, + $limit, + $ticketOnly, + $phase1Only + ); } catch (\Throwable $e) { return $this->respond([ - 'status' => false, - 'message' => 'Sync failed: ' . $e->getMessage(), - 'command' => $cmd, - 'started_at' => $startedAt, + 'status' => false, + 'message' => 'Sync failed: ' . $e->getMessage(), + 'policy_id' => $policyId > 0 ? $policyId : null, + 'tpa' => $tpa, + 'limit' => $limit, + 'phase1' => $phase1Only, + 'ticket_only' => $ticketOnly, + 'started_at' => $startedAt, ], 500); } - $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); + $ok = ! empty($result['status']); return $this->respond([ - 'status' => ! $failed, - 'message' => $failed ? 'Sync completed with errors. See summary.' : 'Sync completed.', - 'command' => $cmd, + 'status' => $ok, + 'message' => $result['message'] ?? ($ok ? 'Sync completed.' : 'Sync failed.'), 'policy_id' => $policyId > 0 ? $policyId : null, 'tpa' => $tpa, 'limit' => $limit, - 'ticket_only' => $ticketOnly, + 'phase1' => $phase1Only, + 'ticket_only' => $ticketOnly && ! $phase1Only, '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); + 'summary' => $result['summary'] ?? null, + 'phases' => $result['phases'] ?? [], + 'tpa_results' => $result['tpa_results'] ?? [], + 'logs' => $result['logs'] ?? [], + ], $ok ? 200 : 500); } /** - * Turn CLI sync text into a readable structured payload. - * - * @return array{ - * summary: array, - * phases: list, - * tpa_results: list>, - * logs: list - * } + * True when GET/POST param is 1/true/yes (case-insensitive). */ - protected function parseSyncOutput(string $output): array + protected function isTruthyParam(string $name): bool { - $lines = preg_split('/\R+/', $output) ?: []; - $logs = []; - foreach ($lines as $line) { - $line = trim($line); - if ($line !== '') { - $logs[] = $line; - } + $raw = $this->request->getGet($name) ?? $this->request->getPost($name); + if ($raw === null) { + return false; } - $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, - ]; + return in_array(strtolower(trim((string) $raw)), ['1', 'true', 'yes'], true); } } diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 44d47589..351d674f 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -4072,8 +4072,8 @@ class PolicyTransactionController extends BaseController $user_id = $this->request->getGet('user_id') ?? $this->request->getPost('user_id'); if ($date_type == 'statement_month' && $start_date && $end_date) { - $start_date = (string) date('Y-m-01', strtotime($start_date)); - $end_date = (string) date('Y-m-t', strtotime($end_date)); + $start_date = (string) date('Y-m-01', strtotime($start_date)); + $end_date = (string) date('Y-m-t', strtotime($end_date)); } $normalize = static function ($value) { @@ -4090,7 +4090,7 @@ class PolicyTransactionController extends BaseController $ids = array_filter(explode(',', $sanitized_post_data['ids'] ?? '')); if (!empty($ids)) { - $idsStr = implode(',', array_map('intval', $ids)); + $idsStr = implode(',', array_map('intval', $ids)); $where = "policy_transaction.id IN ($idsStr)"; } else { $where = []; diff --git a/app/Libraries/ClaimReportSyncService.php b/app/Libraries/ClaimReportSyncService.php new file mode 100644 index 00000000..b787c876 --- /dev/null +++ b/app/Libraries/ClaimReportSyncService.php @@ -0,0 +1,444 @@ + + */ + public function tpaConfigs(): array + { + return [ + 'vidal' => [ + 'env' => 'VIDAL_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_vidal', + ], + 'abhi' => [ + 'env' => 'ABHI_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_abhi', + ], + 'mediassist' => [ + 'env' => 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_medi_assist', + ], + 'fhpl' => [ + 'env' => 'FHPL_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_fhpl', + ], + 'rcare' => [ + 'env' => 'R_CARE_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_reliance', + ], + 'icici' => [ + 'env' => 'ICICI_PRIMARY_KEY_CONSTANT', + 'table' => 'claims_dump_icici', + ], + ]; + } + + /** + * @return array{ + * status: bool, + * message: string, + * summary: array{mapped:int,skipped:int,errors:int,message:?string}, + * phases: list, + * tpa_results: list>, + * logs: list + * } + */ + public function sync( + int $policyId = 0, + string $tpa = 'all', + int $limit = 500, + bool $ticketOnly = false, + bool $phase1Only = false + ): array { + $logs = []; + $phases = []; + $tpaResults = []; + $db = db_connect(); + + $log = static function (string $line) use (&$logs): void { + $logs[] = $line; + }; + + if (!$db->tableExists('claim_report')) { + $msg = 'Table claim_report does not exist. Run: php spark migrate'; + $log($msg); + return $this->result(false, $msg, 0, 0, 1, $phases, $tpaResults, $logs); + } + + // phase1Only and ticketOnly are mutually exclusive; phase1 wins. + if ($phase1Only) { + $ticketOnly = false; + } + + $tpa = strtolower(trim($tpa)); + if ($tpa === '') { + $tpa = 'all'; + } + if ($limit <= 0) { + $limit = 500; + } + + $totalMapped = 0; + $totalSkipped = 0; + $errors = 0; + + if (!$ticketOnly) { + $phase1 = 'Phase 1: sync from TPA dump tables (linked ticket_id rows)...'; + $phases[] = $phase1; + $log($phase1); + + $configs = $this->tpaConfigs(); + if ($tpa !== 'all') { + if (!isset($configs[$tpa])) { + $msg = 'Unknown tpa=' . $tpa . '. Use: ' . implode('|', array_keys($configs)) . '|all'; + $log($msg); + return $this->result(false, $msg, 0, 0, 1, $phases, $tpaResults, $logs); + } + $configs = [$tpa => $configs[$tpa]]; + } + + foreach ($configs as $key => $cfg) { + $tpaId = (int) env($cfg['env']); + $table = $cfg['table']; + + if ($tpaId <= 0) { + $detail = "{$key}: env {$cfg['env']} not set"; + $log('[SKIP] ' . $detail); + $tpaResults[] = ['tpa' => $key, 'mapped' => 0, 'status' => 'skipped', 'detail' => $detail]; + continue; + } + + if (!$db->tableExists($table)) { + $detail = "{$key}: table {$table} missing"; + $log('[SKIP] ' . $detail); + $tpaResults[] = ['tpa' => $key, 'mapped' => 0, 'status' => 'skipped', 'detail' => $detail]; + continue; + } + + try { + $service = TpaClaimsImportFactory::make($tpaId); + } catch (InvalidArgumentException $e) { + $detail = $key . ': ' . $e->getMessage(); + $log('[SKIP] ' . $detail); + $tpaResults[] = ['tpa' => $key, 'mapped' => 0, 'status' => 'skipped', 'detail' => $detail]; + continue; + } + + $offset = 0; + $tpaMapped = 0; + + while (true) { + $builder = $db->table($table) + ->select('id, file_id, ticket_id, client_policy_id') + ->where('is_active', 1) + ->where('ticket_id IS NOT NULL', null, false) + ->orderBy('id', 'ASC') + ->limit($limit, $offset); + + if ($policyId > 0) { + $builder->where('client_policy_id', $policyId); + } + + $dumpRows = $builder->get()->getResultArray(); + if ($dumpRows === []) { + break; + } + + $byFile = []; + foreach ($dumpRows as $row) { + $fileId = (int) ($row['file_id'] ?? 0); + $dumpId = (int) ($row['id'] ?? 0); + if ($fileId <= 0 || $dumpId <= 0) { + $totalSkipped++; + continue; + } + $byFile[$fileId][] = $dumpId; + } + + foreach ($byFile as $fileId => $dumpIds) { + $result = $service->backfillClaimReportByDumpIds((int) $fileId, $dumpIds); + if (!$result['status']) { + $log("[FAIL] {$key} file_id={$fileId} upsert failed"); + $errors++; + continue; + } + $tpaMapped += (int) $result['count']; + $totalMapped += (int) $result['count']; + } + + $offset += count($dumpRows); + $log(" {$key}: processed {$offset} dump rows (mapped so far {$tpaMapped})"); + + if (count($dumpRows) < $limit) { + break; + } + } + + $done = "[DONE] {$key} mapped≈{$tpaMapped}"; + $log($done); + $tpaResults[] = [ + 'tpa' => $key, + 'mapped' => $tpaMapped, + 'status' => 'done', + 'detail' => $done, + ]; + } + } else { + $phase = 'Skipping dump tables (ticket-only).'; + $phases[] = $phase; + $log($phase); + } + + if ($phase1Only) { + $skip = 'Phase 2 skipped (phase1=true — TPA dump tables only).'; + $phases[] = $skip; + $log($skip); + $doneMsg = "Done. dump mapped≈{$totalMapped}, skipped={$totalSkipped}, errors={$errors}"; + $log($doneMsg); + + return $this->result( + $errors === 0, + $errors === 0 ? 'Phase 1 sync completed.' : 'Phase 1 sync completed with errors.', + $totalMapped, + $totalSkipped, + $errors, + $phases, + $tpaResults, + $logs, + $doneMsg + ); + } + + $phase2 = 'Phase 2: fill gaps from ticket_master dump-sourced claims...'; + $phases[] = $phase2; + $log($phase2); + + $tmResult = $this->syncFromTicketMaster($db, $policyId, $limit, $log); + if ($tmResult === false) { + return $this->result(false, 'ticket_master sync failed', $totalMapped, $totalSkipped, $errors + 1, $phases, $tpaResults, $logs); + } + + $totalMapped += $tmResult['mapped']; + $totalSkipped += $tmResult['skipped']; + + $doneMsg = "Done. dump+ticket mapped≈{$totalMapped}, skipped={$totalSkipped}, errors={$errors}"; + $log($doneMsg); + + return $this->result( + $errors === 0, + $errors === 0 ? 'Sync completed.' : 'Sync completed with errors. See summary.', + $totalMapped, + $totalSkipped, + $errors, + $phases, + $tpaResults, + $logs, + $doneMsg + ); + } + + /** + * @param callable(string):void $log + * @return array{mapped:int,skipped:int}|false + */ + private function syncFromTicketMaster($db, int $policyId, int $limit, callable $log) + { + if (!$db->tableExists('ticket_master')) { + $log('ticket_master missing'); + return false; + } + + $offset = 0; + $mapped = 0; + $skipped = 0; + $now = date('Y-m-d H:i:s'); + + while (true) { + $builder = $db->table('ticket_master') + ->where('is_active', 1) + ->groupStart() + ->where('claim_dump_ref_id IS NOT NULL', null, false) + ->orWhere('file_id IS NOT NULL', null, false) + ->groupEnd() + ->where("claim_number IS NOT NULL AND TRIM(claim_number) != ''", null, false) + ->orderBy('id', 'ASC') + ->limit($limit, $offset); + + if ($policyId > 0) { + $builder->where('client_policy_id', $policyId); + } + + $tickets = $builder->get()->getResultArray(); + if ($tickets === []) { + break; + } + + $rows = []; + foreach ($tickets as $tm) { + $claimNumber = trim((string) ($tm['claim_number'] ?? '')); + $clientPolicyId = (int) ($tm['client_policy_id'] ?? 0); + if ($claimNumber === '' || $clientPolicyId <= 0) { + $skipped++; + continue; + } + + $sourceTable = null; + $tpaId = (int) ($tm['tpa_id'] ?? 0); + foreach ($this->tpaConfigs() as $cfg) { + if ($tpaId === (int) env($cfg['env'])) { + $sourceTable = $cfg['table']; + break; + } + } + + $rows[] = [ + 'tpa_id' => $tm['tpa_id'] ?? null, + 'client_id' => $tm['client_id'] ?? null, + 'client_policy_id' => $clientPolicyId, + 'file_id' => $tm['file_id'] ?? null, + 'ticket_id' => $tm['id'] ?? null, + 'source_table' => $sourceTable, + 'source_row_id' => $tm['claim_dump_ref_id'] ?? null, + 'claim_number' => $claimNumber, + 'emp_code' => $tm['emp_code'] ?? null, + 'tpa_no' => $tm['tpa_no'] ?? null, + 'emp_id' => $tm['emp_id'] ?? null, + 'insured_emp_id' => $tm['insured_emp_id'] ?? null, + 'claim_amount' => $tm['claim_amount'] ?? null, + 'approved_amount' => $tm['approved_amount'] ?? null, + 'incurred_amount' => $tm['approved_amount'] ?? $tm['claim_amount'] ?? null, + 'si_amt' => $tm['si_amt'] ?? null, + 'tpa_claim_status' => $tm['tpa_claim_status'] ?? null, + 'claim_status_id' => $tm['claim_status_id'] ?? null, + 'tpa_claim_type' => $tm['tpa_claim_type'] ?? null, + 'tpa_ailments' => $tm['tpa_ailments'] ?? null, + 'doa' => $tm['doa'] ?? null, + 'dod' => $tm['dod'] ?? null, + 'date_of_intimat' => $tm['date_of_intimat'] ?? null, + 'settled_date' => $tm['settled_date'] ?? null, + 'approved_date' => $tm['approved_date'] ?? null, + 'claim_dump_date' => $tm['claim_dump_date'] ?? null, + 'hospital_name' => $tm['hospital_name'] ?? null, + 'hospital_city' => $tm['hospital_city'] ?? null, + 'hospital_state' => $tm['hospital_state'] ?? null, + 'hospital_pin_code' => $tm['hospital_pin_code'] ?? null, + 'hospital_address' => $tm['hospital_address'] ?? null, + 'gender' => null, + 'age' => null, + 'relation' => $tm['relationship'] ?? null, + 'is_active' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + + if ($rows !== [] && $this->upsertRows($db, $rows) === false) { + $log('ticket_master upsert failed at offset ' . $offset); + return false; + } + + $mapped += count($rows); + $offset += count($tickets); + $log(" ticket_master: processed {$offset} rows (mapped {$mapped})"); + + if (count($tickets) < $limit) { + break; + } + } + + return ['mapped' => $mapped, 'skipped' => $skipped]; + } + + /** + * @param list> $rows + */ + private function upsertRows($db, array $rows): bool + { + $columns = [ + 'tpa_id', 'client_id', 'client_policy_id', 'file_id', 'ticket_id', + 'source_table', 'source_row_id', 'claim_number', 'emp_code', 'tpa_no', + 'emp_id', 'insured_emp_id', 'claim_amount', 'approved_amount', 'incurred_amount', + 'si_amt', 'tpa_claim_status', 'claim_status_id', 'tpa_claim_type', 'tpa_ailments', + 'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', 'claim_dump_date', + 'hospital_name', 'hospital_city', 'hospital_state', 'hospital_pin_code', 'hospital_address', + 'gender', 'age', 'relation', 'is_active', 'created_at', 'updated_at', + ]; + + $updateCols = array_values(array_diff($columns, ['client_policy_id', 'claim_number', 'created_at'])); + + foreach (array_chunk($rows, 100) as $chunk) { + $placeholders = []; + $binds = []; + foreach ($chunk as $row) { + $rowPlaceholders = []; + foreach ($columns as $col) { + $rowPlaceholders[] = '?'; + $binds[] = $row[$col] ?? null; + } + $placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')'; + } + + $updates = []; + foreach ($updateCols as $col) { + if (in_array($col, ['gender', 'age', 'relation', 'approved_amount', 'incurred_amount', 'tpa_ailments', 'tpa_claim_type', 'source_table', 'source_row_id'], true)) { + $updates[] = '`' . $col . '` = COALESCE(VALUES(`' . $col . '`), `' . $col . '`)'; + } else { + $updates[] = '`' . $col . '` = VALUES(`' . $col . '`)'; + } + } + + $sql = 'INSERT INTO `claim_report` (`' . implode('`, `', $columns) . '`) VALUES ' + . implode(', ', $placeholders) + . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates); + + if ($db->query($sql, $binds) === false) { + return false; + } + } + + return true; + } + + /** + * @param list $phases + * @param list> $tpaResults + * @param list $logs + * @return array + */ + private function result( + bool $status, + string $message, + int $mapped, + int $skipped, + int $errors, + array $phases, + array $tpaResults, + array $logs, + ?string $summaryMessage = null + ): array { + return [ + 'status' => $status, + 'message' => $message, + 'summary' => [ + 'mapped' => $mapped, + 'skipped' => $skipped, + 'errors' => $errors, + 'message' => $summaryMessage, + ], + 'phases' => $phases, + 'tpa_results' => $tpaResults, + 'logs' => $logs, + ]; + } +}