VED-982: Create POST MNS Notification#1211
VED-982: Create POST MNS Notification#1211Akol125 wants to merge 32 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
dlzhry2nhs
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
We could have this resolved in a seperate ticket
There was a problem hiding this comment.
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.
|
You have 1 Sonar issue. Only minor, will be a quick fix. |
dlzhry2nhs
left a comment
There was a problem hiding this comment.
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"]) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
dlzhry2nhs
left a comment
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
Not entirely sure this test is required.
| self.assertEqual(result, [{"S": "item1"}, {"S": "item2"}]) | ||
|
|
||
|
|
||
| if __name__ == "__main__": |
| @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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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 :)
|




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.