clqms-be/tests/feature/Test/TestCreateVariantsTest.php
mahdahar 694c5a6211 fix: wrap test group members under testdefgrp
Align CALC/GROUP request and response payloads to use the testdefgrp.members structure in controller handling, feature tests, and OpenAPI schemas/examples for a consistent API contract.
2026-04-02 09:06:42 +07:00

684 lines
23 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Test;
use App\Models\Test\TestDefSiteModel;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;
class TestCreateVariantsTest extends CIUnitTestCase
{
use FeatureTestTrait;
private const SITE_ID = 1;
protected string $endpoint = 'api/test';
private TestDefSiteModel $testModel;
protected function setUp(): void
{
parent::setUp();
$this->testModel = new TestDefSiteModel();
}
public function testCreateTechnicalWithoutReferenceOrTestMap(): void
{
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type);
}
}
public function testCreateTechnicalCanAcceptNullSiteAndNumericFields(): void
{
$payload = $this->buildTechnicalPayload('TEST', [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
'Decimal' => null,
'Factor' => 2.5,
'ReqQty' => 1.75,
'ReqQtyUnit' => 'uL',
]);
$payload['SiteID'] = null;
$payload['SeqScr'] = null;
$payload['SeqRpt'] = null;
$payload['refnum'] = [
[
'NumRefType' => 'NMRC',
'RangeType' => 'REF',
'Sex' => '2',
'LowSign' => 'GE',
'Low' => 10,
'HighSign' => 'LE',
'High' => 20,
'AgeStart' => null,
'AgeEnd' => null,
'Flag' => 'N',
'Interpretation' => 'Nullable range',
'SpcType' => 'GEN',
],
];
$response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$response->assertStatus(201);
$testSiteId = json_decode($response->getJSON(), true)['data']['TestSiteId'];
$show = $this->call('get', $this->endpoint . '/' . $testSiteId);
$show->assertStatus(200);
$data = json_decode($show->getJSON(), true)['data'];
$this->assertNull($data['SiteID']);
$this->assertNull($data['SeqScr']);
$this->assertNull($data['SeqRpt']);
$this->assertNull($data['Decimal']);
$this->assertSame(2.5, (float) $data['Factor']);
$this->assertSame(1.75, (float) $data['ReqQty']);
$this->assertNull($data['refnum'][0]['AgeStart']);
$this->assertNull($data['refnum'][0]['AgeEnd']);
}
public function testCreateTechnicalWithNumericReference(): void
{
$refnum = $this->buildRefNumEntries('NMRC', true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
], $refnum);
}
}
public function testCreateTechnicalNumericReferenceReturnsAgeInDays(): void
{
$payload = $this->buildTechnicalPayload('TEST', [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
]);
$payload['refnum'] = [
[
'NumRefType' => 'NMRC',
'RangeType' => 'REF',
'Sex' => '2',
'LowSign' => 'GE',
'Low' => 10,
'HighSign' => 'LE',
'High' => 20,
'AgeStart' => 6570,
'AgeEnd' => 36135,
'Flag' => 'N',
'Interpretation' => 'Adult range in days',
'SpcType' => 'GEN',
],
];
$response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$response->assertStatus(201);
$testSiteId = json_decode($response->getJSON(), true)['data']['TestSiteId'];
$show = $this->call('get', $this->endpoint . '/' . $testSiteId);
$show->assertStatus(200);
$data = json_decode($show->getJSON(), true)['data'];
$this->assertSame(6570, (int) $data['refnum'][0]['AgeStart']);
$this->assertSame(36135, (int) $data['refnum'][0]['AgeEnd']);
}
public function testNumericRefRangeNotesPersistAfterCreate(): void
{
$notes = 'Auto note ' . uniqid();
$refnum = $this->buildRefNumEntries('NMRC', false);
$refnum[0]['Notes'] = $notes;
$payload = $this->buildTechnicalPayload('TEST', [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
]);
$payload['refnum'] = $refnum;
$response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$response->assertStatus(201);
$json = json_decode($response->getJSON(), true);
$testSiteId = $json['data']['TestSiteId'];
$show = $this->call('get', $this->endpoint . '/' . $testSiteId);
$show->assertStatus(200);
$showJson = json_decode($show->getJSON(), true);
$this->assertSame($notes, $showJson['data']['refnum'][0]['Notes']);
}
public function testNumericRefRangeSpcTypePersistAfterCreate(): void
{
$spcType = 'EDTA';
$refnum = $this->buildRefNumEntries('NMRC', false);
$refnum[0]['SpcType'] = $spcType;
$payload = $this->buildTechnicalPayload('TEST', [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
]);
$payload['refnum'] = $refnum;
$response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$response->assertStatus(201);
$json = json_decode($response->getJSON(), true);
$testSiteId = $json['data']['TestSiteId'];
$show = $this->call('get', $this->endpoint . '/' . $testSiteId);
$show->assertStatus(200);
$showJson = json_decode($show->getJSON(), true);
$this->assertSame($spcType, $showJson['data']['refnum'][0]['SpcType']);
}
public function testCreateTechnicalWithThresholdReference(): void
{
$refnum = $this->buildRefNumEntries('THOLD', true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'NMRIC',
'RefType' => 'THOLD',
], $refnum);
}
}
public function testCreateTechnicalWithTextReference(): void
{
$reftxt = $this->buildRefTxtEntries('TEXT', true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'TEXT',
'RefType' => 'TEXT',
], null, $reftxt);
}
}
public function testCreateTechnicalWithValuesetReference(): void
{
$reftxt = $this->buildRefTxtEntries('VSET', true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'VSET',
'RefType' => 'VSET',
], null, $reftxt);
}
}
public function testCreateTechnicalWithNumericReferenceAndTestMap(): void
{
$refnum = $this->buildRefNumEntries('NMRC', true);
$testmap = $this->buildTestMap(true, true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
], $refnum, null, $testmap);
}
}
public function testCreateTechnicalWithThresholdReferenceAndTestMap(): void
{
$refnum = $this->buildRefNumEntries('THOLD', true);
$testmap = $this->buildTestMap(true, true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'NMRIC',
'RefType' => 'THOLD',
], $refnum, null, $testmap);
}
}
public function testCreateTechnicalWithTextReferenceAndTestMap(): void
{
$reftxt = $this->buildRefTxtEntries('TEXT', true);
$testmap = $this->buildTestMap(true, true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'TEXT',
'RefType' => 'TEXT',
], null, $reftxt, $testmap);
}
}
public function testCreateTechnicalWithValuesetReferenceAndTestMap(): void
{
$reftxt = $this->buildRefTxtEntries('VSET', true);
$testmap = $this->buildTestMap(true, true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'VSET',
'RefType' => 'VSET',
], null, $reftxt, $testmap);
}
}
public function testCreateTechnicalValuesetWithoutReferenceButWithMap(): void
{
$testmap = $this->buildTestMap(false, true);
foreach (['TEST', 'PARAM'] as $type) {
$this->assertTechnicalCreated($type, [
'ResultType' => 'VSET',
'RefType' => 'VSET',
], null, null, $testmap);
}
}
public function testCreateTechnicalWithFlatTestMapPayload(): void
{
$testCode = $this->generateTestCode('TEST');
$payload = [
'SiteID' => self::SITE_ID,
'TestSiteCode' => $testCode,
'TestSiteName' => 'Auto Flat Map Create',
'TestType' => 'TEST',
'isVisibleScr' => 1,
'isVisibleRpt' => 1,
'isCountStat' => 1,
'details' => [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
],
'testmap' => $this->buildFlatTestMap($testCode),
];
$response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$response->assertStatus(201);
$createdId = json_decode($response->getJSON(), true)['data']['TestSiteId'];
$show = $this->call('get', $this->endpoint . '/' . $createdId);
$show->assertStatus(200);
$data = json_decode($show->getJSON(), true)['data'];
$this->assertNotEmpty($data['testmap']);
$this->assertSame('HIS', $data['testmap'][0]['HostType']);
$this->assertSame('SITE', $data['testmap'][0]['ClientType']);
}
public function testPatchTechnicalWithFlatTestMapPayload(): void
{
$payload = $this->buildTechnicalPayload('TEST', [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
]);
$create = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$create->assertStatus(201);
$createJson = json_decode($create->getJSON(), true);
$testSiteId = $createJson['data']['TestSiteId'];
$testCode = $payload['TestSiteCode'];
$patchPayload = [
'details' => [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE',
],
'testmap' => $this->buildFlatTestMap($testCode),
];
$patch = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/' . $testSiteId, $patchPayload);
$patch->assertStatus(200);
$show = $this->call('get', $this->endpoint . '/' . $testSiteId);
$show->assertStatus(200);
$data = json_decode($show->getJSON(), true)['data'];
$this->assertNotEmpty($data['testmap']);
$this->assertSame('HIS', $data['testmap'][0]['HostType']);
$this->assertSame('SITE', $data['testmap'][0]['ClientType']);
}
public function testCreateCalculatedTestWithoutReferenceOrMap(): void
{
$this->assertCalculatedCreated(false);
}
public function testCreateCalculatedTestWithReferenceAndTestMap(): void
{
$refnum = $this->buildRefNumEntries('NMRC', true);
$testmap = $this->buildTestMap(true, true);
$members = $this->resolveMemberIds(['GLU', 'CREA']);
$this->assertCalculatedCreated(true, $refnum, $testmap, $members);
}
public function testCreateGroupTestWithMembers(): void
{
$members = $this->resolveMemberIds(['GLU', 'CREA']);
$testmap = $this->buildTestMap(true, true);
$payload = $this->buildGroupPayload($members, $testmap);
$response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$response->assertStatus(201);
$response->assertJSONFragment([
'status' => 'created',
'message' => 'Test created successfully',
]);
$json = json_decode($response->getJSON(), true);
$this->assertArrayHasKey('data', $json);
$this->assertArrayHasKey('TestSiteId', $json['data']);
$this->assertIsInt($json['data']['TestSiteId']);
$show = $this->call('get', $this->endpoint . '/' . $json['data']['TestSiteId']);
$show->assertStatus(200);
$showData = json_decode($show->getJSON(), true)['data'];
$this->assertArrayHasKey('testdefgrp', $showData);
$this->assertArrayHasKey('members', $showData['testdefgrp']);
$this->assertArrayNotHasKey('members', $showData);
if ($members !== []) {
$this->assertCount(count($members), $showData['testdefgrp']['members']);
}
}
private function assertTechnicalCreated(
string $type,
array $details = [],
?array $refnum = null,
?array $reftxt = null,
?array $testmap = null
): void {
$payload = $this->buildTechnicalPayload($type, $details);
if ($refnum !== null) {
$payload['refnum'] = $refnum;
}
if ($reftxt !== null) {
$payload['reftxt'] = $reftxt;
}
if ($testmap !== null) {
$payload['testmap'] = $testmap;
}
$response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$response->assertStatus(201);
$response->assertJSONFragment([
'status' => 'created',
'message' => 'Test created successfully',
]);
$json = json_decode($response->getJSON(), true);
$this->assertArrayHasKey('data', $json);
$this->assertArrayHasKey('TestSiteId', $json['data']);
$this->assertIsInt($json['data']['TestSiteId']);
}
private function assertCalculatedCreated(
bool $withDetails,
?array $refnum = null,
?array $testmap = null,
array $members = []
): void {
$payload = $this->buildCalculatedPayload($members);
if ($withDetails && $refnum !== null) {
$payload['refnum'] = $refnum;
}
if ($withDetails && $testmap !== null) {
$payload['testmap'] = $testmap;
}
$response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
$response->assertStatus(201);
$response->assertJSONFragment([
'status' => 'created',
'message' => 'Test created successfully',
]);
$json = json_decode($response->getJSON(), true);
$this->assertArrayHasKey('data', $json);
$this->assertArrayHasKey('TestSiteId', $json['data']);
$this->assertIsInt($json['data']['TestSiteId']);
$show = $this->call('get', $this->endpoint . '/' . $json['data']['TestSiteId']);
$show->assertStatus(200);
$showData = json_decode($show->getJSON(), true)['data'];
$this->assertArrayHasKey('testdefgrp', $showData);
$this->assertArrayHasKey('members', $showData['testdefgrp']);
$this->assertArrayNotHasKey('members', $showData);
$this->assertCount(count($members), $showData['testdefgrp']['members']);
}
private function buildTechnicalPayload(string $testType, array $details = []): array
{
$payload = [
'SiteID' => self::SITE_ID,
'TestSiteCode' => $this->generateTestCode($testType),
'TestSiteName' => 'Auto ' . strtoupper($testType),
'TestType' => $testType,
'SeqScr' => 900,
'SeqRpt' => 900,
'isVisibleScr' => 1,
'isVisibleRpt' => 1,
'isCountStat' => 1,
];
$payload['details'] = $this->normalizeDetails($details);
return $payload;
}
private function buildCalculatedPayload(array $members = []): array
{
$payload = [
'SiteID' => self::SITE_ID,
'TestSiteCode' => $this->generateTestCode('CALC'),
'TestSiteName' => 'Auto CALC',
'TestType' => 'CALC',
'SeqScr' => 1000,
'SeqRpt' => 1000,
'isVisibleScr' => 1,
'isVisibleRpt' => 1,
'isCountStat' => 0,
'testdefgrp' => [
'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members),
],
'details' => [
'DisciplineID' => 2,
'DepartmentID' => 2,
'FormulaCode' => '{GLU} + {CREA}',
],
];
return $payload;
}
private function buildGroupPayload(array $members, array $testmap): array
{
return [
'SiteID' => self::SITE_ID,
'TestSiteCode' => $this->generateTestCode('PANEL'),
'TestSiteName' => 'Auto Group',
'TestType' => 'GROUP',
'SeqScr' => 300,
'SeqRpt' => 300,
'isVisibleScr' => 1,
'isVisibleRpt' => 1,
'isCountStat' => 1,
'testdefgrp' => [
'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members),
],
'testmap' => $testmap,
];
}
private function normalizeDetails(array $details): array
{
$normalized = [
'DisciplineID' => $details['DisciplineID'] ?? 2,
'DepartmentID' => $details['DepartmentID'] ?? 2,
'Method' => $details['Method'] ?? 'Automated test',
'Unit1' => $details['Unit1'] ?? 'mg/dL',
'Decimal' => array_key_exists('Decimal', $details) ? $details['Decimal'] : 0,
];
foreach (['ResultType', 'RefType', 'FormulaCode', 'ExpectedTAT', 'Factor', 'ReqQty', 'ReqQtyUnit', 'Unit2', 'VSet', 'CollReq'] as $key) {
if (array_key_exists($key, $details)) {
$normalized[$key] = $details[$key];
}
}
return $normalized;
}
private function buildRefNumEntries(string $numRefType, bool $multiple = false): array
{
$rangeType = $numRefType === 'THOLD' ? 'PANIC' : 'REF';
$entries = [
[
'NumRefType' => $numRefType,
'RangeType' => $rangeType,
'Sex' => '2',
'LowSign' => 'GE',
'Low' => 10,
'HighSign' => 'LE',
'High' => $numRefType === 'THOLD' ? 40 : 20,
'AgeStart' => 0,
'AgeEnd' => 43800,
'Flag' => 'N',
'Interpretation' => 'Normal range',
'SpcType' => 'GEN',
'Notes' => 'Default numeric range note',
],
];
if ($multiple) {
$entries[] = [
'NumRefType' => $numRefType,
'RangeType' => $rangeType,
'Sex' => '1',
'LowSign' => '>',
'Low' => 5,
'HighSign' => '<',
'High' => $numRefType === 'THOLD' ? 50 : 15,
'AgeStart' => 0,
'AgeEnd' => 36135,
'Flag' => 'N',
'Interpretation' => 'Alternate range',
'SpcType' => 'GEN',
'Notes' => 'Alternate numeric range note',
];
}
return $entries;
}
private function buildRefTxtEntries(string $txtRefType, bool $multiple = false): array
{
$entries = [
[
'SpcType' => 'GEN',
'TxtRefType' => $txtRefType,
'Sex' => '2',
'AgeStart' => 0,
'AgeEnd' => 43800,
'RefTxt' => $txtRefType === 'VSET' ? 'NORM=Normal;ABN=Abnormal' : 'NORM=Normal',
'Flag' => 'N',
],
];
if ($multiple) {
$entries[] = [
'SpcType' => 'GEN',
'TxtRefType' => $txtRefType,
'Sex' => '1',
'AgeStart' => 0,
'AgeEnd' => 43800,
'RefTxt' => $txtRefType === 'VSET' ? 'HIGH=High;LOW=Low' : 'ABN=Abnormal',
'Flag' => 'N',
];
}
return $entries;
}
private function buildTestMap(bool $multipleMaps = false, bool $multipleDetails = false): array
{
$map = [
[
'HostType' => 'SITE',
'HostID' => '1',
'ClientType' => 'WST',
'ClientID' => '1',
'details' => [
[
'HostTestCode' => 'GLU',
'HostTestName' => 'Glucose',
'ConDefID' => 1,
'ClientTestCode' => 'GLU_C',
'ClientTestName' => 'Glucose Client',
],
],
],
];
if ($multipleDetails) {
$map[0]['details'][] = [
'HostTestCode' => 'CREA',
'HostTestName' => 'Creatinine',
'ConDefID' => 2,
'ClientTestCode' => 'CREA_C',
'ClientTestName' => 'Creatinine Client',
];
}
if ($multipleMaps) {
$map[] = [
'HostType' => 'WST',
'HostID' => '3',
'ClientType' => 'INST',
'ClientID' => '2',
'details' => [
[
'HostTestCode' => 'HB',
'HostTestName' => 'Hemoglobin',
'ConDefID' => 3,
'ClientTestCode' => 'HB_C',
'ClientTestName' => 'Hemoglobin Client',
],
],
];
}
return $map;
}
private function buildFlatTestMap(string $testCode): array
{
return [
[
'HostType' => 'HIS',
'HostID' => 'LOKAL',
'HostTestCode' => $testCode,
'HostTestName' => 'Host ' . $testCode,
'ClientType' => 'SITE',
'ClientID' => '1',
'ClientTestCode' => $testCode,
'ClientTestName' => 'Client ' . $testCode,
'ConDefID' => null,
],
];
}
private function generateTestCode(string $prefix): string
{
$clean = strtoupper(substr($prefix, 0, 3));
$suffix = strtoupper(substr(md5((string) microtime(true) . random_int(0, 9999)), 0, 6));
return substr($clean . $suffix, 0, 10);
}
private function resolveMemberIds(array $codes): array
{
$ids = [];
foreach ($codes as $code) {
$row = $this->testModel->where('TestSiteCode', $code)->where('EndDate IS NULL')->first();
$this->assertNotEmpty($row, "Seeded test code {$code} not found");
$ids[] = (int) $row['TestSiteID'];
}
return $ids;
}
}