diff --git a/backend/benefit/applications/api/v1/application_batch_views.py b/backend/benefit/applications/api/v1/application_batch_views.py index 33b7e3c1dc..96eeddeed1 100755 --- a/backend/benefit/applications/api/v1/application_batch_views.py +++ b/backend/benefit/applications/api/v1/application_batch_views.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ from django_filters import rest_framework as filters from django_filters.widgets import CSVWidget -from drf_spectacular.utils import extend_schema, OpenApiParameter +from drf_spectacular.utils import extend_schema from rest_framework import filters as drf_filters, status from rest_framework.decorators import action from rest_framework.permissions import AllowAny @@ -21,11 +21,7 @@ ApplicationBatchListSerializer, ApplicationBatchSerializer, ) -from applications.enums import ( - ApplicationBatchStatus, - ApplicationStatus, - ApplicationTalpaStatus, -) +from applications.enums import ApplicationBatchStatus, ApplicationStatus from applications.exceptions import ( BatchCompletionDecisionDateError, BatchCompletionRequiredFieldsError, @@ -33,7 +29,8 @@ ) from applications.models import Application, ApplicationBatch from applications.services.ahjo_integration import export_application_batch -from applications.services.applications_csv_report import ApplicationsCsvService +from applications.services.talpa_csv_service import TalpaCsvService +from calculator.enums import InstalmentStatus from common.authentications import RobotBasicAuthentication from common.permissions import BFIsHandler from common.utils import get_request_ip_address @@ -163,15 +160,6 @@ def export_batch(self, request, *args, **kwargs): return response @extend_schema( - parameters=[ - OpenApiParameter( - name="skip_update", - description="Skip updating the batch status", - required=False, - type=str, - enum=["0", "1"], - ), - ], description="""Get application batches for the TALPA robot. Set skip_update=1 to skip updating the batch status to SENT_TO_TALPA""", methods=["GET"], @@ -188,15 +176,23 @@ def talpa_export_batch(self, request, *args, **kwargs) -> HttpResponse: """ Export ApplicationBatch to CSV format for Talpa Robot """ - skip_update = ( - request.query_params.get("skip_update") - and request.query_params.get("skip_update") == "1" - ) + # if instalments feature is enabled, get the applications based on instalments + # TODO Remove this when instalments feature is completed + if settings.PAYMENT_INSTALMENTS_ENABLED: + applications_for_csv = Application.objects.with_due_instalments( + InstalmentStatus.ACCEPTED + ) + approved_batches = None + else: + # Else get the applications based on the batch + approved_batches = ApplicationBatch.objects.filter( + status=ApplicationBatchStatus.DECIDED_ACCEPTED + ) + applications_for_csv = Application.objects.filter( + batch__in=approved_batches + ).order_by("company__name", "application_number") - approved_batches = ApplicationBatch.objects.filter( - status=ApplicationBatchStatus.DECIDED_ACCEPTED - ) - if approved_batches.count() == 0: + if applications_for_csv.count() == 0: return Response( { "detail": _( @@ -206,10 +202,8 @@ def talpa_export_batch(self, request, *args, **kwargs) -> HttpResponse: }, status=status.HTTP_404_NOT_FOUND, ) - applications = Application.objects.filter(batch__in=approved_batches).order_by( - "company__name", "application_number" - ) - csv_service = ApplicationsCsvService(applications, True) + + csv_service = TalpaCsvService(applications_for_csv) file_name = format_lazy( _("TALPA export {date}"), date=timezone.now().strftime("%Y%m%d_%H%M%S"), @@ -223,37 +217,19 @@ def talpa_export_batch(self, request, *args, **kwargs) -> HttpResponse: response["Content-Disposition"] = "attachment; filename={filename}.csv".format( filename=file_name ) - if settings.TALPA_CALLBACK_ENABLED is False: - # for easier testing in the test environment do not update the batches as sent_to_talpa - # remove this when TALPA integration is ready for production - if not skip_update: - ip_address = get_request_ip_address(request) - try: - # Update all approved batches to SENT_TO_TALPA status in a single query - approved_batches.update(status=ApplicationBatchStatus.SENT_TO_TALPA) - # Update all applications in the approved batches to - # SUCCESSFULLY_SENT_TO_TALPA status and archived=True - for a in applications: - a.talpa_status = ( - ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA - ) - a.archived = True - a.save() - - audit_logging.log( - AnonymousUser, - "", - Operation.READ, - a, - ip_address=ip_address, - additional_information="application csv data was downloaded by TALPA\ - and it was marked as archived", - ) - - except Exception as e: - LOGGER.error( - f"An error occurred while updating batches after Talpa csv download: {e}" - ) + + ip_address = get_request_ip_address(request) + + for a in applications_for_csv: + audit_logging.log( + AnonymousUser, + "", + Operation.READ, + a, + ip_address=ip_address, + additional_information="application csv data was downloaded by TALPA robot", + ) + return response @action(methods=["PATCH"], detail=False) diff --git a/backend/benefit/applications/api/v1/application_views.py b/backend/benefit/applications/api/v1/application_views.py index 1079ce66b6..7fa76eb549 100755 --- a/backend/benefit/applications/api/v1/application_views.py +++ b/backend/benefit/applications/api/v1/application_views.py @@ -807,7 +807,8 @@ def batch_p2p_file(self, request) -> HttpResponse: @transaction.atomic def export_new_accepted_applications_csv_pdf(self, request) -> HttpResponse: return self._csv_pdf_response( - self._create_application_batch(ApplicationStatus.ACCEPTED), True, True + self._create_application_batch(ApplicationStatus.ACCEPTED), + remove_quotes=True, ) @action(methods=["GET"], detail=False) @@ -877,7 +878,6 @@ def _export_filename_without_suffix(): def _csv_response( self, queryset: QuerySet[Application], - prune_data_for_talpa: bool = False, remove_quotes: bool = False, prune_sensitive_data: bool = True, compact_list: bool = False, @@ -891,7 +891,6 @@ def _csv_response( else: csv_service = ApplicationsCsvService( queryset.order_by(self.APPLICATION_ORDERING), - prune_data_for_talpa, prune_sensitive_data, ) response = StreamingHttpResponse( @@ -911,14 +910,13 @@ def _csv_response( def _csv_pdf_response( self, queryset: QuerySet[Application], - prune_data_for_talpa: bool = False, remove_quotes: bool = False, ) -> HttpResponse: ordered_queryset = queryset.order_by(self.APPLICATION_ORDERING) export_filename_without_suffix = self._export_filename_without_suffix() csv_file = prepare_csv_file( - ordered_queryset, prune_data_for_talpa, export_filename_without_suffix + ordered_queryset, remove_quotes, export_filename_without_suffix ) pdf_files: List[ExportFileInfo] = prepare_pdf_files(ordered_queryset) diff --git a/backend/benefit/applications/api/v1/talpa_integration_views.py b/backend/benefit/applications/api/v1/talpa_integration_views.py index e3f4141f41..7dde85bfc8 100644 --- a/backend/benefit/applications/api/v1/talpa_integration_views.py +++ b/backend/benefit/applications/api/v1/talpa_integration_views.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.db import transaction +from django.utils import timezone from rest_framework import status from rest_framework.permissions import AllowAny from rest_framework.response import Response @@ -12,6 +13,7 @@ from applications.api.v1.serializers.talpa_callback import TalpaCallbackSerializer from applications.enums import ApplicationBatchStatus, ApplicationTalpaStatus from applications.models import Application, ApplicationBatch +from calculator.enums import InstalmentStatus from common.authentications import RobotBasicAuthentication from common.utils import get_request_ip_address from shared.audit_log import audit_logging @@ -64,33 +66,71 @@ def _get_applications(self, application_numbers) -> Union[List[Application], Non return None return applications + def _get_applications_and_instalments( + self, application_numbers + ) -> Union[List[Application], None]: + applications = Application.objects.with_due_instalments( + InstalmentStatus.ACCEPTED + ).filter(application_number__in=application_numbers) + + if not applications.exists() and application_numbers: + LOGGER.error( + f"No applications found with numbers: {application_numbers} for update after TALPA download" + ) + return [] + return applications + + @transaction.atomic def _handle_successful_applications( self, application_numbers: list, ip_address: str ): - applications = self._get_applications(application_numbers) - self.update_application_and_related_batch( - applications, - ip_address, - ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA, - ApplicationBatchStatus.SENT_TO_TALPA, - "application was read succesfully by TALPA and archived", - is_archived=True, - ) + if settings.PAYMENT_INSTALMENTS_ENABLED: + applications = self._get_applications_and_instalments(application_numbers) + + self.do_status_updates_based_on_instalments( + applications=applications, + instalment_status=InstalmentStatus.PAID, + ip_address=ip_address, + log_message="instalment was read by TALPA and marked as paid", + is_success=True, + ) + else: + applications = self._get_applications(application_numbers) + self.update_application_and_related_batch( + applications, + ip_address, + ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA, + ApplicationBatchStatus.SENT_TO_TALPA, + "application was read succesfully by TALPA and archived", + is_archived=True, + ) @transaction.atomic def _handle_failed_applications(self, application_numbers: list, ip_address: str): """Update applications and related batch which could not be processed with status REJECTED_BY_TALPA""" - applications = self._get_applications(application_numbers) - self.update_application_and_related_batch( - applications, - ip_address, - ApplicationTalpaStatus.REJECTED_BY_TALPA, - ApplicationBatchStatus.REJECTED_BY_TALPA, - "application was rejected by TALPA", - ) - @staticmethod + if settings.PAYMENT_INSTALMENTS_ENABLED: + applications = self._get_applications_and_instalments(application_numbers) + + self.do_status_updates_based_on_instalments( + applications=applications, + instalment_status=InstalmentStatus.ERROR_IN_TALPA, + ip_address=ip_address, + log_message="there was an error and the instalment was not read by TALPA", + is_success=False, + ) + else: + applications = self._get_applications(application_numbers) + self.update_application_and_related_batch( + applications, + ip_address, + ApplicationTalpaStatus.REJECTED_BY_TALPA, + ApplicationBatchStatus.REJECTED_BY_TALPA, + "application was rejected by TALPA", + ) + def update_application_and_related_batch( + self, applications: List[Application], ip_address: str, application_talpa_status: ApplicationTalpaStatus, @@ -98,7 +138,9 @@ def update_application_and_related_batch( log_message: str, is_archived: bool = False, ): - """Update applications and related batch with given statuses and log the event""" + """Update applications and related batch with given statuses and log the event. + This will be deprecated after the instalments feature is enabled for all applications. + """ applications.update( talpa_status=application_talpa_status, archived=is_archived, @@ -109,11 +151,53 @@ def update_application_and_related_batch( for application in applications: """Add audit log entries for applications which were processed by TALPA""" - audit_logging.log( - AnonymousUser, - "", - Operation.READ, - application, - ip_address=ip_address, - additional_information=log_message, + self.write_to_audit_log(application, ip_address, log_message) + + def do_status_updates_based_on_instalments( + self, + applications: List[Application], + instalment_status: InstalmentStatus, + ip_address: str, + log_message: str, + is_success: bool = False, + ): + """ + After receiving the callback from Talpa, query the currently due instalments of the + successful applications and update the status of the instalments. + If the instalmentis 1/1 or 2/2, e.g the final instalment, + update the application status, batch status and set the application as archived + """ + for application in applications: + instalment = application.calculation.instalments.get( + status=InstalmentStatus.ACCEPTED, + due_date__lte=timezone.now().date(), ) + instalment.status = instalment_status + instalment.save() + + if is_success: + if application.number_of_instalments == 1 or ( + application.number_of_instalments == 2 + and instalment.instalment_number == 2 + ): + self.update_after_all_instalments_are_sent(application) + + """Add audit log entries for applications which were processed by TALPA""" + self.write_to_audit_log(application, ip_address, log_message) + + def update_after_all_instalments_are_sent(self, application: Application): + application.archived = True + application.talpa_status = ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA + application.save() + application.batch.status = ApplicationBatchStatus.SENT_TO_TALPA + application.batch.save() + + def write_to_audit_log(self, application, ip_address, log_message): + audit_logging.log( + AnonymousUser, + "", + Operation.READ, + application, + ip_address=ip_address, + additional_information=log_message, + ) diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py index a4cdad24fc..75a28eec1f 100755 --- a/backend/benefit/applications/models.py +++ b/backend/benefit/applications/models.py @@ -36,6 +36,7 @@ BatchCompletionRequiredFieldsError, BatchTooManyDraftsError, ) +from calculator.enums import InstalmentStatus from common.localized_iban_field import LocalizedIBANField from common.utils import DurationMixin from companies.models import Company @@ -169,6 +170,18 @@ def with_non_downloaded_attachments(self): # Return the filtered applications with the specified prefetched related attachments return qs.prefetch_related(attachments_prefetch) + def with_due_instalments(self, status: InstalmentStatus): + """Query applications with instalments with past due date and a specific status.""" + return ( + self.filter( + calculation__instalments__due_date__lte=timezone.now().date(), + calculation__instalments__status=status, + ) + .select_related("calculation") + .select_related("batch") + .prefetch_related("calculation__instalments") + ) + def get_by_statuses( self, application_statuses: List[ApplicationStatus], @@ -562,6 +575,10 @@ def get_available_benefit_types(self): blank=True, ) + @property + def number_of_instalments(self): + return self.calculation.instalments.count() + @property def calculated_benefit_amount(self): if hasattr(self, "calculation"): diff --git a/backend/benefit/applications/services/ahjo_integration.py b/backend/benefit/applications/services/ahjo_integration.py index 50a2e62ddf..e0938d12fd 100644 --- a/backend/benefit/applications/services/ahjo_integration.py +++ b/backend/benefit/applications/services/ahjo_integration.py @@ -284,13 +284,13 @@ def prepare_pdf_files(apps: QuerySet[Application]) -> List[ExportFileInfo]: def prepare_csv_file( ordered_queryset: QuerySet[Application], - prune_data_for_talpa: bool = False, + remove_quotes: bool = False, export_filename: str = "", ) -> ExportFileInfo: - csv_service = ApplicationsCsvService(ordered_queryset, prune_data_for_talpa) - csv_file_content: bytes = csv_service.get_csv_string(prune_data_for_talpa).encode( - "utf-8" - ) + csv_service = ApplicationsCsvService(ordered_queryset) + csv_file_content: bytes = csv_service.get_csv_string( + remove_quotes=remove_quotes + ).encode("utf-8") csv_filename = f"{export_filename}.csv" csv_file_info: ExportFileInfo = ExportFileInfo( filename=csv_filename, diff --git a/backend/benefit/applications/services/applications_csv_report.py b/backend/benefit/applications/services/applications_csv_report.py index a33fe43a29..af980d1d32 100644 --- a/backend/benefit/applications/services/applications_csv_report.py +++ b/backend/benefit/applications/services/applications_csv_report.py @@ -94,53 +94,16 @@ class ApplicationsCsvService(CsvExportBase): For easier processing, if an application would need two Ahjo rows, the two rows are produced in the output. - If the prune_data_for_talpa flag is set, then only the columns needed for Talpa are included in the output. - - """ - def __init__( - self, applications, prune_data_for_talpa=False, prune_sensitive_data=False - ): + def __init__(self, applications, prune_sensitive_data=False): self.applications = applications self.export_notes = [] - self.prune_data_for_talpa = prune_data_for_talpa self.prune_sensitive_data = prune_sensitive_data @property - def CSV_COLUMNS(self): + def CSV_COLUMNS(self) -> List[CsvColumn]: calculated_benefit_amount = "calculation.calculated_benefit_amount" - """Return only columns that are needed for Talpa""" - if self.prune_data_for_talpa: - talpa_columns = [ - CsvColumn("Hakemusnumero", "application_number"), - CsvColumn("Työnantajan tyyppi", get_organization_type), - CsvColumn("Työnantajan tilinumero", "company_bank_account_number"), - CsvColumn("Työnantajan nimi", "company_name"), - CsvColumn("Työnantajan Y-tunnus", "company.business_id"), - CsvColumn("Työnantajan katuosoite", "effective_company_street_address"), - CsvColumn("Työnantajan postinumero", "effective_company_postcode"), - CsvColumn("Työnantajan postitoimipaikka", "effective_company_city"), - csv_default_column( - "Helsinki-lisän määrä lopullinen", calculated_benefit_amount - ), - csv_default_column("Päättäjän nimike", "batch.decision_maker_title"), - csv_default_column("Päättäjän nimi", "batch.decision_maker_name"), - csv_default_column("Päätöspykälä", "batch.section_of_the_law"), - csv_default_column("Päätöspäivä", "batch.decision_date"), - csv_default_column( - "Asiantarkastajan nimi Ahjo", "batch.expert_inspector_name" - ), - csv_default_column( - "Asiantarkastajan titteli Ahjo", "batch.expert_inspector_title" - ), - csv_default_column("Tarkastajan nimi, P2P", "batch.p2p_inspector_name"), - csv_default_column( - "Tarkastajan sähköposti, P2P", "batch.p2p_inspector_email" - ), - csv_default_column("Hyväksyjän nimi P2P", "batch.p2p_checker_name"), - ] - return talpa_columns columns = [ CsvColumn("Hakemusnumero", "application_number"), @@ -434,13 +397,6 @@ def get_applications(self): def get_row_items(self): with translation.override("fi"): for application in self.get_applications(): - # for applications with multiple ahjo rows, output the same number of rows. - # If no Ahjo rows (calculation incomplete), always output just one row. - if self.prune_data_for_talpa: - # For Talpa, only one row per application is needed - application.application_row_idx = 1 - yield application - continue for application_row_idx, unused in enumerate( application.ahjo_rows or [None] ): diff --git a/backend/benefit/applications/services/talpa_csv_service.py b/backend/benefit/applications/services/talpa_csv_service.py new file mode 100644 index 0000000000..96be91d2e5 --- /dev/null +++ b/backend/benefit/applications/services/talpa_csv_service.py @@ -0,0 +1,81 @@ +import decimal +import logging + +from django.conf import settings +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.utils import timezone, translation + +from applications.models import Application +from applications.services.applications_csv_report import ( + ApplicationsCsvService, + csv_default_column, +) +from applications.services.csv_export_base import CsvColumn, get_organization_type +from calculator.enums import InstalmentStatus + +LOGGER = logging.getLogger(__name__) + + +class TalpaCsvService(ApplicationsCsvService): + """Return only columns that are needed for Talpa""" + + def get_relevant_instalment_amount( + self, application: Application + ) -> decimal.Decimal: + """Return the amount of the currently accepted and due instalment""" + # TODO remove this flag when the feature is enabled ready for production + if settings.PAYMENT_INSTALMENTS_ENABLED: + try: + instalment = application.calculation.instalments.get( + status=InstalmentStatus.ACCEPTED, + due_date__lte=timezone.now().date(), + ) + return instalment.amount + except ObjectDoesNotExist: + LOGGER.error( + f"Valid payable Instalment not found for application {application.application_number}" + ) + except MultipleObjectsReturned: + LOGGER.error( + f"Multiple payable Instalments found for application \ +{application.application_number}, there should be only one" + ) + return application.calculation.calculated_benefit_amount + + @property + def CSV_COLUMNS(self): + columns = [ + CsvColumn("Hakemusnumero", "application_number"), + CsvColumn("Työnantajan tyyppi", get_organization_type), + CsvColumn("Työnantajan tilinumero", "company_bank_account_number"), + CsvColumn("Työnantajan nimi", "company_name"), + CsvColumn("Työnantajan Y-tunnus", "company.business_id"), + CsvColumn("Työnantajan katuosoite", "effective_company_street_address"), + CsvColumn("Työnantajan postinumero", "effective_company_postcode"), + CsvColumn("Työnantajan postitoimipaikka", "effective_company_city"), + csv_default_column( + "Helsinki-lisän määrä lopullinen", self.get_relevant_instalment_amount + ), + csv_default_column("Päättäjän nimike", "batch.decision_maker_title"), + csv_default_column("Päättäjän nimi", "batch.decision_maker_name"), + csv_default_column("Päätöspykälä", "batch.section_of_the_law"), + csv_default_column("Päätöspäivä", "batch.decision_date"), + csv_default_column( + "Asiantarkastajan nimi Ahjo", "batch.expert_inspector_name" + ), + csv_default_column( + "Asiantarkastajan titteli Ahjo", "batch.expert_inspector_title" + ), + csv_default_column("Tarkastajan nimi, P2P", "batch.p2p_inspector_name"), + csv_default_column( + "Tarkastajan sähköposti, P2P", "batch.p2p_inspector_email" + ), + csv_default_column("Hyväksyjän nimi P2P", "batch.p2p_checker_name"), + ] + return columns + + def get_row_items(self): + with translation.override("fi"): + for application in self.get_applications(): + application.application_row_idx = 1 + yield application diff --git a/backend/benefit/applications/tests/conftest.py b/backend/benefit/applications/tests/conftest.py index 699a4eb83b..0ca1a669f1 100755 --- a/backend/benefit/applications/tests/conftest.py +++ b/backend/benefit/applications/tests/conftest.py @@ -41,6 +41,7 @@ from applications.services.applications_power_bi_csv_report import ( ApplicationsPowerBiCsvService, ) +from applications.services.talpa_csv_service import TalpaCsvService from applications.tests.factories import ( AcceptedDecisionProposalFactory, AhjoDecisionTextFactory, @@ -174,7 +175,7 @@ def pruned_applications_csv_service(): # retrieve the objects through the default manager so that annotations are added application1 = DecidedApplicationFactory(application_number=100001) application2 = DecidedApplicationFactory(application_number=100002) - return ApplicationsCsvService( + return TalpaCsvService( Application.objects.filter(pk__in=[application1.pk, application2.pk]).order_by( "application_number" ), @@ -187,14 +188,14 @@ def pruned_applications_csv_service_with_one_application( applications_csv_service, application_batch ): application1 = application_batch.applications.all().first() - return ApplicationsCsvService(Application.objects.filter(pk=application1.pk), True) + return TalpaCsvService(Application.objects.filter(pk=application1.pk), True) @pytest.fixture def sanitized_csv_service_with_one_application(application_batch): application1 = application_batch.applications.all().first() return ApplicationsCsvService( - Application.objects.filter(pk=application1.pk), True, True + Application.objects.filter(pk=application1.pk), prune_sensitive_data=True ) diff --git a/backend/benefit/applications/tests/test_application_batch_api.py b/backend/benefit/applications/tests/test_application_batch_api.py index 7fdd5700ab..7d21a7c024 100755 --- a/backend/benefit/applications/tests/test_application_batch_api.py +++ b/backend/benefit/applications/tests/test_application_batch_api.py @@ -7,17 +7,11 @@ import pytest import pytz from dateutil.relativedelta import relativedelta -from django.conf import settings from django.http import HttpResponse from rest_framework.reverse import reverse from applications.api.v1.serializers.application import ApplicationBatchSerializer -from applications.enums import ( - AhjoDecision, - ApplicationBatchStatus, - ApplicationStatus, - ApplicationTalpaStatus, -) +from applications.enums import AhjoDecision, ApplicationBatchStatus, ApplicationStatus from applications.exceptions import BatchTooManyDraftsError from applications.models import Application, ApplicationBatch from applications.tests.conftest import * # noqa @@ -824,7 +818,10 @@ def test_application_batch_export(mock_export, handler_api_client, application_b assert response.status_code == 400 -def test_application_batches_talpa_export(anonymous_client, application_batch): +def test_application_batches_talpa_export( + anonymous_client, application_batch, settings +): + settings.PAYMENT_INSTALMENTS_ENABLED = False url = reverse("v1:applicationbatch-talpa-export-batch") response = anonymous_client.get(url) assert response.status_code == 401 @@ -850,24 +847,22 @@ def test_application_batches_talpa_export(anonymous_client, application_batch): app_batch_2.status = ApplicationBatchStatus.DECIDED_ACCEPTED fill_as_valid_batch_completion_and_save(app_batch_2) - # Export accepted batches then change it status response = anonymous_client.get(f"{url}?skip_update=0") - application_batch.refresh_from_db() - app_batch_2.refresh_from_db() - assert application_batch.status == ApplicationBatchStatus.SENT_TO_TALPA - assert app_batch_2.status == ApplicationBatchStatus.SENT_TO_TALPA - - applications = Application.objects.filter( - batch__in=[application_batch, app_batch_2] - ) - for application in applications: - assert ( - application.talpa_status - == ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA - ) - assert application.archived is True - assert isinstance(response, HttpResponse) assert response.headers["Content-Type"] == "text/csv" assert response.status_code == 200 + + +def test_application_instalments_talpa_export(anonymous_client, settings): + settings.PAYMENT_INSTALMENTS_ENABLED = True + url = reverse("v1:applicationbatch-talpa-export-batch") + + # Add basic auth header + credentials = base64.b64encode(settings.TALPA_ROBOT_AUTH_CREDENTIAL.encode("utf-8")) + anonymous_client.credentials( + HTTP_AUTHORIZATION="Basic {}".format(credentials.decode("utf-8")) + ) + response = anonymous_client.get(f"{url}?skip_update=0") + assert response.status_code == 404 + assert "There is no available application to export" in response.data["detail"] diff --git a/backend/benefit/applications/tests/test_talpa_integration.py b/backend/benefit/applications/tests/test_talpa_integration.py index ac6c1b86c4..76692bb354 100644 --- a/backend/benefit/applications/tests/test_talpa_integration.py +++ b/backend/benefit/applications/tests/test_talpa_integration.py @@ -1,5 +1,5 @@ import decimal -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import pytest from django.urls import reverse @@ -11,6 +11,8 @@ ) from applications.tests.conftest import * # noqa from applications.tests.conftest import split_lines_at_semicolon +from calculator.enums import InstalmentStatus +from calculator.models import Instalment from common.tests.conftest import * # noqa from helsinkibenefit.tests.conftest import * # noqa from shared.audit_log.models import AuditLogEntry @@ -149,11 +151,41 @@ def test_talpa_callback_is_disabled( assert response.data == {"message": "Talpa callback is disabled"} +@pytest.mark.parametrize( + "instalments_enabled, number_of_instalments", + [ + (False, 1), + (True, 1), + (False, 2), + (True, 2), + ], +) @pytest.mark.django_db def test_talpa_callback_success( - talpa_client, decided_application, application_batch, settings + talpa_client, + decided_application, + application_batch, + settings, + instalments_enabled, + number_of_instalments, ): settings.TALPA_CALLBACK_ENABLED = True + settings.PAYMENT_INSTALMENTS_ENABLED = instalments_enabled + decided_application.calculation.instalments.all().delete() + + if instalments_enabled: + for i in range(number_of_instalments): + due_date = datetime.now(timezone.utc).date() + if i == 1: + due_date = timezone.now() + timedelta(days=181) + + Instalment.objects.create( + calculation=decided_application.calculation, + amount=decimal.Decimal("123.45"), + instalment_number=i + 1, + status=InstalmentStatus.ACCEPTED, + due_date=due_date, + ) decided_application.batch = application_batch decided_application.save() @@ -174,6 +206,7 @@ def test_talpa_callback_success( assert response.data == {"message": "Callback received"} audit_log_entry = AuditLogEntry.objects.latest("created_at") + assert ( audit_log_entry.message["audit_event"]["target"]["id"] == f"{decided_application.id}" @@ -181,21 +214,69 @@ def test_talpa_callback_success( decided_application.refresh_from_db() - assert ( - decided_application.talpa_status - == ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA - ) - - assert decided_application.batch.status == ApplicationBatchStatus.SENT_TO_TALPA - - assert decided_application.archived is True - - + if instalments_enabled: + instalments = decided_application.calculation.instalments.filter( + due_date__lte=timezone.now().date() + ) + assert len(instalments) == 1 + for instalment in instalments: + assert instalment.status == InstalmentStatus.PAID + if number_of_instalments == 1: + assert ( + decided_application.talpa_status + == ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA + ) + assert decided_application.archived is True + assert ( + decided_application.batch.status == ApplicationBatchStatus.SENT_TO_TALPA + ) + else: + assert ( + decided_application.talpa_status + == ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA + ) + + assert decided_application.batch.status == ApplicationBatchStatus.SENT_TO_TALPA + + assert decided_application.archived is True + + +@pytest.mark.parametrize( + "instalments_enabled, number_of_instalments", + [ + (False, 1), + (True, 1), + (False, 2), + (True, 2), + ], +) @pytest.mark.django_db def test_talpa_callback_rejected_application( - talpa_client, decided_application, application_batch, settings + talpa_client, + decided_application, + application_batch, + settings, + instalments_enabled, + number_of_instalments, ): settings.TALPA_CALLBACK_ENABLED = True + settings.PAYMENT_INSTALMENTS_ENABLED = instalments_enabled + decided_application.calculation.instalments.all().delete() + + if instalments_enabled: + for i in range(number_of_instalments): + due_date = datetime.now(timezone.utc).date() + if i == 1: + due_date = timezone.now() + timedelta(days=181) + + Instalment.objects.create( + calculation=decided_application.calculation, + amount=decimal.Decimal("123.45"), + instalment_number=i + 1, + status=InstalmentStatus.ACCEPTED, + due_date=due_date, + ) + decided_application.batch = application_batch decided_application.save() @@ -204,7 +285,7 @@ def test_talpa_callback_rejected_application( ) payload = { - "status": "Success", + "status": "Failure", "successful_applications": [], "failed_applications": [decided_application.application_number], } @@ -217,5 +298,20 @@ def test_talpa_callback_rejected_application( decided_application.refresh_from_db() decided_application.archived = False - assert decided_application.talpa_status == ApplicationTalpaStatus.REJECTED_BY_TALPA - assert decided_application.batch.status == ApplicationBatchStatus.REJECTED_BY_TALPA + if instalments_enabled: + instalments = decided_application.calculation.instalments.filter( + due_date__lte=timezone.now().date() + ) + assert len(instalments) == 1 + for instalment in instalments: + assert instalment.status == InstalmentStatus.ERROR_IN_TALPA + + assert decided_application.archived is False + + else: + assert ( + decided_application.talpa_status == ApplicationTalpaStatus.REJECTED_BY_TALPA + ) + assert ( + decided_application.batch.status == ApplicationBatchStatus.REJECTED_BY_TALPA + )