Skip to content

Commit

Permalink
feat: ui to recover from first instalment talpa error (hl-1582) (#3654)
Browse files Browse the repository at this point in the history
* chore: remove empty file

* refactor: rename pendingInstalment to secondInstalment

* feat: add first instalment listing to endpoint

* fix: always have applicant origin as applicant if not full clone

* feat: ui to recover from first instalment talpa failure

* feat: handle logic for both instalments when status changes

* fix: lint issue with switch case
  • Loading branch information
sirtawast authored Dec 17, 2024
1 parent 4aa300d commit 801797f
Show file tree
Hide file tree
Showing 21 changed files with 323 additions and 110 deletions.
Empty file.
44 changes: 29 additions & 15 deletions backend/benefit/applications/api/v1/serializers/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,15 @@
from users.utils import get_company_from_request, get_request_user_from_context


def _get_pending_instalment(application):
"""Get the latest pending instalment for the application"""
def _get_instalment(application, instalment_number):
"""Get the second instalment for the application"""
try:
instalments = application.calculation.instalments.filter(
instalment_number__gt=1
instalment = (
application.calculation.instalments.filter(
instalment_number=instalment_number
).first()
or None
)
instalment = instalments.filter(instalment_number=2).first() or None
if instalment is not None:
return InstalmentSerializer(instalment).data
except AttributeError:
Expand Down Expand Up @@ -1638,10 +1640,14 @@ class HandlerApplicationSerializer(BaseApplicationSerializer):

ahjo_error = serializers.SerializerMethodField()

pending_instalment = serializers.SerializerMethodField("get_pending_instalment")
first_instalment = serializers.SerializerMethodField("get_first_instalment")
second_instalment = serializers.SerializerMethodField("get_second_instalment")

def get_pending_instalment(self, application):
return _get_pending_instalment(application)
def get_first_instalment(self, application):
return _get_instalment(application, 1)

def get_second_instalment(self, application):
return _get_instalment(application, 2)

def get_latest_ahjo_error(self, obj) -> Union[Dict, None]:
"""Get the latest Ahjo error for the application"""
Expand Down Expand Up @@ -1703,13 +1709,15 @@ class Meta(BaseApplicationSerializer.Meta):
"handler",
"handled_by_ahjo_automation",
"ahjo_error",
"pending_instalment",
"first_instalment",
"second_instalment",
]
read_only_fields = BaseApplicationSerializer.Meta.read_only_fields + [
"latest_decision_comment",
"handled_at",
"handler",
"pending_instalment",
"first_instalment",
"second_instalment",
]

@transaction.atomic
Expand Down Expand Up @@ -1912,7 +1920,8 @@ class Meta:
"batch",
"ahjo_error",
"talpa_status",
"pending_instalment",
"first_instalment",
"second_instalment",
]

read_only_fields = [
Expand All @@ -1937,7 +1946,8 @@ class Meta:
"batch",
"ahjo_error",
"talpa_status",
"pending_instalment",
"first_instalment",
"second_instalment",
]

archived = serializers.BooleanField()
Expand All @@ -1958,10 +1968,14 @@ class Meta:
"Timestamp when the application was handled (accepted/rejected/cancelled)"
),
)
pending_instalment = serializers.SerializerMethodField("get_pending_instalment")
first_instalment = serializers.SerializerMethodField("get_first_instalment")
second_instalment = serializers.SerializerMethodField("get_second_instalment")

def get_first_instalment(self, application):
return _get_instalment(application, 1)

def get_pending_instalment(self, application):
return _get_pending_instalment(application)
def get_second_instalment(self, application):
return _get_instalment(application, 2)

ahjo_error = serializers.SerializerMethodField("get_latest_ahjo_error")

Expand Down
6 changes: 5 additions & 1 deletion backend/benefit/applications/services/clone_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ def clone_application_based_on_other(
"alternative_company_postcode": application_base.alternative_company_postcode,
"alternative_company_street_address": application_base.alternative_company_street_address,
"applicant_language": "fi",
"application_origin": ApplicationOrigin.APPLICANT,
"application_origin": (
application_base.application_origin
if clone_all_data
else ApplicationOrigin.APPLICANT
),
"application_step": ApplicationStep.STEP_1,
"archived": False,
"association_has_business_activities": application_base.association_has_business_activities
Expand Down
45 changes: 40 additions & 5 deletions backend/benefit/calculator/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from applications.enums import ApplicationTalpaStatus
from calculator.api.v1.serializers import (
InstalmentSerializer,
PreviousBenefitSerializer,
Expand Down Expand Up @@ -53,9 +54,43 @@ def patch(self, request, instalment_id):
)
if serializer.is_valid():
serializer.save()
if instalment_status == InstalmentStatus.CANCELLED:
application = instalment.calculation.application
application.archived = True
application.save()
return Response(serializer.data, status=status.HTTP_200_OK)
application = instalment.calculation.application
instalment_count = instalment.calculation.instalments.count()

if instalment.instalment_number == 1:
if instalment_status == InstalmentStatus.WAITING:
application.talpa_status = (
ApplicationTalpaStatus.NOT_PROCESSED_BY_TALPA
)
application.save()

if instalment_status == InstalmentStatus.PAID:
if instalment.instalment_number == 1:
application.talpa_status = (
ApplicationTalpaStatus.SUCCESSFULLY_SENT_TO_TALPA
)
else:
application.talpa_status = (
ApplicationTalpaStatus.PARTIALLY_SENT_TO_TALPA
)
instalment.amount_paid = instalment.amount_after_recoveries

if instalment_count == 1:
application.archived = True

instalment.save()
application.save()
return Response(serializer.data, status=status.HTTP_200_OK)
if instalment.instalment_number == 2:
first_instalment = instalment.calculation.instalments.get(
instalment_number=1
)
if (
instalment_status == InstalmentStatus.CANCELLED
and first_instalment.amount_paid is not None
):
application.archived = True
application.save()
return Response(serializer.data, status=status.HTTP_200_OK)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
4 changes: 4 additions & 0 deletions frontend/benefit/handler/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,10 @@
"noChanges": "Ei muutoksia lomaketietoihin. Mikäli poistit tai lisäsit liitetiedostoja, voit kirjata niiden syyn."
},
"helperText": "Kirjaa milloin ja mitä kautta hakija on ollut yhteydessä. Tiedot tulevat näkyviin hakemuksen muutoshistoriassa."
},
"talpaStatusChange": {
"heading": "Maksun tilan muutos",
"text": "Tuen automaattinen maksu on epäonnistunut. Palauta hakemus odottamaan automaattista Talpa-käsittelyä tai merkitse maksu suoritetuksi jolloin maksutapahtuma on vahvistettava manuaalisesti Talpalta."
}
},
"decision": {
Expand Down
4 changes: 4 additions & 0 deletions frontend/benefit/handler/public/locales/fi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,10 @@
"noChanges": "Ei muutoksia lomaketietoihin. Mikäli poistit tai lisäsit liitetiedostoja, voit kirjata niiden syyn."
},
"helperText": "Kirjaa milloin ja mitä kautta hakija on ollut yhteydessä. Tiedot tulevat näkyviin hakemuksen muutoshistoriassa."
},
"talpaStatusChange": {
"heading": "Maksun tilan muutos",
"text": "Tuen automaattinen maksu on epäonnistunut. Palauta hakemus odottamaan automaattista Talpa-käsittelyä tai merkitse maksu suoritetuksi jolloin maksutapahtuma on vahvistettava manuaalisesti Talpalta."
}
},
"decision": {
Expand Down
4 changes: 4 additions & 0 deletions frontend/benefit/handler/public/locales/sv/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,10 @@
"noChanges": "Ei muutoksia lomaketietoihin. Mikäli poistit tai lisäsit liitetiedostoja, voit kirjata niiden syyn."
},
"helperText": "Kirjaa milloin ja mitä kautta hakija on ollut yhteydessä. Tiedot tulevat näkyviin hakemuksen muutoshistoriassa."
},
"talpaStatusChange": {
"heading": "Maksun tilan muutos",
"text": "Tuen automaattinen maksu on epäonnistunut. Palauta hakemus odottamaan automaattista Talpa-käsittelyä tai merkitse maksu suoritetuksi jolloin maksutapahtuma on vahvistettava manuaalisesti Talpalta."
}
},
"decision": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ALL_APPLICATION_STATUSES, ROUTES } from 'benefit/handler/constants';
import useInstalmentStatusTransition from 'benefit/handler/hooks/useInstalmentStatusTransition';
import {
ApplicationListTableColumns,
ApplicationListTableTransforms,
Expand All @@ -7,7 +8,11 @@ import {
getTagStyleForStatus,
getTalpaTagStyleForStatus,
} from 'benefit/handler/utils/applications';
import { APPLICATION_STATUSES, TALPA_STATUSES } from 'benefit-shared/constants';
import {
APPLICATION_STATUSES,
INSTALMENT_STATUSES,
TALPA_STATUSES,
} from 'benefit-shared/constants';
import {
AhjoError,
ApplicationAlteration,
Expand Down Expand Up @@ -37,6 +42,7 @@ import {
$TagWrapper,
$UnreadMessagesCount,
} from './ApplicationList.sc';
import TalpaStatusChangeModal from './TalpaStatusChangeDialog';
import { useApplicationList } from './useApplicationList';

export interface ApplicationListProps {
Expand Down Expand Up @@ -65,14 +71,14 @@ const buildApplicationUrl = (

const getFirstInstalmentTotalAmount = (
calculatedBenefitAmount: string,
pendingInstalment?: Instalment,
secondInstalment?: Instalment,
alterations?: ApplicationAlteration[]
): string | JSX.Element => {
let firstInstalment = parseInt(calculatedBenefitAmount, 10);
let recoveryAmount = 0;
if (pendingInstalment) {
if (secondInstalment) {
firstInstalment -= parseInt(
String(pendingInstalment?.amountAfterRecoveries),
String(secondInstalment?.amountAfterRecoveries),
10
);
recoveryAmount = alterations
Expand All @@ -83,7 +89,7 @@ const getFirstInstalmentTotalAmount = (
)
: 0;
}
return pendingInstalment ? (
return secondInstalment ? (
<>
{formatFloatToEvenEuros(firstInstalment)} /{' '}
{formatFloatToEvenEuros(
Expand All @@ -100,10 +106,18 @@ const dateForAdditionalInformationNeededBy = (

export const renderPaymentTagPerStatus = (
t: TFunction,
talpaStatus?: TALPA_STATUSES
talpaStatus?: TALPA_STATUSES,
id?: string,
clickTalpaTag?: (id: string, talpaStatus: TALPA_STATUSES) => void
): JSX.Element => (
<$TagWrapper $colors={getTalpaTagStyleForStatus(talpaStatus)}>
<Tag>
<Tag
onClick={
talpaStatus === TALPA_STATUSES.REJECTED_BY_TALPA && id
? () => clickTalpaTag(id, talpaStatus)
: null
}
>
{t(`applications.list.columns.talpaStatuses.${String(talpaStatus)}`)}
</Tag>
</$TagWrapper>
Expand Down Expand Up @@ -150,6 +164,20 @@ const ApplicationList: React.FC<ApplicationListProps> = ({
[isAllStatuses, status]
);

const [showTalpaModal, setShowTaplaModal] = React.useState(false);
const [selectedApplication, setSelectedApplication] = React.useState('');

const {
mutate: changeInstalmentStatus,
isSuccess: isInstalmentStatusChanged,
} = useInstalmentStatusTransition();

React.useEffect(() => {
if (isInstalmentStatusChanged) {
setShowTaplaModal(false);
}
}, [isInstalmentStatusChanged]);

const renderTableActions = React.useCallback(
(
id: string,
Expand Down Expand Up @@ -219,10 +247,6 @@ const ApplicationList: React.FC<ApplicationListProps> = ({
[t]
);

const renderPaymentTagWrapper = React.useCallback(renderPaymentTagPerStatus, [
t,
]);

const columns = React.useMemo(() => {
const cols: ApplicationListTableColumns[] = [
{
Expand Down Expand Up @@ -366,6 +390,10 @@ const ApplicationList: React.FC<ApplicationListProps> = ({
isSortable: true,
});
}
const onTalpaTagClick = (id: string): void => {
setShowTaplaModal(true);
setSelectedApplication(id);
};

if (inPayment) {
cols.push(
Expand All @@ -379,19 +407,27 @@ const ApplicationList: React.FC<ApplicationListProps> = ({
headerName: getHeader('paymentStatus'),
key: 'paymentStatus',
isSortable: true,
transform: ({ talpaStatus }: ApplicationListTableTransforms) =>
renderPaymentTagWrapper(t, talpaStatus as TALPA_STATUSES),
transform: ({
talpaStatus,
firstInstalment,
}: ApplicationListTableTransforms) =>
renderPaymentTagPerStatus(
t,
talpaStatus as TALPA_STATUSES,
firstInstalment?.id,
onTalpaTagClick
),
},
{
headerName: getHeader('calculatedBenefitAmount'),
key: 'calculatedBenefitAmount',
transform: ({
calculatedBenefitAmount,
pendingInstalment,
secondInstalment,
}: ApplicationListTableTransforms) =>
getFirstInstalmentTotalAmount(
String(calculatedBenefitAmount),
pendingInstalment || null
secondInstalment || null
),
}
);
Expand All @@ -412,7 +448,6 @@ const ApplicationList: React.FC<ApplicationListProps> = ({
renderTagWrapper,
renderTableActions,
t,
renderPaymentTagWrapper,
]);

if (isLoading) {
Expand All @@ -434,6 +469,10 @@ const ApplicationList: React.FC<ApplicationListProps> = ({
}

const statusAsString = isAllStatuses ? 'all' : status.join(',');
const handleTalpaStatusChange = (talpaStatus: INSTALMENT_STATUSES): void => {
changeInstalmentStatus({ id: selectedApplication, status: talpaStatus });
};

return (
<$ApplicationList data-testid={`application-list-${statusAsString}`}>
{list.length > 0 ? (
Expand All @@ -450,6 +489,12 @@ const ApplicationList: React.FC<ApplicationListProps> = ({
{t(`${translationsBase}.messages.empty.${statusAsString}`)}
</$EmptyHeading>
)}

<TalpaStatusChangeModal
isOpen={showTalpaModal}
onClose={() => setShowTaplaModal(false)}
onStatusChange={handleTalpaStatusChange}
/>
</$ApplicationList>
);
};
Expand Down
Loading

0 comments on commit 801797f

Please sign in to comment.