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'
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.
\ +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.
\ +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.
\ +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.
{name}, suunnittelija, puhelin: 12345789
Päätös on sähköisesti allekirjoitettu.
\ +Tähän päätökseen tyytymätön voi tehdä kirjallisen oikaisuvaatimuksen. \ +Päätökseen ei saa hakea muutosta valittamalla tuomioistuimeen.
\ +Oikaisuvaatimuksen saa tehdä
\ +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.
\ +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.
\ +Oikaisuvaatimus on tehtävä kirjallisena. \ +Myös sähköinen asiakirja täyttää vaatimuksen kirjallisesta muodosta.
\ +Oikaisuvaatimuksessa on ilmoitettava
Päätöstä koskevia pöytäkirjan otteita ja liitteitä lähetetään pyynnöstä. \ +Asiakirjoja voi tilata Helsingin kaupungin kirjaamosta.
\ +Ote | Otteen liitteet |
---|---|
{company} | Oikaisuvaatimusohje, kaupunginhallitus Liite 1 |