Skip to content

Commit 14a4b79

Browse files
Thomas-BoyleFimranNHSdependabot[bot]
authored
VED-318: Multiple entries of System - http://snomed.info/sct is not allowing for SITE & ROUTE but allowing for VACCINECODE and EXTENSION (#1405)
* Enhance FhirService to retain first SNOMED coding for site and route during immunization creation and update * Ved 318 updated automation test (#1419) * Bump aws-actions/amazon-ecr-login from 2.1.2 to 2.1.3 in the github-actions-minor-patch group (#1415) * Bump aws-actions/amazon-ecr-login Bumps the github-actions-minor-patch group with 1 update: [aws-actions/amazon-ecr-login](https://github.com/aws-actions/amazon-ecr-login). Updates `aws-actions/amazon-ecr-login` from 2.1.2 to 2.1.3 - [Release notes](https://github.com/aws-actions/amazon-ecr-login/releases) - [Changelog](https://github.com/aws-actions/amazon-ecr-login/blob/main/CHANGELOG.md) - [Commits](aws-actions/amazon-ecr-login@f2e9fc6...376925c) --- updated-dependencies: - dependency-name: aws-actions/amazon-ecr-login dependency-version: 2.1.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore: empty commit --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas-Boyle <thomasboyle@kainos.com> * Bump dompurify from 3.3.2 to 3.4.0 (#1417) * Bump dompurify from 3.3.2 to 3.4.0 Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.2 to 3.4.0. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.3.2...3.4.0) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * chore: empty commit --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas-Boyle <thomasboyle@kainos.com> * updated existing test --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas-Boyle <thomasboyle@kainos.com> * chore: empty commit --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: FimranNHS <fatima.imran3@nhs.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 20e026b commit 14a4b79

6 files changed

Lines changed: 229 additions & 29 deletions

File tree

lambdas/backend/src/service/fhir_service.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from authorisation.api_operation_code import ApiOperationCode
2323
from authorisation.authoriser import Authoriser
2424
from common.get_service_url import get_service_url
25-
from common.models.constants import Constants
25+
from common.models.constants import Constants, Urls
2626
from common.models.errors import (
2727
Code,
2828
CustomValidationError,
@@ -62,6 +62,7 @@ class FhirService:
6262
_DATA_MISSING_DATE_TIME_ERROR_MSG = (
6363
"Data quality issue - immunisation with ID %s was found containing no occurrenceDateTime"
6464
)
65+
_SINGLE_SNOMED_CODEABLE_CONCEPT_FIELDS = ("site", "route")
6566

6667
def __init__(
6768
self,
@@ -73,6 +74,36 @@ def __init__(
7374
self.immunization_repo = imms_repo
7475
self.validator = validator
7576

77+
@staticmethod
78+
def _keep_first_snomed_coding(coding: list) -> list:
79+
snomed_seen = False
80+
filtered_coding = []
81+
for coding_entry in coding:
82+
is_snomed_coding = isinstance(coding_entry, dict) and coding_entry.get("system") == Urls.SNOMED
83+
if is_snomed_coding and snomed_seen:
84+
continue
85+
86+
snomed_seen = snomed_seen or is_snomed_coding
87+
filtered_coding.append(coding_entry)
88+
89+
return filtered_coding
90+
91+
@classmethod
92+
def _normalize_single_snomed_codeable_concepts(cls, immunization: dict) -> None:
93+
for field_name in cls._SINGLE_SNOMED_CODEABLE_CONCEPT_FIELDS:
94+
field = immunization.get(field_name)
95+
coding = field.get("coding") if isinstance(field, dict) else None
96+
if isinstance(coding, list):
97+
field["coding"] = cls._keep_first_snomed_coding(coding)
98+
99+
def _validate_immunization(self, immunization: dict) -> None:
100+
self._normalize_single_snomed_codeable_concepts(immunization)
101+
102+
try:
103+
self.validator.validate(immunization)
104+
except (ValueError, MandatoryError) as error:
105+
raise CustomValidationError(message=str(error)) from error
106+
76107
def get_immunization_by_identifier(
77108
self, identifier: Identifier, supplier_name: str, elements: set[str] | None
78109
) -> FhirBundle:
@@ -117,10 +148,7 @@ def create_immunization(self, immunization: dict, supplier_system: str) -> Id:
117148
if immunization.get("id") is not None:
118149
raise CustomValidationError("id field must not be present for CREATE operation")
119150

120-
try:
121-
self.validator.validate(immunization)
122-
except (ValueError, MandatoryError) as error:
123-
raise CustomValidationError(message=str(error)) from error
151+
self._validate_immunization(immunization)
124152

125153
vaccination_type = get_vaccine_type(immunization)
126154

@@ -139,10 +167,7 @@ def create_immunization(self, immunization: dict, supplier_system: str) -> Id:
139167
return self.immunization_repo.create_immunization(immunization_fhir_entity, supplier_system)
140168

141169
def update_immunization(self, imms_id: str, immunization: dict, supplier_system: str, resource_version: int) -> int:
142-
try:
143-
self.validator.validate(immunization)
144-
except (ValueError, MandatoryError) as error:
145-
raise CustomValidationError(message=str(error)) from error
170+
self._validate_immunization(immunization)
146171

147172
immunization_to_update = Immunization.parse_obj(immunization)
148173

lambdas/backend/tests/service/test_fhir_service.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@
3636
NHS_NUMBER_USED_IN_SAMPLE_DATA = "9000000009"
3737

3838

39+
def add_snomed_coding(immunization: dict, field_name: str, code: str, display: str) -> dict:
40+
first_coding = deepcopy(immunization[field_name]["coding"][0])
41+
immunization[field_name]["coding"].append({"system": first_coding["system"], "code": code, "display": display})
42+
return first_coding
43+
44+
3945
class TestFhirServiceBase(unittest.TestCase):
4046
"""Base class for all tests to set up common fixtures"""
4147

@@ -362,6 +368,27 @@ def test_create_immunization(self):
362368
self.validator.validate.assert_called_once_with(req_imms)
363369
self.assertEqual(self._MOCK_NEW_UUID, created_id)
364370

371+
def test_create_immunization_keeps_first_site_and_route_snomed_coding(self):
372+
"""it should keep the first SNOMED coding for site and route during API create"""
373+
self.mock_redis.hget.return_value = "COVID"
374+
self.mock_redis_getter.return_value = self.mock_redis
375+
self.authoriser.authorise.return_value = True
376+
self.imms_repo.check_immunization_identifier_exists.return_value = False
377+
self.imms_repo.create_immunization.return_value = self._MOCK_NEW_UUID
378+
379+
req_imms = create_covid_immunization_dict_no_id(VALID_NHS_NUMBER)
380+
first_site_coding = add_snomed_coding(req_imms, "site", "999999999", "Replacement site that should be ignored")
381+
first_route_coding = add_snomed_coding(
382+
req_imms, "route", "888888888", "Replacement route that should be ignored"
383+
)
384+
385+
created_id = self.pre_validate_fhir_service.create_immunization(req_imms, "Test")
386+
387+
self.assertEqual(self._MOCK_NEW_UUID, created_id)
388+
self.assertEqual(req_imms["site"]["coding"], [first_site_coding])
389+
self.assertEqual(req_imms["route"]["coding"], [first_route_coding])
390+
self.imms_repo.create_immunization.assert_called_once_with(Immunization.parse_obj(req_imms), "Test")
391+
365392
def test_create_immunization_with_id_throws_error(self):
366393
"""it should throw exception if id present in create Immunization"""
367394
imms = create_covid_immunization_dict("an-id", "9990548609")
@@ -539,6 +566,39 @@ def test_update_immunization(self):
539566
self.assertEqual(call_args[3], "Test")
540567
self.authoriser.authorise.assert_called_once_with("Test", ApiOperationCode.UPDATE, {"COVID"})
541568

569+
def test_update_immunization_keeps_first_site_and_route_snomed_coding(self):
570+
"""it should keep the first SNOMED coding for site and route during API update"""
571+
imms_id = "an-id"
572+
original_immunisation = create_covid_immunization_dict(imms_id, VALID_NHS_NUMBER)
573+
identifier = Identifier(
574+
system=original_immunisation["identifier"][0]["system"],
575+
value=original_immunisation["identifier"][0]["value"],
576+
)
577+
updated_immunisation = create_covid_immunization_dict(imms_id, VALID_NHS_NUMBER, "2021-02-07T13:28:00+00:00")
578+
first_site_coding = add_snomed_coding(
579+
updated_immunisation, "site", "999999999", "Replacement site that should be ignored"
580+
)
581+
first_route_coding = add_snomed_coding(
582+
updated_immunisation, "route", "888888888", "Replacement route that should be ignored"
583+
)
584+
existing_resource_meta = ImmunizationRecordMetadata(
585+
identifier=identifier, resource_version=1, is_deleted=False, is_reinstated=False
586+
)
587+
588+
self.imms_repo.get_immunization_resource_and_metadata_by_id.return_value = (
589+
original_immunisation,
590+
existing_resource_meta,
591+
)
592+
self.imms_repo.update_immunization.return_value = 2
593+
self.authoriser.authorise.return_value = True
594+
595+
updated_version = self.fhir_service.update_immunization(imms_id, updated_immunisation, "Test", 1)
596+
597+
self.assertEqual(updated_version, 2)
598+
self.assertEqual(updated_immunisation["site"]["coding"], [first_site_coding])
599+
self.assertEqual(updated_immunisation["route"]["coding"], [first_route_coding])
600+
self.imms_repo.update_immunization.assert_called_once()
601+
542602
def test_update_immunization_raises_validation_exception_when_nhs_number_invalid(self):
543603
"""it should raise a CustomValidationError when the patient's NHS number in the payload is invalid"""
544604
imms_id = "an-id"

lambdas/recordforwarder/tests/service/test_fhir_batch_service.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
from test_common.testing_utils.immunization_utils import create_covid_immunization_dict_no_id
1111

1212

13+
def duplicate_first_coding(immunization: dict, field_name: str) -> None:
14+
immunization[field_name]["coding"].append(deepcopy(immunization[field_name]["coding"][0]))
15+
16+
1317
class TestFhirBatchServiceBase(unittest.TestCase):
1418
"""Base class for all tests to set up common fixtures"""
1519

@@ -97,6 +101,25 @@ def test_create_immunization_post_validation_error(self):
97101
self.assertTrue(expected_msg in error.exception.message)
98102
self.mock_repo.create_immunization.assert_not_called()
99103

104+
def test_create_immunization_duplicate_site_snomed_still_rejected_for_batch(self):
105+
"""it should keep batch validation unchanged for duplicate site SNOMED codings"""
106+
107+
imms = create_covid_immunization_dict_no_id()
108+
duplicate_first_coding(imms, "site")
109+
expected_msg = "Validation errors: site.coding[?(@.system=='http://snomed.info/sct')] must be unique"
110+
111+
with self.assertRaises(CustomValidationError) as error:
112+
self.pre_validate_fhir_service.create_immunization(
113+
immunization=imms,
114+
supplier_system="test_supplier",
115+
vax_type="test_vax",
116+
table=self.mock_table,
117+
imms_pk=None,
118+
)
119+
120+
self.assertEqual(expected_msg, error.exception.message)
121+
self.mock_repo.create_immunization.assert_not_called()
122+
100123

101124
class TestUpdateImmunizationBatchService(TestFhirBatchServiceBase):
102125
def setUp(self):

tests/e2e_automation/features/APITests/create.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Feature: Create the immunization event for a patient
7575
And MNS event will be triggered with correct data for created event
7676

7777
@Delete_cleanUp @vaccine_type_BCG @patient_id_InvalidInPDS @supplier_name_EMIS
78-
Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM fields are mapped to first instance of coding.display fields in imms delta table
78+
Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM , SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are mapped to first instance of coding.display fields in imms delta table
7979
Given Valid json payload is created where vaccination terms has multiple instances of coding
8080
When Trigger the post create request
8181
Then The request will be successful with the status code '201'

tests/e2e_automation/features/APITests/steps/test_create_steps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,13 @@ def createValidJsonPayloadWithProcedureMultipleCodings(context):
9494
valid_json_payload_is_created(context)
9595
procedures_list = get_all_the_vaccination_codes(VACCINATION_PROCEDURE_MAP[context.vaccine_type.upper()])
9696
product_list = get_all_the_vaccination_codes(VACCINE_CODE_MAP[context.vaccine_type.upper()])
97+
site_list = get_all_the_vaccination_codes(SITE_MAP)
98+
route_list = get_all_the_vaccination_codes(ROUTE_MAP)
9799

98100
context.immunization_object.extension[0].valueCodeableConcept.coding = procedures_list
99101
context.immunization_object.vaccineCode.coding = product_list
102+
context.immunization_object.site.coding = site_list
103+
context.immunization_object.route.coding = route_list
100104

101105

102106
@given(

0 commit comments

Comments
 (0)