Skip to content

Commit

Permalink
feat: HL 1559 signer api (#3602)
Browse files Browse the repository at this point in the history
* feat: signer AhjoRequest

* feat: seed signer org ids setting

* feat: improve decisionmaker request error handling

* chore: move responseHandler to own file

* chore: move requestHandler to own file

* feat: signer data from Ahjo
  • Loading branch information
rikuke authored Nov 28, 2024
1 parent c8884fb commit 1e1f71d
Show file tree
Hide file tree
Showing 14 changed files with 538 additions and 53 deletions.
1 change: 1 addition & 0 deletions backend/benefit/applications/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ class AhjoRequestType(models.TextChoices):
GET_DECISION_MAKER = "get_decision_maker", _(
"Get decision maker name from Ahjo API"
)
GET_SIGNER = "get_signer", _("Get signer name a and AD id from Ahjo API")


class DecisionType(models.TextChoices):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ def execute(self):
call_command("delete_applications", keep=180, status="draft")
call_command("check_drafts_to_delete", notify=14, keep=180)
call_command("get_decision_maker")
call_command("get_signer")
call_command("check_and_notify_ending_benefits", notify=30)
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand
from django.utils import timezone

from applications.services.ahjo.request_handler import AhjoRequestHandler
from applications.services.ahjo_authentication import AhjoTokenExpiredException
from applications.services.ahjo_integration import AhjoRequestHandler, get_token
from applications.services.ahjo_integration import get_token

LOGGER = logging.getLogger(__name__)

Expand All @@ -30,7 +32,18 @@ def handle(self, *args, **options):
ahjo_auth_token = self.get_token()
if not ahjo_auth_token:
return

handler = AhjoRequestHandler(ahjo_auth_token, self.request_type)
handler.handle_request_without_application()
self.stdout.write(f"Request {self.request_type.value} made to Ahjo")
try:
handler = AhjoRequestHandler(ahjo_auth_token, self.request_type)
handler.handle_request_without_application()
self.style.SUCCESS(
self.stdout.write(
f"{timezone.now()}: Request {self.request_type.value} made to Ahjo"
)
)
except ValueError as e:
self.style.ERROR(
self.stdout.write(
f"{timezone.now()}: Failed to make request {self.request_type.value} to Ahjo: {e}"
)
)
return
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from applications.enums import AhjoRequestType
from applications.management.commands.ahjo_base_command import AhjoRequestBaseClass


class Command(AhjoRequestBaseClass):
help = (
"Get the decision maker from Ahjo and store it in the database in Ahjo settings"
)
request_type = AhjoRequestType.GET_SIGNER
6 changes: 6 additions & 0 deletions backend/benefit/applications/management/commands/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ def _create_dummy_ahjo_settings():
"id": "1234567-8",
},
)

AhjoSetting.objects.create(
name="ahjo_signer_org_ids",
data=["1234567", "7654321"],
)

AhjoSetting.objects.create(
name="ahjo_decision_maker",
data=[
Expand Down
28 changes: 28 additions & 0 deletions backend/benefit/applications/services/ahjo/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from enum import Enum, unique


@unique
class AhjoSettingName(Enum):
"""
Enum representing different Ahjo setting names.
The @unique decorator ensures that no two enum members have the same value.
"""

DECISION_MAKER = "ahjo_decision_maker"
DECISION_MAKER_ORG_ID = "ahjo_org_identifier"
SIGNER = "ahjo_signer"
SIGNER_ORG_IDS = "ahjo_signer_org_ids"

def __str__(self):
"""
Allow the enum to be used directly as a string when converted.
This makes it easy to use in database queries or comparisons.
"""
return self.value

def __repr__(self):
"""
Provide a clear representation of the enum member.
"""
return f"{self.__class__.__name__}.{self.name}"
39 changes: 39 additions & 0 deletions backend/benefit/applications/services/ahjo/request_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import List, Union

from applications.enums import AhjoRequestType
from applications.services.ahjo.enums import AhjoSettingName
from applications.services.ahjo.setting_response_handler import AhjoResponseHandler
from applications.services.ahjo_authentication import AhjoToken
from applications.services.ahjo_client import (
AhjoApiClient,
AhjoDecisionMakerRequest,
AhjoRequest,
AhjoSignerRequest,
)


class AhjoRequestHandler:
def __init__(self, ahjo_token: AhjoToken, ahjo_request_type: AhjoRequest):
self.ahjo_token = ahjo_token
self.ahjo_request_type = ahjo_request_type

def handle_request_without_application(self):
if self.ahjo_request_type == AhjoRequestType.GET_DECISION_MAKER:
self.get_setting_from_ahjo(
request_class=AhjoDecisionMakerRequest,
setting_name=AhjoSettingName.DECISION_MAKER,
)
if self.ahjo_request_type == AhjoRequestType.GET_SIGNER:
self.get_setting_from_ahjo(
request_class=AhjoSignerRequest,
setting_name=AhjoSettingName.SIGNER,
)

def get_setting_from_ahjo(
self, request_class: AhjoRequest, setting_name: AhjoSettingName
) -> Union[List, None]:
ahjo_client = AhjoApiClient(self.ahjo_token, request_class())
_, result = ahjo_client.send_request_to_ahjo()
AhjoResponseHandler.handle_ahjo_query_response(
setting_name=setting_name, data=result
)
152 changes: 152 additions & 0 deletions backend/benefit/applications/services/ahjo/setting_response_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import logging
from typing import Dict, List, Union

from django.core.exceptions import ValidationError

from applications.models import AhjoSetting
from applications.services.ahjo.enums import AhjoSettingName

LOGGER = logging.getLogger(__name__)


class AhjoResponseHandler:
@staticmethod
def handle_ahjo_query_response(
setting_name: AhjoSettingName, data: Union[None, dict]
) -> None:
"""
Handle the decision maker response from Ahjo API.
Args:
response: Variable that is either None or the JSON response data
Raises:
ValueError: If response data is invalid
ValidationError: If data validation fails
"""

if not data:
raise ValueError(
f"Failed to process Ahjo API response for setting {setting_name}, no data received from Ahjo."
)

try:
# Validate response structure
if not isinstance(data, dict):
raise ValidationError(
f"Invalid response format for setting {setting_name}: expected dictionary"
)
if setting_name == AhjoSettingName.DECISION_MAKER:
filtered_data = AhjoResponseHandler.filter_decision_makers(data)
if setting_name == AhjoSettingName.SIGNER:
filtered_data = AhjoResponseHandler.filter_signers(data)

if not filtered_data:
LOGGER.warning("No valid decision makers found in response")
return

# Store the filtered data
AhjoResponseHandler._save_ahjo_setting(
setting_name=setting_name, filtered_data=filtered_data
)

LOGGER.info(f"Successfully processed {len(filtered_data)} decision makers")

except Exception as e:
LOGGER.error(
f"Failed to process Ahjo api response for setting {setting_name}: {str(e)}",
exc_info=True,
)
raise

@staticmethod
def filter_decision_makers(data: Dict) -> List[Dict[str, str]]:
"""
Filter the decision makers Name and ID from the Ahjo response.
Args:
data: Response data dictionary
Returns:
List of filtered decision maker dictionaries
Raises:
ValidationError: If required fields are missing
"""
try:
# Validate required field exists
if "decisionMakers" not in data:
raise ValidationError("Missing decisionMakers field in response")

result = []
for item in data["decisionMakers"]:
try:
organization = item.get("Organization", {})

# Skip if not a decision maker
if not organization.get("IsDecisionMaker"):
continue

# Validate required fields
name = organization.get("Name")
org_id = organization.get("ID")

if not all([name, org_id]):
LOGGER.warning(
f"Missing required fields for organization: {organization}"
)
continue

result.append({"Name": name, "ID": org_id})

except Exception as e:
LOGGER.warning(f"Failed to process decision maker: {str(e)}")
continue

return result

except Exception as e:
LOGGER.error(f"Error filtering decision makers: {str(e)}")
raise ValidationError(f"Failed to filter decision makers: {str(e)}")

@staticmethod
def _save_ahjo_setting(
setting_name: AhjoSettingName, filtered_data: List[Dict[str, str]]
) -> None:
"""
Save an ahjo setting to database.
Args:
filtered_data: List of filtered setting data dictionaries
Raises:
ValidationError: If database operation fails
"""
try:
AhjoSetting.objects.update_or_create(
name=setting_name, defaults={"data": filtered_data}
)
except Exception as e:
LOGGER.error(f"Failed to save setting {setting_name}: {str(e)}")
raise ValidationError(
f"Failed to save setting {setting_name} to database: {str(e)}"
)

@staticmethod
def filter_signers(data: Dict) -> List[Dict[str, str]]:
"""
Filter the signers Name and ID from the Ahjo response.
Args:
data: Response data dictionary
Returns:
List of filtered signer dictionaries
Raises:
ValidationError: If required fields are missing
"""
result = []
for item in data["agentList"]:
result.append({"ID": item["ID"], "Name": item["Name"]})
return result
52 changes: 49 additions & 3 deletions backend/benefit/applications/services/ahjo_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import logging
from dataclasses import dataclass, field
from typing import List, Optional, Tuple, Union
from urllib.parse import urlencode

import requests
from django.conf import settings
from django.urls import reverse

from applications.enums import AhjoRequestType, AhjoStatus as AhjoStatusEnum
from applications.models import AhjoSetting, AhjoStatus, Application
from applications.services.ahjo.enums import AhjoSettingName
from applications.services.ahjo.exceptions import (
AhjoApiClientException,
InvalidAhjoTokenException,
Expand Down Expand Up @@ -150,7 +152,9 @@ class AhjoDecisionMakerRequest(AhjoRequest):
@staticmethod
def org_identifier() -> str:
try:
return AhjoSetting.objects.get(name="ahjo_org_identifier").data["id"]
return AhjoSetting.objects.get(
name=AhjoSettingName.DECISION_MAKER_ORG_ID
).data["id"]
except AhjoSetting.DoesNotExist:
raise MissingOrganizationIdentifier(
"No organization identifier found in the database."
Expand All @@ -160,6 +164,46 @@ def api_url(self) -> str:
return f"{self.url_base}/agents/decisionmakers?start={self.org_identifier()}"


@dataclass
class AhjoSignerRequest(AhjoRequest):
application = None
request_type = AhjoRequestType.GET_SIGNER
request_method = "GET"

@staticmethod
def org_identifier() -> str:
try:
setting = AhjoSetting.objects.get(name=AhjoSettingName.SIGNER_ORG_IDS)
if not setting.data:
raise ValueError("Signer organization identifier list is empty")

# If data is string or other type, you might want to validate it's actually a list
if not isinstance(setting.data, list):
raise ValueError("Signer organization identifier must be a list")

# Validate the list is not empty
if len(setting.data) == 0:
raise ValueError("Signer organization identifier list is empty")

return setting.data
except AhjoSetting.DoesNotExist:
raise AhjoSetting.DoesNotExist(
"No signer organization identifier(s) found in the database"
)

def api_url(self) -> str:
"""
Construct an url like this:
https://url_base.com/organization/persons?role=decisionMaker&orgid=ID1&orgid=ID2&orgid=ID3
"""
org_ids = self.org_identifier()
params = [("role", "decisionMaker")]
params.extend([("orgid", org_id) for org_id in org_ids])
query_string = urlencode(params)

return f"{self.url_base}/organization/persons?{query_string}"


class AhjoApiClient:
def __init__(self, ahjo_token: AhjoToken, ahjo_request: AhjoRequest) -> None:
self._timeout = settings.AHJO_REQUEST_TIMEOUT
Expand Down Expand Up @@ -204,6 +248,7 @@ def prepare_ahjo_headers(self) -> dict:
AhjoRequestType.GET_DECISION_DETAILS,
AhjoRequestType.SUBSCRIBE_TO_DECISIONS,
AhjoRequestType.GET_DECISION_MAKER,
AhjoRequestType.GET_SIGNER,
]:
url = reverse(
"ahjo_callback_url",
Expand Down Expand Up @@ -257,6 +302,7 @@ def send_request_to_ahjo(
if self._request.request_type not in [
AhjoRequestType.GET_DECISION_DETAILS,
AhjoRequestType.GET_DECISION_MAKER,
AhjoRequestType.GET_SIGNER,
]:
LOGGER.debug(f"Request {self._request} to Ahjo was successful.")
return self._request.application, response.text
Expand Down Expand Up @@ -303,8 +349,8 @@ def handle_http_error(self, e: requests.exceptions.HTTPError) -> None:
except json.JSONDecodeError:
error_json = None

if hasattr(self._request, "application"):
application_number = self._request.application.application_number
if hasattr(self._request, "application") and self.request.application:
application_number = self.request.application.application_number

error_message = self.format_error_message(e, application_number)

Expand Down
Loading

0 comments on commit 1e1f71d

Please sign in to comment.