From 366572a0cb0ef8b074a909ab735767985256cbf5 Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Thu, 26 Mar 2026 14:48:49 +0700 Subject: [PATCH] fix: preserve numeric ref range metadata --- app/Models/RefRange/RefNumModel.php | 84 ++--- public/api-docs.bundled.yaml | 73 ++-- public/components/schemas/tests.yaml | 94 ++--- public/paths/tests.yaml | 325 +++++++++--------- tests/feature/Test/TestCreateVariantsTest.php | 50 +++ 5 files changed, 353 insertions(+), 273 deletions(-) diff --git a/app/Models/RefRange/RefNumModel.php b/app/Models/RefRange/RefNumModel.php index 24ab174..48230c8 100644 --- a/app/Models/RefRange/RefNumModel.php +++ b/app/Models/RefRange/RefNumModel.php @@ -59,27 +59,29 @@ class RefNumModel extends BaseModel $rows = $this->getActiveByTestSiteID($testSiteID); return array_map(function ($r) { - return [ - 'RefNumID' => $r['RefNumID'], - 'NumRefType' => $r['NumRefType'], - 'NumRefTypeLabel' => $r['NumRefType'] ? \App\Libraries\ValueSet::getLabel('numeric_ref_type', $r['NumRefType']) : '', - 'RangeType' => $r['RangeType'], - 'RangeTypeLabel' => $r['RangeType'] ? \App\Libraries\ValueSet::getLabel('range_type', $r['RangeType']) : '', - 'Sex' => $r['Sex'], - 'SexLabel' => $r['Sex'] ? \App\Libraries\ValueSet::getLabel('gender', $r['Sex']) : '', - 'LowSign' => $r['LowSign'], - 'LowSignLabel' => $r['LowSign'] ? \App\Libraries\ValueSet::getLabel('math_sign', $r['LowSign']) : '', - 'HighSign' => $r['HighSign'], - 'HighSignLabel' => $r['HighSign'] ? \App\Libraries\ValueSet::getLabel('math_sign', $r['HighSign']) : '', - 'High' => $r['High'] !== null ? (float) $r['High'] : null, - 'Low' => $r['Low'] !== null ? (float) $r['Low'] : null, - 'AgeStart' => (int) $r['AgeStart'], - 'AgeEnd' => (int) $r['AgeEnd'], - 'Flag' => $r['Flag'], - 'Interpretation' => $r['Interpretation'], - ]; - }, $rows ?? []); - } + return [ + 'RefNumID' => $r['RefNumID'], + 'NumRefType' => $r['NumRefType'], + 'NumRefTypeLabel' => $r['NumRefType'] ? \App\Libraries\ValueSet::getLabel('numeric_ref_type', $r['NumRefType']) : '', + 'RangeType' => $r['RangeType'], + 'RangeTypeLabel' => $r['RangeType'] ? \App\Libraries\ValueSet::getLabel('range_type', $r['RangeType']) : '', + 'SpcType' => $r['SpcType'], + 'Sex' => $r['Sex'], + 'SexLabel' => $r['Sex'] ? \App\Libraries\ValueSet::getLabel('gender', $r['Sex']) : '', + 'LowSign' => $r['LowSign'], + 'LowSignLabel' => $r['LowSign'] ? \App\Libraries\ValueSet::getLabel('math_sign', $r['LowSign']) : '', + 'HighSign' => $r['HighSign'], + 'HighSignLabel' => $r['HighSign'] ? \App\Libraries\ValueSet::getLabel('math_sign', $r['HighSign']) : '', + 'High' => $r['High'] !== null ? (float) $r['High'] : null, + 'Low' => $r['Low'] !== null ? (float) $r['Low'] : null, + 'AgeStart' => (int) $r['AgeStart'], + 'AgeEnd' => (int) $r['AgeEnd'], + 'Flag' => $r['Flag'], + 'Interpretation' => $r['Interpretation'], + 'Notes' => $r['Notes'], + ]; + }, $rows ?? []); + } /** * Disable all numeric reference ranges for a test @@ -105,23 +107,25 @@ class RefNumModel extends BaseModel public function batchInsert($testSiteID, $siteID, $ranges) { foreach ($ranges as $index => $range) { - $this->insert([ - 'TestSiteID' => $testSiteID, - 'SiteID' => $siteID, - 'NumRefType' => $range['NumRefType'], - 'RangeType' => $range['RangeType'], - 'Sex' => $range['Sex'], - 'AgeStart' => (int) ($range['AgeStart'] ?? 0), - 'AgeEnd' => (int) ($range['AgeEnd'] ?? 150), - 'LowSign' => !empty($range['LowSign']) ? $range['LowSign'] : null, - 'Low' => !empty($range['Low']) ? (float) $range['Low'] : null, - 'HighSign' => !empty($range['HighSign']) ? $range['HighSign'] : null, - 'High' => !empty($range['High']) ? (float) $range['High'] : null, - 'Flag' => $range['Flag'] ?? null, - 'Interpretation'=> $range['Interpretation'] ?? null, - 'Display' => $index, - 'CreateDate' => date('Y-m-d H:i:s'), - ]); - } - } + $this->insert([ + 'TestSiteID' => $testSiteID, + 'SiteID' => $siteID, + 'SpcType' => $range['SpcType'] ?? 'GEN', + 'NumRefType' => $range['NumRefType'], + 'RangeType' => $range['RangeType'], + 'Sex' => $range['Sex'], + 'AgeStart' => (int) ($range['AgeStart'] ?? 0), + 'AgeEnd' => (int) ($range['AgeEnd'] ?? 150), + 'LowSign' => !empty($range['LowSign']) ? $range['LowSign'] : null, + 'Low' => !empty($range['Low']) ? (float) $range['Low'] : null, + 'HighSign' => !empty($range['HighSign']) ? $range['HighSign'] : null, + 'High' => !empty($range['High']) ? (float) $range['High'] : null, + 'Flag' => $range['Flag'] ?? null, + 'Interpretation'=> $range['Interpretation'] ?? null, + 'Notes' => $range['Notes'] ?? null, + 'Display' => $index, + 'CreateDate' => date('Y-m-d H:i:s'), + ]); + } + } } diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index ba6cd97..7975d54 100644 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -5182,12 +5182,49 @@ paths: 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: + - 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: 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: @@ -5335,36 +5372,6 @@ paths: 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: - - 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: tags: - Test @@ -7072,6 +7079,9 @@ components: type: string RangeTypeLabel: type: string + SpcType: + type: string + description: Specimen type code (e.g., GEN, EDTA) Sex: type: string SexLabel: @@ -7098,6 +7108,9 @@ components: type: string Interpretation: type: string + Notes: + type: string + description: Optional note attached to the numeric reference range reftxt: type: array description: Text reference ranges (optional). Mutually exclusive with refnum - a test can only have ONE reference type. diff --git a/public/components/schemas/tests.yaml b/public/components/schemas/tests.yaml index 7f0601a..7890d6c 100644 --- a/public/components/schemas/tests.yaml +++ b/public/components/schemas/tests.yaml @@ -223,50 +223,56 @@ TestDefinition: description: Test mappings items: $ref: '#/TestMap' - refnum: - type: array - description: Numeric reference ranges (optional). Mutually exclusive with reftxt - a test can only have ONE reference type. - items: - type: object - properties: - RefNumID: - type: integer - NumRefType: - type: string - enum: [NMRC, THOLD] - description: NMRC=Numeric range, THOLD=Threshold - NumRefTypeLabel: - type: string - RangeType: - type: string - RangeTypeLabel: - type: string - Sex: - type: string - SexLabel: - type: string - LowSign: - type: string - LowSignLabel: - type: string - HighSign: - type: string - HighSignLabel: - type: string - High: - type: number - format: float - Low: - type: number - format: float - AgeStart: - type: integer - AgeEnd: - type: integer - Flag: - type: string - Interpretation: - type: string + refnum: + type: array + description: Numeric reference ranges (optional). Mutually exclusive with reftxt - a test can only have ONE reference type. + items: + type: object + properties: + RefNumID: + type: integer + NumRefType: + type: string + enum: [NMRC, THOLD] + description: NMRC=Numeric range, THOLD=Threshold + NumRefTypeLabel: + type: string + RangeType: + type: string + RangeTypeLabel: + type: string + SpcType: + type: string + description: Specimen type code (e.g., GEN, EDTA) + Sex: + type: string + SexLabel: + type: string + LowSign: + type: string + LowSignLabel: + type: string + HighSign: + type: string + HighSignLabel: + type: string + High: + type: number + format: float + Low: + type: number + format: float + AgeStart: + type: integer + AgeEnd: + type: integer + Flag: + type: string + Interpretation: + type: string + Notes: + type: string + description: Optional note attached to the numeric reference range reftxt: type: array description: Text reference ranges (optional). Mutually exclusive with refnum - a test can only have ONE reference type. diff --git a/public/paths/tests.yaml b/public/paths/tests.yaml index 51324b6..2378522 100644 --- a/public/paths/tests.yaml +++ b/public/paths/tests.yaml @@ -698,148 +698,10 @@ type: string example: 'Invalid member TestSiteID(s): 185, 186. Make sure to use TestSiteID, not SeqScr or other values.' - patch: - tags: [Test] - summary: Update test definition - security: - - 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 +/api/test/{id}: + get: + tags: [Test] + summary: Get test definition by ID security: - bearerAuth: [] parameters: @@ -850,23 +712,168 @@ 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/tests.yaml#/TestDefinition' - '404': - description: Test not found - - delete: + '200': + description: Test definition details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/tests.yaml#/TestDefinition' + '404': + description: Test not found + + 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] summary: Soft delete test definition security: diff --git a/tests/feature/Test/TestCreateVariantsTest.php b/tests/feature/Test/TestCreateVariantsTest.php index c2acec8..aaa2c32 100644 --- a/tests/feature/Test/TestCreateVariantsTest.php +++ b/tests/feature/Test/TestCreateVariantsTest.php @@ -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 { $refnum = $this->buildRefNumEntries('THOLD', true); @@ -321,6 +367,8 @@ class TestCreateVariantsTest extends CIUnitTestCase 'AgeEnd' => 120, 'Flag' => 'N', 'Interpretation' => 'Normal range', + 'SpcType' => 'GEN', + 'Notes' => 'Default numeric range note', ], ]; @@ -337,6 +385,8 @@ class TestCreateVariantsTest extends CIUnitTestCase 'AgeEnd' => 99, 'Flag' => 'N', 'Interpretation' => 'Alternate range', + 'SpcType' => 'GEN', + 'Notes' => 'Alternate numeric range note', ]; }