11from uuid import uuid4
22
3- from nrlf .core .constants import PERMISSION_AUDIT_DATES_FROM_PAYLOAD
3+ from nrlf .core .codes import SpineErrorConcept
4+ from nrlf .core .constants import (
5+ PERMISSION_AUDIT_DATES_FROM_PAYLOAD ,
6+ PERMISSION_SUPERSEDE_IGNORE_DELETE_FAIL ,
7+ )
48from nrlf .core .decorators import request_handler
59from nrlf .core .dynamodb .repository import DocumentPointer , DocumentPointerRepository
10+ from nrlf .core .errors import OperationOutcomeError
611from nrlf .core .logger import LogReference , logger
712from nrlf .core .model import ConnectionMetadata
813from nrlf .core .response import NRLResponse , Response , SpineErrorResponse
@@ -37,48 +42,28 @@ def _set_create_time_fields(
3742 return document_reference
3843
3944
40- @request_handler (body = DocumentReference )
41- def handler (
42- metadata : ConnectionMetadata ,
43- repository : DocumentPointerRepository ,
44- body : DocumentReference ,
45- ) -> Response :
45+ def _create_core_model (resource : DocumentReference , metadata : ConnectionMetadata ):
4646 """
47- Creates a document reference.
48-
49- Args:
50- metadata (ConnectionMetadata): The connection metadata.
51- repository (DocumentPointerRepository): The document pointer repository.
52- body (DocumentReference): The document reference to create.
53-
54- Returns:
55- Response: The response indicating the result of the operation.
47+ Create the DocumentPointer model from the provided DocumentReference
5648 """
57- logger .log (LogReference .PROCREATE000 )
58-
59- logger .log (LogReference .PROCREATE001 , resource = body )
60-
61- id_prefix = "|" .join (metadata .ods_code_parts )
62- body .id = f"{ id_prefix } -{ uuid4 ()} "
63-
64- validator = DocumentReferenceValidator ()
65- result = validator .validate (body )
66-
67- if not result .is_valid :
68- logger .log (LogReference .PROCREATE002 )
69- return Response .from_issues (issues = result .issues , statusCode = "400" )
70-
7149 creation_time = create_fhir_instant ()
7250 document_reference = _set_create_time_fields (
7351 creation_time ,
74- document_reference = result . resource ,
52+ document_reference = resource ,
7553 nrl_permissions = metadata .nrl_permissions ,
7654 )
7755
78- core_model = DocumentPointer .from_document_reference (
56+ return DocumentPointer .from_document_reference (
7957 document_reference , created_on = creation_time
8058 )
8159
60+
61+ def _check_permissions (
62+ core_model : DocumentPointer , metadata : ConnectionMetadata
63+ ) -> Response | None :
64+ """
65+ Check the requester has permissions to create the DocumentReference
66+ """
8267 custodian_parts = tuple (
8368 filter (None , (core_model .custodian , core_model .custodian_suffix ))
8469 )
@@ -105,71 +90,175 @@ def handler(
10590 expression = "type.coding[0].code" ,
10691 )
10792
93+
94+ def _get_document_ids_to_supersede (
95+ resource : DocumentReference ,
96+ core_model : DocumentPointer ,
97+ metadata : ConnectionMetadata ,
98+ repository : DocumentPointerRepository ,
99+ can_ignore_delete_fail : bool ,
100+ ):
101+ """
102+ Get the list of document IDs to supersede based on the relatesTo field
103+ """
104+ if not resource .relatesTo :
105+ return []
106+
107+ logger .log (LogReference .PROCREATE006 , relatesTo = resource .relatesTo )
108108 ids_to_delete = []
109109
110- if result .resource .relatesTo :
111- logger .log (LogReference .PROCREATE006 , relatesTo = result .resource .relatesTo )
112-
113- for idx , relates_to in enumerate (result .resource .relatesTo ):
114- if not (identifier := getattr (relates_to .target .identifier , "value" , None )):
115- logger .log (LogReference .PROCREATE007a )
116- return SpineErrorResponse .BAD_REQUEST (
117- diagnostics = "No identifier value provided for relatesTo target" ,
118- expression = f"relatesTo[{ idx } ].target.identifier.value" ,
119- )
120-
121- producer_id = identifier .split ("-" , 1 )[0 ]
122- if metadata .ods_code_parts != tuple (producer_id .split ("|" )):
123- logger .log (
124- LogReference .PROCREATE007b ,
125- related_identifier = identifier ,
126- ods_code_parts = metadata .ods_code_parts ,
127- )
128- return SpineErrorResponse .BAD_REQUEST (
129- diagnostics = "The relatesTo target identifier value does not include the expected ODS code for this organisation" ,
130- expression = f"relatesTo[{ idx } ].target.identifier.value" ,
131- )
132-
133- if not (existing_pointer := repository .get_by_id (identifier )):
134- logger .log (LogReference .PROCREATE007c , related_identifier = identifier )
135- return SpineErrorResponse .BAD_REQUEST (
136- diagnostics = "The relatesTo target document does not exist" ,
137- expression = f"relatesTo[{ idx } ].target.identifier.value" ,
138- )
139-
140- if existing_pointer .nhs_number != core_model .nhs_number :
141- logger .log (LogReference .PROCREATE007d , related_identifier = identifier )
142- return SpineErrorResponse .BAD_REQUEST (
143- diagnostics = "The relatesTo target document NHS number does not match the NHS number in the request" ,
144- expression = f"relatesTo[{ idx } ].target.identifier.value" ,
145- )
146-
147- if existing_pointer .type != core_model .type :
148- logger .log (LogReference .PROCREATE007e , related_identifier = identifier )
149- return SpineErrorResponse .BAD_REQUEST (
150- diagnostics = "The relatesTo target document type does not match the type in the request" ,
151- expression = f"relatesTo[{ idx } ].target.identifier.value" ,
152- )
153-
154- if relates_to .code == "replaces" :
155- logger .log (
156- LogReference .PROCREATE008 ,
157- relates_to_code = relates_to .code ,
158- identifier = identifier ,
159- )
160- ids_to_delete .append (identifier )
161-
162- if ids_to_delete :
110+ for idx , relates_to in enumerate (resource .relatesTo ):
111+ identifier = _validate_identifier (relates_to , idx )
112+ _validate_producer_id (identifier , metadata , idx )
113+
114+ if not can_ignore_delete_fail :
115+ existing_pointer = _check_existing_pointer (identifier , repository , idx )
116+ _validate_pointer_details (existing_pointer , core_model , identifier , idx )
117+
118+ _append_id_if_replaces (relates_to , ids_to_delete , identifier )
119+
120+ return ids_to_delete
121+
122+
123+ def _validate_identifier (relates_to , idx ):
124+ """
125+ Validate that there is a identifier in relatesTo target
126+ """
127+ identifier = getattr (relates_to .target .identifier , "value" , None )
128+ if not identifier :
129+ logger .log (LogReference .PROCREATE007a )
130+ _raise_operation_outcome_error (
131+ "No identifier value provided for relatesTo target" , idx
132+ )
133+ return identifier
134+
135+
136+ def _validate_producer_id (identifier , metadata , idx ):
137+ """
138+ Validate that there is an ODS code in the relatesTo target identifier
139+ """
140+ producer_id = identifier .split ("-" , 1 )[0 ]
141+ if metadata .ods_code_parts != tuple (producer_id .split ("|" )):
142+ logger .log (
143+ LogReference .PROCREATE007b ,
144+ related_identifier = identifier ,
145+ ods_code_parts = metadata .ods_code_parts ,
146+ )
147+ _raise_operation_outcome_error (
148+ "The relatesTo target identifier value does not include the expected ODS code for this organisation" ,
149+ idx ,
150+ )
151+
152+
153+ def _check_existing_pointer (identifier , repository , idx ):
154+ """
155+ Check that there is an existing pointer that will be deleted when superseding
156+ """
157+ existing_pointer = repository .get_by_id (identifier )
158+ if not existing_pointer :
159+ logger .log (LogReference .PROCREATE007c , related_identifier = identifier )
160+ _raise_operation_outcome_error (
161+ "The relatesTo target document does not exist" , idx
162+ )
163+ return existing_pointer
164+
165+
166+ def _validate_pointer_details (existing_pointer , core_model , identifier , idx ):
167+ """
168+ Validate that the nhs numbers and type matches between the existing pointer and the requested one.
169+ """
170+ if existing_pointer .nhs_number != core_model .nhs_number :
171+ logger .log (LogReference .PROCREATE007d , related_identifier = identifier )
172+ _raise_operation_outcome_error (
173+ "The relatesTo target document NHS number does not match the NHS number in the request" ,
174+ idx ,
175+ )
176+
177+ if existing_pointer .type != core_model .type :
178+ logger .log (LogReference .PROCREATE007e , related_identifier = identifier )
179+ _raise_operation_outcome_error (
180+ "The relatesTo target document type does not match the type in the request" ,
181+ idx ,
182+ )
183+
184+
185+ def _append_id_if_replaces (relates_to , ids_to_delete , identifier ):
186+ """
187+ Append pointer ID if the if the relatesTo code is 'replaces'
188+ """
189+ if relates_to .code == "replaces" :
190+ logger .log (
191+ LogReference .PROCREATE008 ,
192+ relates_to_code = relates_to .code ,
193+ identifier = identifier ,
194+ )
195+ ids_to_delete .append (identifier )
196+
197+
198+ def _raise_operation_outcome_error (diagnostics , idx ):
199+ """
200+ General function to raise an operation outcome error
201+ """
202+ raise OperationOutcomeError (
203+ severity = "error" ,
204+ code = "invalid" ,
205+ details = SpineErrorConcept .from_code ("BAD_REQUEST" ),
206+ diagnostics = diagnostics ,
207+ expression = [f"relatesTo[{ idx } ].target.identifier.value" ],
208+ )
209+
210+
211+ @request_handler (body = DocumentReference )
212+ def handler (
213+ metadata : ConnectionMetadata ,
214+ repository : DocumentPointerRepository ,
215+ body : DocumentReference ,
216+ ) -> Response :
217+ """
218+ Creates a document reference.
219+
220+ Args:
221+ metadata (ConnectionMetadata): The connection metadata.
222+ repository (DocumentPointerRepository): The document pointer repository.
223+ body (DocumentReference): The document reference to create.
224+
225+ Returns:
226+ Response: The response indicating the result of the operation.
227+ """
228+ logger .log (LogReference .PROCREATE000 )
229+ logger .log (LogReference .PROCREATE001 , resource = body )
230+
231+ id_prefix = "|" .join (metadata .ods_code_parts )
232+ body .id = f"{ id_prefix } -{ uuid4 ()} "
233+
234+ validator = DocumentReferenceValidator ()
235+ result = validator .validate (body )
236+
237+ if not result .is_valid :
238+ logger .log (LogReference .PROCREATE002 )
239+ return Response .from_issues (issues = result .issues , statusCode = "400" )
240+
241+ core_model = _create_core_model (result .resource , metadata )
242+ if error_response := _check_permissions (core_model , metadata ):
243+ return error_response
244+
245+ can_ignore_delete_fail = (
246+ PERMISSION_SUPERSEDE_IGNORE_DELETE_FAIL in metadata .nrl_permissions
247+ )
248+
249+ if ids_to_delete := _get_document_ids_to_supersede (
250+ result .resource , core_model , metadata , repository , can_ignore_delete_fail
251+ ):
163252 logger .log (
164253 LogReference .PROCREATE010 ,
165254 pointer_id = result .resource .id ,
166255 ids_to_delete = ids_to_delete ,
167256 )
168- saved_model = repository .supersede (core_model , ids_to_delete )
257+ repository .supersede (core_model , ids_to_delete , can_ignore_delete_fail )
169258 logger .log (LogReference .PROCREATE999 )
170259 return NRLResponse .RESOURCE_SUPERSEDED (resource_id = result .resource .id )
171260
172261 logger .log (LogReference .PROCREATE009 , pointer_id = result .resource .id )
173- saved_model = repository .create (core_model )
262+ repository .create (core_model )
174263 logger .log (LogReference .PROCREATE999 )
175264 return NRLResponse .RESOURCE_CREATED (resource_id = result .resource .id )
0 commit comments