Skip to content

Commit

Permalink
feat: create instalments when calculating benefit (#3479)
Browse files Browse the repository at this point in the history
  • Loading branch information
rikuke authored Oct 30, 2024
1 parent d502a59 commit f9822b4
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 2 deletions.
6 changes: 6 additions & 0 deletions backend/benefit/calculator/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,12 @@ class Instalment(UUIDModel, TimeStampedModel):
blank=True,
)

def __str__(self):
return f"Instalment of {self.amount}€, \
number {self.instalment_number}/{self.calculation.instalments.count()} \
for application {self.calculation.application.application_number}, \
due in {self.due_date}, status: {self.status}."

class Meta:
db_table = "bf_calculator_instalment"
verbose_name = _("instalment")
Expand Down
47 changes: 46 additions & 1 deletion backend/benefit/calculator/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from django.conf import settings
from django.db import transaction
from django.utils import timezone

from applications.enums import ApplicationStatus, BenefitType, PaySubsidyGranted
from calculator.enums import DescriptionType, RowType
Expand All @@ -16,6 +17,7 @@
DescriptionRow,
EmployeeBenefitMonthlyRow,
EmployeeBenefitTotalRow,
Instalment,
ManualOverrideTotalRow,
PaySubsidy,
PaySubsidyMonthlyRow,
Expand All @@ -41,6 +43,7 @@ class HelsinkiBenefitCalculator:
def __init__(self, calculation: Calculation):
self.calculation = calculation
self._row_counter = 0
self.instalment_threshold = settings.INSTALMENT_THRESHOLD

def _get_change_days(
self, pay_subsidies, training_compensations, start_date, end_date
Expand Down Expand Up @@ -156,19 +159,61 @@ def can_calculate(self):
return False
return True

def create_instalments(self, total_benefit_amount: decimal.Decimal) -> None:
"""Create one instalment in the usual case,
two instalments if the benefit amount exceeds the threshold,
with the second instalment due 6 months later.
"""
instalments = self._calculate_instalment_amounts(total_benefit_amount)
self._create_instalments(instalments)

def _calculate_instalment_amounts(
self, total_benefit_amount: decimal.Decimal
) -> list[tuple[int, decimal.Decimal, datetime.datetime]]:
"""Calculate the number of instalments and their amounts based on the total benefit.
Returns a list of tuples containing (instalment_number, amount, due_date).
"""
if total_benefit_amount <= self.instalment_threshold:
return [(1, total_benefit_amount, timezone.now())]

second_instalment_amount = total_benefit_amount - self.instalment_threshold
return [
(1, self.instalment_threshold, timezone.now()),
(
2,
second_instalment_amount,
timezone.now() + datetime.timedelta(days=181),
),
]

def _create_instalments(
self, instalments: list[tuple[int, decimal.Decimal, datetime.datetime]]
) -> None:
"""Create instalment objects from the provided instalment data."""
for instalment_number, amount, due_date in instalments:
Instalment.objects.create(
calculation=self.calculation,
instalment_number=instalment_number,
amount=amount,
due_date=due_date,
)

@transaction.atomic
def calculate(self, override_status=False):
if (
self.calculation.application.status in self.CALCULATION_ALLOWED_STATUSES
or override_status
):
self.calculation.rows.all().delete()
self.calculation.instalments.all().delete()
if self.can_calculate():
self.create_rows()
# the total benefit amount is stored in Calculation model, for easier processing.
self.calculation.calculated_benefit_amount = self.get_amount(
total_benefit_amount = self.get_amount(
RowType.HELSINKI_BENEFIT_TOTAL_EUR
)
self.calculation.calculated_benefit_amount = total_benefit_amount
self.create_instalments(total_benefit_amount)
else:
self.calculation.calculated_benefit_amount = None
self.calculation.save()
Expand Down
64 changes: 63 additions & 1 deletion backend/benefit/calculator/tests/test_calculator_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import factory
import pytest
from django.utils import timezone

from applications.api.v1.serializers.application import (
ApplicantApplicationSerializer,
Expand Down Expand Up @@ -99,7 +100,10 @@ def test_application_try_retrieve_calculation_as_applicant(api_client, applicati


def test_application_create_calculation_on_submit(
request, api_client, handler_api_client, application
request,
api_client,
handler_api_client,
application,
):
# also test that calculation rows are not created yet,
# as all fields are not filled yet.
Expand Down Expand Up @@ -691,3 +695,61 @@ def test_application_calculation_rows_id_exists(
assert response.status_code == 200
assert len(response.data["calculation"]["rows"]) > 1
assert "id" in response.data["calculation"]["rows"][0].keys()


@pytest.mark.parametrize(
"number_of_instalments, has_subsidies",
[(2, False), (1, True)],
)
def test_application_calculation_instalments(
handling_application, settings, number_of_instalments, has_subsidies
):
settings.PAYMENT_INSTALMENTS_ENABLED = True
settings.INSTALMENT_THRESHOLD = 9600
settings.SALARY_BENEFIT_NEW_MAX = 1500

if has_subsidies is False:
handling_application.pay_subsidies.all().delete()
assert handling_application.pay_subsidies.count() == 0
handling_application.pay_subsidy_granted = PaySubsidyGranted.NOT_GRANTED
handling_application.save()

handling_application.calculation.start_date = datetime.now()
handling_application.calculation.end_date = (
handling_application.start_date + timedelta(weeks=52)
)
handling_application.calculation.save()

handling_application.calculation.init_calculator()
handling_application.calculation.calculate()

assert handling_application.calculation.instalments.count() == number_of_instalments
instalment_1 = handling_application.calculation.instalments.all()[0]

assert instalment_1.due_date is not None

due_date = instalment_1.due_date
now_date = timezone.now().date()

assert due_date == now_date

assert due_date == now_date

if number_of_instalments == 1:
assert (
instalment_1.amount
== handling_application.calculation.calculated_benefit_amount
)

if number_of_instalments == 2:
assert instalment_1.amount == decimal.Decimal(settings.INSTALMENT_THRESHOLD)
instalment_2 = handling_application.calculation.instalments.all()[1]
assert (
instalment_2.amount
== handling_application.calculation.calculated_benefit_amount
- decimal.Decimal(settings.INSTALMENT_THRESHOLD)
)

due_date = instalment_2.due_date
future_date = timezone.now() + timedelta(days=181)
assert due_date == future_date.date()
2 changes: 2 additions & 0 deletions backend/benefit/helsinkibenefit/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
TALPA_CALLBACK_ENABLED=(bool, False),
PAYMENT_INSTALMENTS_ENABLED=(bool, False),
SALARY_BENEFIT_NEW_MAX=(int, 1500),
INSTALMENT_THRESHOLD=(int, 9600),
)
if os.path.exists(env_file):
env.read_env(env_file)
Expand Down Expand Up @@ -568,3 +569,4 @@
TALPA_CALLBACK_ENABLED = env.bool("TALPA_CALLBACK_ENABLED")
PAYMENT_INSTALMENTS_ENABLED = env.bool("PAYMENT_INSTALMENTS_ENABLED")
SALARY_BENEFIT_NEW_MAX = env.int("SALARY_BENEFIT_NEW_MAX")
INSTALMENT_THRESHOLD = env.int("INSTALMENT_THRESHOLD")

0 comments on commit f9822b4

Please sign in to comment.