Skip to content

Commit 83202f6

Browse files
authored
VED-898: Refactor RecordAttributes so it handles Immunization FHIR object rather than a dictionary (#1259)
* Refactor RecordAttributes to use Immunization object and update related methods in FhirService and tests. This change enhances type safety and improves the handling of immunization data throughout the repository and service layers. * chore: empty commit EmptyCommit: * Refactor FhirService and validation_utils: Rename variable for clarity and enhance get_vaccine_type to accept Immunization model. Update references accordingly.
1 parent 4f9c904 commit 83202f6

5 files changed

Lines changed: 57 additions & 49 deletions

File tree

lambdas/backend/src/repository/fhir_repository.py

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,27 +66,30 @@ class RecordAttributes:
6666
pk: str
6767
patient_pk: str
6868
patient_sk: str
69-
resource: dict
7069
patient: dict
7170
vaccine_type: str
7271
timestamp: int
7372
identifier: str
74-
75-
def __init__(self, imms: dict, patient: any):
76-
"""Create attributes that may be used in dynamodb table"""
77-
imms_id = imms["id"]
78-
self.pk = _make_immunization_pk(imms_id)
79-
if patient or imms:
80-
nhs_number = get_nhs_number(imms)
81-
self.patient_pk = _make_patient_pk(nhs_number)
82-
self.patient = patient
83-
self.resource = imms
84-
self.timestamp = int(time.time())
85-
self.vaccine_type = get_vaccine_type(imms)
86-
self.system_id = imms["identifier"][0]["system"]
87-
self.system_value = imms["identifier"][0]["value"]
88-
self.patient_sk = f"{self.vaccine_type}#{imms_id}"
89-
self.identifier = f"{self.system_id}#{self.system_value}"
73+
immunization: Immunization
74+
75+
@classmethod
76+
def from_immunization(cls, immunization: Immunization, patient: dict | None = None) -> "RecordAttributes":
77+
"""Build DynamoDB attributes from a FHIR Immunization resource."""
78+
imms_dict = immunization.dict()
79+
patient_resolved = patient if patient is not None else get_contained_patient(imms_dict)
80+
nhs_number = get_nhs_number(imms_dict)
81+
vaccine_type = get_vaccine_type(imms_dict)
82+
first_identifier = immunization.identifier[0]
83+
return cls(
84+
pk=_make_immunization_pk(immunization.id),
85+
patient_pk=_make_patient_pk(nhs_number),
86+
patient_sk=f"{vaccine_type}#{immunization.id}",
87+
patient=patient_resolved,
88+
vaccine_type=vaccine_type,
89+
timestamp=int(time.time()),
90+
identifier=f"{first_identifier.system}#{first_identifier.value}",
91+
immunization=immunization,
92+
)
9093

9194

9295
class ImmunizationRepository:
@@ -162,10 +165,7 @@ def check_immunization_identifier_exists(self, system: str, unique_id: str) -> b
162165

163166
def create_immunization(self, immunization: Immunization, supplier_system: str) -> Id:
164167
"""Creates a new immunization record returning the unique id if successful."""
165-
immunization_as_dict = immunization.dict()
166-
167-
patient = get_contained_patient(immunization_as_dict)
168-
attr = RecordAttributes(immunization_as_dict, patient)
168+
attr = RecordAttributes.from_immunization(immunization)
169169

170170
response = self.table.put_item(
171171
Item={
@@ -188,13 +188,11 @@ def create_immunization(self, immunization: Immunization, supplier_system: str)
188188
def update_immunization(
189189
self,
190190
imms_id: str,
191-
immunization: dict,
191+
immunization: Immunization,
192192
existing_record_meta: ImmunizationRecordMetadata,
193193
supplier_system: str,
194194
) -> int:
195-
# VED-898 - consider refactoring to pass FHIR Immunization object rather than dict between Service -> Repository
196-
patient = get_contained_patient(immunization)
197-
attr = RecordAttributes(immunization, patient)
195+
attr = RecordAttributes.from_immunization(immunization)
198196
reinstate_operation_required = existing_record_meta.is_deleted
199197

200198
update_exp = self._build_update_expression(is_reinstate=reinstate_operation_required)
@@ -245,7 +243,7 @@ def _perform_dynamo_update(
245243
":timestamp": attr.timestamp,
246244
":patient_pk": attr.patient_pk,
247245
":patient_sk": attr.patient_sk,
248-
":imms_resource_val": json.dumps(attr.resource, use_decimal=True),
246+
":imms_resource_val": attr.immunization.json(use_decimal=True),
249247
":operation": "UPDATE",
250248
":version": updated_version,
251249
":supplier_system": supplier_system,

lambdas/backend/src/service/fhir_service.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,34 +143,31 @@ def update_immunization(self, imms_id: str, immunization: dict, supplier_system:
143143
except (ValueError, MandatoryError) as error:
144144
raise CustomValidationError(message=str(error)) from error
145145

146+
immunization_to_update = Immunization.parse_obj(immunization)
147+
146148
existing_immunization_resource, existing_immunization_meta = (
147149
self.immunization_repo.get_immunization_resource_and_metadata_by_id(imms_id, include_deleted=True)
148150
)
149151

150152
if not existing_immunization_resource:
151153
raise ResourceNotFoundError(resource_type="Immunization", resource_id=imms_id)
152154

153-
# If the user is updating the resource vaccination_type, they must have permissions for both the existing and
154-
# new type. In most cases it will be the same, but it is possible for users to update the vacc type
155+
existing_immunization = Immunization.parse_obj(existing_immunization_resource)
156+
155157
if not self.authoriser.authorise(
156158
supplier_system,
157159
ApiOperationCode.UPDATE,
158-
{get_vaccine_type(immunization), get_vaccine_type(existing_immunization_resource)},
160+
{get_vaccine_type(immunization_to_update), get_vaccine_type(existing_immunization)},
159161
):
160162
raise UnauthorizedVaxError()
161163

162-
identifier = Identifier.construct(
163-
system=immunization["identifier"][0]["system"],
164-
value=immunization["identifier"][0]["value"],
165-
)
166-
167-
validate_identifiers_match(identifier, existing_immunization_meta.identifier)
164+
validate_identifiers_match(immunization_to_update.identifier[0], existing_immunization_meta.identifier)
168165

169166
if not existing_immunization_meta.is_deleted:
170167
validate_resource_versions_match(resource_version, existing_immunization_meta.resource_version, imms_id)
171168

172169
return self.immunization_repo.update_immunization(
173-
imms_id, immunization, existing_immunization_meta, supplier_system
170+
imms_id, immunization_to_update, existing_immunization_meta, supplier_system
174171
)
175172

176173
def delete_immunization(self, imms_id: str, supplier_system: str) -> None:

lambdas/backend/tests/repository/test_fhir_repository.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ def test_update_immunisation_is_successful(self):
391391
"""it should update the immunisation record"""
392392
imms_id = "an-imms-id"
393393
imms = create_covid_immunization_dict(imms_id, VALID_NHS_NUMBER)
394+
immunization = Immunization.parse_obj(imms)
394395
identifier = Identifier(system=imms["identifier"][0]["system"], value=imms["identifier"][0]["value"])
395396
existing_record_metadata = ImmunizationRecordMetadata(
396397
identifier=identifier, resource_version=1, is_deleted=False, is_reinstated=False
@@ -400,7 +401,7 @@ def test_update_immunisation_is_successful(self):
400401
self.table.update_item = MagicMock(return_value=dynamo_response)
401402

402403
# When
403-
updated_version = self.repository.update_immunization(imms_id, imms, existing_record_metadata, "Test")
404+
updated_version = self.repository.update_immunization(imms_id, immunization, existing_record_metadata, "Test")
404405

405406
# Then
406407
update_exp = (
@@ -420,7 +421,7 @@ def test_update_immunisation_is_successful(self):
420421
":timestamp": ANY,
421422
":patient_pk": _make_patient_pk(patient_id),
422423
":patient_sk": patient_sk,
423-
":imms_resource_val": json.dumps(imms),
424+
":imms_resource_val": immunization.json(use_decimal=True),
424425
":operation": "UPDATE",
425426
":version": 2,
426427
":supplier_system": "Test",
@@ -433,6 +434,7 @@ def test_update_immunisation_is_successful_when_record_needs_to_be_reinstated(se
433434
"""it should reinstate a deleted record when requested via the update operation"""
434435
imms_id = "an-imms-id"
435436
imms = create_covid_immunization_dict(imms_id, VALID_NHS_NUMBER)
437+
immunization = Immunization.parse_obj(imms)
436438
identifier = Identifier(system=imms["identifier"][0]["system"], value=imms["identifier"][0]["value"])
437439
existing_record_metadata = ImmunizationRecordMetadata(
438440
identifier=identifier, resource_version=2, is_deleted=True, is_reinstated=False
@@ -442,7 +444,7 @@ def test_update_immunisation_is_successful_when_record_needs_to_be_reinstated(se
442444
self.table.update_item = MagicMock(return_value=dynamo_response)
443445

444446
# When
445-
updated_version = self.repository.update_immunization(imms_id, imms, existing_record_metadata, "Test")
447+
updated_version = self.repository.update_immunization(imms_id, immunization, existing_record_metadata, "Test")
446448

447449
# Then
448450
update_exp = (
@@ -462,7 +464,7 @@ def test_update_immunisation_is_successful_when_record_needs_to_be_reinstated(se
462464
":timestamp": ANY,
463465
":patient_pk": _make_patient_pk(patient_id),
464466
":patient_sk": patient_sk,
465-
":imms_resource_val": json.dumps(imms),
467+
":imms_resource_val": immunization.json(use_decimal=True),
466468
":operation": "UPDATE",
467469
":version": 3,
468470
":supplier_system": "Test",
@@ -477,6 +479,7 @@ def test_update_throws_error_when_response_can_not_be_handled(self):
477479
condition, as a check is made first to retrieve the record."""
478480
imms_id = "an-id"
479481
imms = create_covid_immunization_dict(imms_id, VALID_NHS_NUMBER)
482+
immunization = Immunization.parse_obj(imms)
480483
identifier = Identifier(system=imms["identifier"][0]["system"], value=imms["identifier"][0]["value"])
481484
existing_record_metadata = ImmunizationRecordMetadata(
482485
identifier=identifier, resource_version=2, is_deleted=True, is_reinstated=False
@@ -489,7 +492,7 @@ def test_update_throws_error_when_response_can_not_be_handled(self):
489492

490493
with self.assertRaises(ResourceNotFoundError) as e:
491494
# When
492-
self.repository.update_immunization(imms_id, imms, existing_record_metadata, "Test")
495+
self.repository.update_immunization(imms_id, immunization, existing_record_metadata, "Test")
493496

494497
# Then
495498
self.assertEqual(str(e.exception), "Immunization resource does not exist. ID: an-id")
@@ -728,9 +731,10 @@ def run_update_immunization_test(self, imms_id, imms, updated_dose_quantity=None
728731
existing_record_metadata = ImmunizationRecordMetadata(
729732
identifier=identifier, resource_version=1, is_deleted=False, is_reinstated=False
730733
)
734+
immunization = Immunization.parse_obj(imms)
731735

732736
# When
733-
updated_version = self.repository.update_immunization(imms_id, imms, existing_record_metadata, "Test")
737+
updated_version = self.repository.update_immunization(imms_id, immunization, existing_record_metadata, "Test")
734738
self.assertEqual(updated_version, 2)
735739

736740
update_exp = (
@@ -750,7 +754,7 @@ def run_update_immunization_test(self, imms_id, imms, updated_dose_quantity=None
750754
":timestamp": ANY,
751755
":patient_pk": _make_patient_pk(patient_id),
752756
":patient_sk": patient_sk,
753-
":imms_resource_val": json.dumps(imms, use_decimal=True),
757+
":imms_resource_val": immunization.json(use_decimal=True),
754758
":operation": "UPDATE",
755759
":version": 2,
756760
":supplier_system": "Test",

lambdas/backend/tests/service/test_fhir_service.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,9 +529,13 @@ def test_update_immunization(self):
529529
self.imms_repo.get_immunization_resource_and_metadata_by_id.assert_called_once_with(
530530
imms_id, include_deleted=True
531531
)
532-
self.imms_repo.update_immunization.assert_called_once_with(
533-
imms_id, updated_immunisation, existing_resource_meta, "Test"
534-
)
532+
self.imms_repo.update_immunization.assert_called_once()
533+
call_args = self.imms_repo.update_immunization.call_args[0]
534+
self.assertEqual(call_args[0], imms_id)
535+
self.assertIsInstance(call_args[1], Immunization)
536+
self.assertEqual(call_args[1].id, imms_id)
537+
self.assertEqual(call_args[2], existing_resource_meta)
538+
self.assertEqual(call_args[3], "Test")
535539
self.authoriser.authorise.assert_called_once_with("Test", ApiOperationCode.UPDATE, {"COVID"})
536540

537541
def test_update_immunization_raises_validation_exception_when_nhs_number_invalid(self):

lambdas/shared/src/common/models/utils/validation_utils.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Utils for backend folder"""
22

3+
from typing import Any
4+
35
from fhir.resources.R4B.identifier import Identifier
46

57
from common.models.constants import RedisHashKeys, Urls
@@ -61,11 +63,14 @@ def convert_disease_codes_to_vaccine_type(
6163
return vaccine_type
6264

6365

64-
def get_vaccine_type(immunization: dict):
66+
def get_vaccine_type(immunization: dict | Any) -> str:
6567
"""
66-
Take a FHIR immunization resource and returns the vaccine type based on the combination of target diseases.
67-
If combination of disease types does not map to a valid vaccine type, a value error is raised
68+
Take a FHIR immunization resource (dict or Immunization model) and returns the vaccine type
69+
based on the combination of target diseases. If combination of disease types does not map
70+
to a valid vaccine type, a value error is raised.
6871
"""
72+
if not isinstance(immunization, dict):
73+
immunization = immunization.dict()
6974
# Obtain list of target diseases
7075
try:
7176
target_diseases = get_target_disease_codes(immunization)

0 commit comments

Comments
 (0)