'TPA key: icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all)', '--tpa-id' => 'Numeric TPA primary key (overrides --tpa)', '--file-id' => 'Limit to one claim_dump_files.id', '--include-pending' => 'Also process dump rows with ticket_id IS NULL (never linked)', '--apply' => 'Clear orphan links and run Job 2 (default is dry-run list only)', ]; private function tpaConfigs(): array { return [ 'icici' => ['env' => 'ICICI_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_icici'], 'abhi' => ['env' => 'ABHI_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_abhi'], 'mediassist' => ['env' => 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_medi_assist'], 'fhpl' => ['env' => 'FHPL_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_fhpl'], 'rcare' => ['env' => 'R_CARE_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_reliance'], 'vidal' => ['env' => 'VIDAL_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_vidal'], ]; } public function run(array $params) { helper('utility_helper'); $db = db_connect(); $apply = $this->hasFlag('apply'); $includePending = $this->hasFlag('include-pending'); $fileIdFilter = (int) $this->resolveOptionValue('file-id', '0'); $configs = $this->tpaConfigs(); $selected = $this->resolveSelectedTpas($configs); if ($selected === []) { return; } $summary = []; foreach ($selected as $tpaKey) { $cfg = $configs[$tpaKey]; $tpaId = (int) env($cfg['env']); $table = $cfg['table']; CLI::newLine(); CLI::write(str_repeat('=', 64), 'yellow'); CLI::write(strtoupper($tpaKey) . " (tpa_id={$tpaId}, table={$table})", 'yellow'); CLI::write(str_repeat('=', 64), 'yellow'); if (!$this->tableExists($db, $table)) { CLI::error("Table {$table} missing — skipped"); $summary[$tpaKey] = ['ok' => false, 'error' => 'table missing']; continue; } $orphans = $this->findOrphans($db, $table, $fileIdFilter); CLI::write('Orphan dump rows (ticket_id missing in ticket_master): ' . count($orphans)); $pending = []; if ($includePending) { $pending = $this->findPending($db, $table, $fileIdFilter); CLI::write('Pending dump rows (ticket_id IS NULL): ' . count($pending)); } if ($orphans === [] && $pending === []) { $summary[$tpaKey] = ['ok' => true, 'orphans' => 0, 'pending' => 0, 'files' => 0]; continue; } foreach (array_slice($orphans, 0, 20) as $row) { CLI::write(sprintf( ' [orphan] dump_id=%s file_id=%s ticket_id=%s', $row['id'], $row['file_id'], $row['ticket_id'] )); } if (count($orphans) > 20) { CLI::write(' ... and ' . (count($orphans) - 20) . ' more orphans'); } foreach (array_slice($pending, 0, 10) as $row) { CLI::write(sprintf( ' [pending] dump_id=%s file_id=%s', $row['id'], $row['file_id'] )); } $fileIds = array_values(array_unique(array_map( 'intval', array_merge(array_column($orphans, 'file_id'), array_column($pending, 'file_id')) ))); CLI::write('Affected file_ids: ' . implode(', ', $fileIds), 'cyan'); if (!$apply) { CLI::write("Dry-run only. Apply with: php spark tpa:recreate-orphan-tickets --tpa={$tpaKey} --apply", 'yellow'); $summary[$tpaKey] = [ 'ok' => true, 'orphans' => count($orphans), 'pending' => count($pending), 'files' => count($fileIds), 'dry_run' => true, ]; continue; } $fileResults = []; foreach ($fileIds as $fileId) { CLI::write("Processing file_id={$fileId} ...", 'light_red'); try { $service = TpaClaimsImportFactory::make($tpaId); $result = $service->recreateOrphanDumpTickets($fileId); CLI::write(' result: ' . json_encode($result)); $stillOrphan = $this->countOrphansForFile($db, $table, $fileId); $linked = $db->table($table) ->where('file_id', $fileId) ->where('is_active', 1) ->where('ticket_id IS NOT NULL', null, false) ->countAllResults(); $ok = !empty($result['status']) && $stillOrphan === 0; $fileResults[] = [ 'file_id' => $fileId, 'ok' => $ok, 'still_orphan' => $stillOrphan, 'linked' => $linked, 'message' => $result['message'] ?? null, ]; if (!$ok) { CLI::error(" file_id={$fileId} still has {$stillOrphan} orphan(s) or Job2 failed"); } else { CLI::write(" file_id={$fileId} OK (linked={$linked})", 'green'); } if (!empty($result['status'])) { $db->table('claim_dump_files')->where('id', $fileId)->update([ 'status' => 'success', 'reason' => null, ]); } } catch (\Throwable $e) { CLI::error(" file_id={$fileId} ERROR: " . $e->getMessage()); $fileResults[] = [ 'file_id' => $fileId, 'ok' => false, 'error' => $e->getMessage(), ]; } } $allOk = !in_array(false, array_column($fileResults, 'ok'), true); $summary[$tpaKey] = [ 'ok' => $allOk, 'orphans' => count($orphans), 'pending' => count($pending), 'files' => count($fileIds), 'results' => $fileResults, ]; } CLI::newLine(); CLI::write(str_repeat('=', 64), 'cyan'); CLI::write('SUMMARY', 'cyan'); CLI::write(str_repeat('=', 64), 'cyan'); foreach ($summary as $tpaKey => $row) { $line = sprintf( '%-12s %s orphans=%s pending=%s files=%s%s', strtoupper($tpaKey), !empty($row['ok']) ? 'OK' : 'FAIL', $row['orphans'] ?? 0, $row['pending'] ?? 0, $row['files'] ?? 0, !empty($row['dry_run']) ? ' (dry-run)' : (!empty($row['error']) ? ' | ' . $row['error'] : '') ); CLI::write($line, !empty($row['ok']) ? 'green' : 'red'); } if (!$apply) { CLI::newLine(); CLI::write('No DB changes made. Re-run with --apply to clear orphans and insert tickets.', 'yellow'); } } /** * Dump rows with ticket_id set but no active ticket_master row. */ private function findOrphans($db, string $table, int $fileIdFilter = 0): array { $sql = "SELECT d.id, d.file_id, d.ticket_id FROM `{$table}` d LEFT JOIN ticket_master tm ON tm.id = d.ticket_id AND tm.is_active = 1 WHERE d.is_active = 1 AND d.ticket_id IS NOT NULL AND tm.id IS NULL"; $binds = []; if ($fileIdFilter > 0) { $sql .= ' AND d.file_id = ?'; $binds[] = $fileIdFilter; } $sql .= ' ORDER BY d.file_id ASC, d.id ASC'; return $db->query($sql, $binds)->getResultArray(); } /** * Dump rows never linked / rejected (eligible for Job 2 insert). */ private function findPending($db, string $table, int $fileIdFilter = 0): array { $builder = $db->table($table) ->select('id, file_id, ticket_id') ->where('is_active', 1) ->where('ticket_id IS NULL', null, false) ->where('master_reject_reason IS NULL', null, false); if ($fileIdFilter > 0) { $builder->where('file_id', $fileIdFilter); } return $builder->orderBy('file_id', 'ASC')->orderBy('id', 'ASC')->get()->getResultArray(); } private function countOrphansForFile($db, string $table, int $fileId): int { $sql = "SELECT COUNT(*) AS cnt FROM `{$table}` d LEFT JOIN ticket_master tm ON tm.id = d.ticket_id AND tm.is_active = 1 WHERE d.is_active = 1 AND d.file_id = ? AND d.ticket_id IS NOT NULL AND tm.id IS NULL"; $row = $db->query($sql, [$fileId])->getRowArray(); return (int) ($row['cnt'] ?? 0); } private function resolveSelectedTpas(array $configs): array { $tpaIdOpt = (int) $this->resolveOptionValue('tpa-id', '0'); if ($tpaIdOpt > 0) { foreach ($configs as $key => $cfg) { if ((int) env($cfg['env']) === $tpaIdOpt) { return [$key]; } } CLI::error("No TPA config matches --tpa-id={$tpaIdOpt}"); return []; } $tpaOpt = strtolower((string) ($this->resolveOptionValue('tpa', '') ?: 'all')); if ($tpaOpt === '' || $tpaOpt === 'all') { return array_keys($configs); } if (!isset($configs[$tpaOpt])) { CLI::error("Unknown --tpa={$tpaOpt}. Use: " . implode('|', array_keys($configs)) . '|all'); return []; } return [$tpaOpt]; } private function tableExists($db, string $table): bool { return !empty($db->query('SHOW TABLES LIKE ' . $db->escape($table))->getResultArray()); } private function hasFlag(string $name): bool { if (CLI::getOption($name) !== null) { return true; } return in_array('--' . $name, $_SERVER['argv'] ?? [], true); } private function resolveOptionValue(string $name, string $default): string { $opt = CLI::getOption($name); if (is_string($opt) && $opt !== '') { return $opt; } $argv = $_SERVER['argv'] ?? []; foreach ($argv as $i => $arg) { if (preg_match('/^--' . preg_quote($name, '/') . '=(.+)$/', (string) $arg, $m)) { return trim($m[1]); } if ($arg === '--' . $name && isset($argv[$i + 1]) && strpos((string) $argv[$i + 1], '--') !== 0) { return (string) $argv[$i + 1]; } } return $default; } }