Skip to content

Commit

Permalink
fix: HL-1005, HL-916, (#2379)
Browse files Browse the repository at this point in the history
* feat: disable form save when it is in invalid state

* fix: print out pretty values instead of crash if backend returns complex object

* fix: do not validate pay subsidies when application status is changed

* fix: no need for TOS accept if handler changes status

* fix: co-operation negotiations description would not be updated
  • Loading branch information
sirtawast authored Oct 30, 2023
1 parent 94ba1fb commit 779bed2
Show file tree
Hide file tree
Showing 17 changed files with 156 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from applications.benefit_aggregation import get_former_benefit_info
from applications.enums import (
ApplicationActions,
ApplicationOrigin,
ApplicationStatus,
AttachmentRequirement,
Expand Down Expand Up @@ -1128,8 +1129,14 @@ def _validate_employee_consent(self, instance):

def _update_applicant_terms_approval(self, instance, approve_terms):
if ApplicantTermsApproval.terms_approval_needed(instance):
data = self.context["request"].data
action = data["action"] if "action" in data else None

# Ignore applicant's terms if app origin is from handler
if instance.application_origin == ApplicationOrigin.HANDLER:
if (
instance.application_origin == ApplicationOrigin.HANDLER
or action == ApplicationActions.APPLICANT_TOGGLE_EDIT
):
return

if not approve_terms:
Expand Down
6 changes: 6 additions & 0 deletions backend/benefit/applications/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,9 @@ class AhjoStatus(models.TextChoices):
)
DELETE_REQUEST_SENT = "delete_request_sent", _("Delete request sent")
DELETE_REQUEST_RECEIVED = "delete_request_received", _("Delete request received")


class ApplicationActions(models.TextChoices):
APPLICANT_TOGGLE_EDIT = "APPLICANT_TOGGLE_EDIT", _(
"Allow/disallow applicant's modifications"
)
10 changes: 8 additions & 2 deletions backend/benefit/calculator/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

from applications.enums import ApplicationStatus, BenefitType
from applications.enums import ApplicationActions, ApplicationStatus, BenefitType
from applications.models import Application
from calculator.models import (
Calculation,
Expand Down Expand Up @@ -271,7 +271,13 @@ def validate(self, data):
request = self.context.get("request")
if request is None:
return data
if self._are_dates_required():

action = None
if "action" in request.data:
action = request.data["action"]
if self._are_dates_required() and action not in [
ApplicationActions.APPLICANT_TOGGLE_EDIT
]:
if data.get("start_date") is None:
raise serializers.ValidationError(
{"start_date": _("Start date cannot be empty")}
Expand Down
3 changes: 3 additions & 0 deletions frontend/benefit/applicant/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@
"send": {
"heading1": "Terms"
}
},
"errors": {
"dirtyOrInvalidForm": "Please fill any missing or invalid form fields"
}
},
"serviceName": "Helsinki benefit service",
Expand Down
3 changes: 3 additions & 0 deletions frontend/benefit/applicant/public/locales/fi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@
"send": {
"heading1": "Ehdot"
}
},
"errors": {
"dirtyOrInvalidForm": "Täytä lomakkeen puuttuvat tai virheelliset kentät"
}
},
"serviceName": "Helsinki-lisän asiointipalvelu",
Expand Down
3 changes: 3 additions & 0 deletions frontend/benefit/applicant/public/locales/sv/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@
"send": {
"heading1": "Villkoren"
}
},
"errors": {
"dirtyOrInvalidForm": "Fyll i eventuella saknade eller ogiltiga formulärfält"
}
},
"serviceName": "E-tjänsten för Helsingforstillägg",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,6 @@ const ApplicationFormStep1: React.FC<DynamicFormStepComponentProps> = ({
fields.coOperationNegotiations.name,
false
);
formik.setFieldValue(
APPLICATION_FIELDS_STEP1_KEYS.CO_OPERATION_NEGOTIATIONS_DESCRIPTION,
''
);
}}
checked={formik.values.coOperationNegotiations === false}
/>
Expand Down Expand Up @@ -314,7 +310,9 @@ const ApplicationFormStep1: React.FC<DynamicFormStepComponentProps> = ({
<StepperActions
disabledNext={deMinimisTotal() > MAX_DEMINIMIS_AID_TOTAL_AMOUNT}
handleSubmit={handleSubmit}
handleSave={handleSave}
handleSave={
formik.isValid && !isUnfinishedDeMinimisAid ? handleSave : undefined
}
handleDelete={data?.id ? handleDelete : null}
/>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,16 @@ export const getValidationSchema = (
),
[APPLICATION_FIELDS_STEP1_KEYS.CO_OPERATION_NEGOTIATIONS]: Yup.boolean()
.nullable()
.oneOf([true, false])
.required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)),
[APPLICATION_FIELDS_STEP1_KEYS.CO_OPERATION_NEGOTIATIONS_DESCRIPTION]:
Yup.string().when(
APPLICATION_FIELDS_STEP1_KEYS.CO_OPERATION_NEGOTIATIONS,
{
is: (checked: boolean): boolean => checked,
then: Yup.string()
.nullable()
.required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)),
is: true,
then: (schema) =>
schema.required(t(VALIDATION_MESSAGE_KEYS.REQUIRED)),
otherwise: (schema) => schema,
}
),
});
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ const ApplicationFormStep2: React.FC<DynamicFormStepComponentProps> = ({
</FormSection>
<StepperActions
handleSubmit={handleSubmit}
handleSave={handleSave}
handleSave={formik.isValid ? handleSave : undefined}
handleBack={handleBack}
handleDelete={handleDelete}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ import {
PAY_SUBSIDY_GRANTED,
VALIDATION_MESSAGE_KEYS,
} from 'benefit-shared/constants';
import { validateDateIsFromCurrentYearOnwards } from 'benefit-shared/utils/dates';
import startOfYear from 'date-fns/startOfYear';
import { FinnishSSN } from 'finnish-ssn';
import { TFunction } from 'next-i18next';
import { NAMES_REGEX } from 'shared/constants';
import {
convertToUIDateFormat,
validateDateIsFromCurrentYearOnwards,
} from 'shared/utils/date.utils';
import { convertToUIDateFormat } from 'shared/utils/date.utils';
import { getNumberValue } from 'shared/utils/string.utils';
import * as Yup from 'yup';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
import { useTranslation } from 'benefit/applicant/i18n';
import { Button, IconArrowLeft, IconArrowRight, IconCross } from 'hds-react';
import React, { useState } from 'react';
import {
Button,
IconAlertCircleFill,
IconArrowLeft,
IconArrowRight,
IconCross,
} from 'hds-react';
import React, { MouseEvent, useState } from 'react';
import {
$Grid,
$GridCell,
} from 'shared/components/forms/section/FormSection.sc';
import Modal from 'shared/components/modal/Modal';
import { respondAbove } from 'shared/styles/mediaQueries';
import styled from 'styled-components';

type StepperActionsProps = {
lastStep?: boolean;
disabledNext?: boolean;
handleBack?: () => void;
handleDelete?: () => void;
handleSubmit: () => void;
handleSave: () => void;
handleSave?: () => void;
};

const onClickSave = (e: MouseEvent, handleSave: () => void | false): void => {
if (handleSave) {
handleSave();
}
e.preventDefault();
};

const $SaveAction = styled.div`
${respondAbove('sm')`
text-align: center;
`}
`;
const $SaveActionFormErrorText = styled.div`
display: flex;
align-items: center;
color: ${(props) => props.theme.colors.error};
svg {
width: 48px;
fill: ${(props) => props.theme.colors.error};
}
`;

const StepperActions: React.FC<StepperActionsProps> = ({
lastStep,
disabledNext,
Expand Down Expand Up @@ -44,9 +74,24 @@ const StepperActions: React.FC<StepperActionsProps> = ({
)}
</$GridCell>
<$GridCell $colSpan={6} justifySelf="center">
<Button theme="black" variant="secondary" onClick={handleSave}>
{t(`${translationsBase}.saveAndContinueLater`)}
</Button>
<$SaveAction>
<Button
theme="black"
variant="secondary"
onClick={(e) => onClickSave(e, handleSave)}
disabled={!handleSave}
>
{t(`${translationsBase}.saveAndContinueLater`)}
</Button>
{!handleSave && (
<$SaveActionFormErrorText>
<IconAlertCircleFill />
<p aria-live="polite">
{t('common:applications.errors.dirtyOrInvalidForm')}
</p>
</$SaveActionFormErrorText>
)}
</$SaveAction>
</$GridCell>
<$GridCell $colSpan={3} justifySelf="end">
<Button
Expand Down
25 changes: 20 additions & 5 deletions frontend/benefit/applicant/src/hooks/useFormActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ interface FormActions {
onDelete: (id: string) => void;
}

const prettyPrintObject = (object: Record<string, string[]>): string =>
JSON.stringify(object)
.replace(/["[\]{}]/g, '')
.replace(/:/g, ': ')
.replace(/,/g, '\n');

const useFormActions = (application: Partial<Application>): FormActions => {
const router = useRouter();
const currentStep = getApplicationStepFromString(
Expand Down Expand Up @@ -79,11 +85,15 @@ const useFormActions = (application: Partial<Application>): FormActions => {
labelText: t('common:error.generic.label'),
text: isContentTypeHTML
? t('common:error.generic.text')
: Object.entries(errorData).map(([key, value]) => (
<a key={key} href={`#${key}`}>
{value}
</a>
)),
: Object.entries(errorData).map(([key, value]) =>
typeof value === 'string' ? (
<a key={key} href={`#${key}`}>
{value}
</a>
) : (
prettyPrintObject(value)
)
),
});
}
}, [
Expand All @@ -99,6 +109,8 @@ const useFormActions = (application: Partial<Application>): FormActions => {
const getModifiedValues = (currentValues: Application): Application => {
const employee: Employee | undefined = currentValues?.employee ?? undefined;
const {
coOperationNegotiations,
coOperationNegotiationsDescription,
paySubsidyGranted,
startDate,
endDate,
Expand Down Expand Up @@ -150,6 +162,9 @@ const useFormActions = (application: Partial<Application>): FormActions => {
...normalizedValues,
deMinimisAidSet: deMinimisAidData,
benefitType: BENEFIT_TYPES.SALARY,
coOperationNegotiationsDescription: coOperationNegotiations
? coOperationNegotiationsDescription
: '',
deMinimisAid: deMinimisAidData.length > 0,
};
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { APPLICATION_ACTIONS } from 'benefit/handler/constants';
import { useApplicationActions } from 'benefit/handler/hooks/useApplicationActions';
import { APPLICATION_STATUSES } from 'benefit-shared/constants';
import { Application } from 'benefit-shared/types/application';
Expand All @@ -12,7 +13,10 @@ export type Props = {
const EditAction: React.FC<Props> = ({ application }) => {
const translationsBase = 'common:review.actions';
const { t } = useTranslation();
const { updateStatus } = useApplicationActions(application);
const { updateStatus } = useApplicationActions(
application,
APPLICATION_ACTIONS.APPLICANT_TOGGLE_EDIT
);

const [isUpdatingApplication, setIsUpdatingApplication] =
React.useState(false);
Expand Down
4 changes: 4 additions & 0 deletions frontend/benefit/handler/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,7 @@ export const ALL_APPLICATION_STATUSES: APPLICATION_STATUSES[] = [
export enum LOCAL_STORAGE_KEYS {
CSRF_TOKEN = 'csrfToken',
}

export enum APPLICATION_ACTIONS {
APPLICANT_TOGGLE_EDIT = 'APPLICANT_TOGGLE_EDIT',
}
11 changes: 9 additions & 2 deletions frontend/benefit/handler/src/hooks/useApplicationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Application, ApplicationData } from 'benefit-shared/types/application';
import { stringToFloatValue } from 'shared/utils/string.utils';
import snakecaseKeys from 'snakecase-keys';

import { APPLICATION_ACTIONS } from '../constants';
import useUpdateApplicationQuery from './useUpdateApplicationQuery';

type ExtendedComponentProps = {
Expand All @@ -16,7 +17,8 @@ type ExtendedComponentProps = {
};

const useApplicationActions = (
application: Application
application: Application,
action?: APPLICATION_ACTIONS
): ExtendedComponentProps => {
const updateApplicationQuery = useUpdateApplicationQuery();

Expand Down Expand Up @@ -52,7 +54,12 @@ const useApplicationActions = (
},
{ deep: true }
) as ApplicationData;
updateApplicationQuery.mutate(currentApplicationData);

const data = {
...currentApplicationData,
action,
};
updateApplicationQuery.mutate(data);
window.scrollTo(0, 0);
};

Expand Down
18 changes: 17 additions & 1 deletion frontend/benefit/shared/src/utils/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
APPLICATION_START_DATE,
BENEFIT_TYPES,
} from 'benefit-shared/constants';
import { isFuture, parse } from 'date-fns';
import { isFuture, parse, startOfYear } from 'date-fns';
import addMonths from 'date-fns/addMonths';
import subDays from 'date-fns/subDays';
import { parseDate } from 'shared/utils/date.utils';
Expand Down Expand Up @@ -44,6 +44,22 @@ export const getMaxEndDate = (
export const validateFinnishDatePattern = (value = ''): boolean =>
/^([1-9]|[12]\d|3[01])\.([1-9]|1[0-2])\.20\d{2}/.test(value);

export const validateDateIsFromCurrentYearOnwards = (
value: string
): boolean => {
if (!value || value.length < 8) return false;

const isFinnishDate = validateFinnishDatePattern(value);
const date = isFinnishDate
? parse(value, 'd.M.yyyy', new Date())
: parseDate(value);

if (!date || !date?.toJSON()) {
return false;
}
return date ? date >= startOfYear(new Date()) : false;
};

export const validateIsTodayOrPastDate = (value: string): boolean => {
if (!value || value.length < 8) return false;

Expand Down
Loading

0 comments on commit 779bed2

Please sign in to comment.