Skip to content

Commit

Permalink
Hl 1137 decision tmpl (#2833)
Browse files Browse the repository at this point in the history
* feat: add proposal template sections

* feat: add factories and seeding for templates

* feat: add template sections to admin

* feat: api endpoint for proposal templates

* feat: add decision text, xml attachment types

* feat: generate decision text xml files

* feat: generate decision text payload

* feat: send decision proposal payload to Ahjo

* feat: utilities for manual testing

* feat: allow decision callback to succeed

* chore: style fix

* chore: tests
  • Loading branch information
rikuke authored Feb 15, 2024
1 parent 00b32cf commit 8cecb6f
Show file tree
Hide file tree
Showing 20 changed files with 913 additions and 28 deletions.
26 changes: 26 additions & 0 deletions backend/benefit/applications/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django import forms
from django.contrib import admin
from django.utils.safestring import mark_safe

from applications.models import (
AhjoSetting,
Expand All @@ -7,6 +9,7 @@
ApplicationBatch,
ApplicationLogEntry,
Attachment,
DecisionProposalTemplateSection,
DeMinimisAid,
Employee,
)
Expand Down Expand Up @@ -107,6 +110,26 @@ class AhjoSettingAdmin(admin.ModelAdmin):
search_fields = ["name"]


class SafeTextareaWidget(forms.Textarea):
def format_value(self, value):
# Marks the value as safe, preventing auto-escaping
return mark_safe(value) if value is not None else ""


class DecisionProposalTemplateSectionFormAdmin(forms.ModelForm):
class Meta:
model = DecisionProposalTemplateSection
fields = "__all__"
widgets = {
"template_text": SafeTextareaWidget(),
}


class DecisionProposalTemplateSectionAdmin(admin.ModelAdmin):
list_display = ["name"]
search_fields = ["name"]


admin.site.register(Application, ApplicationAdmin)
admin.site.register(ApplicationBatch, ApplicationBatchAdmin)
admin.site.register(DeMinimisAid)
Expand All @@ -115,3 +138,6 @@ class AhjoSettingAdmin(admin.ModelAdmin):
admin.site.register(ApplicationBasis, ApplicationBasisAdmin)
admin.site.register(ApplicationLogEntry)
admin.site.register(AhjoSetting, AhjoSettingAdmin)
admin.site.register(
DecisionProposalTemplateSection, DecisionProposalTemplateSectionAdmin
)
67 changes: 67 additions & 0 deletions backend/benefit/applications/api/v1/ahjo_decision_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from applications.api.v1.serializers.decision_proposal_template import (
DecisionProposalTemplateSectionSerializer,
)
from applications.enums import ApplicationStatus
from applications.models import Application, DecisionProposalTemplateSection
from applications.services.ahjo_decision_service import process_template_sections
from common.permissions import BFIsHandler


class DecisionProposalTemplateSectionList(APIView):
"""
View to list the decision proposal templates with placeholders replaced by actual application data.
* Only handlers are able to access this view.
"""

permission_classes = [BFIsHandler]

@extend_schema(
parameters=[
OpenApiParameter(
name="uuid",
description="UUID of the application",
required=True,
type=OpenApiTypes.UUID,
location=OpenApiParameter.PATH,
),
],
description=("API for querying decision proposal templates"),
request=DecisionProposalTemplateSectionSerializer,
)
def get(self, request, format=None) -> Response:
application_id = self.request.query_params.get("application_id")
try:
application = (
Application.objects.filter(
pk=application_id, status=ApplicationStatus.ACCEPTED
)
.prefetch_related("calculation", "company")
.first()
)
except Application.DoesNotExist:
return Response(
{"message": "Application not found"}, status=status.HTTP_404_NOT_FOUND
)

decision_types = self.request.query_params.getlist("decision_type")

section_types = self.request.query_params.getlist("section_type")
template_sections = DecisionProposalTemplateSection.objects.filter(
section_type__in=section_types, decision_type__in=decision_types
)

replaced_template_sections = process_template_sections(
template_sections, application
)

serializer = DecisionProposalTemplateSectionSerializer(
replaced_template_sections, many=True
)

return Response(serializer.data)
13 changes: 12 additions & 1 deletion backend/benefit/applications/api/v1/ahjo_integration_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ def post(self, request, *args, **kwargs):
{"error": "Application not found"}, status=status.HTTP_404_NOT_FOUND
)

if request_type == AhjoRequestType.SEND_DECISION_PROPOSAL:
return self.handle_success_callback(
request, application, callback_data, request_type
)

if callback_data["message"] == AhjoCallBackStatus.SUCCESS:
return self.handle_success_callback(
request, application, callback_data, request_type
Expand All @@ -141,7 +146,9 @@ def handle_success_callback(
self._handle_delete_callback()
ahjo_status = AhjoStatusEnum.DELETE_REQUEST_RECEIVED
info = f"Application was marked for cancellation in Ahjo with request id: {callback_data['requestId']}"

elif request_type == AhjoRequestType.SEND_DECISION_PROPOSAL:
ahjo_status = AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED
info = "Decision proposal was sent to Ahjo"
AhjoStatus.objects.create(application=application, status=ahjo_status)
audit_logging.log(
request.user,
Expand Down Expand Up @@ -196,6 +203,10 @@ def _save_version_series_id(
attachment.ahjo_version_series_id = cb_record.get("versionSeriesId")
attachment.save()

def handle_decision_proposal_success(self):
# do anything that needs to be done when Ahjo accepts a decision proposal
pass

def _log_failure_details(self, application, callback_data):
LOGGER.error(
f"Received unsuccessful callback for application {application.id} \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@


class AhjoCallbackSerializer(serializers.Serializer):
message = serializers.CharField()
requestId = serializers.UUIDField(format="hex_verbose")
message = serializers.CharField(required=False)
requestId = serializers.UUIDField(format="hex_verbose", required=False)
caseId = serializers.CharField(required=False)
caseGuid = serializers.UUIDField(format="hex_verbose", required=False)
records = serializers.ListField()
records = serializers.ListField(required=False)

# 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,9 @@
from rest_framework import serializers

from applications.models import DecisionProposalTemplateSection


class DecisionProposalTemplateSectionSerializer(serializers.ModelSerializer):
class Meta:
model = DecisionProposalTemplateSection
fields = ["id", "name", "section_type", "template_text"]
19 changes: 19 additions & 0 deletions backend/benefit/applications/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ class AttachmentType(models.TextChoices):
FULL_APPLICATION = "full_application", _("full application")
OTHER_ATTACHMENT = "other_attachment", _("other attachment")
PDF_SUMMARY = "pdf_summary", _("pdf summary")
DECISION_TEXT_XML = "decision_text_xml", _("public decision text xml attachment")
DECISION_TEXT_SECRET_XML = "decision_text_secret_xml", _(
"non-public decision text xml attachment"
)


class AttachmentRequirement(models.TextChoices):
Expand Down Expand Up @@ -188,3 +192,18 @@ class AhjoRequestType(models.TextChoices):
OPEN_CASE = "open_case", _("Open case in Ahjo")
DELETE_APPLICATION = "delete_application", _("Delete application in Ahjo")
UPDATE_APPLICATION = "update_application", _("Update application in Ahjo")
SEND_DECISION_PROPOSAL = "send_decision", _("Send decision to Ahjo")


class DecisionProposalTemplateSectionType(models.TextChoices):
DECISION_SECTION = "decision_section", _(
"Template part for the decision section of a application decision proposal"
)
JUSTIFICATION_SECTION = "justification_section", _(
"Template part for the decision justification section of a decision proposal"
)


class DecisionType(models.TextChoices):
ACCEPTED = "accepted_decision", _("An accepted decision")
DENIED = "denied_decision", _("A denied decision")
24 changes: 23 additions & 1 deletion backend/benefit/applications/management/commands/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,26 @@
ApplicationBatchStatus,
ApplicationOrigin,
ApplicationStatus,
DecisionType,
)
from applications.models import (
AhjoDecisionText,
AhjoStatus,
Application,
ApplicationBasis,
ApplicationBatch,
)
from applications.tests.common import create_decision_text_for_application
from applications.tests.factories import (
AcceptedDecisionProposalFactory,
AcceptedDecisionProposalJustificationFactory,
AdditionalInformationNeededApplicationFactory,
ApplicationBatchFactory,
ApplicationWithAttachmentFactory,
CancelledApplicationFactory,
DecidedApplicationFactory,
DeniedDecisionProposalFactory,
DeniedDecisionProposalJustificationFactory,
HandlingApplicationFactory,
ReceivedApplicationFactory,
RejectedApplicationFactory,
Expand Down Expand Up @@ -74,6 +81,7 @@ def clear_applications():
AhjoStatus.objects.all().delete()
Terms.objects.all().delete()
User.objects.filter(last_login=None).exclude(username="admin").delete()
AhjoDecisionText.objects.all().delete()


def run_seed(number):
Expand All @@ -95,14 +103,19 @@ def _create_batch(
for _ in range(number):
if proposal_for_decision == ApplicationStatus.ACCEPTED:
app = DecidedApplicationFactory()
create_decision_text_for_application(app)
apps.append(app)
AhjoStatus.objects.create(
status=AhjoStatusEnum.SUBMITTED_BUT_NOT_SENT_TO_AHJO,
application=app,
)

elif proposal_for_decision == ApplicationStatus.REJECTED:
apps.append(RejectedApplicationFactory())
app = RejectedApplicationFactory()
create_decision_text_for_application(
app, decision_type=DecisionType.DENIED
)
apps.append(app)
batch.applications.set(apps)
batch.handler = User.objects.filter(is_staff=True).last()
batch.save()
Expand Down Expand Up @@ -166,6 +179,15 @@ def _create_batch(
Application.objects.filter(pk__in=ids).update(modified_at=draft_deletion_threshold)
Application.objects.exclude(pk__in=ids).update(modified_at=draft_notify_threshold)

_create_templates()


def _past_datetime(days: int) -> datetime:
return timezone.now() - timedelta(days=days)


def _create_templates():
AcceptedDecisionProposalFactory(),
AcceptedDecisionProposalJustificationFactory(),
DeniedDecisionProposalFactory(),
DeniedDecisionProposalJustificationFactory(),
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 3.2.23 on 2024-02-14 08:57

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

dependencies = [
('applications', '0055_add_attachment_version_id_and_hash_20240131_1620'),
]

operations = [
migrations.CreateModel(
name='DecisionProposalTemplateSection',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='time created')),
('modified_at', models.DateTimeField(auto_now=True, verbose_name='time modified')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('section_type', models.CharField(choices=[('decision_section', 'Template part for the decision section of a application decision proposal'), ('justification_section', 'Template part for the decision justification section of a decision proposal')], default='decision_section', max_length=64, verbose_name='type of the decision proposal template section')),
('decision_type', models.CharField(choices=[('accepted_decision', 'An accepted decision'), ('denied_decision', 'A denied decision')], default='accepted_decision', max_length=64, verbose_name='type of the decision')),
('language', models.CharField(choices=[('fi', 'suomi'), ('sv', 'svenska'), ('en', 'english')], default='fi', max_length=2)),
('template_text', models.TextField(verbose_name='decision proposal section text content')),
('name', models.CharField(max_length=256, verbose_name='name of the decision proposal template section')),
],
options={
'verbose_name': 'decision proposal template section',
'verbose_name_plural': 'decision proposal template sections',
'db_table': 'bf_applications_decision_proposal_template_section',
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 3.2.23 on 2024-02-14 10:34

from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [
('applications', '0056_decisionproposaltemplatesection'),
]

operations = [
migrations.AlterField(
model_name='attachment',
name='attachment_type',
field=models.CharField(choices=[('employment_contract', 'employment contract'), ('pay_subsidy_decision', 'pay subsidy decision'), ('commission_contract', 'commission contract'), ('education_contract', 'education contract of the apprenticeship office'), ('helsinki_benefit_voucher', 'helsinki benefit voucher'), ('employee_consent', 'employee consent'), ('full_application', 'full application'), ('other_attachment', 'other attachment'), ('pdf_summary', 'pdf summary'), ('decision_text_xml', 'public decision text xml attachment'), ('decision_text_secret_xml', 'non-public decision text xml attachment')], max_length=64, verbose_name='attachment type in business rules'),
),
migrations.AlterField(
model_name='historicalattachment',
name='attachment_type',
field=models.CharField(choices=[('employment_contract', 'employment contract'), ('pay_subsidy_decision', 'pay subsidy decision'), ('commission_contract', 'commission contract'), ('education_contract', 'education contract of the apprenticeship office'), ('helsinki_benefit_voucher', 'helsinki benefit voucher'), ('employee_consent', 'employee consent'), ('full_application', 'full application'), ('other_attachment', 'other attachment'), ('pdf_summary', 'pdf summary'), ('decision_text_xml', 'public decision text xml attachment'), ('decision_text_secret_xml', 'non-public decision text xml attachment')], max_length=64, verbose_name='attachment type in business rules'),
),
migrations.CreateModel(
name='AhjoDecisionText',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='time created')),
('modified_at', models.DateTimeField(auto_now=True, verbose_name='time modified')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('decision_type', models.CharField(choices=[('accepted_decision', 'An accepted decision'), ('denied_decision', 'A denied decision')], default='accepted_decision', max_length=64, verbose_name='type of the decision')),
('language', models.CharField(choices=[('fi', 'suomi'), ('sv', 'svenska'), ('en', 'english')], default='fi', max_length=2)),
('decision_text', models.TextField(verbose_name='decision text content')),
('application', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='application')),
],
options={
'verbose_name': 'ahjo decision text',
'verbose_name_plural': 'ahjo decision texts',
'db_table': 'bf_applications_ahjo_decision_text',
},
),
]
Loading

0 comments on commit 8cecb6f

Please sign in to comment.