FIX_LIVE_CLAIMS_ISSUES&TPA_RECON_LOGIC_IMPROVED

This commit is contained in:
velz 2026-04-18 16:00:07 +05:30
parent 7b67c539ee
commit a83ee69937
5 changed files with 420 additions and 50 deletions

View File

@ -626,6 +626,8 @@ $routes->get("downloadFileTableFile/(:any)", "EmployeeController::downloadFileLi
$routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], function ($routes) {
$routes->post('logout', 'RestAuthenticationController::logout');
//Employee login api's
$routes->post("verifyEmployeeNumber", "RestAuthenticationController::verifyEmployeeWithMobileNumber");
$routes->post("getVerifiedUserData", "RestAuthenticationController::getVerifiedUserData");
@ -669,7 +671,7 @@ $routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], fu
$routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratelimit' , 'appSignature', 'authJWT']], function ($routes) {
$routes->post('logout', 'RestAuthenticationController::logout');
// $routes->post('logout', 'RestAuthenticationController::logout');
$routes->post("ecardRequest", "ApiServiceController::ecardRequest");
$routes->get("getWellnessURL", "ApiServiceController::getWellnessURL");

View File

@ -4033,71 +4033,202 @@ class EmployeeController extends AdminController
public function getTPADataVariationReport($file_id, $type = 'download')
{
$file_info = $this->batchFileModel->where('id', $file_id)->find();
$client_id = $file_info[0]['client_id'];
$client_policy_id = $file_info[0]['client_policy_id'];
$TpaApiDataModel = new TpaApiDataModel();
$emp_data_wo_tpa_id = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id);
$fileInfo = $this->batchFileModel->find((int) $file_id);
if (empty($fileInfo)) {
if ($type === 'view') {
return [];
}
if ($type === 'download') {
return false;
}
return $this->respond(['status' => false, 'code' => 404, 'message' => 'File not found', 'data' => []], 200);
}
//loop emp data with TPA data for matches
foreach ($emp_data_wo_tpa_id as $db_key => $db_row)
{
//get TPA API data from table for current DB ep code
$tpa_temp_data = $TpaApiDataModel->select('*')
->where('emp_code', $db_row['emp_code'])
$client_id = (int) ($fileInfo['client_id'] ?? 0);
$client_policy_id = (int) ($fileInfo['client_policy_id'] ?? 0);
// `call_type` controls whether we should force a fresh reconciliation.
// - job => always recompute + persist rec_type snapshot
// - manual => compute only on first call; otherwise use cached rec_type snapshot
// $callType = strtolower((string) ($this->request->getGet('call_type') ?? 'manual'));
$isJobCall = $type === 'job';
$tpaApiDataModel = new TpaApiDataModel();
// Snapshot existence check:
// If any active row already has a non-empty rec_type, we consider this file
// already classified and can safely use cached mode for non-job calls.
$hasRecTypeSnapshot = $tpaApiDataModel->where('file_id', $file_id)
->where('is_active', 1)
->where('rec_type IS NOT NULL', null, false)
->where('rec_type !=', '')
->countAllResults() > 0;
// Compute mode rules:
// 1) job calls always recompute and overwrite rec_type for deterministic refresh.
// 2) first non-job call computes when no snapshot exists.
// Cached mode is used only for second+ non-job calls.
$shouldComputeAndPersist = $isJobCall || !$hasRecTypeSnapshot;
// echo $shouldComputeAndPersist;
// die;
$this->myLogger->logme(
'error',
'TPA variation report mode selected: ' . json_encode([
'file_id' => (int) $file_id,
'call_type' => $isJobCall,
'mode' => $shouldComputeAndPersist ? 'compute' : 'cached',
])
);
$emp_data_wo_tpa_id = [];
$not_in_nhance = [];
if ($shouldComputeAndPersist) {
// Load once and index in memory to avoid N+1 queries during reconciliation.
$allActiveTpaRows = $tpaApiDataModel->select('*')
->where('file_id', $file_id)
->where('is_active', 1)
->findAll();
$match= $this->reconcileDbWithTpa($db_row,$tpa_temp_data);
$emp_data_wo_tpa_id[$db_key]['match'] = $match;
$tpaByEmpCode = [];
foreach ($allActiveTpaRows as $tpaRow) {
$tpaByEmpCode[$tpaRow['emp_code']][] = $tpaRow;
}
$recTypeById = [];
foreach ($allActiveTpaRows as $tpaRow) {
// Default classification for active rows.
// Later loops will overwrite specific rows as `need_to_review` or `not_in_nhance`.
$recTypeById[$tpaRow['id']] = 'matched';
}
$emp_data_wo_tpa_id = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id);
// Reconcile DB records against TPA records and classify records for rec_type updates.
foreach ($emp_data_wo_tpa_id as $db_key => $db_row) {
$tpa_temp_data = $tpaByEmpCode[$db_row['emp_code']] ?? [];
$match = $this->reconcileDbWithTpa($db_row, $tpa_temp_data);
$emp_data_wo_tpa_id[$db_key]['match'] = $match;
if (($match['status'] ?? '') === 'matched') {
$matchedTpaId = (int) ($match['tpa_record']['id'] ?? 0);
if ($matchedTpaId > 0) {
// If compare-fields list has differences, the row must be reviewed.
// Otherwise keep it as matched.
$recTypeById[$matchedTpaId] = empty($match['not_matching']) ? 'matched' : 'need_to_review';
}
} else {
// No relation-level match found for this DB member.
// Mark all candidate TPA rows for the same emp_code as review-required.
foreach ($tpa_temp_data as $candidate) {
$candidateId = (int) ($candidate['id'] ?? 0);
if ($candidateId > 0) {
$recTypeById[$candidateId] = 'need_to_review';
}
}
}
}
$master_emp_codes = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id, [], true);
$master_emp_codes = array_column($master_emp_codes, 'emp_code');
$not_in_nhance = [];
foreach ($allActiveTpaRows as $tpaRow) {
if (!in_array($tpaRow['emp_code'], $master_emp_codes, true)) {
// TPA member not found in Nhance master employee list for this client/policy.
$not_in_nhance[] = $tpaRow;
$recTypeById[(int) $tpaRow['id']] = 'not_in_nhance';
}
}
if (!empty($recTypeById)) {
$updateRows = [];
foreach ($recTypeById as $id => $recType) {
$updateRows[] = [
'id' => (int) $id,
'rec_type' => $recType,
];
}
// Persist snapshot atomically so subsequent non-job calls can use cached mode.
$db = \Config\Database::connect();
$db->transStart();
$tpaApiDataModel->updateBatch($updateRows, 'id');
$db->transComplete();
}
} else {
// echo 'else';die;
// Cached mode:
// Read previously classified rows from rec_type, keep response shape compatible
// with existing UI/export (`mismatch_data` still contains DB row + match payload).
$not_in_nhance = $tpaApiDataModel->select('*')
->where('is_active', 1)
->where('file_id', $file_id)
->where('rec_type', 'not_in_nhance')
->findAll();
$needToReviewRows = $tpaApiDataModel->select('*')
->where('is_active', 1)
->where('file_id', $file_id)
->where('rec_type', 'need_to_review')
->findAll();
$needToReviewByEmpCode = [];
foreach ($needToReviewRows as $row) {
$needToReviewByEmpCode[$row['emp_code']][] = $row;
}
$baseRows = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id);
foreach ($baseRows as $db_row) {
$candidates = $needToReviewByEmpCode[$db_row['emp_code']] ?? [];
if (empty($candidates)) {
continue;
}
$selectedTpa = null;
foreach ($candidates as $candidate) {
// Prefer same-relation row to mimic reconcileDbWithTpa relation matching.
if (strtolower((string) ($candidate['relation'] ?? '')) === strtolower((string) ($db_row['relationship'] ?? ''))) {
$selectedTpa = $candidate;
break;
}
}
if ($selectedTpa === null) {
$selectedTpa = $candidates[0];
}
$db_row['match'] = [
'status' => 'matched',
'tpa_record' => $selectedTpa,
'not_matching' => [],
];
$emp_data_wo_tpa_id[] = $db_row;
}
}
// d($emp_data_wo_tpa_id);
// die();
//not_in_tpa
$tpa_emp_codes = $TpaApiDataModel->select('emp_code')
// Intentionally keep `not_in_tpa` live from current join/query logic
// (as requested) and do not source it from rec_type snapshot.
$tpa_emp_codes = $tpaApiDataModel->select('emp_code')
->where('file_id', $file_id)
->where('is_active', 1)
->groupBy('emp_code')
->findAll();
$tpa_emp_codes = array_column($tpa_emp_codes, 'emp_code');
$not_in_tpa = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id,$tpa_emp_codes);
// d($not_in_tpa);die();
// not_in_nhance
$master_emp_codes = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id,[],true);
$master_emp_codes = array_column($master_emp_codes, 'emp_code');
$not_in_nhance = $TpaApiDataModel->select('*')
->where('is_active',1)
->where('file_id',$file_id)
->whereNotIn('emp_code',$master_emp_codes)
->findAll();
// d($not_in_nhance);die();
$not_in_tpa = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id, $tpa_emp_codes);
if (!empty($not_in_tpa) || !empty($not_in_nhance) || !empty($emp_data_wo_tpa_id)) {
$response = [
'not_in_tpa' => $not_in_tpa,
'not_in_nhance' => $not_in_nhance,
'mismatch_data' => $emp_data_wo_tpa_id,
'not_in_tpa' => $not_in_tpa,
'not_in_nhance' => $not_in_nhance,
'mismatch_data' => $emp_data_wo_tpa_id,
];
if ($type === 'internal') {
return $response;
}else if ($type === 'download') {
if ($type === 'view') {
return $this->respond(['status' => true, 'code' => 200, 'message' => '', 'data' => $response], 200);
} elseif ($type === 'download') {
$this->exportVariationReportExcel($not_in_tpa, $not_in_nhance, $emp_data_wo_tpa_id);
} else {
return $this->respond(['status' => true,'code' => 200,'message' => '','data' => $response,],200);
return $this->respond(['status' => true, 'code' => 200, 'message' => '', 'data' => $response], 200);
}
} else {
if ($type === 'internal') {
if ($type === 'view') {
return [];
}
@ -4105,12 +4236,13 @@ class EmployeeController extends AdminController
return false;
}
return $this->respond(['status' => false,'code' => 202,'message' => 'No data found','data' => [],],200);
return $this->respond(['status' => false, 'code' => 202, 'message' => 'No data found', 'data' => []], 200);
}
}
public function proceedTPADataVariationNextStep($file_id)
{
// echo $file_id;die;
try {
if (empty($file_id)) {
return $this->respond(
@ -4157,6 +4289,24 @@ class EmployeeController extends AdminController
200
);
}
if($generationResult['status'])
{
if($generationResult['data']['file_id'])
{
//initiate update references b/w tpa_api_data and employess
//initiate deleteion if any
}
else
{
//seems inception file id not there just put a log
$this->myLogger->logme(
'error',
'TPA RECON | Inception file id missing after generating employee upload from Not in Nhance data for file_id:'
);
}
}
} elseif ($tab === 'need_to_review') {
// For the "Need to Review" tab, generate a Correction Excel
// using the same overall pipeline as the Not in Nhance implementation.
@ -6171,4 +6321,218 @@ class EmployeeController extends AdminController
return rmdir($dir);
}
/**
* Reconcile TPA API rows with Nhance employee + employee policy records.
*
* Why this function exists:
* - TPA ingestion stores raw member rows in `tpa_api_data`.
* - Downstream processing may need a stable linkage back to the exact
* `employee_polices.id` record that represents that member in Nhance.
* - This method resolves that linkage and writes it into `tpa_api_data.ref`.
*
* Matching strategy:
* 1) Resolve candidate Nhance members by `emp_code` scoped to the same file's
* client and policy context.
* 2) Perform strict exact match on:
* - emp_code
* - name
* - dob
* - gender
* - relation/relationship
* 3) Update `tpa_api_data.ref` only when the exact match is found.
*
* Important behavior:
* - Only active rows are considered on both sides.
* - Already linked rows (`ref` present) are skipped to avoid accidental override.
* - Updates are done in batch and wrapped in DB transaction for consistency.
*
* @param int|string $file_id Batch file id whose TPA rows must be reconciled.
*
* @return array{
* status: bool,
* message: string,
* data: array{
* file_id:int,
* scanned:int,
* matched:int,
* skipped_already_mapped:int,
* unmatched:int
* }
* }
*/
public function reconTpaApiDataWithEmployeepolicies($file_id): array
{
try {
$fileId = (int) $file_id;
if ($fileId <= 0) {
return [
'status' => false,
'message' => 'Invalid file id provided.',
'data' => [],
];
}
$fileInfo = $this->batchFileModel->find($fileId);
if (empty($fileInfo)) {
return [
'status' => false,
'message' => 'Batch file not found.',
'data' => ['file_id' => $fileId],
];
}
$clientId = (int) ($fileInfo['client_id'] ?? 0);
$clientPolicyId = (int) ($fileInfo['client_policy_id'] ?? 0);
if ($clientId <= 0 || $clientPolicyId <= 0) {
return [
'status' => false,
'message' => 'Client/policy context missing for provided file id.',
'data' => ['file_id' => $fileId],
];
}
$db = \Config\Database::connect();
// Pull only active TPA rows for this file. We include `ref` to skip
// rows that are already mapped by any previous reconciliation run.
$tpaRows = $db->table('tpa_api_data')
->select('id, emp_code, name, dob, relation, gender, ref')
->where('file_id', $fileId)
->where('is_active', 1)
->get()
->getResultArray();
if (empty($tpaRows)) {
return [
'status' => true,
'message' => 'No active TPA rows found for reconciliation.',
'data' => [
'file_id' => $fileId,
'scanned' => 0,
'matched' => 0,
'skipped_already_mapped' => 0,
'unmatched' => 0,
],
];
}
// Build Nhance-side candidate pool once, keyed by emp_code.
// Each candidate represents an active employee policy member.
$dbMembers = $db->table('employee_polices ep')
->select('
ep.id AS employee_policy_id,
emp.emp_code,
emp.name,
emp.relationship,
emp.dob,
emp.gender
')
->join('employees emp', 'ep.employee_id = emp.id')
->where('ep.is_active', 1)
->where('ep.status', 'active')
->where('ep.client_policy_id', $clientPolicyId)
->where('emp.client_id', $clientId)
->where('emp.is_active', 1)
->get()
->getResultArray();
$membersByEmpCode = [];
foreach ($dbMembers as $member) {
$membersByEmpCode[$member['emp_code']][] = $member;
}
$updates = [];
$matched = 0;
$skipped = 0;
$unmatched = 0;
$normalize = static function ($value): string {
return strtolower(trim((string) $value));
};
foreach ($tpaRows as $tpaRow) {
$existingRef = trim((string) ($tpaRow['ref'] ?? ''));
if ($existingRef !== '') {
$skipped++;
continue;
}
$empCode = (string) ($tpaRow['emp_code'] ?? '');
$candidates = $membersByEmpCode[$empCode] ?? [];
if (empty($candidates)) {
$unmatched++;
continue;
}
$tName = $normalize($tpaRow['name'] ?? '');
$tRel = $normalize($tpaRow['relation'] ?? '');
$tDob = (string) ($tpaRow['dob'] ?? '');
$tGender = strtoupper(trim((string) ($tpaRow['gender'] ?? '')));
// Strict exact matching only (no fallback):
// emp_code is already scoped via $membersByEmpCode.
// Remaining fields must all match together.
$picked = null;
foreach ($candidates as $candidate) {
$nameOk = $normalize($candidate['name'] ?? '') === $tName;
$relOk = $normalize($candidate['relationship'] ?? '') === $tRel;
$dobOk = (string) ($candidate['dob'] ?? '') === $tDob;
$genOk = strtoupper(trim((string) ($candidate['gender'] ?? ''))) === $tGender;
if ($nameOk && $relOk && $dobOk && $genOk) {
$picked = $candidate;
break;
}
}
if ($picked === null) {
$unmatched++;
continue;
}
$updates[] = [
'id' => (int) $tpaRow['id'],
'ref' => (int) $picked['employee_policy_id'],
];
$matched++;
}
if (!empty($updates)) {
$db->transStart();
$db->table('tpa_api_data')->updateBatch($updates, 'id');
$db->transComplete();
}
$payload = [
'file_id' => $fileId,
'scanned' => count($tpaRows),
'matched' => $matched,
'skipped_already_mapped' => $skipped,
'unmatched' => $unmatched,
];
$this->myLogger->logme('error', 'TPA ref reconciliation completed: ' . json_encode($payload));
return [
'status' => true,
'message' => 'TPA rows reconciled with employee policies successfully.',
'data' => $payload,
];
} catch (\Throwable $e) {
$this->myLogger->logme(
'error',
'Error in reconTpaApiDataWithEmployeepolicies: ' . $e->getMessage(),
['file_id' => $file_id]
);
return [
'status' => false,
'message' => 'Unable to reconcile TPA rows with employee policies.',
'data' => [
'file_id' => (int) $file_id,
'error' => $e->getMessage(),
],
];
}
}
}

View File

@ -1632,7 +1632,7 @@ class EmployeeRestController extends AdminController
if (! empty($client)) {
$client['client_logo'] = base_url() . 'public/uploads/logo/' . $client['client_logo'];
$clientPolicy = $this->clientPolicyModel->where('client_id', $client['id'])
$clientPolicy = $this->clientPolicyModel->where('client_id', $client['id'])
->where('client_branch_id', $this->request->getGet('client_branch_id'))->findAll();
return $this->respond(['status' => 'success', 'code' => 200, 'data' => ['client' => $client, 'client_policy' => $clientPolicy]], 200);
} else {
@ -2413,6 +2413,7 @@ class EmployeeRestController extends AdminController
->select('id,ticket_type, display_name as claim_status')
->where('is_active', 1)
->where('display_name IS NOT NULL OR display_name <> ""')
->whereIn('claim_status_id',[1,2,3,4])
->groupBy('display_name')
->findAll();

View File

@ -506,7 +506,7 @@ class MediAssistApiController extends BaseController
tm.id,
tm.tpa_no as memberId,
tm.tpa_claim_push_reference_no as claimRefNo,
tm.tpa_claim_id,
tm.tpa_claim_id,tm.doa,
tm.claim_number,
cp.policy_no as policyNo,
cp.policy_start_date as startDate,
@ -845,7 +845,7 @@ class MediAssistApiController extends BaseController
tm.id,
tm.tpa_no as memberId,
tm.tpa_claim_push_reference_no as claimRefNo,
tm.doa,
tm.doa,tm.claim_no,
cp.policy_no as policyNo,
cp.policy_start_date as startDate,
cp.policy_end_date as endDate,
@ -1317,6 +1317,8 @@ class MediAssistApiController extends BaseController
//others
'tpa_claim_type' => $value['typE_OF_CLAIM'],
'tpa_ailments' => ($value['ailment'] ?? '') . ' - ' . ($value['ailmenT_DESC'] ?? ''),
'tpa_claim_push_reference_no' => $value['clM_COMP_REFNO'] ?? null,
];

View File

@ -27,7 +27,8 @@ class TpaApiDataModel extends Model
'desc',
'created_by',
'si',
'doj'
'doj',
'rec_type'
];
// protected $useTimestamps = true;