Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement the app page components for alterations for handler (HL-1247) #2982

Merged
merged 2 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions backend/benefit/applications/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,28 @@ class ApplicationAlterationInline(admin.StackedInline):
fk_name = "application"
extra = 0
fields = (
# Common state
"state",
"alteration_type",
# Applicant-provided data
"end_date",
"resume_date",
"reason",
"contact_person_name",
"recovery_start_date",
"recovery_end_date",
"recovery_amount",
"use_einvoice",
"einvoice_provider_name",
"einvoice_provider_identifier",
"einvoice_address",
# Handler-provided data
"is_recoverable",
"handled_at",
"handled_by",
"recovery_start_date",
"recovery_end_date",
"recovery_amount",
"recovery_justification",
"cancelled_at",
"cancelled_by",
)


Expand Down
61 changes: 49 additions & 12 deletions backend/benefit/applications/api/v1/application_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.conf import settings
from django.core import exceptions
from django.db import transaction
from django.db.models import Q, QuerySet
from django.db.models import Prefetch, Q, QuerySet
from django.http import FileResponse, HttpResponse, StreamingHttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
Expand Down Expand Up @@ -163,9 +163,17 @@ class Meta:
class HandlerApplicationAlterationFilter(filters.FilterSet):
class Meta:
model = ApplicationAlteration
fields = {
"state": ["exact"],
}
fields = []

state = filters.MultipleChoiceFilter(
field_name="state",
widget=CSVWidget,
choices=ApplicationAlterationState.choices,
help_text=(
"Filter by alteration state. Multiple states may be specified as a"
" comma-separated list, such as 'state=handled,cancelled'",
),
)


class BaseApplicationViewSet(AuditLoggingModelViewSet):
Expand Down Expand Up @@ -427,21 +435,40 @@ class HandlerApplicationAlterationViewSet(BaseApplicationAlterationViewSet):
filters.DjangoFilterBackend,
]

FROZEN_STATUSES = [
ApplicationAlterationState.HANDLED,
ApplicationAlterationState.CANCELLED,
]

serializer_class = HandlerApplicationAlterationSerializer
permission_classes = [BFIsHandler]
http_method_names = BaseApplicationAlterationViewSet.http_method_names + ["get"]
filterset_class = HandlerApplicationAlterationFilter

def update(self, request, *args, **kwargs):
if "state" in request.data.keys():
current_state = self.get_object().state
if (
current_state == ApplicationAlterationState.HANDLED
and current_state != request.data["state"]
):
raise PermissionDenied(_("You are not allowed to do this action"))
allowed = True
current_state = self.get_object().state

return super().update(request, *args, **kwargs)
is_simple_state_change = (
"state" in request.data.keys() and len(request.data.keys()) == 1
)
forbidden_if_handled = not (
is_simple_state_change
and request.data["state"]
in HandlerApplicationAlterationViewSet.FROZEN_STATUSES
)

# If the alteration has been handled, the only allowed edit is to cancel it.
# If the alteration has been cancelled, it cannot be modified in any way anymore.
if current_state == ApplicationAlterationState.CANCELLED or (
current_state == ApplicationAlterationState.HANDLED and forbidden_if_handled
):
allowed = False

if allowed:
return super().update(request, *args, **kwargs)
else:
raise PermissionDenied(_("You are not allowed to do this action"))


@extend_schema(
Expand All @@ -467,6 +494,16 @@ def _annotate_unread_messages_count(self, qs):

def get_queryset(self):
qs = super().get_queryset()
qs = qs.prefetch_related(
Prefetch(
"alteration_set",
queryset=ApplicationAlteration.objects.filter(
~Q(state__in=[ApplicationAlterationState.CANCELLED])
),
to_attr="visible_alterations",
)
)

if settings.NEXT_PUBLIC_MOCK_FLAG:
return qs
company = get_company_from_request(self.request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1492,7 +1492,7 @@ def get_company_for_new_application(self, _):
return self.get_logged_in_user_company()

alterations = ApplicantApplicationAlterationSerializer(
source="alteration_set",
source="visible_alterations",
read_only=True,
many=True,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from datetime import date

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
Expand All @@ -8,6 +11,7 @@
from applications.api.v1.serializers.utils import DynamicFieldsModelSerializer
from applications.enums import ApplicationAlterationState, ApplicationAlterationType
from applications.models import ApplicationAlteration
from users.api.v1.serializers import UserSerializer
from users.utils import get_company_from_request, get_request_user_from_context


Expand All @@ -25,6 +29,8 @@ class Meta:
"reason",
"handled_at",
"handled_by",
"cancelled_at",
"cancelled_by",
"recovery_start_date",
"recovery_end_date",
"recovery_amount",
Expand All @@ -45,6 +51,10 @@ class Meta:
"application_number",
"application_employee_first_name",
"application_employee_last_name",
"handled_at",
"handled_by",
"cancelled_at",
"cancelled_by",
]

ALLOWED_APPLICANT_EDIT_STATES = [
Expand All @@ -54,6 +64,7 @@ class Meta:
ALLOWED_HANDLER_EDIT_STATES = [
ApplicationAlterationState.RECEIVED,
ApplicationAlterationState.OPENED,
ApplicationAlterationState.HANDLED,
]

application_company_name = serializers.SerializerMethodField(
Expand All @@ -69,6 +80,14 @@ class Meta:
"get_application_employee_last_name"
)

handled_by = UserSerializer(
read_only=True, help_text="Handler of this alteration, if any"
)
cancelled_by = UserSerializer(
read_only=True,
help_text="The handler responsible for the cancellation of this alteration, if any",
)

def get_application_company_name(self, obj):
return obj.application.company.name

Expand Down Expand Up @@ -162,10 +181,13 @@ def _validate_date_range_within_application_date_range(

return errors

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

for alteration in application.alteration_set.all():
if self_id is not None and alteration.id == self_id:
continue

if (
alteration.recovery_start_date is None
or alteration.recovery_end_date is None
Expand All @@ -189,6 +211,7 @@ def _validate_date_range_overlaps(self, application, start_date, end_date):

def validate(self, data):
merged_data = self._get_merged_object_for_validation(data)
self_id = "id" in merged_data and merged_data["id"] or None

# Verify that the user is allowed to make the request
user = get_request_user_from_context(self)
Expand Down Expand Up @@ -234,7 +257,7 @@ def validate(self, data):

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

if len(errors) > 0:
Expand All @@ -245,17 +268,62 @@ def validate(self, data):

class ApplicantApplicationAlterationSerializer(BaseApplicationAlterationSerializer):
class Meta(BaseApplicationAlterationSerializer.Meta):
read_only_fields = BaseApplicationAlterationSerializer.Meta.read_only_fields + [
fields = [
"id",
"created_at",
"application",
"alteration_type",
"state",
"end_date",
"resume_date",
"reason",
"handled_at",
"handled_by",
"recovery_start_date",
"recovery_end_date",
"recovery_amount",
"is_recoverable",
"use_einvoice",
"einvoice_provider_name",
"einvoice_provider_identifier",
"einvoice_address",
"contact_person_name",
"application_company_name",
"application_number",
"application_employee_first_name",
"application_employee_last_name",
]
read_only_fields = BaseApplicationAlterationSerializer.Meta.read_only_fields + [
"recovery_amount",
"state",
"recovery_start_date",
"recovery_end_date",
"recovery_justification",
"is_recoverable",
"handled_by",
]


class HandlerApplicationAlterationSerializer(BaseApplicationAlterationSerializer):
pass
@transaction.atomic
def update(self, instance, validated_data):
# Add handler/canceller information on state update.
# The transition itself is validated in the viewset and is one-way
# (only received -> handled -> cancelled allowed), so we don't need to
# care about the present values in those fields.

new_state = (
validated_data["state"] if "state" in validated_data else instance.state
)
if instance.state != new_state:
user = get_request_user_from_context(self)

if new_state == ApplicationAlterationState.HANDLED:
instance.handled_at = date.today()
instance.handled_by = user
elif new_state == ApplicationAlterationState.CANCELLED:
instance.cancelled_at = date.today()
instance.cancelled_by = user

instance.save()

return super().update(instance, validated_data)
Loading
Loading