Skip to content

Comments

VED-982: Create POST MNS Notification#1211

Open
Akol125 wants to merge 12 commits intostaging/VED-16-mns-vacc-event-notificationsfrom
VED-982-Notification-Lambda-MNS
Open

VED-982: Create POST MNS Notification#1211
Akol125 wants to merge 12 commits intostaging/VED-16-mns-vacc-event-notificationsfrom
VED-982-Notification-Lambda-MNS

Conversation

@Akol125
Copy link
Contributor

@Akol125 Akol125 commented Feb 16, 2026

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

  • Dev
  • Test
  • Tech Author
  • Product Owner

Review Checklist

ℹ️ This section is to be filled in by the reviewer.

  • I have reviewed the changes in this PR and they fill all of the acceptance criteria of the ticket.
  • If there were infrastructure, operational, or build changes, I have made sure there is sufficient evidence that the changes will work.
  • If there were changes that are outside of the regular release processes e.g. account infrastructure to setup, manual setup for external API integrations, secrets to set, then I have checked that the developer has flagged this to the Tech Lead as release steps.
  • I have checked that no Personal Identifiable Data (PID) is logged as part of the changes.

@github-actions
Copy link
Contributor

This branch is working on a ticket in the NHS England VED JIRA Project. Here's a handy link to the ticket:

VED-982

@Akol125 Akol125 changed the title build base schema VED-982: Create MNS Notification Feb 16, 2026
@Akol125 Akol125 marked this pull request as draft February 17, 2026 09:13
@Akol125 Akol125 changed the base branch from staging/VED-16-mns-vacc-event-notifications to master February 18, 2026 11:35
@Akol125 Akol125 changed the base branch from master to staging/VED-16-mns-vacc-event-notifications February 18, 2026 11:35
@Akol125 Akol125 marked this pull request as ready for review February 18, 2026 15:00
@Akol125 Akol125 changed the base branch from staging/VED-16-mns-vacc-event-notifications to master February 18, 2026 21:26
@Akol125 Akol125 changed the base branch from master to staging/VED-921-New-Vacc-Types February 18, 2026 21:27
@Akol125 Akol125 changed the base branch from staging/VED-921-New-Vacc-Types to staging/VED-16-mns-vacc-event-notifications February 18, 2026 21:27
…tifications' into VED-982-Notification-Lambda-MNS
@sonarqubecloud
Copy link

else:
logger.info(f"Successfully processed all {len(event_records)} messages")

return {"batchItemFailures": batch_item_failures}
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

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?

Copy link
Collaborator

@dlzhry2nhs dlzhry2nhs left a comment

Choose a reason for hiding this comment

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

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):
Copy link
Collaborator

Choose a reason for hiding this comment

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

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):
Copy link
Collaborator

Choose a reason for hiding this comment

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

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:
Copy link
Collaborator

Choose a reason for hiding this comment

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

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

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 = (
Copy link
Collaborator

Choose a reason for hiding this comment

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

MNS_BASE_URL?

message_id, immunisation_id = extract_trace_ids(record)
notification_id = None

try:
Copy link
Collaborator

Choose a reason for hiding this comment

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

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],
Copy link
Collaborator

Choose a reason for hiding this comment

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

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")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Log is incorrect.

logger.warning("No patient details found for NHS number")
return None

gp_ods_code = patient_gp.get("value")
Copy link
Collaborator

Choose a reason for hiding this comment

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

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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants