From 728af1f79200b1b524569ca80716b6be6de316d7 Mon Sep 17 00:00:00 2001 From: Sampo Tawast <5328394+sirtawast@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:19:33 +0200 Subject: [PATCH] fix: app crash on empty calculations (HL-1041) (#2688) * fix: app crash when amount is null * refactor: extract number field validation declarations to a function * fix: validate required fields in handler * feat: show manual calculation results --- .../application/step2/utils/validation.ts | 81 +++----- frontend/benefit/applicant/src/constants.ts | 1 - .../handledView/HandledView.tsx | 7 +- .../SalaryBenefitCalculatorView.tsx | 18 +- .../SalaryCalculatorResults.tsx | 178 ++++++++++-------- .../useSalaryBenefitCalculatorData.ts | 12 +- .../utils/validation.ts | 20 +- .../newApplication/utils/validation.ts | 39 ++-- .../handler/src/hooks/useCalculatorData.ts | 2 + .../handler/src/types/application.d.ts | 2 + frontend/benefit/shared/package.json | 3 +- frontend/benefit/shared/src/constants.ts | 4 +- .../benefit/shared/src/utils/validation.ts | 27 +++ 13 files changed, 214 insertions(+), 180 deletions(-) create mode 100644 frontend/benefit/shared/src/utils/validation.ts diff --git a/frontend/benefit/applicant/src/components/applications/forms/application/step2/utils/validation.ts b/frontend/benefit/applicant/src/components/applications/forms/application/step2/utils/validation.ts index 8a74390a51..6bf6da305e 100644 --- a/frontend/benefit/applicant/src/components/applications/forms/application/step2/utils/validation.ts +++ b/frontend/benefit/applicant/src/components/applications/forms/application/step2/utils/validation.ts @@ -1,12 +1,13 @@ +import { validateNumberField } from '@frontend/benefit-shared/src/utils/validation'; import { EMPLOYEE_MAX_WORKING_HOURS, EMPLOYEE_MIN_WORKING_HOURS, - MAX_MONTHLY_PAY, MAX_SHORT_STRING_LENGTH, } from 'benefit/applicant/constants'; import { APPLICATION_FIELDS_STEP2_KEYS, EMPLOYEE_KEYS, + MAX_MONTHLY_PAY, ORGANIZATION_TYPES, PAY_SUBSIDY_GRANTED, VALIDATION_MESSAGE_KEYS, @@ -17,7 +18,6 @@ import { FinnishSSN } from 'finnish-ssn'; import { TFunction } from 'next-i18next'; import { NAMES_REGEX } from 'shared/constants'; import { convertToUIDateFormat } from 'shared/utils/date.utils'; -import { getNumberValue } from 'shared/utils/string.utils'; import * as Yup from 'yup'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -89,63 +89,26 @@ export const getValidationSchema = ( [EMPLOYEE_KEYS.JOB_TITLE]: Yup.string() .nullable() .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), - [EMPLOYEE_KEYS.WORKING_HOURS]: Yup.number() - .transform((_value, originalValue) => - Number(getNumberValue(originalValue)) - ) - .typeError(t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID)) - .nullable() - .min(EMPLOYEE_MIN_WORKING_HOURS, (param) => ({ - min: param.min, - key: VALIDATION_MESSAGE_KEYS.NUMBER_MIN, - })) - .max(EMPLOYEE_MAX_WORKING_HOURS, (param) => ({ - max: param.max, - key: VALIDATION_MESSAGE_KEYS.NUMBER_MAX, - })) - .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), - [EMPLOYEE_KEYS.VACATION_MONEY]: Yup.number() - .min(0, (param) => ({ - min: param.min, - key: VALIDATION_MESSAGE_KEYS.NUMBER_MIN, - })) - .max(MAX_MONTHLY_PAY, (param) => ({ - max: param.max, - key: VALIDATION_MESSAGE_KEYS.NUMBER_MAX, - })) - .transform((_value, originalValue) => - originalValue ? getNumberValue(originalValue) : null - ) - .typeError(t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID)) - .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), - [EMPLOYEE_KEYS.MONTHLY_PAY]: Yup.number() - .min(0, (param) => ({ - min: param.min, - key: VALIDATION_MESSAGE_KEYS.NUMBER_MIN, - })) - .max(MAX_MONTHLY_PAY, (param) => ({ - max: param.max, - key: VALIDATION_MESSAGE_KEYS.NUMBER_MAX, - })) - .transform((_value, originalValue) => - originalValue ? getNumberValue(originalValue) : null - ) - .typeError(t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID)) - .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), - [EMPLOYEE_KEYS.OTHER_EXPENSES]: Yup.number() - .min(0, (param) => ({ - min: param.min, - key: VALIDATION_MESSAGE_KEYS.NUMBER_MIN, - })) - .max(MAX_MONTHLY_PAY, (param) => ({ - max: param.max, - key: VALIDATION_MESSAGE_KEYS.NUMBER_MAX, - })) - .transform((_value, originalValue) => - originalValue ? getNumberValue(originalValue) : null - ) - .typeError(t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID)) - .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), + [EMPLOYEE_KEYS.WORKING_HOURS]: validateNumberField( + EMPLOYEE_MIN_WORKING_HOURS, + EMPLOYEE_MAX_WORKING_HOURS, + { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + } + ), + [EMPLOYEE_KEYS.VACATION_MONEY]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), + [EMPLOYEE_KEYS.MONTHLY_PAY]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), + [EMPLOYEE_KEYS.OTHER_EXPENSES]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), [EMPLOYEE_KEYS.COLLECTIVE_BARGAINING_AGREEMENT]: Yup.string().required( t(VALIDATION_MESSAGE_KEYS.REQUIRED) ), diff --git a/frontend/benefit/applicant/src/constants.ts b/frontend/benefit/applicant/src/constants.ts index b025c21071..c09d8d7299 100644 --- a/frontend/benefit/applicant/src/constants.ts +++ b/frontend/benefit/applicant/src/constants.ts @@ -18,7 +18,6 @@ export enum ROUTES { } export const MAX_DEMINIMIS_AID_TOTAL_AMOUNT = 200_000; -export const MAX_MONTHLY_PAY = 99_999; export enum SUPPORTED_LANGUAGES { FI = 'fi', diff --git a/frontend/benefit/handler/src/components/applicationReview/handledView/HandledView.tsx b/frontend/benefit/handler/src/components/applicationReview/handledView/HandledView.tsx index 9e988715e0..189d7923e4 100644 --- a/frontend/benefit/handler/src/components/applicationReview/handledView/HandledView.tsx +++ b/frontend/benefit/handler/src/components/applicationReview/handledView/HandledView.tsx @@ -90,7 +90,12 @@ const HandledView: React.FC = ({ data }) => { $colSpan={2} > <$ViewFieldBold large> - {formatFloatToCurrency(totalRow.amount, 'EUR', 'fi-FI', 0)} + {formatFloatToCurrency( + totalRow?.amount || 0, + 'EUR', + 'fi-FI', + 0 + )} diff --git a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryBenefitCalculatorView.tsx b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryBenefitCalculatorView.tsx index 98bda15c19..3b19c3f16b 100644 --- a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryBenefitCalculatorView.tsx +++ b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryBenefitCalculatorView.tsx @@ -66,6 +66,7 @@ const SalaryBenefitCalculatorView: React.FC< handleSubmit, handleClear, isRecalculationRequired, + setIsRecalculationRequired, } = useCalculatorData(CALCULATION_TYPES.SALARY, formik); const eurosPerMonth = 'common:utility.eurosPerMonth'; @@ -78,13 +79,17 @@ const SalaryBenefitCalculatorView: React.FC< <$GridCell $colStart={1} $colSpan={11}> <$TabButton active={!isManualCalculator} - onClick={() => isManualCalculator && changeCalculatorMode()} + onClick={() => + changeCalculatorMode('auto') && setIsRecalculationRequired(true) + } > {t(`${translationsBase}.calculator`)} <$TabButton active={isManualCalculator} - onClick={() => !isManualCalculator && changeCalculatorMode()} + onClick={() => + changeCalculatorMode('manual') && setIsRecalculationRequired(true) + } > {t(`${translationsBase}.calculateManually`)} @@ -610,7 +615,8 @@ const SalaryBenefitCalculatorView: React.FC< {t(`${translationsBase}.calculate`)} @@ -628,7 +634,11 @@ const SalaryBenefitCalculatorView: React.FC< )} - + ); }; diff --git a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryCalculatorResults/SalaryCalculatorResults.tsx b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryCalculatorResults/SalaryCalculatorResults.tsx index 8dec0af7c2..871fcba1c7 100644 --- a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryCalculatorResults/SalaryCalculatorResults.tsx +++ b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryCalculatorResults/SalaryCalculatorResults.tsx @@ -21,97 +21,109 @@ import { const SalaryCalculatorResults: React.FC = ({ data, + isManualCalculator, + isRecalculationRequired, }) => { const theme = useTheme(); const translationsBase = 'common:calculators.result'; const { t } = useTranslation(); const { rowsWithoutTotal, totalRow, totalRowDescription } = extractCalculatorRows(data?.calculation?.rows); - if (rowsWithoutTotal.length > 0) { - return ( - <$GridCell - $colSpan={11} - style={{ - backgroundColor: 'white', - margin: `0 ${theme.spacing.xl4}`, - padding: `${theme.spacing.l} ${theme.spacing.xl4}`, - }} - > - {totalRow && totalRowDescription && ( - <> - <$CalculatorTableHeader> - {t(`${translationsBase}.header`)} - - <$Highlight> -
- {totalRowDescription.descriptionFi} -
-
- {formatFloatToCurrency(totalRow.amount, 'EUR', 'fi-FI', 0)} -
- -
- - )} - <$CalculatorTableHeader style={{ paddingBottom: theme.spacing.m }}> - {t(`${translationsBase}.header2`)} - - {rowsWithoutTotal.map((row) => { - const isDateRange = - CALCULATION_ROW_DESCRIPTION_TYPES.DATE === row.descriptionType; - const isDescriptionRowType = - CALCULATION_ROW_TYPES.DESCRIPTION === row.rowType; - const isPerMonth = CALCULATION_PER_MONTH_ROW_TYPES.includes( - row.rowType - ); - return ( -
- {CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_MONTHLY_EUR === - row.rowType && ( - <$CalculatorTableRow> - <$ViewField isBold> - {t(`${translationsBase}.acceptedBenefit`)} - - - )} - <$CalculatorTableRow - isNewSection={isDateRange} - style={{ - backgroundColor: !isDescriptionRowType - ? theme.colors.silverMediumLight - : 'transparent', - marginBottom: '7px', - }} - > - <$ViewField - isBold={isDateRange || isDescriptionRowType} - isBig={isDateRange} + + if (isRecalculationRequired) { + return null; + } + return ( + <$GridCell + $colSpan={11} + style={{ + backgroundColor: 'white', + margin: `0 ${theme.spacing.xl4}`, + padding: `${theme.spacing.l} ${theme.spacing.xl4}`, + }} + > + {totalRow && ( + <> + <$CalculatorTableHeader> + {t(`${translationsBase}.header`)} + + <$Highlight> +
+ {totalRowDescription + ? totalRowDescription.descriptionFi + : totalRow?.descriptionFi} +
+
+ {formatFloatToCurrency(totalRow.amount, 'EUR', 'fi-FI', 0)} +
+ +
+ + )} + {!isManualCalculator && ( + <> + <$CalculatorTableHeader style={{ paddingBottom: theme.spacing.m }}> + {t(`${translationsBase}.header2`)} + + {rowsWithoutTotal.map((row) => { + const isDateRange = + CALCULATION_ROW_DESCRIPTION_TYPES.DATE === row.descriptionType; + const isDescriptionRowType = + CALCULATION_ROW_TYPES.DESCRIPTION === row.rowType; + const isPerMonth = CALCULATION_PER_MONTH_ROW_TYPES.includes( + row.rowType + ); + return ( +
+ {CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_MONTHLY_EUR === + row.rowType && ( + <$CalculatorTableRow> + <$ViewField isBold> + {t(`${translationsBase}.acceptedBenefit`)} + + + )} + <$CalculatorTableRow + isNewSection={isDateRange} + style={{ + backgroundColor: !isDescriptionRowType + ? theme.colors.silverMediumLight + : 'transparent', + marginBottom: '7px', + }} > - {row.descriptionFi} - - {!isDescriptionRowType && ( - <$ViewField isBold style={{ marginRight: theme.spacing.xl4 }}> - {formatFloatToCurrency(row.amount)} - {isPerMonth && t('common:utility.perMonth')} + <$ViewField + isBold={isDateRange || isDescriptionRowType} + isBig={isDateRange} + > + {row.descriptionFi} - )} - -
- ); - })} - - - ); - } - return null; + {!isDescriptionRowType && ( + <$ViewField + isBold + style={{ marginRight: theme.spacing.xl4 }} + > + {formatFloatToCurrency(row.amount)} + {isPerMonth && t('common:utility.perMonth')} + + )} + +
+ ); + })} + + )} + + + ); }; export default SalaryCalculatorResults; diff --git a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/useSalaryBenefitCalculatorData.ts b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/useSalaryBenefitCalculatorData.ts index b997da1b26..54c9cb1f52 100644 --- a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/useSalaryBenefitCalculatorData.ts +++ b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/useSalaryBenefitCalculatorData.ts @@ -15,7 +15,6 @@ import fromPairs from 'lodash/fromPairs'; import { useTranslation } from 'next-i18next'; import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { Field } from 'shared/components/forms/fields/types'; -import useToggle from 'shared/hooks/useToggle'; import { OptionType } from 'shared/types/common'; import { convertToUIDateFormat, @@ -37,7 +36,7 @@ type ExtendedComponentProps = { getStateAidMaxPercentageSelectValue: () => OptionType | undefined; paySubsidyPercentageOptions: OptionType[]; isManualCalculator: boolean; - changeCalculatorMode: () => void; + changeCalculatorMode: (mode: 'auto' | 'manual') => true; getPaySubsidyPercentageSelectValue: ( percent: number ) => OptionType | undefined; @@ -53,7 +52,7 @@ const useSalaryBenefitCalculatorData = ( ): ExtendedComponentProps => { const { t } = useTranslation(); - const [isManualCalculator, toggleManualCalculator] = useToggle( + const [isManualCalculator, setIsManualCalculator] = useState( !!application.calculation?.overrideMonthlyBenefitAmount ); @@ -101,7 +100,7 @@ const useSalaryBenefitCalculatorData = ( ? application?.trainingCompensations : [], }, - validationSchema: getValidationSchema(), + validationSchema: getValidationSchema(t), validateOnChange: true, validateOnBlur: true, enableReinitialize: true, @@ -149,12 +148,13 @@ const useSalaryBenefitCalculatorData = ( ); }; - const changeCalculatorMode = (): void => { + const changeCalculatorMode = (mode: 'auto' | 'manual'): true => { // Backend detects manual mode if overrideMonthlyBenefitAmount is not null // so to switch to auto mode, we set empty value here if (isManualCalculator) void formik.setFieldValue(fields.overrideMonthlyBenefitAmount.name, null); - toggleManualCalculator(); + setIsManualCalculator(mode === 'manual'); + return true; }; const stateAidMaxPercentageOptions = React.useMemo( diff --git a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/utils/validation.ts b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/utils/validation.ts index df70f5f574..669e1ea804 100644 --- a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/utils/validation.ts +++ b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/utils/validation.ts @@ -1,11 +1,17 @@ +import { validateNumberField } from '@frontend/benefit-shared/src/utils/validation'; import { CALCULATION_EMPLOYMENT_KEYS, + EMPLOYEE_KEYS, + MAX_MONTHLY_PAY, VALIDATION_MESSAGE_KEYS, } from 'benefit-shared/constants'; import { CalculationCommon } from 'benefit-shared/types/application'; +import { TFunction } from 'next-i18next'; import * as Yup from 'yup'; -export const getValidationSchema = (): Yup.SchemaOf => +export const getValidationSchema = ( + t: TFunction +): Yup.SchemaOf => Yup.object().shape({ [CALCULATION_EMPLOYMENT_KEYS.START_DATE]: Yup.string() .typeError(VALIDATION_MESSAGE_KEYS.DATE_FORMAT) @@ -13,4 +19,16 @@ export const getValidationSchema = (): Yup.SchemaOf => [CALCULATION_EMPLOYMENT_KEYS.END_DATE]: Yup.string() .typeError(VALIDATION_MESSAGE_KEYS.DATE_FORMAT) .required(VALIDATION_MESSAGE_KEYS.REQUIRED), + [EMPLOYEE_KEYS.VACATION_MONEY]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), + [EMPLOYEE_KEYS.MONTHLY_PAY]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), + [EMPLOYEE_KEYS.OTHER_EXPENSES]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), }); diff --git a/frontend/benefit/handler/src/components/newApplication/utils/validation.ts b/frontend/benefit/handler/src/components/newApplication/utils/validation.ts index 8b76a7feec..015112216d 100644 --- a/frontend/benefit/handler/src/components/newApplication/utils/validation.ts +++ b/frontend/benefit/handler/src/components/newApplication/utils/validation.ts @@ -10,10 +10,12 @@ import { } from 'benefit/handler/constants'; import { EMPLOYEE_KEYS, + MAX_MONTHLY_PAY, ORGANIZATION_TYPES, PAY_SUBSIDY_GRANTED, VALIDATION_MESSAGE_KEYS, } from 'benefit-shared/constants'; +import { validateNumberField } from 'benefit-shared/utils/validation'; import startOfYear from 'date-fns/startOfYear'; import { FinnishSSN } from 'finnish-ssn'; import { TFunction } from 'next-i18next'; @@ -29,10 +31,7 @@ import { convertToUIDateFormat, validateDateIsFromCurrentYearOnwards, } from 'shared/utils/date.utils'; -import { - getNumberValue, - getNumberValueOrNull, -} from 'shared/utils/string.utils'; +import { getNumberValueOrNull } from 'shared/utils/string.utils'; import * as Yup from 'yup'; import { getValidationSchema as getDeminimisValidationSchema } from '../formContent/companySection/deMinimisAid/utils/validation'; @@ -246,7 +245,7 @@ export const getValidationSchema = ( value ? /^\d+.?\d{1,2}$/.test(String(value)) : false ) .transform((_value, originalValue) => - Number(getNumberValue(originalValue)) + Number(getNumberValueOrNull(originalValue)) ) .typeError(t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID)) .nullable() @@ -259,24 +258,18 @@ export const getValidationSchema = ( key: VALIDATION_MESSAGE_KEYS.NUMBER_MAX, })) .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), - [EMPLOYEE_KEYS.MONTHLY_PAY]: Yup.number() - .transform((_value, originalValue) => - getNumberValueOrNull(originalValue) - ) - .typeError(t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID)) - .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), - [EMPLOYEE_KEYS.VACATION_MONEY]: Yup.number() - .transform((_value, originalValue) => - getNumberValueOrNull(originalValue) - ) - .typeError(t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID)) - .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), - [EMPLOYEE_KEYS.OTHER_EXPENSES]: Yup.number() - .transform((_value, originalValue) => - getNumberValueOrNull(originalValue) - ) - .typeError(t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID)) - .required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)), + [EMPLOYEE_KEYS.MONTHLY_PAY]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), + [EMPLOYEE_KEYS.VACATION_MONEY]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), + [EMPLOYEE_KEYS.OTHER_EXPENSES]: validateNumberField(0, MAX_MONTHLY_PAY, { + required: t(VALIDATION_MESSAGE_KEYS.REQUIRED), + typeError: t(VALIDATION_MESSAGE_KEYS.NUMBER_INVALID), + }), [EMPLOYEE_KEYS.COLLECTIVE_BARGAINING_AGREEMENT]: Yup.string().required( t(VALIDATION_MESSAGE_KEYS.REQUIRED) ), diff --git a/frontend/benefit/handler/src/hooks/useCalculatorData.ts b/frontend/benefit/handler/src/hooks/useCalculatorData.ts index 9bf4c43406..8d9933fb2a 100644 --- a/frontend/benefit/handler/src/hooks/useCalculatorData.ts +++ b/frontend/benefit/handler/src/hooks/useCalculatorData.ts @@ -18,6 +18,7 @@ type ExtendedComponentProps = { handleClear: () => void; getErrorMessage: (fieldName: string) => string | undefined; isRecalculationRequired: boolean; + setIsRecalculationRequired: (value: boolean) => void; }; const useCalculatorData = ( @@ -67,6 +68,7 @@ const useCalculatorData = ( handleSubmit, handleClear, isRecalculationRequired, + setIsRecalculationRequired, }; }; diff --git a/frontend/benefit/handler/src/types/application.d.ts b/frontend/benefit/handler/src/types/application.d.ts index 682fc2e866..eb4278b65f 100644 --- a/frontend/benefit/handler/src/types/application.d.ts +++ b/frontend/benefit/handler/src/types/application.d.ts @@ -70,6 +70,8 @@ export type SubmittedApplication = { export interface ApplicationReviewViewProps { data: Application; + isManualCalculator?: boolean; + isRecalculationRequired?: boolean; isUploading?: boolean; handleUpload?: (attachment: FormData) => void; } diff --git a/frontend/benefit/shared/package.json b/frontend/benefit/shared/package.json index 2bfcf850cb..b16424701d 100644 --- a/frontend/benefit/shared/package.json +++ b/frontend/benefit/shared/package.json @@ -20,7 +20,8 @@ "ibantools": "^4.1.5", "next": "14.0.3", "react": "^18.2.0", - "styled-components": "^5.3.11" + "styled-components": "^5.3.11", + "yup": "^0.32.9" }, "devDependencies": { "eslint-config-adjunct": "^4.11.1", diff --git a/frontend/benefit/shared/src/constants.ts b/frontend/benefit/shared/src/constants.ts index 7984dccfe8..d6cd9fb9cc 100644 --- a/frontend/benefit/shared/src/constants.ts +++ b/frontend/benefit/shared/src/constants.ts @@ -53,6 +53,8 @@ export enum EMPLOYEE_KEYS { EMPLOYEE_COMMISSION_AMOUNT = 'commissionAmount', } +export const MAX_MONTHLY_PAY = 99_999; + export enum APPLICATION_FIELDS_STEP1_KEYS { USE_ALTERNATIVE_ADDRESS = 'useAlternativeAddress', ALTERNATIVE_COMPANY_STREET_ADDRESS = 'alternativeCompanyStreetAddress', @@ -200,4 +202,4 @@ export enum CALCULATION_ROW_DESCRIPTION_TYPES { DEDUCTION = 'deduction', } -export const HELSINKI_CONSENT_COOKIE_NAME = 'city-of-helsinki-cookie-consents' +export const HELSINKI_CONSENT_COOKIE_NAME = 'city-of-helsinki-cookie-consents'; diff --git a/frontend/benefit/shared/src/utils/validation.ts b/frontend/benefit/shared/src/utils/validation.ts new file mode 100644 index 0000000000..5b07e5f810 --- /dev/null +++ b/frontend/benefit/shared/src/utils/validation.ts @@ -0,0 +1,27 @@ +import { VALIDATION_MESSAGE_KEYS } from 'benefit-shared/constants'; +import { getNumberValueOrNull } from 'shared/utils/string.utils'; +import * as Yup from 'yup'; +import { RequiredNumberSchema } from 'yup/lib/number'; +import { AnyObject } from 'yup/lib/types'; + +export const validateNumberField = ( + min: number, + max: number, + texts: { + typeError: string; + required: string; + } +): RequiredNumberSchema => + Yup.number() + .transform((_value, originalValue) => getNumberValueOrNull(originalValue)) + .typeError(texts.typeError) + .nullable() + .min(min, (param) => ({ + min: param.min, + key: VALIDATION_MESSAGE_KEYS.NUMBER_MIN, + })) + .max(max, (param) => ({ + max: param.max, + key: VALIDATION_MESSAGE_KEYS.NUMBER_MAX, + })) + .required(texts.required);