Skip to content

Commit

Permalink
Hl 1305 decision details (#3036)
Browse files Browse the repository at this point in the history
* feat: decision details request to Ahjo

* feat: command for querying decision details

* feat: case insensitive match for decision maker
  • Loading branch information
rikuke authored May 24, 2024
1 parent 721dd43 commit 65cfd08
Show file tree
Hide file tree
Showing 11 changed files with 536 additions and 29 deletions.
12 changes: 12 additions & 0 deletions backend/benefit/applications/enums.py
Original file line number Diff line number Diff line change
@@ -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 _
Expand Down Expand Up @@ -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")


Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,14 +9,19 @@
from applications.enums import (
AhjoRequestType,
AhjoStatus as AhjoStatusEnum,
ApplicationBatchStatus,
ApplicationStatus,
)
from applications.models import Application
from applications.services.ahjo_authentication import (
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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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)
31 changes: 29 additions & 2 deletions backend/benefit/applications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,6 +14,7 @@

from applications.enums import (
AhjoDecision,
AhjoDecisionDetails,
AhjoStatus as AhjoStatusEnum,
ApplicationAlterationState,
ApplicationAlterationType,
Expand Down Expand Up @@ -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,
]

Expand Down Expand Up @@ -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):
"""
Expand Down
40 changes: 31 additions & 9 deletions backend/benefit/applications/services/ahjo_client.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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}"

Expand All @@ -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:
Expand All @@ -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:
Expand Down
41 changes: 39 additions & 2 deletions backend/benefit/applications/services/ahjo_decision_service.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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'<div class="Puheenjohtajanimi">([^<]+)</div>', html_content, re.I
)
if match:
return match.group(1)
else:
raise AhjoDecisionError(
"Decision maker not found in the decision content html", html_content
)
13 changes: 13 additions & 0 deletions backend/benefit/applications/services/ahjo_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from applications.services.ahjo_client import (
AhjoAddRecordsRequest,
AhjoApiClient,
AhjoDecisionDetailsRequest,
AhjoDecisionProposalRequest,
AhjoDeleteCaseRequest,
AhjoOpenCaseRequest,
Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit 65cfd08

Please sign in to comment.