Skip to content

Commit d31c250

Browse files
authored
Merge branch 'master' into VED-1138--update-automation-test
2 parents e68e787 + 650badf commit d31c250

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)