fix: preserve numeric ref range metadata

This commit is contained in:
mahdahar 2026-03-26 14:48:49 +07:00
parent a73b88bc05
commit 366572a0cb
5 changed files with 353 additions and 273 deletions

View File

@ -59,27 +59,29 @@ class RefNumModel extends BaseModel
$rows = $this->getActiveByTestSiteID($testSiteID); $rows = $this->getActiveByTestSiteID($testSiteID);
return array_map(function ($r) { return array_map(function ($r) {
return [ return [
'RefNumID' => $r['RefNumID'], 'RefNumID' => $r['RefNumID'],
'NumRefType' => $r['NumRefType'], 'NumRefType' => $r['NumRefType'],
'NumRefTypeLabel' => $r['NumRefType'] ? \App\Libraries\ValueSet::getLabel('numeric_ref_type', $r['NumRefType']) : '', 'NumRefTypeLabel' => $r['NumRefType'] ? \App\Libraries\ValueSet::getLabel('numeric_ref_type', $r['NumRefType']) : '',
'RangeType' => $r['RangeType'], 'RangeType' => $r['RangeType'],
'RangeTypeLabel' => $r['RangeType'] ? \App\Libraries\ValueSet::getLabel('range_type', $r['RangeType']) : '', 'RangeTypeLabel' => $r['RangeType'] ? \App\Libraries\ValueSet::getLabel('range_type', $r['RangeType']) : '',
'Sex' => $r['Sex'], 'SpcType' => $r['SpcType'],
'SexLabel' => $r['Sex'] ? \App\Libraries\ValueSet::getLabel('gender', $r['Sex']) : '', 'Sex' => $r['Sex'],
'LowSign' => $r['LowSign'], 'SexLabel' => $r['Sex'] ? \App\Libraries\ValueSet::getLabel('gender', $r['Sex']) : '',
'LowSignLabel' => $r['LowSign'] ? \App\Libraries\ValueSet::getLabel('math_sign', $r['LowSign']) : '', 'LowSign' => $r['LowSign'],
'HighSign' => $r['HighSign'], 'LowSignLabel' => $r['LowSign'] ? \App\Libraries\ValueSet::getLabel('math_sign', $r['LowSign']) : '',
'HighSignLabel' => $r['HighSign'] ? \App\Libraries\ValueSet::getLabel('math_sign', $r['HighSign']) : '', 'HighSign' => $r['HighSign'],
'High' => $r['High'] !== null ? (float) $r['High'] : null, 'HighSignLabel' => $r['HighSign'] ? \App\Libraries\ValueSet::getLabel('math_sign', $r['HighSign']) : '',
'Low' => $r['Low'] !== null ? (float) $r['Low'] : null, 'High' => $r['High'] !== null ? (float) $r['High'] : null,
'AgeStart' => (int) $r['AgeStart'], 'Low' => $r['Low'] !== null ? (float) $r['Low'] : null,
'AgeEnd' => (int) $r['AgeEnd'], 'AgeStart' => (int) $r['AgeStart'],
'Flag' => $r['Flag'], 'AgeEnd' => (int) $r['AgeEnd'],
'Interpretation' => $r['Interpretation'], 'Flag' => $r['Flag'],
]; 'Interpretation' => $r['Interpretation'],
}, $rows ?? []); 'Notes' => $r['Notes'],
} ];
}, $rows ?? []);
}
/** /**
* Disable all numeric reference ranges for a test * Disable all numeric reference ranges for a test
@ -105,23 +107,25 @@ class RefNumModel extends BaseModel
public function batchInsert($testSiteID, $siteID, $ranges) public function batchInsert($testSiteID, $siteID, $ranges)
{ {
foreach ($ranges as $index => $range) { foreach ($ranges as $index => $range) {
$this->insert([ $this->insert([
'TestSiteID' => $testSiteID, 'TestSiteID' => $testSiteID,
'SiteID' => $siteID, 'SiteID' => $siteID,
'NumRefType' => $range['NumRefType'], 'SpcType' => $range['SpcType'] ?? 'GEN',
'RangeType' => $range['RangeType'], 'NumRefType' => $range['NumRefType'],
'Sex' => $range['Sex'], 'RangeType' => $range['RangeType'],
'AgeStart' => (int) ($range['AgeStart'] ?? 0), 'Sex' => $range['Sex'],
'AgeEnd' => (int) ($range['AgeEnd'] ?? 150), 'AgeStart' => (int) ($range['AgeStart'] ?? 0),
'LowSign' => !empty($range['LowSign']) ? $range['LowSign'] : null, 'AgeEnd' => (int) ($range['AgeEnd'] ?? 150),
'Low' => !empty($range['Low']) ? (float) $range['Low'] : null, 'LowSign' => !empty($range['LowSign']) ? $range['LowSign'] : null,
'HighSign' => !empty($range['HighSign']) ? $range['HighSign'] : null, 'Low' => !empty($range['Low']) ? (float) $range['Low'] : null,
'High' => !empty($range['High']) ? (float) $range['High'] : null, 'HighSign' => !empty($range['HighSign']) ? $range['HighSign'] : null,
'Flag' => $range['Flag'] ?? null, 'High' => !empty($range['High']) ? (float) $range['High'] : null,
'Interpretation'=> $range['Interpretation'] ?? null, 'Flag' => $range['Flag'] ?? null,
'Display' => $index, 'Interpretation'=> $range['Interpretation'] ?? null,
'CreateDate' => date('Y-m-d H:i:s'), 'Notes' => $range['Notes'] ?? null,
]); 'Display' => $index,
} 'CreateDate' => date('Y-m-d H:i:s'),
} ]);
}
}
} }

View File

@ -5182,12 +5182,49 @@ paths:
message: message:
type: string type: string
example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.'
/api/test/{id}:
get:
tags:
- Test
summary: Get test definition by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: Test Site ID
responses:
'200':
description: Test definition details
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '#/components/schemas/TestDefinition'
'404':
description: Test not found
patch: patch:
tags: tags:
- Test - Test
summary: Update test definition summary: Update test definition
security: security:
- bearerAuth: [] - bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: Test Site ID
requestBody: requestBody:
required: true required: true
content: content:
@ -5335,36 +5372,6 @@ paths:
message: message:
type: string type: string
example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.'
/api/test/{id}:
get:
tags:
- Test
summary: Get test definition by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: Test Site ID
responses:
'200':
description: Test definition details
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '#/components/schemas/TestDefinition'
'404':
description: Test not found
delete: delete:
tags: tags:
- Test - Test
@ -7072,6 +7079,9 @@ components:
type: string type: string
RangeTypeLabel: RangeTypeLabel:
type: string type: string
SpcType:
type: string
description: Specimen type code (e.g., GEN, EDTA)
Sex: Sex:
type: string type: string
SexLabel: SexLabel:
@ -7098,6 +7108,9 @@ components:
type: string type: string
Interpretation: Interpretation:
type: string type: string
Notes:
type: string
description: Optional note attached to the numeric reference range
reftxt: reftxt:
type: array type: array
description: Text reference ranges (optional). Mutually exclusive with refnum - a test can only have ONE reference type. description: Text reference ranges (optional). Mutually exclusive with refnum - a test can only have ONE reference type.

View File

@ -223,50 +223,56 @@ TestDefinition:
description: Test mappings description: Test mappings
items: items:
$ref: '#/TestMap' $ref: '#/TestMap'
refnum: refnum:
type: array type: array
description: Numeric reference ranges (optional). Mutually exclusive with reftxt - a test can only have ONE reference type. description: Numeric reference ranges (optional). Mutually exclusive with reftxt - a test can only have ONE reference type.
items: items:
type: object type: object
properties: properties:
RefNumID: RefNumID:
type: integer type: integer
NumRefType: NumRefType:
type: string type: string
enum: [NMRC, THOLD] enum: [NMRC, THOLD]
description: NMRC=Numeric range, THOLD=Threshold description: NMRC=Numeric range, THOLD=Threshold
NumRefTypeLabel: NumRefTypeLabel:
type: string type: string
RangeType: RangeType:
type: string type: string
RangeTypeLabel: RangeTypeLabel:
type: string type: string
Sex: SpcType:
type: string type: string
SexLabel: description: Specimen type code (e.g., GEN, EDTA)
type: string Sex:
LowSign: type: string
type: string SexLabel:
LowSignLabel: type: string
type: string LowSign:
HighSign: type: string
type: string LowSignLabel:
HighSignLabel: type: string
type: string HighSign:
High: type: string
type: number HighSignLabel:
format: float type: string
Low: High:
type: number type: number
format: float format: float
AgeStart: Low:
type: integer type: number
AgeEnd: format: float
type: integer AgeStart:
Flag: type: integer
type: string AgeEnd:
Interpretation: type: integer
type: string Flag:
type: string
Interpretation:
type: string
Notes:
type: string
description: Optional note attached to the numeric reference range
reftxt: reftxt:
type: array type: array
description: Text reference ranges (optional). Mutually exclusive with refnum - a test can only have ONE reference type. description: Text reference ranges (optional). Mutually exclusive with refnum - a test can only have ONE reference type.

View File

@ -698,148 +698,10 @@
type: string type: string
example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.'
patch: /api/test/{id}:
tags: [Test] get:
summary: Update test definition tags: [Test]
security: summary: Get test definition by ID
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
TestSiteID:
type: integer
description: Test Site ID (required)
TestSiteCode:
type: string
TestSiteName:
type: string
TestType:
type: string
enum: [TEST, PARAM, CALC, GROUP, TITLE]
Description:
type: string
DisciplineID:
type: integer
DepartmentID:
type: integer
ResultType:
type: string
enum: [NMRIC, RANGE, TEXT, VSET, NORES]
RefType:
type: string
enum: [RANGE, THOLD, VSET, TEXT, NOREF]
VSet:
type: integer
ReqQty:
type: number
format: decimal
ReqQtyUnit:
type: string
Unit1:
type: string
Factor:
type: number
format: decimal
Unit2:
type: string
Decimal:
type: integer
CollReq:
type: string
Method:
type: string
ExpectedTAT:
type: integer
SeqScr:
type: integer
SeqRpt:
type: integer
IndentLeft:
type: integer
FontStyle:
type: string
isVisibleScr:
type: integer
isVisibleRpt:
type: integer
isCountStat:
type: integer
testdefcal:
type: object
description: Calculated test metadata persisted in the `testdefcal` table.
properties:
FormulaCode:
type: string
description: Formula expression for calculated tests (e.g., "{TBIL} - {DBIL}")
testdefgrp:
type: object
description: Group member payload stored in the `testdefgrp` table.
properties:
members:
type: array
description: Array of member TestSiteIDs for CALC/GROUP definitions.
items:
type: object
properties:
TestSiteID:
type: integer
description: Foreign key referencing the member test's TestSiteID.
required:
- TestSiteID
refnum:
type: array
items:
type: object
reftxt:
type: array
items:
type: object
testmap:
type: array
items:
type: object
required:
- TestSiteID
responses:
'200':
description: Test definition updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
type: object
properties:
TestSiteId:
type: integer
'400':
description: Validation error (e.g., invalid member TestSiteID)
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: failed
message:
type: string
example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.'
/api/test/{id}:
get:
tags: [Test]
summary: Get test definition by ID
security: security:
- bearerAuth: [] - bearerAuth: []
parameters: parameters:
@ -850,23 +712,168 @@
type: integer type: integer
description: Test Site ID description: Test Site ID
responses: responses:
'200': '200':
description: Test definition details description: Test definition details
content: content:
application/json: application/json:
schema: schema:
type: object type: object
properties: properties:
status: status:
type: string type: string
message: message:
type: string type: string
data: data:
$ref: '../components/schemas/tests.yaml#/TestDefinition' $ref: '../components/schemas/tests.yaml#/TestDefinition'
'404': '404':
description: Test not found description: Test not found
delete: patch:
tags: [Test]
summary: Update test definition
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: Test Site ID
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
TestSiteID:
type: integer
description: Test Site ID (required)
TestSiteCode:
type: string
TestSiteName:
type: string
TestType:
type: string
enum: [TEST, PARAM, CALC, GROUP, TITLE]
Description:
type: string
DisciplineID:
type: integer
DepartmentID:
type: integer
ResultType:
type: string
enum: [NMRIC, RANGE, TEXT, VSET, NORES]
RefType:
type: string
enum: [RANGE, THOLD, VSET, TEXT, NOREF]
VSet:
type: integer
ReqQty:
type: number
format: decimal
ReqQtyUnit:
type: string
Unit1:
type: string
Factor:
type: number
format: decimal
Unit2:
type: string
Decimal:
type: integer
CollReq:
type: string
Method:
type: string
ExpectedTAT:
type: integer
SeqScr:
type: integer
SeqRpt:
type: integer
IndentLeft:
type: integer
FontStyle:
type: string
isVisibleScr:
type: integer
isVisibleRpt:
type: integer
isCountStat:
type: integer
testdefcal:
type: object
description: Calculated test metadata persisted in the `testdefcal` table.
properties:
FormulaCode:
type: string
description: Formula expression for calculated tests (e.g., "{TBIL} - {DBIL}")
testdefgrp:
type: object
description: Group member payload stored in the `testdefgrp` table.
properties:
members:
type: array
description: Array of member TestSiteIDs for CALC/GROUP definitions.
items:
type: object
properties:
TestSiteID:
type: integer
description: Foreign key referencing the member test's TestSiteID.
required:
- TestSiteID
refnum:
type: array
items:
type: object
reftxt:
type: array
items:
type: object
testmap:
type: array
items:
type: object
required:
- TestSiteID
responses:
'200':
description: Test definition updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
type: object
properties:
TestSiteId:
type: integer
'400':
description: Validation error (e.g., invalid member TestSiteID)
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: failed
message:
type: string
example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.'
delete:
tags: [Test] tags: [Test]
summary: Soft delete test definition summary: Soft delete test definition
security: security:

View File

@ -42,6 +42,52 @@ class TestCreateVariantsTest extends CIUnitTestCase
} }
} }
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 public function testCreateTechnicalWithThresholdReference(): void
{ {
$refnum = $this->buildRefNumEntries('THOLD', true); $refnum = $this->buildRefNumEntries('THOLD', true);
@ -321,6 +367,8 @@ class TestCreateVariantsTest extends CIUnitTestCase
'AgeEnd' => 120, 'AgeEnd' => 120,
'Flag' => 'N', 'Flag' => 'N',
'Interpretation' => 'Normal range', 'Interpretation' => 'Normal range',
'SpcType' => 'GEN',
'Notes' => 'Default numeric range note',
], ],
]; ];
@ -337,6 +385,8 @@ class TestCreateVariantsTest extends CIUnitTestCase
'AgeEnd' => 99, 'AgeEnd' => 99,
'Flag' => 'N', 'Flag' => 'N',
'Interpretation' => 'Alternate range', 'Interpretation' => 'Alternate range',
'SpcType' => 'GEN',
'Notes' => 'Alternate numeric range note',
]; ];
} }