diff --git a/backend/benefit/applications/api/v1/application_views.py b/backend/benefit/applications/api/v1/application_views.py index c3798182a4..65b24265b3 100755 --- a/backend/benefit/applications/api/v1/application_views.py +++ b/backend/benefit/applications/api/v1/application_views.py @@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta from django.conf import settings from django.core import exceptions +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db import transaction from django.db.models import Prefetch, Q, QuerySet from django.http import FileResponse, HttpResponse, StreamingHttpResponse @@ -41,13 +42,22 @@ ApplicationOrigin, ApplicationStatus, ) -from applications.models import Application, ApplicationAlteration, ApplicationBatch +from applications.models import ( + AhjoSetting, + Application, + ApplicationAlteration, + ApplicationBatch, +) from applications.services.ahjo_integration import ( ExportFileInfo, generate_zip, prepare_csv_file, prepare_pdf_files, ) +from applications.services.application_alteration_csv_report import ( + AlterationCsvConfigurableFields, + ApplicationAlterationCsvService, +) from applications.services.applications_csv_report import ApplicationsCsvService from applications.services.generate_application_summary import ( generate_application_summary_file, @@ -472,6 +482,64 @@ def update(self, request, *args, **kwargs): else: raise PermissionDenied(_("You are not allowed to do this action")) + @action(methods=["PATCH"], detail=False) + def update_with_csv(self, request): + """ + Update alteration and respond with a CSV file. + """ + alteration = ApplicationAlteration.objects.get( + application_id__in=[request.GET.get("application_id")], + id__in=[request.GET.get("alteration_id")], + ) + + alteration.recovery_justification = request.data.get("recovery_justification") + alteration.recovery_amount = request.data.get("recovery_amount") + alteration.recovery_end_date = request.data.get("recovery_end_date") + alteration.recovery_start_date = request.data.get("recovery_start_date") + + alteration.save() + # CsvService requires a queryset, so we need to create a queryset with the alteration + queryset = ApplicationAlteration.objects.filter( + id__in=[alteration.id], + ) + try: + alteration_fields = AhjoSetting.objects.get( + name="application_alteration_fields" + ) + configurable_fields = AlterationCsvConfigurableFields( + account_number=alteration_fields.data["account_number"], + billing_department=alteration_fields.data["billing_department"], + ) + return self._alterations_csv_response(queryset, configurable_fields) + except ObjectDoesNotExist: + raise ImproperlyConfigured( + "application_alteration_fields fields not found in the ahjo_settings table" + ) + + def _alterations_csv_response( + self, + queryset: QuerySet[ApplicationAlteration], + config: AlterationCsvConfigurableFields, + ) -> StreamingHttpResponse: + """Generate a response with a CSV file containing application alteration data.""" + csv_service = ApplicationAlterationCsvService(queryset, config) + + response = HttpResponse( + csv_service.get_csv_string(True).encode("utf-8"), + content_type="text/csv", + ) + response["Content-Disposition"] = "attachment; filename={filename}.csv".format( + filename=self._alteration_filename() + ) + return response + + @staticmethod + def _alteration_filename(): + return format_lazy( + _("Takaisinmaksu viety {date}"), + date=timezone.now().strftime("%Y%m%d_%H%M%S"), + ) + @extend_schema( description=( @@ -681,6 +749,7 @@ def _csv_response( csv_service.get_csv_string_lines_generator(remove_quotes), content_type="text/csv", ) + response["Content-Disposition"] = "attachment; filename={filename}.csv".format( filename=self._export_filename_without_suffix() ) diff --git a/backend/benefit/applications/management/commands/seed.py b/backend/benefit/applications/management/commands/seed.py index f0b50a5958..0d1725b633 100755 --- a/backend/benefit/applications/management/commands/seed.py +++ b/backend/benefit/applications/management/commands/seed.py @@ -8,6 +8,7 @@ from applications.enums import ( AhjoStatus as AhjoStatusEnum, + ApplicationAlterationType, ApplicationBatchStatus, ApplicationOrigin, ApplicationStatus, @@ -27,6 +28,7 @@ from applications.tests.factories import ( AcceptedDecisionProposalFactory, AdditionalInformationNeededApplicationFactory, + ApplicationAlterationFactory, ApplicationBatchFactory, ApplicationWithAttachmentFactory, CancelledApplicationFactory, @@ -112,6 +114,12 @@ def _create_batch( application=app, ) + ApplicationAlterationFactory( + application=app, + alteration_type=ApplicationAlterationType.TERMINATION, + handled_by=app.calculation.handler, + ) + elif proposal_for_decision == ApplicationStatus.REJECTED: app = RejectedApplicationFactory() create_decision_text_for_application( diff --git a/backend/benefit/applications/services/application_alteration_csv_report.py b/backend/benefit/applications/services/application_alteration_csv_report.py new file mode 100644 index 0000000000..6d2cb7202a --- /dev/null +++ b/backend/benefit/applications/services/application_alteration_csv_report.py @@ -0,0 +1,99 @@ +from dataclasses import dataclass +from typing import Union + +from django.db.models.query import QuerySet + +from applications.models import ApplicationAlteration +from applications.services.csv_export_base import CsvColumn, CsvExportBase + + +@dataclass +class AlterationCsvConfigurableFields: + """Configuration for changeable fields of application alteration csv. + These values can change and should be stored in the db or in the settings.py. + """ + + account_number: str + billing_department: str = "1800 Kaupunginkanslia (Kansl)" + + +class ApplicationAlterationCsvService(CsvExportBase): + def __init__( + self, + application_alterations: QuerySet[ApplicationAlteration], + config: AlterationCsvConfigurableFields, + ): + self.application_alterations = application_alterations + self.billing_department = config.billing_department + self.account_number = config.account_number + + def get_recovery_period( + self, alteration: ApplicationAlteration + ) -> Union[str, None]: + if alteration.recovery_start_date and alteration.recovery_end_date: + start = alteration.recovery_start_date.strftime("%d.%m.%Y") + end = alteration.recovery_end_date.strftime("%d.%m.%Y") + return f"{start} - {end}" + return None + + def get_title(self, alteration: ApplicationAlteration) -> str: + return "Helsinki-lisä takaisinperintä" + + def get_billing_department(self, alteration: ApplicationAlteration) -> str: + return self.billing_department + + def get_account_number(self, alteration: ApplicationAlteration) -> str: + return self.account_number + + def get_handler_name(self, alteration: ApplicationAlteration) -> Union[str, None]: + if alteration.handled_by: + return f"{alteration.handled_by.get_full_name()}, {alteration.handled_by.email}" + return "" + + def get_company_address(self, alteration: ApplicationAlteration) -> str: + return alteration.application.company.get_full_address() + + def get_company_contact_person(self, alteration: ApplicationAlteration) -> str: + application = alteration.application + return f"{application.company_contact_person_first_name} {application.company_contact_person_last_name} \ + {application.company_contact_person_email} {application.company_contact_person_phone_number}" + + def get_recovery_justification(self, alteration: ApplicationAlteration) -> str: + return alteration.recovery_justification + + @property + def CSV_COLUMNS(self): + columns = [ + CsvColumn("Viitetiedot", "application.application_number"), + CsvColumn( + "Aikajakso, jolta tukea peritään takaisin", self.get_recovery_period + ), + CsvColumn("Summatieto", "recovery_amount"), + CsvColumn("Laskutettavan virallinen nimi", "application.company.name"), + CsvColumn("Laskutusosoite", self.get_company_address), + CsvColumn("Y-tunnus", "application.company.business_id"), + CsvColumn("Laskutettavan yhteyshenkilö", self.get_company_contact_person), + CsvColumn("Verkkolaskuosoite/OVT-tunnus", "einvoice_address"), + CsvColumn("Operaattori-/välittäjätunnus", "einvoice_provider_identifier"), + CsvColumn("Tilitunniste", self.get_account_number), + CsvColumn("Lisätietoja antaa", self.get_handler_name), + CsvColumn("Otsikko", self.get_title), + CsvColumn("Laskuttava yksikkö", self.get_billing_department), + CsvColumn("Selite", self.get_recovery_justification), + ] + return columns + + def get_alterations(self): + return self.application_alterations + + def get_row_items(self): + for alteration in self.get_alterations(): + yield alteration + + def get_csv_cell_list_lines_generator(self): + if self.get_alterations(): + yield from super().get_csv_cell_list_lines_generator() + else: + header_row = self._get_header_row() + yield header_row + yield ["Takaisinmaksuja ei löytynyt"] + [""] * (len(header_row) - 1) diff --git a/backend/benefit/applications/tests/conftest.py b/backend/benefit/applications/tests/conftest.py index 25413059f6..5feacfca69 100755 --- a/backend/benefit/applications/tests/conftest.py +++ b/backend/benefit/applications/tests/conftest.py @@ -13,19 +13,30 @@ AhjoDecisionDetails, AhjoRecordTitle, AhjoRecordType, + ApplicationAlterationType, ApplicationStatus, BenefitType, DecisionType, ) -from applications.models import AhjoSetting, Application, ApplicationBatch +from applications.models import ( + AhjoSetting, + Application, + ApplicationAlteration, + ApplicationBatch, +) from applications.services.ahjo_decision_service import ( replace_decision_template_placeholders, ) from applications.services.ahjo_payload import resolve_payload_language +from applications.services.application_alteration_csv_report import ( + AlterationCsvConfigurableFields, + ApplicationAlterationCsvService, +) from applications.services.applications_csv_report import ApplicationsCsvService from applications.tests.factories import ( AcceptedDecisionProposalFactory, AhjoDecisionTextFactory, + ApplicationAlterationFactory, ApplicationBatchFactory, ApplicationFactory, CancelledApplicationFactory, @@ -38,6 +49,7 @@ from common.tests.conftest import * # noqa from companies.tests.conftest import * # noqa from helsinkibenefit.tests.conftest import * # noqa +from shared.common.tests.factories import UserFactory from shared.service_bus.enums import YtjOrganizationCode from terms.tests.conftest import * # noqa from terms.tests.factories import TermsOfServiceApprovalFactory @@ -804,3 +816,43 @@ def batch_for_decision_details(application_with_ahjo_decision): handler=application_with_ahjo_decision.calculation.handler, auto_generated_by_ahjo=True, ) + + +@pytest.fixture +def application_alteration_csv_service(): + application_1 = DecidedApplicationFactory(application_number=100003) + application_2 = DecidedApplicationFactory(application_number=100004) + + handled_by = UserFactory() + + ApplicationAlterationFactory( + application=application_1, + alteration_type=ApplicationAlterationType.TERMINATION, + handled_by=handled_by, + ) + + ApplicationAlterationFactory( + application=application_2, + alteration_type=ApplicationAlterationType.SUSPENSION, + handled_by=handled_by, + ) + + config = AlterationCsvConfigurableFields( + account_number="123456", + ) + + return ApplicationAlterationCsvService( + ApplicationAlteration.objects.filter( + application_id__in=[application_1.id, application_2.id] + ), + config=config, + ) + + +@pytest.fixture +def application_alteration(decided_application): + return ApplicationAlterationFactory( + application=decided_application, + alteration_type=ApplicationAlterationType.TERMINATION, + handled_by=decided_application.handler, + ) diff --git a/backend/benefit/applications/tests/factories.py b/backend/benefit/applications/tests/factories.py index a66986efbb..205843cb3a 100755 --- a/backend/benefit/applications/tests/factories.py +++ b/backend/benefit/applications/tests/factories.py @@ -6,6 +6,7 @@ import factory from applications.enums import ( + ApplicationAlterationState, ApplicationStatus, ApplicationStep, BenefitType, @@ -18,6 +19,7 @@ AhjoDecisionText, Application, APPLICATION_LANGUAGE_CHOICES, + ApplicationAlteration, ApplicationBasis, ApplicationBatch, Attachment, @@ -490,3 +492,60 @@ class DeniedDecisionProposalFactory(factory.django.DjangoModelFactory): class Meta: model = DecisionProposalTemplateSection + + +class ApplicationAlterationFactory(factory.django.DjangoModelFactory): + state = ApplicationAlterationState.RECEIVED + + end_date = factory.Faker( + "date_between_dates", + date_start=factory.LazyAttribute(lambda _: date.today() - timedelta(days=30)), + date_end=factory.LazyAttribute(lambda _: date.today()), + ) + + resume_date = factory.Faker( + "date_between_dates", + date_start=factory.LazyAttribute(lambda _: date.today() - timedelta(days=30)), + date_end=factory.LazyAttribute(lambda _: date.today()), + ) + + reason = factory.Faker("sentence", nb_words=2) + + handled_at = factory.Faker( + "date_between_dates", + date_start=factory.LazyAttribute(lambda _: date.today() - timedelta(days=30)), + date_end=factory.LazyAttribute(lambda _: date.today()), + ) + + recovery_start_date = factory.Faker( + "date_between_dates", + date_start=factory.LazyAttribute(lambda _: date.today() - timedelta(days=30)), + date_end=factory.LazyAttribute(lambda _: date.today()), + ) + + recovery_end_date = factory.Faker( + "date_between_dates", + date_start=factory.LazyAttribute(lambda _: date.today() - timedelta(days=30)), + date_end=factory.LazyAttribute(lambda _: date.today()), + ) + + recovery_amount = factory.Faker( + "pydecimal", left_digits=4, right_digits=2, positive=True + ) + + use_einvoice = factory.Faker("boolean") + + einvoice_provider_name = factory.Faker("company") + + einvoice_provider_identifier = factory.Faker("sentence", nb_words=2) + + einvoice_address = factory.Faker("street_address", locale="fi_FI") + + contact_person_name = factory.Faker("name", locale="fi_FI") + + is_recoverable = factory.Faker("boolean") + + recovery_justification = factory.Faker("sentence", nb_words=2) + + class Meta: + model = ApplicationAlteration diff --git a/backend/benefit/applications/tests/test_applications_report.py b/backend/benefit/applications/tests/test_applications_report.py index 8c052513e3..f5f484220e 100644 --- a/backend/benefit/applications/tests/test_applications_report.py +++ b/backend/benefit/applications/tests/test_applications_report.py @@ -18,7 +18,7 @@ BenefitType, PaySubsidyGranted, ) -from applications.models import ApplicationBatch +from applications.models import AhjoSetting, ApplicationAlteration, ApplicationBatch from applications.tests.common import ( check_csv_cell_list_lines_generator, check_csv_string_lines_generator, @@ -200,6 +200,41 @@ def test_applications_csv_export_new_applications(handler_api_client): assert ApplicationBatch.objects.all().count() == 2 +def test_application_alteration_csv_export( + application_alteration, handler_api_client, decided_application +): + AhjoSetting.objects.create( + name="application_alteration_fields", + data={ + "account_number": "FI1234567890", + "billing_department": "1800 Kaupunginkanslia (Kansl)", + }, + ) + + url = ( + reverse("v1:handler-application-alteration-list") + + f"update_with_csv/?application_id={decided_application.pk}&" + + f"alteration_id={application_alteration.id}" + ) + payload = { + "application": decided_application.pk, + "recovery_start_date": "2024-10-02", + "recovery_end_date": "2024-11-01", + "recovery_amount": "200", + "recovery_justification": "For reasons", + "is_recoverable": True, + } + response = handler_api_client.patch(url, payload) + assert response.status_code == 200 + + updated_alteration = ApplicationAlteration.objects.get(pk=application_alteration.pk) + + assert updated_alteration.recovery_start_date == date(2024, 10, 2) + assert updated_alteration.recovery_end_date == date(2024, 11, 1) + assert updated_alteration.recovery_amount == Decimal("200") + assert updated_alteration.recovery_justification == "For reasons" + + def test_applications_csv_export_without_calculation( handler_api_client, received_application ): @@ -323,6 +358,123 @@ def test_sensitive_data_removed_csv_output(sanitized_csv_service_with_one_applic assert col_heading not in csv_lines[0] +def test_application_alteration_csv_output(application_alteration_csv_service): + csv_lines = split_lines_at_semicolon( + application_alteration_csv_service.get_csv_string() + ) + + alteration_1 = application_alteration_csv_service.get_alterations()[0] + alteration_2 = application_alteration_csv_service.get_alterations()[1] + + assert csv_lines[0][0] == '\ufeff"Viitetiedot"' + assert csv_lines[0][1] == '"Aikajakso, jolta tukea peritään takaisin"' + assert csv_lines[0][2] == '"Summatieto"' + + assert csv_lines[0][3] == '"Laskutettavan virallinen nimi"' + assert csv_lines[0][4] == '"Laskutusosoite"' + assert csv_lines[0][5] == '"Y-tunnus"' + + assert csv_lines[0][6] == '"Laskutettavan yhteyshenkilö"' + assert csv_lines[0][7] == '"Verkkolaskuosoite/OVT-tunnus"' + assert csv_lines[0][8] == '"Operaattori-/välittäjätunnus"' + + assert csv_lines[0][9] == '"Tilitunniste"' + assert csv_lines[0][10] == '"Lisätietoja antaa"' + assert csv_lines[0][11] == '"Otsikko"' + + assert csv_lines[0][12] == '"Laskuttava yksikkö"' + + assert int(csv_lines[1][0]) == alteration_1.application.application_number + assert ( + csv_lines[1][1] + == f'"{application_alteration_csv_service.get_recovery_period(alteration_1)}"' + ) + assert Decimal(csv_lines[1][2]) == alteration_1.recovery_amount + + assert csv_lines[1][3] == f'"{alteration_1.application.company.name}"' + assert ( + csv_lines[1][4] + == f'"{application_alteration_csv_service.get_company_address(alteration_1)}"' + ) + assert csv_lines[1][5] == f'"{alteration_1.application.company.business_id}"' + + assert ( + csv_lines[1][6] + == f'"{application_alteration_csv_service.get_company_contact_person(alteration_1)}"' + ) + assert csv_lines[1][7] == f'"{alteration_1.einvoice_address}"' + assert csv_lines[1][8] == f'"{alteration_1.einvoice_provider_identifier}"' + + assert ( + csv_lines[1][9] + == f'"{application_alteration_csv_service.get_account_number(alteration_1)}"' + ) + assert ( + csv_lines[1][10] + == f'"{application_alteration_csv_service.get_handler_name(alteration_1)}"' + ) + assert ( + csv_lines[1][11] + == f'"{application_alteration_csv_service.get_title(alteration_1)}"' + ) + + assert ( + csv_lines[1][12] + == f'"{application_alteration_csv_service.get_billing_department(alteration_1)}"' + ) + + assert int(csv_lines[2][0]) == alteration_2.application.application_number + assert ( + csv_lines[2][1] + == f'"{application_alteration_csv_service.get_recovery_period(alteration_2)}"' + ) + assert Decimal(csv_lines[2][2]) == alteration_2.recovery_amount + + assert csv_lines[2][3] == f'"{alteration_2.application.company.name}"' + assert ( + csv_lines[2][4] + == f'"{application_alteration_csv_service.get_company_address(alteration_2)}"' + ) + assert csv_lines[2][5] == f'"{alteration_2.application.company.business_id}"' + + assert ( + csv_lines[2][6] + == f'"{application_alteration_csv_service.get_company_contact_person(alteration_2)}"' + ) + assert csv_lines[2][7] == f'"{alteration_2.einvoice_address}"' + assert csv_lines[2][8] == f'"{alteration_2.einvoice_provider_identifier}"' + + assert ( + csv_lines[2][9] + == f'"{application_alteration_csv_service.get_account_number(alteration_2)}"' + ) + assert ( + csv_lines[2][10] + == f'"{application_alteration_csv_service.get_handler_name(alteration_2)}"' + ) + assert ( + csv_lines[2][11] + == f'"{application_alteration_csv_service.get_title(alteration_2)}"' + ) + + assert ( + csv_lines[2][12] + == f'"{application_alteration_csv_service.get_billing_department(alteration_2)}"' + ) + + +def test_write_application_alterations_csv_file( + application_alteration_csv_service, tmp_path +): + alteration = application_alteration_csv_service.get_alterations()[0] + output_file = tmp_path / "output.csv" + application_alteration_csv_service.write_csv_file(output_file) + with open(output_file, encoding="utf-8") as f: + contents = f.read() + print(contents) + assert str(alteration.recovery_amount) in contents + + def test_pruned_applications_csv_output( pruned_applications_csv_service_with_one_application, ): diff --git a/backend/benefit/companies/models.py b/backend/benefit/companies/models.py index 8a34bc0575..c4cbaa9530 100644 --- a/backend/benefit/companies/models.py +++ b/backend/benefit/companies/models.py @@ -20,3 +20,6 @@ class Meta: db_table = "bf_companies_company" verbose_name = _("company") verbose_name_plural = _("companies") + + def get_full_address(self): + return f"{self.street_address}, {self.postcode} {self.city}" diff --git a/frontend/benefit/handler/src/components/alterationHandling/AlterationCsvButton.tsx b/frontend/benefit/handler/src/components/alterationHandling/AlterationCsvButton.tsx index 1f4106f4af..ed24124b1f 100644 --- a/frontend/benefit/handler/src/components/alterationHandling/AlterationCsvButton.tsx +++ b/frontend/benefit/handler/src/components/alterationHandling/AlterationCsvButton.tsx @@ -1,32 +1,60 @@ -import { ApplicationAlteration } from 'benefit-shared/types/application'; -import { Button, ButtonTheme, IconDownload } from 'hds-react'; +import updateApplicationAlterationWithCsvQuery from 'benefit/handler/hooks/useUpdateApplicationAlterationWithCsvQuery'; +import { AlterationCsvProps } from 'benefit/handler/types/application'; +import { Button, IconDownload } from 'hds-react'; import { useTranslation } from 'next-i18next'; import React from 'react'; +import { convertToBackendDateFormat } from 'shared/utils/date.utils'; +import { downloadFile } from 'shared/utils/file.utils'; +import { stringToFloatValue } from 'shared/utils/string.utils'; -type Props = { - alteration: ApplicationAlteration; - theme?: ButtonTheme; - secondary?: boolean; -}; - -const AlterationCsvButton: React.FC = ({ +const AlterationCsvButton: React.FC = ({ theme, secondary, - // eslint-disable-next-line @typescript-eslint/no-unused-vars alteration, + values, + onSubmit, }) => { const { t } = useTranslation(); + const updateMutation = updateApplicationAlterationWithCsvQuery(); + + if (!values) return null; + + const data = { + application: values.application, + recovery_start_date: convertToBackendDateFormat(values.recoveryStartDate), + recovery_end_date: convertToBackendDateFormat(values.recoveryEndDate), + recovery_amount: values.isRecoverable + ? String(stringToFloatValue(values.recoveryAmount)) + : '0', + recovery_justification: values.recoveryJustification, + is_recoverable: values.isRecoverable, + }; + + const handleDownloadCsv = async (): Promise => { + try { + const response = await updateMutation.mutateAsync({ + id: alteration.id, + applicationId: alteration.application, + data, + }); + downloadFile(response, 'csv'); + onSubmit(); // Call the onSubmit function after successful download + } catch (error) { + // Handle error (e.g., show an error message to the user) + } + }; - // TODO: Talpa integration to be implemented in HL-887 return ( ); }; diff --git a/frontend/benefit/handler/src/components/alterationHandling/AlterationHandlingForm.tsx b/frontend/benefit/handler/src/components/alterationHandling/AlterationHandlingForm.tsx index 99ad3be541..17cc98cc0e 100644 --- a/frontend/benefit/handler/src/components/alterationHandling/AlterationHandlingForm.tsx +++ b/frontend/benefit/handler/src/components/alterationHandling/AlterationHandlingForm.tsx @@ -49,6 +49,10 @@ type Props = { onClose: () => void; }; +const handleAlterationCsvDownload = (): void => { + // TODO add any necessary logic here after the CSV download +}; + const AlterationHandlingForm = ({ application, alteration, @@ -234,7 +238,10 @@ const AlterationHandlingForm = ({ <$TalpaGuideText> {t(`${translationBase}.talpaCsv.guideText`)} - + diff --git a/frontend/benefit/handler/src/components/alterationHandling/useAlterationHandlingForm.ts b/frontend/benefit/handler/src/components/alterationHandling/useAlterationHandlingForm.ts index 7d8ebbd0ca..7e0e094c10 100644 --- a/frontend/benefit/handler/src/components/alterationHandling/useAlterationHandlingForm.ts +++ b/frontend/benefit/handler/src/components/alterationHandling/useAlterationHandlingForm.ts @@ -86,7 +86,7 @@ const useAlterationHandling = ({ manualRecoveryAmount: '0', isManual: false, isRecoverable: true, - recoveryJustification: '', + recoveryJustification: alteration.recoveryJustification || '', }, onSubmit: submitForm, validationSchema: getValidationSchema(application, alteration, t), diff --git a/frontend/benefit/handler/src/hooks/useUpdateApplicationAlterationWithCsvQuery.ts b/frontend/benefit/handler/src/hooks/useUpdateApplicationAlterationWithCsvQuery.ts new file mode 100644 index 0000000000..5f829903d5 --- /dev/null +++ b/frontend/benefit/handler/src/hooks/useUpdateApplicationAlterationWithCsvQuery.ts @@ -0,0 +1,48 @@ +import { AxiosError } from 'axios'; +import { BackendEndpoint } from 'benefit-shared/backend-api/backend-api'; +import { ApplicationAlterationData } from 'benefit-shared/types/application'; +import { useMutation, UseMutationResult, useQueryClient } from 'react-query'; +import useBackendAPI from 'shared/hooks/useBackendAPI'; + +import { ErrorData } from '../types/common'; + +const useUpdateApplicationAlterationWithCsvQuery = (): UseMutationResult< + Blob, + AxiosError, + { + id: number, + applicationId: string, + data: Partial + } +> => { + const { axios } = useBackendAPI(); + const queryClient = useQueryClient(); + + return useMutation( + 'updateApplicationAlterationWithCsv', + async ({ id, applicationId, data }) => { + const params = `?application_id=${applicationId}&alteration_id=${id}`; + const endpoint = `${BackendEndpoint.HANDLER_APPLICATION_ALTERATION_UPDATE_WITH_CSV}${params}`; + + const response = await axios.patch(endpoint, { ...data }, { + responseType: 'blob', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (response.data instanceof Blob) { + return response.data; + } + throw new Error('Unexpected response type'); + + }, + { + onSuccess: (_, { applicationId }) => { + void queryClient.invalidateQueries(['applications', applicationId]); + }, + } + ); +}; + +export default useUpdateApplicationAlterationWithCsvQuery; diff --git a/frontend/benefit/handler/src/types/application.d.ts b/frontend/benefit/handler/src/types/application.d.ts index 638a4dd80e..89ed7be8fb 100644 --- a/frontend/benefit/handler/src/types/application.d.ts +++ b/frontend/benefit/handler/src/types/application.d.ts @@ -30,6 +30,7 @@ import { User, } from 'benefit-shared/types/application'; import { FormikProps } from 'formik'; +import { ButtonTheme } from 'hds-react'; import { Field } from 'shared/components/forms/fields/types'; import { Language } from 'shared/i18n/i18n'; @@ -234,3 +235,12 @@ export type ApplicationAlterationHandlingForm = manualRecoveryAmount: string; isManual: boolean; }; + +export type AlterationCsvProps = { + alteration: ApplicationAlteration; + values?: ApplicationAlterationHandlingForm; + theme?: ButtonTheme; + secondary?: boolean; + onSubmit?: () => void; + }; + \ No newline at end of file diff --git a/frontend/benefit/shared/src/backend-api/backend-api.ts b/frontend/benefit/shared/src/backend-api/backend-api.ts index 973ba580a2..aabb0cac57 100644 --- a/frontend/benefit/shared/src/backend-api/backend-api.ts +++ b/frontend/benefit/shared/src/backend-api/backend-api.ts @@ -20,6 +20,7 @@ export const BackendEndpoint = { GET_ORGANISATION: '/v1/company/get/', APPLICATION_ALTERATION: '/v1/applicationalterations/', HANDLER_APPLICATION_ALTERATION: '/v1/handlerapplicationalterations/', + HANDLER_APPLICATION_ALTERATION_UPDATE_WITH_CSV: 'v1/handlerapplicationalterations/update_with_csv/', DECISION_PROPOSAL_TEMPLATE: 'v1/decision-proposal-sections/', DECISION_PROPOSAL_DRAFT: 'v1/decision-proposal-drafts/', SEARCH: 'v1/search/', diff --git a/frontend/shared/src/utils/file.utils.ts b/frontend/shared/src/utils/file.utils.ts index 20a98067cb..db119ca084 100644 --- a/frontend/shared/src/utils/file.utils.ts +++ b/frontend/shared/src/utils/file.utils.ts @@ -4,7 +4,7 @@ import ExportFileType from 'shared/types/export-file-type'; import { DATE_FORMATS } from './date.utils'; -export const downloadFile = (data: string, type: ExportFileType): void => { +export const downloadFile = (data: string | Blob, type: ExportFileType): void => { const now = new Date(); const dateFormat = `${DATE_FORMATS.BACKEND_DATE} HH.mm.ss`; const dateString = format(now, dateFormat);