Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
924dcaa
[PRMP-594] Handler & service for getting single document by ID
FoxMaltas-NHS Oct 30, 2025
3ab6081
[PRMP-594] Completed unit tests for get_document_reference_service
FoxMaltas-NHS Oct 30, 2025
ed5aa5c
[PRMP-594] Removed unneeded environment variable check
FoxMaltas-NHS Oct 30, 2025
e9a5363
[PRMP-594] Changed document reference database filter conditions
FoxMaltas-NHS Nov 3, 2025
a621f8c
[PRMP-594] Removed unused lambda error
FoxMaltas-NHS Nov 3, 2025
bacd42f
[PRMP-594] Rename variable so it doesn't shadow a python builtin
FoxMaltas-NHS Nov 3, 2025
f405c22
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 3, 2025
d43fdd7
[PRMP-594] Switched to using feature flag validation function from PR…
FoxMaltas-NHS Nov 4, 2025
5f96920
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 4, 2025
219c438
[PRMP-594] Refactored checking expected parameters were passed
FoxMaltas-NHS Nov 5, 2025
333e1de
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 5, 2025
9ce22be
[PRMP-594] Removed unnecessary check and error raise
FoxMaltas-NHS Nov 5, 2025
d88ec64
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 6, 2025
1b6ef4a
[PRMP-594] Added file content type to api return body
FoxMaltas-NHS Nov 7, 2025
5fd401a
[PRMP-594] Added lambda handler to deploy workflow
FoxMaltas-NHS Nov 7, 2025
3355bb5
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 7, 2025
f5f9d70
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 19, 2025
5435eb4
[PRMP-594] Remove duplicated feature flag
FoxMaltas-NHS Nov 19, 2025
90ba691
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 20, 2025
e411892
[PRMP-594] Added missing required environment variables
FoxMaltas-NHS Nov 24, 2025
0d177bc
[PRMP-594] Refactored service to use s3, dynamo, and document service…
FoxMaltas-NHS Nov 24, 2025
7710866
[PRMP-594] Fix for service unit tests
FoxMaltas-NHS Nov 24, 2025
7bdfb87
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 24, 2025
cac91f0
[PRMP-594] Switched test_get_document_reference_handler to use confte…
FoxMaltas-NHS Nov 24, 2025
977dfb6
[PRMP-594] Remove unused test fixture
FoxMaltas-NHS Nov 24, 2025
a80fdc3
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 24, 2025
bc92f7a
fix function call
adamwhitingnhs Nov 25, 2025
87c3964
[PRMP-594] Fixed unit test
FoxMaltas-NHS Nov 25, 2025
bda5f24
Merge branch 'main' into PRMP-594
FoxMaltas-NHS Nov 25, 2025
c431b06
Merge branch 'main' into PRMP-594
adamwhitingnhs Nov 25, 2025
3576577
Merge branch 'main' into PRMP-594
adamwhitingnhs Nov 26, 2025
630b6a6
Merge branch 'main' into PRMP-594
DuncanSangsterNHS Nov 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -682,3 +682,17 @@ jobs:
lambda_layer_names: "core_lambda_layer"
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_get_document_reference_by_id_lambda:
name: Deploy get_document_reference_lambda
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml
with:
environment: ${{ inputs.environment}}
python_version: ${{ inputs.python_version }}
build_branch: ${{ inputs.build_branch}}
sandbox: ${{ inputs.sandbox }}
lambda_handler_name: get_document_reference_handler
lambda_aws_name: GetDocRefLambda
lambda_layer_names: "core_lambda_layer"
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}
55 changes: 55 additions & 0 deletions lambdas/handlers/get_document_reference_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
from utils.decorators.override_error_check import override_error_check
from services.feature_flags_service import FeatureFlagService
from enums.feature_flags import FeatureFlags
from services.get_document_reference_service import GetDocumentReferenceService
from utils.decorators.validate_patient_id import validate_patient_id
from utils.lambda_exceptions import FeatureFlagsException
from enums.lambda_error import LambdaError
from utils.lambda_exceptions import GetDocumentRefException
from utils.lambda_response import ApiGatewayResponse
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.set_audit_arg import set_request_context_for_logging
from enums.logging_app_interaction import LoggingAppInteraction
from utils.audit_logging_setup import LoggingService
from utils.request_context import request_context
import json

logger = LoggingService(__name__)

@validate_patient_id
@handle_lambda_exceptions
@set_request_context_for_logging
@ensure_environment_variables(
names=[
"LLOYD_GEORGE_DYNAMODB_NAME",
"PRESIGNED_ASSUME_ROLE",
"APPCONFIG_APPLICATION",
"APPCONFIG_ENVIRONMENT",
"APPCONFIG_CONFIGURATION",
"EDGE_REFERENCE_TABLE",
"CLOUDFRONT_URL",
]
)
@override_error_check
def lambda_handler(event: dict[str, any], context):
request_context.app_interaction = LoggingAppInteraction.VIEW_LG_RECORD.value

feature_flag_service = FeatureFlagService()
feature_flag_service.validate_feature_flag(FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED)

logger.info("Starting document fetch by ID process")

try:
document_id = event["pathParameters"]["id"]
nhs_number = event["queryStringParameters"]["patientId"]
except KeyError:
raise GetDocumentRefException(400, LambdaError.DocumentReferenceMissingParameters)

service = GetDocumentReferenceService()

document_info = service.get_document_url_by_id(document_id, nhs_number)

return ApiGatewayResponse(
status_code=200, body=json.dumps(document_info), methods="GET"
).create_api_gateway_response()
2 changes: 1 addition & 1 deletion lambdas/services/feature_flags_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,4 @@ def validate_feature_flag(self, flag_name: str):

if not flag_object.get(flag_name, False):
logger.info(f"Feature flag '{flag_name}' not enabled, event will not be processed")
raise FeatureFlagsException(404, LambdaError.FeatureFlagDisabled)
raise FeatureFlagsException(404, LambdaError.FeatureFlagDisabled)
83 changes: 83 additions & 0 deletions lambdas/services/get_document_reference_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import os
import uuid
from datetime import datetime, timezone
from services.get_fhir_document_reference_service import (
GetFhirDocumentReferenceService,
)
from utils.audit_logging_setup import LoggingService
from utils.lambda_exceptions import GetDocumentRefException
from enums.lambda_error import LambdaError
from utils.utilities import format_cloudfront_url
from models.document_reference import DocumentReference
from utils.dynamo_query_filter_builder import DynamoQueryFilterBuilder
from enums.dynamo_filter import AttributeOperator
from utils.common_query_filters import NotDeleted

logger = LoggingService(__name__)

class GetDocumentReferenceService:
def __init__(self):
self.fhir_doc_service = GetFhirDocumentReferenceService()
self.document_service = self.fhir_doc_service.document_service
self.dynamo_service = self.document_service.dynamo_service
self.s3_service = self.fhir_doc_service.s3_service
self.lg_table = os.environ.get("LLOYD_GEORGE_DYNAMODB_NAME")
self.cloudfront_table_name = os.environ.get("EDGE_REFERENCE_TABLE")
self.cloudfront_url = os.environ.get("CLOUDFRONT_URL")

def get_document_url_by_id(self, document_id: str, nhs_number: str):
document_reference = self.get_document_reference(document_id, nhs_number)

presigned_s3_url = self.create_document_presigned_url(
document_reference.s3_bucket_name,
document_reference.s3_file_key
)

return {
"url": presigned_s3_url,
"contentType": document_reference.content_type
}

def create_document_presigned_url(self, bucket_name, file_location):
presigned_url_response = self.s3_service.create_download_presigned_url(
s3_bucket_name=bucket_name,
file_key=file_location,
)

presigned_id = str(uuid.uuid4())
deletion_date = datetime.now(timezone.utc)

ttl_half_an_hour_in_seconds = self.s3_service.presigned_url_expiry
dynamo_item_ttl = int(deletion_date.timestamp() + ttl_half_an_hour_in_seconds)
self.dynamo_service.create_item(
self.cloudfront_table_name,
{
"ID": presigned_id,
"presignedUrl": presigned_url_response,
"TTL": dynamo_item_ttl,
},
)
return format_cloudfront_url(presigned_id, self.cloudfront_url)

def get_document_reference(self, document_id: str, nhs_number: str) -> DocumentReference:
filter_builder = DynamoQueryFilterBuilder()
filter_builder.add_condition("DocStatus", AttributeOperator.EQUAL, "final")
filter_builder.add_condition("NhsNumber", AttributeOperator.EQUAL, nhs_number)

table_filter = filter_builder.build()

table_filter = table_filter & NotDeleted

documents = self.document_service.fetch_documents_from_table(
table_name=self.lg_table,
search_condition=document_id,
search_key="ID",
query_filter=table_filter,
)
if len(documents) > 0:
logger.info("Document found for given id")
return documents[0]
else:
raise GetDocumentRefException(
404, LambdaError.DocumentReferenceNotFound
)
3 changes: 3 additions & 0 deletions lambdas/tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@
MOCK_DOCUMENT_REVIEW_BUCKET = "test_document_review_bucket"
MOCK_EDGE_TABLE = "test_edge_reference_table"

MOCK_EDGE_REFERENCE_TABLE = "test_edge_reference_table"

@pytest.fixture
def set_env(monkeypatch):
monkeypatch.setenv("AWS_DEFAULT_REGION", REGION_NAME)
Expand Down Expand Up @@ -232,6 +234,7 @@ def set_env(monkeypatch):
monkeypatch.setenv("EDGE_REFERENCE_TABLE", MOCK_EDGE_TABLE)
monkeypatch.setenv("STAGING_STORE_BUCKET_NAME", MOCK_STAGING_STORE_BUCKET)
monkeypatch.setenv("METADATA_SQS_QUEUE_URL", MOCK_LG_METADATA_SQS_QUEUE)
monkeypatch.setenv("EDGE_REFERENCE_TABLE", MOCK_EDGE_REFERENCE_TABLE)

EXPECTED_PARSED_PATIENT_BASE_CASE = PatientDetails(
givenName=["Jane"],
Expand Down
154 changes: 154 additions & 0 deletions lambdas/tests/unit/handlers/test_get_document_reference_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import pytest
import json
import os
from unittest.mock import patch
from handlers.get_document_reference_handler import lambda_handler
from utils.lambda_exceptions import GetDocumentRefException
from enums.feature_flags import FeatureFlags
from utils.lambda_response import ApiGatewayResponse
from utils.lambda_exceptions import FeatureFlagsException
from enums.lambda_error import LambdaError
from utils.error_response import ErrorResponse


@pytest.fixture
def mock_feature_flag_service(mocker):
yield mocker.patch("handlers.get_document_reference_handler.FeatureFlagService").return_value

@pytest.fixture
def mock_get_document_service(mocker):
yield mocker.patch(
"handlers.get_document_reference_handler.GetDocumentReferenceService"
).return_value

@pytest.fixture
def mock_valid_nhs_number():
yield "4407064188"

@pytest.fixture
def feature_flag():
yield FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED

@pytest.fixture
def mock_interaction_id():
yield "88888888-4444-4444-4444-121212121212"

@pytest.fixture
def mocked_bad_env_vars():
env_vars = {
#"LLOYD_GEORGE_DYNAMODB_NAME": "mock_dynamodb_name",
"PRESIGNED_ASSUME_ROLE": "mock_presigned_role",
"APPCONFIG_APPLICATION": "mock_value",
"APPCONFIG_ENVIRONMENT": "mock_value",
"APPCONFIG_CONFIGURATION": "mock_value",
"EDGE_REFERENCE_TABLE": "mock_value",
"CLOUDFRONT_URL": "mock_value",
}

with patch.dict(os.environ, env_vars):
yield "LLOYD_GEORGE_DYNAMODB_NAME"



def test_handler_valid_request_returns_200(
valid_id_event_with_auth_header,
mock_feature_flag_service,
mock_get_document_service,
mock_valid_nhs_number,
context,
set_env,
feature_flag
):
mock_document_id = "1"
valid_id_event_with_auth_header["pathParameters"] = {"id": mock_document_id}
valid_id_event_with_auth_header["queryStringParameters"]["patientId"] = mock_valid_nhs_number
mock_presigned_s3_url = "https://mock.url/"
mock_content_type = "application/pdf"

expected_body = {
"url": mock_presigned_s3_url,
"contentType": mock_content_type
}

expected_result = ApiGatewayResponse(
status_code=200, body=json.dumps(expected_body), methods="GET"
).create_api_gateway_response()

mock_get_document_service.get_document_url_by_id.return_value = expected_body

result = lambda_handler(valid_id_event_with_auth_header, context)

assert result == expected_result
assert result["body"] == json.dumps(expected_body)

mock_feature_flag_service.validate_feature_flag.assert_called_once_with(
feature_flag
)
mock_get_document_service.get_document_url_by_id.assert_called_once_with(
mock_document_id,
mock_valid_nhs_number)

def test_missing_nhs_number_errors(
valid_id_event_with_auth_header,
mock_feature_flag_service,
context,
set_env,
feature_flag,
):
valid_id_event_with_auth_header["pathParameters"] = {"id": "1"}
valid_id_event_with_auth_header["queryStringParameters"].pop("patientId")

expected_result = ApiGatewayResponse(
status_code=400,
body=LambdaError.PatientIdNoKey.create_error_body(),
methods="GET",
).create_api_gateway_response()

result = lambda_handler(valid_id_event_with_auth_header, context)

assert result == expected_result

def test_missing_document_id_errors(
valid_id_event_with_auth_header,
mock_feature_flag_service,
mock_valid_nhs_number,
context,
set_env,
feature_flag,
mock_interaction_id
):
valid_id_event_with_auth_header["pathParameters"] = {}
valid_id_event_with_auth_header["queryStringParameters"]["patientId"] = mock_valid_nhs_number

expected_error = GetDocumentRefException(400, LambdaError.DocumentReferenceMissingParameters)

expected_result = ApiGatewayResponse(
status_code=400,
body=ErrorResponse(
err_code=expected_error.err_code,
message=expected_error.message,
interaction_id=mock_interaction_id
).create(),
methods="GET"
).create_api_gateway_response()

mock_feature_flag_service.get_feature_flags_by_flag.return_value = {feature_flag: True}

result = lambda_handler(valid_id_event_with_auth_header, context)

assert result == expected_result

def test_env_vars_not_set_errors(
valid_id_event_with_auth_header,
context,
mocked_bad_env_vars
):
expected_result = ApiGatewayResponse(
status_code=500,
body=LambdaError.EnvMissing.create_error_body({"name": mocked_bad_env_vars}),
methods="GET"
).create_api_gateway_response()

result = lambda_handler(valid_id_event_with_auth_header, context)

assert result == expected_result
Loading
Loading