Skip to content

Commit e20271a

Browse files
committed
Merge branch 'develop' of github.com:NHSDigital/NRLF into feature/eema1-NRL-738-AllowSyncFullPointerAccess
2 parents dac4341 + df992c3 commit e20271a

File tree

26 files changed

+772
-419
lines changed

26 files changed

+772
-419
lines changed

api/consumer/readDocumentReference/tests/test_read_document_reference_consumer.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from moto import mock_aws
44

55
from api.consumer.readDocumentReference.read_document_reference import handler
6+
from nrlf.core.constants import PointerTypes
67
from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository
78
from nrlf.tests.data import load_document_reference
89
from nrlf.tests.dynamodb import mock_repository
@@ -109,9 +110,7 @@ def test_read_document_reference_unauthorised_for_type(
109110
repository.create(doc_pointer)
110111

111112
event = create_test_api_gateway_event(
112-
headers=create_headers(
113-
pointer_types=["http://snomed.info/sct|887701000000100"]
114-
),
113+
headers=create_headers(pointer_types=[PointerTypes.EMERGENCY_HEALTHCARE_PLAN]),
115114
path_parameters={"id": doc_pointer.id},
116115
)
117116

@@ -156,7 +155,7 @@ def test_document_reference_invalid_json(repository: DocumentPointerRepository):
156155
repository.create(doc_pointer)
157156

158157
event = create_test_api_gateway_event(
159-
headers=create_headers(pointer_types=["http://snomed.info/sct|736253002"]),
158+
headers=create_headers(pointer_types=[PointerTypes.MENTAL_HEALTH_PLAN]),
160159
path_parameters={"id": doc_pointer.id},
161160
)
162161

Lines changed: 176 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from 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+
)
48
from nrlf.core.decorators import request_handler
59
from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository
10+
from nrlf.core.errors import OperationOutcomeError
611
from nrlf.core.logger import LogReference, logger
712
from nrlf.core.model import ConnectionMetadata
813
from 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

Comments
 (0)