Avoid coercing missing SiteID, Decimal, and age boundaries to hardcoded defaults so payload intent is retained across test creation and reference range inserts. Align patient result age checks and OpenAPI examples with day-based age bounds, with feature coverage for create variants.
578 lines
19 KiB
PHP
578 lines
19 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 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']);
|
|
}
|
|
|
|
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']);
|
|
}
|
|
|
|
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,
|
|
'details' => [
|
|
'DisciplineID' => 2,
|
|
'DepartmentID' => 2,
|
|
'FormulaCode' => '{GLU} + {CREA}',
|
|
'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members),
|
|
],
|
|
];
|
|
|
|
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,
|
|
'details' => [
|
|
'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', 'members', '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 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;
|
|
}
|
|
}
|