Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
89fccdc
[PRMP-643] Add document review models and status enumeration
NogaNHS Oct 22, 2025
397eb0a
[PRMP-643] Refactor document review models and update document handli…
NogaNHS Oct 22, 2025
1b6ff81
[PRMP-643] update MNS service tests for document reviews
NogaNHS Oct 23, 2025
43975a1
Merge branch 'main' into PRMP-643
NogaNHS Oct 23, 2025
81727f4
set default model class to DocumentReference in fetch_documents method
NogaNHS Oct 24, 2025
e403c16
Merge branch 'main' into PRMP-643
NogaNHS Oct 28, 2025
65f132e
Update document review model and status enumeration
NogaNHS Oct 28, 2025
d713065
Refactor update_patient_ods_code to remove unnecessary table parameter
NogaNHS Oct 28, 2025
88b2f1e
Merge branch 'main' into PRMP-643
NogaNHS Oct 28, 2025
03309bb
Refactor type hints to use union operator for document return types
NogaNHS Oct 28, 2025
d0a9d61
Merge branch 'main' into PRMP-643
NogaNHS Oct 28, 2025
88a3a6c
[PRMP-643] PR comments
NogaNHS Oct 29, 2025
7af8e60
[PRMP-643] Implement DocumentReferenceService and DocumentUploadRevie…
NogaNHS Oct 29, 2025
6cc2a43
[PRMP-643] Fix parameter names in document service fetch calls for co…
NogaNHS Oct 30, 2025
ef5149a
[PRMP-643] Add unit tests for DocumentUploadReviewService and update …
NogaNHS Oct 30, 2025
8b97a84
[PRMP-643] Add unit tests for DocumentReferenceService and update Doc…
NogaNHS Oct 30, 2025
fb1a8be
[PRMP-643] format
NogaNHS Oct 30, 2025
4af40b3
[PRMP-643] Add mock environment variables for DocumentUploadReviewSer…
NogaNHS Oct 30, 2025
887b193
[PRMP-643]Update mock environment variable for DocumentReviewService …
NogaNHS Oct 31, 2025
4e8d6fd
[PRMP-589] start implementation of querying table with ods code
steph-torres-nhs Oct 31, 2025
3b81cdd
[PRMP-589] search document service calls dynamo with correct table an…
steph-torres-nhs Oct 31, 2025
1b5be45
[PRMP-589] format
steph-torres-nhs Oct 31, 2025
29817d4
[PRMP-589] merge in PRMP-643
steph-torres-nhs Oct 31, 2025
5f6c7c6
[PRMP-589] dynamo service can take limit a query param
steph-torres-nhs Oct 31, 2025
c3e2d4f
[PRMP-589] dynamo queried with both limit and ods code
steph-torres-nhs Oct 31, 2025
739c551
[PRMP-589] service returns doc review references and last evaluated key
steph-torres-nhs Nov 3, 2025
a01b0b7
[PRMP-589] service returns empty list no documents found, none if not…
steph-torres-nhs Nov 3, 2025
91a1a51
[PRMP-589] error raised on validation error
steph-torres-nhs Nov 3, 2025
fc5bc6c
[PRMP-589] handler returns 400 response no ods in request context
steph-torres-nhs Nov 3, 2025
3a0fd2a
[PRMP-589] implement 200 and 500 handler response
steph-torres-nhs Nov 3, 2025
3515c56
[PRMP-589] format
steph-torres-nhs Nov 3, 2025
f6de5a5
[PRMP-589] add lambda to deploy lambdas workflow
steph-torres-nhs Nov 3, 2025
dbbcb55
[PRMP-589] introduce set logging context
steph-torres-nhs Nov 3, 2025
33aff20
[PRMP-589] refactor model not to use optional types
steph-torres-nhs Nov 4, 2025
f2f89eb
[PRMP-589] return subsection of doc review refs
steph-torres-nhs Nov 4, 2025
25621bf
[PRMP-589] add count to resposne
steph-torres-nhs Nov 4, 2025
4ca6572
[PRMP-589] add exclusive start key to to search doc review journey
steph-torres-nhs Nov 4, 2025
86dc76a
[PRMP-589] format
steph-torres-nhs Nov 4, 2025
6ba6cd4
[PRMP-589] add start_key to handler service call
steph-torres-nhs Nov 4, 2025
f77fb06
[PRMP-589] merge in main
steph-torres-nhs Nov 5, 2025
b9752ca
[PRMP-589] fix broken tests from merging main
steph-torres-nhs Nov 5, 2025
f86609e
[PRMP-589] refactor
steph-torres-nhs Nov 5, 2025
ca9655b
[PRMP-589] refactor
steph-torres-nhs Nov 5, 2025
9e4e801
[PRMP-589] address sonarcloud
steph-torres-nhs Nov 5, 2025
54c14b5
[PRMP-589] amend test
steph-torres-nhs Nov 6, 2025
cdf2770
Merge branch 'main' into PRMP-589
steph-torres-nhs Nov 6, 2025
da26c6a
[PRMP-589] add error handling get ods code from request context no co…
steph-torres-nhs Nov 6, 2025
b5c63f3
[PRMP-589] change process request output
steph-torres-nhs Nov 6, 2025
58ee6e3
[PRMP-589] add feature flag
steph-torres-nhs Nov 6, 2025
81ef459
[PRMP-589] add feature flag
steph-torres-nhs Nov 6, 2025
6c32fb9
[PRMP-589] refactor encoding and decoding start key to handle dict
steph-torres-nhs Nov 6, 2025
c5863cf
Implement query and validation methods for DocumentUploadReviewServic…
NogaNHS Nov 6, 2025
8d441cd
[PRMP-589] fix broken test
steph-torres-nhs Nov 7, 2025
e929874
[PRMP-589] refactor errors thrown by doc upload review service
steph-torres-nhs Nov 7, 2025
de6f8e8
[PRMP-589] refactor doc review lambda errors
steph-torres-nhs Nov 7, 2025
66de70d
[PRMP-589] refactor format of process request output
steph-torres-nhs Nov 7, 2025
107118b
[PRMP-589] add filter expression to querying review doc refs by custo…
steph-torres-nhs Nov 10, 2025
69528bc
[PRMP-589] ensure new arguements for filtering passed down
steph-torres-nhs Nov 10, 2025
1051fa0
[PRMP-589] address pr comments
steph-torres-nhs Nov 11, 2025
92e53a0
[PRMP-589] PR comments
steph-torres-nhs Nov 12, 2025
bd5dc8d
[PRMP-589] conditional return of nextPageToken, include snomed code i…
steph-torres-nhs Nov 12, 2025
7ca3d9f
[PRMP-589] address PR comment
steph-torres-nhs Nov 13, 2025
1ba4197
[PRMP-589] merge in main
steph-torres-nhs Nov 14, 2025
a70e52b
[PRMP-589] return more fields in response
steph-torres-nhs Nov 14, 2025
c5bdcec
[PRMP-589] format
steph-torres-nhs Nov 14, 2025
775fbba
[PRMP-589] catch non integar limit querystrings
steph-torres-nhs Nov 14, 2025
fd63b21
[PRMP-589] update return types
steph-torres-nhs Nov 14, 2025
f804cfd
[PRMP-589] refactor how default query limit is handled
steph-torres-nhs Nov 14, 2025
a72205c
[PRMP-589] ruff issue
steph-torres-nhs Nov 14, 2025
d89eabd
odd format
NogaNHS Nov 14, 2025
08454c6
[PRMP-589] pull in remote
steph-torres-nhs Nov 17, 2025
b5351a7
Merge branch 'main' into PRMP-589
steph-torres-nhs Nov 18, 2025
b08a33c
Merge branch 'PRMP-589' of https://github.com/nhsconnect/national-doc…
steph-torres-nhs Nov 18, 2025
fca9ea5
[PRMP-589] merge in main
steph-torres-nhs Nov 18, 2025
e31bc92
Merge branch 'main' into PRMP-589
steph-torres-nhs Nov 20, 2025
aa8c8ba
[PRMP-589] fix bugs
steph-torres-nhs Nov 20, 2025
00d83bc
[PRMP-589] add route to authoriser
steph-torres-nhs Nov 20, 2025
631a853
[PRMP-589] Refactor DocumentUploadReviewService tests to use 'query_f…
NogaNHS Nov 20, 2025
1349442
Merge branch 'main' into PRMP-589
NogaNHS Nov 25, 2025
92a7fc1
Merge branch 'main' into PRMP-589
steph-torres-nhs Nov 26, 2025
e9967b0
[PRMP-589] handle Decimal type in start key
steph-torres-nhs Nov 26, 2025
e1fc8ab
[PRMP-589] address PR comments
steph-torres-nhs Nov 27, 2025
0f87b41
Merge branch 'main' into PRMP-589
steph-torres-nhs Nov 27, 2025
c3c992f
Merge branch 'main' into PRMP-589
steph-torres-nhs Nov 27, 2025
7cb6535
Merge branch 'main' into PRMP-589
steph-torres-nhs Nov 27, 2025
365ff0e
Merge branch 'main' into PRMP-589
steph-torres-nhs 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
13 changes: 13 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,19 @@ jobs:
lambda_layer_names: "core_lambda_layer"
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_search_document_review_lambda:
name: Deploy Search Document Review
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: search_document_review_handler
lambda_aws_name: SearchDocumentReview
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_get_document_reference_by_id_lambda:
name: Deploy get_document_reference_lambda
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import StrEnum


class DocumentReviewQuerystringParameters(StrEnum):
LIMIT = "limit"
NEXT_PAGE_TOKEN = "nextPageToken"
UPLOADER = "uploader"
NHS_NUMBER = "nhsNumber"

40 changes: 31 additions & 9 deletions lambdas/enums/lambda_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,29 @@ def create_error_body(self, params: Optional[dict] = None, **kwargs) -> str:
"fhir_coding": UKCoreSpineError.VALIDATION_ERROR,
}


"""
Errors for /DocumentReference
"""
DocRefNoBody = {
"err_code": "DR_4001",
"message": "Missing event body",
"fhir_coding": FhirIssueCoding.REQUIRED,
}
}
DocRefPayload = {
"err_code": "DR_4002",
"message": "Invalid json in body",
"fhir_coding": FhirIssueCoding.INVALID,
}
}
DocRefProps = {
"err_code": "DR_4003",
"message": "Request body missing some properties",
"fhircoding": FhirIssueCoding.REQUIRED
"fhircoding": FhirIssueCoding.REQUIRED,
}
DocRefInvalidFiles = {
"err_code": "DR_4004",
"message": "Invalid files or id",
"fhir_coding": FhirIssueCoding.INVALID
}
"fhir_coding": FhirIssueCoding.INVALID,
}
DocRefNoParse = {
"err_code": "DR_4005",
"message": "Failed to parse document upload request data",
Expand Down Expand Up @@ -117,7 +116,7 @@ def create_error_body(self, params: Optional[dict] = None, **kwargs) -> str:
DocRefUploadInternalError = {
"err_code": "DR_5002",
"message": "An error occurred when creating pre-signed url for document reference",
"fhir_coding": FhirIssueCoding.EXCEPTION
"fhir_coding": FhirIssueCoding.EXCEPTION,
}
DocRefPatientSearchInvalid = {
"err_code": "DR_5003",
Expand All @@ -136,12 +135,12 @@ def create_error_body(self, params: Optional[dict] = None, **kwargs) -> str:
UpdateDocNHSNumberMismatch = {
"err_code": "UDR_5005",
"message": "NHS number did not match",
"fhir_coding": FhirIssueCoding.INVARIANT
"fhir_coding": FhirIssueCoding.INVARIANT,
}
UpdateDocNotLatestVersion = {
"err_code": "UDR_5006",
"message": "Document is not the latest version",
"fhir_coding": FhirIssueCoding.INVARIANT
"fhir_coding": FhirIssueCoding.INVARIANT,
}

"""
Expand Down Expand Up @@ -630,3 +629,26 @@ def create_error_body(self, params: Optional[dict] = None, **kwargs) -> str:
"message": "An internal server error occurred",
"fhir_coding": FhirIssueCoding.EXCEPTION,
}

"""
Errors for SearchDocumentReviewReference exceptions
"""
DocumentReviewDB = {
"err_code": "SDR_5001",
"message": RETRIEVE_DOCUMENTS,
}

DocumentReviewValidation = {
"err_code": "SDR_5002",
"message": "Review document model error",
}

SearchDocumentReviewMissingODS = {
"err_code": "SDR_4001",
"message": "Missing ODS code in request context",
}

SearchDocumentInvalidQuerystring = {
"err_code": "SDR_4002",
"message": "Invalid query string passed",
}
118 changes: 118 additions & 0 deletions lambdas/handlers/search_document_review_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import json

from enums.document_review_accepted_querystring_parameters import (
DocumentReviewQuerystringParameters,
)
from enums.feature_flags import FeatureFlags
from enums.lambda_error import LambdaError
from services.feature_flags_service import FeatureFlagService
from services.search_document_review_service import SearchDocumentReviewService
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.exceptions import OdsErrorException
from utils.lambda_exceptions import DocumentReviewException
from utils.lambda_response import ApiGatewayResponse
from utils.request_context import request_context

logger = LoggingService(__name__)


@set_request_context_for_logging
@ensure_environment_variables(names=["DOCUMENT_REVIEW_DYNAMODB_NAME"])
@override_error_check
@handle_lambda_exceptions
def lambda_handler(event, context):
"""
Lambda handler for searching documents pending review by custodian.
Triggered by GET request to /SearchDocumentReview endpoint.
Args:
event: API Gateway event containing query optional query string parameters
QueryStringParameters: limit - Limit for DynamoDB query, defaulted to 50 if not provided,
nhsNumber - Patient NHS number, used for filtering DynamoDB results,
uploader - Author ODS code, used for filtering DynamoDB results,
nextPageToken - Encoded exclusive start key used to query DynamoDB for next page of results.
context: Lambda context

Returns:
API Gateway response containing Document Review references, number of reference returned, and next page token if present.
401 - No ODS code or auth token provided in request.
500 - Document Review Error, Internal Server Error.

"""
try:
feature_flag_service = FeatureFlagService()
upload_lambda_enabled_flag_object = (
feature_flag_service.get_feature_flags_by_flag(
FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED
)
)

if not upload_lambda_enabled_flag_object[
FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED
]:
logger.info("Feature flag not enabled, event will not be processed")
raise DocumentReviewException(403, LambdaError.FeatureFlagDisabled)

ods_code = get_ods_code_from_request_context()

params = parse_querystring_parameters(event)

search_document_reference_service = SearchDocumentReviewService()

references, next_page_token = search_document_reference_service.process_request(
params=params, ods_code=ods_code
)

response = {"documentReviewReferences": references, "count": len(references)}

if next_page_token:
response["nextPageToken"] = next_page_token

return ApiGatewayResponse(
status_code=200,
body=json.dumps(response),
methods="GET",
).create_api_gateway_response()

except OdsErrorException as e:
logger.error(e)
return ApiGatewayResponse(
status_code=401,
body=LambdaError.SearchDocumentReviewMissingODS.create_error_body(),
methods="GET",
).create_api_gateway_response()


def get_ods_code_from_request_context():
logger.info("Getting ODS code from request context")
try:
ods_code = request_context.authorization.get("selected_organisation", {}).get(
"org_ods_code"
)
if not ods_code:
raise OdsErrorException()

return ods_code

except AttributeError as e:
logger.error(e)
raise DocumentReviewException(401, LambdaError.SearchDocumentReviewMissingODS)


def parse_querystring_parameters(event):
logger.info("Parsing query string parameters.")
params = event.get("queryStringParameters", {})

extracted_params = {}

if not params:
return extracted_params

for param in DocumentReviewQuerystringParameters:
if param in params:
extracted_params[param.value] = params.get(param)

return extracted_params
3 changes: 3 additions & 0 deletions lambdas/services/authoriser_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ def deny_access_policy(self, path, user_role, nhs_number: str = None):
case "/Feedback":
deny_resource = False

case "/DocumentReview":
deny_resource = False

case "/DocumentStatus":
deny_resource = (
not patient_access_is_allowed or is_user_gp_clinical or is_user_pcse
Expand Down
99 changes: 81 additions & 18 deletions lambdas/services/base/dynamo_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,33 @@ def get_table(self, table_name: str):
logger.error(str(e), {"Result": "Unable to connect to DB"})
raise e

def query_table(
def query_table_single(
self,
table_name,
search_key,
table_name: str,
search_key: str,
search_condition: str,
index_name: str | None = None,
requested_fields: list[str] = None,
query_filter: Attr | ConditionBase = None,
) -> list[dict]:
requested_fields: list[str] | None = None,
query_filter: Attr | ConditionBase | None = None,
limit: int | None = None,
start_key: dict | None = None,
) -> dict:
"""
Execute a single DynamoDB query and return the full response.

Args:
table_name: Name of the DynamoDB table
search_key: The partition key name to search on
search_condition: The value to match for the search key
index_name: Optional GSI/LSI name
requested_fields: Optional list of fields to project
query_filter: Optional filter expression
limit: Optional limit on number of items to return
start_key: Optional exclusive start key for pagination

Returns:
Full DynamoDB query response including Items, LastEvaluatedKey, etc.
"""
try:
table = self.get_table(table_name)

Expand All @@ -54,30 +72,75 @@ def query_table(

if index_name:
query_params["IndexName"] = index_name

if requested_fields:
projection_expression = ",".join(requested_fields)
query_params["ProjectionExpression"] = projection_expression

if query_filter:
query_params["FilterExpression"] = query_filter
items = []
while True:
results = table.query(**query_params)

if results is None or "Items" not in results:
logger.error(f"Unusable results in DynamoDB: {results!r}")
raise DynamoServiceException("Unrecognised response from DynamoDB")
if start_key:
query_params["ExclusiveStartKey"] = start_key

items += results["Items"]
if limit:
query_params["Limit"] = limit

if "LastEvaluatedKey" in results:
query_params["ExclusiveStartKey"] = results["LastEvaluatedKey"]
else:
break
return items
return table.query(**query_params)
except ClientError as e:
logger.error(str(e), {"Result": f"Unable to query table: {table_name}"})
raise e

def query_table(
self,
table_name: str,
search_key: str,
search_condition: str,
index_name: str | None = None,
requested_fields: list[str] | None = None,
query_filter: Attr | ConditionBase | None = None,
) -> list[dict]:
"""
Execute a DynamoDB query and automatically paginate through all results.

Args:
table_name: Name of the DynamoDB table
search_key: The partition key name to search on
search_condition: The value to match for the search key
index_name: Optional GSI/LSI name
requested_fields: Optional list of fields to project
query_filter: Optional filter expression

Returns:
List of all items from paginated query results
"""
items = []
start_key = None

while True:
results = self.query_table_single(
table_name=table_name,
search_key=search_key,
search_condition=search_condition,
index_name=index_name,
requested_fields=requested_fields,
query_filter=query_filter,
start_key=start_key,
)

if results is None or "Items" not in results:
logger.error(f"Unusable results in DynamoDB: {results!r}")
raise DynamoServiceException("Unrecognised response from DynamoDB")

items += results["Items"]

if "LastEvaluatedKey" in results:
start_key = results["LastEvaluatedKey"]
else:
break

return items

def create_item(self, table_name, item):
try:
table = self.get_table(table_name)
Expand Down
Loading
Loading