Skip to content

Commit 14626b1

Browse files
authored
Merge branch 'main' into bugfix/eja-eli-327-fix-flipflopping-permissions
2 parents 5d10020 + 6ef25eb commit 14626b1

10 files changed

Lines changed: 748 additions & 228 deletions

File tree

infrastructure/modules/lambda/lambda.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
1313

1414
runtime = "python3.13"
1515
timeout = 30
16-
memory_size = 128 # Default
16+
memory_size = 2048
1717

1818
environment {
1919
variables = {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import json
2+
import logging
3+
import uuid
4+
from datetime import UTC, datetime
5+
from enum import Enum
6+
from http import HTTPStatus
7+
from typing import Any
8+
9+
from fhir.resources.operationoutcome import OperationOutcome, OperationOutcomeIssue
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class FHIRIssueSeverity(str, Enum):
15+
FATAL = "fatal"
16+
ERROR = "error"
17+
WARNING = "warning"
18+
INFORMATION = "information"
19+
20+
21+
class FHIRIssueCode(str, Enum):
22+
FORBIDDEN = "forbidden"
23+
PROCESSING = "processing"
24+
VALUE = "value"
25+
26+
27+
class FHIRSpineErrorCode(str, Enum):
28+
INVALID_NHS_NUMBER = "INVALID_NHS_NUMBER"
29+
INVALID_PARAMETER = "INVALID_PARAMETER"
30+
INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
31+
REFERENCE_NOT_FOUND = "REFERENCE_NOT_FOUND"
32+
33+
34+
class APIErrorResponse:
35+
def __init__( # noqa: PLR0913
36+
self,
37+
status_code: HTTPStatus,
38+
fhir_issue_code: FHIRIssueCode,
39+
fhir_issue_severity: FHIRIssueSeverity,
40+
fhir_coding_system: str,
41+
fhir_error_code: str,
42+
fhir_display_message: str,
43+
) -> None:
44+
self.status_code = status_code
45+
self.fhir_issue_code = fhir_issue_code
46+
self.fhir_issue_severity = fhir_issue_severity
47+
self.fhir_coding_system = fhir_coding_system
48+
self.fhir_error_code = fhir_error_code
49+
self.fhir_display_message = fhir_display_message
50+
51+
def build_operation_outcome_issue(self, diagnostics: str, location: list[str] | None) -> OperationOutcomeIssue:
52+
details = {
53+
"coding": [
54+
{
55+
"system": self.fhir_coding_system,
56+
"code": self.fhir_error_code,
57+
"display": self.fhir_display_message,
58+
}
59+
]
60+
}
61+
return OperationOutcomeIssue(
62+
severity=self.fhir_issue_severity,
63+
code=self.fhir_issue_code,
64+
diagnostics=diagnostics,
65+
location=location,
66+
details=details,
67+
) # pyright: ignore[reportCallIssue]
68+
69+
def generate_response(self, diagnostics: str, location_param: str | None = None) -> dict[str, Any]:
70+
issue_location = [f"parameters/{location_param}"] if location_param else None
71+
72+
problem = OperationOutcome(
73+
id=str(uuid.uuid4()),
74+
meta={"lastUpdated": datetime.now(UTC)},
75+
issue=[self.build_operation_outcome_issue(diagnostics, issue_location)],
76+
) # pyright: ignore[reportCallIssue]
77+
78+
response_body = json.dumps(problem.model_dump(by_alias=True, mode="json"))
79+
80+
return {
81+
"statusCode": self.status_code,
82+
"headers": {"Content-Type": "application/fhir+json"},
83+
"body": response_body,
84+
}
85+
86+
def log_and_generate_response(
87+
self, log_message: str, diagnostics: str, location_param: str | None = None
88+
) -> dict[str, Any]:
89+
logger.error(log_message)
90+
return self.generate_response(diagnostics, location_param)
91+
92+
93+
INVALID_INCLUDE_ACTIONS_ERROR = APIErrorResponse(
94+
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
95+
fhir_issue_code=FHIRIssueCode.VALUE,
96+
fhir_issue_severity=FHIRIssueSeverity.ERROR,
97+
fhir_coding_system="https://fhir.nhs.uk/STU3/ValueSet/Spine-ErrorOrWarningCode-1",
98+
fhir_error_code=FHIRSpineErrorCode.INVALID_PARAMETER,
99+
fhir_display_message="The supplied value was not recognised by the API.",
100+
)
101+
102+
INVALID_CATEGORY_ERROR = APIErrorResponse(
103+
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
104+
fhir_issue_code=FHIRIssueCode.VALUE,
105+
fhir_issue_severity=FHIRIssueSeverity.ERROR,
106+
fhir_coding_system="https://fhir.nhs.uk/STU3/ValueSet/Spine-ErrorOrWarningCode-1",
107+
fhir_error_code=FHIRSpineErrorCode.INVALID_PARAMETER,
108+
fhir_display_message="The supplied category was not recognised by the API.",
109+
)
110+
111+
INVALID_CONDITION_FORMAT_ERROR = APIErrorResponse(
112+
status_code=HTTPStatus.BAD_REQUEST,
113+
fhir_issue_code=FHIRIssueCode.VALUE,
114+
fhir_issue_severity=FHIRIssueSeverity.ERROR,
115+
fhir_coding_system="https://fhir.nhs.uk/STU3/ValueSet/Spine-ErrorOrWarningCode-1",
116+
fhir_error_code=FHIRSpineErrorCode.INVALID_PARAMETER,
117+
fhir_display_message="The given conditions were not in the expected format.",
118+
)
119+
120+
NHS_NUMBER_NOT_FOUND_ERROR = APIErrorResponse(
121+
status_code=HTTPStatus.NOT_FOUND,
122+
fhir_issue_code=FHIRIssueCode.PROCESSING,
123+
fhir_issue_severity=FHIRIssueSeverity.ERROR,
124+
fhir_coding_system="https://fhir.nhs.uk/STU3/ValueSet/Spine-ErrorOrWarningCode-1",
125+
fhir_error_code=FHIRSpineErrorCode.REFERENCE_NOT_FOUND,
126+
fhir_display_message="The given NHS number was not found in our datasets. "
127+
"This could be because the number is incorrect or "
128+
"some other reason we cannot process that number.",
129+
)
130+
131+
INTERNAL_SERVER_ERROR = APIErrorResponse(
132+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
133+
fhir_issue_code=FHIRIssueCode.PROCESSING,
134+
fhir_issue_severity=FHIRIssueSeverity.ERROR,
135+
fhir_coding_system="https://fhir.nhs.uk/STU3/ValueSet/Spine-ErrorOrWarningCode-1",
136+
fhir_error_code=FHIRSpineErrorCode.INTERNAL_SERVER_ERROR,
137+
fhir_display_message="An unexpected internal server error occurred.",
138+
)
139+
140+
NHS_NUMBER_MISMATCH_ERROR = APIErrorResponse(
141+
status_code=HTTPStatus.FORBIDDEN,
142+
fhir_issue_code=FHIRIssueCode.FORBIDDEN,
143+
fhir_issue_severity=FHIRIssueSeverity.ERROR,
144+
fhir_coding_system="https://fhir.nhs.uk/STU3/ValueSet/Spine-ErrorOrWarningCode-1",
145+
fhir_error_code=FHIRSpineErrorCode.INVALID_NHS_NUMBER,
146+
fhir_display_message="The provided NHS number does not match the record.",
147+
)

src/eligibility_signposting_api/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from eligibility_signposting_api.config.config import config, init_logging
1212
from eligibility_signposting_api.error_handler import handle_exception
1313
from eligibility_signposting_api.views import eligibility_blueprint
14-
from eligibility_signposting_api.wrapper import validate_matching_nhs_number
14+
from eligibility_signposting_api.wrapper import validate_request_params
1515

1616
init_logging()
1717
logger = logging.getLogger(__name__)
@@ -23,7 +23,7 @@ def main() -> None: # pragma: no cover
2323
app.run(debug=config()["log_level"] == logging.DEBUG)
2424

2525

26-
@validate_matching_nhs_number()
26+
@validate_request_params()
2727
def lambda_handler(event: LambdaEvent, context: LambdaContext) -> dict[str, Any]: # pragma: no cover
2828
"""Run the Flask app as an AWS Lambda."""
2929
app = create_app()
Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import logging
22
import traceback
3-
from http import HTTPStatus
43

5-
from fhir.resources.operationoutcome import OperationOutcome, OperationOutcomeIssue
64
from flask import make_response
75
from flask.typing import ResponseReturnValue
86
from werkzeug.exceptions import HTTPException
97

8+
from eligibility_signposting_api.api_error_response import INTERNAL_SERVER_ERROR
9+
1010
logger = logging.getLogger(__name__)
1111

1212

@@ -17,11 +17,8 @@ def handle_exception(e: Exception) -> ResponseReturnValue | HTTPException:
1717
if isinstance(e, HTTPException):
1818
return e
1919

20-
problem = OperationOutcome(
21-
issue=[
22-
OperationOutcomeIssue(
23-
severity="severe", code="unexpected", diagnostics="".join(traceback.format_exception(e))
24-
) # pyright: ignore[reportCallIssue]
25-
]
20+
full_traceback = "".join(traceback.format_exception(e))
21+
response = INTERNAL_SERVER_ERROR.log_and_generate_response(
22+
log_message=f"An unexpected error occurred: {full_traceback}", diagnostics="An unexpected error occurred."
2623
)
27-
return make_response(problem.model_dump(by_alias=True), HTTPStatus.INTERNAL_SERVER_ERROR)
24+
return make_response(response.get("body"), response.get("statusCode"))

src/eligibility_signposting_api/views/eligibility.py

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@
22
import uuid
33
from datetime import UTC, datetime
44
from http import HTTPStatus
5-
from typing import Never
65

7-
from fhir.resources.R4B.operationoutcome import OperationOutcome, OperationOutcomeIssue
86
from flask import Blueprint, make_response, request
97
from flask.typing import ResponseReturnValue
108
from wireup import Injected
119

10+
from eligibility_signposting_api.api_error_response import NHS_NUMBER_NOT_FOUND_ERROR
1211
from eligibility_signposting_api.audit.audit_context import AuditContext
1312
from eligibility_signposting_api.audit.audit_service import AuditService
1413
from eligibility_signposting_api.model.eligibility import Condition, EligibilityStatus, NHSNumber, Status
1514
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
16-
from eligibility_signposting_api.services.eligibility_services import InvalidQueryParamError
1715
from eligibility_signposting_api.views.response_model import eligibility
1816
from eligibility_signposting_api.views.response_model.eligibility import ProcessedSuggestion
1917

@@ -50,8 +48,6 @@ def check_eligibility(
5048
eligibility_status = eligibility_service.get_eligibility_status(
5149
nhs_number, include_actions_flag=get_include_actions_flag()
5250
)
53-
except InvalidQueryParamError:
54-
return handle_invalid_query_param_error()
5551
except UnknownPersonError:
5652
return handle_unknown_person_error(nhs_number)
5753
else:
@@ -62,49 +58,21 @@ def check_eligibility(
6258
)
6359

6460

65-
def handle_unknown_person_error(nhs_number: NHSNumber) -> ResponseReturnValue:
66-
logger.debug("nhs_number %r not found", nhs_number, extra={"nhs_number": nhs_number})
67-
problem = OperationOutcome(
68-
issue=[
69-
OperationOutcomeIssue(
70-
severity="information",
71-
code="nhs-number-not-found",
72-
diagnostics=f'NHS Number "{nhs_number}" not found.',
73-
) # pyright: ignore[reportCallIssue]
74-
]
75-
)
76-
return make_response(problem.model_dump(by_alias=True, mode="json"), HTTPStatus.NOT_FOUND)
61+
def get_include_actions_flag() -> bool:
62+
if not request.args.get("includeActions"):
63+
logger.info("Defaulting includeActions query param to Y as no value was provided")
64+
include_actions_flag = True
65+
else:
66+
include_actions_flag = request.args.get("includeActions") == "Y"
67+
return include_actions_flag
7768

7869

79-
def handle_invalid_query_param_error() -> ResponseReturnValue:
80-
logger.debug(
81-
"Invalid query param",
82-
)
83-
problem = OperationOutcome(
84-
issue=[
85-
OperationOutcomeIssue(
86-
severity="error",
87-
code="invalid",
88-
diagnostics="Invalid query param key or value.",
89-
) # pyright: ignore[reportCallIssue]
90-
]
70+
def handle_unknown_person_error(nhs_number: NHSNumber) -> ResponseReturnValue:
71+
diagnostics = f"NHS Number '{nhs_number}' was not recognised by the Eligibility Signposting API"
72+
response = NHS_NUMBER_NOT_FOUND_ERROR.log_and_generate_response(
73+
log_message=diagnostics, diagnostics=diagnostics, location_param="id"
9174
)
92-
return make_response(problem.model_dump(by_alias=True, mode="json"), HTTPStatus.BAD_REQUEST)
93-
94-
95-
def get_include_actions_flag() -> bool:
96-
include_actions = request.args.get("includeActions")
97-
if "includeActions" in request.args:
98-
normalized = include_actions.upper() if include_actions is not None else None
99-
if normalized not in ("Y", "N", None):
100-
raise_invalid_query_param_error()
101-
elif len(request.args) != 0 and "includeActions" not in request.args:
102-
raise_invalid_query_param_error()
103-
return include_actions is None or include_actions.upper() == "Y"
104-
105-
106-
def raise_invalid_query_param_error() -> Never:
107-
raise InvalidQueryParamError
75+
return make_response(response.get("body"), response.get("statusCode"))
10876

10977

11078
def build_eligibility_response(eligibility_status: EligibilityStatus) -> eligibility.EligibilityResponse:

0 commit comments

Comments
 (0)