Skip to content

Commit 8b52919

Browse files
authored
[PRMP-588] document review patch endpoint (#870)
1 parent e00f0b9 commit 8b52919

28 files changed

+2780
-110
lines changed

.github/workflows/base-lambdas-reusable-deploy-all.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,20 @@ jobs:
488488
secrets:
489489
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}
490490

491+
deploy_patch_document_review_lambda:
492+
name: Deploy patch document review lambda
493+
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml
494+
with:
495+
environment: ${{ inputs.environment}}
496+
python_version: ${{ inputs.python_version }}
497+
build_branch: ${{ inputs.build_branch}}
498+
sandbox: ${{ inputs.sandbox }}
499+
lambda_handler_name: patch_document_review_handler
500+
lambda_aws_name: PatchDocumentReview
501+
lambda_layer_names: "core_lambda_layer"
502+
secrets:
503+
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}
504+
491505
deploy_get_document_review_lambda:
492506
name: Deploy get document review lambda
493507
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml

lambdas/enums/document_review_accepted_querystring_parameters.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,3 @@ class DocumentReviewQuerystringParameters(StrEnum):
66
NEXT_PAGE_TOKEN = "nextPageToken"
77
UPLOADER = "uploader"
88
NHS_NUMBER = "nhsNumber"
9-

lambdas/enums/document_review_status.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@
44
class DocumentReviewStatus(StrEnum):
55
PENDING_REVIEW = "PENDING_REVIEW"
66
APPROVED = "APPROVED"
7+
APPROVED_PENDING_DOCUMENTS = "APPROVED_PENDING_DOCUMENTS"
78
REJECTED = "REJECTED"
9+
REJECTED_DUPLICATE = "REJECTED_DUPLICATE"
10+
REASSIGNED = "REASSIGNED"
11+
REASSIGNED_PATIENT_UNKNOWN = "REASSIGNED_PATIENT_UNKNOWN"
12+
NEVER_REVIEWED = "NEVER_REVIEWED"

lambdas/enums/lambda_error.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,33 @@ def create_error_body(self, params: Optional[dict] = None, **kwargs) -> str:
486486
"message": "User is unauthorised to view record",
487487
"fhir_coding": UKCoreSpineError.ACCESS_DENIED,
488488
}
489+
"""
490+
Errors for document review lambda
491+
"""
492+
DocumentReviewNotFound = {
493+
"err_code": "DRV_4041",
494+
"message": "Document review not found",
495+
"fhir_coding": UKCoreSpineError.RESOURCE_NOT_FOUND,
496+
}
497+
DocumentReviewGeneralError = {
498+
"err_code": "DRV_4002",
499+
"message": "An error occurred while fetching the document review",
500+
"fhir_coding": FhirIssueCoding.EXCEPTION,
501+
}
502+
UpdateDocStatusUnavailable = {
503+
"err_code": "DRV_4003",
504+
"message": "This Document is not available for review update",
505+
"fhir_coding": FhirIssueCoding.FORBIDDEN,
506+
}
507+
DocumentReviewInvalidBody = {
508+
"err_code": "DRV_4004",
509+
"message": "Invalid request body",
510+
}
511+
DocumentReviewInvalidNhsNumber = {
512+
"err_code": "DRV_4005",
513+
"message": "The NHS number provided is invalid",
514+
}
515+
489516
"""
490517
Errors for get ods report lambda
491518
"""

lambdas/enums/logging_app_interaction.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ class LoggingAppInteraction(Enum):
2020
UPLOAD_CONFIRMATION = "Upload confirmation"
2121
UPDATE_UPLOAD_STATE = "Update upload state"
2222
GET_REVIEW_DOCUMENTS = "Get review documents"
23+
UPDATE_REVIEW = "Update review"

lambdas/handlers/get_document_review_handler.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from utils.decorators.set_audit_arg import set_request_context_for_logging
1313
from utils.decorators.validate_patient_id import validate_patient_id
1414
from utils.lambda_exceptions import GetDocumentReviewException
15+
from utils.lambda_handler_utils import validate_review_path_parameters
1516
from utils.lambda_response import ApiGatewayResponse
1617
from utils.request_context import request_context
1718

@@ -35,15 +36,10 @@ def lambda_handler(event, context):
3536

3637
logger.info("Get Document Review handler has been triggered")
3738
feature_flag_service = FeatureFlagService()
38-
upload_lambda_enabled_flag_object = feature_flag_service.get_feature_flags_by_flag(
39+
feature_flag_service.validate_feature_flag(
3940
FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED
4041
)
4142

42-
if not upload_lambda_enabled_flag_object[FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED]:
43-
logger.info("Feature flag not enabled, event will not be processed")
44-
raise GetDocumentReviewException(404, LambdaError.FeatureFlagDisabled)
45-
46-
# Extract patient_id from query string parameters
4743
query_params = event.get("queryStringParameters", {})
4844
patient_id = query_params.get("patientId", "")
4945

@@ -53,26 +49,19 @@ def lambda_handler(event, context):
5349
400, LambdaError.DocumentReferenceMissingParameters
5450
)
5551

56-
# Extract id from path parameters
57-
path_params = event.get("pathParameters", {})
58-
document_id = path_params.get("id")
59-
60-
if not document_id:
61-
logger.error("Missing id in path parameters")
62-
raise GetDocumentReviewException(
63-
400, LambdaError.DocumentReferenceMissingParameters
64-
)
52+
document_id, document_version = validate_review_path_parameters(event)
6553

6654
request_context.patient_nhs_no = patient_id
6755

6856
logger.info(
6957
f"Retrieving document review for patient_id: {patient_id}, document_id: {document_id}"
7058
)
7159

72-
# Get document review service
7360
document_review_service = GetDocumentReviewService()
7461
document_review = document_review_service.get_document_review(
75-
patient_id=patient_id, document_id=document_id
62+
patient_id=patient_id,
63+
document_id=document_id,
64+
document_version=document_version,
7665
)
7766

7867
if document_review:
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from enums.feature_flags import FeatureFlags
2+
from enums.lambda_error import LambdaError
3+
from enums.logging_app_interaction import LoggingAppInteraction
4+
from models.document_review import PatchDocumentReviewRequest
5+
from pydantic import ValidationError
6+
from services.feature_flags_service import FeatureFlagService
7+
from services.update_document_review_service import UpdateDocumentReviewService
8+
from utils.audit_logging_setup import LoggingService
9+
from utils.decorators.ensure_env_var import ensure_environment_variables
10+
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
11+
from utils.decorators.override_error_check import override_error_check
12+
from utils.decorators.set_audit_arg import set_request_context_for_logging
13+
from utils.decorators.validate_patient_id import validate_patient_id
14+
from utils.lambda_exceptions import UpdateDocumentReviewException
15+
from utils.lambda_handler_utils import validate_review_path_parameters
16+
from utils.lambda_response import ApiGatewayResponse
17+
from utils.request_context import request_context
18+
19+
logger = LoggingService(__name__)
20+
21+
22+
@set_request_context_for_logging
23+
@validate_patient_id
24+
@ensure_environment_variables(
25+
names=[
26+
"DOCUMENT_REVIEW_DYNAMODB_NAME",
27+
]
28+
)
29+
@override_error_check
30+
@handle_lambda_exceptions
31+
def lambda_handler(event, context):
32+
request_context.app_interaction = LoggingAppInteraction.UPDATE_REVIEW.value
33+
34+
logger.info("Patch Document Review handler has been triggered")
35+
feature_flag_service = FeatureFlagService()
36+
feature_flag_service.validate_feature_flag(
37+
FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED
38+
)
39+
40+
query_params = event.get("queryStringParameters", {})
41+
patient_id = query_params.get("patientId")
42+
43+
if not patient_id:
44+
logger.error("Missing patient_id in query string parameters")
45+
raise UpdateDocumentReviewException(400, LambdaError.PatientIdNoKey)
46+
47+
document_id, document_version = validate_review_path_parameters(event)
48+
49+
reviewer_ods_code = request_context.authorization.get(
50+
"selected_organisation", {}
51+
).get("org_ods_code")
52+
53+
if not reviewer_ods_code:
54+
logger.error("Missing ODS code in authorization token")
55+
raise UpdateDocumentReviewException(
56+
401, LambdaError.DocumentReferenceUnauthorised
57+
)
58+
59+
body = event.get("body")
60+
try:
61+
review_request = PatchDocumentReviewRequest.model_validate_json(body)
62+
except ValidationError as e:
63+
logger.error(f"Invalid request body: {str(e)}")
64+
raise UpdateDocumentReviewException(400, LambdaError.DocumentReviewInvalidBody)
65+
66+
document_review_service = UpdateDocumentReviewService()
67+
document_review_service.update_document_review(
68+
patient_id=patient_id,
69+
document_id=document_id,
70+
document_version=document_version,
71+
update_data=review_request,
72+
reviewer_ods_code=reviewer_ods_code,
73+
)
74+
75+
logger.info(
76+
"Document review updated successfully",
77+
{"Result": "Successful document review update"},
78+
)
79+
return ApiGatewayResponse(200, "", "PATCH").create_api_gateway_response()

lambdas/models/document_review.py

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
from enums.document_review_status import DocumentReviewStatus
44
from enums.metadata_field_names import DocumentReferenceMetadataFields
55
from enums.snomed_codes import SnomedCodes
6-
from pydantic import BaseModel, ConfigDict, Field
7-
from pydantic.alias_generators import to_pascal, to_camel
6+
from pydantic import BaseModel, ConfigDict, Field, model_validator
7+
from pydantic.alias_generators import to_camel, to_pascal
8+
from utils.exceptions import InvalidNhsNumberException
9+
from utils.utilities import validate_nhs_number
810

911

1012
class DocumentReviewFileDetails(BaseModel):
@@ -41,10 +43,8 @@ class DocumentUploadReviewReference(BaseModel):
4143
upload_date: int
4244
files: list[DocumentReviewFileDetails] = Field(min_length=1)
4345
nhs_number: str
44-
ttl: int | None = Field(
45-
alias=str(DocumentReferenceMetadataFields.TTL.value), default=None
46-
)
47-
document_reference_id: str | None = Field(default=None)
46+
version: int = Field(default=1)
47+
document_reference_id: str = Field(default=None)
4848
document_snomed_code_type: str = Field(default=SnomedCodes.LLOYD_GEORGE.value.code)
4949

5050
def model_dump_camel_case(self, *args, **kwargs):
@@ -66,4 +66,63 @@ def camelize(self, model: dict) -> dict:
6666
value = result
6767
camel_case_dict[to_camel(key)] = value
6868

69-
return camel_case_dict
69+
return camel_case_dict
70+
71+
class PatchDocumentReviewRequest(BaseModel):
72+
model_config = ConfigDict(
73+
validate_by_alias=True,
74+
populate_by_name=True,
75+
alias_generator=to_camel,
76+
use_enum_values=True,
77+
)
78+
79+
review_status: DocumentReviewStatus = Field(..., description="Review outcome")
80+
document_reference_id: str | None = Field(
81+
default=None,
82+
description="Document reference ID (required when status is APPROVED)",
83+
)
84+
nhs_number: str | None = Field(
85+
default=None,
86+
description="New NHS number (required when status is REASSIGNED)",
87+
)
88+
89+
@model_validator(mode="after")
90+
def validate_document_reference_id(self):
91+
"""Ensure document_reference_id is provided when review_status is APPROVED."""
92+
if (
93+
self.review_status == DocumentReviewStatus.APPROVED
94+
and not self.document_reference_id
95+
):
96+
raise ValueError(
97+
"document_reference_id is required when review_status is APPROVED"
98+
)
99+
elif (
100+
self.review_status != DocumentReviewStatus.APPROVED
101+
and self.document_reference_id
102+
):
103+
raise ValueError(
104+
"document_reference_id is not required when review_status is not APPROVED"
105+
)
106+
return self
107+
108+
@model_validator(mode="after")
109+
def validate_reassign_nhs_number(self):
110+
"""
111+
Validate the reassignment of the NHS number after the input data model has been populated.
112+
113+
Checks whether the `reassigned_nhs_number` field has been provided and is valid when the
114+
`review_status` reflects a reassigned state. Raises an error if validation fails.
115+
"""
116+
if (
117+
self.review_status == DocumentReviewStatus.REASSIGNED
118+
and not self.nhs_number
119+
):
120+
raise ValueError(
121+
"reassigned_nhs_number is required when review_status is REASSIGNED or REASSIGNED_PATIENT_UNKNOWN"
122+
)
123+
elif self.review_status == DocumentReviewStatus.REASSIGNED and self.nhs_number:
124+
try:
125+
validate_nhs_number(self.nhs_number)
126+
except InvalidNhsNumberException:
127+
raise ValueError("Invalid NHS number")
128+
return self

lambdas/services/authoriser_service.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ def deny_access_policy(self, path, user_role, nhs_number: str = None):
9797

9898
case doc_ref if re.match(doc_ref_pattern, doc_ref):
9999
deny_resource = True
100-
if (is_user_gp_admin or is_user_gp_clinical) and patient_access_is_allowed:
100+
if (
101+
is_user_gp_admin or is_user_gp_clinical
102+
) and patient_access_is_allowed:
101103
deny_resource = False
102104
if patient_access_is_allowed and access_to_deceased_patient:
103105
deny_resource = True
@@ -125,9 +127,7 @@ def deny_access_policy(self, path, user_role, nhs_number: str = None):
125127
not patient_access_is_allowed or is_user_gp_clinical or is_user_pcse
126128
)
127129
case path if path.startswith("/DocumentReview/"):
128-
deny_resource = (
129-
not patient_access_is_allowed
130-
)
130+
deny_resource = not patient_access_is_allowed
131131

132132
case "/UploadState":
133133
deny_resource = (

0 commit comments

Comments
 (0)