Skip to content

Commit 377b425

Browse files
authored
VED-834 Refactor delete imms journey (#932)
1 parent 85d0491 commit 377b425

File tree

13 files changed

+146
-152
lines changed

13 files changed

+146
-152
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,17 @@ Steps:
130130
```
131131
132132
7. Install poetry
133+
133134
```
134135
pip install poetry
135136
```
136137
138+
8. Install pre-commit hooks. Ensure you have installed nodejs at the same version or later as per .tool-versions and
139+
then, from the repo root, run:
140+
```
141+
npm install
142+
```
143+
137144
### Setting up a virtual environment with poetry
138145
139146
The steps below must be performed in each Lambda function folder and e2e folder to ensure the environment is correctly configured.

backend/src/controller/aws_apig_response_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Optional
55

66

7-
def create_response(status_code: int, body: Optional[dict | str] = None, headers: Optional[dict] = None):
7+
def create_response(status_code: int, body: Optional[dict | str] = None, headers: Optional[dict] = None) -> dict:
88
"""Creates response body as per Lambda -> API Gateway proxy integration"""
99
if body is not None:
1010
if isinstance(body, dict):

backend/src/controller/fhir_api_exception_handler.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from controller.aws_apig_response_utils import create_response
1010
from models.errors import (
1111
Code,
12+
InvalidImmunizationId,
1213
ResourceNotFoundError,
1314
Severity,
1415
UnauthorizedError,
@@ -17,6 +18,7 @@
1718
)
1819

1920
_CUSTOM_EXCEPTION_TO_STATUS_MAP: dict[Type[Exception], int] = {
21+
InvalidImmunizationId: 400,
2022
UnauthorizedError: 403,
2123
UnauthorizedVaxError: 403,
2224
ResourceNotFoundError: 404,

backend/src/controller/fhir_controller.py

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
from models.errors import (
2020
Code,
2121
IdentifierDuplicationError,
22+
InvalidImmunizationId,
2223
ParameterException,
23-
ResourceNotFoundError,
2424
Severity,
2525
UnauthorizedError,
2626
UnauthorizedVaxError,
@@ -96,8 +96,8 @@ def get_immunization_by_identifier(self, aws_event) -> dict:
9696
def get_immunization_by_id(self, aws_event: APIGatewayProxyEventV1) -> dict:
9797
imms_id = get_path_parameter(aws_event, "id")
9898

99-
if id_error := self._validate_id(imms_id):
100-
return create_response(400, id_error)
99+
if not self._is_valid_immunization_id(imms_id):
100+
raise InvalidImmunizationId()
101101

102102
supplier_system = get_supplier_system_header(aws_event)
103103

@@ -157,10 +157,20 @@ def update_immunization(self, aws_event):
157157

158158
supplier_system = self._identify_supplier_system(aws_event)
159159

160-
# Validate the imms id - start
161-
if id_error := self._validate_id(imms_id):
162-
return create_response(400, json.dumps(id_error))
163-
# Validate the imms id - end
160+
# Refactor to raise InvalidImmunizationId when working on VED-747
161+
if not self._is_valid_immunization_id(imms_id):
162+
return create_response(
163+
400,
164+
json.dumps(
165+
create_operation_outcome(
166+
resource_id=str(uuid.uuid4()),
167+
severity=Severity.error,
168+
code=Code.invalid,
169+
diagnostics="Validation errors: the provided event ID is either missing or not in the expected "
170+
"format.",
171+
)
172+
),
173+
)
164174

165175
# Validate the body of the request - start
166176
try:
@@ -320,31 +330,18 @@ def update_immunization(self, aws_event):
320330
except UnauthorizedVaxError as unauthorized:
321331
return create_response(403, unauthorized.to_operation_outcome())
322332

323-
def delete_immunization(self, aws_event):
324-
try:
325-
if aws_event.get("headers"):
326-
imms_id = aws_event["pathParameters"]["id"]
327-
else:
328-
raise UnauthorizedError()
329-
except UnauthorizedError as unauthorized:
330-
return create_response(403, unauthorized.to_operation_outcome())
333+
@fhir_api_exception_handler
334+
def delete_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
335+
imms_id = get_path_parameter(aws_event, "id")
331336

332-
# Validate the imms id
333-
if id_error := self._validate_id(imms_id):
334-
return create_response(400, json.dumps(id_error))
337+
if not self._is_valid_immunization_id(imms_id):
338+
raise InvalidImmunizationId()
335339

336-
supplier_system = self._identify_supplier_system(aws_event)
340+
supplier_system = get_supplier_system_header(aws_event)
337341

338-
try:
339-
self.fhir_service.delete_immunization(imms_id, supplier_system)
340-
return create_response(204)
342+
self.fhir_service.delete_immunization(imms_id, supplier_system)
341343

342-
except ResourceNotFoundError as not_found:
343-
return create_response(404, not_found.to_operation_outcome())
344-
except UnhandledResponseError as unhandled_error:
345-
return create_response(500, unhandled_error.to_operation_outcome())
346-
except UnauthorizedVaxError as unauthorized:
347-
return create_response(403, unauthorized.to_operation_outcome())
344+
return create_response(204)
348345

349346
def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
350347
try:
@@ -406,17 +403,9 @@ def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
406403
result_json_dict["total"] = 0
407404
return create_response(200, json.dumps(result_json_dict))
408405

409-
def _validate_id(self, _id: str) -> Optional[dict]:
410-
if not re.match(self.immunization_id_pattern, _id):
411-
msg = "Validation errors: the provided event ID is either missing or not in the expected format."
412-
return create_operation_outcome(
413-
resource_id=str(uuid.uuid4()),
414-
severity=Severity.error,
415-
code=Code.invalid,
416-
diagnostics=msg,
417-
)
418-
419-
return None
406+
def _is_valid_immunization_id(self, immunization_id: str) -> bool:
407+
"""Validates if the given unique Immunization ID is valid."""
408+
return False if not re.match(self.immunization_id_pattern, immunization_id) else True
420409

421410
def _validate_identifier_system(self, _id: str, _elements: str) -> Optional[dict]:
422411
if not _id:

backend/src/delete_imms_handler.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import argparse
22
import logging
33
import pprint
4-
import uuid
54

6-
from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE
7-
from controller.aws_apig_response_utils import create_response
85
from controller.fhir_controller import FhirController, make_controller
96
from log_structure import function_info
10-
from models.errors import Code, Severity, create_operation_outcome
117

128
logging.basicConfig(level="INFO")
139
logger = logging.getLogger()
@@ -19,17 +15,7 @@ def delete_imms_handler(event, _context):
1915

2016

2117
def delete_immunization(event, controller: FhirController):
22-
try:
23-
return controller.delete_immunization(event)
24-
except Exception: # pylint: disable = broad-exception-caught
25-
logger.exception("Unhandled exception")
26-
exp_error = create_operation_outcome(
27-
resource_id=str(uuid.uuid4()),
28-
severity=Severity.error,
29-
code=Code.server_error,
30-
diagnostics=GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE,
31-
)
32-
return create_response(500, exp_error)
18+
return controller.delete_immunization(event)
3319

3420

3521
if __name__ == "__main__":

backend/src/models/errors.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ def to_operation_outcome(self) -> dict:
127127
pass
128128

129129

130+
@dataclass
131+
class InvalidImmunizationId(ValidationError):
132+
"""Use this when the unique Immunization ID is invalid"""
133+
134+
def to_operation_outcome(self) -> dict:
135+
return create_operation_outcome(
136+
resource_id=str(uuid.uuid4()),
137+
severity=Severity.error,
138+
code=Code.invalid,
139+
diagnostics="Validation errors: the provided event ID is either missing or not in the expected format.",
140+
)
141+
142+
130143
@dataclass
131144
class InvalidPatientId(ValidationError):
132145
"""Use this when NHS Number is invalid or doesn't exist"""

backend/src/repository/fhir_repository.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -320,22 +320,24 @@ def _perform_dynamo_update(
320320
ReturnValues="ALL_NEW",
321321
ConditionExpression=condition_expression,
322322
)
323-
return self._handle_dynamo_response(response), updated_version
324323
except botocore.exceptions.ClientError as error:
325324
# Either resource didn't exist or it has already been deleted. See ConditionExpression in the request
326325
if error.response["Error"]["Code"] == "ConditionalCheckFailedException":
327326
raise ResourceNotFoundError(resource_type="Immunization", resource_id=imms_id)
328327
else:
328+
# VED-747: consider refactoring to simply re-raise the exception and handle in controller
329329
raise UnhandledResponseError(
330330
message=f"Unhandled error from dynamodb: {error.response['Error']['Code']}",
331331
response=error.response,
332332
)
333333

334-
def delete_immunization(self, imms_id: str, supplier_system: str) -> dict:
334+
return json.loads(response["Attributes"]["Resource"]), updated_version
335+
336+
def delete_immunization(self, imms_id: str, supplier_system: str) -> None:
335337
now_timestamp = int(time.time())
336338

337339
try:
338-
response = self.table.update_item(
340+
self.table.update_item(
339341
Key={"PK": _make_immunization_pk(imms_id)},
340342
UpdateExpression=(
341343
"SET DeletedAt = :timestamp, Operation = :operation, SupplierSystem = :supplier_system"
@@ -345,22 +347,16 @@ def delete_immunization(self, imms_id: str, supplier_system: str) -> dict:
345347
":operation": "DELETE",
346348
":supplier_system": supplier_system,
347349
},
348-
ReturnValues="ALL_NEW",
349350
ConditionExpression=(
350351
Attr("PK").eq(_make_immunization_pk(imms_id))
351352
& (Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated"))
352353
),
353354
)
354-
355-
return self._handle_dynamo_response(response)
356355
except botocore.exceptions.ClientError as error:
357356
if error.response["Error"]["Code"] == "ConditionalCheckFailedException":
358357
raise ResourceNotFoundError(resource_type="Immunization", resource_id=imms_id)
359358
else:
360-
raise UnhandledResponseError(
361-
message=f"Unhandled error from dynamodb: {error.response['Error']['Code']}",
362-
response=error.response,
363-
)
359+
raise error
364360

365361
def find_immunizations(self, patient_identifier: str, vaccine_types: set):
366362
"""it should find all of the specified patient's Immunization events for all of the specified vaccine_types"""
@@ -414,13 +410,6 @@ def get_all_items(self, condition, is_not_deleted):
414410

415411
return all_items
416412

417-
@staticmethod
418-
def _handle_dynamo_response(response):
419-
if response["ResponseMetadata"]["HTTPStatusCode"] == 200:
420-
return json.loads(response["Attributes"]["Resource"])
421-
else:
422-
raise UnhandledResponseError(message="Non-200 response from dynamodb", response=response)
423-
424413
@staticmethod
425414
def _vaccine_type(patientsk) -> str:
426415
parsed = [str.strip(str.lower(s)) for s in patientsk.split("#")]

backend/src/service/fhir_service.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,10 @@ def update_reinstated_immunization(
245245

246246
return UpdateOutcome.UPDATE, Immunization.parse_obj(imms), updated_version
247247

248-
def delete_immunization(self, imms_id: str, supplier_system: str) -> Immunization:
248+
def delete_immunization(self, imms_id: str, supplier_system: str) -> None:
249249
"""
250-
Delete an Immunization if it exits and return the ID back if successful.
251-
Exception will be raised if resource does not exist. Multiple calls to this method won't change
252-
the record in the database.
250+
Delete an Immunization if it exists and return the ID back if successful. An exception will be raised if the
251+
resource does not exist.
253252
"""
254253
existing_immunisation, _ = self.immunization_repo.get_immunization_and_version_by_id(imms_id)
255254

@@ -261,8 +260,7 @@ def delete_immunization(self, imms_id: str, supplier_system: str) -> Immunizatio
261260
if not self.authoriser.authorise(supplier_system, ApiOperationCode.DELETE, {vaccination_type}):
262261
raise UnauthorizedVaxError()
263262

264-
imms = self.immunization_repo.delete_immunization(imms_id, supplier_system)
265-
return Immunization.parse_obj(imms)
263+
self.immunization_repo.delete_immunization(imms_id, supplier_system)
266264

267265
@staticmethod
268266
def is_valid_date_from(immunization: dict, date_from: Union[datetime.date, None]):

0 commit comments

Comments
 (0)