MERGE_TEST_BUG_FIXES

This commit is contained in:
Ubuntu 2026-07-15 11:18:14 +05:30
commit 19fe1bf3b1
5 changed files with 1268 additions and 24 deletions

View File

@ -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();

View File

@ -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'] = "";
}

View File

@ -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;
}
}

View File

@ -0,0 +1,793 @@
<?php
/**
* Deep smoke test: EmployeeRestController::claimsSearch
*
* Covers GET (filter masters) and POST (search) behaviour against the live DB.
*
* Run:
* php tests/smoke_claims_search.php
* php tests/smoke_claims_search.php [client_id]
*/
declare(strict_types=1);
ob_start();
define('FCPATH', __DIR__ . '/../public/');
chdir(FCPATH);
require FCPATH . '../app/Config/Paths.php';
$paths = new Config\Paths();
require rtrim($paths->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<int>|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<int>|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.<key>
$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.<key> — ' . ($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);

View File

@ -0,0 +1,312 @@
<?php
/**
* Smoke test: RestAuthenticationController::getVerifiedHrData
* claims module strip when allowed_active_policies empty
* claims_sub_menu EB / Non-EB based on policy_type.allocg
*
* Run:
* php tests/smoke_verified_hr_claims_submenu.php
*/
declare(strict_types=1);
ob_start();
define('FCPATH', __DIR__ . '/../public/');
chdir(FCPATH);
require FCPATH . '../app/Config/Paths.php';
$paths = new Config\Paths();
require rtrim($paths->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);