Skip to content

Commit

Permalink
Merge branch 'main' into PRMP-1277
Browse files Browse the repository at this point in the history
  • Loading branch information
jack-nhs authored Nov 29, 2024
2 parents 321ecf0 + a7c3c9b commit f4266ac
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 36 deletions.
6 changes: 6 additions & 0 deletions lambdas/enums/nrl_sqs_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import StrEnum, auto


class NrlActionTypes(StrEnum):
CREATE = auto()
DELETE = auto()
9 changes: 9 additions & 0 deletions lambdas/enums/snomed_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import StrEnum


class SnomedCodesType(StrEnum):
LLOYD_GEORGE = "16521000000101"


class SnomedCodesCategory(StrEnum):
CARE_PLAN = "734163000"
31 changes: 18 additions & 13 deletions lambdas/handlers/manage_nrl_pointer_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

from enums.nrl_sqs_upload import NrlActionTypes
from models.nrl_fhir_document_reference import FhirDocumentReference
from models.nrl_sqs_message import NrlSqsMessage
from services.base.nhs_oauth_service import NhsOauthService
Expand Down Expand Up @@ -31,11 +32,7 @@ def lambda_handler(event, context):
ssm_service = SSMService()
oauth_service = NhsOauthService(ssm_service)
nrl_api_service = NrlApiService(ssm_service, oauth_service)
actions_options = {
"POST": nrl_api_service.create_new_pointer,
"UPDATE": nrl_api_service.update_pointer,
"DELETE": nrl_api_service.delete_pointer,
}

unhandled_messages = []

for sqs_message in sqs_messages:
Expand All @@ -50,14 +47,22 @@ def lambda_handler(event, context):
nrl_verified_message = nrl_message.model_dump(
by_alias=True, exclude_none=True, exclude_defaults=True
)
document = (
FhirDocumentReference(
**nrl_verified_message, custodian=nrl_api_service.end_user_ods_code
)
.build_fhir_dict()
.json()
)
actions_options[nrl_message.action](json.loads(document))
match nrl_message.action:
case NrlActionTypes.CREATE:
document = (
FhirDocumentReference(
**nrl_verified_message,
custodian=nrl_api_service.end_user_ods_code,
)
.build_fhir_dict()
.json()
)

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
)
except Exception as error:
unhandled_messages.append(sqs_message)
logger.info(f"Failed to process current message due to error: {error}")
Expand Down
5 changes: 5 additions & 0 deletions lambdas/services/base/sqs_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ def __init__(self, *args, **kwargs):
self.client = boto3.client("sqs", config=config)
super().__init__(*args, **kwargs)

def send_message_fifo(self, queue_url: str, message_body: str, group_id: str):
self.client.send_message(
QueueUrl=queue_url, MessageBody=message_body, MessageGroupId=group_id
)

def send_message_standard(self, queue_url: str, message_body: str):
self.client.send_message(QueueUrl=queue_url, MessageBody=message_body)

Expand Down
26 changes: 25 additions & 1 deletion lambdas/services/document_deletion_service.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import os
import uuid
from typing import Literal

from botocore.exceptions import ClientError
from enums.lambda_error import LambdaError
from enums.nrl_sqs_upload import NrlActionTypes
from enums.s3_lifecycle_tags import S3LifecycleTags
from enums.snomed_codes import SnomedCodesCategory, SnomedCodesType
from enums.supported_document_types import SupportedDocumentTypes
from models.document_reference import DocumentReference
from models.nrl_sqs_message import NrlSqsMessage
from services.base.sqs_service import SQSService
from services.document_service import DocumentService
from services.lloyd_george_stitch_job_service import LloydGeorgeStitchJobService
from utils.audit_logging_setup import LoggingService
Expand All @@ -19,6 +25,7 @@ class DocumentDeletionService:
def __init__(self):
self.document_service = DocumentService()
self.stitch_service = LloydGeorgeStitchJobService()
self.sqs_service = SQSService()

def handle_delete(
self, nhs_number: str, doc_types: list[SupportedDocumentTypes]
Expand All @@ -27,6 +34,8 @@ def handle_delete(
for doc_type in doc_types:
files_deleted += self.delete_specific_doc_type(nhs_number, doc_type)
self.delete_documents_references_in_stitch_table(nhs_number)
if SupportedDocumentTypes.LG in doc_types:
self.send_sqs_message_to_remove_pointer(nhs_number)
return files_deleted

def get_documents_references_in_storage(
Expand All @@ -41,7 +50,7 @@ def get_documents_references_in_storage(

def delete_documents_references_in_stitch_table(self, nhs_number: str):
documents_in_stitch_table = (
self.stitch_service.query_stitch_trace_with_nhs_number(nhs_number)
self.stitch_service.query_stitch_trace_with_nhs_number(nhs_number) or []
)

for record in documents_in_stitch_table:
Expand Down Expand Up @@ -77,3 +86,18 @@ def delete_specific_doc_type(
{"Results": "Failed to delete documents"},
)
raise DocumentDeletionServiceException(500, LambdaError.DocDelClient)

def send_sqs_message_to_remove_pointer(self, nhs_number: str):
delete_nrl_message = NrlSqsMessage(
nhs_number=nhs_number,
action=NrlActionTypes.DELETE,
snomed_code_doc_type=SnomedCodesType.LLOYD_GEORGE,
snomed_code_category=SnomedCodesCategory.CARE_PLAN,
)
sqs_group_id = f"NRL_delete_{uuid.uuid4()}"
nrl_queue_url = os.environ["NRL_SQS_QUEUE_URL"]
self.sqs_service.send_message_fifo(
queue_url=nrl_queue_url,
message_body=delete_nrl_message.model_dump_json(),
group_id=sqs_group_id,
)
51 changes: 44 additions & 7 deletions lambdas/services/nrl_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, ssm_service, auth_service):
retry_strategy = Retry(
total=3,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"],
allowed_methods=["GET", "POST", "DELETE", "OPTIONS"],
backoff_factor=1,
)
adapter = HTTPAdapter(max_retries=retry_strategy)
Expand All @@ -29,6 +29,7 @@ def __init__(self, ssm_service, auth_service):
self.headers = {
"Authorization": f"Bearer {self.auth_service.get_active_access_token()}",
"NHSD-End-User-Organisation-ODS": self.end_user_ods_code,
"Accept": "application/json",
}

def get_end_user_ods_code(self):
Expand All @@ -40,13 +41,11 @@ def get_end_user_ods_code(self):
def create_new_pointer(self, body, retry_on_expired: bool = True):
try:
self.set_x_request_id()
self.headers["Accept"] = "application/json"
response = self.session.post(
url=self.endpoint, headers=self.headers, json=body
)
response.raise_for_status()
logger.info("Successfully created new pointer")
self.headers.pop("Accept")
except HTTPError as e:
logger.error(e.response)
if e.response.status_code == 401 and retry_on_expired:
Expand All @@ -57,11 +56,49 @@ def create_new_pointer(self, body, retry_on_expired: bool = True):
else:
raise NrlApiException("Error while creating new NRL Pointer")

def update_pointer(self):
self.set_x_request_id()
def get_pointer(self, nhs_number, record_type=None, retry_on_expired: bool = True):
try:
self.set_x_request_id()
params = {
"subject:identifier": f"https://fhir.nhs.uk/Id/nhs-number|{nhs_number}"
}
if record_type:
params["type"] = f"http://snomed.info/sct|{record_type}"
response = self.session.get(
url=self.endpoint, params=params, headers=self.headers
)
response.raise_for_status()
return response.json()
except HTTPError as e:
logger.error(e.response.json())
if e.response.status_code == 401 and retry_on_expired:
self.headers["Authorization"] = (
f"Bearer {self.auth_service.get_active_access_token()}"
)
self.get_pointer(nhs_number, record_type, retry_on_expired=False)
else:
raise NrlApiException("Error while getting NRL Pointer")

def delete_pointer(self):
self.set_x_request_id()
def delete_pointer(self, nhs_number, record_type):
search_results = self.get_pointer(nhs_number, record_type).get("entry", [])
for entry in search_results:
self.set_x_request_id()
pointer_id = entry.get("resource", {}).get("id")
url_endpoint = self.endpoint + f"/{pointer_id}"
try:
response = self.session.delete(url=url_endpoint, headers=self.headers)
logger.info(response.json())
response.raise_for_status()
except HTTPError as e:
logger.error(e.response.json())
if e.response.status_code == 401:
self.headers["Authorization"] = (
f"Bearer {self.auth_service.get_active_access_token()}"
)
self.session.delete(url=self.endpoint, headers=self.headers)
else:
logger.error(f"Unable to delete NRL Pointer: {entry}")
continue

def set_x_request_id(self):
self.headers["X-Request-ID"] = str(uuid.uuid4())
2 changes: 2 additions & 0 deletions lambdas/tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
MOCK_PRESIGNED_URL_ROLE_ARN_VALUE = "arn:aws:iam::test123"

STITCH_METADATA_DYNAMODB_NAME_VALUE = "test_stitch_metadata"
NRL_SQS_URL = "https://sqs.us-east-1.amazonaws.com/177715257436/MyQueue"


@pytest.fixture
Expand Down Expand Up @@ -167,6 +168,7 @@ def set_env(monkeypatch):
)
monkeypatch.setenv("NRL_API_ENDPOINT", FAKE_URL)
monkeypatch.setenv("NRL_END_USER_ODS_CODE", "test_nrl_user_ods_ssm_key")
monkeypatch.setenv("NRL_SQS_QUEUE_URL", NRL_SQS_URL)


EXPECTED_PARSED_PATIENT_BASE_CASE = PatientDetails(
Expand Down
17 changes: 5 additions & 12 deletions lambdas/tests/unit/handlers/test_manage_nrl_pointer_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def mock_service(mocker):
return mocked_instance


def build_test_sqs_message(action="POST"):
def build_test_sqs_message(action="create"):
SQS_Message = {
"nhs_number": "123456789",
"snomed_code_doc_type": "16521000000101",
Expand All @@ -31,22 +31,15 @@ def build_test_sqs_message(action="POST"):


def test_process_event_with_one_message(mock_service, context, set_env):
event = {"Records": [build_test_sqs_message("POST")]}
event = {"Records": [build_test_sqs_message("create")]}

lambda_handler(event, context)

mock_service.create_new_pointer.assert_called_once()


def test_process_update_event_with_one_message(mock_service, context, set_env):
event = {"Records": [build_test_sqs_message("UPDATE")]}
lambda_handler(event, context)

mock_service.update_pointer.assert_called_once()


def test_process_delete_event_with_one_message(mock_service, context, set_env):
event = {"Records": [build_test_sqs_message("DELETE")]}
event = {"Records": [build_test_sqs_message("delete")]}

lambda_handler(event, context)

Expand All @@ -55,7 +48,7 @@ def test_process_delete_event_with_one_message(mock_service, context, set_env):

def test_process_event_with_multiple_messages(mock_service, context, set_env):
event = {
"Records": [build_test_sqs_message("POST"), build_test_sqs_message("DELETE")]
"Records": [build_test_sqs_message("create"), build_test_sqs_message("delete")]
}

lambda_handler(event, context)
Expand All @@ -65,7 +58,7 @@ def test_process_event_with_multiple_messages(mock_service, context, set_env):


def test_failed_to_create_a_pointer(mock_service, context, set_env, caplog):
event = {"Records": [build_test_sqs_message("POST")]}
event = {"Records": [build_test_sqs_message("create")]}
mock_service.create_new_pointer.side_effect = NrlApiException("test exception")

lambda_handler(event, context)
Expand Down
31 changes: 30 additions & 1 deletion lambdas/tests/unit/services/base/test_sqs_service.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import json

import pytest
from services.base.sqs_service import SQSService
from tests.unit.conftest import MOCK_LG_METADATA_SQS_QUEUE, TEST_NHS_NUMBER


def test_send_message_with_nhs_number_attr(set_env, mocker):
@pytest.fixture()
def mocked_sqs_client(mocker):
mocked_sqs_client = mocker.MagicMock()

def return_mock(service_name, **_kwargs):
Expand All @@ -13,7 +15,16 @@ def return_mock(service_name, **_kwargs):

mocker.patch("boto3.client", side_effect=return_mock)

return mocked_sqs_client


@pytest.fixture()
def service(mocker, mocked_sqs_client):
service = SQSService()
return service


def test_send_message_with_nhs_number_attr(set_env, mocked_sqs_client, service):

test_message_body = json.dumps(
{"NHS-NO": "1234567890", "files": ["file1.pdf", "file2.pdf"]}
Expand All @@ -34,3 +45,21 @@ def return_mock(service_name, **_kwargs):
MessageBody=test_message_body,
MessageGroupId="test_group_id",
)


def test_send_message_fifo(set_env, mocked_sqs_client, service):
test_message_body = json.dumps(
{"NHS-NO": "1234567890", "files": ["file1.pdf", "file2.pdf"]}
)

service.send_message_fifo(
group_id="test_group_id",
queue_url=MOCK_LG_METADATA_SQS_QUEUE,
message_body=test_message_body,
)

mocked_sqs_client.send_message.assert_called_with(
QueueUrl=MOCK_LG_METADATA_SQS_QUEUE,
MessageBody=test_message_body,
MessageGroupId="test_group_id",
)
32 changes: 30 additions & 2 deletions lambdas/tests/unit/services/test_document_deletion_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
from enums.s3_lifecycle_tags import S3LifecycleTags
from enums.supported_document_types import SupportedDocumentTypes
from services.document_deletion_service import DocumentDeletionService
from tests.unit.conftest import MOCK_ARF_TABLE_NAME, MOCK_LG_TABLE_NAME, TEST_NHS_NUMBER
from tests.unit.conftest import (
MOCK_ARF_TABLE_NAME,
MOCK_LG_TABLE_NAME,
NRL_SQS_URL,
TEST_NHS_NUMBER,
)
from tests.unit.helpers.data.test_documents import (
create_test_doc_store_refs,
create_test_lloyd_george_doc_store_refs,
Expand Down Expand Up @@ -35,7 +40,7 @@ def mocked_document_query(
def mock_deletion_service(set_env, mocker):
mocker.patch("services.document_deletion_service.DocumentService")
mocker.patch("services.document_deletion_service.LloydGeorgeStitchJobService")

mocker.patch("services.document_deletion_service.SQSService")
yield DocumentDeletionService()


Expand Down Expand Up @@ -215,3 +220,26 @@ def test_delete_documents_references_in_stitch_table(mocker, mock_deletion_servi
mock_deletion_service.document_service.dynamo_service.update_item.assert_has_calls(
expected_calls
)


def test_send_sqs_message_to_remove_pointer(mocker, mock_deletion_service):
mocker.patch("uuid.uuid4", return_value="test_uuid")

expected_message_body = (
'{{"nhs_number":"{}",'
'"snomed_code_doc_type":"16521000000101",'
'"snomed_code_category":"734163000",'
'"description":"",'
'"attachment":null,'
'"action":"delete"}}'
).format(TEST_NHS_NUMBER)

mock_deletion_service.send_sqs_message_to_remove_pointer(TEST_NHS_NUMBER)

assert mock_deletion_service.sqs_service.send_message_fifo.call_count == 1

mock_deletion_service.sqs_service.send_message_fifo.assert_called_with(
group_id="NRL_delete_test_uuid",
message_body=expected_message_body,
queue_url=NRL_SQS_URL,
)
Loading

0 comments on commit f4266ac

Please sign in to comment.