Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PRMP-1287] GetDocumentRef lambda #489

Merged
merged 45 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a80ea0f
[PRMP-1287] intial commit
steph-torres-nhs Dec 6, 2024
e8a812d
[PRMP-1287] start to implement user validation
steph-torres-nhs Dec 6, 2024
8e4b2ce
prmp-1287 tidy up code
NogaNHS Dec 6, 2024
fbc5654
prmp-1287 add call to create url
NogaNHS Dec 9, 2024
6b0d651
prmp-1287 remove unnecessary env vars
NogaNHS Dec 9, 2024
cd92075
prmp-1287 optional index name in dynamo service
NogaNHS Dec 9, 2024
71164b9
prmp-1287 rearrange services order
NogaNHS Dec 10, 2024
7935104
prmp-1287 add fhir response
NogaNHS Dec 10, 2024
da12abb
[PRMP-1287] error handling
NogaNHS Dec 12, 2024
bceef6d
[PRMP-1360] add missing fields in NRL request
NogaNHS Dec 13, 2024
40aeeaf
[PRMP-1360] fix typo
NogaNHS Dec 13, 2024
2077418
[PRMP-1287] add handler unit tests
NogaNHS Dec 16, 2024
1c46437
[PRMP-1287] add service unit test
NogaNHS Dec 17, 2024
4e55604
[PRMP-1287] add service unit tests
NogaNHS Dec 17, 2024
f7a6b2b
[PRMP-1287] change import
NogaNHS Dec 17, 2024
8c232c9
[PRMP-1287] add logs for debugging edge lambda
NogaNHS Dec 17, 2024
259058d
Merge branch 'main' into PRMP-1287
oliverbeumkes-nhs Dec 17, 2024
28d38c3
[PRMP-1287] add opertion outcome model
NogaNHS Dec 17, 2024
f203dfe
Merge remote-tracking branch 'origin/PRMP-1287' into PRMP-1287
NogaNHS Dec 17, 2024
9ff541e
[PRMP-1360] change to snomedcode
NogaNHS Dec 18, 2024
640f2e1
Merge branch 'PRMP-1360' into PRMP-1287
NogaNHS Dec 18, 2024
72107ff
Merge remote-tracking branch 'origin/main' into PRMP-1287
NogaNHS Jan 2, 2025
ff13b52
[PRMP-1287] add pydantic models to doc ref
NogaNHS Jan 2, 2025
b11dac0
[PRMP-1287] remove NrlAttachment model
NogaNHS Jan 3, 2025
f2d88b2
[PRMP-1287] remove init files
NogaNHS Jan 3, 2025
ca9ed2b
Merge branch 'refs/heads/main' into PRMP-1287
NogaNHS Jan 3, 2025
f69c670
[PRMP-1287] validating incoming event
NogaNHS Jan 3, 2025
cfe6c2b
[PRMP-1287] small changes
NogaNHS Jan 3, 2025
4e3de59
[PRMP-1286] Change create fhir response to return json
NogaNHS Jan 3, 2025
63fde98
[PRMP-1287] rearrange code for readability
NogaNHS Jan 7, 2025
e92c97d
[PRMP-1287] add unit tests to cover service error scenarios
NogaNHS Jan 7, 2025
f242964
[PRMP-1287] expend test_create_document_reference_fhir_response test
NogaNHS Jan 7, 2025
702ee9b
[PRMP-1287] remove change to script
NogaNHS Jan 7, 2025
36f3e12
[PRMP-1287] update lambda error
steph-torres-nhs Jan 8, 2025
fceb21f
[PRMP-1287] implement logic to include SNOMED code in API call
steph-torres-nhs Jan 9, 2025
4bf85b8
[PRMP-1287] modify NRL pointer url to start with dr:// protocol with …
steph-torres-nhs Jan 9, 2025
9e43228
[PRMP-1287] fix unit test import, move try block up
steph-torres-nhs Jan 10, 2025
7875598
[PRMP-1287] update http status code responses, add check for ods code…
steph-torres-nhs Jan 10, 2025
9fd5203
[PRMP-1287] update logging when ods codes from pds and table do not m…
steph-torres-nhs Jan 13, 2025
6433bce
Merge branch 'main' into PRMP-1287
steph-torres-nhs Jan 13, 2025
e6d6953
[PRMP-1287] reformat
steph-torres-nhs Jan 13, 2025
446f05d
[PRMP-1287] merge main
NogaNHS Jan 17, 2025
a62fa6b
[PRMP-1287] add new lambda error
steph-torres-nhs Jan 17, 2025
a616941
[PRMP-1287] remove duplicate create pointer call
steph-torres-nhs Jan 20, 2025
e4db9f5
[PRMP-1287] changes from review
steph-torres-nhs Jan 20, 2025
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
15 changes: 15 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,21 @@ jobs:
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_get_document_reference_lambda:
name: Deploy get nrl document reference lambda
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml
with:
environment: ${{ inputs.environment}}
python_version: ${{ inputs.python_version }}
build_branch: ${{ inputs.build_branch}}
sandbox: ${{ inputs.sandbox }}
lambda_handler_name: nrl_get_document_reference_handler
lambda_aws_name: GetDocumentReference
lambda_layer_names: 'core_lambda_layer'
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}


deploy_edge_presign_lambda:
name: Deploy edge presign cloudfront lambda
uses: ./.github/workflows/base-lambdas-edge-deploy.yml
Expand Down
File renamed without changes.
14 changes: 14 additions & 0 deletions lambdas/enums/fhir/fhir_issue_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from enum import Enum


class FhirIssueCoding(Enum):
INVALID = ("invalid", "Invalid")
FORBIDDEN = ("forbidden", "Forbidden")
NOT_FOUND = ("not-found", "Not Found")
EXCEPTION = ("exception", "Exception")

def code(self):
return self.value[0]

def display(self):
return self.value[1]
34 changes: 30 additions & 4 deletions lambdas/enums/lambda_error.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
from enum import Enum
from typing import Optional

from enums.fhir.fhir_issue_type import FhirIssueCoding
from utils.error_response import ErrorResponse
from utils.request_context import request_context


class LambdaError(Enum):
def create_error_body(self, params: Optional[dict] = None) -> str:
def create_error_response(self, params: Optional[dict] = None) -> ErrorResponse:
err_code = self.value["err_code"]
message = self.value["message"]
if "%" in message and params:
message = message % params

interaction_id = getattr(request_context, "request_id", None)
error_response = ErrorResponse(
err_code=err_code, message=message, interaction_id=interaction_id
)
return error_response.create()
return error_response

def to_str(self) -> str:
return f"[{self.value['err_code']}] {self.value['message']}"

def create_error_body(self, params: Optional[dict] = None) -> str:
return self.create_error_response(params).create()

"""
Errors for SearchPatientException
"""
Expand Down Expand Up @@ -390,7 +393,29 @@ def to_str(self) -> str:
"err_code": "US_5001",
"message": "Dynamo client error",
}

"""
Errors for fhir get document reference lambda
"""
DocumentReferenceNotFound = {
"err_code": "NRL_DR_4041",
"message": "Document reference not found",
"fhir_coding": FhirIssueCoding.NOT_FOUND,
}
DocumentReferenceGeneralError = {
"err_code": "NRL_DR_4031",
"message": "An error occurred while fetching the document",
"fhir_coding": FhirIssueCoding.EXCEPTION,
}
DocumentReferenceUnauthorised = {
"err_code": "NRL_DR_4011",
"message": "User is unauthorised to view record",
"fhir_coding": FhirIssueCoding.FORBIDDEN,
}
DocumentReferenceInvalidRequest = {
"err_code": "NRL_DR_4001",
"message": "Invalid request",
"fhir_coding": FhirIssueCoding.INVALID,
AndyFlintNHS marked this conversation as resolved.
Show resolved Hide resolved
}
"""
Edge Lambda Errors
"""
Expand Down Expand Up @@ -475,6 +500,7 @@ def to_str(self) -> str:
"message": "Client error",
"err_code": "AB_XXXX",
"interaction_id": "88888888-4444-4444-4444-121212121212",
"fhir_coding": FhirIssueCoding.FORBIDDEN,
}

"""
Expand Down
17 changes: 7 additions & 10 deletions lambdas/handlers/manage_nrl_pointer_handler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json

from enums.nrl_sqs_upload import NrlActionTypes
from models.nrl_fhir_document_reference import FhirDocumentReference
from models.fhir.R4.nrl_fhir_document_reference import DocumentReferenceInfo
from models.nrl_sqs_message import NrlSqsMessage
from services.base.nhs_oauth_service import NhsOauthService
from services.base.ssm_service import SSMService
Expand Down Expand Up @@ -49,16 +49,13 @@ def lambda_handler(event, context):
)
match nrl_message.action:
case NrlActionTypes.CREATE:
document = (
FhirDocumentReference(
**nrl_verified_message,
custodian=nrl_api_service.end_user_ods_code,
)
.build_fhir_dict()
.json()
document = DocumentReferenceInfo(
**nrl_verified_message,
custodian=nrl_api_service.end_user_ods_code,
).create_fhir_document_reference_object()
nrl_api_service.create_new_pointer(
document.model_dump(exclude_none=True)
)

nrl_api_service.create_new_pointer(json.loads(document))
case NrlActionTypes.DELETE:
nrl_api_service.delete_pointer(
nrl_message.nhs_number, nrl_message.snomed_code_doc_type
Expand Down
51 changes: 51 additions & 0 deletions lambdas/handlers/nrl_get_document_reference_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from enums.lambda_error import LambdaError
from services.dynamic_configuration_service import DynamicConfigurationService
from services.nrl_get_document_reference_service import NRLGetDocumentReferenceService
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.lambda_exceptions import NRLGetDocumentReferenceException
from utils.lambda_response import ApiGatewayResponse

logger = LoggingService(__name__)


@handle_lambda_exceptions
@set_request_context_for_logging
@ensure_environment_variables(
names=[
"APPCONFIG_APPLICATION",
"APPCONFIG_CONFIGURATION",
"APPCONFIG_ENVIRONMENT",
"LLOYD_GEORGE_DYNAMODB_NAME",
"PRESIGNED_ASSUME_ROLE",
"CLOUDFRONT_URL",
]
)
def lambda_handler(event, context):
document_id = event.get("pathParameters", {}).get("id", None)
bearer_token = event.get("headers", {}).get("Authorization", None)
configuration_service = DynamicConfigurationService()
configuration_service.set_auth_ssm_prefix()
try:
if not document_id or not bearer_token:
mark-start-nhs marked this conversation as resolved.
Show resolved Hide resolved
raise NRLGetDocumentReferenceException(
AndyFlintNHS marked this conversation as resolved.
Show resolved Hide resolved
400, LambdaError.DocumentReferenceInvalidRequest
)
get_document_service = NRLGetDocumentReferenceService()
document_ref = get_document_service.handle_get_document_reference_request(
document_id, bearer_token
)

return ApiGatewayResponse(
status_code=200, body=document_ref, methods="GET"
).create_api_gateway_response()
except NRLGetDocumentReferenceException as e:
return ApiGatewayResponse(
status_code=e.status_code,
body=e.error.create_error_response().create_error_fhir_response(
e.error.value.get("fhir_coding")
),
methods="GET",
).create_api_gateway_response()
File renamed without changes.
39 changes: 39 additions & 0 deletions lambdas/models/fhir/R4/base_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import List, Optional

from pydantic import BaseModel


class Coding(BaseModel):
system: Optional[str] = "http://hl7.org/fhir/issue-type"
code: Optional[str] = None
display: Optional[str] = None


class CodeableConcept(BaseModel):
coding: Optional[List[Coding]] = None


class Extension(BaseModel):
valueCodeableConcept: Optional[CodeableConcept] = None
url: Optional[str] = None


class Period(BaseModel):
id: Optional[str] = None
start: Optional[str] = None
end: Optional[str] = None


class Identifier(BaseModel):
use: Optional[str] = None
type: Optional[CodeableConcept] = None
system: Optional[str] = None
value: Optional[str] = None
period: Optional[Period] = None


class Reference(BaseModel):
reference: Optional[str] = None
type: Optional[str] = None
identifier: Optional[Identifier] = None
display: Optional[str] = None
157 changes: 157 additions & 0 deletions lambdas/models/fhir/R4/nrl_fhir_document_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from typing import List, Literal, Optional

from enums.snomed_codes import SnomedCode, SnomedCodes
from models.fhir.R4.base_models import (
CodeableConcept,
Coding,
Extension,
Period,
Reference,
)
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel


class NRLFormatCode(Coding):
system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"] = (
"https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"
)
code: Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"] = (
"urn:nhs-ic:unstructured"
)
display: Literal["Contact details (HTTP Unsecured)", "Unstructured Document"] = (
"Unstructured document"
)


class Attachment(BaseModel):
contentType: str = "application/pdf"
language: str = "en-GB"
url: Optional[str] = None
size: Optional[int] = None
hash: Optional[str] = None
title: Optional[str] = None
creation: Optional[str] = None


class ContentStabilityExtensionCoding(Coding):
system: Literal[
"https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability"
] = "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability"
code: Literal["static", "dynamic"] = "static"
display: Literal["Static", "Dynamic"] = "Static"


class ContentStabilityExtensionValueCodeableConcept(CodeableConcept):
coding: List[ContentStabilityExtensionCoding] = [ContentStabilityExtensionCoding()]


class ContentStabilityExtension(Extension):
url: Literal[
"https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability"
] = "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability"
valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept = (
ContentStabilityExtensionValueCodeableConcept()
)


class DocumentReferenceContent(BaseModel):
attachment: Attachment
format: NRLFormatCode = NRLFormatCode()
extension: List[ContentStabilityExtension] = [ContentStabilityExtension()]


class DocumentReferenceContext(BaseModel):
encounter: Optional[List[Reference]] = None
event: Optional[List[CodeableConcept]] = None
period: Optional[Period] = None
facilityType: Optional[CodeableConcept] = None
practiceSetting: CodeableConcept
sourcePatientInfo: Optional[Reference] = None
related: Optional[List[Reference]] = None


class DocumentReference(BaseModel):
resourceType: Literal["DocumentReference"] = "DocumentReference"
status: Literal["current"] = "current"
type: Optional[CodeableConcept] = None
category: Optional[List[CodeableConcept]] = None
subject: Optional[Reference] = None
date: Optional[str] = None
author: Optional[List[Reference]] = None
authenticator: Optional[Reference] = None
custodian: Optional[Reference] = None
content: List[DocumentReferenceContent]
context: Optional[DocumentReferenceContext] = None


class DocumentReferenceInfo(BaseModel):
model_config = ConfigDict(alias_generator=to_camel)
nhs_number: str
custodian: str
snomed_code_doc_type: SnomedCode = SnomedCodes.LLOYD_GEORGE.value
snomed_code_category: SnomedCode = SnomedCodes.CARE_PLAN.value
snomed_code_practice_setting: SnomedCode = (
SnomedCodes.GENERAL_MEDICAL_PRACTICE.value
)
attachment: Optional[Attachment] = Attachment()

def create_fhir_document_reference_object(self):
fhir_base_url = "https://fhir.nhs.uk/Id"
snomed_url = "http://snomed.info/sct"

fhir_document_ref = DocumentReference(
subject={
"identifier": {
"system": fhir_base_url + "/nhs-number",
"value": self.nhs_number,
}
},
custodian={
"identifier": {
"system": fhir_base_url + "/ods-organization-code",
"value": self.custodian,
},
},
type={
"coding": [
{
"system": snomed_url,
"code": self.snomed_code_doc_type.code,
"display": self.snomed_code_doc_type.display_name,
}
]
},
content=[{"attachment": self.attachment}],
category=[
{
"coding": [
{
"system": snomed_url,
"code": self.snomed_code_category.code,
"display": self.snomed_code_category.display_name,
}
]
}
],
author=[
{
"identifier": {
"system": fhir_base_url + "/ods-organization-code",
"value": self.custodian,
}
}
],
context={
"practiceSetting": {
"coding": [
{
"system": snomed_url,
"code": self.snomed_code_practice_setting.code,
"display": self.snomed_code_practice_setting.display_name,
}
]
}
},
)
return fhir_document_ref
Loading
Loading