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