diff --git a/backend/benefit/applications/api/v1/serializers/application.py b/backend/benefit/applications/api/v1/serializers/application.py index f24bf5e054..7a4727f258 100755 --- a/backend/benefit/applications/api/v1/serializers/application.py +++ b/backend/benefit/applications/api/v1/serializers/application.py @@ -66,7 +66,6 @@ PaySubsidySerializer, TrainingCompensationSerializer, ) -from calculator.enums import InstalmentStatus from calculator.models import Calculation from common.delay_call import call_now_or_later, do_delayed_calls_at_end from common.exceptions import BenefitAPIException @@ -92,18 +91,13 @@ from users.utils import get_company_from_request, get_request_user_from_context -def _get_pending_instalment(application, excluded_status=[]): +def _get_pending_instalment(application): """Get the latest pending instalment for the application""" try: instalments = application.calculation.instalments.filter( instalment_number__gt=1 ) - instalment = ( - instalments.exclude(status__in=excluded_status) - .order_by("-due_date") - .first() - or None - ) + instalment = instalments.filter(instalment_number=2).first() or None if instalment is not None: return InstalmentSerializer(instalment).data except AttributeError: @@ -1980,7 +1974,7 @@ class Meta: pending_instalment = serializers.SerializerMethodField("get_pending_instalment") def get_pending_instalment(self, application): - return _get_pending_instalment(application, [InstalmentStatus.COMPLETED]) + return _get_pending_instalment(application) ahjo_error = serializers.SerializerMethodField("get_latest_ahjo_error") diff --git a/backend/benefit/applications/api/v1/serializers/application_alteration.py b/backend/benefit/applications/api/v1/serializers/application_alteration.py index 891a2db31a..ab95803c12 100644 --- a/backend/benefit/applications/api/v1/serializers/application_alteration.py +++ b/backend/benefit/applications/api/v1/serializers/application_alteration.py @@ -18,8 +18,8 @@ class SimpleApplicationAlterationSerializer(DynamicFieldsModelSerializer): class Meta: model = ApplicationAlteration - fields = ["state"] - read_only_fields = ["state"] + fields = ["state", "recovery_amount"] + read_only_fields = ["state", "recovery_amount"] class BaseApplicationAlterationSerializer(DynamicFieldsModelSerializer): diff --git a/backend/benefit/applications/fixtures/test_applications.json b/backend/benefit/applications/fixtures/test_applications.json index cb85b66647..00660a4187 100644 --- a/backend/benefit/applications/fixtures/test_applications.json +++ b/backend/benefit/applications/fixtures/test_applications.json @@ -1471,8 +1471,8 @@ "paper_application_date": null, "de_minimis_aid": false, "batch": "fb9e81a4-cf6d-4f7f-abee-366c8a5bfbfc", - "ahjo_case_id": null, - "ahjo_case_guid": null, + "ahjo_case_id": "HEL 2024-234", + "ahjo_case_guid": "9c66ead0-25c4-4eba-952e-b44774c23056", "handled_by_ahjo_automation": true, "handler": "47ecedfa-351b-4815-bfac-96bdbc640178", "bases": [] @@ -1542,7 +1542,7 @@ "fields": { "created_at": "2024-11-04T10:10:12.358Z", "modified_at": "2024-11-04T10:10:12.358Z", - "status": "submitted_but_not_sent_to_ahjo", + "status": "details_received", "application": "10c25d67-f783-4625-9ff4-2418b629f20a", "error_from_ahjo": null, "ahjo_request_id": null, diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py index 7a6a92dd34..e6101f0c98 100755 --- a/backend/benefit/applications/models.py +++ b/backend/benefit/applications/models.py @@ -593,7 +593,7 @@ def calculated_effective_benefit_amount(self): if original_benefit is not None and self.alteration_set is not None: return original_benefit - sum( [ - alteration.collection_amount + alteration.recovery_amount or 0 for alteration in self.alteration_set.all() ] ) diff --git a/backend/benefit/calculator/api/v1/serializers.py b/backend/benefit/calculator/api/v1/serializers.py index 83692a3948..879facc1be 100644 --- a/backend/benefit/calculator/api/v1/serializers.py +++ b/backend/benefit/calculator/api/v1/serializers.py @@ -60,6 +60,7 @@ class Meta: "amount", "created_at", "modified_at", + "amount_after_recoveries", ] read_only_fields = [ "id", @@ -69,6 +70,7 @@ class Meta: "amount", "created_at", "modified_at", + "amount_after_recoveries", ] def validate_status(self, status): @@ -79,6 +81,13 @@ def validate_status(self, status): return status + amount_after_recoveries = serializers.SerializerMethodField( + "get_amount_after_recoveries", + ) + + def amount_after_recoveries(self, obj): + return getattr(obj, "amount_after_recoveries", None) + status = serializers.ChoiceField( validators=[InstalmentStatusValidator()], choices=InstalmentStatus.choices, diff --git a/backend/benefit/calculator/migrations/0018_instalment_amount_paid.py b/backend/benefit/calculator/migrations/0018_instalment_amount_paid.py new file mode 100644 index 0000000000..c2eb4bea58 --- /dev/null +++ b/backend/benefit/calculator/migrations/0018_instalment_amount_paid.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.11 on 2024-11-15 08:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("calculator", "0017_instalment"), + ] + + operations = [ + migrations.AddField( + model_name="instalment", + name="amount_paid", + field=models.DecimalField( + blank=True, + decimal_places=2, + editable=False, + max_digits=7, + null=True, + verbose_name="To be set only ONCE when final amount is sent to Talpa. The set value should be defined by 'amount' field that is reduced by handled ApplicationAlteration recoveries at the time of Talpa robot visit.", + ), + ), + ] diff --git a/backend/benefit/calculator/models.py b/backend/benefit/calculator/models.py index af3aca8e1b..b5698ff09d 100644 --- a/backend/benefit/calculator/models.py +++ b/backend/benefit/calculator/models.py @@ -9,6 +9,7 @@ from encrypted_fields.fields import EncryptedCharField, SearchField from simple_history.models import HistoricalRecords +from applications.enums import ApplicationAlterationState from applications.models import Application, PAY_SUBSIDY_PERCENT_CHOICES from calculator.enums import DescriptionType, InstalmentStatus, RowType from common.exceptions import BenefitAPIException @@ -854,6 +855,18 @@ class Instalment(UUIDModel, TimeStampedModel): blank=True, ) + amount_paid = models.DecimalField( + max_digits=7, + decimal_places=2, + editable=False, + verbose_name=_( + "To be set only ONCE when final amount is sent to Talpa. The set value should be defined by 'amount' " + "field that is reduced by handled ApplicationAlteration recoveries at the time of Talpa robot visit." + ), + blank=True, + null=True, + ) + due_date = models.DateField(blank=True, null=True, verbose_name=_("Due date")) status = models.CharField( @@ -864,6 +877,25 @@ class Instalment(UUIDModel, TimeStampedModel): blank=True, ) + @property + def amount_after_recoveries(self): + if self.amount_paid: + return max(self.amount_paid, 0) + if self.instalment_number == 1: + return max(self.amount, 0) + + alteration_set = self.calculation.application.alteration_set.filter( + state=ApplicationAlterationState.HANDLED, + ) + if alteration_set.count() == 0: + return max(self.amount, 0) + + return max( + self.amount + - sum([alteration.recovery_amount or 0 for alteration in alteration_set]), + 0, + ) + def __str__(self): return f"Instalment of {self.amount}€, \ number {self.instalment_number}/{self.calculation.instalments.count()} \ diff --git a/frontend/benefit/handler/public/locales/en/common.json b/frontend/benefit/handler/public/locales/en/common.json index 725eaecb63..8a8f1f3e82 100644 --- a/frontend/benefit/handler/public/locales/en/common.json +++ b/frontend/benefit/handler/public/locales/en/common.json @@ -228,7 +228,8 @@ "error_in_talpa": "Virhe maksussa" }, "calculationEndDate": "Viim. tukipäivä", - "calculatedBenefitAmount": "Tukisumma" + "calculatedBenefitAmount": "Tukisumma", + "instalmentAmount": "Maksuerä" }, "messages": { "empty": { @@ -1388,8 +1389,17 @@ "header3": "Maksuerät", "acceptedBenefit": "Myönnettävä Helsinki-lisä", "firstInstalment": "Ensimmäinen maksuerä", - "secondInstalment": "Toinen maksuerä", - "total": "Yhteensä" + "secondInstalment": "Toinen maksuerä, eräpäivä", + "total": "Yhteensä", + "alterationRow": "Muutosilmoitus {{startDate}} - {{endDate}}", + "totalRecoveries": "Takaisinlaskutettavan tuen määrä", + "unpaidRecoveries": { + "title": "Takaisinlaskutettava osuus", + "tooltip": "Huomaa, että takaisinlaskutettava osuus muuttuu, jos uusia muutosilmoituksia hyväksytään tai niitä peruutaan. Jos olet epävarma, tarkasta laskutuksen tilanne Talpalta." + }, + "totalAfterRecoveries": "Tuki takaisinlaskutuksen jälkeen", + "totalPaidSum": "Toteutuneet maksuerät yhteensä", + "totalPlannedSum": "Maksuerät yhteensä" }, "errors": { "trainingCompensation": { diff --git a/frontend/benefit/handler/public/locales/fi/common.json b/frontend/benefit/handler/public/locales/fi/common.json index 049850a98e..14d0e82466 100644 --- a/frontend/benefit/handler/public/locales/fi/common.json +++ b/frontend/benefit/handler/public/locales/fi/common.json @@ -228,7 +228,8 @@ "error_in_talpa": "Virhe maksussa" }, "calculationEndDate": "Viim. tukipäivä", - "calculatedBenefitAmount": "Tukisumma" + "calculatedBenefitAmount": "Tukisumma", + "instalmentAmount": "Maksuerä" }, "messages": { "empty": { @@ -832,7 +833,7 @@ "reportAlteration": "Tee uusi muutosilmoitus" }, "calculation": "Laskelma", - "instalments": "Maksuerät" + "instalments": "Maksetut tuet" }, "alterations": { "new": { @@ -1389,7 +1390,16 @@ "acceptedBenefit": "Myönnettävä Helsinki-lisä", "firstInstalment": "Ensimmäinen maksuerä", "secondInstalment": "Toinen maksuerä", - "total": "Yhteensä" + "total": "Yhteensä", + "alterationRow": "Muutosilmoitus {{startDate}} - {{endDate}}", + "totalRecoveries": "Takaisinlaskutettavan tuen määrä", + "unpaidRecoveries": { + "title": "Takaisinlaskutettava osuus", + "tooltip": "Huomaa, että takaisinlaskutettava osuus muuttuu, jos uusia muutosilmoituksia hyväksytään tai niitä peruutaan. Jos olet epävarma, tarkasta laskutuksen tilanne Talpalta." + }, + "totalAfterRecoveries": "Tuki takaisinlaskutuksen jälkeen", + "totalPaidSum": "Toteutuneet maksuerät yhteensä", + "totalPlannedSum": "Maksuerät yhteensä" }, "errors": { "trainingCompensation": { diff --git a/frontend/benefit/handler/public/locales/sv/common.json b/frontend/benefit/handler/public/locales/sv/common.json index 725eaecb63..c4bbf464e1 100644 --- a/frontend/benefit/handler/public/locales/sv/common.json +++ b/frontend/benefit/handler/public/locales/sv/common.json @@ -228,7 +228,8 @@ "error_in_talpa": "Virhe maksussa" }, "calculationEndDate": "Viim. tukipäivä", - "calculatedBenefitAmount": "Tukisumma" + "calculatedBenefitAmount": "Tukisumma", + "instalmentAmount": "Maksuerä" }, "messages": { "empty": { @@ -832,7 +833,7 @@ "reportAlteration": "Tee uusi muutosilmoitus" }, "calculation": "Laskelma", - "instalments": "Maksuerät" + "instalments": "Maksetut tuet" }, "alterations": { "new": { @@ -1389,7 +1390,16 @@ "acceptedBenefit": "Myönnettävä Helsinki-lisä", "firstInstalment": "Ensimmäinen maksuerä", "secondInstalment": "Toinen maksuerä", - "total": "Yhteensä" + "total": "Yhteensä", + "alterationRow": "Muutosilmoitus {{startDate}} - {{endDate}}", + "totalRecoveries": "Takaisinlaskutettavan tuen määrä", + "unpaidRecoveries": { + "title": "Takaisinlaskutettava osuus", + "tooltip": "Huomaa, että takaisinlaskutettava osuus muuttuu, jos uusia muutosilmoituksia hyväksytään tai niitä peruutaan. Jos olet epävarma, tarkasta laskutuksen tilanne Talpalta." + }, + "totalAfterRecoveries": "Tuki takaisinlaskutuksen jälkeen", + "totalPaidSum": "Toteutuneet maksuerät yhteensä", + "totalPlannedSum": "Maksuerät yhteensä" }, "errors": { "trainingCompensation": { diff --git a/frontend/benefit/handler/src/components/alterationHandling/AlterationHandlingForm.tsx b/frontend/benefit/handler/src/components/alterationHandling/AlterationHandlingForm.tsx index 945c89e4cb..1662c37610 100644 --- a/frontend/benefit/handler/src/components/alterationHandling/AlterationHandlingForm.tsx +++ b/frontend/benefit/handler/src/components/alterationHandling/AlterationHandlingForm.tsx @@ -294,7 +294,10 @@ const AlterationHandlingForm = ({ theme="coat" iconLeft={} disabled={ - isSubmitting || (isSubmitted && hasErrors) || !isCSVDownloadDone + formik.values.isRecoverable && + (isSubmitting || + (isSubmitted && hasErrors) || + !isCSVDownloadDone) } isLoading={isSubmitting} loadingText={t('common:utility.submitting')} diff --git a/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx b/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx index 30b00ae08a..e0b40f129f 100644 --- a/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx +++ b/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx @@ -7,6 +7,7 @@ import { getTagStyleForStatus } from 'benefit/handler/utils/applications'; import { APPLICATION_STATUSES } from 'benefit-shared/constants'; import { AhjoError, + ApplicationAlteration, ApplicationListItemData, Instalment, } from 'benefit-shared/types/application'; @@ -60,16 +61,33 @@ const buildApplicationUrl = ( const getFirstInstalmentTotalAmount = ( calculatedBenefitAmount: string, - pendingInstalment?: Instalment + pendingInstalment?: Instalment, + alterations?: ApplicationAlteration[] ): string | JSX.Element => { let firstInstalment = parseInt(calculatedBenefitAmount, 10); + let recoveryAmount = 0; if (pendingInstalment) { - firstInstalment -= parseInt(String(pendingInstalment?.amount), 10); + firstInstalment -= parseInt( + String(pendingInstalment?.amountAfterRecoveries), + 10 + ); + recoveryAmount = alterations + ? alterations?.reduce( + (prev: number, cur: ApplicationAlteration) => + prev + parseInt(cur.recoveryAmount, 10), + 0 + ) + : 0; } return pendingInstalment ? ( <> {formatFloatToCurrency(firstInstalment, null, 'fi-FI', 0)} /{' '} - {formatFloatToCurrency(calculatedBenefitAmount, 'EUR', 'fi-FI', 0)} + {formatFloatToCurrency( + parseInt(calculatedBenefitAmount, 10) - recoveryAmount, + 'EUR', + 'fi-FI', + 0 + )} ) : ( formatFloatToCurrency(firstInstalment, 'EUR', 'fi-FI', 0) diff --git a/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx b/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx index 93eee21b3b..0f482eb757 100644 --- a/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx +++ b/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx @@ -6,6 +6,7 @@ import { } from 'benefit/handler/types/applicationList'; import { getInstalmentTagStyleForStatus } from 'benefit/handler/utils/applications'; import { + ALTERATION_STATE, APPLICATION_STATUSES, INSTALMENT_STATUSES, } from 'benefit-shared/constants'; @@ -18,6 +19,7 @@ import { IconArrowUndo, IconCheck, IconCross, + IconErrorFill, Table, Tag, } from 'hds-react'; @@ -41,6 +43,7 @@ import { } from '../applicationReview/actions/handlingApplicationActions/HandlingApplicationActions.sc'; import { $HintText, $TableFooter } from '../table/TableExtras.sc'; import { + $AlterationBadge, $EmptyHeading, $Heading, $InstalmentList, @@ -125,7 +128,6 @@ const ApplicationListForInstalments: React.FC = ({ key: 'companyName', isSortable: true, }, - { headerName: getHeader('companyId'), key: 'companyId', @@ -136,15 +138,6 @@ const ApplicationListForInstalments: React.FC = ({ key: 'applicationNum', isSortable: true, }, - - // { - // headerName: getHeader('employeeName'), - // key: 'employeeName', - // isSortable: true, - // }, - ]; - - cols.push( { headerName: getHeader('dueDate'), key: 'dueDate', @@ -162,21 +155,58 @@ const ApplicationListForInstalments: React.FC = ({ isSortable: true, }, { - transform: ({ - calculatedBenefitAmount, - pendingInstalment, - }: ApplicationListTableTransforms) => ( - <> - {formatFloatToCurrency(pendingInstalment?.amount, null, 'fi-FI', 0)}{' '} - /{' '} - {formatFloatToCurrency(calculatedBenefitAmount, 'EUR', 'fi-FI', 0)} - - ), - headerName: getHeader('calculatedBenefitAmount'), - key: 'calculatedBenefitAmount', + transform: ({ pendingInstalment }: ApplicationListTableTransforms) => + pendingInstalment?.amountAfterRecoveries > 0 ? ( + <> + {formatFloatToCurrency( + Math.max(0, pendingInstalment?.amountAfterRecoveries), + null, + 'fi-FI', + 0 + )} + + {' / '} + {formatFloatToCurrency( + pendingInstalment.amount, + 'EUR', + 'fi-FI', + 0 + )} + + ) : ( + <$Wrapper> + <$Column> + {' '} + {formatFloatToCurrency( + pendingInstalment.amountAfterRecoveries, + 'EUR', + 'fi-FI', + 0 + )} + + + ), + headerName: getHeader('instalmentAmount'), + key: 'instalmentAmount', isSortable: true, - } - ); + }, + { + transform: ({ alterations }: ApplicationListTableTransforms) => + alterations?.length > 0 && ( + <$AlterationBadge + $requiresAttention={alterations.some(({ state }) => + [ALTERATION_STATE.RECEIVED, ALTERATION_STATE.OPENED].includes( + state as ALTERATION_STATE + ) + )} + > + {alterations.length} + + ), + headerName: '', + key: 'alterations', + }, + ]; return cols.filter(Boolean); }, [getHeader, t]); diff --git a/frontend/benefit/handler/src/components/applicationList/useApplicationListData.ts b/frontend/benefit/handler/src/components/applicationList/useApplicationListData.ts index 1796c0a696..66db8d5b93 100644 --- a/frontend/benefit/handler/src/components/applicationList/useApplicationListData.ts +++ b/frontend/benefit/handler/src/components/applicationList/useApplicationListData.ts @@ -46,6 +46,7 @@ const useApplicationListData = ( handled_at: handledAt, ahjo_error, pending_instalment, + alterations, } = application; return { @@ -77,6 +78,7 @@ const useApplicationListData = ( decisionDate: convertToUIDateFormat(batch?.decision_date) || '-', calculatedBenefitAmount: calculation?.calculated_benefit_amount || '0', pendingInstalment: camelcaseKeys(pending_instalment), + alterations: alterations || [], }; }) .filter( diff --git a/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.sc.ts b/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.sc.ts index ffc603284f..1491a75848 100644 --- a/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.sc.ts +++ b/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.sc.ts @@ -106,6 +106,12 @@ export const $CalculatorTableRow = styled.div` margin-top: ${(props) => (props.isNewSection ? props.theme.spacing.m : '0')}; `; +export const $RowWrap = styled.div` + display: flex; + align-items: center; + gap: var(--spacing-xs); +`; + export const $CalculatorTableHeader = styled.div` font-size: ${(props) => props.theme.fontSize.heading.l}; padding: ${({ theme: { spacing } }) => `${spacing.xs3} ${spacing.xs}`}; diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.tsx b/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.tsx index 65f8f4b014..ae87fab2fd 100644 --- a/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.tsx +++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.tsx @@ -13,25 +13,21 @@ import { import { CALCULATION_ROW_DESCRIPTION_TYPES, CALCULATION_ROW_TYPES, - INSTALMENT_STATUSES, } from 'benefit-shared/constants'; import { Application } from 'benefit-shared/types/application'; -import { Accordion, IconBagCogwheel, IconGlyphEuro } from 'hds-react'; -import Link from 'next/link'; +import { Accordion, IconGlyphEuro, IconMoneyBag } from 'hds-react'; import { useTranslation } from 'next-i18next'; import * as React from 'react'; import { $ViewField } from 'shared/components/benefit/summaryView/SummaryView.sc'; import { $GridCell } from 'shared/components/forms/section/FormSection.sc'; -import { convertToUIDateFormat } from 'shared/utils/date.utils'; import { formatFloatToCurrency } from 'shared/utils/string.utils'; import { useTheme } from 'styled-components'; -import { renderInstalmentTagPerStatus } from '../../applicationList/ApplicationListForInstalments'; -import { isInPayment } from '../../applicationList/HandlerIndex'; import { $CalculatorTableHeader, $CalculatorTableRow, } from '../ApplicationReview.sc'; +import InstalmentAccordionSections from './InstalmentAccordionSections'; type Props = { data: Application; @@ -48,13 +44,6 @@ const DecisionCalculationAccordion: React.FC = ({ data }) => { const sections = groupCalculatorRows(rowsWithoutTotal); const headingSize = { fontSize: theme.fontSize.heading.l }; - const secondInstalmentText = data.pendingInstalment ? ( - <> - {' '} - {t(`${translationsBase}.secondInstalment`)}{' '} - {convertToUIDateFormat(data.pendingInstalment.dueDate)} - - ) : null; return ( <> @@ -159,88 +148,28 @@ const DecisionCalculationAccordion: React.FC = ({ data }) => { - {data.pendingInstalment && isInPayment(data) && ( - <$DecisionCalculatorAccordion> - <$DecisionCalculatorAccordionIconContainer aria-hidden="true"> - - - + <$DecisionCalculatorAccordionIconContainer aria-hidden="true"> + + + + <$GridCell + $colSpan={11} + style={{ + padding: theme.spacing.m, + }} > - <$GridCell - $colSpan={11} - style={{ - padding: theme.spacing.m, - }} - > - <$CalculatorContainer> - <$Section className=""> - <$CalculatorTableRow> - <$ViewField> - {t(`${translationsBase}.firstInstalment`)} - - {formatFloatToCurrency( - data.calculatedBenefitAmount - - data.pendingInstalment.amount, - 'EUR', - 'fi-FI', - 0 - )}{' '} - - - <$Section className=""> - <$CalculatorTableRow> - <$ViewField> - {[ - INSTALMENT_STATUSES.WAITING, - INSTALMENT_STATUSES.ERROR_IN_TALPA, - INSTALMENT_STATUSES.CANCELLED, - INSTALMENT_STATUSES.ACCEPTED, - ].includes( - data.pendingInstalment.status as INSTALMENT_STATUSES - ) ? ( - {secondInstalmentText} - ) : ( - secondInstalmentText - )} - -
- {renderInstalmentTagPerStatus(t, data.pendingInstalment)} - {formatFloatToCurrency( - data.pendingInstalment.amount, - 'EUR', - 'fi-FI', - 0 - )} -
- - - <$Section className="subtotal"> - <$CalculatorTableRow> - <$ViewField isBold isBig> - {t(`${translationsBase}.total`)} - - {formatFloatToCurrency( - data.calculatedBenefitAmount, - 'EUR', - 'fi-FI', - 0 - )} - - - - -
- - )} + <$CalculatorContainer> + + + +
+ ); }; diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/InstalmentAccordionSections.tsx b/frontend/benefit/handler/src/components/applicationReview/handlingView/InstalmentAccordionSections.tsx new file mode 100644 index 0000000000..836f5069b7 --- /dev/null +++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/InstalmentAccordionSections.tsx @@ -0,0 +1,213 @@ +import { $Section } from 'benefit/handler/components/applicationReview/handlingView/DecisionCalculationAccordion.sc'; +import { + ALTERATION_STATE, + INSTALMENT_STATUSES, +} from 'benefit-shared/constants'; +import { Application } from 'benefit-shared/types/application'; +import { IconArrowRight, Tooltip } from 'hds-react'; +import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; +import * as React from 'react'; +import { $ViewField } from 'shared/components/benefit/summaryView/SummaryView.sc'; +import { convertToUIDateFormat } from 'shared/utils/date.utils'; +import { formatFloatToCurrency } from 'shared/utils/string.utils'; + +import { renderInstalmentTagPerStatus } from '../../applicationList/ApplicationListForInstalments'; +import { + $Column, + $Wrapper, +} from '../actions/handlingApplicationActions/HandlingApplicationActions.sc'; +import { $CalculatorTableRow, $RowWrap } from '../ApplicationReview.sc'; +import useInstalmentAccordionSections from './useInstalmentAccordionSections'; + +type Props = { + data: Application; +}; + +const InstalmentAccordionSections: React.FC = ({ data }) => { + const translationsBase = 'common:calculators.result'; + const { t } = useTranslation(); + + const secondInstalmentText = data.pendingInstalment ? ( + <> + {t(`${translationsBase}.secondInstalment`)}{' '} + {convertToUIDateFormat(data.pendingInstalment.dueDate)} + + ) : null; + + const { amounts, areInstalmentsPaid, isSecondInstalmentReduced } = + useInstalmentAccordionSections(data); + + return ( + <> + <$Section className=""> + <$CalculatorTableRow> + <$ViewField>{t(`${translationsBase}.firstInstalment`)} + {data.pendingInstalment && + formatFloatToCurrency(amounts.firstInstalment, 'EUR', 'fi-FI', 0)} + {!data.pendingInstalment && + formatFloatToCurrency( + data.calculatedBenefitAmount, + 'EUR', + 'fi-FI', + 0 + )} + + + + {data.pendingInstalment && ( + <$Section> + <$CalculatorTableRow> + <$ViewField> + {[ + INSTALMENT_STATUSES.WAITING, + INSTALMENT_STATUSES.ERROR_IN_TALPA, + INSTALMENT_STATUSES.CANCELLED, + INSTALMENT_STATUSES.ACCEPTED, + ].includes( + data.pendingInstalment?.status as INSTALMENT_STATUSES + ) ? ( + {secondInstalmentText} + ) : ( + secondInstalmentText + )} + + <$RowWrap> + {renderInstalmentTagPerStatus(t, data.pendingInstalment)} + + {(isSecondInstalmentReduced || !areInstalmentsPaid) && ( + <> +
+ {isSecondInstalmentReduced && + formatFloatToCurrency( + data.pendingInstalment?.amount, + 'EUR', + 'fi-FI', + 0 + )} +
+ {isSecondInstalmentReduced && } + + )} + + {formatFloatToCurrency( + amounts.secondInstalment, + 'EUR', + 'fi-FI', + 0 + )} + + + + )} + + {amounts.alterations > 0 && ( + <> +
+ + {data.alterations + .filter( + (alteration) => alteration.state === ALTERATION_STATE.HANDLED + ) + .map((alteration) => ( + <$Section className="recoverable"> + <$CalculatorTableRow> + <$ViewField> + {t(`${translationsBase}.alterationRow`, { + startDate: convertToUIDateFormat( + alteration.recoveryStartDate + ), + endDate: convertToUIDateFormat( + alteration.recoveryEndDate + ), + })} + + <$RowWrap> + {formatFloatToCurrency( + alteration.recoveryAmount, + 'EUR', + 'fi-FI', + 0 + )} + + + + ))} + + <$Section className="recoverable"> + <$CalculatorTableRow> + <$ViewField isBold> + {t(`${translationsBase}.totalRecoveries`)} + + <$RowWrap> + {formatFloatToCurrency(amounts.alterations, 'EUR', 'fi-FI', 0)} + + + + + )} +
+ + {amounts.secondInstalment - amounts.alterations < 0 && + areInstalmentsPaid && ( + <> + <$Section className=""> + <$CalculatorTableRow> + <$ViewField isBold> + {data.pendingInstalment?.status === + INSTALMENT_STATUSES.COMPLETED || !data.pendingInstalment + ? t(`${translationsBase}.totalPaidSum`) + : t(`${translationsBase}.totalPlannedSum`)} + + {formatFloatToCurrency(amounts.total, 'EUR', 'fi-FI', 0)} + + + <$Section className=""> + <$CalculatorTableRow> + <$ViewField isBold> + <$Wrapper> + <$Column> + {t(`${translationsBase}.unpaidRecoveries.title`)} + + {t(`${translationsBase}.unpaidRecoveries.tooltip`)} + + + + + {formatFloatToCurrency( + amounts.secondInstalmentMax - amounts.alterations, + 'EUR', + 'fi-FI', + 0 + )} + + +
+ + )} + + <$Section className=""> + <$CalculatorTableRow> + <$ViewField isBold> + {t(`${translationsBase}.totalAfterRecoveries`)} + + {formatFloatToCurrency( + amounts.totalAfterRecoveries, + 'EUR', + 'fi-FI', + 0 + )} + + + + ); +}; + +export default InstalmentAccordionSections; diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/useInstalmentAccordionSections.ts b/frontend/benefit/handler/src/components/applicationReview/handlingView/useInstalmentAccordionSections.ts new file mode 100644 index 0000000000..74f9ce508e --- /dev/null +++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/useInstalmentAccordionSections.ts @@ -0,0 +1,54 @@ +import { + ALTERATION_STATE, + INSTALMENT_STATUSES, +} from 'benefit-shared/constants'; +import { Application } from 'benefit-shared/types/application'; +import { formatFloatToCurrency } from 'shared/utils/string.utils'; + +type Props = { + amounts: { + firstInstalment: number; + secondInstalment: number; + secondInstalmentMax: number; + total: number; + totalAfterRecoveries: number; + alterations: number; + }; + areInstalmentsPaid: boolean; + isSecondInstalmentReduced: boolean; +}; + +const useInstalmentAccordionSections = (data: Application): Props => { + const amounts = { + firstInstalment: data.pendingInstalment + ? data.calculatedBenefitAmount - data.pendingInstalment.amount + : data.calculatedBenefitAmount, + secondInstalment: data.pendingInstalment?.amountAfterRecoveries || 0, + secondInstalmentMax: data.pendingInstalment?.amount || 0, + total: 0, + totalAfterRecoveries: 0, + alterations: + data.alterations + ?.filter((obj) => obj.state === ALTERATION_STATE.HANDLED) + .reduce((prev, cur) => prev + parseInt(cur.recoveryAmount, 10), 0) || 0, + }; + amounts.total = amounts.firstInstalment + amounts.secondInstalment; + amounts.totalAfterRecoveries = + data.calculatedBenefitAmount - amounts.alterations; + + const isSecondInstalmentReduced = + formatFloatToCurrency(amounts.secondInstalment, null, 'fi-FI', 0) !== + formatFloatToCurrency(amounts.secondInstalmentMax, null, 'fi-FI', 0); + + const areInstalmentsPaid = + data.pendingInstalment?.status === INSTALMENT_STATUSES.COMPLETED || + !data.pendingInstalment; + + return { + amounts, + areInstalmentsPaid, + isSecondInstalmentReduced, + }; +}; + +export default useInstalmentAccordionSections; diff --git a/frontend/benefit/handler/src/types/applicationList.d.ts b/frontend/benefit/handler/src/types/applicationList.d.ts index 12d5e43e7b..2d845d7748 100644 --- a/frontend/benefit/handler/src/types/applicationList.d.ts +++ b/frontend/benefit/handler/src/types/applicationList.d.ts @@ -11,6 +11,7 @@ export interface ApplicationListTableTransforms { ahjoError: AhjoError; calculatedBenefitAmount?: string; pendingInstalment?: Instalment; + alterations: ApplicationAlterationData[]; } export interface ApplicationListTableColumns { diff --git a/frontend/benefit/handler/src/utils/applications.ts b/frontend/benefit/handler/src/utils/applications.ts index 49928e06b5..661fc5a375 100644 --- a/frontend/benefit/handler/src/utils/applications.ts +++ b/frontend/benefit/handler/src/utils/applications.ts @@ -83,7 +83,7 @@ export const getInstalmentTagStyleForStatus = ( break; case INSTALMENT_STATUSES.COMPLETED: - background = theme.colors.coatOfArms; + background = theme.colors.success; text = theme.colors.white; break; diff --git a/frontend/benefit/shared/src/types/application.d.ts b/frontend/benefit/shared/src/types/application.d.ts index c9364d67be..3af487fc80 100644 --- a/frontend/benefit/shared/src/types/application.d.ts +++ b/frontend/benefit/shared/src/types/application.d.ts @@ -432,6 +432,7 @@ export type InstalmentData = { instalment_number: number; amount: number; due_date: string; + amount_after_recoveries: number; status: INSTALMENT_STATUSES; }; @@ -440,6 +441,7 @@ export type Instalment = { instalmentNumber: number; amount: number; dueDate: string; + amountAfterRecoveries: number; status: INSTALMENT_STATUSES; };