diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 82a711a4a..a6fb5141e 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -810,10 +810,12 @@ components: extension: type: array items: - $ref: "#/components/schemas/ContentStabilityExtension" - description: Additional extension for content stability. + oneOf: + - $ref: "#/components/schemas/ContentStabilityExtension" + - $ref: "#/components/schemas/RetrievalMechanismExtension" + - $ref: "#/components/schemas/Extension" + description: Additional extensions which include Content Stability and Retrieval Mechanism. minItems: 1 - maxItems: 1 required: - attachment - format @@ -975,6 +977,52 @@ components: - system - code - display + RetrievalMechanismExtension: + allOf: + - $ref: "#/components/schemas/Extension" + - type: object + properties: + url: + type: string + enum: + - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + valueCodeableConcept: + $ref: "#/components/schemas/RetrievalMechanismExtensionValueCodeableConcept" + required: + - url + - valueCodeableConcept + RetrievalMechanismExtensionValueCodeableConcept: + allOf: + - $ref: "#/components/schemas/CodeableConcept" + - type: object + properties: + coding: + type: array + items: + $ref: "#/components/schemas/RetrievalMechanismExtensionCoding" + minItems: 1 + maxItems: 1 + required: + - coding + RetrievalMechanismExtensionCoding: + allOf: + - $ref: "#/components/schemas/Coding" + - type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism" + code: + type: string + enum: ["SSP", "Direct", "LDR"] + display: + type: string + enum: ["Spine Secure Proxy", "Direct", "Large Document Retrieval"] + required: + - system + - code + - display NRLFormatCode: allOf: - $ref: "#/components/schemas/Coding" @@ -990,12 +1038,14 @@ components: enum: - "urn:nhs-ic:record-contact" - "urn:nhs-ic:unstructured" + - "urn:nhs-ic:structured" description: The code representing the format of the document. display: type: string enum: - "Contact details (HTTP Unsecured)" - "Unstructured Document" + - "Structured Document" description: The display text for the code. required: - system diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index bb2289946..d513cec19 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1415,10 +1415,12 @@ components: extension: type: array items: - $ref: "#/components/schemas/ContentStabilityExtension" - description: Additional extension for content stability. + oneOf: + - $ref: "#/components/schemas/ContentStabilityExtension" + - $ref: "#/components/schemas/RetrievalMechanismExtension" + - $ref: "#/components/schemas/Extension" + description: Additional extensions which include Content Stability and Retrieval Mechanism. minItems: 1 - maxItems: 1 required: - attachment - format @@ -1631,6 +1633,52 @@ components: - system - code - display + RetrievalMechanismExtension: + allOf: + - $ref: "#/components/schemas/Extension" + - type: object + properties: + url: + type: string + enum: + - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + valueCodeableConcept: + $ref: "#/components/schemas/RetrievalMechanismExtensionValueCodeableConcept" + required: + - url + - valueCodeableConcept + RetrievalMechanismExtensionValueCodeableConcept: + allOf: + - $ref: "#/components/schemas/CodeableConcept" + - type: object + properties: + coding: + type: array + items: + $ref: "#/components/schemas/RetrievalMechanismExtensionCoding" + minItems: 1 + maxItems: 1 + required: + - coding + RetrievalMechanismExtensionCoding: + allOf: + - $ref: "#/components/schemas/Coding" + - type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism" + code: + type: string + enum: ["SSP", "Direct", "LDR"] + display: + type: string + enum: ["Spine Secure Proxy", "Direct", "Large Document Retrieval"] + required: + - system + - code + - display NRLFormatCode: allOf: - $ref: "#/components/schemas/Coding" @@ -1646,12 +1694,14 @@ components: enum: - "urn:nhs-ic:record-contact" - "urn:nhs-ic:unstructured" + - "urn:nhs-ic:structured" description: The code representing the format of the document. display: type: string enum: - "Contact details (HTTP Unsecured)" - "Unstructured Document" + - "Structured Document" description: The display text for the code. required: - system diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 898ddfe53..36cbefce2 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-06-13T14:43:32+00:00 +# timestamp: 2025-09-05T08:22:10+00:00 from __future__ import annotations @@ -240,6 +240,12 @@ class ContentStabilityExtensionCoding(Coding): display: Literal["Static", "Dynamic"] +class RetrievalMechanismExtensionCoding(Coding): + system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] + code: Literal["SSP", "Direct", "LDR"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Retrieval"] + + class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], @@ -501,6 +507,12 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] +class RetrievalMechanismExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[RetrievalMechanismExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(Parent): odsCode: RequestHeaderOdsCode @@ -574,6 +586,13 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept +class RetrievalMechanismExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + ] + valueCodeableConcept: RetrievalMechanismExtensionValueCodeableConcept + + class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -633,7 +652,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) + List[Union[ContentStabilityExtension, RetrievalMechanismExtension, Extension]], + Field(min_length=1), ] diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index 17236ac77..d72162829 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -677,9 +677,18 @@ def coding_value(self): CONTENT_STABILITY_EXTENSION_URL = ( "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" ) +CONTENT_RETRIEVAL_EXTENSION_URL = "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" CONTENT_STABILITY_SYSTEM_URL = ( "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" ) +CONTENT_RETRIEVAL_SYSTEM_URL = ( + "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism" +) +CONTENT_RETRIEVAL_CODE_MAP = { + "Direct": "Direct", + "SSP": "Spine Secure Proxy", + "LDR": "Large Document Retrieval", +} 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)", diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index 4280801b0..d230d0400 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -3,7 +3,6 @@ from pydantic import ValidationError from pydantic_core import ErrorDetails -from nrlf.core.constants import CONTENT_FORMAT_CODE_URL, CONTENT_STABILITY_SYSTEM_URL from nrlf.core.response import Response from nrlf.core.types import CodeableConcept from nrlf.producer.fhir.r4 import model as producer_model @@ -20,28 +19,25 @@ def format_error_location(loc: List) -> str: return formatted_loc -def append_value_set_url(loc_string: str) -> str: - if loc_string.endswith(("url", "system")): - return "" - - if "content" in loc_string: - if "extension" in loc_string: - return f". See ValueSet: {CONTENT_STABILITY_SYSTEM_URL}" - if "format" in loc_string: - return f". See ValueSet: {CONTENT_FORMAT_CODE_URL}" - - return "" - - -def diag_for_error(error: ErrorDetails) -> str: +def diag_for_error(error: ErrorDetails, value_set: str, root_location: tuple) -> str: loc_string = format_error_location(error["loc"]) + if root_location: + loc_string = format_error_location(root_location) + "." + loc_string + msg = f"{loc_string or 'DocumentReference'}: {error['msg']}" - msg += append_value_set_url(loc_string) + msg += f", see: {value_set}" if value_set else "" return msg -def expression_for_error(error: ErrorDetails) -> Optional[str]: - return format_error_location(error["loc"]) or "DocumentReference" +def expression_for_error(error: ErrorDetails, root_location: tuple) -> Optional[str]: + loc_string = format_error_location(error["loc"]) or "DocumentReference" + if root_location and error["loc"]: + loc_string = ( + format_error_location(root_location) + + "." + + format_error_location(error["loc"]) + ) + return loc_string class OperationOutcomeError(Exception): @@ -91,15 +87,20 @@ def __init__(self, issues: List[OperationOutcomeIssue]): @classmethod def from_validation_error( - cls, exc: ValidationError, details: CodeableConcept, msg: str = "" + cls, + exc: ValidationError, + details: CodeableConcept, + msg: str = "", + value_set: str = "", + root_location: tuple = None, ): issues = [ producer_model.OperationOutcomeIssue( severity="error", code="invalid", details=details, # type: ignore - diagnostics=f"{msg} ({diag_for_error(error)})", - expression=[expression_for_error(error)], # type: ignore + diagnostics=f"{msg} ({diag_for_error(error, value_set, root_location)})", + expression=[expression_for_error(error, root_location)], # type: ignore ) for error in exc.errors() ] diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 9a06a116e..4a53838ba 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -85,42 +85,11 @@ def test_validate_content_missing_format(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required)", "expression": ["content[0].format"], } -def test_validate_content_multiple_content_stability_extensions(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - # Add a second duplicate contentStability extension - document_ref_data["content"][0]["extension"].append( - document_ref_data["content"][0]["extension"][0] - ) - - with pytest.raises(ParseError) as error: - validator.validate(document_ref_data) - - exc = error.value - assert len(exc.issues) == 1 - assert exc.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "invalid", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", - "code": "BAD_REQUEST", - "display": "Bad request", - } - ] - }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: List should have at most 1 item after validation, not 2. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", - "expression": ["content[0].extension"], - } - - def test_validate_content_invalid_content_stability_code(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -146,7 +115,7 @@ def test_validate_content_invalid_content_stability_code(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic'. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)", "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"], } @@ -176,7 +145,7 @@ def test_validate_content_invalid_content_stability_display(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic'. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)", "expression": [ "content[0].extension[0].valueCodeableConcept.coding[0].display" ], @@ -208,7 +177,7 @@ def test_validate_content_invalid_content_stability_system(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].system: Input should be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", + "diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].system: Input should be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)", "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].system"], } @@ -237,7 +206,7 @@ def test_validate_content_invalid_content_stability_url(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability')", + "diagnostics": "Invalid content stability extension (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)", "expression": ["content[0].extension[0].url"], } @@ -470,3 +439,31 @@ def test_validate_missing_display_from_coding_where_mandatory(): "diagnostics": "Failed to parse DocumentReference resource (context.practiceSetting.coding[0].display: Field required)", "expression": ["context.practiceSetting.coding[0].display"], } + + +def test_validate_content_no_content_extension(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0].pop("extension") + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: Field required)", + "expression": ["content[0].extension"], + } diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 9f0f54f0e..03c458ca7 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -4,6 +4,7 @@ from nrlf.core.constants import ( CATEGORY_ATTRIBUTES, + CONTENT_RETRIEVAL_CODE_MAP, ODS_SYSTEM, TYPE_ATTRIBUTES, TYPE_CATEGORIES, @@ -16,9 +17,15 @@ validate_type, ) from nrlf.producer.fhir.r4.model import ( + ContentStabilityExtension, + ContentStabilityExtensionCoding, + ContentStabilityExtensionValueCodeableConcept, DocumentReference, OperationOutcomeIssue, RequestQueryType, + RetrievalMechanismExtension, + RetrievalMechanismExtensionCoding, + RetrievalMechanismExtensionValueCodeableConcept, ) from nrlf.tests.data import load_document_reference_json @@ -1365,6 +1372,92 @@ def test_validate_content_extension_invalid_code_and_display_mismatch(): } +def test_validate_content_extension_missing_content_stability(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Remove all ContentStability extensions + document_ref_data["content"][0]["extension"] = [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct", + } + ] + }, + } + ] + + 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/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity", + } + ] + }, + "diagnostics": "Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')", + "expression": ["content[0].extension"], + } + + +def test_validate_content_extension_mismatch_between_retrieval_mechanism_display_and_code(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Add a retrieval mechanism extension with a valid code but wrong display + document_ref_data["content"][0]["extension"].append( + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Spine Secure Proxy", + } + ] + }, + } + ) + + 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/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity", + } + ] + }, + "diagnostics": "Invalid content extension display: Spine Secure Proxy Expected display is 'Direct'", + "expression": [ + "content[0].extension[1].valueCodeableConcept.coding[0].display" + ], + } + + def test_validate_content_invalid_content_type(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -1473,3 +1566,348 @@ def test_validate_nrl_format_code_display_mismatch( "diagnostics": f"Invalid display for format code '{format_code}'. Expected '{expected_display}'", "expression": ["content[0].format.display"], } + + +def test_validate_content_multiple_content_stability_extensions(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Add a second duplicate contentStability extension + document_ref_data["content"][0]["extension"].append( + document_ref_data["content"][0]["extension"][0] + ) + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + 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/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity", + } + ] + }, + "diagnostics": "Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')", + "expression": ["content[0].extension"], + } + + +def test_validate_content_multiple_content_retrieval_extensions(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Add 2 content retrieval extensions + content_retrieval_extension = { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct", + } + ] + }, + } + document_ref_data["content"][0]["extension"].append(content_retrieval_extension) + document_ref_data["content"][0]["extension"].append(content_retrieval_extension) + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + 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/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity", + } + ] + }, + "diagnostics": "Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')", + "expression": ["content[0].extension"], + } + + +def test_validate_two_content_with_different_retrieval_mechanisms(): + """Test that two content items with different retrieval mechanisms are valid.""" + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + unstructured_format = { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document", + } + + static_content_stability = { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static", + } + ] + }, + } + + # Add retrieval mechanism extension to the first content item, ssp + first_content = { + "attachment": { + "contentType": "application/pdf", + "url": "ssp://example.com/document1.pdf", + }, + "format": unstructured_format, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "SSP", + "display": "Spine Secure Proxy", + } + ] + }, + }, + static_content_stability, + ], + } + + document_ref_data["content"] = [first_content] + + # Add valid ASID identifier in context.related + document_ref_data["context"]["related"] = [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhsSpineASID", + "value": "123456789012", + } + } + ] + + result = validator.validate(document_ref_data) + + assert result.is_valid is True + assert len(result.issues) == 0 + + # Add a second content item with a different retrieval mechanism + second_content = { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.com/document2.pdf", + }, + "format": unstructured_format, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct", + } + ] + }, + }, + static_content_stability, + ], + } + + document_ref_data["content"].append(second_content) + + result = validator.validate(document_ref_data) + + assert result.is_valid is True + assert len(result.issues) == 0 + + +def test_validate_content_retrieval_lowercase_urls(): + """Test that the extension is recognised when 'RetrievalMechanism' is in lowercase and throws an error for mismatching the URL case.""" + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["extension"] = [ + { + "url": "https://fhir.nhs.uk/england/structuredefinition/extension-england-retrievalmechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct", + } + ] + }, + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static", + } + ] + }, + }, + ] + + with pytest.raises(ParseError) as exc_info: + validator.validate(document_ref_data) + + assert len(exc_info.value.issues) == 1 + assert exc_info.value.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request", + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].url"], + } + + +def make_content_stability_extension(code, display): + return ContentStabilityExtension( + url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + valueCodeableConcept=ContentStabilityExtensionValueCodeableConcept( + coding=[ + ContentStabilityExtensionCoding( + system="https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + code=code, + display=display, + ) + ] + ), + ) + + +def make_retrieval_mechanism_extension(code, display): + return RetrievalMechanismExtension( + url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + valueCodeableConcept=RetrievalMechanismExtensionValueCodeableConcept( + coding=[ + RetrievalMechanismExtensionCoding( + system="https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + code=code, + display=display, + ) + ] + ), + ) + + +def test_has_valid_extensions(): + validator = DocumentReferenceValidator() + extensions = [ + make_content_stability_extension("static", "Static"), + make_retrieval_mechanism_extension("Direct", "Direct"), + ] + assert validator._has_valid_extensions(extensions, 0) is True + + +def test_has_valid_extensions_multiple_retrieval_mechanism(): + validator = DocumentReferenceValidator() + extensions = [ + make_content_stability_extension("static", "Static"), + make_retrieval_mechanism_extension("Direct", "Direct"), + make_retrieval_mechanism_extension("SSP", "Spine Secure Proxy"), + ] + assert validator._has_valid_extensions(extensions, 0) is False + assert any( + "Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')" + in issue.diagnostics + for issue in validator.result.issues + ) + + +def test_no_content_extensions(): + validator = DocumentReferenceValidator() + extensions = [] + assert validator._has_valid_extensions(extensions, 0) is False + assert any( + "Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')" + in issue.diagnostics + for issue in validator.result.issues + ) + + +@pytest.mark.parametrize( + "code, display", [("static", "Static"), ("dynamic", "Dynamic")] +) +def test_validate_content_stability_extension_valid(code, display): + validator = DocumentReferenceValidator() + ext = make_content_stability_extension(code, display) + assert validator._validate_content_stability_extension(ext, 0, 0) is True + assert validator.result.issues == [] + + +@pytest.mark.parametrize( + "code, display", [("dynamic", "Static"), ("static", "Dynamic")] +) +def test_validate_content_stability_extension_display_mismatch(code, display): + validator = DocumentReferenceValidator() + ext = make_content_stability_extension(code, display) + assert validator._validate_content_stability_extension(ext, 0, 0) is False + assert any( + f"Invalid content extension display: {display} Extension display must be the same as code either 'Static' or 'Dynamic'" + in issue.diagnostics + for issue in validator.result.issues + ) + + +@pytest.mark.parametrize( + "code, display", + [ + ("SSP", "Spine Secure Proxy"), + ("Direct", "Direct"), + ("LDR", "Large Document Retrieval"), + ], +) +def test_validate_retrieval_mechanism_extension_valid(code, display): + validator = DocumentReferenceValidator() + ext = make_retrieval_mechanism_extension(code, display) + assert validator._validate_retrieval_mechanism_extension(ext, 0, 0) is True + assert validator.result.issues == [] + + +@pytest.mark.parametrize( + "code, display", + [ + (code, wrong_display) + for code, correct_display in CONTENT_RETRIEVAL_CODE_MAP.items() + for wrong_display in CONTENT_RETRIEVAL_CODE_MAP.values() + if wrong_display != correct_display + ], +) +def test_validate_retrieval_mechanism_extension_display_mismatch(code, display): + validator = DocumentReferenceValidator() + ext = make_retrieval_mechanism_extension(code, display) + assert validator._validate_retrieval_mechanism_extension(ext, 0, 0) is False + assert any( + f"Invalid content extension display: {display} Expected display is '{CONTENT_RETRIEVAL_CODE_MAP.get(code)}'" + in issue.diagnostics + for issue in validator.result.issues + ) diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 266e93ab3..6c906cf50 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -10,6 +10,7 @@ ATTACHMENT_CONTENT_TYPES, CATEGORY_ATTRIBUTES, CONTENT_FORMAT_CODE_MAP, + CONTENT_RETRIEVAL_CODE_MAP, ODS_SYSTEM, PRACTICE_SETTING_VALUE_SET_URL, REQUIRED_CREATE_FIELDS, @@ -24,6 +25,10 @@ from nrlf.core.logger import LogReference, logger from nrlf.core.types import DocumentReference, OperationOutcomeIssue, RequestQueryType from nrlf.producer.fhir.r4 import model as producer_model +from nrlf.producer.fhir.r4.model import ( + ContentStabilityExtension, + RetrievalMechanismExtension, +) def validate_type(type_: Optional[RequestQueryType], pointer_types: List[str]) -> bool: @@ -506,19 +511,103 @@ def _validate_content_extension(self, model: DocumentReference): Validate the content.extension field contains an appropriate coding. """ logger.log(LogReference.VALIDATOR001, step="content_extension") - logger.debug("Validating extension") + for i, content in enumerate(model.content): - coding = content.extension[0].valueCodeableConcept.coding[0] - if coding.code != coding.display.lower(): - self.result.add_error( - issue_code="business-rule", - error_code="UNPROCESSABLE_ENTITY", - diagnostics=f"Invalid content extension display: {coding.display} Extension display must be the same as code either 'Static' or 'Dynamic'", - field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].display", - ) + if not self._has_valid_extensions(content.extension, i): return + def _is_content_stability_extension(self, extension): + return "contentstability" in str(extension).lower() + + def _is_retrieval_mechanism_extension(self, extension): + return "retrievalmechanism" in str(extension).lower() + + def _has_valid_extensions(self, extensions, i): + content_stability_count = 0 + content_retrieval_count = 0 + + for extension in extensions: + if self._is_content_stability_extension(extension): + content_stability_count += 1 + elif self._is_retrieval_mechanism_extension(extension): + content_retrieval_count += 1 + + if content_stability_count != 1: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics="Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')", + field=f"content[{i}].extension", + ) + return False + + if content_retrieval_count > 1: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics="Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')", + field=f"content[{i}].extension", + ) + return False + + return self._validate_content_extension_items(extensions, i) + + def _validate_content_extension_items(self, extensions, i): + for j, extension in enumerate(extensions): + if self._is_content_stability_extension(extension): + if not self._validate_content_stability_extension(extension, i, j): + return False + elif self._is_retrieval_mechanism_extension(extension): + if not self._validate_retrieval_mechanism_extension(extension, i, j): + return False + return True + + def _validate_content_stability_extension(self, extension, i, j): + try: + ContentStabilityExtension.model_validate(extension.model_dump()) + except ValidationError as exc: + raise ParseError.from_validation_error( + exc, + details=SpineErrorConcept.from_code("BAD_REQUEST"), + msg="Invalid content stability extension", + value_set="https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability", + root_location=("content", i, "extension", j), + ) from None + coding = extension.valueCodeableConcept.coding[0] + if coding.code != coding.display.lower(): + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics=f"Invalid content extension display: {coding.display} Extension display must be the same as code either 'Static' or 'Dynamic'", + field=f"content[{i}].extension[{j}].valueCodeableConcept.coding[0].display", + ) + return False + return True + + def _validate_retrieval_mechanism_extension(self, extension, i, j): + try: + RetrievalMechanismExtension.model_validate(extension.model_dump()) + except ValidationError as exc: + raise ParseError.from_validation_error( + exc, + details=SpineErrorConcept.from_code("BAD_REQUEST"), + msg="Invalid content retrieval extension", + value_set="https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism", + root_location=("content", i, "extension", j), + ) from None + coding = extension.valueCodeableConcept.coding[0] + expected_retrieval_display = CONTENT_RETRIEVAL_CODE_MAP.get(coding.code) + if coding.display != expected_retrieval_display: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics=f"Invalid content extension display: {coding.display} Expected display is '{expected_retrieval_display}'", + field=f"content[{i}].extension[{j}].valueCodeableConcept.coding[0].display", + ) + return False + return True + def _validate_author(self, model: DocumentReference): """ Validate the author field contains an appropriate coding system and code. diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 514792e24..8b0e3fc64 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-05-22T18:53:21+00:00 +# timestamp: 2025-09-05T08:22:07+00:00 from __future__ import annotations @@ -284,6 +284,12 @@ class ContentStabilityExtensionCoding(Coding): display: Literal["Static", "Dynamic"] +class RetrievalMechanismExtensionCoding(Coding): + system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] + code: Literal["SSP", "Direct", "LDR"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Retrieval"] + + class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], @@ -549,6 +555,12 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] +class RetrievalMechanismExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[RetrievalMechanismExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(Parent): odsCode: RequestHeaderOdsCode @@ -610,6 +622,13 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept +class RetrievalMechanismExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + ] + valueCodeableConcept: RetrievalMechanismExtensionValueCodeableConcept + + class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -669,7 +688,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) + List[Union[ContentStabilityExtension, RetrievalMechanismExtension, Extension]], + Field(min_length=1), ] diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index e7081e66b..87cdefcdc 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-05-22T18:53:21+00:00 +# timestamp: 2025-09-05T08:22:08+00:00 from __future__ import annotations @@ -255,6 +255,12 @@ class ContentStabilityExtensionCoding(Coding): display: Literal["Static", "Dynamic"] +class RetrievalMechanismExtensionCoding(Coding): + system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] + code: Literal["SSP", "Direct", "LDR"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Retrieval"] + + class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], @@ -486,6 +492,12 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] +class RetrievalMechanismExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[RetrievalMechanismExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(Parent): odsCode: RequestHeaderOdsCode @@ -541,6 +553,13 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept +class RetrievalMechanismExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + ] + valueCodeableConcept: RetrievalMechanismExtensionValueCodeableConcept + + class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -594,7 +613,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) + List[Union[ContentStabilityExtension, RetrievalMechanismExtension, Extension]], + Field(min_length=1), ] diff --git a/resources/fhir/NRLF-Retrieval-CodeSystem.json b/resources/fhir/NRLF-Retrieval-CodeSystem.json index 7f58167a9..9d28dc3d2 100644 --- a/resources/fhir/NRLF-Retrieval-CodeSystem.json +++ b/resources/fhir/NRLF-Retrieval-CodeSystem.json @@ -2,7 +2,7 @@ "resourceType": "CodeSystem", "id": "England-RetrievalMechanismNRL", "url": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanismNRL", - "version": "1.0.0", + "version": "1.0.1", "name": "EnglandRetrievalMechanismNRL", "title": "England Retrieval MechanismNRL", "status": "draft", @@ -53,9 +53,9 @@ "definition": "This document can be retrieved via Spine Secure Proxy by authorised organisations. The custodian's ASID will be needed and can be found in the context.related field." }, { - "code": "NDR", - "display": "National Document Repository", - "definition": "This document can be retrieved via the National Document Repository proxy service." + "code": "LDR", + "display": "Large Document Retrieval", + "definition": "This document can be retrieved via the Large Document Retrieval proxy service." } ] } diff --git a/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json b/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json index 4956351c1..6af55f053 100644 --- a/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json +++ b/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json @@ -2,7 +2,7 @@ "resourceType": "ValueSet", "id": "England-RetrievalMechanism", "url": "https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism", - "version": "1.0.0", + "version": "1.0.1", "name": "EnglandRetrievalMechanism", "status": "draft", "date": "2025-02-28", @@ -31,8 +31,8 @@ "display": "Spine Secure Proxy" }, { - "code": "NDR", - "display": "National Document Repository" + "code": "LDR", + "display": "Large Document Retrieval" } ] } diff --git a/tests/features/consumer/readDocumentReference-success.feature b/tests/features/consumer/readDocumentReference-success.feature index aa91cd59f..4fd465fde 100644 --- a/tests/features/consumer/readDocumentReference-success.feature +++ b/tests/features/consumer/readDocumentReference-success.feature @@ -87,6 +87,18 @@ Feature: Consumer - readDocumentReference - Success Scenarios } ] } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } } ] } @@ -192,6 +204,18 @@ Feature: Consumer - readDocumentReference - Success Scenarios } ] } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } } ] } diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 9aa0bf9dc..099ae18c1 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -1044,3 +1044,458 @@ Feature: Producer - createDocumentReference - Failure Scenarios "expression": ["DocumentReference"] } """ + + Scenario: RetrievalMechanism extension is empty + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" + } + ] + }, + "diagnostics": "Request body could not be parsed (DocumentReference: Value error, The following fields are empty: content[0].extension)", + "expression": [ + "DocumentReference" + ] + } + """ + + Scenario: Multiple RetrievalMechanism extensions in content + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 422 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity" + } + ] + }, + "diagnostics": "Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')", + "expression": ["content[0].extension"] + } + """ + + Scenario: RetrievalMechanism extension with mismatched code and display + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Spine Secure Proxy" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 422 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity" + } + ] + }, + "diagnostics": "Invalid content extension display: Spine Secure Proxy Expected display is 'Direct'", + "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].display"] + } + """ + + Scenario: RetrievalMechanism extension with invalid URL case + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/england/structuredefinition/extension-england-retrievalmechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request" + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].url"] + } + """ + + Scenario: RetrievalMechanism extension with invalid code + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "INVALID_CODE", + "display": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request" + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'SSP', 'Direct' or 'LDR', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"] + } + """ + + Scenario: RetrievalMechanism extension with missing display + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request" + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Spine Secure Proxy', 'Direct' or 'Large Document Retrieval', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].display"] + } + """ + + Scenario: RetrievalMechanism extension with missing valueCodeableConcept + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request" + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept: Input should be a valid dictionary or instance of RetrievalMechanismExtensionValueCodeableConcept, see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].valueCodeableConcept"] + } + """ diff --git a/tests/features/producer/createDocumentReference-success.feature b/tests/features/producer/createDocumentReference-success.feature index e1094739d..cc7186b12 100644 --- a/tests/features/producer/createDocumentReference-success.feature +++ b/tests/features/producer/createDocumentReference-success.feature @@ -377,3 +377,117 @@ Feature: Producer - createDocumentReference - Success Scenarios | contentType | application/json+fhir | | formatCode | urn:nhs-ic:structured | | formatDisplay | Structured Document | + + Scenario: Successfully create a DocumentReference with two contents and different retrieval mechanisms + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 1363501000000100 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/doc1.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + } + ] + }, + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/doc2.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "SSP", + "display": "Spine Secure Proxy" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 201 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "information", + "code": "informational", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode", + "code": "RESOURCE_CREATED", + "display": "Resource created" + } + ] + }, + "diagnostics": "The document has been created" + } + """ + And the response has a Location header + And the Location header starts with '/producer/FHIR/R4/DocumentReference/TSTCUS-' + And the resource in the Location header exists with values: + | property | value | + | content[0].attachment.url | https://example.org/doc1.pdf | + | content[0].extension[1].valueCodeableConcept.coding[0].code | Direct | + | content[0].extension[1].valueCodeableConcept.coding[0].display | Direct | + | content[1].attachment.url | https://example.org/doc2.pdf | + | content[1].extension[1].valueCodeableConcept.coding[0].code | SSP | + | content[1].extension[1].valueCodeableConcept.coding[0].display | Spine Secure Proxy | diff --git a/tests/features/producer/readDocumentReference-success.feature b/tests/features/producer/readDocumentReference-success.feature index 9e4c2f8d7..c8d5215d4 100644 --- a/tests/features/producer/readDocumentReference-success.feature +++ b/tests/features/producer/readDocumentReference-success.feature @@ -89,6 +89,18 @@ Feature: Producer - readDocumentReference - Success Scenarios } ] } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } } ] } diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index cd6a7262d..dab741c80 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -1,6 +1,8 @@ from layer.nrlf.core.constants import ( CATEGORY_ATTRIBUTES, CONTENT_FORMAT_CODE_URL, + CONTENT_RETRIEVAL_EXTENSION_URL, + CONTENT_RETRIEVAL_SYSTEM_URL, CONTENT_STABILITY_EXTENSION_URL, CONTENT_STABILITY_SYSTEM_URL, SNOMED_PRACTICE_SETTINGS, @@ -22,6 +24,9 @@ NRLCoding, NRLFormatCode, Reference, + RetrievalMechanismExtension, + RetrievalMechanismExtensionCoding, + RetrievalMechanismExtensionValueCodeableConcept, ) from tests.features.utils.constants import ( DEFAULT_TEST_AUTHOR, @@ -75,7 +80,19 @@ def create_test_document_reference(items: dict) -> DocumentReference: ) ] ), - ) + ), + RetrievalMechanismExtension( + url=CONTENT_RETRIEVAL_EXTENSION_URL, + valueCodeableConcept=RetrievalMechanismExtensionValueCodeableConcept( + coding=[ + RetrievalMechanismExtensionCoding( + system=CONTENT_RETRIEVAL_SYSTEM_URL, + code="Direct", + display="Direct", + ) + ] + ), + ), ], ) ],