Skip to content

Commit acf483a

Browse files
[PRMP-167] Add put fhir document reference base service (#808)
Co-authored-by: Adam Whiting <[email protected]>
1 parent c3a8602 commit acf483a

14 files changed

+1474
-0
lines changed

lambdas/enums/fhir/fhir_issue_type.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33

44
class FhirIssueCoding(Enum):
55
INVALID = ("invalid", "Invalid Content")
6+
REQUIRED = ("required", "Required element missing")
67
FORBIDDEN = ("forbidden", "Forbidden")
78
NOT_FOUND = ("not-found", "Not Found")
89
EXCEPTION = ("exception", "Exception")
910
UNKNOWN = ("unknown", "Unknown User")
11+
CONFLICT = ("conflict", "Edit Version Conflict")
12+
INVARIANT = ("invariant", "Validation rule failed")
13+
DUPLICATE = ("duplicate", "Duplicate")
1014

1115
@property
1216
def code(self):

lambdas/enums/lambda_error.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,85 @@ def create_error_body(self, params: Optional[dict] = None, **kwargs) -> str:
107107
"fhir_coding": UKCoreSpineError.VALIDATION_ERROR,
108108
}
109109

110+
"""
111+
Errors for UpdateDocumentRefException
112+
"""
113+
UpdateDocNoBody = {
114+
"err_code": "UDR_4001",
115+
"message": "Missing event body",
116+
"fhir_coding": FhirIssueCoding.REQUIRED,
117+
}
118+
UpdateDocPayload = {
119+
"err_code": "UDR_4002",
120+
"message": "Invalid json in body",
121+
"fhir_coding": FhirIssueCoding.INVALID,
122+
}
123+
UpdateDocProps = {
124+
"err_code": "UDR_4003",
125+
"message": "Request body missing some properties",
126+
"fhircoding": FhirIssueCoding.REQUIRED
127+
}
128+
UpdateDocFiles = {
129+
"err_code": "UDR_4004",
130+
"message": "Invalid files or id",
131+
"fhir_coding": FhirIssueCoding.INVALID
132+
}
133+
UpdateDocNoParse = {
134+
"err_code": "UDR_4005",
135+
"message": "Failed to parse document upload request data",
136+
"fhir_coding": UKCoreSpineError.VALIDATION_ERROR,
137+
}
138+
UpdateDocNoType = {
139+
"err_code": "UDR_4006",
140+
"message": "Failed to parse document upload request data due to missing document type",
141+
"fhir_coding": UKCoreSpineError.MISSING_VALUE,
142+
}
143+
UpdateDocInvalidType = {
144+
"err_code": "UDR_4007",
145+
"message": "Failed to parse document upload request data due to invalid document type",
146+
"fhir_coding": UKCoreSpineError.INVALID_VALUE,
147+
}
148+
UpdateDocRecordAlreadyInPlace = {
149+
"err_code": "UDR_4008",
150+
"message": "The patient already has a full set of record.",
151+
"fhir_coding": FhirIssueCoding.DUPLICATE,
152+
}
153+
UpdateDocRefOdsCodeNotAllowed = {
154+
"err_code": "UDR_4009",
155+
"message": "ODS code does not match any of the allowed.",
156+
"fhir_coding": FhirIssueCoding.INVALID,
157+
}
158+
UpdateDocPresign = {
159+
"err_code": "UDR_5001",
160+
"message": "An error occurred when creating pre-signed url for document reference",
161+
"fhir_coding": FhirIssueCoding.EXCEPTION,
162+
}
163+
UpdateDocUploadInternalError = {
164+
"err_code": "UDR_5002",
165+
"message": "An error occurred when creating pre-signed url for document reference",
166+
"fhir_coding": FhirIssueCoding.EXCEPTION
167+
}
168+
UpdatePatientSearchInvalid = {
169+
"err_code": "UDR_5003",
170+
"message": "Failed to validate patient",
171+
"fhir_coding": UKCoreSpineError.VALIDATION_ERROR,
172+
}
173+
UpdateDocVersionMismatch = {
174+
"err_code": "UDR_5004",
175+
"message": "Document reference version did not match current document version",
176+
"fhir_coding": FhirIssueCoding.CONFLICT,
177+
}
178+
UpdateDocNHSNumberMismatch = {
179+
"err_code": "UDR_5005",
180+
"message": "NHS number did not match",
181+
"fhir_coding": FhirIssueCoding.INVARIANT
182+
}
183+
UpdateDocNotLatestVersion = {
184+
"err_code": "UDR_5006",
185+
"message": "Document is not the latest version",
186+
"fhir_coding": FhirIssueCoding.INVARIANT
187+
}
188+
110189
"""
111190
Errors for InvalidDocTypeException
112191
"""

lambdas/models/fhir/R4/base_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ class Link(BaseModel):
4747
class Meta(BaseModel):
4848
security: Optional[List[Coding]] = None
4949
tag: Optional[List[Coding]] = None
50+
versionId: Optional[str] = None

lambdas/models/fhir/R4/fhir_document_reference.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
Reference,
1313
)
1414
from pydantic import BaseModel, Field
15+
from utils.exceptions import FhirDocumentReferenceException
1516

1617
from utils.ods_utils import PCSE_ODS_CODE
1718

@@ -127,6 +128,19 @@ class DocumentReference(BaseModel):
127128
context: Optional[DocumentReferenceContext] = None
128129
meta: Optional[Meta] = None
129130

131+
def extract_nhs_number_from_fhir(self) -> str:
132+
"""Extract NHS number from FHIR document"""
133+
# Extract NHS number from subject.identifier where the system identifier is NHS number
134+
if (
135+
self.subject
136+
and self.subject.identifier
137+
and self.subject.identifier.system
138+
== "https://fhir.nhs.uk/Id/nhs-number"
139+
):
140+
return self.subject.identifier.value
141+
else:
142+
raise FhirDocumentReferenceException("NHS number was not found")
143+
130144

131145
class DocumentReferenceInfo(BaseModel):
132146
"""Information needed to create a DocumentReference resource."""
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import base64
2+
import io
3+
import binascii
4+
import os
5+
6+
from utils.audit_logging_setup import LoggingService
7+
from services.base.s3_service import S3Service
8+
from services.base.dynamo_service import DynamoDBService
9+
from models.document_reference import DocumentReference
10+
from botocore.exceptions import ClientError
11+
from enums.snomed_codes import SnomedCode, SnomedCodes
12+
from models.fhir.R4.fhir_document_reference import (
13+
DocumentReference as FhirDocumentReference,
14+
)
15+
from utils.utilities import create_reference_id, get_pds_service, validate_nhs_number
16+
from enums.patient_ods_inactive_status import PatientOdsInactiveStatus
17+
from utils.ods_utils import PCSE_ODS_CODE
18+
from models.fhir.R4.fhir_document_reference import SNOMED_URL
19+
from utils.common_query_filters import CurrentStatusFile
20+
from models.pds_models import PatientDetails
21+
from utils.exceptions import (
22+
InvalidResourceIdException,
23+
PatientNotFoundException,
24+
PdsErrorException,
25+
FhirDocumentReferenceException
26+
)
27+
from services.document_service import DocumentService
28+
from models.fhir.R4.fhir_document_reference import Attachment
29+
from models.fhir.R4.fhir_document_reference import DocumentReferenceInfo
30+
31+
logger = LoggingService(__name__)
32+
33+
class FhirDocumentReferenceServiceBase:
34+
def __init__(self):
35+
presigned_aws_role_arn = os.getenv("PRESIGNED_ASSUME_ROLE")
36+
self.s3_service = S3Service(custom_aws_role=presigned_aws_role_arn)
37+
self.dynamo_service = DynamoDBService()
38+
self.staging_bucket_name = os.getenv("STAGING_STORE_BUCKET_NAME")
39+
self.document_service = DocumentService()
40+
41+
def _store_binary_in_s3(
42+
self, document_reference: DocumentReference, binary_content: bytes
43+
) -> None:
44+
"""Store binary content in S3"""
45+
try:
46+
binary_file = io.BytesIO(base64.b64decode(binary_content, validate=True))
47+
self.s3_service.upload_file_obj(
48+
file_obj=binary_file,
49+
s3_bucket_name=document_reference.s3_bucket_name,
50+
file_key=document_reference.s3_file_key,
51+
)
52+
logger.info(
53+
f"Successfully stored binary content in S3: {document_reference.s3_file_key}"
54+
)
55+
except (binascii.Error, ValueError) as e:
56+
logger.error(f"Failed to decode base64: {str(e)}")
57+
raise FhirDocumentReferenceException(f"Failed to decode base64: {str(e)}")
58+
except MemoryError as e:
59+
logger.error(f"File too large to process: {str(e)}")
60+
raise FhirDocumentReferenceException(f"File too large to process: {str(e)}")
61+
except ClientError as e:
62+
logger.error(f"Failed to store binary in S3: {str(e)}")
63+
raise FhirDocumentReferenceException(f"Failed to store binary in S3: {str(e)}")
64+
except (OSError, IOError) as e:
65+
logger.error(f"I/O error when processing binary content: {str(e)}")
66+
raise FhirDocumentReferenceException(f"I/O error when processing binary content: {str(e)}")
67+
68+
def _create_s3_presigned_url(self, document_reference: DocumentReference) -> str:
69+
"""Create a pre-signed URL for uploading a file"""
70+
try:
71+
response = self.s3_service.create_put_presigned_url(
72+
document_reference.s3_bucket_name, document_reference.s3_file_key
73+
)
74+
logger.info(
75+
f"Successfully created pre-signed URL for {document_reference.s3_file_key}"
76+
)
77+
return response
78+
except ClientError as e:
79+
logger.error(f"Failed to create pre-signed URL: {str(e)}")
80+
raise FhirDocumentReferenceException(f"Failed to create pre-signed URL: {str(e)}")
81+
82+
def _create_document_reference(
83+
self,
84+
nhs_number: str,
85+
doc_type: SnomedCode,
86+
fhir_doc: FhirDocumentReference,
87+
current_gp_ods: str,
88+
version: str,
89+
s3_file_key: str,
90+
) -> DocumentReference:
91+
"""Create a document reference model"""
92+
document_id = create_reference_id()
93+
94+
custodian = fhir_doc.custodian.identifier.value if fhir_doc.custodian else None
95+
if not custodian:
96+
custodian = (
97+
current_gp_ods
98+
if current_gp_ods not in PatientOdsInactiveStatus.list()
99+
else PCSE_ODS_CODE
100+
)
101+
document_reference = DocumentReference(
102+
id=document_id,
103+
nhs_number=nhs_number,
104+
current_gp_ods=current_gp_ods,
105+
custodian=custodian,
106+
s3_bucket_name=self.staging_bucket_name,
107+
author=fhir_doc.author[0].identifier.value,
108+
content_type=fhir_doc.content[0].attachment.contentType,
109+
file_name=fhir_doc.content[0].attachment.title,
110+
document_snomed_code_type=doc_type.code,
111+
doc_status="preliminary",
112+
status="current",
113+
sub_folder="user_upload",
114+
version=version,
115+
s3_file_key=s3_file_key
116+
)
117+
118+
return document_reference
119+
120+
def _get_document_reference(self, document_id: str, table) -> DocumentReference:
121+
documents = self.document_service.fetch_documents_from_table(
122+
table=table,
123+
search_condition=document_id,
124+
search_key="ID",
125+
query_filter=CurrentStatusFile,
126+
)
127+
if len(documents) > 0:
128+
logger.info("Document found for given id")
129+
return documents[0]
130+
else:
131+
raise FhirDocumentReferenceException(f"Did not find any documents for document ID {document_id}")
132+
133+
def _determine_document_type(self, fhir_doc: FhirDocumentReference) -> SnomedCode:
134+
"""Determine the document type based on SNOMED code in the FHIR document"""
135+
if fhir_doc.type and fhir_doc.type.coding:
136+
for coding in fhir_doc.type.coding:
137+
if coding.system == SNOMED_URL:
138+
if coding.code == SnomedCodes.LLOYD_GEORGE.value.code:
139+
return SnomedCodes.LLOYD_GEORGE.value
140+
logger.error("SNOMED code not found in FHIR document")
141+
raise FhirDocumentReferenceException("SNOMED code not found in FHIR document")
142+
143+
def _save_document_reference_to_dynamo(
144+
self, table_name: str, document_reference: DocumentReference
145+
) -> None:
146+
"""Save document reference to DynamoDB"""
147+
try:
148+
self.dynamo_service.create_item(
149+
table_name,
150+
document_reference.model_dump(exclude_none=True, by_alias=True),
151+
)
152+
logger.info(f"Successfully created document reference in {table_name}")
153+
except ClientError as e:
154+
logger.error(f"Failed to create document reference: {str(e)}")
155+
raise FhirDocumentReferenceException(f"Failed to create document reference: {str(e)}")
156+
157+
def _check_nhs_number_with_pds(self, nhs_number: str) -> PatientDetails:
158+
try:
159+
validate_nhs_number(nhs_number)
160+
pds_service = get_pds_service()
161+
return pds_service.fetch_patient_details(nhs_number)
162+
except (
163+
PatientNotFoundException,
164+
InvalidResourceIdException,
165+
PdsErrorException,
166+
) as e:
167+
logger.error(f"Error occurred when fetching patient details: {str(e)}")
168+
raise FhirDocumentReferenceException(f"Error occurred when fetching patient details: {str(e)}")
169+
170+
def _create_fhir_response(
171+
self,
172+
document_reference_ndr: DocumentReference,
173+
presigned_url: str,
174+
) -> str:
175+
"""Create a FHIR response document"""
176+
177+
if presigned_url:
178+
attachment_url = presigned_url
179+
else:
180+
document_retrieve_endpoint = os.getenv(
181+
"DOCUMENT_RETRIEVE_ENDPOINT_APIM", ""
182+
)
183+
attachment_url = (
184+
document_retrieve_endpoint
185+
+ "/"
186+
+ document_reference_ndr.document_snomed_code_type
187+
+ "~"
188+
+ document_reference_ndr.id
189+
)
190+
document_details = Attachment(
191+
title=document_reference_ndr.file_name,
192+
creation=document_reference_ndr.document_scan_creation
193+
or document_reference_ndr.created,
194+
url=attachment_url,
195+
)
196+
fhir_document_reference = (
197+
DocumentReferenceInfo(
198+
nhs_number=document_reference_ndr.nhs_number,
199+
attachment=document_details,
200+
custodian=document_reference_ndr.custodian,
201+
snomed_code_doc_type=SnomedCodes.find_by_code(
202+
document_reference_ndr.document_snomed_code_type
203+
),
204+
)
205+
.create_fhir_document_reference_object(document_reference_ndr)
206+
.model_dump_json(exclude_none=True)
207+
)
208+
209+
return fhir_document_reference

0 commit comments

Comments
 (0)