Skip to content

Commit

Permalink
feat: callback endpoint for decision status
Browse files Browse the repository at this point in the history
  • Loading branch information
rikuke committed May 6, 2024
1 parent 565b899 commit a2eacea
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 19 deletions.
44 changes: 43 additions & 1 deletion backend/benefit/applications/api/v1/ahjo_integration_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from applications.api.v1.serializers.ahjo_callback import AhjoCallbackSerializer
from applications.api.v1.serializers.ahjo_callback import (
AhjoCallbackSerializer,
AhjoDecisionCallbackSerializer,
)
from applications.enums import (
AhjoCallBackStatus,
AhjoDecisionUpdateType,
AhjoRequestType,
AhjoStatus as AhjoStatusEnum,
ApplicationBatchStatus,
Expand Down Expand Up @@ -272,3 +276,41 @@ def _log_failure_details(self, application, callback_data):
f"Ahjo reports failure with record, hash value {cb_record['hashValue']} \
and fileURI {cb_record['fileUri']}"
)


class AhjoDecisionCallbackView(APIView):
authentication_classes = [
TokenAuthentication,
]
permission_classes = [IsAuthenticated, SafeListPermission]

def post(self, request, *args, **kwargs):
serializer = AhjoDecisionCallbackSerializer(data=request.data)

if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

callback_data = serializer.validated_data
ahjo_case_id = callback_data["caseId"]
update_type = callback_data["updatetype"]

application = get_object_or_404(Application, ahjo_case_id=ahjo_case_id)

if update_type == AhjoDecisionUpdateType.ADDED:
AhjoStatus.objects.create(
application=application, status=AhjoStatusEnum.SIGNED_IN_AHJO
)
elif update_type == AhjoDecisionUpdateType.REMOVED:
AhjoStatus.objects.create(
application=application, status=AhjoStatusEnum.REMOVED_IN_AHJO
)

audit_logging.log(
request.user,
"",
Operation.UPDATE,
application,
additional_information=f"Decision proposal update type: {update_type} was received from Ahjo",
)

return Response({"message": "Callback received"}, status=status.HTTP_200_OK)
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ def validate_message(self, message):
if message not in ["Success", "Failure"]:
raise serializers.ValidationError("Invalid message value.")
return message


class AhjoDecisionCallbackSerializer(serializers.Serializer):
updatetype = serializers.CharField(required=True)
id = serializers.CharField(required=True)
caseId = serializers.CharField(required=True)
caseGuid = serializers.UUIDField(format="hex_verbose", required=True)
10 changes: 10 additions & 0 deletions backend/benefit/applications/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ class AhjoStatus(models.TextChoices):
DELETE_REQUEST_RECEIVED = "delete_request_received", _("Delete request received")
NEW_RECORDS_REQUEST_SENT = "new_record_request_sent", _("New record request sent")
NEW_RECORDS_RECEIVED = "new_record_received", _("New record received by Ahjo")
REMOVED_IN_AHJO = "removed", _("Decision cancelled in Ahjo")
SIGNED_IN_AHJO = "signed", _("Decision signed and completed in Ahjo")
UPDATED_IN_AHJO = "updated", _("Decision updated in Ahjo")


class ApplicationActions(models.TextChoices):
Expand All @@ -196,6 +199,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")
SUBSCRIBE_TO_DECISIONS = "subscribe_to_decisions", _("Subscribe to decisions API")


class DecisionType(models.TextChoices):
Expand Down Expand Up @@ -243,6 +247,12 @@ class AhjoRecordTitle(models.TextChoices):
SECRET_ATTACHMENT = "Päätöksen liite", _("Secret decision attachment title")


class AhjoDecisionUpdateType(models.TextChoices):
ADDED = "Added", _("Added")
REMOVED = "Removed", _("removed")
UPDATED = "Updated", _("Updated")


# Call gettext on some of the enums so that "makemessages" command can find them when used dynamically in templates
_("granted")
_("granted_aged")
Expand Down
40 changes: 27 additions & 13 deletions backend/benefit/applications/services/ahjo_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import uuid
from dataclasses import dataclass, field
from typing import Tuple, Union
from typing import Optional, Tuple, Union

import requests
from django.conf import settings
Expand All @@ -19,12 +19,11 @@

@dataclass
class AhjoRequest:
application: Application
request_type = AhjoRequestType
url_base: str = field(
default_factory=lambda: f"{settings.AHJO_REST_API_URL}{API_CASES_BASE}"
)
application: Optional[Application] = None

lang: str = "fi"
url_base: str = field(default_factory=lambda: settings.AHJO_REST_API_URL)

def __str__(self):
return (
Expand All @@ -38,7 +37,9 @@ 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}/{self.application.ahjo_case_id}/records"
return (
f"{self.url_base}{API_CASES_BASE}/{self.application.ahjo_case_id}/records"
)


@dataclass
Expand All @@ -54,7 +55,7 @@ def api_url(self) -> str:
raise MissingHandlerIdError(
f"Application {self.application.id} handler does not have an ad_username"
)
return self.url_base
return f"{self.url_base}{API_CASES_BASE}"


@dataclass
Expand Down Expand Up @@ -99,12 +100,22 @@ def api_url(self) -> str:
)
if not self.application.ahjo_case_id:
raise MissingAhjoCaseIdError("Application does not have an Ahjo case id")
# Remove /records from the url, as it is not needed for delete requests
url = super().api_url().replace("/records", "")
url = f"{self.url_base}{API_CASES_BASE}/{self.application.ahjo_case_id}"
draftsman_id = self.application.calculation.handler.ad_username
return f"{url}?draftsmanid={draftsman_id}&reason={self.reason}&apireqlang={self.lang}"


@dataclass
class AhjoSubscribeDecisionRequest(AhjoRequest):
"""Request to subscribe to a decision in Ahjo."""

request_type = AhjoRequestType.SUBSCRIBE_TO_DECISIONS
request_method = "POST"

def api_url(self) -> str:
return f"{self.url_base}/decisions/subscribe"


class AhjoApiClientException(Exception):
pass

Expand Down Expand Up @@ -140,12 +151,15 @@ def prepare_ahjo_headers(self) -> dict:
"""Prepare the headers for the Ahjo given Ahjo request type.
The headers are used to authenticate the request to Ahjo and register a callback address.
"""

kwargs_dict = {"request_type": self._request.request_type}

if not self._request.request_type == AhjoRequestType.SUBSCRIBE_TO_DECISIONS:
kwargs_dict["uuid"] = str(self._request.application.id)

url = reverse(
"ahjo_callback_url",
kwargs={
"request_type": self._request.request_type,
"uuid": str(self._request.application.id),
},
kwargs=kwargs_dict,
)

return {
Expand Down
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 @@ -26,6 +26,7 @@
AhjoDecisionProposalRequest,
AhjoDeleteCaseRequest,
AhjoOpenCaseRequest,
AhjoSubscribeDecisionRequest,
AhjoUpdateRecordsRequest,
)
from applications.services.ahjo_payload import (
Expand Down Expand Up @@ -608,3 +609,15 @@ def delete_existing_xml_attachments(application: Application):
LOGGER.info(
f"Deleted existing decision text attachments for application {application.id}"
)


def send_subscription_request_to_ahjo(ahjo_auth_token: AhjoToken):
"""Send a subscription request to Ahjo."""
try:
ahjo_request = AhjoSubscribeDecisionRequest()
ahjo_client = AhjoApiClient(ahjo_auth_token, ahjo_request)
response, response_text = ahjo_client.send_request_to_ahjo
except ObjectDoesNotExist as e:
LOGGER.error(f"Object not found: {e}")
except ImproperlyConfigured as e:
LOGGER.error(f"Improperly configured: {e}")
60 changes: 56 additions & 4 deletions backend/benefit/applications/tests/test_ahjo_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import uuid
import zipfile
from datetime import date, timedelta
from typing import List
from typing import List, Union
from unittest.mock import patch

import pytest
Expand All @@ -15,6 +15,7 @@
from applications.api.v1.ahjo_integration_views import AhjoAttachmentView
from applications.enums import (
AhjoCallBackStatus,
AhjoDecisionUpdateType,
AhjoRequestType,
AhjoStatus as AhjoStatusEnum,
ApplicationBatchStatus,
Expand Down Expand Up @@ -395,10 +396,20 @@ def test_get_attachment_unauthorized_ip_not_allowed(
assert response.status_code == 403


def _get_callback_url(request_type: AhjoRequestType, decided_application: Application):
def _get_callback_url(
request_type: AhjoRequestType, decided_application: Union[Application, None]
):
kwargs_dict = {}
route_name = "ahjo_decision_callback_url"

if not request_type == AhjoRequestType.SUBSCRIBE_TO_DECISIONS:
kwargs_dict["request_type"] = request_type
kwargs_dict["uuid"] = str(decided_application.id)
route_name = "ahjo_callback_url"

return reverse(
"ahjo_callback_url",
kwargs={"request_type": request_type, "uuid": decided_application.id},
route_name,
kwargs=kwargs_dict,
)


Expand Down Expand Up @@ -474,6 +485,7 @@ def test_ahjo_callback_success(
attachment.refresh_from_db()
assert response.status_code == 200
assert response.data == {"message": "Callback received"}

if request_type == AhjoRequestType.OPEN_CASE:
assert decided_application.ahjo_case_id == ahjo_callback_payload["caseId"]
assert (
Expand All @@ -487,18 +499,58 @@ def test_ahjo_callback_success(
assert batch.auto_generated_by_ahjo
assert batch.handler == decided_application.calculation.handler
assert batch.status == ApplicationBatchStatus.DRAFT

if request_type == AhjoRequestType.UPDATE_APPLICATION:
assert (
attachment.ahjo_version_series_id
== ahjo_callback_payload["records"][0]["versionSeriesId"]
)

if request_type == AhjoRequestType.SEND_DECISION_PROPOSAL:
batch = decided_application.batch
assert batch.status == ApplicationBatchStatus.AWAITING_AHJO_DECISION

assert decided_application.ahjo_status.latest().status == ahjo_status


@pytest.mark.parametrize(
"updatetype_from_ahjo, status_after_callback",
[
(AhjoDecisionUpdateType.ADDED, AhjoStatusEnum.SIGNED_IN_AHJO),
(AhjoDecisionUpdateType.REMOVED, AhjoStatusEnum.REMOVED_IN_AHJO),
],
)
def test_subscribe_to_decisions_callback_success(
ahjo_client,
ahjo_user_token,
decided_application,
status_after_callback,
settings,
updatetype_from_ahjo,
):
dummy_case_id = "HEL 1999-123"
decided_application.ahjo_case_id = dummy_case_id
decided_application.save()

settings.NEXT_PUBLIC_MOCK_FLAG = True
auth_headers = {"HTTP_AUTHORIZATION": "Token " + ahjo_user_token.key}

callback_payload = {
"updatetype": updatetype_from_ahjo,
"id": f"{uuid.uuid4()}",
"caseId": dummy_case_id,
"caseGuid": f"{uuid.uuid4()}",
}
cb_url = _get_callback_url(AhjoRequestType.SUBSCRIBE_TO_DECISIONS, None)
response = ahjo_client.post(cb_url, **auth_headers, data=callback_payload)

assert response.status_code == 200
assert response.data == {"message": "Callback received"}

decided_application.refresh_from_db()
assert decided_application.ahjo_status.latest().status == status_after_callback


@pytest.mark.django_db
def test_ahjo_open_case_callback_failure(
ahjo_client,
Expand Down
2 changes: 1 addition & 1 deletion backend/benefit/applications/tests/test_ahjo_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_ahjo_requests(
assert request.application == application
assert request.request_type == request_type
assert request.request_method == request_method
assert request.url_base == f"{settings.AHJO_REST_API_URL}{API_CASES_BASE}"
assert request.url_base == f"{settings.AHJO_REST_API_URL}"
assert request.lang == "fi"
assert (
str(request)
Expand Down
6 changes: 6 additions & 0 deletions backend/benefit/helsinkibenefit/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from applications.api.v1.ahjo_integration_views import (
AhjoAttachmentView,
AhjoCallbackView,
AhjoDecisionCallbackView,
)
from applications.api.v1.review_state_views import ReviewStateView
from applications.api.v1.talpa_integration_views import TalpaCallbackView
Expand Down Expand Up @@ -88,6 +89,11 @@
AhjoCallbackView.as_view(),
name="ahjo_callback_url",
),
path(
"v1/ahjo-integration/decision-callback/",
AhjoDecisionCallbackView.as_view(),
name="ahjo_decision_callback_url",
),
path(
"v1/ahjo-integration/attachment/<uuid:uuid>",
AhjoAttachmentView.as_view(),
Expand Down

0 comments on commit a2eacea

Please sign in to comment.