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: ui to recover from first instalment talpa error (hl-1582) #3654

Merged
merged 7 commits into from
Dec 17, 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
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
Loading