Skip to content

Commit

Permalink
feat: add alteration notice API
Browse files Browse the repository at this point in the history
  • Loading branch information
EmiliaMakelaVincit committed Mar 5, 2024
1 parent 58e8ecb commit eb2fcf7
Show file tree
Hide file tree
Showing 12 changed files with 1,119 additions and 55 deletions.
41 changes: 40 additions & 1 deletion backend/benefit/applications/api/v1/application_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@
ApplicantApplicationSerializer,
HandlerApplicationSerializer,
)
from applications.api.v1.serializers.application_alteration import (
ApplicationAlterationSerializer,
)
from applications.api.v1.serializers.attachment import AttachmentSerializer
from applications.enums import (
ApplicationBatchStatus,
ApplicationOrigin,
ApplicationStatus,
)
from applications.models import Application, ApplicationBatch
from applications.models import Application, ApplicationAlteration, ApplicationBatch
from applications.services.ahjo_integration import (
ExportFileInfo,
generate_zip,
Expand Down Expand Up @@ -363,6 +366,42 @@ def get_application_template(self, request, pk=None):
)


class ApplicationAlterationViewSet(AuditLoggingModelViewSet):
serializer_class = ApplicationAlterationSerializer
queryset = ApplicationAlteration.objects.all()
http_method_names = ["post", "patch", "head"]

APPLICANT_UNEDITABLE_FIELDS = [
"state",
"recovery_start_date",
"recovery_end_date",
"handled_at",
"recovery_amount",
]

class Meta:
model = ApplicationAlteration
fields = "__all__"
read_only_fields = [
"handled_at",
"recovery_amount",
]

def _prune_fields(self, request):
if not request.user.is_handler():
for field in self.APPLICANT_UNEDITABLE_FIELDS:
if field in request.data.keys():
request.data.pop(field)

return request

def create(self, request, *args, **kwargs):
return super().create(self._prune_fields(request), *args, **kwargs)

def update(self, request, *args, **kwargs):
return super().update(self._prune_fields(request), *args, **kwargs)


@extend_schema(
description=(
"API for create/read/update/delete operations on Helsinki benefit applications"
Expand Down
15 changes: 15 additions & 0 deletions backend/benefit/applications/api/v1/serializers/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from applications.api.v1.serializers.application_alteration import (
ApplicationAlterationSerializer,
)
from applications.api.v1.serializers.attachment import AttachmentSerializer
from applications.api.v1.serializers.batch import ApplicationBatchSerializer
from applications.api.v1.serializers.de_minimis import DeMinimisAidSerializer
Expand All @@ -37,6 +40,7 @@
AhjoStatus,
Application,
ApplicationBasis,
ApplicationBatch,
ApplicationLogEntry,
Employee,
)
Expand Down Expand Up @@ -126,6 +130,12 @@ class BaseApplicationSerializer(DynamicFieldsModelSerializer):
),
)

alterations = ApplicationAlterationSerializer(
source="alteration_set",
read_only=True,
many=True,
)

class Meta:
model = Application
fields = [
Expand Down Expand Up @@ -189,6 +199,7 @@ class Meta:
"ahjo_status",
"changes",
"archived_for_applicant",
"alterations",
]
read_only_fields = [
"submitted_at",
Expand Down Expand Up @@ -1391,6 +1402,10 @@ def to_representation(self, instance):
return True
return None

class Meta:
model = ApplicationBatch
fields = []


class ApplicantApplicationSerializer(BaseApplicationSerializer):
status = ApplicantApplicationStatusChoiceField(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import PermissionDenied

from applications.api.v1.serializers.utils import DynamicFieldsModelSerializer
from applications.enums import ApplicationAlterationState, ApplicationAlterationType
from applications.models import ApplicationAlteration
from users.utils import get_company_from_request, get_request_user_from_context


class ApplicationAlterationSerializer(DynamicFieldsModelSerializer):
class Meta:
model = ApplicationAlteration
fields = "__all__"

ALLOWED_APPLICANT_EDIT_STATES = [
ApplicationAlterationState.RECEIVED,
]

ALLOWED_HANDLER_EDIT_STATES = [
ApplicationAlterationState.RECEIVED,
ApplicationAlterationState.OPENED,
]

def _get_merged_object_for_validation(self, new_data):
def _get_field(field_name):
if field_name in new_data:
return new_data[field_name]
elif self.instance is not None and hasattr(self.instance, field_name):
return getattr(self.instance, field_name)
else:
return None

return {field: _get_field(field) for field in self.fields}

def _validate_conditional_required_fields(self, is_suspended, data):
errors = []

# Verify fields that are required based on if values in other fields are filled
if is_suspended and data["resume_date"] is None:
errors.append(
ValidationError(
_("Resume date is required if the benefit period wasn't terminated")
)
)
if data["use_alternate_einvoice_provider"] is True:
for field, field_label in [
("einvoice_provider_name", _("E-invoice provider name")),
("einvoice_provider_identifier", _("E-invoice provider identifier")),
("einvoice_address", _("E-invoice address")),
]:
if data[field] == "" or data[field] is None:
errors.append(
ValidationError(
format_lazy(
_(
"{field} must be filled if using an alternative e-invoice address"
),
field=field_label,
)
)
)

return errors

def _validate_date_range_within_application_date_range(
self, application, start_date, end_date
):
errors = []
if start_date < application.start_date:
errors.append(
ValidationError(_("Alteration cannot start before first benefit day"))
)
if start_date > application.end_date:
errors.append(
ValidationError(_("Alteration cannot start after last benefit day"))
)
if end_date is not None:
if end_date > application.end_date:
errors.append(
ValidationError(_("Alteration cannot end after last benefit day"))
)
if start_date > end_date:
errors.append(
ValidationError(
_("Alteration end date cannot be before start date")
)
)

return errors

def _validate_date_range_overlaps(self, application, start_date, end_date):
errors = []

for alteration in application.alteration_set.all():
if (
alteration.recovery_start_date is None
or alteration.recovery_end_date is None
):
continue

if start_date > alteration.recovery_end_date:
continue
if end_date < alteration.recovery_start_date:
continue

errors.append(
ValidationError(
_("Another alteration already overlaps the alteration period")
)
)

return errors

def validate(self, data):
merged_data = self._get_merged_object_for_validation(data)

# Verify that the user is allowed to make the request
user = get_request_user_from_context(self)
request = self.context.get("request")

application = merged_data["application"]

if settings.NEXT_PUBLIC_MOCK_FLAG:
if not (user and user.is_authenticated):
user = get_user_model().objects.all().order_by("username").first()

if not user.is_handler():
company = get_company_from_request(request)
if company != application.company:
raise PermissionDenied(_("You are not allowed to do this action"))

# Verify that the alteration can be edited in its current state
if self.instance is not None:
current_state = self.instance.state
allowed_states = (
self.ALLOWED_HANDLER_EDIT_STATES
if user.is_handler()
else self.ALLOWED_APPLICANT_EDIT_STATES
)

if current_state not in allowed_states:
raise PermissionDenied(
_("The alteration cannot be edited in this state")
)

# Verify that any fields that are required based on another field are filled
errors = []
is_suspended = (
merged_data["alteration_type"] == ApplicationAlterationType.SUSPENSION
)
errors += self._validate_conditional_required_fields(is_suspended, merged_data)

# Verify that the date range is coherent
alteration_start_date = merged_data["end_date"]
alteration_end_date = (
merged_data["resume_date"] if is_suspended else application.end_date
)

errors += self._validate_date_range_within_application_date_range(
application, alteration_start_date, alteration_end_date
)

if alteration_end_date is not None:
errors += self._validate_date_range_overlaps(
application, alteration_start_date, alteration_end_date
)

if len(errors) > 0:
raise ValidationError(errors)

return data
6 changes: 6 additions & 0 deletions backend/benefit/applications/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,9 @@ class DecisionType(models.TextChoices):
class ApplicationAlterationType(models.TextChoices):
TERMINATION = "termination", _("Termination")
SUSPENSION = "suspension", _("Suspension")


class ApplicationAlterationState(models.TextChoices):
RECEIVED = "received", _("Received")
OPENED = "opened", _("Opened")
HANDLED = "handled", _("Handled")
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-02-26 15:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('applications', '0059_applicationalteration'),
]

operations = [
migrations.AddField(
model_name='applicationalteration',
name='state',
field=models.TextField(choices=[('received', 'Received'), ('opened', 'Opened'), ('handled', 'Handled')], default='received', verbose_name='state of alteration'),
),
]
7 changes: 7 additions & 0 deletions backend/benefit/applications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from applications.enums import (
AhjoDecision,
AhjoStatus,
ApplicationAlterationState,
ApplicationAlterationType,
ApplicationBatchStatus,
ApplicationOrigin,
Expand Down Expand Up @@ -1058,6 +1059,12 @@ class ApplicationAlteration(TimeStampedModel):
verbose_name=_("type of alteration"), choices=ApplicationAlterationType.choices
)

state = models.TextField(
verbose_name=_("state of alteration"),
choices=ApplicationAlterationState.choices,
default=ApplicationAlterationState.RECEIVED,
)

end_date = models.DateField(verbose_name=_("new benefit end date"))

resume_date = models.DateField(
Expand Down
Loading

0 comments on commit eb2fcf7

Please sign in to comment.