nhance/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php

1198 lines
44 KiB
PHP

<?php
namespace App\Libraries\TPAClaimsImportServices;
use CodeIgniter\Database\BaseConnection;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use App\Models\TicketMasterModel;
use App\Models\EmployeeModel;
use App\Models\ClaimDumpFileModel;
use App\Models\ClaimsDumpFhplModel;
use App\Models\ClientPolicyModel;
use RuntimeException;
abstract class BaseTpaClaimImportService
{
protected BaseConnection $db;
protected $claimDumpFileModel;
protected $clientPolicyModel;
protected $policyNumberMapping;
protected $tpaTableMapping;
public function __construct()
{
$this->db = db_connect();
$this->claimDumpFileModel = new ClaimDumpFileModel();
$this->clientPolicyModel = new ClientPolicyModel();
$this->policyNumberMapping = [
(int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'Insurer Policy Number',
(int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'Policy Number',
(int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'policy_no',
(int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'Policy No',
(int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'Policy Number',
(int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'POLICY_NO',
];
$this->tpaTableMapping = [
(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',
];
}
/**
* Structured claim-dump process logging. Search logs with: [CLAIM_DUMP]
* Uses error level so entries appear under production logger threshold.
*/
protected function logClaimDump(string $level, string $step, array $context = []): void
{
$parts = [];
foreach ($context as $key => $value) {
if (is_bool($value)) {
$parts[] = $key . '=' . ($value ? 'true' : 'false');
} elseif (is_scalar($value) || $value === null) {
$parts[] = $key . '=' . ($value === null ? 'null' : $value);
} else {
$parts[] = $key . '=' . json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}
$suffix = $parts === [] ? '' : ' ' . implode(' ', $parts);
// Keep severity in the message; always write as error for production visibility.
log_message('error', '[CLAIM_DUMP][' . strtoupper($level) . '][' . $step . ']' . $suffix);
}
/**
* First JOB for insert TPA wise Bulk Upload
*/
public function runTpaClaimDumpInsert(string $filePath, int $fileId): array
{
$this->logClaimDump('info', 'JOB1_START', [
'file_id' => $fileId,
'file_path' => $filePath,
]);
// 1. Start Transaction
$this->db->transBegin();
try {
$fileData = $this->claimDumpFileModel->where('id', $fileId)->first();
if (empty($fileData)) {
$this->logClaimDump('error', 'JOB1_FILE_NOT_FOUND', ['file_id' => $fileId]);
return $this->failTpaClaimDumpInsert($fileId, 'Claim dump file record not found');
}
$client_policy_data = $this->clientPolicyModel->where('id', $fileData['client_policy_id'])->first();
if (empty($client_policy_data)) {
$this->logClaimDump('error', 'JOB1_POLICY_NOT_FOUND', [
'file_id' => $fileId,
'client_policy_id' => $fileData['client_policy_id'] ?? null,
]);
return $this->failTpaClaimDumpInsert($fileId, 'Client policy not found');
}
$this->logClaimDump('info', 'JOB1_FILE_LOADED', [
'file_id' => $fileId,
'tpa_id' => $fileData['tpa_id'] ?? null,
'client_id' => $fileData['client_id'] ?? null,
'client_policy_id' => $fileData['client_policy_id'] ?? null,
'policy_no' => $client_policy_data['policy_no'] ?? null,
]);
// Determine sheet name logic...
if (env('FHPL_PRIMARY_KEY_CONSTANT') == $fileData['tpa_id']) {
$rows = $this->readExcelBySheetName($filePath, 'Claims&Preauth');
$this->logClaimDump('info', 'JOB1_EXCEL_READ', ['file_id' => $fileId, 'sheet' => 'Claims&Preauth', 'rows' => count($rows)]);
} else if (env('R_CARE_PRIMARY_KEY_CONSTANT') == $fileData['tpa_id']) {
$rows = $this->readExcelBySheetName($filePath, 'CL');
$this->logClaimDump('info', 'JOB1_EXCEL_READ', ['file_id' => $fileId, 'sheet' => 'CL', 'rows' => count($rows)]);
} else {
$rows = $this->readExcel($filePath);
$this->logClaimDump('info', 'JOB1_EXCEL_READ', ['file_id' => $fileId, 'sheet' => 'active', 'rows' => count($rows)]);
}
if (empty($rows)) {
$this->logClaimDump('error', 'JOB1_EMPTY_EXCEL', ['file_id' => $fileId]);
return $this->failTpaClaimDumpInsert($fileId, 'Excel file contains no data or wrong file upload');
}
if ($this->fileHasStagingRows($fileId)) {
$this->db->transRollback();
$this->logClaimDump('info', 'JOB1_STAGING_EXISTS_SKIP', ['file_id' => $fileId]);
return [
'status' => true,
'message' => 'Claim dump staging already exists for this file; skipped re-insert.',
];
}
if(isset($this->policyNumberMapping[$fileData['tpa_id']]) && !empty($this->policyNumberMapping[$fileData['tpa_id']])){
$policy_number_column = $this->policyNumberMapping[$fileData['tpa_id']];
}else{
$policy_number_column = 'policy_no';
}
$expectedPolicy = trim((string) ($client_policy_data['policy_no'] ?? ''));
foreach ($rows as $index => $row) {
$filePolicy = trim((string) ($row[$policy_number_column] ?? ''));
if ($filePolicy === '' || $filePolicy !== $expectedPolicy) {
$rowNumber = $index + 2; // header is row 1
$this->logClaimDump('error', 'JOB1_POLICY_MISMATCH', [
'file_id' => $fileId,
'excel_row' => $rowNumber,
'expected' => $expectedPolicy,
'found' => $filePolicy,
'column' => $policy_number_column,
]);
return $this->failTpaClaimDumpInsert(
$fileId,
"Policy number mismatch at Excel row {$rowNumber}. Expected '{$expectedPolicy}', found '{$filePolicy}'."
);
}
}
$this->logClaimDump('info', 'JOB1_POLICY_VALIDATED', [
'file_id' => $fileId,
'policy_no' => $expectedPolicy,
'rows_checked' => count($rows),
]);
$tpaInsertData = $this->mapTPAData($rows, $fileId);
$this->logClaimDump('info', 'JOB1_MAPPED', [
'file_id' => $fileId,
'mapped_count' => count($tpaInsertData),
]);
if (empty($tpaInsertData)) {
$this->logClaimDump('error', 'JOB1_NO_MAPPED_ROWS', ['file_id' => $fileId]);
return $this->failTpaClaimDumpInsert($fileId, 'These records already exist in the system.');
}
$return_res = $this->bulkInsertTPATable($tpaInsertData);
if ($return_res !== true) {
$this->logClaimDump('error', 'JOB1_BULK_INSERT_FAILED', ['file_id' => $fileId]);
return $this->failTpaClaimDumpInsert($fileId, 'TPA Import bulk insert failed');
}
// 2. Commit if everything is fine
$this->db->transCommit();
$this->logClaimDump('info', 'JOB1_SUCCESS', [
'file_id' => $fileId,
'record_count' => count($tpaInsertData),
]);
return ['status' => true, 'message' => 'File uploaded successfully', 'record_count' => count($tpaInsertData)];
} catch (\Throwable $e) {
$this->logClaimDump('error', 'JOB1_EXCEPTION', [
'file_id' => $fileId,
'error' => $e->getMessage(),
'line' => $e->getLine(),
'file' => $e->getFile(),
]);
return $this->failTpaClaimDumpInsert($fileId, 'System error : ' . $e->getMessage());
}
}
/**
* Second JOB for insert Ticket Master table after insert the TPA bulk upload success
*/
public function runTicketMasterInsert(array $params): array
{
$file_id = (int) ($params['file_id'] ?? 0);
$this->logClaimDump('info', 'JOB2_START', ['file_id' => $file_id]);
// Outside the Job 2 transaction so a later rollback cannot restore orphan links.
$orphansCleared = $this->clearOrphanDumpTicketLinks($file_id);
if ($orphansCleared > 0) {
$this->logClaimDump('info', 'JOB2_ORPHANS_CLEARED', [
'file_id' => $file_id,
'count' => $orphansCleared,
]);
}
// 1. Start manual transaction
$this->db->transBegin();
try {
$ticketMasterData = $this->mapClaimMasterData($file_id);
$this->logClaimDump('info', 'JOB2_MAPPED', [
'file_id' => $file_id,
'mapping_status' => !empty($ticketMasterData['status']),
'inserts' => count($ticketMasterData['mapped_array'] ?? []),
'status_updates' => count($ticketMasterData['status_update_array'] ?? []),
'rejects_or_links' => count($ticketMasterData['rejected_reason_array'] ?? []),
'already_processed' => !empty($ticketMasterData['already_processed']),
'message' => $ticketMasterData['message'] ?? null,
]);
// Check if mapping failed
if (!$ticketMasterData['status']) {
// Already-processed dump rows (ticket_id linked) must not be deleted as a "failure".
if (!empty($ticketMasterData['already_processed'])) {
$this->db->transCommit();
$this->logClaimDump('info', 'JOB2_ALREADY_PROCESSED', ['file_id' => $file_id]);
return [
'status' => true,
'message' => $ticketMasterData['message'] ?? 'Claim dump already processed for this file.',
];
}
// Job 1 staging is already committed — roll back Job 2 only; keep dump for retry/export.
$this->db->transRollback();
$this->logClaimDump('error', 'JOB2_MAPPING_FAILED', [
'file_id' => $file_id,
'message' => $ticketMasterData['message'] ?? 'mapping failed',
]);
return $ticketMasterData;
}
$message = $orphansCleared > 0
? "Cleared {$orphansCleared} orphan dump ticket_id link(s). "
: '';
$hasExecutedTask = false;
$hasInserts = !empty($ticketMasterData['mapped_array']);
$hasExistingTicketUpdates = !empty($ticketMasterData['status_update_array']);
$hasRejectedReasons = !empty($ticketMasterData['rejected_reason_array']);
$hasExistingTicketLinks = $this->hasExistingTicketLinkUpdates($ticketMasterData['rejected_reason_array'] ?? []);
// Process Mapped Data
if ($hasInserts) {
$this->logClaimDump('info', 'JOB2_INSERT_TICKETS_START', [
'file_id' => $file_id,
'count' => count($ticketMasterData['mapped_array']),
]);
$insert_res = $this->importClaimMaster($ticketMasterData['mapped_array']);
if (!$insert_res) {
return $this->failTicketMasterInsert($file_id, 'Ticket Master Claim bulk insert failed');
}
// Map newly created ticket IDs back to the TPA staging table
if (!$this->updateTicketIdInTPATable($file_id)) {
return $this->failTicketMasterInsert($file_id, 'Updating ticket_id in TPA table failed');
}
if (!$this->recordHistoryForNewDumpTickets($ticketMasterData['mapped_array'], $file_id)) {
return $this->failTicketMasterInsert($file_id, 'Recording ticket history for dump claims failed');
}
$message .= 'Ticket Master Claim bulk insert success. ';
$hasExecutedTask = true;
$this->logClaimDump('info', 'JOB2_INSERT_TICKETS_DONE', ['file_id' => $file_id]);
}
// Update existing tickets: status and/or missing claim_dump_ref_id
if ($hasExistingTicketUpdates) {
$this->logClaimDump('info', 'JOB2_UPDATE_EXISTING_START', [
'file_id' => $file_id,
'count' => count($ticketMasterData['status_update_array']),
]);
$update_status_res = $this->updateExistingTicketStatuses($ticketMasterData['status_update_array']);
if (!$update_status_res) {
return $this->failTicketMasterInsert($file_id, 'Updating existing tickets failed');
}
if (!$this->recordHistoryForExistingTicketUpdates($ticketMasterData['status_update_array'])) {
return $this->failTicketMasterInsert($file_id, 'Recording ticket history for status updates failed');
}
$message .= 'Existing ticket updated successfully. ';
$hasExecutedTask = true;
$this->logClaimDump('info', 'JOB2_UPDATE_EXISTING_DONE', ['file_id' => $file_id]);
}
// Process dump link / reject updates (ticket_id and/or master_reject_reason)
if ($hasRejectedReasons) {
$this->logClaimDump('info', 'JOB2_DUMP_LINK_REJECT_START', [
'file_id' => $file_id,
'count' => count($ticketMasterData['rejected_reason_array']),
'has_links' => $hasExistingTicketLinks,
]);
$update_res = $this->updateTicketMasterRejectedReasonInTPATable(
$this->normalizeDumpLinkOrRejectUpdates($ticketMasterData['rejected_reason_array'])
);
if ($update_res === false) {
return $this->failTicketMasterInsert($file_id, 'Updating rejected reasons failed');
}
if ($hasExistingTicketLinks) {
$message .= 'Existing claim dump ticket_id linked successfully. ';
} elseif (!$hasInserts && !$hasExistingTicketUpdates) {
$message .= 'Those employee or dependent not in our system. ';
} else {
$message .= 'Ticket Master Claim rejected reason updated successfully. ';
}
$hasExecutedTask = true;
$this->logClaimDump('info', 'JOB2_DUMP_LINK_REJECT_DONE', ['file_id' => $file_id]);
}
// If nothing was processed but no error occurred
if (!$hasExecutedTask) {
return $this->failTicketMasterInsert($file_id, 'No data found to process.');
}
// 2. Commit the transaction
$this->db->transCommit();
// Rejection-only runs are still a completed Job 2 — keep dump rows for error export.
$status = $hasInserts || $hasExistingTicketUpdates || $hasExistingTicketLinks || $hasRejectedReasons;
$this->logClaimDump('info', 'JOB2_SUCCESS', [
'file_id' => $file_id,
'status' => $status,
'has_inserts' => $hasInserts,
'has_existing_updates' => $hasExistingTicketUpdates,
'has_links' => $hasExistingTicketLinks,
'has_rejects' => $hasRejectedReasons,
'message' => trim($message),
]);
return ['status' => $status, 'message' => trim($message)];
} catch (\Throwable $th) {
$fileId = (int) ($params['file_id'] ?? 0);
$this->logClaimDump('error', 'JOB2_EXCEPTION', [
'file_id' => $fileId,
'error' => $th->getMessage(),
'line' => $th->getLine(),
'file' => $th->getFile(),
]);
return $this->failTicketMasterInsert(
$fileId,
'System error during Ticket Master Insert: ' . $th->getMessage()
);
}
}
/**
* Clear dump ticket_id (and reject reason) when the linked ticket is missing or inactive.
* Makes orphan rows pending again so Job 2 can recreate tickets.
*
* @return int Number of dump rows reset
*/
protected function clearOrphanDumpTicketLinks(int $fileId): int
{
if ($fileId <= 0) {
return 0;
}
$fileData = $this->claimDumpFileModel->where('id', $fileId)->first();
if (empty($fileData)) {
return 0;
}
$tpaId = (int) ($fileData['tpa_id'] ?? 0);
$tpaTable = $this->tpaTableMapping[$tpaId] ?? null;
if ($tpaTable === null || !in_array($tpaTable, $this->tpaTableMapping, true)) {
return 0;
}
$sql = "UPDATE `{$tpaTable}` d
LEFT JOIN ticket_master tm ON tm.id = d.ticket_id AND tm.is_active = 1
SET d.ticket_id = NULL,
d.master_reject_reason = NULL
WHERE d.file_id = ?
AND d.is_active = 1
AND d.ticket_id IS NOT NULL
AND tm.id IS NULL";
$this->db->query($sql, [$fileId]);
return $this->db->affectedRows();
}
/**
* Clear orphan dump ticket_id links (ticket missing/inactive), then run Job 2
* so those dump rows are inserted into ticket_master again.
*/
public function recreateOrphanDumpTickets(int $fileId): array
{
$this->logClaimDump('info', 'RECREATE_ORPHANS_START', ['file_id' => $fileId]);
if ($fileId <= 0) {
return ['status' => false, 'message' => 'Invalid file id'];
}
$fileData = $this->claimDumpFileModel->where('id', $fileId)->first();
if (empty($fileData)) {
return ['status' => false, 'message' => 'claim_dump_files row not found'];
}
$cleared = $this->clearOrphanDumpTicketLinks($fileId);
$this->logClaimDump('info', 'RECREATE_ORPHANS_CLEARED', [
'file_id' => $fileId,
'cleared' => $cleared,
]);
// Job 2 also clears orphans; running insert/link for now-pending dump rows.
$result = $this->runTicketMasterInsert(['file_id' => $fileId]);
$result['orphans_cleared'] = $cleared;
$this->logClaimDump(
!empty($result['status']) ? 'info' : 'error',
'RECREATE_ORPHANS_DONE',
[
'file_id' => $fileId,
'cleared' => $cleared,
'status' => !empty($result['status']),
'message' => $result['message'] ?? null,
]
);
return $result;
}
/**
* Soft-delete dump staging + tickets created by this upload (admin truncate / undo).
* Does not deactivate existing tickets that were only linked from this dump (different file_id).
*/
public function softTruncateClaimDump(int $fileId): array
{
$this->logClaimDump('info', 'TRUNCATE_START', ['file_id' => $fileId]);
if ($fileId <= 0) {
$this->logClaimDump('error', 'TRUNCATE_INVALID_FILE_ID', ['file_id' => $fileId]);
return ['status' => false, 'message' => 'Invalid file id'];
}
$fileData = $this->claimDumpFileModel->where('id', $fileId)->where('is_active', 1)->first();
if (empty($fileData)) {
$this->logClaimDump('error', 'TRUNCATE_FILE_NOT_FOUND', ['file_id' => $fileId]);
return ['status' => false, 'message' => 'File not found or already truncated'];
}
if (($fileData['status'] ?? '') === 'processing') {
$this->logClaimDump('error', 'TRUNCATE_BLOCKED_PROCESSING', ['file_id' => $fileId]);
return ['status' => false, 'message' => 'Cannot truncate while import is processing'];
}
$tpaId = (int) ($fileData['tpa_id'] ?? 0);
$tpaTable = $this->tpaTableMapping[$tpaId] ?? null;
if ($tpaTable === null || !in_array($tpaTable, $this->tpaTableMapping, true)) {
$this->logClaimDump('error', 'TRUNCATE_UNSUPPORTED_TPA', [
'file_id' => $fileId,
'tpa_id' => $tpaId,
]);
return ['status' => false, 'message' => 'Unsupported TPA for truncate'];
}
$this->db->transBegin();
try {
$ticketIds = $this->db->table('ticket_master')
->select('id, claim_status_id, created_by')
->where('file_id', $fileId)
->where('is_active', 1)
->get()
->getResultArray();
$this->logClaimDump('info', 'TRUNCATE_TICKETS_FOUND', [
'file_id' => $fileId,
'tpa_table' => $tpaTable,
'ticket_count' => count($ticketIds),
]);
// Soft-deactivate dump rows only — keep ticket_id for audit/traceability.
$this->db->table($tpaTable)
->where('file_id', $fileId)
->where('is_active', 1)
->set(['is_active' => 0])
->update();
$dumpAffected = $this->db->affectedRows();
$this->db->table('ticket_master')
->where('file_id', $fileId)
->where('is_active', 1)
->set(['is_active' => 0])
->update();
$ticketAffected = $this->db->affectedRows();
// Avoid Model callbacks that call get_session_userid() (breaks CLI / after output).
$actorId = null;
try {
if (function_exists('get_session_userid')) {
$actorId = get_session_userid();
}
} catch (\Throwable $e) {
$actorId = null;
}
if ($ticketIds !== []) {
$historyRows = [];
$now = date('Y-m-d H:i:s');
foreach ($ticketIds as $ticket) {
$historyRows[] = [
'ticket_id' => $ticket['id'],
'field_name' => 'claim_status_id',
'display_name' => 'Claim Removed (TPA Dump Truncate)',
'old_value' => $ticket['claim_status_id'],
'new_value' => null,
'created_by' => $ticket['created_by'] ?? $actorId,
'created_at' => $now,
'is_active' => 1,
];
}
if ($this->db->table('ticket_history')->insertBatch($historyRows) === false) {
throw new RuntimeException('Failed to write truncate ticket history');
}
}
$this->db->table('claim_dump_files')
->where('id', $fileId)
->update([
'is_active' => 0,
'status' => 'truncated',
'updated_by' => $actorId,
'updated_at' => date('Y-m-d H:i:s'),
]);
if ($this->db->transStatus() === false) {
$this->db->transRollback();
$this->logClaimDump('error', 'TRUNCATE_DB_FAILED', ['file_id' => $fileId]);
return ['status' => false, 'message' => 'Truncate failed'];
}
$this->db->transCommit();
$this->logClaimDump('info', 'TRUNCATE_SUCCESS', [
'file_id' => $fileId,
'dump_rows_deactivated' => $dumpAffected,
'tickets_deactivated' => $ticketAffected,
]);
return [
'status' => true,
'message' => 'Claim dump truncated successfully',
'tickets_deactivated' => count($ticketIds),
];
} catch (\Throwable $e) {
$this->db->transRollback();
$this->logClaimDump('error', 'TRUNCATE_EXCEPTION', [
'file_id' => $fileId,
'error' => $e->getMessage(),
]);
return ['status' => false, 'message' => 'Truncate failed: ' . $e->getMessage()];
}
}
/**
* Soft-delete TPA staging rows and ticket_master rows for a failed Job 1 dump upload.
* Never touches tickets unless the TPA dump table is resolved (avoids orphan ticket_id links).
*/
protected function cleanupClaimDumpData(int $fileId): void
{
if ($fileId <= 0) {
return;
}
$fileData = $this->claimDumpFileModel->where('id', $fileId)->first();
if (empty($fileData)) {
$this->logClaimDump('warning', 'CLEANUP_FILE_NOT_FOUND', ['file_id' => $fileId]);
return;
}
$tpaId = (int) ($fileData['tpa_id'] ?? 0);
$tpaTable = $this->tpaTableMapping[$tpaId] ?? null;
if ($tpaTable === null) {
$this->logClaimDump('error', 'CLEANUP_UNKNOWN_TPA', [
'file_id' => $fileId,
'tpa_id' => $tpaId,
]);
return;
}
$this->db->table($tpaTable)
->where('file_id', $fileId)
->where('is_active', 1)
->set([
'is_active' => 0,
'ticket_id' => null,
])
->update();
$dumpAffected = $this->db->affectedRows();
$this->db->table('ticket_master')
->where('file_id', $fileId)
->where('is_active', 1)
->set(['is_active' => 0])
->update();
$ticketAffected = $this->db->affectedRows();
$this->logClaimDump('info', 'CLEANUP_DONE', [
'file_id' => $fileId,
'tpa_table' => $tpaTable,
'dump_rows_deactivated' => $dumpAffected,
'tickets_deactivated' => $ticketAffected,
]);
}
protected function rollbackAndCleanupClaimDumpData(int $fileId): void
{
$this->logClaimDump('warning', 'JOB1_ROLLBACK_AND_CLEANUP', ['file_id' => $fileId]);
$this->db->transRollback();
$this->cleanupClaimDumpData($fileId);
}
protected function failTpaClaimDumpInsert(int $fileId, string $message): array
{
$this->logClaimDump('error', 'JOB1_FAIL', [
'file_id' => $fileId,
'message' => $message,
]);
$this->rollbackAndCleanupClaimDumpData($fileId);
return ['status' => false, 'message' => $message];
}
/**
* Job 2 failure: roll back this transaction only.
* Do not delete committed Job 1 dump rows so the file can be retried.
*/
protected function failTicketMasterInsert(int $fileId, string $message): array
{
$this->logClaimDump('error', 'JOB2_FAIL', [
'file_id' => $fileId,
'message' => $message,
]);
$this->db->transRollback();
return ['status' => false, 'message' => $message];
}
/**
* Read Excel and return associative rows (header based)
*/
protected function readExcel(string $filePath): array
{
helper('excel_util_helper');
if (!file_exists($filePath)) {
throw new RuntimeException("File not found: {$filePath}");
}
$spreadsheet = IOFactory::load($filePath);
$sheet = $spreadsheet->getActiveSheet();
$rows = $sheet->toArray(null, true, true, true);
// dd($rows);
if (count($rows) < 2) {
return [];
}
// First row is header
$headers = array_shift($rows);
$headers = array_map('trim', $headers);
$data = [];
foreach ($rows as $row) {
if (check_row_is_empty_or_null($row)) {
continue;
}
$item = [];
foreach ($headers as $key => $headerName) {
if ($headerName !== '') {
$item[$headerName] = $row[$key] ?? null;
}
}
$data[] = $item;
}
return $data;
}
/**
* Read Excel By Sheet name and return associative rows (header based)
*/
protected function readExcelBySheetName(string $filePath, string $sheetName): array
{
if (!file_exists($filePath)) {
throw new RuntimeException("File not found: {$filePath}");
}
$spreadsheet = IOFactory::load($filePath);
// Get sheet by name
$sheet = $spreadsheet->getSheetByName($sheetName);
if ($sheet === null) {
throw new RuntimeException("Sheet '{$sheetName}' not found in Excel file");
}
$rows = $sheet->toArray(null, true, true, true);
// Need at least header + one row
if (count($rows) < 2) {
return [];
}
// First row = headers
$headers = array_shift($rows);
$headers = array_map('trim', $headers);
$data = [];
foreach ($rows as $row) {
// Skip completely empty rows
if (!array_filter($row)) {
continue;
}
$item = [];
foreach ($headers as $key => $headerName) {
if ($headerName !== '') {
$item[$headerName] = $row[$key] ?? null;
}
}
$data[] = $item;
}
return $data;
}
/**
* Dublicate check in the ticket_master table records
*/
protected function checkDublicateTicketMasterClaim(array $param): bool
{
return $this->getExistingTicketMasterClaim($param) !== null;
}
/**
* Fetch existing ticket_master record matching claim identity keys.
* Scoped by client_id / client_policy_id when provided to avoid cross-tenant matches.
*/
protected function getExistingTicketMasterClaim(array $param): ?array
{
$ticketMaster = new TicketMasterModel();
$builder = $ticketMaster
->where('doa', $param['doa'])
->where('tpa_no', $param['tpa_no'])
->where('claim_amount', $param['claim_amount'])
->where('emp_code', $param['emp_code'])
->where('is_active', 1);
if (!empty($param['client_id'])) {
$builder->where('client_id', $param['client_id']);
}
if (!empty($param['client_policy_id'])) {
$builder->where('client_policy_id', $param['client_policy_id']);
}
$ticket = $builder->orderBy('id', 'DESC')->first();
return $ticket ?: null;
}
/**
* Update existing ticket_master rows (status and/or claim_dump_ref_id).
*/
protected function updateExistingTicketStatuses(array $statusUpdates): bool
{
if (empty($statusUpdates)) {
return true;
}
$payload = [];
foreach ($statusUpdates as $update) {
$row = $update;
unset($row['old_claim_status_id']);
$payload[] = $row;
}
$ticketMasterModel = new TicketMasterModel();
return $ticketMasterModel->updateBatch($payload, 'id') !== false;
}
/**
* Write ticket_history for newly created dump tickets (by claim_dump_ref_id + file_id).
*/
protected function recordHistoryForNewDumpTickets(array $mappedTickets, int $fileId): bool
{
if (empty($mappedTickets) || $fileId <= 0) {
return true;
}
$refIds = [];
foreach ($mappedTickets as $row) {
if (!empty($row['claim_dump_ref_id'])) {
$refIds[] = (int) $row['claim_dump_ref_id'];
}
}
$refIds = array_values(array_unique($refIds));
if ($refIds === []) {
return true;
}
$tickets = $this->db->table('ticket_master')
->select('id, claim_status_id, created_by')
->where('file_id', $fileId)
->whereIn('claim_dump_ref_id', $refIds)
->where('is_active', 1)
->get()
->getResultArray();
if ($tickets === []) {
return true;
}
$historyRows = [];
foreach ($tickets as $ticket) {
$historyRows[] = [
'ticket_id' => $ticket['id'],
'field_name' => 'claim_status_id',
'display_name' => 'Claim Created (TPA Dump)',
'old_value' => null,
'new_value' => $ticket['claim_status_id'],
'created_by' => $ticket['created_by'] ?? null,
'is_active' => 1,
];
}
$historyModel = new \App\Models\TicketHistoryModel();
return $historyModel->insertBatch($historyRows) !== false;
}
/**
* Write ticket_history when dump import updates an existing ticket's claim_status_id.
*/
protected function recordHistoryForExistingTicketUpdates(array $statusUpdates): bool
{
if (empty($statusUpdates)) {
return true;
}
$historyRows = [];
foreach ($statusUpdates as $update) {
if (empty($update['id']) || !array_key_exists('claim_status_id', $update)) {
continue;
}
$historyRows[] = [
'ticket_id' => $update['id'],
'field_name' => 'claim_status_id',
'display_name' => 'Claim Status Updated (TPA Dump)',
'old_value' => $update['old_claim_status_id'] ?? null,
'new_value' => $update['claim_status_id'],
'created_by' => $update['updated_by'] ?? null,
'is_active' => 1,
];
}
if ($historyRows === []) {
return true;
}
$historyModel = new \App\Models\TicketHistoryModel();
return $historyModel->insertBatch($historyRows) !== false;
}
/**
* True when rejected_reason rows include ticket_id links for existing tickets.
*/
protected function hasExistingTicketLinkUpdates(array $rejectedReasonArray): bool
{
foreach ($rejectedReasonArray as $row) {
if (!empty($row['ticket_id'])) {
return true;
}
}
return false;
}
/**
* Ensure link and reject dump updates share the same columns for updateBatch.
*/
protected function normalizeDumpLinkOrRejectUpdates(array $rows): array
{
$normalized = [];
foreach ($rows as $row) {
if (empty($row['id'])) {
continue;
}
$normalized[] = [
'id' => $row['id'],
'ticket_id' => $row['ticket_id'] ?? null,
'master_reject_reason' => array_key_exists('master_reject_reason', $row)
? $row['master_reject_reason']
: null,
];
}
return $normalized;
}
/**
* If ticket already exists:
* - update claim_status_id when changed
* - set claim_dump_ref_id on ticket_master when null
* - set ticket_id on TPA dump row when null (via rejected_reason_array)
*
* Link updates do not write master_reject_reason (that field is for true rejects only).
*
* Returns true when the row was handled as an existing ticket (caller should continue).
*/
protected function handleExistingTicketStatusUpdate(
?array $existingTicket,
int $newStatusId,
int $dumpRowId,
array &$statusUpdateArray,
array &$rejectedReasonArray
): bool {
if (empty($existingTicket)) {
return false;
}
$ticketId = $existingTicket['id'];
$currentStatusId = (int) ($existingTicket['claim_status_id'] ?? 0);
$statusChanged = $currentStatusId !== (int) $newStatusId;
$claimDumpRefIdMissing = empty($existingTicket['claim_dump_ref_id']);
$ticketUpdate = ['id' => $ticketId];
if ($statusChanged) {
$ticketUpdate['claim_status_id'] = $newStatusId;
$ticketUpdate['old_claim_status_id'] = $currentStatusId;
}
if ($claimDumpRefIdMissing) {
$ticketUpdate['claim_dump_ref_id'] = $dumpRowId;
}
if (count($ticketUpdate) > 1) {
$statusUpdateArray[] = $ticketUpdate;
}
// Dump rows reaching here already have ticket_id NULL; link them to the existing ticket.
$rejectedReasonArray[] = [
'id' => $dumpRowId,
'ticket_id' => $ticketId,
];
return true;
}
/**
* Dublicate check in the TPA specific table records
*/
protected function checkDuplicateTpaClaim(string $table, array $params): bool
{
return $this->db->table($table)
->where($params)
->where('is_active', 1)
->countAllResults() > 0;
}
/**
* Dublicate check in the TPA specific table records
*/
protected function getTpaClaimDumpData(string $table, array $params): array
{
return $this->db
->table($table)
->where('is_active', 1)
->where('file_id', $params['file_id'])
->where('ticket_id IS NULL')
->where('master_reject_reason IS NULL')
->get()
->getResultArray();
}
/**
* Response when no pending dump rows are left for ticket_master mapping.
* Orphan ticket_id links are cleared before mapping (see clearOrphanDumpTicketLinks).
* Remaining linked/rejected rows are treated as already processed (not a wipe failure).
*/
protected function emptyClaimMasterMappingResponse(string $table, int $fileId): array
{
$alreadyProcessed = $this->db->table($table)
->where('file_id', $fileId)
->where('is_active', 1)
->groupStart()
->where('ticket_id IS NOT NULL', null, false)
->orWhere('master_reject_reason IS NOT NULL', null, false)
->groupEnd()
->countAllResults() > 0;
if ($alreadyProcessed) {
return [
'status' => false,
'already_processed' => true,
'message' => 'Claim dump already processed for this file.',
];
}
return ['status' => false, 'message' => 'No data to insert in TICKET MASTER'];
}
/**
* Resolve employee (self) and insured member for a claim dump row.
*/
public function getEmployeeDetails(int $client_id, int $client_policy_id, ?string $emp_code, ?string $relation = null): array
{
if ($emp_code === null || trim($emp_code) === '') {
return [];
}
$emp_code = trim($emp_code);
$relation = $relation !== null ? strtolower(trim($relation)) : '';
$client_policy_data = $this->clientPolicyModel->where('id', $client_policy_id)->first();
if (!empty($client_policy_data)) {
if (!empty($client_policy_data['base_policy'])) {
$client_policy_id = $client_policy_data['base_policy'];
}
}
$EmployeeModel = new EmployeeModel();
// When relation is unknown, still resolve the employee (self); insured join is skipped.
$insuredJoin = "insured.emp_code = employees.emp_code
AND insured.client_id = employees.client_id";
if ($relation !== '') {
$insuredJoin .= " AND LOWER(insured.relationship) = " . $EmployeeModel->db->escape($relation);
} else {
// No usable relation — do not match any insured row.
$insuredJoin .= " AND 1 = 0";
}
$employeeData = $EmployeeModel
->select([
'employees.id AS id',
'employees.email_corporate AS emp_mail',
'employees.mobile AS emp_mobile',
'employees.name AS name',
'employees.emp_code AS emp_code',
// insured employee
'insured.id AS insured_emp_id',
'insured.name AS insured_name'
])
->join(
'employee_polices',
'employees.id = employee_polices.employee_id'
)
->join(
'employees AS insured',
$insuredJoin,
'left'
)
->where('employees.is_active', 1)
->where('employee_polices.is_active', 1)
->where('employee_polices.client_policy_id', $client_policy_id)
->where('employees.client_id', $client_id)
->where('employees.emp_code', $emp_code)
->where('LOWER(employees.relationship)', 'self')
->first();
return $employeeData ?? [];
}
public function checkStatusMapping($statusArray, $statusString)
{
$statusString = trim((string) $statusString);
if ($statusString === '') {
return null;
}
foreach ($statusArray as $key => $value) {
if (strtolower($statusString) === strtolower(trim((string) $key))) {
return $value;
}
}
return null;
}
/**
* Map dump status text to claim_status_id.
* Falls back to 61 when status is empty or not found in the mapping.
*/
protected function resolveClaimStatusId(
array $statusMapping,
?string $statusString,
int $dumpRowId,
array &$rejectedReasonArray
): ?int {
$statusString = trim((string) ($statusString ?? ''));
if ($statusString === '') {
return 61;
}
$statusId = $this->checkStatusMapping($statusMapping, $statusString);
if ($statusId === null) {
return 61;
}
return (int) $statusId;
}
/**
* True when this file already has staging rows in its TPA dump table.
*/
protected function fileHasStagingRows(int $fileId): bool
{
if ($fileId <= 0) {
return false;
}
$fileData = $this->claimDumpFileModel->where('id', $fileId)->first();
if (empty($fileData)) {
return false;
}
$tpaId = (int) ($fileData['tpa_id'] ?? 0);
$tpaTable = $this->tpaTableMapping[$tpaId] ?? null;
if ($tpaTable === null || !in_array($tpaTable, $this->tpaTableMapping, true)) {
return false;
}
return $this->db->table($tpaTable)
->where('file_id', $fileId)
->where('is_active', 1)
->countAllResults() > 0;
}
/**
* Map Excel rows to TPA table structure
*/
abstract protected function mapTPAData(array $rows, $fileId): array;
/**
* Map DB rows to Ticket Master table structure
*/
abstract protected function mapClaimMasterData($fileId): array;
/**
* Insert into TPA-specific table (bulk)
*/
abstract protected function bulkInsertTPATable(array $data): bool;
/**
* Insert into Ticket-Master-specific table (bulk)
*/
abstract protected function importClaimMaster(array $data): bool;
/**
* Update TPA table with ticket_master primary key for a given file.
*/
abstract protected function updateTicketIdInTPATable(int $fileId): bool;
/**
* Update TPA table with ticket_master insert rejected reason
*/
abstract protected function updateTicketMasterRejectedReasonInTPATable(array $data): bool;
}