Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0c85068
[NDR-423] FHIR GET and SEARCH by ID
jameslinnell Feb 25, 2026
2ccb829
[NDR-423] Unit tests
jameslinnell Feb 25, 2026
109a62c
[NDR-423] e2e tests
jameslinnell Feb 25, 2026
d014a0b
[NDR-423] 404 test
jameslinnell Feb 25, 2026
460daa1
[NDR-423] Don't return deleted items on get_item
jameslinnell Feb 25, 2026
e167ce0
[NDR-423] Search tests
jameslinnell Feb 25, 2026
dcc71b0
[NDR-423] Docker fix
jameslinnell Feb 26, 2026
b73c3cf
[NDR-423] Update unit tests
jameslinnell Feb 26, 2026
edd2b74
[NDR-423] upload service now checks PDM by ID
jameslinnell Feb 26, 2026
5234431
[NDR-423] LG FHIR E2E tests fix
jameslinnell Feb 26, 2026
6bb66a2
[NDR-423] Update file update
jameslinnell Feb 26, 2026
d470b62
[NDR-423] Fix upload test
jameslinnell Feb 26, 2026
16290a3
[NDR-423] LG FHIR test
jameslinnell Feb 26, 2026
cd95937
[NDR-423] Removed now broken 500 error
jameslinnell Feb 27, 2026
c83c138
[NDR-423] Update tests
jameslinnell Mar 3, 2026
0108a21
[NDR-423] Update tests
jameslinnell Mar 4, 2026
53d05cf
[NDR-423] Fix DocStatus tests
jameslinnell Mar 4, 2026
c9c37af
[NDR-423] Update DocStatus
jameslinnell Mar 4, 2026
864f335
[NDR-423] Remove print
jameslinnell Mar 4, 2026
2c7f8ea
[NDR-423] PR changes
jameslinnell Mar 12, 2026
2117250
[NDR-423] PR changes
jameslinnell Mar 12, 2026
fb2e159
[NDR-423] Small change
jameslinnell Mar 12, 2026
f8f044d
[NDR-423] Fix search tests
jameslinnell Mar 12, 2026
870d820
[NDR-423] Search fix
jameslinnell Mar 12, 2026
1f0c842
[NDR-423] Fix e2e test help message
jameslinnell Mar 12, 2026
9f3826c
[NDR-423] Remove search test file that has been renamed in a previous PR
jameslinnell Mar 12, 2026
74783a8
[NDR-423] Removed MTLS 500 error test
jameslinnell Mar 13, 2026
11317fd
[NDR-423] Still return SNOMED
jameslinnell Mar 16, 2026
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: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ python 3.11.14
shellcheck 0.11.0
terraform 1.14.6
terraform-docs 0.20.0
trivy 0.69.2
trivy 0.69.2
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,14 @@ download-api-certs: ## Downloads mTLS certificates (use with dev envs only). Usa
rm -rf ./lambdas/mtls_env_certs/$(WORKSPACE)
./scripts/aws/download-api-certs.sh $(WORKSPACE)

test-lg-fhir-api-e2e: ## Runs LG FHIR API E2E tests. See readme for required environment variables. Usage: make test-fhir-api-e2e-lg CONTAINER=<true|false>
test-lg-fhir-api-e2e: ## Runs LG FHIR API E2E tests. See readme for required environment variables. Usage: make test-lg-fhir-api-e2e CONTAINER=<true|false>
ifeq ($(CONTAINER), true)
cd ./lambdas && PYTHONPATH=. poetry run pytest tests/e2e/api --ignore=tests/e2e/api/fhir -vv
else
cd ./lambdas && ./venv/bin/python3 -m pytest tests/e2e/api --ignore=tests/e2e/api/fhir -vv
endif

test-core-fhir-api-e2e: guard-WORKSPACE ## Runs Core FHIR API E2E tests. Usage: make test-fhir-api-e2e-core WORKSPACE=<workspace> CONTAINER=<true|false>
test-core-fhir-api-e2e: guard-WORKSPACE ## Runs Core FHIR API E2E tests. Usage: make test-core-fhir-api-e2e WORKSPACE=<workspace> CONTAINER=<true|false>
./scripts/test/run-e2e-fhir-api-tests.sh --workspace $(WORKSPACE) --container $(CONTAINER)
rm -rf ./lambdas/mtls_env_certs/$(WORKSPACE)

Expand Down
311 changes: 155 additions & 156 deletions apim/specification.yaml

Large diffs are not rendered by default.

105 changes: 83 additions & 22 deletions lambdas/handlers/get_fhir_document_reference_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from enums.lambda_error import LambdaError
import uuid
from typing import Optional, Tuple

from oauthlib.oauth2 import WebApplicationClient

from enums.lambda_error import LambdaError
from enums.mtls import MtlsCommonNames
from enums.snomed_codes import SnomedCodes
from services.base.ssm_service import SSMService
from services.dynamic_configuration_service import DynamicConfigurationService
from services.get_fhir_document_reference_service import GetFhirDocumentReferenceService
Expand All @@ -15,6 +21,7 @@
SearchPatientException,
)
from utils.lambda_handler_utils import extract_bearer_token
from utils.lambda_header_utils import validate_common_name_in_mtls
from utils.lambda_response import ApiGatewayResponse

logger = LoggingService(__name__)
Expand All @@ -29,44 +36,52 @@
"APPCONFIG_ENVIRONMENT",
"PRESIGNED_ASSUME_ROLE",
"CLOUDFRONT_URL",
]
],
)
def lambda_handler(event, context):
try:
common_name = validate_common_name_in_mtls(event.get("requestContext"))
bearer_token = extract_bearer_token(event, context)
selected_role_id = event.get("headers", {}).get("cis2-urid", None)

document_id, snomed_code = extract_document_parameters(event)
if not snomed_code:
snomed_code = _determine_document_type(common_name=common_name)

get_document_service = GetFhirDocumentReferenceService()
document_reference = get_document_service.handle_get_document_reference_request(
snomed_code, document_id
snomed_code,
document_id,
)

if selected_role_id and bearer_token:
verify_user_authorisation(
bearer_token, selected_role_id, document_reference.nhs_number
bearer_token,
selected_role_id,
document_reference.nhs_number,
)

document_reference_response = (
get_document_service.create_document_reference_fhir_response(
document_reference
document_reference,
)
)

logger.info(
f"Successfully retrieved document reference for document_id: {document_id}, snomed_code: {snomed_code}"
f"Successfully retrieved document reference for document_id: {document_id}",
)

return ApiGatewayResponse(
status_code=200, body=document_reference_response, methods="GET"
status_code=200,
body=document_reference_response,
methods="GET",
).create_api_gateway_response()

except GetFhirDocumentReferenceException as exception:
return ApiGatewayResponse(
status_code=exception.status_code,
body=exception.error.create_error_response().create_error_fhir_response(
exception.error.value.get("fhir_coding")
exception.error.value.get("fhir_coding"),
),
methods="GET",
).create_api_gateway_response()
Expand All @@ -75,12 +90,13 @@ def lambda_handler(event, context):
def extract_document_parameters(event):
"""Extract document ID and SNOMED code from path parameters"""
path_params = event.get("pathParameters", {}).get("id", None)
document_id, snomed_code = get_id_and_snomed_from_path_parameters(path_params)
document_id, snomed_code = get_id_from_path_parameters(path_params)

if not document_id or not snomed_code:
logger.error("Missing document id or snomed code in request path parameters.")
if not document_id:
logger.error("Missing document ID in request path parameters.")
raise GetFhirDocumentReferenceException(
400, LambdaError.DocumentReferenceMissingParameters
400,
LambdaError.DocumentReferenceMissingParameters,
)

return document_id, snomed_code
Expand All @@ -100,27 +116,72 @@ def verify_user_authorisation(bearer_token, selected_role_id, nhs_number):

org_ods_code = oidc_service.fetch_user_org_code(userinfo, selected_role_id)
smartcard_role_code, _ = oidc_service.fetch_user_role_code(
userinfo, selected_role_id, "R"
userinfo,
selected_role_id,
"R",
)
except (OidcApiException, AuthorisationException) as e:
logger.error(f"Authorization error: {str(e)}")
raise GetFhirDocumentReferenceException(
403, LambdaError.DocumentReferenceUnauthorised
403,
LambdaError.DocumentReferenceUnauthorised,
)

try:
search_patient_service = SearchPatientDetailsService(
smartcard_role_code, org_ods_code
smartcard_role_code,
org_ods_code,
)
search_patient_service.handle_search_patient_request(nhs_number, False)
except SearchPatientException as e:
raise GetFhirDocumentReferenceException(e.status_code, e.error)


def get_id_and_snomed_from_path_parameters(path_parameters):
"""Extract document ID and SNOMED code from path parameters"""
if path_parameters:
params = path_parameters.split("~")
if len(params) == 2:
return params[1], params[0]
return None, None
def get_id_from_path_parameters(path_parameters) -> Tuple[Optional[str], Optional[str]]:
"""Extract uuid from path parameters.

Accepts:
- '1234~uuid'
- 'uuid'
"""
snomed_code = None
if not path_parameters:
return None, None

params = path_parameters.split("~")
if len(params) > 2:
raise GetFhirDocumentReferenceException(
400,
LambdaError.DocRefInvalidFiles,
)

if len(params) > 1:
snomed_code = params[0] if params[0] else None
doc_id = params[1]
else:
doc_id = params[-1]
if not is_uuid(doc_id):
raise GetFhirDocumentReferenceException(
400,
LambdaError.DocRefInvalidFiles,
)
return doc_id, snomed_code


def is_uuid(value: str) -> bool:
try:
uuid.UUID(value)
return True
except (ValueError, TypeError):
return False


def _determine_document_type(common_name: MtlsCommonNames | None) -> str:
if not common_name:
return SnomedCodes.LLOYD_GEORGE.value.code

if common_name not in MtlsCommonNames:
logger.error(f"mTLS common name {common_name} - is not supported")
raise GetFhirDocumentReferenceException(400, LambdaError.DocRefInvalidType)

return SnomedCodes.PATIENT_DATA.value.code
2 changes: 1 addition & 1 deletion lambdas/models/fhir/R4/fhir_document_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def create_fhir_document_reference_object(

fhir_doc_ref = DocumentReference(
resourceType="DocumentReference",
id=f"{self.snomed_code_doc_type.code}~{document.id}",
id=document.id,
docStatus=document.doc_status,
type=CodeableConcept(
coding=self._create_snomed_coding(self.snomed_code_doc_type),
Expand Down
Loading
Loading