Skip to content
Draft
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
2 changes: 2 additions & 0 deletions api/consumer/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1250,12 +1250,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from nrlf.core.codes import SpineErrorConcept
from nrlf.core.constants import (
PERMISSION_ALLOW_PROXY_ODS_CODES,
PERMISSION_AUDIT_DATES_FROM_PAYLOAD,
PERMISSION_SUPERSEDE_IGNORE_DELETE_FAIL,
PointerTypes,
)
from nrlf.core.decorators import request_handler
from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository
Expand Down Expand Up @@ -64,10 +66,30 @@ def _check_permissions(
"""
Check the requester has permissions to create the DocumentReference
"""

# Allow BARS proxy to create an appointment document reference for any organisation
if (
metadata.ods_code in ["V4TOL", "V4T0L"]
and core_model.type == PointerTypes.APPOINTMENT.value
):
return

allow_ods_code_proxying = False
if PERMISSION_ALLOW_PROXY_ODS_CODES in metadata.nrl_permissions:
if metadata.ods_code == metadata.nrl_proxy_ods_code:
allow_ods_code_proxying = True
else:
logger.log(
LogReference.PROCREATE003,
ods_code=metadata.ods_code,
proxy_ods_code=metadata.nrl_proxy_ods_code,
warning="Unable to allow ods code proxying as the ods code does not match the configured proxy ods code",
)

custodian_parts = tuple(
filter(None, (core_model.custodian, core_model.custodian_suffix))
)
if metadata.ods_code_parts != custodian_parts:
if not allow_ods_code_proxying and metadata.ods_code_parts != custodian_parts:
logger.log(
LogReference.PROCREATE004,
ods_code_parts=metadata.ods_code_parts,
Expand Down
2 changes: 2 additions & 0 deletions api/producer/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1868,12 +1868,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pydantic import ValidationError

from nrlf.core.codes import SpineErrorConcept
from nrlf.core.constants import PointerTypes
from nrlf.core.decorators import DocumentPointerRepository, request_handler
from nrlf.core.dynamodb.model import DocumentPointer
from nrlf.core.errors import OperationOutcomeError
Expand Down Expand Up @@ -61,7 +62,17 @@ def handler(

core_model = DocumentPointer.from_document_reference(document_reference)

if metadata.ods_code_parts != tuple(core_model.producer_id.split("|")):
if (
metadata.ods_code in ["V4TOL", "V4T0L"]
and core_model.type == PointerTypes.APPOINTMENT.value
):
# If bars app - don't validate the ods code against the pointer
logger.log(
LogReference.PROUPDATE002,
allow_rule="Allowing bars to update pointer",
pointer_id=core_model.id,
)
elif metadata.ods_code_parts != tuple(core_model.producer_id.split("|")):
logger.log(
LogReference.PROUPDATE004,
metadata_ods_code_parts=metadata.ods_code_parts,
Expand Down
14 changes: 11 additions & 3 deletions layer/nrlf/consumer/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:39+00:00
# timestamp: 2025-02-13T08:42:14+00:00

from __future__ import annotations

Expand Down 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
8 changes: 4 additions & 4 deletions layer/nrlf/core/authoriser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ def get_pointer_types(
ods_code = connection_metadata.ods_code
ods_code_extension = connection_metadata.ods_code_extension

if ods_code_extension:
key = f"{app_id}/{ods_code}.{ods_code_extension}.json"
else:
key = f"{app_id}/{ods_code}.json"
# if ods_code_extension:
# key = f"{app_id}/{ods_code}.{ods_code_extension}.json"
# else:
key = f"{app_id}/{ods_code}.json"

logger.log(LogReference.S3PERMISSIONS001, bucket=config.AUTH_STORE, key=key)
s3_client = get_s3_client()
Expand Down
17 changes: 17 additions & 0 deletions layer/nrlf/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@ class Source(Enum):
"incorporates",
"summarizes",
}

CLIENT_RP_DETAILS = "nhsd-client-rp-details"
CONNECTION_METADATA = "nhsd-connection-metadata"
PERMISSION_AUDIT_DATES_FROM_PAYLOAD = "audit-dates-from-payload"
PERMISSION_SUPERSEDE_IGNORE_DELETE_FAIL = "supersede-ignore-delete-fail"
PERMISSION_ALLOW_ALL_POINTER_TYPES = "allow-all-pointer-types"

# Bars prototype - Allow a application to proxy as any organisation
METADATA_PROXY_ODS_CODE = "nrl.proxy-ods-code"
PERMISSION_ALLOW_PROXY_ODS_CODES = "allow-proxy-ods-codes"


NHSD_REQUEST_ID_HEADER = "NHSD-Request-Id"
NHSD_CORRELATION_ID_HEADER = "NHSD-Correlation-Id"
Expand All @@ -66,6 +71,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 +90,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 +119,9 @@ def coding_value(self):
Categories.DIAGNOSTIC_PROCEDURE.value: {
"display": "Diagnostic procedure",
},
Categories.RECORD_ARTIFACT.value: {
"display": "Record artifact (record artifact)",
},
}

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

TYPE_CATEGORIES = {
Expand All @@ -182,6 +195,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,
#
# Bars
PointerTypes.APPOINTMENT.value: Categories.RECORD_ARTIFACT.value,
}

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


Expand Down
2 changes: 2 additions & 0 deletions layer/nrlf/core/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import nrlf.consumer.fhir.r4.model as consumer_model
import nrlf.producer.fhir.r4.model as producer_model
from nrlf.core.constants import METADATA_PROXY_ODS_CODE


class _NhsNumberMixin:
Expand Down Expand Up @@ -56,6 +57,7 @@ class ConnectionMetadata(BaseModel):
ods_code_extension: str | None = Field(alias="nrl.ods-code-extension", default=None)
nrl_permissions: list[str] = Field(alias="nrl.permissions", default_factory=list)
nrl_app_id: str = Field(alias="nrl.app-id")
nrl_proxy_ods_code: str | None = Field(alias=METADATA_PROXY_ODS_CODE, default=None)
is_test_event: bool = Field(alias="nrl.test-event", default=False)
client_rp_details: ClientRpDetails

Expand Down
3 changes: 3 additions & 0 deletions layer/nrlf/core/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ODS_SYSTEM,
TYPE_ATTRIBUTES,
TYPE_CATEGORIES,
Categories,
PointerTypes,
)
from nrlf.core.errors import ParseError
Expand Down Expand Up @@ -430,6 +431,7 @@ def test_validate_category_too_many_category():
[
(category_str.split("|")[1], display_dict["display"])
for category_str, display_dict in CATEGORY_ATTRIBUTES.items()
if category_str != Categories.RECORD_ARTIFACT.value
],
)
def test_validate_category_coding_display_mismatch(
Expand Down Expand Up @@ -639,6 +641,7 @@ def test_validate_type_coding_invalid_system():
[
(type_str, display_dict["display"])
for type_str, display_dict in TYPE_ATTRIBUTES.items()
if type_str != PointerTypes.APPOINTMENT.value
],
)
def test_validate_type_coding_display_mismatch(type_str: str, display: str):
Expand Down
16 changes: 15 additions & 1 deletion layer/nrlf/core/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ def _validate_type(self, model: DocumentReference):
)
return

# Bypass display validation for bars appointments
if type_id == PointerTypes.APPOINTMENT.value:
return

type_attributes = TYPE_ATTRIBUTES.get(type_id, {})
if coding.display != type_attributes.get("display"):
self.result.add_error(
Expand Down Expand Up @@ -423,6 +427,10 @@ def _validate_category(self, model: DocumentReference):
)
return

# Bypass display validation for bars record artifacts
if category_id == Categories.RECORD_ARTIFACT.value:
return

category_attributes = CATEGORY_ATTRIBUTES.get(category_id, {})
if coding.display != category_attributes.get("display"):
self.result.add_error(
Expand Down Expand Up @@ -623,7 +631,13 @@ def _validate_content(self, model: DocumentReference):
}

for i, content in enumerate(model.content):
if content.attachment.contentType not in ["application/pdf", "text/html"]:
if content.attachment.contentType not in [
"application/pdf",
"text/html",
"application/fhir+json",
"application/json",
"application/json+fhir",
]:
self.result.add_error(
issue_code="value",
error_code="INVALID_RESOURCE",
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-02-13T08:42:12+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-02-13T08:42:13+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
Loading