Skip to content

Commit 6c7c977

Browse files
authored
[NDR-290] Allow incoming FHIR to exclude type and author. (#938)
1 parent 95f1ad6 commit 6c7c977

8 files changed

+324
-55
lines changed

lambdas/models/document_reference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class DocumentReference(BaseModel):
9999
)
100100

101101
id: str = Field(..., alias=str(DocumentReferenceMetadataFields.ID.value))
102-
author: str = Field(default=None)
102+
author: str | None = None
103103
content_type: str = Field(default=DEFAULT_CONTENT_TYPE)
104104
created: str = Field(
105105
default_factory=lambda: datetime.now(timezone.utc).strftime(DATE_FORMAT)

lambdas/models/fhir/R4/fhir_document_reference.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,11 @@ class DocumentReference(BaseModel):
116116
"unknown",
117117
] = "final"
118118
status: Literal["current", "superseded", "entered-in-error"] = "current"
119-
type: Optional[CodeableConcept]
119+
type: Optional[CodeableConcept] = None
120120
category: Optional[List[CodeableConcept]] = None
121121
subject: Optional[Reference]
122122
date: Optional[str] = None
123-
author: Optional[List[Reference]]
123+
author: Optional[List[Reference]] = None
124124
authenticator: Optional[Reference] = None
125125
custodian: Optional[Reference] = None
126126
content: List[DocumentReferenceContent]

lambdas/services/post_fhir_document_reference_service.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ def process_fhir_document_reference(
4343
fhir_document
4444
)
4545

46-
# Extract NHS number from the FHIR document
46+
# Extract NHS number and author from the FHIR document
4747
try:
4848
nhs_number = validated_fhir_doc.extract_nhs_number_from_fhir()
49+
author = self._extract_author_from_fhir(validated_fhir_doc)
4950
except FhirDocumentReferenceException:
5051
raise DocumentRefException(400, LambdaError.DocRefNoParse)
5152

@@ -63,6 +64,7 @@ def process_fhir_document_reference(
6364
# Create a document reference model
6465
document_reference = self._create_document_reference(
6566
nhs_number,
67+
author,
6668
doc_type,
6769
validated_fhir_doc,
6870
patient_details.general_practice_ods,
@@ -82,6 +84,23 @@ def process_fhir_document_reference(
8284
logger.error(f"AWS client error: {str(e)}")
8385
raise DocumentRefException(500, LambdaError.InternalServerError)
8486

87+
def _extract_author_from_fhir(self, fhir_doc: FhirDocumentReference) -> str | None:
88+
authors = getattr(fhir_doc, "author", None)
89+
90+
if not authors:
91+
return None
92+
93+
try:
94+
identifier = authors[0].identifier
95+
if not identifier or identifier.value is None:
96+
logger.error("FHIR document validation error: author identifier value")
97+
raise DocumentRefException(400, LambdaError.DocRefNoParse)
98+
99+
return identifier.value
100+
except (AttributeError, IndexError) as e:
101+
logger.error(f"FHIR document validation error: {str(e)}")
102+
raise DocumentRefException(400, LambdaError.DocRefNoParse)
103+
85104
def _determine_document_type(
86105
self, fhir_doc: FhirDocumentReference, common_name: MtlsCommonNames | None
87106
) -> SnomedCode:
@@ -109,6 +128,7 @@ def _determine_document_type(
109128
def _create_document_reference(
110129
self,
111130
nhs_number: str,
131+
author: str | None,
112132
doc_type: SnomedCode,
113133
fhir_doc: FhirDocumentReference,
114134
current_gp_ods: str,
@@ -133,7 +153,7 @@ def _create_document_reference(
133153
current_gp_ods=current_gp_ods,
134154
custodian=custodian,
135155
s3_bucket_name=self.staging_bucket_name,
136-
author=fhir_doc.author[0].identifier.value,
156+
author=author,
137157
content_type=fhir_doc.content[0].attachment.contentType,
138158
file_name=fhir_doc.content[0].attachment.title,
139159
document_snomed_code_type=doc_type.code,

lambdas/tests/e2e/api/fhir/test_upload_document_fhir_api.py renamed to lambdas/tests/e2e/api/fhir/test_upload_document_fhir_api_failure.py

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import json
23
import logging
34
import os
45

@@ -36,44 +37,6 @@ def retrieve_document_with_retry(doc_id, condition):
3637
return fetch_with_retry_mtls(session, retrieve_url, headers, condition)
3738

3839

39-
def test_create_document_base64(test_data):
40-
record = {
41-
"ods": "H81109",
42-
"nhs_number": "9912003071",
43-
}
44-
45-
sample_pdf_path = os.path.join(os.path.dirname(__file__), "files", "dummy.pdf")
46-
with open(sample_pdf_path, "rb") as f:
47-
record["data"] = base64.b64encode(f.read()).decode("utf-8")
48-
payload = pdm_data_helper.create_upload_payload(record)
49-
50-
raw_upload_response = upload_document(payload)
51-
assert raw_upload_response.status_code == 200
52-
record["id"] = raw_upload_response.json()["id"].split("~")[1]
53-
test_data.append(record)
54-
55-
# Validate attachment URL
56-
upload_response = raw_upload_response.json()
57-
attachment_url = upload_response["content"][0]["attachment"]["url"]
58-
assert (
59-
f"https://{APIM_ENDPOINT}/national-document-repository/FHIR/R4/DocumentReference/{PDM_SNOMED}~"
60-
in attachment_url
61-
)
62-
63-
def condition(response_json):
64-
logging.info(response_json)
65-
return response_json["content"][0]["attachment"].get("data", False)
66-
67-
raw_retrieve_response = retrieve_document_with_retry(
68-
upload_response["id"], condition
69-
)
70-
retrieve_response = raw_retrieve_response.json()
71-
72-
# Validate base64 data
73-
base64_data = retrieve_response["content"][0]["attachment"]["data"]
74-
assert base64.b64decode(base64_data, validate=True)
75-
76-
7740
def test_create_document_presign_fails():
7841
record = {
7942
"ods": "H81109",
@@ -187,7 +150,11 @@ def test_forbidden_with_invalid_cert(temp_cert_and_key):
187150
assert body["message"] == "Forbidden"
188151

189152

190-
def test_create_document_saves_raw(test_data):
153+
@pytest.mark.parametrize(
154+
"author_payload",
155+
[({}), {"identifier": {"system": "https://fhir.nhs.uk/Id/ods-organization-code"}}],
156+
)
157+
def test_create_document_with_invalid_author_returns_error(test_data, author_payload):
191158
record = {
192159
"ods": "H81109",
193160
"nhs_number": "9912003071",
@@ -196,15 +163,14 @@ def test_create_document_saves_raw(test_data):
196163
sample_pdf_path = os.path.join(os.path.dirname(__file__), "files", "dummy.pdf")
197164
with open(sample_pdf_path, "rb") as f:
198165
record["data"] = base64.b64encode(f.read()).decode("utf-8")
199-
payload = pdm_data_helper.create_upload_payload(record)
166+
payload = pdm_data_helper.create_upload_payload(record=record, return_json=True)
167+
payload["author"][0] = author_payload
168+
payload = json.dumps(payload)
200169

201170
raw_upload_response = upload_document(payload)
202-
assert raw_upload_response.status_code == 200
203-
record["id"] = raw_upload_response.json()["id"].split("~")[1]
204-
test_data.append(record)
205-
206-
doc_ref = pdm_data_helper.retrieve_document_reference(record=record)
207-
assert "Item" in doc_ref
208-
assert "RawRequest" in doc_ref["Item"]
209-
assert doc_ref["Item"]["RawRequest"]
210-
assert doc_ref["Item"]["RawRequest"] == payload
171+
response_json = raw_upload_response.json()
172+
assert raw_upload_response.status_code == 400
173+
assert response_json["resourceType"] == "OperationOutcome"
174+
assert (
175+
response_json["issue"][0]["details"]["coding"][0]["code"] == "VALIDATION_ERROR"
176+
)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import base64
2+
import logging
3+
import os
4+
5+
import pytest
6+
import requests
7+
8+
from lambdas.tests.e2e.api.fhir.conftest import (
9+
MTLS_ENDPOINT,
10+
create_mtls_session,
11+
fetch_with_retry_mtls,
12+
)
13+
from lambdas.tests.e2e.conftest import APIM_ENDPOINT, PDM_SNOMED
14+
from lambdas.tests.e2e.helpers.data_helper import PdmDataHelper
15+
16+
pdm_data_helper = PdmDataHelper()
17+
18+
19+
def upload_document(payload):
20+
"""Helper to upload DocumentReference."""
21+
url = f"https://{MTLS_ENDPOINT}/DocumentReference"
22+
headers = {
23+
"X-Correlation-Id": "1234",
24+
}
25+
session = create_mtls_session()
26+
return session.post(url, headers=headers, data=payload)
27+
28+
29+
def retrieve_document_with_retry(doc_id, condition):
30+
"""Poll until condition is met on DocumentReference retrieval."""
31+
retrieve_url = f"https://{MTLS_ENDPOINT}/DocumentReference/{doc_id}"
32+
headers = {
33+
"X-Correlation-Id": "1234",
34+
}
35+
session = create_mtls_session()
36+
return fetch_with_retry_mtls(session, retrieve_url, headers, condition)
37+
38+
39+
def test_create_document_base64(test_data):
40+
record = {
41+
"ods": "H81109",
42+
"nhs_number": "9912003071",
43+
}
44+
45+
sample_pdf_path = os.path.join(os.path.dirname(__file__), "files", "dummy.pdf")
46+
with open(sample_pdf_path, "rb") as f:
47+
record["data"] = base64.b64encode(f.read()).decode("utf-8")
48+
payload = pdm_data_helper.create_upload_payload(record)
49+
50+
raw_upload_response = upload_document(payload)
51+
assert raw_upload_response.status_code == 200
52+
record["id"] = raw_upload_response.json()["id"].split("~")[1]
53+
test_data.append(record)
54+
55+
# Validate attachment URL
56+
upload_response = raw_upload_response.json()
57+
attachment_url = upload_response["content"][0]["attachment"]["url"]
58+
assert (
59+
f"https://{APIM_ENDPOINT}/national-document-repository/FHIR/R4/DocumentReference/{PDM_SNOMED}~"
60+
in attachment_url
61+
)
62+
63+
def condition(response_json):
64+
logging.info(response_json)
65+
return response_json["content"][0]["attachment"].get("data", False)
66+
67+
raw_retrieve_response = retrieve_document_with_retry(
68+
upload_response["id"], condition
69+
)
70+
retrieve_response = raw_retrieve_response.json()
71+
72+
# Validate base64 data
73+
base64_data = retrieve_response["content"][0]["attachment"]["data"]
74+
assert base64.b64decode(base64_data, validate=True)
75+
76+
77+
def test_create_document_saves_raw(test_data):
78+
record = {
79+
"ods": "H81109",
80+
"nhs_number": "9912003071",
81+
}
82+
83+
sample_pdf_path = os.path.join(os.path.dirname(__file__), "files", "dummy.pdf")
84+
with open(sample_pdf_path, "rb") as f:
85+
record["data"] = base64.b64encode(f.read()).decode("utf-8")
86+
payload = pdm_data_helper.create_upload_payload(record)
87+
88+
raw_upload_response = upload_document(payload)
89+
assert raw_upload_response.status_code == 200
90+
record["id"] = raw_upload_response.json()["id"].split("~")[1]
91+
test_data.append(record)
92+
93+
doc_ref = pdm_data_helper.retrieve_document_reference(record=record)
94+
assert "Item" in doc_ref
95+
assert "RawRequest" in doc_ref["Item"]
96+
assert "Author" in doc_ref["Item"]
97+
assert doc_ref["Item"]["RawRequest"]
98+
assert doc_ref["Item"]["RawRequest"] == payload
99+
100+
101+
def test_create_document_without_author_or_type(test_data):
102+
record = {
103+
"ods": "H81109",
104+
"nhs_number": "9912003071",
105+
}
106+
107+
sample_pdf_path = os.path.join(os.path.dirname(__file__), "files", "dummy.pdf")
108+
with open(sample_pdf_path, "rb") as f:
109+
record["data"] = base64.b64encode(f.read()).decode("utf-8")
110+
payload = pdm_data_helper.create_upload_payload(
111+
record=record, exclude=["type", "author"]
112+
)
113+
114+
for field in ["type", "author"]:
115+
assert field not in payload
116+
raw_upload_response = upload_document(payload)
117+
assert raw_upload_response.status_code == 200
118+
record["id"] = raw_upload_response.json()["id"].split("~")[1]
119+
test_data.append(record)
120+
121+
doc_ref = pdm_data_helper.retrieve_document_reference(record=record)
122+
assert "Item" in doc_ref
123+
assert "RawRequest" in doc_ref["Item"]
124+
assert "Author" not in doc_ref["Item"]
125+
assert doc_ref["Item"]["RawRequest"]
126+
assert doc_ref["Item"]["RawRequest"] == payload
127+
for field in ["type", "author"]:
128+
assert field not in doc_ref["Item"]["RawRequest"]

lambdas/tests/e2e/helpers/data_helper.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def retrieve_document_reference(self, record):
9393
table_name=self.dynamo_table, key={"ID": record["id"]}
9494
)
9595

96-
def create_upload_payload(self, record):
96+
def create_upload_payload(self, record, exclude=[], return_json=False):
9797
"""Helper to build DocumentReference payload."""
9898
payload = {
9999
"resourceType": "DocumentReference",
@@ -137,8 +137,16 @@ def create_upload_payload(self, record):
137137
}
138138
],
139139
}
140+
141+
for field in exclude:
142+
payload.pop(field, None)
143+
140144
if "data" in record:
141145
payload["content"][0]["attachment"]["data"] = record["data"]
146+
147+
if return_json:
148+
return payload
149+
142150
return json.dumps(payload)
143151

144152
def tidyup(self, record):

0 commit comments

Comments
 (0)