From 2067d01883545f3d7780d0cc1988481728d31d41 Mon Sep 17 00:00:00 2001 From: rikuke <33894149+rikuke@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:44:08 +0300 Subject: [PATCH] Hl 1283 ahjo errors (#3208) * feat: log validation errors returned by Ahjo * feat: add error_from_ahjo to ahjo_status * feat: save error received from Ahjo callback * feat: errors from ahjo request to handler's JSON --- .../api/v1/ahjo_integration_views.py | 3 ++ .../api/v1/serializers/ahjo_callback.py | 1 + .../api/v1/serializers/ahjo_status.py | 9 +++++ .../api/v1/serializers/application.py | 16 +++++++++ .../0078_ahjostatus_error_from_ahjo.py | 17 ++++++++++ backend/benefit/applications/models.py | 3 ++ .../applications/services/ahjo_client.py | 23 +++++++++++-- .../tests/test_ahjo_integration.py | 34 ++++++++++++++++++- 8 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 backend/benefit/applications/api/v1/serializers/ahjo_status.py create mode 100644 backend/benefit/applications/migrations/0078_ahjostatus_error_from_ahjo.py diff --git a/backend/benefit/applications/api/v1/ahjo_integration_views.py b/backend/benefit/applications/api/v1/ahjo_integration_views.py index e9bfe7b9d8..e9eca62348 100644 --- a/backend/benefit/applications/api/v1/ahjo_integration_views.py +++ b/backend/benefit/applications/api/v1/ahjo_integration_views.py @@ -253,6 +253,9 @@ def handle_failure_callback( self, application: Application, callback_data: dict ) -> Response: self._log_failure_details(application, callback_data) + latest_status = application.ahjo_status.latest() + latest_status.error_from_ahjo = callback_data.get("failureDetails", None) + latest_status.save() return Response( {"message": "Callback received but request was unsuccessful at AHJO"}, status=status.HTTP_200_OK, diff --git a/backend/benefit/applications/api/v1/serializers/ahjo_callback.py b/backend/benefit/applications/api/v1/serializers/ahjo_callback.py index 09116e670e..4be8a83a32 100644 --- a/backend/benefit/applications/api/v1/serializers/ahjo_callback.py +++ b/backend/benefit/applications/api/v1/serializers/ahjo_callback.py @@ -7,6 +7,7 @@ class AhjoCallbackSerializer(serializers.Serializer): caseId = serializers.CharField(required=False) caseGuid = serializers.UUIDField(format="hex_verbose", required=False) records = serializers.ListField(required=False) + failureDetails = serializers.ListField(required=False) # You can add additional validation here if needed def validate_message(self, message): diff --git a/backend/benefit/applications/api/v1/serializers/ahjo_status.py b/backend/benefit/applications/api/v1/serializers/ahjo_status.py new file mode 100644 index 0000000000..d813262d61 --- /dev/null +++ b/backend/benefit/applications/api/v1/serializers/ahjo_status.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from applications.models import AhjoStatus + + +class AhjoStatusSerializer(serializers.ModelSerializer): + class Meta: + model = AhjoStatus + fields = ["modified_at", "error_from_ahjo"] diff --git a/backend/benefit/applications/api/v1/serializers/application.py b/backend/benefit/applications/api/v1/serializers/application.py index d498d6b40d..522ffe97e9 100755 --- a/backend/benefit/applications/api/v1/serializers/application.py +++ b/backend/benefit/applications/api/v1/serializers/application.py @@ -13,6 +13,7 @@ from rest_framework import serializers from rest_framework.exceptions import PermissionDenied +from applications.api.v1.serializers.ahjo_status import AhjoStatusSerializer from applications.api.v1.serializers.application_alteration import ( ApplicantApplicationAlterationSerializer, HandlerApplicationAlterationSerializer, @@ -1549,6 +1550,7 @@ class HandlerApplicationSerializer(BaseApplicationSerializer): * latest_decision_comment * handled_at * paper_application_date + * ahjo_error """ # more status transitions @@ -1612,6 +1614,19 @@ class HandlerApplicationSerializer(BaseApplicationSerializer): ), ) + ahjo_error = serializers.SerializerMethodField() + + def get_latest_ahjo_error(self, obj) -> Union[Dict, None]: + """Get the latest Ahjo error for the application""" + try: + status = obj.ahjo_status.latest() + except AhjoStatus.DoesNotExist: + return None + return AhjoStatusSerializer(status).data + + def get_ahjo_error(self, obj): + return self.get_latest_ahjo_error(obj) + def get_changes(self, obj): return { "handler": get_application_change_history_made_by_handler(obj), @@ -1663,6 +1678,7 @@ class Meta(BaseApplicationSerializer.Meta): "decision_proposal_draft", "handler", "handled_by_ahjo_automation", + "ahjo_error", ] read_only_fields = BaseApplicationSerializer.Meta.read_only_fields + [ "latest_decision_comment", diff --git a/backend/benefit/applications/migrations/0078_ahjostatus_error_from_ahjo.py b/backend/benefit/applications/migrations/0078_ahjostatus_error_from_ahjo.py new file mode 100644 index 0000000000..ab32499417 --- /dev/null +++ b/backend/benefit/applications/migrations/0078_ahjostatus_error_from_ahjo.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.11 on 2024-08-12 09:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("applications", "0077_alter_attachment_attachment_file"), + ] + + operations = [ + migrations.AddField( + model_name="ahjostatus", + name="error_from_ahjo", + field=models.JSONField(null=True), + ), + ] diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py index 9659590b9f..07423e2d28 100755 --- a/backend/benefit/applications/models.py +++ b/backend/benefit/applications/models.py @@ -1162,6 +1162,9 @@ class AhjoStatus(TimeStampedModel): related_name="ahjo_status", on_delete=models.CASCADE, ) + error_from_ahjo = JSONField( + null=True, + ) def __str__(self): return self.status diff --git a/backend/benefit/applications/services/ahjo_client.py b/backend/benefit/applications/services/ahjo_client.py index 0b910790bd..6993ebc04e 100644 --- a/backend/benefit/applications/services/ahjo_client.py +++ b/backend/benefit/applications/services/ahjo_client.py @@ -231,9 +231,7 @@ def send_request_to_ahjo( f"Missing Ahjo case id for application {self._request.application.application_number}: {e}" ) except requests.exceptions.HTTPError as e: - LOGGER.error( - f"A HTTP error occurred while sending {self._request} to Ahjo: {e}" - ) + self.handle_http_error(e) except requests.exceptions.RequestException as e: LOGGER.error( f"A network error occurred while sending {self._request} to Ahjo: {e}" @@ -243,3 +241,22 @@ def send_request_to_ahjo( f"An error occurred while sending {self._request} to Ahjo: {e}" ) return None, None + + def handle_http_error(self, e: requests.exceptions.HTTPError) -> Tuple[None, dict]: + """Handle HTTP errors that occur when sending a request to Ahjo. + Also log any validation errors received from Ahjo. + """ + error_response = e.response + application_number = self._request.application.application_number + try: + error_json = error_response.json() + except json.JSONDecodeError: + error_json = None + + error_message = f"A HTTP error occurred while sending {self._request} for application \ +{application_number} to Ahjo: {e}" + + if error_json: + error_message += f" Error message: {error_json}" + + LOGGER.error(error_message) diff --git a/backend/benefit/applications/tests/test_ahjo_integration.py b/backend/benefit/applications/tests/test_ahjo_integration.py index ef28616a7c..a859e30655 100644 --- a/backend/benefit/applications/tests/test_ahjo_integration.py +++ b/backend/benefit/applications/tests/test_ahjo_integration.py @@ -561,6 +561,22 @@ def test_subscribe_to_decisions_callback_success( assert decided_application.ahjo_status.latest().status == status_after_callback +@pytest.mark.parametrize( + "request_type, last_ahjo_status, failure_details", + [ + ( + AhjoRequestType.OPEN_CASE, + AhjoStatusEnum.REQUEST_TO_OPEN_CASE_SENT, + [ + { + "id": "INVALID_RECORD_TYPE", + "message": "Asiakirjan tyyppi ei ole sallittu.", + "context": "(Tähän tulisi tietoa virheen esiintymispaikasta jos mahdollista antaa.)", + } + ], + ), + ], +) @pytest.mark.django_db def test_ahjo_open_case_callback_failure( ahjo_client, @@ -568,15 +584,25 @@ def test_ahjo_open_case_callback_failure( decided_application, settings, ahjo_callback_payload, + request_type, + last_ahjo_status, + failure_details, ): ahjo_callback_payload.pop("caseId", None) ahjo_callback_payload.pop("caseGuid", None) ahjo_callback_payload["message"] = AhjoCallBackStatus.FAILURE + AhjoStatus.objects.create( + application=decided_application, + status=last_ahjo_status, + ) + + ahjo_callback_payload["failureDetails"] = failure_details + url = reverse( "ahjo_callback_url", kwargs={ - "request_type": AhjoRequestType.OPEN_CASE, + "request_type": request_type, "uuid": decided_application.id, }, ) @@ -589,6 +615,12 @@ def test_ahjo_open_case_callback_failure( "message": "Callback received but request was unsuccessful at AHJO" } + decided_application.refresh_from_db() + + latest_status = decided_application.ahjo_status.latest() + assert latest_status.status == last_ahjo_status + assert latest_status.error_from_ahjo == ahjo_callback_payload["failureDetails"] + @pytest.mark.parametrize( "request_type",