diff --git a/lib/unified_health_data/adapters/lab_or_test_adapter.rb b/lib/unified_health_data/adapters/lab_or_test_adapter.rb index 863f9161d042..1f6f8fe6025c 100644 --- a/lib/unified_health_data/adapters/lab_or_test_adapter.rb +++ b/lib/unified_health_data/adapters/lab_or_test_adapter.rb @@ -36,6 +36,64 @@ def initialize(mr_log: nil) 'LP29684-5' => 'Radiology' }.freeze + # + # Interpretation code map based on https://terminology.hl7.org/3.1.0/CodeSystem-v3-ObservationInterpretation.html + # + + INTERPRETATION_MAP = { + 'CAR' => 'Carrier', + 'CARRIER' => 'Carrier', + '<' => 'Off scale low', + '>' => 'Off scale high', + 'A' => 'Abnormal', + 'AA' => 'Critical abnormal', + 'AC' => 'Anti-complementary substances present', + 'B' => 'Better', + 'D' => 'Significant change down', + 'DET' => 'Detected', + 'E' => 'Equivocal', + 'EX' => 'Outside threshold', + 'EXP' => 'Expected', + 'H' => 'High', + 'H*' => 'Critical high', + 'HH' => 'Critical high', + 'HU' => 'Significantly high', + 'H>' => 'Significantly high', + 'HM' => 'Hold for Medical Review', + 'HX' => 'Above high threshold', + 'I' => 'Intermediate', + 'IE' => 'Insufficient evidence', + 'IND' => 'Indeterminate', + 'L' => 'Low', + 'L*' => 'Critical low', + 'LL' => 'Critical low', + 'LU' => 'Significantly low', + 'L<' => 'Significantly low', + 'LX' => 'Below low threshold', + 'MS' => 'Moderately susceptible', + 'N' => 'Normal', + 'NCL' => 'No CLSI defined breakpoint', + 'ND' => 'Not detected', + 'NEG' => 'Negative', + 'NR' => 'Non-reactive', + 'NS' => 'Non-susceptible', + 'OBX' => 'Interpretation qualifiers in separate OBX segments', + 'POS' => 'Positive', + 'QCF' => 'Quality control failure', + 'R' => 'Resistant', + 'RR' => 'Reactive', + 'S' => 'Susceptible', + 'SDD' => 'Susceptible-dose dependent', + 'SYN-R' => 'Synergy - resistant', + 'SYN-S' => 'Synergy - susceptible', + 'TOX' => 'Cytotoxic substance present', + 'U' => 'Significant change up', + 'UNE' => 'Unexpected', + 'VS' => 'Very susceptible', + 'W' => 'Worse', + 'WR' => 'Weakly reactive' + }.freeze + def parse_labs(records) return [] if records.blank? @@ -466,17 +524,29 @@ def extract_interpretation(obs) return nil if interpretations.blank? fallback_text = nil + fallback_code = nil + interpretations.each do |interp| if interp['coding'].present? hl7_coding = interp['coding'].find do |coding| coding['system']&.include?('v3-ObservationInterpretation') && coding['code'].present? end - return hl7_coding['code'] if hl7_coding + if hl7_coding + # Priority 1: Mapped human-friendly display from INTERPRETATION_MAP + mapped = INTERPRETATION_MAP[hl7_coding['code']] + return mapped if mapped.present? + + # Capture raw code as absolute last resort fallback + fallback_code ||= hl7_coding['code'] + end end - fallback_text ||= interp['text'] if interp['text'].present? + + # Priority 2: text or coding.display from the CodeableConcept + fallback_text ||= extract_codeable_concept_display(interp) end - fallback_text + # Prefer human-readable text/display over raw code + fallback_text || fallback_code end def format_observation_value(obs) diff --git a/modules/mobile/docs/openapi.json b/modules/mobile/docs/openapi.json index 8bacf51bdaff..d77b0db69273 100644 --- a/modules/mobile/docs/openapi.json +++ b/modules/mobile/docs/openapi.json @@ -28526,7 +28526,7 @@ "properties": { "type": { "type": "string", - "example": "labsAndTests" + "example": "DiagnosticReport" }, "id": { "type": "string", @@ -28537,49 +28537,158 @@ "type": "object", "additionalProperties": false, "required": [ - "category", - "subject", - "effectiveDateTime", - "issued", - "code", + "display", + "testCode", "status" ], "properties": { - "category": { + "display": { "type": "string", - "example": "LAB" + "description": "User-friendly display name for the lab or test", + "example": "CHEM 7" }, - "subject": { + "testCode": { "type": "string", - "example": "Patient/1234567" + "description": "HL7 v2-0074 diagnostic service section code or LOINC code", + "example": "CH" }, - "effectiveDateTime": { + "testCodeDisplay": { "type": "string", - "format": "date-time", - "example": "2021-01-01T00:00:00Z" + "nullable": true, + "description": "Human-readable display name for the test code", + "example": "Chemistry and hematology" }, - "issued": { + "dateCompleted": { "type": "string", - "format": "date-time", - "example": "2021-01-01T00:00:00Z" + "nullable": true, + "description": "Date/time the test was completed, in facility local time when available", + "example": "2025-01-23T15:01:52-07:00" }, - "code": { - "type": "object", - "properties": { - "coding": { - "type": "array", - "items": { - "type": "object" - } - }, - "text": { - "type": "string" - } + "sampleTested": { + "type": "string", + "nullable": true, + "description": "Specimen type used for the test", + "example": "BLOOD" + }, + "encodedData": { + "type": "string", + "nullable": true, + "description": "Base64-encoded presentedForm data (e.g., radiology report text)", + "example": "" + }, + "location": { + "type": "string", + "nullable": true, + "description": "Name of the performing organization or location", + "example": "CHYSHR TEST LAB" + }, + "orderedBy": { + "type": "string", + "nullable": true, + "description": "Name of the ordering practitioner", + "example": "John Doe" + }, + "bodySite": { + "type": "string", + "nullable": true, + "description": "Body site related to the test, if applicable", + "example": "Left arm" + }, + "comments": { + "type": "array", + "nullable": true, + "description": "Comments from the DiagnosticReport or ServiceRequest", + "items": { + "type": "string" } }, "status": { "type": "string", + "description": "FHIR status of the DiagnosticReport", "example": "final" + }, + "source": { + "type": "string", + "nullable": true, + "description": "Data source identifier (e.g., vista or oracle-health)", + "example": "vista" + }, + "facilityTimezone": { + "type": "string", + "nullable": true, + "description": "IANA timezone ID for the facility where the test was performed", + "example": "America/Denver" + }, + "observations": { + "type": "array", + "nullable": true, + "description": "Individual test result observations within this report", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "testCode": { + "type": "string", + "description": "Name of the individual observation test", + "example": "GLUCOSE" + }, + "value": { + "type": "object", + "nullable": true, + "description": "The observation result value", + "properties": { + "text": { + "type": "string", + "nullable": true, + "description": "Formatted value text", + "example": "99.0 mg/dL" + }, + "type": { + "type": "string", + "nullable": true, + "description": "Value type (quantity, codeable-concept, string, date-time)", + "example": "quantity" + } + } + }, + "referenceRange": { + "type": "string", + "nullable": true, + "description": "Reference range for the observation", + "example": "70 - 110 mg/dL" + }, + "status": { + "type": "string", + "description": "FHIR status of the observation", + "example": "final" + }, + "interpretation": { + "type": "string", + "nullable": true, + "description": "Human-readable interpretation display string mapped from HL7 v3 ObservationInterpretation codes (e.g., High, Low, Critical high). Falls back to CodeableConcept text or coding display if code is unmapped.", + "example": "High" + }, + "comments": { + "type": "array", + "description": "Notes attached to the observation", + "items": { + "type": "string" + } + }, + "bodySite": { + "type": "string", + "nullable": true, + "description": "Body site for the observation", + "example": "" + }, + "sampleTested": { + "type": "string", + "nullable": true, + "description": "Specimen type for the observation", + "example": "" + } + } + } } } } diff --git a/modules/mobile/docs/schemas/v1/LabsAndTestsV1.yml b/modules/mobile/docs/schemas/v1/LabsAndTestsV1.yml index 7830ae586a5d..0ae5b3664551 100644 --- a/modules/mobile/docs/schemas/v1/LabsAndTestsV1.yml +++ b/modules/mobile/docs/schemas/v1/LabsAndTestsV1.yml @@ -16,7 +16,7 @@ properties: properties: type: type: string - example: labsAndTests + example: DiagnosticReport id: type: string example: abe3f152-90b0-45cb-8776-4958bad0e0ef @@ -25,39 +25,132 @@ properties: type: object additionalProperties: false required: - - category - - subject - - effectiveDateTime - - issued - - code + - display + - testCode - status properties: - category: + display: type: string - example: LAB - subject: + description: User-friendly display name for the lab or test + example: CHEM 7 + testCode: type: string - example: Patient/1234567 - effectiveDateTime: + description: HL7 v2-0074 diagnostic service section code or LOINC code + example: CH + testCodeDisplay: type: string - format: date-time - example: 2021-01-01T00:00:00Z - issued: + nullable: true + description: Human-readable display name for the test code + example: Chemistry and hematology + dateCompleted: type: string - format: date-time - example: 2021-01-01T00:00:00Z - code: - type: object - properties: - coding: - type: array - items: - type: object - text: - type: string + nullable: true + description: Date/time the test was completed, in facility local time when available + example: '2025-01-23T15:01:52-07:00' + sampleTested: + type: string + nullable: true + description: Specimen type used for the test + example: BLOOD + encodedData: + type: string + nullable: true + description: Base64-encoded presentedForm data (e.g., radiology report text) + example: '' + location: + type: string + nullable: true + description: Name of the performing organization or location + example: CHYSHR TEST LAB + orderedBy: + type: string + nullable: true + description: Name of the ordering practitioner + example: John Doe + bodySite: + type: string + nullable: true + description: Body site related to the test, if applicable + example: Left arm + comments: + type: array + nullable: true + description: Comments from the DiagnosticReport or ServiceRequest + items: + type: string status: type: string + description: FHIR status of the DiagnosticReport example: final + source: + type: string + nullable: true + description: Data source identifier (e.g., vista or oracle-health) + example: vista + facilityTimezone: + type: string + nullable: true + description: IANA timezone ID for the facility where the test was performed + example: America/Denver + observations: + type: array + nullable: true + description: Individual test result observations within this report + items: + type: object + additionalProperties: false + properties: + testCode: + type: string + description: Name of the individual observation test + example: GLUCOSE + value: + type: object + nullable: true + description: The observation result value + properties: + text: + type: string + nullable: true + description: Formatted value text + example: 99.0 mg/dL + type: + type: string + nullable: true + description: Value type (quantity, codeable-concept, string, date-time) + example: quantity + referenceRange: + type: string + nullable: true + description: Reference range for the observation + example: 70 - 110 mg/dL + status: + type: string + description: FHIR status of the observation + example: final + interpretation: + type: string + nullable: true + description: >- + Human-readable interpretation display string mapped from HL7 v3 + ObservationInterpretation codes (e.g., High, Low, Critical high). + Falls back to CodeableConcept text or coding display if code is unmapped. + example: High + comments: + type: array + description: Notes attached to the observation + items: + type: string + bodySite: + type: string + nullable: true + description: Body site for the observation + example: '' + sampleTested: + type: string + nullable: true + description: Specimen type for the observation + example: '' meta: type: object additionalProperties: false diff --git a/spec/lib/unified_health_data/adapters/lab_or_test_adapter_spec.rb b/spec/lib/unified_health_data/adapters/lab_or_test_adapter_spec.rb index 75ae49a46413..f5a38636e7d1 100644 --- a/spec/lib/unified_health_data/adapters/lab_or_test_adapter_spec.rb +++ b/spec/lib/unified_health_data/adapters/lab_or_test_adapter_spec.rb @@ -2759,7 +2759,7 @@ end describe '#extract_interpretation' do - it 'extracts the HL7 v3 ObservationInterpretation code when present' do + it 'returns the mapped display string for HL7 v3 ObservationInterpretation code when present' do obs = { 'code' => { 'text' => 'Glucose' }, 'interpretation' => [ @@ -2783,7 +2783,7 @@ ] } result = adapter.send(:extract_interpretation, obs) - expect(result).to eq('H') + expect(result).to eq('High') end it 'extracts critical high (HH) code' do @@ -2803,7 +2803,7 @@ ] } result = adapter.send(:extract_interpretation, obs) - expect(result).to eq('HH') + expect(result).to eq('Critical high') end it 'extracts Low (L) code' do @@ -2823,7 +2823,7 @@ ] } result = adapter.send(:extract_interpretation, obs) - expect(result).to eq('L') + expect(result).to eq('Low') end it 'extracts critical low (LL) code' do @@ -2843,7 +2843,7 @@ ] } result = adapter.send(:extract_interpretation, obs) - expect(result).to eq('LL') + expect(result).to eq('Critical low') end it 'extracts Abnormal (A) code' do @@ -2870,7 +2870,7 @@ ] } result = adapter.send(:extract_interpretation, obs) - expect(result).to eq('A') + expect(result).to eq('Abnormal') end it 'falls back to interpretation text when no HL7 v3 coding is present' do @@ -2921,7 +2921,7 @@ expect(result).to be_nil end - it 'prefers HL7 v3 code from a later entry over text from an earlier entry' do + it 'prefers HL7 v3 mapped display from a later entry over text from an earlier entry' do obs = { 'code' => { 'text' => 'Glucose' }, 'interpretation' => [ @@ -2950,7 +2950,63 @@ ] } result = adapter.send(:extract_interpretation, obs) - expect(result).to eq('H') + expect(result).to eq('High') + end + + it 'falls back to HL7 coding display when code is not in INTERPRETATION_MAP' do + obs = { + 'code' => { 'text' => 'Some Test' }, + 'interpretation' => [ + { + 'coding' => [ + { + 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation', + 'code' => 'UNKNOWN_FUTURE_CODE', + 'display' => 'Some New Interpretation' + } + ] + } + ] + } + result = adapter.send(:extract_interpretation, obs) + expect(result).to eq('Some New Interpretation') + end + + it 'prefers interpretation text over raw HL7 code when code is not in INTERPRETATION_MAP' do + obs = { + 'code' => { 'text' => 'Some Test' }, + 'interpretation' => [ + { + 'coding' => [ + { + 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation', + 'code' => 'UNKNOWN_FUTURE_CODE' + } + ], + 'text' => 'Vendor Specific Interpretation' + } + ] + } + result = adapter.send(:extract_interpretation, obs) + expect(result).to eq('Vendor Specific Interpretation') + end + + it 'falls back to raw HL7 code when code is not in INTERPRETATION_MAP and no display or text exists' do + obs = { + 'code' => { 'text' => 'Some Test' }, + 'interpretation' => [ + { + 'coding' => [ + { + 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation', + 'code' => 'UNKNOWN_FUTURE_CODE' + } + ] + } + ] + } + result = adapter.send(:extract_interpretation, obs) + expect(result).to eq('UNKNOWN_FUTURE_CODE') end end @@ -2975,7 +3031,7 @@ ] } result = adapter.send(:build_observation, obs, []) - expect(result.interpretation).to eq('H') + expect(result.interpretation).to eq('High') end it 'sets interpretation to nil when not present in FHIR data' do @@ -3063,7 +3119,7 @@ end end - describe '#parse_labs' do + describe '#parse_labs with filtering' do context 'with multiple records with mixed statuses' do it 'filters records and returns only those with allowed statuses' do records = [