Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: callback endpoint for decision status #2977

Merged
merged 4 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -313,3 +317,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
)
# TODO what to do if updatetype is "updated"
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 @@ -178,6 +178,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 @@ -197,6 +200,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 @@ -244,6 +248,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import logging

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

from applications.services.ahjo_integration import (
get_token,
send_subscription_request_to_ahjo,
)

LOGGER = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Subscribe to decisions API for decision status updates"

def handle(self, *args, **options):
try:
ahjo_auth_token = get_token()
except ImproperlyConfigured as e:
LOGGER.error(f"Failed to get auth token from Ahjo: {e}")
return
send_subscription_request_to_ahjo(ahjo_auth_token)
self.stdout.write("Subscribed to decisions API")
return
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 3.2.23 on 2024-04-29 10:07
# Generated by Django 3.2.23 on 2024-05-07 05:49

from django.db import migrations, models

Expand All @@ -13,6 +13,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='ahjostatus',
name='status',
field=models.CharField(choices=[('submitted_but_not_sent_to_ahjo', 'Submitted but not sent to AHJO'), ('request_to_open_case_sent', 'Request to open the case sent to AHJO'), ('case_opened', 'Case opened in AHJO'), ('update_request_sent', 'Update request sent'), ('update_request_received', 'Update request received'), ('decision_proposal_sent', 'Decision proposal sent'), ('decision_proposal_accepted', 'Decision proposal accepted'), ('decision_proposal_rejected', 'Decision proposal rejected'), ('scheduled_for_deletion', 'Scheduled for deletion'), ('delete_request_sent', 'Delete request sent'), ('delete_request_received', 'Delete request received'), ('new_record_request_sent', 'New record request sent'), ('new_record_received', 'New record received by Ahjo')], default='submitted_but_not_sent_to_ahjo', max_length=64, verbose_name='status'),
field=models.CharField(choices=[('submitted_but_not_sent_to_ahjo', 'Submitted but not sent to AHJO'), ('request_to_open_case_sent', 'Request to open the case sent to AHJO'), ('case_opened', 'Case opened in AHJO'), ('update_request_sent', 'Update request sent'), ('update_request_received', 'Update request received'), ('decision_proposal_sent', 'Decision proposal sent'), ('decision_proposal_accepted', 'Decision proposal accepted'), ('decision_proposal_rejected', 'Decision proposal rejected'),('scheduled_for_deletion', 'Scheduled for deletion'),
('delete_request_sent', 'Delete request sent'), ('delete_request_received', 'Delete request received'), ('new_record_request_sent', 'New record request sent'), ('new_record_received', 'New record received by Ahjo'), ('removed', 'Decision cancelled in Ahjo'), ('signed', 'Decision signed and completed in Ahjo'), ('updated', 'Decision updated in Ahjo')], default='submitted_but_not_sent_to_ahjo', max_length=64, verbose_name='status'),
),
]
64 changes: 40 additions & 24 deletions backend/benefit/applications/services/ahjo_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import json
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,17 +18,14 @@

@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 (
f"Request of type {self.request_type} for application {self.application.id}"
)
return f"Request of type {self.request_type}"

def api_url(self) -> str:
if not self.application.calculation.handler.ad_username:
Expand All @@ -38,7 +34,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 +52,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 +97,23 @@ 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."""

application = None
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 @@ -139,22 +148,30 @@ def ahjo_token(self, token: AhjoToken) -> None:
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.
If the request is a subscription request, the headers are prepared \
without a callback address in the JSON payload and the Accept and X-CallbackURL headers \
are not needed.
"""
url = reverse(
"ahjo_callback_url",
kwargs={
"request_type": self._request.request_type,
"uuid": str(self._request.application.id),
},
)

return {
headers_dict = {
"Authorization": f"Bearer {self.ahjo_token.access_token}",
"Accept": "application/hal+json",
"X-CallbackURL": f"{settings.API_BASE_URL}{url}",
"Content-Type": "application/json",
}

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

headers_dict["Accept"] = "application/hal+json"
headers_dict["X-CallbackURL"] = f"{settings.API_BASE_URL}{url}"

return headers_dict

def send_request_to_ahjo(
self,
data: Union[dict, None] = None,
Expand Down Expand Up @@ -183,7 +200,6 @@ def send_request_to_ahjo(
if response.ok:
LOGGER.debug(f"Request {self._request} to Ahjo was successful.")
return self._request.application, response.text
return self._request.application, str(uuid.uuid4())
except MissingHandlerIdError as e:
LOGGER.error(f"Missing handler id: {e}")
except MissingAhjoCaseIdError as e:
Expand Down
20 changes: 20 additions & 0 deletions backend/benefit/applications/services/ahjo_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

import jinja2
import pdfkit
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.core.files.base import ContentFile
from django.db.models import QuerySet
from django.urls import reverse

from applications.enums import (
AhjoStatus as AhjoStatusEnum,
Expand All @@ -26,6 +28,7 @@
AhjoDecisionProposalRequest,
AhjoDeleteCaseRequest,
AhjoOpenCaseRequest,
AhjoSubscribeDecisionRequest,
AhjoUpdateRecordsRequest,
)
from applications.services.ahjo_payload import (
Expand Down Expand Up @@ -608,3 +611,20 @@ 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,
) -> Union[Tuple[None, str], None]:
"""Send a subscription request to Ahjo."""
try:
ahjo_request = AhjoSubscribeDecisionRequest()
ahjo_client = AhjoApiClient(ahjo_auth_token, ahjo_request)
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}")
Loading
Loading