Skip to content

Commit

Permalink
feat: secret xml attachment as per the new design (#2899)
Browse files Browse the repository at this point in the history
* feat: secret xml attachment as per the  new design

* chore: refactor XML building into a separate class

* feat: title format  for the generated xml files
  • Loading branch information
rikuke committed Mar 27, 2024
1 parent a9fc149 commit 733065b
Show file tree
Hide file tree
Showing 10 changed files with 500 additions and 112 deletions.
55 changes: 15 additions & 40 deletions backend/benefit/applications/services/ahjo_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.core.files.base import ContentFile
from django.db.models import F, OuterRef, QuerySet, Subquery
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import translation

from applications.enums import (
AhjoRequestType,
Expand All @@ -38,11 +36,14 @@
prepare_open_case_payload,
prepare_update_application_payload,
)
from applications.services.ahjo_xml_builder import (
AhjoPublicXMLBuilder,
AhjoSecretXMLBuilder,
)
from applications.services.applications_csv_report import ApplicationsCsvService
from applications.services.generate_application_summary import (
generate_application_summary_file,
)
from calculator.enums import RowType
from companies.models import Company


Expand Down Expand Up @@ -385,36 +386,7 @@ def export_application_batch(batch) -> bytes:


# Constants
XML_VERSION = "<?xml version='1.0' encoding='UTF-8'?>"
PDF_CONTENT_TYPE = "application/pdf"
XML_CONTENT_TYPE = "application/xml"


def generate_secret_xml_string(application: Application) -> str:
calculation_rows = application.calculation.rows.all()
sub_total_rows = calculation_rows.filter(
row_type=RowType.HELSINKI_BENEFIT_SUB_TOTAL_EUR
)

# Set the locale for this thread to the application's language
translation.activate(application.applicant_language)

context = {
"application": application,
"benefit_type": "Palkan Helsinki-lisä",
"calculation_rows": sub_total_rows,
"language": application.applicant_language,
}
xml_content = render_to_string("secret_decision.xml", context)

# Reset the locale to the default
translation.deactivate()

return xml_content


def generate_public_xml_string(content: str) -> str:
return f"""{XML_VERSION}{content}"""


def generate_application_attachment(
Expand All @@ -429,17 +401,20 @@ def generate_application_attachment(
content_type = PDF_CONTENT_TYPE
elif type == AttachmentType.DECISION_TEXT_XML:
decision = AhjoDecisionText.objects.get(application=application)
xml_string = generate_public_xml_string(decision.decision_text)

xml_builder = AhjoPublicXMLBuilder(application, decision)
xml_string = xml_builder.generate_xml()

attachment_data = xml_string.encode("utf-8")
attachment_filename = f"decision_text_{application.application_number}.xml"
content_type = XML_CONTENT_TYPE
attachment_filename = xml_builder.generate_xml_file_name()
content_type = xml_builder.content_type
elif type == AttachmentType.DECISION_TEXT_SECRET_XML:
xml_string = generate_secret_xml_string(application)
xml_builder = AhjoSecretXMLBuilder(application)
xml_string = xml_builder.generate_xml()

attachment_data = xml_string.encode("utf-8")
attachment_filename = (
f"decision_text_secret_{application.application_number}.xml"
)
content_type = XML_CONTENT_TYPE
attachment_filename = xml_builder.generate_xml_file_name()
content_type = xml_builder.content_type
else:
raise ValueError(f"Invalid attachment type {type}")

Expand Down
163 changes: 163 additions & 0 deletions backend/benefit/applications/services/ahjo_xml_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from dataclasses import dataclass
from datetime import date
from typing import List, Tuple

from django.template.loader import render_to_string
from django.utils import translation
from django.utils.translation import gettext_lazy as _

from applications.models import AhjoDecisionText, Application
from calculator.enums import RowType
from calculator.models import Calculation, CalculationRow

XML_VERSION = "<?xml version='1.0' encoding='UTF-8'?>"

AhjoXMLString = str


class AhjoXMLBuilder:
def __init__(self, application: Application) -> None:
self.application = application
self.content_type = "application/xml"

def generate_xml(self) -> AhjoXMLString:
raise NotImplementedError("Subclasses must implement generate_xml")

def generate_xml_file_name(self) -> str:
raise NotImplementedError("Subclasses must implement generate_xml_file_name")


class AhjoPublicXMLBuilder(AhjoXMLBuilder):
"""Generates the XML for the public decision."""

def __init__(
self, application: Application, ahjo_decision_text: AhjoDecisionText
) -> None:
super().__init__(application)
self.ahjo_decision_text = ahjo_decision_text

def generate_xml(self) -> AhjoXMLString:
return f"{XML_VERSION}{self.ahjo_decision_text.decision_text}"

def generate_xml_file_name(self) -> str:
date_str = self.application.created_at.strftime("%d.%m.%Y")
return f"Hakemus {date_str}, päätösteksti, {self.application.application_number}.xml"


SECRET_ATTACHMENT_TEMPLATE = "secret_decision.xml"


@dataclass
class BenefitPeriodRow:
start_date: date
end_date: date
amount_per_month: float
total_amount: float


class AhjoSecretXMLBuilder(AhjoXMLBuilder):
def generate_xml(self) -> AhjoXMLString:
context = self.get_context_for_secret_xml()
# Set the locale for this thread to the application's language
translation.activate(self.application.applicant_language)

xml_content = render_to_string(SECRET_ATTACHMENT_TEMPLATE, context)

# Reset the locale to the default
translation.deactivate()
return xml_content

def _get_period_rows_for_xml(
self,
calculation: Calculation,
) -> Tuple[CalculationRow, List[CalculationRow],]:
total_amount_row = calculation.rows.filter(
row_type=RowType.HELSINKI_BENEFIT_TOTAL_EUR
).first()

row_types_to_list = [
RowType.HELSINKI_BENEFIT_MONTHLY_EUR,
RowType.HELSINKI_BENEFIT_SUB_TOTAL_EUR,
]
calculation_rows = calculation.rows.filter(row_type__in=row_types_to_list)
return total_amount_row, calculation_rows

def _prepare_multiple_period_rows(
self,
calculation_rows: List[CalculationRow],
) -> List[BenefitPeriodRow]:
"""In the case where there are multiple benefit periods,
there will be multiple pairs of sub total rows and a corresponding monthly eur rows.
Here we prepare the calculation rows per payment period by looping through the calculation rows
and parsing the data into a list of BenefitPeriodRows.
"""
calculation_rows_for_xml = []

for idx, r in enumerate(calculation_rows):
# check that we do not go out of bounds
if 0 <= idx + 1 < len(calculation_rows):
# the dates are in the next row, which is always a sub total row
start_date = calculation_rows[idx + 1].start_date
end_date = calculation_rows[idx + 1].end_date
# get data only from rows that have start_date and end_date
if start_date and end_date:
calculation_rows_for_xml.append(
BenefitPeriodRow(
start_date=start_date,
end_date=end_date,
amount_per_month=r.amount,
# the total amount is also in the next row
total_amount=calculation_rows[idx + 1].amount,
)
)

return calculation_rows_for_xml

def _prepare_single_period_row(
self,
calculation_row_per_month: CalculationRow,
total_amount_row: CalculationRow,
) -> List[BenefitPeriodRow]:
"""In the case where there is only one benefit period,
we combine the monthly eur row and the salary benefit total row into a single BenefitPeriodRow.
"""
calculation_rows_for_xml = []
calculation_rows_for_xml.append(
BenefitPeriodRow(
start_date=total_amount_row.start_date,
end_date=total_amount_row.end_date,
amount_per_month=calculation_row_per_month.amount,
total_amount=total_amount_row.amount,
)
)
return calculation_rows_for_xml

def get_context_for_secret_xml(self) -> dict:
total_amount_row, calculation_rows = self._get_period_rows_for_xml(
self.application.calculation
)
# If there is only one period, we can combine the monthly eur row and the total row into a single row
if (
len(calculation_rows) == 1
and calculation_rows[0].row_type == RowType.HELSINKI_BENEFIT_MONTHLY_EUR
):
calculation_data_for_xml = self._prepare_single_period_row(
calculation_rows[0], total_amount_row
)
else:
calculation_data_for_xml = self._prepare_multiple_period_rows(
calculation_rows
)
return {
"application": self.application,
"benefit_type": _("Salary Benefit"),
"calculation_periods": calculation_data_for_xml,
"total_amount_row": total_amount_row,
"language": self.application.applicant_language,
}

def generate_xml_file_name(self) -> str:
date_str = self.application.created_at.strftime("%d.%m.%Y")
return (
f"Hakemus {date_str}, liite 1/1, {self.application.application_number}.xml"
)
34 changes: 23 additions & 11 deletions backend/benefit/applications/templates/secret_decision.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,47 @@
<td>{{ application.company.business_id }}</td>
</tr>
<tr>
<th scope="row">{{ _("Employee last name") }}</th>
<td>{{ application.employee.last_name }}</td>
</tr>
<tr>
<th scope="row">{{ _("Employee first name") }}</th>
<td>{{ application.employee.first_name }}</td>
<th scope="row">{{ _("Employee name") }}</th>
<td>{{ application.employee.first_name }} {{ application.employee.last_name }}</td>
</tr>
<tr>
<th scope="row">{{ _("Benefit type") }}</th>
<td>{{ benefit_type }}</td>
</tr>

<tr>
<th scope="row">{{ _("Applying for dates") }}</th>
<td>{{ total_amount_row.start_date | date:"d.m.Y" }} - {{ total_amount_row.end_date |date:"d.m.Y" }}</td>
</tr>
<tr>
<th>{{ _("Received aid") }}</th>
<td>{{ total_amount_row.amount }} €</td>
</tr>
{% if application.calculation.granted_as_de_minimis_aid %}
<tr>
<th>{{ _("Additional information") }}</th>
<td>{{ _("De Minimis description") }}</td>
</tr>
{% endif %}
</tbody>
</table>
<table>
<table class="rowbordersonly">
<thead>
<tr>
<th>{{ _("Applying for dates") }}</th>
<th>{{ _("Benefit per month") }}</th>
<th>{{ _("Total benefit") }}</th>
<th>{{ _("Total benefit") }} </th>
</tr>
</thead>
<tbody>
{% for row in calculation_rows %}
{% for row in calculation_periods %}
<tr>
<td>{{ row.start_date|date:"d.m.Y" }} - {{ row.end_date|date:"d.m.Y" }}</td>
<td>{{ row.amount }}</td>
<th>{{ application.calculation.calculated_benefit_amount }}</th>
<td class="sum">{{ row.amount_per_month }} €/kk</td>
<td class="sum">{{ row.total_amount }} €</td>
</tr>
{% endfor %}
<tr><th>{{ _("Total for benefit time period") }}</th><td class="sum"><strong>{{ total_amount_row.amount }} €</strong></td></tr>
</tbody>
</table>
</main>
Expand Down
20 changes: 19 additions & 1 deletion backend/benefit/applications/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
from django.conf import settings
from django.utils import timezone

from applications.enums import ApplicationStatus, BenefitType
from applications.enums import ApplicationStatus, BenefitType, DecisionType
from applications.models import Application
from applications.services.ahjo_decision_service import (
replace_decision_template_placeholders,
)
from applications.services.applications_csv_report import ApplicationsCsvService
from applications.tests.factories import (
AcceptedDecisionProposalFactory,
AhjoDecisionTextFactory,
ApplicationBatchFactory,
ApplicationFactory,
CancelledApplicationFactory,
Expand Down Expand Up @@ -410,6 +414,20 @@ def denied_ahjo_decision_section():
return DeniedDecisionProposalFactory()


@pytest.fixture()
def accepted_ahjo_decision_text(decided_application):
template = AcceptedDecisionProposalFactory()
replaced_decision_text = replace_decision_template_placeholders(
template.template_text, decided_application
)
return AhjoDecisionTextFactory(
decision_type=DecisionType.ACCEPTED,
application=decided_application,
decision_text=replaced_decision_text,
language=decided_application.applicant_language,
)


def split_lines_at_semicolon(csv_string):
# split CSV into lines and columns without using the csv library
csv_lines = csv_string.splitlines()
Expand Down
6 changes: 6 additions & 0 deletions backend/benefit/applications/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)
from applications.models import (
AhjoDecision,
AhjoDecisionText,
Application,
APPLICATION_LANGUAGE_CHOICES,
ApplicationBasis,
Expand All @@ -32,6 +33,11 @@
from users.tests.factories import BFHandlerUserFactory


class AhjoDecisionTextFactory(factory.django.DjangoModelFactory):
class Meta:
model = AhjoDecisionText


class AttachmentFactory(factory.django.DjangoModelFactory):
attachment_type = factory.Faker(
"random_element", elements=[v for v in AttachmentType.values]
Expand Down
Loading

0 comments on commit 733065b

Please sign in to comment.