Skip to content

Commit

Permalink
Hl 1132 ahjo versionid (#2789)
Browse files Browse the repository at this point in the history
* feat: version series id, file hash for attachment

* feat: save version series id in callback success

* chore: handle callback failure

* chore: refactor to reduce cognitive complexity
  • Loading branch information
rikuke authored Feb 5, 2024
1 parent d0bd02c commit 57fc4ee
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 32 deletions.
97 changes: 74 additions & 23 deletions backend/benefit/applications/api/v1/ahjo_integration_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
LOGGER = logging.getLogger(__name__)


class AhjoCallbackError(Exception):
pass


class AhjoAttachmentView(APIView):
authentication_classes = [
TokenAuthentication,
Expand Down Expand Up @@ -104,26 +108,41 @@ def post(self, request, *args, **kwargs):

callback_data = serializer.validated_data
application_id = self.kwargs["uuid"]
request_type = self.kwargs["request_type"]

if callback_data["message"] == AhjoCallBackStatus.SUCCESS:
ahjo_request_id = callback_data["requestId"]
application = get_object_or_404(Application, pk=application_id)
request_type = self.kwargs["request_type"]
try:
application = Application.objects.get(pk=application_id)
except Application.DoesNotExist:
return Response(
{"error": "Application not found"}, status=status.HTTP_404_NOT_FOUND
)

if callback_data["message"] == AhjoCallBackStatus.SUCCESS:
return self.handle_success_callback(
request, application, callback_data, request_type
)
elif callback_data["message"] == AhjoCallBackStatus.FAILURE:
return self.handle_failure_callback(application, callback_data)

def handle_success_callback(
self,
request,
application: Application,
callback_data: dict,
request_type: AhjoRequestType,
) -> Response:
try:
if request_type == AhjoRequestType.OPEN_CASE:
application = self._handle_open_case_callback(
application, callback_data
)
self._handle_open_case_success(application, callback_data)
ahjo_status = AhjoStatusEnum.CASE_OPENED
info = f"""Application ahjo_case_guid and ahjo_case_id
were updated by Ahjo request id: {ahjo_request_id}"""
info = f"Application ahjo_case_guid and ahjo_case_id were updated by Ahjo \
with request id: {callback_data['requestId']}"
elif request_type == AhjoRequestType.DELETE_APPLICATION:
self._handle_delete_callback()
ahjo_status = AhjoStatusEnum.DELETE_REQUEST_RECEIVED
info = f"""Application was marked for cancellation in Ahjo with request id: {ahjo_request_id}"""
info = f"Application was marked for cancellation in Ahjo with request id: {callback_data['requestId']}"

AhjoStatus.objects.create(application=application, status=ahjo_status)

audit_logging.log(
request.user,
"",
Expand All @@ -132,27 +151,59 @@ def post(self, request, *args, **kwargs):
additional_information=info,
)

return Response({"message": "Callback received"}, status=status.HTTP_200_OK)
except AhjoCallbackError as e:
return Response(
{"message": "Callback received"},
status=status.HTTP_200_OK,
{"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
)

elif callback_data["message"] == AhjoCallBackStatus.FAILURE:
LOGGER.error(
f"""Error: Received unsuccessful callback from Ahjo
for application {application_id} with request_id {callback_data['requestId']}"""
)
return Response(
{"message": "Callback received but unsuccessful"},
status=status.HTTP_200_OK,
)
def handle_failure_callback(
self, application: Application, callback_data: dict
) -> Response:
self._log_failure_details(application, callback_data)
return Response(
{"message": "Callback received but request was unsuccessful at AHJO"},
status=status.HTTP_400_BAD_REQUEST,
)

def _handle_open_case_callback(self, application: Application, callback_data: dict):
def _handle_open_case_success(self, application: Application, callback_data: dict):
"""Update the application with the case id (diaarinumero) and case guid from the Ahjo callback data."""
application.ahjo_case_guid = callback_data["caseGuid"]
application.ahjo_case_id = callback_data["caseId"]
cb_records = callback_data.get("records", [])

self._save_version_series_id(application, cb_records)

application.save()
return application

def _handle_delete_callback(self):
# do anything that needs to be done when Ahjo sends a delete callback
pass

def _save_version_series_id(
self, application: Application, cb_records: list
) -> None:
"""Save the version series id for each attachment in the callback data \
if the calculated sha256 hashes match."""
attachment_map = {
attachment.ahjo_hash_value: attachment
for attachment in application.attachments.all()
}
for cb_record in cb_records:
attachment = attachment_map.get(cb_record.get("hashValue"))
if attachment:
attachment.ahjo_version_series_id = cb_record.get("versionSeriesId")
attachment.save()

def _log_failure_details(self, application, callback_data):
LOGGER.error(
f"Received unsuccessful callback for application {application.id} \
with request_id {callback_data['requestId']}"
)
for cb_record in callback_data.get("records", []):
if cb_record.get("status") == AhjoCallBackStatus.FAILURE:
LOGGER.error(
f"Ahjo reports failure with record, hash value {cb_record['hashValue']} \
and fileURI {cb_record['fileUri']}"
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class AhjoCallbackSerializer(serializers.Serializer):
requestId = serializers.UUIDField(format="hex_verbose")
caseId = serializers.CharField()
caseGuid = serializers.UUIDField(format="hex_verbose")
records = serializers.ListField()

# You can add additional validation here if needed
def validate_message(self, message):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 3.2.23 on 2024-01-31 14:20

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('applications', '0054_alter_historicalattachment_attachment_type'),
]

operations = [
migrations.AddField(
model_name='attachment',
name='ahjo_hash_value',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AddField(
model_name='attachment',
name='ahjo_version_series_id',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AddField(
model_name='historicalattachment',
name='ahjo_hash_value',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AddField(
model_name='historicalattachment',
name='ahjo_version_series_id',
field=models.CharField(blank=True, max_length=64, null=True),
),
]
5 changes: 5 additions & 0 deletions backend/benefit/applications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,11 @@ class Attachment(UUIDModel, TimeStampedModel):
verbose_name=_("technical content type of the attachment"),
)
attachment_file = models.FileField(verbose_name=_("application attachment content"))

ahjo_version_series_id = models.CharField(max_length=64, null=True, blank=True)

ahjo_hash_value = models.CharField(max_length=64, null=True, blank=True)

history = HistoricalRecords(
table_name="bf_applications_attachment_history", cascade_delete_history=True
)
Expand Down
2 changes: 2 additions & 0 deletions backend/benefit/applications/services/ahjo_payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def _prepare_record_document_dict(attachment: Attachment) -> dict:
# If were running in mock mode, use the local file URI
file_url = reverse("ahjo_attachment_url", kwargs={"uuid": attachment.id})
hash_value = hash_file(attachment.attachment_file)
attachment.ahjo_hash_value = hash_value
attachment.save()
return {
"FileName": f"{attachment.attachment_file.name}",
"FormatName": f"{attachment.content_type}",
Expand Down
74 changes: 65 additions & 9 deletions backend/benefit/applications/tests/test_ahjo_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from applications.tests.factories import ApplicationFactory, DecidedApplicationFactory
from calculator.models import Calculation
from calculator.tests.factories import PaySubsidyFactory
from common.utils import hash_file
from companies.tests.factories import CompanyFactory
from helsinkibenefit.tests.conftest import * # noqa
from shared.common.tests.utils import normalize_whitespace
Expand Down Expand Up @@ -371,6 +372,24 @@ def test_get_attachment_unauthorized_ip_not_allowed(
assert response.status_code == 403


@pytest.fixture
def ahjo_callback_payload():
return {
"message": "",
"requestId": f"{uuid.uuid4()}",
"caseId": "HEL 2023-999999",
"caseGuid": f"{uuid.uuid4()}",
"records": [
{
"fileURI": "https://example.com",
"status": "Success",
"hashValue": "",
"versionSeriesId": f"{uuid.uuid4()}",
}
],
}


@pytest.mark.parametrize(
"request_type, ahjo_status",
[
Expand All @@ -392,30 +411,67 @@ def test_ahjo_callback_success(
settings,
request_type,
ahjo_status,
ahjo_callback_payload,
):
settings.NEXT_PUBLIC_MOCK_FLAG = True
auth_headers = {"HTTP_AUTHORIZATION": "Token " + ahjo_user_token.key}
callback_payload = {
"message": AhjoCallBackStatus.SUCCESS,
"requestId": f"{uuid.uuid4()}",
"caseId": "HEL 2023-999999",
"caseGuid": f"{uuid.uuid4()}",
}
attachment = generate_pdf_summary_as_attachment(decided_application)
attachment_hash_value = hash_file(attachment.attachment_file)
attachment.ahjo_hash_value = attachment_hash_value
attachment.save()
ahjo_callback_payload["message"] = AhjoCallBackStatus.SUCCESS
ahjo_callback_payload["records"][0]["hashValue"] = attachment_hash_value

url = reverse(
"ahjo_callback_url",
kwargs={"request_type": request_type, "uuid": decided_application.id},
)
response = ahjo_client.post(url, **auth_headers, data=callback_payload)
response = ahjo_client.post(url, **auth_headers, data=ahjo_callback_payload)

decided_application.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 == callback_payload["caseId"]
assert str(decided_application.ahjo_case_guid) == callback_payload["caseGuid"]
attachment.refresh_from_db()

assert decided_application.ahjo_case_id == ahjo_callback_payload["caseId"]
assert (
str(decided_application.ahjo_case_guid) == ahjo_callback_payload["caseGuid"]
)
assert (
attachment.ahjo_version_series_id
== ahjo_callback_payload["records"][0]["versionSeriesId"]
)
assert decided_application.ahjo_status.latest().status == ahjo_status


@pytest.mark.django_db
def test_ahjo_open_case_callback_failure(
ahjo_client,
ahjo_user_token,
decided_application,
settings,
ahjo_callback_payload,
):
ahjo_callback_payload["message"] = AhjoCallBackStatus.FAILURE

url = reverse(
"ahjo_callback_url",
kwargs={
"request_type": AhjoRequestType.OPEN_CASE,
"uuid": decided_application.id,
},
)
settings.NEXT_PUBLIC_MOCK_FLAG = True
auth_headers = {"HTTP_AUTHORIZATION": "Token " + ahjo_user_token.key}
response = ahjo_client.post(url, **auth_headers, data=ahjo_callback_payload)

assert response.status_code == 400
assert response.data == {
"message": "Callback received but request was unsuccessful at AHJO"
}


@pytest.mark.parametrize(
"request_type",
[
Expand Down

0 comments on commit 57fc4ee

Please sign in to comment.