Skip to content

VED-982: Create POST MNS Notification#1211

Open
Akol125 wants to merge 32 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 32 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
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.

Thanks for resolving. I have resolved my existing comments. Just a couple of additional raised but getting there.

Returns: List of failed item identifiers for partial batch failure
"""
batch_item_failures = []
mns_service = get_mns_service(mns_env=mns_env)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is performant enough because of the hacky cache used by the existing authenticator.

However, I really recommend looking at Matt's performance test PR and how we will manage auth going forward. Maybe we can try get that merged first, or use a similar approach on this branch.

With this current approach, we will end up initialised a new Authenticator and MNS Service each time. Instead we should have a long-lived class which allows us to make use of reused Lambda execution environments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could have this resolved in a seperate ticket

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, potentially or as a sub task. As long as we do not move the 8 pointer into testing until all those changes are ready. We will likely not be able to move it there for a while anyway as we have the dependency on MNS.

@dlzhry2nhs
Copy link
Collaborator

You have 1 Sonar issue. Only minor, will be a quick fix.

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.

Is not too far away now, before we can maybe say dev work is ready pending discussions and onboarding to MNS.

Testers can also pick up setting up automation tests in the meantime.

Think there are some old comments to do with the terraform which still need looking at. Then have raised some further code change requests in this review, along with a request to look at the tests.


patient_age = calculate_age_at_vaccination(imms_data["person_dob"], imms_data["date_and_time"])

gp_ods_code = get_practitioner_details_from_pds(imms_data["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.

Not a comment for action yet, but let's keep this under review. I understand MNS may offer a service where they enrich the payload with further filtering information, so we may not even need to make this call. But let's see.

class TestGetPractitionerDetailsFromPds(unittest.TestCase):
"""Tests for get_practitioner_details_from_pds function."""

@patch("create_notification.pds_get_patient_details")
Copy link
Collaborator

Choose a reason for hiding this comment

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

We are mocking very far away from the actual external services, and mocking a lot of our own helper functions.
If we change this helper function - pds_get_patient_details - and accidentally introduce a breaking change, we won't know about it (unless we have an e2e test), as we will have mocked the output.

Again, I would highly recommend having some tests that mock at the Python requests level. The responses library I shared earlier might be good for this, but fine if you feel more comfortable setting up manual mocks.

Finally, I think it's okay to leave the unit tests as they are here for create_notification, but I would advocate having some higher-level i.e. integration tests in test_lambda_handler otherwise we just have fairly brittle unit tests that do not guard very well against regressions.

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.

Image

Also you have a new failure, please resolve and test. The test would be that everything in your Lambda should succeed up to publishing the message to MNS. We expect this to fail, as they do not accept this event type. We should see all failed messages delivered to the Dead Letter Queue.

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.

These are my last comments, I promise. Then we can resolve and open for the new team to review. I'm happy with the code changes, just a couple of minor additional pointers on the tests.


@patch("create_notification.get_practitioner_details_from_pds")
@patch("create_notification.get_service_url")
def test_create_mns_notification_uuid_generated(self, mock_get_service_url, mock_get_gp):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not entirely sure this test is required.

self.assertEqual(result, [{"S": "item1"}, {"S": "item2"}])


if __name__ == "__main__":
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not required.

Comment on lines +92 to +137
@patch("create_notification.get_practitioner_details_from_pds")
@patch("create_notification.get_service_url")
def test_create_mns_notification_dataref_format_real_payload(self, mock_get_service_url, mock_get_gp):
mock_get_service_url.return_value = self.expected_immunisation_url
mock_get_gp.return_value = self.expected_gp_ods_code

result = create_mns_notification(self.sample_sqs_event)

expected_dataref = f"{self.expected_immunisation_url}/Immunization/d058014c-b0fd-4471-8db9-3316175eb825"
self.assertEqual(result["dataref"], expected_dataref)

@patch("create_notification.get_practitioner_details_from_pds")
@patch("create_notification.get_service_url")
def test_create_mns_notification_filtering_fields_real_payload(self, mock_get_service_url, mock_get_gp):
mock_get_service_url.return_value = self.expected_immunisation_url
mock_get_gp.return_value = self.expected_gp_ods_code

result = create_mns_notification(self.sample_sqs_event)

filtering = result["filtering"]
self.assertEqual(filtering["generalpractitioner"], self.expected_gp_ods_code)
self.assertEqual(filtering["sourceorganisation"], "B0C4P")
self.assertEqual(filtering["sourceapplication"], "TPP")
self.assertEqual(filtering["immunisationtype"], "hib")
self.assertEqual(filtering["action"], "CREATE")
self.assertIsInstance(filtering["subjectage"], str)

@patch("create_notification.get_practitioner_details_from_pds")
@patch("create_notification.get_service_url")
def test_create_mns_notification_age_calculation_real_payload(self, mock_get_service_url, mock_get_gp):
mock_get_service_url.return_value = self.expected_immunisation_url
mock_get_gp.return_value = self.expected_gp_ods_code

result = create_mns_notification(self.sample_sqs_event)

self.assertEqual(result["filtering"]["subjectage"], "21")

@patch("create_notification.get_practitioner_details_from_pds")
@patch("create_notification.get_service_url")
def test_create_mns_notification_calls_get_practitioner_real_payload(self, mock_get_service_url, mock_get_gp):
mock_get_service_url.return_value = self.expected_immunisation_url
mock_get_gp.return_value = self.expected_gp_ods_code

create_mns_notification(self.sample_sqs_event)

mock_get_gp.assert_called_once_with("9481152782")
Copy link
Collaborator

Choose a reason for hiding this comment

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

All of these have the same setup and are testing the same thing. It's just that we seem to be splitting the assertions across different tests. This is an inefficient pattern.

We should do one test and then assert the expected mocks were called and assert against the full payload. We could even construct an expected payload and assertDictEquals. But am equally fine with us splitting the assertions into different sections. But we just need 1 test.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No longer needed?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Worth putting in a test_data or sample_data directory. We appear to use that pattern in other Lambda test directories.

mock_get_mns.assert_called_once()


class TestLambdaHandler(unittest.TestCase):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Think you are already on with this. I'm a bit tied up in the morning, but will keep the afternoon free to pair if help needed.

If we set up some nice integration level tests using moto for secrets, responses for external HTTP calls, asserting against everything including logs, and demonstrating the scenarios mentioned in the ticket ACs, I think we could potentially just have TestLambdaHandler and everything below it will get tested.

The unit tests that exist for test_create_notification provide us good unit test coverage already :)

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 3, 2026

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.

3 participants