Skip to content

Commit 28629a0

Browse files
committed
[NRL-1051] Allow non-conformant docref in a Bundle resource
1 parent 73fb108 commit 28629a0

File tree

6 files changed

+150
-22
lines changed

6 files changed

+150
-22
lines changed

api/producer/processTransaction/process_transaction_bundle.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Any, Dict
12
from uuid import uuid4
23

34
from nrlf.core.codes import SpineErrorConcept
@@ -25,6 +26,24 @@
2526
OperationOutcomeIssue,
2627
)
2728

29+
# TODO - Figure out sensible defaults
30+
# NOTE: while type, category and custodian are not required in MHDS profile, they will be required by NRLF
31+
DEFAULT_MHDS_AUTHOR = {
32+
"identifier": {
33+
"value": "X26",
34+
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
35+
}
36+
}
37+
DEFAULT_MHDS_PRACTICE_SETTING_CODING = {
38+
"system": "http://snomed.info/sct",
39+
"code": "394802000",
40+
"display": "General medical practice",
41+
}
42+
DEFAULT_MHDS_PROPERTIES: dict[str, Any] = {
43+
"author": [DEFAULT_MHDS_AUTHOR],
44+
"context": {"practiceSetting": {"coding": [DEFAULT_MHDS_PRACTICE_SETTING_CODING]}},
45+
}
46+
2847

2948
def _set_create_time_fields(
3049
create_time: str, document_reference: DocumentReference, nrl_permissions: list[str]
@@ -268,13 +287,27 @@ def create_document_reference(
268287

269288

270289
def _convert_document_reference(
271-
document_reference: DocumentReference, requested_profile: str
290+
raw_resource: Dict[str, Any], requested_profile: str
272291
) -> DocumentReference:
273292
"""
274293
Convert the DocumentReference to the requested profile
275294
"""
276-
# TODO - Implement conversion logic from MHDS profile to NRLF FHIR profile
277-
return document_reference
295+
if requested_profile.endswith(
296+
"profiles.ihe.net/ITI/MHD/StructureDefinition/IHE.MHD.UnContained.Comprehensive.ProvideBundle"
297+
):
298+
docref_properties: dict[str, Any] = {}
299+
docref_properties.update(DEFAULT_MHDS_PROPERTIES)
300+
docref_properties.update(raw_resource)
301+
docref_properties["status"] = "current"
302+
return DocumentReference(**docref_properties)
303+
304+
raise OperationOutcomeError(
305+
severity="error",
306+
code="exception",
307+
diagnostics="Unable to parse DocumentReference. Only IHE.MHD.UnContained.Comprehensive.ProvideBundle profile is supported",
308+
expression=["meta.profile[0]"],
309+
details=SpineErrorConcept.from_code("BAD_REQUEST"),
310+
)
278311

279312

280313
@request_handler(body=Bundle)
@@ -322,12 +355,11 @@ def handler(
322355
resource=Bundle(resourceType="Bundle", type="transaction-response")
323356
)
324357

325-
document_references: list[DocumentReference] = []
326-
358+
entries: list[BundleEntry] = []
327359
issues: list[BaseModel] = []
328360

329361
for entry in body.entry:
330-
if not entry.resource or entry.resource.resourceType != "DocumentReference":
362+
if not entry.resource or entry.resource["resourceType"] != "DocumentReference":
331363
issues.append(
332364
OperationOutcomeIssue(
333365
severity="error",
@@ -349,18 +381,29 @@ def handler(
349381
)
350382
)
351383

352-
document_references.append(DocumentReference.model_validate(entry.resource))
384+
entries.append(entry)
353385

354386
if issues:
355387
return Response.from_issues(issues, statusCode="400")
356388

357389
responses: list[Response] = []
358-
for document_reference in document_references:
390+
for entry in entries:
359391
try:
392+
if not entry.resource:
393+
raise OperationOutcomeError(
394+
severity="error",
395+
code="exception",
396+
diagnostics="No resource provided",
397+
expression=["entry.resource"],
398+
details=SpineErrorConcept.from_code("BAD_REQUEST"),
399+
)
400+
360401
if requested_profile:
361402
document_reference = _convert_document_reference(
362-
document_reference, requested_profile
403+
entry.resource, requested_profile
363404
)
405+
else:
406+
document_reference = DocumentReference(**(entry.resource))
364407

365408
create_response = create_document_reference(
366409
metadata, repository, document_reference

api/producer/processTransaction/tests/test_process_transaction_bundle.py

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
from freezegun import freeze_time
55
from moto import mock_aws
66

7-
from api.producer.processTransaction.process_transaction_bundle import handler
7+
from api.producer.processTransaction.process_transaction_bundle import (
8+
DEFAULT_MHDS_PROPERTIES,
9+
handler,
10+
)
811
from nrlf.core.dynamodb.repository import DocumentPointerRepository
912
from nrlf.producer.fhir.r4.model import (
1013
Bundle,
1114
BundleEntry,
1215
BundleEntryRequest,
13-
DocumentReference,
16+
Meta,
17+
ProfileItem,
1418
)
1519
from nrlf.tests.data import load_document_reference
1620
from nrlf.tests.dynamodb import mock_repository
@@ -26,10 +30,12 @@
2630
@mock_repository
2731
@freeze_time("2024-03-21T12:34:56.789")
2832
@freeze_uuid("00000000-0000-0000-0000-000000000001")
29-
def test_create_single_document_reference_with_transaction_happy_path(
33+
def test_create_single_nrl_document_reference_with_transaction_happy_path(
3034
repository: DocumentPointerRepository,
3135
):
32-
doc_ref: DocumentReference = load_document_reference("Y05868-736253002-Valid")
36+
doc_ref = load_document_reference("Y05868-736253002-Valid").model_dump(
37+
exclude_none=True
38+
)
3339

3440
request_bundle = Bundle(
3541
entry=[
@@ -79,7 +85,86 @@ def test_create_single_document_reference_with_transaction_happy_path(
7985
assert created_doc_pointer.created_on == "2024-03-21T12:34:56.789Z"
8086
assert created_doc_pointer.updated_on is None
8187
assert json.loads(created_doc_pointer.document) == {
82-
**(doc_ref.model_dump(exclude_none=True)),
88+
**doc_ref,
89+
"meta": {
90+
"lastUpdated": "2024-03-21T12:34:56.789Z",
91+
},
92+
"date": "2024-03-21T12:34:56.789Z",
93+
"id": "Y05868-00000000-0000-0000-0000-000000000001",
94+
}
95+
96+
97+
@mock_aws
98+
@mock_repository
99+
@freeze_time("2024-03-21T12:34:56.789")
100+
@freeze_uuid("00000000-0000-0000-0000-000000000001")
101+
def test_create_single_mhds_document_reference_with_transaction_happy_path(
102+
repository: DocumentPointerRepository,
103+
):
104+
raw_doc_ref = load_document_reference("Y05868-736253002-Valid").model_dump(
105+
exclude_none=True
106+
)
107+
108+
raw_doc_ref.pop("author")
109+
raw_doc_ref.pop("context")
110+
111+
request_bundle = Bundle(
112+
meta=Meta(
113+
profile=[
114+
ProfileItem(
115+
"http://hl7.org/fhir/profiles.ihe.net/ITI/MHD/StructureDefinition/IHE.MHD.UnContained.Comprehensive.ProvideBundle"
116+
)
117+
]
118+
),
119+
entry=[
120+
BundleEntry(
121+
resource=raw_doc_ref, request=BundleEntryRequest(url="/", method="POST")
122+
)
123+
],
124+
resourceType="Bundle",
125+
type="transaction",
126+
)
127+
128+
event = create_test_api_gateway_event(
129+
headers=create_headers(),
130+
body=request_bundle.model_dump_json(),
131+
)
132+
133+
result = handler(event, create_mock_context())
134+
body = result.pop("body")
135+
136+
assert result == {
137+
"statusCode": "200",
138+
"headers": {
139+
**default_response_headers(),
140+
},
141+
"isBase64Encoded": False,
142+
}
143+
144+
parsed_body = json.loads(body)
145+
assert parsed_body == {
146+
"resourceType": "Bundle",
147+
"type": "transaction-response",
148+
"entry": [
149+
{
150+
"response": {
151+
"status": "201",
152+
"location": "/producer/FHIR/R4/DocumentReference/Y05868-00000000-0000-0000-0000-000000000001",
153+
},
154+
},
155+
],
156+
}
157+
158+
created_doc_pointer = repository.get_by_id(
159+
"Y05868-00000000-0000-0000-0000-000000000001"
160+
)
161+
162+
assert created_doc_pointer is not None
163+
assert created_doc_pointer.created_on == "2024-03-21T12:34:56.789Z"
164+
assert created_doc_pointer.updated_on is None
165+
assert json.loads(created_doc_pointer.document) == {
166+
**raw_doc_ref,
167+
**DEFAULT_MHDS_PROPERTIES,
83168
"meta": {
84169
"lastUpdated": "2024-03-21T12:34:56.789Z",
85170
},

api/producer/swagger.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1505,7 +1505,7 @@ components:
15051505
pattern: \S*
15061506
description: "The Absolute URL for the resource. The fullUrl SHALL NOT disagree with the id in the resource – i.e. if the fullUrl is not a urn:uuid, the URL shall be version–independent URL consistent with the Resource.id. The fullUrl is a version independent reference to the resource. The fullUrl element SHALL have a value except that: \n* fullUrl can be empty on a POST (although it does not need to when specifying a temporary id for reference in the bundle)\n* Results from operations might involve resources that are not identified."
15071507
resource:
1508-
$ref: "#/components/schemas/DocumentReference"
1508+
type: object
15091509
description: The Resource for the entry. The purpose/meaning of the resource is determined by the Bundle.type.
15101510
search:
15111511
$ref: "#/components/schemas/BundleEntrySearch"

layer/nrlf/consumer/fhir/r4/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: swagger.yaml
3-
# timestamp: 2024-11-20T09:43:58+00:00
3+
# timestamp: 2024-12-02T21:57:23+00:00
44

55
from __future__ import annotations
66

layer/nrlf/producer/fhir/r4/model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# generated by datamodel-codegen:
22
# filename: swagger.yaml
3-
# timestamp: 2024-11-20T10:10:52+00:00
3+
# timestamp: 2024-12-02T21:57:21+00:00
44

55
from __future__ import annotations
66

7-
from typing import Annotated, List, Literal, Optional
7+
from typing import Annotated, Any, Dict, List, Literal, Optional
88

99
from pydantic import BaseModel, ConfigDict, Field, RootModel
1010

@@ -698,7 +698,7 @@ class BundleEntry(BaseModel):
698698
),
699699
] = None
700700
resource: Annotated[
701-
Optional[DocumentReference],
701+
Optional[Dict[str, Any]],
702702
Field(
703703
description="The Resource for the entry. The purpose/meaning of the resource is determined by the Bundle.type."
704704
),

layer/nrlf/producer/fhir/r4/strict_model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# generated by datamodel-codegen:
22
# filename: swagger.yaml
3-
# timestamp: 2024-11-20T10:10:54+00:00
3+
# timestamp: 2024-12-02T21:57:22+00:00
44

55
from __future__ import annotations
66

7-
from typing import Annotated, List, Literal, Optional
7+
from typing import Annotated, Any, Dict, List, Literal, Optional
88

99
from pydantic import (
1010
BaseModel,
@@ -609,7 +609,7 @@ class BundleEntry(BaseModel):
609609
),
610610
] = None
611611
resource: Annotated[
612-
Optional[DocumentReference],
612+
Optional[Dict[str, Any]],
613613
Field(
614614
description="The Resource for the entry. The purpose/meaning of the resource is determined by the Bundle.type."
615615
),

0 commit comments

Comments
 (0)