uktrade/lite-api

View on GitHub
api/applications/views/applications.py

Summary

Maintainability
D
2 days
Test Coverage
A
96%
from copy import deepcopy
from uuid import UUID

from django.db import transaction
from django.db.models import F, Q
from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.timezone import now
from rest_framework import status
from rest_framework.exceptions import PermissionDenied, ValidationError, ParseError
from rest_framework.generics import (
    CreateAPIView,
    ListAPIView,
    ListCreateAPIView,
    RetrieveAPIView,
    RetrieveUpdateDestroyAPIView,
    UpdateAPIView,
)
from rest_framework.views import APIView

from api.appeals.models import Appeal
from api.appeals.serializers import AppealSerializer
from api.applications import constants
from api.applications.creators import validate_application_ready_for_submission, _validate_agree_to_declaration
from api.applications.helpers import (
    get_application_create_serializer,
    get_application_view_serializer,
    get_application_update_serializer,
    validate_and_create_goods_on_licence,
    auto_match_sanctions,
)
from api.applications.libraries.application_helpers import (
    optional_str_to_bool,
    can_status_be_set_by_gov_user,
    create_submitted_audit,
    check_user_can_set_status,
)
from api.applications.libraries.case_status_helpers import submit_application
from api.applications.libraries.edit_applications import (
    save_and_audit_have_you_been_informed_ref,
    set_case_flags_on_submitted_standard_application,
)
from api.applications.libraries.get_applications import get_application
from api.applications.libraries.goods_on_applications import add_goods_flags_to_submitted_application
from api.applications.libraries.licence import get_default_duration
from api.applications.models import (
    BaseApplication,
    SiteOnApplication,
    GoodOnApplication,
    ExternalLocationOnApplication,
    PartyOnApplication,
    StandardApplication,
)
from api.applications.notify import notify_exporter_case_opened_for_editing
from api.applications.serializers.generic_application import (
    GenericApplicationListSerializer,
    GenericApplicationCopySerializer,
)
from api.applications.serializers.standard_application import StandardApplicationRequiresSerialNumbersSerializer
from api.audit_trail import service as audit_trail_service
from api.audit_trail.enums import AuditType
from api.cases.enums import AdviceLevel, AdviceType, CaseTypeSubTypeEnum, CaseTypeEnum
from api.cases.generated_documents.models import GeneratedCaseDocument
from api.cases.generated_documents.helpers import auto_generate_case_document
from api.cases.libraries.get_flags import get_flags
from api.cases.notify import notify_exporter_appeal_acknowledgement
from api.cases.serializers import ApplicationManageSubStatusSerializer
from api.cases.celery_tasks import get_application_target_sla
from api.core.authentication import ExporterAuthentication, SharedAuthentication, GovAuthentication
from api.core.constants import ExporterPermissions, GovPermissions, AutoGeneratedDocuments
from api.core.decorators import (
    application_in_state,
    authorised_to_view_application,
    allowed_application_types,
)
from api.core.helpers import str_to_bool
from api.core.permissions import (
    assert_user_has_permission,
    IsExporterInOrganisation,
)
from api.applications.views.helpers.advice import ensure_lu_countersign_complete
from api.flags.enums import FlagStatuses, SystemFlags
from api.goods.serializers import GoodCreateSerializer
from api.goods.models import FirearmGoodDetails
from api.goodstype.models import GoodsType
from api.licences.enums import LicenceStatus
from api.licences.helpers import get_licence_reference_code, update_licence_status
from api.licences.models import Licence
from api.licences.serializers.create_licence import LicenceCreateSerializer
from lite_content.lite_api import strings
from api.organisations.libraries.get_organisation import get_request_user_organisation, get_request_user_organisation_id
from api.organisations.models import Site
from api.staticdata.statuses.enums import CaseStatusEnum
from api.staticdata.statuses.libraries.case_status_validate import is_case_status_draft
from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status
from api.staticdata.statuses.serializers import CaseSubStatusSerializer
from api.staticdata.statuses.models import CaseSubStatus
from api.users.libraries.notifications import get_case_notifications
from api.users.models import ExporterUser
from api.workflow.flagging_rules_automation import apply_flagging_rules_to_case

from lite_routing.routing_rules_internal.routing_engine import run_routing_rules
from api.cases.libraries.finalise import remove_flags_on_finalisation, remove_flags_from_audit_trail


class ApplicationList(ListCreateAPIView):
    authentication_classes = (ExporterAuthentication,)
    serializer_class = GenericApplicationListSerializer

    def get_queryset(self):
        """
        Filter applications on submitted
        """
        try:
            submitted = optional_str_to_bool(self.request.GET.get("submitted"))
        except ValueError:
            return BaseApplication.objects.none()

        organisation = get_request_user_organisation(self.request)

        if submitted is None:
            applications = BaseApplication.objects.filter(organisation=organisation)
        elif submitted:
            applications = BaseApplication.objects.submitted(organisation)
        else:
            applications = BaseApplication.objects.drafts(organisation)

        users_sites = Site.objects.get_by_user_and_organisation(self.request.user.exporteruser, organisation)
        disallowed_applications = SiteOnApplication.objects.exclude(site__id__in=users_sites).values_list(
            "application", flat=True
        )
        applications = applications.exclude(id__in=disallowed_applications)

        return applications.prefetch_related("status", "case_type").select_subclasses()

    def get_paginated_response(self, data):
        data = get_case_notifications(data, self.request)
        return super().get_paginated_response(data)

    def post(self, request, **kwargs):
        """
        Create a new application
        """
        data = request.data
        if not data.get("application_type"):
            raise ValidationError({"application_type": [strings.Applications.Generic.SELECT_AN_APPLICATION_TYPE]})
        case_type = data.pop("application_type", None)
        serializer = get_application_create_serializer(case_type)
        serializer = serializer(
            data=data,
            case_type_id=CaseTypeEnum.reference_to_id(case_type),
            context=get_request_user_organisation(request),
        )
        if serializer.is_valid(raise_exception=True):
            application = serializer.save()
            return JsonResponse(data={"id": application.id}, status=status.HTTP_201_CREATED)


class ApplicationsRequireSerialNumbersList(ListAPIView):
    authentication_classes = (ExporterAuthentication,)
    serializer_class = StandardApplicationRequiresSerialNumbersSerializer

    def get_queryset(self):
        organisation = get_request_user_organisation(self.request)

        applications = StandardApplication.objects.filter(organisation=organisation).prefetch_related(
            "goods__firearm_details"
        )
        applications = applications.filter(
            status__in=[
                get_case_status_by_status(CaseStatusEnum.SUBMITTED),
                get_case_status_by_status(CaseStatusEnum.FINALISED),
            ]
        )
        applications = applications.filter(
            Q(
                goods__firearm_details__serial_numbers_available__in=FirearmGoodDetails.SerialNumberAvailability.get_has_serial_numbers_values()
            )
            & (
                Q(goods__firearm_details__serial_numbers__len__lt=F("goods__firearm_details__number_of_items"))
                | Q(goods__firearm_details__serial_numbers__contains=[""])
            )
        )

        return applications


class ApplicationExisting(APIView):
    """
    This view returns boolean values depending on the type of organisation:
    Standard - Whether the organisation has any drafts/applications
    """

    authentication_classes = (ExporterAuthentication,)

    def get(self, request):
        organisation = get_request_user_organisation(request)
        has_applications = BaseApplication.objects.filter(organisation=organisation).exists()
        return JsonResponse(
            data={
                "applications": has_applications,
            }
        )


class ApplicationDetail(RetrieveUpdateDestroyAPIView):
    authentication_classes = (ExporterAuthentication,)

    @authorised_to_view_application(ExporterUser)
    def get(self, request, pk):
        """
        Retrieve an application instance
        """
        application = get_application(pk)
        serializer = get_application_view_serializer(application)
        data = serializer(
            application,
            context={
                "user_type": request.user.type,
                "exporter_user": request.user.exporteruser,
                "organisation_id": get_request_user_organisation_id(request),
            },
        ).data

        return JsonResponse(data=data, status=status.HTTP_200_OK)

    @authorised_to_view_application(ExporterUser)
    @application_in_state(is_editable=True)
    def put(self, request, pk):
        """
        Update an application instance
        """
        application = get_application(pk)
        update_serializer = get_application_update_serializer(application)
        case = application.get_case()
        data = request.data.copy()
        serializer = update_serializer(
            application, data=data, context=get_request_user_organisation(request), partial=True
        )

        # Prevent minor edits of the clearance level
        if not application.is_major_editable() and request.data.get("clearance_level"):
            return JsonResponse(
                data={"errors": {"clearance_level": [strings.Applications.Generic.NOT_POSSIBLE_ON_MINOR_EDIT]}},
                status=status.HTTP_400_BAD_REQUEST,
            )

        if not serializer.is_valid():
            return JsonResponse(data={"errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)

        if application.case_type.sub_type == CaseTypeSubTypeEnum.HMRC:
            serializer.save()
            return JsonResponse(data={}, status=status.HTTP_200_OK)

        # Audit block
        if request.data.get("name"):
            old_name = application.name

            serializer.save()

            audit_trail_service.create(
                actor=request.user.exporteruser,
                verb=AuditType.UPDATED_APPLICATION_NAME,
                target=case,
                payload={"old_name": old_name, "new_name": serializer.data.get("name")},
            )
            return JsonResponse(data={}, status=status.HTTP_200_OK)

        if request.data.get("clearance_level"):
            serializer.save()
            return JsonResponse(data={}, status=status.HTTP_200_OK)

        if application.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD:
            save_and_audit_have_you_been_informed_ref(request, application, serializer)
            serializer.save()

        return JsonResponse(data={}, status=status.HTTP_200_OK)

    @authorised_to_view_application(ExporterUser)
    def delete(self, request, pk):
        """
        Deleting an application should only be allowed for draft applications
        """
        application = get_application(pk)

        if not is_case_status_draft(application.status.status):
            return JsonResponse(
                data={"errors": strings.Applications.Generic.DELETE_SUBMITTED_APPLICATION_ERROR},
                status=status.HTTP_400_BAD_REQUEST,
            )
        application.delete()
        return JsonResponse(
            data={"status": strings.Applications.Generic.DELETE_DRAFT_APPLICATION}, status=status.HTTP_200_OK
        )


class ApplicationSubmission(APIView):
    authentication_classes = (ExporterAuthentication,)

    @transaction.atomic
    @application_in_state(is_major_editable=True)
    @authorised_to_view_application(ExporterUser)
    def put(self, request, pk):
        """
        Submit a draft application which will set its submitted_at datetime and status before creating a case
        Depending on the application subtype, this will also submit the declaration of the licence
        """
        application = get_application(pk)
        old_status = application.status.status

        if application.case_type.sub_type != CaseTypeSubTypeEnum.HMRC:
            assert_user_has_permission(
                request.user.exporteruser, ExporterPermissions.SUBMIT_LICENCE_APPLICATION, application.organisation
            )

        errors = validate_application_ready_for_submission(application)

        if errors:
            return JsonResponse(data={"errors": errors}, status=status.HTTP_400_BAD_REQUEST)

        # Queries are completed directly when submit is clicked on the task list
        # HMRC are completed when submit is clicked on the summary page (page after task list)
        # Applications are completed when submit is clicked on the declaration page (page after summary page)

        if application.case_type.sub_type in [CaseTypeSubTypeEnum.EUA, CaseTypeSubTypeEnum.GOODS] or (
            CaseTypeSubTypeEnum.HMRC and request.data.get("submit_hmrc")
        ):
            application.submitted_by = request.user.exporteruser
            create_submitted_audit(request, application, old_status)
            submit_application(application)
            if request.data.get("submit_hmrc"):
                auto_generate_case_document(
                    "application_form",
                    application,
                    AutoGeneratedDocuments.APPLICATION_FORM,
                    request.build_absolute_uri(),
                )

        elif application.case_type.sub_type in [
            CaseTypeSubTypeEnum.STANDARD,
            CaseTypeSubTypeEnum.OPEN,
            CaseTypeSubTypeEnum.F680,
            CaseTypeSubTypeEnum.GIFTING,
            CaseTypeSubTypeEnum.EXHIBITION,
        ]:
            if request.data.get("submit_declaration"):
                errors = _validate_agree_to_declaration(request, errors)
                if errors:
                    return JsonResponse(data={"errors": errors}, status=status.HTTP_400_BAD_REQUEST)

                # If a valid declaration is provided, save the application
                application.submitted_by = request.user.exporteruser
                application.agreed_to_foi = request.data.get("agreed_to_foi")
                application.foi_reason = request.data.get("foi_reason", "")
                submit_application(application)

                if application.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD:
                    set_case_flags_on_submitted_standard_application(application)

                add_goods_flags_to_submitted_application(application)
                apply_flagging_rules_to_case(application)
                create_submitted_audit(request, application, old_status)
                auto_generate_case_document(
                    "application_form",
                    application,
                    AutoGeneratedDocuments.APPLICATION_FORM,
                    request.build_absolute_uri(),
                )
                run_routing_rules(application)

                # Set the sites on this application as used so their name/site records located at are no longer editable
                sites_on_application = SiteOnApplication.objects.filter(application=application)
                Site.objects.filter(id__in=sites_on_application.values_list("site_id", flat=True)).update(
                    is_used_on_application=True
                )

        if application.case_type.sub_type in [
            CaseTypeSubTypeEnum.STANDARD,
            CaseTypeSubTypeEnum.OPEN,
            CaseTypeSubTypeEnum.HMRC,
        ]:
            if UUID(SystemFlags.ENFORCEMENT_CHECK_REQUIRED) not in application.flags.values_list("id", flat=True):
                application.flags.add(SystemFlags.ENFORCEMENT_CHECK_REQUIRED)

        if application.case_type.sub_type in [CaseTypeSubTypeEnum.STANDARD, CaseTypeSubTypeEnum.OPEN]:
            auto_match_sanctions(application)

        # Serialize for the response message
        serializer = get_application_view_serializer(application)
        serializer = serializer(application, context={"user_type": request.user.type})

        application_data = serializer.data

        data = {"application": {"reference_code": application.reference_code, **application_data}}

        if application.reference_code:
            data["reference_code"] = application.reference_code

        return JsonResponse(data=data, status=status.HTTP_200_OK)


class ApplicationManageStatus(APIView):
    authentication_classes = (SharedAuthentication,)

    @transaction.atomic
    def put(self, request, pk):
        application = get_application(pk)
        data = deepcopy(request.data)

        error_response = check_user_can_set_status(request, application, data)
        if error_response:
            return error_response

        update_licence_status(application, data["status"])

        case_status = get_case_status_by_status(data["status"])
        data["status"] = str(case_status.pk)
        old_status = application.status

        serializer = get_application_update_serializer(application)
        serializer = serializer(application, data=data, partial=True)

        if not serializer.is_valid():
            return JsonResponse(data={"errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)

        application = serializer.save()

        if CaseStatusEnum.is_terminal(old_status.status) and not CaseStatusEnum.is_terminal(application.status.status):
            # we reapply flagging rules if the status is reopened from a terminal state
            apply_flagging_rules_to_case(application)

        audit_trail_service.create(
            actor=request.user,
            verb=AuditType.UPDATED_STATUS,
            target=application.get_case(),
            payload={
                "status": {
                    "new": case_status.status,
                    "old": old_status.status,
                },
                "additional_text": data.get("note"),
            },
        )

        if old_status != application.status:
            run_routing_rules(case=application, keep_status=True)

            if application.status.status == CaseStatusEnum.APPLICANT_EDITING:
                notify_exporter_case_opened_for_editing(application)

        data = get_application_view_serializer(application)(application, context={"user_type": request.user.type}).data

        # Remove needed flags when case is Withdrawn/Closed
        if case_status.status in [CaseStatusEnum.WITHDRAWN, CaseStatusEnum.CLOSED]:
            remove_flags_on_finalisation(application.get_case())
            remove_flags_from_audit_trail(application.get_case())

        return JsonResponse(data={"data": data}, status=status.HTTP_200_OK)


class ApplicationManageSubStatus(UpdateAPIView):
    authentication_classes = (GovAuthentication,)
    queryset = StandardApplication.objects.all()
    serializer_class = ApplicationManageSubStatusSerializer

    def put(self, request, pk):
        case = get_application(pk).get_case()
        sub_status = request.data.get("sub_status")
        response_data = super().put(request, pk)

        if not sub_status:
            sub_status = None
        else:
            sub_status = CaseSubStatus.objects.get(id=sub_status).name
        # Update the model
        audit_trail_service.create(
            actor=request.user,
            verb=AuditType.UPDATED_SUB_STATUS,
            target=case,
            payload={"sub_status": sub_status, "status": CaseStatusEnum.get_text(case.status.status)},
        )
        return response_data


class ApplicationSubStatuses(ListAPIView):
    authentication_classes = (GovAuthentication,)
    serializer_class = CaseSubStatusSerializer
    pagination_class = None

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.application = get_object_or_404(StandardApplication, pk=self.kwargs["pk"])

    def get_queryset(self):
        return self.application.status.sub_statuses.all().order_by("order")


class ApplicationFinaliseView(APIView):
    authentication_classes = (GovAuthentication,)

    def get(self, request, pk):
        """
        Get goods to set licenced quantity for, with advice
        """

        approved_goods_on_application = (
            GoodOnApplication.objects.filter(
                application_id=pk,
                good__advice__level=AdviceLevel.FINAL,
                good__advice__type__in=[AdviceType.APPROVE, AdviceType.PROVISO, AdviceType.NO_LICENCE_REQUIRED],
                good__advice__case_id=pk,
                good__advice__good_id__isnull=False,
            )
            .annotate(
                advice_type=F("good__advice__type"),
                advice_text=F("good__advice__text"),
                advice_proviso=F("good__advice__proviso"),
            )
            .distinct()
        )

        good_on_applications_with_advice = [
            {
                "id": str(goa.id),
                "good": GoodCreateSerializer(goa.good).data,
                "unit": goa.unit,
                "quantity": goa.quantity,
                "control_list_entries": [
                    {"rating": item.rating, "text": item.text} for item in goa.control_list_entries.all()
                ],
                "is_good_controlled": goa.is_good_controlled,
                "value": goa.value,
                "advice": {
                    "type": AdviceType.as_representation(goa.advice_type),
                    "text": goa.advice_text,
                    "proviso": goa.advice_proviso,
                },
            }
            for goa in approved_goods_on_application
        ]

        return JsonResponse({"goods": good_on_applications_with_advice})

    @transaction.atomic  # noqa
    def put(self, request, pk):
        """
        Finalise an application
        """
        application = get_application(pk)
        # Check permissions
        is_mod_clearance = application.case_type.sub_type in CaseTypeSubTypeEnum.mod
        if not can_status_be_set_by_gov_user(
            request.user.govuser, application.status.status, CaseStatusEnum.FINALISED, is_mod_clearance
        ):
            return JsonResponse(
                data={"errors": [strings.Applications.Generic.Finalise.Error.SET_FINALISED]},
                status=status.HTTP_400_BAD_REQUEST,
            )

        licence_data = request.data.copy()

        action = licence_data.get("action")
        if not action:
            return JsonResponse(
                data={"errors": [strings.Applications.Finalise.Error.NO_ACTION_GIVEN]},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Check countersigning requirements and required countersignatures are present
        ensure_lu_countersign_complete(application)
        # Check if any blocking flags are on the case
        blocking_flags = (
            get_flags(application.get_case())
            .filter(status=FlagStatuses.ACTIVE, blocks_finalising=True)
            .order_by("name")
            .values_list("name", flat=True)
        )
        if blocking_flags:
            raise PermissionDenied(
                [f"{strings.Applications.Finalise.Error.BLOCKING_FLAGS}{','.join(list(blocking_flags))}"]
            )

        # Refusals & NLRs
        if action in [AdviceType.REFUSE, AdviceType.NO_LICENCE_REQUIRED]:
            return JsonResponse(data={"application": str(application.id)}, status=status.HTTP_200_OK)

        # Approvals & Provisos
        else:
            try:
                active_licence = Licence.objects.get_active_licence(application)
                default_licence_duration = active_licence.duration
            except Licence.DoesNotExist:
                default_licence_duration = get_default_duration(application)

            licence_data["duration"] = licence_data.get("duration", default_licence_duration)

            # Check change default duration permission
            if licence_data["duration"] != default_licence_duration and not request.user.govuser.has_permission(
                GovPermissions.MANAGE_LICENCE_DURATION
            ):
                raise PermissionDenied([strings.Applications.Finalise.Error.SET_DURATION_PERMISSION])

            # Validate date
            try:
                start_date = timezone.datetime(
                    year=int(licence_data["year"]), month=int(licence_data["month"]), day=int(licence_data["day"])
                )
            except (KeyError, ValueError):
                raise ParseError({"start_date": [strings.Applications.Finalise.Error.INVALID_DATE]})

            # Delete existing draft if one exists
            try:
                licence = Licence.objects.get_draft_licence(application)
            except Licence.DoesNotExist:
                licence = None

            licence_data["start_date"] = start_date.strftime("%Y-%m-%d")

            if licence:
                # Update Draft Licence object
                licence_serializer = LicenceCreateSerializer(instance=licence, data=licence_data, partial=True)
            else:
                # Create Draft Licence object
                licence_data["case"] = application.id
                licence_data["status"] = LicenceStatus.DRAFT
                licence_data["reference_code"] = get_licence_reference_code(application.reference_code)
                licence_serializer = LicenceCreateSerializer(data=licence_data)

            if not licence_serializer.is_valid():
                raise ParseError(licence_serializer.errors)

            licence = licence_serializer.save()

            # Delete draft licence document that may now be invalid
            GeneratedCaseDocument.objects.filter(
                case_id=pk, advice_type=AdviceType.APPROVE, visible_to_exporter=False
            ).delete()

            # Only validate & save GoodsOnLicence (quantities & values) for Standard applications
            if application.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD:
                errors = validate_and_create_goods_on_licence(pk, licence.id, request.data)
                if errors:
                    raise ParseError(errors)

            return JsonResponse(data=LicenceCreateSerializer(licence).data, status=status.HTTP_200_OK)


class ApplicationDurationView(APIView):
    authentication_classes = (GovAuthentication,)

    def get(self, request, pk):
        """
        Retrieve default duration for an application.
        """
        application = get_application(pk)

        duration = get_default_duration(application)

        return JsonResponse(data={"licence_duration": duration}, status=status.HTTP_200_OK)


class ApplicationCopy(APIView):
    authentication_classes = (ExporterAuthentication,)

    @transaction.atomic
    def post(self, request, pk):
        """
        Copy an application
        In this function we get the application and remove it's relation to itself on the database, which allows for us
        keep most of the data in relation to the application intact.
        """
        self.old_application_id = pk
        old_application = get_application(pk)

        data = request.data

        serializer = GenericApplicationCopySerializer(
            data=data, context={"application_type": old_application.case_type}
        )

        if not serializer.is_valid():
            return JsonResponse(data={"errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)

        # Deepcopy so new_application is not a pointer to old_application
        # (if not deepcopied, any changes done on one applies to the other)
        self.new_application = deepcopy(old_application)

        if self.new_application.case_type.sub_type == CaseTypeSubTypeEnum.F680:
            for field in constants.F680.ADDITIONAL_INFORMATION_FIELDS:
                setattr(self.new_application, field, None)

        # Clear references to parent objects, and current application instance object
        self.strip_id_for_application_copy()

        # Replace the reference and have you been informed (if required) with users answer. Also sets some defaults
        self.new_application.name = request.data["name"]
        if (
            self.new_application.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD
            and not self.new_application.case_type.id == CaseTypeEnum.SICL.id
        ):
            self.new_application.have_you_been_informed = request.data.get("have_you_been_informed")
            self.new_application.reference_number_on_information_form = request.data.get(
                "reference_number_on_information_form"
            )
        self.new_application.status = get_case_status_by_status(CaseStatusEnum.DRAFT)
        self.new_application.copy_of_id = self.old_application_id

        # Remove SLA data
        self.new_application.sla_days = 0
        self.new_application.sla_remaining_days = get_application_target_sla(self.new_application.case_type.sub_type)
        self.new_application.last_closed_at = None
        self.new_application.sla_updated_at = None

        # Remove data that should not be copied
        self.remove_data_from_application_copy()

        # Need to save here to create the pk/id for relationships
        self.new_application.save()

        # Create new foreign key connection using data from old application (this is for tables pointing to the case)
        self.create_foreign_relations_for_new_application()
        self.duplicate_goodstypes_for_new_application()

        # Get all parties connected to the application and produce a copy (and replace reference for each one)
        self.duplicate_parties_on_new_application()

        # Remove usage & licenced quantity/ value
        self.new_application.goods_type.update(usage=0)

        # Save
        self.new_application.created_at = now()
        self.new_application.save()
        return JsonResponse(data={"data": self.new_application.id}, status=status.HTTP_201_CREATED)

    def strip_id_for_application_copy(self):
        """
        The current object id and pk need removed, and the pointers otherwise save() will determine the object exists
        """
        self.new_application.pk = None
        self.new_application.id = None
        self.new_application.case_ptr = None
        self.new_application.base_application_ptr = None

    def remove_data_from_application_copy(self):
        """
        Removes data of fields that are stored on the case model, and we wish not to copy.
        """
        set_none = [
            "case_officer",
            "reference_code",
            "submitted_at",
            "licence_duration",
            "is_informed_wmd",
            "informed_wmd_ref",
            "is_suspected_wmd",
            "suspected_wmd_ref",
            "is_military_end_use_controls",
            "military_end_use_controls_ref",
            "is_eu_military",
            "is_compliant_limitations_eu",
            "compliant_limitations_eu_ref",
            "is_shipped_waybill_or_lading",
            "non_waybill_or_lading_route_details",
            "intended_end_use",
            "temp_export_details",
            "is_temp_direct_control",
            "temp_direct_control_details",
            "proposed_return_date",
        ]
        for attribute in set_none:
            setattr(self.new_application, attribute, None)

    def duplicate_parties_on_new_application(self):
        """
        Generates a copy of each party, and recreates any old application Party relations using the new copied party.
        Deleted parties are not copied over.
        """
        party_on_old_application = PartyOnApplication.objects.filter(
            application_id=self.old_application_id, deleted_at__isnull=True
        )
        for old_party_on_app in party_on_old_application:
            old_party_on_app.pk = None
            old_party_on_app.id = None

            # copy party
            old_party_id = old_party_on_app.party.id
            party = old_party_on_app.party
            party.id = None
            party.pk = None
            if not party.copy_of:
                party.copy_of_id = old_party_id
            party.created_at = now()
            party.save()

            old_party_on_app.party = party
            old_party_on_app.application = self.new_application
            old_party_on_app.created_at = now()
            old_party_on_app.save()

    def create_foreign_relations_for_new_application(self):
        """
        Recreates any connections from foreign tables existing on the current application,
         we wish to move to the new application.
        """
        # This is the super set of all many to many related objects for ALL application types.
        # The loop below caters for the possibility that any of the relationships are not relevant to the current
        #  application type
        relationships = [
            GoodOnApplication,
            SiteOnApplication,
            ExternalLocationOnApplication,
        ]

        for relation in relationships:
            old_application_relation_results = relation.objects.filter(application_id=self.old_application_id).all()

            for result in old_application_relation_results:
                result.pk = None
                result.id = None
                result.application = self.new_application
                # Some models listed above are not inheriting timestampable models,
                # as such we need to ensure created_at exists
                if getattr(result, "created_at", False):
                    result.created_at = now()
                result.save()

    def duplicate_goodstypes_for_new_application(self):
        """
        Creates a duplicate GoodsType and attaches it to the new application if applicable.
        """
        # GoodsType has more logic than in "create_foreign_relations_for_new_application",
        # such as listing the countries on the goodstype, and flags as such it is seperated.
        for good in GoodsType.objects.filter(application_id=self.old_application_id).all():
            old_good_countries = list(good.countries.all())
            old_good_flags = list(good.flags.all())
            old_good_control_list_entries = list(good.control_list_entries.all())
            good.pk = None
            good.id = None
            good.application = self.new_application
            good.created_at = now()
            good.save()
            good.countries.set(old_good_countries)
            good.flags.set(old_good_flags)
            good.control_list_entries.set(old_good_control_list_entries)


class ApplicationRouteOfGoods(UpdateAPIView):
    authentication_classes = (ExporterAuthentication,)

    @authorised_to_view_application(ExporterUser)
    @application_in_state(is_major_editable=True)
    @allowed_application_types([CaseTypeSubTypeEnum.OPEN, CaseTypeSubTypeEnum.STANDARD])
    def put(self, request, pk):
        """Update an application instance with route of goods data."""

        application = get_application(pk)
        serializer = get_application_update_serializer(application)
        case = application.get_case()
        data = request.data.copy()

        serializer = serializer(application, data=data, context=get_request_user_organisation(request), partial=True)
        if not serializer.is_valid():
            return JsonResponse(data={"errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)

        previous_answer = application.is_shipped_waybill_or_lading
        new_answer = str_to_bool(data.get("is_shipped_waybill_or_lading"))

        if previous_answer != new_answer:
            self.add_audit_entry(request, case, "is shipped waybill or lading", previous_answer, new_answer)

        if not new_answer:
            previous_details = application.non_waybill_or_lading_route_details
            new_details = data.get("non_waybill_or_lading_route_details")

            if previous_details != new_details:
                self.add_audit_entry(
                    request, case, "non_waybill_or_lading_route_details", previous_details, new_details
                )

        serializer.save()
        return JsonResponse(data={}, status=status.HTTP_200_OK)

    @staticmethod
    def add_audit_entry(request, case, field, previous_value, new_value):
        audit_trail_service.create(
            actor=request.user,
            verb=AuditType.UPDATED_ROUTE_OF_GOODS,
            target=case,
            payload={"route_of_goods_field": field, "previous_value": previous_value, "new_value": new_value},
        )


class BaseApplicationAppeal:
    authentication_classes = (ExporterAuthentication,)
    permission_classes = [
        IsExporterInOrganisation,
    ]
    serializer_class = AppealSerializer

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)

        try:
            self.application = BaseApplication.objects.get(pk=self.kwargs["pk"])
        except BaseApplication.DoesNotExist:
            raise Http404()

    def get_organisation(self):
        return self.application.organisation

    def notify_exporter_appeal_received(self):
        notify_exporter_appeal_acknowledgement(self.application)


class ApplicationAppeals(BaseApplicationAppeal, CreateAPIView):
    def perform_create(self, serializer):
        super().perform_create(serializer)
        self.application.set_appealed(
            serializer.instance,
            self.request.user.exporteruser,
        )
        self.notify_exporter_appeal_received()


class ApplicationAppeal(BaseApplicationAppeal, RetrieveAPIView):
    lookup_url_kwarg = "appeal_pk"
    queryset = Appeal.objects.all()