VED-982: Create POST MNS Notification#1211
VED-982: Create POST MNS Notification#1211Akol125 wants to merge 12 commits intostaging/VED-16-mns-vacc-event-notificationsfrom
Conversation
|
This branch is working on a ticket in the NHS England VED JIRA Project. Here's a handy link to the ticket: VED-982 |
…tifications' into VED-982-Notification-Lambda-MNS
|
| else: | ||
| logger.info(f"Successfully processed all {len(event_records)} messages") | ||
|
|
||
| return {"batchItemFailures": batch_item_failures} |
There was a problem hiding this comment.
If we are using batchItemFailures (I am in favour of us doing this as it is a good pattern), then we need to switch this on in the terraform. You can do this in the event source mapping for the Lambda function.
You will need to do this in order for this to work. It might be good to take a look at some test results. E.g. if the secrets are not configured for now, then we would expect item failures to be raised and results to be posted to the DLQ. This would be a good test.
| description = <<EOT | ||
| The effective deployment scope used for resource naming and isolation. | ||
| This resolves to either the base environment (e.g., dev, test, prod) or a | ||
| sub-environment (e.g., dev-blue) when sub-environment scoping is enabled. |
There was a problem hiding this comment.
There is no such thing as dev-blue. It may be more informative to mention int-blue - perhaps there is already a description for this variable at the higher level?
dlzhry2nhs
left a comment
There was a problem hiding this comment.
Some suggestions. Happy to pick up on Monday if anything does not make sense.
Some pretty major findings that mean it fundamentally won't work. And I think there is a lot of opportunity to make this new Lambda a bit cleaner. But a good start nonetheless.
| @@ -1,6 +0,0 @@ | |||
| class IdSyncException(Exception): | |||
There was a problem hiding this comment.
At some point we should tidy up IdSync as there are quite a lot of bad patterns in here. However, I would prefer if we keep the IdSyncException in here - it is specifically for some strange handling that has been built in here that I would like us not to bleed into our new feature.
Or at the very least, we return it. If you fancy we could also improve the IdSync if you want to as part of this work.
| } | ||
| ] | ||
| } | ||
| # def test_get_nhs_number_from_pds_resource(self): |
There was a problem hiding this comment.
Please remove or uncomment, depending on what you were planning to do with this.
|
|
||
|
|
||
| # Get Patient details from external service PDS using NHS number from MNS notification | ||
| def pds_get_patient_details(nhs_number: str) -> dict: |
There was a problem hiding this comment.
Please leave this in ID Sync. This is a weird wrapper used over there.
For our new service it would suffice to import the pds_service and use the relevant method for it.
| except Exception as e: | ||
| msg = "Error retrieving patient details from PDS" | ||
| logger.exception(msg) | ||
| raise PdsSyncException(message=msg) from e |
There was a problem hiding this comment.
I am not keen on the new Lambda using this custom exception. It has nothing to do with Sync. The PDS sync process is the Id Sync SQS + Lambda process.
The new Lambda is just running a PDS get to retrieve data and enrich the outgoing event.
| @@ -13,9 +13,9 @@ | |||
|
|
|||
| apigee_env = os.getenv("APIGEE_ENVIRONMENT", "int") | |||
| MNS_URL = ( | |||
| message_id, immunisation_id = extract_trace_ids(record) | ||
| notification_id = None | ||
|
|
||
| try: |
There was a problem hiding this comment.
As a minimum, I expect us to only have one line within the try catch. A function called something like process_record would suffice.
It would be even better if we could use a class-based approach and try to abstract away all the complexity of SQS -> Lambda batch failure responses. Happy to discuss online, but if we go down such a route, it could be really nice to have a base class that maybe handles obtaining the generic fields from the event, exposes a process_records method, and also handles the batchItemFailures response structure.
It could have an unimplemented process_record method that inheriting classes implement. In our case, that would be the MNS Publishing business logic. This will make things much nicer.
You could then bind things to the class that we need such as relevant clients (MNS and PDS). I would argue there should be a standard MnsPublisherService which the Lambda Handler class could use that would work purely in terms of business objects i.e. not care about the external SQS event structure. But that might be slightly too much work. As a minimum, we need the first approach, but really worth considering restructuring with a bit more thought.
| "id": str(uuid.uuid4()), | ||
| "source": immunisation_url, | ||
| "type": IMMUNISATION_TYPE, | ||
| "time": imms_data[SQSEventFields.DATE_AND_TIME_KEY], |
There was a problem hiding this comment.
Rather than having constants for the dict keys, could it be worth having TypedDicts or something more stringent?
If not for the imms data, certainly for the outbound MNS payload.
| patient_details = pds_get_patient_details(nhs_number) | ||
| patient_gp = patient_details.get("generalPractitioner") | ||
| if not patient_gp: | ||
| logger.warning("No patient details found for NHS number") |
| logger.warning("No patient details found for NHS number") | ||
| return None | ||
|
|
||
| gp_ods_code = patient_gp.get("value") |
There was a problem hiding this comment.
The returned value will be an array, if it exists. This code will blow up.
Please do not guess. Ask if you are unsure, or read the API documents: https://digital.nhs.uk/developer/api-catalogue/personal-demographics-service-fhir#get-/Patient/-id-
Having looked at the docs, I think we also need to check the end key to ensure we only retrieve the GP ODS code if the registration is still active. We do not want old practices to get notifications if a registration has been ended.
| notification_id = mns_notification_payload.get("id", None) # generated UUID for MNS | ||
| logger.info("Processing message", trace_id=notification_id) | ||
|
|
||
| mns_pub_response = MnsService.publish_notification(mns_notification_payload) |
There was a problem hiding this comment.
This will not work. You have to have an instance of a class. Because of the way authentication has been built in currently, you have to instantiate each time with an authenticator.
I would advocate looking at Matt's changes for the performance test. Either that will merge first and you can start using it, or you could replicate it here. But either way, you need to fix the fundamental issue raised above.



Summary
Routine Change
Summary
Implemented MNS integration to publish real-time CloudEvents notifications for immunisation record changes (create/update/delete).
Key Components
Lambda Handler
Processes SQS events from DynamoDB streams
Publishes to MNS with partial batch failure handling
Structured logging with trace IDs (message, immunisation, notification)
Notification Creation
Builds CloudEvents-compliant payloads from DynamoDB stream data
Fetches GP ODS codes from PDS
Calculates patient age at vaccination
Populates filtering attributes (organisation, application, vaccine type, action)
Utilities
Recursive extraction from nested DynamoDB events with type descriptor unwrapping
MNS service class for API interactions (subscribe, publish, delete)
Enum-based field mappings for type safety
Testing
Unit test coverage using real SQS payloads
Mocked external dependencies (PDS, MNS)
Comprehensive edge case and error handling tests
Add any other relevant notes or explanations here. Remove this line if you have nothing to add.
Reviews Required
Review Checklist
ℹ️ This section is to be filled in by the reviewer.