diff --git a/backend/benefit/applications/enums.py b/backend/benefit/applications/enums.py index 741963c2f9..dd8e5cfd0b 100644 --- a/backend/benefit/applications/enums.py +++ b/backend/benefit/applications/enums.py @@ -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): diff --git a/backend/benefit/applications/jobs/daily/daily_application_jobs.py b/backend/benefit/applications/jobs/daily/daily_application_jobs.py index f63b8e12e4..68d0840c43 100755 --- a/backend/benefit/applications/jobs/daily/daily_application_jobs.py +++ b/backend/benefit/applications/jobs/daily/daily_application_jobs.py @@ -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) diff --git a/backend/benefit/applications/management/commands/ahjo_base_command.py b/backend/benefit/applications/management/commands/ahjo_base_command.py index 487daadf78..b07d8763c8 100644 --- a/backend/benefit/applications/management/commands/ahjo_base_command.py +++ b/backend/benefit/applications/management/commands/ahjo_base_command.py @@ -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__) @@ -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 diff --git a/backend/benefit/applications/management/commands/get_signer.py b/backend/benefit/applications/management/commands/get_signer.py new file mode 100644 index 0000000000..e740b3ab43 --- /dev/null +++ b/backend/benefit/applications/management/commands/get_signer.py @@ -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 diff --git a/backend/benefit/applications/management/commands/seed.py b/backend/benefit/applications/management/commands/seed.py index 007ccec8b5..c46dc8c162 100755 --- a/backend/benefit/applications/management/commands/seed.py +++ b/backend/benefit/applications/management/commands/seed.py @@ -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=[ diff --git a/backend/benefit/applications/services/ahjo/enums.py b/backend/benefit/applications/services/ahjo/enums.py new file mode 100644 index 0000000000..14f56428c4 --- /dev/null +++ b/backend/benefit/applications/services/ahjo/enums.py @@ -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}" diff --git a/backend/benefit/applications/services/ahjo/request_handler.py b/backend/benefit/applications/services/ahjo/request_handler.py new file mode 100644 index 0000000000..efa38e0cfd --- /dev/null +++ b/backend/benefit/applications/services/ahjo/request_handler.py @@ -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 + ) diff --git a/backend/benefit/applications/services/ahjo/setting_response_handler.py b/backend/benefit/applications/services/ahjo/setting_response_handler.py new file mode 100644 index 0000000000..eb095d066d --- /dev/null +++ b/backend/benefit/applications/services/ahjo/setting_response_handler.py @@ -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 diff --git a/backend/benefit/applications/services/ahjo_client.py b/backend/benefit/applications/services/ahjo_client.py index cd9681d9d8..7c2b0a1cf3 100644 --- a/backend/benefit/applications/services/ahjo_client.py +++ b/backend/benefit/applications/services/ahjo_client.py @@ -2,6 +2,7 @@ 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 @@ -9,6 +10,7 @@ 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, @@ -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." @@ -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 @@ -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", @@ -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 @@ -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) diff --git a/backend/benefit/applications/services/ahjo_integration.py b/backend/benefit/applications/services/ahjo_integration.py index a4702073a3..b4df349cc0 100644 --- a/backend/benefit/applications/services/ahjo_integration.py +++ b/backend/benefit/applications/services/ahjo_integration.py @@ -18,14 +18,12 @@ from lxml.etree import XMLSchemaParseError, XMLSyntaxError from applications.enums import ( - AhjoRequestType, AhjoStatus as AhjoStatusEnum, ApplicationStatus, AttachmentType, ) from applications.models import ( AhjoDecisionText, - AhjoSetting, AhjoStatus, Application, ApplicationBatch, @@ -40,11 +38,9 @@ AhjoAddRecordsRequest, AhjoApiClient, AhjoDecisionDetailsRequest, - AhjoDecisionMakerRequest, AhjoDecisionProposalRequest, AhjoDeleteCaseRequest, AhjoOpenCaseRequest, - AhjoRequest, AhjoSubscribeDecisionRequest, AhjoUpdateRecordsRequest, ) @@ -660,42 +656,3 @@ def get_decision_details_from_ahjo( ahjo_request = AhjoDecisionDetailsRequest(application) ahjo_client = AhjoApiClient(ahjo_token, ahjo_request) return ahjo_client.send_request_to_ahjo() - - -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_decision_maker_from_ahjo() - else: - raise ValueError("Invalid request type") - - def get_decision_maker_from_ahjo(self) -> Union[List, None]: - ahjo_client = AhjoApiClient(self.ahjo_token, AhjoDecisionMakerRequest()) - result = ahjo_client.send_request_to_ahjo() - AhjoResponseHandler.handle_decisionmaker_response(result) - - -class AhjoResponseHandler: - @staticmethod - def handle_decisionmaker_response(response: tuple[None, dict]) -> None: - filtered_data = AhjoResponseHandler.filter_decision_makers(response[1]) - if filtered_data: - AhjoSetting.objects.update_or_create( - name="ahjo_decision_maker", defaults={"data": filtered_data} - ) - - @staticmethod - def filter_decision_makers(data: dict) -> List[dict]: - """Filter the decision makers Name and ID from the Ahjo response.""" - result = [] - for item in data["decisionMakers"]: - organization = item.get("Organization") - if organization and organization.get("IsDecisionMaker"): - result.append( - {"Name": organization.get("Name"), "ID": organization.get("ID")} - ) - return result diff --git a/backend/benefit/applications/tests/conftest.py b/backend/benefit/applications/tests/conftest.py index ae36dce9b8..a716fbd8da 100755 --- a/backend/benefit/applications/tests/conftest.py +++ b/backend/benefit/applications/tests/conftest.py @@ -1094,6 +1094,16 @@ def decisionmaker_response(): ) +@pytest.fixture +def invalid_decisionmaker_response(): + return { + "decisionMakers": [ + {"Organization": {"Name": None, "ID": "ORG001", "IsDecisionMaker": True}}, + {"Organization": {"Name": "Test Org", "ID": None, "IsDecisionMaker": True}}, + ] + } + + @pytest.fixture def fake_decisionmakers(): return [ @@ -1135,6 +1145,43 @@ def signer_settings(fake_signers): ) +@pytest.fixture +def signer_response(): + return { + "agentList": [ + { + "agentId": "kissa213", + "links": [], + "ID": "kissa213", + "Name": "Testaaja, Tiina", + "Title": None, + "Role": "decisionMaker", + "Email": None, + }, + { + "agentId": "koira123", + "links": [], + "ID": "koira123", + "Name": "Testaaja, Timo", + "Title": None, + "Role": "decisionMaker", + "Email": None, + }, + { + "agentId": "kala123", + "links": [], + "ID": "kala123", + "Name": "Testaaja, Teppo", + "Title": None, + "Role": "decisionMaker", + "Email": None, + }, + ], + "count": 3, + "links": [], + } + + @pytest.fixture def non_expired_token(): return AhjoToken( diff --git a/backend/benefit/applications/tests/test_ahjo_request_handler.py b/backend/benefit/applications/tests/test_ahjo_request_handler.py new file mode 100644 index 0000000000..86f8fb9b48 --- /dev/null +++ b/backend/benefit/applications/tests/test_ahjo_request_handler.py @@ -0,0 +1,21 @@ +import pytest + +from applications.enums import AhjoRequestType +from applications.services.ahjo.request_handler import AhjoRequestHandler + + +@pytest.mark.parametrize( + "request_type", + [ + (AhjoRequestType.GET_DECISION_MAKER), + (AhjoRequestType.GET_SIGNER), + ], +) +def test_init_sets_token_and_request_type(request_type, non_expired_token): + """ + Test that the constructor correctly sets token and request type + """ + request_handler = AhjoRequestHandler(non_expired_token, request_type) + + assert request_handler.ahjo_token == non_expired_token + assert request_handler.ahjo_request_type == request_type diff --git a/backend/benefit/applications/tests/test_ahjo_requests.py b/backend/benefit/applications/tests/test_ahjo_requests.py index f9857732e3..7767741025 100644 --- a/backend/benefit/applications/tests/test_ahjo_requests.py +++ b/backend/benefit/applications/tests/test_ahjo_requests.py @@ -9,6 +9,7 @@ from applications.enums import AhjoRequestType, AhjoStatus as AhjoStatusEnum from applications.models import AhjoSetting, AhjoStatus +from applications.services.ahjo.enums import AhjoSettingName from applications.services.ahjo.exceptions import ( AhjoApiClientException, InvalidAhjoTokenException, @@ -23,6 +24,7 @@ AhjoDecisionProposalRequest, AhjoDeleteCaseRequest, AhjoOpenCaseRequest, + AhjoSignerRequest, AhjoSubscribeDecisionRequest, AhjoUpdateRecordsRequest, ) @@ -50,6 +52,12 @@ def ahjo_open_case_request(application_with_ahjo_case_id): "GET", "/agents/decisionmakers?start=", ), + ( + AhjoSignerRequest, + AhjoRequestType.GET_SIGNER, + "GET", + "/organization/persons?role=decisionMaker", + ), ], ) def test_ahjo_requests_without_application( @@ -60,7 +68,13 @@ def test_ahjo_requests_without_application( settings, non_expired_token, ): - AhjoSetting.objects.create(name="ahjo_org_identifier", data={"id": "1234567-8"}) + AhjoSetting.objects.create( + name=AhjoSettingName.DECISION_MAKER_ORG_ID, data={"id": "1234567-8"} + ) + AhjoSetting.objects.create( + name=AhjoSettingName.SIGNER_ORG_IDS, data=["1234567", "7654321"] + ) + settings.API_BASE_URL = "http://test.com" request_instance = ahjo_request_class() @@ -72,6 +86,12 @@ def test_ahjo_requests_without_application( assert f"{settings.AHJO_REST_API_URL}{url_part}" in request_instance.api_url() + if request_type == AhjoRequestType.GET_SIGNER: + assert ( + request_instance.api_url() + == f"{settings.AHJO_REST_API_URL}{url_part}&orgid=1234567&orgid=7654321" + ) + client = AhjoApiClient(non_expired_token, request_instance) with requests_mock.Mocker() as m: diff --git a/backend/benefit/applications/tests/test_ahjo_response_handler.py b/backend/benefit/applications/tests/test_ahjo_response_handler.py index 3265a9b9eb..f4cd357efe 100644 --- a/backend/benefit/applications/tests/test_ahjo_response_handler.py +++ b/backend/benefit/applications/tests/test_ahjo_response_handler.py @@ -1,4 +1,10 @@ -from applications.services.ahjo_integration import AhjoResponseHandler +from unittest.mock import patch + +import pytest +from django.core.exceptions import ValidationError + +from applications.services.ahjo.enums import AhjoSettingName +from applications.services.ahjo.setting_response_handler import AhjoResponseHandler def test_ahjo_response_handler_filter_decision_makers(decisionmaker_response): @@ -18,3 +24,142 @@ def test_ahjo_response_handler_filter_decision_makers(decisionmaker_response): "ID": decisionmaker_response[1]["decisionMakers"][1]["Organization"]["ID"], }, ] + + +def test_ahjo_response_handler_filter_signers(signer_response): + result = AhjoResponseHandler.filter_signers(signer_response) + assert len(result) == 3 + assert result == [ + { + "Name": signer_response["agentList"][0]["Name"], + "ID": signer_response["agentList"][0]["ID"], + }, + { + "Name": signer_response["agentList"][1]["Name"], + "ID": signer_response["agentList"][1]["ID"], + }, + { + "Name": signer_response["agentList"][2]["Name"], + "ID": signer_response["agentList"][2]["ID"], + }, + ] + + +@pytest.mark.parametrize( + "setting_name", + [ + (AhjoSettingName.DECISION_MAKER), + (AhjoSettingName.SIGNER), + ], +) +@patch("applications.models.AhjoSetting.objects.update_or_create") +def test_handle_ahjo_query_response_successful( + mock_update_or_create, decisionmaker_response, setting_name, signer_response +): + """Test successful handling of setting response""" + if setting_name == AhjoSettingName.DECISION_MAKER: + mock_response = decisionmaker_response[1] + data = [ + { + "Name": decisionmaker_response[1]["decisionMakers"][0]["Organization"][ + "Name" + ], + "ID": decisionmaker_response[1]["decisionMakers"][0]["Organization"][ + "ID" + ], + }, + { + "Name": decisionmaker_response[1]["decisionMakers"][1]["Organization"][ + "Name" + ], + "ID": decisionmaker_response[1]["decisionMakers"][1]["Organization"][ + "ID" + ], + }, + ] + if setting_name == AhjoSettingName.SIGNER: + mock_response = signer_response + data = [ + { + "Name": signer_response["agentList"][0]["Name"], + "ID": signer_response["agentList"][0]["ID"], + }, + { + "Name": signer_response["agentList"][1]["Name"], + "ID": signer_response["agentList"][1]["ID"], + }, + { + "Name": signer_response["agentList"][2]["Name"], + "ID": signer_response["agentList"][2]["ID"], + }, + ] + + AhjoResponseHandler.handle_ahjo_query_response( + setting_name=setting_name, data=mock_response + ) + + mock_update_or_create.assert_called_once_with( + name=setting_name, + defaults={"data": data}, + ) + + +def test_filter_decision_makers_invalid_response(invalid_decisionmaker_response): + """Test filtering decision makers with incomplete data""" + filtered_data = AhjoResponseHandler.filter_decision_makers( + invalid_decisionmaker_response + ) + + # Verify no decision makers are returned due to missing fields + assert len(filtered_data) == 0 + + +def test_filter_decision_makers_missing_decisionmakers_key(): + """Test handling of response without decisionMakers key""" + with pytest.raises( + ValidationError, match="Missing decisionMakers field in response" + ): + AhjoResponseHandler.filter_decision_makers({}) + + +@pytest.mark.parametrize( + "setting_name, test_data", + [ + (AhjoSettingName.DECISION_MAKER, {"Name": "Test Org", "ID": "ORG001"}), + (AhjoSettingName.SIGNER, [{"Name": "Test Signer", "ID": "SIGN001"}]), + ], +) +@patch("applications.models.AhjoSetting.objects.update_or_create") +def test_save_ahjo_setting(mock_update_or_create, setting_name, test_data): + """Test saving decision makers to database""" + + AhjoResponseHandler._save_ahjo_setting(setting_name, test_data) + + mock_update_or_create.assert_called_once_with( + name=setting_name, defaults={"data": test_data} + ) + + +@pytest.mark.parametrize( + "setting_name, test_data", + [ + (AhjoSettingName.DECISION_MAKER, {"Name": "Test Org", "ID": "ORG001"}), + ( + AhjoSettingName.SIGNER, + [ + {"Name": "Test Signer", "ID": "SIGN001"}, + {"Name": "Test Signer 2", "ID": "SIGN002"}, + ], + ), + ], +) +def test_save_ahjo_settings_database_error(setting_name, test_data): + """Test handling of database save error""" + with patch( + "applications.models.AhjoSetting.objects.update_or_create", + side_effect=Exception("Database Error"), + ): + with pytest.raises( + ValidationError, match=f"Failed to save setting {setting_name} to database" + ): + AhjoResponseHandler._save_ahjo_setting(setting_name, test_data)