Skip to content

Commit 7ef5988

Browse files
committed
feat(etl): FTRS-1992 Rebased
2 parents b381842 + ea2fca7 commit 7ef5988

File tree

17 files changed

+485
-140
lines changed

17 files changed

+485
-140
lines changed
Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
from fhir.resources.R4B.operationoutcome import OperationOutcome, OperationOutcomeIssue
22
from pydantic import ValidationError
33

4+
OPERATION_OUTCOME_SYSTEM = "http://terminology.hl7.org/CodeSystem/operation-outcome"
5+
6+
FHIR_OPERATION_OUTCOME_CODES: dict[str, tuple[str, str]] = {
7+
"invalid": ("MSG_PARAM_INVALID", "Parameter content is invalid"),
8+
"not-found": ("MSG_NO_EXIST", "Resource does not exist"),
9+
"exception": ("MSG_ERROR_PARSING", "Error processing request"),
10+
"structure": ("MSG_BAD_SYNTAX", "Bad Syntax"),
11+
"required": ("MSG_RESOURCE_REQUIRED", "A resource is required"),
12+
"value": ("MSG_PARAM_INVALID", "Parameter content is invalid"),
13+
"processing": ("MSG_ERROR_PARSING", "Error processing request"),
14+
"duplicate": ("MSG_DUPLICATE_ID", "Duplicate Id for resource"),
15+
"informational": ("MSG_UPDATED", "Existing resource updated"),
16+
"success": ("MSG_UPDATED", "Existing resource updated"),
17+
}
18+
419

520
class OperationOutcomeException(Exception):
621
def __init__(self, outcome: dict) -> None:
@@ -18,21 +33,47 @@ class OperationOutcomeHandler:
1833
"""
1934

2035
@staticmethod
21-
def build(
36+
def _build_details(code: str, text: str) -> dict:
37+
fhir_code, display = FHIR_OPERATION_OUTCOME_CODES.get(
38+
code, ("MSG_ERROR_PARSING", "Error processing request")
39+
)
40+
return {
41+
"coding": [
42+
{
43+
"system": OPERATION_OUTCOME_SYSTEM,
44+
"code": fhir_code,
45+
"display": display,
46+
}
47+
],
48+
"text": text,
49+
}
50+
51+
@staticmethod
52+
def build( # noqa: PLR0913
2253
diagnostics: str,
2354
code: str = "invalid",
2455
severity: str = "error",
56+
details_text: str | None = None,
2557
details: dict | None = None,
58+
expression: list[str] | None = None,
2659
issues: list | None = None,
2760
) -> dict:
2861
if issues is None:
29-
issue_dict = {
62+
if details is None:
63+
details = OperationOutcomeHandler._build_details(
64+
code, details_text or diagnostics
65+
)
66+
67+
issue_dict: dict = {
3068
"severity": severity,
3169
"code": code,
70+
"details": details,
3271
"diagnostics": diagnostics,
3372
}
34-
if details:
35-
issue_dict["details"] = details
73+
74+
if expression:
75+
issue_dict["expression"] = expression
76+
3677
issues = [issue_dict]
3778

3879
fhir_issues = [OperationOutcomeIssue(**issue) for issue in issues]
@@ -48,42 +89,20 @@ def from_exception(
4889
"""
4990
Build an OperationOutcome from an exception.
5091
"""
51-
details = {
52-
"coding": [
53-
{
54-
"system": "http://terminology.hl7.org/CodeSystem/operation-outcome",
55-
"code": "exception",
56-
"display": "Exception",
57-
}
58-
],
59-
"text": f"An unexpected error occurred: {str(exc)}",
60-
}
61-
6292
return OperationOutcomeHandler.build(
6393
diagnostics=str(exc),
6494
code=code,
6595
severity=severity,
66-
details=details,
96+
details_text=f"An unexpected error occurred: {exc}",
6797
)
6898

6999
@staticmethod
70100
def from_validation_error(
71101
e: ValidationError,
72102
) -> dict:
73-
details = {
74-
"coding": [
75-
{
76-
"system": "http://terminology.hl7.org/CodeSystem/operation-outcome",
77-
"code": "invalid",
78-
"display": "Invalid Resource",
79-
}
80-
],
81-
"text": str(e),
82-
}
83-
84103
return OperationOutcomeHandler.build(
85104
diagnostics="Validation failed for resource.",
86105
code="invalid",
87106
severity="error",
88-
details=details,
107+
details_text=str(e),
89108
)

application/packages/python/ftrs_common/fhir/r4b/organisation_mapper.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ class OrganizationMapper(FhirMapper):
3030
# --- FHIR Builders ---
3131
def _build_meta_profile(self) -> dict:
3232
return {
33-
"profile": ["https://fhir.nhs.uk/StructureDefinition/UKCore-Organization"]
33+
"profile": [
34+
"https://fhir.hl7.org.uk/StructureDefinition/UKCore-Organization"
35+
]
3436
}
3537

3638
def _build_identifier(self, ods_code: str) -> list[Identifier]:

application/packages/python/ftrs_common/tests/fhir/test_operation_outcome.py

Lines changed: 130 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from ftrs_common.fhir.operation_outcome import (
2+
FHIR_OPERATION_OUTCOME_CODES,
3+
OPERATION_OUTCOME_SYSTEM,
24
OperationOutcomeException,
35
OperationOutcomeHandler,
46
)
@@ -25,6 +27,18 @@ def test_operation_outcome_handler_build_basic() -> None:
2527
assert outcome["issue"][0]["diagnostics"] == diagnostics
2628
assert outcome["issue"][0]["code"] == "invalid"
2729
assert outcome["issue"][0]["severity"] == "error"
30+
assert "details" in outcome["issue"][0]
31+
assert "coding" in outcome["issue"][0]["details"]
32+
assert (
33+
outcome["issue"][0]["details"]["coding"][0]["system"]
34+
== OPERATION_OUTCOME_SYSTEM
35+
)
36+
assert outcome["issue"][0]["details"]["coding"][0]["code"] == "MSG_PARAM_INVALID"
37+
assert (
38+
outcome["issue"][0]["details"]["coding"][0]["display"]
39+
== "Parameter content is invalid"
40+
)
41+
assert outcome["issue"][0]["details"]["text"] == diagnostics
2842

2943

3044
def test_operation_outcome_handler_build_with_details_and_issues() -> None:
@@ -36,16 +50,96 @@ def test_operation_outcome_handler_build_with_details_and_issues() -> None:
3650
assert outcome["issue"][0]["diagnostics"] == "Warn"
3751

3852

53+
def test_operation_outcome_handler_build_with_custom_code() -> None:
54+
diagnostics: str = "Resource not found"
55+
outcome = OperationOutcomeHandler.build(
56+
diagnostics, code="not-found", severity="error"
57+
)
58+
assert outcome["issue"][0]["code"] == "not-found"
59+
assert outcome["issue"][0]["details"]["coding"][0]["code"] == "MSG_NO_EXIST"
60+
assert (
61+
outcome["issue"][0]["details"]["coding"][0]["display"]
62+
== "Resource does not exist"
63+
)
64+
65+
66+
def test_operation_outcome_handler_build_with_unknown_code() -> None:
67+
diagnostics: str = "Unknown error"
68+
outcome = OperationOutcomeHandler.build(diagnostics, code="unknown-code")
69+
assert outcome["issue"][0]["details"]["coding"][0]["code"] == "MSG_ERROR_PARSING"
70+
assert (
71+
outcome["issue"][0]["details"]["coding"][0]["display"]
72+
== "Error processing request"
73+
)
74+
75+
76+
def test_operation_outcome_handler_build_with_details_text() -> None:
77+
diagnostics: str = "Detailed diagnostics"
78+
details_text: str = "Human readable text"
79+
outcome = OperationOutcomeHandler.build(diagnostics, details_text=details_text)
80+
assert outcome["issue"][0]["diagnostics"] == diagnostics
81+
assert outcome["issue"][0]["details"]["text"] == details_text
82+
83+
84+
def test_operation_outcome_handler_build_with_expression() -> None:
85+
diagnostics: str = "Invalid field"
86+
expression: list[str] = ["Organization.identifier[0].value"]
87+
outcome = OperationOutcomeHandler.build(diagnostics, expression=expression)
88+
assert outcome["issue"][0]["expression"] == expression
89+
90+
91+
def test_operation_outcome_handler_build_with_custom_details() -> None:
92+
diagnostics: str = "Test diagnostics"
93+
custom_details: dict = {
94+
"coding": [
95+
{
96+
"system": "https://custom.system",
97+
"code": "CUSTOM_CODE",
98+
"display": "Custom Display",
99+
}
100+
],
101+
"text": "Custom text",
102+
}
103+
outcome = OperationOutcomeHandler.build(diagnostics, details=custom_details)
104+
assert outcome["issue"][0]["details"] == custom_details
105+
106+
107+
def test_operation_outcome_handler_build_with_issues() -> None:
108+
diagnostics: str = "Test diagnostics"
109+
details: dict = {"text": "More info"}
110+
issues: list = [
111+
{
112+
"severity": "warning",
113+
"code": "processing",
114+
"diagnostics": "Warn",
115+
"details": {"text": "Warning details"},
116+
}
117+
]
118+
outcome = OperationOutcomeHandler.build(diagnostics, details=details, issues=issues)
119+
assert outcome["issue"][0]["severity"] == "warning"
120+
assert outcome["issue"][0]["diagnostics"] == "Warn"
121+
122+
39123
def test_operation_outcome_handler_from_exception() -> None:
40124
exc = Exception("Boom!")
41125
outcome = OperationOutcomeHandler.from_exception(exc)
42126
assert outcome["issue"][0]["diagnostics"] == "Boom!"
43127
assert outcome["issue"][0]["code"] == "exception"
44128
assert outcome["issue"][0]["severity"] == "fatal"
129+
assert (
130+
outcome["issue"][0]["details"]["coding"][0]["system"]
131+
== OPERATION_OUTCOME_SYSTEM
132+
)
133+
assert outcome["issue"][0]["details"]["coding"][0]["code"] == "MSG_ERROR_PARSING"
134+
assert (
135+
outcome["issue"][0]["details"]["coding"][0]["display"]
136+
== "Error processing request"
137+
)
138+
assert "An unexpected error occurred" in outcome["issue"][0]["details"]["text"]
45139

46140

47-
def test_operation_outcome_handler_from_validation_error_default_diagnostics() -> None:
48-
dummy_model_path = "tests.fhir.test_operation_outcome.DummyModel"
141+
def test_operation_outcome_handler_from_validation_error() -> None:
142+
dummy_model_path: str = "tests.fhir.test_operation_outcome.DummyModel"
49143

50144
error = ValidationError.from_exception_data(
51145
dummy_model_path,
@@ -63,26 +157,39 @@ def test_operation_outcome_handler_from_validation_error_default_diagnostics() -
63157
assert outcome["issue"][0]["diagnostics"] == "Validation failed for resource."
64158
assert outcome["issue"][0]["code"] == "invalid"
65159
assert outcome["issue"][0]["severity"] == "error"
66-
assert "Invalid Resource" in outcome["issue"][0]["details"]["coding"][0]["display"]
67-
68-
69-
def test_operation_outcome_handler_from_validation_error_with_diagnostics() -> None:
70-
dummy_model_path = "tests.fhir.test_operation_outcome.DummyModel"
71-
72-
error = ValidationError.from_exception_data(
73-
dummy_model_path,
74-
[
75-
{
76-
"type": "value_error",
77-
"loc": ("foo",),
78-
"msg": "Fake error",
79-
"input": "bad_value",
80-
"ctx": {"error": ValueError("Fake error")},
81-
}
82-
],
160+
assert (
161+
outcome["issue"][0]["details"]["coding"][0]["system"]
162+
== OPERATION_OUTCOME_SYSTEM
163+
)
164+
assert outcome["issue"][0]["details"]["coding"][0]["code"] == "MSG_PARAM_INVALID"
165+
assert (
166+
outcome["issue"][0]["details"]["coding"][0]["display"]
167+
== "Parameter content is invalid"
83168
)
84169

85-
outcome = OperationOutcomeHandler.from_validation_error(error)
86-
assert outcome["issue"][0]["diagnostics"] == "Validation failed for resource."
87-
assert outcome["issue"][0]["code"] == "invalid"
88-
assert outcome["issue"][0]["severity"] == "error"
170+
171+
def test_fhir_operation_outcome_codes_mapping() -> None:
172+
"""Test that all expected codes are mapped correctly."""
173+
expected_mappings: dict[str, tuple[str, str]] = {
174+
"invalid": ("MSG_PARAM_INVALID", "Parameter content is invalid"),
175+
"not-found": ("MSG_NO_EXIST", "Resource does not exist"),
176+
"exception": ("MSG_ERROR_PARSING", "Error processing request"),
177+
"structure": ("MSG_BAD_SYNTAX", "Bad Syntax"),
178+
"required": ("MSG_RESOURCE_REQUIRED", "A resource is required"),
179+
"value": ("MSG_PARAM_INVALID", "Parameter content is invalid"),
180+
"processing": ("MSG_ERROR_PARSING", "Error processing request"),
181+
"duplicate": ("MSG_DUPLICATE_ID", "Duplicate Id for resource"),
182+
"informational": ("MSG_UPDATED", "Existing resource updated"),
183+
"success": ("MSG_UPDATED", "Existing resource updated"),
184+
}
185+
for code, expected in expected_mappings.items():
186+
assert FHIR_OPERATION_OUTCOME_CODES[code] == expected
187+
188+
189+
def test_build_details_helper() -> None:
190+
"""Test the _build_details helper method."""
191+
details = OperationOutcomeHandler._build_details("invalid", "Test text")
192+
assert details["coding"][0]["system"] == OPERATION_OUTCOME_SYSTEM
193+
assert details["coding"][0]["code"] == "MSG_PARAM_INVALID"
194+
assert details["coding"][0]["display"] == "Parameter content is invalid"
195+
assert details["text"] == "Test text"

application/packages/python/ftrs_common/tests/fhir/test_organisation_mapper.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def test_to_fhir_maps_fields_correctly() -> None:
6666
assert fhir_org.telecom[0].use is None
6767
assert (
6868
fhir_org.meta.profile[0]
69-
== "https://fhir.nhs.uk/StructureDefinition/UKCore-Organization"
69+
== "https://fhir.hl7.org.uk/StructureDefinition/UKCore-Organization"
7070
)
7171
assert fhir_org.extension is not None
7272
assert len(fhir_org.extension) == EXPECTED_EXTENTION_LENGTH
@@ -95,7 +95,7 @@ def test__build_meta_profile() -> None:
9595
mapper = OrganizationMapper()
9696
meta = mapper._build_meta_profile()
9797
assert meta == {
98-
"profile": ["https://fhir.nhs.uk/StructureDefinition/UKCore-Organization"]
98+
"profile": ["https://fhir.hl7.org.uk/StructureDefinition/UKCore-Organization"]
9999
}
100100

101101

@@ -466,7 +466,7 @@ def test_to_fhir_bundle_single_org() -> None:
466466
assert resource.telecom[0].value == "020 7972 3272"
467467
assert (
468468
resource.meta.profile[0]
469-
== "https://fhir.nhs.uk/StructureDefinition/UKCore-Organization"
469+
== "https://fhir.hl7.org.uk/StructureDefinition/UKCore-Organization"
470470
)
471471

472472

docs/specification/dos-ingest-api-oas-v2.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ components:
821821
items:
822822
type: string
823823
example:
824-
- https://fhir.nhs.uk/StructureDefinition/UKCore-OrganizationAffiliation
824+
- https://fhir.hl7.org.uk/StructureDefinition/UKCore-OrganizationAffiliation
825825
organization:
826826
type: object
827827
properties:
@@ -902,7 +902,7 @@ components:
902902
items:
903903
type: string
904904
example:
905-
- https://fhir.nhs.uk/StructureDefinition/UKCore-Organization
905+
- https://fhir.hl7.org.uk/StructureDefinition/UKCore-Organization
906906
active:
907907
type: boolean
908908
example: true

0 commit comments

Comments
 (0)