Skip to content

Commit

Permalink
fix: app crash on empty calculations (HL-1041) (#2688)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sirtawast authored Jan 9, 2024
1 parent 96224fd commit 728af1f
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 180 deletions.
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
),
Expand Down
1 change: 0 additions & 1 deletion frontend/benefit/applicant/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ const HandledView: React.FC<ApplicationReviewViewProps> = ({ data }) => {
$colSpan={2}
>
<$ViewFieldBold large>
{formatFloatToCurrency(totalRow.amount, 'EUR', 'fi-FI', 0)}
{formatFloatToCurrency(
totalRow?.amount || 0,
'EUR',
'fi-FI',
0
)}
</$ViewFieldBold>
</$GridCell>
</$HandledRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const SalaryBenefitCalculatorView: React.FC<
handleSubmit,
handleClear,
isRecalculationRequired,
setIsRecalculationRequired,
} = useCalculatorData(CALCULATION_TYPES.SALARY, formik);

const eurosPerMonth = 'common:utility.eurosPerMonth';
Expand All @@ -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>
<$TabButton
active={isManualCalculator}
onClick={() => !isManualCalculator && changeCalculatorMode()}
onClick={() =>
changeCalculatorMode('manual') && setIsRecalculationRequired(true)
}
>
{t(`${translationsBase}.calculateManually`)}
</$TabButton>
Expand Down Expand Up @@ -610,7 +615,8 @@ const SalaryBenefitCalculatorView: React.FC<
{t(`${translationsBase}.calculate`)}
</Button>
<Button onClick={handleClear} theme="coat" variant="secondary">
{t(`${translationsBase}.clear`)}
{isManualCalculator && t(`${translationsBase}.clear`)}
{!isManualCalculator && t(`${translationsBase}.reset`)}
</Button>
</$GridCell>

Expand All @@ -628,7 +634,11 @@ const SalaryBenefitCalculatorView: React.FC<
</$Notification>
</$GridCell>
)}
<SalaryCalculatorResults data={data} />
<SalaryCalculatorResults
data={data}
isManualCalculator={isManualCalculator}
isRecalculationRequired={isRecalculationRequired}
/>
</ReviewSection>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,97 +21,109 @@ import {

const SalaryCalculatorResults: React.FC<ApplicationReviewViewProps> = ({
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`)}
</$CalculatorTableHeader>
<$Highlight>
<div style={{ fontSize: theme.fontSize.body.xl }}>
{totalRowDescription.descriptionFi}
</div>
<div style={{ fontSize: theme.fontSize.heading.xl }}>
{formatFloatToCurrency(totalRow.amount, 'EUR', 'fi-FI', 0)}
</div>
</$Highlight>
<hr style={{ margin: theme.spacing.s }} />
</>
)}
<$CalculatorTableHeader style={{ paddingBottom: theme.spacing.m }}>
{t(`${translationsBase}.header2`)}
</$CalculatorTableHeader>
{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 (
<div key={row.id}>
{CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_MONTHLY_EUR ===
row.rowType && (
<$CalculatorTableRow>
<$ViewField isBold>
{t(`${translationsBase}.acceptedBenefit`)}
</$ViewField>
</$CalculatorTableRow>
)}
<$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`)}
</$CalculatorTableHeader>
<$Highlight>
<div style={{ fontSize: theme.fontSize.body.xl }}>
{totalRowDescription
? totalRowDescription.descriptionFi
: totalRow?.descriptionFi}
</div>
<div style={{ fontSize: theme.fontSize.heading.xl }}>
{formatFloatToCurrency(totalRow.amount, 'EUR', 'fi-FI', 0)}
</div>
</$Highlight>
<hr style={{ margin: theme.spacing.s }} />
</>
)}
{!isManualCalculator && (
<>
<$CalculatorTableHeader style={{ paddingBottom: theme.spacing.m }}>
{t(`${translationsBase}.header2`)}
</$CalculatorTableHeader>
{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 (
<div key={row.id}>
{CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_MONTHLY_EUR ===
row.rowType && (
<$CalculatorTableRow>
<$ViewField isBold>
{t(`${translationsBase}.acceptedBenefit`)}
</$ViewField>
</$CalculatorTableRow>
)}
<$CalculatorTableRow
isNewSection={isDateRange}
style={{
backgroundColor: !isDescriptionRowType
? theme.colors.silverMediumLight
: 'transparent',
marginBottom: '7px',
}}
>
{row.descriptionFi}
</$ViewField>
{!isDescriptionRowType && (
<$ViewField isBold style={{ marginRight: theme.spacing.xl4 }}>
{formatFloatToCurrency(row.amount)}
{isPerMonth && t('common:utility.perMonth')}
<$ViewField
isBold={isDateRange || isDescriptionRowType}
isBig={isDateRange}
>
{row.descriptionFi}
</$ViewField>
)}
</$CalculatorTableRow>
</div>
);
})}
<Koros
dense
type="pulse"
style={{
fill: theme.colors.coatOfArmsLight,
margin: `${theme.spacing.l} 0 -65px -65px`,
width: 'calc(100% + 130px)',
}}
/>
</$GridCell>
);
}
return null;
{!isDescriptionRowType && (
<$ViewField
isBold
style={{ marginRight: theme.spacing.xl4 }}
>
{formatFloatToCurrency(row.amount)}
{isPerMonth && t('common:utility.perMonth')}
</$ViewField>
)}
</$CalculatorTableRow>
</div>
);
})}
</>
)}
<Koros
dense
type="pulse"
style={{
fill: theme.colors.coatOfArmsLight,
margin: `${theme.spacing.l} 0 -35px -65px`,
width: 'calc(100% + 130px)',
}}
/>
</$GridCell>
);
};

export default SalaryCalculatorResults;
Loading

0 comments on commit 728af1f

Please sign in to comment.