Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions layer/nrlf/consumer/fhir/r4/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,19 @@ class NRLFormatCode(Coding):
Field(description="The system URL for the NRLF Format Code."),
]
code: Annotated[
Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"],
Literal[
"urn:nhs-ic:record-contact",
"urn:nhs-ic:unstructured",
"urn:nhs-ic:structured",
],
Field(description="The code representing the format of the document."),
]
display: Annotated[
Literal["Contact details (HTTP Unsecured)", "Unstructured Document"],
Literal[
"Contact details (HTTP Unsecured)",
"Unstructured Document",
"Structured Document",
],
Field(description="The display text for the code."),
]

Expand Down
23 changes: 23 additions & 0 deletions layer/nrlf/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class PointerTypes(Enum):
PERSONALISED_CARE_AND_SUPPORT_PLAN = "http://snomed.info/sct|2181441000000107"
MRA_UPPER_LIMB_ARTERY = "https://nicip.nhs.uk|MAULR"
MRI_AXILLA_BOTH = "https://nicip.nhs.uk|MAXIB"
APPOINTMENT = "http://snomed.info/sct|749001000000101"

@staticmethod
def list():
Expand All @@ -84,6 +85,7 @@ class Categories(Enum):
CLINICAL_NOTE = "http://snomed.info/sct|823651000000106"
DIAGNOSTIC_STUDIES_REPORT = "http://snomed.info/sct|721981007"
DIAGNOSTIC_PROCEDURE = "http://snomed.info/sct|103693007"
RECORD_ARTIFACT = "http://snomed.info/sct|419891008"

@staticmethod
def list():
Expand Down Expand Up @@ -112,6 +114,7 @@ def coding_value(self):
Categories.DIAGNOSTIC_PROCEDURE.value: {
"display": "Diagnostic procedure",
},
Categories.RECORD_ARTIFACT.value: {"display": "Record artifact"},
}

TYPE_ATTRIBUTES = {
Expand Down Expand Up @@ -157,6 +160,9 @@ def coding_value(self):
PointerTypes.MRI_AXILLA_BOTH.value: {
"display": "MRI Axilla Both",
},
PointerTypes.APPOINTMENT.value: {
"display": "Appointment",
},
}

TYPE_CATEGORIES = {
Expand All @@ -182,6 +188,9 @@ def coding_value(self):
# Imaging
PointerTypes.MRA_UPPER_LIMB_ARTERY.value: Categories.DIAGNOSTIC_STUDIES_REPORT.value,
PointerTypes.MRI_AXILLA_BOTH.value: Categories.DIAGNOSTIC_PROCEDURE.value,
#
# Bookings and Referrals
PointerTypes.APPOINTMENT.value: Categories.RECORD_ARTIFACT.value,
}

PRACTICE_SETTING_VALUE_SET_URL = (
Expand Down Expand Up @@ -653,6 +662,7 @@ def coding_value(self):
"24291000087104": "Geriatric chronic pain management service",
"1323501000000109": "Special care dentistry service",
"1423561000000102": "Acute oncology service",
"394802001": "General medicine",
}


Expand All @@ -664,3 +674,16 @@ def coding_value(self):
"https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability"
)
CONTENT_FORMAT_CODE_URL = "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"
CONTENT_FORMAT_CODE_MAP = {
"urn:nhs-ic:record-contact": "Contact details (HTTP Unsecured)",
"urn:nhs-ic:unstructured": "Unstructured Document",
"urn:nhs-ic:structured": "Structured Document",
}

ATTACHMENT_CONTENT_TYPES = {
"application/pdf",
"text/html",
"application/json",
"application/fhir+json",
"application/json+fhir",
}
65 changes: 54 additions & 11 deletions layer/nrlf/core/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,40 @@ def test_validate_content_format_invalid_code_for_unstructured_document():
}


def test_validate_content_format_invalid_code_for_structured_document():
validator = DocumentReferenceValidator()
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")

document_ref_data["content"][0]["attachment"]["contentType"] = "application/json"

document_ref_data["content"][0]["format"] = {
"system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode",
"code": "urn:nhs-ic:record-contact",
"display": "Contact details (HTTP Unsecured)",
}

result = validator.validate(document_ref_data)

assert result.is_valid is False
assert result.resource.id == "Y05868-99999-99999-999999"
assert len(result.issues) == 1
assert result.issues[0].model_dump(exclude_none=True) == {
"severity": "error",
"code": "business-rule",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
"code": "UNPROCESSABLE_ENTITY",
"display": "Unprocessable Entity",
}
]
},
"diagnostics": "Invalid content format code: urn:nhs-ic:record-contact format code must be 'urn:nhs-ic:structured' for Structured Document attachments.",
"expression": ["content[0].format.code"],
}


def test_validate_content_format_invalid_code_for_contact_details():
validator = DocumentReferenceValidator()
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
Expand Down Expand Up @@ -1353,23 +1387,25 @@ def test_validate_content_invalid_content_type():
}
]
},
"diagnostics": "Invalid contentType: invalid/type. Must be 'application/pdf' or 'text/html'",
"diagnostics": "Invalid contentType: invalid/type. Must be 'application/pdf', 'text/html' or 'application/fhir+json'",
"expression": ["content[0].attachment.contentType"],
}


@pytest.mark.parametrize(
"format_code, format_display",
"content_type, format_code, format_display",
[
("urn:nhs-ic:record-contact", "Contact details (HTTP Unsecured)"),
("urn:nhs-ic:unstructured", "Unstructured Document"),
("text/html", "urn:nhs-ic:record-contact", "Contact details (HTTP Unsecured)"),
("application/pdf", "urn:nhs-ic:unstructured", "Unstructured Document"),
("application/json+fhir", "urn:nhs-ic:structured", "Structured Document"),
],
)
def test_validate_nrl_format_code_valid_match(format_code, format_display):
def test_validate_nrl_format_code_valid_match(
content_type, format_code, format_display
):
validator = DocumentReferenceValidator()
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
if format_code == "urn:nhs-ic:record-contact":
document_ref_data["content"][0]["attachment"]["contentType"] = "text/html"
document_ref_data["content"][0]["attachment"]["contentType"] = content_type

document_ref_data["content"][0]["format"] = {
"system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode",
Expand All @@ -1383,27 +1419,34 @@ def test_validate_nrl_format_code_valid_match(format_code, format_display):


@pytest.mark.parametrize(
"format_code, format_display, expected_display",
"content_type, format_code, format_display, expected_display",
[
(
"application/pdf",
"urn:nhs-ic:unstructured",
"Contact details (HTTP Unsecured)",
"Unstructured Document",
),
(
"text/html",
"urn:nhs-ic:record-contact",
"Unstructured Document",
"Contact details (HTTP Unsecured)",
),
(
"application/fhir+json",
"urn:nhs-ic:structured",
"Unstructured Document",
"Structured Document",
),
],
)
def test_validate_nrl_format_code_display_mismatch(
format_code, format_display, expected_display
content_type, format_code, format_display, expected_display
):
validator = DocumentReferenceValidator()
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
if format_code == "urn:nhs-ic:record-contact":
document_ref_data["content"][0]["attachment"]["contentType"] = "text/html"
document_ref_data["content"][0]["attachment"]["contentType"] = content_type

document_ref_data["content"][0]["format"] = {
"system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode",
Expand Down
30 changes: 21 additions & 9 deletions layer/nrlf/core/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from nrlf.consumer.fhir.r4.model import RequestQueryCategory
from nrlf.core.codes import SpineErrorConcept
from nrlf.core.constants import (
ATTACHMENT_CONTENT_TYPES,
CATEGORY_ATTRIBUTES,
CONTENT_FORMAT_CODE_MAP,
ODS_SYSTEM,
PRACTICE_SETTING_VALUE_SET_URL,
REQUIRED_CREATE_FIELDS,
Expand Down Expand Up @@ -483,6 +485,21 @@ def _validate_content_format(self, model: DocumentReference):
diagnostics=f"Invalid content format code: {content.format.code} format code must be 'urn:nhs-ic:unstructured' for Unstructured Document attachments.",
field=f"content[{i}].format.code",
)
elif (
content.attachment.contentType
in {
"application/json",
"application/fhir+json",
"application/json+fhir",
}
and content.format.code != "urn:nhs-ic:structured"
):
self.result.add_error(
issue_code="business-rule",
error_code="UNPROCESSABLE_ENTITY",
diagnostics=f"Invalid content format code: {content.format.code} format code must be 'urn:nhs-ic:structured' for Structured Document attachments.",
field=f"content[{i}].format.code",
)

def _validate_content_extension(self, model: DocumentReference):
"""
Expand Down Expand Up @@ -613,28 +630,23 @@ def _validate_practiceSetting(self, model: DocumentReference):

def _validate_content(self, model: DocumentReference):
"""
Validate that the contentType is present and is either 'application/pdf' or 'text/html'.
Validate that the contentType is present and supported.
"""
logger.log(LogReference.VALIDATOR001, step="content")

format_code_display_map = {
"urn:nhs-ic:record-contact": "Contact details (HTTP Unsecured)",
"urn:nhs-ic:unstructured": "Unstructured Document",
}

for i, content in enumerate(model.content):
if content.attachment.contentType not in ["application/pdf", "text/html"]:
if content.attachment.contentType not in ATTACHMENT_CONTENT_TYPES:
self.result.add_error(
issue_code="business-rule",
error_code="UNPROCESSABLE_ENTITY",
diagnostics=f"Invalid contentType: {content.attachment.contentType}. Must be 'application/pdf' or 'text/html'",
diagnostics=f"Invalid contentType: {content.attachment.contentType}. Must be 'application/pdf', 'text/html' or 'application/fhir+json'",
field=f"content[{i}].attachment.contentType",
)

# Validate NRLFormatCode
format_code = content.format.code
format_display = content.format.display
expected_display = format_code_display_map.get(format_code)
expected_display = CONTENT_FORMAT_CODE_MAP.get(format_code)
if expected_display and format_display != expected_display:
self.result.add_error(
issue_code="business-rule",
Expand Down
14 changes: 11 additions & 3 deletions layer/nrlf/producer/fhir/r4/model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: swagger.yaml
# timestamp: 2025-02-07T14:10:35+00:00
# timestamp: 2025-05-22T18:53:21+00:00

from __future__ import annotations

Expand Down Expand Up @@ -290,11 +290,19 @@ class NRLFormatCode(Coding):
Field(description="The system URL for the NRLF Format Code."),
]
code: Annotated[
Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"],
Literal[
"urn:nhs-ic:record-contact",
"urn:nhs-ic:unstructured",
"urn:nhs-ic:structured",
],
Field(description="The code representing the format of the document."),
]
display: Annotated[
Literal["Contact details (HTTP Unsecured)", "Unstructured Document"],
Literal[
"Contact details (HTTP Unsecured)",
"Unstructured Document",
"Structured Document",
],
Field(description="The display text for the code."),
]

Expand Down
14 changes: 11 additions & 3 deletions layer/nrlf/producer/fhir/r4/strict_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: swagger.yaml
# timestamp: 2025-02-07T14:10:37+00:00
# timestamp: 2025-05-22T18:53:21+00:00

from __future__ import annotations

Expand Down Expand Up @@ -261,11 +261,19 @@ class NRLFormatCode(Coding):
Field(description="The system URL for the NRLF Format Code."),
]
code: Annotated[
Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"],
Literal[
"urn:nhs-ic:record-contact",
"urn:nhs-ic:unstructured",
"urn:nhs-ic:structured",
],
Field(description="The code representing the format of the document."),
]
display: Annotated[
Literal["Contact details (HTTP Unsecured)", "Unstructured Document"],
Literal[
"Contact details (HTTP Unsecured)",
"Unstructured Document",
"Structured Document",
],
Field(description="The display text for the code."),
]

Expand Down
8 changes: 6 additions & 2 deletions resources/fhir/NRLF-FormatCode-ValueSet.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"resourceType": "ValueSet",
"id": "NRLF-FormatCode",
"url": "https://fhir.nhs.uk/England/ValueSet/England-NRLFormatCode",
"version": "1.0.0",
"version": "1.0.3",
"name": "NRLF Format Code",
"status": "draft",
"date": "2025-01-28T00:00:00+00:00",
"date": "2025-05-22T00:00:00+00:00",
"publisher": "NHS Digital",
"contact": {
"name": "NRL Team at NHS Digital",
Expand All @@ -29,6 +29,10 @@
{
"code": "urn:nhs-ic:unstructured",
"display": "Unstructured Document"
},
{
"code": "urn:nhs-ic:structured",
"display": "Structured Document"
}
]
}
Expand Down
8 changes: 6 additions & 2 deletions resources/fhir/NRLF-PracticeSetting-ValueSet.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"resourceType": "ValueSet",
"id": "NRLF-PracticeSetting",
"url": "https://fhir.nhs.uk/England/ValueSet/England-NRLPracticeSetting",
"version": "1.1.2",
"version": "1.1.3",
"name": "NRLF Record Practice Setting",
"status": "draft",
"date": "2025-01-28T00:00:00+00:00",
"date": "2025-05-22T00:00:00+00:00",
"publisher": "NHS Digital",
"contact": {
"name": "NRL Team at NHS Digital",
Expand Down Expand Up @@ -1881,6 +1881,10 @@
{
"code": "1423561000000102",
"display": "Acute oncology service"
},
{
"code": "394802001",
"display": "General medicine"
}
]
}
Expand Down
Loading
Loading