From c27f54c9e0a7409f1c3ec719f46ae483f85ca1cf Mon Sep 17 00:00:00 2001 From: adamwhitingnhs Date: Mon, 26 Jan 2026 13:53:05 +0000 Subject: [PATCH 1/5] [PRMP-1347] Add doc type filter to doc ref search --- .../requests/getDocumentSearchResults.ts | 4 +- .../document_reference_search_handler.py | 14 +++++-- .../document_reference_search_service.py | 13 +++++- .../test_document_reference_search_handler.py | 41 ++++++++++++++++++- .../test_document_reference_search_service.py | 10 +++++ 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/app/src/helpers/requests/getDocumentSearchResults.ts b/app/src/helpers/requests/getDocumentSearchResults.ts index bbcd8ed000..6afb16317f 100644 --- a/app/src/helpers/requests/getDocumentSearchResults.ts +++ b/app/src/helpers/requests/getDocumentSearchResults.ts @@ -21,7 +21,7 @@ const getDocumentSearchResults = async ({ nhsNumber, baseUrl, baseHeaders, - docType = DOCUMENT_TYPE.ALL, + docType, }: DocumentSearchResultsArgs): Promise> => { const gatewayUrl = baseUrl + endpoints.DOCUMENT_SEARCH; @@ -32,7 +32,7 @@ const getDocumentSearchResults = async ({ }, params: { patientId: nhsNumber?.replaceAll(/\s/g, ''), // replace whitespace - docType: docType, + docType: docType == DOCUMENT_TYPE.ALL ? undefined : docType, }, }); return response?.data; diff --git a/lambdas/handlers/document_reference_search_handler.py b/lambdas/handlers/document_reference_search_handler.py index 7e1aafdef0..e33aa7451f 100755 --- a/lambdas/handlers/document_reference_search_handler.py +++ b/lambdas/handlers/document_reference_search_handler.py @@ -13,6 +13,7 @@ extract_nhs_number_from_event, validate_patient_id, ) +from utils.document_type_utils import extract_document_type_to_enum from utils.lambda_response import ApiGatewayResponse from utils.request_context import request_context @@ -29,6 +30,8 @@ def lambda_handler(event, context): logger.info("Starting document reference search process") nhs_number = extract_nhs_number_from_event(event) + doc_type = event.get("queryStringParameters", {}).get("docType", None) + document_snomed_code = extract_document_type_to_enum(doc_type) if doc_type else None request_context.patient_nhs_no = nhs_number document_reference_search_service = DocumentReferenceSearchService() @@ -38,11 +41,14 @@ def lambda_handler(event, context): doc_upload_iteration2_enabled = upload_lambda_enabled_flag_object[ FeatureFlags.UPLOAD_DOCUMENT_ITERATION_2_ENABLED ] - doc_status_filter = ( - {"doc_status": "final"} if doc_upload_iteration2_enabled else None - ) + additional_filters = { + "doc_status": "final" if doc_upload_iteration2_enabled else None, + "document_snomed_code": document_snomed_code[0].value if document_snomed_code else None + } response = document_reference_search_service.get_document_references( - nhs_number, check_upload_completed=True, additional_filters=doc_status_filter + nhs_number, + check_upload_completed=True, + additional_filters=additional_filters ) logger.info("User is able to view docs", {"Result": "Successful viewing docs"}) diff --git a/lambdas/services/document_reference_search_service.py b/lambdas/services/document_reference_search_service.py index 0784c7dabf..d4760be1f2 100644 --- a/lambdas/services/document_reference_search_service.py +++ b/lambdas/services/document_reference_search_service.py @@ -124,7 +124,7 @@ def _search_tables_for_documents( return document_resources or None def _get_filter_expression( - self, filters: dict[str, str] = None, upload_completed=False + self, filters: dict[str, str | None] = None, upload_completed=False ): if filters: return self._build_filter_expression(filters) @@ -192,6 +192,9 @@ def _build_document_model(self, document: DocumentReference) -> dict: def _build_filter_expression(self, filter_values: dict[str, str]): filter_builder = DynamoQueryFilterBuilder() for filter_key, filter_value in filter_values.items(): + if filter_value is None: + continue + if filter_key == "custodian": filter_builder.add_condition( attribute=str(DocumentReferenceMetadataFields.CURRENT_GP_ODS.value), @@ -207,6 +210,14 @@ def _build_filter_expression(self, filter_values: dict[str, str]): attr_operator=AttributeOperator.EQUAL, filter_value=filter_value, ) + elif filter_key == "document_snomed_code": + filter_builder.add_condition( + attribute=str( + DocumentReferenceMetadataFields.DOCUMENT_SNOMED_CODE_TYPE.value + ), + attr_operator=AttributeOperator.EQUAL, + filter_value=filter_value, + ) if filter_values: filter_expression = filter_builder.build() & NotDeleted else: diff --git a/lambdas/tests/unit/handlers/test_document_reference_search_handler.py b/lambdas/tests/unit/handlers/test_document_reference_search_handler.py index c9411818f5..ce3f5ca3b8 100755 --- a/lambdas/tests/unit/handlers/test_document_reference_search_handler.py +++ b/lambdas/tests/unit/handlers/test_document_reference_search_handler.py @@ -156,7 +156,7 @@ def test_lambda_handler_with_feature_flag_enabled_applies_doc_status_filter( mocked_service.get_document_references.assert_called_once_with( "9000000009", check_upload_completed=True, - additional_filters={"doc_status": "final"}, + additional_filters={"doc_status": "final", "document_snomed_code": None}, ) @@ -191,5 +191,42 @@ def test_lambda_handler_with_feature_flag_disabled_no_doc_status_filter( mocked_service.get_document_references.assert_called_once_with( "9000000009", check_upload_completed=True, - additional_filters=None, + additional_filters={"doc_status": None, "document_snomed_code": None}, ) + +def test_lambda_handler_with_doc_type_applies_doc_type_filter( + set_env, mocker, valid_id_event_without_auth_header, context +): + """Test that when feature flag is ON, doc_status filter is applied""" + mocked_service_class = mocker.patch( + "handlers.document_reference_search_handler.DocumentReferenceSearchService" + ) + mocked_service = mocked_service_class.return_value + mocked_service.get_document_references.return_value = EXPECTED_RESPONSE + + mocked_feature_flag_service = mocker.patch( + "handlers.document_reference_search_handler.FeatureFlagService" + ) + mocked_feature_flag_instance = mocked_feature_flag_service.return_value + mocked_feature_flag_instance.get_feature_flags_by_flag.return_value = { + FeatureFlags.UPLOAD_DOCUMENT_ITERATION_2_ENABLED: False + } + + expected = ApiGatewayResponse( + 200, json.dumps(EXPECTED_RESPONSE), "GET" + ).create_api_gateway_response() + + doc_type = "16521000000101" + valid_id_event_without_auth_header["queryStringParameters"]["docType"] = doc_type + + actual = lambda_handler(valid_id_event_without_auth_header, context) + + assert expected == actual + mocked_feature_flag_instance.get_feature_flags_by_flag.assert_called_once_with( + FeatureFlags.UPLOAD_DOCUMENT_ITERATION_2_ENABLED + ) + mocked_service.get_document_references.assert_called_once_with( + "9000000009", + check_upload_completed=True, + additional_filters={"doc_status": None, "document_snomed_code": doc_type}, + ) \ No newline at end of file diff --git a/lambdas/tests/unit/services/test_document_reference_search_service.py b/lambdas/tests/unit/services/test_document_reference_search_service.py index 93fecdcd74..65396a4490 100644 --- a/lambdas/tests/unit/services/test_document_reference_search_service.py +++ b/lambdas/tests/unit/services/test_document_reference_search_service.py @@ -503,3 +503,13 @@ def test_build_filter_expression_defaults(mock_document_service): actual_filter = mock_document_service._build_filter_expression(filter_values) assert actual_filter == expected_filter + +def test_build_filter_expression_document_snomed_code(mock_document_service): + filter_values = {"document_snomed_code": "16521000000101"} + expected_filter = Attr("DocumentSnomedCodeType").eq("16521000000101") & ( + Attr("Deleted").eq("") | Attr("Deleted").not_exists() + ) + + actual_filter = mock_document_service._build_filter_expression(filter_values) + + assert expected_filter == actual_filter From c45baf49ecafcf3384f4aebd2d2e8cfc2f81a029 Mon Sep 17 00:00:00 2001 From: Kamen Bachvarov Date: Mon, 2 Feb 2026 12:07:16 +0000 Subject: [PATCH 2/5] snomed code type test adjustment --- lambdas/tests/unit/enums/test_metadata_field_names.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lambdas/tests/unit/enums/test_metadata_field_names.py b/lambdas/tests/unit/enums/test_metadata_field_names.py index b4c1708130..72e616f2c6 100755 --- a/lambdas/tests/unit/enums/test_metadata_field_names.py +++ b/lambdas/tests/unit/enums/test_metadata_field_names.py @@ -21,8 +21,9 @@ def test_returns_all_as_list(): assert DocumentReferenceMetadataFields.UPLOADED.value in subject assert DocumentReferenceMetadataFields.UPLOADING.value in subject assert DocumentReferenceMetadataFields.LAST_UPDATED.value in subject + assert DocumentReferenceMetadataFields.FILE_SIZE.value in subject assert DocumentReferenceMetadataFields.DOC_STATUS.value in subject - assert DocumentReferenceMetadataFields.DOCUMENT_SCAN_CREATION.value in subject assert DocumentReferenceMetadataFields.CUSTODIAN.value in subject - assert DocumentReferenceMetadataFields.FILE_SIZE.value in subject - assert DocumentReferenceMetadataFields.DOCUMENT_SNOMED_CODE_TYPE.value in subject \ No newline at end of file + assert DocumentReferenceMetadataFields.DOCUMENT_SCAN_CREATION.value in subject + assert DocumentReferenceMetadataFields.DOCUMENT_SNOMED_CODE_TYPE.value in subject + From 541fe3e946336be3f8cae1e8f1c3993ca752cac0 Mon Sep 17 00:00:00 2001 From: adamwhitingnhs Date: Mon, 2 Feb 2026 13:18:10 +0000 Subject: [PATCH 3/5] pr comments --- .../document_reference_search_handler.py | 20 +++++++++--- .../document_reference_search_service.py | 3 -- .../test_document_reference_search_handler.py | 32 ++++++++++++++++++- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/lambdas/handlers/document_reference_search_handler.py b/lambdas/handlers/document_reference_search_handler.py index e33aa7451f..74c5d37bff 100755 --- a/lambdas/handlers/document_reference_search_handler.py +++ b/lambdas/handlers/document_reference_search_handler.py @@ -1,6 +1,7 @@ import json from enums.feature_flags import FeatureFlags +from enums.lambda_error import LambdaError from enums.logging_app_interaction import LoggingAppInteraction from services.document_reference_search_service import DocumentReferenceSearchService from services.feature_flags_service import FeatureFlagService @@ -14,6 +15,7 @@ validate_patient_id, ) from utils.document_type_utils import extract_document_type_to_enum +from utils.lambda_exceptions import DocumentRefSearchException from utils.lambda_response import ApiGatewayResponse from utils.request_context import request_context @@ -30,8 +32,13 @@ def lambda_handler(event, context): logger.info("Starting document reference search process") nhs_number = extract_nhs_number_from_event(event) + doc_type = event.get("queryStringParameters", {}).get("docType", None) - document_snomed_code = extract_document_type_to_enum(doc_type) if doc_type else None + try: + document_snomed_code = extract_document_type_to_enum(doc_type) if doc_type else None + except ValueError: + raise DocumentRefSearchException(400, LambdaError.DocTypeInvalid) + request_context.patient_nhs_no = nhs_number document_reference_search_service = DocumentReferenceSearchService() @@ -41,10 +48,13 @@ def lambda_handler(event, context): doc_upload_iteration2_enabled = upload_lambda_enabled_flag_object[ FeatureFlags.UPLOAD_DOCUMENT_ITERATION_2_ENABLED ] - additional_filters = { - "doc_status": "final" if doc_upload_iteration2_enabled else None, - "document_snomed_code": document_snomed_code[0].value if document_snomed_code else None - } + + additional_filters = {} + if doc_upload_iteration2_enabled: + additional_filters["doc_status"] = "final" + if document_snomed_code: + additional_filters["document_snomed_code"] = document_snomed_code[0].value + response = document_reference_search_service.get_document_references( nhs_number, check_upload_completed=True, diff --git a/lambdas/services/document_reference_search_service.py b/lambdas/services/document_reference_search_service.py index d4760be1f2..a4046bb45e 100644 --- a/lambdas/services/document_reference_search_service.py +++ b/lambdas/services/document_reference_search_service.py @@ -192,9 +192,6 @@ def _build_document_model(self, document: DocumentReference) -> dict: def _build_filter_expression(self, filter_values: dict[str, str]): filter_builder = DynamoQueryFilterBuilder() for filter_key, filter_value in filter_values.items(): - if filter_value is None: - continue - if filter_key == "custodian": filter_builder.add_condition( attribute=str(DocumentReferenceMetadataFields.CURRENT_GP_ODS.value), diff --git a/lambdas/tests/unit/handlers/test_document_reference_search_handler.py b/lambdas/tests/unit/handlers/test_document_reference_search_handler.py index ce3f5ca3b8..9f3d3395a5 100755 --- a/lambdas/tests/unit/handlers/test_document_reference_search_handler.py +++ b/lambdas/tests/unit/handlers/test_document_reference_search_handler.py @@ -229,4 +229,34 @@ def test_lambda_handler_with_doc_type_applies_doc_type_filter( "9000000009", check_upload_completed=True, additional_filters={"doc_status": None, "document_snomed_code": doc_type}, - ) \ No newline at end of file + ) + + +def test_lambda_handler_with_invalid_doc_type_returns_400( + set_env, mocker, valid_id_event_without_auth_header, context +): + """Test that when an invalid docType is provided, a 400 error is returned""" + mocker.patch( + "handlers.document_reference_search_handler.DocumentReferenceSearchService" + ) + mocker.patch( + "handlers.document_reference_search_handler.FeatureFlagService" + ) + + invalid_doc_type = "invalid_doc_type" + valid_id_event_without_auth_header["queryStringParameters"]["docType"] = invalid_doc_type + + expected_body = json.dumps( + { + "message": "Invalid document type requested", + "err_code": "VDT_4002", + "interaction_id": "88888888-4444-4444-4444-121212121212", + } + ) + expected = ApiGatewayResponse( + 400, expected_body, "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_event_without_auth_header, context) + + assert expected == actual \ No newline at end of file From 99a800546f5741d16872843018c6c1958fd1a161 Mon Sep 17 00:00:00 2001 From: adamwhitingnhs Date: Mon, 2 Feb 2026 13:20:06 +0000 Subject: [PATCH 4/5] remove test comments --- .../unit/handlers/test_document_reference_search_handler.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lambdas/tests/unit/handlers/test_document_reference_search_handler.py b/lambdas/tests/unit/handlers/test_document_reference_search_handler.py index 9f3d3395a5..ce072e4010 100755 --- a/lambdas/tests/unit/handlers/test_document_reference_search_handler.py +++ b/lambdas/tests/unit/handlers/test_document_reference_search_handler.py @@ -128,7 +128,6 @@ def test_lambda_handler_when_dynamo_tables_env_variable_not_supplied_then_return def test_lambda_handler_with_feature_flag_enabled_applies_doc_status_filter( set_env, mocker, valid_id_event_without_auth_header, context ): - """Test that when feature flag is ON, doc_status filter is applied""" mocked_service_class = mocker.patch( "handlers.document_reference_search_handler.DocumentReferenceSearchService" ) @@ -163,7 +162,6 @@ def test_lambda_handler_with_feature_flag_enabled_applies_doc_status_filter( def test_lambda_handler_with_feature_flag_disabled_no_doc_status_filter( set_env, mocker, valid_id_event_without_auth_header, context ): - """Test that when feature flag is OFF, no doc_status filter is applied""" mocked_service_class = mocker.patch( "handlers.document_reference_search_handler.DocumentReferenceSearchService" ) @@ -197,7 +195,6 @@ def test_lambda_handler_with_feature_flag_disabled_no_doc_status_filter( def test_lambda_handler_with_doc_type_applies_doc_type_filter( set_env, mocker, valid_id_event_without_auth_header, context ): - """Test that when feature flag is ON, doc_status filter is applied""" mocked_service_class = mocker.patch( "handlers.document_reference_search_handler.DocumentReferenceSearchService" ) @@ -235,7 +232,6 @@ def test_lambda_handler_with_doc_type_applies_doc_type_filter( def test_lambda_handler_with_invalid_doc_type_returns_400( set_env, mocker, valid_id_event_without_auth_header, context ): - """Test that when an invalid docType is provided, a 400 error is returned""" mocker.patch( "handlers.document_reference_search_handler.DocumentReferenceSearchService" ) From 6be21d70377465f8e4ae5532601a7600fab8453c Mon Sep 17 00:00:00 2001 From: adamwhitingnhs Date: Mon, 2 Feb 2026 14:23:28 +0000 Subject: [PATCH 5/5] fix unit tests --- .../unit/handlers/test_document_reference_search_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lambdas/tests/unit/handlers/test_document_reference_search_handler.py b/lambdas/tests/unit/handlers/test_document_reference_search_handler.py index ce072e4010..4b6523dad2 100755 --- a/lambdas/tests/unit/handlers/test_document_reference_search_handler.py +++ b/lambdas/tests/unit/handlers/test_document_reference_search_handler.py @@ -155,7 +155,7 @@ def test_lambda_handler_with_feature_flag_enabled_applies_doc_status_filter( mocked_service.get_document_references.assert_called_once_with( "9000000009", check_upload_completed=True, - additional_filters={"doc_status": "final", "document_snomed_code": None}, + additional_filters={"doc_status": "final"}, ) @@ -189,7 +189,7 @@ def test_lambda_handler_with_feature_flag_disabled_no_doc_status_filter( mocked_service.get_document_references.assert_called_once_with( "9000000009", check_upload_completed=True, - additional_filters={"doc_status": None, "document_snomed_code": None}, + additional_filters={}, ) def test_lambda_handler_with_doc_type_applies_doc_type_filter( @@ -225,7 +225,7 @@ def test_lambda_handler_with_doc_type_applies_doc_type_filter( mocked_service.get_document_references.assert_called_once_with( "9000000009", check_upload_completed=True, - additional_filters={"doc_status": None, "document_snomed_code": doc_type}, + additional_filters={"document_snomed_code": doc_type}, )