diff --git a/backend/benefit/applications/api/v1/ahjo_integration_views.py b/backend/benefit/applications/api/v1/ahjo_integration_views.py index 0e4cea8c1e..7d27e860fb 100644 --- a/backend/benefit/applications/api/v1/ahjo_integration_views.py +++ b/backend/benefit/applications/api/v1/ahjo_integration_views.py @@ -246,6 +246,7 @@ def handle_success_callback( {"message": "Callback received"}, status=status.HTTP_200_OK ) except AhjoCallbackError as e: + LOGGER.error(str(e)) return Response( {"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @@ -329,6 +330,10 @@ def _save_version_series_id( def handle_decision_proposal_success(self, application: Application): # do anything that needs to be done when Ahjo has received a decision proposal request + if not application.batch: + raise AhjoCallbackError( + f"Application {application.id} has no batch when Ahjo has received a decision proposal request" + ) batch = application.batch batch.status = ApplicationBatchStatus.AWAITING_AHJO_DECISION batch.save() diff --git a/backend/benefit/applications/enums.py b/backend/benefit/applications/enums.py index b26f9ed805..741963c2f9 100644 --- a/backend/benefit/applications/enums.py +++ b/backend/benefit/applications/enums.py @@ -186,6 +186,9 @@ class AhjoStatus(models.TextChoices): REMOVED_IN_AHJO = "removed", _("Decision cancelled in Ahjo") SIGNED_IN_AHJO = "signed", _("Decision signed and completed in Ahjo") UPDATED_IN_AHJO = "updated", _("Decision updated in Ahjo") + DECISION_DETAILS_REQUEST_SENT = "decision_details_request_sent", _( + "Decision details request sent" + ) DETAILS_RECEIVED_FROM_AHJO = "details_received", _( "Decision details received from Ahjo" ) diff --git a/backend/benefit/applications/jobs/hourly/hourly_ahjo_job.py b/backend/benefit/applications/jobs/hourly/hourly_ahjo_job.py index 3826fd4bef..e2fcebea86 100644 --- a/backend/benefit/applications/jobs/hourly/hourly_ahjo_job.py +++ b/backend/benefit/applications/jobs/hourly/hourly_ahjo_job.py @@ -16,7 +16,35 @@ class Job(HourlyJob): def execute(self): call_command("refresh_ahjo_token") + retry_threshold = 1 + if settings.ENABLE_AHJO_AUTOMATION: + call_command( + "send_ahjo_requests", + request_type=AhjoRequestType.OPEN_CASE, + retry_failed_older_than_hours=retry_threshold, + ) + call_command( + "send_ahjo_requests", + request_type=AhjoRequestType.SEND_DECISION_PROPOSAL, + retry_failed_older_than_hours=retry_threshold, + ) + call_command( + "send_ahjo_requests", + request_type=AhjoRequestType.UPDATE_APPLICATION, + retry_failed_older_than_hours=retry_threshold, + ) + + call_command( + "send_ahjo_requests", + request_type=AhjoRequestType.DELETE_APPLICATION, + retry_failed_older_than_hours=retry_threshold, + ) call_command( "send_ahjo_requests", request_type=AhjoRequestType.GET_DECISION_DETAILS ) + call_command( + "send_ahjo_requests", + request_type=AhjoRequestType.GET_DECISION_DETAILS, + retry_failed_older_than_hours=retry_threshold, + ) diff --git a/backend/benefit/applications/management/commands/send_ahjo_requests.py b/backend/benefit/applications/management/commands/send_ahjo_requests.py index 4bcafa34dc..0acfa8b95a 100644 --- a/backend/benefit/applications/management/commands/send_ahjo_requests.py +++ b/backend/benefit/applications/management/commands/send_ahjo_requests.py @@ -14,6 +14,7 @@ ApplicationStatus, ) from applications.models import AhjoStatus, Application +from applications.services.ahjo_application_service import AhjoApplicationsService from applications.services.ahjo_authentication import ( AhjoToken, AhjoTokenExpiredException, @@ -39,6 +40,7 @@ class Command(BaseCommand): {AhjoRequestType.OPEN_CASE}, {AhjoRequestType.SEND_DECISION_PROPOSAL}, \ {AhjoRequestType.ADD_RECORDS}, {AhjoRequestType.UPDATE_APPLICATION}, \ {AhjoRequestType.GET_DECISION_DETAILS}, {AhjoRequestType.DELETE_APPLICATION}" + is_retry = False def add_arguments(self, parser): parser.add_argument( @@ -60,58 +62,14 @@ def add_arguments(self, parser): help="Run the command without making actual changes", ) - def get_applications_for_request( - self, request_type: AhjoRequestType - ) -> QuerySet[Application]: - if request_type == AhjoRequestType.OPEN_CASE: - applications = Application.objects.get_by_statuses( - [ - ApplicationStatus.HANDLING, - ApplicationStatus.ACCEPTED, - ApplicationStatus.REJECTED, - ], - [AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO], - True, - ) - elif request_type == AhjoRequestType.SEND_DECISION_PROPOSAL: - applications = Application.objects.get_for_ahjo_decision() - - elif request_type == AhjoRequestType.ADD_RECORDS: - applications = Application.objects.with_non_downloaded_attachments() - - elif request_type == AhjoRequestType.UPDATE_APPLICATION: - applications = Application.objects.get_by_statuses( - [ApplicationStatus.ACCEPTED, ApplicationStatus.REJECTED], - [AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED], - False, - ) - elif request_type == AhjoRequestType.GET_DECISION_DETAILS: - applications = Application.objects.get_by_statuses( - [ApplicationStatus.ACCEPTED, ApplicationStatus.REJECTED], - [AhjoStatusEnum.SIGNED_IN_AHJO], - False, - ) - elif request_type == AhjoRequestType.DELETE_APPLICATION: - applications = Application.objects.get_by_statuses( - [ - ApplicationStatus.ACCEPTED, - ApplicationStatus.CANCELLED, - ApplicationStatus.REJECTED, - ApplicationStatus.HANDLING, - ApplicationStatus.DRAFT, - ApplicationStatus.RECEIVED, - ], - AhjoStatusEnum.SCHEDULED_FOR_DELETION, - False, - ) - - # Only send applications that have automation enabled - applications_with_ahjo_automation = applications.filter( - handled_by_ahjo_automation=True + parser.add_argument( + "--retry-failed-older-than", + type=int, + default=0, + help="Retry sending requests for applications that have \ +not moved to the next status in the last x hours", ) - return applications_with_ahjo_automation - def handle(self, *args, **options): try: ahjo_auth_token = get_token() @@ -125,8 +83,14 @@ def handle(self, *args, **options): number_to_process = options["number"] dry_run = options["dry_run"] request_type = options["request_type"] + retry_failed_older_than_hours = options["retry_failed_older_than"] + + if retry_failed_older_than_hours > 0: + self.is_retry = True - applications = self.get_applications_for_request(request_type) + applications = AhjoApplicationsService.get_applications_for_request( + request_type, retry_failed_older_than_hours + ) if not applications: self.stdout.write(self._print_with_timestamp("No applications to process")) @@ -135,8 +99,11 @@ def handle(self, *args, **options): applications = applications[:number_to_process] if dry_run: + message_start = "retry" if self.is_retry else "send" + self.stdout.write( - f"Would send {request_type} requests for {len(applications)} applications to Ahjo" + f"Would {message_start} sending {request_type} \ +requests for {len(applications)} applications to Ahjo" ) for application in applications: @@ -159,12 +126,15 @@ def run_requests( successful_applications = [] failed_applications = [] - self.stdout.write( - self._print_with_timestamp( - f"Sending {ahjo_request_type} request to Ahjo \ -for {len(applications)} applications" - ) + application_numbers = ", ".join( + str(app.application_number) for app in applications ) + message_start = "Retrying" if self.is_retry else "Sending" + + message = f"{message_start} {ahjo_request_type} request to Ahjo \ +for {len(applications)} applications: {application_numbers}" + + self.stdout.write(self._print_with_timestamp(message)) request_handler = self._get_request_handler(ahjo_request_type) diff --git a/backend/benefit/applications/migrations/0085_alter_ahjostatus_status.py b/backend/benefit/applications/migrations/0085_alter_ahjostatus_status.py new file mode 100644 index 0000000000..ee4113c4e5 --- /dev/null +++ b/backend/benefit/applications/migrations/0085_alter_ahjostatus_status.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.11 on 2024-10-03 07:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("applications", "0084_ahjostatus_validation_error_from_ahjo"), + ] + + operations = [ + migrations.AlterField( + model_name="ahjostatus", + name="status", + field=models.CharField( + choices=[ + ( + "submitted_but_not_sent_to_ahjo", + "Submitted but not sent to AHJO", + ), + ( + "request_to_open_case_sent", + "Request to open the case sent to AHJO", + ), + ("case_opened", "Case opened in AHJO"), + ("update_request_sent", "Update request sent"), + ("update_request_received", "Update request received"), + ("decision_proposal_sent", "Decision proposal sent"), + ("decision_proposal_accepted", "Decision proposal accepted"), + ("decision_proposal_rejected", "Decision proposal rejected"), + ("scheduled_for_deletion", "Scheduled for deletion"), + ("delete_request_sent", "Delete request sent"), + ("delete_request_received", "Delete request received"), + ("new_record_request_sent", "New record request sent"), + ("new_record_received", "New record received by Ahjo"), + ("removed", "Decision cancelled in Ahjo"), + ("signed", "Decision signed and completed in Ahjo"), + ("updated", "Decision updated in Ahjo"), + ("decision_details_request_sent", "Decision details request sent"), + ("details_received", "Decision details received from Ahjo"), + ], + default="submitted_but_not_sent_to_ahjo", + max_length=64, + verbose_name="status", + ), + ), + ] diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py index 7c0b643d7a..aaeffc9aca 100755 --- a/backend/benefit/applications/models.py +++ b/backend/benefit/applications/models.py @@ -1,5 +1,5 @@ import re -from datetime import date +from datetime import date, timedelta from typing import List from dateutil.relativedelta import relativedelta @@ -8,6 +8,7 @@ from django.db import connection, models from django.db.models import Exists, F, JSONField, OuterRef, Prefetch, Subquery from django.db.models.constraints import UniqueConstraint +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from encrypted_fields.fields import EncryptedCharField, SearchField from phonenumber_field.modelfields import PhoneNumberField @@ -173,6 +174,8 @@ def get_by_statuses( application_statuses: List[ApplicationStatus], ahjo_statuses: List[AhjoStatusEnum], has_no_case_id: bool, + retry_failed_older_than_hours: int = 0, + retry_status: AhjoStatusEnum = None, ): """ Query applications by their latest AhjoStatus relation @@ -183,6 +186,10 @@ def get_by_statuses( or AhjoStatusEnum.NEW_RECORDS_RECEIVED """ + # if hours are specified, then the retry_status is used to query the applications for the re-attempt + if retry_failed_older_than_hours > 0 and retry_status: + ahjo_statuses = [retry_status] + # Subquery to get the latest AhjoStatus id for each application latest_ahjo_status_subquery = ( AhjoStatus.objects.filter(application=OuterRef("pk")) @@ -190,6 +197,12 @@ def get_by_statuses( .values("id")[:1] ) + latest_ahjo_status_created_at_subquery = ( + AhjoStatus.objects.filter(application=OuterRef("pk")) + .order_by("-created_at") + .values("created_at")[:1] + ) + # Excluded attachment types excluded_types = [ AttachmentType.PDF_SUMMARY, @@ -204,55 +217,91 @@ def get_by_statuses( attachments_prefetch = Prefetch("attachments", queryset=attachments_queryset) # Annotate applications with the latest AhjoStatus id and filter accordingly - applications = ( - self.annotate(latest_ahjo_status_id=Subquery(latest_ahjo_status_subquery)) - .filter( - status__in=application_statuses, - ahjo_status__id=F("latest_ahjo_status_id"), - ahjo_status__status__in=ahjo_statuses, - ahjo_case_id__isnull=has_no_case_id, - ) - .prefetch_related(attachments_prefetch, "calculation", "company") + applications = self.annotate( + latest_ahjo_status_id=Subquery(latest_ahjo_status_subquery), + latest_ahjo_status_created_at=Subquery( + latest_ahjo_status_created_at_subquery.values("created_at") + ), + ).filter( + status__in=application_statuses, + ahjo_status__id=F("latest_ahjo_status_id"), + ahjo_status__status__in=ahjo_statuses, + ahjo_case_id__isnull=has_no_case_id, ) - return applications + # Dynamically add time-based filter if requested, so that applications that have + # been in a certain AhjoStatus for a certain time are queried + if retry_failed_older_than_hours > 0: + time_in_past = timezone.now() - timedelta( + hours=retry_failed_older_than_hours + ) + applications = applications.filter( + latest_ahjo_status_created_at__lt=time_in_past + ) - def get_for_ahjo_decision(self): + return applications.prefetch_related( + attachments_prefetch, "calculation", "company" + ) + + def get_for_ahjo_decision( + self, + application_statuses: List[ApplicationStatus], + ahjo_statuses: List[AhjoStatusEnum], + has_no_case_id: bool = False, + retry_failed_older_than_hours: int = 0, + retry_status: AhjoStatusEnum = None, + ): """ Get applications that are in a state where a decision proposal should be sent to Ahjo. This means applications that have been accepted or rejected, their talpa status is not_processed_by_talpa, have a ahjo_case_id, a decisiontext and their latest ahjo_status is case_opened. """ + if retry_failed_older_than_hours > 0 and retry_status: + ahjo_statuses = [retry_status] + latest_ahjo_status_subquery = ( AhjoStatus.objects.filter(application=OuterRef("pk")) .order_by("-created_at") .values("id")[:1] ) - return ( + latest_ahjo_status_created_at_subquery = ( + AhjoStatus.objects.filter(application=OuterRef("pk")) + .order_by("-created_at") + .values("created_at")[:1] + ) + + applications = ( self.get_queryset() - .annotate(latest_ahjo_status_id=Subquery(latest_ahjo_status_subquery)) + .annotate( + latest_ahjo_status_id=Subquery(latest_ahjo_status_subquery), + latest_ahjo_status_created_at=Subquery( + latest_ahjo_status_created_at_subquery.values("created_at") + ), + ) .filter( - status__in=[ - ApplicationStatus.ACCEPTED, - ApplicationStatus.REJECTED, - ], + status__in=application_statuses, talpa_status=ApplicationTalpaStatus.NOT_PROCESSED_BY_TALPA, - ahjo_case_id__isnull=False, + ahjo_case_id__isnull=has_no_case_id, ahjodecisiontext__isnull=False, id__in=AhjoDecisionText.objects.filter( application_id=OuterRef("id") ).values("application_id"), ahjo_status__id=F("latest_ahjo_status_id"), - ahjo_status__status__in=[ - AhjoStatusEnum.CASE_OPENED, - AhjoStatusEnum.NEW_RECORDS_RECEIVED, - ], + ahjo_status__status__in=ahjo_statuses, ) - .select_related("ahjodecisiontext") ) + if retry_failed_older_than_hours > 0: + time_in_past = timezone.now() - timedelta( + hours=retry_failed_older_than_hours + ) + applications = applications.filter( + latest_ahjo_status_created_at__lt=time_in_past + ) + return applications.select_related("ahjodecisiontext") + class Application(UUIDModel, TimeStampedModel, DurationMixin): """ diff --git a/backend/benefit/applications/services/ahjo_application_service.py b/backend/benefit/applications/services/ahjo_application_service.py new file mode 100644 index 0000000000..201f7a31d8 --- /dev/null +++ b/backend/benefit/applications/services/ahjo_application_service.py @@ -0,0 +1,111 @@ +from django.db.models import QuerySet + +from applications.enums import ( + AhjoRequestType, + AhjoStatus as AhjoStatusEnum, + ApplicationStatus, +) +from applications.models import Application + + +class AhjoQueryParameters: + """This class resolves the query parameters which are used to query applications + based on the request type.""" + + @staticmethod + def resolve( + request_type: AhjoRequestType, retry_failed_older_than_hours: int = 0 + ) -> dict: + ahjo_application_query_parameters = { + AhjoRequestType.OPEN_CASE: { + "application_statuses": [ + ApplicationStatus.HANDLING, + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO], + "has_no_case_id": True, + "retry_status": AhjoStatusEnum.REQUEST_TO_OPEN_CASE_SENT, + }, + AhjoRequestType.SEND_DECISION_PROPOSAL: { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [ + AhjoStatusEnum.CASE_OPENED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + ], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DECISION_PROPOSAL_SENT, + }, + AhjoRequestType.UPDATE_APPLICATION: { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [ + AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + ], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.UPDATE_REQUEST_SENT, + }, + AhjoRequestType.DELETE_APPLICATION: { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.CANCELLED, + ApplicationStatus.REJECTED, + ApplicationStatus.HANDLING, + ApplicationStatus.DRAFT, + ApplicationStatus.RECEIVED, + ], + "ahjo_statuses": [AhjoStatusEnum.SCHEDULED_FOR_DELETION], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DELETE_REQUEST_SENT, + }, + AhjoRequestType.GET_DECISION_DETAILS: { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [AhjoStatusEnum.SIGNED_IN_AHJO], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DECISION_DETAILS_REQUEST_SENT, + }, + } + parameters = ahjo_application_query_parameters.get(request_type) + if parameters is None: + raise ValueError(f"Unsupported request type: {request_type}") + if retry_failed_older_than_hours > 0: + parameters["retry_failed_older_than_hours"] = retry_failed_older_than_hours + return parameters + + +class AhjoApplicationsService: + @staticmethod + def get_applications_for_request( + request_type: AhjoRequestType, + retry_failed_older_than_hours: int = 0, + ) -> QuerySet[Application]: + if request_type in [ + AhjoRequestType.OPEN_CASE, + AhjoRequestType.UPDATE_APPLICATION, + AhjoRequestType.DELETE_APPLICATION, + AhjoRequestType.GET_DECISION_DETAILS, + ]: + parameters = AhjoQueryParameters.resolve( + request_type, retry_failed_older_than_hours + ) + applications = Application.objects.get_by_statuses(**parameters) + + elif request_type == AhjoRequestType.SEND_DECISION_PROPOSAL: + parameters = AhjoQueryParameters.resolve( + request_type, retry_failed_older_than_hours + ) + applications = Application.objects.get_for_ahjo_decision(**parameters) + + elif request_type == AhjoRequestType.ADD_RECORDS: + applications = Application.objects.with_non_downloaded_attachments() + + return applications.filter(handled_by_ahjo_automation=True) diff --git a/backend/benefit/applications/services/ahjo_client.py b/backend/benefit/applications/services/ahjo_client.py index 5bc2bc5600..d01230ef57 100644 --- a/backend/benefit/applications/services/ahjo_client.py +++ b/backend/benefit/applications/services/ahjo_client.py @@ -119,6 +119,7 @@ class AhjoDecisionDetailsRequest(AhjoRequest): """Request to get a decision detail from Ahjo.""" request_type = AhjoRequestType.GET_DECISION_DETAILS + result_status = AhjoStatusEnum.DECISION_DETAILS_REQUEST_SENT request_method = "GET" def api_url(self) -> str: @@ -170,6 +171,10 @@ def __init__(self, ahjo_token: AhjoToken, ahjo_request: AhjoRequest) -> None: def ahjo_token(self) -> AhjoToken: return self._ahjo_token + @property + def request(self) -> AhjoRequest: + return self._request + @ahjo_token.setter def ahjo_token(self, token: AhjoToken) -> None: if not isinstance(token, AhjoToken): diff --git a/backend/benefit/applications/services/ahjo_integration.py b/backend/benefit/applications/services/ahjo_integration.py index 36d3df7f2d..4477a25bb5 100644 --- a/backend/benefit/applications/services/ahjo_integration.py +++ b/backend/benefit/applications/services/ahjo_integration.py @@ -26,6 +26,7 @@ AhjoSetting, AhjoStatus, Application, + ApplicationBatch, Attachment, ) from applications.services.ahjo_authentication import AhjoConnector, AhjoToken @@ -559,6 +560,12 @@ def send_decision_proposal_to_ahjo( ahjo_client = AhjoApiClient(ahjo_token, ahjo_request) decision = AhjoDecisionText.objects.get(application=application) + # if application does not have a batch for some reason, create one + if not application.batch: + batch = ApplicationBatch.objects.create(auto_generated_by_ahjo=True) + application.batch = batch + application.save() + delete_existing_xml_attachments(application) decision_xml = generate_application_attachment( diff --git a/backend/benefit/applications/tests/conftest.py b/backend/benefit/applications/tests/conftest.py index a0e12ef18d..699a4eb83b 100755 --- a/backend/benefit/applications/tests/conftest.py +++ b/backend/benefit/applications/tests/conftest.py @@ -1,7 +1,7 @@ import os import random import uuid -from datetime import date, timedelta +from datetime import date, datetime, timedelta import factory import pytest @@ -25,6 +25,7 @@ ApplicationAlteration, ApplicationBatch, ) +from applications.services.ahjo_authentication import AhjoToken from applications.services.ahjo_decision_service import ( replace_decision_template_placeholders, ) @@ -1109,3 +1110,13 @@ def decision_maker_settings(fake_decisionmakers): name="ahjo_decision_maker", data=fake_decisionmakers, ) + + +@pytest.fixture +def non_expired_token(): + return AhjoToken( + access_token="access_token", + refresh_token="refresh_token", + expires_in=30000, + created_at=datetime.now(timezone.utc), + ) diff --git a/backend/benefit/applications/tests/test_ahjo_authentication.py b/backend/benefit/applications/tests/test_ahjo_authentication.py index 27c781c7d6..b8a350d296 100644 --- a/backend/benefit/applications/tests/test_ahjo_authentication.py +++ b/backend/benefit/applications/tests/test_ahjo_authentication.py @@ -38,16 +38,6 @@ def expired_token(): ) -@pytest.fixture -def non_expired_token(): - return AhjoToken( - access_token="access_token", - refresh_token="refresh_token", - expires_in=TOKEN_EXPIRY_SECONDS, - created_at=datetime.now(timezone.utc), - ) - - @pytest.fixture def ahjo_setting(): return AhjoSetting.objects.create( diff --git a/backend/benefit/applications/tests/test_ahjo_integration.py b/backend/benefit/applications/tests/test_ahjo_integration.py index 3c2ae7fe5a..f0030281dc 100644 --- a/backend/benefit/applications/tests/test_ahjo_integration.py +++ b/backend/benefit/applications/tests/test_ahjo_integration.py @@ -32,6 +32,10 @@ ApplicationBatch, Attachment, ) +from applications.services.ahjo_application_service import ( + AhjoApplicationsService, + AhjoQueryParameters, +) from applications.services.ahjo_integration import ( ACCEPTED_TITLE, export_application_batch, @@ -707,6 +711,37 @@ def test_ahjo_callback_unauthorized_ip_not_allowed( assert response.status_code == 403 +def test_ahjo_callback_raises_error_without_batch( + ahjo_callback_payload, decided_application, ahjo_client, ahjo_user_token, settings +): + settings.NEXT_PUBLIC_MOCK_FLAG = True + + status = AhjoStatus.objects.create( + application=decided_application, + status=AhjoStatusEnum.CASE_OPENED, + ) + status.created_at = timezone.now() - timedelta(days=5) + status.save() + + auth_headers = {"HTTP_AUTHORIZATION": "Token " + ahjo_user_token.key} + attachment = generate_application_attachment( + decided_application, AttachmentType.PDF_SUMMARY + ) + attachment_hash_value = hash_file(attachment.attachment_file) + attachment.ahjo_hash_value = attachment_hash_value + attachment.save() + ahjo_callback_payload["message"] = AhjoCallBackStatus.SUCCESS + ahjo_callback_payload["records"][0]["hashValue"] = attachment_hash_value + + url = _get_callback_url(AhjoRequestType.SEND_DECISION_PROPOSAL, decided_application) + # with pytest.raises(AhjoCallbackError): + response = ahjo_client.post(url, **auth_headers, data=ahjo_callback_payload) + assert response.status_code == 500 + assert response.data == { + "error": f"Application {decided_application.id} has no batch when Ahjo has received a decision proposal request" + } + + @pytest.mark.django_db def test_get_application_for_ahjo_success(decided_application): user = decided_application.calculation.handler @@ -792,43 +827,271 @@ def test_generate_ahjo_secret_decision_text_xml(decided_application): os.remove(attachment.attachment_file.path) -@pytest.mark.django_db -def test_get_applications_for_ahjo_update( - multiple_applications_with_ahjo_case_id, +@pytest.mark.parametrize( + "ahjo_request_type, query_parameters,retry_failed_older_than_hours", + [ + ( + AhjoRequestType.OPEN_CASE, + { + "application_statuses": [ + ApplicationStatus.HANDLING, + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO], + "has_no_case_id": True, + "retry_status": AhjoStatusEnum.REQUEST_TO_OPEN_CASE_SENT, + }, + 0, + ), + ( + AhjoRequestType.SEND_DECISION_PROPOSAL, + { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [ + AhjoStatusEnum.CASE_OPENED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + ], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DECISION_PROPOSAL_SENT, + }, + 0, + ), + ( + AhjoRequestType.UPDATE_APPLICATION, + { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [ + AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + ], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.UPDATE_REQUEST_SENT, + }, + 0, + ), + ( + AhjoRequestType.DELETE_APPLICATION, + { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.CANCELLED, + ApplicationStatus.REJECTED, + ApplicationStatus.HANDLING, + ApplicationStatus.DRAFT, + ApplicationStatus.RECEIVED, + ], + "ahjo_statuses": [AhjoStatusEnum.SCHEDULED_FOR_DELETION], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DELETE_REQUEST_SENT, + }, + 0, + ), + ( + AhjoRequestType.GET_DECISION_DETAILS, + { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [AhjoStatusEnum.SIGNED_IN_AHJO], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DECISION_DETAILS_REQUEST_SENT, + }, + 0, + ), + ( + AhjoRequestType.OPEN_CASE, + { + "application_statuses": [ + ApplicationStatus.HANDLING, + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO], + "has_no_case_id": True, + "retry_status": AhjoStatusEnum.REQUEST_TO_OPEN_CASE_SENT, + }, + 1, + ), + ( + AhjoRequestType.SEND_DECISION_PROPOSAL, + { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [ + AhjoStatusEnum.CASE_OPENED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + ], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DECISION_PROPOSAL_SENT, + }, + 1, + ), + ( + AhjoRequestType.UPDATE_APPLICATION, + { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [ + AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + ], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.UPDATE_REQUEST_SENT, + }, + 1, + ), + ( + AhjoRequestType.DELETE_APPLICATION, + { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.CANCELLED, + ApplicationStatus.REJECTED, + ApplicationStatus.HANDLING, + ApplicationStatus.DRAFT, + ApplicationStatus.RECEIVED, + ], + "ahjo_statuses": [AhjoStatusEnum.SCHEDULED_FOR_DELETION], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DELETE_REQUEST_SENT, + }, + 1, + ), + ( + AhjoRequestType.GET_DECISION_DETAILS, + { + "application_statuses": [ + ApplicationStatus.ACCEPTED, + ApplicationStatus.REJECTED, + ], + "ahjo_statuses": [AhjoStatusEnum.SIGNED_IN_AHJO], + "has_no_case_id": False, + "retry_status": AhjoStatusEnum.DECISION_DETAILS_REQUEST_SENT, + }, + 1, + ), + ], +) +def test_ahjo_query_parameter_resolver( + ahjo_request_type, query_parameters, retry_failed_older_than_hours ): - for a in multiple_applications_with_ahjo_case_id[:5]: - AhjoStatus.objects.create( - application=a, - status=AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, - ) - for a in multiple_applications_with_ahjo_case_id[5:]: - AhjoStatus.objects.create( - application=a, - status=AhjoStatusEnum.NEW_RECORDS_RECEIVED, + parameters = AhjoQueryParameters.resolve( + ahjo_request_type, retry_failed_older_than_hours + ) + if retry_failed_older_than_hours: + assert ( + parameters["retry_failed_older_than_hours"] == retry_failed_older_than_hours ) + else: + assert parameters == query_parameters - applications_for_ahjo_update = Application.objects.get_by_statuses( - [ - ApplicationStatus.ACCEPTED, - ApplicationStatus.REJECTED, - ], - [ - AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, - AhjoStatusEnum.NEW_RECORDS_RECEIVED, - ], - False, - ) - assert applications_for_ahjo_update.count() == len( - multiple_applications_with_ahjo_case_id - ) +@pytest.fixture +def mock_get_by_statuses(): + with patch( + "applications.models.Application.objects.get_by_statuses", autospec=True + ) as mock: + yield mock + + +@pytest.fixture +def mock_get_for_ahjo_decision(): + with patch( + "applications.models.Application.objects.get_for_ahjo_decision", autospec=True + ) as mock: + yield mock + + +@pytest.fixture +def mock_with_non_downloaded_attachments(): + with patch( + "applications.models.Application.objects.with_non_downloaded_attachments", + autospec=True, + ) as mock: + yield mock + + +@pytest.mark.parametrize( + "request_type", + [ + (AhjoRequestType.OPEN_CASE), + (AhjoRequestType.SEND_DECISION_PROPOSAL), + (AhjoRequestType.UPDATE_APPLICATION), + (AhjoRequestType.DELETE_APPLICATION), + (AhjoRequestType.GET_DECISION_DETAILS), + ], +) +@pytest.mark.django_db +def test_applications_service( + request_type, mock_get_by_statuses, mock_get_for_ahjo_decision +): + parameters = AhjoQueryParameters.resolve( + request_type + ) # Example parameters, adjust as needed + + with patch( + "applications.services.ahjo_application_service.AhjoQueryParameters.resolve", + return_value=parameters, + ): + AhjoApplicationsService.get_applications_for_request(request_type) + + if request_type == AhjoRequestType.SEND_DECISION_PROPOSAL: + mock_get_for_ahjo_decision.assert_called_once_with(**parameters) + else: + mock_get_by_statuses.assert_called_once_with(**parameters) +@pytest.fixture +def wanted_open_case_attachments(): + return [ + AttachmentType.EMPLOYMENT_CONTRACT, + AttachmentType.PAY_SUBSIDY_DECISION, + AttachmentType.COMMISSION_CONTRACT, + AttachmentType.EDUCATION_CONTRACT, + AttachmentType.HELSINKI_BENEFIT_VOUCHER, + AttachmentType.EMPLOYEE_CONSENT, + AttachmentType.OTHER_ATTACHMENT, + AttachmentType.FULL_APPLICATION, + ] + + +@pytest.fixture +def unwanted_open_case_attachments(): + return [ + AttachmentType.PDF_SUMMARY, + AttachmentType.DECISION_TEXT_XML, + AttachmentType.DECISION_TEXT_SECRET_XML, + ] + + +@pytest.mark.parametrize( + "retry_failed_older_than_hours, latest_ahjo_status", + [ + (0, AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO), + (1, AhjoStatusEnum.REQUEST_TO_OPEN_CASE_SENT), + ], +) @pytest.mark.django_db -def test_get_applications_for_open_case( +def test_get_applications_for_open_case_request( multiple_decided_applications, multiple_decided_applications_for_open_case, multiple_handling_applications, + wanted_open_case_attachments, + unwanted_open_case_attachments, + retry_failed_older_than_hours, + latest_ahjo_status, ): now = timezone.now() wanted_applications_for_open_case = ( @@ -837,7 +1100,7 @@ def test_get_applications_for_open_case( for application in wanted_applications_for_open_case: status = AhjoStatus.objects.create( application=application, - status=AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO, + status=latest_ahjo_status, ) status.created_at = now - timedelta(days=1) @@ -853,33 +1116,12 @@ def test_get_applications_for_open_case( ahjo_status.created_at = now + timedelta(days=index) ahjo_status.save() - wanted_open_case_attachments = [ - AttachmentType.EMPLOYMENT_CONTRACT, - AttachmentType.PAY_SUBSIDY_DECISION, - AttachmentType.COMMISSION_CONTRACT, - AttachmentType.EDUCATION_CONTRACT, - AttachmentType.HELSINKI_BENEFIT_VOUCHER, - AttachmentType.EMPLOYEE_CONSENT, - AttachmentType.OTHER_ATTACHMENT, - AttachmentType.FULL_APPLICATION, - ] - - unwanted_open_case_attachments = [ - AttachmentType.PDF_SUMMARY, - AttachmentType.DECISION_TEXT_XML, - AttachmentType.DECISION_TEXT_SECRET_XML, - ] - - applications_for_open_case = Application.objects.get_by_statuses( - [ - ApplicationStatus.HANDLING, - ApplicationStatus.ACCEPTED, - ApplicationStatus.REJECTED, - ], - [AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO], - True, + parameters = AhjoQueryParameters.resolve( + AhjoRequestType.OPEN_CASE, retry_failed_older_than_hours ) + applications_for_open_case = Application.objects.get_by_statuses(**parameters) + for app in applications_for_open_case: attachments = app.attachments.all() for a in attachments: @@ -890,8 +1132,107 @@ def test_get_applications_for_open_case( assert applications_for_open_case.count() == len(wanted_applications_for_open_case) +@pytest.mark.parametrize( + "latest_ahjo_status, retry_failed_older_than_hours", + [ + (AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, 0), + (AhjoStatusEnum.NEW_RECORDS_RECEIVED, 0), + (AhjoStatusEnum.UPDATE_REQUEST_SENT, 1), + (AhjoStatusEnum.UPDATE_REQUEST_SENT, 24), + ], +) @pytest.mark.django_db -def test_with_non_downloaded_attachments(decided_application): +def test_get_applications_for_update_request( + multiple_applications_with_ahjo_case_id, + latest_ahjo_status, + retry_failed_older_than_hours, +): + now = timezone.now() + + for a in multiple_applications_with_ahjo_case_id[:5]: + ahjo_status = AhjoStatus.objects.create( + application=a, + status=latest_ahjo_status, + ) + ahjo_status.created_at = ( + now - timedelta(hours=retry_failed_older_than_hours) - timedelta(minutes=1) + ) + ahjo_status.save() + + for a in multiple_applications_with_ahjo_case_id[5:]: + ahjo_status = AhjoStatus.objects.create( + application=a, + status=latest_ahjo_status, + ) + ahjo_status.created_at = ( + now - timedelta(hours=retry_failed_older_than_hours) - timedelta(minutes=1) + ) + ahjo_status.save() + + parameters = AhjoQueryParameters.resolve( + AhjoRequestType.UPDATE_APPLICATION, retry_failed_older_than_hours + ) + for application in multiple_applications_with_ahjo_case_id: + print(application.ahjo_status.latest().status) + + applications_for_ahjo_update = Application.objects.get_by_statuses(**parameters) + + assert applications_for_ahjo_update.count() == len( + multiple_applications_with_ahjo_case_id + ) + + +@pytest.mark.parametrize( + "application_status, latest_ahjo_status, retry_failed_older_than_hours", + [ + (ApplicationStatus.ACCEPTED, AhjoStatusEnum.SCHEDULED_FOR_DELETION, 0), + (ApplicationStatus.CANCELLED, AhjoStatusEnum.SCHEDULED_FOR_DELETION, 0), + (ApplicationStatus.REJECTED, AhjoStatusEnum.SCHEDULED_FOR_DELETION, 0), + (ApplicationStatus.HANDLING, AhjoStatusEnum.SCHEDULED_FOR_DELETION, 0), + (ApplicationStatus.DRAFT, AhjoStatusEnum.SCHEDULED_FOR_DELETION, 0), + (ApplicationStatus.RECEIVED, AhjoStatusEnum.DELETE_REQUEST_SENT, 1), + (ApplicationStatus.ACCEPTED, AhjoStatusEnum.DELETE_REQUEST_SENT, 1), + (ApplicationStatus.CANCELLED, AhjoStatusEnum.DELETE_REQUEST_SENT, 1), + (ApplicationStatus.REJECTED, AhjoStatusEnum.DELETE_REQUEST_SENT, 1), + (ApplicationStatus.HANDLING, AhjoStatusEnum.DELETE_REQUEST_SENT, 1), + (ApplicationStatus.DRAFT, AhjoStatusEnum.DELETE_REQUEST_SENT, 1), + (ApplicationStatus.RECEIVED, AhjoStatusEnum.DELETE_REQUEST_SENT, 1), + ], +) +def test_get_applications_for_delete_request( + handling_application, + application_status, + latest_ahjo_status, + retry_failed_older_than_hours, +): + now = timezone.now() + + handling_application.status = application_status + handling_application.ahjo_case_id = "HEL 1999-123" + handling_application.save() + + ahjo_status = AhjoStatus.objects.create( + application_id=handling_application.id, + status=latest_ahjo_status, + ) + + ahjo_status.created_at = ( + now - timedelta(hours=retry_failed_older_than_hours) - timedelta(minutes=1) + ) + ahjo_status.save() + + parameters = AhjoQueryParameters.resolve( + AhjoRequestType.DELETE_APPLICATION, retry_failed_older_than_hours + ) + + applications_for_delete = Application.objects.get_by_statuses(**parameters) + + assert applications_for_delete.count() == 1 + assert applications_for_delete[0] == handling_application + + +@pytest.mark.django_db +def test_get_applications_for_add_records_request(decided_application): applications = Application.objects.with_non_downloaded_attachments() assert applications.count() == 0 @@ -1059,7 +1400,7 @@ def test_with_non_downloaded_attachments(decided_application): ], ) @pytest.mark.django_db -def test_get_for_ahjo_decision( +def test_get_applications_for_ahjo_decision_proposal_request( decided_application, application_status, ahjo_status, @@ -1079,9 +1420,227 @@ def test_get_for_ahjo_decision( AhjoDecisionText.objects.create( application=decided_application, decision_text="test" ) + parameters = AhjoQueryParameters.resolve(AhjoRequestType.SEND_DECISION_PROPOSAL) + applications = Application.objects.get_for_ahjo_decision(**parameters) + assert applications.count() == count + - applications = Application.objects.get_for_ahjo_decision() +@pytest.mark.parametrize( + "application_status, latest_ahjo_status, talpa_status, retry_failed_older_than_hours, wanted_count", + [ + ( + ApplicationStatus.ACCEPTED, + AhjoStatusEnum.DECISION_PROPOSAL_SENT, + ApplicationTalpaStatus.NOT_PROCESSED_BY_TALPA, + 1, + 1, + ), + ( + ApplicationStatus.REJECTED, + AhjoStatusEnum.DECISION_PROPOSAL_SENT, + ApplicationTalpaStatus.NOT_PROCESSED_BY_TALPA, + 1, + 1, + ), + ], +) +@pytest.mark.django_db +def test_retry_get_applications_for_ahjo_decision_proposal_request( + application_with_ahjo_case_id, + application_status, + latest_ahjo_status, + talpa_status, + retry_failed_older_than_hours, + wanted_count, +): + application_with_ahjo_case_id.status = application_status + application_with_ahjo_case_id.talpa_status = talpa_status + application_with_ahjo_case_id.save() + + ahjo_status = AhjoStatus.objects.create( + application_id=application_with_ahjo_case_id.id, + status=latest_ahjo_status, + ) + + ahjo_status.created_at = ( + timezone.now() + - timedelta(hours=retry_failed_older_than_hours) + - timedelta(minutes=1) + ) + ahjo_status.save() + + AhjoDecisionText.objects.create( + application=application_with_ahjo_case_id, decision_text="test" + ) + parameters = AhjoQueryParameters.resolve( + AhjoRequestType.SEND_DECISION_PROPOSAL, retry_failed_older_than_hours + ) + applications = Application.objects.get_for_ahjo_decision(**parameters) + assert applications.count() == wanted_count + assert applications.first() == application_with_ahjo_case_id + + +@pytest.mark.parametrize( + "application_status, ahjo_status, case_id, count", + [ + ( + ApplicationStatus.DRAFT, + AhjoStatusEnum.CASE_OPENED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.HANDLING, + AhjoStatusEnum.CASE_OPENED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.RECEIVED, + AhjoStatusEnum.CASE_OPENED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.CANCELLED, + AhjoStatusEnum.CASE_OPENED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.ADDITIONAL_INFORMATION_NEEDED, + AhjoStatusEnum.CASE_OPENED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.ACCEPTED, + AhjoStatusEnum.CASE_OPENED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.REJECTED, + AhjoStatusEnum.CASE_OPENED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.ACCEPTED, + AhjoStatusEnum.DECISION_PROPOSAL_SENT, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.REJECTED, + AhjoStatusEnum.DECISION_PROPOSAL_SENT, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.ACCEPTED, + AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.REJECTED, + AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.ACCEPTED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.REJECTED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.ACCEPTED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.REJECTED, + AhjoStatusEnum.NEW_RECORDS_RECEIVED, + dummy_case_id, + 0, + ), + ( + ApplicationStatus.REJECTED, + AhjoStatusEnum.SIGNED_IN_AHJO, + dummy_case_id, + 1, + ), + ( + ApplicationStatus.ACCEPTED, + AhjoStatusEnum.SIGNED_IN_AHJO, + dummy_case_id, + 1, + ), + ], +) +def test_get_applications_for_ahjo_details_request( + decided_application, + application_status, + ahjo_status, + case_id, + count, +): + decided_application.status = application_status + decided_application.ahjo_case_id = case_id + decided_application.save() + + decided_application.ahjo_status.create(status=ahjo_status) + + parameters = AhjoQueryParameters.resolve(AhjoRequestType.GET_DECISION_DETAILS) + applications = Application.objects.get_by_statuses(**parameters) assert applications.count() == count + if count: + assert applications.first() == decided_application + + +@pytest.mark.parametrize( + "application_status, latest_ahjo_status, retry_failed_older_than_hours", + [ + (ApplicationStatus.ACCEPTED, AhjoStatusEnum.DECISION_DETAILS_REQUEST_SENT, 1), + (ApplicationStatus.REJECTED, AhjoStatusEnum.DECISION_DETAILS_REQUEST_SENT, 1), + ], +) +def test_retry_get_applications_for_ahjo_details_request( + application_with_ahjo_case_id, + application_status, + latest_ahjo_status, + retry_failed_older_than_hours, +): + now = timezone.now() + application_with_ahjo_case_id.status = application_status + + application_with_ahjo_case_id.save() + + ahjo_status = AhjoStatus.objects.create( + application_id=application_with_ahjo_case_id.id, + status=latest_ahjo_status, + ) + + ahjo_status.created_at = ( + now - timedelta(hours=retry_failed_older_than_hours) - timedelta(minutes=1) + ) + ahjo_status.save() + + parameters = AhjoQueryParameters.resolve( + AhjoRequestType.GET_DECISION_DETAILS, retry_failed_older_than_hours + ) + applications = Application.objects.get_by_statuses(**parameters) + assert applications.count() == 1 + assert applications.first() == application_with_ahjo_case_id @pytest.mark.parametrize( diff --git a/backend/benefit/applications/tests/test_ahjo_requests.py b/backend/benefit/applications/tests/test_ahjo_requests.py index b08c27051b..773d2b8a64 100644 --- a/backend/benefit/applications/tests/test_ahjo_requests.py +++ b/backend/benefit/applications/tests/test_ahjo_requests.py @@ -1,19 +1,21 @@ -from datetime import datetime, timezone +from datetime import timedelta from unittest.mock import patch import pytest import requests import requests_mock from django.urls import reverse +from django.utils import timezone from applications.enums import AhjoRequestType, AhjoStatus as AhjoStatusEnum -from applications.models import AhjoStatus +from applications.models import AhjoSetting, AhjoStatus from applications.services.ahjo_authentication import AhjoToken, InvalidTokenException from applications.services.ahjo_client import ( AhjoAddRecordsRequest, AhjoApiClient, AhjoApiClientException, AhjoDecisionDetailsRequest, + AhjoDecisionMakerRequest, AhjoDecisionProposalRequest, AhjoDeleteCaseRequest, AhjoOpenCaseRequest, @@ -25,68 +27,125 @@ API_CASES_BASE = "/cases" -@pytest.fixture -def dummy_token(): - return AhjoToken( - access_token="test", - expires_in=3600, - refresh_token="test", - created_at=datetime.now(timezone.utc), - ) - - @pytest.fixture def ahjo_open_case_request(application_with_ahjo_case_id): return AhjoOpenCaseRequest(application_with_ahjo_case_id) @pytest.mark.parametrize( - "ahjo_request_class, request_type, request_method, callback_route", + "ahjo_request_class, request_type, request_method, url_part", [ - (AhjoOpenCaseRequest, AhjoRequestType.OPEN_CASE, "POST", "ahjo_callback_url"), + ( + AhjoSubscribeDecisionRequest, + AhjoRequestType.SUBSCRIBE_TO_DECISIONS, + "POST", + "/decisions/subscribe", + ), + ( + AhjoDecisionMakerRequest, + AhjoRequestType.GET_DECISION_MAKER, + "GET", + "/agents/decisionmakers?start=", + ), + ], +) +def test_ahjo_requests_without_application( + ahjo_request_class, + request_type, + request_method, + url_part, + settings, + non_expired_token, +): + AhjoSetting.objects.create(name="ahjo_org_identifier", data={"id": "1234567-8"}) + settings.API_BASE_URL = "http://test.com" + request_instance = ahjo_request_class() + + assert request_instance.request_type == request_type + assert request_instance.request_method == request_method + assert request_instance.url_base == f"{settings.AHJO_REST_API_URL}" + assert request_instance.lang == "fi" + assert str(request_instance) == f"Request of type {request_type}" + + assert f"{settings.AHJO_REST_API_URL}{url_part}" in request_instance.api_url() + + client = AhjoApiClient(non_expired_token, request_instance) + + with requests_mock.Mocker() as m: + m.register_uri( + request_instance.request_method, + request_instance.api_url(), + text="ahjoRequestGuid", + ) + client.send_request_to_ahjo({"foo": "bar"}) + assert m.called + + +@pytest.mark.parametrize( + "ahjo_request_class, request_type, request_method, callback_route, ahjo_status_after_request", + [ + ( + AhjoOpenCaseRequest, + AhjoRequestType.OPEN_CASE, + "POST", + "ahjo_callback_url", + AhjoStatusEnum.REQUEST_TO_OPEN_CASE_SENT, + ), ( AhjoDecisionProposalRequest, AhjoRequestType.SEND_DECISION_PROPOSAL, "POST", "ahjo_callback_url", + AhjoStatusEnum.DECISION_PROPOSAL_SENT, ), ( AhjoUpdateRecordsRequest, AhjoRequestType.UPDATE_APPLICATION, "PUT", "ahjo_callback_url", + AhjoStatusEnum.UPDATE_REQUEST_SENT, ), ( AhjoDeleteCaseRequest, AhjoRequestType.DELETE_APPLICATION, "DELETE", "ahjo_callback_url", + AhjoStatusEnum.DELETE_REQUEST_SENT, ), ( AhjoAddRecordsRequest, AhjoRequestType.ADD_RECORDS, "POST", "ahjo_callback_url", + AhjoStatusEnum.NEW_RECORDS_REQUEST_SENT, ), ( - AhjoSubscribeDecisionRequest, - AhjoRequestType.SUBSCRIBE_TO_DECISIONS, - "POST", + AhjoDecisionDetailsRequest, + AhjoRequestType.GET_DECISION_DETAILS, + "GET", "", + AhjoStatusEnum.DECISION_DETAILS_REQUEST_SENT, ), - (AhjoDecisionDetailsRequest, AhjoRequestType.GET_DECISION_DETAILS, "GET", ""), ], ) -def test_ahjo_requests( +def test_ahjo_application_requests( ahjo_request_class, application_with_ahjo_case_id, callback_route, - dummy_token, + non_expired_token, request_type, request_method, settings, + ahjo_status_after_request, ): application = application_with_ahjo_case_id + ahjo_status = AhjoStatus.objects.create( + application=application, status=AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO + ) + + ahjo_status.created_at = timezone.now() - timedelta(days=5) + ahjo_status.save() + handler = application.calculation.handler handler.ad_username = "test" handler.save() @@ -121,8 +180,6 @@ def test_ahjo_requests( request.api_url() == f"{url}?draftsmanid={draftsman_id}&reason={reason}&apireqlang={request.lang}" ) - elif request.request_type == AhjoRequestType.SUBSCRIBE_TO_DECISIONS: - assert request.api_url() == f"{settings.AHJO_REST_API_URL}/decisions/subscribe" elif request.request_type == AhjoRequestType.GET_DECISION_DETAILS: assert ( @@ -130,10 +187,9 @@ def test_ahjo_requests( == f"{settings.AHJO_REST_API_URL}/decisions/{application.ahjo_case_id}" ) - client = AhjoApiClient(dummy_token, request) + client = AhjoApiClient(non_expired_token, request) if request.request_type not in [ - AhjoRequestType.SUBSCRIBE_TO_DECISIONS, AhjoRequestType.GET_DECISION_DETAILS, ]: url = reverse( @@ -145,14 +201,14 @@ def test_ahjo_requests( ) assert client.prepare_ahjo_headers() == { - "Authorization": f"Bearer {dummy_token.access_token}", + "Authorization": f"Bearer {non_expired_token.access_token}", "Accept": "application/hal+json", "X-CallbackURL": f"{settings.API_BASE_URL}{url}", "Content-Type": "application/json", } else: assert client.prepare_ahjo_headers() == { - "Authorization": f"Bearer {dummy_token.access_token}", + "Authorization": f"Bearer {non_expired_token.access_token}", "Content-Type": "application/json", } @@ -162,6 +218,10 @@ def test_ahjo_requests( ) client.send_request_to_ahjo({"foo": "bar"}) assert m.called + application.refresh_from_db() + assert ( + application.ahjo_status.latest().status == ahjo_status_after_request.value + ) @pytest.mark.parametrize( @@ -217,7 +277,7 @@ def test_requests_exceptions( application_with_ahjo_case_id, decided_application, ahjo_request_class, - dummy_token, + non_expired_token, request_type, request_method, previous_status, @@ -246,7 +306,7 @@ def test_requests_exceptions( configured_request = ahjo_request_class(application) - client = AhjoApiClient(dummy_token, configured_request) + client = AhjoApiClient(non_expired_token, configured_request) with requests_mock.Mocker() as m: # an example of a real validation error response from ahjo diff --git a/backend/benefit/applications/tests/test_talpa_integration.py b/backend/benefit/applications/tests/test_talpa_integration.py index 1bd0db05ef..ac6c1b86c4 100644 --- a/backend/benefit/applications/tests/test_talpa_integration.py +++ b/backend/benefit/applications/tests/test_talpa_integration.py @@ -1,5 +1,5 @@ -import datetime import decimal +from datetime import datetime, timezone import pytest from django.urls import reverse @@ -101,12 +101,13 @@ def test_talpa_csv_date(pruned_applications_csv_service_with_one_application): application = ( pruned_applications_csv_service_with_one_application.get_applications().first() ) - application.batch.decision_date = datetime.date(2021, 8, 27) + now = datetime.now(timezone.utc) + application.batch.decision_date = now application.batch.save() csv_lines = split_lines_at_semicolon( pruned_applications_csv_service_with_one_application.get_csv_string() ) - assert csv_lines[1][12] == '"2021-08-27"' + assert csv_lines[1][12] == f'"{now.strftime("%Y-%m-%d")}"' def test_write_talpa_csv_file( diff --git a/backend/benefit/helsinkibenefit/settings.py b/backend/benefit/helsinkibenefit/settings.py index bfd8f5e0c7..2158ec80f4 100644 --- a/backend/benefit/helsinkibenefit/settings.py +++ b/backend/benefit/helsinkibenefit/settings.py @@ -175,6 +175,7 @@ ENABLE_CLAMAV=(bool, False), CLAMAV_URL=(str, ""), ENABLE_AHJO_AUTOMATION=(bool, False), + AHJO_RETRY_FAILED_OLDER_THAN=(int, 1), TALPA_CALLBACK_ENABLED=(bool, False), ) if os.path.exists(env_file): @@ -561,4 +562,5 @@ CLAMAV_URL = env.str("CLAMAV_URL") ENABLE_AHJO_AUTOMATION = env.bool("ENABLE_AHJO_AUTOMATION") DEFAULT_SYSTEM_EMAIL = env.str("DEFAULT_SYSTEM_EMAIL") +AHJO_RETRY_FAILED_OLDER_THAN = env.int("AHJO_RETRY_FAILED_OLDER_THAN") TALPA_CALLBACK_ENABLED = env.bool("TALPA_CALLBACK_ENABLED")