Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PRMP-1188 Create a lambda to handle MNS notifications #470

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d11669e
[PRMP-1188] create lambda to handle mns sqs messages
steph-torres-nhs Nov 21, 2024
76970c3
[PRMP-1188] introduce MNS SQS message model
steph-torres-nhs Nov 21, 2024
bc0fe45
[PRMP-1188] continue work on MNS notification handler
steph-torres-nhs Nov 22, 2024
81707f5
[PRMP-1188] start creating MNSNofiticationSerive
steph-torres-nhs Nov 22, 2024
aa21f1e
[PRMP-1188] add dynamo service to mns notification service
steph-torres-nhs Nov 22, 2024
f970374
[PRMP-1188] continue handle death notification process
steph-torres-nhs Nov 22, 2024
d608e19
[PRMP-1188] handle gp change notification
steph-torres-nhs Nov 22, 2024
e7b69bb
[PRMP-1188] add lambda to workflow
steph-torres-nhs Nov 25, 2024
488b81b
[PRMP-1188] remove unnecessary fields from model
steph-torres-nhs Nov 25, 2024
8c17876
[PMRP-1188] change table env name to correct name
steph-torres-nhs Nov 25, 2024
104f8c1
[PRMP-1188] refactor log messages
steph-torres-nhs Nov 26, 2024
d47671e
[PRMP-1188] implement sending message back to queue
steph-torres-nhs Nov 26, 2024
41cf2f4
[PRMP-1188] add new MNS_SQS_QUEUE env to conftest
steph-torres-nhs Nov 27, 2024
50c8573
Merge branch 'refs/heads/main' into PRMP-1188
NogaNHS Nov 27, 2024
395c46e
[PRMP-1188] fix unit test
NogaNHS Nov 27, 2024
5bcc4fd
[PRMP-1188] remove subscription from enum
steph-torres-nhs Nov 27, 2024
68b676a
[PRMP-1188] remove capital letters from test names
steph-torres-nhs Nov 27, 2024
dc50dce
Merge branch 'main' into PRMP-1188
steph-torres-nhs Nov 29, 2024
5d90083
[PMRP-1188] continue with PR comment ammendents
steph-torres-nhs Nov 29, 2024
681a5d6
[PMRP-1188] add fixtures methods used frequently in test file
steph-torres-nhs Nov 29, 2024
48b54d0
[PRMP-1188] implement updating lastUpdated field
steph-torres-nhs Dec 2, 2024
1f94d46
[PRMP-1188] add assert with to sending message back to queue test
steph-torres-nhs Dec 2, 2024
caaf8bd
[PRMP-1188] add conditional to deploy lambdas
steph-torres-nhs Dec 2, 2024
4ffb470
[PRMP-1188] change workflow conditional
steph-torres-nhs Dec 2, 2024
41b6ca3
[PRMP-1188] change workflow to deploy MNS lambda run when not in deve…
steph-torres-nhs Dec 2, 2024
7c0fe28
Merge branch 'main' into PRMP-1188
NogaNHS Dec 2, 2024
7943515
[PRMP-1188] try different conditional for conditional deployment of l…
steph-torres-nhs Dec 2, 2024
8422e69
Merge branch 'main' into PRMP-1188
steph-torres-nhs Dec 2, 2024
bbfd720
[PRMP-1188] address PR comments
steph-torres-nhs Dec 6, 2024
f21a205
Merge branch 'PRMP-1188' of https://github.com/nhsconnect/national-do…
steph-torres-nhs Dec 6, 2024
d1e93c2
Merge branch 'main' into PRMP-1188
NogaNHS Dec 10, 2024
9a8af85
Merge branch 'main' into PRMP-1188
NogaNHS Dec 10, 2024
c34ad77
Merge branch 'main' into PRMP-1188
oliverbeumkes-nhs Dec 13, 2024
85ea123
[PRMP-1188] remove conditional deploy of lambda, doesn't work as expe…
steph-torres-nhs Dec 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -402,4 +402,21 @@ jobs:
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_mns_notification_lambda:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a condition for when the env is a sandbox?

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 }}
steph-torres-nhs marked this conversation as resolved.
Show resolved Hide resolved
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 }}

7 changes: 3 additions & 4 deletions .github/workflows/subscribe-to-mns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@ 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still want this to be a placeholder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the workflow that we want is currently on a different ticket that hasn't yet been merged in.

runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
defaults:
run:
working-directory: lambdas
steps:
- name: Placeholder
run: |
echo "Running placeholder job on ${inputs.sandbox}"
run: |
echo "Running placeholder job on ${inputs.sandbox}"

6 changes: 6 additions & 0 deletions lambdas/enums/mns_notification_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import StrEnum


class MNSNotificationTypes(StrEnum):
CHANGE_OF_GP = "pds-change-of-gp-1"
oliverbeumkes-nhs marked this conversation as resolved.
Show resolved Hide resolved
DEATH_NOTIFICATION = "pds-death-notification-1"
50 changes: 50 additions & 0 deletions lambdas/handlers/mns_notification_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import json

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__)


@set_request_context_for_logging
@ensure_environment_variables(
names=[
"APPCONFIG_CONFIGURATION",
"APPCONFIG_ENVIRONMENT",
"LLOYD_GEORGE_DYNAMODB_NAME",
"MNS_NOTIFICATION_QUEUE_URL",
]
)
@override_error_check
@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:
try:
sqs_message = json.loads(sqs_message["body"])

mns_message = MNSSQSMessage(**sqs_message)
MNSSQSMessage.model_validate(mns_message)

NogaNHS marked this conversation as resolved.
Show resolved Hide resolved
request_context.patient_nhs_no = mns_message.subject.nhs_number

if mns_message.type in MNSNotificationTypes.__members__.values():
notification_service.handle_mns_notification(mns_message)

except ValidationError as error:
logger.error("Malformed MNS notification message")
logger.error(error)
except Exception as error:
abbas-khan10 marked this conversation as resolved.
Show resolved Hide resolved
logger.error(f"Error processing SQS message: {error}.")
logger.info("Continuing to next message.")
23 changes: 23 additions & 0 deletions lambdas/models/mns_sqs_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pydantic import AliasGenerator, BaseModel, ConfigDict
from pydantic.alias_generators import to_camel


class MNSMessageSubject(BaseModel):
model_config = ConfigDict(
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, serialization_alias=to_camel
),
)
id: str
type: str
subject: MNSMessageSubject
data: dict
146 changes: 146 additions & 0 deletions lambdas/services/process_mns_message_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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
from services.base.dynamo_service import DynamoDBService
from services.base.sqs_service import SQSService
from utils.audit_logging_setup import LoggingService
from utils.exceptions import PdsErrorException
from utils.utilities import get_pds_service

logger = LoggingService(__name__)


class MNSNotificationService:
def __init__(self):
self.dynamo_service = DynamoDBService()
self.table = os.getenv("LLOYD_GEORGE_DYNAMODB_NAME")
self.pds_service = get_pds_service()
self.sqs_service = SQSService()
self.queue = os.getenv("MNS_NOTIFICATION_QUEUE_URL")

def handle_mns_notification(self, message: MNSSQSMessage):
try:
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")
self.send_message_back_to_queue(message)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a log saying we're putting the message back?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this queue working in a different way? I've never had to put anything back on a message queue manually, it's always been done on an acknowledgement system. If the message isn't acknowledged -> retry after a set amount of time. is the system I'm used to.


except ClientError as e:
logger.info(
f"Unable to process message: {message.id}, of type: {message.type}"
)
logger.info(f"{e}")

def handle_gp_change_notification(self, message: MNSSQSMessage):
patient_document_references = self.get_patient_documents(
message.subject.nhs_number
)

if not self.patient_is_present_in_ndr(patient_document_references):
return
Comment on lines +48 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that both the change of GP and the death notifications (and I'd imagine any potential future notifications) involve this step of pulling out the document references and then checking for the presence of this patient in the NDR, I'd suggest we pull these lines out and do this check on line 29 before the if/elif to avoid duplication and clean these steps up.


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={
DocumentReferenceMetadataFields.CURRENT_GP_ODS.value: updated_ods_code,
DocumentReferenceMetadataFields.LAST_UPDATED.value: int(
datetime.now().timestamp()
),
},
)

logger.info("Update complete for change of GP")

def handle_death_notification(self, message: MNSSQSMessage):
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

case DeathNotificationStatus.REMOVED:
patient_documents = self.get_patient_documents(
message.subject.nhs_number
)
if not self.patient_is_present_in_ndr(patient_documents):
return

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.")

case DeathNotificationStatus.FORMAL:
patient_documents = self.get_patient_documents(
message.subject.nhs_number
)
if not self.patient_is_present_in_ndr(patient_documents):
return

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...")
response = self.dynamo_service.query_table_by_index(
table_name=self.table,
index_name="NhsNumberIndex",
search_key="NhsNumber",
search_condition=nhs_number,
)
steph-torres-nhs marked this conversation as resolved.
Show resolved Hide resolved
return response["Items"]

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(
table_name=self.table,
key=document["ID"],
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:
patient_details = self.pds_service.fetch_patient_details(nhs_number)
return patient_details.general_practice_ods

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)
)

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
3 changes: 3 additions & 0 deletions lambdas/tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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"
Expand Down Expand Up @@ -171,9 +172,11 @@ 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)
monkeypatch.setenv("NRL_SQS_QUEUE_URL", NRL_SQS_URL)



EXPECTED_PARSED_PATIENT_BASE_CASE = PatientDetails(
givenName=["Jane"],
familyName="Smith",
Expand Down
Loading
Loading