diff --git a/backend/benefit/applications/enums.py b/backend/benefit/applications/enums.py index 77857efe7c..2e4e419d36 100644 --- a/backend/benefit/applications/enums.py +++ b/backend/benefit/applications/enums.py @@ -1,3 +1,6 @@ +from dataclasses import dataclass +from datetime import datetime + from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ @@ -200,6 +203,7 @@ class AhjoRequestType(models.TextChoices): UPDATE_APPLICATION = "update_application", _("Update application in Ahjo") ADD_RECORDS = "add_records", _("Send new records to Ahjo") SEND_DECISION_PROPOSAL = "send_decision", _("Send decision to Ahjo") + GET_DECISION_DETAILS = "get_decision_details", _("Get decision details from Ahjo") SUBSCRIBE_TO_DECISIONS = "subscribe_to_decisions", _("Subscribe to decisions API") @@ -254,6 +258,14 @@ class AhjoDecisionUpdateType(models.TextChoices): UPDATED = "Updated", _("Updated") +@dataclass +class AhjoDecisionDetails: + decision_maker_name: str + decision_maker_title: str + section_of_the_law: str + decision_date: datetime + + # Call gettext on some of the enums so that "makemessages" command can find them when used dynamically in templates _("granted") _("granted_aged") diff --git a/backend/benefit/applications/management/commands/send_ahjo_requests.py b/backend/benefit/applications/management/commands/send_ahjo_requests.py index 498ecfce57..470dd8e925 100644 --- a/backend/benefit/applications/management/commands/send_ahjo_requests.py +++ b/backend/benefit/applications/management/commands/send_ahjo_requests.py @@ -1,5 +1,6 @@ import logging import time +from typing import Dict, List, Union from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand @@ -8,6 +9,7 @@ from applications.enums import ( AhjoRequestType, AhjoStatus as AhjoStatusEnum, + ApplicationBatchStatus, ApplicationStatus, ) from applications.models import Application @@ -15,7 +17,11 @@ AhjoToken, AhjoTokenExpiredException, ) +from applications.services.ahjo_decision_service import ( + parse_details_from_decision_response, +) from applications.services.ahjo_integration import ( + get_decision_details_from_ahjo, get_token, send_decision_proposal_to_ahjo, send_new_attachment_records_to_ahjo, @@ -70,6 +76,12 @@ def get_applications_for_request( [ApplicationStatus.ACCEPTED, ApplicationStatus.REJECTED], AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED, ) + elif request_type == AhjoRequestType.GET_DECISION_DETAILS: + applications = Application.objects.get_by_statuses( + [ApplicationStatus.ACCEPTED, ApplicationStatus.REJECTED], + AhjoStatusEnum.SIGNED_IN_AHJO, + ) + return applications def handle(self, *args, **options): @@ -151,7 +163,8 @@ def run_requests( ) ) self.stdout.write( - f"Submitting {len(successful_applications)} open case requests took {elapsed_time} seconds to run." + f"Submitting {len(successful_applications)} {ahjo_request_type} \ +requests took {elapsed_time} seconds to run." ) if failed_applications: self.stdout.write( @@ -160,24 +173,56 @@ def run_requests( ) ) - def _handle_successful_request( + def _handle_details_request_success( + self, application: Application, response_dict: Dict + ) -> str: + """Extract the details from the dict and update the application batch with them and also + with the p2p settings from ahjo_settings table""" + + details = parse_details_from_decision_response(response_dict) + + batch_status_to_update = ApplicationBatchStatus.DECIDED_ACCEPTED + if application.status == ApplicationStatus.REJECTED: + batch_status_to_update = ApplicationBatchStatus.DECIDED_REJECTED + + batch = application.batch + batch.update_batch_after_details_request(batch_status_to_update, details) + + return f"Successfully received and updated decision details \ +for application {application.id} and batch {batch.id} from Ahjo" + + def _handle_application_request_success( self, - counter: int, application: Application, + counter: int, response_text: str, request_type: AhjoRequestType, - ): + ) -> str: # The guid is returned in the response text in text format {guid}, so remove brackets here response_text = response_text.replace("{", "").replace("}", "") application.ahjo_case_guid = response_text application.save() - self.stdout.write( - self.style.SUCCESS( - f"{counter}. Successfully submitted {request_type} request for application {application.id} to Ahjo, \ + return f"{counter}. Successfully submitted {request_type} request for application {application.id} to Ahjo, \ received GUID: {response_text}" + + def _handle_successful_request( + self, + counter: int, + application: Application, + response_content: Union[str, List], + request_type: AhjoRequestType, + ) -> None: + if request_type == AhjoRequestType.GET_DECISION_DETAILS: + success_text = self._handle_details_request_success( + application, response_content[0] ) - ) + else: + success_text = self._handle_application_request_success( + application, counter, response_content, request_type + ) + + self.stdout.write(self.style.SUCCESS(success_text)) def _handle_failed_request( self, counter: int, application: Application, request_type: AhjoRequestType @@ -194,5 +239,6 @@ def _get_request_handler(self, request_type: AhjoRequestType): AhjoRequestType.SEND_DECISION_PROPOSAL: send_decision_proposal_to_ahjo, AhjoRequestType.ADD_RECORDS: send_new_attachment_records_to_ahjo, AhjoRequestType.UPDATE_APPLICATION: update_application_summary_record_in_ahjo, + AhjoRequestType.GET_DECISION_DETAILS: get_decision_details_from_ahjo, } return request_handlers.get(request_type) diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py index 8b2aec90bb..6f69f7620d 100755 --- a/backend/benefit/applications/models.py +++ b/backend/benefit/applications/models.py @@ -3,6 +3,7 @@ from dateutil.relativedelta import relativedelta from django.conf import settings +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db import connection, models from django.db.models import Exists, F, JSONField, OuterRef, Prefetch, Subquery from django.db.models.constraints import UniqueConstraint @@ -13,6 +14,7 @@ from applications.enums import ( AhjoDecision, + AhjoDecisionDetails, AhjoStatus as AhjoStatusEnum, ApplicationAlterationState, ApplicationAlterationType, @@ -800,8 +802,6 @@ def raise_error(): ] required_fields_accepted_ahjo = required_fields_rejected + [ - self.expert_inspector_name, - self.expert_inspector_title, self.p2p_checker_name, ] @@ -829,6 +829,33 @@ def save(self, *args, **kwargs): self.full_clean() super().save(*args, **kwargs) + def update_batch_after_details_request( + self, status_to_update: ApplicationBatchStatus, details: AhjoDecisionDetails + ): + """ + Update the application batch with the details received from the Ahjo decision request and with + details from the applications_ahjo_setting table. + """ + try: + if not self.auto_generated_by_ahjo: + raise ImproperlyConfigured( + "This batch was not auto-generated by Ahjo, so it should not be updated" + ) + self.decision_maker_name = details.decision_maker_name + self.decision_maker_title = details.decision_maker_title + self.section_of_the_law = details.section_of_the_law + self.decision_date = details.decision_date + + p2p_settings = AhjoSetting.objects.get(name="p2p_settings") + self.p2p_checker_name = p2p_settings.data["acceptor_name"] + self.p2p_inspector_name = p2p_settings.data["inspector_name"] + self.p2p_inspector_email = p2p_settings.data["inspector_email"] + + self.status = status_to_update + self.save() + except ObjectDoesNotExist: + raise ImproperlyConfigured("No p2p settings found in the database") + @property def applications_can_be_modified(self): """ diff --git a/backend/benefit/applications/services/ahjo_client.py b/backend/benefit/applications/services/ahjo_client.py index a2339901a8..220a7dc69c 100644 --- a/backend/benefit/applications/services/ahjo_client.py +++ b/backend/benefit/applications/services/ahjo_client.py @@ -1,7 +1,7 @@ import json import logging from dataclasses import dataclass, field -from typing import Optional, Tuple, Union +from typing import List, Optional, Tuple, Union import requests from django.conf import settings @@ -114,6 +114,18 @@ def api_url(self) -> str: return f"{self.url_base}/decisions/subscribe" +class AhjoDecisionDetailsRequest(AhjoRequest): + """Request to get a decision detail from Ahjo.""" + + request_type = AhjoRequestType.GET_DECISION_DETAILS + request_method = "GET" + + def api_url(self) -> str: + if not self.application.ahjo_case_id: + raise MissingAhjoCaseIdError("Application does not have an Ahjo case id") + return f"{self.url_base}/decisions/{self.application.ahjo_case_id}" + + class AhjoApiClientException(Exception): pass @@ -157,16 +169,18 @@ def prepare_ahjo_headers(self) -> dict: "Authorization": f"Bearer {self.ahjo_token.access_token}", "Content-Type": "application/json", } - - if not self._request.request_type == AhjoRequestType.SUBSCRIBE_TO_DECISIONS: + # Other request types than GET_DECISION_DETAILS require a callback url + if self._request.request_type not in [ + AhjoRequestType.GET_DECISION_DETAILS, + AhjoRequestType.SUBSCRIBE_TO_DECISIONS, + ]: url = reverse( "ahjo_callback_url", kwargs={ - "uuid": str(self._request.application.id), "request_type": self._request.request_type, + "uuid": str(self._request.application.id), }, ) - headers_dict["Accept"] = "application/hal+json" headers_dict["X-CallbackURL"] = f"{settings.API_BASE_URL}{url}" @@ -175,12 +189,14 @@ def prepare_ahjo_headers(self) -> dict: def send_request_to_ahjo( self, data: Union[dict, None] = None, - ) -> Union[Tuple[Application, str], None]: + ) -> Union[Tuple[Application, str], Tuple[Application, List], None]: """Send a request to Ahjo. The request can be either opening a new case (POST), updating the records of an existing case (PUT), sending a decision proposal (POST)or deleting an application (DELETE). - Returns a tuple of the application and the request id given by Ahjo if the request was successful. + + Returns a tuple of the application and the response content, + which can be the id given by Ahjo or a JSON list depending on the request type. """ try: @@ -198,8 +214,14 @@ def send_request_to_ahjo( response.raise_for_status() if response.ok: - LOGGER.debug(f"Request {self._request} to Ahjo was successful.") - return self._request.application, response.text + if ( + not self._request.request_type + == AhjoRequestType.GET_DECISION_DETAILS + ): + LOGGER.debug(f"Request {self._request} to Ahjo was successful.") + return self._request.application, response.text + else: + return self._request.application, response.json() except MissingHandlerIdError as e: LOGGER.error(f"Missing handler id: {e}") except MissingAhjoCaseIdError as e: diff --git a/backend/benefit/applications/services/ahjo_decision_service.py b/backend/benefit/applications/services/ahjo_decision_service.py index f5c40698e7..7e9129174c 100644 --- a/backend/benefit/applications/services/ahjo_decision_service.py +++ b/backend/benefit/applications/services/ahjo_decision_service.py @@ -1,9 +1,11 @@ +import re +from datetime import datetime from string import Template -from typing import List +from typing import Dict, List from django.conf import settings -from applications.enums import DecisionType, HandlerRole +from applications.enums import AhjoDecisionDetails, DecisionType, HandlerRole from applications.models import ( AhjoDecisionText, Application, @@ -105,3 +107,38 @@ def _generate_decision_text_string( return replace_decision_template_placeholders( decision_string, decision_type, application ) + + +def parse_details_from_decision_response(decision_data: Dict) -> AhjoDecisionDetails: + """Extract the decision details from the given decision data""" + try: + html_content = decision_data["Content"] + decision_maker_name = parse_decision_maker_from_html(html_content) + decision_maker_title = decision_data["Organization"]["Name"] + section_of_the_law = decision_data["Section"] + decision_date_str = decision_data["DateDecision"] + decision_date = datetime.strptime(decision_date_str, "%Y-%m-%dT%H:%M:%S.%f") + + return AhjoDecisionDetails( + decision_maker_name=decision_maker_name, + decision_maker_title=decision_maker_title, + section_of_the_law=f"{section_of_the_law} §", + decision_date=decision_date, + ) + except KeyError as e: + raise AhjoDecisionError(f"Error in parsing decision details: {e}") + except ValueError as e: + raise AhjoDecisionError(f"Error in parsing decision details date: {e}") + + +def parse_decision_maker_from_html(html_content: str) -> str: + """Parse the decision maker from the given html string""" + match = re.search( + r'
([^<]+)
', html_content, re.I + ) + if match: + return match.group(1) + else: + raise AhjoDecisionError( + "Decision maker not found in the decision content html", html_content + ) diff --git a/backend/benefit/applications/services/ahjo_integration.py b/backend/benefit/applications/services/ahjo_integration.py index f98137ae3f..9fb45ac9f4 100644 --- a/backend/benefit/applications/services/ahjo_integration.py +++ b/backend/benefit/applications/services/ahjo_integration.py @@ -25,6 +25,7 @@ from applications.services.ahjo_client import ( AhjoAddRecordsRequest, AhjoApiClient, + AhjoDecisionDetailsRequest, AhjoDecisionProposalRequest, AhjoDeleteCaseRequest, AhjoOpenCaseRequest, @@ -623,7 +624,19 @@ def send_subscription_request_to_ahjo( url = reverse("ahjo_decision_callback_url") data = {"callbackUrl": f"{settings.API_BASE_URL}{url}"} return ahjo_client.send_request_to_ahjo(data) + except ObjectDoesNotExist as e: + LOGGER.error(f"Object not found: {e}") + except ImproperlyConfigured as e: + LOGGER.error(f"Improperly configured: {e}") + +def get_decision_details_from_ahjo( + application: Application, ahjo_token: AhjoToken +) -> Union[List, None]: + try: + ahjo_request = AhjoDecisionDetailsRequest(application) + ahjo_client = AhjoApiClient(ahjo_token, ahjo_request) + return ahjo_client.send_request_to_ahjo() except ObjectDoesNotExist as e: LOGGER.error(f"Object not found: {e}") except ImproperlyConfigured as e: diff --git a/backend/benefit/applications/tests/conftest.py b/backend/benefit/applications/tests/conftest.py index c0c2b17b48..0bddc46beb 100755 --- a/backend/benefit/applications/tests/conftest.py +++ b/backend/benefit/applications/tests/conftest.py @@ -1,5 +1,6 @@ import os import random +import uuid from datetime import timedelta import factory @@ -490,3 +491,245 @@ def application_with_ahjo_decision(application_with_ahjo_case_id): language="fi", ) return application_with_ahjo_case_id + + +@pytest.fixture +def ahjo_decision_detail_response(application_with_ahjo_decision): + id = uuid.uuid4() + handler = application_with_ahjo_decision.calculation.handler + name = f"{handler.first_name} {handler.last_name}" + company = application_with_ahjo_decision.company + content = f'\ +\ +\ +Avustuksen myöntäminen, Työllisyyspalvelut, työllisyydenhoidon Helsinki-lisä vuonna 2024\ +
\ +
16 §
\ +

Avustuksen myöntäminen, Työllisyyspalvelut, työllisyydenhoidon Helsinki-lisä vuonna 2024

\ +
HEL 2024-004415 T 02 05 01 00
\ +

Päätös

\ +

Helsinki-lisä-suunnittelija päätti myöntää {company}:lle \ +työnantajan Helsinki-lisää käytettäväksi helsinkiläisen työllistämiseksi \ +448 euroa kuukaudessa palkkatuetulle \ +ajalle 4.12.2023-3.10.2024 ja 800 euroa kuukaudessa ajalle 4.10.2024-3.12.2024, \ +jolta työnantaja ei saa palkkatukea. Yhteensä Helsinki-lisää myönnetään 6080 euroa. \ +

Helsinki-lisään on varattu talousarviossa Helsingin kaupungin \ +Työllisyyspalveluille vuosittain budjetoitu määräraha. Avustuksen kustannukset maksetaan \ +kaupungin Työllisyyspalveluille osoitetusta määrärahasta talousarvion erikseen määritellyltä kohdalta. \ +Työnantajan Helsinki-lisä on aina harkinnanvarainen.

\ +

Päätösteksti

\ +

Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt \ +tukea rahallisesti yksityisen ja kolmannen sektorin työnantajia, \ +jotka tarjoavat työtä kaupungin työllisyydenhoidon kohderyhmiin kuuluville helsinkiläisille.

\ +

Avustus

Kaupunginhallituksen elinkeinojaosto on päätöksellään 11.9.2023 § 30 \ +hyväksynyt työnantajan Helsinki-lisän myöntämistä koskevat ehdot. \ +Helsinki-lisän myöntämisessä noudatetaan lisäksi kaupunginhallituksen 28.10.2019 § 723 \ +hyväksymiä Helsingin kaupungin avustusten yleisohjeita.

\ +

Helsinki-lisä on myönnetty työllistettävän henkilön palkkauksesta aiheutuviin kustannuksiin, \ +ei kuitenkaan bruttopalkkaan siltä ajalta, \ +jolta työnantaja saa myös palkkatukea. Tuen määrään vaikuttavat samoihin kustannuksiin \ +myönnetyt muut tuet (esim. palkkatuki ja oppisopimuksen koulutuskorvaus). \ +Yritykselle ja taloudellista toimintaa harjoittavalle yhteisölle avustus \ +myönnetään vähämerkityksisenä tukena eli ns. de minimis -tukena, ei koskaan yleisen ryhmäpoikkeusasetuksen \ +(komission asetus (EU) N:o 651/2014) perusteella eli ns. RPA-tukena. Yleishyödyllisille yhteisöille avustus myönnetään \ +valtiontukisääntelyn ulkopuolisena tukena, \ +jos yhdistys ei harjoita taloudellista toimintaa.

Avustusta myönnetään valtiontukisäännöissä määrätyn \ +kasautumissäännön sekä tuen enimmäisintensiteetin mukaisesti \ +(tuen määrä suhteessa tukikelpoisiin kustannuksiin). Helsinki-lisä voi olla enintään 800 euroa kuukaudessa. \ +Avustusta ei saa siirtää toisen tahon tai henkilön käytettäväksi. \ +Avustus maksetaan hakemuksessa ilmoitetulle pankkitilille.

\ +

Helsinki-lisää saa käyttää ainoastaan kaupunginhallituksen \ +elinkeinojaoston päätöksen 11.9.2023 §30 mukaisiin tarkoituksiin. \ +Avustuksen saajan tulee viipymättä palauttaa virheellisesti, \ +liikaa tai ilmeisen perusteettomasti saamansa avustus. \ +Väärinkäyttötapauksessa kaupunki voi periä maksetun avustuksen takaisin. \ +Avustuksen saaja sitoutuu antamaan tarvittaessa kaupungille \ +tarvittavat tiedot sen varmistamiseksi, että avustusta ei ole käytetty ehtojen vastaisesti. \ +Mikäli avustus maksetaan ennen päätöksen lainvoimaisuutta, avustuksen saaja sitoutuu palauttamaan \ +jo maksetut avustukset, jos päätös muutoksenhaun johdosta muuttuu.

\ +

Valtiontukiarviointi

Yritykselle ja taloudellista toimintaa harjoittavalle yhteisölle \ +avustus myönnetään vähämerkityksisenä tukena eli ns. de minimis -tukena. \ +Tuen myöntämisessä noudatetaan komission asetusta (EU) 2023/2831, annettu 13.12.2023, \ +Euroopan unionista tehdyn sopimuksen 107 ja 108 artiklan soveltamisesta vähämerkityksiseen \ +tukeen (EUVL L2023/281, 15.12.2023).

\ +

Kullekin hakijalle myönnettävän de minimis -tuen määrä ilmenee hakijakohtaisesta liitteestä. \ +Avustuksen saajalle voidaan myöntää de minimis -tukena enintään 300 000 euroa kuluvan vuoden ja \ +kahden sitä edeltäneen kahden vuoden muodostaman jakson aikana. \ +Avustuksen saaja vastaa siitä, että eri tahojen (mm. ministeriöt, ministeriöiden alaiset viranomaiset, \ +Business Finland, Finnvera Oyj, kunnat, maakuntien liitot) \ +myöntämien de minimis -tukien yhteismäärä ei ylitä tätä määrää. Avustuksen saaja on avustushakemuksessa \ +ilmoittanut kaupungille kaikkien saamiensa de minimis -tukien määrät \ +ja myöntöajankohdat.

\ +

Lisätiedot

\ +

{name}, suunnittelija, puhelin: 12345789

testi@testi.test

\ +
\ +

Päätös on sähköisesti allekirjoitettu.

\ +

{name}
\ +
helsinki-lisä-suunnittelija

\ +

Liitteet

\ +
1 Salassa pidettävä (Salassa pidettävä, JulkL (621/1999) 24.1 § 25 k)
\ +

Muutoksenhaku

OHJEET OIKAISUVAATIMUKSEN TEKEMISEKSI

\ +

Tähän päätökseen tyytymätön voi tehdä kirjallisen oikaisuvaatimuksen. \ +Päätökseen ei saa hakea muutosta valittamalla tuomioistuimeen.

\ +
Oikaisuvaatimusoikeus

Oikaisuvaatimuksen saa tehdä

\ +
\ +
Oikaisuvaatimusaika

Oikaisuvaatimus on tehtävä 14 päivän kuluessa päätöksen tiedoksisaannista.

\ +

Oikaisuvaatimuksen on saavuttava Helsingin kaupungin kirjaamoon määräajan viimeisenä päivänä \ +ennen kirjaamon aukioloajan päättymistä.

\ +

Mikäli päätös on annettu tiedoksi postitse, asianosaisen katsotaan saaneen päätöksestä tiedon, \ +jollei muuta näytetä, seitsemän päivän kuluttua kirjeen lähettämisestä. \ +Kunnan jäsenen katsotaan saaneen päätöksestä tiedon seitsemän päivän \ +kuluttua siitä, kun pöytäkirja on nähtävänä yleisessä tietoverkossa.

\ +

Mikäli päätös on annettu tiedoksi sähköisenä viestinä, asianosaisen katsotaan saaneen päätöksestä tiedon, \ +jollei muuta näytetä, kolmen päivän kuluttua viestin lähettämisestä.

\ +

Tiedoksisaantipäivää ei lueta oikaisuvaatimusaikaan. Jos oikaisuvaatimusajan viimeinen päivä on pyhäpäivä, \ +itsenäisyyspäivä, vapunpäivä, joulu- tai juhannusaatto tai arkilauantai, \ +saa oikaisuvaatimuksen tehdä ensimmäisenä arkipäivänä sen jälkeen.

\ +
Oikaisuvaatimusviranomainen

Viranomainen, jolle oikaisuvaatimus tehdään, on Helsingin kaupunginhallitus.

\ +

Oikaisuvaatimusviranomaisen asiointiosoite on seuraava:

\ +

Suojattu sähköposti: https://securemail.hel.fi/

\ +

Käytäthän aina suojattua sähköpostia, kun lähetät henkilökohtaisia tietojasi.

\ +

Muistathan asioinnin yhteydessä mainita kirjaamisnumeron (esim. HEL 2021-000123), \ +mikäli asiasi on jo vireillä Helsingin kaupungissa.

\ +
\ +\ +\ +\ +\ +\ +
\ +
Sähköpostiosoite:
helsinki.kirjaamo@hel.fi
Postiosoite:
PL 10
 
00099 HELSINGIN KAUPUNKI
Käyntiosoite:
Pohjoisesplanadi 11-13
Puhelinnumero:
09 310 13700
\ +

Kirjaamon aukioloaika on maanantaista perjantaihin klo 08.15-16.00.

\ +
Oikaisuvaatimuksen muoto ja sisältö

Oikaisuvaatimus on tehtävä kirjallisena. \ +Myös sähköinen asiakirja täyttää vaatimuksen kirjallisesta muodosta.

\ +

Oikaisuvaatimuksessa on ilmoitettava

\ +
Pöytäkirja

Päätöstä koskevia pöytäkirjan otteita ja liitteitä lähetetään pyynnöstä. \ +Asiakirjoja voi tilata Helsingin kaupungin kirjaamosta.

\ +

Otteet

\ +\ +
OteOtteen liitteet
{company}
Oikaisuvaatimusohje, kaupunginhallitus
Liite 1
' + + return [ + { + "links": [ + { + "rel": "self", + "href": "https://ahjo.hel.fi:9802/ahjorest/v1/decisions/%7B02A081B6-7B7C-4309-B95A-3A53D222B4CE%7D", + } + ], + "NativeId": "{02A081B6-7B7C-4309-B95A-3A53D222B4CE}", + "Title": "Avustuksen myöntäminen, Työllisyyspalvelut, työllisyydenhoidon Helsinki-lisä vuonna 2024", + "CaseIDLabel": f"{application_with_ahjo_decision.ahjo_case_id}", + "Section": "16", + "Content": content, + "Motion": "", + "ClassificationCode": "02 05 01 00", + "ClassificationTitle": "Kunnan myöntämät avustukset", + "Organization": { + "links": [ + { + "rel": "self", + "href": "https://ahjo.hel.fi:9802/ahjorest/v1/organization?orgid=U02120013070VH2&apireqlang=fi", + } + ], + "Name": "Helsinki-lisä-suunnittelija", + "ID": "U02120013070VH2", + "TypeId": "12", + "Existing": "true", + "Formed": "2023-05-26T00:00:00.000", + "Dissolved": "2100-01-01T00:00:00.000", + "Type": "Viranhaltija", + "Sector": { + "links": [], + "SectorID": "U50", + "Sector": "Keskushallinto", + "PendingCases": 0, + }, + "OrganizationLevelAbove": { + "organizations": [ + { + "links": [ + { + "rel": "self", + "href": "https://test.test", + }, + { + "rel": "self", + "href": "https:/test.test", + }, + ], + "Name": "Työnantajille myönnettävät taloudelliset tuet", + "ID": "U02120013070", + "TypeId": "20", + "Existing": "true", + "Formed": "2023-03-01T00:00:00.000", + "Dissolved": "2100-01-01T00:00:00.000", + } + ], + "count": 1, + "links": [], + }, + "OrganizationLevelBelow": { + "organizations": [], + "count": 0, + "links": [], + }, + }, + "Meeting": None, + "VotingResults": [], + "Attachments": [ + { + "links": [], + "Title": "Salassa pidettävä", + "AttachmentNumber": "1", + "PublicityClass": "Salassa pidettävä", + "SecurityReasons": ["JulkL (621/1999) 24.1 § 25 k"], + } + ], + "PreviousDecisions": [], + "PDF": { + "links": [ + { + "rel": "self", + "href": "https://test.test", + } + ], + "Title": "Avustuksen myöntäminen, Työllisyyspalvelut, työllisyydenhoidon Helsinki-lisä vuonna 2024", + "AttachmentNumber": None, + "PublicityClass": "Julkinen", + "SecurityReasons": None, + "VersionSeriesId": "{6176A8B5-B5D3-CB83-86F4-8EC1DCA00004}", + "NativeId": "{02A081B6-7B7C-4309-B95A-3A53D222B4CE}", + "Type": "viranhaltijan päätös", + "FileURI": "https://ahjojulkaisu.hel.fi/6176A8B5-B5D3-CB83-86F4-8EC1DCA00004.pdf", + "Language": "fi", + "PersonalData": "Sisältää henkilötietoja", + "Issued": "2024-04-09T03:00:00.000", + }, + "MinutesPDF": { + "links": [], + "Title": "Helsinki-lisä-suunnittelija viranhaltijan pöytäkirja 09.04.2024/16, \ +julkinen, julkaisujärjestelmä", + "AttachmentNumber": None, + "PublicityClass": "Julkinen", + "SecurityReasons": None, + "VersionSeriesId": "{157BBFB1-D9D8-C132-996B-8EC1DD100001}", + "NativeId": "{15E07229-9B33-437D-BEAB-F0FE1F5C5F94}", + "Type": "viranhaltijan pöytäkirja", + "FileURI": "https://ahjojulkaisu.hel.fi/157BBFB1-D9D8-C132-996B-8EC1DD100001.pdf", + "Language": "fi", + "PersonalData": "Sisältää henkilötietoja", + "Issued": "2024-04-09T03:00:00.000", + }, + "DateDecision": "2024-04-09T03:00:00.000", + "DecisionHistoryPDF": None, + "DecisionHistoryHTML": "", + "CaseID": f"{application_with_ahjo_decision.ahjo_case_id}", + } + ] diff --git a/backend/benefit/applications/tests/test_ahjo_decisions.py b/backend/benefit/applications/tests/test_ahjo_decisions.py index 08b43cd3c0..602ce44e13 100644 --- a/backend/benefit/applications/tests/test_ahjo_decisions.py +++ b/backend/benefit/applications/tests/test_ahjo_decisions.py @@ -1,11 +1,13 @@ import uuid +from datetime import datetime import pytest from rest_framework.reverse import reverse -from applications.enums import DecisionType +from applications.enums import AhjoDecisionDetails, DecisionType from applications.models import AhjoDecisionText from applications.services.ahjo_decision_service import ( + parse_details_from_decision_response, replace_decision_template_placeholders, ) @@ -148,3 +150,24 @@ def test_decision_text_api_put(decided_application, handler_api_client): assert decision_text.decision_type == data["decision_type"] assert decision_text.language == data["language"] assert decision_text.decision_text == data["decision_text"] + + +def test_parse_details_from_decision_response( + ahjo_decision_detail_response, application_with_ahjo_decision +): + details = parse_details_from_decision_response(ahjo_decision_detail_response[0]) + handler = application_with_ahjo_decision.calculation.handler + + assert isinstance(details, AhjoDecisionDetails) + assert details.decision_maker_name == f"{handler.first_name} {handler.last_name}" + assert ( + details.decision_maker_title + == ahjo_decision_detail_response[0]["Organization"]["Name"] + ) + assert isinstance(details.decision_date, datetime) + assert details.decision_date == datetime.strptime( + ahjo_decision_detail_response[0]["DateDecision"], "%Y-%m-%dT%H:%M:%S.%f" + ) + assert ( + details.section_of_the_law == ahjo_decision_detail_response[0]["Section"] + " §" + ) diff --git a/backend/benefit/applications/tests/test_ahjo_requests.py b/backend/benefit/applications/tests/test_ahjo_requests.py index 32f094bd21..4755647894 100644 --- a/backend/benefit/applications/tests/test_ahjo_requests.py +++ b/backend/benefit/applications/tests/test_ahjo_requests.py @@ -12,6 +12,7 @@ AhjoAddRecordsRequest, AhjoApiClient, AhjoApiClientException, + AhjoDecisionDetailsRequest, AhjoDecisionProposalRequest, AhjoDeleteCaseRequest, AhjoOpenCaseRequest, @@ -72,6 +73,7 @@ def ahjo_open_case_request(application_with_ahjo_case_id): "POST", "", ), + (AhjoDecisionDetailsRequest, AhjoRequestType.GET_DECISION_DETAILS, "GET", ""), ], ) def test_ahjo_requests( @@ -96,6 +98,7 @@ def test_ahjo_requests( assert request.url_base == f"{settings.AHJO_REST_API_URL}" assert request.lang == "fi" assert str(request) == f"Request of type {request_type}" + if request.request_type == AhjoRequestType.OPEN_CASE: assert request.api_url() == f"{settings.AHJO_REST_API_URL}{API_CASES_BASE}" @@ -120,9 +123,18 @@ def test_ahjo_requests( 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 ( + request.api_url() + == f"{settings.AHJO_REST_API_URL}/decisions/{application.ahjo_case_id}" + ) + client = AhjoApiClient(dummy_token, request) - if not request.request_type == AhjoRequestType.SUBSCRIBE_TO_DECISIONS: + if request.request_type not in [ + AhjoRequestType.SUBSCRIBE_TO_DECISIONS, + AhjoRequestType.GET_DECISION_DETAILS, + ]: url = reverse( callback_route, kwargs={ @@ -160,6 +172,7 @@ def test_ahjo_requests( (AhjoDeleteCaseRequest, AhjoRequestType.DELETE_APPLICATION, "DELETE"), (AhjoAddRecordsRequest, AhjoRequestType.ADD_RECORDS, "POST"), (AhjoSubscribeDecisionRequest, AhjoRequestType.SUBSCRIBE_TO_DECISIONS, "POST"), + (AhjoDecisionDetailsRequest, AhjoRequestType.GET_DECISION_DETAILS, "GET"), ], ) @patch("applications.services.ahjo_client.LOGGER") diff --git a/backend/benefit/applications/tests/test_application_tasks.py b/backend/benefit/applications/tests/test_application_tasks.py index 1ee14ac5f8..95d662859c 100755 --- a/backend/benefit/applications/tests/test_application_tasks.py +++ b/backend/benefit/applications/tests/test_application_tasks.py @@ -174,9 +174,16 @@ def test_user_is_notified_of_upcoming_application_deletion(drafts_about_to_be_de "applications.management.commands.send_ahjo_requests.Application.objects.get_by_statuses", "applications.management.commands.send_ahjo_requests.update_application_summary_record_in_ahjo", ), + ( + AhjoRequestType.GET_DECISION_DETAILS, + "applications.management.commands.send_ahjo_requests.Application.objects.get_by_statuses", + "applications.management.commands.send_ahjo_requests.get_decision_details_from_ahjo", + ), ], ) -def test_send_ahjo_requests(request_type, patch_db_function, patch_request): +def test_send_ahjo_requests( + request_type, patch_db_function, patch_request, ahjo_decision_detail_response +): AhjoSetting.objects.create(name="ahjo_code", data={"code": "12345"}) with patch( "applications.management.commands.send_ahjo_requests.get_token" @@ -186,12 +193,17 @@ def test_send_ahjo_requests(request_type, patch_db_function, patch_request): mock_get_token.return_value = MagicMock(AhjoToken) number_to_send = 5 - mock_get_applications.return_value = [ - MagicMock(spec=Application) for _ in range(number_to_send) - ] + if request_type == AhjoRequestType.GET_DECISION_DETAILS: + mock_response = ahjo_decision_detail_response + else: + mock_response = "{response_text}" + + applications = [MagicMock(spec=Application) for _ in range(number_to_send)] + + mock_get_applications.return_value = applications mock_send_request.return_value = ( MagicMock(spec=Application), - "{response_text}", + mock_response, ) # Call the command diff --git a/backend/benefit/applications/tests/test_models.py b/backend/benefit/applications/tests/test_models.py index d10286a028..e5a9996131 100755 --- a/backend/benefit/applications/tests/test_models.py +++ b/backend/benefit/applications/tests/test_models.py @@ -4,7 +4,8 @@ from applications.enums import AhjoDecision, ApplicationBatchStatus from applications.exceptions import BatchCompletionRequiredFieldsError -from applications.models import Application, ApplicationBatch, Employee +from applications.models import AhjoSetting, Application, ApplicationBatch, Employee +from applications.services.ahjo_decision_service import AhjoDecisionDetails from applications.tests.factories import BaseApplicationBatchFactory from applications.tests.test_application_batch_api import ( fill_as_valid_batch_completion_and_save, @@ -31,6 +32,64 @@ def test_application_batch(application_batch): assert application.batch == application_batch +@pytest.fixture +def p2p_settings(): + return AhjoSetting.objects.create( + name="p2p_settings", + data={ + "acceptor_name": "Test Test", + "inspector_name": "Test Inspector", + "inspector_email": "inspector@test.test", + }, + ) + + +@pytest.fixture +def decision_details(): + return AhjoDecisionDetails( + decision_maker_name="Test Test", + decision_maker_title="Test Title", + section_of_the_law="16 §", + decision_date=date.today(), + ) + + +@pytest.mark.parametrize( + "batch_status, proposal_for_decision", + [ + (ApplicationBatchStatus.DECIDED_ACCEPTED, AhjoDecision.DECIDED_ACCEPTED), + (ApplicationBatchStatus.DECIDED_REJECTED, AhjoDecision.DECIDED_REJECTED), + ], +) +def test_application_batch_update_after_details_request( + application_with_ahjo_decision, + batch_status, + decision_details, + p2p_settings, + proposal_for_decision, +): + application_batch = ApplicationBatch.objects.create( + status=ApplicationBatchStatus.DRAFT, + handler=application_with_ahjo_decision.calculation.handler, + proposal_for_decision=proposal_for_decision, + auto_generated_by_ahjo=True, + ) + + application_batch.update_batch_after_details_request(batch_status, decision_details) + assert application_batch.decision_maker_name == decision_details.decision_maker_name + assert ( + application_batch.decision_maker_title == decision_details.decision_maker_title + ) + assert application_batch.section_of_the_law == decision_details.section_of_the_law + assert application_batch.decision_date == decision_details.decision_date + + assert application_batch.p2p_checker_name == p2p_settings.data["acceptor_name"] + assert application_batch.p2p_inspector_name == p2p_settings.data["inspector_name"] + assert application_batch.p2p_inspector_email == p2p_settings.data["inspector_email"] + + assert application_batch.status == batch_status + + @pytest.mark.parametrize( "status, expected_result", [