Skip to content

Commit 650badf

Browse files
authored
VED-1252: Site & Route - In case of multiple lists of valid system, it's storing only 1st list and ignores rest all lists in Imms Event table (#1446)
* Refactor FhirService validation to use deep copy of immunization data - Updated `_validate_immunization` method to validate a deep copy of the immunization input, ensuring original data remains unchanged. - Enhanced test cases to validate against the modified structure, ensuring all SNOMED codings are persisted during creation and update of immunizations. * Add scenario to verify preservation of site and route codings in immunization event table - Introduced a new scenario in the create.feature file to validate that site and route codings are preserved in the imms event table when the first coding system is invalid. - Implemented corresponding step definitions in test_create_steps.py to create a valid JSON payload with multiple SNOMED codings and validate the response against expected outcomes. - Enhanced the test to ensure that the delta table uses the first valid SNOMED site and route coding and that all codings from the request are preserved in the imms event table.
1 parent 2925739 commit 650badf

4 files changed

Lines changed: 154 additions & 22 deletions

File tree

lambdas/backend/src/service/fhir_service.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,11 @@ def _normalize_single_snomed_codeable_concepts(cls, immunization: dict) -> None:
9797
field["coding"] = cls._keep_first_snomed_coding(coding)
9898

9999
def _validate_immunization(self, immunization: dict) -> None:
100-
self._normalize_single_snomed_codeable_concepts(immunization)
100+
immunization_to_validate = copy.deepcopy(immunization)
101+
self._normalize_single_snomed_codeable_concepts(immunization_to_validate)
101102

102103
try:
103-
self.validator.validate(immunization)
104+
self.validator.validate(immunization_to_validate)
104105
except (ValueError, MandatoryError) as error:
105106
raise CustomValidationError(message=str(error)) from error
106107

lambdas/backend/tests/service/test_fhir_service.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,19 @@
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
39+
def add_duplicate_snomed_with_leading_non_snomed_coding(
40+
immunization: dict, field_name: str, code: str, display: str
41+
) -> list:
42+
first_snomed_coding = deepcopy(immunization[field_name]["coding"][0])
43+
leading_non_snomed_coding = deepcopy(first_snomed_coding)
44+
leading_non_snomed_coding["system"] = "http://snomed.info/test"
45+
duplicate_snomed_coding = {**first_snomed_coding, "code": code, "display": display}
46+
immunization[field_name]["coding"] = [
47+
leading_non_snomed_coding,
48+
first_snomed_coding,
49+
duplicate_snomed_coding,
50+
]
51+
return deepcopy(immunization[field_name]["coding"])
4352

4453

4554
class TestFhirServiceBase(unittest.TestCase):
@@ -354,6 +363,7 @@ def test_create_immunization(self):
354363

355364
nhs_number = VALID_NHS_NUMBER
356365
req_imms = create_covid_immunization_dict_no_id(nhs_number)
366+
expected_validated_imms = deepcopy(req_imms)
357367

358368
# When
359369
created_id, created_version = self.fhir_service.create_immunization(req_imms, "Test")
@@ -366,30 +376,31 @@ def test_create_immunization(self):
366376
self.assertEqual(create_identifier.value, "ACME-vacc123456")
367377
self.imms_repo.create_immunization.assert_called_once_with(Immunization.parse_obj(req_imms), "Test")
368378

369-
self.validator.validate.assert_called_once_with(req_imms)
379+
self.validator.validate.assert_called_once_with(expected_validated_imms)
370380
self.assertEqual(self._MOCK_NEW_UUID, created_id)
371381
self.assertEqual(1, created_version)
372382

373-
def test_create_immunization_keeps_first_site_and_route_snomed_coding(self):
374-
"""it should keep the first SNOMED coding for site and route during API create"""
383+
def test_create_immunization_persists_all_site_and_route_codings(self):
384+
"""it should validate against the first SNOMED coding and persist all codings during API create"""
375385
self.mock_redis.hget.return_value = "COVID"
376386
self.mock_redis_getter.return_value = self.mock_redis
377387
self.authoriser.authorise.return_value = True
378388
self.imms_repo.get_immunization_by_identifier.return_value = (None, None)
379389
self.imms_repo.create_immunization.return_value = self._MOCK_NEW_UUID
380390

381391
req_imms = create_covid_immunization_dict_no_id(VALID_NHS_NUMBER)
382-
first_site_coding = add_snomed_coding(req_imms, "site", "999999999", "Replacement site that should be ignored")
383-
first_route_coding = add_snomed_coding(
392+
expected_site_codings = add_duplicate_snomed_with_leading_non_snomed_coding(
393+
req_imms, "site", "999999999", "Replacement site that should be ignored"
394+
)
395+
expected_route_codings = add_duplicate_snomed_with_leading_non_snomed_coding(
384396
req_imms, "route", "888888888", "Replacement route that should be ignored"
385397
)
386398

387399
created_id, created_version = self.pre_validate_fhir_service.create_immunization(req_imms, "Test")
388400

389401
self.assertEqual(self._MOCK_NEW_UUID, created_id)
390-
self.assertEqual(1, created_version)
391-
self.assertEqual(req_imms["site"]["coding"], [first_site_coding])
392-
self.assertEqual(req_imms["route"]["coding"], [first_route_coding])
402+
self.assertEqual(req_imms["site"]["coding"], expected_site_codings)
403+
self.assertEqual(req_imms["route"]["coding"], expected_route_codings)
393404
self.imms_repo.create_immunization.assert_called_once_with(Immunization.parse_obj(req_imms), "Test")
394405

395406
def test_create_immunization_with_id_throws_error(self):
@@ -511,6 +522,7 @@ def test_raises_duplicate_error_if_identifier_already_exits(self):
511522

512523
nhs_number = VALID_NHS_NUMBER
513524
req_imms = create_covid_immunization_dict_no_id(nhs_number)
525+
expected_validated_imms = deepcopy(req_imms)
514526

515527
# When
516528
with self.assertRaises(IdentifierDuplicationError) as error:
@@ -523,8 +535,7 @@ def test_raises_duplicate_error_if_identifier_already_exits(self):
523535
self.assertEqual(duplicate_identifier.system, "https://supplierABC/identifiers/vacc")
524536
self.assertEqual(duplicate_identifier.value, "ACME-vacc123456")
525537
self.imms_repo.create_immunization.assert_not_called()
526-
self.imms_repo.update_immunization.assert_not_called()
527-
self.validator.validate.assert_called_once_with(req_imms)
538+
self.validator.validate.assert_called_once_with(expected_validated_imms)
528539
self.assertEqual(
529540
"The provided identifier: https://supplierABC/identifiers/vacc#ACME-vacc123456 is duplicated",
530541
str(error.exception),
@@ -614,19 +625,19 @@ def test_update_immunization(self):
614625
self.assertEqual(call_args[3], "Test")
615626
self.authoriser.authorise.assert_called_once_with("Test", ApiOperationCode.UPDATE, {"COVID"})
616627

617-
def test_update_immunization_keeps_first_site_and_route_snomed_coding(self):
618-
"""it should keep the first SNOMED coding for site and route during API update"""
628+
def test_update_immunization_persists_all_site_and_route_codings(self):
629+
"""it should validate against the first SNOMED coding and persist all codings during API update"""
619630
imms_id = "an-id"
620631
original_immunisation = create_covid_immunization_dict(imms_id, VALID_NHS_NUMBER)
621632
identifier = Identifier(
622633
system=original_immunisation["identifier"][0]["system"],
623634
value=original_immunisation["identifier"][0]["value"],
624635
)
625636
updated_immunisation = create_covid_immunization_dict(imms_id, VALID_NHS_NUMBER, "2021-02-07T13:28:00+00:00")
626-
first_site_coding = add_snomed_coding(
637+
expected_site_codings = add_duplicate_snomed_with_leading_non_snomed_coding(
627638
updated_immunisation, "site", "999999999", "Replacement site that should be ignored"
628639
)
629-
first_route_coding = add_snomed_coding(
640+
expected_route_codings = add_duplicate_snomed_with_leading_non_snomed_coding(
630641
updated_immunisation, "route", "888888888", "Replacement route that should be ignored"
631642
)
632643
existing_resource_meta = ImmunizationRecordMetadata(
@@ -643,9 +654,11 @@ def test_update_immunization_keeps_first_site_and_route_snomed_coding(self):
643654
updated_version = self.fhir_service.update_immunization(imms_id, updated_immunisation, "Test", 1)
644655

645656
self.assertEqual(updated_version, 2)
646-
self.assertEqual(updated_immunisation["site"]["coding"], [first_site_coding])
647-
self.assertEqual(updated_immunisation["route"]["coding"], [first_route_coding])
657+
self.assertEqual(updated_immunisation["site"]["coding"], expected_site_codings)
658+
self.assertEqual(updated_immunisation["route"]["coding"], expected_route_codings)
648659
self.imms_repo.update_immunization.assert_called_once()
660+
call_args = self.imms_repo.update_immunization.call_args[0]
661+
self.assertEqual(call_args[1], Immunization.parse_obj(updated_immunisation))
649662

650663
def test_update_immunization_raises_validation_exception_when_nhs_number_invalid(self):
651664
"""it should raise a CustomValidationError when the patient's NHS number in the payload is invalid"""

tests/e2e_automation/features/APITests/create.feature

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ Feature: Create the immunization event for a patient
9292
And The terms are mapped to correct instance of coding.display fields in imms delta table
9393
And MNS event will not be triggered for the event
9494

95+
@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_Postman_Auth
96+
Scenario: Verify that site and route codings are preserved in imms event table when the first coding system is invalid
97+
Given Valid json payload is created where site and route have multiple SNOMED codings after an invalid system
98+
When Trigger the post create request
99+
Then The request will be successful with the status code '201'
100+
And The location key and Etag in header will contain the Immunization Id and version
101+
And The delta table will use the first valid SNOMED site and route coding
102+
And The imms event table will preserve every site and route coding from the request
103+
And MNS event will be triggered with correct data for created event
104+
95105
@smoke
96106
@Delete_cleanUp @vaccine_type_PERTUSSIS @patient_id_Random @supplier_name_EMIS
97107
Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are mapped to coding.display in imms delta table in case of only one instance of coding

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
parse_imms_int_imms_event_response,
1414
validate_imms_delta_record_with_created_event,
1515
)
16+
from src.objectModels.api_data_objects import CodeableConcept, Coding
1617
from src.objectModels.api_immunization_builder import (
1718
build_site_route,
1819
build_vaccine_procedure_code,
1920
build_vaccine_procedure_extension,
21+
create_extension,
2022
get_vaccine_details,
2123
)
2224
from utilities.api_fhir_immunization_helper import (
@@ -121,6 +123,81 @@ def createValidJsonPayloadWithProcedureMultipleCodingsDifferentSystem(context):
121123
context.immunization_object.route.coding[0].system = "http://example.com/different-system"
122124

123125

126+
def build_coding(system, code, display, value_string, value_id):
127+
return Coding(
128+
system=system,
129+
code=code,
130+
display=display,
131+
extension=[
132+
create_extension(
133+
"https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay",
134+
stringValue=value_string,
135+
),
136+
create_extension(
137+
"http://hl7.org/fhir/StructureDefinition/coding-sctdescid",
138+
idValue=value_id,
139+
),
140+
],
141+
)
142+
143+
144+
@given("Valid json payload is created where site and route have multiple SNOMED codings after an invalid system")
145+
def create_valid_json_payload_with_multiple_site_route_snomed_codings(context):
146+
valid_json_payload_is_created(context)
147+
context.immunization_object.site = CodeableConcept(
148+
coding=[
149+
build_coding(
150+
"http://snomed.info/test",
151+
"368208006",
152+
"Right upper arm structure (body structure)",
153+
"Test Value string site 111",
154+
"5306706018",
155+
),
156+
build_coding(
157+
"http://snomed.info/sct",
158+
"368209003",
159+
"Left upper arm structure (body structure)",
160+
"Test Value string site 222",
161+
"5306706020",
162+
),
163+
build_coding(
164+
"http://snomed.info/sct",
165+
"368208008",
166+
"Mid upper arm structure (body structure)",
167+
"Test Value string site 333",
168+
"5306706030",
169+
),
170+
],
171+
text="Test String for site",
172+
)
173+
context.immunization_object.route = CodeableConcept(
174+
coding=[
175+
build_coding(
176+
"http://snomed.info/test",
177+
"78421000",
178+
"Intramuscular route (qualifier value)",
179+
"Test Value string route 111",
180+
"5306706040",
181+
),
182+
build_coding(
183+
"http://snomed.info/sct",
184+
"78421000",
185+
"Intramuscular route (qualifier value)",
186+
"Test Value string route 222",
187+
"5306706050",
188+
),
189+
build_coding(
190+
"http://snomed.info/sct",
191+
"34206005",
192+
"Subcutaneous route (qualifier value)",
193+
"Test Value string route 333",
194+
"5306706060",
195+
),
196+
],
197+
text="Test String for route",
198+
)
199+
200+
124201
@given(
125202
"Valid json payload is created where vaccination terms has one instance of coding with no text or value string field"
126203
)
@@ -267,6 +344,37 @@ def validate_procedure_term_correct_coding_in_delta_table(context):
267344
)
268345

269346

347+
@then("The delta table will use the first valid SNOMED site and route coding")
348+
def validate_delta_table_uses_first_valid_snomed_site_route_coding(context):
349+
actual_terms = get_all_term_text(context)
350+
expected_site_term = context.create_object.site.coding[1].extension[0].valueString
351+
expected_route_term = context.create_object.route.coding[1].extension[0].valueString
352+
assert actual_terms["site_term"] == expected_site_term, (
353+
f"Expected site of vaccination term '{expected_site_term}', but got '{actual_terms['site_term']}'"
354+
)
355+
assert actual_terms["route_term"] == expected_route_term, (
356+
f"Expected route of vaccination term '{expected_route_term}', but got '{actual_terms['route_term']}'"
357+
)
358+
359+
360+
@then("The imms event table will preserve every site and route coding from the request")
361+
def validate_imms_event_table_preserves_all_site_route_codings(context):
362+
table_query_response = fetch_immunization_events_detail(context.aws_profile_name, context.ImmsID, context.S3_env)
363+
assert "Item" in table_query_response, f"Item not found in response for ImmsID: {context.ImmsID}"
364+
365+
resource_json_str = table_query_response["Item"].get("Resource")
366+
assert resource_json_str, "Resource field missing in item."
367+
resource = json.loads(resource_json_str)
368+
369+
for field_name in ("site", "route"):
370+
expected = context.request[field_name]
371+
actual = resource[field_name]
372+
assert len(actual["coding"]) == 3, (
373+
f"Expected {field_name}.coding to contain 3 entries, but got {len(actual['coding'])}"
374+
)
375+
assert actual == expected, f"Expected {field_name} to match request, but got {actual}"
376+
377+
270378
@then("The terms are mapped to correct coding.display fields in imms delta table")
271379
def validate_procedure_term_second_display_in_delta_table(context):
272380
actual_terms = get_all_term_text(context)

0 commit comments

Comments
 (0)