From 2af4a030caf83f83e23f87d77c81dadb195da3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kras?= Date: Wed, 12 Apr 2023 00:11:47 +0200 Subject: [PATCH 1/3] HANS-298: Sort requirements --- src/convert_hl7v2_fhir/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/convert_hl7v2_fhir/requirements.txt b/src/convert_hl7v2_fhir/requirements.txt index 4108d8b..f205ffb 100644 --- a/src/convert_hl7v2_fhir/requirements.txt +++ b/src/convert_hl7v2_fhir/requirements.txt @@ -1,6 +1,6 @@ -hl7==0.4.5 aws_lambda_powertools==2.9.1 -pydantic==1.10.5 +boto3==1.26.104 fhir.resources==6.5.0 +hl7==0.4.5 Jinja2==3.1.2 -boto3==1.26.104 \ No newline at end of file +pydantic==1.10.5 From cadf5c8ed1a7fb31cdcd92884ae071acb98e5cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kras?= Date: Thu, 13 Apr 2023 19:19:44 +0200 Subject: [PATCH 2/3] HANS-298: Small refactor of is_nhs_number_valid --- src/convert_hl7v2_fhir/controllers/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/convert_hl7v2_fhir/controllers/utils.py b/src/convert_hl7v2_fhir/controllers/utils.py index 0d1bf96..4d9221c 100644 --- a/src/convert_hl7v2_fhir/controllers/utils.py +++ b/src/convert_hl7v2_fhir/controllers/utils.py @@ -1,5 +1,4 @@ def is_nhs_number_valid(nhs_number: str) -> bool: - # check length if len(nhs_number) != 10: return False @@ -7,10 +6,10 @@ def is_nhs_number_valid(nhs_number: str) -> bool: check_digit = nhs_number[9] main_part = nhs_number[0:9] - sum = 0 + sum_of_digits = 0 for digit_index in range(0, 9): - sum += (10 - digit_index) * int(main_part[digit_index]) - calculated_check_digit = 11 - (sum % 11) + sum_of_digits += (10 - digit_index) * int(main_part[digit_index]) + calculated_check_digit = 11 - (sum_of_digits % 11) if calculated_check_digit == 10: return False From b3cc2b27fad0f155f17d14c54bb8c463307d9386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kras?= Date: Fri, 14 Apr 2023 02:51:10 +0200 Subject: [PATCH 3/3] HANS-298: Refactor HL7v2 converter --- src/convert_hl7v2_fhir/app.py | 114 ++++---- .../controllers/convertor.py | 65 ----- .../controllers/hl7/__init__.py | 0 .../controllers/{ => hl7}/exceptions.py | 0 .../controllers/hl7/hl7_builder.py | 107 +++++++ .../hl7_conversions.py} | 37 +-- .../controllers/hl7/hl7_message_controller.py | 262 ++++++++++++++++++ .../controllers/hl7builder.py | 82 ------ .../controllers/hl7utils.py | 106 ------- .../controllers/templates/admit-bundle.json | 174 ------------ src/convert_hl7v2_fhir/requirements.txt | 1 - src/email_care_provider/app.py | 2 +- .../management_interface/settings.py | 2 +- src/nems_subscription_create/app.py | 2 +- .../controllers/verify_patient.py | 2 +- src/nems_subscription_delete/app.py | 2 +- .../convert_hl7v2_fhir/test_app.py | 154 +++++----- .../controllers/test_hl7conversions.py | 4 +- .../controllers/test_hl7utils.py | 8 +- 19 files changed, 535 insertions(+), 589 deletions(-) delete mode 100644 src/convert_hl7v2_fhir/controllers/convertor.py create mode 100644 src/convert_hl7v2_fhir/controllers/hl7/__init__.py rename src/convert_hl7v2_fhir/controllers/{ => hl7}/exceptions.py (100%) create mode 100644 src/convert_hl7v2_fhir/controllers/hl7/hl7_builder.py rename src/convert_hl7v2_fhir/controllers/{hl7conversions.py => hl7/hl7_conversions.py} (69%) create mode 100644 src/convert_hl7v2_fhir/controllers/hl7/hl7_message_controller.py delete mode 100644 src/convert_hl7v2_fhir/controllers/hl7builder.py delete mode 100644 src/convert_hl7v2_fhir/controllers/hl7utils.py delete mode 100644 src/convert_hl7v2_fhir/controllers/templates/admit-bundle.json diff --git a/src/convert_hl7v2_fhir/app.py b/src/convert_hl7v2_fhir/app.py index 0e3e5e6..80b1637 100644 --- a/src/convert_hl7v2_fhir/app.py +++ b/src/convert_hl7v2_fhir/app.py @@ -1,18 +1,22 @@ +import hl7 from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.typing import LambdaContext from boto3 import client from botocore.exceptions import ClientError, NoRegionError -import hl7 -from controllers.hl7builder import generate_ACK_message, v2ErrorCode, v2ErrorSeverity -from controllers.convertor import HL7v2ConversionController -from controllers.exceptions import ( +from controllers.hl7.hl7_message_controller import HL7MessageController +from controllers.hl7.exceptions import ( InvalidNHSNumberError, MissingNHSNumberError, MissingFieldOrComponentError, MissingSegmentError, ) -from internal_integrations.sqs.settings import SQSSettings +from controllers.hl7.hl7_builder import ( + generate_ack_message, + HL7ErrorCode, + HL7ErrorSeverity, +) +from internal_integrations.sqs.settings import get_sqs_settings _LOGGER = Logger() @@ -22,52 +26,52 @@ def lambda_handler(event: dict, context: LambdaContext): # hl7 messages expect \r rather than \r\n (and the parsing library) # will reject otherwise (with a KeyError) - msg_parsed = hl7.parse(body.replace("\n", "")) + hl7_message = hl7.parse(body.replace("\n", "")) - ack = _create_ack(msg_parsed) + ack_message = _create_ack_message(hl7_message) try: - fhir_json = _convert(msg_parsed) - _send_to_sqs(fhir_json) + fhir_bundle = HL7MessageController(hl7_message).to_fhir_bundle() + _send_to_sqs(fhir_bundle.json()) _LOGGER.info("Successfully processed message") except InvalidNHSNumberError as ex: _LOGGER.error(ex) - ack = _create_nak( - msg_parsed, - v2ErrorCode.DATA_TYPE_ERROR, - v2ErrorSeverity.ERROR, + ack_message = _create_nak( + hl7_message, + HL7ErrorCode.DATA_TYPE_ERROR, + HL7ErrorSeverity.ERROR, "NHS Number in message was invalid", ) except MissingNHSNumberError as ex: _LOGGER.error(ex) - ack = _create_nak( - msg_parsed, - v2ErrorCode.UNKNOWN_KEY_IDENTIFIER, - v2ErrorSeverity.ERROR, + ack_message = _create_nak( + hl7_message, + HL7ErrorCode.UNKNOWN_KEY_IDENTIFIER, + HL7ErrorSeverity.ERROR, "NHS Number missing from message", ) except MissingSegmentError as ex: _LOGGER.error(ex) - ack = _create_nak( - msg_parsed, - v2ErrorCode.SEGMENT_SEQUENCE_ERROR, - v2ErrorSeverity.ERROR, + ack_message = _create_nak( + hl7_message, + HL7ErrorCode.SEGMENT_SEQUENCE_ERROR, + HL7ErrorSeverity.ERROR, "Required segment was missing: " + str(ex), ) except MissingFieldOrComponentError as ex: _LOGGER.error(ex) - ack = _create_nak( - msg_parsed, - v2ErrorCode.REQUIRED_FIELD_MISSING, - v2ErrorSeverity.ERROR, + ack_message = _create_nak( + hl7_message, + HL7ErrorCode.REQUIRED_FIELD_MISSING, + HL7ErrorSeverity.ERROR, "Required field was missing: " + str(ex), ) except (ClientError, NoRegionError) as ex: _LOGGER.error(ex) - ack = _create_nak( - msg_parsed, - v2ErrorCode.APPLICATION_INTERNAL_ERROR, - v2ErrorSeverity.ERROR, + ack_message = _create_nak( + hl7_message, + HL7ErrorCode.APPLICATION_INTERNAL_ERROR, + HL7ErrorSeverity.ERROR, "Issue reaching SQS service: " + str(ex), ) except Exception as ex: @@ -76,53 +80,51 @@ def lambda_handler(event: dict, context: LambdaContext): # otherwise hospital system will not know we have had # an internal server error - we will log as error though _LOGGER.error(ex) - ack = _create_nak( - msg_parsed, - v2ErrorCode.APPLICATION_INTERNAL_ERROR, - v2ErrorSeverity.FATAL_ERROR, + ack_message = _create_nak( + hl7_message, + HL7ErrorCode.APPLICATION_INTERNAL_ERROR, + HL7ErrorSeverity.FATAL_ERROR, str(ex), ) return { "statusCode": 200, "headers": {"content-type": "x-application/hl7-v2+er; charset=utf-8"}, - "body": ack, + "body": ack_message, } def _send_to_sqs(body: str): sqs = client("sqs") - sqs_settings = SQSSettings() + sqs_settings = get_sqs_settings() sqs.send_message(QueueUrl=sqs_settings.converted_queue_url, MessageBody=body) def _create_nak( - msg_parsed: hl7.Message, err_code: str, err_sev: str, err_msg: str + hl7_message: hl7.Message, + error_code: HL7ErrorCode, + error_severity: HL7ErrorSeverity, + err_msg: str, ) -> str: - sending_app = msg_parsed["MSH"][0][3][0] - sending_facility = msg_parsed["MSH"][0][4][0] - msg_control_id = msg_parsed["MSH"][0][10][0] - return generate_ACK_message( - recipient_app=sending_app, - recipient_facility=sending_facility, + sending_application = hl7_message.extract_field("MSH", 0, 3, 0) + sending_facility = hl7_message.extract_field("MSH", 0, 4, 0) + msg_control_id = hl7_message.extract_field("MSH", 0, 10, 0) + return generate_ack_message( + receiving_application=sending_application, + receiving_facility=sending_facility, replying_to_msgid=msg_control_id, - hl7_error_code=err_code, - error_severity=err_sev, + hl7_error_code=error_code, + error_severity=error_severity, error_message=err_msg, ) -def _create_ack(msg_parsed: hl7.Message) -> str: - sending_app = msg_parsed["MSH"][0][3][0] - sending_facility = msg_parsed["MSH"][0][4][0] - msg_control_id = msg_parsed["MSH"][0][10][0] - return generate_ACK_message( - recipient_app=sending_app, - recipient_facility=sending_facility, +def _create_ack_message(hl7_message: hl7.Message) -> str: + sending_application = hl7_message.extract_field("MSH", 0, 3, 0) + sending_facility = hl7_message.extract_field("MSH", 0, 4, 0) + msg_control_id = hl7_message.extract_field("MSH", 0, 10, 0) + return generate_ack_message( + receiving_application=sending_application, + receiving_facility=sending_facility, replying_to_msgid=msg_control_id, ) - - -def _convert(v2msg: str) -> str: - convertor = HL7v2ConversionController() - return convertor.convert(v2msg) diff --git a/src/convert_hl7v2_fhir/controllers/convertor.py b/src/convert_hl7v2_fhir/controllers/convertor.py deleted file mode 100644 index b21bae0..0000000 --- a/src/convert_hl7v2_fhir/controllers/convertor.py +++ /dev/null @@ -1,65 +0,0 @@ -from fhir.resources.bundle import Bundle -from hl7 import Message -from jinja2 import Environment, PackageLoader, select_autoescape -from uuid import uuid4 - -from controllers.hl7utils import get_nhs_number, get_str -from controllers.hl7conversions import ( - to_fhir_date, - to_fhir_datetime, - to_fhir_admission_method, - to_fhir_encounter_class, -) - - -class HL7v2ConversionController: - def convert(self, v2msg_parsed: Message) -> Bundle: - env = Environment( - loader=PackageLoader("controllers.templates", ""), - autoescape=select_autoescape(["json"]), - ) - - env.filters["convert_date"] = to_fhir_date - env.filters["convert_datetime"] = to_fhir_datetime - env.filters["map_admissionmethod"] = to_fhir_admission_method - env.filters["map_encounterclass"] = to_fhir_encounter_class - - template = env.get_template("admit-bundle.json") - - # hopefully we'll be able to fill out the extra info - # based on which IP we receive the message from (and can - # perform a mapping from IP to below object) - metadata = { - "organization": { - "identifier": {"value": "XXX"}, - "name": "SIMULATED HOSPITAL NHS FOUNDATION TRUST", - }, - "location": { - "identifier": {"value": "XXXY1"}, - "address": {"postalCode": "XX20 5XX", "city": "Exampletown"}, - }, - } - - # generate a set of UUIDs for each resource in the Bundle - uuids = { - "messageHeader": uuid4(), - "patient": uuid4(), - "location": uuid4(), - "organization": uuid4(), - "encounter": uuid4(), - } - - final = template.render( - msg=v2msg_parsed, - uuid_method=uuid4, - get=get_str, - get_nhs_number=get_nhs_number, - meta=metadata, - uuids=uuids, - print=print, - ) - - # check validity by attempting parse - Bundle.parse_raw(str(final)) - - return final diff --git a/src/convert_hl7v2_fhir/controllers/hl7/__init__.py b/src/convert_hl7v2_fhir/controllers/hl7/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/convert_hl7v2_fhir/controllers/exceptions.py b/src/convert_hl7v2_fhir/controllers/hl7/exceptions.py similarity index 100% rename from src/convert_hl7v2_fhir/controllers/exceptions.py rename to src/convert_hl7v2_fhir/controllers/hl7/exceptions.py diff --git a/src/convert_hl7v2_fhir/controllers/hl7/hl7_builder.py b/src/convert_hl7v2_fhir/controllers/hl7/hl7_builder.py new file mode 100644 index 0000000..e5e5976 --- /dev/null +++ b/src/convert_hl7v2_fhir/controllers/hl7/hl7_builder.py @@ -0,0 +1,107 @@ +from datetime import datetime +from enum import Enum, IntEnum +from typing import Optional +from uuid import uuid4, UUID + + +class HL7ErrorCode(IntEnum): + """https://hl7-definition.caristix.com/v2/HL7v2.8/Tables/0357""" + + ACCEPTED = 0 + SEGMENT_SEQUENCE_ERROR = 100 + REQUIRED_FIELD_MISSING = 101 + DATA_TYPE_ERROR = 102 + TABLE_VALUE_NOT_FOUND = 103 + VALUE_TOO_LONG = 104 + UNSUPPORTED_MESSAGE_TYPE = 200 + UNSUPPORTED_EVENT_CODE = 201 + UNSUPPORTED_PROCESSING_ID = 202 + UNSUPPORTED_VERSION_ID = 203 + UNKNOWN_KEY_IDENTIFIER = 204 + DUPLICATE_KEY_IDENTIFIER = 205 + APPLICATION_RECORD_LOCKED = 206 + APPLICATION_INTERNAL_ERROR = 207 + + +class HL7ErrorSeverity(str, Enum): + """https://hl7-definition.caristix.com/v2/HL7v2.8/Tables/0516""" + + ERROR = "E" + FATAL_ERROR = "F" + INFORMATION = "I" + WARNING = "W" + + +def generate_ack_message( + receiving_application: str, + receiving_facility: str, + replying_to_msgid: str, + care_provider_email: Optional[str] = None, + care_provider_orgname: Optional[str] = None, + hl7_error_code: Optional[HL7ErrorCode] = None, + error_severity: Optional[str] = None, + error_message: Optional[str] = None, +): + """https://hl7-definition.caristix.com/v2/HL7v2.8/TriggerEvents/ACK""" + + message_header = _generate_msh_segment(receiving_application, receiving_facility) + + segment_err = "" + segment_zha = "" + accept_code = "" + + if hl7_error_code: + # generate an ACK based on rejecting the message and processing no further + accept_code = "AR" + # for ERR layout, see https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/ERR + segment_err = f"\rERR|||{hl7_error_code}|{error_severity}||||{error_message}" + else: + # generate an ACK based on accepting the message and sending care provider email as response + accept_code = "AA" + if care_provider_email: + # custom defined segment (see tech docs) + segment_zha = f"\rZHA|{care_provider_orgname}|{care_provider_email}" + + # see https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/MSA + segment_msa = f"MSA|{accept_code}|{replying_to_msgid}" + return message_header + "\r" + segment_msa + segment_err + segment_zha + + +def _generate_msh_segment( + receiving_application: str, + receiving_facility: str, + message_control_id: Optional[UUID] = None, +): + """https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/MSH""" + + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + msg_id = message_control_id or uuid4() + return f"MSH|^~\\&|HANS|NHSENGLAND|{receiving_application}|{receiving_facility}|{timestamp}||ACK^A01|{msg_id}|||||||||||||||" + + +def _generate_msa_segment( + accept_code: str, + message_control_id: str, +): + """https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/MSA""" + + return f"MSA|{accept_code}|{message_control_id}" + + +def _generate_err_segment( + error_code: HL7ErrorCode, + error_severity: HL7ErrorSeverity, + error_message: str, +): + """https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/ERR""" + + return f"ERR|||{error_code}|{error_severity}||||{error_message}" + + +def _generate_zha_segment( + care_provider_orgname: str, + care_provider_email: str, +): + """custom defined segment (see tech docs)""" + + return f"ZHA|{care_provider_orgname}|{care_provider_email}" diff --git a/src/convert_hl7v2_fhir/controllers/hl7conversions.py b/src/convert_hl7v2_fhir/controllers/hl7/hl7_conversions.py similarity index 69% rename from src/convert_hl7v2_fhir/controllers/hl7conversions.py rename to src/convert_hl7v2_fhir/controllers/hl7/hl7_conversions.py index cb7437d..3e727d8 100644 --- a/src/convert_hl7v2_fhir/controllers/hl7conversions.py +++ b/src/convert_hl7v2_fhir/controllers/hl7/hl7_conversions.py @@ -1,6 +1,5 @@ -from typing import Optional +from datetime import datetime -from controllers.exceptions import MissingFieldOrComponentError # note - this is pilot partner specific # so will need implementing with our pilot @@ -63,36 +62,26 @@ } -def to_fhir_date(hl7_DTM: Optional[str]) -> str: - if len(hl7_DTM) > 8: - hl7_DTM = hl7_DTM[:8] +def to_fhir_date(hl7_date: str) -> str: + return str(datetime.strptime(hl7_date[:8], "%Y%m%d").date()) - return hl7_DTM[0:4] + "-" + hl7_DTM[4:6] + "-" + hl7_DTM[6:8] - -def to_fhir_datetime(hl7_DTM: str) -> str: - if len(hl7_DTM) != 14: +def to_fhir_datetime(hl7_datetime: str) -> str: + if len(hl7_datetime) != 14: raise ValueError( - "Expected HL7v2 DTM (with time) of length 14 but recieved length " - + str(len(hl7_DTM)) + "Expected HL7v2 DTM (with time) of length 14 but received length " + + str(len(hl7_datetime)) + " instead" ) - return ( - to_fhir_date(hl7_DTM) - + "T" - + hl7_DTM[8:10] - + ":" - + hl7_DTM[10:12] - + ":" - + hl7_DTM[12:14] - + "Z" + return datetime.strptime(hl7_datetime, "%Y%m%d%H%M%S").strftime( + "%Y-%m-%dT%H:%M:%SZ" ) -def to_fhir_admission_method(hl7_CWE: str) -> str: - return ADMISSION_METHOD_MAP[hl7_CWE] +def to_fhir_admission_method(hl7_cwe: str) -> str: + return ADMISSION_METHOD_MAP[hl7_cwe] -def to_fhir_encounter_class(hl7_CWE: str) -> str: - return ENCOUNTER_CLASS_MAP[hl7_CWE] +def to_fhir_encounter_class(hl7_cwe: str) -> str: + return ENCOUNTER_CLASS_MAP[hl7_cwe] diff --git a/src/convert_hl7v2_fhir/controllers/hl7/hl7_message_controller.py b/src/convert_hl7v2_fhir/controllers/hl7/hl7_message_controller.py new file mode 100644 index 0000000..9b9689f --- /dev/null +++ b/src/convert_hl7v2_fhir/controllers/hl7/hl7_message_controller.py @@ -0,0 +1,262 @@ +import re +from typing import Any, Dict, Optional +from uuid import uuid4, UUID + +from fhir.resources.bundle import Bundle +from hl7 import Message + +from controllers.hl7.exceptions import ( + MissingSegmentError, + MissingFieldOrComponentError, + MissingNHSNumberError, + InvalidNHSNumberError, +) +from controllers.hl7.hl7_conversions import ( + to_fhir_date, + to_fhir_datetime, + to_fhir_admission_method, + to_fhir_encounter_class, +) +from controllers.utils import is_nhs_number_valid + + +class HL7MessageController: + def __init__( + self, + hl7_message: Message, + message_header_uuid: Optional[UUID] = None, + organization_uuid: Optional[UUID] = None, + encounter_uuid: Optional[UUID] = None, + patient_uuid: Optional[UUID] = None, + location_uuid: Optional[UUID] = None, + metadata: Optional[Dict[str, Any]] = None, + ): + self.hl7_message = hl7_message + self.message_header_uuid = message_header_uuid or uuid4() + self.organization_uuid = organization_uuid or uuid4() + self.encounter_uuid = encounter_uuid or uuid4() + self.patient_uuid = patient_uuid or uuid4() + self.location_uuid = location_uuid or uuid4() + # hopefully we'll be able to fill out the extra info + # based on which IP we receive the message from (and can + # perform a mapping from IP to below object) + self.metadata = metadata or { + "organization": { + "identifier": {"value": "XXX"}, + "name": "SIMULATED HOSPITAL NHS FOUNDATION TRUST", + }, + "location": { + "identifier": {"value": "XXXY1"}, + "address": {"postalCode": "XX20 5XX", "city": "Exampletown"}, + }, + } + + def to_fhir_bundle(self) -> Bundle: + bundle_json = { + "resourceType": "Bundle", + "type": "message", + "meta": { + "profile": [ + "https://fhir.simplifier.net/Hospital-Activity-Notification-Service/StructureDefinition/ActivityNotification-Bundle" + ] + }, + "entry": [ + self._create_header(), + self._create_patient(), + self._create_location(), + self._create_organization(), + self._create_encounter(), + ], + } + return Bundle(**bundle_json) + + def _extract_field(self, segment_name: str, *indexes: int) -> str: + try: + _field = self.hl7_message[segment_name] + for index in indexes: + _field = _field[index] + except KeyError: + raise MissingSegmentError(f"Required segment '{segment_name}' was missing.") + except IndexError: + field_indexes = ".".join(str(i) for i in indexes) + raise MissingFieldOrComponentError( + f"Required field '{segment_name}.{field_indexes}' was missing." + ) + return str(_field) + + def _create_header(self) -> Dict[str, Any]: + return { + "fullUrl": f"urn:uuid:{self.message_header_uuid}", + "resource": { + "resourceType": "MessageHeader", + "id": str(self.message_header_uuid), + "meta": { + "profile": [ + "https://fhir.simplifier.net/Hospital-Activity-Notification-Service/StructureDefinition/ActivityNotification-UKCore-MessageHeader" + ] + }, + "eventCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v2-0003", + "code": self._extract_field("EVN", 0, 1), + }, + "source": {"endpoint": "http://example.com/fhir/R4"}, + "responsible": {"reference": f"urn:uuid:{self.organization_uuid}"}, + "focus": [{"reference": f"urn:uuid:{self.encounter_uuid}"}], + }, + } + + def _create_patient(self) -> Dict[str, Any]: + nhs_number = self._extract_nhs_number() + family_name = self._extract_field("PID", 0, 5, 0, 0) + given_name = [ + name + for name in ( + self._extract_field("PID", 0, 5, 0, 1), + self._extract_field("PID", 0, 5, 0, 2), + ) + if name + ] + birth_date = to_fhir_date(self._extract_field("PID", 0, 7)) + + return { + "fullUrl": f"urn:uuid:{self.patient_uuid}", + "resource": { + "resourceType": "Patient", + "id": str(self.patient_uuid), + "meta": { + "profile": [ + "https://fhir.simplifier.net/Hospital-Activity-Notification-Service/StructureDefinition/ActivityNotification-UKCore-Patient" + ] + }, + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": nhs_number, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatusEngland", + "code": "01", + "display": "Number present and verified", + } + ] + }, + } + ], + } + ], + "name": [{"use": "usual", "family": family_name, "given": given_name}], + "birthDate": birth_date, + }, + } + + def _create_location(self): + location_name = [ + name + for name in ( + self._extract_field("PV1", 0, 3, 0, 0), + self._extract_field("PV1", 0, 3, 0, 3), + ) + if name + ] + return { + "fullUrl": f"urn:uuid:{self.location_uuid}", + "resource": { + "resourceType": "Location", + "id": str(self.location_uuid), + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/StructureDefinition/UKCore-Location" + ] + }, + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/ods-site-code", + "value": self.metadata["location"]["identifier"]["value"], + } + ], + "status": "active", + "name": ", ".join(location_name), + "address": { + "line": location_name, + "city": self.metadata["location"]["address"]["city"], + "postalCode": self.metadata["location"]["address"]["postalCode"], + }, + }, + } + + def _create_organization(self): + return { + "fullUrl": f"urn:uuid:{self.organization_uuid}", + "resource": { + "resourceType": "Organization", + "id": str(self.organization_uuid), + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/StructureDefinition/UKCore-Organization" + ] + }, + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": self.metadata["organization"]["identifier"]["value"], + } + ], + "name": self.metadata["organization"]["name"], + }, + } + + def _create_encounter(self): + resource_class = to_fhir_encounter_class(self._extract_field("PV1", 0, 2)) + resource_period_start = to_fhir_datetime(self._extract_field("PV1", 0, 44)) + admission_method_coding = to_fhir_admission_method( + self._extract_field("PV1", 0, 4) + ) + return { + "fullUrl": f"urn:uuid:{self.encounter_uuid}", + "resource": { + "resourceType": "Encounter", + "id": str(self.encounter_uuid), + "meta": { + "profile": [ + "https://fhir.simplifier.net/Hospital-Activity-Notification-Service/StructureDefinition/ActivityNotification-UKCore-Encounter" + ] + }, + "status": "in-progress", + "subject": {"reference": f"urn:uuid:{self.patient_uuid}"}, + "class": resource_class, + "period": {"start": resource_period_start}, + "location": [ + { + "status": "active", + "location": {"reference": f"urn:uuid:{self.location_uuid}"}, + } + ], + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AdmissionMethod", + "valueCodeableConcept": {"coding": [admission_method_coding]}, + } + ], + }, + } + + def _extract_nhs_number(self): + pid_segment = self._extract_field("PID") + potential_nhs_numbers = set(re.findall(r"\d{10}", pid_segment)) + if "NHSNMBR" not in pid_segment or not potential_nhs_numbers: + raise MissingNHSNumberError + + try: + return next( + ( + potential_nhs_number + for potential_nhs_number in potential_nhs_numbers + if is_nhs_number_valid(potential_nhs_number) + ) + ) + except StopIteration: + raise InvalidNHSNumberError diff --git a/src/convert_hl7v2_fhir/controllers/hl7builder.py b/src/convert_hl7v2_fhir/controllers/hl7builder.py deleted file mode 100644 index 8879054..0000000 --- a/src/convert_hl7v2_fhir/controllers/hl7builder.py +++ /dev/null @@ -1,82 +0,0 @@ -from enum import Enum -from datetime import datetime -from typing import Optional, Literal -from uuid import uuid4 - - -# enum of v2 error codes (from https://hl7-definition.caristix.com/v2/HL7v2.8/Tables/0357) -class v2ErrorCode(Enum): - ACCEPTED = "0" - SEGMENT_SEQUENCE_ERROR = "100" - REQUIRED_FIELD_MISSING = "101" - DATA_TYPE_ERROR = "102" - TABLE_VALUE_NOT_FOUND = "103" - VALUE_TOO_LONG = "104" - UNSUPPORTED_MESSAGE_TYPE = "200" - UNSUPPORTED_EVENT_CODE = "201" - UNSUPPORTED_PROCESSING_ID = "202" - UNSUPPORTED_VERSION_ID = "203" - UNKNOWN_KEY_IDENTIFIER = "204" - DUPLICATE_KEY_IDENTIFIER = "205" - APPLICATION_RECORD_LOCKED = "206" - APPLICATION_INTERNAL_ERROR = "207" - - def __str__(self): - return self.value - - -# enum of v2 severity codes (from https://hl7-definition.caristix.com/v2/HL7v2.8/Tables/0516) -class v2ErrorSeverity(Enum): - ERROR = "E" - FATAL_ERROR = "F" - INFORMATION = "I" - WARNING = "W" - - def __str__(self): - return self.value - - -def generate_MSH_segment( - recipient_app: str, - recipient_facility: str, - message_control_id: Optional[str] = None, -): - timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - msg_id = message_control_id or uuid4() - - # see https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/MSH - return f"MSH|^~\\&|HANS|NHSENGLAND|{recipient_app}|{recipient_facility}|{timestamp}||ACK^A01|{msg_id}|||||||||||||||" - - -# for ACK message structure see: https://hl7-definition.caristix.com/v2/HL7v2.8/TriggerEvents/ACK -def generate_ACK_message( - recipient_app: str, - recipient_facility: str, - replying_to_msgid: str, - care_provider_email: Optional[str] = None, - care_provider_orgname: Optional[str] = None, - hl7_error_code: Optional[str] = None, - error_severity: Optional[str] = None, - error_message: Optional[str] = None, -): - segment_msh = generate_MSH_segment(recipient_app, recipient_facility) - - segment_err = "" - segment_zha = "" - accept_code = "" - - if hl7_error_code: - # generate an ACK based on rejecting the message and processing no further - accept_code = "AR" - # for ERR layout, see https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/ERR - segment_err = f"\rERR|||{hl7_error_code}|{error_severity}||||{error_message}" - else: - # generate an ACK based on accepting the message and sending care provider email as response - accept_code = "AA" - if care_provider_email: - # custom defined segment (see tech docs) - segment_zha = f"\rZHA|{care_provider_orgname}|{care_provider_email}" - - # see https://hl7-definition.caristix.com/v2/HL7v2.8/Segments/MSA - segment_msa = f"MSA|{accept_code}|{replying_to_msgid}" - return segment_msh + "\r" + segment_msa + segment_err + segment_zha diff --git a/src/convert_hl7v2_fhir/controllers/hl7utils.py b/src/convert_hl7v2_fhir/controllers/hl7utils.py deleted file mode 100644 index d7c9a52..0000000 --- a/src/convert_hl7v2_fhir/controllers/hl7utils.py +++ /dev/null @@ -1,106 +0,0 @@ -from typing import Optional - -from hl7 import Message - -from controllers.exceptions import ( - MissingNHSNumberError, - InvalidNHSNumberError, - MissingFieldOrComponentError, - MissingSegmentError, -) -from controllers.utils import is_nhs_number_valid - - -def get( - msg: Message, - segment_type: str, - segment_repetition: int = 0, - field: Optional[int] = None, - repetition: Optional[int] = None, - component: Optional[int] = None, - sub_component: Optional[int] = None, -): - try: - seg = msg.segments(segment_type)[segment_repetition] - except KeyError: - raise MissingSegmentError(f"Required segment '{segment_type}' was missing.") - - if field is None: - return seg - else: - try: - fld = seg[field] - except IndexError: - raise MissingFieldOrComponentError( - f"Required field '{segment_type}.{field}' was missing." - ) - - if repetition is None: - return fld - else: - try: - rep = fld[repetition] - except IndexError: - raise MissingFieldOrComponentError( - f"Required field repetition '{segment_type}.{field}.{repetition}' was missing." - ) - if component is None: - return rep - else: - try: - cmp = rep[component] - except IndexError: - raise MissingFieldOrComponentError( - f"Required component '{segment_type}.{field}.{repetition}.{component}' was missing." - ) - if sub_component is None: - return cmp - else: - try: - sub_cmp = cmp[sub_component] - except IndexError: - raise MissingFieldOrComponentError( - f"Required sub-component '{segment_type}.{field}.{repetition}.{component}.{sub_component}' was missing." - ) - return sub_cmp if sub_cmp != "" else None - - -def get_str( - msg: Message, - segment_type: str, - segment_repetition: int = 0, - field: Optional[int] = None, - repetition: Optional[int] = None, - component: Optional[int] = None, - sub_component: Optional[int] = None, -) -> Optional[str]: - ret = get( - msg, - segment_type, - segment_repetition, - field, - repetition, - component, - sub_component, - ) - - return str(ret) if str(ret) != "" else None - - -def get_nhs_number(msg: Message) -> str: - ids = list(get(msg, "PID", 0, 3)) - - # need to also add on the single ID prior (this may or may not be a duplicate) - if get(msg, "PID", 0, 2, 0): - ids.append(get(msg, "PID", 0, 2, 0)) - - for id in ids: - if id[4][0] == "NHSNMBR": - nhs_num = id[0][0] - if not is_nhs_number_valid(nhs_num): - raise InvalidNHSNumberError - else: - return nhs_num - - # if none match that criteria - raise MissingNHSNumberError diff --git a/src/convert_hl7v2_fhir/controllers/templates/admit-bundle.json b/src/convert_hl7v2_fhir/controllers/templates/admit-bundle.json deleted file mode 100644 index a7e6fe0..0000000 --- a/src/convert_hl7v2_fhir/controllers/templates/admit-bundle.json +++ /dev/null @@ -1,174 +0,0 @@ -{ - "resourceType": "Bundle", - "type": "message", - "meta": { - "profile": [ - "https://fhir.simplifier.net/Hospital-Activity-Notification-Service/StructureDefinition/ActivityNotification-Bundle" - ] - }, - "entry": [ - { - "fullUrl": "urn:uuid:{{uuids.messageHeader}}", - "resource": { - "resourceType": "MessageHeader", - "id": "{{uuids.messageHeader}}", - "meta": { - "profile": [ - "https://fhir.simplifier.net/Hospital-Activity-Notification-Service/StructureDefinition/ActivityNotification-UKCore-MessageHeader" - ] - }, - "eventCoding": { - "system": "http://terminology.hl7.org/CodeSystem/v2-0003", - "code": "{{get(msg, 'EVN', 0, 1)}}" - }, - "source": { - "endpoint": "http://example.com/fhir/R4" - }, - "responsible": { - "reference": "urn:uuid:{{uuids.organization}}" - }, - "focus": [ - { - "reference": "urn:uuid:{{uuids.encounter}}" - } - ] - } - }, - { - "fullUrl": "urn:uuid:{{uuids.patient}}", - "resource": { - "resourceType": "Patient", - "id": "{{uuids.patient}}", - "meta": { - "profile": [ - "https://fhir.simplifier.net/Hospital-Activity-Notification-Service/StructureDefinition/ActivityNotification-UKCore-Patient" - ] - }, - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "{{ get_nhs_number(msg) }}", - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", - "valueCodeableConcept": { - "coding": [ - { - "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatusEngland", - "code": "01", - "display": "Number present and verified" - } - ] - } - } - ] - } - ], - "name": [ - { - "use": "usual", - "family": "{{get(msg, 'PID', 0, 5, 0, 0)}}"{% if get(msg, 'PID', 0, 5, 0, 1) %}, - "given": [ - "{{get(msg, 'PID', 0, 5, 0, 1)}}"{% if get(msg, 'PID', 0, 5, 0, 2) %}, - "{{get(msg, 'PID', 0, 5, 0, 2)}}" - {% endif %} - ]{% endif %} - } - ], - "birthDate": "{{get(msg, 'PID', 0, 7) | convert_date }}" - } - }, - { - "fullUrl": "urn:uuid:{{uuids.location}}", - "resource": { - "resourceType": "Location", - "id": "{{uuids.location}}", - "meta": { - "profile": [ - "https://fhir.hl7.org.uk/StructureDefinition/UKCore-Location" - ] - }, - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/ods-site-code", - "value": "{{ meta.location.identifier.value }}" - } - ], - "status": "active", - "name": "{{ get(msg, 'PV1', 0, 3, 0, 0) }}, {{ get(msg, 'PV1', 0, 3, 0, 3) }}", - "address": { - "line": [ - "{{get(msg, 'PV1', 0, 3, 0, 0)}}", - "{{get(msg, 'PV1', 0, 3, 0, 3)}}" - ], - "city": "{{ meta.location.address.city }}", - "postalCode": "{{ meta.location.address.postalCode }}" - } - } - }, - { - "fullUrl": "urn:uuid:{{uuids.organization}}", - "resource": { - "resourceType": "Organization", - "id": "{{uuids.organization}}", - "meta": { - "profile": [ - "https://fhir.hl7.org.uk/StructureDefinition/UKCore-Organization" - ] - }, - "identifier": [ - { - "system": "https://fhir.nhs.uk/Id/ods-organization-code", - "value": "{{ meta.organization.identifier.value }}" - } - ], - "name": "{{ meta.organization.name }}" - } - }, - { - "fullUrl": "urn:uuid:{{uuids.encounter}}", - "resource": { - "resourceType": "Encounter", - "id": "{{uuids.encounter}}", - "meta": { - "profile": [ - "https://fhir.simplifier.net/Hospital-Activity-Notification-Service/StructureDefinition/ActivityNotification-UKCore-Encounter" - ] - }, - "status": "in-progress", - "subject": { - "reference": "urn:uuid:{{uuids.patient}}" - }, - "class": { - "system": "{{(get(msg, 'PV1', 0, 2) | map_encounterclass).system}}", - "code": "{{(get(msg, 'PV1', 0, 2) | map_encounterclass).code}}", - "display": "{{(get(msg, 'PV1', 0, 2) | map_encounterclass).display}}" - }, - "period": { - "start": "{{get(msg, 'PV1', 0, 44) | convert_datetime}}" - }, - "location": [ - { - "status": "active", - "location": { - "reference": "urn:uuid:{{uuids.location}}" - } - } - ], - "extension": [ - { - "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AdmissionMethod", - "valueCodeableConcept": { - "coding": [ - { - "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AdmissionMethodEngland", - "code": "{{(get(msg, 'PV1', 0, 4) | map_admissionmethod).code}}" - } - ] - } - } - ] - } - } - ] -} \ No newline at end of file diff --git a/src/convert_hl7v2_fhir/requirements.txt b/src/convert_hl7v2_fhir/requirements.txt index f205ffb..7b91141 100644 --- a/src/convert_hl7v2_fhir/requirements.txt +++ b/src/convert_hl7v2_fhir/requirements.txt @@ -2,5 +2,4 @@ aws_lambda_powertools==2.9.1 boto3==1.26.104 fhir.resources==6.5.0 hl7==0.4.5 -Jinja2==3.1.2 pydantic==1.10.5 diff --git a/src/email_care_provider/app.py b/src/email_care_provider/app.py index 5aa0f84..a2f30b1 100644 --- a/src/email_care_provider/app.py +++ b/src/email_care_provider/app.py @@ -18,4 +18,4 @@ def lambda_handler(event: dict, context: LambdaContext): patient_birth_date=bundle.patient.birthDate, location_name=bundle.location.name, admitted_at=bundle.encounter.period.start, - ) \ No newline at end of file + ) diff --git a/src/email_care_provider/internal_integrations/management_interface/settings.py b/src/email_care_provider/internal_integrations/management_interface/settings.py index eee0368..4109f40 100644 --- a/src/email_care_provider/internal_integrations/management_interface/settings.py +++ b/src/email_care_provider/internal_integrations/management_interface/settings.py @@ -1,6 +1,6 @@ from functools import lru_cache -from pydantic import BaseSettings, HttpUrl +from pydantic import BaseSettings class ManagementInterfaceSettings(BaseSettings): diff --git a/src/nems_subscription_create/app.py b/src/nems_subscription_create/app.py index 4e6f325..bcfdfcb 100644 --- a/src/nems_subscription_create/app.py +++ b/src/nems_subscription_create/app.py @@ -5,7 +5,7 @@ from pydantic import ValidationError from pydantic.env_settings import SettingsError -from controllers.exceptions import ( +from controllers.hl7.exceptions import ( IncorrectNHSNumber, PatientNotFound, InternalError, diff --git a/src/nems_subscription_create/controllers/verify_patient.py b/src/nems_subscription_create/controllers/verify_patient.py index 9ee0c80..c6c2730 100644 --- a/src/nems_subscription_create/controllers/verify_patient.py +++ b/src/nems_subscription_create/controllers/verify_patient.py @@ -3,7 +3,7 @@ from fhir.resources.humanname import HumanName -from controllers.exceptions import ( +from controllers.hl7.exceptions import ( NameMissmatch, BirthDateMissmatch, IncorrectNHSNumber, diff --git a/src/nems_subscription_delete/app.py b/src/nems_subscription_delete/app.py index abfd370..23f7975 100644 --- a/src/nems_subscription_delete/app.py +++ b/src/nems_subscription_delete/app.py @@ -27,4 +27,4 @@ def lambda_handler(event: dict, context: LambdaContext): diagnostics="Missing subscription id in path parameters", ) - return {"statusCode": 200} \ No newline at end of file + return {"statusCode": 200} diff --git a/tests/integration/convert_hl7v2_fhir/test_app.py b/tests/integration/convert_hl7v2_fhir/test_app.py index fa4bd81..936abe9 100644 --- a/tests/integration/convert_hl7v2_fhir/test_app.py +++ b/tests/integration/convert_hl7v2_fhir/test_app.py @@ -1,103 +1,119 @@ -import pytest +from typing import Dict -from aws_lambda_powertools.utilities.typing import LambdaContext -from hl7 import parse +import hl7 -from app import lambda_handler, _send_to_sqs +from app import lambda_handler -msg_known_good = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN~2478684691^^^NHSNBR^NHSNMBR||Esterkin^AKI Scenario 6^^^Miss^^CURRENT||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|\rPV1|1|I|RenalWard^MainRoom^Bed 1^Simulated Hospital^^BED^MainBuilding^5|28b|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|||MED|||||||||6145914547062969032^^^^visitid||||||||||||||||||||||ARRIVED|||20200508130643||" +RAW_HL7_MESSAGE_GOOD = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN~2478684691^^^NHSNBR^NHSNMBR||Esterkin^AKI Scenario 6^^^Miss^^CURRENT||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|\rPV1|1|I|RenalWard^MainRoom^Bed 1^Simulated Hospital^^BED^MainBuilding^5|28b|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|||MED|||||||||6145914547062969032^^^^visitid||||||||||||||||||||||ARRIVED|||20200508130643||" +RAW_HL7_MESSAGE_INVALID_NHS_NUMBER = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN~247868469^^^NHSNBR^NHSNMBR||Esterkin^AKI Scenario 6^^^Miss^^CURRENT||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|\rPV1|1|I|RenalWard^MainRoom^Bed 1^Simulated Hospital^^BED^MainBuilding^5|28b|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|||MED|||||||||6145914547062969032^^^^visitid||||||||||||||||||||||ARRIVED|||20200508130643||" +RAW_HL7_MESSAGE_MISSING_NHS_NUMBER = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN||Esterkin^AKI Scenario 6^^^Miss^^CURRENT||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|\rPV1|1|I|RenalWard^MainRoom^Bed 1^Simulated Hospital^^BED^MainBuilding^5|28b|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|||MED|||||||||6145914547062969032^^^^visitid||||||||||||||||||||||ARRIVED|||20200508130643||" +RAW_HL7_MESSAGE_MISSING_SEGMENT = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN~2478684691^^^NHSNBR^NHSNMBR||Esterkin^AKI Scenario 6^^^Miss^^CURRENT||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|" +RAW_HL7_MESSAGE_MISSING_FIELD = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN~2478684691^^^NHSNBR^NHSNMBR||||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|\rPV1|1|I|RenalWard^MainRoom^Bed 1^Simulated Hospital^^BED^MainBuilding^5|28b|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|||MED|||||||||6145914547062969032^^^^visitid||||||||||||||||||||||ARRIVED|||20200508130643||" -msg_invalid_nhs_no = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN~247868469^^^NHSNBR^NHSNMBR||Esterkin^AKI Scenario 6^^^Miss^^CURRENT||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|\rPV1|1|I|RenalWard^MainRoom^Bed 1^Simulated Hospital^^BED^MainBuilding^5|28b|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|||MED|||||||||6145914547062969032^^^^visitid||||||||||||||||||||||ARRIVED|||20200508130643||" -msg_missing_nhs_no = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN||Esterkin^AKI Scenario 6^^^Miss^^CURRENT||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|\rPV1|1|I|RenalWard^MainRoom^Bed 1^Simulated Hospital^^BED^MainBuilding^5|28b|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|||MED|||||||||6145914547062969032^^^^visitid||||||||||||||||||||||ARRIVED|||20200508130643||" -msg_missing_segment = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN~2478684691^^^NHSNBR^NHSNMBR||Esterkin^AKI Scenario 6^^^Miss^^CURRENT||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|" -msg_missing_field = "MSH|^~\\&|SIMHOSP|SFAC|RAPP|RFAC|20200508130643||ADT^A01|5|T|2.3|||AL||44|ASCII\rEVN|A01|20200508130643|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|\rPID|1|2590157853^^^SIMULATOR MRN^MRN|2590157853^^^SIMULATOR MRN^MRN~2478684691^^^NHSNBR^NHSNMBR||||19890118000000|F|||170 Juice Place^^London^^RW21 6KC^GBR^HOME||020 5368 1665^HOME|||||||||R^Other - Chinese^^^||||||||\rPD1|||FAMILY PRACTICE^^12345|\rPV1|1|I|RenalWard^MainRoom^Bed 1^Simulated Hospital^^BED^MainBuilding^5|28b|||C006^Wolf^Kathy^^^Dr^^^DRNBR^PRSNL^^^ORGDR|||MED|||||||||6145914547062969032^^^^visitid||||||||||||||||||||||ARRIVED|||20200508130643||" +_DUMMY_LAMBDA_CONTEXT = { + "function_name": "test", + "function_memory_size": "test", + "function_arn": "test", + "function_request_id": "test", +} -def _create_dummy_context() -> dict: - return { - "function_name": "test", - "function_memory_size": "test", - "function_arn": "test", - "function_request_id": "test", - } +def _create_lambda_body(hl7_raw_message: str) -> Dict[str, str]: + return {"body": hl7_raw_message} -def _create_lambda_body(v2msg: str) -> dict: - return {"body": v2msg} +def test_lambda_handler__message_body_contains_ack(mocker): + # given + _send_to_sqs_mocked = mocker.patch("app._send_to_sqs") + event = _create_lambda_body(RAW_HL7_MESSAGE_GOOD) + # when + response = lambda_handler(event, _DUMMY_LAMBDA_CONTEXT) + message = hl7.parse(response["body"]) -def test_lambda_handler__ACK_message_in_body(mocker): - mocker.patch("app._send_to_sqs") - # given - resp = lambda_handler(_create_lambda_body(msg_known_good), _create_dummy_context()) - msg = parse(resp["body"]) - # test - assert msg["MSH"] - assert msg["MSA"] + # then + assert message["MSH"] + assert message["MSA"] -def test_lambda_handler__ACK_correct_recipient(mocker): - mocker.patch("app._send_to_sqs") +def test_lambda_handler__ack_correct_recipient(mocker): # given - resp = lambda_handler(_create_lambda_body(msg_known_good), _create_dummy_context()) - msg = parse(resp["body"]) - # test - assert msg["MSH"][0][5][0] == "SIMHOSP" - assert msg["MSH"][0][6][0] == "SFAC" + mocker.patch("app._send_to_sqs") + # when + response = lambda_handler( + _create_lambda_body(RAW_HL7_MESSAGE_GOOD), _DUMMY_LAMBDA_CONTEXT + ) + message = hl7.parse(response["body"]) -def test_lambda_handler__good_message_AA(mocker): - mocker.patch("app._send_to_sqs") + # then + assert message["MSH"][0][5][0] == "SIMHOSP" + assert message["MSH"][0][6][0] == "SFAC" + + +def test_lambda_handler__good_message_correct_accept_code(mocker): # given - resp = lambda_handler(_create_lambda_body(msg_known_good), _create_dummy_context()) - msg = parse(resp["body"]) - # test - assert msg["MSA"][0][1][0] == "AA" + mocker.patch("app._send_to_sqs") + + # when + response = lambda_handler( + _create_lambda_body(RAW_HL7_MESSAGE_GOOD), _DUMMY_LAMBDA_CONTEXT + ) + message = hl7.parse(response["body"]) + + # then + assert message["MSA"][0][1][0] == "AA" -def test_lambda_handler__invalid_nhs_num_AR(mocker): +def test_lambda_handler__correct_accept_code_for_invalid_nhs_number(mocker): mocker.patch("app._send_to_sqs") # given - resp = lambda_handler( - _create_lambda_body(msg_invalid_nhs_no), _create_dummy_context() + response = lambda_handler( + _create_lambda_body(RAW_HL7_MESSAGE_INVALID_NHS_NUMBER), _DUMMY_LAMBDA_CONTEXT ) - msg = parse(resp["body"]) - # test - assert msg["MSA"][0][1][0] == "AR" - assert msg["ERR"][0][3][0] == "102" + message = hl7.parse(response["body"]) + # then + assert message["MSA"][0][1][0] == "AR" + assert message["ERR"][0][3][0] == "102" -def test_lambda_handler__missing_nhs_num_AR(mocker): + +def test_lambda_handler__correct_accept_code_for_missing_nhs_number(mocker): mocker.patch("app._send_to_sqs") # given - resp = lambda_handler( - _create_lambda_body(msg_missing_nhs_no), _create_dummy_context() + response = lambda_handler( + _create_lambda_body(RAW_HL7_MESSAGE_MISSING_NHS_NUMBER), _DUMMY_LAMBDA_CONTEXT ) - msg = parse(resp["body"]) - # test - assert msg["MSA"][0][1][0] == "AR" - assert msg["ERR"][0][3][0] == "204" + message = hl7.parse(response["body"]) + + # then + assert message["MSA"][0][1][0] == "AR" + assert message["ERR"][0][3][0] == "204" def test_lambda_handler__missing_segment(mocker): - mocker.patch("app._send_to_sqs") # given - resp = lambda_handler( - _create_lambda_body(msg_missing_segment), _create_dummy_context() - ) - msg = parse(resp["body"]) - # test - assert msg["MSA"][0][1][0] == "AR" - assert msg["ERR"][0][3][0] == "100" + mocker.patch("app._send_to_sqs") + event = _create_lambda_body(RAW_HL7_MESSAGE_MISSING_SEGMENT) + + # when + response = lambda_handler(event, _DUMMY_LAMBDA_CONTEXT) + message = hl7.parse(response["body"]) + + # then + assert message["MSA"][0][1][0] == "AR" + assert message["ERR"][0][3][0] == "100" def test_lambda_handler__missing_field(mocker): - mocker.patch("app._send_to_sqs") # given - resp = lambda_handler( - _create_lambda_body(msg_missing_field), _create_dummy_context() - ) - msg = parse(resp["body"]) - # test - assert msg["MSA"][0][1][0] == "AR" - assert msg["ERR"][0][3][0] == "101" + mocker.patch("app._send_to_sqs") + event = _create_lambda_body(RAW_HL7_MESSAGE_MISSING_FIELD) + + # when + response = lambda_handler(event, _DUMMY_LAMBDA_CONTEXT) + message = hl7.parse(response["body"]) + + # then + assert message["MSA"][0][1][0] == "AR" + assert message["ERR"][0][3][0] == "101" diff --git a/tests/unit/convert_hl7v2_fhir/controllers/test_hl7conversions.py b/tests/unit/convert_hl7v2_fhir/controllers/test_hl7conversions.py index 072c52d..a06d030 100644 --- a/tests/unit/convert_hl7v2_fhir/controllers/test_hl7conversions.py +++ b/tests/unit/convert_hl7v2_fhir/controllers/test_hl7conversions.py @@ -1,6 +1,6 @@ import pytest -from controllers.hl7conversions import to_fhir_date, to_fhir_datetime +from controllers.hl7.hl7_conversions import to_fhir_date, to_fhir_datetime @pytest.mark.parametrize( @@ -11,7 +11,7 @@ def test_to_fhir_date__valid_DTM_converts_OK(input, expected): def test_to_fhir_datetime__invalid_DTM_raises_value_error(): - with pytest.raises(ValueError) as e_info: + with pytest.raises(ValueError): to_fhir_datetime("20230302") diff --git a/tests/unit/convert_hl7v2_fhir/controllers/test_hl7utils.py b/tests/unit/convert_hl7v2_fhir/controllers/test_hl7utils.py index 8cbd609..547bdd4 100644 --- a/tests/unit/convert_hl7v2_fhir/controllers/test_hl7utils.py +++ b/tests/unit/convert_hl7v2_fhir/controllers/test_hl7utils.py @@ -1,15 +1,13 @@ -import pytest - from controllers.utils import is_nhs_number_valid def test_is_nhs_number_valid__invalid_modulus_fails(): - assert is_nhs_number_valid("9999999993") == False + assert not is_nhs_number_valid("9999999993") def test_is_nhs_number_valid__invalid_length_fails(): - assert is_nhs_number_valid("1234") == False + assert not is_nhs_number_valid("1234") def test_is_nhs_number_valid__valid_number_passes(): - assert is_nhs_number_valid("9999999999") == True + assert is_nhs_number_valid("9999999999")