Skip to content

Commit

Permalink
fix: HL 1558 ahjo fixes (#3565)
Browse files Browse the repository at this point in the history
* fix: only accepted applications with instalments

* fix: duplicate decision proposal request bug

* chore: refactor ahjo exceptions into one place

* feat: more readable error messages for handler

* feat: log request_type on callback failure

* feat: print application numbers after requests

* feat: remove decision callback required fields

* feat: always return 200 OK after decision callback
  • Loading branch information
rikuke authored Nov 26, 2024
1 parent f40b280 commit 0ea3745
Show file tree
Hide file tree
Showing 14 changed files with 372 additions and 125 deletions.
85 changes: 53 additions & 32 deletions backend/benefit/applications/api/v1/ahjo_integration_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,14 @@
DEFAULT_AHJO_CALLBACK_ERROR_MESSAGE,
)
from applications.models import AhjoStatus, Application, ApplicationBatch, Attachment
from applications.services.ahjo.exceptions import AhjoCallbackError
from common.permissions import BFIsHandler, SafeListPermission
from shared.audit_log import audit_logging
from shared.audit_log.enums import Operation

LOGGER = logging.getLogger(__name__)


class AhjoCallbackError(Exception):
pass


class AhjoApplicationView(APIView):
permission_classes = [BFIsHandler]

Expand Down Expand Up @@ -176,7 +173,11 @@ def post(self, request, *args, **kwargs):
request, application, callback_data, request_type
)
elif callback_data["message"] == AhjoCallBackStatus.FAILURE:
return self.handle_failure_callback(application, callback_data)
return self.handle_failure_callback(
application=application,
callback_data=callback_data,
request_type=request_type,
)

def cb_info_message(
self,
Expand Down Expand Up @@ -252,7 +253,10 @@ def handle_success_callback(
)

def handle_failure_callback(
self, application: Application, callback_data: dict
self,
application: Application,
callback_data: dict,
request_type: AhjoRequestType,
) -> Response:
latest_status = application.ahjo_status.latest()

Expand All @@ -264,7 +268,11 @@ def handle_failure_callback(
"failureDetails", DEFAULT_AHJO_CALLBACK_ERROR_MESSAGE
)
latest_status.save()
self._log_failure_details(application, callback_data)
self._log_failure_details(
application=application,
callback_data=callback_data,
request_type=request_type,
)
return Response(
{"message": "Callback received but request was unsuccessful at AHJO"},
status=status.HTTP_200_OK,
Expand Down Expand Up @@ -338,9 +346,14 @@ def handle_decision_proposal_success(self, application: Application):
batch.status = ApplicationBatchStatus.AWAITING_AHJO_DECISION
batch.save()

def _log_failure_details(self, application, callback_data):
def _log_failure_details(
self,
application: Application,
callback_data: dict,
request_type: AhjoRequestType,
):
LOGGER.error(
f"Received unsuccessful callback for application {application.id} \
f"Received unsuccessful callback for {request_type} for application {application.id} \
with request_id {callback_data['requestId']}, callback data: {callback_data}"
)
for cb_record in callback_data.get("records", []):
Expand All @@ -364,29 +377,37 @@ def post(self, request, *args, **kwargs):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

callback_data = serializer.validated_data
ahjo_case_id = callback_data["caseId"]
update_type = callback_data["updatetype"]

application = get_object_or_404(
Application, ahjo_case_id=ahjo_case_id, handled_by_ahjo_automation=True
)
ahjo_case_id = callback_data.get("caseId", None)
update_type = callback_data.get("updatetype", None)

if ahjo_case_id:
try:
application = Application.objects.get(
ahjo_case_id=ahjo_case_id,
handled_by_ahjo_automation=True,
)
if update_type == AhjoDecisionUpdateType.ADDED:
AhjoStatus.objects.create(
application=application, status=AhjoStatusEnum.SIGNED_IN_AHJO
)
elif update_type == AhjoDecisionUpdateType.REMOVED:
AhjoStatus.objects.create(
application=application, status=AhjoStatusEnum.REMOVED_IN_AHJO
)

if update_type == AhjoDecisionUpdateType.ADDED:
AhjoStatus.objects.create(
application=application, status=AhjoStatusEnum.SIGNED_IN_AHJO
)
elif update_type == AhjoDecisionUpdateType.REMOVED:
AhjoStatus.objects.create(
application=application, status=AhjoStatusEnum.REMOVED_IN_AHJO
)
# TODO what to do if updatetype is "updated"
audit_logging.log(
request.user,
"",
Operation.UPDATE,
application,
additional_information=f"Decision proposal update type: {update_type} was received \
from Ahjo for application {application.application_number}",
)
# TODO what to do if updatetype is "updated"
audit_logging.log(
request.user,
"",
Operation.UPDATE,
application,
additional_information=f"Decision proposal callback of type: {update_type} was received \
from Ahjo for application {application.application_number}",
)
except Application.DoesNotExist:
# Ahjo needs a 200 OK response even if an application is not found
return Response(
{"message": "Callback received"}, status=status.HTTP_200_OK
)

return Response({"message": "Callback received"}, status=status.HTTP_200_OK)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def validate_message(self, message):


class AhjoDecisionCallbackSerializer(serializers.Serializer):
updatetype = serializers.CharField(required=True)
id = serializers.CharField(required=True)
caseId = serializers.CharField(required=True)
caseGuid = serializers.UUIDField(format="hex_verbose", required=True)
updatetype = serializers.CharField(required=False)
id = serializers.CharField(required=False)
caseId = serializers.CharField(required=False)
caseGuid = serializers.UUIDField(format="hex_verbose", required=False)
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ApplicationStatus,
)
from applications.models import AhjoStatus, Application
from applications.services.ahjo.exceptions import DecisionProposalAlreadyAcceptedError
from applications.services.ahjo_application_service import AhjoApplicationsService
from applications.services.ahjo_authentication import (
AhjoToken,
Expand Down Expand Up @@ -70,6 +71,9 @@ def add_arguments(self, parser):
not moved to the next status in the last x hours",
)

def get_application_numbers(self, applications: QuerySet[Application]) -> str:
return ", ".join(str(app.application_number) for app in applications)

def handle(self, *args, **options):
try:
ahjo_auth_token = get_token()
Expand Down Expand Up @@ -126,9 +130,8 @@ def run_requests(
successful_applications = []
failed_applications = []

application_numbers = ", ".join(
str(app.application_number) for app in applications
)
application_numbers = self.get_application_numbers(applications)

message_start = "Retrying" if self.is_retry else "Sending"

message = f"{message_start} {ahjo_request_type} request to Ahjo \
Expand All @@ -143,6 +146,7 @@ def run_requests(
ValueError: "Value error for application",
ObjectDoesNotExist: "Object not found error for application",
ImproperlyConfigured: "Improperly configured error for application",
DecisionProposalAlreadyAcceptedError: "Decision proposal error for application",
}

for application in applications:
Expand All @@ -153,11 +157,12 @@ def run_requests(
application, ahjo_auth_token
)
except tuple(exception_messages.keys()) as e:
LOGGER.error(
f"{exception_messages[type(e)]} {application.application_number}: {e}"
)
error_text = f"{exception_messages[type(e)]} {application.application_number}: {e}"
LOGGER.error(error_text)
failed_applications.append(application)
self._handle_failed_request(counter, application, ahjo_request_type)
self._handle_failed_request(
counter, application, ahjo_request_type, error_text
)
continue

if sent_application:
Expand Down Expand Up @@ -187,10 +192,14 @@ def _print_results(
elapsed_time,
):
if successful_applications:
successful_application_numbers = self.get_application_numbers(
successful_applications
)
self.stdout.write(
self.style.SUCCESS(
self._print_with_timestamp(
f"Sent {ahjo_request_type} requests for {len(successful_applications)} applications to Ahjo"
f"Sent {ahjo_request_type} requests for {len(successful_applications)} \
application(s): {successful_application_numbers} to Ahjo"
)
)
)
Expand All @@ -199,10 +208,15 @@ def _print_results(
requests took {elapsed_time} seconds to run."
)
if failed_applications:
failed_application_numbers = self.get_application_numbers(
failed_applications
)

self.stdout.write(
self.style.ERROR(
self._print_with_timestamp(
f"Failed to submit {ahjo_request_type} {len(failed_applications)} applications to Ahjo"
f"Failed to submit {ahjo_request_type} {len(failed_applications)} \
application(s): {failed_application_numbers} to Ahjo"
)
)
)
Expand Down Expand Up @@ -265,13 +279,21 @@ def _handle_successful_request(
self.stdout.write(self.style.SUCCESS(self._print_with_timestamp(success_text)))

def _handle_failed_request(
self, counter: int, application: Application, request_type: AhjoRequestType
self,
counter: int,
application: Application,
request_type: AhjoRequestType,
error_text: str = None,
):
additional_error_text = ""
if error_text:
additional_error_text = f"Error: {error_text}"

self.stdout.write(
self.style.ERROR(
self._print_with_timestamp(
f"{counter}. Failed to submit {request_type} for application {application.id} \
number: {application.application_number}, to Ahjo"
number: {application.application_number}, to Ahjo. {additional_error_text}"
)
)
)
Expand Down
5 changes: 4 additions & 1 deletion backend/benefit/applications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def with_due_instalments(self, status: InstalmentStatus):
"""Query applications with instalments with past due date and a specific status."""
return (
self.filter(
status=ApplicationStatus.ACCEPTED,
calculation__instalments__due_date__lte=timezone.now().date(),
calculation__instalments__status=status,
)
Expand Down Expand Up @@ -1251,7 +1252,9 @@ class AhjoStatus(TimeStampedModel):
)

def __str__(self):
return self.status
return f"{self.status} for application {self.application.application_number}, \
created_at: {self.created_at}, modified_at: {self.modified_at}, \
ahjo_request_id: {self.ahjo_request_id}"

class Meta:
db_table = "bf_applications_ahjo_status"
Expand Down
Empty file.
105 changes: 105 additions & 0 deletions backend/benefit/applications/services/ahjo/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from applications.models import AhjoStatus


class DecisionProposalError(Exception):
"""Custom exception for errors in the sending of decision proposals."""

pass


class DecisionProposalAlreadyAcceptedError(DecisionProposalError):
"""
Raised when a decision proposal already has been accepted in Ahjo,
but for some reason a decision proposal for the application is still being sent.
Attributes:
ahjo_status (AhjosStatus): The decision_proposal_accepted status.
"""

def __init__(self, message: str, ahjo_status: AhjoStatus) -> None:
self.message = message
self.ahjo_status = ahjo_status
super().__init__(self.message)


class AhjoApiClientException(Exception):
"""
Raised when an error occurs in the AhjoApiClient.
"""

pass


class MissingAhjoCaseIdError(AhjoApiClientException):
"""
Raised when a Ahjo request that requires a case id is missing the case id.
"""

pass


class MissingHandlerIdError(AhjoApiClientException):
"""
Raised when a Ahjo request that requires a handler id is missing the handler id.
"""

pass


class MissingOrganizationIdentifier(Exception):
"""
Raised when an organization identifier is missing from AhjoSettings in the database.
"""

pass


class AhjoTokenExpiredException(Exception):
"""
Raised when the Ahjo token has expired. The token should be re-configured manually, see instructions at:
https://helsinkisolutionoffice.atlassian.net/wiki/spaces/KAN/pages/8687517756/Siirto+yll+pitoon#Ahjo-autentikaatio-tokenin-haku-ja-asettaminen-manuaalisesti.
"""

pass


class AhjoTokenRetrievalException(Exception):
"""
Raised when the Ahjo token has expired or it could not be otherwise refreshed automatically.
The token should be re-configured manually, see instructions at:
https://helsinkisolutionoffice.atlassian.net/wiki/spaces/KAN/pages/8687517756/Siirto+yll+pitoon#Ahjo-autentikaatio-tokenin-haku-ja-asettaminen-manuaalisesti.
"""

pass


class InvalidAhjoTokenException(Exception):
"""
Raised when the Ahjo token is missing data or is otherwise invalid.
"""

pass


class AhjoCallbackError(Exception):
"""
Raised when an error occurs in the Ahjo callback.
"""

pass


class AhjoDecisionError(Exception):
"""
Raised when an error occurs in substituting application data into the decision text.
"""

pass


class AhjoDecisionDetailsParsingError(Exception):
"""
Raised when an error occurs in parsing the decision details after a details query to Ahjo.
"""

pass
Loading

0 comments on commit 0ea3745

Please sign in to comment.