nhance/app/Commands/RecreateOrphanDumpTickets.php
2026-07-13 10:14:37 +05:30

320 lines
12 KiB
PHP

<?php
namespace App\Commands;
use App\Libraries\TpaClaimsImportFactory;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Find TPA dump rows whose ticket_id is missing/inactive in ticket_master,
* clear the orphan link, then run Job 2 to insert tickets again.
*
* Usage:
* php spark tpa:recreate-orphan-tickets
* php spark tpa:recreate-orphan-tickets --tpa=icici
* php spark tpa:recreate-orphan-tickets --tpa-id=6 --apply
* php spark tpa:recreate-orphan-tickets --file-id=84 --apply
* php spark tpa:recreate-orphan-tickets --tpa=all --apply
*/
class RecreateOrphanDumpTickets extends BaseCommand
{
protected $group = 'TPA';
protected $name = 'tpa:recreate-orphan-tickets';
protected $description = 'Recreate ticket_master rows for dump ticket_id orphans';
protected $usage = 'tpa:recreate-orphan-tickets [--tpa=NAME|all] [--tpa-id=ID] [--file-id=ID] [--include-pending] [--apply]';
protected $options = [
'--tpa' => '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;
}
}