Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions lambdas/backend/src/repository/fhir_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import boto3
import botocore.exceptions
import simplejson as json
from boto3.dynamodb.conditions import Attr, Key
from boto3.dynamodb.conditions import Attr, ConditionBase, Key
from botocore.config import Config
from fhir.resources.R4B.fhirtypes import Id
from fhir.resources.R4B.identifier import Identifier
Expand Down Expand Up @@ -299,39 +299,38 @@ def delete_immunization(self, imms_id: str, supplier_system: str) -> None:
else:
raise error

def find_immunizations(self, patient_identifier: str, vaccine_types: set) -> list[dict]:
condition = Key("PatientPK").eq(_make_patient_pk(patient_identifier))
is_not_deleted = Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated")
def search_immunizations(self, patient_identifier: str, vaccine_types: set) -> list[dict]:
"""Searches for immunisations by patient identifier (NHS Number) and the vaccination type"""
nhs_number_key_condition = Key("PatientPK").eq(_make_patient_pk(patient_identifier))
vacc_type_condition = self._build_vacc_type_key_condition(vaccine_types)
filter_condition = vacc_type_condition & (Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated"))

ieds_resources = self.get_all_items(condition, is_not_deleted)
ieds_resources = self.get_all_items(nhs_number_key_condition, filter_condition)

if not ieds_resources:
return []

# Filter the response to contain only the requested vaccine types
filtered_ieds_resources = [x for x in ieds_resources if self._vaccine_type(x["PatientSK"]) in vaccine_types]

# Return a list of the FHIR immunization resource JSON items
final_resources = [
{
**json.loads(item["Resource"]),
"meta": {"versionId": int(item.get("Version", 1))},
}
for item in filtered_ieds_resources
for item in ieds_resources
]

return final_resources

def get_all_items(self, condition, is_not_deleted):
def get_all_items(self, key_condition: ConditionBase, filter_condition: ConditionBase):
"""Query DynamoDB and paginate through all results."""
all_items = []
last_evaluated_key = None

while True:
query_args = {
"IndexName": "PatientGSI",
"KeyConditionExpression": condition,
"FilterExpression": is_not_deleted,
"KeyConditionExpression": key_condition,
"FilterExpression": filter_condition,
}
if last_evaluated_key:
query_args["ExclusiveStartKey"] = last_evaluated_key
Expand All @@ -349,6 +348,16 @@ def get_all_items(self, condition, is_not_deleted):

return all_items

@staticmethod
def _build_vacc_type_key_condition(vacc_types: set) -> ConditionBase:
vacc_type_condition = None

for vacc_type in vacc_types:
key_cond = Attr("PatientSK").begins_with(vacc_type)
vacc_type_condition = key_cond if vacc_type_condition is None else vacc_type_condition | key_cond

return vacc_type_condition

@staticmethod
def _vaccine_type(patient_sk: str) -> str:
parsed = [str.strip(s) for s in patient_sk.split("#")]
Expand Down
2 changes: 1 addition & 1 deletion lambdas/backend/src/service/fhir_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def search_immunizations(
raise UnauthorizedVaxError()

# Have to retrieve first and then inspect resource to filter by date
all_resources = self.immunization_repo.find_immunizations(nhs_number, permitted_vacc_types)
all_resources = self.immunization_repo.search_immunizations(nhs_number, permitted_vacc_types)
filtered_resources = self._filter_search_results_by_date_and_status(
immunizations=all_resources, date_from=date_from, date_to=date_to, status=Constants.COMPLETED_STATUS
)
Expand Down
17 changes: 9 additions & 8 deletions lambdas/backend/tests/repository/test_fhir_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,13 +596,14 @@ def test_find_immunizations_returns_empty_list_when_not_found(self):
self.table.query = MagicMock(return_value=dynamo_response)

# When
result = self.repository.find_immunizations(nhs_number, vaccine_types={"COVID"})
result = self.repository.search_immunizations(nhs_number, vaccine_types={"COVID"})

# Then
self.table.query.assert_called_once_with(
IndexName="PatientGSI",
KeyConditionExpression=Key("PatientPK").eq(_make_patient_pk(nhs_number)),
FilterExpression=Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated"),
FilterExpression=Attr("PatientSK").begins_with("COVID")
& (Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated")),
)
self.assertEqual(result, [])

Expand All @@ -627,7 +628,7 @@ def test_find_immunizations_returns_resources_including_meta(self):
self.table.query = MagicMock(return_value=dynamo_response)

# When
results = self.repository.find_immunizations("an-id", {"COVID"})
results = self.repository.search_immunizations("an-id", {"COVID"})

# Then
self.assertListEqual(results, [imms1, imms2])
Expand All @@ -650,19 +651,19 @@ def test_find_immunizations_filters_any_vacc_types_not_in_the_request(self):
},
{
"Resource": json.dumps(imms3),
"PatientSK": "MMR#some_other_text",
"PatientSK": "FLU#some_other_text",
"Version": "4",
},
]

dynamo_response = {"ResponseMetadata": {"HTTPStatusCode": 200}, "Items": items}
self.table.query = MagicMock(return_value=dynamo_response)
self.table.query = MagicMock(side_effect=[dynamo_response])

# When
results = self.repository.find_immunizations("an-id", {"COVID", "FLU"})
results = self.repository.search_immunizations("an-id", {"COVID", "FLU"})

# Then
self.assertListEqual(results, [imms1, imms2])
self.assertListEqual(results, [imms1, imms2, imms3])

def test_bad_response_from_dynamo(self):
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
Expand All @@ -672,7 +673,7 @@ def test_bad_response_from_dynamo(self):

with self.assertRaises(UnhandledResponseError) as e:
# When
self.repository.find_immunizations("an-id", {"COVID"})
self.repository.search_immunizations("an-id", {"COVID"})

# Then
self.assertDictEqual(e.exception.response, response)
Expand Down
24 changes: 12 additions & 12 deletions lambdas/backend/tests/service/test_fhir_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,15 +752,15 @@ def test_search_immunizations_returns_results_as_a_search_bundle(self, mock_uuid
mock_resource = create_covid_immunization_dict("1234-some-id")
vaccine_type = "COVID"
self.authoriser.filter_permitted_vacc_types.return_value = {vaccine_type}
self.imms_repo.find_immunizations.return_value = [mock_resource]
self.imms_repo.search_immunizations.return_value = [mock_resource]

# When
result = self.fhir_service.search_immunizations(
VALID_NHS_NUMBER, {vaccine_type}, self.MOCK_SUPPLIER_SYSTEM_NAME, None, None, None
)

# Then
self.imms_repo.find_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
self.imms_repo.search_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
mock_uuid.assert_called_once()
self.authoriser.filter_permitted_vacc_types.assert_called_once_with(
self.MOCK_SUPPLIER_SYSTEM_NAME, ApiOperationCode.SEARCH, {"COVID"}
Expand Down Expand Up @@ -797,7 +797,7 @@ def test_search_immunizations_filters_by_date_and_status(self, mock_uuid):

vaccine_type = "COVID"
self.authoriser.filter_permitted_vacc_types.return_value = {vaccine_type}
self.imms_repo.find_immunizations.return_value = [
self.imms_repo.search_immunizations.return_value = [
mock_resource_filtered_date_from,
mock_resource,
mock_resource_filtered_date_to,
Expand All @@ -815,7 +815,7 @@ def test_search_immunizations_filters_by_date_and_status(self, mock_uuid):
)

# Then
self.imms_repo.find_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
self.imms_repo.search_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
mock_uuid.assert_called_once()
self.authoriser.filter_permitted_vacc_types.assert_called_once_with(
self.MOCK_SUPPLIER_SYSTEM_NAME, ApiOperationCode.SEARCH, {"COVID"}
Expand Down Expand Up @@ -848,15 +848,15 @@ def test_search_immunizations_adds_include_to_searched_url(self, mock_uuid):
mock_resource = create_covid_immunization_dict("1234-some-id")
vaccine_type = "COVID"
self.authoriser.filter_permitted_vacc_types.return_value = {vaccine_type}
self.imms_repo.find_immunizations.return_value = [mock_resource]
self.imms_repo.search_immunizations.return_value = [mock_resource]

# When
result = self.fhir_service.search_immunizations(
VALID_NHS_NUMBER, {vaccine_type}, self.MOCK_SUPPLIER_SYSTEM_NAME, None, None, "Patient.identifier"
)

# Then
self.imms_repo.find_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
self.imms_repo.search_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
mock_uuid.assert_called_once()
self.authoriser.filter_permitted_vacc_types.assert_called_once_with(
self.MOCK_SUPPLIER_SYSTEM_NAME, ApiOperationCode.SEARCH, {"COVID"}
Expand Down Expand Up @@ -884,15 +884,15 @@ def test_search_immunizations_returns_empty_bundle_when_no_results_found(self):
"""it should return an empty search bundle when no results are found"""
vaccine_type = "FLU"
self.authoriser.filter_permitted_vacc_types.return_value = {vaccine_type}
self.imms_repo.find_immunizations.return_value = []
self.imms_repo.search_immunizations.return_value = []

# When
result = self.fhir_service.search_immunizations(
VALID_NHS_NUMBER, {vaccine_type}, self.MOCK_SUPPLIER_SYSTEM_NAME, None, None, None
)

# Then
self.imms_repo.find_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
self.imms_repo.search_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
self.authoriser.filter_permitted_vacc_types.assert_called_once_with(
self.MOCK_SUPPLIER_SYSTEM_NAME, ApiOperationCode.SEARCH, {vaccine_type}
)
Expand Down Expand Up @@ -920,7 +920,7 @@ def test_search_immunizations_includes_an_error_outcome_within_results_if_client
mock_resource = create_covid_immunization_dict("1234-some-id")
vaccine_type = "COVID"
self.authoriser.filter_permitted_vacc_types.return_value = {vaccine_type}
self.imms_repo.find_immunizations.return_value = [mock_resource]
self.imms_repo.search_immunizations.return_value = [mock_resource]

# When
result = self.fhir_service.search_immunizations(
Expand All @@ -929,7 +929,7 @@ def test_search_immunizations_includes_an_error_outcome_within_results_if_client

# Then
# Does not pass FLU in as client is only permitted to retrieve COVID vaccinations
self.imms_repo.find_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
self.imms_repo.search_immunizations.assert_called_once_with(VALID_NHS_NUMBER, {vaccine_type})
mock_uuid.assert_called_once()
self.authoriser.filter_permitted_vacc_types.assert_called_once_with(
self.MOCK_SUPPLIER_SYSTEM_NAME, ApiOperationCode.SEARCH, {"COVID", "FLU"}
Expand Down Expand Up @@ -969,14 +969,14 @@ def test_search_raises_unauthorised_error_if_no_permissions(self):
self.authoriser.filter_permitted_vacc_types.assert_called_once_with(
self.MOCK_SUPPLIER_SYSTEM_NAME, ApiOperationCode.SEARCH, {vaccine_type}
)
self.imms_repo.find_immunizations.assert_not_called()
self.imms_repo.search_immunizations.assert_not_called()

def test_search_immunizations_includes_operation_outcome_when_invalid_immunization_targets_provided(self):
"""it should include an OperationOutcome in the bundle when invalid -immunization.target values were provided"""
mock_resource = create_covid_immunization_dict("1234-some-id")
vaccine_type = "COVID"
self.authoriser.filter_permitted_vacc_types.return_value = {vaccine_type}
self.imms_repo.find_immunizations.return_value = [mock_resource]
self.imms_repo.search_immunizations.return_value = [mock_resource]

result = self.fhir_service.search_immunizations(
VALID_NHS_NUMBER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_make_empty_search_bundle_with_target_disease_not_in_mapping_returns_bun
def test_search_immunizations_with_target_disease_codes_for_url_echoes_target_disease_in_bundle_link(self):
"""it should include target-disease param in bundle self link when target_disease_codes_for_url is set"""
self.authoriser.filter_permitted_vacc_types.return_value = {"MMR"}
self.imms_repo.find_immunizations.return_value = []
self.imms_repo.search_immunizations.return_value = []

result = self.fhir_service.search_immunizations(
"9000000009",
Expand All @@ -70,7 +70,7 @@ def test_search_immunizations_with_target_disease_codes_for_url_echoes_target_di
def test_search_immunizations_with_invalid_target_diseases_adds_operation_outcomes(self):
"""it should add one OperationOutcome entry per invalid target disease diagnostic"""
self.authoriser.filter_permitted_vacc_types.return_value = {"MMR"}
self.imms_repo.find_immunizations.return_value = []
self.imms_repo.search_immunizations.return_value = []

result = self.fhir_service.search_immunizations(
"9000000009",
Expand Down
Loading