Skip to content

Commit d01350a

Browse files
authored
PRMP-643 MNS update with review table (#840)
1 parent d777726 commit d01350a

21 files changed

+1281
-261
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from enum import StrEnum
2+
3+
4+
class DocumentReviewStatus(StrEnum):
5+
PENDING_REVIEW = "PENDING_REVIEW"
6+
APPROVED = "APPROVED"
7+
REJECTED = "REJECTED"

lambdas/enums/feature_flags.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ class FeatureFlags(StrEnum):
1010
"lloydGeorgeValidationStrictModeEnabled"
1111
)
1212
UPLOAD_DOCUMENT_ITERATION_2_ENABLED = "uploadDocumentIteration2Enabled"
13+
UPLOAD_DOCUMENT_ITERATION_3_ENABLED = "uploadDocumentIteration3Enabled"

lambdas/handlers/mns_notification_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"APPCONFIG_CONFIGURATION",
2020
"APPCONFIG_ENVIRONMENT",
2121
"LLOYD_GEORGE_DYNAMODB_NAME",
22+
"DOCUMENT_REVIEW_DYNAMODB_NAME",
2223
"MNS_NOTIFICATION_QUEUE_URL",
2324
]
2425
)

lambdas/models/document_review.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import uuid
2+
from typing import Optional
3+
4+
from enums.document_review_status import DocumentReviewStatus
5+
from enums.metadata_field_names import DocumentReferenceMetadataFields
6+
from enums.snomed_codes import SnomedCodes
7+
from pydantic import BaseModel, ConfigDict, Field
8+
from pydantic.alias_generators import to_pascal
9+
10+
11+
class DocumentReviewFileDetails(BaseModel):
12+
model_config = ConfigDict(
13+
validate_by_alias=True,
14+
validate_by_name=True,
15+
alias_generator=to_pascal,
16+
)
17+
18+
file_name: str
19+
file_location: str
20+
21+
22+
class DocumentUploadReviewReference(BaseModel):
23+
model_config = ConfigDict(
24+
validate_by_alias=True,
25+
validate_by_name=True,
26+
alias_generator=to_pascal,
27+
use_enum_values=True,
28+
)
29+
id: str = Field(
30+
default_factory=lambda: str(uuid.uuid4()),
31+
alias=str(DocumentReferenceMetadataFields.ID.value),
32+
)
33+
author: str
34+
custodian: str
35+
review_status: DocumentReviewStatus = Field(
36+
default=DocumentReviewStatus.PENDING_REVIEW
37+
)
38+
review_reason: str
39+
review_date: int = Field(default=None)
40+
reviewer: str = Field(default=None)
41+
upload_date: int
42+
files: list[DocumentReviewFileDetails]
43+
nhs_number: str
44+
ttl: Optional[int] = Field(
45+
alias=str(DocumentReferenceMetadataFields.TTL.value), default=None
46+
)
47+
document_reference_id: str = Field(default=None)
48+
document_snomed_code_type: str = Field(default=SnomedCodes.LLOYD_GEORGE.value.code)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from datetime import datetime
2+
3+
from enums.supported_document_types import SupportedDocumentTypes
4+
from models.document_reference import DocumentReference
5+
from services.document_service import DocumentService
6+
from utils.audit_logging_setup import LoggingService
7+
from utils.dynamo_utils import filter_uploaded_docs_and_recently_uploading_docs
8+
from utils.exceptions import FileUploadInProgress, NoAvailableDocument
9+
10+
logger = LoggingService(__name__)
11+
12+
13+
class DocumentReferenceService(DocumentService):
14+
"""Service for handling DocumentReference operations."""
15+
16+
def __init__(self, doc_type: SupportedDocumentTypes = SupportedDocumentTypes.LG):
17+
super().__init__()
18+
self.doc_type = doc_type
19+
20+
@property
21+
def table_name(self) -> str:
22+
return self.doc_type.get_dynamodb_table_name()
23+
24+
@property
25+
def model_class(self) -> type:
26+
return DocumentReference
27+
28+
@property
29+
def s3_bucket(self) -> str:
30+
return self.doc_type.get_s3_bucket_name()
31+
32+
def get_available_lloyd_george_record_for_patient(
33+
self, nhs_number: str
34+
) -> list[DocumentReference]:
35+
"""Get available Lloyd George records for a patient, checking for upload status."""
36+
filter_expression = filter_uploaded_docs_and_recently_uploading_docs()
37+
available_docs = self.fetch_documents_from_table_with_nhs_number(
38+
nhs_number,
39+
query_filter=filter_expression,
40+
)
41+
42+
file_in_progress_message = (
43+
"The patients Lloyd George record is in the process of being uploaded"
44+
)
45+
if not available_docs:
46+
raise NoAvailableDocument()
47+
for document in available_docs:
48+
if document.uploading and not document.uploaded:
49+
raise FileUploadInProgress(file_in_progress_message)
50+
return available_docs
51+
52+
def update_patient_ods_code(
53+
self,
54+
patient_documents: list[DocumentReference],
55+
updated_ods_code: str,
56+
) -> None:
57+
update_field = {"current_gp_ods", "custodian", "last_updated"}
58+
if not patient_documents:
59+
return
60+
61+
for reference in patient_documents:
62+
logger.info("Updating patient document reference...")
63+
64+
if (
65+
reference.current_gp_ods != updated_ods_code
66+
or reference.custodian != updated_ods_code
67+
):
68+
reference.current_gp_ods = updated_ods_code
69+
reference.custodian = updated_ods_code
70+
reference.last_updated = int(datetime.now().timestamp())
71+
72+
self.update_document(
73+
document=reference, update_fields_name=update_field
74+
)

lambdas/services/document_service.py

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,39 @@
55
from enums.metadata_field_names import DocumentReferenceMetadataFields
66
from enums.supported_document_types import SupportedDocumentTypes
77
from models.document_reference import DocumentReference
8-
from pydantic import ValidationError
8+
from pydantic import BaseModel, ValidationError
99
from services.base.dynamo_service import DynamoDBService
1010
from services.base.s3_service import S3Service
1111
from utils.audit_logging_setup import LoggingService
1212
from utils.common_query_filters import NotDeleted
13-
from utils.dynamo_utils import filter_uploaded_docs_and_recently_uploading_docs
14-
from utils.exceptions import (
15-
DocumentServiceException,
16-
FileUploadInProgress,
17-
NoAvailableDocument,
18-
)
13+
from utils.exceptions import DocumentServiceException
1914

2015
logger = LoggingService(__name__)
2116

2217

2318
class DocumentService:
19+
"""Service for document operations."""
20+
2421
def __init__(self):
2522
self.s3_service = S3Service()
2623
self.dynamo_service = DynamoDBService()
24+
self._lg_table_name = os.getenv("LLOYD_GEORGE_DYNAMODB_NAME")
25+
self._lg_s3_bucket = os.getenv("LLOYD_GEORGE_BUCKET_NAME")
26+
27+
@property
28+
def table_name(self) -> str:
29+
"""DynamoDB table name. Can be overridden by child classes."""
30+
return self._lg_table_name
31+
32+
@property
33+
def s3_bucket(self) -> str:
34+
"""S3 bucket name. Can be overridden by child classes."""
35+
return self._lg_s3_bucket
36+
37+
@property
38+
def model_class(self) -> type[BaseModel]:
39+
"""Pydantic model class. Can be overridden by child classes."""
40+
return DocumentReference
2741

2842
def fetch_available_document_references_by_type(
2943
self,
@@ -34,56 +48,74 @@ def fetch_available_document_references_by_type(
3448
table_name = doc_type.get_dynamodb_table_name()
3549

3650
return self.fetch_documents_from_table_with_nhs_number(
37-
nhs_number, table_name, query_filter=query_filter
51+
nhs_number,
52+
table_name,
53+
query_filter=query_filter,
3854
)
3955

4056
def fetch_documents_from_table_with_nhs_number(
41-
self, nhs_number: str, table: str, query_filter: Attr | ConditionBase = None
42-
) -> list[DocumentReference]:
57+
self,
58+
nhs_number: str,
59+
table: str | None = None,
60+
query_filter: Attr | ConditionBase = None,
61+
model_class: type[BaseModel] = None,
62+
) -> list:
63+
"""Fetch documents by NHS number from specified or configured table."""
64+
table_name = table or self.table_name
65+
4366
documents = self.fetch_documents_from_table(
44-
table=table,
4567
index_name="NhsNumberIndex",
4668
search_key="NhsNumber",
4769
search_condition=nhs_number,
4870
query_filter=query_filter,
71+
table_name=table_name,
72+
model_class=model_class,
4973
)
50-
5174
return documents
5275

5376
def fetch_documents_from_table(
5477
self,
55-
table: str,
5678
search_condition: str,
5779
search_key: str,
5880
index_name: str | None = None,
5981
query_filter: Attr | ConditionBase = None,
60-
) -> list[DocumentReference]:
82+
table_name: str | None = None,
83+
model_class: type[BaseModel] = None,
84+
) -> list:
85+
"""Fetch documents from specified or configured table using model_class."""
6186
documents = []
87+
table_name = table_name or self.table_name
88+
model_class = model_class or self.model_class
6289

6390
response = self.dynamo_service.query_table(
64-
table_name=table,
91+
table_name=table_name,
6592
index_name=index_name,
6693
search_key=search_key,
6794
search_condition=search_condition,
6895
query_filter=query_filter,
6996
)
7097
for item in response:
7198
try:
72-
document = DocumentReference.model_validate(item)
99+
document = model_class.model_validate(item)
73100
documents.append(document)
74101
except ValidationError as e:
75102
logger.error(f"Validation error on document: {item}")
76103
logger.error(f"{e}")
77104
continue
78105
return documents
79106

80-
def get_nhs_numbers_based_on_ods_code(self, ods_code: str) -> list[str]:
107+
def get_nhs_numbers_based_on_ods_code(
108+
self, ods_code: str, table_name: str | None = None
109+
) -> list[str]:
110+
"""Get unique NHS numbers for patients with given ODS code."""
111+
table_name = table_name or self.table_name
112+
81113
documents = self.fetch_documents_from_table(
82-
table=os.environ["LLOYD_GEORGE_DYNAMODB_NAME"],
83114
index_name="OdsCodeIndex",
84115
search_key=DocumentReferenceMetadataFields.CURRENT_GP_ODS.value,
85116
search_condition=ods_code,
86117
query_filter=NotDeleted,
118+
table_name=table_name,
87119
)
88120
nhs_numbers = list({document.nhs_number for document in documents})
89121
return nhs_numbers
@@ -139,21 +171,27 @@ def delete_document_object(self, bucket: str, key: str):
139171

140172
def update_document(
141173
self,
142-
table_name: str,
143-
document_reference: DocumentReference,
144-
update_fields_name: set[str] = None,
174+
table_name: str | None = None,
175+
document: BaseModel = None,
176+
update_fields_name: set[str] | None = None,
145177
):
178+
"""Update document in specified or configured table."""
179+
table_name = table_name or self.table_name
180+
146181
self.dynamo_service.update_item(
147182
table_name=table_name,
148-
key_pair={DocumentReferenceMetadataFields.ID.value: document_reference.id},
149-
updated_fields=document_reference.model_dump(
183+
key_pair={DocumentReferenceMetadataFields.ID.value: document.id},
184+
updated_fields=document.model_dump(
150185
exclude_none=True, by_alias=True, include=update_fields_name
151186
),
152187
)
153188

154189
def hard_delete_metadata_records(
155-
self, table_name: str, document_references: list[DocumentReference]
190+
self, table_name: str, document_references: list[BaseModel]
156191
):
192+
"""Permanently delete metadata from specified or configured table."""
193+
table_name = table_name or self.table_name
194+
157195
logger.info(f"Deleting items in table: {table_name} (HARD DELETE)")
158196
primary_key_name = DocumentReferenceMetadataFields.ID.value
159197
for reference in document_references:
@@ -162,41 +200,26 @@ def hard_delete_metadata_records(
162200
self.dynamo_service.delete_item(table_name, deletion_key)
163201

164202
@staticmethod
165-
def is_upload_in_process(record: DocumentReference):
203+
def is_upload_in_process(record: DocumentReference) -> bool:
204+
"""Check if a document upload is currently in progress."""
166205
return (
167206
not record.uploaded
168207
and record.uploading
169208
and record.last_updated_within_three_minutes()
170209
and record.doc_status != "final"
171210
)
172211

173-
def get_available_lloyd_george_record_for_patient(
174-
self, nhs_number
175-
) -> list[DocumentReference]:
176-
filter_expression = filter_uploaded_docs_and_recently_uploading_docs()
177-
available_docs = self.fetch_available_document_references_by_type(
178-
nhs_number,
179-
SupportedDocumentTypes.LG,
180-
query_filter=filter_expression,
181-
)
182-
183-
file_in_progress_message = (
184-
"The patients Lloyd George record is in the process of being uploaded"
185-
)
186-
if not available_docs:
187-
raise NoAvailableDocument()
188-
for document in available_docs:
189-
if document.uploading and not document.uploaded:
190-
raise FileUploadInProgress(file_in_progress_message)
191-
return available_docs
192-
193212
def get_batch_document_references_by_id(
194213
self, document_ids: list[str], doc_type: SupportedDocumentTypes
195-
) -> list[DocumentReference]:
214+
) -> list:
196215
table_name = doc_type.get_dynamodb_table_name()
216+
217+
table_name = table_name or self.table_name
218+
model_class = self.model_class
219+
197220
response = self.dynamo_service.batch_get_items(
198221
table_name=table_name, key_list=document_ids
199222
)
200223

201-
found_docs = [DocumentReference.model_validate(item) for item in response]
224+
found_docs = [model_class.model_validate(item) for item in response]
202225
return found_docs

0 commit comments

Comments
 (0)