From d11669e46c49a773755de65e8cd995d1eab32f81 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Thu, 21 Nov 2024 16:10:27 +0000 Subject: [PATCH 01/26] [PRMP-1188] create lambda to handle mns sqs messages --- lambdas/enums/mns_notification_types.py | 6 +++ lambdas/handlers/mns_notification_handler.py | 48 +++++++++++++++++++ lambdas/models/mns_sqs_message.py | 22 +++++++++ .../services/process_mns_message_service.py | 0 .../handlers/test_mns_notification_handler.py | 33 +++++++++++++ .../test_process_mns_message_service.py | 0 6 files changed, 109 insertions(+) create mode 100644 lambdas/enums/mns_notification_types.py create mode 100644 lambdas/handlers/mns_notification_handler.py create mode 100644 lambdas/models/mns_sqs_message.py create mode 100644 lambdas/services/process_mns_message_service.py create mode 100644 lambdas/tests/unit/handlers/test_mns_notification_handler.py create mode 100644 lambdas/tests/unit/services/test_process_mns_message_service.py diff --git a/lambdas/enums/mns_notification_types.py b/lambdas/enums/mns_notification_types.py new file mode 100644 index 000000000..9ae50aa02 --- /dev/null +++ b/lambdas/enums/mns_notification_types.py @@ -0,0 +1,6 @@ +from enum import StrEnum + + +class MNSNotificationTypes(StrEnum): + CHANGE_OF_GP = "pds-change-of-gp-1" + DEATH_NOTIFICATION = "pds-death-notification-1" diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py new file mode 100644 index 000000000..bc1e1b5fd --- /dev/null +++ b/lambdas/handlers/mns_notification_handler.py @@ -0,0 +1,48 @@ +import json + +from models.mns_sqs_message import MNSSQSMessage +from utils.audit_logging_setup import LoggingService +from utils.decorators.ensure_env_var import ensure_environment_variables +from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions +from utils.decorators.override_error_check import override_error_check +from utils.decorators.set_audit_arg import set_request_context_for_logging + +logger = LoggingService(__name__) + + +@set_request_context_for_logging +@ensure_environment_variables( + names=[ + "APPCONFIG_CONFIGURATION", + "APPCONFIG_ENVIRONMENT", + "LLOYD_GEORGE_DYNAMODB_NAME", + "MNS_NOTIFICATION_QUEUE_URL", + # might not need the name of the queue, as this is what is trigging the lamdba + ] +) +# need to check what this does +@override_error_check +@handle_lambda_exceptions +def lambda_handler(event, context): + logger.info(f"Received MNS notification event: {event}") + + sqs_messages = event.get("Records", []) + + for sqs_message in sqs_messages: + try: + sqs_message = json.loads(sqs_message["body"]) + mns_message = MNSSQSMessage(**sqs_message) + MNSSQSMessage.model_validate(mns_message) + except Exception as error: + logger.error(f"Error processing SQS message: {error}.") + logger.info("Continuing to next message.") + pass + + # how do we want to handle empty messages, bulk up load + # if "Records" not in event or len(event["Records"]) < 1: + # http_status_code = 400 + # response_body = f"No sqs messages found in event: {event}. Event ignored." + # logger.error(response_body, {"Result": "Did not process MNS notification."}) + # return ApiGatewayResponse( + # status_code=http_status_code, body=response_body, methods="GET" + # ) diff --git a/lambdas/models/mns_sqs_message.py b/lambdas/models/mns_sqs_message.py new file mode 100644 index 000000000..d0446897e --- /dev/null +++ b/lambdas/models/mns_sqs_message.py @@ -0,0 +1,22 @@ +from pydantic import AliasGenerator, BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + + +class MNSMessageSubject(BaseModel): + model_config = ConfigDict( + alias_generator=AliasGenerator(serialization_alias=to_camel), + ) + + nhs_number: str + family_name: str + dob: str + + +class MNSSQSMessage(BaseModel): + model_config = ConfigDict( + alias_generator=AliasGenerator(serialization_alias=to_camel), + ) + + id: str + type: str + subject: MNSMessageSubject diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py new file mode 100644 index 000000000..e69de29bb diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py new file mode 100644 index 000000000..1bd11098e --- /dev/null +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -0,0 +1,33 @@ +from enums.mns_notification_types import MNSNotificationTypes +from unit.conftest import FAKE_URL, TEST_NHS_NUMBER, TEST_UUID +from unit.services.base.test_cloudwatch_logs_query_service import MOCK_START_TIME + +MOCK_GP_CHANGE_MESSAGE_BODY = { + "id": TEST_UUID, + "type": MNSNotificationTypes.CHANGE_OF_GP, + "subject": { + "nhsNumber": TEST_NHS_NUMBER, + "familyName": "SMITH", + "dob": "2010-10-22", + }, + "source": { + "name": FAKE_URL, + "identifiers": { + "system": FAKE_URL, + "value": TEST_UUID, + }, + }, + "time": MOCK_START_TIME, + "data": { + "fullUrl": FAKE_URL, + "versionId": TEST_UUID, + "provenance": { + "name": "Fake GP", + "identifiers": { + "system": FAKE_URL, + "value": TEST_UUID, + }, + }, + "registrationEncounterCode": "00", + }, +} diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py new file mode 100644 index 000000000..e69de29bb From 76970c3417d3a35bc193051be1b9d553311d9ef1 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Thu, 21 Nov 2024 17:20:53 +0000 Subject: [PATCH 02/26] [PRMP-1188] introduce MNS SQS message model --- lambdas/handlers/mns_notification_handler.py | 5 ++++- lambdas/models/mns_sqs_message.py | 5 +++++ .../handlers/test_mns_notification_handler.py | 22 ++++++++++++++----- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py index bc1e1b5fd..e17141800 100644 --- a/lambdas/handlers/mns_notification_handler.py +++ b/lambdas/handlers/mns_notification_handler.py @@ -16,7 +16,6 @@ "APPCONFIG_CONFIGURATION", "APPCONFIG_ENVIRONMENT", "LLOYD_GEORGE_DYNAMODB_NAME", - "MNS_NOTIFICATION_QUEUE_URL", # might not need the name of the queue, as this is what is trigging the lamdba ] ) @@ -31,8 +30,12 @@ def lambda_handler(event, context): for sqs_message in sqs_messages: try: sqs_message = json.loads(sqs_message["body"]) + # event_type = sqs_message["type"] + # nhs_number = sqs_message["subject"]["nhsNumber"] mns_message = MNSSQSMessage(**sqs_message) MNSSQSMessage.model_validate(mns_message) + + return mns_message.subject.nhs_number except Exception as error: logger.error(f"Error processing SQS message: {error}.") logger.info("Continuing to next message.") diff --git a/lambdas/models/mns_sqs_message.py b/lambdas/models/mns_sqs_message.py index d0446897e..e87616198 100644 --- a/lambdas/models/mns_sqs_message.py +++ b/lambdas/models/mns_sqs_message.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic import AliasGenerator, BaseModel, ConfigDict from pydantic.alias_generators import to_camel @@ -20,3 +22,6 @@ class MNSSQSMessage(BaseModel): id: str type: str subject: MNSMessageSubject + source: Optional[dict] = None + time: Optional[str] = None + data: Optional[dict] = None diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index 1bd11098e..a21cba304 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -1,13 +1,16 @@ +import json + from enums.mns_notification_types import MNSNotificationTypes +from handlers.mns_notification_handler import lambda_handler from unit.conftest import FAKE_URL, TEST_NHS_NUMBER, TEST_UUID from unit.services.base.test_cloudwatch_logs_query_service import MOCK_START_TIME MOCK_GP_CHANGE_MESSAGE_BODY = { "id": TEST_UUID, - "type": MNSNotificationTypes.CHANGE_OF_GP, + "type": MNSNotificationTypes.CHANGE_OF_GP.value, "subject": { - "nhsNumber": TEST_NHS_NUMBER, - "familyName": "SMITH", + "nhs_number": TEST_NHS_NUMBER, + "family_name": "SMITH", "dob": "2010-10-22", }, "source": { @@ -17,10 +20,10 @@ "value": TEST_UUID, }, }, - "time": MOCK_START_TIME, + "time": f"{MOCK_START_TIME}", "data": { - "fullUrl": FAKE_URL, - "versionId": TEST_UUID, + "full_url": FAKE_URL, + "version_id": TEST_UUID, "provenance": { "name": "Fake GP", "identifiers": { @@ -31,3 +34,10 @@ "registrationEncounterCode": "00", }, } + + +def test_handler(context, set_env): + + event = {"Records": [{"body": json.dumps(MOCK_GP_CHANGE_MESSAGE_BODY)}]} + + assert lambda_handler(event, context) == TEST_NHS_NUMBER From bc0fe45f5ec78c84a94035207277a7c31720cc19 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 22 Nov 2024 08:22:46 +0000 Subject: [PATCH 03/26] [PRMP-1188] continue work on MNS notification handler --- lambdas/enums/mns_notification_types.py | 4 ++ lambdas/handlers/mns_notification_handler.py | 19 +++---- .../services/process_mns_message_service.py | 6 ++ .../handlers/test_mns_notification_handler.py | 57 +++++++++++++++++-- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/lambdas/enums/mns_notification_types.py b/lambdas/enums/mns_notification_types.py index 9ae50aa02..dce9c209d 100644 --- a/lambdas/enums/mns_notification_types.py +++ b/lambdas/enums/mns_notification_types.py @@ -4,3 +4,7 @@ class MNSNotificationTypes(StrEnum): CHANGE_OF_GP = "pds-change-of-gp-1" DEATH_NOTIFICATION = "pds-death-notification-1" + + @staticmethod + def list() -> list[str]: + return [str(type.value) for type in MNSNotificationTypes] diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py index e17141800..7883dfa79 100644 --- a/lambdas/handlers/mns_notification_handler.py +++ b/lambdas/handlers/mns_notification_handler.py @@ -1,6 +1,8 @@ import json +from enums.mns_notification_types import MNSNotificationTypes from models.mns_sqs_message import MNSSQSMessage +from services.process_mns_message_service import MNSNotificationService from utils.audit_logging_setup import LoggingService from utils.decorators.ensure_env_var import ensure_environment_variables from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions @@ -16,7 +18,6 @@ "APPCONFIG_CONFIGURATION", "APPCONFIG_ENVIRONMENT", "LLOYD_GEORGE_DYNAMODB_NAME", - # might not need the name of the queue, as this is what is trigging the lamdba ] ) # need to check what this does @@ -35,17 +36,11 @@ def lambda_handler(event, context): mns_message = MNSSQSMessage(**sqs_message) MNSSQSMessage.model_validate(mns_message) - return mns_message.subject.nhs_number + if mns_message.type in MNSNotificationTypes.list(): + notification_service = MNSNotificationService(mns_message) + notification_service.handle_mns_notification() + + continue except Exception as error: logger.error(f"Error processing SQS message: {error}.") logger.info("Continuing to next message.") - pass - - # how do we want to handle empty messages, bulk up load - # if "Records" not in event or len(event["Records"]) < 1: - # http_status_code = 400 - # response_body = f"No sqs messages found in event: {event}. Event ignored." - # logger.error(response_body, {"Result": "Did not process MNS notification."}) - # return ApiGatewayResponse( - # status_code=http_status_code, body=response_body, methods="GET" - # ) diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index e69de29bb..b8d0453d7 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -0,0 +1,6 @@ +class MNSNotificationService: + def __init__(self, message): + self.message = message + + def handle_mns_notification(self): + pass diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index a21cba304..c5bb6216a 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -1,9 +1,12 @@ import json +from copy import deepcopy +import pytest from enums.mns_notification_types import MNSNotificationTypes from handlers.mns_notification_handler import lambda_handler -from unit.conftest import FAKE_URL, TEST_NHS_NUMBER, TEST_UUID -from unit.services.base.test_cloudwatch_logs_query_service import MOCK_START_TIME +from tests.unit.conftest import FAKE_URL, TEST_NHS_NUMBER, TEST_UUID + +MOCK_DATE = "2010-10-22" MOCK_GP_CHANGE_MESSAGE_BODY = { "id": TEST_UUID, @@ -11,7 +14,7 @@ "subject": { "nhs_number": TEST_NHS_NUMBER, "family_name": "SMITH", - "dob": "2010-10-22", + "dob": MOCK_DATE, }, "source": { "name": FAKE_URL, @@ -20,7 +23,7 @@ "value": TEST_UUID, }, }, - "time": f"{MOCK_START_TIME}", + "time": MOCK_DATE, "data": { "full_url": FAKE_URL, "version_id": TEST_UUID, @@ -35,9 +38,51 @@ }, } +MOCK_DEATH_MESSAGE_BODY = {} + +MOCK_OTHER_NOTIFICATION_MESSAGE_BODY = deepcopy(MOCK_GP_CHANGE_MESSAGE_BODY) +MOCK_OTHER_NOTIFICATION_MESSAGE_BODY["type"] = "imms-vaccinations-1" + + +@pytest.fixture +def mock_service(mocker): + mocked_class = mocker.patch( + "handlers.mns_notification_handler.MNSNotificationService" + ) + mocked_instance = mocked_class.return_value + return mocked_instance -def test_handler(context, set_env): + +def test_handle_notification_called_message_type_GP_change( + context, set_env, mock_service +): event = {"Records": [{"body": json.dumps(MOCK_GP_CHANGE_MESSAGE_BODY)}]} + lambda_handler(event, context) + + mock_service.handle_mns_notification.assert_called() + + +def test_handle_notification_called_message_type_death_notification( + context, set_env, mock_service +): + pass + + +def test_handle_notification_not_called_message_type_not_death_or_GP_notification( + context, set_env, mock_service +): + + event = {"Records": [{"body": json.dumps(MOCK_OTHER_NOTIFICATION_MESSAGE_BODY)}]} + lambda_handler(event, context) + + mock_service.handle_mns_notification.assert_not_called() + + +def test_handle_notification_not_called_no_records_in_event( + context, set_env, mock_service +): + event = {"Records": []} + lambda_handler(event, context) - assert lambda_handler(event, context) == TEST_NHS_NUMBER + mock_service.handle_mns_notification.assert_not_called() From 81707f590f524f141512b4d1b53837677296f50c Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 22 Nov 2024 09:25:09 +0000 Subject: [PATCH 04/26] [PRMP-1188] start creating MNSNofiticationSerive --- .../services/process_mns_message_service.py | 23 ++++++++++++-- .../handlers/test_mns_notification_handler.py | 3 +- .../test_process_mns_message_service.py | 30 +++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index b8d0453d7..6120ea6d2 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -1,6 +1,23 @@ +from enums.mns_notification_types import MNSNotificationTypes +from models.mns_sqs_message import MNSSQSMessage +from utils.audit_logging_setup import LoggingService + +logger = LoggingService(__name__) + + class MNSNotificationService: - def __init__(self, message): - self.message = message + def __init__(self): + self.message = None + + def handle_mns_notification(self, message: MNSSQSMessage): + action = { + MNSNotificationTypes.CHANGE_OF_GP.value: self.handle_gp_change_notification, + MNSNotificationTypes.DEATH_NOTIFICATION.value: self.handle_death_notification, + } + action.get(message.type)(message) + + def handle_gp_change_notification(self, message): + pass - def handle_mns_notification(self): + def handle_death_notification(self, message): pass diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index c5bb6216a..685368b54 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -38,7 +38,8 @@ }, } -MOCK_DEATH_MESSAGE_BODY = {} +MOCK_DEATH_MESSAGE_BODY = deepcopy(MOCK_GP_CHANGE_MESSAGE_BODY) +MOCK_DEATH_MESSAGE_BODY["type"] = MNSNotificationTypes.DEATH_NOTIFICATION.value MOCK_OTHER_NOTIFICATION_MESSAGE_BODY = deepcopy(MOCK_GP_CHANGE_MESSAGE_BODY) MOCK_OTHER_NOTIFICATION_MESSAGE_BODY["type"] = "imms-vaccinations-1" diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index e69de29bb..abf242bf5 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -0,0 +1,30 @@ +import pytest +from models.mns_sqs_message import MNSSQSMessage +from services.process_mns_message_service import MNSNotificationService +from tests.unit.handlers.test_mns_notification_handler import ( + MOCK_DEATH_MESSAGE_BODY, + MOCK_GP_CHANGE_MESSAGE_BODY, +) + + +@pytest.fixture +def mns_service(mocker): + service = MNSNotificationService() + mocker.patch.object(service, "handle_gp_change_notification") + + yield service + + +gp_change_message = MNSSQSMessage(**MOCK_GP_CHANGE_MESSAGE_BODY) +death_notification_message = MNSSQSMessage(**MOCK_DEATH_MESSAGE_BODY) + + +def test_handle_gp_change_message_called_message_type_GP_change(mns_service): + + mns_service.handle_mns_notification(gp_change_message) + mns_service.handle_gp_change_notification.assert_called() + + +def test_handle_gp_change_message_not_called_message_death_message(mns_service): + mns_service.handle_mns_notification(death_notification_message) + mns_service.handle_gp_change_notification.assert_not_called() From aa21f1ecba75f1c57b14c43c15de7c21f555685d Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 22 Nov 2024 12:57:11 +0000 Subject: [PATCH 05/26] [PRMP-1188] add dynamo service to mns notification service --- lambdas/models/mns_sqs_message.py | 8 ++- .../services/process_mns_message_service.py | 38 +++++++++++++- .../handlers/test_mns_notification_handler.py | 52 ++++++++++++++++--- .../test_process_mns_message_service.py | 34 +++++++++++- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/lambdas/models/mns_sqs_message.py b/lambdas/models/mns_sqs_message.py index e87616198..f95ed124a 100644 --- a/lambdas/models/mns_sqs_message.py +++ b/lambdas/models/mns_sqs_message.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import AliasGenerator, BaseModel, ConfigDict from pydantic.alias_generators import to_camel @@ -22,6 +20,6 @@ class MNSSQSMessage(BaseModel): id: str type: str subject: MNSMessageSubject - source: Optional[dict] = None - time: Optional[str] = None - data: Optional[dict] = None + source: dict + time: str + data: dict diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 6120ea6d2..551c74dd3 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -1,5 +1,9 @@ +import os + +from enums.death_notification_status import DeathNotificationStatus from enums.mns_notification_types import MNSNotificationTypes from models.mns_sqs_message import MNSSQSMessage +from services.base.dynamo_service import DynamoDBService from utils.audit_logging_setup import LoggingService logger = LoggingService(__name__) @@ -8,6 +12,8 @@ class MNSNotificationService: def __init__(self): self.message = None + self.dynamo_service = DynamoDBService() + self.table = os.getenv("LLOYD_GEORGE_DYNAMODB_TABLE") def handle_mns_notification(self, message: MNSSQSMessage): action = { @@ -16,8 +22,36 @@ def handle_mns_notification(self, message: MNSSQSMessage): } action.get(message.type)(message) - def handle_gp_change_notification(self, message): + def handle_subscription_notification(self, message): + pass + + def handle_gp_change_notification(self, message: MNSSQSMessage): + pass + + def handle_death_notification(self, message: MNSSQSMessage): + if self.is_informal_death_notification(message): + return + + if not self.have_patient_in_table(message): + return + + def is_informal_death_notification(self, message: MNSSQSMessage) -> bool: + return ( + message.data["deathNotificationStatus"] + == DeathNotificationStatus.INFORMAL.value + ) + + def have_patient_in_table(self, message: MNSSQSMessage): + response = self.dynamo_service.query_table_by_index( + table_name=self.table, + index_name="NhsNumberIndex", + search_key="NhsNumber", + search_condition=message.subject.nhs_number, + ) + return len(response["Items"]) < 1 + + def update_dynamo_table(self, code): pass - def handle_death_notification(self, message): + def get_current_ods_code(self): pass diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index 685368b54..f10e4724d 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -6,7 +6,7 @@ from handlers.mns_notification_handler import lambda_handler from tests.unit.conftest import FAKE_URL, TEST_NHS_NUMBER, TEST_UUID -MOCK_DATE = "2010-10-22" +MOCK_TIME = "2022-04-05T17:31:00.000Z" MOCK_GP_CHANGE_MESSAGE_BODY = { "id": TEST_UUID, @@ -14,7 +14,7 @@ "subject": { "nhs_number": TEST_NHS_NUMBER, "family_name": "SMITH", - "dob": MOCK_DATE, + "dob": "2017-10-02", }, "source": { "name": FAKE_URL, @@ -23,7 +23,7 @@ "value": TEST_UUID, }, }, - "time": MOCK_DATE, + "time": MOCK_TIME, "data": { "full_url": FAKE_URL, "version_id": TEST_UUID, @@ -38,12 +38,42 @@ }, } -MOCK_DEATH_MESSAGE_BODY = deepcopy(MOCK_GP_CHANGE_MESSAGE_BODY) -MOCK_DEATH_MESSAGE_BODY["type"] = MNSNotificationTypes.DEATH_NOTIFICATION.value +MOCK_DEATH_MESSAGE_BODY = { + "id": TEST_UUID, + "type": "pds-death-notification-1", + "subject": { + "dob": "2017-10-02", + "family_name": "DAWKINS", + "nhs_number": TEST_NHS_NUMBER, + }, + "source": { + "name": "NHS DIGITAL", + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhsSpineASID", + "value": "477121000324", + }, + }, + "time": MOCK_TIME, + "data": { + "versionId": 'W/"16"', + "fullUrl": "https://int.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient/9912003888", + "deathNotificationStatus": "2", + "provenance": { + "name": "The GP Practice", + "identifiers": { + "system": "https://fhir.nhs.uk/Id/nhsSpineASID", + "value": "477121000323", + }, + }, + }, +} MOCK_OTHER_NOTIFICATION_MESSAGE_BODY = deepcopy(MOCK_GP_CHANGE_MESSAGE_BODY) MOCK_OTHER_NOTIFICATION_MESSAGE_BODY["type"] = "imms-vaccinations-1" +MOCK_INFORMAL_DEATH_MESSAGE_BODY = deepcopy(MOCK_DEATH_MESSAGE_BODY) +MOCK_INFORMAL_DEATH_MESSAGE_BODY["data"]["deathNotificationStatus"] = "1" + @pytest.fixture def mock_service(mocker): @@ -67,7 +97,10 @@ def test_handle_notification_called_message_type_GP_change( def test_handle_notification_called_message_type_death_notification( context, set_env, mock_service ): - pass + event = {"Records": [{"body": json.dumps(MOCK_DEATH_MESSAGE_BODY)}]} + lambda_handler(event, context) + + mock_service.handle_mns_notification.assert_called() def test_handle_notification_not_called_message_type_not_death_or_GP_notification( @@ -87,3 +120,10 @@ def test_handle_notification_not_called_no_records_in_event( lambda_handler(event, context) mock_service.handle_mns_notification.assert_not_called() + + +def test_handle_notification_not_called_informal_death(context, set_env, mock_service): + event = {"Records": [{"body": json.dumps(MOCK_INFORMAL_DEATH_MESSAGE_BODY)}]} + lambda_handler(event, context) + + mock_service.handle_mns_notification.assert_not_called() diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index abf242bf5..8968f0f06 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -4,19 +4,31 @@ from tests.unit.handlers.test_mns_notification_handler import ( MOCK_DEATH_MESSAGE_BODY, MOCK_GP_CHANGE_MESSAGE_BODY, + MOCK_INFORMAL_DEATH_MESSAGE_BODY, ) +from tests.unit.helpers.data.dynamo_responses import MOCK_SEARCH_RESPONSE @pytest.fixture -def mns_service(mocker): +def mns_service(mocker, set_env): service = MNSNotificationService() mocker.patch.object(service, "handle_gp_change_notification") + mocker.patch.object(service, "dynamo_service") yield service +@pytest.fixture +def mock_dynamo_service(mns_service, mocker): + mock_dynamo_service = mns_service.dynamo_service + mocker.patch.object(mock_dynamo_service, "update_item") + mocker.patch.object(mock_dynamo_service, "query_table_by_index") + yield mock_dynamo_service + + gp_change_message = MNSSQSMessage(**MOCK_GP_CHANGE_MESSAGE_BODY) death_notification_message = MNSSQSMessage(**MOCK_DEATH_MESSAGE_BODY) +informal_death_notification_message = MNSSQSMessage(**MOCK_INFORMAL_DEATH_MESSAGE_BODY) def test_handle_gp_change_message_called_message_type_GP_change(mns_service): @@ -28,3 +40,23 @@ def test_handle_gp_change_message_called_message_type_GP_change(mns_service): def test_handle_gp_change_message_not_called_message_death_message(mns_service): mns_service.handle_mns_notification(death_notification_message) mns_service.handle_gp_change_notification.assert_not_called() + + +def test_is_informal_death_notification(mns_service): + assert ( + mns_service.is_informal_death_notification(death_notification_message) is True + ) + assert ( + mns_service.is_informal_death_notification(informal_death_notification_message) + is False + ) + + +def test_have_patient_in_table(mns_service): + result = mns_service.have_patient_in_table(gp_change_message) + mns_service.dynamo_service.query_table_by_index.assert_called() + mns_service.dynamo_service.return_value.query_table_by_index.return_value = ( + MOCK_SEARCH_RESPONSE + ) + + assert result is True From f970374769c7ad08fc63ddff3a514d6e56dde409 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 22 Nov 2024 17:03:09 +0000 Subject: [PATCH 06/26] [PRMP-1188] continue handle death notification process --- .../services/process_mns_message_service.py | 75 +++++++++++++++++-- .../test_process_mns_message_service.py | 32 ++++++-- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 551c74dd3..340e7df35 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -1,10 +1,19 @@ import os +from datetime import time +from urllib.error import HTTPError from enums.death_notification_status import DeathNotificationStatus from enums.mns_notification_types import MNSNotificationTypes +from enums.patient_ods_inactive_status import PatientOdsInactiveStatus from models.mns_sqs_message import MNSSQSMessage +from models.pds_models import Patient +from pydantic_core._pydantic_core import ValidationError from services.base.dynamo_service import DynamoDBService +from services.base.nhs_oauth_service import NhsOauthService +from services.base.ssm_service import SSMService from utils.audit_logging_setup import LoggingService +from utils.exceptions import PdsErrorException, PdsResponseValidationException +from utils.utilities import get_pds_service logger = LoggingService(__name__) @@ -14,6 +23,10 @@ def __init__(self): self.message = None self.dynamo_service = DynamoDBService() self.table = os.getenv("LLOYD_GEORGE_DYNAMODB_TABLE") + pds_service_class = get_pds_service() + ssm_service = SSMService() + auth_service = NhsOauthService(ssm_service) + self.pds_service = pds_service_class(ssm_service, auth_service) def handle_mns_notification(self, message: MNSSQSMessage): action = { @@ -30,28 +43,74 @@ def handle_gp_change_notification(self, message: MNSSQSMessage): def handle_death_notification(self, message: MNSSQSMessage): if self.is_informal_death_notification(message): + logger.info( + "Patient is deceased - INFORMAL, moving on to the next message." + ) return - if not self.have_patient_in_table(message): + patient_documents = self.get_patient_documents(message.subject.nhs_number) + + if len(patient_documents) < 1: + logger.info("Patient is not held in the National Document Repository.") + logger.info("Moving on to the next message.") + return + + if ( + message.data["deathNotificationStatus"] + == DeathNotificationStatus.FORMAL.value + ): + self.update_patient_details( + patient_documents, PatientOdsInactiveStatus.DECEASED.value + ) return + update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) + self.update_patient_details(patient_documents, update_ods_code) + def is_informal_death_notification(self, message: MNSSQSMessage) -> bool: return ( message.data["deathNotificationStatus"] == DeathNotificationStatus.INFORMAL.value ) - def have_patient_in_table(self, message: MNSSQSMessage): + def get_patient_documents(self, nhs_number: str): + logger.info("Checking if patient is held in the National Document Repository.") response = self.dynamo_service.query_table_by_index( table_name=self.table, index_name="NhsNumberIndex", search_key="NhsNumber", - search_condition=message.subject.nhs_number, + search_condition=nhs_number, ) - return len(response["Items"]) < 1 + return response["Items"] - def update_dynamo_table(self, code): - pass + def update_patient_details(self, patient_documents: dict, code: str) -> None: + for document in patient_documents: + self.dynamo_service.update_item( + table_name=self.table, + key=document["ID"], + updated_fields={"CurrentGpOds": code}, + ) - def get_current_ods_code(self): - pass + def get_updated_gp_ods(self, nhs_number: str) -> str: + time.sleep(0.2) # buffer to avoid over stretching PDS API + logger.info("Getting the latest ODS code from PDS...") + + try: + pds_response = self.pds_service.pds_request( + nhs_number=nhs_number, retry_on_expired=True + ) + pds_response.raise_for_status() + + pds_response_json = pds_response.json() + + patient = Patient.model_validate(pds_response_json) + + return patient.get_ods_code_or_inactive_status_for_gp() + except HTTPError as e: + raise PdsErrorException( + f"PDS returned a {e.response.status_code} code response for patient with NHS number {nhs_number}" + ) + except ValidationError: + raise PdsResponseValidationException( + f"PDS returned an invalid response for patient with NHS number {nhs_number}" + ) diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index 8968f0f06..f15b0d02c 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -1,4 +1,7 @@ +from unittest.mock import call + import pytest +from enums.patient_ods_inactive_status import PatientOdsInactiveStatus from models.mns_sqs_message import MNSSQSMessage from services.process_mns_message_service import MNSNotificationService from tests.unit.handlers.test_mns_notification_handler import ( @@ -32,7 +35,6 @@ def mock_dynamo_service(mns_service, mocker): def test_handle_gp_change_message_called_message_type_GP_change(mns_service): - mns_service.handle_mns_notification(gp_change_message) mns_service.handle_gp_change_notification.assert_called() @@ -52,11 +54,25 @@ def test_is_informal_death_notification(mns_service): ) -def test_have_patient_in_table(mns_service): - result = mns_service.have_patient_in_table(gp_change_message) - mns_service.dynamo_service.query_table_by_index.assert_called() - mns_service.dynamo_service.return_value.query_table_by_index.return_value = ( - MOCK_SEARCH_RESPONSE +def test_update_patient_details(mns_service): + mns_service.update_patient_details( + MOCK_SEARCH_RESPONSE["Items"], PatientOdsInactiveStatus.DECEASED.value ) - - assert result is True + calls = [ + call( + table_name=mns_service.table, + key="3d8683b9-1665-40d2-8499-6e8302d507ff", + updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED.value}, + ), + call( + table_name=mns_service.table, + key="4d8683b9-1665-40d2-8499-6e8302d507ff", + updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED.value}, + ), + call( + table_name=mns_service.table, + key="5d8683b9-1665-40d2-8499-6e8302d507ff", + updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED.value}, + ), + ] + mns_service.dynamo_service.update_item.assert_has_calls(calls, any_order=False) From d608e199fdbe96eef4f48b3598e93c11a798b77e Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 22 Nov 2024 18:25:55 +0000 Subject: [PATCH 07/26] [PRMP-1188] handle gp change notification --- .../services/process_mns_message_service.py | 30 ++++++-- .../test_process_mns_message_service.py | 72 +++++++++++++++---- 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 340e7df35..60ff1c5af 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -39,7 +39,25 @@ def handle_subscription_notification(self, message): pass def handle_gp_change_notification(self, message: MNSSQSMessage): - pass + + patient_document_references = self.get_patient_documents( + message.subject.nhs_number + ) + + if len(patient_document_references) < 1: + logger.info("Patient is not held in the National Document Repository.") + logger.info("Moving on to the next message.") + return + + updated_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) + + for reference in patient_document_references: + if reference["CurrentGpOds"] is not updated_ods_code: + self.dynamo_service.update_item( + table_name=self.table, + key=reference["ID"], + updated_fields={"CurrentGpOds": updated_ods_code}, + ) def handle_death_notification(self, message: MNSSQSMessage): if self.is_informal_death_notification(message): @@ -62,11 +80,13 @@ def handle_death_notification(self, message: MNSSQSMessage): self.update_patient_details( patient_documents, PatientOdsInactiveStatus.DECEASED.value ) - return update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) self.update_patient_details(patient_documents, update_ods_code) + # def do_not_have_patient_records(self, patient_documents_references): + # return len(patient_documents_references) < 1 + def is_informal_death_notification(self, message: MNSSQSMessage) -> bool: return ( message.data["deathNotificationStatus"] @@ -74,7 +94,7 @@ def is_informal_death_notification(self, message: MNSSQSMessage) -> bool: ) def get_patient_documents(self, nhs_number: str): - logger.info("Checking if patient is held in the National Document Repository.") + logger.info("Getting patient document references...") response = self.dynamo_service.query_table_by_index( table_name=self.table, index_name="NhsNumberIndex", @@ -83,7 +103,7 @@ def get_patient_documents(self, nhs_number: str): ) return response["Items"] - def update_patient_details(self, patient_documents: dict, code: str) -> None: + def update_patient_details(self, patient_documents: list[dict], code: str) -> None: for document in patient_documents: self.dynamo_service.update_item( table_name=self.table, @@ -91,6 +111,8 @@ def update_patient_details(self, patient_documents: dict, code: str) -> None: updated_fields={"CurrentGpOds": code}, ) + # logger.info(f"Updated patient CurrentGpOds from {patient_documents[0]["CurrentGpOds"]} to {code}") + def get_updated_gp_ods(self, nhs_number: str) -> str: time.sleep(0.2) # buffer to avoid over stretching PDS API logger.info("Getting the latest ODS code from PDS...") diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index f15b0d02c..2aabf6e74 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -4,53 +4,54 @@ from enums.patient_ods_inactive_status import PatientOdsInactiveStatus from models.mns_sqs_message import MNSSQSMessage from services.process_mns_message_service import MNSNotificationService +from tests.unit.conftest import TEST_CURRENT_GP_ODS from tests.unit.handlers.test_mns_notification_handler import ( MOCK_DEATH_MESSAGE_BODY, MOCK_GP_CHANGE_MESSAGE_BODY, MOCK_INFORMAL_DEATH_MESSAGE_BODY, ) -from tests.unit.helpers.data.dynamo_responses import MOCK_SEARCH_RESPONSE +from tests.unit.helpers.data.dynamo_responses import ( + MOCK_EMPTY_RESPONSE, + MOCK_SEARCH_RESPONSE, +) +from tests.unit.helpers.mock_services import FakePDSService @pytest.fixture def mns_service(mocker, set_env): service = MNSNotificationService() - mocker.patch.object(service, "handle_gp_change_notification") + service.pds_service = FakePDSService mocker.patch.object(service, "dynamo_service") - + mocker.patch.object(service, "get_updated_gp_ods") yield service -@pytest.fixture -def mock_dynamo_service(mns_service, mocker): - mock_dynamo_service = mns_service.dynamo_service - mocker.patch.object(mock_dynamo_service, "update_item") - mocker.patch.object(mock_dynamo_service, "query_table_by_index") - yield mock_dynamo_service - - gp_change_message = MNSSQSMessage(**MOCK_GP_CHANGE_MESSAGE_BODY) death_notification_message = MNSSQSMessage(**MOCK_DEATH_MESSAGE_BODY) informal_death_notification_message = MNSSQSMessage(**MOCK_INFORMAL_DEATH_MESSAGE_BODY) -def test_handle_gp_change_message_called_message_type_GP_change(mns_service): +def test_handle_gp_change_message_called_message_type_GP_change(mns_service, mocker): + mocker.patch.object(mns_service, "handle_gp_change_notification") mns_service.handle_mns_notification(gp_change_message) + mns_service.handle_gp_change_notification.assert_called() -def test_handle_gp_change_message_not_called_message_death_message(mns_service): +def test_handle_gp_change_message_not_called_message_death_message(mns_service, mocker): + mocker.patch.object(mns_service, "handle_gp_change_notification") mns_service.handle_mns_notification(death_notification_message) + mns_service.handle_gp_change_notification.assert_not_called() def test_is_informal_death_notification(mns_service): assert ( - mns_service.is_informal_death_notification(death_notification_message) is True + mns_service.is_informal_death_notification(death_notification_message) is False ) assert ( mns_service.is_informal_death_notification(informal_death_notification_message) - is False + is True ) @@ -76,3 +77,44 @@ def test_update_patient_details(mns_service): ), ] mns_service.dynamo_service.update_item.assert_has_calls(calls, any_order=False) + + +def test_update_gp_ods_not_called_empty_dynamo_response(mns_service): + mns_service.dynamo_service.query_table_by_index.return_value = MOCK_EMPTY_RESPONSE + mns_service.handle_gp_change_notification(gp_change_message) + + mns_service.get_updated_gp_ods.assert_not_called() + + +def test_update_gp_ods_not_called_ods_codes_are_the_same(mns_service): + mns_service.dynamo_service.query_table_by_index.return_value = MOCK_SEARCH_RESPONSE + mns_service.get_updated_gp_ods.return_value = TEST_CURRENT_GP_ODS + mns_service.handle_gp_change_notification(gp_change_message) + + mns_service.dynamo_service.update_item.assert_not_called() + + +def test_handle_gp_change_updates_gp_ods_code(mns_service): + mns_service.dynamo_service.query_table_by_index.return_value = MOCK_SEARCH_RESPONSE + other_gp_ods = "Z12345" + mns_service.get_updated_gp_ods.return_value = other_gp_ods + mns_service.handle_gp_change_notification(gp_change_message) + + calls = [ + call( + table_name=mns_service.table, + key="3d8683b9-1665-40d2-8499-6e8302d507ff", + updated_fields={"CurrentGpOds": other_gp_ods}, + ), + call( + table_name=mns_service.table, + key="4d8683b9-1665-40d2-8499-6e8302d507ff", + updated_fields={"CurrentGpOds": other_gp_ods}, + ), + call( + table_name=mns_service.table, + key="5d8683b9-1665-40d2-8499-6e8302d507ff", + updated_fields={"CurrentGpOds": other_gp_ods}, + ), + ] + mns_service.dynamo_service.update_item.assert_has_calls(calls, any_order=False) From e7b69bb4a5f82e81537bc768cad7ee11a60ae130 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 25 Nov 2024 11:13:13 +0000 Subject: [PATCH 08/26] [PRMP-1188] add lambda to workflow --- .../base-lambdas-reusable-deploy-all.yml | 13 +++++++++++++ lambdas/handlers/mns_notification_handler.py | 5 ++--- .../services/process_mns_message_service.py | 19 +++++++++++++------ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.github/workflows/base-lambdas-reusable-deploy-all.yml b/.github/workflows/base-lambdas-reusable-deploy-all.yml index 195632f5b..f1185c986 100644 --- a/.github/workflows/base-lambdas-reusable-deploy-all.yml +++ b/.github/workflows/base-lambdas-reusable-deploy-all.yml @@ -402,4 +402,17 @@ jobs: secrets: AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }} + deploy_mns_notification_lambda: + name: Deploy mns notification lambda + uses: ./.github/workflows/base-lambdas-reusable-deploy.yml + with: + environment: ${{ inputs.environment}} + python_version: ${{ inputs.python_version }} + build_branch: ${{ inputs.build_branch}} + sandbox: ${{ inputs.sandbox }} + lambda_handler_name: mns_notification_handler + lambda_aws_name: MNSNotificationLambda + lambda_layer_names: 'core_lambda_layer' + secrets: + AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }} diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py index 7883dfa79..3e32c5ae1 100644 --- a/lambdas/handlers/mns_notification_handler.py +++ b/lambdas/handlers/mns_notification_handler.py @@ -20,7 +20,6 @@ "LLOYD_GEORGE_DYNAMODB_NAME", ] ) -# need to check what this does @override_error_check @handle_lambda_exceptions def lambda_handler(event, context): @@ -37,8 +36,8 @@ def lambda_handler(event, context): MNSSQSMessage.model_validate(mns_message) if mns_message.type in MNSNotificationTypes.list(): - notification_service = MNSNotificationService(mns_message) - notification_service.handle_mns_notification() + notification_service = MNSNotificationService() + notification_service.handle_mns_notification(mns_message) continue except Exception as error: diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 60ff1c5af..5ad5a6c9e 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -29,11 +29,18 @@ def __init__(self): self.pds_service = pds_service_class(ssm_service, auth_service) def handle_mns_notification(self, message: MNSSQSMessage): - action = { - MNSNotificationTypes.CHANGE_OF_GP.value: self.handle_gp_change_notification, - MNSNotificationTypes.DEATH_NOTIFICATION.value: self.handle_death_notification, - } - action.get(message.type)(message) + try: + action = { + MNSNotificationTypes.CHANGE_OF_GP.value: self.handle_gp_change_notification, + MNSNotificationTypes.DEATH_NOTIFICATION.value: self.handle_death_notification, + } + action.get(message.type)(message) + + except KeyError as e: + logger.info(f"Do not have key {message.type}") + logger.info(f"Unable to process message: {message.id}") + logger.info(f"{e}") + return def handle_subscription_notification(self, message): pass @@ -111,7 +118,7 @@ def update_patient_details(self, patient_documents: list[dict], code: str) -> No updated_fields={"CurrentGpOds": code}, ) - # logger.info(f"Updated patient CurrentGpOds from {patient_documents[0]["CurrentGpOds"]} to {code}") + logger.info("Updating patient document reference") def get_updated_gp_ods(self, nhs_number: str) -> str: time.sleep(0.2) # buffer to avoid over stretching PDS API From 488b81bfb22be696b5ee0a314a35a43d87bd8a61 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 25 Nov 2024 14:34:00 +0000 Subject: [PATCH 09/26] [PRMP-1188] remove unnecessary fields from model --- lambdas/enums/mns_notification_types.py | 3 +- lambdas/handlers/mns_notification_handler.py | 19 ++++++++-- lambdas/models/mns_sqs_message.py | 6 ---- .../services/process_mns_message_service.py | 36 ++++++++++--------- .../handlers/test_mns_notification_handler.py | 17 --------- .../test_process_mns_message_service.py | 7 ++++ 6 files changed, 45 insertions(+), 43 deletions(-) diff --git a/lambdas/enums/mns_notification_types.py b/lambdas/enums/mns_notification_types.py index dce9c209d..9e6f327a8 100644 --- a/lambdas/enums/mns_notification_types.py +++ b/lambdas/enums/mns_notification_types.py @@ -4,7 +4,8 @@ class MNSNotificationTypes(StrEnum): CHANGE_OF_GP = "pds-change-of-gp-1" DEATH_NOTIFICATION = "pds-death-notification-1" + SUBSCRIPTION = "Subscription" @staticmethod - def list() -> list[str]: + def list() -> [str]: return [str(type.value) for type in MNSNotificationTypes] diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py index 3e32c5ae1..9e33fb07d 100644 --- a/lambdas/handlers/mns_notification_handler.py +++ b/lambdas/handlers/mns_notification_handler.py @@ -1,5 +1,6 @@ import json +import requests from enums.mns_notification_types import MNSNotificationTypes from models.mns_sqs_message import MNSSQSMessage from services.process_mns_message_service import MNSNotificationService @@ -30,12 +31,15 @@ def lambda_handler(event, context): for sqs_message in sqs_messages: try: sqs_message = json.loads(sqs_message["body"]) - # event_type = sqs_message["type"] - # nhs_number = sqs_message["subject"]["nhsNumber"] + + if sqs_message["type"] == MNSNotificationTypes.SUBSCRIPTION: + handle_subscription(sqs_message) + continue + mns_message = MNSSQSMessage(**sqs_message) MNSSQSMessage.model_validate(mns_message) + if mns_message.type in MNSNotificationTypes.__members__.values(): - if mns_message.type in MNSNotificationTypes.list(): notification_service = MNSNotificationService() notification_service.handle_mns_notification(mns_message) @@ -43,3 +47,12 @@ def lambda_handler(event, context): except Exception as error: logger.error(f"Error processing SQS message: {error}.") logger.info("Continuing to next message.") + + +def handle_subscription(message): + try: + url = message["SubscribeURL"] + response = requests.get(url) + response.raise_for_status() + except Exception as error: + logger.error(f"Error processing subscription request: {error}.") diff --git a/lambdas/models/mns_sqs_message.py b/lambdas/models/mns_sqs_message.py index f95ed124a..2cf7a777b 100644 --- a/lambdas/models/mns_sqs_message.py +++ b/lambdas/models/mns_sqs_message.py @@ -6,20 +6,14 @@ class MNSMessageSubject(BaseModel): model_config = ConfigDict( alias_generator=AliasGenerator(serialization_alias=to_camel), ) - nhs_number: str - family_name: str - dob: str class MNSSQSMessage(BaseModel): model_config = ConfigDict( alias_generator=AliasGenerator(serialization_alias=to_camel), ) - id: str type: str subject: MNSMessageSubject - source: dict - time: str data: dict diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 5ad5a6c9e..1d54e751b 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -2,6 +2,7 @@ from datetime import time from urllib.error import HTTPError +from botocore.exceptions import ClientError from enums.death_notification_status import DeathNotificationStatus from enums.mns_notification_types import MNSNotificationTypes from enums.patient_ods_inactive_status import PatientOdsInactiveStatus @@ -73,23 +74,26 @@ def handle_death_notification(self, message: MNSSQSMessage): ) return - patient_documents = self.get_patient_documents(message.subject.nhs_number) - - if len(patient_documents) < 1: - logger.info("Patient is not held in the National Document Repository.") - logger.info("Moving on to the next message.") - return - - if ( - message.data["deathNotificationStatus"] - == DeathNotificationStatus.FORMAL.value - ): - self.update_patient_details( - patient_documents, PatientOdsInactiveStatus.DECEASED.value - ) + try: + patient_documents = self.get_patient_documents(message.subject.nhs_number) + + if len(patient_documents) < 1: + logger.info("Patient is not held in the National Document Repository.") + logger.info("Moving on to the next message.") + return + + if ( + message.data["deathNotificationStatus"] + == DeathNotificationStatus.FORMAL.value + ): + self.update_patient_details( + patient_documents, PatientOdsInactiveStatus.DECEASED.value + ) - update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) - self.update_patient_details(patient_documents, update_ods_code) + update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) + self.update_patient_details(patient_documents, update_ods_code) + except ClientError as e: + logger.info(f"{e}") # def do_not_have_patient_records(self, patient_documents_references): # return len(patient_documents_references) < 1 diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index f10e4724d..9e349ffd2 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -103,16 +103,6 @@ def test_handle_notification_called_message_type_death_notification( mock_service.handle_mns_notification.assert_called() -def test_handle_notification_not_called_message_type_not_death_or_GP_notification( - context, set_env, mock_service -): - - event = {"Records": [{"body": json.dumps(MOCK_OTHER_NOTIFICATION_MESSAGE_BODY)}]} - lambda_handler(event, context) - - mock_service.handle_mns_notification.assert_not_called() - - def test_handle_notification_not_called_no_records_in_event( context, set_env, mock_service ): @@ -120,10 +110,3 @@ def test_handle_notification_not_called_no_records_in_event( lambda_handler(event, context) mock_service.handle_mns_notification.assert_not_called() - - -def test_handle_notification_not_called_informal_death(context, set_env, mock_service): - event = {"Records": [{"body": json.dumps(MOCK_INFORMAL_DEATH_MESSAGE_BODY)}]} - lambda_handler(event, context) - - mock_service.handle_mns_notification.assert_not_called() diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index 2aabf6e74..ad61aa7ae 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -55,6 +55,13 @@ def test_is_informal_death_notification(mns_service): ) +def test_handle_notification_not_called_message_type_not_death_or_GP_notification( + mns_service, +): + mns_service.is_informal_death_notification(informal_death_notification_message) + mns_service.get_updated_gp_ods.assert_not_called() + + def test_update_patient_details(mns_service): mns_service.update_patient_details( MOCK_SEARCH_RESPONSE["Items"], PatientOdsInactiveStatus.DECEASED.value From 8c17876bd170d4a1dc1fd0b0c20c99ee40812d3a Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 25 Nov 2024 17:03:14 +0000 Subject: [PATCH 10/26] [PMRP-1188] change table env name to correct name --- lambdas/enums/mns_notification_types.py | 4 --- lambdas/handlers/mns_notification_handler.py | 3 +- lambdas/models/mns_sqs_message.py | 4 +-- .../services/process_mns_message_service.py | 32 +++++++++++-------- .../handlers/test_mns_notification_handler.py | 10 +++--- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/lambdas/enums/mns_notification_types.py b/lambdas/enums/mns_notification_types.py index 9e6f327a8..4d3524596 100644 --- a/lambdas/enums/mns_notification_types.py +++ b/lambdas/enums/mns_notification_types.py @@ -5,7 +5,3 @@ class MNSNotificationTypes(StrEnum): CHANGE_OF_GP = "pds-change-of-gp-1" DEATH_NOTIFICATION = "pds-death-notification-1" SUBSCRIPTION = "Subscription" - - @staticmethod - def list() -> [str]: - return [str(type.value) for type in MNSNotificationTypes] diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py index 9e33fb07d..6bc1edfb3 100644 --- a/lambdas/handlers/mns_notification_handler.py +++ b/lambdas/handlers/mns_notification_handler.py @@ -38,11 +38,10 @@ def lambda_handler(event, context): mns_message = MNSSQSMessage(**sqs_message) MNSSQSMessage.model_validate(mns_message) - if mns_message.type in MNSNotificationTypes.__members__.values(): + if mns_message.type in MNSNotificationTypes: notification_service = MNSNotificationService() notification_service.handle_mns_notification(mns_message) - continue except Exception as error: logger.error(f"Error processing SQS message: {error}.") diff --git a/lambdas/models/mns_sqs_message.py b/lambdas/models/mns_sqs_message.py index 2cf7a777b..8e192dbfa 100644 --- a/lambdas/models/mns_sqs_message.py +++ b/lambdas/models/mns_sqs_message.py @@ -4,14 +4,14 @@ class MNSMessageSubject(BaseModel): model_config = ConfigDict( - alias_generator=AliasGenerator(serialization_alias=to_camel), + alias_generator=AliasGenerator(validation_alias=to_camel), ) nhs_number: str class MNSSQSMessage(BaseModel): model_config = ConfigDict( - alias_generator=AliasGenerator(serialization_alias=to_camel), + alias_generator=AliasGenerator(validation_alias=to_camel), ) id: str type: str diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 1d54e751b..eeed4901d 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -1,5 +1,5 @@ import os -from datetime import time +import time from urllib.error import HTTPError from botocore.exceptions import ClientError @@ -23,7 +23,7 @@ class MNSNotificationService: def __init__(self): self.message = None self.dynamo_service = DynamoDBService() - self.table = os.getenv("LLOYD_GEORGE_DYNAMODB_TABLE") + self.table = os.getenv("LLOYD_GEORGE_DYNAMODB_NAME") pds_service_class = get_pds_service() ssm_service = SSMService() auth_service = NhsOauthService(ssm_service) @@ -31,13 +31,13 @@ def __init__(self): def handle_mns_notification(self, message: MNSSQSMessage): try: - action = { - MNSNotificationTypes.CHANGE_OF_GP.value: self.handle_gp_change_notification, - MNSNotificationTypes.DEATH_NOTIFICATION.value: self.handle_death_notification, - } - action.get(message.type)(message) + if message.type == MNSNotificationTypes.CHANGE_OF_GP.value: + self.handle_gp_change_notification(message) - except KeyError as e: + if message.type == MNSNotificationTypes.DEATH_NOTIFICATION.value: + self.handle_death_notification(message) + + except Exception as e: logger.info(f"Do not have key {message.type}") logger.info(f"Unable to process message: {message.id}") logger.info(f"{e}") @@ -116,13 +116,17 @@ def get_patient_documents(self, nhs_number: str): def update_patient_details(self, patient_documents: list[dict], code: str) -> None: for document in patient_documents: - self.dynamo_service.update_item( - table_name=self.table, - key=document["ID"], - updated_fields={"CurrentGpOds": code}, - ) + try: + self.dynamo_service.update_item( + table_name=self.table, + key=document["ID"], + updated_fields={"CurrentGpOds": code}, + ) - logger.info("Updating patient document reference") + logger.info("Updating patient document reference") + except ClientError as e: + logger.error("Unable to update patient document reference") + raise e def get_updated_gp_ods(self, nhs_number: str) -> str: time.sleep(0.2) # buffer to avoid over stretching PDS API diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index 9e349ffd2..76277abd7 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -12,7 +12,7 @@ "id": TEST_UUID, "type": MNSNotificationTypes.CHANGE_OF_GP.value, "subject": { - "nhs_number": TEST_NHS_NUMBER, + "nhsNumber": TEST_NHS_NUMBER, "family_name": "SMITH", "dob": "2017-10-02", }, @@ -25,8 +25,8 @@ }, "time": MOCK_TIME, "data": { - "full_url": FAKE_URL, - "version_id": TEST_UUID, + "fullUrl": FAKE_URL, + "versionId": TEST_UUID, "provenance": { "name": "Fake GP", "identifiers": { @@ -43,8 +43,8 @@ "type": "pds-death-notification-1", "subject": { "dob": "2017-10-02", - "family_name": "DAWKINS", - "nhs_number": TEST_NHS_NUMBER, + "familyName": "DAWKINS", + "nhsNumber": TEST_NHS_NUMBER, }, "source": { "name": "NHS DIGITAL", From 104f8c100d1e90875762eda92d6d5a63352e6402 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Tue, 26 Nov 2024 09:10:49 +0000 Subject: [PATCH 11/26] [PRMP-1188] refactor log messages --- .../services/process_mns_message_service.py | 18 ++++++++++-------- .../handlers/test_mns_notification_handler.py | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index eeed4901d..b6e7dd762 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -38,8 +38,9 @@ def handle_mns_notification(self, message: MNSSQSMessage): self.handle_death_notification(message) except Exception as e: - logger.info(f"Do not have key {message.type}") - logger.info(f"Unable to process message: {message.id}") + logger.info( + f"Unable to process message: {message.id}, of type: {message.type}" + ) logger.info(f"{e}") return @@ -67,6 +68,8 @@ def handle_gp_change_notification(self, message: MNSSQSMessage): updated_fields={"CurrentGpOds": updated_ods_code}, ) + logger.info("Update complete for change of GP") + def handle_death_notification(self, message: MNSSQSMessage): if self.is_informal_death_notification(message): logger.info( @@ -89,14 +92,14 @@ def handle_death_notification(self, message: MNSSQSMessage): self.update_patient_details( patient_documents, PatientOdsInactiveStatus.DECEASED.value ) + logger.info("Update complete, patient marked DECE.") + return update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) self.update_patient_details(patient_documents, update_ods_code) + logger.info("Update complete for death notification change.") except ClientError as e: - logger.info(f"{e}") - - # def do_not_have_patient_records(self, patient_documents_references): - # return len(patient_documents_references) < 1 + logger.error(f"{e}") def is_informal_death_notification(self, message: MNSSQSMessage) -> bool: return ( @@ -116,14 +119,13 @@ def get_patient_documents(self, nhs_number: str): def update_patient_details(self, patient_documents: list[dict], code: str) -> None: for document in patient_documents: + logger.info("Updating patient document reference...") try: self.dynamo_service.update_item( table_name=self.table, key=document["ID"], updated_fields={"CurrentGpOds": code}, ) - - logger.info("Updating patient document reference") except ClientError as e: logger.error("Unable to update patient document reference") raise e diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index 76277abd7..5c0e14352 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -81,7 +81,8 @@ def mock_service(mocker): "handlers.mns_notification_handler.MNSNotificationService" ) mocked_instance = mocked_class.return_value - return mocked_instance + mocker.patch.object(mocked_instance, "dynamo_service") + yield mocked_instance def test_handle_notification_called_message_type_GP_change( From d47671efbbd721e6516b799fbdbda1382e9777d4 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Tue, 26 Nov 2024 18:03:16 +0000 Subject: [PATCH 12/26] [PRMP-1188] implement sending message back to queue --- lambdas/handlers/mns_notification_handler.py | 26 ++++----- lambdas/models/mns_sqs_message.py | 8 ++- .../services/process_mns_message_service.py | 56 ++++++++----------- .../handlers/test_mns_notification_handler.py | 5 +- .../test_process_mns_message_service.py | 28 ++++++++++ 5 files changed, 75 insertions(+), 48 deletions(-) diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py index 6bc1edfb3..45bc63dd7 100644 --- a/lambdas/handlers/mns_notification_handler.py +++ b/lambdas/handlers/mns_notification_handler.py @@ -1,6 +1,5 @@ import json -import requests from enums.mns_notification_types import MNSNotificationTypes from models.mns_sqs_message import MNSSQSMessage from services.process_mns_message_service import MNSNotificationService @@ -19,6 +18,7 @@ "APPCONFIG_CONFIGURATION", "APPCONFIG_ENVIRONMENT", "LLOYD_GEORGE_DYNAMODB_NAME", + "MNS_NOTIFICATION_QUEUE_URL", ] ) @override_error_check @@ -32,26 +32,26 @@ def lambda_handler(event, context): try: sqs_message = json.loads(sqs_message["body"]) - if sqs_message["type"] == MNSNotificationTypes.SUBSCRIPTION: - handle_subscription(sqs_message) - continue + # if sqs_message["type"] == MNSNotificationTypes.SUBSCRIPTION: + # handle_subscription(sqs_message) + # continue mns_message = MNSSQSMessage(**sqs_message) MNSSQSMessage.model_validate(mns_message) - if mns_message.type in MNSNotificationTypes: + if mns_message.type in MNSNotificationTypes.__members__.values(): notification_service = MNSNotificationService() notification_service.handle_mns_notification(mns_message) - continue + logger.info("Continuing to next message.") except Exception as error: logger.error(f"Error processing SQS message: {error}.") logger.info("Continuing to next message.") -def handle_subscription(message): - try: - url = message["SubscribeURL"] - response = requests.get(url) - response.raise_for_status() - except Exception as error: - logger.error(f"Error processing subscription request: {error}.") +# def handle_subscription(message): +# try: +# url = message["SubscribeURL"] +# response = requests.get(url) +# response.raise_for_status() +# except Exception as error: +# logger.error(f"Error processing subscription request: {error}.") diff --git a/lambdas/models/mns_sqs_message.py b/lambdas/models/mns_sqs_message.py index 8e192dbfa..2431da3be 100644 --- a/lambdas/models/mns_sqs_message.py +++ b/lambdas/models/mns_sqs_message.py @@ -4,14 +4,18 @@ class MNSMessageSubject(BaseModel): model_config = ConfigDict( - alias_generator=AliasGenerator(validation_alias=to_camel), + alias_generator=AliasGenerator( + validation_alias=to_camel, serialization_alias=to_camel + ), ) nhs_number: str class MNSSQSMessage(BaseModel): model_config = ConfigDict( - alias_generator=AliasGenerator(validation_alias=to_camel), + alias_generator=AliasGenerator( + validation_alias=to_camel, serialization_alias=to_camel + ), ) id: str type: str diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index b6e7dd762..9a0d380e8 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -1,19 +1,16 @@ import os -import time -from urllib.error import HTTPError from botocore.exceptions import ClientError from enums.death_notification_status import DeathNotificationStatus from enums.mns_notification_types import MNSNotificationTypes from enums.patient_ods_inactive_status import PatientOdsInactiveStatus from models.mns_sqs_message import MNSSQSMessage -from models.pds_models import Patient -from pydantic_core._pydantic_core import ValidationError from services.base.dynamo_service import DynamoDBService from services.base.nhs_oauth_service import NhsOauthService +from services.base.sqs_service import SQSService from services.base.ssm_service import SSMService from utils.audit_logging_setup import LoggingService -from utils.exceptions import PdsErrorException, PdsResponseValidationException +from utils.exceptions import PdsErrorException from utils.utilities import get_pds_service logger = LoggingService(__name__) @@ -28,15 +25,23 @@ def __init__(self): ssm_service = SSMService() auth_service = NhsOauthService(ssm_service) self.pds_service = pds_service_class(ssm_service, auth_service) + self.unprocessed_message = [] + self.sqs_service = SQSService() + self.queue = os.getenv("MNS_NOTIFICATION_QUEUE_URL") def handle_mns_notification(self, message: MNSSQSMessage): try: if message.type == MNSNotificationTypes.CHANGE_OF_GP.value: + logger.info("Handling GP change notification.") self.handle_gp_change_notification(message) if message.type == MNSNotificationTypes.DEATH_NOTIFICATION.value: + logger.info("Handling death status notification.") self.handle_death_notification(message) + except PdsErrorException("Error when requesting patient from PDS"): + self.send_message_back_to_queue(message) + except Exception as e: logger.info( f"Unable to process message: {message.id}, of type: {message.type}" @@ -44,9 +49,6 @@ def handle_mns_notification(self, message: MNSSQSMessage): logger.info(f"{e}") return - def handle_subscription_notification(self, message): - pass - def handle_gp_change_notification(self, message: MNSSQSMessage): patient_document_references = self.get_patient_documents( @@ -95,9 +97,13 @@ def handle_death_notification(self, message: MNSSQSMessage): logger.info("Update complete, patient marked DECE.") return - update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) - self.update_patient_details(patient_documents, update_ods_code) - logger.info("Update complete for death notification change.") + if ( + message.data["deathNotificationStatus"] + == DeathNotificationStatus.REMOVED.value + ): + update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) + self.update_patient_details(patient_documents, update_ods_code) + logger.info("Update complete for death notification change.") except ClientError as e: logger.error(f"{e}") @@ -131,25 +137,11 @@ def update_patient_details(self, patient_documents: list[dict], code: str) -> No raise e def get_updated_gp_ods(self, nhs_number: str) -> str: - time.sleep(0.2) # buffer to avoid over stretching PDS API - logger.info("Getting the latest ODS code from PDS...") + patient_details = self.pds_service.fetch_patient_details(nhs_number) + return patient_details.general_practice_ods - try: - pds_response = self.pds_service.pds_request( - nhs_number=nhs_number, retry_on_expired=True - ) - pds_response.raise_for_status() - - pds_response_json = pds_response.json() - - patient = Patient.model_validate(pds_response_json) - - return patient.get_ods_code_or_inactive_status_for_gp() - except HTTPError as e: - raise PdsErrorException( - f"PDS returned a {e.response.status_code} code response for patient with NHS number {nhs_number}" - ) - except ValidationError: - raise PdsResponseValidationException( - f"PDS returned an invalid response for patient with NHS number {nhs_number}" - ) + def send_message_back_to_queue(self, message: MNSSQSMessage): + logger.info("Sending message back to queue...") + self.sqs_service.send_message_standard( + queue_url=self.queue, message_body=message.model_dump_json(by_alias=True) + ) diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index 5c0e14352..94f5ba3e0 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -13,7 +13,7 @@ "type": MNSNotificationTypes.CHANGE_OF_GP.value, "subject": { "nhsNumber": TEST_NHS_NUMBER, - "family_name": "SMITH", + "familyName": "SMITH", "dob": "2017-10-02", }, "source": { @@ -74,6 +74,9 @@ MOCK_INFORMAL_DEATH_MESSAGE_BODY = deepcopy(MOCK_DEATH_MESSAGE_BODY) MOCK_INFORMAL_DEATH_MESSAGE_BODY["data"]["deathNotificationStatus"] = "1" +MOCK_REMOVED_DEATH_MESSAGE_BODY = deepcopy(MOCK_DEATH_MESSAGE_BODY) +MOCK_REMOVED_DEATH_MESSAGE_BODY["data"]["deathNotificationStatus"] = "U" + @pytest.fixture def mock_service(mocker): diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index ad61aa7ae..206a56a07 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -9,12 +9,14 @@ MOCK_DEATH_MESSAGE_BODY, MOCK_GP_CHANGE_MESSAGE_BODY, MOCK_INFORMAL_DEATH_MESSAGE_BODY, + MOCK_REMOVED_DEATH_MESSAGE_BODY, ) from tests.unit.helpers.data.dynamo_responses import ( MOCK_EMPTY_RESPONSE, MOCK_SEARCH_RESPONSE, ) from tests.unit.helpers.mock_services import FakePDSService +from utils.exceptions import PdsErrorException @pytest.fixture @@ -23,12 +25,14 @@ def mns_service(mocker, set_env): service.pds_service = FakePDSService mocker.patch.object(service, "dynamo_service") mocker.patch.object(service, "get_updated_gp_ods") + mocker.patch.object(service, "sqs_service") yield service gp_change_message = MNSSQSMessage(**MOCK_GP_CHANGE_MESSAGE_BODY) death_notification_message = MNSSQSMessage(**MOCK_DEATH_MESSAGE_BODY) informal_death_notification_message = MNSSQSMessage(**MOCK_INFORMAL_DEATH_MESSAGE_BODY) +removed_death_notification_message = MNSSQSMessage(**MOCK_REMOVED_DEATH_MESSAGE_BODY) def test_handle_gp_change_message_called_message_type_GP_change(mns_service, mocker): @@ -62,6 +66,15 @@ def test_handle_notification_not_called_message_type_not_death_or_GP_notificatio mns_service.get_updated_gp_ods.assert_not_called() +def test_pds_is_called_death_notification_removed(mns_service, mocker): + mocker.patch.object(mns_service, "update_patient_details") + mns_service.dynamo_service.query_table_by_index.return_value = MOCK_SEARCH_RESPONSE + mns_service.handle_mns_notification(removed_death_notification_message) + + mns_service.get_updated_gp_ods.assert_called() + mns_service.update_patient_details.assert_called() + + def test_update_patient_details(mns_service): mns_service.update_patient_details( MOCK_SEARCH_RESPONSE["Items"], PatientOdsInactiveStatus.DECEASED.value @@ -93,6 +106,13 @@ def test_update_gp_ods_not_called_empty_dynamo_response(mns_service): mns_service.get_updated_gp_ods.assert_not_called() +def test_update_gp_ods_called_dynamo_response(mns_service): + mns_service.dynamo_service.query_table_by_index.return_value = MOCK_SEARCH_RESPONSE + mns_service.handle_gp_change_notification(gp_change_message) + + mns_service.get_updated_gp_ods.assert_called() + + def test_update_gp_ods_not_called_ods_codes_are_the_same(mns_service): mns_service.dynamo_service.query_table_by_index.return_value = MOCK_SEARCH_RESPONSE mns_service.get_updated_gp_ods.return_value = TEST_CURRENT_GP_ODS @@ -125,3 +145,11 @@ def test_handle_gp_change_updates_gp_ods_code(mns_service): ), ] mns_service.dynamo_service.update_item.assert_has_calls(calls, any_order=False) + + +def test_messages_is_put_back_on_the_queue_when_pds_error_raised(mns_service, mocker): + mns_service.get_updated_gp_ods.side_effect = PdsErrorException + mocker.patch.object(mns_service, "send_message_back_to_queue") + mns_service.handle_gp_change_notification(gp_change_message) + + mns_service.send_message_back_to_queue.assert_called() From 41cf2f4d99950a9cfecebdc1f1151fb2d1021fc0 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Wed, 27 Nov 2024 08:34:08 +0000 Subject: [PATCH 13/26] [PRMP-1188] add new MNS_SQS_QUEUE env to conftest --- lambdas/handlers/mns_notification_handler.py | 13 ------------- lambdas/services/process_mns_message_service.py | 1 - lambdas/tests/unit/conftest.py | 2 ++ .../unit/handlers/test_mns_notification_handler.py | 1 + .../services/test_process_mns_message_service.py | 3 +-- 5 files changed, 4 insertions(+), 16 deletions(-) diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py index 45bc63dd7..16d71e77e 100644 --- a/lambdas/handlers/mns_notification_handler.py +++ b/lambdas/handlers/mns_notification_handler.py @@ -32,10 +32,6 @@ def lambda_handler(event, context): try: sqs_message = json.loads(sqs_message["body"]) - # if sqs_message["type"] == MNSNotificationTypes.SUBSCRIPTION: - # handle_subscription(sqs_message) - # continue - mns_message = MNSSQSMessage(**sqs_message) MNSSQSMessage.model_validate(mns_message) @@ -46,12 +42,3 @@ def lambda_handler(event, context): except Exception as error: logger.error(f"Error processing SQS message: {error}.") logger.info("Continuing to next message.") - - -# def handle_subscription(message): -# try: -# url = message["SubscribeURL"] -# response = requests.get(url) -# response.raise_for_status() -# except Exception as error: -# logger.error(f"Error processing subscription request: {error}.") diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 9a0d380e8..75abba59b 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -25,7 +25,6 @@ def __init__(self): ssm_service = SSMService() auth_service = NhsOauthService(ssm_service) self.pds_service = pds_service_class(ssm_service, auth_service) - self.unprocessed_message = [] self.sqs_service = SQSService() self.queue = os.getenv("MNS_NOTIFICATION_QUEUE_URL") diff --git a/lambdas/tests/unit/conftest.py b/lambdas/tests/unit/conftest.py index c9f3c34be..97ddc0889 100644 --- a/lambdas/tests/unit/conftest.py +++ b/lambdas/tests/unit/conftest.py @@ -31,6 +31,7 @@ MOCK_LG_STAGING_STORE_BUCKET_ENV_NAME = "STAGING_STORE_BUCKET_NAME" MOCK_LG_METADATA_SQS_QUEUE_ENV_NAME = "METADATA_SQS_QUEUE_URL" MOCK_LG_INVALID_SQS_QUEUE_ENV_NAME = "INVALID_SQS_QUEUE_URL" +MOCK_MNS_SQS_QUEUE_ENV_NAME = "MNS_SQS_QUEUE_URL" MOCK_LG_BULK_UPLOAD_DYNAMO_ENV_NAME = "BULK_UPLOAD_DYNAMODB_NAME" MOCK_AUTH_DYNAMODB_NAME = "AUTH_DYNAMODB_NAME" @@ -167,6 +168,7 @@ def set_env(monkeypatch): ) monkeypatch.setenv("NRL_API_ENDPOINT", FAKE_URL) monkeypatch.setenv("NRL_END_USER_ODS_CODE", "test_nrl_user_ods_ssm_key") + monkeypatch.setenv("MNS_NOTIFICATION_QUEUE_URL", MOCK_MNS_SQS_QUEUE_ENV_NAME) EXPECTED_PARSED_PATIENT_BASE_CASE = PatientDetails( diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index 94f5ba3e0..b3b0f8b2a 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -85,6 +85,7 @@ def mock_service(mocker): ) mocked_instance = mocked_class.return_value mocker.patch.object(mocked_instance, "dynamo_service") + mocker.patch.object(mocked_instance, "pds_service") yield mocked_instance diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index 206a56a07..81f982d7c 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -15,17 +15,16 @@ MOCK_EMPTY_RESPONSE, MOCK_SEARCH_RESPONSE, ) -from tests.unit.helpers.mock_services import FakePDSService from utils.exceptions import PdsErrorException @pytest.fixture def mns_service(mocker, set_env): service = MNSNotificationService() - service.pds_service = FakePDSService mocker.patch.object(service, "dynamo_service") mocker.patch.object(service, "get_updated_gp_ods") mocker.patch.object(service, "sqs_service") + mocker.patch.object(service, "pds_service") yield service From 395c46ebc7a82aafcd1f98117f294c060864a894 Mon Sep 17 00:00:00 2001 From: NogaNHS Date: Wed, 27 Nov 2024 11:10:38 +0000 Subject: [PATCH 14/26] [PRMP-1188] fix unit test --- lambdas/services/process_mns_message_service.py | 12 +++--------- .../services/test_process_mns_message_service.py | 9 +++++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 75abba59b..d1d8f30a3 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -6,9 +6,7 @@ from enums.patient_ods_inactive_status import PatientOdsInactiveStatus from models.mns_sqs_message import MNSSQSMessage from services.base.dynamo_service import DynamoDBService -from services.base.nhs_oauth_service import NhsOauthService from services.base.sqs_service import SQSService -from services.base.ssm_service import SSMService from utils.audit_logging_setup import LoggingService from utils.exceptions import PdsErrorException from utils.utilities import get_pds_service @@ -21,10 +19,7 @@ def __init__(self): self.message = None self.dynamo_service = DynamoDBService() self.table = os.getenv("LLOYD_GEORGE_DYNAMODB_NAME") - pds_service_class = get_pds_service() - ssm_service = SSMService() - auth_service = NhsOauthService(ssm_service) - self.pds_service = pds_service_class(ssm_service, auth_service) + self.pds_service = get_pds_service() self.sqs_service = SQSService() self.queue = os.getenv("MNS_NOTIFICATION_QUEUE_URL") @@ -34,11 +29,11 @@ def handle_mns_notification(self, message: MNSSQSMessage): logger.info("Handling GP change notification.") self.handle_gp_change_notification(message) - if message.type == MNSNotificationTypes.DEATH_NOTIFICATION.value: + elif message.type == MNSNotificationTypes.DEATH_NOTIFICATION.value: logger.info("Handling death status notification.") self.handle_death_notification(message) - except PdsErrorException("Error when requesting patient from PDS"): + except PdsErrorException: self.send_message_back_to_queue(message) except Exception as e: @@ -49,7 +44,6 @@ def handle_mns_notification(self, message: MNSSQSMessage): return def handle_gp_change_notification(self, message: MNSSQSMessage): - patient_document_references = self.get_patient_documents( message.subject.nhs_number ) diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index 81f982d7c..f10ef25dc 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -19,12 +19,12 @@ @pytest.fixture -def mns_service(mocker, set_env): +def mns_service(mocker, set_env, monkeypatch): + monkeypatch.setenv("PDS_FHIR_IS_STUBBED", "False") service = MNSNotificationService() mocker.patch.object(service, "dynamo_service") mocker.patch.object(service, "get_updated_gp_ods") mocker.patch.object(service, "sqs_service") - mocker.patch.object(service, "pds_service") yield service @@ -147,8 +147,9 @@ def test_handle_gp_change_updates_gp_ods_code(mns_service): def test_messages_is_put_back_on_the_queue_when_pds_error_raised(mns_service, mocker): - mns_service.get_updated_gp_ods.side_effect = PdsErrorException + mocker.patch.object(mns_service, "handle_gp_change_notification") + mns_service.handle_gp_change_notification.side_effect = PdsErrorException() mocker.patch.object(mns_service, "send_message_back_to_queue") - mns_service.handle_gp_change_notification(gp_change_message) + mns_service.handle_mns_notification(gp_change_message) mns_service.send_message_back_to_queue.assert_called() From 5bcc4fd08bcde597c82c7d59cc0a0a6c69200285 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Wed, 27 Nov 2024 12:30:38 +0000 Subject: [PATCH 15/26] [PRMP-1188] remove subscription from enum --- lambdas/enums/mns_notification_types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lambdas/enums/mns_notification_types.py b/lambdas/enums/mns_notification_types.py index 4d3524596..9ae50aa02 100644 --- a/lambdas/enums/mns_notification_types.py +++ b/lambdas/enums/mns_notification_types.py @@ -4,4 +4,3 @@ class MNSNotificationTypes(StrEnum): CHANGE_OF_GP = "pds-change-of-gp-1" DEATH_NOTIFICATION = "pds-death-notification-1" - SUBSCRIPTION = "Subscription" From 68b676ab16113d24be01ac33210d2419b4622232 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Wed, 27 Nov 2024 12:40:19 +0000 Subject: [PATCH 16/26] [PRMP-1188] remove capital letters from test names --- lambdas/tests/unit/handlers/test_mns_notification_handler.py | 2 +- .../tests/unit/services/test_process_mns_message_service.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lambdas/tests/unit/handlers/test_mns_notification_handler.py b/lambdas/tests/unit/handlers/test_mns_notification_handler.py index b3b0f8b2a..cce52779a 100644 --- a/lambdas/tests/unit/handlers/test_mns_notification_handler.py +++ b/lambdas/tests/unit/handlers/test_mns_notification_handler.py @@ -89,7 +89,7 @@ def mock_service(mocker): yield mocked_instance -def test_handle_notification_called_message_type_GP_change( +def test_handle_notification_called_message_type_gp_change( context, set_env, mock_service ): diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index f10ef25dc..25c03e637 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -34,7 +34,7 @@ def mns_service(mocker, set_env, monkeypatch): removed_death_notification_message = MNSSQSMessage(**MOCK_REMOVED_DEATH_MESSAGE_BODY) -def test_handle_gp_change_message_called_message_type_GP_change(mns_service, mocker): +def test_handle_gp_change_message_called_message_type_gp_change(mns_service, mocker): mocker.patch.object(mns_service, "handle_gp_change_notification") mns_service.handle_mns_notification(gp_change_message) @@ -58,7 +58,7 @@ def test_is_informal_death_notification(mns_service): ) -def test_handle_notification_not_called_message_type_not_death_or_GP_notification( +def test_handle_notification_not_called_message_type_not_death_or_gp_notification( mns_service, ): mns_service.is_informal_death_notification(informal_death_notification_message) From 5d90083716950a7fadf5563ed945ef4d26ed66e3 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 29 Nov 2024 16:33:05 +0000 Subject: [PATCH 17/26] [PMRP-1188] continue with PR comment ammendents --- lambdas/handlers/mns_notification_handler.py | 14 ++- .../services/process_mns_message_service.py | 90 +++++++++---------- .../test_process_mns_message_service.py | 69 +++++++++++--- 3 files changed, 113 insertions(+), 60 deletions(-) diff --git a/lambdas/handlers/mns_notification_handler.py b/lambdas/handlers/mns_notification_handler.py index 16d71e77e..012d3401b 100644 --- a/lambdas/handlers/mns_notification_handler.py +++ b/lambdas/handlers/mns_notification_handler.py @@ -2,12 +2,14 @@ from enums.mns_notification_types import MNSNotificationTypes from models.mns_sqs_message import MNSSQSMessage +from pydantic_core._pydantic_core import ValidationError from services.process_mns_message_service import MNSNotificationService from utils.audit_logging_setup import LoggingService from utils.decorators.ensure_env_var import ensure_environment_variables from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions from utils.decorators.override_error_check import override_error_check from utils.decorators.set_audit_arg import set_request_context_for_logging +from utils.request_context import request_context logger = LoggingService(__name__) @@ -25,7 +27,7 @@ @handle_lambda_exceptions def lambda_handler(event, context): logger.info(f"Received MNS notification event: {event}") - + notification_service = MNSNotificationService() sqs_messages = event.get("Records", []) for sqs_message in sqs_messages: @@ -35,10 +37,14 @@ def lambda_handler(event, context): mns_message = MNSSQSMessage(**sqs_message) MNSSQSMessage.model_validate(mns_message) + request_context.patient_nhs_no = mns_message.subject.nhs_number + if mns_message.type in MNSNotificationTypes.__members__.values(): - notification_service = MNSNotificationService() notification_service.handle_mns_notification(mns_message) - logger.info("Continuing to next message.") + + except ValidationError as error: + logger.error("Malformed MNS notification message") + logger.error(error) except Exception as error: logger.error(f"Error processing SQS message: {error}.") - logger.info("Continuing to next message.") + logger.info("Continuing to next message.") diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index d1d8f30a3..88be71035 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -16,7 +16,6 @@ class MNSNotificationService: def __init__(self): - self.message = None self.dynamo_service = DynamoDBService() self.table = os.getenv("LLOYD_GEORGE_DYNAMODB_NAME") self.pds_service = get_pds_service() @@ -25,32 +24,30 @@ def __init__(self): def handle_mns_notification(self, message: MNSSQSMessage): try: - if message.type == MNSNotificationTypes.CHANGE_OF_GP.value: + if message.type == MNSNotificationTypes.CHANGE_OF_GP: logger.info("Handling GP change notification.") self.handle_gp_change_notification(message) - elif message.type == MNSNotificationTypes.DEATH_NOTIFICATION.value: + elif message.type == MNSNotificationTypes.DEATH_NOTIFICATION: logger.info("Handling death status notification.") self.handle_death_notification(message) except PdsErrorException: + logger.info("An error occurred when calling PDS") self.send_message_back_to_queue(message) - except Exception as e: + except ClientError as e: logger.info( f"Unable to process message: {message.id}, of type: {message.type}" ) logger.info(f"{e}") - return def handle_gp_change_notification(self, message: MNSSQSMessage): patient_document_references = self.get_patient_documents( message.subject.nhs_number ) - if len(patient_document_references) < 1: - logger.info("Patient is not held in the National Document Repository.") - logger.info("Moving on to the next message.") + if not self.patient_is_present_in_ndr(patient_document_references): return updated_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) @@ -72,40 +69,37 @@ def handle_death_notification(self, message: MNSSQSMessage): ) return - try: - patient_documents = self.get_patient_documents(message.subject.nhs_number) - - if len(patient_documents) < 1: - logger.info("Patient is not held in the National Document Repository.") - logger.info("Moving on to the next message.") - return - - if ( - message.data["deathNotificationStatus"] - == DeathNotificationStatus.FORMAL.value - ): - self.update_patient_details( - patient_documents, PatientOdsInactiveStatus.DECEASED.value - ) - logger.info("Update complete, patient marked DECE.") - return - - if ( - message.data["deathNotificationStatus"] - == DeathNotificationStatus.REMOVED.value - ): - update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) - self.update_patient_details(patient_documents, update_ods_code) - logger.info("Update complete for death notification change.") - except ClientError as e: - logger.error(f"{e}") + patient_documents = self.get_patient_documents(message.subject.nhs_number) + + if not self.patient_is_present_in_ndr(patient_documents): + return + + if self.is_formal_death_notification(message): + self.update_patient_details( + patient_documents, PatientOdsInactiveStatus.DECEASED + ) + logger.info( + f"Update complete, patient marked {PatientOdsInactiveStatus.DECEASED}." + ) + + elif self.is_removed_death_notification(message): + update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) + self.update_patient_details(patient_documents, update_ods_code) + logger.info("Update complete for death notification change.") def is_informal_death_notification(self, message: MNSSQSMessage) -> bool: return ( - message.data["deathNotificationStatus"] - == DeathNotificationStatus.INFORMAL.value + message.data["deathNotificationStatus"] == DeathNotificationStatus.INFORMAL + ) + + def is_removed_death_notification(self, message: MNSSQSMessage) -> bool: + return ( + message.data["deathNotificationStatus"] == DeathNotificationStatus.REMOVED ) + def is_formal_death_notification(self, message: MNSSQSMessage) -> bool: + return message.data["deathNotificationStatus"] == DeathNotificationStatus.FORMAL + def get_patient_documents(self, nhs_number: str): logger.info("Getting patient document references...") response = self.dynamo_service.query_table_by_index( @@ -119,15 +113,11 @@ def get_patient_documents(self, nhs_number: str): def update_patient_details(self, patient_documents: list[dict], code: str) -> None: for document in patient_documents: logger.info("Updating patient document reference...") - try: - self.dynamo_service.update_item( - table_name=self.table, - key=document["ID"], - updated_fields={"CurrentGpOds": code}, - ) - except ClientError as e: - logger.error("Unable to update patient document reference") - raise e + self.dynamo_service.update_item( + table_name=self.table, + key=document["ID"], + updated_fields={"CurrentGpOds": code}, + ) def get_updated_gp_ods(self, nhs_number: str) -> str: patient_details = self.pds_service.fetch_patient_details(nhs_number) @@ -138,3 +128,11 @@ def send_message_back_to_queue(self, message: MNSSQSMessage): self.sqs_service.send_message_standard( queue_url=self.queue, message_body=message.model_dump_json(by_alias=True) ) + + def patient_is_present_in_ndr(self, dynamo_response): + if len(dynamo_response) < 1: + logger.info("Patient is not held in the National Document Repository.") + logger.info("Moving on to the next message.") + return False + else: + return True diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index 25c03e637..a628db547 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -25,39 +25,89 @@ def mns_service(mocker, set_env, monkeypatch): mocker.patch.object(service, "dynamo_service") mocker.patch.object(service, "get_updated_gp_ods") mocker.patch.object(service, "sqs_service") + + mocker.patch.object(service, "handle_death_notification") yield service +@pytest.fixture +def mock_handle_gp_change(mns_service, mocker): + method = mocker.patch.object(mns_service, "handle_gp_change_notification") + yield method + + gp_change_message = MNSSQSMessage(**MOCK_GP_CHANGE_MESSAGE_BODY) death_notification_message = MNSSQSMessage(**MOCK_DEATH_MESSAGE_BODY) informal_death_notification_message = MNSSQSMessage(**MOCK_INFORMAL_DEATH_MESSAGE_BODY) removed_death_notification_message = MNSSQSMessage(**MOCK_REMOVED_DEATH_MESSAGE_BODY) -def test_handle_gp_change_message_called_message_type_gp_change(mns_service, mocker): - mocker.patch.object(mns_service, "handle_gp_change_notification") +def test_handle_gp_change_message_called_message_type_gp_change( + mns_service, mock_handle_gp_change +): mns_service.handle_mns_notification(gp_change_message) - mns_service.handle_gp_change_notification.assert_called() + mns_service.handle_death_notification.assert_not_called() + mock_handle_gp_change.handle_gp_change_notification.assert_called() -def test_handle_gp_change_message_not_called_message_death_message(mns_service, mocker): - mocker.patch.object(mns_service, "handle_gp_change_notification") +def test_handle_gp_change_message_not_called_message_death_message(mns_service): mns_service.handle_mns_notification(death_notification_message) mns_service.handle_gp_change_notification.assert_not_called() + mns_service.handle_death_notification.assert_called_with(death_notification_message) + + +def test_has_patient_in_ndr_populate_response_from_dynamo(mns_service): + response = MOCK_SEARCH_RESPONSE["Items"] + assert mns_service.patient_is_present_in_ndr(response) is True + + +def test_has_patient_in_ndr_empty_dynamo_response(mns_service): + response = MOCK_EMPTY_RESPONSE["Items"] + assert mns_service.patient_is_present_in_ndr(response) is False def test_is_informal_death_notification(mns_service): assert ( mns_service.is_informal_death_notification(death_notification_message) is False ) + assert ( + mns_service.is_informal_death_notification(removed_death_notification_message) + is False + ) assert ( mns_service.is_informal_death_notification(informal_death_notification_message) is True ) +def test_is_removed_death_notification(mns_service): + assert ( + mns_service.is_removed_death_notification(death_notification_message) is False + ) + assert ( + mns_service.is_removed_death_notification(informal_death_notification_message) + is False + ) + assert ( + mns_service.is_removed_death_notification(removed_death_notification_message) + is True + ) + + +def test_is_formal_death_notification(mns_service): + assert ( + mns_service.is_formal_death_notification(removed_death_notification_message) + is False + ) + assert ( + mns_service.is_formal_death_notification(informal_death_notification_message) + is False + ) + assert mns_service.is_formal_death_notification(death_notification_message) is True + + def test_handle_notification_not_called_message_type_not_death_or_gp_notification( mns_service, ): @@ -76,23 +126,23 @@ def test_pds_is_called_death_notification_removed(mns_service, mocker): def test_update_patient_details(mns_service): mns_service.update_patient_details( - MOCK_SEARCH_RESPONSE["Items"], PatientOdsInactiveStatus.DECEASED.value + MOCK_SEARCH_RESPONSE["Items"], PatientOdsInactiveStatus.DECEASED ) calls = [ call( table_name=mns_service.table, key="3d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED.value}, + updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED}, ), call( table_name=mns_service.table, key="4d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED.value}, + updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED}, ), call( table_name=mns_service.table, key="5d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED.value}, + updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED}, ), ] mns_service.dynamo_service.update_item.assert_has_calls(calls, any_order=False) @@ -147,7 +197,6 @@ def test_handle_gp_change_updates_gp_ods_code(mns_service): def test_messages_is_put_back_on_the_queue_when_pds_error_raised(mns_service, mocker): - mocker.patch.object(mns_service, "handle_gp_change_notification") mns_service.handle_gp_change_notification.side_effect = PdsErrorException() mocker.patch.object(mns_service, "send_message_back_to_queue") mns_service.handle_mns_notification(gp_change_message) From 681a5d6d460fd6c861fe831b564da85312aeb512 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 29 Nov 2024 17:09:02 +0000 Subject: [PATCH 18/26] [PMRP-1188] add fixtures methods used frequently in test file --- .../test_process_mns_message_service.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index a628db547..863240665 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -25,15 +25,20 @@ def mns_service(mocker, set_env, monkeypatch): mocker.patch.object(service, "dynamo_service") mocker.patch.object(service, "get_updated_gp_ods") mocker.patch.object(service, "sqs_service") + yield service - mocker.patch.object(service, "handle_death_notification") + +@pytest.fixture +def mock_handle_gp_change(mocker, mns_service): + service = mns_service + mocker.patch.object(service, "handle_gp_change_notification") yield service @pytest.fixture -def mock_handle_gp_change(mns_service, mocker): - method = mocker.patch.object(mns_service, "handle_gp_change_notification") - yield method +def mock_handle_death_notification(mocker, mns_service): + service = mns_service + mocker.patch.object(service, "handle_death_notification") gp_change_message = MNSSQSMessage(**MOCK_GP_CHANGE_MESSAGE_BODY) @@ -43,15 +48,17 @@ def mock_handle_gp_change(mns_service, mocker): def test_handle_gp_change_message_called_message_type_gp_change( - mns_service, mock_handle_gp_change + mns_service, mock_handle_gp_change, mock_handle_death_notification ): mns_service.handle_mns_notification(gp_change_message) mns_service.handle_death_notification.assert_not_called() - mock_handle_gp_change.handle_gp_change_notification.assert_called() + mns_service.handle_gp_change_notification.assert_called_with(gp_change_message) -def test_handle_gp_change_message_not_called_message_death_message(mns_service): +def test_handle_gp_change_message_not_called_message_death_message( + mns_service, mock_handle_death_notification, mock_handle_gp_change +): mns_service.handle_mns_notification(death_notification_message) mns_service.handle_gp_change_notification.assert_not_called() @@ -196,7 +203,9 @@ def test_handle_gp_change_updates_gp_ods_code(mns_service): mns_service.dynamo_service.update_item.assert_has_calls(calls, any_order=False) -def test_messages_is_put_back_on_the_queue_when_pds_error_raised(mns_service, mocker): +def test_messages_is_put_back_on_the_queue_when_pds_error_raised( + mns_service, mocker, mock_handle_gp_change +): mns_service.handle_gp_change_notification.side_effect = PdsErrorException() mocker.patch.object(mns_service, "send_message_back_to_queue") mns_service.handle_mns_notification(gp_change_message) From 48b54d09a1aac53ce91cd6c3a3646c67a2bb92b2 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 2 Dec 2024 08:56:27 +0000 Subject: [PATCH 19/26] [PRMP-1188] implement updating lastUpdated field --- .../services/process_mns_message_service.py | 16 ++- .../test_process_mns_message_service.py | 113 +++++++++++------- 2 files changed, 87 insertions(+), 42 deletions(-) diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index 88be71035..f0aec246c 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -1,7 +1,9 @@ import os +from datetime import datetime from botocore.exceptions import ClientError from enums.death_notification_status import DeathNotificationStatus +from enums.metadata_field_names import DocumentReferenceMetadataFields from enums.mns_notification_types import MNSNotificationTypes from enums.patient_ods_inactive_status import PatientOdsInactiveStatus from models.mns_sqs_message import MNSSQSMessage @@ -57,7 +59,12 @@ def handle_gp_change_notification(self, message: MNSSQSMessage): self.dynamo_service.update_item( table_name=self.table, key=reference["ID"], - updated_fields={"CurrentGpOds": updated_ods_code}, + updated_fields={ + DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: updated_ods_code, + DocumentReferenceMetadataFields.LAST_UPDATED.value: int( + datetime.now().timestamp() + ), + }, ) logger.info("Update complete for change of GP") @@ -116,7 +123,12 @@ def update_patient_details(self, patient_documents: list[dict], code: str) -> No self.dynamo_service.update_item( table_name=self.table, key=document["ID"], - updated_fields={"CurrentGpOds": code}, + updated_fields={ + DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: code, + DocumentReferenceMetadataFields.LAST_UPDATED.value: int( + datetime.now().timestamp() + ), + }, ) def get_updated_gp_ods(self, nhs_number: str) -> str: diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index 863240665..f7b077286 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -1,7 +1,10 @@ +from datetime import datetime from unittest.mock import call import pytest +from enums.metadata_field_names import DocumentReferenceMetadataFields from enums.patient_ods_inactive_status import PatientOdsInactiveStatus +from freezegun import freeze_time from models.mns_sqs_message import MNSSQSMessage from services.process_mns_message_service import MNSNotificationService from tests.unit.conftest import TEST_CURRENT_GP_ODS @@ -41,6 +44,8 @@ def mock_handle_death_notification(mocker, mns_service): mocker.patch.object(service, "handle_death_notification") +MOCK_UPDATE_TIME = "2024-01-01 12:00:00" + gp_change_message = MNSSQSMessage(**MOCK_GP_CHANGE_MESSAGE_BODY) death_notification_message = MNSSQSMessage(**MOCK_DEATH_MESSAGE_BODY) informal_death_notification_message = MNSSQSMessage(**MOCK_INFORMAL_DEATH_MESSAGE_BODY) @@ -75,44 +80,40 @@ def test_has_patient_in_ndr_empty_dynamo_response(mns_service): assert mns_service.patient_is_present_in_ndr(response) is False -def test_is_informal_death_notification(mns_service): - assert ( - mns_service.is_informal_death_notification(death_notification_message) is False - ) - assert ( - mns_service.is_informal_death_notification(removed_death_notification_message) - is False - ) - assert ( - mns_service.is_informal_death_notification(informal_death_notification_message) - is True - ) +@pytest.mark.parametrize( + "event_message, output", + [ + (removed_death_notification_message, False), + (informal_death_notification_message, True), + (death_notification_message, False), + ], +) +def test_is_informal_death_notification(mns_service, event_message, output): + assert mns_service.is_informal_death_notification(event_message) is output -def test_is_removed_death_notification(mns_service): - assert ( - mns_service.is_removed_death_notification(death_notification_message) is False - ) - assert ( - mns_service.is_removed_death_notification(informal_death_notification_message) - is False - ) - assert ( - mns_service.is_removed_death_notification(removed_death_notification_message) - is True - ) +@pytest.mark.parametrize( + "event_message, output", + [ + (removed_death_notification_message, True), + (informal_death_notification_message, False), + (death_notification_message, False), + ], +) +def test_is_removed_death_notification(mns_service, event_message, output): + assert mns_service.is_removed_death_notification(event_message) is output -def test_is_formal_death_notification(mns_service): - assert ( - mns_service.is_formal_death_notification(removed_death_notification_message) - is False - ) - assert ( - mns_service.is_formal_death_notification(informal_death_notification_message) - is False - ) - assert mns_service.is_formal_death_notification(death_notification_message) is True +@pytest.mark.parametrize( + "event_message, output", + [ + (removed_death_notification_message, False), + (informal_death_notification_message, False), + (death_notification_message, True), + ], +) +def test_is_formal_death_notification(mns_service, event_message, output): + assert mns_service.is_formal_death_notification(event_message) is output def test_handle_notification_not_called_message_type_not_death_or_gp_notification( @@ -131,6 +132,7 @@ def test_pds_is_called_death_notification_removed(mns_service, mocker): mns_service.update_patient_details.assert_called() +@freeze_time(MOCK_UPDATE_TIME) def test_update_patient_details(mns_service): mns_service.update_patient_details( MOCK_SEARCH_RESPONSE["Items"], PatientOdsInactiveStatus.DECEASED @@ -139,17 +141,32 @@ def test_update_patient_details(mns_service): call( table_name=mns_service.table, key="3d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED}, + updated_fields={ + DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: PatientOdsInactiveStatus.DECEASED, + DocumentReferenceMetadataFields.LAST_UPDATED.value: int( + datetime.fromisoformat(MOCK_UPDATE_TIME).timestamp() + ), + }, ), call( table_name=mns_service.table, key="4d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED}, + updated_fields={ + DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: PatientOdsInactiveStatus.DECEASED, + DocumentReferenceMetadataFields.LAST_UPDATED.value: int( + datetime.fromisoformat(MOCK_UPDATE_TIME).timestamp() + ), + }, ), call( table_name=mns_service.table, key="5d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": PatientOdsInactiveStatus.DECEASED}, + updated_fields={ + DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: PatientOdsInactiveStatus.DECEASED, + DocumentReferenceMetadataFields.LAST_UPDATED.value: int( + datetime.fromisoformat(MOCK_UPDATE_TIME).timestamp() + ), + }, ), ] mns_service.dynamo_service.update_item.assert_has_calls(calls, any_order=False) @@ -177,6 +194,7 @@ def test_update_gp_ods_not_called_ods_codes_are_the_same(mns_service): mns_service.dynamo_service.update_item.assert_not_called() +@freeze_time(MOCK_UPDATE_TIME) def test_handle_gp_change_updates_gp_ods_code(mns_service): mns_service.dynamo_service.query_table_by_index.return_value = MOCK_SEARCH_RESPONSE other_gp_ods = "Z12345" @@ -187,17 +205,32 @@ def test_handle_gp_change_updates_gp_ods_code(mns_service): call( table_name=mns_service.table, key="3d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": other_gp_ods}, + updated_fields={ + DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: other_gp_ods, + DocumentReferenceMetadataFields.LAST_UPDATED.value: int( + datetime.fromisoformat(MOCK_UPDATE_TIME).timestamp() + ), + }, ), call( table_name=mns_service.table, key="4d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": other_gp_ods}, + updated_fields={ + DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: other_gp_ods, + DocumentReferenceMetadataFields.LAST_UPDATED.value: int( + datetime.fromisoformat(MOCK_UPDATE_TIME).timestamp() + ), + }, ), call( table_name=mns_service.table, key="5d8683b9-1665-40d2-8499-6e8302d507ff", - updated_fields={"CurrentGpOds": other_gp_ods}, + updated_fields={ + DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: other_gp_ods, + DocumentReferenceMetadataFields.LAST_UPDATED.value: int( + datetime.fromisoformat(MOCK_UPDATE_TIME).timestamp() + ), + }, ), ] mns_service.dynamo_service.update_item.assert_has_calls(calls, any_order=False) From 1f94d461d0ed0f79065143d52c86d6fb3033acc7 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 2 Dec 2024 09:01:29 +0000 Subject: [PATCH 20/26] [PRMP-1188] add assert with to sending message back to queue test --- lambdas/tests/unit/services/test_process_mns_message_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index f7b077286..0d667381b 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -243,4 +243,4 @@ def test_messages_is_put_back_on_the_queue_when_pds_error_raised( mocker.patch.object(mns_service, "send_message_back_to_queue") mns_service.handle_mns_notification(gp_change_message) - mns_service.send_message_back_to_queue.assert_called() + mns_service.send_message_back_to_queue.assert_called_with(gp_change_message) From caaf8bd41bc351140bcadbebd4fd465d109b2a9d Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 2 Dec 2024 11:14:56 +0000 Subject: [PATCH 21/26] [PRMP-1188] add conditional to deploy lambdas --- .github/workflows/base-lambdas-reusable-deploy-all.yml | 1 + .github/workflows/subscribe-to-mns.yml | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/base-lambdas-reusable-deploy-all.yml b/.github/workflows/base-lambdas-reusable-deploy-all.yml index f1185c986..08712e8e8 100644 --- a/.github/workflows/base-lambdas-reusable-deploy-all.yml +++ b/.github/workflows/base-lambdas-reusable-deploy-all.yml @@ -404,6 +404,7 @@ jobs: deploy_mns_notification_lambda: name: Deploy mns notification lambda + if: !contains( ['ndra', 'ndrb', 'ndrc', 'ndrd'], ${{ inputs.sandbox }} ) uses: ./.github/workflows/base-lambdas-reusable-deploy.yml with: environment: ${{ inputs.environment}} diff --git a/.github/workflows/subscribe-to-mns.yml b/.github/workflows/subscribe-to-mns.yml index c49f68eae..1325ea5ef 100644 --- a/.github/workflows/subscribe-to-mns.yml +++ b/.github/workflows/subscribe-to-mns.yml @@ -16,11 +16,10 @@ on: AWS_ASSUME_ROLE: required: true permissions: - pull-requests: write id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout jobs: - batch_update_build_docker_image: + placeholder: runs-on: ubuntu-latest environment: ${{ inputs.environment }} defaults: @@ -28,6 +27,6 @@ jobs: working-directory: lambdas steps: - name: Placeholder - run: | - echo "Running placeholder job on ${inputs.sandbox}" + run: | + echo "Running placeholder job on ${inputs.sandbox}" From 4ffb470c54a2ceda3b6eac3b865c1395b1d329db Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 2 Dec 2024 11:29:18 +0000 Subject: [PATCH 22/26] [PRMP-1188] change workflow conditional --- .github/workflows/base-lambdas-reusable-deploy-all.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/base-lambdas-reusable-deploy-all.yml b/.github/workflows/base-lambdas-reusable-deploy-all.yml index 08712e8e8..b2b4658b0 100644 --- a/.github/workflows/base-lambdas-reusable-deploy-all.yml +++ b/.github/workflows/base-lambdas-reusable-deploy-all.yml @@ -404,7 +404,7 @@ jobs: deploy_mns_notification_lambda: name: Deploy mns notification lambda - if: !contains( ['ndra', 'ndrb', 'ndrc', 'ndrd'], ${{ inputs.sandbox }} ) + if: ${{ inputs.environment }} == 'development' uses: ./.github/workflows/base-lambdas-reusable-deploy.yml with: environment: ${{ inputs.environment}} From 41b6ca3780d7f7fd81a1f968037f8e006341b2a0 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 2 Dec 2024 11:39:44 +0000 Subject: [PATCH 23/26] [PRMP-1188] change workflow to deploy MNS lambda run when not in development env --- .github/workflows/base-lambdas-reusable-deploy-all.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/base-lambdas-reusable-deploy-all.yml b/.github/workflows/base-lambdas-reusable-deploy-all.yml index b2b4658b0..1bb13071d 100644 --- a/.github/workflows/base-lambdas-reusable-deploy-all.yml +++ b/.github/workflows/base-lambdas-reusable-deploy-all.yml @@ -404,7 +404,7 @@ jobs: deploy_mns_notification_lambda: name: Deploy mns notification lambda - if: ${{ inputs.environment }} == 'development' + if: ${{ inputs.environment }} != 'development' uses: ./.github/workflows/base-lambdas-reusable-deploy.yml with: environment: ${{ inputs.environment}} From 794351575169f21b262a13fcc26222a3035b0dd4 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Mon, 2 Dec 2024 13:55:41 +0000 Subject: [PATCH 24/26] [PRMP-1188] try different conditional for conditional deployment of lambda --- .github/workflows/base-lambdas-reusable-deploy-all.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/base-lambdas-reusable-deploy-all.yml b/.github/workflows/base-lambdas-reusable-deploy-all.yml index 1bb13071d..779447fe4 100644 --- a/.github/workflows/base-lambdas-reusable-deploy-all.yml +++ b/.github/workflows/base-lambdas-reusable-deploy-all.yml @@ -404,7 +404,10 @@ jobs: deploy_mns_notification_lambda: name: Deploy mns notification lambda - if: ${{ inputs.environment }} != 'development' + strategy: + matrix: + env_to_build_on: ['prod', 'pre-prod', 'ndr-test', 'ndr-dev'] + if: ${{ matrix.env_to_build_on }} == ${{ inputs.sandbox }} uses: ./.github/workflows/base-lambdas-reusable-deploy.yml with: environment: ${{ inputs.environment}} From bbfd72058b4e794feabb9a92109f91aff15d50fc Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 6 Dec 2024 12:56:26 +0000 Subject: [PATCH 25/26] [PRMP-1188] address PR comments --- .../services/process_mns_message_service.py | 76 +++++++++---------- .../test_process_mns_message_service.py | 44 +---------- 2 files changed, 40 insertions(+), 80 deletions(-) diff --git a/lambdas/services/process_mns_message_service.py b/lambdas/services/process_mns_message_service.py index f0aec246c..ca8fb6e81 100644 --- a/lambdas/services/process_mns_message_service.py +++ b/lambdas/services/process_mns_message_service.py @@ -26,13 +26,13 @@ def __init__(self): def handle_mns_notification(self, message: MNSSQSMessage): try: - if message.type == MNSNotificationTypes.CHANGE_OF_GP: - logger.info("Handling GP change notification.") - self.handle_gp_change_notification(message) - - elif message.type == MNSNotificationTypes.DEATH_NOTIFICATION: - logger.info("Handling death status notification.") - self.handle_death_notification(message) + match message.type: + case MNSNotificationTypes.CHANGE_OF_GP: + logger.info("Handling GP change notification.") + self.handle_gp_change_notification(message) + case MNSNotificationTypes.DEATH_NOTIFICATION: + logger.info("Handling death status notification.") + self.handle_death_notification(message) except PdsErrorException: logger.info("An error occurred when calling PDS") @@ -70,42 +70,38 @@ def handle_gp_change_notification(self, message: MNSSQSMessage): logger.info("Update complete for change of GP") def handle_death_notification(self, message: MNSSQSMessage): - if self.is_informal_death_notification(message): - logger.info( - "Patient is deceased - INFORMAL, moving on to the next message." - ) - return - - patient_documents = self.get_patient_documents(message.subject.nhs_number) - - if not self.patient_is_present_in_ndr(patient_documents): - return - - if self.is_formal_death_notification(message): - self.update_patient_details( - patient_documents, PatientOdsInactiveStatus.DECEASED - ) - logger.info( - f"Update complete, patient marked {PatientOdsInactiveStatus.DECEASED}." - ) + death_notification_type = message.data["deathNotificationStatus"] + match death_notification_type: + case DeathNotificationStatus.INFORMAL: + logger.info( + "Patient is deceased - INFORMAL, moving on to the next message." + ) + return - elif self.is_removed_death_notification(message): - update_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) - self.update_patient_details(patient_documents, update_ods_code) - logger.info("Update complete for death notification change.") + case DeathNotificationStatus.REMOVED: + patient_documents = self.get_patient_documents( + message.subject.nhs_number + ) + if not self.patient_is_present_in_ndr(patient_documents): + return - def is_informal_death_notification(self, message: MNSSQSMessage) -> bool: - return ( - message.data["deathNotificationStatus"] == DeathNotificationStatus.INFORMAL - ) + updated_ods_code = self.get_updated_gp_ods(message.subject.nhs_number) + self.update_patient_ods_code(patient_documents, updated_ods_code) + logger.info("Update complete for death notification change.") - def is_removed_death_notification(self, message: MNSSQSMessage) -> bool: - return ( - message.data["deathNotificationStatus"] == DeathNotificationStatus.REMOVED - ) + case DeathNotificationStatus.FORMAL: + patient_documents = self.get_patient_documents( + message.subject.nhs_number + ) + if not self.patient_is_present_in_ndr(patient_documents): + return - def is_formal_death_notification(self, message: MNSSQSMessage) -> bool: - return message.data["deathNotificationStatus"] == DeathNotificationStatus.FORMAL + self.update_patient_ods_code( + patient_documents, PatientOdsInactiveStatus.DECEASED + ) + logger.info( + f"Update complete, patient marked {PatientOdsInactiveStatus.DECEASED}." + ) def get_patient_documents(self, nhs_number: str): logger.info("Getting patient document references...") @@ -117,7 +113,7 @@ def get_patient_documents(self, nhs_number: str): ) return response["Items"] - def update_patient_details(self, patient_documents: list[dict], code: str) -> None: + def update_patient_ods_code(self, patient_documents: list[dict], code: str) -> None: for document in patient_documents: logger.info("Updating patient document reference...") self.dynamo_service.update_item( diff --git a/lambdas/tests/unit/services/test_process_mns_message_service.py b/lambdas/tests/unit/services/test_process_mns_message_service.py index 0d667381b..2cd50e1ae 100644 --- a/lambdas/tests/unit/services/test_process_mns_message_service.py +++ b/lambdas/tests/unit/services/test_process_mns_message_service.py @@ -80,61 +80,25 @@ def test_has_patient_in_ndr_empty_dynamo_response(mns_service): assert mns_service.patient_is_present_in_ndr(response) is False -@pytest.mark.parametrize( - "event_message, output", - [ - (removed_death_notification_message, False), - (informal_death_notification_message, True), - (death_notification_message, False), - ], -) -def test_is_informal_death_notification(mns_service, event_message, output): - assert mns_service.is_informal_death_notification(event_message) is output - - -@pytest.mark.parametrize( - "event_message, output", - [ - (removed_death_notification_message, True), - (informal_death_notification_message, False), - (death_notification_message, False), - ], -) -def test_is_removed_death_notification(mns_service, event_message, output): - assert mns_service.is_removed_death_notification(event_message) is output - - -@pytest.mark.parametrize( - "event_message, output", - [ - (removed_death_notification_message, False), - (informal_death_notification_message, False), - (death_notification_message, True), - ], -) -def test_is_formal_death_notification(mns_service, event_message, output): - assert mns_service.is_formal_death_notification(event_message) is output - - def test_handle_notification_not_called_message_type_not_death_or_gp_notification( mns_service, ): - mns_service.is_informal_death_notification(informal_death_notification_message) + mns_service.handle_mns_notification(informal_death_notification_message) mns_service.get_updated_gp_ods.assert_not_called() def test_pds_is_called_death_notification_removed(mns_service, mocker): - mocker.patch.object(mns_service, "update_patient_details") + mocker.patch.object(mns_service, "update_patient_ods_code") mns_service.dynamo_service.query_table_by_index.return_value = MOCK_SEARCH_RESPONSE mns_service.handle_mns_notification(removed_death_notification_message) mns_service.get_updated_gp_ods.assert_called() - mns_service.update_patient_details.assert_called() + mns_service.update_patient_ods_code.assert_called() @freeze_time(MOCK_UPDATE_TIME) def test_update_patient_details(mns_service): - mns_service.update_patient_details( + mns_service.update_patient_ods_code( MOCK_SEARCH_RESPONSE["Items"], PatientOdsInactiveStatus.DECEASED ) calls = [ From 85ea123d841a0f7ccb639e90ab593bd823ab56c4 Mon Sep 17 00:00:00 2001 From: Steph Torres Date: Fri, 20 Dec 2024 11:38:39 +0000 Subject: [PATCH 26/26] [PRMP-1188] remove conditional deploy of lambda, doesn't work as expected --- .github/workflows/base-lambdas-reusable-deploy-all.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/base-lambdas-reusable-deploy-all.yml b/.github/workflows/base-lambdas-reusable-deploy-all.yml index 779447fe4..f1185c986 100644 --- a/.github/workflows/base-lambdas-reusable-deploy-all.yml +++ b/.github/workflows/base-lambdas-reusable-deploy-all.yml @@ -404,10 +404,6 @@ jobs: deploy_mns_notification_lambda: name: Deploy mns notification lambda - strategy: - matrix: - env_to_build_on: ['prod', 'pre-prod', 'ndr-test', 'ndr-dev'] - if: ${{ matrix.env_to_build_on }} == ${{ inputs.sandbox }} uses: ./.github/workflows/base-lambdas-reusable-deploy.yml with: environment: ${{ inputs.environment}}