Skip to content

Commit 4097546

Browse files
[PRM-442] ITOC feedback -> IM (Slack/Teams) (#747)
1 parent da92042 commit 4097546

18 files changed

+597
-72
lines changed

.github/workflows/base-lambdas-reusable-deploy-all.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ jobs:
316316
sandbox: ${{ inputs.sandbox }}
317317
lambda_handler_name: send_feedback_handler
318318
lambda_aws_name: SendFeedbackLambda
319-
lambda_layer_names: 'core_lambda_layer'
319+
lambda_layer_names: 'core_lambda_layer,alerting_lambda_layer'
320320
secrets:
321321
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}
322322

lambdas/enums/lambda_error.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ def create_error_body(self, params: Optional[dict] = None, **kwargs) -> str:
302302
"message": "Failed to fetch parameters for sending email from SSM param store",
303303
}
304304

305+
305306
"""
306307
Errors for Feature Flags lambda
307308
"""

lambdas/enums/message_templates.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import StrEnum
2+
3+
4+
class MessageTemplates(StrEnum):
5+
ITOC_FEEDBACK_TEST_SLACK = "./models/templates/itoc_slack_feedback_blocks.json"
6+
ITOC_FEEDBACK_TEST_TEAMS = "./models/templates/itoc_feedback_teams_message.json"

lambdas/handlers/send_feedback_handler.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
1+
import os
2+
13
from enums.lambda_error import LambdaError
24
from enums.logging_app_interaction import LoggingAppInteraction
5+
from models.feedback_model import Feedback
6+
from pydantic import ValidationError
37
from services.send_feedback_service import SendFeedbackService
8+
from services.send_test_feedback_service import SendTestFeedbackService
49
from utils.audit_logging_setup import LoggingService
510
from utils.decorators.ensure_env_var import ensure_environment_variables
611
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
712
from utils.decorators.override_error_check import override_error_check
813
from utils.decorators.set_audit_arg import set_request_context_for_logging
14+
from utils.exceptions import OdsErrorException
915
from utils.lambda_exceptions import SendFeedbackException
1016
from utils.lambda_response import ApiGatewayResponse
1117
from utils.request_context import request_context
1218

1319
logger = LoggingService(__name__)
20+
failure_msg = "Failed to send feedback by email"
1421

1522

1623
@set_request_context_for_logging
1724
@override_error_check
1825
@ensure_environment_variables(
19-
["FROM_EMAIL_ADDRESS", "EMAIL_SUBJECT", "EMAIL_RECIPIENT_SSM_PARAM_KEY"]
26+
[
27+
"FROM_EMAIL_ADDRESS",
28+
"EMAIL_SUBJECT",
29+
"EMAIL_RECIPIENT_SSM_PARAM_KEY",
30+
"ITOC_TESTING_SLACK_BOT_TOKEN",
31+
"ITOC_TESTING_CHANNEL_ID",
32+
"ITOC_TESTING_TEAMS_WEBHOOK",
33+
"ITOC_TESTING_ODS_CODES",
34+
]
2035
)
2136
@handle_lambda_exceptions
2237
def lambda_handler(event, context):
2338
request_context.app_interaction = LoggingAppInteraction.SEND_FEEDBACK.value
39+
ods_code = request_context.authorization.get("selected_organisation", {}).get(
40+
"org_ods_code"
41+
)
42+
if not ods_code:
43+
raise OdsErrorException("No ODS code provided")
2444

2545
logger.info("Send feedback handler triggered")
2646

@@ -32,14 +52,40 @@ def lambda_handler(event, context):
3252
)
3353
raise SendFeedbackException(400, LambdaError.FeedbackMissingBody)
3454

55+
logger.info("Parsing feedback content...")
56+
try:
57+
feedback = Feedback.model_validate_json(event_body)
58+
59+
except ValidationError as e:
60+
logger.error(e)
61+
logger.error(
62+
LambdaError.FeedbackInvalidBody.to_str(),
63+
{"result": failure_msg},
64+
)
65+
raise SendFeedbackException(400, LambdaError.FeedbackInvalidBody)
66+
3567
logger.info("Setting up SendFeedbackService...")
36-
feedback_service = SendFeedbackService()
3768

69+
feedback_service = SendFeedbackService()
3870
logger.info("SendFeedbackService ready, start processing feedback")
39-
feedback_service.process_feedback(event_body)
4071

72+
feedback_service.process_feedback(feedback)
4173
logger.info("Process complete", {"Result": "Successfully sent feedback by email"})
4274

75+
if is_itoc_test_feedback(ods_code):
76+
logger.info("Setting up SendTestFeedbackService")
77+
78+
test_feedback_service = SendTestFeedbackService()
79+
test_feedback_service.process_feedback(feedback)
80+
81+
82+
4383
return ApiGatewayResponse(
4484
200, "Feedback email processed", "POST"
4585
).create_api_gateway_response()
86+
87+
88+
def is_itoc_test_feedback(ods_code: str) -> bool:
89+
ods_codes = os.environ["ITOC_TESTING_ODS_CODES"].split(",")
90+
return ods_code in ods_codes
91+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"type": "message",
3+
"attachments": [
4+
{
5+
"contentType": "application/vnd.microsoft.card.adaptive",
6+
"contentUrl": null,
7+
"content": {
8+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
9+
"type": "AdaptiveCard",
10+
"version": "1.4",
11+
"body": [
12+
{
13+
"type": "TextBlock",
14+
"size": "Medium",
15+
"weight": "Bolder",
16+
"text": "ITOC Testing",
17+
"wrap": true
18+
},
19+
{
20+
"type": "TextBlock",
21+
"text": "Name: {{ name }}"
22+
},
23+
{
24+
"type": "TextBlock",
25+
"text": "Overall experience: {{ experience }}",
26+
"wrap": true
27+
},
28+
{
29+
"type": "TextBlock",
30+
"text": "Feedback: {{ feedback }}",
31+
"wrap": true
32+
}
33+
]
34+
}
35+
}
36+
]
37+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[
2+
{
3+
"type": "header",
4+
"text": {
5+
"type": "plain_text",
6+
"text": "ITOC Testing"
7+
}
8+
},
9+
{
10+
"type": "divider"
11+
},
12+
{
13+
"type": "section",
14+
"text": {
15+
"type": "plain_text",
16+
"text": "Name: {{ name }}"
17+
}
18+
},
19+
{
20+
"type": "section",
21+
"text": {
22+
"type": "plain_text",
23+
"text": "Overall Experience: {{ experience }}"
24+
}
25+
},
26+
{
27+
"type": "section",
28+
"text": {
29+
"type": "plain_text",
30+
"text": "Feedback: {{ feedback }}"
31+
}
32+
}
33+
]

lambdas/requirements/layers/requirements_core_lambda_layer.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ boto3==1.34.128
44
botocore==1.34.128
55
charset-normalizer==3.2.0
66
cryptography==44.0.1
7-
email-validator==2.1.0.post1
87
idna==3.7
98
inflection==0.5.1
109
jmespath==1.0.1
1110
oauthlib==3.2.2
1211
packaging==23.0.0
1312
pydantic==2.11
13+
pydantic[email]==2.11
1414
pypdf==6.0.0
1515
requests==2.32.4
1616
responses==0.23.1

lambdas/services/send_feedback_service.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from botocore.exceptions import ClientError
66
from enums.lambda_error import LambdaError
77
from models.feedback_model import Feedback
8-
from pydantic import ValidationError
98
from services.base.ssm_service import SSMService
109
from utils.audit_logging_setup import LoggingService
1110
from utils.lambda_exceptions import SendFeedbackException
@@ -21,18 +20,7 @@ def __init__(self):
2120
self.email_subject: str = os.environ["EMAIL_SUBJECT"]
2221
self.recipient_email_list: list[str] = self.get_email_recipients_list()
2322

24-
def process_feedback(self, body: str):
25-
logger.info("Parsing feedback content...")
26-
try:
27-
feedback = Feedback.model_validate_json(body)
28-
except ValidationError as e:
29-
logger.error(e)
30-
logger.error(
31-
LambdaError.FeedbackInvalidBody.to_str(),
32-
{"Result": failure_msg},
33-
)
34-
raise SendFeedbackException(400, LambdaError.FeedbackInvalidBody)
35-
23+
def process_feedback(self, feedback: Feedback):
3624
email_body_html = self.build_email_body(feedback)
3725
self.send_feedback_by_email(email_body_html)
3826

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import json
2+
import os
3+
4+
import requests
5+
from enums.message_templates import MessageTemplates
6+
from enums.supported_document_types import logger
7+
from jinja2 import Template
8+
from models.feedback_model import Feedback
9+
from requests.exceptions import HTTPError
10+
11+
12+
class SendTestFeedbackService:
13+
14+
def process_feedback(self, feedback: Feedback):
15+
self.send_itoc_feedback_via_slack(feedback)
16+
self.send_itoc_feedback_via_teams(feedback)
17+
18+
19+
def send_itoc_feedback_via_slack(self, feedback: Feedback):
20+
logger.info("Sending ITOC test feedback via slack")
21+
headers = {
22+
"Content-type": "application/json; charset=utf-8",
23+
"Authorization": "Bearer " + os.environ["ITOC_TESTING_SLACK_BOT_TOKEN"],
24+
}
25+
26+
body = {
27+
"blocks": self.compose_message(
28+
feedback, MessageTemplates.ITOC_FEEDBACK_TEST_SLACK
29+
),
30+
"channel": os.environ["ITOC_TESTING_CHANNEL_ID"],
31+
}
32+
try:
33+
response = requests.post(
34+
url="https://slack.com/api/chat.postMessage", json=body, headers=headers
35+
)
36+
response.raise_for_status()
37+
logger.info("Successfully sent ITOC test feedback via slack.")
38+
except HTTPError as e:
39+
logger.error(e)
40+
logger.error("Failed to send ITOC test feedback via slack.")
41+
42+
def send_itoc_feedback_via_teams(self, feedback: Feedback):
43+
logger.info("Sending ITOC test feedback via teams")
44+
try:
45+
payload = self.compose_message(
46+
feedback, MessageTemplates.ITOC_FEEDBACK_TEST_TEAMS
47+
)
48+
49+
headers = {"Content-type": "application/json"}
50+
51+
response = requests.post(
52+
url=os.environ["ITOC_TESTING_TEAMS_WEBHOOK"],
53+
headers=headers,
54+
json=payload,
55+
)
56+
response.raise_for_status()
57+
logger.info("ITOC test feedback successfully sent via teams.")
58+
except HTTPError as e:
59+
logger.error(e)
60+
logger.error("ITOC test feedback failed via teams.")
61+
62+
def compose_message(self, feedback: Feedback, messaging_template: str):
63+
logger.info("Composing ITOC test feedback message...")
64+
with open(messaging_template, "r") as f:
65+
template_content = f.read()
66+
67+
template = Template(template_content)
68+
69+
context = {
70+
"name": feedback.respondent_name,
71+
"experience": feedback.experience,
72+
"feedback": feedback.feedback_content,
73+
}
74+
75+
rendered_json = template.render(context)
76+
return json.loads(rendered_json)

lambdas/tests/unit/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@
121121

122122
TEST_BASE_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
123123

124+
125+
MOCK_ITOC_ODS_CODES = "Y12345,Y12"
126+
MOCK_ITOC_SLACK_CHANNEL_ID = "slack_channel_id"
127+
MOCK_ITOC_TEST_EMAIL_ADDRESS = "[email protected]"
128+
MOCK_ITOC_TEAMS_WEBHOOK = "https://webhook.team"
124129
MOCK_CONFLUENCE_URL = "https://confluence.example.com"
125130
MOCK_ALARM_HISTORY_TABLE = "test_alarm_history_table"
126131
MOCK_TEAMS_WEBHOOK = "test_teams_webhook"
@@ -203,11 +208,16 @@ def set_env(monkeypatch):
203208
"DOCUMENT_RETRIEVE_ENDPOINT_APIM", f"{APIM_API_URL}/DocumentReference"
204209
)
205210
monkeypatch.setenv("VIRUS_SCAN_STUB", "True")
211+
monkeypatch.setenv("ITOC_TESTING_SLACK_BOT_TOKEN", MOCK_SLACK_BOT_TOKEN)
212+
monkeypatch.setenv("ITOC_TESTING_CHANNEL_ID", MOCK_ITOC_SLACK_CHANNEL_ID)
213+
monkeypatch.setenv("ITOC_TESTING_EMAIL_ADDRESS", MOCK_ITOC_TEST_EMAIL_ADDRESS)
214+
monkeypatch.setenv("ITOC_TESTING_TEAMS_WEBHOOK", MOCK_ITOC_TEAMS_WEBHOOK)
206215
monkeypatch.setenv("CONFLUENCE_BASE_URL", MOCK_CONFLUENCE_URL)
207216
monkeypatch.setenv("ALARM_HISTORY_DYNAMODB_NAME", MOCK_ALARM_HISTORY_TABLE)
208217
monkeypatch.setenv("TEAMS_WEBHOOK_URL", MOCK_TEAMS_WEBHOOK)
209218
monkeypatch.setenv("SLACK_BOT_TOKEN", MOCK_SLACK_BOT_TOKEN)
210219
monkeypatch.setenv("SLACK_CHANNEL_ID", MOCK_ALERTING_SLACK_CHANNEL_ID)
220+
monkeypatch.setenv("ITOC_TESTING_ODS_CODES", MOCK_ITOC_ODS_CODES)
211221

212222

213223
EXPECTED_PARSED_PATIENT_BASE_CASE = PatientDetails(
@@ -350,3 +360,9 @@ def expect_not_to_raise(exception, message_when_fail=""):
350360
except exception:
351361
message_when_fail = message_when_fail or "DID RAISE {0}".format(exception)
352362
raise pytest.fail(message_when_fail)
363+
364+
365+
@pytest.fixture
366+
def mock_jwt_encode(mocker):
367+
decoded_token = {"selected_organisation": {"org_ods_code": "ODS123"}}
368+
yield mocker.patch("jwt.decode", return_value=decoded_token)

0 commit comments

Comments
 (0)