Skip to content

Commit

Permalink
feat: implement the app page components for alterations for handler
Browse files Browse the repository at this point in the history
Includes the following features and related fixes:
- Added cancellation fields in the alteration database model
- Decision information box, partially shared with the applicant
  interface
- Decision calculation accordion component
- Alteration accordion component for handler with expanded details
  compared to those in the applicant view
- Alteration deletion in the "received" state (similar to applicant
  view)
- Alteration cancellation in the "handled" state
- Sorting by application number in the pending alteration list view
- Added link to the application in the pending alteration list view
- Fixed state filter for application alteration list endpoint
- Fixed alteration count bubble appearing in the navigation
  even if the number of pending alterations is zero
- Fixed cancelled alterations showing up in the applicant interface
- Fixed patching a handled alteration reporting an overlapping
  alteration with itself
  • Loading branch information
EmiliaMakelaVincit committed May 8, 2024
1 parent 1508b7c commit 8e48052
Show file tree
Hide file tree
Showing 46 changed files with 2,039 additions and 243 deletions.
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
56 changes: 45 additions & 11 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,37 @@ 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"]
allowed = True
current_state = self.get_object().state

# 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:
allowed = False
elif current_state == ApplicationAlterationState.HANDLED:
if not (
"state" in request.data.keys()
and len(request.data.keys()) == 1
and request.data["state"]
in HandlerApplicationAlterationViewSet.FROZEN_STATUSES
):
raise PermissionDenied(_("You are not allowed to do this action"))
allowed = False

return super().update(request, *args, **kwargs)
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 +491,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 @@ -1491,7 +1491,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)

0 comments on commit 8e48052

Please sign in to comment.