Skip to content

Commit fae18b2

Browse files
authored
VED-747 Refactor update immunisation endpoint (#966)
1 parent 9768338 commit fae18b2

17 files changed

+690
-1319
lines changed

backend/src/controller/aws_apig_event_utils.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from aws_lambda_typing.events import APIGatewayProxyEventV1
66

7-
from controller.constants import SUPPLIER_SYSTEM_HEADER_NAME
8-
from models.errors import UnauthorizedError
7+
from controller.constants import E_TAG_HEADER_NAME, SUPPLIER_SYSTEM_HEADER_NAME
8+
from models.errors import ResourceVersionNotProvided, UnauthorizedError
99
from utils import dict_utils
1010

1111

@@ -14,11 +14,22 @@ def get_path_parameter(event: APIGatewayProxyEventV1, param_name: str) -> str:
1414

1515

1616
def get_supplier_system_header(event: APIGatewayProxyEventV1) -> str:
17-
"""Retrieves the supplier system header from the API Gateway event"""
17+
"""Retrieves the supplier system header from the API Gateway event. Raises an Unauthorized error if not present."""
1818
supplier_system: Optional[str] = dict_utils.get_field(dict(event), "headers", SUPPLIER_SYSTEM_HEADER_NAME)
1919

2020
if supplier_system is None:
2121
# SupplierSystem header must be provided for looking up permissions
2222
raise UnauthorizedError()
2323

2424
return supplier_system
25+
26+
27+
def get_resource_version_header(event: APIGatewayProxyEventV1) -> str:
28+
"""Retrieves the resource version header from the API Gateway event. Raises a ResourceVersionNotProvided if not
29+
present."""
30+
resource_version_header: Optional[str] = dict_utils.get_field(dict(event), "headers", E_TAG_HEADER_NAME)
31+
32+
if resource_version_header is None:
33+
raise ResourceVersionNotProvided(resource_type="Immunization")
34+
35+
return resource_version_header

backend/src/controller/fhir_api_exception_handler.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111
Code,
1212
CustomValidationError,
1313
IdentifierDuplicationError,
14+
InconsistentIdentifierError,
15+
InconsistentIdError,
16+
InconsistentResourceVersion,
1417
InvalidImmunizationId,
1518
InvalidJsonError,
19+
InvalidResourceVersion,
1620
ResourceNotFoundError,
21+
ResourceVersionNotProvided,
1722
Severity,
1823
UnauthorizedError,
1924
UnauthorizedVaxError,
@@ -22,9 +27,14 @@
2227
)
2328

2429
_CUSTOM_EXCEPTION_TO_STATUS_MAP: dict[Type[Exception], int] = {
30+
InconsistentResourceVersion: 400,
31+
InconsistentIdentifierError: 400, # Identifier refers to the local FHIR identifier composed of system and value.
32+
InconsistentIdError: 400, # ID refers to the top-level ID of the FHIR resource.
2533
InvalidImmunizationId: 400,
2634
InvalidJsonError: 400,
35+
InvalidResourceVersion: 400,
2736
CustomValidationError: 400,
37+
ResourceVersionNotProvided: 400,
2838
UnauthorizedError: 403,
2939
UnauthorizedVaxError: 403,
3040
ResourceNotFoundError: 404,

backend/src/controller/fhir_controller.py

Lines changed: 25 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,22 @@
1212

1313
from controller.aws_apig_event_utils import (
1414
get_path_parameter,
15+
get_resource_version_header,
1516
get_supplier_system_header,
1617
)
1718
from controller.aws_apig_response_utils import create_response
1819
from controller.constants import E_TAG_HEADER_NAME
1920
from controller.fhir_api_exception_handler import fhir_api_exception_handler
2021
from models.errors import (
2122
Code,
22-
IdentifierDuplicationError,
23+
InconsistentIdError,
2324
InvalidImmunizationId,
2425
InvalidJsonError,
26+
InvalidResourceVersion,
2527
ParameterException,
2628
Severity,
2729
UnauthorizedError,
2830
UnauthorizedVaxError,
29-
ValidationError,
3031
create_operation_outcome,
3132
)
3233
from models.utils.generic_utils import check_keys_in_sources
@@ -124,189 +125,31 @@ def create_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
124125
headers={"Location": f"{self._API_SERVICE_URL}/Immunization/{created_resource_id}", "E-Tag": "1"},
125126
)
126127

127-
def update_immunization(self, aws_event):
128-
try:
129-
if aws_event.get("headers"):
130-
imms_id = aws_event["pathParameters"]["id"]
131-
else:
132-
raise UnauthorizedError()
133-
except UnauthorizedError as unauthorized:
134-
return create_response(403, unauthorized.to_operation_outcome())
135-
136-
supplier_system = self._identify_supplier_system(aws_event)
128+
@fhir_api_exception_handler
129+
def update_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
130+
imms_id = get_path_parameter(aws_event, "id")
131+
supplier_system = get_supplier_system_header(aws_event)
132+
resource_version = get_resource_version_header(aws_event)
137133

138-
# Refactor to raise InvalidImmunizationId when working on VED-747
139134
if not self._is_valid_immunization_id(imms_id):
140-
return create_response(
141-
400,
142-
json.dumps(
143-
create_operation_outcome(
144-
resource_id=str(uuid.uuid4()),
145-
severity=Severity.error,
146-
code=Code.invalid,
147-
diagnostics="Validation errors: the provided event ID is either missing or not in the expected "
148-
"format.",
149-
)
150-
),
151-
)
135+
raise InvalidImmunizationId()
152136

153-
# Validate the body of the request - start
154-
try:
155-
imms = json.loads(aws_event["body"], parse_float=Decimal)
156-
# Validate the imms id in the path params and body of request - start
157-
if imms.get("id") != imms_id:
158-
exp_error = create_operation_outcome(
159-
resource_id=str(uuid.uuid4()),
160-
severity=Severity.error,
161-
code=Code.invariant,
162-
diagnostics=(
163-
f"Validation errors: The provided immunization id:{imms_id} doesn't match with the content of "
164-
"the request body"
165-
),
166-
)
167-
return create_response(400, json.dumps(exp_error))
168-
# Validate the imms id in the path params and body of request - end
169-
except JSONDecodeError as e:
170-
return self._create_bad_request(f"Request's body contains malformed JSON: {e}")
171-
except Exception as e:
172-
return self._create_bad_request(f"Request's body contains string: {e}")
173-
# Validate the body of the request - end
137+
if not self._is_valid_resource_version(resource_version):
138+
raise InvalidResourceVersion(resource_version=resource_version)
174139

175-
# Validate if the imms resource does not exist - start
176140
try:
177-
existing_record = self.fhir_service.get_immunization_by_id_all(imms_id, imms)
178-
if not existing_record:
179-
exp_error = create_operation_outcome(
180-
resource_id=str(uuid.uuid4()),
181-
severity=Severity.error,
182-
code=Code.not_found,
183-
diagnostics=(
184-
f"Validation errors: The requested immunization resource with id:{imms_id} was not found."
185-
),
186-
)
187-
return create_response(404, json.dumps(exp_error))
188-
189-
if "diagnostics" in existing_record:
190-
exp_error = create_operation_outcome(
191-
resource_id=str(uuid.uuid4()),
192-
severity=Severity.error,
193-
code=Code.invariant,
194-
diagnostics=existing_record["diagnostics"],
195-
)
196-
return create_response(400, json.dumps(exp_error))
197-
except ValidationError as error:
198-
return create_response(400, error.to_operation_outcome())
199-
# Validate if the imms resource does not exist - end
141+
immunization = json.loads(aws_event["body"], parse_float=Decimal)
142+
except JSONDecodeError as e:
143+
# Consider moving the start of the message into a const
144+
raise InvalidJsonError(message=str(f"Request's body contains malformed JSON: {e}"))
200145

201-
existing_resource_version = int(existing_record["Version"])
202-
existing_resource_vacc_type = existing_record["VaccineType"]
146+
if immunization.get("id") != imms_id:
147+
raise InconsistentIdError(imms_id=imms_id)
203148

204-
try:
205-
# Validate if the imms resource to be updated is a logically deleted resource - start
206-
if existing_record["DeletedAt"]:
207-
outcome, resource, updated_version = self.fhir_service.reinstate_immunization(
208-
imms_id,
209-
imms,
210-
existing_resource_version,
211-
existing_resource_vacc_type,
212-
supplier_system,
213-
)
214-
# Validate if the imms resource to be updated is a logically deleted resource-end
215-
else:
216-
# Validate if imms resource version is part of the request - start
217-
if "E-Tag" not in aws_event["headers"]:
218-
exp_error = create_operation_outcome(
219-
resource_id=str(uuid.uuid4()),
220-
severity=Severity.error,
221-
code=Code.invariant,
222-
diagnostics=(
223-
"Validation errors: Immunization resource version not specified in the request headers"
224-
),
225-
)
226-
return create_response(400, json.dumps(exp_error))
227-
# Validate if imms resource version is part of the request - end
228-
229-
# Validate the imms resource version provided in the request headers - start
230-
try:
231-
resource_version_header = int(aws_event["headers"]["E-Tag"])
232-
except (TypeError, ValueError):
233-
resource_version = aws_event["headers"]["E-Tag"]
234-
exp_error = create_operation_outcome(
235-
resource_id=str(uuid.uuid4()),
236-
severity=Severity.error,
237-
code=Code.invariant,
238-
diagnostics=(
239-
f"Validation errors: Immunization resource version:{resource_version} in the request "
240-
"headers is invalid."
241-
),
242-
)
243-
return create_response(400, json.dumps(exp_error))
244-
# Validate the imms resource version provided in the request headers - end
245-
246-
# Validate if resource version has changed since the last retrieve - start
247-
if existing_resource_version > resource_version_header:
248-
exp_error = create_operation_outcome(
249-
resource_id=str(uuid.uuid4()),
250-
severity=Severity.error,
251-
code=Code.invariant,
252-
diagnostics=(
253-
f"Validation errors: The requested immunization resource {imms_id} has changed since the "
254-
"last retrieve."
255-
),
256-
)
257-
return create_response(400, json.dumps(exp_error))
258-
if existing_resource_version < resource_version_header:
259-
exp_error = create_operation_outcome(
260-
resource_id=str(uuid.uuid4()),
261-
severity=Severity.error,
262-
code=Code.invariant,
263-
diagnostics=(
264-
f"Validation errors: The requested immunization resource {imms_id} version is inconsistent "
265-
"with the existing version."
266-
),
267-
)
268-
return create_response(400, json.dumps(exp_error))
269-
# Validate if resource version has changed since the last retrieve - end
270-
271-
# Check if the record is reinstated record - start
272-
if existing_record["Reinstated"] is True:
273-
outcome, resource, updated_version = self.fhir_service.update_reinstated_immunization(
274-
imms_id,
275-
imms,
276-
existing_resource_version,
277-
existing_resource_vacc_type,
278-
supplier_system,
279-
)
280-
else:
281-
outcome, resource, updated_version = self.fhir_service.update_immunization(
282-
imms_id,
283-
imms,
284-
existing_resource_version,
285-
existing_resource_vacc_type,
286-
supplier_system,
287-
)
288-
289-
# Check if the record is reinstated record - end
290-
291-
# Check for errors returned on update
292-
if "diagnostics" in resource:
293-
exp_error = create_operation_outcome(
294-
resource_id=str(uuid.uuid4()),
295-
severity=Severity.error,
296-
code=Code.invariant,
297-
diagnostics=resource["diagnostics"],
298-
)
299-
return create_response(400, json.dumps(exp_error))
300-
if outcome:
301-
return create_response(
302-
200, None, {"E-Tag": updated_version}
303-
) # include e-tag here, is it not included in the response resource
304-
except ValidationError as error:
305-
return create_response(400, error.to_operation_outcome())
306-
except IdentifierDuplicationError as duplicate:
307-
return create_response(422, duplicate.to_operation_outcome())
308-
except UnauthorizedVaxError as unauthorized:
309-
return create_response(403, unauthorized.to_operation_outcome())
149+
updated_resource_version = self.fhir_service.update_immunization(
150+
imms_id, immunization, supplier_system, int(resource_version)
151+
)
152+
return create_response(200, None, {E_TAG_HEADER_NAME: updated_resource_version})
310153

311154
@fhir_api_exception_handler
312155
def delete_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
@@ -385,6 +228,10 @@ def _is_valid_immunization_id(self, immunization_id: str) -> bool:
385228
"""Validates if the given unique Immunization ID is valid."""
386229
return False if not re.match(self._IMMUNIZATION_ID_PATTERN, immunization_id) else True
387230

231+
@staticmethod
232+
def _is_valid_resource_version(resource_version: str) -> bool:
233+
return resource_version.isdigit() and int(resource_version) > 0
234+
388235
def _validate_identifier_system(self, _id: str, _elements: str) -> Optional[dict]:
389236
if not _id:
390237
return create_operation_outcome(

backend/src/models/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,5 @@ class Constants:
5656
SUPPLIER_PERMISSIONS_KEY = "supplier_permissions"
5757
VACCINE_TYPE_TO_DISEASES_HASH_KEY = "vacc_to_diseases"
5858
DISEASES_TO_VACCINE_TYPE_HASH_KEY = "diseases_to_vacc"
59+
60+
REINSTATED_RECORD_STATUS = "reinstated"

0 commit comments

Comments
 (0)