diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index dfcf11a6..bc2fc7b3 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -5,6 +5,7 @@ use App\Controllers\EmployeeServiceController; use App\Controllers\Jobs; use App\Controllers\MediAssistApiController; use App\Controllers\TicketController; +use App\Helpers\JWTToken; use App\Helpers\MailHelper; use App\Helpers\sendMailNotification; use App\Models\AddImgModel; @@ -2547,18 +2548,18 @@ class EmployeeRestController extends AdminController if ($this->request->is('get')) { $client_id = $this->request->getGet('client_id') ?? null; - - $data['claim_status'] = $this->claimStatusModel - ->select('id, ticket_type, display_name as claim_status') - ->where('is_active', 1) - ->groupStart() - ->where('display_name IS NOT NULL') - ->orWhere('display_name <>', '') - ->groupEnd() - ->whereIn('ticket_type', [1,2,3,4]) - ->groupBy('display_name') - ->findAll(); + + $data['claim_status'] = $this->claimStatusModel + ->select('id, ticket_type, display_name as claim_status') + ->where('is_active', 1) + ->groupStart() + ->where('display_name IS NOT NULL') + ->orWhere('display_name <>', '') + ->groupEnd() + ->whereIn('ticket_type', [1, 2, 3, 4]) + ->groupBy('display_name') + ->findAll(); if (!empty($client_id)) { @@ -2566,7 +2567,7 @@ class EmployeeRestController extends AdminController $client_data = $this->clientModel->where('MD5(id)', $client_id)->first(); $client_id = $client_data['id'] ?? null; } - + $client_policy_data = $this->clientPolicyModel ->where('client_id', $client_id) ->where('is_active', 1) @@ -2575,35 +2576,30 @@ class EmployeeRestController extends AdminController $data['ticket_type'] = []; $addedTypes = []; - + foreach ($client_policy_data as $value) { - - if (in_array($value['policy_type_id'], [2,3,4,5]) && !in_array('1', $addedTypes)) { + + if (in_array($value['policy_type_id'], [2, 3, 4, 5]) && !in_array('1', $addedTypes)) { $data['ticket_type'][] = ["ticket_type" => "1", "type_name" => "Claim-GMC"]; $addedTypes[] = '1'; - } elseif (in_array($value['policy_type_id'], [1]) && !in_array('2', $addedTypes)) { $data['ticket_type'][] = ["ticket_type" => "2", "type_name" => "Claim-GPA"]; $addedTypes[] = '2'; - } elseif (in_array($value['policy_type_id'], [6]) && !in_array('3', $addedTypes)) { $data['ticket_type'][] = ["ticket_type" => "3", "type_name" => "EDLI"]; $addedTypes[] = '3'; - } elseif (in_array($value['policy_type_id'], [7]) && !in_array('4', $addedTypes)) { $data['ticket_type'][] = ["ticket_type" => "4", "type_name" => "GTLI"]; $addedTypes[] = '4'; - } elseif (in_array($value['policy_type_id'], [72]) && !in_array('72', $addedTypes)) { $data['ticket_type'][] = ["ticket_type" => "72", "type_name" => "OPD"]; $addedTypes[] = '72'; } } - usort($data['ticket_type'], fn($a,$b) => $a['ticket_type'] <=> $b['ticket_type']); - + usort($data['ticket_type'], fn($a, $b) => $a['ticket_type'] <=> $b['ticket_type']); } else { - + $data['ticket_type'] = [ ["ticket_type" => "1", "type_name" => "Claim-GMC"], ["ticket_type" => "2", "type_name" => "Claim-GPA"], @@ -2634,10 +2630,29 @@ class EmployeeRestController extends AdminController unset($search_data['client_branch_id']); $policy_number = isset($search_data['policy_no']) ? $search_data['policy_no'] : null; + // From JWT (ignore body active_policy_ids to prevent widening access) + $active_policy_ids = JWTToken::getValueFromToken($this->request, 'allowed_active_policies', []); + // print_rr($active_policy_ids); die; + + log_message('error', 'claim search active_policy_ids: ' . json_encode($active_policy_ids, JSON_PRETTY_PRINT)); + if (! is_array($active_policy_ids)) { + $active_policy_ids = []; + } + + if (empty($active_policy_ids)) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => []], 200); + } + $from_date = isset($search_data['from_date']) ? $search_data['from_date'] : null; $to_date = isset($search_data['to_date']) ? $search_data['to_date'] : null; $claim_status_id = isset($search_data['claim_status_id']) ? $search_data['claim_status_id'] : null; - unset($search_data['from_date'], $search_data['to_date'], $search_data['claim_status_id'], $search_data['policy_no']); + unset( + $search_data['from_date'], + $search_data['to_date'], + $search_data['claim_status_id'], + $search_data['policy_no'], + $search_data['active_policy_ids'] + ); $where = []; @@ -2761,6 +2776,10 @@ class EmployeeRestController extends AdminController $builder->where('cp.policy_no', $policy_number); } + if (! empty($active_policy_ids)) { + $builder->whereIn('cp.id', $active_policy_ids); + } + $builder->orderBy('tm.id', 'DESC'); $data = $builder->get()->getResultArray(); diff --git a/app/Controllers/RestAuthenticationController.php b/app/Controllers/RestAuthenticationController.php index 15014b49..8ed847f7 100755 --- a/app/Controllers/RestAuthenticationController.php +++ b/app/Controllers/RestAuthenticationController.php @@ -762,8 +762,65 @@ class RestAuthenticationController extends AdminController if (!empty($HRAccessData) && isset($HRAccessData['allowed_modules'])) { - $decoded = json_decode($HRAccessData['allowed_modules'], true); + $decoded = json_decode($HRAccessData['allowed_modules'], true) ?? []; + $allowedActivePolicies = json_decode($HRAccessData['allowed_active_policies'] ?? '[]', true) ?? []; + if (!is_array($allowedActivePolicies)) { + $allowedActivePolicies = []; + } + + if (empty($allowedActivePolicies) && is_array($decoded)) { + if (isset($decoded['post']) && is_array($decoded['post'])) { + $decoded['post'] = array_values(array_filter( + $decoded['post'], + static fn($module) => (int) $module !== 4 + )); + } elseif (!isset($decoded['pre']) && !isset($decoded['post'])) { + $decoded = array_values(array_filter( + $decoded, + static fn($module) => (int) $module !== 4 + )); + } + $HRAccessData['allowed_modules'] = json_encode($decoded); + } + + $claimsSubMenu = []; + $hasClaimsModule = false; + if (is_array($decoded)) { + if (isset($decoded['post']) && is_array($decoded['post'])) { + $hasClaimsModule = in_array(4, array_map('intval', $decoded['post']), true); + } elseif (!isset($decoded['pre']) && !isset($decoded['post'])) { + $hasClaimsModule = in_array(4, array_map('intval', $decoded), true); + } + } + + if ($hasClaimsModule) { + $claimsSubMenu[] = 'EB'; + + $hasNonEbPolicy = false; + if (!empty($allowedActivePolicies)) { + $db = \Config\Database::connect(); + $policyAllocgRows = $db->table('client_policy') + ->select('policy_type.allocg') + ->join('policy_type', 'policy_type.id = client_policy.policy_type_id', 'left') + ->whereIn('client_policy.id', $allowedActivePolicies) + ->get() + ->getResultArray(); + + foreach ($policyAllocgRows as $policyRow) { + if (($policyRow['allocg'] ?? '') !== 'EB') { + $hasNonEbPolicy = true; + break; + } + } + } + + if ($hasNonEbPolicy) { + $claimsSubMenu[] = 'Non-EB'; + } + } + $getAllhrData[$key]['allowed_modules'] = $decoded; + $getAllhrData[$key]['claims_sub_menu'] = $claimsSubMenu; $HRAccessData['pre_client_id'] = md5($HRAccessData['pre_client_id']); $HRAccessData['post_client_id'] = md5($HRAccessData['post_client_id']); @@ -772,6 +829,7 @@ class RestAuthenticationController extends AdminController } else { $getAllhrData[$key]['allowed_modules'] = []; + $getAllhrData[$key]['claims_sub_menu'] = []; $getAllhrData[$key]['token'] = ""; } diff --git a/app/Helpers/JWTToken.php b/app/Helpers/JWTToken.php index f671197a..f098a504 100755 --- a/app/Helpers/JWTToken.php +++ b/app/Helpers/JWTToken.php @@ -185,5 +185,67 @@ class JWTToken return $decoded; } + /** + * Decode Authorization Bearer JWT into an associative array. + * Returns null when header is missing or token is invalid. + */ + public static function getDecodedTokenArray(?RequestInterface $request = null): ?array + { + $request = $request ?? \Config\Services::request(); + $authHeader = $request->getHeaderLine('Authorization'); + + if ($authHeader === '' || empty(env('JWT_SECRET'))) { + return null; + } + + try { + $result = self::validateJWT($authHeader); + } catch (\Throwable $e) { + return null; + } + + if (($result['status'] ?? false) !== true) { + return null; + } + + return isset($result['decoded']) && is_array($result['decoded']) + ? $result['decoded'] + : null; + } + + /** + * Get any claim value from the request Authorization JWT by key. + * + * Usage: + * JWTToken::getValueFromToken($this->request, 'allowed_active_policies', []); + * JWTToken::getValueFromToken($this->request, 'post_hr_id'); + * + * JSON-encoded string claims (e.g. "[1,2]" / '{"a":1}') are decoded to arrays. + * + * @param mixed $default Returned when token/key is missing or invalid + * @return mixed + */ + public static function getValueFromToken(RequestInterface $request, string $key, $default = null) + { + $decoded = self::getDecodedTokenArray($request); + if ($decoded === null || ! array_key_exists($key, $decoded)) { + return $default; + } + + $value = $decoded[$key]; + + if (is_string($value)) { + $trimmed = trim($value); + if ($trimmed !== '' && ($trimmed[0] === '{' || $trimmed[0] === '[')) { + $parsed = json_decode($trimmed, true); + if (json_last_error() === JSON_ERROR_NONE) { + return $parsed; + } + } + } + + return $value; + } + } diff --git a/tests/smoke_claims_search.php b/tests/smoke_claims_search.php new file mode 100644 index 00000000..e3c923bd --- /dev/null +++ b/tests/smoke_claims_search.php @@ -0,0 +1,793 @@ +systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development')); + +$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; +if (is_file($boot)) { + require_once $boot; +} + +// Smoke encode/decode needs a secret; use a local fallback if JWT_SECRET is unset. +if (empty(env('JWT_SECRET'))) { + $smokeJwtSecret = 'smoke-claims-search-jwt-secret'; + putenv('JWT_SECRET=' . $smokeJwtSecret); + $_ENV['JWT_SECRET'] = $smokeJwtSecret; + $_SERVER['JWT_SECRET'] = $smokeJwtSecret; +} + +use App\Controllers\EmployeeRestController; +use App\Helpers\JWTToken; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\UserAgent; +use Config\Services; +use Firebase\JWT\JWT; + +$pass = 0; +$fail = 0; +$skip = 0; +$results = []; +$notes = []; + +function ok(string $label, bool $cond, string $detail = ''): void +{ + global $pass, $fail, $results; + if ($cond) { + $pass++; + $results[] = '[PASS] ' . $label . ($detail !== '' ? " — {$detail}" : ''); + } else { + $fail++; + $results[] = '[FAIL] ' . $label . ($detail !== '' ? " — {$detail}" : ''); + } +} + +function skip(string $label, string $detail = ''): void +{ + global $skip, $results; + $skip++; + $results[] = '[SKIP] ' . $label . ($detail !== '' ? " — {$detail}" : ''); +} + +function note(string $msg): void +{ + global $notes; + $notes[] = $msg; +} + +/** + * Build a HS512 JWT with allowed_active_policies (does not touch DB). + * + * @param list|string|null $activePolicyIds + */ +function makeHrJwt($activePolicyIds): string +{ + $claim = is_array($activePolicyIds) + ? json_encode(array_values($activePolicyIds)) + : $activePolicyIds; + + $payload = [ + 'id' => 1, + 'post_hr_id' => 1, + 'allowed_active_policies' => $claim, + ]; + + return JWT::encode($payload, (string) env('JWT_SECRET'), 'HS512'); +} + +/** + * Invoke claimsSearch with GET query or POST JSON body. + * + * @param list|string|null|false $tokenPolicyIds false = no Authorization header + * @return array{http:int,body:array,raw:string,error:?string} + */ +function invokeClaimsSearch(string $method, array $payload = [], $tokenPolicyIds = false): array +{ + try { + $uri = new URI('http://localhost/nhance_v2/claimsSearch'); + $bodyString = null; + + if (strtoupper($method) === 'GET') { + if ($payload !== []) { + $uri->setQuery(http_build_query($payload)); + } + } else { + $bodyString = json_encode($payload, JSON_THROW_ON_ERROR); + } + + $request = new IncomingRequest( + config('App'), + $uri, + $bodyString, + new UserAgent() + ); + $request->setMethod(strtolower($method)); + + if (strtoupper($method) === 'GET') { + $request->setGlobal('get', $payload); + } else { + $request->setHeader('Content-Type', 'application/json'); + $request->setGlobal('post', []); + } + + if ($tokenPolicyIds !== false) { + $request->setHeader('Authorization', 'Bearer ' . makeHrJwt($tokenPolicyIds)); + } + + // Fresh controller per call — TicketController caches claimType in ctor + $controller = new EmployeeRestController(); + $response = Services::response(); + $logger = Services::logger(); + $controller->initController($request, $response, $logger); + + $resp = $controller->claimsSearch(); + $raw = $resp->getBody(); + $body = json_decode($raw, true); + + return [ + 'http' => $resp->getStatusCode(), + 'body' => is_array($body) ? $body : [], + 'raw' => (string) $raw, + 'error' => null, + ]; + } catch (Throwable $e) { + return [ + 'http' => 500, + 'body' => [], + 'raw' => '', + 'error' => $e->getMessage(), + ]; + } +} + +function ticketTypeIds(array $ticketTypes): array +{ + return array_map('strval', array_column($ticketTypes, 'ticket_type')); +} + +// ─── Fixtures from live DB ─────────────────────────────────────────────────── + +$db = db_connect(); + +$cliClientId = isset($argv[1]) && ctype_digit((string) $argv[1]) ? (int) $argv[1] : null; + +$sampleTicket = $db->query( + "SELECT tm.id, tm.client_id, tm.client_policy_id, tm.claim_number, tm.claim_status_id, + tm.ticket_type_id, tm.emp_code, tm.emp_name, tm.created_at, tm.is_active, + cp.policy_no + FROM ticket_master tm + LEFT JOIN client_policy cp ON cp.id = tm.client_policy_id + WHERE tm.is_active = 1 AND tm.client_id IS NOT NULL + ORDER BY tm.id DESC + LIMIT 1" +)->getRowArray(); + +if ($cliClientId !== null) { + $override = $db->query( + "SELECT tm.id, tm.client_id, tm.client_policy_id, tm.claim_number, tm.claim_status_id, + tm.ticket_type_id, tm.emp_code, tm.emp_name, tm.created_at, tm.is_active, + cp.policy_no + FROM ticket_master tm + LEFT JOIN client_policy cp ON cp.id = tm.client_policy_id + WHERE tm.is_active = 1 AND tm.client_id = ? + ORDER BY tm.id DESC + LIMIT 1", + [$cliClientId] + )->getRowArray(); + if ($override) { + $sampleTicket = $override; + } +} + +ok('Fixture: active ticket available', !empty($sampleTicket['id']), json_encode($sampleTicket ?: [])); + +$clientId = (int) ($sampleTicket['client_id'] ?? 0); +$clientMd5 = $clientId > 0 ? md5((string) $clientId) : ''; +$policyNo = $sampleTicket['policy_no'] ?? null; +$policyId = $sampleTicket['client_policy_id'] ?? null; +$claimStatusId = $sampleTicket['claim_status_id'] ?? null; +$ticketTypeId = $sampleTicket['ticket_type_id'] ?? null; +$empCode = $sampleTicket['emp_code'] ?? null; +$createdAt = $sampleTicket['created_at'] ?? null; + +$policyTypes = $clientId > 0 + ? $db->query( + 'SELECT DISTINCT policy_type_id FROM client_policy WHERE client_id = ? AND is_active = 1', + [$clientId] + )->getResultArray() + : []; +$policyTypeIds = array_map('intval', array_column($policyTypes, 'policy_type_id')); + +$opdClient = $db->query( + 'SELECT client_id FROM client_policy WHERE policy_type_id = 72 AND is_active = 1 LIMIT 1' +)->getRowArray(); + +$statusRow = $db->query( + "SELECT id, display_name FROM ticket_claim_status + WHERE is_active = 1 AND display_name IS NOT NULL AND display_name <> '' + LIMIT 1" +)->getRowArray(); + +$sameDisplayIds = []; +if (!empty($statusRow['display_name'])) { + $sameDisplayIds = array_map( + 'intval', + array_column( + $db->query( + 'SELECT id FROM ticket_claim_status WHERE is_active = 1 AND display_name = ?', + [$statusRow['display_name']] + )->getResultArray(), + 'id' + ) + ); +} + +$inactiveTicket = $db->query( + 'SELECT id, client_id FROM ticket_master WHERE is_active = 0 AND client_id IS NOT NULL LIMIT 1' +)->getRowArray(); + +$expectedRowKeys = [ + 'id', 'ticket_type_id', 'original_status', 'status', 'cl_type', 'claim_no', + 'claim_status_id', 'emp_name', 'emp_code', 'client_name', 'ticket_created_date', + 'policy_no', 'policy_type', 'client_policy_no', +]; + +echo "========== claimsSearch DEEP SMOKE ==========\n"; +echo 'client_id=' . $clientId . ' md5=' . $clientMd5 . "\n"; +echo 'sample_ticket_id=' . ($sampleTicket['id'] ?? 'n/a') . "\n"; +echo 'policy_types=' . json_encode($policyTypeIds) . "\n\n"; + +// ═══════════════════════════════════════════════════════════════════════════ +// GET — filter masters +// ═══════════════════════════════════════════════════════════════════════════ + +$getNoClient = invokeClaimsSearch('GET'); +$d = $getNoClient['body']['data'] ?? []; +ok( + 'GET no client_id: envelope success/200', + ($getNoClient['body']['status'] ?? '') === 'success' && (int) ($getNoClient['body']['code'] ?? 0) === 200, + json_encode(['status' => $getNoClient['body']['status'] ?? null, 'code' => $getNoClient['body']['code'] ?? null]) +); +ok('GET no client_id: has claim_status', isset($d['claim_status']) && is_array($d['claim_status']) && count($d['claim_status']) > 0, 'count=' . count($d['claim_status'] ?? [])); +ok('GET no client_id: has claim_type', isset($d['claim_type']) && is_array($d['claim_type']), 'keys=' . json_encode(array_keys($d['claim_type'] ?? []))); +ok( + 'GET no client_id: default ticket_type = GMC/GPA/EDLI/GTLI', + ticketTypeIds($d['ticket_type'] ?? []) === ['1', '2', '3', '4'], + json_encode($d['ticket_type'] ?? []) +); +ok( + 'GET no client_id: default ticket_type excludes OPD (72)', + !in_array('72', ticketTypeIds($d['ticket_type'] ?? []), true), + json_encode(ticketTypeIds($d['ticket_type'] ?? [])) +); + +$statusSample = $d['claim_status'][0] ?? []; +ok( + 'GET claim_status rows expose id/ticket_type/claim_status', + isset($statusSample['id'], $statusSample['ticket_type'], $statusSample['claim_status']), + json_encode($statusSample) +); + +$claimType = $d['claim_type'] ?? []; +ok( + 'GET claim_type structure (type 1 + 2)', + isset($claimType[1][1], $claimType[2][1]) + && $claimType[1][1] === 'Main Hospitalization' + && $claimType[2][1] === 'TTD', + json_encode($claimType) +); + +// Numeric client_id — expected ticket types from policy_type_id mapping +if ($clientId > 0) { + $getClient = invokeClaimsSearch('GET', ['client_id' => $clientId]); + $types = ticketTypeIds($getClient['body']['data']['ticket_type'] ?? []); + + $expect = []; + if (array_intersect($policyTypeIds, [2, 3, 4, 5])) { + $expect[] = '1'; // Claim-GMC + } + if (in_array(1, $policyTypeIds, true)) { + $expect[] = '2'; // Claim-GPA + } + if (in_array(6, $policyTypeIds, true)) { + $expect[] = '3'; // EDLI + } + if (in_array(7, $policyTypeIds, true)) { + $expect[] = '4'; // GTLI + } + if (in_array(72, $policyTypeIds, true)) { + $expect[] = '72'; // OPD + } + sort($expect, SORT_NUMERIC); + $got = $types; + sort($got, SORT_NUMERIC); + + ok( + 'GET numeric client_id: ticket_type mapped from active policies', + $got === $expect, + 'expected=' . json_encode($expect) . ' got=' . json_encode($types) . ' policy_types=' . json_encode($policyTypeIds) + ); + ok( + 'GET numeric client_id: ticket_type sorted ascending', + $types === $got, + json_encode($types) + ); + + $getMd5 = invokeClaimsSearch('GET', ['client_id' => $clientMd5]); + ok( + 'GET MD5 client_id resolves same ticket_type as numeric', + ticketTypeIds($getMd5['body']['data']['ticket_type'] ?? []) === $types, + 'md5_types=' . json_encode(ticketTypeIds($getMd5['body']['data']['ticket_type'] ?? [])) + ); +} else { + skip('GET numeric/MD5 client_id mapping', 'no sample client'); +} + +if (!empty($opdClient['client_id'])) { + $getOpd = invokeClaimsSearch('GET', ['client_id' => (int) $opdClient['client_id']]); + ok( + 'GET OPD client includes ticket_type 72', + in_array('72', ticketTypeIds($getOpd['body']['data']['ticket_type'] ?? []), true), + json_encode($getOpd['body']['data']['ticket_type'] ?? []) + ); +} else { + skip('GET OPD ticket_type', 'no active OPD policy_type_id=72 client'); +} + +$getBogusMd5 = invokeClaimsSearch('GET', ['client_id' => str_repeat('a', 32)]); +ok( + 'GET unknown MD5 client_id: still success envelope with empty/partial ticket_type', + ($getBogusMd5['body']['status'] ?? '') === 'success' + && isset($getBogusMd5['body']['data']['ticket_type']), + json_encode([ + 'status' => $getBogusMd5['body']['status'] ?? null, + 'ticket_type' => $getBogusMd5['body']['data']['ticket_type'] ?? null, + ]) +); + +// ═══════════════════════════════════════════════════════════════════════════ +// POST — search +// ═══════════════════════════════════════════════════════════════════════════ + +$postNoToken = invokeClaimsSearch('POST', ['client_id' => $clientId ?: 1], false); +ok( + 'POST without JWT token: failed/404 empty', + $postNoToken['http'] === 200 + && ($postNoToken['body']['status'] ?? '') === 'failed' + && (int) ($postNoToken['body']['code'] ?? 0) === 404 + && ($postNoToken['body']['data'] ?? null) === [], + json_encode($postNoToken['body']) +); + +$postEmptyClaim = invokeClaimsSearch('POST', ['client_id' => $clientId ?: 1], []); +ok( + 'POST JWT allowed_active_policies=[] → failed/404 empty', + ($postEmptyClaim['body']['status'] ?? '') === 'failed' + && (int) ($postEmptyClaim['body']['code'] ?? 0) === 404 + && ($postEmptyClaim['body']['data'] ?? null) === [], + json_encode($postEmptyClaim['body']) +); + +$postNullClaim = invokeClaimsSearch('POST', ['client_id' => $clientId ?: 1], null); +ok( + 'POST JWT allowed_active_policies=null → failed/404 empty', + ($postNullClaim['body']['status'] ?? '') === 'failed' + && (int) ($postNullClaim['body']['code'] ?? 0) === 404 + && ($postNullClaim['body']['data'] ?? null) === [], + json_encode($postNullClaim['body']) +); + +// Helper can parse JSON-string claim shape used in HR tokens +$helperJwt = makeHrJwt([(int) ($policyId ?: 1)]); +$helperReq = new IncomingRequest( + config('App'), + new URI('http://localhost/nhance_v2/claimsSearch'), + null, + new UserAgent() +); +$helperReq->setHeader('Authorization', 'Bearer ' . $helperJwt); +ok( + 'JWTToken::getValueFromToken(request, key) parses JSON claim', + JWTToken::getValueFromToken($helperReq, 'allowed_active_policies', []) === [(int) ($policyId ?: 1)], + json_encode(JWTToken::getValueFromToken($helperReq, 'allowed_active_policies', [])) +); +ok( + 'JWTToken::getValueFromToken returns default for missing key', + JWTToken::getValueFromToken($helperReq, 'not_a_real_claim', 'missing') === 'missing', + (string) JWTToken::getValueFromToken($helperReq, 'not_a_real_claim', 'missing') +); + +if ($clientId > 0 && !empty($policyId)) { + $tokenPolicies = [(int) $policyId]; + $basePost = ['client_id' => $clientId]; + + $byClient = invokeClaimsSearch('POST', $basePost, $tokenPolicies); + $rows = $byClient['body']['data'] ?? []; + $allMatch = is_array($rows) && count($rows) > 0; + if ($allMatch) { + $ids = array_column($rows, 'id'); + $mismatch = $db->query( + 'SELECT COUNT(*) AS c FROM ticket_master WHERE id IN (' . implode(',', array_map('intval', $ids)) . ') AND client_id <> ?', + [$clientId] + )->getRowArray(); + ok( + 'POST client_id: all returned tickets belong to client', + (int) ($mismatch['c'] ?? 1) === 0, + 'rows=' . count($rows) . ' mismatches=' . ($mismatch['c'] ?? '?') + ); + ok( + 'POST client_id: includes fixture ticket', + in_array((string) $sampleTicket['id'], array_map('strval', $ids), true) + || in_array((int) $sampleTicket['id'], array_map('intval', $ids), true), + 'fixture_id=' . $sampleTicket['id'] + ); + + $missingKeys = []; + foreach ($expectedRowKeys as $k) { + if (!array_key_exists($k, $rows[0])) { + $missingKeys[] = $k; + } + } + ok('POST result row has expected keys', $missingKeys === [], 'missing=' . json_encode($missingKeys)); + ok( + 'POST status is UPPER display/fallback', + is_string($rows[0]['status'] ?? null) + && ($rows[0]['status'] === '' || $rows[0]['status'] === strtoupper($rows[0]['status'])), + 'status=' . ($rows[0]['status'] ?? 'null') + ); + ok( + 'POST cl_type present (Reimbursement fallback or tpa_claim_type)', + isset($rows[0]['cl_type']) && $rows[0]['cl_type'] !== '', + 'cl_type=' . ($rows[0]['cl_type'] ?? 'null') + ); + ok( + 'POST with active_policy_ids: ordered by id DESC', + count($rows) < 2 || (int) $rows[0]['id'] >= (int) $rows[1]['id'], + 'first=' . ($rows[0]['id'] ?? '?') . ' second=' . ($rows[1]['id'] ?? 'n/a') + ); + } else { + ok('POST client_id: returns data for known client', false, json_encode($byClient['body'])); + } + + $byMd5 = invokeClaimsSearch('POST', [ + 'client_id' => $clientMd5, + ], $tokenPolicies); + ok( + 'POST MD5 client_id: same row count as numeric', + count($byMd5['body']['data'] ?? []) === count($rows), + 'numeric=' . count($rows) . ' md5=' . count($byMd5['body']['data'] ?? []) + ); + + // Body active_policy_ids must NOT widen access beyond token + $bodyWiden = invokeClaimsSearch('POST', [ + 'client_id' => $clientId, + 'active_policy_ids' => [(int) $policyId, 999999999], + ], $tokenPolicies); + ok( + 'POST ignores body active_policy_ids (token is source of truth)', + count($bodyWiden['body']['data'] ?? []) === count($rows), + 'token_only=' . count($rows) . ' with_body_extra=' . count($bodyWiden['body']['data'] ?? []) + ); + + // Date range — day of fixture created_at + if (!empty($createdAt)) { + $day = date('d-m-Y', strtotime($createdAt)); + $byDate = invokeClaimsSearch('POST', $basePost + [ + 'from_date' => $day, + 'to_date' => $day, + ], $tokenPolicies); + $dateRows = $byDate['body']['data'] ?? []; + $idsOnDay = array_map('strval', array_column($dateRows, 'id')); + ok( + 'POST from_date+to_date includes fixture on that day', + in_array((string) $sampleTicket['id'], $idsOnDay, true), + 'day=' . $day . ' count=' . count($dateRows) + ); + + $onlyFrom = invokeClaimsSearch('POST', $basePost + [ + 'from_date' => $day, + ], $tokenPolicies); + // Code requires BOTH from_date AND to_date — partial should not apply date filter + ok( + 'POST from_date alone does NOT restrict by date (needs both)', + count($onlyFrom['body']['data'] ?? []) >= count($dateRows), + 'with_both=' . count($dateRows) . ' from_only=' . count($onlyFrom['body']['data'] ?? []) + ); + + $farPast = invokeClaimsSearch('POST', $basePost + [ + 'from_date' => '01-01-2000', + 'to_date' => '02-01-2000', + ], $tokenPolicies); + ok( + 'POST date range with no tickets → failed/404 empty', + ($farPast['body']['status'] ?? '') === 'failed' + && (int) ($farPast['body']['code'] ?? 0) === 404 + && ($farPast['body']['data'] ?? null) === [], + json_encode([ + 'status' => $farPast['body']['status'] ?? null, + 'code' => $farPast['body']['code'] ?? null, + 'data' => $farPast['body']['data'] ?? null, + ]) + ); + } + + if (!empty($ticketTypeId)) { + $byType = invokeClaimsSearch('POST', $basePost + [ + 'ticket_type_id' => $ticketTypeId, + ], $tokenPolicies); + $typeRows = $byType['body']['data'] ?? []; + $badType = 0; + foreach ($typeRows as $r) { + if ((string) ($r['ticket_type_id'] ?? '') !== (string) $ticketTypeId) { + $badType++; + } + } + ok( + 'POST dynamic where ticket_type_id filters correctly', + count($typeRows) > 0 && $badType === 0, + 'ticket_type_id=' . $ticketTypeId . ' rows=' . count($typeRows) . ' bad=' . $badType + ); + } + + if (!empty($empCode)) { + $byEmp = invokeClaimsSearch('POST', $basePost + [ + 'emp_code' => $empCode, + ], $tokenPolicies); + $empRows = $byEmp['body']['data'] ?? []; + $badEmp = 0; + foreach ($empRows as $r) { + if ((string) ($r['emp_code'] ?? '') !== (string) $empCode) { + $badEmp++; + } + } + ok( + 'POST dynamic where emp_code filters correctly', + count($empRows) > 0 && $badEmp === 0, + 'emp_code=' . $empCode . ' rows=' . count($empRows) . ' bad=' . $badEmp + ); + } + + // Empty / zero values must be ignored in dynamic where + $ignoreZero = invokeClaimsSearch('POST', $basePost + [ + 'ticket_type_id' => 0, + 'claim_type' => '', + 'priority' => '0', + ], $tokenPolicies); + ok( + 'POST empty/0 dynamic fields ignored (count ≈ client-only)', + count($ignoreZero['body']['data'] ?? []) === count($rows), + 'client_only=' . count($rows) . ' with_empties=' . count($ignoreZero['body']['data'] ?? []) + ); + + // client_branch_id is explicitly unset — must not break search + $withBranch = invokeClaimsSearch('POST', $basePost + [ + 'client_branch_id' => 999999, + ], $tokenPolicies); + ok( + 'POST client_branch_id is ignored (unset before where)', + count($withBranch['body']['data'] ?? []) === count($rows), + 'without=' . count($rows) . ' with_branch=' . count($withBranch['body']['data'] ?? []) + ); + + if (!empty($policyNo)) { + $byPolicy = invokeClaimsSearch('POST', $basePost + [ + 'policy_no' => $policyNo, + ], $tokenPolicies); + $policyRows = $byPolicy['body']['data'] ?? []; + $badPol = 0; + foreach ($policyRows as $r) { + if (($r['client_policy_no'] ?? null) !== null + && (string) $r['client_policy_no'] !== (string) $policyNo + && (string) ($r['policy_no'] ?? '') !== (string) $policyNo + ) { + if ((string) ($r['client_policy_no'] ?? '') !== (string) $policyNo) { + $badPol++; + } + } + } + ok( + 'POST policy_no filters via client_policy.policy_no', + count($policyRows) > 0 && $badPol === 0, + 'policy_no=' . $policyNo . ' rows=' . count($policyRows) . ' bad=' . $badPol + ); + } else { + skip('POST policy_no filter', 'fixture has no policy_no'); + } + + $byActive = invokeClaimsSearch('POST', $basePost, $tokenPolicies); + ok( + 'POST token active_policy_ids does not fatal', + $byActive['error'] === null, + $byActive['error'] ?? 'ok' + ); + $activeRows = $byActive['body']['data'] ?? []; + $badActive = 0; + foreach ($activeRows as $r) { + if ((string) ($r['client_policy_id'] ?? '') !== (string) $policyId) { + $badActive++; + } + } + ok( + 'POST token allowed_active_policies restricts to those client_policy ids', + $byActive['error'] === null && count($activeRows) > 0 && $badActive === 0, + 'policy_id=' . $policyId . ' rows=' . count($activeRows) . ' bad=' . $badActive + ); + + $byMulti = invokeClaimsSearch('POST', $basePost, [(int) $policyId, 0]); + ok( + 'POST token with zero id still returns fixture policy rows', + $byMulti['error'] === null && count($byMulti['body']['data'] ?? []) >= count($activeRows), + 'single=' . count($activeRows) . ' multi=' . count($byMulti['body']['data'] ?? []) + ); + + if (!empty($claimStatusId) || !empty($statusRow['id'])) { + $statusFilterId = (int) ($statusRow['id'] ?? $claimStatusId); + $byStatus = invokeClaimsSearch('POST', $basePost + [ + 'claim_status_id' => $statusFilterId, + ], $tokenPolicies); + $statusRows = $byStatus['body']['data'] ?? []; + + $allowed = $sameDisplayIds ?: [(int) $statusFilterId]; + $badSt = 0; + foreach ($statusRows as $r) { + if (!in_array((int) ($r['claim_status_id'] ?? 0), $allowed, true)) { + $badSt++; + } + } + ok( + 'POST claim_status_id expands via display_name helper', + $badSt === 0, + 'filter_id=' . $statusFilterId + . ' allowed=' . json_encode($allowed) + . ' rows=' . count($statusRows) + . ' bad=' . $badSt + . ' display=' . ($statusRow['display_name'] ?? '') + ); + + $ctrl = new EmployeeRestController(); + $expanded = $ctrl->getTicketClaimStatusIdBasedOnTheDisplayName($statusFilterId); + sort($expanded); + $expectedExpanded = $allowed; + sort($expectedExpanded); + ok( + 'Helper getTicketClaimStatusIdBasedOnTheDisplayName matches DB', + array_map('intval', $expanded) === array_map('intval', $expectedExpanded), + 'helper=' . json_encode($expanded) . ' db=' . json_encode($expectedExpanded) + ); + } + + // Combined filters should be AND + if (!empty($ticketTypeId) && !empty($createdAt)) { + $day = date('d-m-Y', strtotime($createdAt)); + $combined = invokeClaimsSearch('POST', $basePost + [ + 'ticket_type_id' => $ticketTypeId, + 'from_date' => $day, + 'to_date' => $day, + 'emp_code' => $empCode, + ], $tokenPolicies); + $cRows = $combined['body']['data'] ?? []; + $okCombined = true; + foreach ($cRows as $r) { + if ((string) ($r['ticket_type_id'] ?? '') !== (string) $ticketTypeId) { + $okCombined = false; + } + if ($empCode && (string) ($r['emp_code'] ?? '') !== (string) $empCode) { + $okCombined = false; + } + } + ok( + 'POST combined filters use AND semantics', + $okCombined && ( + in_array((string) $sampleTicket['id'], array_map('strval', array_column($cRows, 'id')), true) + || count($cRows) === 0 + ), + 'rows=' . count($cRows) . ' fixture_in=' . (in_array((string) $sampleTicket['id'], array_map('strval', array_column($cRows, 'id')), true) ? 'yes' : 'no') + ); + } + + // Unknown client → empty failed + $unknown = invokeClaimsSearch('POST', [ + 'client_id' => 999999999, + ], $tokenPolicies); + ok( + 'POST unknown client_id → failed/404 empty data', + ($unknown['body']['status'] ?? '') === 'failed' + && (int) ($unknown['body']['code'] ?? 0) === 404 + && ($unknown['body']['data'] ?? null) === [], + json_encode($unknown['body']) + ); + + // Inactive tickets must never appear + if (!empty($inactiveTicket['id'])) { + $huntInactive = invokeClaimsSearch('POST', [ + 'client_id' => (int) $inactiveTicket['client_id'], + ], $tokenPolicies); + $inactiveFound = false; + foreach ($huntInactive['body']['data'] ?? [] as $r) { + if ((int) ($r['id'] ?? 0) === (int) $inactiveTicket['id']) { + $inactiveFound = true; + break; + } + } + ok( + 'POST excludes is_active=0 tickets', + !$inactiveFound, + 'inactive_id=' . $inactiveTicket['id'] + ); + } else { + skip('POST inactive exclusion', 'no inactive ticket fixture'); + } + + // SQL-ish junk in dates — should not fatal + $inj = invokeClaimsSearch('POST', $basePost + [ + 'from_date' => "01-01-2020' OR '1'='1", + 'to_date' => '31-12-2020', + ], $tokenPolicies); + ok( + 'POST crafted from_date does not fatal', + isset($inj['body']['status']) || $inj['error'] !== null, + 'status=' . ($inj['body']['status'] ?? 'null') . ' http=' . $inj['http'] . ' error=' . ($inj['error'] ?? 'null') + ); + if (($inj['body']['status'] ?? '') === 'success' && count($inj['body']['data'] ?? []) > 0) { + note('SECURITY: date filter is string-interpolated; crafted from_date still returned rows — review SQL injection risk'); + } + + // Dynamic where column injection attempt — unknown keys become tm. + $bogusCol = invokeClaimsSearch('POST', $basePost + [ + 'definitely_not_a_col' => 'x', + ], $tokenPolicies); + ok( + 'POST unknown dynamic field handled without uncaught fatal', + true, + 'error=' . ($bogusCol['error'] ?? 'null') + . ' status=' . ($bogusCol['body']['status'] ?? 'null') + . ' code=' . ($bogusCol['body']['code'] ?? 'null') + ); + if ($bogusCol['error'] !== null + || str_contains(strtolower($bogusCol['raw']), 'unknown column') + || str_contains(strtolower($bogusCol['raw']), 'sqlstate') + ) { + note('BUG: unknown JSON keys are passed into WHERE as tm. — ' . ($bogusCol['error'] ?: 'SQL error in response')); + } +} else { + skip('POST client-scoped filters', 'no sample client/policy'); +} + +// ─── Report ───────────────────────────────────────────────────────────────── + +ob_end_clean(); + +foreach ($results as $line) { + echo $line . PHP_EOL; +} + +if ($notes !== []) { + echo PHP_EOL . '──── Notes / findings ────' . PHP_EOL; + foreach ($notes as $n) { + echo '- ' . $n . PHP_EOL; + } +} + +echo PHP_EOL . "Summary: PASS={$pass} FAIL={$fail} SKIP={$skip}" . PHP_EOL; +exit($fail > 0 ? 1 : 0); diff --git a/tests/smoke_verified_hr_claims_submenu.php b/tests/smoke_verified_hr_claims_submenu.php new file mode 100644 index 00000000..8add4ae4 --- /dev/null +++ b/tests/smoke_verified_hr_claims_submenu.php @@ -0,0 +1,312 @@ +systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development')); + +$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; +if (is_file($boot)) { + require_once $boot; +} + +if (empty(env('JWT_SECRET'))) { + $smokeJwtSecret = 'smoke-verified-hr-claims-submenu-jwt-secret'; + putenv('JWT_SECRET=' . $smokeJwtSecret); + $_ENV['JWT_SECRET'] = $smokeJwtSecret; + $_SERVER['JWT_SECRET'] = $smokeJwtSecret; +} + +use App\Controllers\RestAuthenticationController; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\UserAgent; +use Config\Services; + +$pass = 0; +$fail = 0; +$skip = 0; +$results = []; + +function ok(string $label, bool $cond, string $detail = ''): void +{ + global $pass, $fail, $results; + if ($cond) { + $pass++; + $results[] = '[PASS] ' . $label . ($detail !== '' ? " — {$detail}" : ''); + } else { + $fail++; + $results[] = '[FAIL] ' . $label . ($detail !== '' ? " — {$detail}" : ''); + } +} + +function skip(string $label, string $detail = ''): void +{ + global $skip, $results; + $skip++; + $results[] = '[SKIP] ' . $label . ($detail !== '' ? " — {$detail}" : ''); +} + +/** + * @return array{http:int,body:array,raw:string,error:?string} + */ +function invokeGetVerifiedHrData(array $payload): array +{ + try { + $uri = new URI('http://localhost/nhance_v2/employeeRest/getVerifiedHrData'); + $bodyString = json_encode($payload, JSON_THROW_ON_ERROR); + + $request = new IncomingRequest( + config('App'), + $uri, + $bodyString, + new UserAgent() + ); + $request->setMethod('post'); + $request->setHeader('Content-Type', 'application/json'); + $request->setHeader('User-Agent', 'SmokeTest/1.0'); + $request->setGlobal('post', []); + + Services::injectMock('request', $request); + + $controller = new RestAuthenticationController(); + $response = Services::response(); + $logger = Services::logger(); + $controller->initController($request, $response, $logger); + + // Local DB user may lack INSERT on auth_history; stub so smoke can focus on claims logic. + $authHistoryStub = new class { + public function insert($data = null, bool $returnID = true) + { + return true; + } + }; + $ref = new ReflectionProperty(RestAuthenticationController::class, 'authHistoryModel'); + $ref->setAccessible(true); + $ref->setValue($controller, $authHistoryStub); + + $resp = $controller->getVerifiedHrData(); + $raw = $resp->getBody(); + $body = json_decode($raw, true); + + return [ + 'http' => $resp->getStatusCode(), + 'body' => is_array($body) ? $body : [], + 'raw' => (string) $raw, + 'error' => null, + ]; + } catch (Throwable $e) { + return [ + 'http' => 500, + 'body' => [], + 'raw' => '', + 'error' => $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine(), + ]; + } +} + +function findHrRowForClient(array $dataRows, int $clientId, int $hrId): ?array +{ + foreach ($dataRows as $row) { + if ((int) ($row['client_id'] ?? 0) === $clientId && (int) ($row['id'] ?? 0) === $hrId) { + return $row; + } + } + + return $dataRows[0] ?? null; +} + +function postModulesContainClaims(?array $allowedModules): bool +{ + if (!is_array($allowedModules)) { + return false; + } + if (isset($allowedModules['post']) && is_array($allowedModules['post'])) { + return in_array(4, array_map('intval', $allowedModules['post']), true); + } + if (!isset($allowedModules['pre']) && !isset($allowedModules['post'])) { + return in_array(4, array_map('intval', $allowedModules), true); + } + + return false; +} + +$db = db_connect(); + +$fixture = $db->query( + "SELECT hac.id AS hac_id, hac.post_hr_id, hac.post_client_id, hac.post_branch_id, + hac.allowed_modules, hac.allowed_active_policies, + lc.mobile, lc.email, lc.otp AS original_otp + FROM hr_access_control hac + INNER JOIN level_contacts lc ON lc.id = hac.post_hr_id + WHERE hac.is_active = 1 + AND lc.is_active = 1 + AND lc.contact_type = 'client' + AND lc.mobile IS NOT NULL + AND lc.mobile <> '' + AND hac.allowed_modules LIKE '%4%' + AND hac.allowed_active_policies IS NOT NULL + AND hac.allowed_active_policies <> '[]' + AND hac.allowed_active_policies <> '' + ORDER BY hac.id DESC + LIMIT 1" +)->getRowArray(); + +$nonEbPolicy = $db->query( + "SELECT cp.id, pt.allocg + FROM client_policy cp + JOIN policy_type pt ON pt.id = cp.policy_type_id + WHERE pt.allocg <> 'EB' + AND cp.is_active = 1 + ORDER BY cp.id DESC + LIMIT 1" +)->getRowArray(); + +if (!$fixture) { + skip('Fixture HR with claims + policies', 'none found'); + foreach ($results as $line) { + echo $line . PHP_EOL; + } + echo PHP_EOL . "PASS={$pass} FAIL={$fail} SKIP={$skip}" . PHP_EOL; + exit($fail > 0 ? 1 : 0); +} + +ok('Fixture HR with claims module', true, "hac_id={$fixture['hac_id']} hr={$fixture['post_hr_id']} mobile={$fixture['mobile']}"); + +$otp = '999111'; +$originalPolicies = $fixture['allowed_active_policies']; +$originalModules = $fixture['allowed_modules']; +$originalOtp = $fixture['original_otp']; +$hrId = (int) $fixture['post_hr_id']; +$clientId = (int) $fixture['post_client_id']; +$hacId = (int) $fixture['hac_id']; +$mobile = $fixture['mobile']; + +$restore = static function () use ($db, $hacId, $hrId, $originalPolicies, $originalModules, $originalOtp): void { + $db->table('hr_access_control')->where('id', $hacId)->update([ + 'allowed_active_policies' => $originalPolicies, + 'allowed_modules' => $originalModules, + ]); + $db->table('level_contacts')->where('id', $hrId)->update([ + 'otp' => $originalOtp, + ]); +}; + +register_shutdown_function($restore); + +// ── Case 1: EB-only policies → claims_sub_menu = ["EB"] ────────────────────── + +$db->table('level_contacts')->where('id', $hrId)->update(['otp' => $otp]); + +$case1 = invokeGetVerifiedHrData([ + 'mobile_no' => $mobile, + 'otp' => $otp, +]); + +ok('Case1 HTTP/error free', $case1['error'] === null, (string) ($case1['error'] ?? 'ok')); +ok('Case1 status success', ($case1['body']['status'] ?? '') === 'success', json_encode($case1['body']['message'] ?? $case1['body']['status'] ?? null)); + +$case1Rows = $case1['body']['data'] ?? []; +$case1Row = is_array($case1Rows) ? findHrRowForClient($case1Rows, $clientId, $hrId) : null; + +ok('Case1 row found', is_array($case1Row), 'client_id=' . $clientId); +ok( + 'Case1 claims still present', + postModulesContainClaims($case1Row['allowed_modules'] ?? null), + json_encode($case1Row['allowed_modules'] ?? null) +); +ok( + 'Case1 claims_sub_menu is EB only', + ($case1Row['claims_sub_menu'] ?? null) === ['EB'], + json_encode($case1Row['claims_sub_menu'] ?? null) +); + +// ── Case 2: include Non-EB policy → claims_sub_menu = ["EB","Non-EB"] ───────── + +if (!$nonEbPolicy) { + skip('Case2 Non-EB policy fixture', 'no non-EB client_policy found'); +} else { + $mixedPolicies = json_decode($originalPolicies, true) ?: []; + $mixedPolicies[] = (int) $nonEbPolicy['id']; + $mixedPolicies = array_values(array_unique(array_map('intval', $mixedPolicies))); + + $db->table('hr_access_control')->where('id', $hacId)->update([ + 'allowed_active_policies' => json_encode($mixedPolicies), + ]); + $db->table('level_contacts')->where('id', $hrId)->update(['otp' => $otp]); + + $case2 = invokeGetVerifiedHrData([ + 'mobile_no' => $mobile, + 'otp' => $otp, + ]); + + ok('Case2 HTTP/error free', $case2['error'] === null, (string) ($case2['error'] ?? 'ok')); + ok('Case2 status success', ($case2['body']['status'] ?? '') === 'success', json_encode($case2['body']['status'] ?? null)); + + $case2Rows = $case2['body']['data'] ?? []; + $case2Row = is_array($case2Rows) ? findHrRowForClient($case2Rows, $clientId, $hrId) : null; + + ok( + 'Case2 claims_sub_menu has EB + Non-EB', + ($case2Row['claims_sub_menu'] ?? null) === ['EB', 'Non-EB'], + json_encode($case2Row['claims_sub_menu'] ?? null) . ' nonEbPolicy=' . $nonEbPolicy['id'] . '/' . $nonEbPolicy['allocg'] + ); +} + +// ── Case 3: empty policies → claims stripped, claims_sub_menu = [] ──────────── + +$db->table('hr_access_control')->where('id', $hacId)->update([ + 'allowed_active_policies' => '[]', +]); +$db->table('level_contacts')->where('id', $hrId)->update(['otp' => $otp]); + +$case3 = invokeGetVerifiedHrData([ + 'mobile_no' => $mobile, + 'otp' => $otp, +]); + +ok('Case3 HTTP/error free', $case3['error'] === null, (string) ($case3['error'] ?? 'ok')); +ok('Case3 status success', ($case3['body']['status'] ?? '') === 'success', json_encode($case3['body']['status'] ?? null)); + +$case3Rows = $case3['body']['data'] ?? []; +$case3Row = is_array($case3Rows) ? findHrRowForClient($case3Rows, $clientId, $hrId) : null; + +ok( + 'Case3 claims module removed', + !postModulesContainClaims($case3Row['allowed_modules'] ?? null), + json_encode($case3Row['allowed_modules'] ?? null) +); +ok( + 'Case3 claims_sub_menu empty', + ($case3Row['claims_sub_menu'] ?? null) === [], + json_encode($case3Row['claims_sub_menu'] ?? null) +); + +$restore(); + +ob_end_clean(); + +foreach ($results as $line) { + echo $line . PHP_EOL; +} +echo PHP_EOL . "PASS={$pass} FAIL={$fail} SKIP={$skip}" . PHP_EOL; + +exit($fail > 0 ? 1 : 0);