From a6930229077731e973cb2f2a649040d6c9925483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Fri, 15 Nov 2024 14:35:01 +0000 Subject: [PATCH 01/34] feat(native-app): add third party error template and use in health overview (#16860) * feat: add third party error template and use in health overview * fix: use arrow function instead of function --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/native/app/src/messages/en.ts | 3 + apps/native/app/src/messages/is.ts | 2 + .../src/screens/health/health-overview.tsx | 360 ++++++++++-------- .../app/src/stores/organizations-store.ts | 5 + .../src/ui/lib/problem/problem-template.tsx | 22 +- .../native/app/src/ui/lib/problem/problem.tsx | 21 +- .../lib/problem/third-party-service-error.tsx | 30 ++ .../utils/get-organization-slug-from-error.ts | 36 ++ 8 files changed, 319 insertions(+), 160 deletions(-) create mode 100644 apps/native/app/src/ui/lib/problem/third-party-service-error.tsx create mode 100644 apps/native/app/src/utils/get-organization-slug-from-error.ts diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index 734168b2c9bb..17260fc7d745 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -594,6 +594,9 @@ export const en: TranslatedMessages = { 'problem.offline.title': 'No internet connection', 'problem.offline.message': 'An error occurred while communicating with the service provider', + 'problem.thirdParty.title': 'No connection', + 'problem.thirdParty.message': + 'An error occurred while communicating with the service provider', // passkeys 'passkeys.headingTitle': 'Sign in with Island.is app', diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index 0135ba4a659e..bd6967d93b48 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -595,6 +595,8 @@ export const is = { 'Ef þú telur þig eiga gögn sem ættu að birtast hér, vinsamlegast hafðu samband við þjónustuaðila.', 'problem.offline.title': 'Samband næst ekki', 'problem.offline.message': 'Villa kom upp í samskiptum við þjónustuaðila', + 'problem.thirdParty.title': 'Samband næst ekki', + 'problem.thirdParty.message': 'Villa kom upp í samskiptum við þjónustuaðila', // passkeys 'passkeys.headingTitle': 'Innskrá með Ísland.is appinu', diff --git a/apps/native/app/src/screens/health/health-overview.tsx b/apps/native/app/src/screens/health/health-overview.tsx index 25fce0d06d70..2c6778c7fb34 100644 --- a/apps/native/app/src/screens/health/health-overview.tsx +++ b/apps/native/app/src/screens/health/health-overview.tsx @@ -1,4 +1,12 @@ -import { Alert, Button, Heading, Input, InputRow, Typography } from '@ui' +import { + Alert, + Button, + Heading, + Input, + InputRow, + Problem, + Typography, +} from '@ui' import React, { useCallback, useMemo, useState } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import { @@ -12,6 +20,7 @@ import { } from 'react-native' import { NavigationFunctionComponent } from 'react-native-navigation' import styled, { useTheme } from 'styled-components/native' +import { ApolloError } from '@apollo/client' import { useGetHealthCenterQuery, @@ -77,6 +86,10 @@ const HeadingSection: React.FC = ({ title, onPress }) => { ) } +const showErrorComponent = (error: ApolloError) => { + return +} + const { getNavigationOptions, useNavigationOptions } = createNavigationOptionHooks((theme, intl) => ({ topBar: { @@ -247,49 +260,57 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ ) } /> - - - - - - + {(healthCenterRes.data || healthCenterRes.loading) && ( + <> + + + + + + + + )} + {healthCenterRes.error && + !healthCenterRes.data && + showErrorComponent(healthCenterRes.error)} openBrowser(`${origin}/minarsidur/heilsa/yfirlit`, componentId) } /> - {healthInsuranceData?.isInsured || healthInsuranceRes.loading ? ( + {(healthInsuranceRes.data && healthInsuranceData?.isInsured) || + healthInsuranceRes.loading ? ( ) : ( - + !healthInsuranceRes.error && + healthInsuranceRes.data && ( + + ) )} + {healthInsuranceRes.error && + !healthInsuranceRes.data && + showErrorComponent(healthInsuranceRes.error)} - - - - - - - - - - + {(paymentOverviewRes.loading || paymentOverviewRes.data) && ( + <> + + + + + + + + + + + + )} + {paymentOverviewRes.error && + !paymentOverviewRes.data && + paymentStatusRes.error && + !paymentStatusRes.data && + showErrorComponent(paymentOverviewRes.error)} - - - - - - + {(medicinePurchaseRes.loading || medicinePurchaseRes.data) && ( + <> + + + + + + + + )} + {medicinePurchaseRes.error && + !medicinePurchaseRes.data && + showErrorComponent(medicinePurchaseRes.error)} diff --git a/apps/native/app/src/stores/organizations-store.ts b/apps/native/app/src/stores/organizations-store.ts index 2d3580092443..3b9cb837e73a 100644 --- a/apps/native/app/src/stores/organizations-store.ts +++ b/apps/native/app/src/stores/organizations-store.ts @@ -30,6 +30,7 @@ interface Organization { interface OrganizationsStore extends State { organizations: Organization[] getOrganizationLogoUrl(forName: string, size?: number): ImageSourcePropType + getOrganizationNameBySlug(slug: string): string actions: any } @@ -72,6 +73,10 @@ export const organizationsStore = create( const uri = `${url}?w=${size}&h=${size}&fit=pad&fm=png` return { uri } }, + getOrganizationNameBySlug(slug: string) { + const org = get().organizations.find((o) => o.slug === slug) + return org?.title ?? '' + }, actions: { updateOriganizations: async () => { const client = await getApolloClientAsync() diff --git a/apps/native/app/src/ui/lib/problem/problem-template.tsx b/apps/native/app/src/ui/lib/problem/problem-template.tsx index b15358fbdc05..1ebe261c581f 100644 --- a/apps/native/app/src/ui/lib/problem/problem-template.tsx +++ b/apps/native/app/src/ui/lib/problem/problem-template.tsx @@ -10,6 +10,7 @@ export type ProblemTemplateBaseProps = { title: string message: string | ReactNode withContainer?: boolean + size?: 'small' | 'large' } interface WithIconProps extends ProblemTemplateBaseProps { @@ -68,6 +69,7 @@ const getColorsByVariant = ( const Host = styled.View<{ borderColor: Colors noContainer?: boolean + size: 'small' | 'large' }>` border-color: ${({ borderColor, theme }) => theme.color[borderColor]}; border-width: 1px; @@ -76,11 +78,12 @@ const Host = styled.View<{ justify-content: center; align-items: center; flex: 1; - row-gap: ${({ theme }) => theme.spacing[3]}px; + row-gap: ${({ theme, size }) => + size === 'small' ? theme.spacing[2] : theme.spacing[3]}px; padding: ${({ theme }) => theme.spacing[2]}px; ${({ noContainer, theme }) => noContainer && `margin: ${theme.spacing[2]}px;`} - min-height: 280px; + min-height: ${({ size }) => (size === 'large' ? '280' : '142')}px; ` const Tag = styled(View)<{ @@ -114,12 +117,13 @@ export const ProblemTemplate = ({ showIcon, tag, withContainer, + size = 'large', }: ProblemTemplateProps) => { const { borderColor, tagColor, tagBackgroundColor } = getColorsByVariant(variant) return ( - + {tag && ( @@ -129,10 +133,18 @@ export const ProblemTemplate = ({ )} {showIcon && } - + {title} - {message} + + {message} + ) diff --git a/apps/native/app/src/ui/lib/problem/problem.tsx b/apps/native/app/src/ui/lib/problem/problem.tsx index 40963f69836c..4fa3d4e984d1 100644 --- a/apps/native/app/src/ui/lib/problem/problem.tsx +++ b/apps/native/app/src/ui/lib/problem/problem.tsx @@ -1,7 +1,10 @@ import { useEffect } from 'react' + import { useTranslate } from '../../../hooks/use-translate' import { useOfflineStore } from '../../../stores/offline-store' import { ProblemTemplate, ProblemTemplateBaseProps } from './problem-template' +import { getOrganizationSlugFromError } from '../../../utils/get-organization-slug-from-error' +import { ThirdPartyServiceError } from './third-party-service-error' enum ProblemTypes { error = 'error', @@ -20,7 +23,7 @@ type ProblemBaseProps = { title?: string message?: string logError?: boolean -} & Pick +} & Pick interface ErrorProps extends ProblemBaseProps { type?: 'error' @@ -61,6 +64,7 @@ export const Problem = ({ logError = false, withContainer, showIcon, + size = 'large', }: ProblemProps) => { const t = useTranslate() const { isConnected } = useOfflineStore() @@ -73,6 +77,7 @@ export const Problem = ({ message: message ?? t('problem.error.message'), tag: tag ?? t('problem.error.tag'), variant: 'error', + size: size ?? 'large', } as const useEffect(() => { @@ -90,6 +95,7 @@ export const Problem = ({ variant="warning" title={title ?? t('problem.offline.title')} message={message ?? t('problem.offline.message')} + size={size} /> ) } @@ -99,6 +105,18 @@ export const Problem = ({ switch (type) { case ProblemTypes.error: + if (error) { + const organizationSlug = getOrganizationSlugFromError(error) + + if (organizationSlug) { + return ( + + ) + } + } return case ProblemTypes.noData: @@ -109,6 +127,7 @@ export const Problem = ({ variant="info" title={title ?? t('problem.noData.title')} message={message ?? t('problem.noData.message')} + size={size} /> ) diff --git a/apps/native/app/src/ui/lib/problem/third-party-service-error.tsx b/apps/native/app/src/ui/lib/problem/third-party-service-error.tsx new file mode 100644 index 000000000000..d2a714c18694 --- /dev/null +++ b/apps/native/app/src/ui/lib/problem/third-party-service-error.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { useIntl } from 'react-intl' + +import { ProblemTemplate } from './problem-template' +import { useOrganizationsStore } from '../../../stores/organizations-store' + +type ThirdPartyServiceErrorProps = { + organizationSlug: string + size: 'small' | 'large' +} + +export const ThirdPartyServiceError = ({ + organizationSlug, + size, +}: ThirdPartyServiceErrorProps) => { + const intl = useIntl() + + const { getOrganizationNameBySlug } = useOrganizationsStore() + const organizationName = getOrganizationNameBySlug(organizationSlug) + + return ( + + ) +} diff --git a/apps/native/app/src/utils/get-organization-slug-from-error.ts b/apps/native/app/src/utils/get-organization-slug-from-error.ts new file mode 100644 index 000000000000..1463c87cb8db --- /dev/null +++ b/apps/native/app/src/utils/get-organization-slug-from-error.ts @@ -0,0 +1,36 @@ +import { ApolloError } from '@apollo/client' + +type PartialProblem = { + organizationSlug?: string +} + +type CustomExtension = { + code: string + problem?: PartialProblem + exception?: { + problem?: PartialProblem + } +} + +/** + * Extracts the organization slug from the Apollo error, if it exists. + */ +export const getOrganizationSlugFromError = (error: ApolloError | unknown) => { + const graphQLErrors = (error as ApolloError)?.graphQLErrors + + if (graphQLErrors) { + for (const graphQLError of graphQLErrors) { + const extensions = graphQLError.extensions as CustomExtension + + const organizationSlug = + extensions?.problem?.organizationSlug ?? + extensions?.exception?.problem?.organizationSlug + + if (organizationSlug) { + return organizationSlug + } + } + } + + return undefined +} From b9cf31e8ef7a10426b84e98ec484df266982c03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81sd=C3=ADs=20Erna=20Gu=C3=B0mundsd=C3=B3ttir?= Date: Fri, 15 Nov 2024 15:23:46 +0000 Subject: [PATCH 02/34] feat(my-pages): add info and deadline text (#16896) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../law-and-order/src/lib/law-and-order.service.ts | 2 ++ .../domains/law-and-order/src/models/subpoena.model.ts | 8 ++++---- libs/clients/judicial-system-sp/src/clientConfig.json | 4 ++++ .../law-and-order/src/screens/Subpoena/Subpoena.graphql | 2 ++ .../law-and-order/src/screens/Subpoena/Subpoena.tsx | 8 ++++++-- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/libs/api/domains/law-and-order/src/lib/law-and-order.service.ts b/libs/api/domains/law-and-order/src/lib/law-and-order.service.ts index ca8f4038a1e0..d773090945c3 100644 --- a/libs/api/domains/law-and-order/src/lib/law-and-order.service.ts +++ b/libs/api/domains/law-and-order/src/lib/law-and-order.service.ts @@ -162,6 +162,8 @@ export class LawAndOrderService { (alert) => alert.type === AlertMessageTypeEnum.Success, )?.message, description: subpoenaData.subtitle, + information: subpoenaData.subpoenaInfoText, + deadline: subpoenaData.subpoenaNotificationDeadline, }, } return data diff --git a/libs/api/domains/law-and-order/src/models/subpoena.model.ts b/libs/api/domains/law-and-order/src/models/subpoena.model.ts index c71f0f365388..d54a1af82517 100644 --- a/libs/api/domains/law-and-order/src/models/subpoena.model.ts +++ b/libs/api/domains/law-and-order/src/models/subpoena.model.ts @@ -5,9 +5,6 @@ import { Group } from './group.model' @ObjectType('LawAndOrderSubpoenaTexts') export class Text { - @Field({ nullable: true }) - intro?: string - @Field({ nullable: true }) confirmation?: string @@ -15,7 +12,10 @@ export class Text { description?: string @Field({ nullable: true }) - claim?: string + information?: string + + @Field({ nullable: true }) + deadline?: string } @ObjectType('LawAndOrderSubpoenaData') diff --git a/libs/clients/judicial-system-sp/src/clientConfig.json b/libs/clients/judicial-system-sp/src/clientConfig.json index 48c81122e582..2e4d54e84c54 100644 --- a/libs/clients/judicial-system-sp/src/clientConfig.json +++ b/libs/clients/judicial-system-sp/src/clientConfig.json @@ -450,6 +450,8 @@ "type": "object", "properties": { "title": { "type": "string" }, + "subpoenaInfoText": { "type": "string" }, + "subpoenaNotificationDeadline": { "type": "string" }, "subtitle": { "type": "string" }, "groups": { "type": "array", @@ -468,6 +470,8 @@ }, "required": [ "title", + "subpoenaInfoText", + "subpoenaNotificationDeadline", "subtitle", "groups", "alerts", diff --git a/libs/portals/my-pages/law-and-order/src/screens/Subpoena/Subpoena.graphql b/libs/portals/my-pages/law-and-order/src/screens/Subpoena/Subpoena.graphql index 1551e0e65a5f..5134408d6183 100644 --- a/libs/portals/my-pages/law-and-order/src/screens/Subpoena/Subpoena.graphql +++ b/libs/portals/my-pages/law-and-order/src/screens/Subpoena/Subpoena.graphql @@ -21,6 +21,8 @@ query GetSubpoena($input: LawAndOrderSubpoenaInput!, $locale: String!) { texts { confirmation description + information + deadline } } } diff --git a/libs/portals/my-pages/law-and-order/src/screens/Subpoena/Subpoena.tsx b/libs/portals/my-pages/law-and-order/src/screens/Subpoena/Subpoena.tsx index 819da2fc84f4..06b836951131 100644 --- a/libs/portals/my-pages/law-and-order/src/screens/Subpoena/Subpoena.tsx +++ b/libs/portals/my-pages/law-and-order/src/screens/Subpoena/Subpoena.tsx @@ -134,9 +134,13 @@ const Subpoena = () => { )} - {formatMessage(messages.subpoenaInfoText)} + {subpoena.texts?.information ?? + formatMessage(messages.subpoenaInfoText)} + + + {subpoena.texts?.deadline ?? + formatMessage(messages.subpoenaInfoText2)} - {formatMessage(messages.subpoenaInfoText2)} {!loading && subpoena.data.hasChosen === false && ( From 9d639375d97e8adff584540b131b29175508b6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Albert?= Date: Fri, 15 Nov 2024 15:31:39 +0000 Subject: [PATCH 03/34] feat(driving-license): start on advanced driving-license (#16647) * start on advanced driving-license * code rabbit comment changes * more comment changes * change * minor tweaks - advanced license * tweaks * text tweak --------- Co-authored-by: albinagu <47886428+albinagu@users.noreply.github.com> Co-authored-by: albina Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/fields/Alert.tsx | 2 +- .../AdvancedLicenseSelection.tsx | 130 ++++++++++++++ .../fields/AdvancedLicenseSelection/index.tsx | 1 + .../driving-license/src/fields/index.ts | 1 + .../src/forms/prerequisites/getForm.ts | 21 ++- .../sectionAdvancedLicenseSelection.ts | 35 ++++ .../prerequisites/sectionApplicationFor.ts | 13 ++ .../driving-license/src/lib/constants.ts | 132 +++++++++++++++ .../driving-license/src/lib/dataSchema.ts | 19 ++- .../src/lib/drivingLicenseTemplate.ts | 4 +- .../src/lib/getApplicationFeatureFlags.ts | 2 + .../driving-license/src/lib/messages.ts | 158 ++++++++++++++++++ 12 files changed, 512 insertions(+), 6 deletions(-) create mode 100644 libs/application/templates/driving-license/src/fields/AdvancedLicenseSelection/AdvancedLicenseSelection.tsx create mode 100644 libs/application/templates/driving-license/src/fields/AdvancedLicenseSelection/index.tsx create mode 100644 libs/application/templates/driving-license/src/forms/prerequisites/sectionAdvancedLicenseSelection.ts diff --git a/libs/application/templates/driving-license-duplicate/src/fields/Alert.tsx b/libs/application/templates/driving-license-duplicate/src/fields/Alert.tsx index 97547f2646d6..da86370f043b 100644 --- a/libs/application/templates/driving-license-duplicate/src/fields/Alert.tsx +++ b/libs/application/templates/driving-license-duplicate/src/fields/Alert.tsx @@ -27,7 +27,7 @@ export const Alert: FC> = ({ }) => { const { formatMessage } = useLocale() const { title, type, message, heading } = field.props as Field - console.log('message', formatText(message, application, formatMessage)) + return ( {heading && ( diff --git a/libs/application/templates/driving-license/src/fields/AdvancedLicenseSelection/AdvancedLicenseSelection.tsx b/libs/application/templates/driving-license/src/fields/AdvancedLicenseSelection/AdvancedLicenseSelection.tsx new file mode 100644 index 000000000000..7976512917fc --- /dev/null +++ b/libs/application/templates/driving-license/src/fields/AdvancedLicenseSelection/AdvancedLicenseSelection.tsx @@ -0,0 +1,130 @@ +import React, { FC, useEffect, useState } from 'react' + +import { Box, Checkbox, ErrorMessage, Text } from '@island.is/island-ui/core' +import { FieldBaseProps } from '@island.is/application/types' +import { useFormContext } from 'react-hook-form' +import { + organizedAdvancedLicenseMap, + AdvancedLicense as AdvancedLicenseEnum, +} from '../../lib/constants' +import { useLocale } from '@island.is/localization' +import { m } from '../../lib/messages' + +const AdvancedLicenseSelection: FC> = ({ + errors, +}) => { + const { formatMessage } = useLocale() + const { setValue, watch } = useFormContext() + + const requiredMessage = (errors as { advancedLicense?: string }) + ?.advancedLicense + ? formatMessage(m.applicationForAdvancedRequiredError) + : '' + + const advancedLicenseValue = watch('advancedLicense') ?? [] + + const [selectedLicenses, setSelectedLicenses] = + useState>(advancedLicenseValue) + + useEffect(() => { + setValue('advancedLicense', selectedLicenses) + }, [selectedLicenses, setValue]) + + return ( + + {Object.entries(organizedAdvancedLicenseMap).map(([, options], index) => { + const group = options.find((x) => x.group)?.group + const groupAge = options.find((x) => x.minAge)?.minAge + + return ( + + + + {group ? formatMessage(m[`groupTitle${group}`]) : ''} + + + {formatMessage(m[`applicationForAdvancedAgeRequired`], { + age: String(groupAge), + })} + + + {options.map((option) => { + const name = `field-${option.code}` + + return ( + + { + setSelectedLicenses((prev) => { + return prev.includes(option.code) + ? prev + .filter((item) => item !== option.code) + .filter( + (item) => item !== option.professional?.code, + ) + : [...prev, option.code] + }) + }} + /> + {option?.professional?.code && ( + + { + setSelectedLicenses((prev) => { + if (e.target.checked && option.professional?.code) { + return [...prev, option.professional.code] + } + + return prev.filter( + (item) => item !== option.professional?.code, + ) + }) + }} + /> + + )} + + ) + })} + + ) + })} + {!selectedLicenses?.length && requiredMessage && ( + +
{requiredMessage}
+
+ )} +
+ ) +} + +export { AdvancedLicenseSelection } diff --git a/libs/application/templates/driving-license/src/fields/AdvancedLicenseSelection/index.tsx b/libs/application/templates/driving-license/src/fields/AdvancedLicenseSelection/index.tsx new file mode 100644 index 000000000000..051ad8e7ef5f --- /dev/null +++ b/libs/application/templates/driving-license/src/fields/AdvancedLicenseSelection/index.tsx @@ -0,0 +1 @@ +export { AdvancedLicenseSelection } from './AdvancedLicenseSelection' diff --git a/libs/application/templates/driving-license/src/fields/index.ts b/libs/application/templates/driving-license/src/fields/index.ts index 6f42ecaa4bb4..6e5d3bd15a6b 100644 --- a/libs/application/templates/driving-license/src/fields/index.ts +++ b/libs/application/templates/driving-license/src/fields/index.ts @@ -4,5 +4,6 @@ export { EligibilitySummary } from './EligibilitySummary' export { SubmitAndDecline } from './SubmitAndDecline' export { LinkExistingApplication } from './LinkExistingApplication' export { PaymentPending } from './PaymentPending' +export { AdvancedLicenseSelection } from './AdvancedLicenseSelection' export { QualityPhoto } from './QualityPhoto' export { default as HealthRemarks } from './HealthRemarks' diff --git a/libs/application/templates/driving-license/src/forms/prerequisites/getForm.ts b/libs/application/templates/driving-license/src/forms/prerequisites/getForm.ts index 9bf9a5b197a7..28d94b416070 100644 --- a/libs/application/templates/driving-license/src/forms/prerequisites/getForm.ts +++ b/libs/application/templates/driving-license/src/forms/prerequisites/getForm.ts @@ -8,13 +8,23 @@ import { sectionApplicationFor } from './sectionApplicationFor' import { sectionRequirements } from './sectionRequirements' import { sectionExistingApplication } from './sectionExistingApplication' import { sectionDigitalLicenseInfo } from './sectionDigitalLicenseInfo' +import { sectionAdvancedLicenseSelection } from './sectionAdvancedLicenseSelection' + +interface DrivingLicenseFormConfig { + allowFakeData?: boolean + allowPickLicense?: boolean + allowBELicense?: boolean + allow65Renewal?: boolean + allowAdvanced?: boolean +} export const getForm = ({ allowFakeData = false, allowPickLicense = false, allowBELicense = false, allow65Renewal = false, -}): Form => + allowAdvanced = false, +}: DrivingLicenseFormConfig): Form => buildForm({ id: 'DrivingLicenseApplicationPrerequisitesForm', title: '', @@ -31,8 +41,15 @@ export const getForm = ({ sectionExternalData, sectionExistingApplication, ...(allowPickLicense - ? [sectionApplicationFor(allowBELicense, allow65Renewal)] + ? [ + sectionApplicationFor( + allowBELicense, + allow65Renewal, + allowAdvanced, + ), + ] : []), + ...(allowAdvanced ? [sectionAdvancedLicenseSelection] : []), sectionDigitalLicenseInfo, sectionRequirements, ], diff --git a/libs/application/templates/driving-license/src/forms/prerequisites/sectionAdvancedLicenseSelection.ts b/libs/application/templates/driving-license/src/forms/prerequisites/sectionAdvancedLicenseSelection.ts new file mode 100644 index 000000000000..e58582a6577b --- /dev/null +++ b/libs/application/templates/driving-license/src/forms/prerequisites/sectionAdvancedLicenseSelection.ts @@ -0,0 +1,35 @@ +import { + buildCustomField, + buildMultiField, + buildSubSection, + getValueViaPath, +} from '@island.is/application/core' +import { m } from '../../lib/messages' +import { LicenseTypes } from '../../lib/constants' + +export const sectionAdvancedLicenseSelection = buildSubSection({ + id: 'sectionAdvancedLicenseSelection', + title: m.applicationForAdvancedLicenseTitle, + condition: (answers) => { + const applicationFor = getValueViaPath( + answers, + 'applicationFor', + ) + + return applicationFor != null && applicationFor === LicenseTypes.B_ADVANCED + }, + children: [ + buildMultiField({ + id: 'advancedLicenseSelectionFields', + title: m.applicationForAdvancedLicenseSectionTitle, + description: m.applicationForAdvancedLicenseSectionDescription, + children: [ + buildCustomField({ + id: 'advancedLicense', + title: '', + component: 'AdvancedLicenseSelection', + }), + ], + }), + ], +}) diff --git a/libs/application/templates/driving-license/src/forms/prerequisites/sectionApplicationFor.ts b/libs/application/templates/driving-license/src/forms/prerequisites/sectionApplicationFor.ts index cf6ba528b6bb..571231ab2c73 100644 --- a/libs/application/templates/driving-license/src/forms/prerequisites/sectionApplicationFor.ts +++ b/libs/application/templates/driving-license/src/forms/prerequisites/sectionApplicationFor.ts @@ -7,6 +7,7 @@ import { import { m } from '../../lib/messages' import { DrivingLicense } from '../../lib/types' import { + B_ADVANCED, B_FULL, B_FULL_RENEWAL_65, B_TEMP, @@ -17,6 +18,7 @@ import { export const sectionApplicationFor = ( allowBELicense = false, allow65Renewal = false, + allowAdvanced = false, ) => buildSubSection({ id: 'applicationFor', @@ -112,6 +114,17 @@ export const sectionApplicationFor = ( }) } + if (allowAdvanced) { + options = options.concat({ + label: m.applicationForAdvancedLicenseTitle, + subLabel: m.applicationForAdvancedLicenseDescription, + value: B_ADVANCED, + disabled: !categories?.some( + (c) => c.nr.toUpperCase() === 'B' && c.validToCode !== 8, + ), + }) + } + return options }, }), diff --git a/libs/application/templates/driving-license/src/lib/constants.ts b/libs/application/templates/driving-license/src/lib/constants.ts index 0e4e00652e02..c10fc573f53b 100644 --- a/libs/application/templates/driving-license/src/lib/constants.ts +++ b/libs/application/templates/driving-license/src/lib/constants.ts @@ -8,9 +8,141 @@ export enum ApiActions { export const B_FULL = 'B-full' export const B_TEMP = 'B-temp' export const B_FULL_RENEWAL_65 = 'B-full-renewal-65' +export const B_ADVANCED = 'B-advanced' export const BE = 'BE' export const DELIVERY_FEE = 'deliveryFee' +export enum LicenseTypes { + 'B_FULL' = 'B-full', + 'B_TEMP' = 'B-temp', + 'B_FULL_RENEWAL_65' = 'B-full-renewal-65', + 'BE' = 'BE', + 'B_ADVANCED' = 'B-advanced', +} + +export enum Pickup { + 'POST' = 'post', + 'DISTRICT' = 'district', +} + +export enum AdvancedLicenseGroupCodes { + 'C1' = 'C1', + 'C' = 'C', + 'D1' = 'D1', + 'D' = 'D', +} + +export enum MainAdvancedLicense { + 'C1' = 'C1', + 'D1' = 'D1', + 'C' = 'C', + 'D' = 'D', + 'C1E' = 'C1E', + 'D1E' = 'D1E', + 'CE' = 'CE', + 'DE' = 'DE', +} + +export enum ProfessionalAdvancedLicense { + 'C1A' = 'C1A', + 'D1A' = 'D1A', + 'CA' = 'CA', + 'DA' = 'DA', +} + +export const AdvancedLicense = { + ...MainAdvancedLicense, + ...ProfessionalAdvancedLicense, +} as const + +type AdvancedLicenseMapItem = { + minAge: number + group: keyof typeof AdvancedLicenseGroupCodes + code: keyof typeof MainAdvancedLicense + professional?: { + minAge: number + code: keyof typeof ProfessionalAdvancedLicense + } +} + +export const advancedLicenseMap: AdvancedLicenseMapItem[] = [ + // C1 + { + code: 'C1', + group: 'C1', + minAge: 18, + professional: { + code: 'C1A', + minAge: 18, + }, + }, + { + code: 'C1E', + group: 'C1', + minAge: 18, + }, + + // C + { + code: 'C', + group: 'C', + minAge: 21, + professional: { + code: 'CA', + minAge: 21, + }, + }, + { + code: 'CE', + group: 'C', + minAge: 21, + }, + + // D1 + { + code: 'D1', + group: 'D1', + minAge: 21, + professional: { + code: 'D1A', + minAge: 21, + }, + }, + { + code: 'D1E', + group: 'D1', + minAge: 21, + }, + + // D + { + code: 'D', + group: 'D', + minAge: 23, + professional: { + code: 'DA', + minAge: 23, + }, + }, + { + code: 'DE', + group: 'D', + minAge: 23, + }, +] + +export const organizedAdvancedLicenseMap = advancedLicenseMap.reduce< + Record +>((acc, item) => { + if (!acc[item.group]) { + acc[item.group] = [] + } + + acc[item.group].push(item) + + return acc +}, {}) + export const CHARGE_ITEM_CODES: Record = { [B_TEMP]: 'AY114', [B_FULL]: 'AY110', diff --git a/libs/application/templates/driving-license/src/lib/dataSchema.ts b/libs/application/templates/driving-license/src/lib/dataSchema.ts index 82d68194a468..981719fbf325 100644 --- a/libs/application/templates/driving-license/src/lib/dataSchema.ts +++ b/libs/application/templates/driving-license/src/lib/dataSchema.ts @@ -1,5 +1,14 @@ import { z } from 'zod' -import { YES, NO, B_FULL_RENEWAL_65, BE, B_TEMP, B_FULL } from './constants' +import { + YES, + NO, + B_FULL_RENEWAL_65, + BE, + B_TEMP, + B_FULL, + B_ADVANCED, + AdvancedLicense, +} from './constants' import { parsePhoneNumberFromString } from 'libphonenumber-js' import { Pickup } from './types' @@ -36,7 +45,13 @@ export const dataSchema = z.object({ ]), requirementsMet: z.boolean().refine((v) => v), certificate: z.array(z.enum([YES, NO])).nonempty(), - applicationFor: z.enum([B_FULL, B_TEMP, BE, B_FULL_RENEWAL_65]), + applicationFor: z.enum([B_FULL, B_TEMP, BE, B_FULL_RENEWAL_65, B_ADVANCED]), + advancedLicense: z + .array(z.enum(Object.values(AdvancedLicense) as [string, ...string[]])) + .nonempty() + .refine((value) => { + return value.length > 0 + }), email: z.string().email(), phone: z.string().refine((v) => isValidPhoneNumber(v)), drivingInstructor: z.string().min(1), diff --git a/libs/application/templates/driving-license/src/lib/drivingLicenseTemplate.ts b/libs/application/templates/driving-license/src/lib/drivingLicenseTemplate.ts index d7c7a9ea2d27..56ea5c57742f 100644 --- a/libs/application/templates/driving-license/src/lib/drivingLicenseTemplate.ts +++ b/libs/application/templates/driving-license/src/lib/drivingLicenseTemplate.ts @@ -53,7 +53,7 @@ import { Pickup } from './types' const getCodes = (application: Application) => { const applicationFor = getValueViaPath< - 'B-full' | 'B-temp' | 'BE' | 'B-full-renewal-65' + 'B-full' | 'B-temp' | 'BE' | 'B-full-renewal-65' | 'B-advanced' >(application.answers, 'applicationFor', 'B-full') const pickup = getValueViaPath(application.answers, 'pickup') @@ -142,6 +142,8 @@ const template: ApplicationTemplate< featureFlags[DrivingLicenseFeatureFlags.ALLOW_BE_LICENSE], allow65Renewal: featureFlags[DrivingLicenseFeatureFlags.ALLOW_65_RENEWAL], + allowAdvanced: + featureFlags[DrivingLicenseFeatureFlags.ALLOW_ADVANCED], }) }, write: 'all', diff --git a/libs/application/templates/driving-license/src/lib/getApplicationFeatureFlags.ts b/libs/application/templates/driving-license/src/lib/getApplicationFeatureFlags.ts index 9a7186289db8..2905a9b30d89 100644 --- a/libs/application/templates/driving-license/src/lib/getApplicationFeatureFlags.ts +++ b/libs/application/templates/driving-license/src/lib/getApplicationFeatureFlags.ts @@ -5,6 +5,7 @@ export enum DrivingLicenseFeatureFlags { ALLOW_LICENSE_SELECTION = 'applicationTemplateDrivingLicenseAllowLicenseSelection', ALLOW_BE_LICENSE = 'isBEApplicationEnabled', ALLOW_65_RENEWAL = 'is65RenewalApplicationEnabled', + ALLOW_ADVANCED = 'isDrivingLicenseAdvancedEnabled', } export const getApplicationFeatureFlags = async ( @@ -15,6 +16,7 @@ export const getApplicationFeatureFlags = async ( DrivingLicenseFeatureFlags.ALLOW_LICENSE_SELECTION, DrivingLicenseFeatureFlags.ALLOW_BE_LICENSE, DrivingLicenseFeatureFlags.ALLOW_65_RENEWAL, + DrivingLicenseFeatureFlags.ALLOW_ADVANCED, ] return ( diff --git a/libs/application/templates/driving-license/src/lib/messages.ts b/libs/application/templates/driving-license/src/lib/messages.ts index 07431e977201..778c8fa73979 100644 --- a/libs/application/templates/driving-license/src/lib/messages.ts +++ b/libs/application/templates/driving-license/src/lib/messages.ts @@ -878,6 +878,164 @@ export const m = defineMessages({ description: 'Health declaration answers indicate that health certificate is required and BE application does not support health certificate requirement', }, + applicationForAdvancedLicenseTitle: { + id: 'dl.application:applicationForAdvancedLicenseTitle', + defaultMessage: 'Aukin ökuréttindi / meirapróf', + description: 'Option title for selecting advanced driving license', + }, + applicationForAdvancedLicenseDescription: { + id: 'dl.application:applicationForAdvancedLicenseDescription', + defaultMessage: 'Texti kemur hér', + description: 'Option description for selecting advanced driving license', + }, + applicationForAdvancedLicenseSectionTitle: { + id: 'dl.application:applicationForAdvancedLicenseSectionTitle', + defaultMessage: 'Veldu réttindi', + description: 'Option title for selecting advanced driving license', + }, + applicationForAdvancedLicenseSectionDescription: { + id: 'dl.application:applicationForAdvancedLicenseSectionDescription', + defaultMessage: 'Í þessari umsókn er verið að sækja um:', + description: 'Option description for selecting advanced driving license', + }, + applicationForAdvancedAgeRequired: { + id: 'dl.application:applicationForAdvancedAgeFor', + defaultMessage: 'Réttindaaldur er {age} ára.', + description: 'Required age for {licenses} is {age} years', + }, + groupTitleC1: { + id: 'dl.application:groupTitleC1', + defaultMessage: 'Minni vörubíll og eftirvagn (C1 og C1E)', + description: 'C1 group title', + }, + groupTitleC: { + id: 'dl.application:groupTitleC1', + defaultMessage: 'Vörubíll og eftirvagn (C og CE)', + description: 'C1 group title', + }, + groupTitleD1: { + id: 'dl.application:groupTitleC1', + defaultMessage: 'Lítil rúta og eftirvagn (D1 og D1E)', + description: 'C1 group title', + }, + groupTitleD: { + id: 'dl.application:groupTitleC1', + defaultMessage: 'Stór rúta og eftirvagn (D og DE)', + description: 'C1 group title', + }, + applicationForAdvancedLicenseTitleC1: { + id: 'dl.application:applicationForAdvancedLicenseTitleC1', + defaultMessage: 'Minni vörubíll (C1)', + description: 'C1 title', + }, + applicationForAdvancedLicenseLabelC1: { + id: 'dl.application:applicationForAdvancedLicenseLabelC1', + defaultMessage: + 'Gefur réttindi til að aka bifreið fyrir 8 farþega eða færri, sem er þyngri en 3.500 kg en þó ekki þyngri en 7.500 kg. Sá sem hefur C1 réttindi má tengja eftirvagn/tengitæki sem er 750 kg eða minna af leyfðri heildarþyngd. Til þess að mega draga þyngri eftirvagna/tengitæki þarf að taka C1E réttindi.', + description: 'C1 description', + }, + applicationForAdvancedLicenseLabelC1A: { + id: 'dl.application:applicationForAdvancedLicenseLabelC1A', + defaultMessage: 'Sækja um leyfi í atvinnuskyni', + description: 'C1A description', + }, + applicationForAdvancedLicenseTitleD1: { + id: 'dl.application:applicationForAdvancedLicenseTitleD1', + defaultMessage: 'Lítil rúta (D1)', + description: 'D1 title', + }, + applicationForAdvancedLicenseLabelD1: { + id: 'dl.application:applicationForAdvancedLicenseLabelD1', + defaultMessage: + 'Gefur réttindi til að aka hópbifreið sem er gerð fyrir að hámarki 16 farþega. Sá sem hefur D1 réttindi má tengja eftirvagn/tengitæki sem er 750 kg eða minna að leyfðri heildarþyngd.', + description: 'D1 description', + }, + applicationForAdvancedLicenseLabelD1A: { + id: 'dl.application:applicationForAdvancedLicenseLabelD1A', + defaultMessage: 'Sækja um leyfi í atvinnuskyni', + description: 'D1A description', + }, + applicationForAdvancedLicenseTitleC: { + id: 'dl.application:applicationForAdvancedLicenseTitleC', + defaultMessage: 'Vörubíll (C)', + description: 'C title', + }, + applicationForAdvancedLicenseLabelC: { + id: 'dl.application:applicationForAdvancedLicenseLabelC', + defaultMessage: + 'Gefur réttindi til að aka vörubifreið fyrir 8 farþega eða færri, sem er þyngri en 7.500 kg. C flokkur gefur einnig réttindi til að aka bifreiðinni með eftirvagni sem er 750 kg eða minna af leyfðri heildarþyngd.', + description: 'C description', + }, + applicationForAdvancedLicenseLabelCA: { + id: 'dl.application:applicationForAdvancedLicenseLabelCA', + defaultMessage: 'Sækja um leyfi í atvinnuskyni', + description: 'CA description', + }, + applicationForAdvancedLicenseTitleD: { + id: 'dl.application:applicationForAdvancedLicenseTitleD', + defaultMessage: 'Stór rúta (D)', + description: 'D title', + }, + applicationForAdvancedLicenseLabelD: { + id: 'dl.application:applicationForAdvancedLicenseLabelD', + defaultMessage: + 'Gefur réttindi til að aka bifreið sem gerð er fyrir fleiri en 8 farþega auk ökumanns. Sá sem hefur D réttindi má tengja eftirvagn/tengitæki sem er 750 kg eða minna af leyfðri heildarþyngd.', + description: 'D description', + }, + applicationForAdvancedLicenseLabelDA: { + id: 'dl.application:applicationForAdvancedLicenseLabelDA', + defaultMessage: 'Sækja um leyfi í atvinnuskyni', + description: 'DA description', + }, + applicationForAdvancedLicenseTitleC1E: { + id: 'dl.application:applicationForAdvancedLicenseTitleC1E', + defaultMessage: 'Minni vörubíll og eftirvagn (C1E)', + description: 'C1E title', + }, + applicationForAdvancedLicenseLabelC1E: { + id: 'dl.application:applicationForAdvancedLicenseLabelC1E', + defaultMessage: + 'Gefur réttindi til að aka vörubifreið/stórum pallbíl í flokki C1 með eftirvagni sem er þyngri en 750 kg að heildarþunga. Þó má sameiginlegur heildarþungi beggja ökutækja ekki fara yfir 12.000 kg. ', + description: 'C1E description', + }, + applicationForAdvancedLicenseTitleD1E: { + id: 'dl.application:applicationForAdvancedLicenseTitleD1E', + defaultMessage: 'Lítil rúta og eftirvagn (D1)', + description: 'D1E title', + }, + applicationForAdvancedLicenseLabelD1E: { + id: 'dl.application:applicationForAdvancedLicenseLabelD1E', + defaultMessage: + 'Gefur réttindi til að aka bifreið í B-flokki með eftirvagn í BE-flokki og hópbifreið í D1 flokki með eftirvagn sem er þyngri en 750 kg að heildarþunga. Þó má sameiginlegur heildarþungi beggja ökutækja ekki fara yfir 12.000 kg.', + description: 'D1E description', + }, + applicationForAdvancedLicenseTitleCE: { + id: 'dl.application:applicationForAdvancedLicenseTitleCE', + defaultMessage: 'Vörubíll og eftirvagn (CE)', + description: 'CE title', + }, + applicationForAdvancedLicenseLabelCE: { + id: 'dl.application:applicationForAdvancedLicenseLabelCE', + defaultMessage: + 'Gefur réttindi til að aka vörubifreið í flokki C með eftirvagni sem er þyngri en 750 kg að heildarþunga.', + description: 'CE description', + }, + applicationForAdvancedLicenseTitleDE: { + id: 'dl.application:applicationForAdvancedLicenseTitleDE', + defaultMessage: 'Stór rúta og eftirvagn (DE)', + description: 'DE title', + }, + applicationForAdvancedLicenseLabelDE: { + id: 'dl.application:applicationForAdvancedLicenseLabelDE', + defaultMessage: + 'Að loknum D réttindum, er hægt að taka að auki DE, sem gefur réttindi til að aka hópbifreið í flokki D með eftirvagni sem er þyngri en 750 kg að heildarþunga. Þeir nemendur sem taka eftirvagnaréttindi í flokki DE og gilda þau réttindi einnig fyrir CE.', + description: 'DE description', + }, + applicationForAdvancedRequiredError: { + id: 'dl.application:applicationForAdvancedRequiredError', + defaultMessage: 'Þú verður að velja að minnsta kosti einn valmöguleika', + description: 'You must select at least one option', + }, }) export const requirementsMessages = defineMessages({ From e6dbd6d771c1aa7e32a45e0e19e3018404bd83c7 Mon Sep 17 00:00:00 2001 From: unakb Date: Fri, 15 Nov 2024 16:25:34 +0000 Subject: [PATCH 04/34] chore(j-s): Civil claimant spokesperson assigned notifications (#16750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(j-s): Handle advocate assigned to defendant notification * Update defendant.service.ts * feat(j-s): Send civil claimant notification when assigned * Update defendantNotification.service.ts * fix(j-s): Small fix on modal texts * fix(j-s): Stop using advocate for defender emails * fix(j-s): remove advocate assigned from user roles rules * fix(j-s): remove and change tests * fix(j-s): Tests * test(j-s): Defendant notification tests * Update update.spec.ts * Update update.spec.ts * Update sendDefenderAssignedNotifications.spec.ts * Update defendantNotification.service.ts * test(j-s): Add tests * fix(j-s): Tests * test(j-s): Added more civil claimant tests * test(j-s): Added more civil claimant tests * Update civilClaimantNotification.service.ts * Update internalNotification.controller.ts * Update notification.module.ts * Update sendAdvocateAssignedNotifications.spec.ts * Update civilClaimant.service.ts * Update civilClaimant.service.ts * test(j-s): Civil claimant exists guard tests --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Guðjón Guðjónsson --- .../backend/src/app/formatters/formatters.ts | 43 ----- .../backend/src/app/formatters/index.ts | 1 - .../backend/src/app/messages/notifications.ts | 26 --- .../defendant/civilClaimant.service.ts | 27 ++- .../guards/civilClaimaint.decorator.ts | 7 + .../guards/civilClaimantExists.guard.ts | 42 +++++ .../test/civilClaimantExistsGuard.spec.ts | 117 ++++++++++++ .../src/app/modules/defendant/index.ts | 5 +- .../civilClaimantController/create.spec.ts | 80 ++++++++ .../createGuards.spec.ts | 25 +++ .../createRolesRules.spec.ts | 35 ++++ .../civilClaimantController/delete.spec.ts | 79 ++++++++ .../deleteGuards.spec.ts | 25 +++ .../deleteRolesRules.spec.ts | 26 +++ .../civilClaimantController/update.spec.ts | 140 ++++++++++++++ .../updateGuards.spec.ts | 25 +++ .../updateRolesRules.spec.ts | 35 ++++ .../test/createTestingDefendantModule.ts | 35 +++- .../notification/caseNotification.service.ts | 75 +------- .../civilClaimantNotification.service.ts | 171 ++++++++++++++++++ .../civilClaimantNotification.strings.ts | 17 ++ .../dto/civilClaimantNotification.dto.ts | 12 ++ .../internalNotification.controller.ts | 40 +++- .../notification/notification.module.ts | 8 +- .../test/createTestingNotificationModule.ts | 2 + ...dSpokespersonAssignedNotifications.spec.ts | 167 +++++++++++++++++ .../sendAdvocateAssignedNotifications.spec.ts | 114 ------------ .../message/src/lib/message.ts | 2 + libs/judicial-system/types/src/index.ts | 1 + .../types/src/lib/notification.ts | 5 + 30 files changed, 1124 insertions(+), 263 deletions(-) create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts create mode 100644 apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.service.ts create mode 100644 apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.strings.ts create mode 100644 apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts create mode 100644 apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts diff --git a/apps/judicial-system/backend/src/app/formatters/formatters.ts b/apps/judicial-system/backend/src/app/formatters/formatters.ts index d48904e27885..b5859c04b70b 100644 --- a/apps/judicial-system/backend/src/app/formatters/formatters.ts +++ b/apps/judicial-system/backend/src/app/formatters/formatters.ts @@ -668,49 +668,6 @@ export const formatCustodyRestrictions = ( }) } -export const formatAdvocateAssignedEmailNotification = ( - formatMessage: FormatMessage, - theCase: Case, - advocateType: AdvocateType, - overviewUrl?: string, -): SubjectAndBody => { - const subject = - advocateType === AdvocateType.DEFENDER - ? formatMessage( - notifications.advocateAssignedEmail.subjectAccessToCaseFiles, - { - court: capitalize(theCase.court?.name ?? ''), - }, - ) - : formatMessage(notifications.advocateAssignedEmail.subjectAccess, { - courtCaseNumber: theCase.courtCaseNumber, - }) - - const body = - advocateType === AdvocateType.DEFENDER - ? formatMessage( - notifications.advocateAssignedEmail.bodyAccessToCaseFiles, - { - defenderHasAccessToRVG: Boolean(overviewUrl), - courtCaseNumber: capitalize(theCase.courtCaseNumber ?? ''), - court: theCase.court?.name ?? '', - courtName: theCase.court?.name.replace('dómur', 'dómi') ?? '', - linkStart: ``, - linkEnd: '', - }, - ) - : formatMessage(notifications.advocateAssignedEmail.bodyAccess, { - defenderHasAccessToRVG: Boolean(overviewUrl), - court: theCase.court?.name, - advocateType, - courtCaseNumber: capitalize(theCase.courtCaseNumber ?? ''), - linkStart: ``, - linkEnd: '', - }) - - return { body, subject } -} - export const formatCourtOfAppealJudgeAssignedEmailNotification = ( formatMessage: FormatMessage, caseNumber: string, diff --git a/apps/judicial-system/backend/src/app/formatters/index.ts b/apps/judicial-system/backend/src/app/formatters/index.ts index 48f8af67b53e..063734cdaacb 100644 --- a/apps/judicial-system/backend/src/app/formatters/index.ts +++ b/apps/judicial-system/backend/src/app/formatters/index.ts @@ -21,7 +21,6 @@ export { formatProsecutorReceivedByCourtSmsNotification, formatDefenderCourtDateLinkEmailNotification, formatDefenderResubmittedToCourtEmailNotification, - formatAdvocateAssignedEmailNotification, formatCourtIndictmentReadyForCourtEmailNotification, formatDefenderRoute, formatDefenderReadyForCourtEmailNotification, diff --git a/apps/judicial-system/backend/src/app/messages/notifications.ts b/apps/judicial-system/backend/src/app/messages/notifications.ts index fe12aea5011d..258eeedc852f 100644 --- a/apps/judicial-system/backend/src/app/messages/notifications.ts +++ b/apps/judicial-system/backend/src/app/messages/notifications.ts @@ -607,32 +607,6 @@ export const notifications = { 'Notaður sem texti í tölvupósti til verjanda vegna breytingar á lengd gæslu/einangrunar/vistunar þar sem úrskurðað var í einangrun.', }, }), - advocateAssignedEmail: defineMessages({ - subjectAccessToCaseFiles: { - id: 'judicial.system.backend:notifications.defender_assigned_email.subject_access_to_case_files', - defaultMessage: '{court} - aðgangur að málsgögnum', - description: - 'Fyrirsögn í pósti til verjanda þegar hann er skráður á mál.', - }, - subjectAccess: { - id: 'judicial.system.backend:notifications.defender_assigned_email.subject_access', - defaultMessage: 'Skráning í máli {courtCaseNumber}', - description: - 'Fyrirsögn í pósti til verjanda þegar hann er skráður á mál.', - }, - bodyAccessToCaseFiles: { - id: 'judicial.system.backend:notifications.defender_assigned_email.body_access_to_case_files', - defaultMessage: - '{court} hefur skráð þig verjanda í máli {courtCaseNumber}.

{defenderHasAccessToRVG, select, true {Gögn málsins eru aðgengileg á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast gögn málsins hjá {courtName} ef þau hafa ekki þegar verið afhent}}.', - description: 'Texti í pósti til verjanda þegar hann er skráður á mál.', - }, - bodyAccess: { - id: 'judicial.system.backend:notifications.defender_assigned_email.body_access', - defaultMessage: - '{court} hefur skráð þig {advocateType, select, LAWYER {lögmann einkaréttarkröfuhafa} LEGAL_RIGHTS_PROTECTOR {réttargæslumann einkaréttarkröfuhafa} other {verjanda}} í máli {courtCaseNumber}.

{defenderHasAccessToRVG, select, true {Sjá nánar á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast málið hjá {courtName}.}}.', - description: 'Texti í pósti til verjanda þegar hann er skráður á mál.', - }, - }), defendantsNotUpdatedAtCourt: defineMessages({ subject: { id: 'judicial.system.backend:notifications.defendants_not_updated_at_court.subject', diff --git a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts index 950e62bf84da..285092718ba5 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts @@ -7,7 +7,11 @@ import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' -import { CaseState } from '@island.is/judicial-system/types' +import { MessageService, MessageType } from '@island.is/judicial-system/message' +import { + CaseState, + CivilClaimantNotificationType, +} from '@island.is/judicial-system/types' import { Case } from '../case/models/case.model' import { UpdateCivilClaimantDto } from './dto/updateCivilClaimant.dto' @@ -18,6 +22,7 @@ export class CivilClaimantService { constructor( @InjectModel(CivilClaimant) private readonly civilClaimantModel: typeof CivilClaimant, + private readonly messageService: MessageService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -27,6 +32,24 @@ export class CivilClaimantService { }) } + private async sendUpdateCivilClaimantMessages( + update: UpdateCivilClaimantDto, + updatedCivilClaimant: CivilClaimant, + ): Promise { + if (update.isSpokespersonConfirmed === true) { + return this.messageService.sendMessagesToQueue([ + { + type: MessageType.CIVIL_CLAIMANT_NOTIFICATION, + caseId: updatedCivilClaimant.caseId, + body: { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + }, + elementId: updatedCivilClaimant.id, + }, + ]) + } + } + async update( caseId: string, civilClaimantId: string, @@ -49,6 +72,8 @@ export class CivilClaimantService { throw new Error(`Could not update civil claimant ${civilClaimantId}`) } + await this.sendUpdateCivilClaimantMessages(update, civilClaimants[0]) + return civilClaimants[0] } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts new file mode 100644 index 000000000000..9f508c5e60e5 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator } from '@nestjs/common' + +import { CivilClaimant } from '../models/civilClaimant.model' + +export const CurrentCivilClaimant = createParamDecorator( + (data, { args: [_1, { req }] }): CivilClaimant => req.civilClaimant, +) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts new file mode 100644 index 000000000000..401886582d41 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts @@ -0,0 +1,42 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common' + +import { Case } from '../../case' + +@Injectable() +export class CivilClaimantExistsGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() + + const theCase: Case = request.case + + if (!theCase) { + throw new BadRequestException('Missing case') + } + + const civilClaimantId = request.params.civilClaimantId + + if (!civilClaimantId) { + throw new BadRequestException('Missing civil claimant id') + } + + const civilClaimant = theCase.civilClaimants?.find( + (civilClaimants) => civilClaimants.id === civilClaimantId, + ) + + if (!civilClaimant) { + throw new NotFoundException( + `Civil claimant ${civilClaimantId} of case ${theCase.id} does not exist`, + ) + } + + request.civilClaimant = civilClaimant + + return true + } +} diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts new file mode 100644 index 000000000000..4e5cd39db640 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts @@ -0,0 +1,117 @@ +import { uuid } from 'uuidv4' + +import { + BadRequestException, + ExecutionContext, + NotFoundException, +} from '@nestjs/common' + +import { CivilClaimantExistsGuard } from '../civilClaimantExists.guard' + +interface Then { + result: boolean + error: Error +} + +type GivenWhenThen = () => Promise + +describe('Civil Claimant Exists Guard', () => { + const mockRequest = jest.fn() + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + givenWhenThen = async (): Promise => { + const guard = new CivilClaimantExistsGuard() + const then = {} as Then + + try { + then.result = guard.canActivate({ + switchToHttp: () => ({ getRequest: mockRequest }), + } as unknown as ExecutionContext) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('civil claimant exists', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const civilClaimant = { id: civilClaimantId, caseId } + const theCase = { id: caseId, civilClaimants: [civilClaimant] } + const request = { + params: { caseId, civilClaimantId }, + case: theCase, + civilClaimant: undefined, + } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce(request) + + then = await givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + expect(request.civilClaimant).toBe(civilClaimant) + }) + }) + + describe('civil claimant does not exist', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const theCase = { id: caseId, civilClaimants: [] } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ + params: { caseId, civilClaimantId }, + case: theCase, + }) + + then = await givenWhenThen() + }) + + it('should throw NotFoundException', () => { + expect(then.error).toBeInstanceOf(NotFoundException) + expect(then.error.message).toBe( + `Civil claimant ${civilClaimantId} of case ${caseId} does not exist`, + ) + }) + }) + + describe('missing case', () => { + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ params: {} }) + + then = await givenWhenThen() + }) + + it('should throw BadRequestException', () => { + expect(then.error).toBeInstanceOf(BadRequestException) + expect(then.error.message).toBe('Missing case') + }) + }) + + describe('missing civil claimant id', () => { + const caseId = uuid() + const theCase = { id: caseId, civilClaimants: [] } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ params: { caseId }, case: theCase }) + + then = await givenWhenThen() + }) + + it('should throw BadRequestException', () => { + expect(then.error).toBeInstanceOf(BadRequestException) + expect(then.error.message).toBe('Missing civil claimant id') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/index.ts b/apps/judicial-system/backend/src/app/modules/defendant/index.ts index d9914e3dcda4..5fb8a9c29896 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/index.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/index.ts @@ -1,6 +1,9 @@ export { Defendant } from './models/defendant.model' export { DefendantService } from './defendant.service' -export { CivilClaimant } from './models/civilClaimant.model' export { DefendantExistsGuard } from './guards/defendantExists.guard' export { CurrentDefendant } from './guards/defendant.decorator' + +export { CivilClaimant } from './models/civilClaimant.model' export { CivilClaimantService } from './civilClaimant.service' +export { CivilClaimantExistsGuard } from './guards/civilClaimantExists.guard' +export { CurrentCivilClaimant } from './guards/civilClaimaint.decorator' diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts new file mode 100644 index 000000000000..4684346f3209 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts @@ -0,0 +1,80 @@ +import { uuid } from 'uuidv4' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { Case } from '../../../case' +import { CivilClaimant } from '../../models/civilClaimant.model' + +interface Then { + result: CivilClaimant + error: Error +} + +type GivenWhenThen = (caseId?: string) => Promise + +describe('CivilClaimantController - Create', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const theCase = { id: caseId } as Case + const civilClaimantToCreate = { + caseId, + } + const createdCivilClaimant = { id: civilClaimantId, caseId } + + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { civilClaimantModel, civilClaimantController } = + await createTestingDefendantModule() + + mockCivilClaimantModel = civilClaimantModel + + const mockCreate = mockCivilClaimantModel.create as jest.Mock + mockCreate.mockResolvedValue(createdCivilClaimant) + + givenWhenThen = async () => { + const then = {} as Then + + await civilClaimantController + .create(theCase.id, theCase) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('civil claimant creation', () => { + let then: Then + + beforeEach(async () => { + then = await givenWhenThen(caseId) + }) + it('should create a civil claimant', () => { + expect(mockCivilClaimantModel.create).toHaveBeenCalledWith( + civilClaimantToCreate, + ) + }) + + it('should return the created civil claimant', () => { + expect(then.result).toEqual(createdCivilClaimant) + }) + }) + + describe('civil claimant creation fails', () => { + let then: Then + + beforeEach(async () => { + const mockCreate = mockCivilClaimantModel.create as jest.Mock + mockCreate.mockRejectedValue(new Error('Test error')) + + then = await givenWhenThen(caseId) + }) + + it('should throw an error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toEqual('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts new file mode 100644 index 000000000000..738952366261 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Create guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.create, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts new file mode 100644 index 000000000000..326946023e4f --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts @@ -0,0 +1,35 @@ +import { + districtCourtAssistantRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Create rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [ + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.create, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts new file mode 100644 index 000000000000..8a2c17bc5e10 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts @@ -0,0 +1,79 @@ +import { uuid } from 'uuidv4' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { CivilClaimant } from '../../models/civilClaimant.model' +import { DeleteCivilClaimantResponse } from '../../models/deleteCivilClaimant.response' + +interface Then { + result: DeleteCivilClaimantResponse + error: Error +} + +type GivenWhenThen = ( + caseId?: string, + civilClaimaintId?: string, +) => Promise + +describe('CivilClaimantController - Delete', () => { + const caseId = uuid() + const civilClaimantId = uuid() + + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { civilClaimantController, civilClaimantModel } = + await createTestingDefendantModule() + + mockCivilClaimantModel = civilClaimantModel + + const mockDestroy = mockCivilClaimantModel.destroy as jest.Mock + mockDestroy.mockRejectedValue(new Error('Test error')) + + givenWhenThen = async () => { + const then = {} as Then + + try { + then.result = await civilClaimantController.delete( + caseId, + civilClaimantId, + ) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('civil claimant deleted', () => { + let then: Then + + beforeEach(async () => { + const mockDestroy = mockCivilClaimantModel.destroy as jest.Mock + mockDestroy.mockResolvedValue(1) + + then = await givenWhenThen(caseId, civilClaimantId) + }) + it('should delete civil claimant', () => { + expect(mockCivilClaimantModel.destroy).toHaveBeenCalledWith({ + where: { caseId, id: civilClaimantId }, + }) + expect(then.result).toEqual({ deleted: true }) + }) + }) + + describe('civil claimant deletion fails', () => { + let then: Then + + beforeEach(async () => { + then = await givenWhenThen() + }) + + it('should throw Error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toBe('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts new file mode 100644 index 000000000000..20e8ef89e2f6 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Delete guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.delete, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts new file mode 100644 index 000000000000..46039a55bc8b --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts @@ -0,0 +1,26 @@ +import { + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Delete rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [prosecutorRule, prosecutorRepresentativeRule] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.delete, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts new file mode 100644 index 000000000000..d8661906faa4 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts @@ -0,0 +1,140 @@ +import { uuid } from 'uuidv4' + +import { MessageService, MessageType } from '@island.is/judicial-system/message' +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { UpdateCivilClaimantDto } from '../../dto/updateCivilClaimant.dto' +import { CivilClaimant } from '../../models/civilClaimant.model' + +interface Then { + result: CivilClaimant + error: Error +} + +type GivenWhenThen = ( + caseId: string, + civilClaimantId: string, + updateData: UpdateCivilClaimantDto, +) => Promise + +describe('CivilClaimantController - Update', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const civilClaimaint = { + id: civilClaimantId, + caseId, + nationalId: uuid(), + name: 'Original Name', + } as CivilClaimant + + let mockMessageService: MessageService + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { messageService, civilClaimantModel, civilClaimantController } = + await createTestingDefendantModule() + + mockMessageService = messageService + mockCivilClaimantModel = civilClaimantModel + + givenWhenThen = async ( + caseId: string, + civilClaimantId: string, + updateData: UpdateCivilClaimantDto, + ) => { + const then = {} as Then + + await civilClaimantController + .update(caseId, civilClaimantId, updateData) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('civil claimant updated', () => { + const civilClaimantUpdate = { name: 'Updated Name' } + const updatedCivilClaimant = { + id: civilClaimantId, + caseId, + ...civilClaimantUpdate, + } + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1, [updatedCivilClaimant]]) + + then = await givenWhenThen(caseId, civilClaimantId, civilClaimantUpdate) + }) + + it('should update the civil claimant', () => { + expect(mockCivilClaimantModel.update).toHaveBeenCalledWith( + civilClaimantUpdate, + { + where: { id: civilClaimantId, caseId }, + returning: true, + }, + ) + expect(mockMessageService.sendMessagesToQueue).not.toHaveBeenCalled() + }) + + it('should return the updated civil claimant', () => { + expect(then.result).toBe(updatedCivilClaimant) + }) + }) + + describe('civil claimant spokesperson confirmed', () => { + const civilClaimantUpdate = { isSpokespersonConfirmed: true } + const updatedCivilClaimant = { + id: civilClaimantId, + caseId, + ...civilClaimantUpdate, + } + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1, [updatedCivilClaimant]]) + + then = await givenWhenThen(caseId, civilClaimantId, civilClaimantUpdate) + }) + + it('should queue spokesperson assigned message', () => { + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ + { + type: MessageType.CIVIL_CLAIMANT_NOTIFICATION, + caseId, + elementId: civilClaimantId, + body: { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + }, + }, + ]) + }) + + it('should return the updated civil claimant', () => { + expect(then.result).toBe(updatedCivilClaimant) + }) + }) + + describe('civil claimant update fails', () => { + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockRejectedValue(new Error('Test error')) + + then = await givenWhenThen(caseId, civilClaimantId, {}) + }) + + it('should throw an error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toEqual('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts new file mode 100644 index 000000000000..d333af01f86f --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Update guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.update, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts new file mode 100644 index 000000000000..38a579850480 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts @@ -0,0 +1,35 @@ +import { + districtCourtAssistantRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Update rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [ + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.update, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts index 3b18ac9b8c8c..7ca4abf0ae2d 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts @@ -13,9 +13,12 @@ import { MessageService } from '@island.is/judicial-system/message' import { CaseService } from '../../case' import { CourtService } from '../../court' import { UserService } from '../../user' +import { CivilClaimantController } from '../civilClaimant.controller' +import { CivilClaimantService } from '../civilClaimant.service' import { DefendantController } from '../defendant.controller' import { DefendantService } from '../defendant.service' import { InternalDefendantController } from '../internalDefendant.controller' +import { CivilClaimant } from '../models/civilClaimant.model' import { Defendant } from '../models/defendant.model' jest.mock('@island.is/judicial-system/message') @@ -26,7 +29,11 @@ jest.mock('../../case/case.service') export const createTestingDefendantModule = async () => { const defendantModule = await Test.createTestingModule({ imports: [ConfigModule.forRoot({ load: [sharedAuthModuleConfig] })], - controllers: [DefendantController, InternalDefendantController], + controllers: [ + DefendantController, + InternalDefendantController, + CivilClaimantController, + ], providers: [ SharedAuthModule, MessageService, @@ -52,7 +59,19 @@ export const createTestingDefendantModule = async () => { findByPk: jest.fn(), }, }, + { + provide: getModelToken(CivilClaimant), + useValue: { + findOne: jest.fn(), + findAll: jest.fn(), + create: jest.fn(), + update: jest.fn(), + destroy: jest.fn(), + findByPk: jest.fn(), + }, + }, DefendantService, + CivilClaimantService, ], }).compile() @@ -77,6 +96,17 @@ export const createTestingDefendantModule = async () => { InternalDefendantController, ) + const civilClaimantModel = await defendantModule.resolve< + typeof CivilClaimant + >(getModelToken(CivilClaimant)) + + const civilClaimantService = + defendantModule.get(CivilClaimantService) + + const civilClaimantController = defendantModule.get( + CivilClaimantController, + ) + defendantModule.close() return { @@ -87,5 +117,8 @@ export const createTestingDefendantModule = async () => { defendantService, defendantController, internalDefendantController, + civilClaimantService, + civilClaimantController, + civilClaimantModel, } } diff --git a/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts index beca473bbbb5..5f47cbac54c6 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts @@ -50,7 +50,6 @@ import { } from '@island.is/judicial-system/types' import { - formatAdvocateAssignedEmailNotification, formatCourtHeadsUpSmsNotification, formatCourtIndictmentReadyForCourtEmailNotification, formatCourtOfAppealJudgeAssignedEmailNotification, @@ -1506,17 +1505,7 @@ export class CaseNotificationService extends BaseNotificationService { if (!advocateEmail) { return false } - if (isIndictmentCase(theCase.type)) { - const hasSentNotificationBefore = this.hasReceivedNotification( - CaseNotificationType.ADVOCATE_ASSIGNED, - advocateEmail, - theCase.notifications, - ) - - if (hasSentNotificationBefore) { - return false - } - } else if (isInvestigationCase(theCase.type)) { + if (isInvestigationCase(theCase.type)) { const isDefenderIncludedInSessionArrangements = theCase.sessionArrangements && [ @@ -1527,7 +1516,7 @@ export class CaseNotificationService extends BaseNotificationService { if (!isDefenderIncludedInSessionArrangements) { return false } - } else { + } else if (isRequestCase(theCase.type)) { const hasDefenderBeenNotified = this.hasReceivedNotification( [ CaseNotificationType.READY_FOR_COURT, @@ -1546,70 +1535,12 @@ export class CaseNotificationService extends BaseNotificationService { return true } - private sendAdvocateAssignedNotification( - theCase: Case, - advocateType: AdvocateType, - advocateNationalId?: string, - advocateName?: string, - advocateEmail?: string, - ): Promise { - const { subject, body } = formatAdvocateAssignedEmailNotification( - this.formatMessage, - theCase, - advocateType, - advocateNationalId && - formatDefenderRoute(this.config.clientUrl, theCase.type, theCase.id), - ) - - return this.sendEmail( - subject, - body, - advocateName, - advocateEmail, - undefined, - Boolean(advocateNationalId) === false, - ) - } - private async sendAdvocateAssignedNotifications( theCase: Case, ): Promise { const promises: Promise[] = [] - if (isIndictmentCase(theCase.type)) { - if (theCase.civilClaimants) { - for (const civilClaimant of theCase.civilClaimants) { - const { - spokespersonEmail, - spokespersonIsLawyer, - spokespersonName, - spokespersonNationalId, - hasSpokesperson, - } = civilClaimant - - const shouldSend = - hasSpokesperson && - this.shouldSendAdvocateAssignedNotification( - theCase, - spokespersonEmail, - ) - - if (shouldSend === true) { - promises.push( - this.sendAdvocateAssignedNotification( - theCase, - spokespersonIsLawyer - ? AdvocateType.LAWYER - : AdvocateType.LEGAL_RIGHTS_PROTECTOR, - spokespersonNationalId, - spokespersonName, - spokespersonEmail, - ), - ) - } - } - } - } else if (DateLog.arraignmentDate(theCase.dateLogs)?.date) { + if (DateLog.arraignmentDate(theCase.dateLogs)?.date) { const shouldSend = this.shouldSendAdvocateAssignedNotification( theCase, theCase.defenderEmail, diff --git a/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.service.ts new file mode 100644 index 000000000000..c80250801348 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.service.ts @@ -0,0 +1,171 @@ +import { MessageDescriptor } from '@formatjs/intl' + +import { + Inject, + Injectable, + InternalServerErrorException, +} from '@nestjs/common' +import { InjectModel } from '@nestjs/sequelize' + +import { IntlService } from '@island.is/cms-translations' +import { EmailService } from '@island.is/email-service' +import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' +import { type ConfigType } from '@island.is/nest/config' + +import { DEFENDER_INDICTMENT_ROUTE } from '@island.is/judicial-system/consts' +import { capitalize } from '@island.is/judicial-system/formatters' +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +import { Case } from '../case' +import { CivilClaimant } from '../defendant' +import { EventService } from '../event' +import { DeliverResponse } from './models/deliver.response' +import { Notification, Recipient } from './models/notification.model' +import { BaseNotificationService } from './baseNotification.service' +import { strings } from './civilClaimantNotification.strings' +import { notificationModuleConfig } from './notification.config' + +@Injectable() +export class CivilClaimantNotificationService extends BaseNotificationService { + constructor( + @InjectModel(Notification) + notificationModel: typeof Notification, + @Inject(notificationModuleConfig.KEY) + config: ConfigType, + @Inject(LOGGER_PROVIDER) logger: Logger, + intlService: IntlService, + emailService: EmailService, + eventService: EventService, + ) { + super( + notificationModel, + emailService, + intlService, + config, + eventService, + logger, + ) + } + + private async sendEmails( + civilClaimant: CivilClaimant, + theCase: Case, + notificationType: CivilClaimantNotificationType, + subject: MessageDescriptor, + body: MessageDescriptor, + ) { + const courtName = capitalize(theCase.court?.name) + const courtCaseNumber = theCase.courtCaseNumber + const spokespersonHasAccessToRVG = !!civilClaimant.spokespersonNationalId + + const formattedSubject = this.formatMessage(subject, { + courtName, + courtCaseNumber, + }) + + const formattedBody = this.formatMessage(body, { + courtName, + courtCaseNumber, + spokespersonHasAccessToRVG, + spokespersonIsLawyer: civilClaimant.spokespersonIsLawyer, + linkStart: ``, + linkEnd: '', + }) + const promises: Promise[] = [] + + if (civilClaimant.isSpokespersonConfirmed) { + promises.push( + this.sendEmail( + formattedSubject, + formattedBody, + civilClaimant.spokespersonName, + civilClaimant.spokespersonEmail, + undefined, + true, + ), + ) + } + + const recipients = await Promise.all(promises) + + return this.recordNotification(theCase.id, notificationType, recipients) + } + + private shouldSendSpokespersonAssignedNotification( + theCase: Case, + civilClaimant: CivilClaimant, + ): boolean { + if ( + !civilClaimant.spokespersonEmail || + !civilClaimant.isSpokespersonConfirmed + ) { + return false + } + + const hasSentNotificationBefore = this.hasReceivedNotification( + CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + civilClaimant.spokespersonEmail, + theCase.notifications, + ) + + if (!hasSentNotificationBefore) { + return true + } + + return false + } + + private async sendSpokespersonAssignedNotification( + civilClaimant: CivilClaimant, + theCase: Case, + ): Promise { + const shouldSend = this.shouldSendSpokespersonAssignedNotification( + theCase, + civilClaimant, + ) + + if (shouldSend) { + return this.sendEmails( + civilClaimant, + theCase, + CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + strings.civilClaimantSpokespersonAssignedSubject, + strings.civilClaimantSpokespersonAssignedBody, + ) + } + + // Nothing should be sent so we return a successful response + return { delivered: true } + } + + private sendNotification( + notificationType: CivilClaimantNotificationType, + civilClaimant: CivilClaimant, + theCase: Case, + ): Promise { + switch (notificationType) { + case CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED: + return this.sendSpokespersonAssignedNotification(civilClaimant, theCase) + default: + throw new InternalServerErrorException( + `Invalid notification type: ${notificationType}`, + ) + } + } + + async sendCivilClaimantNotification( + type: CivilClaimantNotificationType, + civilClaimant: CivilClaimant, + theCase: Case, + ): Promise { + await this.refreshFormatMessage() + + try { + return await this.sendNotification(type, civilClaimant, theCase) + } catch (error) { + this.logger.error('Failed to send notification', error) + + return { delivered: false } + } + } +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.strings.ts b/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.strings.ts new file mode 100644 index 000000000000..d9beac015345 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.strings.ts @@ -0,0 +1,17 @@ +import { defineMessage } from '@formatjs/intl' + +export const strings = { + civilClaimantSpokespersonAssignedSubject: defineMessage({ + id: 'judicial.system.backend:civil_claimant_notifications.spokesperson_assigned_subject', + defaultMessage: '{courtName} - aðgangur að máli', + description: + 'Subject of the notification when a civil claimant spokesperson is assigned and confirmed', + }), + civilClaimantSpokespersonAssignedBody: defineMessage({ + id: 'judicial.system.backend:civil_claimant_notifications.indictment_assigned_body', + defaultMessage: + '{courtName} hefur skráð þig {spokespersonIsLawyer, select, true {lögmann einkaréttarkröfuhafa} other {réttargæslumann einkaréttarkröfuhafa}} í máli {courtCaseNumber}.

{spokespersonHasAccessToRVG, select, true {Sjá nánar á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast málið hjá dómstólnum.}}.', + description: + 'Body of the notification when a civil claimant spokesperson is assigned and confirmed', + }), +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts b/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts new file mode 100644 index 000000000000..5557dbdfe354 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts @@ -0,0 +1,12 @@ +import { IsEnum, IsNotEmpty } from 'class-validator' + +import { ApiProperty } from '@nestjs/swagger' + +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +export class CivilClaimantNotificationDto { + @IsNotEmpty() + @IsEnum(CivilClaimantNotificationType) + @ApiProperty({ enum: CivilClaimantNotificationType }) + readonly type!: CivilClaimantNotificationType +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts index 53237a099dd2..c0e2ca1a0003 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts @@ -18,15 +18,24 @@ import { } from '@island.is/judicial-system/message' import { Case, CaseHasExistedGuard, CurrentCase } from '../case' -import { CurrentDefendant, Defendant, DefendantExistsGuard } from '../defendant' +import { + CivilClaimant, + CivilClaimantExistsGuard, + CurrentCivilClaimant, + CurrentDefendant, + Defendant, + DefendantExistsGuard, +} from '../defendant' import { SubpoenaExistsGuard } from '../subpoena' import { CaseNotificationDto } from './dto/caseNotification.dto' +import { CivilClaimantNotificationDto } from './dto/civilClaimantNotification.dto' import { DefendantNotificationDto } from './dto/defendantNotification.dto' import { InstitutionNotificationDto } from './dto/institutionNotification.dto' import { NotificationDispatchDto } from './dto/notificationDispatch.dto' import { SubpoenaNotificationDto } from './dto/subpoenaNotification.dto' import { DeliverResponse } from './models/deliver.response' import { CaseNotificationService } from './caseNotification.service' +import { CivilClaimantNotificationService } from './civilClaimantNotification.service' import { DefendantNotificationService } from './defendantNotification.service' import { InstitutionNotificationService } from './institutionNotification.service' import { NotificationDispatchService } from './notificationDispatch.service' @@ -42,6 +51,7 @@ export class InternalNotificationController { private readonly institutionNotificationService: InstitutionNotificationService, private readonly subpoenaNotificationService: SubpoenaNotificationService, private readonly defendantNotificationService: DefendantNotificationService, + private readonly civilClaimantNotificationService: CivilClaimantNotificationService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -123,6 +133,34 @@ export class InternalNotificationController { ) } + @Post( + `case/:caseId/${ + messageEndpoint[MessageType.CIVIL_CLAIMANT_NOTIFICATION] + }/:civilClaimantId`, + ) + @UseGuards(CaseHasExistedGuard, CivilClaimantExistsGuard) + @ApiCreatedResponse({ + type: DeliverResponse, + description: 'Sends civil claimant related notifications', + }) + sendCivilClaimantNotification( + @Param('caseId') caseId: string, + @Param('civilClaimantId') civilClaimantId: string, + @CurrentCase() theCase: Case, + @CurrentCivilClaimant() civilClaimant: CivilClaimant, + @Body() notificationDto: CivilClaimantNotificationDto, + ): Promise { + this.logger.debug( + `Sending ${notificationDto.type} notification for civil claimant ${civilClaimantId} and case ${caseId}`, + ) + + return this.civilClaimantNotificationService.sendCivilClaimantNotification( + notificationDto.type, + civilClaimant, + theCase, + ) + } + @Post(messageEndpoint[MessageType.NOTIFICATION_DISPATCH]) @ApiCreatedResponse({ type: DeliverResponse, diff --git a/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts b/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts index 6dcb837ace05..14fb32c0a565 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts @@ -18,6 +18,7 @@ import { } from '../index' import { Notification } from './models/notification.model' import { CaseNotificationService } from './caseNotification.service' +import { CivilClaimantNotificationService } from './civilClaimantNotification.service' import { DefendantNotificationService } from './defendantNotification.service' import { InstitutionNotificationService } from './institutionNotification.service' import { InternalNotificationController } from './internalNotification.controller' @@ -43,12 +44,13 @@ import { SubpoenaNotificationService } from './subpoenaNotification.service' ], controllers: [NotificationController, InternalNotificationController], providers: [ - NotificationService, CaseNotificationService, - NotificationDispatchService, + CivilClaimantNotificationService, + DefendantNotificationService, InstitutionNotificationService, + NotificationService, + NotificationDispatchService, SubpoenaNotificationService, - DefendantNotificationService, ], }) export class NotificationModule {} diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts index a269d181001b..267327b2235b 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts @@ -25,6 +25,7 @@ import { eventModuleConfig, EventService } from '../../event' import { InstitutionService } from '../../institution' import { UserService } from '../../user' import { CaseNotificationService } from '../caseNotification.service' +import { CivilClaimantNotificationService } from '../civilClaimantNotification.service' import { DefendantNotificationService } from '../defendantNotification.service' import { InstitutionNotificationService } from '../institutionNotification.service' import { InternalNotificationController } from '../internalNotification.controller' @@ -104,6 +105,7 @@ export const createTestingNotificationModule = async () => { NotificationDispatchService, InstitutionNotificationService, DefendantNotificationService, + CivilClaimantNotificationService, ], }) .useMocker((token) => { diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts new file mode 100644 index 000000000000..42bf7c2a0ea8 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts @@ -0,0 +1,167 @@ +import { uuid } from 'uuidv4' + +import { EmailService } from '@island.is/email-service' +import { ConfigType } from '@island.is/nest/config' + +import { DEFENDER_INDICTMENT_ROUTE } from '@island.is/judicial-system/consts' +import { + CaseType, + CivilClaimantNotificationType, +} from '@island.is/judicial-system/types' + +import { createTestingNotificationModule } from '../../createTestingNotificationModule' + +import { Case } from '../../../../case' +import { CivilClaimant } from '../../../../defendant' +import { CivilClaimantNotificationDto } from '../../../dto/civilClaimantNotification.dto' +import { DeliverResponse } from '../../../models/deliver.response' +import { Notification } from '../../../models/notification.model' +import { notificationModuleConfig } from '../../../notification.config' + +jest.mock('../../../../../factories') + +interface Then { + result: DeliverResponse + error: Error +} + +type GivenWhenThen = ( + caseId: string, + civilClaimantId: string, + theCase: Case, + civilClaimant: CivilClaimant, + notificationDto: CivilClaimantNotificationDto, +) => Promise + +describe('InternalNotificationController - Send spokesperson assigned notifications', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const court = { name: 'Héraðsdómur Reykjavíkur' } as Case['court'] + + let mockEmailService: EmailService + let mockConfig: ConfigType + let mockNotificationModel: typeof Notification + let givenWhenThen: GivenWhenThen + + let civilClaimantNotificationDTO: CivilClaimantNotificationDto + + beforeEach(async () => { + const { + emailService, + notificationConfig, + internalNotificationController, + notificationModel, + } = await createTestingNotificationModule() + + civilClaimantNotificationDTO = { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + } + + mockEmailService = emailService + mockConfig = notificationConfig + mockNotificationModel = notificationModel + + givenWhenThen = async ( + caseId: string, + civilClaimantId: string, + theCase: Case, + civilClaimant: CivilClaimant, + notificationDto: CivilClaimantNotificationDto, + ) => { + const then = {} as Then + + try { + then.result = + await internalNotificationController.sendCivilClaimantNotification( + caseId, + civilClaimantId, + theCase, + civilClaimant, + notificationDto, + ) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe.each([ + { isSpokespersonConfirmed: true, shouldSendEmail: true }, + { isSpokespersonConfirmed: false, shouldSendEmail: false }, + ])( + 'when sending a spokesperson assigned notification', + ({ isSpokespersonConfirmed, shouldSendEmail }) => { + const civilClaimant = { + id: civilClaimantId, + caseId, + isSpokespersonConfirmed, + spokespersonIsLawyer: true, + spokespersonNationalId: '1234567890', + spokespersonName: 'Ben 10', + spokespersonEmail: 'ben10@omnitrix.is', + } as CivilClaimant + + beforeEach(async () => { + await givenWhenThen( + caseId, + civilClaimantId, + { + id: caseId, + court, + courtCaseNumber: 'R-123-456', + type: CaseType.INDICTMENT, + civilClaimants: [civilClaimant], + hasCivilClaims: true, + } as Case, + civilClaimant, + civilClaimantNotificationDTO, + ) + }) + + test(`should ${ + shouldSendEmail ? '' : 'not ' + }send a spokesperson assigned notification`, async () => { + if (shouldSendEmail) { + expect(mockEmailService.sendEmail).toBeCalledTimes(1) + expect(mockEmailService.sendEmail).toBeCalledWith({ + from: { + name: mockConfig.email.fromName, + address: mockConfig.email.fromEmail, + }, + to: [ + { + name: civilClaimant.spokespersonName, + address: civilClaimant.spokespersonEmail, + }, + ], + replyTo: { + name: mockConfig.email.replyToName, + address: mockConfig.email.replyToEmail, + }, + attachments: undefined, + subject: `Héraðsdómur Reykjavíkur - aðgangur að máli`, + html: expect.stringContaining(DEFENDER_INDICTMENT_ROUTE), + text: expect.stringContaining( + `Héraðsdómur Reykjavíkur hefur skráð þig lögmann einkaréttarkröfuhafa í máli R-123-456`, + ), + }) + expect(mockNotificationModel.create).toHaveBeenCalledTimes(1) + expect(mockNotificationModel.create).toHaveBeenCalledWith({ + caseId, + type: civilClaimantNotificationDTO.type, + recipients: [ + { + address: civilClaimant.spokespersonEmail, + success: shouldSendEmail, + }, + ], + }) + } else { + expect(mockEmailService.sendEmail).not.toBeCalled() + } + }) + }, + ) +}) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts index eff36227b407..d439505025a9 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts @@ -83,120 +83,6 @@ describe('InternalNotificationController - Send defender assigned notifications' } }) - describe('when the case has civil claims and the advocate is a lawyer', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: true, - spokespersonNationalId: '1234567890', - spokespersonEmail: 'recipient@gmail.com', - spokespersonName: 'John Doe', - spokespersonIsLawyer: true, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(1) - expect(mockEmailService.sendEmail).toHaveBeenCalledWith({ - from: { - name: mockConfig.email.fromName, - address: mockConfig.email.fromEmail, - }, - to: [ - { - name: civilClaimant.spokespersonName, - address: civilClaimant.spokespersonEmail, - }, - ], - replyTo: { - name: mockConfig.email.replyToName, - address: mockConfig.email.replyToEmail, - }, - attachments: undefined, - subject: `Skráning í máli ${theCase.courtCaseNumber}`, - text: expect.anything(), // same as html but stripped html tags - html: `Héraðsdómur Reykjavíkur hefur skráð þig lögmann einkaréttarkröfuhafa í máli ${theCase.courtCaseNumber}.

Sjá nánar á yfirlitssíðu málsins í Réttarvörslugátt.`, - }) - }) - }) - - describe('when the case has civil claims and the advocate is a legal rights protector', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: true, - spokespersonNationalId: '1234567890', - spokespersonEmail: 'recipient@gmail.com', - spokespersonName: 'John Doe', - spokespersonIsLawyer: false, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(1) - expect(mockEmailService.sendEmail).toHaveBeenCalledWith({ - from: { - name: mockConfig.email.fromName, - address: mockConfig.email.fromEmail, - }, - to: [ - { - name: civilClaimant.spokespersonName, - address: civilClaimant.spokespersonEmail, - }, - ], - replyTo: { - name: mockConfig.email.replyToName, - address: mockConfig.email.replyToEmail, - }, - attachments: undefined, - subject: `Skráning í máli ${theCase.courtCaseNumber}`, - text: expect.anything(), // same as html but stripped html tags - html: `Héraðsdómur Reykjavíkur hefur skráð þig réttargæslumann einkaréttarkröfuhafa í máli ${theCase.courtCaseNumber}.

Sjá nánar á yfirlitssíðu málsins í Réttarvörslugátt.`, - }) - }) - }) - - describe('when the case has civil claims and civil claimant does not have representation', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: false, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).not.toHaveBeenCalled() - }) - }) - describe('when sending assigned defender notifications in a restriction case', () => { const caseId = uuid() const theCase = { diff --git a/libs/judicial-system/message/src/lib/message.ts b/libs/judicial-system/message/src/lib/message.ts index 4cccc3b48615..c8650bc09256 100644 --- a/libs/judicial-system/message/src/lib/message.ts +++ b/libs/judicial-system/message/src/lib/message.ts @@ -30,6 +30,7 @@ export enum MessageType { INSTITUTION_NOTIFICATION = 'INSTITUTION_NOTIFICATION', NOTIFICATION_DISPATCH = 'NOTIFICATION_DISPATCH', DEFENDANT_NOTIFICATION = 'DEFENDANT_NOTIFICATION', + CIVIL_CLAIMANT_NOTIFICATION = 'CIVIL_CLAIMANT_NOTIFICATION', } export const messageEndpoint: { [key in MessageType]: string } = { @@ -66,6 +67,7 @@ export const messageEndpoint: { [key in MessageType]: string } = { INSTITUTION_NOTIFICATION: 'institutionNotification', NOTIFICATION_DISPATCH: 'notification/dispatch', DEFENDANT_NOTIFICATION: 'defendantNotification', + CIVIL_CLAIMANT_NOTIFICATION: 'civilClaimantNotification', } export type Message = { diff --git a/libs/judicial-system/types/src/index.ts b/libs/judicial-system/types/src/index.ts index 494e5067a5e2..4357ea6ad607 100644 --- a/libs/judicial-system/types/src/index.ts +++ b/libs/judicial-system/types/src/index.ts @@ -18,6 +18,7 @@ export { InstitutionNotificationType, NotificationDispatchType, DefendantNotificationType, + CivilClaimantNotificationType, notificationTypes, } from './lib/notification' export type { Institution } from './lib/institution' diff --git a/libs/judicial-system/types/src/lib/notification.ts b/libs/judicial-system/types/src/lib/notification.ts index 981ab499ff70..6c449ffc196e 100644 --- a/libs/judicial-system/types/src/lib/notification.ts +++ b/libs/judicial-system/types/src/lib/notification.ts @@ -30,6 +30,10 @@ export enum SubpoenaNotificationType { SERVICE_FAILED = 'SERVICE_FAILED', } +export enum CivilClaimantNotificationType { + SPOKESPERSON_ASSIGNED = 'SPOKESPERSON_ASSIGNED', +} + export enum InstitutionNotificationType { INDICTMENTS_WAITING_FOR_CONFIRMATION = 'INDICTMENTS_WAITING_FOR_CONFIRMATION', } @@ -58,6 +62,7 @@ export enum NotificationType { DEFENDER_ASSIGNED = DefendantNotificationType.DEFENDER_ASSIGNED, SERVICE_SUCCESSFUL = SubpoenaNotificationType.SERVICE_SUCCESSFUL, SERVICE_FAILED = SubpoenaNotificationType.SERVICE_FAILED, + SPOKESPERSON_ASSIGNED = CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, INDICTMENTS_WAITING_FOR_CONFIRMATION = InstitutionNotificationType.INDICTMENTS_WAITING_FOR_CONFIRMATION, } From 4f478138d76a296046175ce767bb5b51bca26661 Mon Sep 17 00:00:00 2001 From: jonarnarbriem <107482569+jonarnarbriem@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:52:33 +0000 Subject: [PATCH 05/34] chore(codeowners): modify ownership of health directorate clients (#16904) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7aa4c0b9d758..df1c36fb075a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -195,7 +195,8 @@ codemagic.yaml /libs/portals/admin/regulations-admin/ @island-is/hugsmidjan /libs/portals/admin/document-provider/ @island-is/hugsmidjan @island-is/core /libs/clients/icelandic-health-insurance/rights-portal/ @island-is/hugsmidjan -/libs/clients/health-directorate @island-is/hugsmidjan @island-is/origo +/libs/clients/health-directorate @island-is/hugsmidjan +/libs/clients/health-directorate/src/lib/clients/occupational-license @island-is/hugsmidjan @island-is/origo /libs/clients/mms/grade @island-is/hugsmidjan /libs/portals/admin/air-discount-scheme @island-is/hugsmidjan /libs/application/templates/official-journal-of-iceland/ @island-is/hugsmidjan From b8905ab79d5b836f0c0983eb2c5ed996ea17fd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Fri, 15 Nov 2024 17:06:58 +0000 Subject: [PATCH 06/34] feat(native-app): add organ donation to app (#16401) * feat: add organ donation to app * feat: better usage of limitations string * fix: send locale into organ donor query * fix: improve type safety and null checks * fix: use correct res for organ donation * fix: create organDonationData * feat: use error component for organ donation as well * fix: update english translation for third party error --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../app/src/graphql/queries/health.graphql | 16 ++++ apps/native/app/src/messages/en.ts | 16 +++- apps/native/app/src/messages/is.ts | 14 ++++ .../src/screens/health/health-overview.tsx | 84 ++++++++++++++++++- 4 files changed, 126 insertions(+), 4 deletions(-) diff --git a/apps/native/app/src/graphql/queries/health.graphql b/apps/native/app/src/graphql/queries/health.graphql index d0a5d93185da..ee4446c8f783 100644 --- a/apps/native/app/src/graphql/queries/health.graphql +++ b/apps/native/app/src/graphql/queries/health.graphql @@ -77,3 +77,19 @@ query GetVaccinations($locale: String) { } } } + +query GetOrganDonorStatus($locale: String) { + healthDirectorateOrganDonation(locale: $locale) { + donor { + isDonor + limitations { + hasLimitations + limitedOrgansList { + id + name + } + comment + } + } + } +} diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index 17260fc7d745..24a6e3413d63 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -594,7 +594,7 @@ export const en: TranslatedMessages = { 'problem.offline.title': 'No internet connection', 'problem.offline.message': 'An error occurred while communicating with the service provider', - 'problem.thirdParty.title': 'No connection', + 'problem.thirdParty.title': 'Service unreachable', 'problem.thirdParty.message': 'An error occurred while communicating with the service provider', @@ -662,4 +662,18 @@ export const en: TranslatedMessages = { 'health.vaccinations.noVaccinationsDescription': 'If you believe you have data that should appear here, please contact service provider.', 'health.vaccinations.directorateOfHealth': 'The directorate of Health', + + // health - organ donation + 'health.organDonation': 'Organ Donation', + 'health.organDonation.change': 'Breyta afstöðu', + 'health.organDonation.isDonor': 'Ég er líffæragjafi', + 'health.organDonation.isDonorWithLimitations': + 'Ég heimila líffæragjöf, með takmörkunum.', + 'health.organDonation.isNotDonor': 'Ég heimila ekki líffæragjöf', + 'health.organDonation.isDonorDescription': + 'Öll líffærin mín má nota til ígræðslu.', + 'health.organDonation.isNotDonorDescription': + 'Engin líffæri má nota til ígræðslu.', + 'health.organDonation.isDonorWithLimitationsDescription': + 'Öll líffærin mín má nota til ígræðslu fyrir utan: {limitations}.', } diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index bd6967d93b48..a0ee501f24b1 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -663,4 +663,18 @@ export const is = { 'health.vaccinations.noVaccinationsDescription': 'Ef þú telur þig eiga gögn sem ættu að birtast hér, vinsamlegast hafðu samband við þjónustuaðila.', 'health.vaccinations.directorateOfHealth': 'Embætti landlæknis', + + // health - organ donation + 'health.organDonation': 'Líffæragjöf', + 'health.organDonation.change': 'Breyta afstöðu', + 'health.organDonation.isDonor': 'Ég er líffæragjafi', + 'health.organDonation.isDonorWithLimitations': + 'Ég heimila líffæragjöf, með takmörkunum.', + 'health.organDonation.isNotDonor': 'Ég heimila ekki líffæragjöf', + 'health.organDonation.isDonorDescription': + 'Öll líffærin mín má nota til ígræðslu.', + 'health.organDonation.isNotDonorDescription': + 'Engin líffæri má nota til ígræðslu.', + 'health.organDonation.isDonorWithLimitationsDescription': + 'Öll líffærin mín má nota til ígræðslu fyrir utan: {limitations}.', } diff --git a/apps/native/app/src/screens/health/health-overview.tsx b/apps/native/app/src/screens/health/health-overview.tsx index 2c6778c7fb34..740cac1bbb45 100644 --- a/apps/native/app/src/screens/health/health-overview.tsx +++ b/apps/native/app/src/screens/health/health-overview.tsx @@ -26,6 +26,7 @@ import { useGetHealthCenterQuery, useGetHealthInsuranceOverviewQuery, useGetMedicineDataQuery, + useGetOrganDonorStatusQuery, useGetPaymentOverviewQuery, useGetPaymentStatusQuery, } from '../../graphql/types/schema' @@ -36,6 +37,7 @@ import { useBrowser } from '../../lib/use-browser' import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' import { navigateTo } from '../../lib/deep-linking' import { useFeatureFlag } from '../../contexts/feature-flag-provider' +import { useLocale } from '../../hooks/use-locale' const Host = styled(SafeAreaView)` padding-horizontal: ${({ theme }) => theme.spacing[2]}px; @@ -52,10 +54,15 @@ const ButtonWrapper = styled.View` interface HeadingSectionProps { title: string + linkTextId?: string onPress: () => void } -const HeadingSection: React.FC = ({ title, onPress }) => { +const HeadingSection: React.FC = ({ + title, + onPress, + linkTextId, +}) => { const theme = useTheme() return ( @@ -74,7 +81,7 @@ const HeadingSection: React.FC = ({ title, onPress }) => { color={theme.color.blue400} style={{ marginRight: 4 }} > - + @@ -111,10 +118,17 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ const { width } = useWindowDimensions() const buttonStyle = { flex: 1, minWidth: width * 0.5 - theme.spacing[3] } const isVaccinationsEnabled = useFeatureFlag('isVaccinationsEnabled', false) + const isOrganDonationEnabled = useFeatureFlag('isOrganDonationEnabled', false) const now = useMemo(() => new Date().toISOString(), []) const medicinePurchaseRes = useGetMedicineDataQuery() + const organDonationRes = useGetOrganDonorStatusQuery({ + variables: { + locale: useLocale(), + }, + skip: !isOrganDonationEnabled, + }) const healthInsuranceRes = useGetHealthInsuranceOverviewQuery() const healthCenterRes = useGetHealthCenterQuery() const paymentStatusRes = useGetPaymentStatusQuery() @@ -137,12 +151,25 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ const paymentStatusData = paymentStatusRes.data?.rightsPortalCopaymentStatus const paymentOverviewData = paymentOverviewRes.data?.rightsPortalPaymentOverview?.items?.[0] + const organDonationData = + organDonationRes.data?.healthDirectorateOrganDonation.donor const isMedicinePeriodActive = medicinePurchaseData?.active || (medicinePurchaseData?.dateTo && new Date(medicinePurchaseData.dateTo) > new Date()) + const isOrganDonor = organDonationData?.isDonor ?? false + + const isOrganDonorWithLimitations = + isOrganDonor && (organDonationData?.limitations?.hasLimitations ?? false) + + const organLimitations = isOrganDonorWithLimitations + ? organDonationData?.limitations?.limitedOrgansList?.map( + (organ) => organ.name, + ) ?? [] + : [] + useConnectivityIndicator({ componentId, refetching, @@ -165,7 +192,8 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ healthCenterRes.refetch(), paymentStatusRes.refetch(), paymentOverviewRes.refetch(), - ] + isOrganDonationEnabled && organDonationRes.refetch(), + ].filter(Boolean) await Promise.all(promises) } catch (e) { // noop @@ -178,6 +206,8 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ healthCenterRes, paymentStatusRes, paymentOverviewRes, + organDonationRes, + isOrganDonationEnabled, ]) return ( @@ -506,6 +536,54 @@ export const HealthOverviewScreen: NavigationFunctionComponent = ({ {medicinePurchaseRes.error && !medicinePurchaseRes.data && showErrorComponent(medicinePurchaseRes.error)} + {isOrganDonationEnabled && ( + + openBrowser( + `${origin}/minarsidur/heilsa/liffaeragjof/skraning`, + componentId, + ) + } + /> + )} + {isOrganDonationEnabled && ( + + + + )} + {isOrganDonationEnabled && + organDonationRes.error && + !organDonationRes.data && + showErrorComponent(organDonationRes.error)}
From d89ae9c84ef5c7127b8e6409b8837e29dcfea4cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3hanna=20Magn=C3=BAsd=C3=B3ttir?= Date: Mon, 18 Nov 2024 10:02:46 +0000 Subject: [PATCH 07/34] fix(transport-authority): Handle all warnSever (E, L, W) + dummy mileage in vehicle validation (#16662) * Fix typo * Fix mileageReading vs isRequired Stop using mileageRequired from answers in getSelectedVehicle and always look at the value from externalData (it should not change) * Make sure to always use requiresMileageRegistration from currentvehicleswithmileageandinsp (not basicVehicleInformation) Cleanup make+vehcom (basicVehicleInformation) vs make (currentvehicles) * Cleanup * cleanup * cleanup * cleanup * Use shared function to extract messages from error body Display always E, L and W messages (but E and L first) * Use dummy mileage value when validating per vehicle (owner/operator change) * Catch error from mileage api * Fix the way validation errors are displayed in SGS ownerchange Allow all users to retry submit application if all approvals are finished * Apply same change to change co-owner + change operator Display ValidationErrorMessages always in overview, no matter who is reviewing * Cleanup * Cleanup in LicensePlateRenewal + OrderVehicleLicensePlate Add validation per plate if user has more than 5 * Fix the way vehicle subModel is displayed * Fixes after review * Fix the way errors are displayed for RenewLicensePlate Add MocablePayment * Add validation for OrderVehicleLicensePlate * Cleanup * Fix comment --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/graphql/dto/index.ts | 1 + .../graphql/dto/plateOrderAnswers.input.ts | 40 +++ .../src/lib/graphql/main.resolver.ts | 14 ++ .../src/lib/graphql/models/index.ts | 1 + .../models/plateOrderValidation.model.ts | 19 ++ .../src/lib/transportAuthority.service.ts | 83 +++++-- .../change-co-owner-of-vehicle.service.ts | 77 +++--- .../change-operator-of-vehicle.service.ts | 22 +- .../license-plate-renewal.service.ts | 98 ++++---- .../order-vehicle-license-plate.service.ts | 231 ++++++++++++------ .../transfer-of-vehicle-ownership.service.ts | 77 +++--- .../src/dataProviders/index.ts | 9 + .../src/fields/ApplicationStatus/index.tsx | 4 +- .../src/fields/Overview/index.tsx | 42 +++- .../fields/ValidationErrorMessages/index.tsx | 37 ++- .../InformationSection/vehicleSubSection.ts | 27 +- .../externalDataSection.ts | 5 + .../src/lib/ChangeCoOwnerOfVehicleTemplate.ts | 6 +- .../src/lib/messages/review.ts | 7 +- .../src/utils/canReviewerApprove.ts | 65 +++++ .../src/utils/getSelectedVehicle.ts | 2 +- .../src/utils/hasReviewerApproved.ts | 38 --- .../src/utils/index.ts | 2 +- .../src/utils/isLastReviewer.ts | 57 +++-- .../src/dataProviders/index.ts | 9 + .../src/fields/ApplicationStatus/index.tsx | 4 +- .../src/fields/Overview/index.tsx | 42 +++- .../fields/ValidationErrorMessages/index.tsx | 37 ++- .../InformationSection/vehicleSubSection.ts | 18 +- .../externalDataSection.ts | 5 + .../lib/ChangeOperatorOfVehicleTemplate.ts | 6 +- .../src/lib/messages/review.ts | 7 +- .../src/utils/canReviewerApprove.ts | 65 +++++ .../src/utils/getSelectedVehicle.ts | 2 +- .../src/utils/hasReviewerApproved.ts | 38 --- .../src/utils/index.ts | 2 +- .../src/utils/isLastReviewer.ts | 57 +++-- .../src/dataProviders/index.ts | 9 + .../src/fields/PlateField/PlateRadioField.tsx | 19 +- .../fields/PlateField/PlateSelectField.tsx | 80 ++++-- .../informationSubSection.ts | 27 +- .../prerequisitesSection.ts | 5 + .../src/hooks/useLazyPlateDetails.ts | 19 ++ .../src/hooks/useLazyQuery.ts | 24 ++ .../license-plate-renewal/src/index.ts | 1 + .../src/lib/LicensePlateRenewalTemplate.ts | 5 +- .../src/lib/messages/applicationCheck.ts | 16 ++ .../src/lib/messages/error.ts | 5 + .../src/lib/messages/index.ts | 1 + .../src/lib/messages/information.ts | 5 - .../src/shared/constants.ts | 1 - .../src/utils/checkCanRenew.ts | 6 + .../src/utils/getSelectedVehicle.ts | 2 +- .../license-plate-renewal/src/utils/index.ts | 1 + .../src/dataProviders/index.ts | 9 + .../src/fields/PickPlateSize.tsx | 18 +- .../fields/ValidationErrorMessages/index.tsx | 104 ++++++++ .../VehiclesField/VehicleSelectField.tsx | 8 +- .../src/fields/index.ts | 1 + .../InformationSection/plateSizeSubSection.ts | 5 +- .../OrderVehicleLicensePlateForm/index.ts | 10 +- .../paymentSection.ts | 71 ++++++ .../prerequisitesSection.ts | 5 + .../src/graphql/queries.ts | 12 + .../order-vehicle-license-plate/src/index.ts | 1 + .../lib/OrderVehicleLicensePlateTemplate.ts | 5 + .../src/lib/messages/applicationCheck.ts | 16 ++ .../src/lib/messages/index.ts | 1 + .../src/lib/messages/payment.ts | 20 ++ .../src/shared/constants.ts | 1 + .../src/utils/getSelectedVehicle.ts | 4 +- .../src/dataProviders/index.ts | 9 + .../src/fields/MainOwner.tsx | 7 +- .../InformationSection/vehicleSubSection.ts | 30 +-- .../prerequisitesSection.ts | 5 + ...rVehicleRegistrationCertificateTemplate.ts | 2 + .../src/utils/getSelectedVehicle.ts | 2 +- .../src/fields/ApplicationStatus/index.tsx | 4 +- .../src/fields/CoOwner/index.tsx | 7 +- .../src/fields/Overview/index.tsx | 172 +++---------- .../fields/Overview/sections/BuyerSection.tsx | 4 +- .../Overview/sections/InsuranceSection.tsx | 4 +- .../fields/ValidationErrorMessages/index.tsx | 43 ++-- .../InformationSection/vehicleSubSection.ts | 26 +- .../lib/TransferOfVehicleOwnershipTemplate.ts | 4 +- .../src/lib/messages/review.ts | 7 +- .../src/lib/messages/steps.ts | 2 +- .../src/utils/canReviewerApprove.ts | 93 +++++++ .../src/utils/getSelectedVehicle.ts | 2 +- .../src/utils/hasReviewerApproved.ts | 64 ----- .../src/utils/index.ts | 2 +- .../src/utils/isLastReviewer.ts | 98 +++++--- .../src/lib/vehicleOperatorsClient.module.ts | 2 + .../src/lib/vehicleOperatorsClient.service.ts | 138 ++++++----- .../vehicle-owner-change/src/index.ts | 5 + .../lib/vehicleOwnerChangeClient.module.ts | 2 + .../lib/vehicleOwnerChangeClient.service.ts | 145 ++++++----- .../src/lib/vehicleOwnerChangeClient.utils.ts | 63 +++++ .../lib/vehiclePlateOrderingClient.service.ts | 120 +++++---- .../lib/vehiclePlateRenewalClient.service.ts | 74 +++--- 100 files changed, 1902 insertions(+), 1007 deletions(-) create mode 100644 libs/api/domains/transport-authority/src/lib/graphql/dto/plateOrderAnswers.input.ts create mode 100644 libs/api/domains/transport-authority/src/lib/graphql/models/plateOrderValidation.model.ts create mode 100644 libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/canReviewerApprove.ts delete mode 100644 libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/hasReviewerApproved.ts create mode 100644 libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/canReviewerApprove.ts delete mode 100644 libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/hasReviewerApproved.ts create mode 100644 libs/application/templates/transport-authority/license-plate-renewal/src/hooks/useLazyPlateDetails.ts create mode 100644 libs/application/templates/transport-authority/license-plate-renewal/src/hooks/useLazyQuery.ts create mode 100644 libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/applicationCheck.ts create mode 100644 libs/application/templates/transport-authority/license-plate-renewal/src/utils/checkCanRenew.ts create mode 100644 libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/ValidationErrorMessages/index.tsx create mode 100644 libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/paymentSection.ts create mode 100644 libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/applicationCheck.ts create mode 100644 libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/canReviewerApprove.ts delete mode 100644 libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/hasReviewerApproved.ts diff --git a/libs/api/domains/transport-authority/src/lib/graphql/dto/index.ts b/libs/api/domains/transport-authority/src/lib/graphql/dto/index.ts index 4ae849d54bcc..88d82dba0190 100644 --- a/libs/api/domains/transport-authority/src/lib/graphql/dto/index.ts +++ b/libs/api/domains/transport-authority/src/lib/graphql/dto/index.ts @@ -2,3 +2,4 @@ export * from './ownerChangeAnswers.input' export * from './operatorChangeAnswers.input' export * from './checkTachoNet.input' export * from './plateAvailability.input' +export * from './plateOrderAnswers.input' diff --git a/libs/api/domains/transport-authority/src/lib/graphql/dto/plateOrderAnswers.input.ts b/libs/api/domains/transport-authority/src/lib/graphql/dto/plateOrderAnswers.input.ts new file mode 100644 index 000000000000..b79fcc3117d0 --- /dev/null +++ b/libs/api/domains/transport-authority/src/lib/graphql/dto/plateOrderAnswers.input.ts @@ -0,0 +1,40 @@ +import { Field, InputType } from '@nestjs/graphql' + +@InputType() +export class PlateOrderAnswersPickVehicle { + @Field(() => String, { nullable: false }) + plate!: string +} + +@InputType() +export class PlateOrderAnswersPlateSize { + @Field(() => [String], { nullable: true }) + frontPlateSize?: string[] + + @Field(() => [String], { nullable: true }) + rearPlateSize?: string[] +} + +@InputType() +export class OperatorChangeAnswersPlateDelivery { + @Field(() => String, { nullable: true }) + deliveryMethodIsDeliveryStation?: string + + @Field(() => String, { nullable: true }) + deliveryStationTypeCode?: string + + @Field(() => [String], { nullable: true }) + includeRushFee?: string[] +} + +@InputType() +export class PlateOrderAnswers { + @Field(() => PlateOrderAnswersPickVehicle, { nullable: false }) + pickVehicle!: PlateOrderAnswersPickVehicle + + @Field(() => PlateOrderAnswersPlateSize, { nullable: false }) + plateSize!: PlateOrderAnswersPlateSize + + @Field(() => OperatorChangeAnswersPlateDelivery, { nullable: false }) + plateDelivery!: OperatorChangeAnswersPlateDelivery +} diff --git a/libs/api/domains/transport-authority/src/lib/graphql/main.resolver.ts b/libs/api/domains/transport-authority/src/lib/graphql/main.resolver.ts index e06ff583d9da..f1a4945921b7 100644 --- a/libs/api/domains/transport-authority/src/lib/graphql/main.resolver.ts +++ b/libs/api/domains/transport-authority/src/lib/graphql/main.resolver.ts @@ -15,6 +15,7 @@ import { CheckTachoNetInput, OperatorChangeAnswers, PlateAvailabilityInput, + PlateOrderAnswers, } from './dto' import { CheckTachoNetExists, @@ -25,6 +26,7 @@ import { VehiclePlateOrderChecksByPermno, MyPlateOwnershipChecksByRegno, PlateAvailability, + PlateOrderValidation, } from './models' import { CoOwnerChangeAnswers } from './dto/coOwnerChangeAnswers.input' @@ -123,6 +125,18 @@ export class MainResolver { ) } + @Scopes(ApiScope.samgongustofaVehicles) + @Query(() => PlateOrderValidation, { nullable: true }) + vehiclePlateOrderValidation( + @CurrentUser() user: User, + @Args('answers') answers: PlateOrderAnswers, + ) { + return this.transportAuthorityApi.validateApplicationForPlateOrder( + user, + answers, + ) + } + @Scopes(ApiScope.internal) @Query(() => MyPlateOwnershipChecksByRegno, { name: 'myPlateOwnershipChecksByRegno', diff --git a/libs/api/domains/transport-authority/src/lib/graphql/models/index.ts b/libs/api/domains/transport-authority/src/lib/graphql/models/index.ts index 89277ee12a7f..e9cbbe331e7a 100644 --- a/libs/api/domains/transport-authority/src/lib/graphql/models/index.ts +++ b/libs/api/domains/transport-authority/src/lib/graphql/models/index.ts @@ -3,3 +3,4 @@ export * from './operatorChangeValidation.model' export * from './checkTachoNetExists.model' export * from './getCurrentVehicles.model' export * from './plateAvailability.model' +export * from './plateOrderValidation.model' diff --git a/libs/api/domains/transport-authority/src/lib/graphql/models/plateOrderValidation.model.ts b/libs/api/domains/transport-authority/src/lib/graphql/models/plateOrderValidation.model.ts new file mode 100644 index 000000000000..4bf0b7cf3f0b --- /dev/null +++ b/libs/api/domains/transport-authority/src/lib/graphql/models/plateOrderValidation.model.ts @@ -0,0 +1,19 @@ +import { Field, ObjectType } from '@nestjs/graphql' + +@ObjectType() +export class PlateOrderValidationMessage { + @Field(() => String, { nullable: true }) + errorNo?: string | null + + @Field(() => String, { nullable: true }) + defaultMessage?: string | null +} + +@ObjectType() +export class PlateOrderValidation { + @Field(() => Boolean) + hasError!: boolean + + @Field(() => [PlateOrderValidationMessage], { nullable: true }) + errorMessages?: PlateOrderValidationMessage[] | null +} diff --git a/libs/api/domains/transport-authority/src/lib/transportAuthority.service.ts b/libs/api/domains/transport-authority/src/lib/transportAuthority.service.ts index f2fee3b09a5f..19c9e5fe9de0 100644 --- a/libs/api/domains/transport-authority/src/lib/transportAuthority.service.ts +++ b/libs/api/domains/transport-authority/src/lib/transportAuthority.service.ts @@ -3,7 +3,11 @@ import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' import { VehicleOwnerChangeClient } from '@island.is/clients/transport-authority/vehicle-owner-change' import { DigitalTachographDriversCardClient } from '@island.is/clients/transport-authority/digital-tachograph-drivers-card' import { VehicleOperatorsClient } from '@island.is/clients/transport-authority/vehicle-operators' -import { VehiclePlateOrderingClient } from '@island.is/clients/transport-authority/vehicle-plate-ordering' +import { + SGS_DELIVERY_STATION_CODE, + SGS_DELIVERY_STATION_TYPE, + VehiclePlateOrderingClient, +} from '@island.is/clients/transport-authority/vehicle-plate-ordering' import { VehiclePlateRenewalClient } from '@island.is/clients/transport-authority/vehicle-plate-renewal' import { VehicleServiceFjsV1Client } from '@island.is/clients/vehicle-service-fjs-v1' import { @@ -14,6 +18,7 @@ import { OwnerChangeAnswers, OperatorChangeAnswers, CheckTachoNetInput, + PlateOrderAnswers, } from './graphql/dto' import { OwnerChangeValidation, @@ -22,6 +27,7 @@ import { VehicleOwnerchangeChecksByPermno, VehicleOperatorChangeChecksByPermno, VehiclePlateOrderChecksByPermno, + PlateOrderValidation, } from './graphql/models' import { ApolloError } from 'apollo-server-express' import { CoOwnerChangeAnswers } from './graphql/dto/coOwnerChangeAnswers.input' @@ -109,6 +115,7 @@ export class TransportAuthorityApi { return { basicVehicleInformation: { permno: vehicle.permno, + // Note: subModel (vehcom+speccom) has already been added to this field make: vehicle.make, color: vehicle.colorName, requireMileage: vehicle.requiresMileageRegistration, @@ -134,12 +141,6 @@ export class TransportAuthorityApi { // the current timestamp const todayStr = new Date().toISOString() - // No need to continue with this validation in user is neither seller nor buyer - // (only time application data changes is on state change from these roles) - if (user.nationalId !== sellerSsn && user.nationalId !== buyerSsn) { - return null - } - const filteredBuyerCoOwnerAndOperator = answers?.buyerCoOwnerAndOperator?.filter( ({ wasRemoved }) => wasRemoved !== 'true', @@ -279,6 +280,7 @@ export class TransportAuthorityApi { : null, basicVehicleInformation: { color: vehicle.colorName, + // Note: subModel (vehcom+speccom) has already been added to this field make: vehicle.make, permno: vehicle.permno, requireMileage: vehicle.requiresMileageRegistration, @@ -291,13 +293,6 @@ export class TransportAuthorityApi { user: User, answers: OperatorChangeAnswers, ): Promise { - // No need to continue with this validation in user is not owner - // (only time application data changes is on state change from that role) - const ownerSsn = answers?.owner?.nationalId - if (user.nationalId !== ownerSsn) { - return null - } - const permno = answers?.pickVehicle?.plate const filteredOldOperators = answers?.oldOperators?.filter( @@ -347,12 +342,13 @@ export class TransportAuthorityApi { }) // Get validation - const validation = await this.vehiclePlateOrderingClient.validatePlateOrder( - auth, - permno, - vehicleInfo?.platetypefront || '', - vehicleInfo?.platetyperear || '', - ) + const validation = + await this.vehiclePlateOrderingClient.validateVehicleForPlateOrder( + auth, + permno, + vehicleInfo?.platetypefront || '', + vehicleInfo?.platetyperear || '', + ) return { validationErrorMessages: validation?.hasError @@ -360,12 +356,57 @@ export class TransportAuthorityApi { : null, basicVehicleInformation: { color: vehicleInfo.color, - make: `${vehicleInfo.make} ${vehicleInfo.vehcom}`, + make: `${vehicleInfo.make} ${this.getVehicleSubModel(vehicleInfo)}`, permno: vehicleInfo.permno, }, } } + private getVehicleSubModel(vehicle: BasicVehicleInformationDto) { + return [vehicle.vehcom, vehicle.speccom].filter(Boolean).join(' ') + } + + async validateApplicationForPlateOrder( + user: User, + answers: PlateOrderAnswers, + ): Promise { + const YES = 'yes' + + const includeRushFee = + answers?.plateDelivery?.includeRushFee?.includes(YES) || false + + // Check if used selected delivery method: Pick up at delivery station + const deliveryStationTypeCode = + answers?.plateDelivery?.deliveryStationTypeCode + let deliveryStationType: string + let deliveryStationCode: string + if ( + answers.plateDelivery?.deliveryMethodIsDeliveryStation === YES && + deliveryStationTypeCode + ) { + // Split up code+type (was merged when we fetched that data) + deliveryStationType = deliveryStationTypeCode.split('_')[0] + deliveryStationCode = deliveryStationTypeCode.split('_')[1] + } else { + // Otherwise we will default to option "Pick up at Samgöngustofa" + deliveryStationType = SGS_DELIVERY_STATION_TYPE + deliveryStationCode = SGS_DELIVERY_STATION_CODE + } + + const result = + await this.vehiclePlateOrderingClient.validateAllForPlateOrder( + user, + answers?.pickVehicle?.plate, + answers?.plateSize?.frontPlateSize?.[0] || '', + answers?.plateSize?.rearPlateSize?.[0] || '', + deliveryStationType, + deliveryStationCode, + includeRushFee, + ) + + return result + } + async getMyPlateOwnershipChecksByRegno( auth: User, regno: string, diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts index 9fc9f0c56b2d..fb0c2b9ec91e 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts @@ -101,7 +101,7 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { ) } - // A. vehicleCount > 20 + // Case: count > 20 // Display search box, validate vehicle when permno is entered if (totalRecords > 20) { return { @@ -114,13 +114,13 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { const vehicles = await Promise.all( resultData.map(async (vehicle) => { - // B. 20 >= vehicleCount > 5 + // Case: 20 >= count > 5 // Display dropdown, validate vehicle when selected in dropdown if (totalRecords > 5) { return this.mapVehicle(auth, vehicle, false) } - // C. vehicleCount <= 5 + // Case: count <= 5 // Display radio buttons, validate all vehicles now return this.mapVehicle(auth, vehicle, true) }), @@ -468,34 +468,51 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { const mileage = answers?.vehicleMileage?.value - await this.vehicleOwnerChangeClient.saveOwnerChange(auth, { - permno: permno, - seller: { - ssn: ownerSsn, - email: ownerEmail, - }, - buyer: { - ssn: ownerSsn, - email: ownerEmail, + const submitResult = await this.vehicleOwnerChangeClient.saveOwnerChange( + auth, + { + permno: permno, + seller: { + ssn: ownerSsn, + email: ownerEmail, + }, + buyer: { + ssn: ownerSsn, + email: ownerEmail, + }, + dateOfPurchase: new Date(application.created), + dateOfPurchaseTimestamp: createdStr.substring(11, createdStr.length), + saleAmount: currentOwnerChange?.saleAmount, + mileage: mileage ? Number(mileage) || 0 : null, + insuranceCompanyCode: currentOwnerChange?.insuranceCompanyCode, + operators: currentOperators?.map((operator) => ({ + ssn: operator.ssn || '', + // Note: It should be ok that the email we send in is empty, since we dont get + // the email when fetching current operators, and according to them (SGS), they + // are not using the operator email in their API (not being saved in their DB) + email: null, + isMainOperator: operator.isMainOperator || false, + })), + coOwners: filteredCoOwners.map((x) => ({ + ssn: x.nationalId || '', + email: x.email || '', + })), }, - dateOfPurchase: new Date(application.created), - dateOfPurchaseTimestamp: createdStr.substring(11, createdStr.length), - saleAmount: currentOwnerChange?.saleAmount, - mileage: mileage ? Number(mileage) || 0 : null, - insuranceCompanyCode: currentOwnerChange?.insuranceCompanyCode, - operators: currentOperators?.map((operator) => ({ - ssn: operator.ssn || '', - // Note: It should be ok that the email we send in is empty, since we dont get - // the email when fetching current operators, and according to them (SGS), they - // are not using the operator email in their API (not being saved in their DB) - email: null, - isMainOperator: operator.isMainOperator || false, - })), - coOwners: filteredCoOwners.map((x) => ({ - ssn: x.nationalId || '', - email: x.email || '', - })), - }) + ) + + if ( + submitResult.hasError && + submitResult.errorMessages && + submitResult.errorMessages.length > 0 + ) { + throw new TemplateApiError( + { + title: applicationCheck.validation.alertTitle, + summary: submitResult.errorMessages, + }, + 400, + ) + } // 3. Notify everyone in the process that the application has successfully been submitted diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts index fd9d7a62ffb9..62e407eb1094 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts @@ -99,7 +99,7 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { ) } - // A. vehicleCount > 20 + // Case: count > 20 // Display search box, validate vehicle when permno is entered if (totalRecords > 20) { return { @@ -112,13 +112,13 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { const vehicles = await Promise.all( resultData.map(async (vehicle) => { - // B. 20 >= vehicleCount > 5 + // Case: 20 >= count > 5 // Display dropdown, validate vehicle when selected in dropdown if (totalRecords > 5) { return this.mapVehicle(auth, vehicle, false) } - // C. vehicleCount <= 5 + // Case: count <= 5 // Display radio buttons, validate all vehicles now return this.mapVehicle(auth, vehicle, true) }), @@ -439,13 +439,27 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { const mileage = answers?.vehicleMileage?.value - await this.vehicleOperatorsClient.saveOperators( + const submitResult = await this.vehicleOperatorsClient.saveOperators( auth, permno, operators, mileage ? Number(mileage) || 0 : null, ) + if ( + submitResult.hasError && + submitResult.errorMessages && + submitResult.errorMessages.length > 0 + ) { + throw new TemplateApiError( + { + title: applicationCheck.validation.alertTitle, + summary: submitResult.errorMessages, + }, + 400, + ) + } + // 3. Notify everyone in the process that the application has successfully been submitted // 3a. Get list of users that need to be notified diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/license-plate-renewal/license-plate-renewal.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/license-plate-renewal/license-plate-renewal.service.ts index a53ab7eae0c5..7b2781e29cf8 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/license-plate-renewal/license-plate-renewal.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/license-plate-renewal/license-plate-renewal.service.ts @@ -3,7 +3,10 @@ import { SharedTemplateApiService } from '../../../shared' import { TemplateApiModuleActionProps } from '../../../../types' import { BaseTemplateApiService } from '../../../base-template-api.service' import { ApplicationTypes } from '@island.is/application/types' -import { LicensePlateRenewalAnswers } from '@island.is/application/templates/transport-authority/license-plate-renewal' +import { + applicationCheck, + LicensePlateRenewalAnswers, +} from '@island.is/application/templates/transport-authority/license-plate-renewal' import { PlateOwnership, PlateOwnershipValidation, @@ -12,6 +15,7 @@ import { import { TemplateApiError } from '@island.is/nest/problem' import { info } from 'kennitala' import { error } from '@island.is/application/templates/transport-authority/license-plate-renewal' +import { User } from '@island.is/auth-nest-tools' @Injectable() export class LicensePlateRenewalService extends BaseTemplateApiService { @@ -26,8 +30,10 @@ export class LicensePlateRenewalService extends BaseTemplateApiService { const result = await this.vehiclePlateRenewalClient.getMyPlateOwnerships( auth, ) + const totalRecords = (result && result.length) || 0 + // Validate that user has at least 1 plate ownership - if (!result || !result.length) { + if (!totalRecords) { throw new TemplateApiError( { title: error.plateOwnershipEmptyList, @@ -37,55 +43,46 @@ export class LicensePlateRenewalService extends BaseTemplateApiService { ) } - return await Promise.all( - result.map(async (item: PlateOwnership) => { - let validation: PlateOwnershipValidation | undefined - - // Only validate if fewer than 5 items - if (result.length <= 5) { - validation = - await this.vehiclePlateRenewalClient.validatePlateOwnership( - auth, - item.regno, - ) + const plateOwnerships = await Promise.all( + result.map(async (plateOwnership) => { + // Case: count > 5 + // Display dropdown, validate plate ownership when selected in dropdown + if (totalRecords > 5) { + return this.mapPlateOwnership(auth, plateOwnership, false) } - return { - regno: item.regno, - startDate: item.startDate, - endDate: item.endDate, - validationErrorMessages: validation?.hasError - ? validation.errorMessages - : null, - } + // Case: count <= 5 + // Display radio buttons, validate all plate ownerships now + return this.mapPlateOwnership(auth, plateOwnership, true) }), ) - } - async validateApplication({ - application, - auth, - }: TemplateApiModuleActionProps) { - const answers = application.answers as LicensePlateRenewalAnswers - const regno = answers?.pickPlate?.regno + return plateOwnerships + } - const result = await this.vehiclePlateRenewalClient.validatePlateOwnership( - auth, - regno, - ) + private async mapPlateOwnership( + auth: User, + plateOwnership: PlateOwnership, + fetchExtraData: boolean, + ) { + let validation: PlateOwnershipValidation | undefined - // If we get any error messages, we will just throw an error with a default title - // We will fetch these error messages again through graphql in the template, to be able - // to translate the error message - if (result.hasError && result.errorMessages?.length) { - throw new TemplateApiError( - { - title: error.validationAlertTitle, - summary: error.validationAlertTitle, - }, - 400, + if (fetchExtraData) { + // Get plate renewal validation + validation = await this.vehiclePlateRenewalClient.validatePlateOwnership( + auth, + plateOwnership.regno, ) } + + return { + regno: plateOwnership.regno, + startDate: plateOwnership.startDate, + endDate: plateOwnership.endDate, + validationErrorMessages: validation?.hasError + ? validation.errorMessages + : null, + } } async submitApplication({ @@ -108,7 +105,22 @@ export class LicensePlateRenewalService extends BaseTemplateApiService { const regno = answers?.pickPlate?.regno // Submit the application - await this.vehiclePlateRenewalClient.renewPlateOwnership(auth, regno) + const submitResult = + await this.vehiclePlateRenewalClient.renewPlateOwnership(auth, regno) + + if ( + submitResult.hasError && + submitResult.errorMessages && + submitResult.errorMessages.length > 0 + ) { + throw new TemplateApiError( + { + title: applicationCheck.validation.alertTitle, + summary: submitResult.errorMessages, + }, + 400, + ) + } } private async handlePayment({ diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/order-vehicle-license-plate/order-vehicle-license-plate.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/order-vehicle-license-plate/order-vehicle-license-plate.service.ts index c7c1b851bfd0..261883a4385c 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/order-vehicle-license-plate/order-vehicle-license-plate.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/order-vehicle-license-plate/order-vehicle-license-plate.service.ts @@ -3,7 +3,10 @@ import { SharedTemplateApiService } from '../../../shared' import { TemplateApiModuleActionProps } from '../../../../types' import { BaseTemplateApiService } from '../../../base-template-api.service' import { ApplicationTypes } from '@island.is/application/types' -import { OrderVehicleLicensePlateAnswers } from '@island.is/application/templates/transport-authority/order-vehicle-license-plate' +import { + applicationCheck, + OrderVehicleLicensePlateAnswers, +} from '@island.is/application/templates/transport-authority/order-vehicle-license-plate' import { PlateOrderValidation, SGS_DELIVERY_STATION_CODE, @@ -11,9 +14,12 @@ import { VehiclePlateOrderingClient, } from '@island.is/clients/transport-authority/vehicle-plate-ordering' import { VehicleCodetablesClient } from '@island.is/clients/transport-authority/vehicle-codetables' -import { VehicleSearchApi } from '@island.is/clients/vehicles' +import { + CurrentVehiclesWithMilageAndNextInspDto, + VehicleSearchApi, +} from '@island.is/clients/vehicles' import { YES, coreErrorMessages } from '@island.is/application/core' -import { Auth, AuthMiddleware } from '@island.is/auth-nest-tools' +import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' import { TemplateApiError } from '@island.is/nest/problem' @Injectable() @@ -55,33 +61,22 @@ export class OrderVehicleLicensePlateService extends BaseTemplateApiService { async getCurrentVehiclesWithPlateOrderChecks({ auth, }: TemplateApiModuleActionProps) { - const countResult = - ( - await this.vehiclesApiWithAuth( - auth, - ).currentvehicleswithmileageandinspGet({ - showOwned: true, - showCoowned: false, - showOperated: false, - page: 1, - pageSize: 1, - }) - ).totalRecords || 0 - if (countResult && countResult > 20) { - return { - totalRecords: countResult, - vehicles: [], - } - } - const result = await this.vehiclesApiWithAuth(auth).currentVehiclesGet({ - persidNo: auth.nationalId, + // Get max 20 vehicles and total count of vehicles + // Note: Should be enough to only get 20, because if totalRecords + // is higher than 20, then we won't return any vehicles + const result = await this.vehiclesApiWithAuth( + auth, + ).currentvehicleswithmileageandinspGet({ showOwned: true, showCoowned: false, showOperated: false, + page: 1, + pageSize: 20, }) + const totalRecords = result.totalRecords || 0 // Validate that user has at least 1 vehicle - if (!result || !result.length) { + if (!totalRecords) { throw new TemplateApiError( { title: coreErrorMessages.vehiclesEmptyListOwner, @@ -91,44 +86,73 @@ export class OrderVehicleLicensePlateService extends BaseTemplateApiService { ) } + // Case: count > 20 + // Display search box, validate vehicle when permno is entered + if (totalRecords > 20) { + return { + totalRecords: totalRecords, + vehicles: [], + } + } + + const resultData = result.data || [] + + const vehicles = await Promise.all( + resultData.map(async (vehicle) => { + // Case: 20 >= count > 5 + // Display dropdown, validate vehicle when selected in dropdown + if (totalRecords > 5) { + return this.mapVehicle(auth, vehicle, false) + } + + // Case: count <= 5 + // Display radio buttons, validate all vehicles now + return this.mapVehicle(auth, vehicle, true) + }), + ) + + return { + totalRecords: totalRecords, + vehicles: vehicles, + } + } + + private async mapVehicle( + auth: User, + vehicle: CurrentVehiclesWithMilageAndNextInspDto, + fetchExtraData: boolean, + ) { + let validation: PlateOrderValidation | undefined + + if (fetchExtraData) { + // Get basic information about vehicle + const vehicleInfo = await this.vehiclesApiWithAuth( + auth, + ).basicVehicleInformationGet({ + clientPersidno: auth.nationalId, + permno: vehicle.permno || '', + regno: undefined, + vin: undefined, + }) + + // Get plate order validation + validation = + await this.vehiclePlateOrderingClient.validateVehicleForPlateOrder( + auth, + vehicle.permno || '', + vehicleInfo?.platetypefront || '', + vehicleInfo?.platetyperear || '', + ) + } + return { - totalRecords: countResult, - vehicles: await Promise.all( - result?.map(async (vehicle) => { - let validation: PlateOrderValidation | undefined - - // Only validate if fewer than 5 items - if (result.length <= 5) { - // Get basic information about vehicle - const vehicleInfo = await this.vehiclesApiWithAuth( - auth, - ).basicVehicleInformationGet({ - clientPersidno: auth.nationalId, - permno: vehicle.permno || '', - regno: undefined, - vin: undefined, - }) - // Get validation - validation = - await this.vehiclePlateOrderingClient.validatePlateOrder( - auth, - vehicle.permno || '', - vehicleInfo?.platetypefront || '', - vehicleInfo?.platetyperear || '', - ) - } - - return { - permno: vehicle.permno || undefined, - make: vehicle.make || undefined, - color: vehicle.color || undefined, - role: vehicle.role || undefined, - validationErrorMessages: validation?.hasError - ? validation.errorMessages - : null, - } - }), - ), + permno: vehicle.permno || undefined, + make: vehicle.make || undefined, + color: vehicle.colorName || undefined, + role: vehicle.role || undefined, + validationErrorMessages: validation?.hasError + ? validation.errorMessages + : null, } } @@ -136,6 +160,58 @@ export class OrderVehicleLicensePlateService extends BaseTemplateApiService { return await this.vehicleCodetablesClient.getPlateTypes() } + async validateApplication({ + application, + auth, + }: TemplateApiModuleActionProps) { + const answers = application.answers as OrderVehicleLicensePlateAnswers + + const includeRushFee = + answers?.plateDelivery?.includeRushFee?.includes(YES) || false + + // Check if used selected delivery method: Pick up at delivery station + const deliveryStationTypeCode = + answers?.plateDelivery?.deliveryStationTypeCode + let deliveryStationType: string + let deliveryStationCode: string + if ( + answers.plateDelivery?.deliveryMethodIsDeliveryStation === YES && + deliveryStationTypeCode + ) { + // Split up code+type (was merged when we fetched that data) + deliveryStationType = deliveryStationTypeCode.split('_')[0] + deliveryStationCode = deliveryStationTypeCode.split('_')[1] + } else { + // Otherwise we will default to option "Pick up at Samgöngustofa" + deliveryStationType = SGS_DELIVERY_STATION_TYPE + deliveryStationCode = SGS_DELIVERY_STATION_CODE + } + + const result = + await this.vehiclePlateOrderingClient.validateAllForPlateOrder( + auth, + answers?.pickVehicle?.plate, + answers?.plateSize?.frontPlateSize?.[0], + answers?.plateSize?.rearPlateSize?.[0], + deliveryStationType, + deliveryStationCode, + includeRushFee, + ) + + // If we get any error messages, we will just throw an error with a default title + // We will fetch these error messages again through graphql in the template, to be able + // to translate the error message + if (result.hasError && result.errorMessages?.length) { + throw new TemplateApiError( + { + title: applicationCheck.validation.alertTitle, + summary: applicationCheck.validation.alertTitle, + }, + 400, + ) + } + } + async submitApplication({ application, auth, @@ -181,13 +257,30 @@ export class OrderVehicleLicensePlateService extends BaseTemplateApiService { deliveryStationCode = SGS_DELIVERY_STATION_CODE } - await this.vehiclePlateOrderingClient.savePlateOrders(auth, { - permno: answers?.pickVehicle?.plate, - frontType: answers?.plateSize?.frontPlateSize?.[0], - rearType: answers?.plateSize?.rearPlateSize?.[0], - deliveryStationType: deliveryStationType, - deliveryStationCode: deliveryStationCode, - expressOrder: includeRushFee, - }) + const submitResult = await this.vehiclePlateOrderingClient.savePlateOrders( + auth, + { + permno: answers?.pickVehicle?.plate, + frontType: answers?.plateSize?.frontPlateSize?.[0], + rearType: answers?.plateSize?.rearPlateSize?.[0], + deliveryStationType: deliveryStationType, + deliveryStationCode: deliveryStationCode, + expressOrder: includeRushFee, + }, + ) + + if ( + submitResult.hasError && + submitResult.errorMessages && + submitResult.errorMessages.length > 0 + ) { + throw new TemplateApiError( + { + title: applicationCheck.validation.alertTitle, + summary: submitResult.errorMessages, + }, + 400, + ) + } } } diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts index 7f31c0891d47..2ea45322f6c7 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts @@ -103,7 +103,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { ) } - // A. vehicleCount > 20 + // Case: count > 20 // Display search box, validate vehicle when permno is entered if (totalRecords > 20) { return { @@ -116,13 +116,13 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { const vehicles = await Promise.all( resultData.map(async (vehicle) => { - // B. 20 >= vehicleCount > 5 + // Case: 20 >= count > 5 // Display dropdown, validate vehicle when selected in dropdown if (totalRecords > 5) { return this.mapVehicle(auth, vehicle, false) } - // C. vehicleCount <= 5 + // Case: count <= 5 // Display radio buttons, validate all vehicles now return this.mapVehicle(auth, vehicle, true) }), @@ -584,34 +584,51 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { const mileage = answers?.vehicleMileage?.value - await this.vehicleOwnerChangeClient.saveOwnerChange(auth, { - permno: answers?.pickVehicle?.plate, - seller: { - ssn: answers?.seller?.nationalId, - email: answers?.seller?.email, - }, - buyer: { - ssn: answers?.buyer?.nationalId, - email: answers?.buyer?.email, + const submitResult = await this.vehicleOwnerChangeClient.saveOwnerChange( + auth, + { + permno: answers?.pickVehicle?.plate, + seller: { + ssn: answers?.seller?.nationalId, + email: answers?.seller?.email, + }, + buyer: { + ssn: answers?.buyer?.nationalId, + email: answers?.buyer?.email, + }, + dateOfPurchase: new Date(answers?.vehicle?.date), + dateOfPurchaseTimestamp: createdStr.substring(11, createdStr.length), + saleAmount: Number(answers?.vehicle?.salePrice || '0') || 0, + mileage: mileage ? Number(mileage) || 0 : null, + insuranceCompanyCode: answers?.insurance?.value, + coOwners: buyerCoOwners?.map((coOwner) => ({ + ssn: coOwner.nationalId || '', + email: coOwner.email || '', + })), + operators: buyerOperators?.map((operator) => ({ + ssn: operator.nationalId || '', + email: operator.email || '', + isMainOperator: + buyerOperators.length > 1 + ? operator.nationalId === answers.buyerMainOperator?.nationalId + : true, + })), }, - dateOfPurchase: new Date(answers?.vehicle?.date), - dateOfPurchaseTimestamp: createdStr.substring(11, createdStr.length), - saleAmount: Number(answers?.vehicle?.salePrice || '0') || 0, - mileage: mileage ? Number(mileage) || 0 : null, - insuranceCompanyCode: answers?.insurance?.value, - coOwners: buyerCoOwners?.map((coOwner) => ({ - ssn: coOwner.nationalId || '', - email: coOwner.email || '', - })), - operators: buyerOperators?.map((operator) => ({ - ssn: operator.nationalId || '', - email: operator.email || '', - isMainOperator: - buyerOperators.length > 1 - ? operator.nationalId === answers.buyerMainOperator?.nationalId - : true, - })), - }) + ) + + if ( + submitResult.hasError && + submitResult.errorMessages && + submitResult.errorMessages.length > 0 + ) { + throw new TemplateApiError( + { + title: applicationCheck.validation.alertTitle, + summary: submitResult.errorMessages, + }, + 400, + ) + } // 3. Notify everyone in the process that the application has successfully been submitted diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/dataProviders/index.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/dataProviders/index.ts index b48b425c53ce..b59c159b6b5b 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/dataProviders/index.ts +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/dataProviders/index.ts @@ -1,6 +1,7 @@ import { defineTemplateApi, InstitutionNationalIds, + MockablePaymentCatalogApi, PaymentCatalogApi, } from '@island.is/application/types' @@ -13,6 +14,14 @@ export const SamgongustofaPaymentCatalogApi = PaymentCatalogApi.configure({ externalDataId: 'payment', }) +export const MockableSamgongustofaPaymentCatalogApi = + MockablePaymentCatalogApi.configure({ + params: { + organizationId: InstitutionNationalIds.SAMGONGUSTOFA, + }, + externalDataId: 'payment', + }) + export const CurrentVehiclesApi = defineTemplateApi({ action: 'getCurrentVehiclesWithOwnerchangeChecks', externalDataId: 'currentVehicleList', diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/ApplicationStatus/index.tsx b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/ApplicationStatus/index.tsx index 299592820227..0d3b7aa5e325 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/ApplicationStatus/index.tsx +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/ApplicationStatus/index.tsx @@ -8,7 +8,7 @@ import { useLocale } from '@island.is/localization' import { FC } from 'react' import { review } from '../../lib/messages' import { ReviewScreenProps } from '../../shared' -import { getReviewSteps, hasReviewerApproved } from '../../utils' +import { getReviewSteps, canReviewerApprove } from '../../utils' import { MessageWithLinkButtonFormField } from '@island.is/application/ui-fields' import { StatusStep } from './StatusStep' import { coreMessages } from '@island.is/application/core' @@ -21,7 +21,7 @@ export const ApplicationStatus: FC< const steps = getReviewSteps(application) - const showReviewButton = !hasReviewerApproved( + const showReviewButton = canReviewerApprove( reviewerNationalId, application.answers, ) diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/Overview/index.tsx b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/Overview/index.tsx index 67abcbd743e2..8be2109222a1 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/Overview/index.tsx +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/Overview/index.tsx @@ -13,8 +13,9 @@ import { overview, review, error as errorMsg } from '../../lib/messages' import { VehicleSection, OwnerSection, CoOwnersSection } from './sections' import { getApproveAnswers, - hasReviewerApproved, + canReviewerApprove, isLastReviewer, + canReviewerReApprove, } from '../../utils' import { RejectConfirmationModal } from './RejectConfirmationModal' import { @@ -22,6 +23,8 @@ import { SUBMIT_APPLICATION, } from '@island.is/application/graphql' import { useLazyQuery, useMutation } from '@apollo/client' +import { States } from '../../lib/constants' +import { ValidationErrorMessages } from '../ValidationErrorMessages' export const Overview: FC< React.PropsWithChildren @@ -42,11 +45,14 @@ export const Overview: FC< const [submitApplication, { error }] = useMutation(SUBMIT_APPLICATION, { onError: (e) => { console.error(e, e.message) + setButtonLoading(false) return }, }) - const [loading, setLoading] = useState(false) + const [buttonLoading, setButtonLoading] = useState(false) + const [shouldLoadValidation, setShouldLoadValidation] = useState(false) + const [validationErrorFound, setValidationErrorFound] = useState(false) const doApproveAndSubmit = async () => { // Need to get updated application answers, in case any other reviewer has approved @@ -107,11 +113,11 @@ export const Overview: FC< }) if (resSubmit?.data) { - setLoading(false) + setButtonLoading(false) setStep && setStep('conclusion') } } else { - setLoading(false) + setButtonLoading(false) setStep && setStep('conclusion') } } @@ -127,7 +133,8 @@ export const Overview: FC< } const onApproveButtonClick = async () => { - setLoading(true) + setButtonLoading(true) + setShouldLoadValidation(true) await doApproveAndSubmit() } @@ -144,7 +151,14 @@ export const Overview: FC< - {error && ( + {!buttonLoading && shouldLoadValidation && ( + + )} + + {!validationErrorFound && error && ( @@ -156,7 +170,7 @@ export const Overview: FC< - {!hasReviewerApproved(reviewerNationalId, application.answers) && ( + {canReviewerApprove(reviewerNationalId, application.answers) && ( )} + {canReviewerReApprove(reviewerNationalId, application.answers) && + application.state !== States.COMPLETED && ( + + + + )} diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/ValidationErrorMessages/index.tsx b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/ValidationErrorMessages/index.tsx index 245df4cdeeee..bb20ad4ad494 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/ValidationErrorMessages/index.tsx +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/ValidationErrorMessages/index.tsx @@ -2,16 +2,25 @@ import { gql, useQuery } from '@apollo/client' import { OwnerChangeValidationMessage } from '@island.is/api/schema' import { getValueViaPath } from '@island.is/application/core' import { FieldBaseProps } from '@island.is/application/types' -import { AlertMessage, Box, Text } from '@island.is/island-ui/core' +import { + AlertMessage, + Box, + Bullet, + BulletList, +} from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' -import { FC, useEffect } from 'react' +import { Dispatch, FC, SetStateAction, useEffect } from 'react' import { ChangeCoOwnerOfVehicleAnswers } from '../..' import { VALIDATE_VEHICLE_CO_OWNER_CHANGE } from '../../graphql/queries' import { applicationCheck } from '../../lib/messages' +interface Props { + setValidationErrorFound?: Dispatch> +} + export const ValidationErrorMessages: FC< - React.PropsWithChildren -> = (props) => { + React.PropsWithChildren +> = ({ setValidationErrorFound, ...props }) => { const { application, setFieldLoadingState } = props const { formatMessage } = useLocale() @@ -47,12 +56,18 @@ export const ValidationErrorMessages: FC< })), }, }, + onCompleted: (data) => { + if (data?.vehicleCoOwnerChangeValidation?.hasError) { + setValidationErrorFound?.(true) + } + }, + fetchPolicy: 'no-cache', }, ) useEffect(() => { setFieldLoadingState?.(loading) - }, [loading]) + }, [loading, setFieldLoadingState]) return data?.vehicleCoOwnerChangeValidation?.hasError && data.vehicleCoOwnerChangeValidation.errorMessages.length > 0 ? ( @@ -62,7 +77,7 @@ export const ValidationErrorMessages: FC< title={formatMessage(applicationCheck.validation.alertTitle)} message={ -
    + {data.vehicleCoOwnerChangeValidation.errorMessages.map( (error: OwnerChangeValidationMessage) => { const message = formatMessage( @@ -80,15 +95,13 @@ export const ValidationErrorMessages: FC< error?.errorNo return ( -
  • - - {message || defaultMessage || fallbackMessage} - -
  • + + {message || defaultMessage || fallbackMessage} + ) }, )} -
+
} /> diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/forms/ChangeCoOwnerOfVehicleForm/InformationSection/vehicleSubSection.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/forms/ChangeCoOwnerOfVehicleForm/InformationSection/vehicleSubSection.ts index efac7e83c97d..32932c24bd91 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/forms/ChangeCoOwnerOfVehicleForm/InformationSection/vehicleSubSection.ts +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/forms/ChangeCoOwnerOfVehicleForm/InformationSection/vehicleSubSection.ts @@ -3,11 +3,9 @@ import { buildMultiField, buildTextField, buildSubSection, - buildCustomField, buildHiddenInput, } from '@island.is/application/core' import { information } from '../../../lib/messages' -import { VehiclesCurrentVehicle } from '../../../shared' import { getSelectedVehicle } from '../../../utils' export const vehicleSubSection = buildSubSection({ @@ -29,8 +27,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.permno + ) + return vehicle?.permno }, }), buildTextField({ @@ -43,8 +41,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.make + ) + return vehicle?.make }, }), buildHiddenInput({ @@ -53,8 +51,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.requireMileage || false + ) + return vehicle?.requireMileage || false }, }), buildHiddenInput({ @@ -63,8 +61,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.mileageReading || '' + ) + return vehicle?.mileageReading || '' }, }), buildTextField({ @@ -73,18 +71,15 @@ export const vehicleSubSection = buildSubSection({ width: 'full', variant: 'number', condition: (answers, externalData) => { - const vehicle = getSelectedVehicle( - externalData, - answers, - ) as VehiclesCurrentVehicle + const vehicle = getSelectedVehicle(externalData, answers) return vehicle?.requireMileage || false }, placeholder(application) { const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.mileageReading + ) + return vehicle?.mileageReading ? `Síðasta skráning ${vehicle.mileageReading} Km` : '' }, diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/forms/ChangeCoOwnerOfVehicleForm/externalDataSection.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/forms/ChangeCoOwnerOfVehicleForm/externalDataSection.ts index 2883e932a9f9..ccd5f7be745b 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/forms/ChangeCoOwnerOfVehicleForm/externalDataSection.ts +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/forms/ChangeCoOwnerOfVehicleForm/externalDataSection.ts @@ -9,6 +9,7 @@ import { UserProfileApi, SamgongustofaPaymentCatalogApi, CurrentVehiclesApi, + MockableSamgongustofaPaymentCatalogApi, } from '../../dataProviders' export const externalDataSection = buildSection({ @@ -41,6 +42,10 @@ export const externalDataSection = buildSection({ title: externalData.payment.title, subTitle: externalData.payment.subTitle, }), + buildDataProviderItem({ + provider: MockableSamgongustofaPaymentCatalogApi, + title: '', + }), ], }), ], diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/lib/ChangeCoOwnerOfVehicleTemplate.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/lib/ChangeCoOwnerOfVehicleTemplate.ts index 2837bd7bb640..714000aa6b9a 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/lib/ChangeCoOwnerOfVehicleTemplate.ts +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/lib/ChangeCoOwnerOfVehicleTemplate.ts @@ -30,12 +30,13 @@ import { UserProfileApi, SamgongustofaPaymentCatalogApi, CurrentVehiclesApi, + MockableSamgongustofaPaymentCatalogApi, } from '../dataProviders' import { application as applicationMessage } from './messages' import { assign } from 'xstate' import set from 'lodash/set' import { AuthDelegationType } from '@island.is/shared/types' -import { getChargeItemCodes, hasReviewerApproved } from '../utils' +import { getChargeItemCodes, canReviewerApprove } from '../utils' import { ApiScope } from '@island.is/auth/scopes' import { buildPaymentState } from '@island.is/application/utils' import { getExtraData } from '../utils/getChargeItemCodes' @@ -65,7 +66,7 @@ const reviewStatePendingAction = ( role: string, nationalId: string, ): PendingAction => { - if (nationalId && !hasReviewerApproved(nationalId, application.answers)) { + if (nationalId && canReviewerApprove(nationalId, application.answers)) { return { title: corePendingActionMessages.waitingForReviewTitle, content: corePendingActionMessages.youNeedToReviewDescription, @@ -145,6 +146,7 @@ const template: ApplicationTemplate< IdentityApi, UserProfileApi, SamgongustofaPaymentCatalogApi, + MockableSamgongustofaPaymentCatalogApi, CurrentVehiclesApi, ], }, diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/lib/messages/review.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/lib/messages/review.ts index c777091d9f69..464483f7f07d 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/lib/messages/review.ts +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/lib/messages/review.ts @@ -110,8 +110,13 @@ export const review = { }, approve: { id: 'ta.ccov.application:review.buttons.approve', - defaultMessage: `Samþykkja`, + defaultMessage: 'Samþykkja', description: 'Approve button in review process', }, + tryAgain: { + id: 'ta.ccov.application:review.buttons.tryAgain', + defaultMessage: 'Reyna aftur', + description: 'Try again button in review process', + }, }), } diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/canReviewerApprove.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/canReviewerApprove.ts new file mode 100644 index 000000000000..4e925416b81f --- /dev/null +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/canReviewerApprove.ts @@ -0,0 +1,65 @@ +import { FormValue } from '@island.is/application/types' +import { getValueViaPath } from '@island.is/application/core' +import { CoOwnersInformation, OwnerCoOwnersInformation } from '../shared' +import { applicationHasPendingApproval } from './isLastReviewer' + +// Function to check if the reviewer is authorized to approve and hasn't done that yet +export const canReviewerApprove = ( + reviewerNationalId: string, + answers: FormValue, +): boolean => { + // Check if reviewer is old co-owner and has not approved + const oldCoOwners = getValueViaPath( + answers, + 'ownerCoOwners', + [], + ) as OwnerCoOwnersInformation[] + if ( + oldCoOwners.some( + ({ nationalId, approved }) => + nationalId === reviewerNationalId && !approved, + ) + ) { + return true + } + + // Check if reviewer is new co-owner and has not approved + const newCoOwners = ( + getValueViaPath(answers, 'coOwners', []) as CoOwnersInformation[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + if ( + newCoOwners.some( + ({ nationalId, approved }) => + nationalId === reviewerNationalId && !approved, + ) + ) { + return true + } + + return false +} + +// Special case to allow any reviewer to trigger an external API call to complete co-owner change +// Necessary when approve is updated in answers, but application is still stuck in REVIEW state +// then any user can try to 'push' the application to the next state +export const canReviewerReApprove = ( + reviewerNationalId: string, + answers: FormValue, +): boolean => { + const oldCoOwners = getValueViaPath( + answers, + 'ownerCoOwners', + [], + ) as OwnerCoOwnersInformation[] + const newCoOwners = ( + getValueViaPath(answers, 'coOwners', []) as CoOwnersInformation[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + + const isReviewerAuthorized = [ + oldCoOwners.some(({ nationalId }) => nationalId === reviewerNationalId), + newCoOwners.some(({ nationalId }) => nationalId === reviewerNationalId), + ].some(Boolean) + + // Check if the reviewer is authorized and if all required approvals have been completed + return isReviewerAuthorized && !applicationHasPendingApproval(answers) +} diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/getSelectedVehicle.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/getSelectedVehicle.ts index a13ea829dc3c..38e162f6a245 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/getSelectedVehicle.ts +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/getSelectedVehicle.ts @@ -5,7 +5,7 @@ import { CurrentVehiclesAndRecords, VehiclesCurrentVehicle } from '../shared' export const getSelectedVehicle = ( externalData: ExternalData, answers: FormValue, -): VehiclesCurrentVehicle => { +): VehiclesCurrentVehicle | undefined => { if (answers.findVehicle) { const vehicle = getValueViaPath( answers, diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/hasReviewerApproved.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/hasReviewerApproved.ts deleted file mode 100644 index 164158ecdfbb..000000000000 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/hasReviewerApproved.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { FormValue } from '@island.is/application/types' -import { getValueViaPath } from '@island.is/application/core' -import { CoOwnersInformation, OwnerCoOwnersInformation } from '../shared' - -export const hasReviewerApproved = ( - reviewerNationalId: string, - answers: FormValue, -) => { - // Check if reviewer is old co-owner and has not approved - const ownerCoOwners = getValueViaPath( - answers, - 'ownerCoOwners', - [], - ) as OwnerCoOwnersInformation[] - const ownerCoOwner = ownerCoOwners.find( - (x) => x.nationalId === reviewerNationalId, - ) - if (ownerCoOwner) { - const hasApproved = ownerCoOwner?.approved || false - if (!hasApproved) return false - } - - // Check if reviewer is new co-owner and has not approved - const coOwners = getValueViaPath( - answers, - 'coOwners', - [], - ) as CoOwnersInformation[] - const coOwner = coOwners - .filter(({ wasRemoved }) => wasRemoved !== 'true') - .find((x) => x.nationalId === reviewerNationalId) - if (coOwner) { - const hasApproved = coOwner?.approved || false - if (!hasApproved) return false - } - - return true -} diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/index.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/index.ts index 6aa4812ab24e..b446cdce56ca 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/index.ts +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/index.ts @@ -1,6 +1,6 @@ export { getChargeItemCodes } from './getChargeItemCodes' export { getSelectedVehicle } from './getSelectedVehicle' -export { hasReviewerApproved } from './hasReviewerApproved' +export { canReviewerApprove, canReviewerReApprove } from './canReviewerApprove' export { getReviewSteps } from './getReviewSteps' export { isLastReviewer } from './isLastReviewer' export { getApproveAnswers } from './getApproveAnswers' diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/isLastReviewer.ts b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/isLastReviewer.ts index cb0199e9c966..9ab1916cd600 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/isLastReviewer.ts +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/utils/isLastReviewer.ts @@ -2,39 +2,50 @@ import { getValueViaPath } from '@island.is/application/core' import { FormValue } from '@island.is/application/types' import { OwnerCoOwnersInformation, CoOwnersInformation } from '../shared' -export const isLastReviewer = ( - reviewerNationalId: string, +// Function to check if an application has pending approval +export const applicationHasPendingApproval = ( answers: FormValue, -) => { - // First check if any reviewer that is not the current user has not approved - const ownerCoOwners = getValueViaPath( + excludeNationalId?: string, +): boolean => { + // Check if any old co-owners have not approved + const oldCoOwners = getValueViaPath( answers, 'ownerCoOwners', [], ) as OwnerCoOwnersInformation[] - const approvedOwnerCoOwner = ownerCoOwners.find((ownerCoOwner) => { - return ( - ownerCoOwner.nationalId !== reviewerNationalId && !ownerCoOwner.approved + if ( + oldCoOwners.some( + ({ nationalId, approved }) => + (!excludeNationalId || nationalId !== excludeNationalId) && !approved, ) - }) - if (approvedOwnerCoOwner) { - return false + ) { + return true } - const coOwners = getValueViaPath( - answers, - 'coOwners', - [], - ) as CoOwnersInformation[] - const approvedCoOwner = coOwners - .filter(({ wasRemoved }) => wasRemoved !== 'true') - .find( - (coOwner) => - coOwner.nationalId !== reviewerNationalId && !coOwner.approved, + // Check if any new co-owners have not approved + const newCoOwners = ( + getValueViaPath(answers, 'coOwners', []) as CoOwnersInformation[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + if ( + newCoOwners.some( + ({ nationalId, approved }) => + (!excludeNationalId || nationalId !== excludeNationalId) && !approved, ) - if (approvedCoOwner) { - return false + ) { + return true } + return false +} + +// Function to check if the current reviewer is the last one who needs to approve +export const isLastReviewer = ( + reviewerNationalId: string, + answers: FormValue, +): boolean => { + // If there are pending approvals (excluding current reviewer), then he is not the last reviewer + if (applicationHasPendingApproval(answers, reviewerNationalId)) return false + + // Otherwise, the only review missing is from the current reviewer return true } diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/dataProviders/index.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/dataProviders/index.ts index 85a5a4b28121..924a4a2c5375 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/dataProviders/index.ts +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/dataProviders/index.ts @@ -1,6 +1,7 @@ import { defineTemplateApi, InstitutionNationalIds, + MockablePaymentCatalogApi, PaymentCatalogApi, } from '@island.is/application/types' @@ -13,6 +14,14 @@ export const SamgongustofaPaymentCatalogApi = PaymentCatalogApi.configure({ externalDataId: 'payment', }) +export const MockableSamgongustofaPaymentCatalogApi = + MockablePaymentCatalogApi.configure({ + params: { + organizationId: InstitutionNationalIds.SAMGONGUSTOFA, + }, + externalDataId: 'payment', + }) + export const CurrentVehiclesApi = defineTemplateApi({ action: 'getCurrentVehiclesWithOperatorChangeChecks', externalDataId: 'currentVehicleList', diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/ApplicationStatus/index.tsx b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/ApplicationStatus/index.tsx index 27dea9781631..3781f051aa49 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/ApplicationStatus/index.tsx +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/ApplicationStatus/index.tsx @@ -8,7 +8,7 @@ import { useLocale } from '@island.is/localization' import { FC } from 'react' import { review } from '../../lib/messages' import { ReviewScreenProps } from '../../shared' -import { getReviewSteps, hasReviewerApproved } from '../../utils' +import { getReviewSteps, canReviewerApprove } from '../../utils' import { MessageWithLinkButtonFormField } from '@island.is/application/ui-fields' import { StatusStep } from './StatusStep' import { coreMessages } from '@island.is/application/core' @@ -21,7 +21,7 @@ export const ApplicationStatus: FC< const steps = getReviewSteps(application) - const showReviewButton = !hasReviewerApproved( + const showReviewButton = canReviewerApprove( reviewerNationalId, application.answers, ) diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/Overview/index.tsx b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/Overview/index.tsx index 809b77c0d538..e238eead5248 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/Overview/index.tsx +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/Overview/index.tsx @@ -18,8 +18,9 @@ import { } from './sections' import { getApproveAnswers, - hasReviewerApproved, + canReviewerApprove, isLastReviewer, + canReviewerReApprove, } from '../../utils' import { RejectConfirmationModal } from './RejectConfirmationModal' import { @@ -27,6 +28,8 @@ import { SUBMIT_APPLICATION, } from '@island.is/application/graphql' import { useLazyQuery, useMutation } from '@apollo/client' +import { States } from '../../lib/constants' +import { ValidationErrorMessages } from '../ValidationErrorMessages' export const Overview: FC< React.PropsWithChildren @@ -47,11 +50,14 @@ export const Overview: FC< const [submitApplication, { error }] = useMutation(SUBMIT_APPLICATION, { onError: (e) => { console.error(e, e.message) + setButtonLoading(false) return }, }) - const [loading, setLoading] = useState(false) + const [buttonLoading, setButtonLoading] = useState(false) + const [shouldLoadValidation, setShouldLoadValidation] = useState(false) + const [validationErrorFound, setValidationErrorFound] = useState(false) const doApproveAndSubmit = async () => { // Need to get updated application answers, in case any other reviewer has approved @@ -112,11 +118,11 @@ export const Overview: FC< }) if (resSubmit?.data) { - setLoading(false) + setButtonLoading(false) setStep && setStep('conclusion') } } else { - setLoading(false) + setButtonLoading(false) setStep && setStep('conclusion') } } @@ -132,7 +138,8 @@ export const Overview: FC< } const onApproveButtonClick = async () => { - setLoading(true) + setButtonLoading(true) + setShouldLoadValidation(true) await doApproveAndSubmit() } @@ -150,7 +157,14 @@ export const Overview: FC< - {error && ( + {!buttonLoading && shouldLoadValidation && ( + + )} + + {!validationErrorFound && error && ( @@ -162,7 +176,7 @@ export const Overview: FC< - {!hasReviewerApproved(reviewerNationalId, application.answers) && ( + {canReviewerApprove(reviewerNationalId, application.answers) && ( )} + {canReviewerReApprove(reviewerNationalId, application.answers) && + application.state !== States.COMPLETED && ( + + + + )} diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/ValidationErrorMessages/index.tsx b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/ValidationErrorMessages/index.tsx index 093b87cd3f4f..27aee1ba9b8c 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/ValidationErrorMessages/index.tsx +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/ValidationErrorMessages/index.tsx @@ -2,16 +2,25 @@ import { gql, useQuery } from '@apollo/client' import { OperatorChangeValidationMessage } from '@island.is/api/schema' import { getValueViaPath } from '@island.is/application/core' import { FieldBaseProps } from '@island.is/application/types' -import { AlertMessage, Box, Text } from '@island.is/island-ui/core' +import { + AlertMessage, + Box, + Bullet, + BulletList, +} from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' -import { FC, useEffect } from 'react' +import { Dispatch, FC, SetStateAction, useEffect } from 'react' import { ChangeOperatorOfVehicleAnswers } from '../..' import { VALIDATE_VEHICLE_OPERATOR_CHANGE } from '../../graphql/queries' import { applicationCheck } from '../../lib/messages' +interface Props { + setValidationErrorFound?: Dispatch> +} + export const ValidationErrorMessages: FC< - React.PropsWithChildren -> = (props) => { + React.PropsWithChildren +> = ({ setValidationErrorFound, ...props }) => { const { application, setFieldLoadingState } = props const { formatMessage } = useLocale() @@ -49,12 +58,18 @@ export const ValidationErrorMessages: FC< : null, }, }, + onCompleted: (data) => { + if (data?.vehicleOperatorChangeValidation?.hasError) { + setValidationErrorFound?.(true) + } + }, + fetchPolicy: 'no-cache', }, ) useEffect(() => { setFieldLoadingState?.(loading) - }, [loading]) + }, [loading, setFieldLoadingState]) return data?.vehicleOperatorChangeValidation?.hasError && data.vehicleOperatorChangeValidation.errorMessages.length > 0 ? ( @@ -64,7 +79,7 @@ export const ValidationErrorMessages: FC< title={formatMessage(applicationCheck.validation.alertTitle)} message={ -
    + {data.vehicleOperatorChangeValidation.errorMessages.map( (error: OperatorChangeValidationMessage) => { const message = formatMessage( @@ -82,15 +97,13 @@ export const ValidationErrorMessages: FC< error?.errorNo return ( -
  • - - {message || defaultMessage || fallbackMessage} - -
  • + + {message || defaultMessage || fallbackMessage} + ) }, )} -
+
} /> diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/forms/ChangeOperatorOfVehicleForm/InformationSection/vehicleSubSection.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/forms/ChangeOperatorOfVehicleForm/InformationSection/vehicleSubSection.ts index 70cd26729e83..91dcd0eaba17 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/forms/ChangeOperatorOfVehicleForm/InformationSection/vehicleSubSection.ts +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/forms/ChangeOperatorOfVehicleForm/InformationSection/vehicleSubSection.ts @@ -6,7 +6,6 @@ import { buildHiddenInput, } from '@island.is/application/core' import { information } from '../../../lib/messages' -import { VehiclesCurrentVehicle } from '../../../shared' import { getSelectedVehicle } from '../../../utils' export const vehicleSubSection = buildSubSection({ @@ -27,7 +26,7 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) return vehicle?.permno }, }), @@ -41,7 +40,7 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) return vehicle?.make }, }), @@ -51,7 +50,7 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) return vehicle?.requireMileage || false }, }), @@ -61,8 +60,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.mileageReading || '' + ) + return vehicle?.mileageReading || '' }, }), buildTextField({ @@ -71,17 +70,14 @@ export const vehicleSubSection = buildSubSection({ width: 'full', variant: 'number', condition: (answers, externalData) => { - const vehicle = getSelectedVehicle( - externalData, - answers, - ) as VehiclesCurrentVehicle + const vehicle = getSelectedVehicle(externalData, answers) return vehicle?.requireMileage || false }, placeholder(application) { const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) return vehicle?.mileageReading ? `Síðasta skráning ${vehicle.mileageReading} Km` : '' diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/forms/ChangeOperatorOfVehicleForm/externalDataSection.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/forms/ChangeOperatorOfVehicleForm/externalDataSection.ts index 9132f1d53cb7..051a18a09f69 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/forms/ChangeOperatorOfVehicleForm/externalDataSection.ts +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/forms/ChangeOperatorOfVehicleForm/externalDataSection.ts @@ -9,6 +9,7 @@ import { UserProfileApi, SamgongustofaPaymentCatalogApi, CurrentVehiclesApi, + MockableSamgongustofaPaymentCatalogApi, } from '../../dataProviders' export const externalDataSection = buildSection({ @@ -40,6 +41,10 @@ export const externalDataSection = buildSection({ provider: SamgongustofaPaymentCatalogApi, title: '', }), + buildDataProviderItem({ + provider: MockableSamgongustofaPaymentCatalogApi, + title: '', + }), ], }), ], diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/lib/ChangeOperatorOfVehicleTemplate.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/lib/ChangeOperatorOfVehicleTemplate.ts index 1d5144b1d48b..68f932caa4a9 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/lib/ChangeOperatorOfVehicleTemplate.ts +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/lib/ChangeOperatorOfVehicleTemplate.ts @@ -26,11 +26,12 @@ import { UserProfileApi, SamgongustofaPaymentCatalogApi, CurrentVehiclesApi, + MockableSamgongustofaPaymentCatalogApi, } from '../dataProviders' import { application as applicationMessage } from './messages' import { assign } from 'xstate' import set from 'lodash/set' -import { hasReviewerApproved, isRemovingOperatorOnly } from '../utils' +import { canReviewerApprove, isRemovingOperatorOnly } from '../utils' import { AuthDelegationType } from '@island.is/shared/types' import { ApiScope } from '@island.is/auth/scopes' import { buildPaymentState } from '@island.is/application/utils' @@ -61,7 +62,7 @@ const reviewStatePendingAction = ( role: string, nationalId: string, ): PendingAction => { - if (nationalId && !hasReviewerApproved(nationalId, application.answers)) { + if (nationalId && canReviewerApprove(nationalId, application.answers)) { return { title: corePendingActionMessages.waitingForReviewTitle, content: corePendingActionMessages.youNeedToReviewDescription, @@ -141,6 +142,7 @@ const template: ApplicationTemplate< IdentityApi, UserProfileApi, SamgongustofaPaymentCatalogApi, + MockableSamgongustofaPaymentCatalogApi, CurrentVehiclesApi, ], }, diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/lib/messages/review.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/lib/messages/review.ts index 90130992c5a0..9811e33b6e07 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/lib/messages/review.ts +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/lib/messages/review.ts @@ -123,8 +123,13 @@ export const review = { }, approve: { id: 'ta.cov.application:review.buttons.approve', - defaultMessage: `Samþykkja`, + defaultMessage: 'Samþykkja', description: 'Approve button in review process', }, + tryAgain: { + id: 'ta.cov.application:review.buttons.tryAgain', + defaultMessage: 'Reyna aftur', + description: 'Try again button in review process', + }, }), } diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/canReviewerApprove.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/canReviewerApprove.ts new file mode 100644 index 000000000000..98830e4526b6 --- /dev/null +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/canReviewerApprove.ts @@ -0,0 +1,65 @@ +import { FormValue } from '@island.is/application/types' +import { getValueViaPath } from '@island.is/application/core' +import { OperatorInformation, UserInformation } from '../shared' +import { applicationHasPendingApproval } from './isLastReviewer' + +// Function to check if the reviewer is authorized to approve and hasn't done that yet +export const canReviewerApprove = ( + reviewerNationalId: string, + answers: FormValue, +): boolean => { + // Check if reviewer is co-owner and has not approved + const coOwners = getValueViaPath( + answers, + 'ownerCoOwner', + [], + ) as UserInformation[] + if ( + coOwners.some( + ({ nationalId, approved }) => + nationalId === reviewerNationalId && !approved, + ) + ) { + return true + } + + // Check if reviewer is new operator and has not approved + const newOperators = ( + getValueViaPath(answers, 'operators', []) as OperatorInformation[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + if ( + newOperators.some( + ({ nationalId, approved }) => + nationalId === reviewerNationalId && !approved, + ) + ) { + return true + } + + return false +} + +// Special case to allow any reviewer to trigger an external API call to complete owner change +// Necessary when approve is updated in answers, but application is still stuck in REVIEW state +// then any user can try to 'push' the application to the next state +export const canReviewerReApprove = ( + reviewerNationalId: string, + answers: FormValue, +): boolean => { + const coOwners = getValueViaPath( + answers, + 'ownerCoOwner', + [], + ) as UserInformation[] + const newOperators = ( + getValueViaPath(answers, 'operators', []) as OperatorInformation[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + + const isReviewerAuthorized = [ + coOwners.some(({ nationalId }) => nationalId === reviewerNationalId), + newOperators.some(({ nationalId }) => nationalId === reviewerNationalId), + ].some(Boolean) + + // Check if the reviewer is authorized and if all required approvals have been completed + return isReviewerAuthorized && !applicationHasPendingApproval(answers) +} diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/getSelectedVehicle.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/getSelectedVehicle.ts index a13ea829dc3c..38e162f6a245 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/getSelectedVehicle.ts +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/getSelectedVehicle.ts @@ -5,7 +5,7 @@ import { CurrentVehiclesAndRecords, VehiclesCurrentVehicle } from '../shared' export const getSelectedVehicle = ( externalData: ExternalData, answers: FormValue, -): VehiclesCurrentVehicle => { +): VehiclesCurrentVehicle | undefined => { if (answers.findVehicle) { const vehicle = getValueViaPath( answers, diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/hasReviewerApproved.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/hasReviewerApproved.ts deleted file mode 100644 index c53aad9fc6e8..000000000000 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/hasReviewerApproved.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { FormValue } from '@island.is/application/types' -import { getValueViaPath } from '@island.is/application/core' -import { OperatorInformation, UserInformation } from '../shared' - -export const hasReviewerApproved = ( - reviewerNationalId: string, - answers: FormValue, -) => { - // Check if reviewer is owner coowner and has not approved - const ownerCoOwners = getValueViaPath( - answers, - 'ownerCoOwner', - [], - ) as UserInformation[] - const ownerCoOwner = ownerCoOwners.find( - (ownerCoOwner) => ownerCoOwner.nationalId === reviewerNationalId, - ) - if (ownerCoOwner) { - const hasApproved = ownerCoOwner?.approved || false - if (!hasApproved) return false - } - - // Check if reviewer is operator and has not approved - const operators = getValueViaPath( - answers, - 'operators', - [], - ) as OperatorInformation[] - const operator = operators - .filter(({ wasRemoved }) => wasRemoved !== 'true') - .find((operator) => operator.nationalId === reviewerNationalId) - if (operator) { - const hasApproved = operator?.approved || false - if (!hasApproved) return false - } - - return true -} diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/index.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/index.ts index 290b6e7851b3..b2b0a8ee552c 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/index.ts +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/index.ts @@ -1,6 +1,6 @@ export { getChargeItemCodes } from './getChargeItemCodes' export { getReviewSteps } from './getReviewSteps' -export { hasReviewerApproved } from './hasReviewerApproved' +export { canReviewerApprove, canReviewerReApprove } from './canReviewerApprove' export { getApproveAnswers } from './getApproveAnswers' export { isLastReviewer } from './isLastReviewer' export { getRejecter } from './getRejecter' diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/isLastReviewer.ts b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/isLastReviewer.ts index 87a99d12d73c..223849b33eea 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/isLastReviewer.ts +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/utils/isLastReviewer.ts @@ -2,39 +2,50 @@ import { getValueViaPath } from '@island.is/application/core' import { FormValue } from '@island.is/application/types' import { OperatorInformation, UserInformation } from '../shared' -export const isLastReviewer = ( - reviewerNationalId: string, +// Function to check if an application has pending approval +export const applicationHasPendingApproval = ( answers: FormValue, -) => { - // First check if any reviewer that is not the current user has not approved - const ownerCoOwners = getValueViaPath( + excludeNationalId?: string, +): boolean => { + // Check if any co-owners have not approved + const coOwners = getValueViaPath( answers, 'ownerCoOwner', [], ) as UserInformation[] - const approvedOwnerCoOwner = ownerCoOwners.find((ownerCoOwner) => { - return ( - ownerCoOwner.nationalId !== reviewerNationalId && !ownerCoOwner.approved + if ( + coOwners.some( + ({ nationalId, approved }) => + (!excludeNationalId || nationalId !== excludeNationalId) && !approved, ) - }) - if (approvedOwnerCoOwner) { - return false + ) { + return true } - const operators = getValueViaPath( - answers, - 'operators', - [], - ) as OperatorInformation[] - const approvedOperator = operators - .filter(({ wasRemoved }) => wasRemoved !== 'true') - .find( - (operator) => - operator.nationalId !== reviewerNationalId && !operator.approved, + // Check if any new operators have not approved + const newOperators = ( + getValueViaPath(answers, 'operators', []) as OperatorInformation[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + if ( + newOperators.some( + ({ nationalId, approved }) => + (!excludeNationalId || nationalId !== excludeNationalId) && !approved, ) - if (approvedOperator) { - return false + ) { + return true } + return false +} + +// Function to check if the current reviewer is the last one who needs to approve +export const isLastReviewer = ( + reviewerNationalId: string, + answers: FormValue, +): boolean => { + // If there are pending approvals (excluding current reviewer), then he is not the last reviewer + if (applicationHasPendingApproval(answers, reviewerNationalId)) return false + + // Otherwise, the only review missing is from the current reviewer return true } diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/dataProviders/index.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/dataProviders/index.ts index 712988da9d9f..b16e607530df 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/dataProviders/index.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/dataProviders/index.ts @@ -1,6 +1,7 @@ import { defineTemplateApi, InstitutionNationalIds, + MockablePaymentCatalogApi, PaymentCatalogApi, } from '@island.is/application/types' @@ -13,6 +14,14 @@ export const SamgongustofaPaymentCatalogApi = PaymentCatalogApi.configure({ externalDataId: 'payment', }) +export const MockableSamgongustofaPaymentCatalogApi = + MockablePaymentCatalogApi.configure({ + params: { + organizationId: InstitutionNationalIds.SAMGONGUSTOFA, + }, + externalDataId: 'payment', + }) + export const MyPlateOwnershipsApi = defineTemplateApi({ action: 'getMyPlateOwnershipList', externalDataId: 'myPlateOwnershipList', diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateRadioField.tsx b/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateRadioField.tsx index 7a89fef1f4cf..74e8dbadfeda 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateRadioField.tsx +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateRadioField.tsx @@ -14,6 +14,7 @@ import { useLocale } from '@island.is/localization' import { information } from '../../lib/messages' import { useFormContext } from 'react-hook-form' import { getValueViaPath } from '@island.is/application/core' +import { checkCanRenew } from '../../utils' interface Option { value: string @@ -38,9 +39,10 @@ export const PlateRadioField: FC< const options = [] as Option[] for (const [index, plate] of plates.entries()) { - const inThreeMonths = new Date().setMonth(new Date().getMonth() + 3) - const canRenew = +new Date(plate.endDate) <= +inThreeMonths - const disabled = !!plate?.validationErrorMessages?.length || !canRenew + const hasError = !!plate.validationErrorMessages?.length + const canRenew = checkCanRenew(plate) + const disabled = hasError || !canRenew + options.push({ value: `${index}`, label: ( @@ -55,13 +57,13 @@ export const PlateRadioField: FC< {plate.regno} - + {formatMessage(information.labels.pickPlate.expiresTag, { date: formatDateFns(new Date(plate.endDate), 'do MMM yyyy'), })} - {disabled && ( + {hasError && ( - {!!plate.validationErrorMessages?.length && - plate.validationErrorMessages?.map((error) => { - return {error.defaultMessage} - })} + {plate.validationErrorMessages?.map((error) => { + return {error.defaultMessage} + })} } diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateSelectField.tsx b/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateSelectField.tsx index 127b4e2b4f66..9ba3b36c4d53 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateSelectField.tsx +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateSelectField.tsx @@ -1,6 +1,6 @@ import { FieldBaseProps, Option } from '@island.is/application/types' import { useLocale } from '@island.is/localization' -import { FC, useState } from 'react' +import { FC, useCallback, useEffect, useState } from 'react' import { Box, SkeletonLoader, @@ -8,12 +8,15 @@ import { Bullet, BulletList, ActionCard, + InputError, } from '@island.is/island-ui/core' import { PlateOwnership } from '../../shared' -import { information } from '../../lib/messages' +import { error, information } from '../../lib/messages' import { SelectController } from '@island.is/shared/form-fields' import { getValueViaPath } from '@island.is/application/core' import { useFormContext } from 'react-hook-form' +import { useLazyPlateDetails } from '../../hooks/useLazyPlateDetails' +import { checkCanRenew } from '../../utils' interface PlateSearchFieldProps { myPlateOwnershipList: PlateOwnership[] @@ -21,7 +24,7 @@ interface PlateSearchFieldProps { export const PlateSelectField: FC< React.PropsWithChildren -> = ({ myPlateOwnershipList, application }) => { +> = ({ myPlateOwnershipList, application, errors, setFieldLoadingState }) => { const { formatMessage, formatDateFns } = useLocale() const { setValue } = useFormContext() @@ -43,27 +46,59 @@ export const PlateSelectField: FC< } : null, ) + const [plate, setPlate] = useState( + getValueViaPath(application.answers, 'pickPlate.regno', '') as string, + ) + + const getPlateDetails = useLazyPlateDetails() + const getPlateDetailsCallback = useCallback( + async ({ regno }: { regno: string }) => { + const { data } = await getPlateDetails({ + regno, + }) + return data + }, + [getPlateDetails], + ) const onChange = (option: Option) => { const currentPlate = myPlateOwnershipList[parseInt(option.value, 10)] setIsLoading(true) if (currentPlate.regno) { - setSelectedPlate({ + getPlateDetailsCallback({ regno: currentPlate.regno, - startDate: currentPlate.startDate, - endDate: currentPlate.endDate, - validationErrorMessages: currentPlate.validationErrorMessages, }) - setValue('pickPlate.regno', disabled ? '' : selectedPlate.regno) - setIsLoading(false) + .then((response) => { + setSelectedPlate({ + regno: currentPlate.regno, + startDate: currentPlate.startDate, + endDate: currentPlate.endDate, + validationErrorMessages: + response?.myPlateOwnershipChecksByRegno?.validationErrorMessages, + }) + + const resHasError = + !!response?.myPlateOwnershipChecksByRegno?.validationErrorMessages + ?.length + const resCanRenew = checkCanRenew(currentPlate) + const resDisabled = resHasError || !resCanRenew + + setPlate(resDisabled ? '' : currentPlate.regno || '') + setValue('pickPlate.regno', resDisabled ? '' : currentPlate.regno) + setIsLoading(false) + }) + .catch((error) => console.error(error)) } } - const inThreeMonths = new Date().setMonth(new Date().getMonth() + 3) - const canRenew = - selectedPlate && +new Date(selectedPlate.endDate) <= +inThreeMonths - const disabled = - (selectedPlate && !!selectedPlate.validationErrorMessages?.length) || - !canRenew + + const hasError = !!selectedPlate?.validationErrorMessages?.length + const canRenew = checkCanRenew(selectedPlate) + const disabled = hasError || !canRenew + + useEffect(() => { + setFieldLoadingState?.(isLoading) + }, [isLoading]) + return ( )} - {selectedPlate && disabled && ( + {selectedPlate && hasError && ( - {!!selectedPlate.validationErrorMessages?.length && - selectedPlate.validationErrorMessages?.map( - (error) => { - return {error.defaultMessage} - }, - )} + {selectedPlate.validationErrorMessages?.map((error) => { + return {error.defaultMessage} + })} } @@ -126,6 +159,9 @@ export const PlateSelectField: FC< )} + {!isLoading && plate.length === 0 && (errors as any)?.pickPlate && ( + + )} ) } diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/forms/LicensePlateRenewalForm/InformationSection/informationSubSection.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/forms/LicensePlateRenewalForm/InformationSection/informationSubSection.ts index 6394cd67542d..155cdf5b8e34 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/forms/LicensePlateRenewalForm/InformationSection/informationSubSection.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/forms/LicensePlateRenewalForm/InformationSection/informationSubSection.ts @@ -30,10 +30,11 @@ export const informationSubSection = buildSubSection({ width: 'half', readOnly: true, defaultValue: (application: Application) => { - return getSelectedVehicle( + const vehicle = getSelectedVehicle( application.externalData, application.answers, - ).regno + ) + return vehicle?.regno }, }), buildDescriptionField({ @@ -49,15 +50,12 @@ export const informationSubSection = buildSubSection({ width: 'half', readOnly: true, defaultValue: (application: Application) => { - return format( - new Date( - getSelectedVehicle( - application.externalData, - application.answers, - ).startDate, - ), - 'dd.MM.yyyy', + const vehicle = getSelectedVehicle( + application.externalData, + application.answers, ) + const dateFrom = vehicle ? new Date(vehicle.startDate) : new Date() + return format(dateFrom, 'dd.MM.yyyy') }, }), buildTextField({ @@ -67,12 +65,11 @@ export const informationSubSection = buildSubSection({ width: 'half', readOnly: true, defaultValue: (application: Application) => { - const dateTo = new Date( - getSelectedVehicle( - application.externalData, - application.answers, - ).endDate, + const vehicle = getSelectedVehicle( + application.externalData, + application.answers, ) + const dateTo = vehicle ? new Date(vehicle?.endDate) : new Date() return format( dateTo.setFullYear(dateTo.getFullYear() + 8), 'dd.MM.yyyy', diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/forms/LicensePlateRenewalForm/prerequisitesSection.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/forms/LicensePlateRenewalForm/prerequisitesSection.ts index aa3cba6698d4..532ee1267e0e 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/forms/LicensePlateRenewalForm/prerequisitesSection.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/forms/LicensePlateRenewalForm/prerequisitesSection.ts @@ -8,6 +8,7 @@ import { IdentityApi, SamgongustofaPaymentCatalogApi, MyPlateOwnershipsApi, + MockableSamgongustofaPaymentCatalogApi, } from '../../dataProviders' export const prerequisitesSection = buildSection({ @@ -34,6 +35,10 @@ export const prerequisitesSection = buildSection({ provider: SamgongustofaPaymentCatalogApi, title: '', }), + buildDataProviderItem({ + provider: MockableSamgongustofaPaymentCatalogApi, + title: '', + }), ], }), ], diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/hooks/useLazyPlateDetails.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/hooks/useLazyPlateDetails.ts new file mode 100644 index 000000000000..6ed497a6d88c --- /dev/null +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/hooks/useLazyPlateDetails.ts @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client' +import { MyPlateOwnershipChecksByRegno } from '@island.is/api/schema' +import { GET_MY_PLATE_OWNERSHIP_CHECKS_BY_REGNO } from '../graphql/queries' +import { useLazyQuery } from './useLazyQuery' + +export const useLazyPlateDetails = () => { + return useLazyQuery< + { + myPlateOwnershipChecksByRegno: MyPlateOwnershipChecksByRegno + }, + { + regno: string + } + >( + gql` + ${GET_MY_PLATE_OWNERSHIP_CHECKS_BY_REGNO} + `, + ) +} diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/hooks/useLazyQuery.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/hooks/useLazyQuery.ts new file mode 100644 index 000000000000..f8964864e9f3 --- /dev/null +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/hooks/useLazyQuery.ts @@ -0,0 +1,24 @@ +import { + DocumentNode, + OperationVariables, + useApolloClient, +} from '@apollo/client' +import { useCallback } from 'react' + +export const useLazyQuery = < + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode, +) => { + const client = useApolloClient() + + return useCallback( + (variables: TVariables) => + client.query({ + query, + variables, + }), + [client], + ) +} diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/index.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/index.ts index b758ce9adb96..ed5617b1cf2e 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/index.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/index.ts @@ -8,5 +8,6 @@ export type LicensePlateRenewalAnswers = LicensePlateRenewal export * from './utils' export * from './lib/messages/error' +export * from './lib/messages/applicationCheck' export default template diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/LicensePlateRenewalTemplate.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/LicensePlateRenewalTemplate.ts index 52194f92cf09..36fdce40e474 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/LicensePlateRenewalTemplate.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/LicensePlateRenewalTemplate.ts @@ -25,6 +25,7 @@ import { LicensePlateRenewalSchema } from './dataSchema' import { SamgongustofaPaymentCatalogApi, MyPlateOwnershipsApi, + MockableSamgongustofaPaymentCatalogApi, } from '../dataProviders' import { AuthDelegationType } from '@island.is/shared/types' import { ApiScope } from '@island.is/auth/scopes' @@ -86,9 +87,6 @@ const template: ApplicationTemplate< }, lifecycle: EphemeralStateLifeCycle, onExit: [ - defineTemplateApi({ - action: ApiActions.validateApplication, - }), defineTemplateApi({ action: ApiActions.submitApplication, }), @@ -111,6 +109,7 @@ const template: ApplicationTemplate< delete: true, api: [ SamgongustofaPaymentCatalogApi, + MockableSamgongustofaPaymentCatalogApi, MyPlateOwnershipsApi, IdentityApi, ], diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/applicationCheck.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/applicationCheck.ts new file mode 100644 index 000000000000..1697e26d3606 --- /dev/null +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/applicationCheck.ts @@ -0,0 +1,16 @@ +import { defineMessages } from 'react-intl' + +export const applicationCheck = { + validation: defineMessages({ + alertTitle: { + id: 'ta.lpr.application:applicationCheck.validation.alertTitle', + defaultMessage: 'Það kom upp villa', + description: 'Application check validation alert title', + }, + fallbackErrorMessage: { + id: 'ta.lpr.application:applicationCheck.validation.fallbackErrorMessage', + defaultMessage: 'Það kom upp villa við að sannreyna gögn', + description: 'Fallback error message for validation', + }, + }), +} diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/error.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/error.ts index 35b5ba7b0355..756a92386086 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/error.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/error.ts @@ -1,6 +1,11 @@ import { defineMessages } from 'react-intl' export const error = defineMessages({ + requiredValidPlate: { + id: 'ta.lpr.application:error.requiredValidPlate', + defaultMessage: 'Einkamerki þarf að vera gilt', + description: 'Error message if the plate chosen is invalid or not chosen', + }, errorDataProvider: { id: 'ta.lpr.application:error.dataProvider', defaultMessage: 'Reyndu aftur síðar', diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/index.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/index.ts index b1dfa8e15ab3..cde6d64fc218 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/index.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/index.ts @@ -4,3 +4,4 @@ export * from './information' export * from './payment' export * from './conclusion' export * from './error' +export * from './applicationCheck' diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/information.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/information.ts index d14ae07c9f27..e3db808d044e 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/information.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/messages/information.ts @@ -52,11 +52,6 @@ export const information = { defaultMessage: 'Ekki er hægt að endurnýja einkamerki vegna:', description: 'Pick plate has an error title', }, - isNotDebtLessTag: { - id: 'ta.lpr.application:information.labels.pickVehicle.isNotDebtLessTag', - defaultMessage: 'Ógreidd bifreiðagjöld', - description: 'Pick plate is not debt less tag', - }, expiresTag: { id: 'ta.lpr.application:information.labels.pickVehicle.expiresTag', defaultMessage: 'Rennur út {date}', diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/shared/constants.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/shared/constants.ts index f62f9cd86c1f..b67eb7433299 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/shared/constants.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/shared/constants.ts @@ -1,4 +1,3 @@ export enum ApiActions { - validateApplication = 'validateApplication', submitApplication = 'submitApplication', } diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/utils/checkCanRenew.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/utils/checkCanRenew.ts new file mode 100644 index 000000000000..1d2ac5fe148f --- /dev/null +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/utils/checkCanRenew.ts @@ -0,0 +1,6 @@ +import { PlateOwnership } from '../shared' + +export const checkCanRenew = (plate?: PlateOwnership | null): boolean => { + const inThreeMonths = new Date().setMonth(new Date().getMonth() + 3) + return plate ? +new Date(plate.endDate) <= +inThreeMonths : false +} diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/utils/getSelectedVehicle.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/utils/getSelectedVehicle.ts index 49782eda7261..324c3ceb1d91 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/utils/getSelectedVehicle.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/utils/getSelectedVehicle.ts @@ -5,7 +5,7 @@ import { PlateOwnership } from '../shared' export const getSelectedVehicle = ( externalData: ExternalData, answers: FormValue, -) => { +): PlateOwnership | undefined => { const currentVehicleList = (externalData?.['myPlateOwnershipList']?.data as PlateOwnership[]) || [] const vehicleValue = getValueViaPath(answers, 'pickPlate.value', '') as string diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/utils/index.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/utils/index.ts index 69aa4fbeac9d..2ad52b10bbab 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/utils/index.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/utils/index.ts @@ -16,3 +16,4 @@ export const getExtraData = (application: Application): ExtraData[] => { } export { getSelectedVehicle } from './getSelectedVehicle' +export { checkCanRenew } from './checkCanRenew' diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/dataProviders/index.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/dataProviders/index.ts index 962d63233cb0..e90307fa5ae4 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/dataProviders/index.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/dataProviders/index.ts @@ -1,6 +1,7 @@ import { defineTemplateApi, InstitutionNationalIds, + MockablePaymentCatalogApi, PaymentCatalogApi, } from '@island.is/application/types' @@ -11,6 +12,14 @@ export const SamgongustofaPaymentCatalogApi = PaymentCatalogApi.configure({ externalDataId: 'payment', }) +export const MockableSamgongustofaPaymentCatalogApi = + MockablePaymentCatalogApi.configure({ + params: { + organizationId: InstitutionNationalIds.SAMGONGUSTOFA, + }, + externalDataId: 'payment', + }) + export const CurrentVehiclesApi = defineTemplateApi({ action: 'getCurrentVehiclesWithPlateOrderChecks', externalDataId: 'currentVehicleList', diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/PickPlateSize.tsx b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/PickPlateSize.tsx index d29a97fcb2e6..64c8732fc832 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/PickPlateSize.tsx +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/PickPlateSize.tsx @@ -11,7 +11,7 @@ import { CheckboxController } from '@island.is/shared/form-fields' import { gql, useQuery } from '@apollo/client' import { GET_VEHICLE_INFORMATION } from '../graphql/queries' import { getErrorViaPath } from '@island.is/application/core' -import { PlateType, VehiclesCurrentVehicle } from '../shared' +import { PlateType } from '../shared' import { information } from '../lib/messages' import { getSelectedVehicle } from '../utils' import { useFormContext } from 'react-hook-form' @@ -26,7 +26,7 @@ export const PickPlateSize: FC> = ( const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) const { data, loading, error } = useQuery( gql` @@ -35,7 +35,7 @@ export const PickPlateSize: FC> = ( { variables: { input: { - permno: vehicle.permno, + permno: vehicle?.permno, regno: '', vin: '', }, @@ -54,9 +54,9 @@ export const PickPlateSize: FC> = ( // Plate type front should always be defined (rear type can be empty in some cases) const plateTypeFrontError = !currentPlateTypeFront - const noPlateMatchError = - plateTypeList?.filter((x) => x.code === currentPlateTypeFront)?.length === - 0 ?? false + const plateTypeFrontList = + plateTypeList?.filter((x) => x.code === currentPlateTypeFront) || [] + const plateTypeFrontListEmptyError = plateTypeFrontList.length === 0 useEffect(() => { if (!loading && currentPlateTypeRear === null) { @@ -66,7 +66,7 @@ export const PickPlateSize: FC> = ( useEffect(() => { setFieldLoadingState?.(loading || !!error) - }, [loading, error]) + }, [loading, error, setFieldLoadingState]) return ( @@ -77,7 +77,7 @@ export const PickPlateSize: FC> = ( repeat={2} borderRadius="large" /> - ) : !error && !plateTypeFrontError && !noPlateMatchError ? ( + ) : !error && !plateTypeFrontError && !plateTypeFrontListEmptyError ? ( <> {formatMessage(information.labels.plateSize.frontPlateSubtitle)} @@ -137,7 +137,7 @@ export const PickPlateSize: FC> = ( > +} + +export const ValidationErrorMessages: FC< + React.PropsWithChildren +> = ({ setValidationErrorFound, ...props }) => { + const { application, setFieldLoadingState } = props + + const { formatMessage } = useLocale() + + const answers = application.answers as OrderVehicleLicensePlateAnswers + + const { data, loading } = useQuery( + gql` + ${VALIDATE_VEHICLE_PLATE_ORDER} + `, + { + variables: { + answers: { + pickVehicle: { + plate: answers?.pickVehicle?.plate, + }, + plateSize: { + frontPlateSize: answers?.plateSize?.frontPlateSize, + rearPlateSize: answers?.plateSize?.rearPlateSize, + }, + plateDelivery: { + deliveryMethodIsDeliveryStation: + answers?.plateDelivery?.deliveryMethodIsDeliveryStation, + deliveryStationTypeCode: + answers?.plateDelivery?.deliveryStationTypeCode, + includeRushFee: answers?.plateDelivery?.includeRushFee, + }, + }, + }, + onCompleted: (data) => { + if (data?.vehiclePlateOrderValidation?.hasError) { + setValidationErrorFound?.(true) + } + }, + fetchPolicy: 'no-cache', + }, + ) + + useEffect(() => { + setFieldLoadingState?.(loading) + }, [loading, setFieldLoadingState]) + + return data?.vehiclePlateOrderValidation?.hasError && + data.vehiclePlateOrderValidation.errorMessages.length > 0 ? ( + + + + {data.vehiclePlateOrderValidation.errorMessages.map( + (error: PlateOrderValidationMessage) => { + const message = formatMessage( + getValueViaPath( + applicationCheck.validation, + error?.errorNo || '', + ), + ) + const defaultMessage = error.defaultMessage + const fallbackMessage = + formatMessage( + applicationCheck.validation.fallbackErrorMessage, + ) + + ' - ' + + error?.errorNo + + return ( + + {message || defaultMessage || fallbackMessage} + + ) + }, + )} + + + } + /> + + ) : null +} diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/VehicleSelectField.tsx b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/VehicleSelectField.tsx index 833cbfac19cf..f8ef114e8373 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/VehicleSelectField.tsx +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/VehicleSelectField.tsx @@ -1,6 +1,6 @@ import { FieldBaseProps, Option } from '@island.is/application/types' import { useLocale } from '@island.is/localization' -import { FC, useCallback, useState } from 'react' +import { FC, useCallback, useEffect, useState } from 'react' import { ActionCard, AlertMessage, @@ -27,7 +27,7 @@ interface VehicleSearchFieldProps { export const VehicleSelectField: FC< React.PropsWithChildren -> = ({ currentVehicleList, application, errors }) => { +> = ({ currentVehicleList, application, errors, setFieldLoadingState }) => { const { formatMessage } = useLocale() const { setValue } = useFormContext() @@ -101,6 +101,10 @@ export const VehicleSelectField: FC< const disabled = selectedVehicle && !!selectedVehicle.validationErrorMessages?.length + useEffect(() => { + setFieldLoadingState?.(isLoading) + }, [isLoading]) + return ( { + const chargeItemCodes = getChargeItemCodesWithAnswers( + formValue as OrderVehicleLicensePlate, + ) + const allItems = externalData?.payment?.data as [ + { + priceAmount: number + chargeItemName: string + chargeItemCode: string + }, + ] + const items = chargeItemCodes.map((chargeItemCode) => { + return allItems.find( + (item) => item.chargeItemCode === chargeItemCode, + ) + }) + return items.length > 0 + }, + }, + ], + }), + ], + }), + ], +}) diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/prerequisitesSection.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/prerequisitesSection.ts index b2954cf408f8..1538341271a0 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/prerequisitesSection.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/prerequisitesSection.ts @@ -9,6 +9,7 @@ import { CurrentVehiclesApi, DeliveryStationsApi, PlateTypesApi, + MockableSamgongustofaPaymentCatalogApi, } from '../../dataProviders' export const prerequisitesSection = buildSection({ @@ -30,6 +31,10 @@ export const prerequisitesSection = buildSection({ provider: SamgongustofaPaymentCatalogApi, title: '', }), + buildDataProviderItem({ + provider: MockableSamgongustofaPaymentCatalogApi, + title: '', + }), buildDataProviderItem({ provider: DeliveryStationsApi, title: '', diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/graphql/queries.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/graphql/queries.ts index 526d8a5f041d..5d5bd05d0ecf 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/graphql/queries.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/graphql/queries.ts @@ -32,3 +32,15 @@ export const GET_VEHICLE_INFORMATION = ` } } ` + +export const VALIDATE_VEHICLE_PLATE_ORDER = ` + query GetVehiclePlateOrderValidation($answers: PlateOrderAnswers!) { + vehiclePlateOrderValidation(answers: $answers) { + hasError + errorMessages { + errorNo + defaultMessage + } + } + } +` diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/index.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/index.ts index 59a1addbc8fb..b91f9a24ef2c 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/index.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/index.ts @@ -8,5 +8,6 @@ export type OrderVehicleLicensePlateAnswers = OrderVehicleLicensePlate export * from './utils' export * from './lib/messages/externalData' +export * from './lib/messages/applicationCheck' export default template diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/OrderVehicleLicensePlateTemplate.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/OrderVehicleLicensePlateTemplate.ts index 40cf0138972b..07b5ce555e52 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/OrderVehicleLicensePlateTemplate.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/OrderVehicleLicensePlateTemplate.ts @@ -25,6 +25,7 @@ import { CurrentVehiclesApi, DeliveryStationsApi, PlateTypesApi, + MockableSamgongustofaPaymentCatalogApi, } from '../dataProviders' import { AuthDelegationType } from '@island.is/shared/types' import { ApiScope } from '@island.is/auth/scopes' @@ -84,6 +85,9 @@ const template: ApplicationTemplate< ], }, lifecycle: EphemeralStateLifeCycle, + onExit: defineTemplateApi({ + action: ApiActions.validateApplication, + }), roles: [ { id: Roles.APPLICANT, @@ -103,6 +107,7 @@ const template: ApplicationTemplate< delete: true, api: [ SamgongustofaPaymentCatalogApi, + MockableSamgongustofaPaymentCatalogApi, CurrentVehiclesApi, DeliveryStationsApi, PlateTypesApi, diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/applicationCheck.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/applicationCheck.ts new file mode 100644 index 000000000000..364b2bcbd13a --- /dev/null +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/applicationCheck.ts @@ -0,0 +1,16 @@ +import { defineMessages } from 'react-intl' + +export const applicationCheck = { + validation: defineMessages({ + alertTitle: { + id: 'ta.ovlp.application:applicationCheck.validation.alertTitle', + defaultMessage: 'Það kom upp villa', + description: 'Application check validation alert title', + }, + fallbackErrorMessage: { + id: 'ta.ovlp.application:applicationCheck.validation.fallbackErrorMessage', + defaultMessage: 'Það kom upp villa við að sannreyna gögn', + description: 'Fallback error message for validation', + }, + }), +} diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/index.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/index.ts index dc41f673cb74..e6de91a1f1cb 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/index.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/index.ts @@ -4,3 +4,4 @@ export * from './information' export * from './payment' export * from './confirmation' export * from './error' +export * from './applicationCheck' diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/payment.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/payment.ts index ba47ab991e24..59016c396c95 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/payment.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/lib/messages/payment.ts @@ -7,8 +7,28 @@ export const payment = { defaultMessage: 'Greiðsla', description: 'Title of for payment section', }, + pageTitle: { + id: 'ta.ovlp.application:payment.general.pageTitle', + defaultMessage: 'Greiðsla', + description: 'Title of for payment page', + }, + confirm: { + id: 'ta.ovlp.application:payment.general.confirm', + defaultMessage: 'Staðfesta', + description: 'confirm', + }, }), paymentChargeOverview: defineMessages({ + forPayment: { + id: 'ta.ovlp.application:payment.paymentChargeOverview.forPayment', + defaultMessage: 'Til greiðslu', + description: 'For payment label', + }, + total: { + id: 'ta.ovlp.application:payment.paymentChargeOverview.total', + defaultMessage: 'Samtals', + description: 'Total amount label', + }, frontLabel: { id: 'ta.ovlp.application:payment.paymentChargeOverview.frontLabel', defaultMessage: 'merki að framan', diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/shared/constants.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/shared/constants.ts index b67eb7433299..f62f9cd86c1f 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/shared/constants.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/shared/constants.ts @@ -1,3 +1,4 @@ export enum ApiActions { + validateApplication = 'validateApplication', submitApplication = 'submitApplication', } diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/utils/getSelectedVehicle.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/utils/getSelectedVehicle.ts index fbba4d58c387..ce19939d3633 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/utils/getSelectedVehicle.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/utils/getSelectedVehicle.ts @@ -5,7 +5,7 @@ import { VehiclesCurrentVehicle, CurrentVehiclesAndRecords } from '../shared' export const getSelectedVehicle = ( externalData: ExternalData, answers: FormValue, -): VehiclesCurrentVehicle => { +): VehiclesCurrentVehicle | undefined => { if (answers.findVehicle) { const vehicle = getValueViaPath( answers, @@ -23,5 +23,5 @@ export const getSelectedVehicle = ( '', ) as string - return currentVehicleList?.vehicles[parseInt(vehicleIndex, 10)] + return currentVehicleList?.vehicles?.[parseInt(vehicleIndex, 10)] } diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/dataProviders/index.ts b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/dataProviders/index.ts index f0bb5f857cfd..146dc78e37f9 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/dataProviders/index.ts +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/dataProviders/index.ts @@ -1,6 +1,7 @@ import { defineTemplateApi, InstitutionNationalIds, + MockablePaymentCatalogApi, PaymentCatalogApi, } from '@island.is/application/types' @@ -13,6 +14,14 @@ export const SamgongustofaPaymentCatalogApi = PaymentCatalogApi.configure({ externalDataId: 'payment', }) +export const MockableSamgongustofaPaymentCatalogApi = + MockablePaymentCatalogApi.configure({ + params: { + organizationId: InstitutionNationalIds.SAMGONGUSTOFA, + }, + externalDataId: 'payment', + }) + interface CurrentVehiclesParameters { showOwned?: boolean showCoOwned?: boolean diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/MainOwner.tsx b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/MainOwner.tsx index e4f0e995464d..c49b7f28397f 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/MainOwner.tsx +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/MainOwner.tsx @@ -1,5 +1,4 @@ import { gql, useQuery } from '@apollo/client' -import { VehiclesCurrentVehicle } from '../shared' import { FieldBaseProps } from '@island.is/application/types' import { AlertMessage, @@ -26,7 +25,7 @@ export const MainOwner: FC> = ( const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) const { data, loading, error } = useQuery( gql` @@ -35,7 +34,7 @@ export const MainOwner: FC> = ( { variables: { input: { - permno: vehicle.permno, + permno: vehicle?.permno, regno: '', vin: '', }, @@ -47,7 +46,7 @@ export const MainOwner: FC> = ( useEffect(() => { setFieldLoadingState?.(loading || !!error) - }, [loading, error]) + }, [loading, error, setFieldLoadingState]) return loading ? ( diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/InformationSection/vehicleSubSection.ts b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/InformationSection/vehicleSubSection.ts index d3b8c8e4a1b2..0c31277ba22c 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/InformationSection/vehicleSubSection.ts +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/InformationSection/vehicleSubSection.ts @@ -6,7 +6,6 @@ import { buildDescriptionField, buildCustomField, } from '@island.is/application/core' -import { VehiclesCurrentVehicle } from '../../../shared' import { information } from '../../../lib/messages' import { getSelectedVehicle } from '../../../utils' @@ -34,7 +33,7 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) return vehicle?.permno }, }), @@ -48,7 +47,7 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) return vehicle?.make }, }), @@ -57,10 +56,7 @@ export const vehicleSubSection = buildSubSection({ title: '', component: 'MainOwner', condition: (formValue, externalData) => { - const vehicle = getSelectedVehicle( - externalData, - formValue, - ) as VehiclesCurrentVehicle + const vehicle = getSelectedVehicle(externalData, formValue) return vehicle?.role !== 'Eigandi' }, }), @@ -70,10 +66,7 @@ export const vehicleSubSection = buildSubSection({ titleVariant: 'h5', space: 3, condition: (formValue, externalData) => { - const vehicle = getSelectedVehicle( - externalData, - formValue, - ) as VehiclesCurrentVehicle + const vehicle = getSelectedVehicle(externalData, formValue) return vehicle?.role === 'Eigandi' }, }), @@ -83,10 +76,7 @@ export const vehicleSubSection = buildSubSection({ titleVariant: 'h5', space: 3, condition: (formValue, externalData) => { - const vehicle = getSelectedVehicle( - externalData, - formValue, - ) as VehiclesCurrentVehicle + const vehicle = getSelectedVehicle(externalData, formValue) return vehicle?.role !== 'Eigandi' }, }), @@ -116,10 +106,7 @@ export const vehicleSubSection = buildSubSection({ width: 'half', readOnly: true, condition: (formValue, externalData) => { - const vehicle = getSelectedVehicle( - externalData, - formValue, - ) as VehiclesCurrentVehicle + const vehicle = getSelectedVehicle(externalData, formValue) return vehicle?.role === 'Eigandi' }, defaultValue: (application: Application) => @@ -132,10 +119,7 @@ export const vehicleSubSection = buildSubSection({ width: 'half', readOnly: true, condition: (formValue, externalData) => { - const vehicle = getSelectedVehicle( - externalData, - formValue, - ) as VehiclesCurrentVehicle + const vehicle = getSelectedVehicle(externalData, formValue) return vehicle?.role === 'Eigandi' }, defaultValue: (application: Application) => diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/prerequisitesSection.ts b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/prerequisitesSection.ts index 4c1ab06ac0ff..9cc00d156b20 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/prerequisitesSection.ts +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/prerequisitesSection.ts @@ -8,6 +8,7 @@ import { IdentityApi, SamgongustofaPaymentCatalogApi, CurrentVehiclesApi, + MockableSamgongustofaPaymentCatalogApi, } from '../../dataProviders' export const prerequisitesSection = buildSection({ @@ -34,6 +35,10 @@ export const prerequisitesSection = buildSection({ provider: SamgongustofaPaymentCatalogApi, title: '', }), + buildDataProviderItem({ + provider: MockableSamgongustofaPaymentCatalogApi, + title: '', + }), ], }), ], diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/lib/OrderVehicleRegistrationCertificateTemplate.ts b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/lib/OrderVehicleRegistrationCertificateTemplate.ts index ad71d254401d..cbe4113c072e 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/lib/OrderVehicleRegistrationCertificateTemplate.ts +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/lib/OrderVehicleRegistrationCertificateTemplate.ts @@ -24,6 +24,7 @@ import { IdentityApi, SamgongustofaPaymentCatalogApi, CurrentVehiclesApi, + MockableSamgongustofaPaymentCatalogApi, } from '../dataProviders' import { AuthDelegationType } from '@island.is/shared/types' import { ApiScope } from '@island.is/auth/scopes' @@ -107,6 +108,7 @@ const template: ApplicationTemplate< api: [ IdentityApi, SamgongustofaPaymentCatalogApi, + MockableSamgongustofaPaymentCatalogApi, CurrentVehiclesApi, ], }, diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/utils/getSelectedVehicle.ts b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/utils/getSelectedVehicle.ts index a849d4c77742..deb5579e7385 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/utils/getSelectedVehicle.ts +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/utils/getSelectedVehicle.ts @@ -5,7 +5,7 @@ import { CurrentVehiclesAndRecords, VehiclesCurrentVehicle } from '../shared' export const getSelectedVehicle = ( externalData: ExternalData, answers: FormValue, -): VehiclesCurrentVehicle => { +): VehiclesCurrentVehicle | undefined => { if (answers.findVehicle) { const vehicle = getValueViaPath( answers, diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/ApplicationStatus/index.tsx b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/ApplicationStatus/index.tsx index 14a13ea40c7a..8a20a0ee11b9 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/ApplicationStatus/index.tsx +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/ApplicationStatus/index.tsx @@ -9,7 +9,7 @@ import { FC } from 'react' import { review } from '../../lib/messages' import { States } from '../../lib/constants' import { ReviewScreenProps } from '../../shared' -import { getReviewSteps, hasReviewerApproved } from '../../utils' +import { getReviewSteps, canReviewerApprove } from '../../utils' import { StatusStep } from './StatusStep' import { MessageWithLinkButtonFormField } from '@island.is/application/ui-fields' import { coreMessages } from '@island.is/application/core' @@ -27,7 +27,7 @@ export const ApplicationStatus: FC< const steps = getReviewSteps(application, coOwnersAndOperators || []) - const showReviewButton = !hasReviewerApproved( + const showReviewButton = canReviewerApprove( reviewerNationalId, application.answers, ) diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/CoOwner/index.tsx b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/CoOwner/index.tsx index b31da67bb076..50e921cb8561 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/CoOwner/index.tsx +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/CoOwner/index.tsx @@ -14,7 +14,6 @@ import { InputController } from '@island.is/shared/form-fields' import { FC, useEffect } from 'react' import { GET_VEHICLE_INFORMATION } from '../../graphql/queries' import { information } from '../../lib/messages' -import { VehiclesCurrentVehicle } from '../../shared' import { getSelectedVehicle } from '../../utils' export const CoOwner: FC> = (props) => { @@ -26,7 +25,7 @@ export const CoOwner: FC> = (props) => { const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle + ) const { data, loading, error } = useQuery( gql` @@ -35,7 +34,7 @@ export const CoOwner: FC> = (props) => { { variables: { input: { - permno: vehicle.permno, + permno: vehicle?.permno, regno: '', vin: '', }, @@ -45,7 +44,7 @@ export const CoOwner: FC> = (props) => { useEffect(() => { setFieldLoadingState?.(loading || !!error) - }, [loading, error]) + }, [loading, error, setFieldLoadingState]) return !loading && !error ? ( data?.vehiclesDetail?.coOwners && diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/Overview/index.tsx b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/Overview/index.tsx index b0c19cfc84ef..ce401d658ae1 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/Overview/index.tsx +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/Overview/index.tsx @@ -5,17 +5,11 @@ import { Text, Divider, Button, - AlertMessage, InputError, } from '@island.is/island-ui/core' import { ReviewScreenProps } from '../../shared' import { useLocale } from '@island.is/localization' -import { - applicationCheck, - overview, - review, - error as errorMsg, -} from '../../lib/messages' +import { overview, review, error as errorMsg } from '../../lib/messages' import { States } from '../../lib/constants' import { VehicleSection, @@ -27,7 +21,8 @@ import { } from './sections' import { getApproveAnswers, - hasReviewerApproved, + canReviewerApprove, + canReviewerReApprove, isLastReviewer, } from '../../utils' import { RejectConfirmationModal } from './RejectConfirmationModal' @@ -35,14 +30,10 @@ import { APPLICATION_APPLICATION, SUBMIT_APPLICATION, } from '@island.is/application/graphql' -import { gql, useLazyQuery, useMutation } from '@apollo/client' +import { useLazyQuery, useMutation } from '@apollo/client' import { getValueViaPath } from '@island.is/application/core' -import { - OwnerChangeAnswers, - OwnerChangeValidationMessage, -} from '@island.is/api/schema' -import { VALIDATE_VEHICLE_OWNER_CHANGE } from '../../graphql/queries' import { TransferOfVehicleOwnershipAnswers } from '../..' +import { ValidationErrorMessages } from '../ValidationErrorMessages' export const Overview: FC< React.PropsWithChildren @@ -71,11 +62,14 @@ export const Overview: FC< const [submitApplication, { error }] = useMutation(SUBMIT_APPLICATION, { onError: (e) => { console.error(e, e.message) + setButtonLoading(false) return }, }) - const [loading, setLoading] = useState(false) + const [buttonLoading, setButtonLoading] = useState(false) + const [shouldLoadValidation, setShouldLoadValidation] = useState(false) + const [validationErrorFound, setValidationErrorFound] = useState(false) const isBuyer = (getValueViaPath(answers, 'buyer.nationalId', '') as string) === @@ -142,33 +136,17 @@ export const Overview: FC< }) if (resSubmit?.data) { - setLoading(false) + setButtonLoading(false) setStep && setStep('conclusion') } } else { - setLoading(false) + setButtonLoading(false) setStep && setStep('conclusion') } } } } - const [validateVehicleThenApproveAndSubmit, { data }] = useLazyQuery< - any, - { answers: OwnerChangeAnswers } - >( - gql` - ${VALIDATE_VEHICLE_OWNER_CHANGE} - `, - { - onCompleted: async (data) => { - if (!data?.vehicleOwnerChangeValidation?.hasError) { - await doApproveAndSubmit() - } - }, - }, - ) - const onBackButtonClick = () => { setStep && setStep('states') } @@ -182,65 +160,10 @@ export const Overview: FC< setNoInsuranceError(true) } else { setNoInsuranceError(false) - setLoading(true) - if (isBuyer) { - // Need to get updated application, in case buyer has changed co-owner - const applicationInfo = await getApplicationInfo({ - variables: { - input: { - id: application.id, - }, - locale: 'is', - }, - fetchPolicy: 'no-cache', - }) - const updatedApplication = applicationInfo?.data?.applicationApplication - - if (updatedApplication) { - const currentAnswers: OwnerChangeAnswers = updatedApplication.answers - - validateVehicleThenApproveAndSubmit({ - variables: { - answers: { - pickVehicle: { - plate: currentAnswers?.pickVehicle?.plate, - }, - vehicle: { - date: currentAnswers?.vehicle?.date, - salePrice: currentAnswers?.vehicle?.salePrice, - }, - vehicleMileage: { - value: answers?.vehicleMileage?.value, - }, - seller: { - email: currentAnswers?.seller?.email, - nationalId: currentAnswers?.seller?.nationalId, - }, - buyer: { - email: currentAnswers?.buyer?.email, - nationalId: currentAnswers?.buyer?.nationalId, - }, - buyerCoOwnerAndOperator: - currentAnswers?.buyerCoOwnerAndOperator?.map((x) => ({ - nationalId: x.nationalId || '', - email: x.email || '', - type: x.type, - wasRemoved: x.wasRemoved, - })), - buyerMainOperator: currentAnswers?.buyerMainOperator - ? { - nationalId: currentAnswers.buyerMainOperator.nationalId, - } - : null, - insurance: insurance ? { value: insurance } : null, - }, - }, - }) - } - } else { - await doApproveAndSubmit() - } + setButtonLoading(true) + setShouldLoadValidation(true) + await doApproveAndSubmit() } } @@ -278,60 +201,27 @@ export const Overview: FC< noInsuranceError={noInsuranceError} /> - {error && ( + {!buttonLoading && shouldLoadValidation && ( + + )} + + {!validationErrorFound && error && ( )} - {data?.vehicleOwnerChangeValidation?.hasError && - data.vehicleOwnerChangeValidation.errorMessages.length > 0 ? ( - - -
    - {data.vehicleOwnerChangeValidation.errorMessages.map( - (error: OwnerChangeValidationMessage) => { - const message = formatMessage( - getValueViaPath( - applicationCheck.validation, - error?.errorNo || '', - ), - ) - const defaultMessage = error.defaultMessage - const fallbackMessage = - formatMessage( - applicationCheck.validation.fallbackErrorMessage, - ) + - ' - ' + - error?.errorNo - - return ( -
  • - - {message || defaultMessage || fallbackMessage} - -
  • - ) - }, - )} -
-
- } - /> -
- ) : null} - - {!hasReviewerApproved(reviewerNationalId, application.answers) && + {canReviewerApprove(reviewerNationalId, application.answers) && application.state !== States.COMPLETED && ( @@ -346,7 +236,7 @@ export const Overview: FC< + + )} diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/Overview/sections/BuyerSection.tsx b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/Overview/sections/BuyerSection.tsx index 3aefa52bbfb4..dcb08a7493fa 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/Overview/sections/BuyerSection.tsx +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/Overview/sections/BuyerSection.tsx @@ -8,7 +8,7 @@ import { information, overview, review } from '../../../lib/messages' import { States } from '../../../lib/constants' import { ReviewGroup } from '../../ReviewGroup' import { ReviewScreenProps } from '../../../shared' -import { formatPhoneNumber, hasReviewerApproved } from '../../../utils' +import { formatPhoneNumber, canReviewerApprove } from '../../../utils' import kennitala from 'kennitala' export const BuyerSection: FC< @@ -30,7 +30,7 @@ export const BuyerSection: FC< > +} + export const ValidationErrorMessages: FC< - React.PropsWithChildren -> = (props) => { + React.PropsWithChildren +> = ({ showErrorOnly, setValidationErrorFound, ...props }) => { const { application, setFieldLoadingState } = props const { formatMessage } = useLocale() @@ -61,12 +72,18 @@ export const ValidationErrorMessages: FC< : null, }, }, + onCompleted: (data) => { + if (data?.vehicleOwnerChangeValidation?.hasError) { + setValidationErrorFound?.(true) + } + }, + fetchPolicy: 'no-cache', }, ) useEffect(() => { setFieldLoadingState?.(loading) - }, [loading]) + }, [loading, setFieldLoadingState]) return data?.vehicleOwnerChangeValidation?.hasError && data.vehicleOwnerChangeValidation.errorMessages.length > 0 ? ( @@ -76,7 +93,7 @@ export const ValidationErrorMessages: FC< title={formatMessage(applicationCheck.validation.alertTitle)} message={ -
    + {data.vehicleOwnerChangeValidation.errorMessages.map( (error: OwnerChangeValidationMessage) => { const message = formatMessage( @@ -94,20 +111,18 @@ export const ValidationErrorMessages: FC< error?.errorNo return ( -
  • - - {message || defaultMessage || fallbackMessage} - -
  • + + {message || defaultMessage || fallbackMessage} + ) }, )} -
+
} />
- ) : ( + ) : !showErrorOnly ? ( - ) + ) : null } diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/forms/TransferOfVehicleOwnershipForm/InformationSection/vehicleSubSection.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/forms/TransferOfVehicleOwnershipForm/InformationSection/vehicleSubSection.ts index 24869654cb25..7aba9479e20f 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/forms/TransferOfVehicleOwnershipForm/InformationSection/vehicleSubSection.ts +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/forms/TransferOfVehicleOwnershipForm/InformationSection/vehicleSubSection.ts @@ -7,7 +7,6 @@ import { buildHiddenInput, } from '@island.is/application/core' import { information } from '../../../lib/messages' -import { VehiclesCurrentVehicle } from '../../../shared' import { getSelectedVehicle } from '../../../utils' export const vehicleSubSection = buildSubSection({ @@ -29,8 +28,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.permno + ) + return vehicle?.permno }, }), buildTextField({ @@ -43,8 +42,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.make + ) + return vehicle?.make }, }), buildTextField({ @@ -72,8 +71,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.requireMileage || false + ) + return vehicle?.requireMileage || false }, }), buildHiddenInput({ @@ -82,8 +81,8 @@ export const vehicleSubSection = buildSubSection({ const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.mileageReading || '' + ) + return vehicle?.mileageReading || '' }, }), buildTextField({ @@ -92,18 +91,15 @@ export const vehicleSubSection = buildSubSection({ width: 'full', variant: 'number', condition: (answers, externalData) => { - const vehicle = getSelectedVehicle( - externalData, - answers, - ) as VehiclesCurrentVehicle + const vehicle = getSelectedVehicle(externalData, answers) return vehicle?.requireMileage || false }, placeholder(application) { const vehicle = getSelectedVehicle( application.externalData, application.answers, - ) as VehiclesCurrentVehicle - return vehicle.mileageReading + ) + return vehicle?.mileageReading ? `Síðasta skráning ${vehicle.mileageReading} Km` : '' }, diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/TransferOfVehicleOwnershipTemplate.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/TransferOfVehicleOwnershipTemplate.ts index c77287a619be..a1a53cc58794 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/TransferOfVehicleOwnershipTemplate.ts +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/TransferOfVehicleOwnershipTemplate.ts @@ -34,7 +34,7 @@ import { CurrentVehiclesApi, InsuranceCompaniesApi, } from '../dataProviders' -import { getChargeItemCodes, getExtraData, hasReviewerApproved } from '../utils' +import { getChargeItemCodes, getExtraData, canReviewerApprove } from '../utils' import { ApiScope } from '@island.is/auth/scopes' import { buildPaymentState } from '@island.is/application/utils' @@ -63,7 +63,7 @@ const reviewStatePendingAction = ( role: string, nationalId: string, ): PendingAction => { - if (nationalId && !hasReviewerApproved(nationalId, application.answers)) { + if (nationalId && canReviewerApprove(nationalId, application.answers)) { return { title: corePendingActionMessages.waitingForReviewTitle, content: corePendingActionMessages.youNeedToReviewDescription, diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/messages/review.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/messages/review.ts index dbd3a26196d1..fef98cc77fff 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/messages/review.ts +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/messages/review.ts @@ -157,8 +157,13 @@ export const review = { }, approve: { id: 'ta.tvo.application:review.buttons.approve', - defaultMessage: `Samþykkja`, + defaultMessage: 'Samþykkja', description: 'Approve button in review process', }, + tryAgain: { + id: 'ta.tvo.application:review.buttons.tryAgain', + defaultMessage: 'Reyna aftur', + description: 'Try again button in review process', + }, }), } diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/messages/steps.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/messages/steps.ts index 07d8a78ec0f5..3f62e174505f 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/messages/steps.ts +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/lib/messages/steps.ts @@ -27,7 +27,7 @@ export const review = { }, approve: { id: 'ta.tvo.application:review.buttons.approve', - defaultMessage: `Samþykkja`, + defaultMessage: 'Samþykkja', description: 'Approve button in review process', }, }), diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/canReviewerApprove.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/canReviewerApprove.ts new file mode 100644 index 000000000000..0c4bfe337640 --- /dev/null +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/canReviewerApprove.ts @@ -0,0 +1,93 @@ +import { FormValue } from '@island.is/application/types' +import { getValueViaPath } from '@island.is/application/core' +import { CoOwnerAndOperator, UserInformation } from '../shared' +import { applicationHasPendingApproval } from './isLastReviewer' + +// Function to check if the reviewer is authorized to approve and hasn't done that yet +export const canReviewerApprove = ( + reviewerNationalId: string, + answers: FormValue, +): boolean => { + // Check if reviewer is buyer and has not approved + const buyer = getValueViaPath(answers, 'buyer') as UserInformation + if (buyer?.nationalId === reviewerNationalId && !buyer.approved) { + return true + } + + // Check if reviewer is buyer's co-owner or operator and has not approved + const buyerCoOwnersAndOperators = ( + getValueViaPath( + answers, + 'buyerCoOwnerAndOperator', + [], + ) as CoOwnerAndOperator[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + if ( + buyerCoOwnersAndOperators.some( + ({ nationalId, approved }) => + nationalId === reviewerNationalId && !approved, + ) + ) { + return true + } + + // Check if reviewer is seller's co-owner and has not approved + const sellerCoOwners = getValueViaPath( + answers, + 'sellerCoOwner', + [], + ) as CoOwnerAndOperator[] + if ( + sellerCoOwners.some( + ({ nationalId, approved }) => + nationalId === reviewerNationalId && !approved, + ) + ) { + return true + } + + return false +} + +// Special case to allow seller (or any reviewer) to trigger an external API call to complete owner change +// Necessary when approve is updated in answers, but application is still stuck in REVIEW state +// then any user can try to 'push' the application to the next state +export const canReviewerReApprove = ( + reviewerNationalId: string, + answers: FormValue, +): boolean => { + const sellerNationalId = getValueViaPath( + answers, + 'seller.nationalId', + '', + ) as string + const buyerNationalId = getValueViaPath( + answers, + 'buyer.nationalId', + '', + ) as string + const buyerCoOwnersAndOperators = ( + getValueViaPath( + answers, + 'buyerCoOwnerAndOperator', + [], + ) as CoOwnerAndOperator[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + const sellerCoOwners = getValueViaPath( + answers, + 'sellerCoOwner', + [], + ) as CoOwnerAndOperator[] + + const isReviewerAuthorized = [ + sellerNationalId === reviewerNationalId, + buyerNationalId === reviewerNationalId, + buyerCoOwnersAndOperators.some( + ({ nationalId }) => nationalId === reviewerNationalId, + ), + sellerCoOwners.some(({ nationalId }) => nationalId === reviewerNationalId), + ].some(Boolean) + + // Check if the reviewer is authorized and if all required approvals have been completed + return isReviewerAuthorized && !applicationHasPendingApproval(answers) +} diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/getSelectedVehicle.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/getSelectedVehicle.ts index a13ea829dc3c..38e162f6a245 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/getSelectedVehicle.ts +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/getSelectedVehicle.ts @@ -5,7 +5,7 @@ import { CurrentVehiclesAndRecords, VehiclesCurrentVehicle } from '../shared' export const getSelectedVehicle = ( externalData: ExternalData, answers: FormValue, -): VehiclesCurrentVehicle => { +): VehiclesCurrentVehicle | undefined => { if (answers.findVehicle) { const vehicle = getValueViaPath( answers, diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/hasReviewerApproved.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/hasReviewerApproved.ts deleted file mode 100644 index 02d342d8c7f7..000000000000 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/hasReviewerApproved.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { FormValue } from '@island.is/application/types' -import { getValueViaPath } from '@island.is/application/core' -import { CoOwnerAndOperator, UserInformation } from '../shared' -import { isLastReviewer } from './isLastReviewer' - -export const hasReviewerApproved = ( - reviewerNationalId: string, - answers: FormValue, -) => { - // Check if reviewer is buyer and has not approved - if ( - (getValueViaPath(answers, 'buyer.nationalId', '') as string) === - reviewerNationalId - ) { - const buyer = getValueViaPath(answers, 'buyer') as UserInformation - const hasApproved = buyer?.approved || false - if (!hasApproved) return false - } - - // Check if reviewer is buyers coowner or operator and has not approved - const filteredBuyerCoOwnersAndOperators = ( - getValueViaPath( - answers, - 'buyerCoOwnerAndOperator', - [], - ) as CoOwnerAndOperator[] - ).filter(({ wasRemoved }) => wasRemoved !== 'true') - const buyerCoOwnerAndOperator = filteredBuyerCoOwnersAndOperators.find( - (coOwnerOrOperator) => coOwnerOrOperator.nationalId === reviewerNationalId, - ) - if (buyerCoOwnerAndOperator) { - const hasApproved = buyerCoOwnerAndOperator?.approved || false - if (!hasApproved) return false - } - - // Check if reviewer is sellers coowner and has not approved - const sellerCoOwners = getValueViaPath( - answers, - 'sellerCoOwner', - [], - ) as CoOwnerAndOperator[] - const sellerCoOwner = sellerCoOwners.find( - (coOwner) => coOwner.nationalId === reviewerNationalId, - ) - if (sellerCoOwner) { - const hasApproved = sellerCoOwner?.approved || false - if (!hasApproved) return false - } - - // Check if reviewer is seller and everyone else has approved - if ( - (getValueViaPath(answers, 'seller.nationalId', '') as string) === - reviewerNationalId && - isLastReviewer( - reviewerNationalId, - answers, - filteredBuyerCoOwnersAndOperators, - ) - ) { - return false - } - - return true -} diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/index.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/index.ts index ac668869ba8b..51bafc528202 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/index.ts +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/index.ts @@ -13,7 +13,7 @@ export const formatPhoneNumber = (value: string): string => export { getSelectedVehicle } from './getSelectedVehicle' export { getReviewSteps } from './getReviewSteps' -export { hasReviewerApproved } from './hasReviewerApproved' +export { canReviewerApprove, canReviewerReApprove } from './canReviewerApprove' export { getApproveAnswers } from './getApproveAnswers' export { isLastReviewer } from './isLastReviewer' export { getRejecter } from './getRejecter' diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/isLastReviewer.ts b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/isLastReviewer.ts index 8c3dacd58168..f57645404e72 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/isLastReviewer.ts +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/utils/isLastReviewer.ts @@ -2,77 +2,97 @@ import { getValueViaPath } from '@island.is/application/core' import { FormValue } from '@island.is/application/types' import { CoOwnerAndOperator, UserInformation } from '../shared' -export const isLastReviewer = ( - reviewerNationalId: string, +// Function to check if an application has pending approval +export const applicationHasPendingApproval = ( answers: FormValue, - newBuyerCoOwnerAndOperator: CoOwnerAndOperator[], -) => { - // 1. First check if any reviewer that is not the current user has not approved - - // Buyer + excludeNationalId?: string, +): boolean => { + // Check if buyer has not approved const buyer = getValueViaPath(answers, 'buyer', {}) as UserInformation - if (buyer.nationalId !== reviewerNationalId && !buyer.approved) { - return false + if ( + (!excludeNationalId || buyer.nationalId !== excludeNationalId) && + !buyer.approved + ) { + return true } - // Buyer's co-owner / Buyer's operator - const oldBuyerCoOwnersAndOperators = ( + // Check if any buyer's co-owners/operators have not approved + const buyerCoOwnersAndOperators = ( getValueViaPath( answers, 'buyerCoOwnerAndOperator', [], ) as CoOwnerAndOperator[] ).filter(({ wasRemoved }) => wasRemoved !== 'true') - const buyerCoOwnerAndOperatorHasNotApproved = - oldBuyerCoOwnersAndOperators.find((coOwnerOrOperator) => { - return ( - coOwnerOrOperator.nationalId !== reviewerNationalId && - !coOwnerOrOperator.approved - ) - }) - if (buyerCoOwnerAndOperatorHasNotApproved) { - return false + if ( + buyerCoOwnersAndOperators.some( + ({ nationalId, approved }) => + (!excludeNationalId || nationalId !== excludeNationalId) && !approved, + ) + ) { + return true } - // Seller's co-owner + // Check if any seller's co-owners have not approved const sellerCoOwners = getValueViaPath( answers, 'sellerCoOwner', [], ) as CoOwnerAndOperator[] - const sellerCoOwnerNotApproved = sellerCoOwners.find( - (coOwner) => coOwner.nationalId !== reviewerNationalId && !coOwner.approved, - ) - if (sellerCoOwnerNotApproved) { - return false + if ( + sellerCoOwners.some( + ({ nationalId, approved }) => + (!excludeNationalId || nationalId !== excludeNationalId) && !approved, + ) + ) { + return true } - // 2. Then check if user which is the last reviewer is a buyer and is adding more reviewers + return false +} + +// Function to check if the current reviewer is the last one who needs to approve +export const isLastReviewer = ( + reviewerNationalId: string, + answers: FormValue, + newBuyerCoOwnerAndOperator: CoOwnerAndOperator[], +): boolean => { + // If there are pending approvals (excluding current reviewer), then he is not the last reviewer + if (applicationHasPendingApproval(answers, reviewerNationalId)) return false + // If the current reviewer is the buyer, check for changes in buyer's co-owners/operators list + const buyer = getValueViaPath(answers, 'buyer', {}) as UserInformation if (buyer.nationalId === reviewerNationalId) { - // Check if buyerCoOwnerAndOperator did not change, then buyer is last reviewer + const oldBuyerCoOwnersAndOperators = ( + getValueViaPath( + answers, + 'buyerCoOwnerAndOperator', + [], + ) as CoOwnerAndOperator[] + ).filter(({ wasRemoved }) => wasRemoved !== 'true') + + // If no changes in buyer co-owner/operator list, the buyer is the last reviewer if (newBuyerCoOwnerAndOperator === oldBuyerCoOwnersAndOperators) { return true } - // Check if buyer added to buyerCoOwnerAndOperator, then buyer is not the last reviewer + // If new buyer co-owners/operators have been added, buyer is not the last reviewer if ( newBuyerCoOwnerAndOperator.length > oldBuyerCoOwnersAndOperators.length ) { return false } - //Check if buyer added (and removed) in buyerCoOwnerAndOperator, then buyer is not the last reviewer - const newReviewer = newBuyerCoOwnerAndOperator.find((newReviewer) => { - const sameReviewer = oldBuyerCoOwnersAndOperators.find( - (oldReviewer) => oldReviewer.nationalId === newReviewer.nationalId, - ) - return !sameReviewer - }) - if (newReviewer) { - return false - } + // If new reviewers were added (and others removed), the buyer is not the last reviewer + const newReviewerAdded = newBuyerCoOwnerAndOperator.some( + ({ nationalId }) => + !oldBuyerCoOwnersAndOperators.some( + (oldReviewer) => oldReviewer.nationalId === nationalId, + ), + ) + return !newReviewerAdded } + // Otherwise, the only review missing is from the current reviewer return true } diff --git a/libs/clients/transport-authority/vehicle-operators/src/lib/vehicleOperatorsClient.module.ts b/libs/clients/transport-authority/vehicle-operators/src/lib/vehicleOperatorsClient.module.ts index aaf3401c166d..e382021fb5d5 100644 --- a/libs/clients/transport-authority/vehicle-operators/src/lib/vehicleOperatorsClient.module.ts +++ b/libs/clients/transport-authority/vehicle-operators/src/lib/vehicleOperatorsClient.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common' import { VehicleOperatorsClient } from './vehicleOperatorsClient.service' import { exportedApis } from './apiConfiguration' +import { VehiclesMileageClientModule } from '@island.is/clients/vehicles-mileage' @Module({ + imports: [VehiclesMileageClientModule], providers: [...exportedApis, VehicleOperatorsClient], exports: [VehicleOperatorsClient], }) diff --git a/libs/clients/transport-authority/vehicle-operators/src/lib/vehicleOperatorsClient.service.ts b/libs/clients/transport-authority/vehicle-operators/src/lib/vehicleOperatorsClient.service.ts index cb5989c587a5..4d2b7d23558d 100644 --- a/libs/clients/transport-authority/vehicle-operators/src/lib/vehicleOperatorsClient.service.ts +++ b/libs/clients/transport-authority/vehicle-operators/src/lib/vehicleOperatorsClient.service.ts @@ -1,20 +1,32 @@ import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' import { Injectable } from '@nestjs/common' -import { ReturnTypeMessage } from '../../gen/fetch' import { OperatorApi } from '../../gen/fetch/apis' import { Operator, OperatorChangeValidation, } from './vehicleOperatorsClient.types' +import { + ErrorMessage, + getCleanErrorMessagesFromTryCatch, +} from '@island.is/clients/transport-authority/vehicle-owner-change' +import { MileageReadingApi } from '@island.is/clients/vehicles-mileage' +import { logger } from '@island.is/logging' @Injectable() export class VehicleOperatorsClient { - constructor(private readonly operatorsApi: OperatorApi) {} + constructor( + private readonly operatorsApi: OperatorApi, + private readonly mileageReadingApi: MileageReadingApi, + ) {} private operatorsApiWithAuth(auth: Auth) { return this.operatorsApi.withMiddleware(new AuthMiddleware(auth)) } + private mileageReadingApiWithAuth(auth: Auth) { + return this.mileageReadingApi.withMiddleware(new AuthMiddleware(auth)) + } + public async getOperators(auth: User, permno: string): Promise { const result = await this.operatorsApiWithAuth(auth).permnoGet({ apiVersion: '3.0', @@ -36,16 +48,36 @@ export class VehicleOperatorsClient { auth: User, permno: string, ): Promise { - return await this.validateAllForOperatorChange(auth, permno, null) + // Get current mileage reading + let currentMileage = 0 + try { + const mileageReadings = await this.mileageReadingApiWithAuth( + auth, + ).getMileageReading({ permno }) + currentMileage = mileageReadings?.[0]?.mileage || 0 + } catch (e) { + logger.error(e) + return { + hasError: true, + errorMessages: [{ defaultMessage: e.message }], + } + } + + return await this.validateAllForOperatorChange( + auth, + permno, + null, + currentMileage + 1, + ) } public async validateAllForOperatorChange( auth: User, permno: string, operators: Operator[] | null, - mileage?: number | null, + mileage: number | null, ): Promise { - let errorList: ReturnTypeMessage[] | undefined + let errorMessages: ErrorMessage[] | undefined // In case we dont have the operators selected yet, // then we will send in the owner as operator @@ -72,35 +104,14 @@ export class VehicleOperatorsClient { }, }) } catch (e) { - // Note: We need to wrap in try-catch to get the error messages, because if this action results in error, - // we get 4xx error (instead of 200 with error messages) with the errorList in this field - // ("body.Errors" for input validation, and "body" for data validation (in database)), - // that is of the same class as 200 result schema - if (e?.body?.Errors && Array.isArray(e.body.Errors)) { - errorList = e.body.Errors as ReturnTypeMessage[] - } else if (e?.body && Array.isArray(e.body)) { - errorList = e.body as ReturnTypeMessage[] - } else { - throw e - } + // Note: We had to wrap in try-catch to get the error messages, because if this action results in error, + // we get 4xx error (instead of 200 with error messages) with the error messages in the body + errorMessages = getCleanErrorMessagesFromTryCatch(e) } - const warnSeverityError = 'E' - const warnSeverityLock = 'L' - errorList = errorList?.filter( - (x) => - x.errorMess && - (x.warnSever === warnSeverityError || x.warnSever === warnSeverityLock), - ) - return { - hasError: !!errorList?.length, - errorMessages: errorList?.map((item) => { - return { - errorNo: (item.warnSever || '_') + item.warningSerialNumber, - defaultMessage: item.errorMess, - } - }), + hasError: !!errorMessages?.length, + errorMessages: errorMessages, } } @@ -112,33 +123,48 @@ export class VehicleOperatorsClient { permno: string, operators: Operator[], mileage?: number | null, - ): Promise { + ): Promise { + let errorMessages: ErrorMessage[] | undefined + if (operators.length === 0) { - await this.operatorsApiWithAuth(auth).closeWithoutcontractPost({ - apiVersion: '3.0', - apiVersion2: '3.0', - postCloseOperatorsWithoutContractModel: { - permno: permno, - endDate: new Date(), - reportingPersonIdNumber: auth.nationalId, - }, - }) + try { + await this.operatorsApiWithAuth(auth).closeWithoutcontractPost({ + apiVersion: '3.0', + apiVersion2: '3.0', + postCloseOperatorsWithoutContractModel: { + permno: permno, + endDate: new Date(), + reportingPersonIdNumber: auth.nationalId, + }, + }) + } catch (e) { + errorMessages = getCleanErrorMessagesFromTryCatch(e) + } } else { - await this.operatorsApiWithAuth(auth).withoutcontractPost({ - apiVersion: '3.0', - apiVersion2: '3.0', - postOperatorsWithoutContractModel: { - permno: permno, - startDate: new Date(), - reportingPersonIdNumber: auth.nationalId, - onlyRunFlexibleWarning: false, - mileage: mileage, - operators: operators.map((operator) => ({ - personIdNumber: operator.ssn || '', - mainOperator: operator.isMainOperator ? 1 : 0, - })), - }, - }) + try { + await this.operatorsApiWithAuth(auth).withoutcontractPost({ + apiVersion: '3.0', + apiVersion2: '3.0', + postOperatorsWithoutContractModel: { + permno: permno, + startDate: new Date(), + reportingPersonIdNumber: auth.nationalId, + onlyRunFlexibleWarning: false, + mileage: mileage, + operators: operators.map((operator) => ({ + personIdNumber: operator.ssn || '', + mainOperator: operator.isMainOperator ? 1 : 0, + })), + }, + }) + } catch (e) { + errorMessages = getCleanErrorMessagesFromTryCatch(e) + } + } + + return { + hasError: !!errorMessages?.length, + errorMessages: errorMessages, } } } diff --git a/libs/clients/transport-authority/vehicle-owner-change/src/index.ts b/libs/clients/transport-authority/vehicle-owner-change/src/index.ts index 7cea2073a52a..2e9322e2605b 100644 --- a/libs/clients/transport-authority/vehicle-owner-change/src/index.ts +++ b/libs/clients/transport-authority/vehicle-owner-change/src/index.ts @@ -3,3 +3,8 @@ export * from './lib/vehicleOwnerChangeClient.module' export * from './lib/vehicleOwnerChangeClient.types' export { VehicleOwnerChangeClientConfig } from './lib/vehicleOwnerChangeClient.config' + +export { + ErrorMessage, + getCleanErrorMessagesFromTryCatch, +} from './lib/vehicleOwnerChangeClient.utils' diff --git a/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.module.ts b/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.module.ts index bbf856b6ebb3..0b7b6d2ed27a 100644 --- a/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.module.ts +++ b/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common' import { VehicleOwnerChangeClient } from './vehicleOwnerChangeClient.service' import { exportedApis } from './apiConfiguration' +import { VehiclesMileageClientModule } from '@island.is/clients/vehicles-mileage' @Module({ + imports: [VehiclesMileageClientModule], providers: [...exportedApis, VehicleOwnerChangeClient], exports: [VehicleOwnerChangeClient], }) diff --git a/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.service.ts b/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.service.ts index b8c27504d343..24a062649dcc 100644 --- a/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.service.ts +++ b/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.service.ts @@ -1,29 +1,58 @@ import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' import { Injectable } from '@nestjs/common' -import { ReturnTypeMessage } from '../../gen/fetch' import { OwnerChangeApi } from '../../gen/fetch/apis' import { NewestOwnerChange, OwnerChange, OwnerChangeValidation, } from './vehicleOwnerChangeClient.types' -import { getDateAtTimestamp } from './vehicleOwnerChangeClient.utils' +import { + ErrorMessage, + getCleanErrorMessagesFromTryCatch, + getDateAtTimestamp, +} from './vehicleOwnerChangeClient.utils' +import { MileageReadingApi } from '@island.is/clients/vehicles-mileage' +import { logger } from '@island.is/logging' @Injectable() export class VehicleOwnerChangeClient { - constructor(private readonly ownerchangeApi: OwnerChangeApi) {} + constructor( + private readonly ownerchangeApi: OwnerChangeApi, + private readonly mileageReadingApi: MileageReadingApi, + ) {} private ownerchangeApiWithAuth(auth: Auth) { return this.ownerchangeApi.withMiddleware(new AuthMiddleware(auth)) } + private mileageReadingApiWithAuth(auth: Auth) { + return this.mileageReadingApi.withMiddleware(new AuthMiddleware(auth)) + } + public async validateVehicleForOwnerChange( auth: User, permno: string, ): Promise { // Note: since the vehiclecheck endpoint is funky, we will instead just use the personcheck endpoint // and send in dummy data where needed + const todayStr = new Date().toISOString() + + // Get current mileage reading + let currentMileage = 0 + try { + const mileageReadings = await this.mileageReadingApiWithAuth( + auth, + ).getMileageReading({ permno }) + currentMileage = mileageReadings?.[0]?.mileage || 0 + } catch (e) { + logger.error(e) + return { + hasError: true, + errorMessages: [{ defaultMessage: e.message }], + } + } + return await this.validateAllForOwnerChange(auth, { permno: permno, seller: { @@ -40,6 +69,7 @@ export class VehicleOwnerChangeClient { insuranceCompanyCode: null, operators: null, coOwners: null, + mileage: currentMileage + 1, }) } @@ -49,7 +79,7 @@ export class VehicleOwnerChangeClient { ): Promise { const useGroup = '000' - let errorList: ReturnTypeMessage[] | undefined + let errorMessages: ErrorMessage[] | undefined try { // Note: If insurance company has not been supplied (we have not required the user to fill in at this point), @@ -67,7 +97,7 @@ export class VehicleOwnerChangeClient { ownerChange.dateOfPurchaseTimestamp, ) - // Note: we have manually changed this endpoint to void, since the messages we want only + // Note: we have manually changed this endpoint to void (in clientConfig), since the messages we want only // come with error code 400. If this function returns an array of ReturnTypeMessage, then // we will get an error with code 204, since the openapi generator tries to convert empty result // into an array of ReturnTypeMessage @@ -92,44 +122,14 @@ export class VehicleOwnerChangeClient { }, }) } catch (e) { - // Note: We need to wrap in try-catch to get the error messages, because if this action results in error, - // we get 4xx error (instead of 200 with error messages) with the errorList in this field - // ("body.Errors" for input validation, and "body" for data validation (in database)), - // that is of the same class as 200 result schema - if (e?.body?.Errors && Array.isArray(e.body.Errors)) { - errorList = e.body.Errors as ReturnTypeMessage[] - } else if (e?.body && Array.isArray(e.body)) { - errorList = e.body as ReturnTypeMessage[] - } else { - throw e - } + // Note: We had to wrap in try-catch to get the error messages, because if this action results in error, + // we get 4xx error (instead of 200 with error messages) with the error messages in the body + errorMessages = getCleanErrorMessagesFromTryCatch(e) } - const warnSeverityError = 'E' - const warnSeverityLock = 'L' - errorList = errorList?.filter( - (x) => - x.errorMess && - (x.warnSever === warnSeverityError || x.warnSever === warnSeverityLock), - ) - return { - hasError: !!errorList?.length, - errorMessages: errorList?.map((item) => { - let errorNo = item.warningSerialNumber?.toString() - - // Note: For vehicle locks, we need to do some special parsing since - // the error number (warningSerialNumber) is always -1 for locks, - // but the number is included in the errorMess field (value before the first space) - if (item.warnSever === warnSeverityLock) { - errorNo = item.errorMess?.split(' ')[0] - } - - return { - errorNo: (item.warnSever || '_') + errorNo, - defaultMessage: item.errorMess, - } - }), + hasError: !!errorMessages?.length, + errorMessages: errorMessages, } } @@ -160,7 +160,7 @@ export class VehicleOwnerChangeClient { public async saveOwnerChange( auth: User, ownerChange: OwnerChange, - ): Promise { + ): Promise { const useGroup = '000' // Note: API throws error if timestamp is 00:00:00, so we will use @@ -170,31 +170,42 @@ export class VehicleOwnerChangeClient { ownerChange.dateOfPurchaseTimestamp, ) - await this.ownerchangeApiWithAuth(auth).rootPost({ - apiVersion: '2.0', - apiVersion2: '2.0', - postOwnerChange: { - permno: ownerChange.permno, - sellerPersonIdNumber: ownerChange.seller.ssn, - sellerEmail: ownerChange.seller.email, - buyerPersonIdNumber: ownerChange.buyer.ssn, - buyerEmail: ownerChange.buyer.email, - dateOfPurchase: purchaseDate, - saleAmount: ownerChange.saleAmount, - insuranceCompanyCode: ownerChange.insuranceCompanyCode || '', - useGroup: useGroup, - operatorEmail: ownerChange.operators?.find((x) => x.isMainOperator) - ?.email, - operators: ownerChange.operators?.map((operator) => ({ - personIdNumber: operator.ssn, - mainOperator: operator.isMainOperator ? 1 : 0, - })), - coOwners: ownerChange.coOwners?.map((coOwner) => ({ - personIdNumber: coOwner.ssn, - })), - reportingPersonIdNumber: auth.nationalId, - mileage: ownerChange.mileage, - }, - }) + let errorMessages: ErrorMessage[] | undefined + + try { + await this.ownerchangeApiWithAuth(auth).rootPost({ + apiVersion: '2.0', + apiVersion2: '2.0', + postOwnerChange: { + permno: ownerChange.permno, + sellerPersonIdNumber: ownerChange.seller.ssn, + sellerEmail: ownerChange.seller.email, + buyerPersonIdNumber: ownerChange.buyer.ssn, + buyerEmail: ownerChange.buyer.email, + dateOfPurchase: purchaseDate, + saleAmount: ownerChange.saleAmount, + insuranceCompanyCode: ownerChange.insuranceCompanyCode || '', + useGroup: useGroup, + operatorEmail: ownerChange.operators?.find((x) => x.isMainOperator) + ?.email, + operators: ownerChange.operators?.map((operator) => ({ + personIdNumber: operator.ssn, + mainOperator: operator.isMainOperator ? 1 : 0, + })), + coOwners: ownerChange.coOwners?.map((coOwner) => ({ + personIdNumber: coOwner.ssn, + })), + reportingPersonIdNumber: auth.nationalId, + mileage: ownerChange.mileage, + }, + }) + } catch (e) { + errorMessages = getCleanErrorMessagesFromTryCatch(e) + } + + return { + hasError: !!errorMessages?.length, + errorMessages: errorMessages, + } } } diff --git a/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.utils.ts b/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.utils.ts index 530e731c2a64..78b75aed8566 100644 --- a/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.utils.ts +++ b/libs/clients/transport-authority/vehicle-owner-change/src/lib/vehicleOwnerChangeClient.utils.ts @@ -1,6 +1,69 @@ +import { ReturnTypeMessage } from '../../gen/fetch' + // Returns date object with at the selected timestamp export const getDateAtTimestamp = (oldDate: Date, timestamp: string): Date => { const newDate = oldDate instanceof Date && !isNaN(oldDate.getDate()) ? oldDate : new Date() return new Date(newDate.toISOString().substring(0, 10) + 'T' + timestamp) } + +export interface ErrorMessage { + errorNo: string | undefined + defaultMessage: string | null | undefined +} + +export const getCleanErrorMessagesFromTryCatch = (e: any): ErrorMessage[] => { + let errorList: ReturnTypeMessage[] | undefined + let filteredErrorList: ReturnTypeMessage[] | undefined + + // Note: Error message will be in the field "body.Errors" for input validation, + // and "body" for data validation (in database) + // Note: The schema for the errors is the same as the schema for the 200 response + // result schema (ReturnTypeMessage[]) + if (e?.body?.Errors && Array.isArray(e.body.Errors)) { + errorList = e.body.Errors as ReturnTypeMessage[] + } else if (e?.body && Array.isArray(e.body)) { + errorList = e.body as ReturnTypeMessage[] + } else { + throw e + } + + const warnSeverityError = 'E' + const warnSeverityLock = 'L' + const warnSeverityWarning = 'W' + + // Note: All three types of warnSever (E, L, W) will cause the api to throw error + // (and not continue execute on SGS side). But some messages are duplicated as both + // E and W. To prevent us from displaying the duplicated W error, we will first + // check for E and L, if none, then display W errors + + // First check if there are any E or L errors + filteredErrorList = errorList?.filter( + (x) => + x.errorMess && + (x.warnSever === warnSeverityError || x.warnSever === warnSeverityLock), + ) + + // If not, then check for W error + if (!filteredErrorList?.length) { + filteredErrorList = errorList?.filter( + (x) => x.errorMess && x.warnSever === warnSeverityWarning, + ) + } + + return filteredErrorList?.map((item) => { + let errorNo = item.warningSerialNumber?.toString() + + // Note: For vehicle locks, we need to do some special parsing since + // the error number (warningSerialNumber) is always -1 for locks, + // but the number is included in the errorMess field (value before the first space) + if (item.warnSever === warnSeverityLock) { + errorNo = item.errorMess?.split(' ')[0] + } + + return { + errorNo: errorNo ? (item.warnSever || '_') + errorNo : undefined, + defaultMessage: item.errorMess, + } + }) +} diff --git a/libs/clients/transport-authority/vehicle-plate-ordering/src/lib/vehiclePlateOrderingClient.service.ts b/libs/clients/transport-authority/vehicle-plate-ordering/src/lib/vehiclePlateOrderingClient.service.ts index 6120232c061b..069bb05e5bf9 100644 --- a/libs/clients/transport-authority/vehicle-plate-ordering/src/lib/vehiclePlateOrderingClient.service.ts +++ b/libs/clients/transport-authority/vehicle-plate-ordering/src/lib/vehiclePlateOrderingClient.service.ts @@ -1,6 +1,5 @@ import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' import { Injectable } from '@nestjs/common' -import { ReturnTypeMessage } from '../../gen/fetch' import { PlateOrderingApi } from '../../gen/fetch/apis' import { DeliveryStation, @@ -9,6 +8,10 @@ import { PlateOrder, PlateOrderValidation, } from './vehiclePlateOrderingClient.types' +import { + ErrorMessage, + getCleanErrorMessagesFromTryCatch, +} from '@island.is/clients/transport-authority/vehicle-owner-change' @Injectable() export class VehiclePlateOrderingClient { @@ -35,84 +38,93 @@ export class VehiclePlateOrderingClient { })) } - public async validatePlateOrder( + public async validateVehicleForPlateOrder( auth: User, permno: string, frontType: string, rearType: string, ): Promise { - let errorList: ReturnTypeMessage[] | undefined + // Dummy values + // Note: option "Pick up at Samgöngustofa" which is always valid + const deliveryStationType = SGS_DELIVERY_STATION_TYPE + const deliveryStationCode = SGS_DELIVERY_STATION_CODE + const expressOrder = false - try { - // Dummy values - // Note: option "Pick up at Samgöngustofa" which is always valid - const deliveryStationType = SGS_DELIVERY_STATION_TYPE - const deliveryStationCode = SGS_DELIVERY_STATION_CODE - const expressOrder = false + return await this.validateAllForPlateOrder( + auth, + permno, + frontType, + rearType, + deliveryStationType, + deliveryStationCode, + expressOrder, + ) + } + + public async validateAllForPlateOrder( + auth: User, + permno: string, + frontType: string, + rearType: string, + deliveryStationType: string, + deliveryStationCode: string, + expressOrder: boolean, + ): Promise { + let errorMessages: ErrorMessage[] | undefined + try { await this.plateOrderingApiWithAuth(auth).orderplatesPost({ apiVersion: '1.0', apiVersion2: '1.0', postOrderPlatesModel: { permno: permno, - frontType: frontType, - rearType: rearType, - stationToDeliverTo: deliveryStationCode, - stationType: deliveryStationType, + frontType: frontType || null, + rearType: rearType || null, + stationToDeliverTo: deliveryStationCode || '', + stationType: deliveryStationType || '', expressOrder: expressOrder, checkOnly: true, // to make sure we are only validating }, }) } catch (e) { - // Note: We need to wrap in try-catch to get the error messages, because if this action results in error, - // we get 4xx error (instead of 200 with error messages) with the errorList in this field - // ("body.Errors" for input validation, and "body" for data validation (in database)), - // that is of the same class as 200 result schema - if (e?.body?.Errors && Array.isArray(e.body.Errors)) { - errorList = e.body.Errors as ReturnTypeMessage[] - } else if (e?.body && Array.isArray(e.body)) { - errorList = e.body as ReturnTypeMessage[] - } else { - throw e - } + // Note: We had to wrap in try-catch to get the error messages, because if this action results in error, + // we get 4xx error (instead of 200 with error messages) with the error messages in the body + errorMessages = getCleanErrorMessagesFromTryCatch(e) } - const warnSeverityError = 'E' - const warnSeverityWarning = 'W' - errorList = errorList?.filter( - (x) => - x.errorMess && - (x.warnSever === warnSeverityError || - x.warnSever === warnSeverityWarning), - ) - return { - hasError: !!errorList?.length, - errorMessages: errorList?.map((item) => { - return { - errorNo: (item.warnSever || '_') + item.warningSerialNumber, - defaultMessage: item.errorMess, - } - }), + hasError: !!errorMessages?.length, + errorMessages: errorMessages, } } public async savePlateOrders( auth: User, plateOrder: PlateOrder, - ): Promise { - await this.plateOrderingApiWithAuth(auth).orderplatesPost({ - apiVersion: '1.0', - apiVersion2: '1.0', - postOrderPlatesModel: { - permno: plateOrder.permno, - frontType: plateOrder.frontType, - rearType: plateOrder.rearType || null, - stationToDeliverTo: plateOrder.deliveryStationCode || '', - stationType: plateOrder.deliveryStationType || '', - expressOrder: plateOrder.expressOrder, - checkOnly: false, - }, - }) + ): Promise { + let errorMessages: ErrorMessage[] | undefined + + try { + await this.plateOrderingApiWithAuth(auth).orderplatesPost({ + apiVersion: '1.0', + apiVersion2: '1.0', + postOrderPlatesModel: { + permno: plateOrder.permno, + frontType: plateOrder.frontType || null, + rearType: plateOrder.rearType || null, + stationToDeliverTo: plateOrder.deliveryStationCode || '', + stationType: plateOrder.deliveryStationType || '', + expressOrder: plateOrder.expressOrder, + checkOnly: false, + }, + }) + } catch (e) { + errorMessages = getCleanErrorMessagesFromTryCatch(e) + } + + return { + hasError: !!errorMessages?.length, + errorMessages: errorMessages, + } } } diff --git a/libs/clients/transport-authority/vehicle-plate-renewal/src/lib/vehiclePlateRenewalClient.service.ts b/libs/clients/transport-authority/vehicle-plate-renewal/src/lib/vehiclePlateRenewalClient.service.ts index 09502fddece5..d96b4fc29371 100644 --- a/libs/clients/transport-authority/vehicle-plate-renewal/src/lib/vehiclePlateRenewalClient.service.ts +++ b/libs/clients/transport-authority/vehicle-plate-renewal/src/lib/vehiclePlateRenewalClient.service.ts @@ -6,12 +6,10 @@ import { PlateOwnershipValidation, } from './vehiclePlateRenewalClient.types' import { PlateOwnershipApiWithoutIdsAuth } from './apiConfiguration' - -interface ReturnTypeMessage { - warnSever?: string | null - errorMess?: string | null - warningSerialNumber?: number | null -} +import { + ErrorMessage, + getCleanErrorMessagesFromTryCatch, +} from '@island.is/clients/transport-authority/vehicle-owner-change' @Injectable() export class VehiclePlateRenewalClient { @@ -49,7 +47,7 @@ export class VehiclePlateRenewalClient { auth: User, regno: string, ): Promise { - let errorList: ReturnTypeMessage[] | undefined + let errorMessages: ErrorMessage[] | undefined try { await this.plateOwnershipApiWithAuth(auth).renewplateownershipPost({ @@ -62,45 +60,41 @@ export class VehiclePlateRenewalClient { }, }) } catch (e) { - // Note: We need to wrap in try-catch to get the error messages, because if this action results in error, - // we get 4xx error (instead of 200 with error messages) with the errorList in this field - // ("body.Errors" for input validation, and "body" for data validation (in database)), - // that is of the same class as 200 result schema - if (e?.body?.Errors) { - errorList = e.body.Errors as ReturnTypeMessage[] - } else if (e?.body) { - errorList = e.body as ReturnTypeMessage[] - } else { - throw e - } + // Note: We had to wrap in try-catch to get the error messages, because if this action results in error, + // we get 4xx error (instead of 200 with error messages) with the error messages in the body + errorMessages = getCleanErrorMessagesFromTryCatch(e) } - const warnSeverityError = 'E' - errorList = errorList?.filter( - (x) => x.errorMess && x.warnSever === warnSeverityError, - ) - return { - hasError: !!errorList?.length, - errorMessages: errorList?.map((item) => { - return { - errorNo: (item.warnSever || '_') + item.warningSerialNumber, - defaultMessage: item.errorMess, - } - }), + hasError: !!errorMessages?.length, + errorMessages: errorMessages, } } - public async renewPlateOwnership(auth: User, regno: string): Promise { - await this.plateOwnershipApiWithAuth(auth).renewplateownershipPost({ - apiVersion: '1.0', - apiVersion2: '1.0', - postRenewPlateOwnershipModel: { - regno: regno, - persidno: auth.nationalId, - check: false, - }, - }) + public async renewPlateOwnership( + auth: User, + regno: string, + ): Promise { + let errorMessages: ErrorMessage[] | undefined + + try { + await this.plateOwnershipApiWithAuth(auth).renewplateownershipPost({ + apiVersion: '1.0', + apiVersion2: '1.0', + postRenewPlateOwnershipModel: { + regno: regno, + persidno: auth.nationalId, + check: false, + }, + }) + } catch (e) { + errorMessages = getCleanErrorMessagesFromTryCatch(e) + } + + return { + hasError: !!errorMessages?.length, + errorMessages: errorMessages, + } } public async getPlateAvailability(regno: string) { From 74eaed47771d0b8142c32e6644442ec98baa782b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Eorkell=20M=C3=A1ni=20=C3=9Eorkelsson?= Date: Mon, 18 Nov 2024 10:51:24 +0000 Subject: [PATCH 08/34] feat(grants): Grants plaza UI (#16505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: wip grants home page * feat: add client * feat: init service * feat: can create on contentful * chore: nx format:write update dirty files * feat: add generic tags query * feat: populate tag groups * fix: add slug * feat: search resulsts page * feat: ui updates * feat: generate grant model * feat: generate types * feat: implement es * feat: add elastic search for grants * feat: fetching grant from cms works * feat: add plaza ui * fix: remove old stuff * chore: remove from base * feat: populate filter tags * feat: update as * feat: filtering works * chore: clean up logs * fix: applcation status filter * feat: create grant * feat: more routes * feat: single grant works * chore: rename card * chore: fix real import * fix: homepage links correctly to search results * fix: better es config * chore: add to environmetn * feat: Elastic search worksga . * fix: es filtering * feat: initial render filtering * feat: small fixes * feat: add fund type * feat: add fund to grant * feat: added fund to single grant page * feat: update content types * fix: add nothing found state * chore: add border radius * fix: search query * chore: nx format:write update dirty files * fix: deploy search indexer * chore: add todo reminder * chore: update tag * chore: update page title * fix: change string2 * chore: revert unexclusion * chore: update contentful Fund model * fix: update grant model2 * fix: change grant status default * fix: richtext ordering * fix: fund link works * chore: coderabbit improvements * chore: search labels * chore: simplify wrapper * fix: header image render * fix: coderabbit suggestions * fix: undefined dates * fix: bunny comments * fix: some fixes * fix: use undefined if applicationId missing * fix: wrong label * fix: image props * fix: remove loading * fix. dates * fix: update args * fix: alt text * fix: localize desc * fix: wrong desc * chore: forgot codeowners * fix: review comments plus simpler url param parsing * fix: remove grant from nested content types * fix: remove unnecessary locale passing * chore: revert graphql changes * chore: codeowners * fix: wrapper stuff * fix: remove loading * fix: mobile improvements * fix: no image if tablet * chore: also mobile * chore: extract content logic * chore: improts * fix: localization * fix: update types2 * fix: bunny suggestions * fix: bit o'padding * fix: add 404 if invalid grant * fix: build broke --------- Co-authored-by: Ásdís Erna Guðmundsdóttir Co-authored-by: andes-it Co-authored-by: Ásdís Erna Guðmundsdóttir Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .github/CODEOWNERS | 5 +- .../HeaderWithImage/HeaderWithImage.css.ts | 11 + .../Grant/HeaderWithImage/HeaderWithImage.tsx | 80 ++++ .../components/Grant/HeaderWithImage/index.ts | 3 + .../Grant/SearchSection/SearchSection.css.ts | 11 + .../Grant/SearchSection/SearchSection.tsx | 62 +++ .../components/Grant/SearchSection/index.tsx | 3 + .../components/Grant/Wrapper/GrantWrapper.tsx | 32 ++ apps/web/components/Grant/index.ts | 3 + .../web/components/PlazaCard/PlazaCard.css.ts | 5 + apps/web/components/PlazaCard/PlazaCard.tsx | 171 ++++++++ .../SearchSection/SearchSection.tsx | 15 +- apps/web/components/real.ts | 2 + .../hooks/useLinkResolver/useLinkResolver.ts | 12 + apps/web/pages/styrkjatorg/index.ts | 11 + apps/web/pages/styrkjatorg/leit/index.ts | 12 + apps/web/pages/styrkjatorg/styrkur/[id].ts | 12 + apps/web/public/assets/sofa.svg | 1 + apps/web/screens/Grants/Grant/Grant.tsx | 220 ++++++++++ .../web/screens/Grants/Grant/GrantSidebar.tsx | 133 ++++++ apps/web/screens/Grants/Home/GrantsHome.tsx | 203 +++++++++ .../Grants/SearchResults/SearchResults.tsx | 413 ++++++++++++++++++ .../SearchResults/SearchResultsContent.tsx | 151 +++++++ .../SearchResults/SearchResultsFilter.tsx | 137 ++++++ apps/web/screens/Grants/messages.ts | 165 +++++++ apps/web/screens/queries/GenericTag.ts | 15 + apps/web/screens/queries/Grants.ts | 126 ++++++ apps/web/screens/queries/index.tsx | 1 + libs/cms/src/lib/cms.contentful.service.ts | 43 +- libs/cms/src/lib/cms.elasticsearch.service.ts | 118 +++++ libs/cms/src/lib/cms.resolver.ts | 30 ++ .../dto/getGenericTagsInTagGroups.input.ts | 14 + libs/cms/src/lib/dto/getGrants.input.ts | 45 ++ libs/cms/src/lib/dto/getSingleGrant.input.ts | 14 + libs/cms/src/lib/environments/environment.ts | 1 + .../src/lib/generated/contentfulTypes.d.ts | 167 ++++++- libs/cms/src/lib/models/fund.model.ts | 35 ++ libs/cms/src/lib/models/grant.model.ts | 129 ++++++ libs/cms/src/lib/models/grantList.model.ts | 12 + libs/cms/src/lib/search/cmsSync.module.ts | 2 + .../lib/search/importers/grants.service.ts | 133 ++++++ libs/cms/src/lib/search/mapping.service.ts | 3 + libs/shared/types/src/lib/api-cms-domain.ts | 1 + 43 files changed, 2749 insertions(+), 13 deletions(-) create mode 100644 apps/web/components/Grant/HeaderWithImage/HeaderWithImage.css.ts create mode 100644 apps/web/components/Grant/HeaderWithImage/HeaderWithImage.tsx create mode 100644 apps/web/components/Grant/HeaderWithImage/index.ts create mode 100644 apps/web/components/Grant/SearchSection/SearchSection.css.ts create mode 100644 apps/web/components/Grant/SearchSection/SearchSection.tsx create mode 100644 apps/web/components/Grant/SearchSection/index.tsx create mode 100644 apps/web/components/Grant/Wrapper/GrantWrapper.tsx create mode 100644 apps/web/components/Grant/index.ts create mode 100644 apps/web/components/PlazaCard/PlazaCard.css.ts create mode 100644 apps/web/components/PlazaCard/PlazaCard.tsx create mode 100644 apps/web/pages/styrkjatorg/index.ts create mode 100644 apps/web/pages/styrkjatorg/leit/index.ts create mode 100644 apps/web/pages/styrkjatorg/styrkur/[id].ts create mode 100644 apps/web/public/assets/sofa.svg create mode 100644 apps/web/screens/Grants/Grant/Grant.tsx create mode 100644 apps/web/screens/Grants/Grant/GrantSidebar.tsx create mode 100644 apps/web/screens/Grants/Home/GrantsHome.tsx create mode 100644 apps/web/screens/Grants/SearchResults/SearchResults.tsx create mode 100644 apps/web/screens/Grants/SearchResults/SearchResultsContent.tsx create mode 100644 apps/web/screens/Grants/SearchResults/SearchResultsFilter.tsx create mode 100644 apps/web/screens/Grants/messages.ts create mode 100644 apps/web/screens/queries/Grants.ts create mode 100644 libs/cms/src/lib/dto/getGenericTagsInTagGroups.input.ts create mode 100644 libs/cms/src/lib/dto/getGrants.input.ts create mode 100644 libs/cms/src/lib/dto/getSingleGrant.input.ts create mode 100644 libs/cms/src/lib/models/fund.model.ts create mode 100644 libs/cms/src/lib/models/grant.model.ts create mode 100644 libs/cms/src/lib/models/grantList.model.ts create mode 100644 libs/cms/src/lib/search/importers/grants.service.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index df1c36fb075a..00932606d1b3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -131,6 +131,9 @@ codemagic.yaml /apps/portals/my-pages*/ @island-is/hugsmidjan /apps/services/regulations-admin-backend/ @island-is/hugsmidjan /apps/services/user-profile/ @island-is/hugsmidjan @island-is/juni @island-is/aranja +/apps/web/components/Grant/ @island-is/hugsmidjan +/apps/web/components/PlazaCard/ @island-is/hugsmidjan +/apps/web/screens/Grants/ @island-is/hugsmidjan /apps/web/screens/Regulations/ @island-is/hugsmidjan /apps/web/components/Regulations/ @island-is/hugsmidjan /apps/web/screens/OfficialJournalOfIceland/ @island-is/hugsmidjan @@ -390,7 +393,7 @@ codemagic.yaml # QA /apps/system-e2e/ @island-is/qa -/libs/testing/e2e @island-is/qa +/libs/testing/e2e @island-is/qa # Islandis /apps/system-e2e/src/tests/islandis/admin-portal/ @island-is/aranja diff --git a/apps/web/components/Grant/HeaderWithImage/HeaderWithImage.css.ts b/apps/web/components/Grant/HeaderWithImage/HeaderWithImage.css.ts new file mode 100644 index 000000000000..459cc3529107 --- /dev/null +++ b/apps/web/components/Grant/HeaderWithImage/HeaderWithImage.css.ts @@ -0,0 +1,11 @@ +import { style } from '@vanilla-extract/css' + +import { spacing } from '@island.is/island-ui/theme' + +export const introImage = style({ + marginBottom: -spacing[2], + maxHeight: '17em', + display: 'block', + margin: 'auto', + width: '100%', +}) diff --git a/apps/web/components/Grant/HeaderWithImage/HeaderWithImage.tsx b/apps/web/components/Grant/HeaderWithImage/HeaderWithImage.tsx new file mode 100644 index 000000000000..9cd40930294d --- /dev/null +++ b/apps/web/components/Grant/HeaderWithImage/HeaderWithImage.tsx @@ -0,0 +1,80 @@ +import { ReactNode } from 'react' + +import { + Box, + GridColumn, + GridContainer, + GridRow, + Text, +} from '@island.is/island-ui/core' + +import * as styles from './HeaderWithImage.css' + +export type HeaderProps = { + title: string + description?: string + imageLayout?: 'left' | 'right' + breadcrumbs: ReactNode + children?: ReactNode +} + +export type ImageProps = HeaderProps & { + featuredImage: string + featuredImageAlt: string +} + +export type NoImageProps = HeaderProps & { + featuredImage: never + featuredImageAlt: never +} + +export type HeaderWithImageProps = ImageProps | NoImageProps + +export const HeaderWithImage = (props: HeaderWithImageProps) => { + const renderImage = () => { + if (!props.featuredImage) return null + + return ( + + + {props.featuredImageAlt} + + + ) + } + + const renderContent = () => ( + + {props.breadcrumbs} + + {props.title} + + {props.description && {props.description}} + {props.children} + + ) + + return ( + + + {props.imageLayout === 'left' && renderImage()} + {renderContent()} + {props.imageLayout === 'right' && renderImage()} + + + ) +} diff --git a/apps/web/components/Grant/HeaderWithImage/index.ts b/apps/web/components/Grant/HeaderWithImage/index.ts new file mode 100644 index 000000000000..a58794eff2d7 --- /dev/null +++ b/apps/web/components/Grant/HeaderWithImage/index.ts @@ -0,0 +1,3 @@ +import { HeaderWithImage } from './HeaderWithImage' + +export const GrantHeaderWithImage = HeaderWithImage diff --git a/apps/web/components/Grant/SearchSection/SearchSection.css.ts b/apps/web/components/Grant/SearchSection/SearchSection.css.ts new file mode 100644 index 000000000000..459cc3529107 --- /dev/null +++ b/apps/web/components/Grant/SearchSection/SearchSection.css.ts @@ -0,0 +1,11 @@ +import { style } from '@vanilla-extract/css' + +import { spacing } from '@island.is/island-ui/theme' + +export const introImage = style({ + marginBottom: -spacing[2], + maxHeight: '17em', + display: 'block', + margin: 'auto', + width: '100%', +}) diff --git a/apps/web/components/Grant/SearchSection/SearchSection.tsx b/apps/web/components/Grant/SearchSection/SearchSection.tsx new file mode 100644 index 000000000000..8eed1a10354f --- /dev/null +++ b/apps/web/components/Grant/SearchSection/SearchSection.tsx @@ -0,0 +1,62 @@ +import { + Box, + Inline, + Input, + Tag, + TagVariant, + Text, +} from '@island.is/island-ui/core' + +import { + HeaderWithImage, + HeaderWithImageProps, +} from '../HeaderWithImage/HeaderWithImage' + +type QuickLink = { + title: string + href: string + variant?: TagVariant +} + +type SearchSectionProps = HeaderWithImageProps & { + searchPlaceholder: string + quickLinks: Array + searchUrl: string + shortcutsTitle: string +} + +export const SearchSection = (props: SearchSectionProps) => { + return ( + + + + + + + + {props.shortcutsTitle} + + + {props.quickLinks.map((q) => ( + + {q.title} + + ))} + + + + ) +} diff --git a/apps/web/components/Grant/SearchSection/index.tsx b/apps/web/components/Grant/SearchSection/index.tsx new file mode 100644 index 000000000000..88f6803f6f8f --- /dev/null +++ b/apps/web/components/Grant/SearchSection/index.tsx @@ -0,0 +1,3 @@ +import { SearchSection } from './SearchSection' + +export const GrantSearchSection = SearchSection diff --git a/apps/web/components/Grant/Wrapper/GrantWrapper.tsx b/apps/web/components/Grant/Wrapper/GrantWrapper.tsx new file mode 100644 index 000000000000..7e0296669044 --- /dev/null +++ b/apps/web/components/Grant/Wrapper/GrantWrapper.tsx @@ -0,0 +1,32 @@ +import { ReactNode, useEffect, useState } from 'react' +import { useWindowSize } from 'react-use' + +import { BreadCrumbItem } from '@island.is/island-ui/core' +import { theme } from '@island.is/island-ui/theme' + +import { HeadWithSocialSharing } from '../../HeadWithSocialSharing/HeadWithSocialSharing' + +type WrapperProps = { + pageTitle: string + pageDescription?: string + pageFeaturedImage?: string + children?: ReactNode +} + +export const GrantWrapper = ({ + pageTitle, + pageDescription, + pageFeaturedImage, + children, +}: WrapperProps) => { + return ( + <> + + {children} + + ) +} diff --git a/apps/web/components/Grant/index.ts b/apps/web/components/Grant/index.ts new file mode 100644 index 000000000000..fc69b2811bdd --- /dev/null +++ b/apps/web/components/Grant/index.ts @@ -0,0 +1,3 @@ +export { GrantWrapper } from './Wrapper/GrantWrapper' +export { GrantSearchSection } from './SearchSection' +export { GrantHeaderWithImage } from './HeaderWithImage' diff --git a/apps/web/components/PlazaCard/PlazaCard.css.ts b/apps/web/components/PlazaCard/PlazaCard.css.ts new file mode 100644 index 000000000000..bb5b75f34928 --- /dev/null +++ b/apps/web/components/PlazaCard/PlazaCard.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css' + +export const container = style({ + maxWidth: '476px', +}) diff --git a/apps/web/components/PlazaCard/PlazaCard.tsx b/apps/web/components/PlazaCard/PlazaCard.tsx new file mode 100644 index 000000000000..f18c1e2da060 --- /dev/null +++ b/apps/web/components/PlazaCard/PlazaCard.tsx @@ -0,0 +1,171 @@ +import * as React from 'react' + +import { + Box, + Button, + Icon, + IconMapIcon, + Stack, + Tag, + Text, +} from '@island.is/island-ui/core' +import { ActionCardProps } from '@island.is/island-ui/core/types' + +import * as styles from './PlazaCard.css' + +const eyebrowColor = 'blueberry600' + +interface Props { + title: string + text: string + logo?: string + logoAlt?: string + eyebrow?: string + subEyebrow?: string + detailLines?: Array<{ + icon: IconMapIcon + text: string + }> + tag?: ActionCardProps['tag'] + cta?: ActionCardProps['cta'] +} + +export const PlazaCard = ({ + title, + text, + eyebrow, + subEyebrow, + detailLines, + tag, + logo, + logoAlt, + cta, +}: Props) => { + const renderLogo = () => { + if (!logo) { + return null + } + + return ( + + {logoAlt} + + ) + } + + const renderEyebrow = () => { + if (!eyebrow) { + return null + } + + return ( + + {subEyebrow ? ( + + + {eyebrow} + + + {subEyebrow} + + + ) : ( + + {eyebrow} + + )} + {renderLogo()} + + ) + } + + const renderDetails = () => { + if (!detailLines?.length) { + return null + } + + return ( + + + {detailLines?.map((d, index) => ( + + + + {d.text} + + + ))} + + + ) + } + + const renderTag = () => { + if (!tag) { + return null + } + + return {tag.label} + } + + const renderCta = () => { + if (!cta) { + return null + } + + return ( + + ) + } + + return ( + + + {renderEyebrow()} + + {title} + + {text && ( + + {text} + + )} + {renderDetails()} + + {renderTag()} + {renderCta()} + + + + ) +} diff --git a/apps/web/components/SearchSection/SearchSection.tsx b/apps/web/components/SearchSection/SearchSection.tsx index 905d525b3f5f..2c19f84f4062 100644 --- a/apps/web/components/SearchSection/SearchSection.tsx +++ b/apps/web/components/SearchSection/SearchSection.tsx @@ -1,25 +1,26 @@ import React, { ReactNode, useEffect, useState } from 'react' import { useWindowSize } from 'react-use' import dynamic from 'next/dynamic' + import { - Text, + Box, + GridColumn, GridContainer, GridRow, - GridColumn, - Box, - Stack, Inline, - Tag, Link, + Stack, + Tag, + Text, } from '@island.is/island-ui/core' import { theme } from '@island.is/island-ui/theme' +import { TestSupport } from '@island.is/island-ui/utils' import { Locale } from '@island.is/shared/types' import { SearchInput } from '@island.is/web/components' -import { LinkType, useLinkResolver } from '@island.is/web/hooks/useLinkResolver' import { GetFrontpageQuery } from '@island.is/web/graphql/schema' +import { LinkType, useLinkResolver } from '@island.is/web/hooks/useLinkResolver' import * as styles from './SearchSection.css' -import { TestSupport } from '@island.is/island-ui/utils' const DefaultIllustration = dynamic(() => import('./Illustration'), { ssr: false, diff --git a/apps/web/components/real.ts b/apps/web/components/real.ts index e6f84f68ca80..d6e81e23def1 100644 --- a/apps/web/components/real.ts +++ b/apps/web/components/real.ts @@ -12,6 +12,7 @@ */ export * from './Card/Card' +export * from './PlazaCard/PlazaCard' export * from './Header/Header' export * from './SearchInput/SearchInput' export * from './LanguageToggler/LanguageToggler' @@ -21,6 +22,7 @@ export * from './SideMenu/SideMenu' export * from './StoryList/StoryList' export * from './ContentLink/ContentLink' export * from './NewsCard/NewsCard' +export * from './Grant' export * from './LogoList/LogoList' export * from './List/List' export * from './Bullet/Bullet' diff --git a/apps/web/hooks/useLinkResolver/useLinkResolver.ts b/apps/web/hooks/useLinkResolver/useLinkResolver.ts index 2e8796315fc5..b295b3f6c28b 100644 --- a/apps/web/hooks/useLinkResolver/useLinkResolver.ts +++ b/apps/web/hooks/useLinkResolver/useLinkResolver.ts @@ -307,6 +307,18 @@ export const routesTemplate = { is: '/undirskriftalistar', en: '/en/petitions', }, + styrkjatorg: { + is: '/styrkjatorg', + en: '', + }, + styrkjatorgsearch: { + is: '/styrkjatorg/leit', + en: '', + }, + styrkjatorggrant: { + is: '/styrkjatorg/styrkur/[id]', + en: '', + }, } // This considers one block ("[someVar]") to be one variable and ignores the path variables name diff --git a/apps/web/pages/styrkjatorg/index.ts b/apps/web/pages/styrkjatorg/index.ts new file mode 100644 index 000000000000..407e2ceaa326 --- /dev/null +++ b/apps/web/pages/styrkjatorg/index.ts @@ -0,0 +1,11 @@ +import withApollo from '@island.is/web/graphql/withApollo' +import { withLocale } from '@island.is/web/i18n' +import GrantsHome from '@island.is/web/screens/Grants/Home/GrantsHome' +import { getServerSidePropsWrapper } from '@island.is/web/utils/getServerSidePropsWrapper' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore make web strict +const Screen = withApollo(withLocale('is')(GrantsHome)) + +export default Screen + +export const getServerSideProps = getServerSidePropsWrapper(Screen) diff --git a/apps/web/pages/styrkjatorg/leit/index.ts b/apps/web/pages/styrkjatorg/leit/index.ts new file mode 100644 index 000000000000..6360f482e3ff --- /dev/null +++ b/apps/web/pages/styrkjatorg/leit/index.ts @@ -0,0 +1,12 @@ +import withApollo from '@island.is/web/graphql/withApollo' +import { withLocale } from '@island.is/web/i18n' +import GrantsSearchResults from '@island.is/web/screens/Grants/SearchResults/SearchResults' +import { getServerSidePropsWrapper } from '@island.is/web/utils/getServerSidePropsWrapper' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore make web strict +// +const Screen = withApollo(withLocale('is')(GrantsSearchResults)) + +export default Screen + +export const getServerSideProps = getServerSidePropsWrapper(Screen) diff --git a/apps/web/pages/styrkjatorg/styrkur/[id].ts b/apps/web/pages/styrkjatorg/styrkur/[id].ts new file mode 100644 index 000000000000..e59a8c014b13 --- /dev/null +++ b/apps/web/pages/styrkjatorg/styrkur/[id].ts @@ -0,0 +1,12 @@ +import withApollo from '@island.is/web/graphql/withApollo' +import { withLocale } from '@island.is/web/i18n' +import GrantSinglePage from '@island.is/web/screens/Grants/Grant/Grant' +import { getServerSidePropsWrapper } from '@island.is/web/utils/getServerSidePropsWrapper' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore make web strict +// +const Screen = withApollo(withLocale('is')(GrantSinglePage)) + +export default Screen + +export const getServerSideProps = getServerSidePropsWrapper(Screen) diff --git a/apps/web/public/assets/sofa.svg b/apps/web/public/assets/sofa.svg new file mode 100644 index 000000000000..c0c403237583 --- /dev/null +++ b/apps/web/public/assets/sofa.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/screens/Grants/Grant/Grant.tsx b/apps/web/screens/Grants/Grant/Grant.tsx new file mode 100644 index 000000000000..d3f4caf7aa06 --- /dev/null +++ b/apps/web/screens/Grants/Grant/Grant.tsx @@ -0,0 +1,220 @@ +import { useIntl } from 'react-intl' +import NextLink from 'next/link' +import { useRouter } from 'next/router' + +import { SliceType } from '@island.is/island-ui/contentful' +import { + ActionCard, + Box, + Breadcrumbs, + Divider, + Stack, + Text, +} from '@island.is/island-ui/core' +import { Locale } from '@island.is/shared/types' +import { GrantWrapper } from '@island.is/web/components' +import { + ContentLanguage, + CustomPageUniqueIdentifier, + Grant, + Query, + QueryGetSingleGrantArgs, +} from '@island.is/web/graphql/schema' +import { useLinkResolver } from '@island.is/web/hooks' +import { withMainLayout } from '@island.is/web/layouts/main' +import { webRichText } from '@island.is/web/utils/richText' + +import { + CustomScreen, + withCustomPageWrapper, +} from '../../CustomPage/CustomPageWrapper' +import SidebarLayout from '../../Layouts/SidebarLayout' +import { GET_GRANT_QUERY } from '../../queries' +import { m } from '../messages' +import { GrantSidebar } from './GrantSidebar' +import { CustomNextError } from '@island.is/web/units/errors' + +const GrantSinglePage: CustomScreen = ({ grant, locale }) => { + const { formatMessage } = useIntl() + const { linkResolver } = useLinkResolver() + const router = useRouter() + + const baseUrl = linkResolver('styrkjatorg', [], locale).href + const currentUrl = linkResolver( + 'styrkjatorggrant', + [grant?.applicationId ?? ''], + locale, + ).href + + const breadcrumbItems = [ + { + title: 'Ísland.is', + href: linkResolver('homepage', [], locale).href, + }, + { + title: formatMessage(m.home.title), + href: baseUrl, + }, + { + title: grant?.name ?? formatMessage(m.home.grant), + href: currentUrl, + isTag: true, + }, + ] + + if (!grant) { + return null + } + + return ( + + } + > + + + { + return item?.href ? ( + + {link} + + ) : ( + link + ) + }} + /> + + + {grant.name} + + {grant.description} + + router.push(grant.applicationUrl?.slug ?? ''), + icon: 'open', + iconType: 'outline', + }} + /> + {grant.specialEmphasis?.length ? ( + <> + + {webRichText( + grant.specialEmphasis as SliceType[], + undefined, + locale, + )} + + + + ) : undefined} + {grant.whoCanApply?.length ? ( + <> + + {formatMessage(m.single.whoCanApply)} + + {webRichText( + grant.whoCanApply as SliceType[], + undefined, + locale, + )} + + + + + ) : undefined} + {grant.howToApply?.length ? ( + + {formatMessage(m.single.howToApply)} + + {webRichText( + grant.howToApply as SliceType[], + undefined, + locale, + )} + + + ) : undefined} + {grant.applicationDeadline?.length ? ( + + {webRichText( + grant.applicationDeadline as SliceType[], + undefined, + locale, + )} + + ) : undefined} + {grant.applicationHints?.length ? ( + + {webRichText( + grant.applicationHints as SliceType[], + undefined, + locale, + )} + + ) : undefined} + + + + ) +} + +interface GrantSingleProps { + grant?: Grant + locale: Locale +} + +const GrantSingle: CustomScreen = ({ + grant, + customPageData, + locale, +}) => { + return ( + + ) +} + +GrantSingle.getProps = async ({ apolloClient, locale, query }) => { + const { + data: { getSingleGrant: grant }, + } = await apolloClient.query({ + query: GET_GRANT_QUERY, + variables: { + input: { + lang: locale as ContentLanguage, + id: String(query.id), + }, + }, + }) + + if (!grant) { + throw new CustomNextError(404, 'Grant not found') + } + + return { + grant: grant ?? undefined, + locale: locale as Locale, + showSearchInHeader: false, + themeConfig: { + footerVersion: 'organization', + }, + } +} + +export default withMainLayout( + withCustomPageWrapper(CustomPageUniqueIdentifier.Grants, GrantSingle), +) diff --git a/apps/web/screens/Grants/Grant/GrantSidebar.tsx b/apps/web/screens/Grants/Grant/GrantSidebar.tsx new file mode 100644 index 000000000000..1d97d89b388c --- /dev/null +++ b/apps/web/screens/Grants/Grant/GrantSidebar.tsx @@ -0,0 +1,133 @@ +import { useMemo } from 'react' +import { useIntl } from 'react-intl' + +import { Box, Button, LinkV2, Stack, Text } from '@island.is/island-ui/core' +import { Locale } from '@island.is/shared/types' +import { isDefined } from '@island.is/shared/utils' +import { InstitutionPanel } from '@island.is/web/components' +import { Grant } from '@island.is/web/graphql/schema' +import { LinkType, useLinkResolver } from '@island.is/web/hooks' + +import { m } from '../messages' + +interface Props { + grant: Grant + locale: Locale +} + +const generateLine = (heading: string, content?: React.ReactNode) => { + if (!content) { + return null + } + return ( + + + {heading} + + {content} + + ) +} + +export const GrantSidebar = ({ grant, locale }: Props) => { + const { formatMessage } = useIntl() + const { linkResolver } = useLinkResolver() + + const detailPanelData = useMemo( + () => + [ + generateLine( + formatMessage(m.single.fund), + grant?.fund?.link?.slug ? ( + + + {grant.fund.title} + + + ) : undefined, + ), + generateLine( + formatMessage(m.single.category), + grant?.categoryTags + ? grant.categoryTags + .map((ct) => ct.title) + .filter(isDefined) + .join(', ') + : undefined, + ), + generateLine( + formatMessage(m.single.type), + grant?.typeTag?.title ? ( + {grant.typeTag?.title} + ) : undefined, + ), + generateLine( + formatMessage(m.single.deadline), + grant?.applicationDeadlineText ? ( + {grant.applicationDeadlineText} + ) : undefined, + ), + generateLine( + formatMessage(m.single.status), + grant?.statusText ? ( + {grant.statusText} + ) : undefined, + ), + ].filter(isDefined) ?? [], + [grant, formatMessage, linkResolver], + ) + + const filesPanelData = useMemo( + () => + grant.files + ?.map((f, index) => { + if (!f.url) { + return null + } + return ( + + + + ) + }) + .filter(isDefined) ?? [], + [grant.files], + ) + + return ( + + + {detailPanelData.length ? ( + + {detailPanelData} + + ) : undefined} + {filesPanelData.length ? ( + + {filesPanelData} + + ) : undefined} + + ) +} diff --git a/apps/web/screens/Grants/Home/GrantsHome.tsx b/apps/web/screens/Grants/Home/GrantsHome.tsx new file mode 100644 index 000000000000..d106bee459fe --- /dev/null +++ b/apps/web/screens/Grants/Home/GrantsHome.tsx @@ -0,0 +1,203 @@ +import { useIntl } from 'react-intl' +import NextLink from 'next/link' + +import { + ArrowLink, + Box, + Breadcrumbs, + CategoryCard, + GridColumn, + GridContainer, + GridRow, + Stack, + Text, +} from '@island.is/island-ui/core' +import { Locale } from '@island.is/shared/types' +import { GrantSearchSection } from '@island.is/web/components' +import { SLICE_SPACING } from '@island.is/web/constants' +import { + ContentLanguage, + CustomPageUniqueIdentifier, + GenericTag, + Query, + QueryGetGenericTagsInTagGroupsArgs, +} from '@island.is/web/graphql/schema' +import { useLinkResolver } from '@island.is/web/hooks' +import { withMainLayout } from '@island.is/web/layouts/main' + +import { + CustomScreen, + withCustomPageWrapper, +} from '../../CustomPage/CustomPageWrapper' +import { GET_GENERIC_TAGS_IN_TAG_GROUPS_QUERY } from '../../queries/GenericTag' +import { m } from '../messages' + +const GrantsHomePage: CustomScreen = ({ + categories, + locale, + customPageData, +}) => { + const { formatMessage } = useIntl() + const { linkResolver } = useLinkResolver() + + const baseUrl = linkResolver('styrkjatorg', [], locale).href + const searchUrl = linkResolver('styrkjatorgsearch', [], locale).href + + const breadcrumbItems = [ + { + title: 'Ísland.is', + href: linkResolver('homepage', [], locale).href, + }, + { + title: formatMessage(m.home.title), + href: baseUrl, + }, + ] + + return ( + + + { + return item?.href ? ( + + {link} + + ) : ( + link + ) + }} + /> + ) + } + /> + + + + + + {formatMessage(m.home.popularCategories)} + + + {formatMessage(m.home.allGrants)} + + + + + {categories?.map((c) => ( + + + + ))} + + + + + + ) +} + +interface GrantsHomeProps { + organization?: Query['getOrganization'] + categories?: Array + locale: Locale +} + +const GrantsHome: CustomScreen = ({ + categories, + organization, + customPageData, + locale, +}) => { + return ( + + ) +} + +GrantsHome.getProps = async ({ apolloClient, locale }) => { + const tagGroupCategory = 'grant-category' + //Todo: add more organizations ?? + + const { + data: { getGenericTagsInTagGroups: tags }, + } = await apolloClient.query({ + query: GET_GENERIC_TAGS_IN_TAG_GROUPS_QUERY, + variables: { + input: { + lang: locale as ContentLanguage, + tagGroupSlugs: [tagGroupCategory], + }, + }, + }) + + return { + categories: tags ?? [], + locale: locale as Locale, + showSearchInHeader: false, + themeConfig: { + footerVersion: 'organization', + }, + } +} + +export default withMainLayout( + withCustomPageWrapper(CustomPageUniqueIdentifier.Grants, GrantsHome), +) diff --git a/apps/web/screens/Grants/SearchResults/SearchResults.tsx b/apps/web/screens/Grants/SearchResults/SearchResults.tsx new file mode 100644 index 000000000000..b0ce8ffde03d --- /dev/null +++ b/apps/web/screens/Grants/SearchResults/SearchResults.tsx @@ -0,0 +1,413 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useIntl } from 'react-intl' +import { useWindowSize } from 'react-use' +import debounce from 'lodash/debounce' +import NextLink from 'next/link' +import { useRouter } from 'next/router' +import { + parseAsArrayOf, + parseAsInteger, + parseAsString, +} from 'next-usequerystate' +import { useLazyQuery } from '@apollo/client' + +import { + Box, + BreadCrumbItem, + Breadcrumbs, + FilterInput, + Text, +} from '@island.is/island-ui/core' +import { theme } from '@island.is/island-ui/theme' +import { debounceTime } from '@island.is/shared/constants' +import { Locale } from '@island.is/shared/types' +import { GrantHeaderWithImage, GrantWrapper } from '@island.is/web/components' +import { + ContentLanguage, + CustomPageUniqueIdentifier, + GenericTag, + Grant, + GrantList, + Query, + QueryGetGenericTagsInTagGroupsArgs, + QueryGetGrantsArgs, +} from '@island.is/web/graphql/schema' +import { useLinkResolver } from '@island.is/web/hooks' +import { withMainLayout } from '@island.is/web/layouts/main' + +import { + CustomScreen, + withCustomPageWrapper, +} from '../../CustomPage/CustomPageWrapper' +import SidebarLayout from '../../Layouts/SidebarLayout' +import { GET_GENERIC_TAGS_IN_TAG_GROUPS_QUERY } from '../../queries/GenericTag' +import { GET_GRANTS_QUERY } from '../../queries/Grants' +import { m } from '../messages' +import { SearchResultsContent } from './SearchResultsContent' +import { GrantsSearchResultsFilter } from './SearchResultsFilter' + +export interface SearchState { + page?: number + query?: string + status?: Array + category?: Array + type?: Array + organization?: Array +} + +const GrantsSearchResultsPage: CustomScreen = ({ + locale, + initialGrants, + tags, +}) => { + const { formatMessage } = useIntl() + const router = useRouter() + const { linkResolver } = useLinkResolver() + + const parentUrl = linkResolver('styrkjatorg', [], locale).href + const currentUrl = linkResolver('styrkjatorgsearch', [], locale).href + + const [grants, setGrants] = useState>(initialGrants ?? []) + const [searchState, setSearchState] = useState() + const [initialRender, setInitialRender] = useState(true) + + const { width } = useWindowSize() + const isMobile = width <= theme.breakpoints.md + + const [getGrants, { error }] = useLazyQuery< + { getGrants: GrantList }, + QueryGetGrantsArgs + >(GET_GRANTS_QUERY) + + //load params into search state on first render + useEffect(() => { + const searchParams = new URLSearchParams(document.location.search) + const page = searchParams.get('page') + const statuses = searchParams.getAll('status') + const categories = searchParams.getAll('category') + const types = searchParams.getAll('type') + const organizations = searchParams.getAll('organization') + + setSearchState({ + page: page ? Number.parseInt(page) : undefined, + query: searchParams.get('query') ?? undefined, + status: statuses.length ? statuses : undefined, + category: categories.length ? categories : undefined, + type: types.length ? types : undefined, + organization: organizations.length ? organizations : undefined, + }) + }, []) + + const updateUrl = useCallback(() => { + if (!searchState) { + return + } + router.replace( + { + pathname: currentUrl, + query: Object.entries(searchState) + .filter(([_, value]) => !!value) + .reduce( + (accumulator, [searchStateKey, searchStateValue]) => ({ + ...accumulator, + [searchStateKey]: searchStateValue, + }), + {}, + ), + }, + undefined, + { shallow: true }, + ) + }, [searchState, router, currentUrl]) + + const fetchGrants = useCallback(() => { + if (initialRender) { + setInitialRender(false) + return + } + getGrants({ + variables: { + input: { + categories: searchState?.category, + lang: locale, + organizations: searchState?.organization, + page: searchState?.page, + search: searchState?.query, + size: 8, + statuses: searchState?.status, + types: searchState?.type, + }, + }, + }) + .then((res) => { + if (res.data) { + setGrants(res.data.getGrants.items) + } else if (res.error) { + setGrants([]) + console.error('Error fetching grants', res.error) + } + }) + .catch((err) => { + setGrants([]) + console.error('Error fetching grants', err) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchState, initialRender]) + + //SEARCH STATE UPDATES + const debouncedSearchUpdate = useMemo(() => { + return debounce(() => { + updateUrl() + fetchGrants() + }, debounceTime.search) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchState]) + + useEffect(() => { + debouncedSearchUpdate() + return () => { + debouncedSearchUpdate.cancel() + } + }, [debouncedSearchUpdate]) + + const updateSearchStateValue = ( + categoryId: keyof SearchState, + values: unknown, + ) => { + setSearchState({ + ...searchState, + [categoryId]: values, + }) + } + + const breadcrumbItems: Array = [ + { + title: 'Ísland.is', + href: linkResolver('homepage', [], locale).href, + }, + { + title: formatMessage(m.home.title), + href: parentUrl, + }, + { + title: formatMessage(m.search.results), + href: currentUrl, + isTag: true, + }, + ] + + const onResetFilter = () => { + setSearchState({ + page: undefined, + query: undefined, + status: undefined, + category: undefined, + type: undefined, + organization: undefined, + }) + router.replace(currentUrl, {}, { shallow: true }) + } + + const hitsMessage = useMemo(() => { + if (!grants) { + return + } + if (grants.length === 1) { + return formatMessage(m.search.resultFound, { + arg: {grants.length}, + }) + } + return formatMessage(m.search.resultsFound, { + arg: {grants.length}, + }) + }, [formatMessage, grants]) + + return ( + + { + return item?.href ? ( + + {link} + + ) : ( + link + ) + }} + /> + ) + } + /> + + {!isMobile && ( + + + + {formatMessage(m.search.search)} + + + + + updateSearchStateValue('query', option) + } + /> + + + + } + > + + + + + )} + {isMobile && ( + + + + {formatMessage(m.search.search)} + + + + updateSearchStateValue('query', option)} + backgroundColor={'white'} + /> + + + {hitsMessage} + + + + + )} +
+ + ) +} + +interface GrantsHomeProps { + locale: Locale + initialGrants?: Array + tags?: Array +} + +const GrantsSearchResults: CustomScreen = ({ + initialGrants, + tags, + customPageData, + locale, +}) => { + return ( + + ) +} + +GrantsSearchResults.getProps = async ({ apolloClient, locale, query }) => { + const arrayParser = parseAsArrayOf(parseAsString) + + const parseArray = (arg: string | string[] | undefined) => { + const array = arrayParser.parseServerSide(arg) + + if (array && array.length > 0) { + return array + } + + return undefined + } + const [ + { + data: { getGrants }, + }, + { + data: { getGenericTagsInTagGroups }, + }, + ] = await Promise.all([ + apolloClient.query({ + query: GET_GRANTS_QUERY, + variables: { + input: { + lang: locale as ContentLanguage, + page: parseAsInteger.withDefault(1).parseServerSide(query?.page), + search: parseAsString.parseServerSide(query?.query) ?? undefined, + categories: parseArray(query?.category), + statuses: parseArray(query?.status), + types: parseArray(query?.type), + organizations: parseArray(query?.organization), + }, + }, + }), + apolloClient.query({ + query: GET_GENERIC_TAGS_IN_TAG_GROUPS_QUERY, + variables: { + input: { + lang: locale as ContentLanguage, + tagGroupSlugs: ['grant-category', 'grant-type'], + }, + }, + }), + ]) + return { + initialGrants: getGrants.items, + tags: getGenericTagsInTagGroups ?? undefined, + locale: locale as Locale, + themeConfig: { + footerVersion: 'organization', + }, + } +} + +export default withMainLayout( + withCustomPageWrapper(CustomPageUniqueIdentifier.Grants, GrantsSearchResults), +) diff --git a/apps/web/screens/Grants/SearchResults/SearchResultsContent.tsx b/apps/web/screens/Grants/SearchResults/SearchResultsContent.tsx new file mode 100644 index 000000000000..163c5f3b08c4 --- /dev/null +++ b/apps/web/screens/Grants/SearchResults/SearchResultsContent.tsx @@ -0,0 +1,151 @@ +import { useIntl } from 'react-intl' +import { useWindowSize } from 'react-use' +import format from 'date-fns/format' +import { useRouter } from 'next/router' + +import { Box, Inline, TagVariant, Text } from '@island.is/island-ui/core' +import { theme } from '@island.is/island-ui/theme' +import { Locale } from '@island.is/shared/types' +import { isDefined } from '@island.is/shared/utils' +import { PlazaCard } from '@island.is/web/components' +import { Grant, GrantStatus } from '@island.is/web/graphql/schema' +import { useLinkResolver } from '@island.is/web/hooks' + +import { m } from '../messages' + +interface Props { + grants?: Array + subheader?: React.ReactNode + locale: Locale +} + +export const SearchResultsContent = ({ grants, subheader, locale }: Props) => { + const { formatMessage } = useIntl() + const router = useRouter() + const { linkResolver } = useLinkResolver() + + const { width } = useWindowSize() + const isMobile = width <= theme.breakpoints.md + const isTablet = width <= theme.breakpoints.lg && width > theme.breakpoints.md + + return ( + <> + {!isMobile && ( + + {subheader} + + )} + {grants?.length ? ( + + {grants?.map((grant) => { + if (!grant) { + return null + } + + let tagVariant: TagVariant | undefined + switch (grant.status) { + case GrantStatus.Open: + tagVariant = 'mint' + break + case GrantStatus.Closed: + tagVariant = 'rose' + break + case GrantStatus.OpensSoon: + tagVariant = 'purple' + break + default: + break + } + + return ( + + {grant.applicationId && ( + { + router.push( + linkResolver( + 'styrkjatorggrant', + [grant?.applicationId ?? ''], + locale, + ).href, + ) + }, + icon: 'arrowForward', + }} + detailLines={[ + grant.dateFrom && grant.dateTo + ? { + icon: 'calendar' as const, + text: `${format( + new Date(grant.dateFrom), + 'dd.MM.', + )}-${format(new Date(grant.dateTo), 'dd.MM.yyyy')}`, + } + : null, + { + icon: 'time' as const, + //todo: fix when the text is ready + text: 'Frestur til 16.08.2024, kl. 15.00', + }, + grant.categoryTags + ? { + icon: 'informationCircle' as const, + text: grant.categoryTags + .map((ct) => ct.title) + .filter(isDefined) + .join(', '), + } + : undefined, + ].filter(isDefined)} + /> + )} + + ) + })} + + ) : undefined} + {!grants?.length && ( + + + + {formatMessage(m.search.noResultsFound)} + + + {!(isTablet || isMobile) && ( + {formatMessage(m.search.noResultsFound)} + )} + + )} + + ) +} diff --git a/apps/web/screens/Grants/SearchResults/SearchResultsFilter.tsx b/apps/web/screens/Grants/SearchResults/SearchResultsFilter.tsx new file mode 100644 index 000000000000..df716f5c3d7e --- /dev/null +++ b/apps/web/screens/Grants/SearchResults/SearchResultsFilter.tsx @@ -0,0 +1,137 @@ +import { useIntl } from 'react-intl' + +import { + Box, + Filter, + FilterMultiChoice, + FilterProps, +} from '@island.is/island-ui/core' +import { isDefined } from '@island.is/shared/utils' +import { GenericTag } from '@island.is/web/graphql/schema' + +import { m } from '../messages' +import { SearchState } from './SearchResults' + +interface Props { + onSearchUpdate: (categoryId: keyof SearchState, value: unknown) => void + onReset: () => void + searchState?: SearchState + tags: Array + url: string + variant?: FilterProps['variant'] +} + +export const GrantsSearchResultsFilter = ({ + onSearchUpdate, + onReset, + searchState, + tags, + url, + variant = 'default', +}: Props) => { + const { formatMessage } = useIntl() + const categoryFilters = tags?.filter( + (t) => t.genericTagGroup?.slug === 'grant-category', + ) + + const typeFilters = tags?.filter( + (t) => t.genericTagGroup?.slug === 'grant-type', + ) + + return ( + { + e.preventDefault() + }} + > + + + { + onSearchUpdate( + categoryId as keyof SearchState, + selected.length ? selected : undefined, + ) + }} + onClear={(categoryId) => { + onSearchUpdate(categoryId as keyof SearchState, undefined) + }} + categories={[ + { + id: 'status', + label: formatMessage(m.search.applicationStatus), + selected: searchState?.['status'] ?? [], + filters: [ + { + value: 'open', + label: formatMessage(m.search.applicationOpen), + }, + { + value: 'open-soon', + label: formatMessage(m.search.applicationOpensSoon), + }, + { + value: 'closed', + label: formatMessage(m.search.applicationClosed), + }, + ], + }, + categoryFilters + ? { + id: 'category', + label: formatMessage(m.search.category), + selected: searchState?.['category'] ?? [], + filters: categoryFilters.map((t) => ({ + value: t.slug, + label: t.title, + })), + } + : undefined, + typeFilters + ? { + id: 'type', + label: formatMessage(m.search.type), + selected: searchState?.['type'] ?? [], + filters: typeFilters.map((t) => ({ + value: t.slug, + label: t.title, + })), + } + : undefined, + { + id: 'organization', + label: formatMessage(m.search.organization), + + selected: searchState?.['organization'] ?? [], + filters: [ + { + value: 'rannis', + label: 'Rannís', + }, + { + value: 'tonlistarmidstod', + label: 'Tónlistarmiðstöð', + }, + { + value: 'kvikmyndastod', + label: 'Kvikmyndastöð', + }, + ], + }, + ].filter(isDefined)} + /> + + + + ) +} diff --git a/apps/web/screens/Grants/messages.ts b/apps/web/screens/Grants/messages.ts new file mode 100644 index 000000000000..15a1cf7d595a --- /dev/null +++ b/apps/web/screens/Grants/messages.ts @@ -0,0 +1,165 @@ +import { defineMessages } from 'react-intl' + +export const m = { + general: defineMessages({ + seeMore: { + id: 'web.grants:general.seeMore', + defaultMessage: 'Skoða nánar', + }, + }), + search: defineMessages({ + search: { + id: 'web.grants:search.search', + defaultMessage: 'Leit', + }, + description: { + id: 'web.grants:search.description', + defaultMessage: 'Lýsing', + }, + results: { + id: 'web.grants:search.results', + defaultMessage: 'Leitarniðurstöður', + }, + inputPlaceholder: { + id: 'web.grants:search.inputPlaceholder', + defaultMessage: 'Sía eftir leitarorði', + }, + clearFilters: { + id: 'web.grants:search.clearFilters', + defaultMessage: 'Hreinsa allar síur', + }, + clearFilterCategory: { + id: 'web.grants:search.clearFilterCategory', + defaultMessage: 'Hreinsa flokk', + }, + applicationStatus: { + id: 'web.grants:search.applicationStatus', + defaultMessage: 'Staða umsóknar', + }, + applicationOpen: { + id: 'web.grants:search.applicationOpen', + defaultMessage: 'Opið fyrir umsóknir', + }, + applicationClosed: { + id: 'web.grants:search.applicationClosed', + defaultMessage: 'Lokað fyrir umsóknir', + }, + applicationOpensSoon: { + id: 'web.grants:search.applicationOpensSoon', + defaultMessage: 'Opnar fljótlega', + }, + category: { + id: 'web.grants:search.category', + defaultMessage: 'Flokkun', + }, + type: { + id: 'web.grants:search.type', + defaultMessage: 'Tegund', + }, + resultFound: { + id: 'web.grants:search.resultFound', + defaultMessage: '{arg} styrkur fannst', + }, + resultsFound: { + id: 'web.grants:search.resultsFound', + defaultMessage: '{arg} styrkir fundust', + }, + noResultsFound: { + id: 'web.grants:search.noResultsFound', + defaultMessage: 'Engir styrkir fundust', + }, + organization: { + id: 'web.grants:search.organization', + defaultMessage: 'Stofnun', + }, + }), + single: defineMessages({ + fund: { + id: 'web.grants:single.fund', + defaultMessage: 'Sjóður', + }, + category: { + id: 'web.grants:single.category', + defaultMessage: 'Styrkjaflokkun', + }, + provider: { + id: 'web.grants:single.provider', + defaultMessage: 'Þjónustuaðili', + }, + unknownInstitution: { + id: 'web.grants:single.unknownInstitution', + defaultMessage: 'Óþekkt stofnun', + }, + type: { + id: 'web.grants:single.type', + defaultMessage: 'Tegund', + }, + deadline: { + id: 'web.grants:single.deadline', + defaultMessage: 'Umsóknarfrestur', + }, + status: { + id: 'web.grants:single.status', + defaultMessage: 'Staða', + }, + whatIsGranted: { + id: 'web.grants:single.whatIsGranted', + defaultMessage: 'Hvað er styrkt?', + }, + specialEmphasis: { + id: 'web.grants:single.specialEmphasis', + defaultMessage: 'Sérstakar áherslur', + }, + whoCanApply: { + id: 'web.grants:single.whoCanApply', + defaultMessage: 'Hverjir geta sótt um?', + }, + apply: { + id: 'web.grants:single.apply', + defaultMessage: 'Sækja um', + }, + howToApply: { + id: 'web.grants:single.howToApply', + defaultMessage: 'Hvernig er sótt um?', + }, + }), + home: defineMessages({ + title: { + id: 'web.grants:home.title', + defaultMessage: 'Styrkjatorg', + }, + grant: { + id: 'web.grants:home.gramt', + defaultMessage: 'Styrkur', + }, + description: { + id: 'web.grants:home.description', + defaultMessage: 'Styrkjatorg lýsing .....', + }, + featuredImage: { + id: 'web.grants:home.featuredImage', + defaultMessage: + 'https://images.ctfassets.net/8k0h54kbe6bj/5LqU9yD9nzO5oOijpZF0K0/b595e1cf3e72bc97b2f9d869a53f5da9/LE_-_Jobs_-_S3.png', + }, + featuredImageAlt: { + id: 'web.grants:home.featuredImageAlt', + defaultMessage: 'Mynd af klemmuspjaldi ásamt blýanti', + }, + inputPlaceholder: { + id: 'web.grants:home.inputPlaceholder', + defaultMessage: 'Leitaðu á styrkjatorgi', + }, + mostVisited: { + id: 'web.grants:home.mostVisited', + defaultMessage: 'Mest sótt', + }, + popularCategories: { + id: 'web.grants:home.popularCategories', + defaultMessage: 'Vinsælir flokkar', + }, + allGrants: { + id: 'web.grants:home.allGrants', + defaultMessage: 'Allir styrkir', + }, + }), +} diff --git a/apps/web/screens/queries/GenericTag.ts b/apps/web/screens/queries/GenericTag.ts index 91b0797138c8..7ed6c061300c 100644 --- a/apps/web/screens/queries/GenericTag.ts +++ b/apps/web/screens/queries/GenericTag.ts @@ -8,3 +8,18 @@ export const GET_GENERIC_TAG_BY_SLUG_QUERY = gql` } } ` + +export const GET_GENERIC_TAGS_IN_TAG_GROUPS_QUERY = gql` + query GetGenericTagInTagGroups($input: GetGenericTagsInTagGroupsInput!) { + getGenericTagsInTagGroups(input: $input) { + id + title + slug + genericTagGroup { + id + title + slug + } + } + } +` diff --git a/apps/web/screens/queries/Grants.ts b/apps/web/screens/queries/Grants.ts new file mode 100644 index 000000000000..b042c9e46aa4 --- /dev/null +++ b/apps/web/screens/queries/Grants.ts @@ -0,0 +1,126 @@ +import gql from 'graphql-tag' +import { nestedFields, slices } from './fragments' + +export const GET_GRANTS_QUERY = gql` + query GetGrants($input: GetGrantsInput!) { + getGrants(input: $input) { + items { + id + name + description + applicationId + applicationDeadlineText + applicationUrl { + slug + type + } + dateFrom + dateTo + isOpen + status + statusText + categoryTags { + id + title + genericTagGroup { + title + } + } + typeTag { + id + title + genericTagGroup { + title + } + } + fund { + id + title + link { + slug + type + } + featuredImage { + id + url + } + parentOrganization { + id + title + logo { + url + } + } + } + } + } + } +` + +export const GET_GRANT_QUERY = gql` + query GetGrant($input: GetSingleGrantInput!) { + getSingleGrant(input: $input) { + id + name + description + applicationId + applicationUrl { + slug + type + } + applicationDeadlineText + statusText + categoryTags { + id + title + } + typeTag { + id + title + } + files { + ...AssetFields + } + fund { + id + title + link { + slug + type + } + featuredImage { + id + url + } + parentOrganization { + id + title + logo { + url + } + } + } + specialEmphasis { + ...AllSlices + ${nestedFields} + } + whoCanApply { + ...AllSlices + ${nestedFields} + } + howToApply { + ...AllSlices + ${nestedFields} + } + applicationDeadline { + ...AllSlices + ${nestedFields} + } + applicationHints { + ...AllSlices + ${nestedFields} + } + } + } + ${slices} +` diff --git a/apps/web/screens/queries/index.tsx b/apps/web/screens/queries/index.tsx index a1e0347e1114..9b1c24ac57bc 100644 --- a/apps/web/screens/queries/index.tsx +++ b/apps/web/screens/queries/index.tsx @@ -13,6 +13,7 @@ export * from './Adgerdir' export * from './Frontpage' export * from './TellUsAStory' export * from './ServiceWebForms' +export * from './Grants' export * from './ApiCatalogue' export * from './SubpageHeader' export * from './ErrorPage' diff --git a/libs/cms/src/lib/cms.contentful.service.ts b/libs/cms/src/lib/cms.contentful.service.ts index 71bcfabb44cc..f54e8a4794c9 100644 --- a/libs/cms/src/lib/cms.contentful.service.ts +++ b/libs/cms/src/lib/cms.contentful.service.ts @@ -77,10 +77,13 @@ import { mapImage } from './models/image.model' import { EmailSignup, mapEmailSignup } from './models/emailSignup.model' import { GetTabSectionInput } from './dto/getTabSection.input' import { mapTabSection, TabSection } from './models/tabSection.model' -import { GetGenericTagBySlugInput } from './dto/getGenericTagBySlug.input' import { GenericTag, mapGenericTag } from './models/genericTag.model' import { GetEmailSignupInput } from './dto/getEmailSignup.input' import { LifeEventPage, mapLifeEventPage } from './models/lifeEventPage.model' +import { GetGenericTagBySlugInput } from './dto/getGenericTagBySlug.input' +import { GetGenericTagsInTagGroupsInput } from './dto/getGenericTagsInTagGroups.input' +import { Grant, mapGrant } from './models/grant.model' +import { GrantList } from './models/grantList.model' import { mapManual } from './models/manual.model' import { mapServiceWebPage } from './models/serviceWebPage.model' import { mapEvent } from './models/event.model' @@ -599,6 +602,19 @@ export class CmsContentfulService { return (result.items as types.INews[]).map(mapNews)[0] ?? null } + async getGrant(lang: string, id: string): Promise { + const params = { + ['content_type']: 'grant', + 'fields.grantApplicationId': id, + } + + const result = await this.contentfulRepository + .getLocalizedEntries(lang, params) + .catch(errorHandler('getGrant')) + + return (result.items as types.IGrant[]).map(mapGrant)[0] ?? null + } + async getSingleEvent(lang: string, slug: string) { const params = { ['content_type']: 'event', @@ -1110,4 +1126,29 @@ export class CmsContentfulService { return (result.items as types.IGenericTag[]).map(mapGenericTag)[0] ?? null } + + async getGenericTagsInTagGroups({ + lang = 'is', + tagGroupSlugs, + }: GetGenericTagsInTagGroupsInput): Promise | null> { + let params + if (tagGroupSlugs) { + params = { + ['content_type']: 'genericTag', + 'fields.genericTagGroup.fields.slug[in]': tagGroupSlugs.join(','), + 'fields.genericTagGroup.sys.contentType.sys.id': 'genericTagGroup', + } + } else { + params = { + ['content_type']: 'genericTag', + 'fields.genericTagGroup.sys.contentType.sys.id': 'genericTagGroup', + } + } + + const result = await this.contentfulRepository + .getLocalizedEntries(lang, params) + .catch(errorHandler('getGenericTag')) + + return (result.items as types.IGenericTag[]).map(mapGenericTag) + } } diff --git a/libs/cms/src/lib/cms.elasticsearch.service.ts b/libs/cms/src/lib/cms.elasticsearch.service.ts index 1a248a16e320..6f83b2649b4e 100644 --- a/libs/cms/src/lib/cms.elasticsearch.service.ts +++ b/libs/cms/src/lib/cms.elasticsearch.service.ts @@ -46,6 +46,10 @@ import { GetGenericListItemBySlugInput } from './dto/getGenericListItemBySlug.in import { GenericListItem } from './models/genericListItem.model' import { GetTeamMembersInput } from './dto/getTeamMembers.input' import { TeamMemberResponse } from './models/teamMemberResponse.model' +import { GetGrantsInput } from './dto/getGrants.input' +import { Grant } from './models/grant.model' +import { GrantList } from './models/grantList.model' +import { logger } from '@island.is/logging' @Injectable() export class CmsElasticsearchService { @@ -605,6 +609,120 @@ export class CmsElasticsearchService { .filter(Boolean) } + async getGrants( + index: string, + { + lang, + search, + page = 1, + size = 8, + statuses, + categories, + types, + organizations, + }: GetGrantsInput, + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const must: Record[] = [ + { + term: { + type: { + value: 'webGrant', + }, + }, + }, + ] + + let queryString = search ? search.toLowerCase() : '' + + if (lang === 'is') { + queryString = queryString.replace('`', '') + } + + const sort: ('_score' | sortRule)[] = [ + { + [SortField.RELEASE_DATE]: { + order: SortDirection.DESC, + }, + }, + // Sort items with equal values by ascending title order + { 'title.sort': { order: SortDirection.ASC } }, + ] + + // Order by score first in case there is a query string + if (queryString.length > 0 && queryString !== '*') { + sort.unshift('_score') + } + + if (queryString) { + must.push({ + simple_query_string: { + query: queryString + '*', + fields: ['title^100', 'content'], + analyze_wildcard: true, + }, + }) + } + + const tagFilters: Array> = [] + + if (categories) { + tagFilters.push(categories) + } + if (types) { + tagFilters.push(types) + } + if (statuses) { + tagFilters.push(statuses) + } + if (organizations) { + tagFilters.push(organizations) + } + + tagFilters.forEach((filter) => { + must.push({ + nested: { + path: 'tags', + query: { + bool: { + must: [ + { + terms: { + 'tags.key': filter, + }, + }, + { + term: { + 'tags.type': 'genericTag', + }, + }, + ], + }, + }, + }, + }) + }) + + const grantListResponse: ApiResponse> = + await this.elasticService.findByQuery(index, { + query: { + bool: { + must, + }, + }, + sort, + size, + from: (page - 1) * size, + }) + + return { + total: grantListResponse.body.hits.total.value, + items: grantListResponse.body.hits.hits.map((response) => + JSON.parse(response._source.response ?? '[]'), + ), + } + } + async getPublishedMaterial( index: string, { diff --git a/libs/cms/src/lib/cms.resolver.ts b/libs/cms/src/lib/cms.resolver.ts index 63ad79020440..462b5e4531ec 100644 --- a/libs/cms/src/lib/cms.resolver.ts +++ b/libs/cms/src/lib/cms.resolver.ts @@ -121,6 +121,11 @@ import { TeamMemberResponse } from './models/teamMemberResponse.model' import { TeamList } from './models/teamList.model' import { TeamMember } from './models/teamMember.model' import { LatestGenericListItems } from './models/latestGenericListItems.model' +import { GetGenericTagsInTagGroupsInput } from './dto/getGenericTagsInTagGroups.input' +import { Grant } from './models/grant.model' +import { GetGrantsInput } from './dto/getGrants.input' +import { GetSingleGrantInput } from './dto/getSingleGrant.input' +import { GrantList } from './models/grantList.model' const defaultCache: CacheControlOptions = { maxAge: CACHE_CONTROL_MAX_AGE } @@ -444,6 +449,23 @@ export class CmsResolver { ) } + @CacheControl(defaultCache) + @Query(() => GrantList) + async getGrants(@Args('input') input: GetGrantsInput): Promise { + return this.cmsElasticsearchService.getGrants( + getElasticsearchIndex(input.lang), + input, + ) + } + + @CacheControl(defaultCache) + @Query(() => Grant, { nullable: true }) + async getSingleGrant( + @Args('input') { lang, id }: GetSingleGrantInput, + ): Promise { + return this.cmsContentfulService.getGrant(lang, id) + } + @CacheControl(defaultCache) @Query(() => News, { nullable: true }) getSingleNews( @@ -606,6 +628,14 @@ export class CmsResolver { return this.cmsContentfulService.getGenericTagBySlug(input) } + @CacheControl(defaultCache) + @Query(() => [GenericTag], { nullable: true }) + getGenericTagsInTagGroups( + @Args('input') input: GetGenericTagsInTagGroupsInput, + ): Promise | null> { + return this.cmsContentfulService.getGenericTagsInTagGroups(input) + } + @CacheControl(defaultCache) @Query(() => Manual, { nullable: true }) getSingleManual( diff --git a/libs/cms/src/lib/dto/getGenericTagsInTagGroups.input.ts b/libs/cms/src/lib/dto/getGenericTagsInTagGroups.input.ts new file mode 100644 index 000000000000..b91d2891d97d --- /dev/null +++ b/libs/cms/src/lib/dto/getGenericTagsInTagGroups.input.ts @@ -0,0 +1,14 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsArray, IsOptional, IsString } from 'class-validator' + +@InputType() +export class GetGenericTagsInTagGroupsInput { + @Field(() => String) + @IsString() + lang = 'is-IS' + + @Field(() => [String], { nullable: true }) + @IsArray() + @IsOptional() + tagGroupSlugs?: Array +} diff --git a/libs/cms/src/lib/dto/getGrants.input.ts b/libs/cms/src/lib/dto/getGrants.input.ts new file mode 100644 index 000000000000..bfe512bad196 --- /dev/null +++ b/libs/cms/src/lib/dto/getGrants.input.ts @@ -0,0 +1,45 @@ +import { IsArray, IsInt, IsOptional, IsString } from 'class-validator' +import { Field, InputType, Int } from '@nestjs/graphql' +import { ElasticsearchIndexLocale } from '@island.is/content-search-index-manager' + +@InputType() +export class GetGrantsInput { + @Field({ nullable: true }) + @IsString() + @IsOptional() + search?: string + + @Field(() => String) + @IsString() + lang: ElasticsearchIndexLocale = 'is' + + @Field(() => Int, { nullable: true }) + @IsInt() + @IsOptional() + page?: number = 1 + + @Field(() => Int, { nullable: true }) + @IsInt() + @IsOptional() + size?: number = 8 + + @Field(() => [String], { nullable: true }) + @IsArray() + @IsOptional() + statuses?: string[] + + @Field(() => [String], { nullable: true }) + @IsArray() + @IsOptional() + categories?: string[] + + @Field(() => [String], { nullable: true }) + @IsArray() + @IsOptional() + types?: string[] + + @Field(() => [String], { nullable: true }) + @IsArray() + @IsOptional() + organizations?: string[] +} diff --git a/libs/cms/src/lib/dto/getSingleGrant.input.ts b/libs/cms/src/lib/dto/getSingleGrant.input.ts new file mode 100644 index 000000000000..95e668413368 --- /dev/null +++ b/libs/cms/src/lib/dto/getSingleGrant.input.ts @@ -0,0 +1,14 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsString } from 'class-validator' +import { ElasticsearchIndexLocale } from '@island.is/content-search-index-manager' + +@InputType() +export class GetSingleGrantInput { + @Field() + @IsString() + id!: string + + @Field(() => String) + @IsString() + lang: ElasticsearchIndexLocale = 'is' +} diff --git a/libs/cms/src/lib/environments/environment.ts b/libs/cms/src/lib/environments/environment.ts index 0e973855b528..6b4bb5935d35 100644 --- a/libs/cms/src/lib/environments/environment.ts +++ b/libs/cms/src/lib/environments/environment.ts @@ -8,6 +8,7 @@ export default { 'articleCategory', 'news', 'page', + 'grant', 'vidspyrnaPage', 'menu', 'groupedMenu', diff --git a/libs/cms/src/lib/generated/contentfulTypes.d.ts b/libs/cms/src/lib/generated/contentfulTypes.d.ts index 83dd0e9ffc84..1391d3119346 100644 --- a/libs/cms/src/lib/generated/contentfulTypes.d.ts +++ b/libs/cms/src/lib/generated/contentfulTypes.d.ts @@ -8,7 +8,7 @@ export interface IAccordionSliceFields { title?: string | undefined /** Type */ - type: 'accordion' | 'accordion_minimal' | 'CTA' | 'category_card' + type: 'accordion' | 'accordion_minimal' | 'category_card' /** Accordion Items */ accordionItems?: IOneColumnText[] | undefined @@ -770,6 +770,7 @@ export interface ICustomPageFields { | 'PensionCalculator' | 'OfficialJournalOfIceland' | 'Vacancies' + | 'Grants' | undefined /** Alert Banner */ @@ -1549,6 +1550,39 @@ export interface IFrontpageSlider extends Entry { } } +export interface IFundFields { + /** Title */ + fundTitle: string + + /** Link */ + fundLink?: IArticle | IOrganizationSubpage | ILinkUrl | undefined + + /** Featured Image */ + fundFeaturedImage: Asset + + /** Parent Organization */ + fundParentOrganization: IOrganization +} + +/** Fund is a part of "Styrkjatorg". */ + +export interface IFund extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'fund' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + export interface IGenericListFields { /** Internal Title */ internalTitle: string @@ -1596,7 +1630,7 @@ export interface IGenericListItemFields { title: string /** Date */ - date: string + date?: string | undefined /** Card Intro */ cardIntro?: Document | undefined @@ -1749,6 +1783,9 @@ export interface IGenericTagGroupFields { /** Slug */ slug: string + + /** Items */ + items?: Record | undefined } /** A way to group together generic tags */ @@ -1770,6 +1807,85 @@ export interface IGenericTagGroup extends Entry { } } +export interface IGrantFields { + /** Title */ + grantName: string + + /** Description */ + grantDescription?: string | undefined + + /** Grant Application ID */ + grantApplicationId?: string | undefined + + /** Application deadline text */ + grantApplicationDeadlineText: string[] + + /** Application Url */ + granApplicationUrl?: ILinkUrl | undefined + + /** Special emphasis */ + grantSpecialEmphasis?: Document | undefined + + /** Who can apply? */ + grantWhoCanApply?: Document | undefined + + /** How to apply? */ + grantHowToApply?: Document | undefined + + /** Application deadline */ + grantApplicationDeadline?: Document | undefined + + /** Application hints */ + grantApplicationHints?: Document | undefined + + /** Date from */ + grantDateFrom?: string | undefined + + /** Date to */ + grantDateTo?: string | undefined + + /** Is open? */ + grantIsOpen?: boolean | undefined + + /** Status */ + grantStatus?: + | 'Opið fyrir umsóknir' + | 'Lokað fyrir umsóknir' + | 'Opnar fljótlega' + | undefined + + /** Files */ + grantFiles?: Asset[] | undefined + + /** Category tags */ + grantCategoryTags?: IGenericTag[] | undefined + + /** Type tag */ + grantTypeTag?: IGenericTag | undefined + + /** Fund */ + grantFund: IFund +} + +/** Grant is a part of "Styrkjatorg". */ + +export interface IGrant extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'grant' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + export interface IGraphCardFields { /** Graph Title */ graphTitle: string @@ -1935,7 +2051,7 @@ export interface IIntroLinkImageFields { /** Image */ image: Asset - /** Left Image */ + /** Image Position */ leftImage?: boolean | undefined /** Link Title */ @@ -2011,7 +2127,7 @@ export interface ILatestGenericListItemsFields { genericList?: IGenericList | undefined /** See more page */ - seeMorePage?: IOrganizationSubpage | undefined + seeMorePage: IOrganizationSubpage /** See more link text */ seeMoreLinkText?: string | undefined @@ -2135,6 +2251,34 @@ export interface ILifeEventPage extends Entry { } } +export interface ILifeEventPageListSliceFields { + /** Title */ + title?: string | undefined + + /** List */ + lifeEventPageList?: (ILifeEventPage | IAnchorPage)[] | undefined +} + +/** !!DO NOT USE!! - This content type has been deprecated. Use Anchor Page List */ + +export interface ILifeEventPageListSlice + extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: 'lifeEventPageListSlice' + linkType: 'ContentType' + type: 'Link' + } + } + } +} + export interface ILinkFields { /** Text */ text: string @@ -2737,6 +2881,9 @@ export interface IOneColumnTextFields { /** Show Title */ showTitle?: boolean | undefined + + /** Filter tags */ + filterTags?: IGenericTag[] | undefined } export interface IOneColumnText extends Entry { @@ -2924,6 +3071,9 @@ export interface IOrganizationFields { /** Kennitala */ kennitala?: string | undefined + + /** Alert Banner */ + alertBanner?: IAlertBanner | undefined } export interface IOrganization extends Entry { @@ -2982,6 +3132,7 @@ export interface IOrganizationPageFields { | IAnchorPageList | ISectionWithImage | IChartNumberBox + | ILatestGenericListItems )[] | undefined @@ -3176,6 +3327,7 @@ export interface IOrganizationSubpageFields { | ISectionWithVideo | ISectionHeading | ILatestEventsSlice + | IGenericList )[] | undefined @@ -3878,6 +4030,10 @@ export interface ISliceConnectedComponentFields { | 'SpecificHousingBenefitSupportCalculator' | 'GrindavikResidentialPropertyPurchaseCalculator' | 'Ums/CostOfLivingCalculator' + | 'Sveinslisti/JourneymanList' + | 'Starfsrettindi/ProfessionRights' + | 'VMST/ParentalLeaveCalculator' + | 'DigitalIceland/BenefitsOfDigitalProcesses' | undefined /** Localized JSON */ @@ -5133,12 +5289,14 @@ export type CONTENT_TYPE = | 'formField' | 'frontpage' | 'frontpageSlider' + | 'fund' | 'genericList' | 'genericListItem' | 'genericOverviewPage' | 'genericPage' | 'genericTag' | 'genericTagGroup' + | 'grant' | 'graphCard' | 'groupedMenu' | 'hnippTemplate' @@ -5148,6 +5306,7 @@ export type CONTENT_TYPE = | 'latestGenericListItems' | 'latestNewsSlice' | 'lifeEventPage' + | 'lifeEventPageListSlice' | 'link' | 'linkedPage' | 'linkGroup' diff --git a/libs/cms/src/lib/models/fund.model.ts b/libs/cms/src/lib/models/fund.model.ts new file mode 100644 index 000000000000..447e103695d7 --- /dev/null +++ b/libs/cms/src/lib/models/fund.model.ts @@ -0,0 +1,35 @@ +import { Field, ObjectType, ID } from '@nestjs/graphql' + +import { IFund } from '../generated/contentfulTypes' +import { CacheField } from '@island.is/nest/graphql' +import { ReferenceLink, mapReferenceLink } from './referenceLink.model' +import { Image, mapImage } from './image.model' +import { Organization, mapOrganization } from './organization.model' + +@ObjectType('OrganizationFund') +export class Fund { + @Field(() => ID) + id!: string + + @Field() + title!: string + + @CacheField(() => ReferenceLink, { nullable: true }) + link?: ReferenceLink + + @CacheField(() => Image, { nullable: true }) + featuredImage?: Image | null + + @CacheField(() => Organization) + parentOrganization!: Organization +} + +export const mapFund = ({ fields, sys }: IFund): Fund => ({ + id: sys.id, + title: fields.fundTitle, + link: fields.fundLink ? mapReferenceLink(fields.fundLink) : undefined, + featuredImage: fields.fundFeaturedImage + ? mapImage(fields.fundFeaturedImage) + : undefined, + parentOrganization: mapOrganization(fields.fundParentOrganization), +}) diff --git a/libs/cms/src/lib/models/grant.model.ts b/libs/cms/src/lib/models/grant.model.ts new file mode 100644 index 000000000000..9a1d0e182a27 --- /dev/null +++ b/libs/cms/src/lib/models/grant.model.ts @@ -0,0 +1,129 @@ +import { Field, ObjectType, ID, registerEnumType } from '@nestjs/graphql' + +import { IGrant } from '../generated/contentfulTypes' +import { GenericTag, mapGenericTag } from './genericTag.model' +import { CacheField } from '@island.is/nest/graphql' +import { mapDocument, SliceUnion } from '../unions/slice.union' +import { Asset, mapAsset } from './asset.model' +import { ReferenceLink, mapReferenceLink } from './referenceLink.model' +import { Fund, mapFund } from './fund.model' + +enum GrantStatus { + CLOSED, + OPEN, + OPENS_SOON, + INACTIVE, +} + +registerEnumType(GrantStatus, { name: 'GrantStatus' }) + +@ObjectType() +export class Grant { + @Field(() => ID) + id!: string + + @Field() + name!: string + + @Field({ nullable: true }) + description?: string + + @Field({ nullable: true }) + applicationId?: string + + @Field(() => [String], { nullable: true }) + applicationDeadlineText?: Array + + @CacheField(() => ReferenceLink, { nullable: true }) + applicationUrl?: ReferenceLink + + @CacheField(() => [SliceUnion]) + specialEmphasis?: Array + + @CacheField(() => [SliceUnion]) + whoCanApply?: Array + + @CacheField(() => [SliceUnion]) + howToApply?: Array + + @CacheField(() => [SliceUnion]) + applicationDeadline?: Array + + @CacheField(() => [SliceUnion]) + applicationHints?: Array + + @Field({ nullable: true }) + dateFrom?: string + + @Field({ nullable: true }) + dateTo?: string + + @Field({ nullable: true }) + isOpen?: boolean + + @Field({ nullable: true }) + statusText?: string + + @CacheField(() => GrantStatus, { nullable: true }) + status?: GrantStatus + + @CacheField(() => [Asset], { nullable: true }) + files?: Array + + @CacheField(() => [GenericTag], { nullable: true }) + categoryTags?: Array + + @CacheField(() => GenericTag, { nullable: true }) + typeTag?: GenericTag + + @CacheField(() => Fund, { nullable: true }) + fund?: Fund +} + +export const mapGrant = ({ fields, sys }: IGrant): Grant => ({ + id: sys.id, + name: fields.grantName, + description: fields.grantDescription, + applicationId: fields.grantApplicationId, + applicationDeadlineText: fields.grantApplicationDeadlineText, + applicationUrl: fields.granApplicationUrl?.fields + ? mapReferenceLink(fields.granApplicationUrl) + : undefined, + + specialEmphasis: fields.grantSpecialEmphasis + ? mapDocument(fields.grantSpecialEmphasis, sys.id + ':special-emphasis') + : [], + whoCanApply: fields.grantWhoCanApply + ? mapDocument(fields.grantWhoCanApply, sys.id + ':who-can-apply') + : [], + howToApply: fields.grantHowToApply + ? mapDocument(fields.grantHowToApply, sys.id + ':how-to-apply') + : [], + applicationDeadline: fields.grantApplicationDeadline + ? mapDocument( + fields.grantApplicationDeadline, + sys.id + ':application-deadline', + ) + : [], + applicationHints: fields.grantApplicationHints + ? mapDocument(fields.grantApplicationHints, sys.id + ':application-hints') + : [], + dateFrom: fields.grantDateFrom, + dateTo: fields.grantDateTo, + isOpen: fields.grantIsOpen ?? undefined, + statusText: fields.grantStatus ?? 'Óvirkur sjóður', + status: + fields.grantStatus === 'Opið fyrir umsóknir' + ? GrantStatus.OPEN + : fields.grantStatus === 'Lokað fyrir umsóknir' + ? GrantStatus.CLOSED + : fields.grantStatus === 'Opnar fljótlega' + ? GrantStatus.OPENS_SOON + : undefined, + fund: fields.grantFund ? mapFund(fields.grantFund) : undefined, + files: (fields.grantFiles ?? []).map((file) => mapAsset(file)) ?? [], + categoryTags: fields.grantCategoryTags + ? fields.grantCategoryTags.map((tag) => mapGenericTag(tag)) + : undefined, + typeTag: fields.grantTypeTag ? mapGenericTag(fields.grantTypeTag) : undefined, +}) diff --git a/libs/cms/src/lib/models/grantList.model.ts b/libs/cms/src/lib/models/grantList.model.ts new file mode 100644 index 000000000000..43263d4ae912 --- /dev/null +++ b/libs/cms/src/lib/models/grantList.model.ts @@ -0,0 +1,12 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql' +import { CacheField } from '@island.is/nest/graphql' +import { Grant } from './grant.model' + +@ObjectType() +export class GrantList { + @Field(() => Int) + total!: number + + @CacheField(() => [Grant]) + items!: Grant[] +} diff --git a/libs/cms/src/lib/search/cmsSync.module.ts b/libs/cms/src/lib/search/cmsSync.module.ts index d6edead06251..671094405728 100644 --- a/libs/cms/src/lib/search/cmsSync.module.ts +++ b/libs/cms/src/lib/search/cmsSync.module.ts @@ -33,6 +33,7 @@ import { CustomPageSyncService } from './importers/customPage.service' import { GenericListItemSyncService } from './importers/genericListItem.service' import { TeamListSyncService } from './importers/teamList.service' import { MappingService } from './mapping.service' +import { GrantsSyncService } from './importers/grants.service' @Module({ imports: [ @@ -54,6 +55,7 @@ import { MappingService } from './mapping.service' NewsSyncService, AdgerdirPageSyncService, MenuSyncService, + GrantsSyncService, GroupedMenuSyncService, OrganizationPageSyncService, OrganizationSubpageSyncService, diff --git a/libs/cms/src/lib/search/importers/grants.service.ts b/libs/cms/src/lib/search/importers/grants.service.ts new file mode 100644 index 000000000000..a1c35e33bb20 --- /dev/null +++ b/libs/cms/src/lib/search/importers/grants.service.ts @@ -0,0 +1,133 @@ +import { MappedData } from '@island.is/content-search-indexer/types' +import { logger } from '@island.is/logging' +import { Injectable } from '@nestjs/common' +import { Entry } from 'contentful' +import isCircular from 'is-circular' +import { IGrant } from '../../generated/contentfulTypes' +import { CmsSyncProvider, processSyncDataInput } from '../cmsSync.service' +import { + createTerms, + extractChildEntryIds, + extractStringsFromObject, + pruneNonSearchableSliceUnionFields, +} from './utils' +import { mapGrant } from '../../models/grant.model' +import { isDefined } from '@island.is/shared/utils' + +@Injectable() +export class GrantsSyncService implements CmsSyncProvider { + processSyncData(entries: processSyncDataInput) { + // only process grants that we consider not to be empty and dont have circular structures + return entries.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (entry: Entry): entry is IGrant => + entry.sys.contentType.sys.id === 'grant' && + entry.fields.grantName && + !isCircular(entry), + ) + } + + doMapping(entries: IGrant[]) { + return entries + .map((entry) => { + try { + const mapped = mapGrant(entry) + if (isCircular(mapped)) { + logger.warn('Circular reference found in grants', { + id: entry?.sys?.id, + }) + return false + } + + const content = [ + mapped.specialEmphasis + ? extractStringsFromObject( + mapped?.specialEmphasis?.map( + pruneNonSearchableSliceUnionFields, + ), + ) + : undefined, + mapped.whoCanApply + ? extractStringsFromObject( + mapped?.whoCanApply?.map(pruneNonSearchableSliceUnionFields), + ) + : undefined, + mapped.howToApply + ? extractStringsFromObject( + mapped?.howToApply?.map(pruneNonSearchableSliceUnionFields), + ) + : undefined, + mapped.applicationDeadline + ? extractStringsFromObject( + mapped?.applicationDeadline?.map( + pruneNonSearchableSliceUnionFields, + ), + ) + : undefined, + mapped?.applicationHints + ? extractStringsFromObject( + mapped?.applicationHints?.map( + pruneNonSearchableSliceUnionFields, + ), + ) + : undefined, + ] + .filter(isDefined) + .join() + + const tags: Array<{ + key: string + type: string + value?: string + }> = [ + mapped.typeTag + ? { + key: mapped.typeTag.slug, + type: 'genericTag', + value: mapped.typeTag.title, + } + : null, + ].filter(isDefined) + + mapped.categoryTags?.forEach((tag) => { + if (tag) { + tags.push({ + key: tag.slug, + type: 'genericTag', + value: tag.title, + }) + } + }) + + // Tag the document with the ids of its children so we can later look up what document a child belongs to + const childEntryIds = extractChildEntryIds(entry) + for (const id of childEntryIds) { + tags.push({ + key: id, + type: 'hasChildEntryWithId', + }) + } + + return { + _id: mapped.id, + title: mapped.name, + content, + contentWordCount: content.split(/\s+/).length, + type: 'webGrant', + termPool: createTerms([mapped.name]), + response: JSON.stringify({ ...mapped, typename: 'Grant' }), + tags, + dateCreated: entry.sys.createdAt, + dateUpdated: new Date().getTime().toString(), + } + } catch (error) { + logger.warn('Failed to import grants', { + error: error.message, + id: entry?.sys?.id, + }) + return false + } + }) + .filter((value): value is MappedData => Boolean(value)) + } +} diff --git a/libs/cms/src/lib/search/mapping.service.ts b/libs/cms/src/lib/search/mapping.service.ts index 981675af8792..51630b3c3e9b 100644 --- a/libs/cms/src/lib/search/mapping.service.ts +++ b/libs/cms/src/lib/search/mapping.service.ts @@ -25,6 +25,7 @@ import { CustomPageSyncService } from './importers/customPage.service' import { GenericListItemSyncService } from './importers/genericListItem.service' import { TeamListSyncService } from './importers/teamList.service' import type { CmsSyncProvider, processSyncDataInput } from './cmsSync.service' +import { GrantsSyncService } from './importers/grants.service' @Injectable() export class MappingService { @@ -53,6 +54,7 @@ export class MappingService { private readonly manualSyncService: ManualSyncService, private readonly manualChapterItemSyncService: ManualChapterItemSyncService, private readonly customPageSyncService: CustomPageSyncService, + private readonly grantSyncService: GrantsSyncService, private readonly genericListItemSyncService: GenericListItemSyncService, private readonly teamListSyncService: TeamListSyncService, ) { @@ -73,6 +75,7 @@ export class MappingService { this.supportQNASyncService, this.linkSyncService, this.enhancedAssetService, + this.grantSyncService, this.vacancyService, this.serviceWebPageSyncService, this.eventSyncService, diff --git a/libs/shared/types/src/lib/api-cms-domain.ts b/libs/shared/types/src/lib/api-cms-domain.ts index 8bfa67cde1eb..3f34d00e515f 100644 --- a/libs/shared/types/src/lib/api-cms-domain.ts +++ b/libs/shared/types/src/lib/api-cms-domain.ts @@ -6,6 +6,7 @@ export enum CustomPageUniqueIdentifier { PensionCalculator = 'PensionCalculator', OfficialJournalOfIceland = 'OfficialJournalOfIceland', Vacancies = 'Vacancies', + Grants = 'Grants', } export interface StatisticSourceValue { From 16dd1fb6ea5872b70ff43800a9de8a7dd370981c Mon Sep 17 00:00:00 2001 From: HjorturJ <34068269+HjorturJ@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:42:04 +0000 Subject: [PATCH 09/34] fix(application-system-api): Copy object on upload fix (#16883) * bucket change and debug error log * removing log and adding post processing to presignedposts * chore: nx format:write update dirty files * replacing more buckets * adding bucket to env * undoing bucket changes * newline change * undo frontend change * double check its the right character at the end --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/nest/aws/src/lib/s3.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/nest/aws/src/lib/s3.service.ts b/libs/nest/aws/src/lib/s3.service.ts index caa898cd2218..0e534dcd33c1 100644 --- a/libs/nest/aws/src/lib/s3.service.ts +++ b/libs/nest/aws/src/lib/s3.service.ts @@ -141,7 +141,12 @@ export class S3Service { params: PresignedPostOptions, ): Promise { try { - return await createPresignedPost(this.s3Client, params) + // The S3 Aws sdk v3 returns a trailing forward slash + const post = await createPresignedPost(this.s3Client, params) + if (post.url.endsWith('/')) { + post.url = post.url.slice(0, -1) + } + return post } catch (error) { this.logger.error( `An error occurred while trying to create a presigned post for file: ${params.Key} in bucket: ${params.Bucket}`, From f1ff30d817ad497c5657b0b2bdab466b34d9345b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dvar=20Oddsson?= Date: Mon, 18 Nov 2024 11:59:07 +0000 Subject: [PATCH 10/34] fix(j-s): Civil claimants national id (#16780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Save civil claimants national id even though its not found in the national registry * Remove debug code * Remove debug code * Remove debug code * Fix validation * Add React back --------- Co-authored-by: Guðjón Guðjónsson Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Indictments/Processing/Processing.tsx | 15 +++++++------ .../island-ui/core/src/lib/Toast/Toast.css.ts | 10 ++++----- libs/island-ui/core/src/lib/Toast/Toast.tsx | 21 ++++++++++--------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx index c2a21ccaf8a9..f7b17f45e9e1 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx @@ -258,17 +258,20 @@ const Processing: FC = () => { ) useEffect(() => { - if (!personData || !personData.items || personData.items.length === 0) { - setNationalIdNotFound(true) + if (!civilClaimantNationalIdUpdate) { return } - setNationalIdNotFound(false) + const items = personData?.items || [] + const person = items[0] + + setNationalIdNotFound(items.length === 0) + const update = { caseId: workingCase.id, - civilClaimantId: civilClaimantNationalIdUpdate?.civilClaimantId || '', - name: personData?.items[0].name, - nationalId: personData.items[0].kennitala, + civilClaimantId: civilClaimantNationalIdUpdate.civilClaimantId || '', + nationalId: civilClaimantNationalIdUpdate.nationalId, + ...(person?.name ? { name: person.name } : {}), } handleUpdateCivilClaimant(update) diff --git a/libs/island-ui/core/src/lib/Toast/Toast.css.ts b/libs/island-ui/core/src/lib/Toast/Toast.css.ts index 4bfc19872fe2..1a6e0062a834 100644 --- a/libs/island-ui/core/src/lib/Toast/Toast.css.ts +++ b/libs/island-ui/core/src/lib/Toast/Toast.css.ts @@ -14,7 +14,7 @@ globalStyle(`${root} .Toastify__toast-container`, { transform: 'translate3d(0, 0, 9999px)', position: 'fixed', padding: '4px', - width: '320px', + width: '432px', boxSizing: 'border-box', color: theme.color.dark400, '@media': { @@ -97,18 +97,19 @@ globalStyle(`${root} .Toastify__toast`, { minHeight: '64px', boxSizing: 'border-box', marginBottom: theme.spacing[2], - padding: theme.spacing[1], + padding: theme.spacing[2], borderWidth: 1, borderStyle: 'solid', borderRadius: theme.border.radius.large, display: 'flex', - justifyContent: 'space-between', overflow: 'hidden', cursor: 'pointer', direction: 'ltr', fontWeight: theme.typography.medium, '@media': { [`(max-width: ${theme.breakpoints.sm}px)`]: { + padding: theme.spacing[1], + minHeight: '48px', marginBottom: 0, }, }, @@ -147,7 +148,6 @@ globalStyle(`${root} .Toastify__toast--error`, { }) globalStyle(`${root} .Toastify__toast-body`, { flex: '1 1 auto', - margin: 'auto 0', }) /** Close button **/ @@ -160,7 +160,7 @@ globalStyle(`${root} .Toastify__close-button`, { cursor: 'pointer', opacity: 0.7, transition: '0.3s ease', - alignSelf: 'flex-start', + alignSelf: 'center', }) globalStyle(`${root} .Toastify__close-button--default`, { color: '#000', diff --git a/libs/island-ui/core/src/lib/Toast/Toast.tsx b/libs/island-ui/core/src/lib/Toast/Toast.tsx index 4e221e38c91b..9d8de91f883e 100644 --- a/libs/island-ui/core/src/lib/Toast/Toast.tsx +++ b/libs/island-ui/core/src/lib/Toast/Toast.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React, { FC } from 'react' import { ToastContainer as ToastifyContainer, toast as toastify, @@ -6,10 +6,11 @@ import { ToastOptions, } from 'react-toastify' import cn from 'classnames' + import { Box } from '../Box/Box' -import { Icon } from '../Icon/Icon' import { Text } from '../Text/Text' import * as toastStyles from './Toast.css' +import { Icon } from '../IconRC/Icon' interface ToastProps { hideProgressBar?: boolean @@ -32,16 +33,16 @@ const RenderMessage = ({ info: 'blue400', } as const const icons = { - error: 'toasterError', - success: 'toasterSuccess', - warning: 'toasterWarning', - info: 'toasterInfo', + error: 'warning', + success: 'checkmarkCircle', + warning: 'warning', + info: 'informationCircle', } as const return ( - - - + + + {message} @@ -50,7 +51,7 @@ const RenderMessage = ({ ) } -export const ToastContainer: React.FC> = ({ +export const ToastContainer: FC = ({ hideProgressBar = false, timeout = 5000, closeButton = false, From 37d8204c867fad5a160e5b89a82892fcd4acd4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:12:38 +0000 Subject: [PATCH 11/34] feat(web): Change fallback theme for organization pages (#16913) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Organization/Wrapper/OrganizationWrapper.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index d73d63528bda..784ebd3e9d98 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -205,13 +205,7 @@ export const getThemeConfig = ( } } - return { - themeConfig: { - headerColorScheme: 'white', - headerButtonColorScheme: 'negative', - footerVersion, - }, - } + return { themeConfig: { footerVersion } } } export const OrganizationHeader: React.FC< From 0159521caad1f06500b00b6c667948dc6cac108a Mon Sep 17 00:00:00 2001 From: norda-gunni <161026627+norda-gunni@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:35:00 +0000 Subject: [PATCH 12/34] chore(application-system): Add normalization for sms phone numbers (#16900) * chore(application-system): Add normalization for sms phone numbers * Add tests for phone number normalization --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../lib/modules/shared/shared.service.spec.ts | 159 ++++++++++++++++++ .../src/lib/modules/shared/shared.service.ts | 28 ++- 2 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 libs/application/template-api-modules/src/lib/modules/shared/shared.service.spec.ts diff --git a/libs/application/template-api-modules/src/lib/modules/shared/shared.service.spec.ts b/libs/application/template-api-modules/src/lib/modules/shared/shared.service.spec.ts new file mode 100644 index 000000000000..ddc63927d4d0 --- /dev/null +++ b/libs/application/template-api-modules/src/lib/modules/shared/shared.service.spec.ts @@ -0,0 +1,159 @@ +import { Test } from '@nestjs/testing' +import { SharedTemplateApiService } from './shared.service' +import { LOGGER_PROVIDER } from '@island.is/logging' +import type { Logger } from '@island.is/logging' +import { EmailService } from '@island.is/email-service' +import { SmsService } from '@island.is/nova-sms' +import { ApplicationService } from '@island.is/application/api/core' +import { PaymentService } from '@island.is/application/api/payment' +import { sharedModuleConfig } from './shared.config' +import { FormValue } from '@island.is/application/types' +import { + Application, + ApplicationStatus, + ActionCardMetaData, +} from '@island.is/application/types' +import { ApplicationTypes } from '@island.is/application/types' + +describe('SharedTemplateApiService', () => { + let service: SharedTemplateApiService + let smsService: jest.Mocked + let logger: jest.Mocked + + const mockActionCard: ActionCardMetaData = { + title: 'Sample Application', + description: 'This is a sample application description', + tag: { + label: 'In Review', + variant: 'blue', + }, + history: [ + { + date: new Date('2024-03-20'), + log: 'Application submitted', + }, + ], + } + + const mockApplication: Application = { + id: '12345-abcde', + state: 'submitted', + actionCard: mockActionCard, + applicant: 'user123', + assignees: ['reviewer1', 'reviewer2'], + applicantActors: ['user123'], + typeId: ApplicationTypes.EXAMPLE, + modified: new Date('2024-03-20T10:30:00'), + created: new Date('2024-03-19T15:45:00'), + answers: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + }, + externalData: {}, + name: 'John Doe Application', + institution: 'Example University', + progress: 75, + status: ApplicationStatus.IN_PROGRESS, + draftTotalSteps: 4, + draftFinishedSteps: 3, + } + + const mockConfig = { + templateApi: { + clientLocationOrigin: 'http://example.com', + jwtSecret: 'secret', + email: 'test@example.com', + }, + } + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + SharedTemplateApiService, + { + provide: LOGGER_PROVIDER, + useValue: { + warn: jest.fn(), + }, + }, + { + provide: EmailService, + useValue: {}, + }, + { + provide: SmsService, + useValue: { + sendSms: jest.fn(), + }, + }, + { + provide: sharedModuleConfig.KEY, + useValue: mockConfig, + }, + { + provide: ApplicationService, + useValue: {}, + }, + { + provide: PaymentService, + useValue: {}, + }, + ], + }).compile() + + service = module.get(SharedTemplateApiService) + smsService = module.get(SmsService) + logger = module.get(LOGGER_PROVIDER) + }) + + describe('sendSms', () => { + it('should successfully send an SMS with normalized phone number', async () => { + // Arrange + const mockSmsTemplateGenerator = jest.fn().mockReturnValue({ + phoneNumber: '+354 1234567', + message: 'Test message', + }) + + // Act + await service.sendSms(mockSmsTemplateGenerator, mockApplication) + + // Assert + expect(mockSmsTemplateGenerator).toHaveBeenCalledWith(mockApplication, { + clientLocationOrigin: mockConfig.templateApi.clientLocationOrigin, + }) + expect(smsService.sendSms).toHaveBeenCalledWith('1234567', 'Test message') + expect(logger.warn).toHaveBeenCalledTimes(2) + }) + + it('should normalize phone numbers with special characters', async () => { + // Arrange + const mockSmsTemplateGenerator = jest.fn().mockReturnValue({ + phoneNumber: '+354-123-4567', + message: 'Test message', + }) + + // Act + await service.sendSms(mockSmsTemplateGenerator, mockApplication) + + // Assert + expect(smsService.sendSms).toHaveBeenCalledWith('1234567', 'Test message') + expect(logger.warn).toHaveBeenCalledTimes(2) + }) + + it('should handle phone numbers longer than 7 digits', async () => { + // Arrange + const mockSmsTemplateGenerator = jest.fn().mockReturnValue({ + phoneNumber: '3541234567', + message: 'Test message', + }) + + // Act + await service.sendSms(mockSmsTemplateGenerator, mockApplication) + + // Assert + expect(smsService.sendSms).toHaveBeenCalledWith('1234567', 'Test message') + expect(logger.warn).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/libs/application/template-api-modules/src/lib/modules/shared/shared.service.ts b/libs/application/template-api-modules/src/lib/modules/shared/shared.service.ts index 64fdabb796a1..3f48ce61e62b 100644 --- a/libs/application/template-api-modules/src/lib/modules/shared/shared.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/shared/shared.service.ts @@ -58,7 +58,12 @@ export class SharedTemplateApiService { clientLocationOrigin, }) - return this.smsService.sendSms(phoneNumber, message) + const normalizedPhoneNumber = this.normalizePhoneNumber( + phoneNumber, + application.id, + ) + + return this.smsService.sendSms(normalizedPhoneNumber, message) } async assignApplicationThroughSms( @@ -75,7 +80,26 @@ export class SharedTemplateApiService { assignLink, ) - return this.smsService.sendSms(phoneNumber, message) + const normalizedPhoneNumber = this.normalizePhoneNumber( + phoneNumber, + application.id, + ) + + return this.smsService.sendSms(normalizedPhoneNumber, message) + } + + normalizePhoneNumber(phoneNumber: string, applicationId: string) { + if (phoneNumber.trim().length > 7) { + this.logger.warn( + `Recipient number for application ${applicationId} is longer than 7 characters, attempting to recover`, + ) + } + if (phoneNumber.match(/\D/g)) { + this.logger.warn( + `Recipient number for application ${applicationId} contains non-numeric characters, attempting to recover`, + ) + } + return phoneNumber.trim().replace(/\D/g, '').slice(-7) } async sendEmail( From b19bea9f3c6bddf43448eb822f18b11b33532cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3r=C3=B0ur=20H?= Date: Mon, 18 Nov 2024 12:48:30 +0000 Subject: [PATCH 13/34] fix(my-pages): Tsconfig paths (#16901) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/portals/my-pages/air-discount/tsconfig.json | 2 +- .../my-pages/air-discount/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/applications/tsconfig.json | 2 +- .../my-pages/applications/tsconfig.lib.json | 14 +++++++++----- libs/portals/my-pages/assets/tsconfig.json | 2 +- libs/portals/my-pages/consent/tsconfig.json | 2 +- libs/portals/my-pages/consent/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/constants/tsconfig.json | 2 +- libs/portals/my-pages/constants/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/core/tsconfig.json | 2 +- libs/portals/my-pages/documents/tsconfig.json | 2 +- libs/portals/my-pages/documents/tsconfig.lib.json | 14 +++++++++----- .../my-pages/education-career/tsconfig.json | 2 +- .../my-pages/education-career/tsconfig.lib.json | 4 ++-- .../my-pages/education-degree/tsconfig.json | 2 +- .../my-pages/education-degree/tsconfig.lib.json | 4 ++-- .../my-pages/education-license/tsconfig.json | 2 +- .../my-pages/education-license/tsconfig.lib.json | 4 ++-- .../education-student-assessment/tsconfig.json | 2 +- .../education-student-assessment/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/education/tsconfig.json | 2 +- libs/portals/my-pages/education/tsconfig.lib.json | 14 +++++++++----- libs/portals/my-pages/education/tsconfig.spec.json | 4 ++-- libs/portals/my-pages/finance/tsconfig.json | 2 +- libs/portals/my-pages/finance/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/graphql/tsconfig.json | 2 +- libs/portals/my-pages/health/tsconfig.json | 2 +- libs/portals/my-pages/health/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/information/tsconfig.json | 2 +- .../portals/my-pages/information/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/law-and-order/tsconfig.json | 2 +- libs/portals/my-pages/licenses/tsconfig.json | 2 +- libs/portals/my-pages/licenses/tsconfig.lib.json | 4 ++-- .../my-pages/occupational-licenses/tsconfig.json | 2 +- .../occupational-licenses/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/petitions/tsconfig.json | 2 +- libs/portals/my-pages/petitions/tsconfig.lib.json | 4 ++-- libs/portals/my-pages/restrictions/tsconfig.json | 2 +- libs/portals/my-pages/sessions/tsconfig.json | 2 +- libs/portals/my-pages/sessions/tsconfig.lib.json | 4 ++-- .../signature-collection/tsconfig.lib.json | 4 ++-- .../social-insurance-maintenance/tsconfig.json | 2 +- .../social-insurance-maintenance/tsconfig.lib.json | 4 ++-- 43 files changed, 84 insertions(+), 72 deletions(-) diff --git a/libs/portals/my-pages/air-discount/tsconfig.json b/libs/portals/my-pages/air-discount/tsconfig.json index 4b421814593c..3512bf7afeea 100644 --- a/libs/portals/my-pages/air-discount/tsconfig.json +++ b/libs/portals/my-pages/air-discount/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true, diff --git a/libs/portals/my-pages/air-discount/tsconfig.lib.json b/libs/portals/my-pages/air-discount/tsconfig.lib.json index b1fdab0f8413..9668c0996130 100644 --- a/libs/portals/my-pages/air-discount/tsconfig.lib.json +++ b/libs/portals/my-pages/air-discount/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/applications/tsconfig.json b/libs/portals/my-pages/applications/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/applications/tsconfig.json +++ b/libs/portals/my-pages/applications/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/applications/tsconfig.lib.json b/libs/portals/my-pages/applications/tsconfig.lib.json index 1d50996ab513..9668c0996130 100644 --- a/libs/portals/my-pages/applications/tsconfig.lib.json +++ b/libs/portals/my-pages/applications/tsconfig.lib.json @@ -4,16 +4,20 @@ "outDir": "../../../../dist/out-tsc", "types": ["node"] }, + "files": [ + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" + ], "exclude": [ "**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", "jest.config.ts" ], - "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"], - "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" - ] + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] } diff --git a/libs/portals/my-pages/assets/tsconfig.json b/libs/portals/my-pages/assets/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/assets/tsconfig.json +++ b/libs/portals/my-pages/assets/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/consent/tsconfig.json b/libs/portals/my-pages/consent/tsconfig.json index 4b421814593c..3512bf7afeea 100644 --- a/libs/portals/my-pages/consent/tsconfig.json +++ b/libs/portals/my-pages/consent/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true, diff --git a/libs/portals/my-pages/consent/tsconfig.lib.json b/libs/portals/my-pages/consent/tsconfig.lib.json index b1fdab0f8413..9668c0996130 100644 --- a/libs/portals/my-pages/consent/tsconfig.lib.json +++ b/libs/portals/my-pages/consent/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/constants/tsconfig.json b/libs/portals/my-pages/constants/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/constants/tsconfig.json +++ b/libs/portals/my-pages/constants/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/constants/tsconfig.lib.json b/libs/portals/my-pages/constants/tsconfig.lib.json index 1d50996ab513..df044b77b9a5 100644 --- a/libs/portals/my-pages/constants/tsconfig.lib.json +++ b/libs/portals/my-pages/constants/tsconfig.lib.json @@ -13,7 +13,7 @@ ], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"], "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ] } diff --git a/libs/portals/my-pages/core/tsconfig.json b/libs/portals/my-pages/core/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/core/tsconfig.json +++ b/libs/portals/my-pages/core/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/documents/tsconfig.json b/libs/portals/my-pages/documents/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/documents/tsconfig.json +++ b/libs/portals/my-pages/documents/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/documents/tsconfig.lib.json b/libs/portals/my-pages/documents/tsconfig.lib.json index 1d50996ab513..9668c0996130 100644 --- a/libs/portals/my-pages/documents/tsconfig.lib.json +++ b/libs/portals/my-pages/documents/tsconfig.lib.json @@ -4,16 +4,20 @@ "outDir": "../../../../dist/out-tsc", "types": ["node"] }, + "files": [ + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" + ], "exclude": [ "**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", "jest.config.ts" ], - "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"], - "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" - ] + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] } diff --git a/libs/portals/my-pages/education-career/tsconfig.json b/libs/portals/my-pages/education-career/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/education-career/tsconfig.json +++ b/libs/portals/my-pages/education-career/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/education-career/tsconfig.lib.json b/libs/portals/my-pages/education-career/tsconfig.lib.json index bc322cb27aa7..d547efefcf0b 100644 --- a/libs/portals/my-pages/education-career/tsconfig.lib.json +++ b/libs/portals/my-pages/education-career/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/education-degree/tsconfig.json b/libs/portals/my-pages/education-degree/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/education-degree/tsconfig.json +++ b/libs/portals/my-pages/education-degree/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/education-degree/tsconfig.lib.json b/libs/portals/my-pages/education-degree/tsconfig.lib.json index bc322cb27aa7..d547efefcf0b 100644 --- a/libs/portals/my-pages/education-degree/tsconfig.lib.json +++ b/libs/portals/my-pages/education-degree/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/education-license/tsconfig.json b/libs/portals/my-pages/education-license/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/education-license/tsconfig.json +++ b/libs/portals/my-pages/education-license/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/education-license/tsconfig.lib.json b/libs/portals/my-pages/education-license/tsconfig.lib.json index bc322cb27aa7..d547efefcf0b 100644 --- a/libs/portals/my-pages/education-license/tsconfig.lib.json +++ b/libs/portals/my-pages/education-license/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/education-student-assessment/tsconfig.json b/libs/portals/my-pages/education-student-assessment/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/education-student-assessment/tsconfig.json +++ b/libs/portals/my-pages/education-student-assessment/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/education-student-assessment/tsconfig.lib.json b/libs/portals/my-pages/education-student-assessment/tsconfig.lib.json index bc322cb27aa7..d547efefcf0b 100644 --- a/libs/portals/my-pages/education-student-assessment/tsconfig.lib.json +++ b/libs/portals/my-pages/education-student-assessment/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/education/tsconfig.json b/libs/portals/my-pages/education/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/education/tsconfig.json +++ b/libs/portals/my-pages/education/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/education/tsconfig.lib.json b/libs/portals/my-pages/education/tsconfig.lib.json index 1d50996ab513..9668c0996130 100644 --- a/libs/portals/my-pages/education/tsconfig.lib.json +++ b/libs/portals/my-pages/education/tsconfig.lib.json @@ -4,16 +4,20 @@ "outDir": "../../../../dist/out-tsc", "types": ["node"] }, + "files": [ + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" + ], "exclude": [ "**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", "jest.config.ts" ], - "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"], - "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" - ] + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] } diff --git a/libs/portals/my-pages/education/tsconfig.spec.json b/libs/portals/my-pages/education/tsconfig.spec.json index 9c453e8cbe59..47e877796468 100644 --- a/libs/portals/my-pages/education/tsconfig.spec.json +++ b/libs/portals/my-pages/education/tsconfig.spec.json @@ -18,7 +18,7 @@ "jest.config.ts" ], "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ] } diff --git a/libs/portals/my-pages/finance/tsconfig.json b/libs/portals/my-pages/finance/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/finance/tsconfig.json +++ b/libs/portals/my-pages/finance/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/finance/tsconfig.lib.json b/libs/portals/my-pages/finance/tsconfig.lib.json index 1d50996ab513..df044b77b9a5 100644 --- a/libs/portals/my-pages/finance/tsconfig.lib.json +++ b/libs/portals/my-pages/finance/tsconfig.lib.json @@ -13,7 +13,7 @@ ], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"], "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ] } diff --git a/libs/portals/my-pages/graphql/tsconfig.json b/libs/portals/my-pages/graphql/tsconfig.json index f094e2ad0a12..55d3aaeea3e3 100644 --- a/libs/portals/my-pages/graphql/tsconfig.json +++ b/libs/portals/my-pages/graphql/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "include": [], "files": [], "references": [ diff --git a/libs/portals/my-pages/health/tsconfig.json b/libs/portals/my-pages/health/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/health/tsconfig.json +++ b/libs/portals/my-pages/health/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/health/tsconfig.lib.json b/libs/portals/my-pages/health/tsconfig.lib.json index 1d50996ab513..df044b77b9a5 100644 --- a/libs/portals/my-pages/health/tsconfig.lib.json +++ b/libs/portals/my-pages/health/tsconfig.lib.json @@ -13,7 +13,7 @@ ], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"], "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ] } diff --git a/libs/portals/my-pages/information/tsconfig.json b/libs/portals/my-pages/information/tsconfig.json index 4f13c3264df0..f621b7e9bab5 100644 --- a/libs/portals/my-pages/information/tsconfig.json +++ b/libs/portals/my-pages/information/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true, diff --git a/libs/portals/my-pages/information/tsconfig.lib.json b/libs/portals/my-pages/information/tsconfig.lib.json index bc322cb27aa7..d547efefcf0b 100644 --- a/libs/portals/my-pages/information/tsconfig.lib.json +++ b/libs/portals/my-pages/information/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/law-and-order/tsconfig.json b/libs/portals/my-pages/law-and-order/tsconfig.json index 4daaf45cd328..c88d07daddd5 100644 --- a/libs/portals/my-pages/law-and-order/tsconfig.json +++ b/libs/portals/my-pages/law-and-order/tsconfig.json @@ -16,5 +16,5 @@ "path": "./tsconfig.spec.json" } ], - "extends": "../../../tsconfig.base.json" + "extends": "../../../../tsconfig.base.json" } diff --git a/libs/portals/my-pages/licenses/tsconfig.json b/libs/portals/my-pages/licenses/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/licenses/tsconfig.json +++ b/libs/portals/my-pages/licenses/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/licenses/tsconfig.lib.json b/libs/portals/my-pages/licenses/tsconfig.lib.json index bc322cb27aa7..d547efefcf0b 100644 --- a/libs/portals/my-pages/licenses/tsconfig.lib.json +++ b/libs/portals/my-pages/licenses/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/occupational-licenses/tsconfig.json b/libs/portals/my-pages/occupational-licenses/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/occupational-licenses/tsconfig.json +++ b/libs/portals/my-pages/occupational-licenses/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/occupational-licenses/tsconfig.lib.json b/libs/portals/my-pages/occupational-licenses/tsconfig.lib.json index bc322cb27aa7..d547efefcf0b 100644 --- a/libs/portals/my-pages/occupational-licenses/tsconfig.lib.json +++ b/libs/portals/my-pages/occupational-licenses/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/petitions/tsconfig.json b/libs/portals/my-pages/petitions/tsconfig.json index 45d3d172fde4..4aee5eb425be 100644 --- a/libs/portals/my-pages/petitions/tsconfig.json +++ b/libs/portals/my-pages/petitions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true diff --git a/libs/portals/my-pages/petitions/tsconfig.lib.json b/libs/portals/my-pages/petitions/tsconfig.lib.json index 6394d9f9cd81..ae80da66e65a 100644 --- a/libs/portals/my-pages/petitions/tsconfig.lib.json +++ b/libs/portals/my-pages/petitions/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx"], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] diff --git a/libs/portals/my-pages/restrictions/tsconfig.json b/libs/portals/my-pages/restrictions/tsconfig.json index 89f8ac0850c1..93614c49d348 100644 --- a/libs/portals/my-pages/restrictions/tsconfig.json +++ b/libs/portals/my-pages/restrictions/tsconfig.json @@ -13,5 +13,5 @@ "path": "./tsconfig.lib.json" } ], - "extends": "../../../tsconfig.base.json" + "extends": "../../../../tsconfig.base.json" } diff --git a/libs/portals/my-pages/sessions/tsconfig.json b/libs/portals/my-pages/sessions/tsconfig.json index 4b421814593c..3512bf7afeea 100644 --- a/libs/portals/my-pages/sessions/tsconfig.json +++ b/libs/portals/my-pages/sessions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "allowJs": true, diff --git a/libs/portals/my-pages/sessions/tsconfig.lib.json b/libs/portals/my-pages/sessions/tsconfig.lib.json index b1fdab0f8413..9668c0996130 100644 --- a/libs/portals/my-pages/sessions/tsconfig.lib.json +++ b/libs/portals/my-pages/sessions/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": [ "**/*.spec.ts", diff --git a/libs/portals/my-pages/signature-collection/tsconfig.lib.json b/libs/portals/my-pages/signature-collection/tsconfig.lib.json index 6394d9f9cd81..ae80da66e65a 100644 --- a/libs/portals/my-pages/signature-collection/tsconfig.lib.json +++ b/libs/portals/my-pages/signature-collection/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx"], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] diff --git a/libs/portals/my-pages/social-insurance-maintenance/tsconfig.json b/libs/portals/my-pages/social-insurance-maintenance/tsconfig.json index 89f8ac0850c1..93614c49d348 100644 --- a/libs/portals/my-pages/social-insurance-maintenance/tsconfig.json +++ b/libs/portals/my-pages/social-insurance-maintenance/tsconfig.json @@ -13,5 +13,5 @@ "path": "./tsconfig.lib.json" } ], - "extends": "../../../tsconfig.base.json" + "extends": "../../../../tsconfig.base.json" } diff --git a/libs/portals/my-pages/social-insurance-maintenance/tsconfig.lib.json b/libs/portals/my-pages/social-insurance-maintenance/tsconfig.lib.json index 6394d9f9cd81..ae80da66e65a 100644 --- a/libs/portals/my-pages/social-insurance-maintenance/tsconfig.lib.json +++ b/libs/portals/my-pages/social-insurance-maintenance/tsconfig.lib.json @@ -5,8 +5,8 @@ "types": ["node"] }, "files": [ - "../../../node_modules/@nx/react/typings/cssmodule.d.ts", - "../../../node_modules/@nx/react/typings/image.d.ts" + "../../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../../node_modules/@nx/react/typings/image.d.ts" ], "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx"], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] From cbecc81b6235bc7a76e2b32fd66d3527dfb3a8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Svanhildur=20Einarsd=C3=B3ttir?= <54863023+svanaeinars@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:13:19 +0000 Subject: [PATCH 14/34] chore(tests): Standardize imports and add page navigavion helper (#16914) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/testing/e2e/src/index.ts | 3 ++- libs/testing/e2e/src/lib/helpers/api-tools.ts | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/libs/testing/e2e/src/index.ts b/libs/testing/e2e/src/index.ts index 1e4c8baa004e..f7fba34a0d79 100644 --- a/libs/testing/e2e/src/index.ts +++ b/libs/testing/e2e/src/index.ts @@ -9,4 +9,5 @@ export * from './lib/session/session' export * from './lib/modules/urls' export * from './lib/helpers/utils' export * from './lib/config/playwright-config' -export { test, expect, Page, Locator, BrowserContext } from '@playwright/test' +export { test, expect } from '@playwright/test' +export type { Page, Locator, BrowserContext } from '@playwright/test' diff --git a/libs/testing/e2e/src/lib/helpers/api-tools.ts b/libs/testing/e2e/src/lib/helpers/api-tools.ts index 09e4042c1b3f..a4fd275f2143 100644 --- a/libs/testing/e2e/src/lib/helpers/api-tools.ts +++ b/libs/testing/e2e/src/lib/helpers/api-tools.ts @@ -1,4 +1,4 @@ -import { Page } from '@playwright/test' +import { Page, BrowserContext } from '@playwright/test' /** * Waits for a network request to complete and verifies its operation name. @@ -21,3 +21,19 @@ export const verifyRequestCompletion = async ( return await response.json() } + +/** + * Creates a new page in the given browser context and navigates to the specified URL. + * + * @param context - The browser context in which to create the new page. + * @param url - The URL to navigate to. + * @returns A promise that resolves to the created page. + */ +export const createPageAndNavigate = async ( + context: BrowserContext, + url: string, +) => { + const page = await context.newPage() + await page.goto(url) + return page +} From aab5c31e6fa10d3f5cb4c34c13f9005658e85e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Svanhildur=20Einarsd=C3=B3ttir?= <54863023+svanaeinars@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:26:22 +0000 Subject: [PATCH 15/34] chore(tests): Improve E2E Documentation (#16909) * chore(tests): Improve E2E Documentation * Update libs/testing/e2e/README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/testing/e2e/README.md | 221 ++++++++++++++++++++++++++++++++----- 1 file changed, 194 insertions(+), 27 deletions(-) diff --git a/libs/testing/e2e/README.md b/libs/testing/e2e/README.md index 7fa5ac12b3db..22bb04155947 100644 --- a/libs/testing/e2e/README.md +++ b/libs/testing/e2e/README.md @@ -1,48 +1,215 @@ -# E2E Testing +# E2E Testing Library -This library was generated with [Nx](https://nx.dev). It contains utility functions and configuration files that assist with end-to-end (E2E) testing in Playwright for various apps. +This library contains utility functions, shared configuration, and documentation to assist with end-to-end (E2E) testing for all apps using Playwright. -## Overview +## 📚 Overview -This library includes: +### Contents -- **Helper Functions:** Utility functions designed to streamline E2E testing with Playwright. These functions cater to different applications across the project and help automate common testing workflows. -- **Global Playwright Configuration:** The `createGlobalConfig` function provides a shared Playwright configuration used across multiple applications. It standardizes the testing environment. +- **🔧 Helper Functions**: Utility functions to simplify and standardize E2E testing across apps. +- **⚙️ Shared Playwright Configuration**: A common configuration used as the base for all app-specific Playwright configurations. +- **🌍 Multi-Environment Testing**: Support for running tests in `local`, `dev`, `staging`, and `prod` environments. -## Mockoon Usage Guide for E2E Tests +## 🚀 How to Use This Library -This section explains how to use [Mockoon](https://mockoon.com/) to set up mock APIs for end-to-end (e2e) testing. +### Importing Helper Functions -### What is Mockoon? +To use helper functions in your tests, import them directly from this library: -[Mockoon](https://mockoon.com/) is an open-source tool for creating mock APIs quickly and easily. It allows developers to simulate backend servers without relying on live backend services. This is especially useful for e2e testing, where consistency and repeatability of backend responses are important. +```typescript +import { myHelperFunction } from '@island.is/testing/e2e' +``` + +### Extending the Common Playwright Config + +Each app should create its own `playwright.config.ts` file that extends the shared configuration: + +```typescript +import { createPlaywrightConfig } from '@island.is/testing/e2e' + +const playwrightConfig = createPlaywrightConfig({ + webServerUrl: '', + command: '', + // Add any app-specific configurations here +}) + +export default playwrightConfig +``` + +## 🏃 Running Tests + +Use the following command structure to run tests for any app: + +```bash +yarn e2e +``` + +### Useful Playwright Commands and Flags -Mockoon provides both a graphical user interface (GUI) for managing API mock files and a command-line interface (CLI) for running these mock APIs in various environments, such as pipelines. +- **Run with UI Mode**: Launch the tests with a UI to select and debug tests interactively. -### Opening an Existing Mock File in Mockoon + ```bash + yarn e2e --ui + ``` -To view or modify an existing mock file: +- **Run Tests Without Caching**: Ensure a fresh run of tests without using cached results. -1. Open Mockoon. -2. Click on **+** and then click on **Open Local Environment**. -3. Choose the desired mock file, such as `apps//e2e/mocks/.json`. + ```bash + yarn e2e --skip-nx-cache + ``` -This will load the mock configuration into the Mockoon UI, allowing you to inspect and edit the mock endpoints. +- **Run a Specific Project**: Run only the tests defined under a specific project in your Playwright config: -### Creating a Mock File with Mockoon UI + ```bash + yarn e2e -- --project=smoke + yarn e2e -- --project=acceptance + yarn e2e -- --project=everything + ``` -To create or modify a mock file: +- **View the Test Report**: After running tests, use this command to view the generated report: -1. Download and install [Mockoon](https://mockoon.com/download/) if you haven't already. -2. Open Mockoon and create a new environment: - - Click on **+** and then click on **New Local Environment**. - - Nema your mock file and choose a location for it e.g. `apps//e2e/mocks/.json`. - - Add endpoints, routes, and response details as needed. + ```bash + yarn playwright show-report + ``` -### Running a Mockoon Server with the CLI +- **Run Specific Tests**: Use `--grep` to run tests matching a specific pattern: -To run a mock server with the cli, use the following command: + ```bash + yarn e2e --grep "Home Page Test" + ``` + +- **Debug Mode**: Run tests in debug mode for better visibility: + + ```bash + yarn e2e --debug + ``` + +## ✍️ Writing Tests + +Run `yarn playwright codegen --output ` and modify the output. The selectors need special attention; they should be transformed to use roles or `data-testid` attributes for stability (see below on how to). + +### 🤔 What to Test + +Writing tests for every possible combination is time-consuming for you and the CI pipeline, with diminishing value beyond the most common cases. + +You should therefore aim to write test for: + +- Most common usage patterns +- Usage/patterns that MUST NOT break +- Problematic cases likely to cause an error/bug + +### 🏗️ Test structure + +Test cases are written in spec files. Tests that do not modify anything (e.g., _create_ an application, _change_ the user’s name, etc.) and verify basic functionality are called **smoke tests**. Tests that are more detailed and/or make any changes at all, are called **acceptance tests**. Test cases are put into folders by what app they are testing, smoke/acceptance test, and each file tests some aspect of an app. Here is an example of the folder layout for testing the search engine and front-page of the `web` project (within the system-e2e app): + +```shell +web/ (app name) +├── smoke/ (test type) +│ └── home-page.spec.ts (feature name, kebab-case) +└── acceptance/ + └── search.spec.ts +``` + +### 🗃️ Spec files + +A spec file should have only one description (`test.describe`) of what part of an app is being tested. Therein can be one or more test cases (`test`) with a description of what scenario each test case is testing. Setup and tear down can be done in `test.beforeAll`, `test.beforeEach`, `test.afterAll`, and `test.afterEach`. You should not _rely_ on `after*` ever running, and you should prepare your environment every time _before_ each test. For example: + +```jsx +test.describe('Overview part of banking app', () => { + test.beforeAll(() => { + // Create/clear database + // Seed database + }) + + /* NOTE: there is no guarantee this will run */ + test.afterAll(() => { + // Tear down database + // Log out + }) + + test.beforeEach(() => { + // Log in + // Basic state reset, e.g. clear inbox + }) + + test('should get paid', () => { + // Make user get money using page.selector, page.click, etc. + // Verify money is present + }) +}) +``` + +Each test case (`test`) should test a specific scenario from end-to-end. If your test is getting long and complicated consider breaking it up within a `test` with `test.step`; each step will run in succession and the failure/success report is easier to read. Let’s take the operating licence application as an example; test various routes/cases: + +- Hotel permit with food, but no alcohol +- Hotel permit with food and alcohol +- Bar with only alcohol +- Home accommodation (AirBnB style), no food, no alcohol + +### 🧰 Using Fixtures and Mocking Server Responses + +Fixtures are essential for setting up controlled test data to ensure predictable test behavior. Mocking server responses can help simulate specific backend scenarios without relying on live data. Use the following approach to mock server responses: + +```typescript +await page.route('/api/endpoint', (route) => + route.fulfill({ + status: 200, + body: JSON.stringify({ key: 'mockData' }), + }), +) +``` + +### 😬 Tricky element searching + +Some apps, like service-portal and application-system-form, load their components _very_ asynchronously. This can be an issue when targeting some elements, but they do not appear on the first page load, but instead load after the basic page has loaded. + +In such cases you can wait for the elements to exist with `page.waitFor*` ([docs](https://playwright.dev/docs/api/class-page#page-wait-for-event)): + +```jsx +// Wait for there to be at least 3 checkboxes +await page.waitForSelector(':nth-match("role=checkbox", 3') +// Wait for any arbitrary function +await page.waitForFunction(async () => { + const timer = page.locator('role=timer') + const timeLeft = await timer.textContent() + return Number(timeLeft) < 10 +}) +``` + +## 🤖 Mockoon Usage Guide for E2E Tests + +### ❓ What is Mockoon? + +[Mockoon](https://mockoon.com/) is a tool for creating mock APIs, useful for simulating backend services during E2E testing. + +### 📂 Opening and Creating Mock Files + +- **To open an existing mock file**: Navigate to `apps//e2e/mocks/.json` in the Mockoon UI. +- **To create a new mock file**: + 1. Download [Mockoon](https://mockoon.com/download/). + 2. Create a new environment and save it to `apps//e2e/mocks/`. + +### 🖥️ Running Mockoon Server with CLI + +To start a mock server: ```bash -yarn mockoon-cli start --data ./apps//e2e/mocks/.json --port +yarn mockoon-cli start --data ./apps//e2e/mocks/.json --port ``` + +## 🛠️ Troubleshooting and FAQs + +### 🔄 Common Issues + +- **500: Internal Server Error**: If not related to your code, contact the DevOps team. +- **💀 ESOCKETTIMEDOUT**: Likely an infrastructure issue. Reach out to DevOps if persistent. +- **⌛ Tests Timing Out**: Increase the timeout if known network issues exist: + + ```typescript + await page.goto('/my-url', { timeout: Timeout.medium }) + ``` + +## 📖 Additional Resources + +- Refer to each app directory's `README.md` for app-specific details and usage instructions. +- Check out the [Playwright Documentation](https://playwright.dev) for further information. From 23b699566b1db81dcf95ec0acf16dc05b8af6f1c Mon Sep 17 00:00:00 2001 From: kksteini <77672665+kksteini@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:34:40 +0000 Subject: [PATCH 16/34] fix(application-ir): Separate texts (#16902) * fix(application-ir): Separate texts * Update libs/application/templates/inheritance-report/src/lib/messages.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Use updated message in view --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../src/forms/sections/prepaidInheritance/applicant.ts | 2 +- .../templates/inheritance-report/src/lib/messages.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/applicant.ts b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/applicant.ts index 2de640649a88..dc53eb07648d 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/applicant.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/applicant.ts @@ -16,7 +16,7 @@ export const prePaidApplicant = buildSection({ buildMultiField({ id: 'prePaidApplicant', title: m.applicantsInfo, - description: m.applicantsInfoSubtitle, + description: m.applicantsPrePaidInfoSubtitle, children: [ buildNationalIdWithNameField({ id: 'prePaidApplicant', diff --git a/libs/application/templates/inheritance-report/src/lib/messages.ts b/libs/application/templates/inheritance-report/src/lib/messages.ts index fc3969ea9920..8880f3a158e1 100644 --- a/libs/application/templates/inheritance-report/src/lib/messages.ts +++ b/libs/application/templates/inheritance-report/src/lib/messages.ts @@ -227,6 +227,12 @@ export const m = defineMessages({ 'Vinsamlegast farðu yfir upplýsingarnar og athugaðu hvort þær séu réttar.', description: '', }, + applicantsPrePaidInfoSubtitle: { + id: 'ir.application:applicantsPrePaidInfoSubtitle', + defaultMessage: + 'Vinsamlegast farðu yfir upplýsingarnar og athugaðu hvort þær séu réttar.', + description: 'Subtitle text shown above prepaid applicant information form', + }, name: { id: 'ir.application:name', defaultMessage: 'Nafn', From 882582391aff736ae9ad6c1b76f4f7de8115e6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnlaugur=20Gu=C3=B0mundsson?= <34029342+GunnlaugurG@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:43:07 +0000 Subject: [PATCH 17/34] feat(ids-admin): Change delete to archive (#16917) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/portals/admin/ids-admin/src/lib/messages.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/portals/admin/ids-admin/src/lib/messages.ts b/libs/portals/admin/ids-admin/src/lib/messages.ts index 37ca1fe0afaa..4fa97ac192d2 100644 --- a/libs/portals/admin/ids-admin/src/lib/messages.ts +++ b/libs/portals/admin/ids-admin/src/lib/messages.ts @@ -836,11 +836,11 @@ export const m = defineMessages({ }, successDeletingClient: { id: 'ap.ids-admin:success-deleting-client', - defaultMessage: 'Successfully deleted application', + defaultMessage: 'Successfully archived application', }, delete: { id: 'ap.ids-admin:delete', - defaultMessage: 'Delete', + defaultMessage: 'Archive', }, closeDeleteModal: { id: 'ap.ids-admin:close-delete-modal', @@ -852,12 +852,12 @@ export const m = defineMessages({ }, deleteClient: { id: 'ap.ids-admin:delete-client-all-env', - defaultMessage: 'Delete application', + defaultMessage: 'Archive application', }, deleteClientAlertMessage: { id: 'ap.ids-admin:delete-client-alert-message', defaultMessage: - 'The client ID will be deleted from all available environments and cannot be reused. Authentications will stop working immediately for your application.', + 'The client ID will be archived from all available environments and cannot be reused. Authentications will stop working immediately for your application.', }, partiallyCreatedClient: { id: 'ap.ids-admin:partially-created-client', From 16e680448b9e6f4d1d54124f46e2ea371cc5d85e Mon Sep 17 00:00:00 2001 From: Ylfa <55542991+ylfahfa@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:52:27 +0000 Subject: [PATCH 18/34] fix(income-plan): amend dynamic eligible messages (#16918) * use correct variable to determine ip year * add option to dynamic eligible text * format * fix text --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../income-plan/src/forms/IncomePlanForm.ts | 7 ++++--- .../income-plan/src/lib/constants.ts | 1 + .../income-plan/src/lib/incomePlanUtils.ts | 5 +++-- .../income-plan/src/lib/messages.ts | 9 ++++++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts b/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts index 1ca22ef46eba..d7d70518d272 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts @@ -69,8 +69,9 @@ export const IncomePlanForm: Form = buildForm({ id: 'incomePlanTable', title: incomePlanFormMessage.info.section, description: (application: Application) => { - const { incomePlanConditions, latestIncomePlan } = - getApplicationExternalData(application.externalData) + const { latestIncomePlan } = getApplicationExternalData( + application.externalData, + ) const hasLatestIncomePlan = !isEmpty(latestIncomePlan) const baseMessage = hasLatestIncomePlan ? incomePlanFormMessage.incomePlan @@ -80,7 +81,7 @@ export const IncomePlanForm: Form = buildForm({ return { ...baseMessage, values: { - incomePlanYear: incomePlanConditions.incomePlanYear, + incomePlanYear: latestIncomePlan.year, }, } }, diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/lib/constants.ts b/libs/application/templates/social-insurance-administration/income-plan/src/lib/constants.ts index a87cc7cbb9f7..83ac9bea41be 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/lib/constants.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/lib/constants.ts @@ -10,6 +10,7 @@ export const DIVIDENDS_IN_FOREIGN_BANKS = export const ISK = 'IKR' export const INCOME = 'Atvinnutekjur' export const INCOME_PLANS_CLOSED = 'INCOME_PLANS_CLOSED' +export const NO_ACTIVE_APPLICATIONS = 'NO_ACTIVE_APPLICATIONS' export enum RatioType { YEARLY = 'yearly', diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/lib/incomePlanUtils.ts b/libs/application/templates/social-insurance-administration/income-plan/src/lib/incomePlanUtils.ts index a9a021aea627..a01e51b430bb 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/lib/incomePlanUtils.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/lib/incomePlanUtils.ts @@ -8,7 +8,7 @@ import { LatestIncomePlan, WithholdingTax, } from '../types' -import { INCOME_PLANS_CLOSED } from './constants' +import { NO_ACTIVE_APPLICATIONS, INCOME_PLANS_CLOSED } from './constants' import { incomePlanFormMessage } from './messages' export const getApplicationExternalData = ( @@ -133,8 +133,9 @@ export const isEligible = (externalData: ExternalData): boolean => { export const eligibleText = (externalData: ExternalData) => { const { isEligible } = getApplicationExternalData(externalData) - return isEligible.reasonCode === INCOME_PLANS_CLOSED ? incomePlanFormMessage.pre.isNotEligibleClosedDescription + : isEligible.reasonCode === NO_ACTIVE_APPLICATIONS + ? incomePlanFormMessage.pre.isNotEligibleNoActiveApplicationDescription : incomePlanFormMessage.pre.isNotEligibleDescription } diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/lib/messages.ts b/libs/application/templates/social-insurance-administration/income-plan/src/lib/messages.ts index 1fd0c4c269c2..cc32fe4202c6 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/lib/messages.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/lib/messages.ts @@ -24,10 +24,17 @@ export const incomePlanFormMessage: MessageDir = { isNotEligibleDescription: { id: 'ip.application:is.not.eligible.description#markdown', defaultMessage: - '* Það eru innan við 10 dagar síðan síðasta tekjuáætlun þín var tekin í vinnslu hjá Tryggingastofnun.\n\nEf þú telur það ekki eiga við um þig, vinsamlegast hafið samband við [tr@tr.is](mailto:tr@tr.is)', + 'Ástæðan fyrir því er eftirfarandi:\n* Það eru innan við 10 dagar síðan síðasta tekjuáætlun þín var tekin í vinnslu hjá Tryggingastofnun.\n\nEf þú telur það ekki eiga við um þig, vinsamlegast hafið samband við [tr@tr.is](mailto:tr@tr.is)', description: '* It has been less than 10 days since your last income plan was processed by the Social Insurance Administration.\n\nIf you do not think that apply to you, please contact [tr@tr.is](mailto:tr @tr.is)', }, + isNotEligibleNoActiveApplicationDescription: { + id: 'ip.application:is.not.eligible.no.active.application.description#markdown', + defaultMessage: + 'Ástæðan fyrir því er eftirfarandi:\n* Þú ert ekki með virka umsókn hjá Tryggingastofnun.\n\nEf þú telur það ekki eiga við um þig, vinsamlegast hafið samband við [tr@tr.is](mailto:tr@tr.is)', + description: + 'The reason for this is the following:\n* You do not have any active applications at the Social Insurance Administration.\n\nIf you do not think that apply to you, please contact [tr@tr.is](mailto:tr @tr.is)', + }, isNotEligibleClosedDescription: { id: 'ip.application:is.not.eligible.closed.description#markdown', defaultMessage: From 487c5989194853ee7b05dd45eea319a4be0a1dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3hanna=20Magn=C3=BAsd=C3=B3ttir?= Date: Mon, 18 Nov 2024 14:14:48 +0000 Subject: [PATCH 19/34] fix(samgongustofa): update instructor - dont log unecessary errors (#16916) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/application/core/src/lib/messages.ts | 5 +++++ .../driving-license-book-update-instructor.service.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/libs/application/core/src/lib/messages.ts b/libs/application/core/src/lib/messages.ts index d39632da5fcf..bf55e326b06a 100644 --- a/libs/application/core/src/lib/messages.ts +++ b/libs/application/core/src/lib/messages.ts @@ -739,6 +739,11 @@ export const coreErrorMessages = defineMessages({ defaultMessage: 'Þú uppfyllir ekki skilyrði fyrir umsókn um nafnskírteini', description: 'Requirements for id card application not met', }, + drivingLicenseBookActiveBookNotFound: { + id: 'application.system:core.fetch.data.drivingLicenseBookActiveBookNotFound', + defaultMessage: 'Ekki fannst virk ökunámsbók', + description: 'Did not find active student book', + }, }) export const coreDelegationsMessages = defineMessages({ diff --git a/libs/application/template-api-modules/src/lib/modules/templates/driving-license-book-update-instructor/driving-license-book-update-instructor.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/driving-license-book-update-instructor/driving-license-book-update-instructor.service.ts index 39075c44bea1..b24b7fadd825 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/driving-license-book-update-instructor/driving-license-book-update-instructor.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/driving-license-book-update-instructor/driving-license-book-update-instructor.service.ts @@ -4,6 +4,8 @@ import { DrivingLicenseBookUpdateInstructorAnswers } from '@island.is/applicatio import { BaseTemplateApiService } from '../../base-template-api.service' import { ApplicationTypes } from '@island.is/application/types' import { DrivingLicenseBookClientApiFactory } from '@island.is/clients/driving-license-book' +import { TemplateApiError } from '@island.is/nest/problem' +import { coreErrorMessages } from '@island.is/application/core' @Injectable() export class DrivingLicenseBookUpdateInstructorService extends BaseTemplateApiService { @@ -21,7 +23,13 @@ export class DrivingLicenseBookUpdateInstructorService extends BaseTemplateApiSe ) if (!overview?.active) { - throw new Error('Did not find active student book') + throw new TemplateApiError( + { + title: coreErrorMessages.drivingLicenseBookActiveBookNotFound, + summary: coreErrorMessages.drivingLicenseBookActiveBookNotFound, + }, + 400, + ) } const teacherNationalId = overview.book?.teacherNationalId From 760b5fe26e97042d2b4918f508bac1a4a5def3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:33:32 +0000 Subject: [PATCH 20/34] feat(web): Outbound link tracking in Plausible (#16899) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/constants/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/constants/index.ts b/apps/web/constants/index.ts index c52757d262e3..09b99769622a 100644 --- a/apps/web/constants/index.ts +++ b/apps/web/constants/index.ts @@ -7,6 +7,7 @@ export const PROJECT_STORIES_TAG_ID = '9yqOTwQYzgyej5kItFTtd' export const ADGERDIR_INDIVIDUALS_TAG_ID = '4kLt3eRht5yJoakIHWsusb' export const ADGERDIR_COMPANIES_TAG_ID = '4ZWcwoW2IiB2AhtzQpzdIW' export const FRONTPAGE_NEWS_TAG_ID = 'forsidufrettir' -export const PLAUSIBLE_SCRIPT_SRC = 'https://plausible.io/js/plausible.js' +export const PLAUSIBLE_SCRIPT_SRC = + 'https://plausible.io/js/script.outbound-links.js' export const DIGITAL_ICELAND_PLAUSIBLE_TRACKING_DOMAIN = 'island.is/s/stafraent-island' From d33043ff959797b37e993ccb38a2e22b64a90680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigr=C3=BAn=20Tinna=20Gissurard=C3=B3ttir?= <39527334+sigruntg@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:23:31 +0000 Subject: [PATCH 21/34] fix(work-accident-notification): work accident bug fixes (#16922) * moving part of companyinfo to basicinfo to stop autofill * some fixes * extract strings * fix inputs being empty on back button click --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../application/core/src/lib/fieldBuilders.ts | 4 ++++ .../work-accident-notification.service.ts | 12 +++++------ .../work-accident-notification/project.json | 6 ++++++ .../src/fields/AccidentLocation/index.tsx | 19 +++++++++++++----- .../fields/EmployeeStartTimeError/index.tsx | 11 ++++++++-- .../AccidentSection/about.ts | 7 ++----- .../EmployeeSection/employee.ts | 16 +++------------ .../InformationSection/companySection.ts | 15 ++++++++------ .../prerequisitesSection.ts | 1 - .../src/lib/dataSchema.ts | 9 +++++++-- .../src/lib/messages/accident.ts | 6 +++--- .../utils/getCompanyInformationForOverview.ts | 20 ++++++++++++++----- .../src/utils/index.ts | 1 + libs/application/types/src/lib/Fields.ts | 2 ++ .../src/lib/DateFormField/DateFormField.tsx | 4 ++++ 15 files changed, 84 insertions(+), 49 deletions(-) diff --git a/libs/application/core/src/lib/fieldBuilders.ts b/libs/application/core/src/lib/fieldBuilders.ts index af82c47fc7f8..2f1a86f6a03c 100644 --- a/libs/application/core/src/lib/fieldBuilders.ts +++ b/libs/application/core/src/lib/fieldBuilders.ts @@ -118,6 +118,8 @@ export const buildDateField = ( const { maxDate, minDate, + minYear, + maxYear, excludeDates, placeholder, backgroundColor = 'blue', @@ -130,6 +132,8 @@ export const buildDateField = ( placeholder, maxDate, minDate, + minYear, + maxYear, excludeDates, type: FieldTypes.DATE, component: FieldComponents.DATE, diff --git a/libs/application/template-api-modules/src/lib/modules/templates/aosh/work-accident-notification/work-accident-notification.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/aosh/work-accident-notification/work-accident-notification.service.ts index 54839ce19a07..b078f12def46 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/aosh/work-accident-notification/work-accident-notification.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/aosh/work-accident-notification/work-accident-notification.service.ts @@ -12,10 +12,8 @@ import { } from '@island.is/clients/work-accident-ver' import { getDateAndTime, - getValueList, mapVictimData, } from './work-accident-notification.utils' -import { getValueViaPath } from '@island.is/application/core' import { TemplateApiError } from '@island.is/nest/problem' @Injectable() @@ -57,20 +55,20 @@ export class WorkAccidentNotificationTemplateService extends BaseTemplateApiServ const answers = application.answers as unknown as WorkAccidentNotification const payload = { accidentForCreationDto: { - companySSN: answers.companyInformation.nationalId, + companySSN: answers.basicInformation.nationalId, sizeOfEnterprise: parseInt( - answers.companyInformation.numberOfEmployees, + answers.basicInformation.numberOfEmployees, 10, ), nameOfBranchOrDepartment: answers.companyInformation.nameOfBranch ?? - answers.companyInformation.name, + answers.basicInformation.name, address: answers.companyInformation.addressOfBranch ?? - answers.companyInformation.address, + answers.basicInformation.address, postcode: answers.companyInformation.postnumberOfBranch?.slice(0, 3) ?? - answers.companyInformation.postnumber.slice(0, 3), + answers.basicInformation.postnumber.slice(0, 3), workplaceHealthAndSafety: answers.companyLaborProtection.workhealthAndSafetyOccupation?.map( (code: string) => { diff --git a/libs/application/templates/aosh/work-accident-notification/project.json b/libs/application/templates/aosh/work-accident-notification/project.json index ab889cfaf462..a2309411789d 100644 --- a/libs/application/templates/aosh/work-accident-notification/project.json +++ b/libs/application/templates/aosh/work-accident-notification/project.json @@ -7,6 +7,12 @@ "targets": { "lint": { "executor": "@nx/eslint:lint" + }, + "extract-strings": { + "executor": "nx:run-commands", + "options": { + "command": "yarn ts-node -P libs/localization/tsconfig.lib.json libs/localization/scripts/extract 'libs/application/templates/aosh/work-accident-notification/src/**/*.{js,ts,tsx}'" + } } } } diff --git a/libs/application/templates/aosh/work-accident-notification/src/fields/AccidentLocation/index.tsx b/libs/application/templates/aosh/work-accident-notification/src/fields/AccidentLocation/index.tsx index df31e4cfa436..69426791673e 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/fields/AccidentLocation/index.tsx +++ b/libs/application/templates/aosh/work-accident-notification/src/fields/AccidentLocation/index.tsx @@ -42,7 +42,15 @@ export const AccidentLocation: FC> = ( ) const [minorGroupOptions, setMinorGroupOptions] = useState< WorkingEnvironmentDto[] - >([]) + >( + selectedMajorGroup + ? minorGroups.filter( + (group) => + group.code?.substring(0, 2) === + selectedMajorGroup?.value?.substring(0, 2), + ) + : [], + ) return ( @@ -56,7 +64,7 @@ export const AccidentLocation: FC> = ( label={formatMessage( accident.about.locationOfAccidentMajorGroup, )} - name="subMajorGroupSelect" + name="accident.accidentLocationParentGroup" options={majorGroups.map((option) => ({ label: option.name || '', value: option.code, @@ -80,7 +88,7 @@ export const AccidentLocation: FC> = ( /> ) }} - name={'subMajorGroup'} + name={'accident.accidentLocationParentGroup'} /> @@ -92,11 +100,12 @@ export const AccidentLocation: FC> = ( label={formatMessage( accident.about.locationOfAccidentMinorGroup, )} - name="subMajorGroupSelect" + name="accident.accidentLocation" options={minorGroupOptions.map((group) => ({ label: group.name || '', value: group.code, }))} + isDisabled={!selectedMajorGroup} value={selectedMinorGroup} backgroundColor="blue" onChange={(v) => { @@ -113,7 +122,7 @@ export const AccidentLocation: FC> = ( /> ) }} - name={'subMajorGroup'} + name={'accident.accidentLocation'} /> diff --git a/libs/application/templates/aosh/work-accident-notification/src/fields/EmployeeStartTimeError/index.tsx b/libs/application/templates/aosh/work-accident-notification/src/fields/EmployeeStartTimeError/index.tsx index 03bcd17321be..2d9bfe513c0e 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/fields/EmployeeStartTimeError/index.tsx +++ b/libs/application/templates/aosh/work-accident-notification/src/fields/EmployeeStartTimeError/index.tsx @@ -5,7 +5,8 @@ import { useFormContext } from 'react-hook-form' import { employee } from '../../lib/messages' import { useLocale } from '@island.is/localization' import { WorkAccidentNotification } from '../../lib/dataSchema' -import { dateIsWithin36Hours } from '../../utils' +import { dateIsWithin36Hours, formatDate } from '../../utils' +import { getValueViaPath } from '@island.is/application/core' interface EmployeeStartTimeErrorProps { field: { @@ -23,6 +24,10 @@ export const EmployeeStartTimeError: FC< const { getValues } = useFormContext() const { formatMessage } = useLocale() const [inputError, setInputError] = useState(false) + const startDate = + getValueViaPath(application.answers, 'accident.date') ?? '' + const startTime = + getValueViaPath(application.answers, 'accident.time') ?? '' setBeforeSubmitCallback?.(async () => { const values = getValues('employee') @@ -48,7 +53,9 @@ export const EmployeeStartTimeError: FC< {inputError && ( - {formatMessage(employee.employee.errorMessage)} + {`${formatMessage(employee.employee.errorMessage)}, ${formatDate( + startDate, + )} ${startTime.slice(0, 2)}:${startTime.slice(2, 4)}`} )} diff --git a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/AccidentSection/about.ts b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/AccidentSection/about.ts index 96e389a19054..7c583e68c242 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/AccidentSection/about.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/AccidentSection/about.ts @@ -3,7 +3,6 @@ import { buildCustomField, buildDateField, buildDescriptionField, - buildHiddenInput, buildMultiField, buildSelectField, buildSubSection, @@ -34,6 +33,7 @@ export const aboutSection = buildSubSection({ width: 'half', required: true, minDate: new Date('1.1.2020'), + maxDate: new Date(), }), buildTextField({ id: 'accident.time', @@ -155,11 +155,8 @@ export const aboutSection = buildSubSection({ title: accident.about.locationOfAccidentHeading, marginTop: 3, }), - buildHiddenInput({ - id: 'accident.accidentLocationParentGroup', - }), buildCustomField({ - id: 'accident.accidentLocation', + id: 'accident', title: '', component: 'AccidentLocation', }), diff --git a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/EmployeeSection/employee.ts b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/EmployeeSection/employee.ts index bb0fa88d47a3..afb361691175 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/EmployeeSection/employee.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/EmployeeSection/employee.ts @@ -3,7 +3,6 @@ import { buildCustomField, buildDateField, buildDescriptionField, - buildHiddenInput, buildMultiField, buildNationalIdWithNameField, buildSelectField, @@ -118,6 +117,9 @@ export const employeeSubSection = (index: number) => width: 'half', required: true, title: employee.employee.startDate, + maxDate: new Date(), + minYear: 1940, + maxYear: new Date().getFullYear(), }), buildSelectField({ id: `employee[${index}].employmentTime`, @@ -235,18 +237,6 @@ export const employeeSubSection = (index: number) => titleVariant: 'h5', marginTop: 3, }), - buildHiddenInput({ - id: `employee[${index}].victimsOccupationMajor`, - }), - buildHiddenInput({ - id: `employee[${index}].victimsOccupationSubMajor`, - }), - buildHiddenInput({ - id: `employee[${index}].victimsOccupationMinor`, - }), - buildHiddenInput({ - id: `employee[${index}].victimsOccupationUnit`, - }), buildCustomField( { id: `employee[${index}].victimsOccupation`, diff --git a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/InformationSection/companySection.ts b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/InformationSection/companySection.ts index 2575705bba0f..99818937fb19 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/InformationSection/companySection.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/InformationSection/companySection.ts @@ -38,12 +38,12 @@ export const companySection = buildSubSection({ }, }), buildDescriptionField({ - id: 'companyInformation.description', + id: 'basicInformation.description', title: information.labels.company.descriptionField, titleVariant: 'h5', }), buildTextField({ - id: 'companyInformation.nationalId', + id: 'basicInformation.nationalId', title: information.labels.company.nationalId, backgroundColor: 'white', width: 'half', @@ -59,10 +59,11 @@ export const companySection = buildSubSection({ }, }), buildTextField({ - id: 'companyInformation.name', + id: 'basicInformation.name', title: information.labels.company.name, backgroundColor: 'white', width: 'half', + readOnly: true, defaultValue: (application: Application) => { const name = getValueViaPath( application.externalData, @@ -73,10 +74,11 @@ export const companySection = buildSubSection({ }, }), buildTextField({ - id: 'companyInformation.address', + id: 'basicInformation.address', title: information.labels.company.address, backgroundColor: 'white', width: 'half', + readOnly: true, defaultValue: (application: Application) => { const streetAddress = getValueViaPath( application.externalData, @@ -87,10 +89,11 @@ export const companySection = buildSubSection({ }, }), buildTextField({ - id: 'companyInformation.postnumber', + id: 'basicInformation.postnumber', title: information.labels.company.postNumberAndTown, backgroundColor: 'white', width: 'half', + readOnly: true, defaultValue: (application: Application) => { const postalCode = getValueViaPath( application.externalData, @@ -105,7 +108,7 @@ export const companySection = buildSubSection({ }, }), buildSelectField({ - id: 'companyInformation.numberOfEmployees', + id: 'basicInformation.numberOfEmployees', title: information.labels.company.numberOfEmployees, width: 'half', required: true, diff --git a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/prerequisitesSection.ts b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/prerequisitesSection.ts index 11a5028f4ece..f7ef164ff598 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/prerequisitesSection.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/prerequisitesSection.ts @@ -4,7 +4,6 @@ import { buildDataProviderItem, buildSubmitField, coreMessages, - buildHiddenInput, } from '@island.is/application/core' import { externalData } from '../../lib/messages' import { diff --git a/libs/application/templates/aosh/work-accident-notification/src/lib/dataSchema.ts b/libs/application/templates/aosh/work-accident-notification/src/lib/dataSchema.ts index f2c032533d83..34fd239f7894 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/lib/dataSchema.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/lib/dataSchema.ts @@ -27,7 +27,7 @@ const accidentSchema = z.object({ wentWrong: z.string().min(1).max(499), }) -const companySchema = z.object({ +const basicCompanySchema = z.object({ nationalId: z .string() .refine( @@ -35,10 +35,13 @@ const companySchema = z.object({ nationalId && nationalId.length !== 0 && kennitala.isValid(nationalId), ), address: z.string(), - addressOfBranch: z.string().optional(), name: z.string(), numberOfEmployees: z.string(), postnumber: z.string(), +}) + +const companySchema = z.object({ + addressOfBranch: z.string().optional(), nameOfBranch: z.string().optional(), // VER needs to confirm requirement here for individuals vs company postnumberOfBranch: z.string().optional(), industryClassification: z.string().optional(), @@ -191,6 +194,7 @@ const projectPurchaseSchema = z export const WorkAccidentNotificationAnswersSchema = z.object({ approveExternalData: z.boolean().refine((v) => v), + basicInformation: basicCompanySchema, companyInformation: companySchema, companyLaborProtection: companyLaborProtectionSchema, accident: accidentSchema, @@ -208,6 +212,7 @@ export const WorkAccidentNotificationAnswersSchema = z.object({ export type WorkAccidentNotification = z.TypeOf< typeof WorkAccidentNotificationAnswersSchema > +export type BasicCompanyType = z.TypeOf export type CompanyType = z.TypeOf export type CompanyLaborProtectionType = z.TypeOf< typeof companyLaborProtectionSchema diff --git a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/accident.ts b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/accident.ts index 2b54f6e6b49b..185bde25b75e 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/accident.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/accident.ts @@ -84,7 +84,7 @@ export const accident = { wasDoingPlaceholder: { id: 'aosh.wan.application:accident.about.wasDoingPlaceholder#markdown', defaultMessage: - 'Tilgreinið hvaða verkfæri eða vélar voru notaðar.\nDæmi:\n - Vann með handborvél\n - Vann með handborvél', + 'Tilgreinið hvaða verkfæri eða vélar voru notaðar.\nDæmi:\n - Vann með handborvél\n - Var að styðja sjúkling á leið til baðherbergis', description: 'Placeholder of wasDoing text area', }, wentWrongTitle: { @@ -95,7 +95,7 @@ export const accident = { wenWrongPlaceholder: { id: 'aosh.wan.application:accident.about.wenWrongPlaceholder#markdown', defaultMessage: - 'Tilgreinið hvaða verkfæri eða vélar voru notaðar.\nDæmi:\n - Vann með handborvél\n - Vann með handborvél', + 'Tilgreinið hvaða verkfæri, vélar eða byrði áttu þátt í því.\nDæmi:\n - Borinn brotnaði í efninu\n - Sjúklingurinn gat skyndilega ekki staðið á fótunum', description: 'Placeholder of wentWrong text area', }, howTitle: { @@ -106,7 +106,7 @@ export const accident = { howPlaceholder: { id: 'aosh.wan.application:accident.about.howPlaceholder#markdown', defaultMessage: - 'Tilgreinið hvaða verkfæri eða vélar voru notaðar.\nDæmi:\n - Vann með handborvél\n - Vann með handborvél', + 'Tilgreinið hvaða verkfæri, vélar eða byrði var orsök slyssins.\nDæmi:\n - Borinn lenti í hendinni\n - Við að reyna að halda sjúklingnum uppi hrasaði slasaði og fékk hnykk á bakið', description: 'Placeholder of how text area', }, locationOfAccidentHeading: { diff --git a/libs/application/templates/aosh/work-accident-notification/src/utils/getCompanyInformationForOverview.ts b/libs/application/templates/aosh/work-accident-notification/src/utils/getCompanyInformationForOverview.ts index 81a8637ffe61..bdc845fa82e8 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/utils/getCompanyInformationForOverview.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/utils/getCompanyInformationForOverview.ts @@ -6,7 +6,11 @@ import { } from '@island.is/application/types' import { information, overview } from '../lib/messages' import { format as formatKennitala } from 'kennitala' -import { CompanyLaborProtectionType, CompanyType } from '../lib/dataSchema' +import { + BasicCompanyType, + CompanyLaborProtectionType, + CompanyType, +} from '../lib/dataSchema' import { SizeOfTheEnterpriseDto, WorkplaceHealthAndSafetyDto, @@ -18,6 +22,10 @@ export const getCompanyInformationForOverview = ( externalData: ExternalData, formatMessage: FormatMessage, ) => { + const basicCompany = getValueViaPath( + answers, + 'basicInformation', + ) const company = getValueViaPath(answers, 'companyInformation') const companyLaborProtection = getValueViaPath( answers, @@ -34,13 +42,15 @@ export const getCompanyInformationForOverview = ( 'aoshData.data.workplaceHealthAndSafety', ) ?? [] const chosenSizeOfEnterprise = sizeOfEnterprises.find( - (size) => company?.numberOfEmployees === size?.code, + (size) => basicCompany?.numberOfEmployees === size?.code, ) return [ - company?.name ?? undefined, - company?.nationalId ? formatKennitala(company.nationalId) : undefined, - `${company?.address ?? ''}, ${company?.postnumber ?? ''}`, + basicCompany?.name ?? undefined, + basicCompany?.nationalId + ? formatKennitala(basicCompany.nationalId) + : undefined, + `${basicCompany?.address ?? ''}, ${basicCompany?.postnumber ?? ''}`, company?.industryClassification ?? undefined, chosenSizeOfEnterprise?.name ?? undefined, `${formatMessage(information.labels.workhealth.sectionTitle)}: ${ diff --git a/libs/application/templates/aosh/work-accident-notification/src/utils/index.ts b/libs/application/templates/aosh/work-accident-notification/src/utils/index.ts index e3e0d34b45e6..8dd880060ee4 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/utils/index.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/utils/index.ts @@ -9,3 +9,4 @@ export { isValidPhoneNumber, } from './dateManipulation' export { isCompany } from './isCompany' +export { formatDate } from './formatDate' diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index e4bab487a79d..138b02bce010 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -313,6 +313,8 @@ export interface DateField extends InputField { component: FieldComponents.DATE maxDate?: MaybeWithApplicationAndField minDate?: MaybeWithApplicationAndField + minYear?: number + maxYear?: number excludeDates?: MaybeWithApplicationAndField backgroundColor?: DatePickerBackgroundColor onChange?(date: string): void diff --git a/libs/application/ui-fields/src/lib/DateFormField/DateFormField.tsx b/libs/application/ui-fields/src/lib/DateFormField/DateFormField.tsx index faeb76a86d30..3b90993c051d 100644 --- a/libs/application/ui-fields/src/lib/DateFormField/DateFormField.tsx +++ b/libs/application/ui-fields/src/lib/DateFormField/DateFormField.tsx @@ -41,6 +41,8 @@ export const DateFormField: FC> = ({ excludeDates, minDate, maxDate, + minYear, + maxYear, onChange, readOnly, } = field @@ -139,6 +141,8 @@ export const DateFormField: FC> = ({ excludeDates={finalExcludeDates} minDate={finalMinDate} maxDate={finalMaxDate} + minYear={minYear} + maxYear={maxYear} backgroundColor={backgroundColor} readOnly={readOnly} label={formatTextWithLocale(title, application, lang, formatMessage)} From a8e03f5c275b6d708d6a6d4615b0150bd10f0078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Mon, 18 Nov 2024 16:21:07 +0000 Subject: [PATCH 22/34] fix(j-s): String Length Validation (#16924) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/app/modules/case/dto/createCase.dto.ts | 8 ++++++++ .../src/app/modules/case/dto/updateCase.dto.ts | 13 +++++++++++++ .../modules/defendant/dto/createDefendant.dto.ts | 16 +++++++++++++++- .../defendant/dto/updateCivilClaimant.dto.ts | 8 +++++++- .../modules/defendant/dto/updateDefendant.dto.ts | 11 +++++++++++ .../src/app/modules/file/dto/createFile.dto.ts | 6 ++++++ .../modules/file/dto/createPresignedPost.dto.ts | 4 +++- .../src/app/modules/file/dto/updateFile.dto.ts | 2 ++ .../dto/updateIndictmentCount.dto.ts | 3 +++ .../modules/subpoena/dto/updateSubpoena.dto.ts | 9 ++++++++- .../src/app/modules/user/dto/createUser.dto.ts | 6 ++++++ .../src/app/modules/user/dto/updateUser.dto.ts | 5 +++++ .../InvestigationCase/Defendant/Defendant.tsx | 1 + 13 files changed, 88 insertions(+), 4 deletions(-) diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts index f3f920723a31..32bf0f7589d1 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts @@ -6,6 +6,7 @@ import { IsObject, IsOptional, IsString, + MaxLength, } from 'class-validator' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' @@ -31,6 +32,7 @@ export class CreateCaseDto { readonly indictmentSubtypes?: IndictmentSubtypeMap @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly description?: string @@ -38,26 +40,31 @@ export class CreateCaseDto { @IsNotEmpty() @IsArray() @ArrayMinSize(1) + @MaxLength(255) @IsString({ each: true }) @ApiProperty({ type: String, isArray: true }) readonly policeCaseNumbers!: string[] @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string @@ -68,6 +75,7 @@ export class CreateCaseDto { readonly requestSharedWithDefender?: RequestSharedWithDefender @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly leadInvestigator?: string diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts index 4930b0b976bb..a34a8a14d219 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts @@ -9,6 +9,7 @@ import { IsOptional, IsString, IsUUID, + MaxLength, ValidateNested, } from 'class-validator' @@ -43,6 +44,7 @@ class UpdateDateLog { readonly date?: Date @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly location?: string @@ -60,6 +62,7 @@ export class UpdateCaseDto { readonly indictmentSubtypes?: IndictmentSubtypeMap @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly description?: string @@ -67,26 +70,31 @@ export class UpdateCaseDto { @IsOptional() @IsArray() @ArrayMinSize(1) + @MaxLength(255) @IsString({ each: true }) @ApiPropertyOptional({ type: String, isArray: true }) readonly policeCaseNumbers?: string[] @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string @@ -107,6 +115,7 @@ export class UpdateCaseDto { readonly courtId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly leadInvestigator?: string @@ -124,6 +133,7 @@ export class UpdateCaseDto { readonly requestedCourtDate?: Date @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly translator?: string @@ -207,6 +217,7 @@ export class UpdateCaseDto { readonly sharedWithProsecutorsOfficeId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly courtCaseNumber?: string @@ -229,6 +240,7 @@ export class UpdateCaseDto { readonly courtDate?: UpdateDateLog @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly courtLocation?: string @@ -407,6 +419,7 @@ export class UpdateCaseDto { readonly defendantStatementDate?: Date @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly appealCaseNumber?: string diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/createDefendant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/createDefendant.dto.ts index eee5ea58da99..9c32a38660c7 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/createDefendant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/createDefendant.dto.ts @@ -1,4 +1,10 @@ -import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' +import { + IsBoolean, + IsEnum, + IsOptional, + IsString, + MaxLength, +} from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -11,11 +17,13 @@ export class CreateDefendantDto { readonly noNationalId?: boolean @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly nationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly name?: string @@ -26,31 +34,37 @@ export class CreateDefendantDto { readonly gender?: Gender @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly address?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly citizenship?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateCivilClaimant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateCivilClaimant.dto.ts index 3f85ec624a7f..44bcbed23e49 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateCivilClaimant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateCivilClaimant.dto.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsOptional, IsString } from 'class-validator' +import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -9,11 +9,13 @@ export class UpdateCivilClaimantDto { readonly noNationalId?: boolean @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly name?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly nationalId?: string @@ -29,21 +31,25 @@ export class UpdateCivilClaimantDto { readonly spokespersonIsLawyer?: boolean @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly spokespersonNationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly spokespersonName?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly spokespersonEmail?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly spokespersonPhoneNumber?: string diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts index 8e9321fbf730..c59f91dcaa0d 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts @@ -5,6 +5,7 @@ import { IsEnum, IsOptional, IsString, + MaxLength, } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -24,11 +25,13 @@ export class UpdateDefendantDto { readonly noNationalId?: boolean @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly nationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly name?: string @@ -39,31 +42,37 @@ export class UpdateDefendantDto { readonly gender?: Gender @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly address?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly citizenship?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string @@ -106,11 +115,13 @@ export class UpdateDefendantDto { readonly requestedDefenderChoice?: DefenderChoice @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly requestedDefenderNationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly requestedDefenderName?: string diff --git a/apps/judicial-system/backend/src/app/modules/file/dto/createFile.dto.ts b/apps/judicial-system/backend/src/app/modules/file/dto/createFile.dto.ts index de31caf5e46b..b17cfaaacb6c 100644 --- a/apps/judicial-system/backend/src/app/modules/file/dto/createFile.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/file/dto/createFile.dto.ts @@ -6,6 +6,7 @@ import { IsNumber, IsOptional, IsString, + MaxLength, } from 'class-validator' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' @@ -14,6 +15,7 @@ import { CaseFileCategory } from '@island.is/judicial-system/types' export class CreateFileDto { @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly type!: string @@ -24,6 +26,7 @@ export class CreateFileDto { readonly category?: CaseFileCategory @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly key!: string @@ -34,6 +37,7 @@ export class CreateFileDto { readonly size!: number @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly policeCaseNumber?: string @@ -55,11 +59,13 @@ export class CreateFileDto { readonly displayDate?: Date @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly policeFileId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly userGeneratedFilename?: string diff --git a/apps/judicial-system/backend/src/app/modules/file/dto/createPresignedPost.dto.ts b/apps/judicial-system/backend/src/app/modules/file/dto/createPresignedPost.dto.ts index 1dc58cf5f6db..e9bdeb9d4a7a 100644 --- a/apps/judicial-system/backend/src/app/modules/file/dto/createPresignedPost.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/file/dto/createPresignedPost.dto.ts @@ -1,14 +1,16 @@ -import { IsNotEmpty, IsString } from 'class-validator' +import { IsNotEmpty, IsString, MaxLength } from 'class-validator' import { ApiProperty } from '@nestjs/swagger' export class CreatePresignedPostDto { @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly fileName!: string @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly type!: string diff --git a/apps/judicial-system/backend/src/app/modules/file/dto/updateFile.dto.ts b/apps/judicial-system/backend/src/app/modules/file/dto/updateFile.dto.ts index f645b71bf9bb..a769eeda2ddb 100644 --- a/apps/judicial-system/backend/src/app/modules/file/dto/updateFile.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/file/dto/updateFile.dto.ts @@ -7,6 +7,7 @@ import { IsOptional, IsString, IsUUID, + MaxLength, Min, ValidateIf, ValidateNested, @@ -21,6 +22,7 @@ export class UpdateFileDto { readonly id!: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly userGeneratedFilename?: string diff --git a/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts b/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts index 103ce2051ea3..b1226cdc54e0 100644 --- a/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts @@ -4,6 +4,7 @@ import { IsObject, IsOptional, IsString, + MaxLength, } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -13,11 +14,13 @@ import { IndictmentCountOffense } from '@island.is/judicial-system/types' export class UpdateIndictmentCountDto { @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly policeCaseNumber?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly vehicleRegistrationNumber?: string diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts b/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts index 36a14e1be0ef..89e83477f4a2 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' +import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -11,6 +11,7 @@ export class UpdateSubpoenaDto { readonly serviceStatus?: ServiceStatus @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly servedBy?: string @@ -31,21 +32,25 @@ export class UpdateSubpoenaDto { readonly defenderChoice?: DefenderChoice @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderNationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderName?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderEmail?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly defenderPhoneNumber?: string @@ -56,11 +61,13 @@ export class UpdateSubpoenaDto { readonly requestedDefenderChoice?: DefenderChoice @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly requestedDefenderNationalId?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly requestedDefenderName?: string diff --git a/apps/judicial-system/backend/src/app/modules/user/dto/createUser.dto.ts b/apps/judicial-system/backend/src/app/modules/user/dto/createUser.dto.ts index 1460431b37f7..d6b8207fba4d 100644 --- a/apps/judicial-system/backend/src/app/modules/user/dto/createUser.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/user/dto/createUser.dto.ts @@ -4,6 +4,7 @@ import { IsNotEmpty, IsString, IsUUID, + MaxLength, } from 'class-validator' import { ApiProperty } from '@nestjs/swagger' @@ -12,26 +13,31 @@ import { UserRole } from '@island.is/judicial-system/types' export class CreateUserDto { @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly nationalId!: string @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly name!: string @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly title!: string @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly mobileNumber!: string @IsNotEmpty() + @MaxLength(255) @IsString() @ApiProperty({ type: String }) readonly email!: string diff --git a/apps/judicial-system/backend/src/app/modules/user/dto/updateUser.dto.ts b/apps/judicial-system/backend/src/app/modules/user/dto/updateUser.dto.ts index fcebedbf7405..071bd218261b 100644 --- a/apps/judicial-system/backend/src/app/modules/user/dto/updateUser.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/user/dto/updateUser.dto.ts @@ -4,6 +4,7 @@ import { IsOptional, IsString, IsUUID, + MaxLength, } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -12,21 +13,25 @@ import { UserRole } from '@island.is/judicial-system/types' export class UpdateUserDto { @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly name?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly title?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly mobileNumber?: string @IsOptional() + @MaxLength(255) @IsString() @ApiPropertyOptional({ type: String }) readonly email?: string diff --git a/apps/judicial-system/web/src/routes/Prosecutor/InvestigationCase/Defendant/Defendant.tsx b/apps/judicial-system/web/src/routes/Prosecutor/InvestigationCase/Defendant/Defendant.tsx index 58d28cb6146b..2470cb389ec4 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/InvestigationCase/Defendant/Defendant.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/InvestigationCase/Defendant/Defendant.tsx @@ -336,6 +336,7 @@ const Defendant = () => { setWorkingCase, ) } + maxLength={255} /> From 6e102a02cfa4bffff2b292b2fc553f1a28155eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:48:47 +0000 Subject: [PATCH 23/34] chore(web): Move organization subpage content into separate component (#16915) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/screens/Organization/SubPage.tsx | 285 ++++++++++++---------- 1 file changed, 156 insertions(+), 129 deletions(-) diff --git a/apps/web/screens/Organization/SubPage.tsx b/apps/web/screens/Organization/SubPage.tsx index 0db2c57fee70..b9a118306519 100644 --- a/apps/web/screens/Organization/SubPage.tsx +++ b/apps/web/screens/Organization/SubPage.tsx @@ -1,4 +1,3 @@ -import { Locale } from '@island.is/shared/types' import { useRouter } from 'next/router' import { ParsedUrlQuery } from 'querystring' @@ -14,6 +13,7 @@ import { Stack, Text, } from '@island.is/island-ui/core' +import { Locale } from '@island.is/shared/types' import { getThemeConfig, OrganizationWrapper, @@ -60,46 +60,13 @@ export interface SubPageProps { customContentfulIds?: (string | undefined)[] } -const SubPage: Screen = ({ - organizationPage, +const SubPageContent = ({ subpage, namespace, - locale, - customContent, - customBreadcrumbItems, - customContentfulIds, - backLink, -}) => { - const router = useRouter() - const { activeLocale } = useI18n() - + organizationPage, +}: Pick) => { const n = useNamespace(namespace) - const { linkResolver } = useLinkResolver() - - const contentfulIds = customContentfulIds - ? customContentfulIds - : [organizationPage?.id, subpage?.id] - - useContentfulId(...contentfulIds) - - const pathname = new URL(router.asPath, 'https://island.is').pathname - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict - const navList: NavigationItem[] = organizationPage?.menuLinks.map( - ({ primaryLink, childrenLinks }) => ({ - title: primaryLink?.text, - href: primaryLink?.url, - active: - primaryLink?.url === pathname || - childrenLinks.some((link) => link.url === pathname), - items: childrenLinks.map(({ text, url }) => ({ - title: text, - href: url, - active: url === pathname, - })), - }), - ) - + const { activeLocale } = useI18n() const content = ( <> {subpage?.showTableOfContents && ( @@ -147,45 +114,8 @@ const SubPage: Screen = ({ ) - return ( - + <> @@ -199,62 +129,58 @@ const SubPage: Screen = ({ subpage?.links?.length ? '7/12' : '12/12', ]} > - {customContent ? ( - customContent - ) : ( - <> - - - {subpage?.title} - - - - + + + {subpage?.title} + + + + + {subpage?.signLanguageVideo?.url && ( + + + {subpage.title} + + {content} + {renderSlices( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore make web strict + subpage.slices, + subpage.sliceCustomRenderer, + subpage.sliceExtraText, + namespace, + organizationPage?.slug, + )} + + } /> - {subpage?.signLanguageVideo?.url && ( - - - {subpage.title} - - {content} - {renderSlices( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict - subpage.slices, - subpage.sliceCustomRenderer, - subpage.sliceExtraText, - namespace, - organizationPage?.slug, - )} - - } - /> - )} - - - )} + )} + + - {!customContent && content} + {content} @@ -277,6 +203,107 @@ const SubPage: Screen = ({ organizationPage.slug, )} + + ) +} + +const SubPage: Screen = ({ + organizationPage, + subpage, + namespace, + locale, + customContent, + customBreadcrumbItems, + customContentfulIds, + backLink, +}) => { + const router = useRouter() + + const n = useNamespace(namespace) + const { linkResolver } = useLinkResolver() + + const contentfulIds = customContentfulIds + ? customContentfulIds + : [organizationPage?.id, subpage?.id] + + useContentfulId(...contentfulIds) + + const pathname = new URL(router.asPath, 'https://island.is').pathname + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore make web strict + const navList: NavigationItem[] = organizationPage?.menuLinks.map( + ({ primaryLink, childrenLinks }) => ({ + title: primaryLink?.text, + href: primaryLink?.url, + active: + primaryLink?.url === pathname || + childrenLinks.some((link) => link.url === pathname), + items: childrenLinks.map(({ text, url }) => ({ + title: text, + href: url, + active: url === pathname, + })), + }), + ) + + return ( + + {customContent ? ( + + + + + {customContent} + + + + + ) : ( + + )} ) } From a2b25ec5e9e127cae18612134d49e8590a9444bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigr=C3=BAn=20Tinna=20Gissurard=C3=B3ttir?= <39527334+sigruntg@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:20:38 +0000 Subject: [PATCH 24/34] fix(work-accident-notification): alert message (#16925) * moving part of companyinfo to basicinfo to stop autofill * some fixes * extract strings * fix inputs being empty on back button click * adding alert --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../EmployeeSection/employee.ts | 7 +++++++ .../src/lib/messages/employee.ts | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/EmployeeSection/employee.ts b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/EmployeeSection/employee.ts index afb361691175..5ac5d07f705b 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/EmployeeSection/employee.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/forms/WorkAccidentNotificationForm/EmployeeSection/employee.ts @@ -166,6 +166,13 @@ export const employeeSubSection = (index: number) => })) }, }), + buildAlertMessageField({ + id: 'employee.startTimeAlert', + title: '', + message: employee.employee.startTimeAlert, + alertType: 'info', + marginBottom: 0, + }), buildDateField({ id: `employee[${index}].startOfWorkdayDate`, width: 'half', diff --git a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/employee.ts b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/employee.ts index dad48b068ec9..48156be856d6 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/employee.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/employee.ts @@ -80,12 +80,12 @@ export const employee = { }, startOfWorkdayDate: { id: 'aosh.wan.application:employee.time', - defaultMessage: 'Dagsetning byrjun starfsdags', + defaultMessage: 'Dagsetning', description: 'Start of employees workday date', }, time: { id: 'aosh.wan.application:employee.time', - defaultMessage: 'Tími byrjun starfsdags', + defaultMessage: 'Tími', description: 'Start of employees workday time', }, timePlaceholder: { @@ -155,5 +155,11 @@ export const employee = { 'Starfsdagur má byrja allt að 36 tímum fyrir slys og að tímasetningu slyss', description: 'Error message for employee start time', }, + startTimeAlert: { + id: 'aosh.wan.application:employee.startTimeAlert', + defaultMessage: + 'Dagsetning og tími þegar starfsmaður mætti til vinnu á slysadegi', + description: 'Error message for employee start time', + }, }), } From b3e05fdf3beed56056bedfc26bbd9e7fbb550e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dvar=20Oddsson?= Date: Tue, 19 Nov 2024 09:04:12 +0000 Subject: [PATCH 25/34] fix(j-s): Validation fix (#16930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Checkpoint * Refactor AlertMessage * Format date * Cleanup * Cleanup * Merge * Add key * Refactor * Remove console.log * Merge * Merge * Fix e2e * Resolve merge conflict --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Guðjón Guðjónsson --- .../backend/src/app/modules/case/dto/createCase.dto.ts | 2 +- .../backend/src/app/modules/case/dto/updateCase.dto.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts index 32bf0f7589d1..a54904d19aab 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/createCase.dto.ts @@ -40,7 +40,7 @@ export class CreateCaseDto { @IsNotEmpty() @IsArray() @ArrayMinSize(1) - @MaxLength(255) + @MaxLength(255, { each: true }) @IsString({ each: true }) @ApiProperty({ type: String, isArray: true }) readonly policeCaseNumbers!: string[] diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts index a34a8a14d219..6eec48f22a86 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts @@ -70,7 +70,7 @@ export class UpdateCaseDto { @IsOptional() @IsArray() @ArrayMinSize(1) - @MaxLength(255) + @MaxLength(255, { each: true }) @IsString({ each: true }) @ApiPropertyOptional({ type: String, isArray: true }) readonly policeCaseNumbers?: string[] From d743d2e0323e95dcc54302c85d81afdc6f951b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafn=20=C3=81rnason?= Date: Tue, 19 Nov 2024 10:20:21 +0000 Subject: [PATCH 26/34] fix(endorsment-system): Add assets to project (#16923) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/services/endorsements/api/project.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/services/endorsements/api/project.json b/apps/services/endorsements/api/project.json index 22c4fe386761..6298812ea242 100644 --- a/apps/services/endorsements/api/project.json +++ b/apps/services/endorsements/api/project.json @@ -33,6 +33,11 @@ "glob": "*", "input": "libs/email-service/src/tools/design", "output": "./email-service-assets" + }, + { + "glob": "*", + "input": "apps/services/endorsements/api/src/assets", + "output": "apps/services/endorsements/api/src/assets" } ] }, From 910627cb5748c8755377c2e9d9a596ed5f69eb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rarinn=20Gunnar=20=C3=81rnason?= Date: Tue, 19 Nov 2024 10:30:25 +0000 Subject: [PATCH 27/34] fix(application-system): TableRepeater cancel option (#16929) * cancel active item functionality * setup default row and header support for nationalidWithname --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/application/core/src/lib/messages.ts | 15 +++++++ libs/application/types/src/lib/Fields.ts | 1 + .../TableRepeaterFormField.tsx | 42 ++++++++++++++----- .../src/lib/TableRepeaterFormField/utils.ts | 25 ++++++++++- 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/libs/application/core/src/lib/messages.ts b/libs/application/core/src/lib/messages.ts index bf55e326b06a..3190a3542b1a 100644 --- a/libs/application/core/src/lib/messages.ts +++ b/libs/application/core/src/lib/messages.ts @@ -41,6 +41,11 @@ export const coreMessages = defineMessages({ defaultMessage: 'Bæta við', description: 'Add button', }, + buttonCancel: { + id: 'application.system:button.cancel', + defaultMessage: 'Hætta við', + description: 'Cancel button', + }, cardButtonInProgress: { id: 'application.system:card.button.inProgress', defaultMessage: 'Opna umsókn', @@ -136,6 +141,16 @@ export const coreMessages = defineMessages({ defaultMessage: 'Ekki tókst að búa til umsókn af gerðinni: {type}', description: 'Failed to create application of type: {type}', }, + nationalId: { + id: 'application.system:nationalId', + defaultMessage: 'Kennitala', + description: 'National ID', + }, + name: { + id: 'application.system:name', + defaultMessage: 'Nafn', + description: 'Name', + }, applications: { id: 'application.system:applications', defaultMessage: 'Þínar umsóknir', diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index 138b02bce010..19ed08cafa1d 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -626,6 +626,7 @@ export type TableRepeaterField = BaseField & { component: FieldComponents.TABLE_REPEATER formTitle?: StaticText addItemButtonText?: StaticText + cancelButtonText?: StaticText saveItemButtonText?: StaticText getStaticTableData?: (application: Application) => Record[] removeButtonTooltipText?: StaticText diff --git a/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterFormField.tsx b/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterFormField.tsx index 5d3b8f3f6bc0..ec1e39bf08f0 100644 --- a/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterFormField.tsx +++ b/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterFormField.tsx @@ -22,7 +22,11 @@ import { useLocale } from '@island.is/localization' import { FieldDescription } from '@island.is/shared/form-fields' import { FC, useState } from 'react' import { useFieldArray, useFormContext, useWatch } from 'react-hook-form' -import { handleCustomMappedValues } from './utils' +import { + buildDefaultTableHeader, + buildDefaultTableRows, + handleCustomMappedValues, +} from './utils' import { Item } from './TableRepeaterItem' import { Locale } from '@island.is/shared/types' @@ -47,6 +51,7 @@ export const TableRepeaterFormField: FC = ({ title, titleVariant = 'h4', addItemButtonText = coreMessages.buttonAdd, + cancelButtonText = coreMessages.buttonCancel, saveItemButtonText = coreMessages.reviewButtonSubmit, removeButtonTooltipText = coreMessages.deleteFieldText, editButtonTooltipText = coreMessages.editFieldText, @@ -71,8 +76,8 @@ export const TableRepeaterFormField: FC = ({ const activeField = activeIndex >= 0 ? fields[activeIndex] : null const savedFields = fields.filter((_, index) => index !== activeIndex) const tableItems = items.filter((x) => x.displayInTable !== false) - const tableHeader = table?.header ?? tableItems.map((item) => item.label) - const tableRows = table?.rows ?? tableItems.map((item) => item.id) + const tableHeader = table?.header ?? buildDefaultTableHeader(tableItems) + const tableRows = table?.rows ?? buildDefaultTableRows(tableItems) const staticData = getStaticTableData?.(application) const canAddItem = maxRows ? savedFields.length < maxRows : true @@ -89,6 +94,11 @@ export const TableRepeaterFormField: FC = ({ } } + const handleCancelItem = (index: number) => { + setActiveIndex(-1) + remove(index) + } + const handleNewItem = () => { append({}) setActiveIndex(fields.length) @@ -239,14 +249,24 @@ export const TableRepeaterFormField: FC = ({ /> ))} - - + + + + + + + ) : ( diff --git a/libs/application/ui-fields/src/lib/TableRepeaterFormField/utils.ts b/libs/application/ui-fields/src/lib/TableRepeaterFormField/utils.ts index fd1962a41f14..f1b0c79ee077 100644 --- a/libs/application/ui-fields/src/lib/TableRepeaterFormField/utils.ts +++ b/libs/application/ui-fields/src/lib/TableRepeaterFormField/utils.ts @@ -1,3 +1,4 @@ +import { coreMessages } from '@island.is/application/core' import { TableRepeaterItem } from '@island.is/application/types' type Item = { @@ -31,7 +32,7 @@ const handleNationalIdWithNameItem = ( // with a nested object inside it. This function will extract the nested // object and merge it with the rest of the values. const newValues = values.map((value) => { - if (typeof value[item.id] === 'object' && value[item.id] !== null) { + if (!!value[item.id] && typeof value[item.id] === 'object') { const { [item.id]: nestedObject, ...rest } = value return { ...nestedObject, ...rest } } @@ -40,3 +41,25 @@ const handleNationalIdWithNameItem = ( return newValues } + +export const buildDefaultTableHeader = (items: Array) => + items + .map((item) => + // nationalIdWithName is a special case where the value is an object of name and nationalId + item.component === 'nationalIdWithName' + ? [coreMessages.name, coreMessages.nationalId] + : item.label, + ) + .flat(2) + +export const buildDefaultTableRows = ( + items: Array, +) => + items + .map((item) => + // nationalIdWithName is a special case where the value is an object of name and nationalId + item.component === 'nationalIdWithName' + ? ['name', 'nationalId'] + : item.id, + ) + .flat(2) From 01c8d4de847f56522863308df92847a0cfaccc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3n=20Levy?= Date: Tue, 19 Nov 2024 10:46:46 +0000 Subject: [PATCH 28/34] fix(datadog): disable rum (#16876) * fix(datadog): disable rum * fix: mask all data * fix: only disable trackInteractions * fix: cleanup --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/user-monitoring/src/lib/user-monitoring.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/user-monitoring/src/lib/user-monitoring.ts b/libs/user-monitoring/src/lib/user-monitoring.ts index e1315a4118b5..478d458340a7 100644 --- a/libs/user-monitoring/src/lib/user-monitoring.ts +++ b/libs/user-monitoring/src/lib/user-monitoring.ts @@ -20,7 +20,7 @@ const initDdRum = (params: DdRumInitParams) => { version: params.version, sampleRate: 100, sessionReplaySampleRate: 0, - trackInteractions: true, + trackInteractions: false, allowedTracingOrigins: [ 'https://island.is', /https:\/\/.*\.island\.is/, From 53378fd06da10fb4c534ecd31c2d414bab020d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigr=C3=BAn=20Tinna=20Gissurard=C3=B3ttir?= <39527334+sigruntg@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:32:02 +0000 Subject: [PATCH 29/34] fix(work-accident-notification): fixing duplicate message ids (#16935) --- .../src/lib/messages/employee.ts | 4 ++-- .../src/lib/messages/information.ts | 2 +- .../src/lib/messages/overview.ts | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/employee.ts b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/employee.ts index 48156be856d6..f8d0d5e135a4 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/employee.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/employee.ts @@ -69,7 +69,7 @@ export const employee = { description: 'Employment time in the same job', }, employmentRate: { - id: 'aosh.wan.application:employee.employmentTime', + id: 'aosh.wan.application:employee.employmentRate', defaultMessage: 'Starfshlutfall', description: 'Employment rate %', }, @@ -79,7 +79,7 @@ export const employee = { description: 'Employees work hour arrangement', }, startOfWorkdayDate: { - id: 'aosh.wan.application:employee.time', + id: 'aosh.wan.application:employee.startOfWorkdayDate', defaultMessage: 'Dagsetning', description: 'Start of employees workday date', }, diff --git a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/information.ts b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/information.ts index ae458fb96080..f8e92b4b6986 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/information.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/information.ts @@ -14,7 +14,7 @@ export const information = { description: 'Description of information page', }, pageTitle: { - id: 'aosh.wan.application:information.company.pageTitle', + id: 'aosh.wan.application:information.general.pageTitle', defaultMessage: 'Tilkynningaraðili', description: 'Title of company information section', }, diff --git a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/overview.ts b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/overview.ts index e364a4957dc8..715d57d0654b 100644 --- a/libs/application/templates/aosh/work-accident-notification/src/lib/messages/overview.ts +++ b/libs/application/templates/aosh/work-accident-notification/src/lib/messages/overview.ts @@ -47,37 +47,37 @@ export const overview = { description: 'Date and time label', }, didAoshCome: { - id: 'aosh.wan.application:overview.labels.dateAndTime', + id: 'aosh.wan.application:overview.labels.didAoshCome', defaultMessage: 'Kom Vinnueftirlitið?', description: 'Date and time label', }, didPoliceCome: { - id: 'aosh.wan.application:overview.labels.dateAndTime', + id: 'aosh.wan.application:overview.labels.didPoliceCome', defaultMessage: 'Kom Lögreglan?', description: 'Date and time label', }, injuredAmount: { - id: 'aosh.wan.application:overview.labels.dateAndTime', + id: 'aosh.wan.application:overview.labels.injuredAmount', defaultMessage: 'Fjöldi slasaðra', description: 'Date and time label', }, municipality: { - id: 'aosh.wan.application:overview.labels.dateAndTime', + id: 'aosh.wan.application:overview.labels.municipality', defaultMessage: 'Sveitarfélag þar sem slysið átti sér stað', description: 'Date and time label', }, exactLocation: { - id: 'aosh.wan.application:overview.labels.dateAndTime', + id: 'aosh.wan.application:overview.labels.exactLocation', defaultMessage: 'Nákvæm staðsetning slyssins', description: 'Date and time label', }, accidentDescription: { - id: 'aosh.wan.application:overview.labels.dateAndTime', + id: 'aosh.wan.application:overview.labels.accidentDescription', defaultMessage: 'Tildrög slyssins', description: 'Date and time label', }, locationOfAccident: { - id: 'aosh.wan.application:overview.labels.dateAndTime', + id: 'aosh.wan.application:overview.labels.locationOfAccident', defaultMessage: 'Vettvangur slyssins', description: 'Date and time label', }, From 025f1f8ea6a5125c3987690bf42da1352ddbe0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3hanna=20Magn=C3=BAsd=C3=B3ttir?= Date: Tue, 19 Nov 2024 11:43:22 +0000 Subject: [PATCH 30/34] fix(samgongustofa): shared vehicle radio form field (#16905) * Fix typo * Fix mileageReading vs isRequired Stop using mileageRequired from answers in getSelectedVehicle and always look at the value from externalData (it should not change) * Make sure to always use requiresMileageRegistration from currentvehicleswithmileageandinsp (not basicVehicleInformation) Cleanup make+vehcom (basicVehicleInformation) vs make (currentvehicles) * Cleanup * cleanup * cleanup * cleanup * Use shared function to extract messages from error body Display always E, L and W messages (but E and L first) * Use dummy mileage value when validating per vehicle (owner/operator change) * Catch error from mileage api * Fix the way validation errors are displayed in SGS ownerchange Allow all users to retry submit application if all approvals are finished * Apply same change to change co-owner + change operator Display ValidationErrorMessages always in overview, no matter who is reviewing * Cleanup * Cleanup in LicensePlateRenewal + OrderVehicleLicensePlate Add validation per plate if user has more than 5 * Fix the way vehicle subModel is displayed * Fixes after review * Fix the way errors are displayed for RenewLicensePlate Add MocablePayment * Add validation for OrderVehicleLicensePlate * Cleanup * Fix comment * Create RadioFormField * Fix field currentVehicleList * Use RadioFormField in all SGS applications * Use RadioFormField in all SGS applications * Cleanup * cleanup * cleanup field names * Fixes after review --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../VehiclesField/VehicleRadioField.tsx | 142 --------- .../src/fields/VehiclesField/index.tsx | 28 +- .../VehiclesField/VehicleRadioField.tsx | 142 --------- .../src/fields/VehiclesField/index.tsx | 28 +- .../src/fields/PlateField/PlateRadioField.tsx | 110 ------- .../src/fields/PlateField/index.tsx | 30 +- .../src/lib/dataSchema.ts | 2 +- .../VehiclesField/VehicleRadioField.tsx | 119 -------- .../src/fields/VehiclesField/index.tsx | 23 +- .../InformationSection/plateSizeSubSection.ts | 4 +- .../VehiclesField/VehicleRadioField.tsx | 62 ---- .../VehiclesField/VehicleSelectField.tsx | 19 +- .../src/fields/VehiclesField/index.tsx | 28 +- .../InformationSection/vehicleSubSection.ts | 4 +- .../VehiclesField/VehicleRadioField.tsx | 142 --------- .../src/fields/VehiclesField/index.tsx | 28 +- libs/application/types/src/lib/Fields.ts | 19 ++ .../VehicleRadioFormField/VehicleDetails.ts | 22 ++ .../VehicleRadioFormField.tsx | 281 ++++++++++++++++++ libs/application/ui-fields/src/lib/index.ts | 1 + 20 files changed, 478 insertions(+), 756 deletions(-) delete mode 100644 libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/VehiclesField/VehicleRadioField.tsx delete mode 100644 libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/VehiclesField/VehicleRadioField.tsx delete mode 100644 libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateRadioField.tsx delete mode 100644 libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/VehicleRadioField.tsx delete mode 100644 libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/VehicleRadioField.tsx delete mode 100644 libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/VehiclesField/VehicleRadioField.tsx create mode 100644 libs/application/ui-fields/src/lib/VehicleRadioFormField/VehicleDetails.ts create mode 100644 libs/application/ui-fields/src/lib/VehicleRadioFormField/VehicleRadioFormField.tsx diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/VehiclesField/VehicleRadioField.tsx b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/VehiclesField/VehicleRadioField.tsx deleted file mode 100644 index 72ac2c0a3258..000000000000 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/VehiclesField/VehicleRadioField.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { - Box, - Text, - AlertMessage, - BulletList, - Bullet, - InputError, -} from '@island.is/island-ui/core' -import { FC, useState } from 'react' -import { VehiclesCurrentVehicleWithOwnerchangeChecks } from '../../shared' -import { RadioController } from '@island.is/shared/form-fields' -import { useFormContext } from 'react-hook-form' -import { getValueViaPath } from '@island.is/application/core' -import { FieldBaseProps } from '@island.is/application/types' -import { useLocale } from '@island.is/localization' -import { applicationCheck, information, error } from '../../lib/messages' - -interface Option { - value: string - label: React.ReactNode - disabled?: boolean -} - -interface VehicleSearchFieldProps { - currentVehicleList: VehiclesCurrentVehicleWithOwnerchangeChecks[] -} - -export const VehicleRadioField: FC< - React.PropsWithChildren -> = ({ currentVehicleList, application, errors }) => { - const { formatMessage } = useLocale() - const { setValue } = useFormContext() - - const [plate, setPlate] = useState( - getValueViaPath(application.answers, 'pickVehicle.plate', '') as string, - ) - - const onRadioControllerSelect = (s: string) => { - const currentVehicle = currentVehicleList[parseInt(s, 10)] - const permno = currentVehicle.permno || '' - - setPlate(permno) - setValue('pickVehicle.plate', permno) - setValue('pickVehicle.color', currentVehicle.color || undefined) - setValue('pickVehicle.type', currentVehicle.make || undefined) - setValue('vehicleMileage.requireMileage', currentVehicle?.requireMileage) - setValue('vehicleMileage.mileageReading', currentVehicle?.mileageReading) - if (permno) setValue('vehicleInfo.plate', permno) - if (permno) setValue('vehicleInfo.type', currentVehicle.make) - } - - const vehicleOptions = ( - vehicles: VehiclesCurrentVehicleWithOwnerchangeChecks[], - ) => { - const options = [] as Option[] - - for (const [index, vehicle] of vehicles.entries()) { - const disabled = - !vehicle.isDebtLess || !!vehicle.validationErrorMessages?.length - options.push({ - value: `${index}`, - label: ( - - - - {vehicle.make} - - - {vehicle.color} - {vehicle.permno} - - - {disabled && ( - - - - {!vehicle.isDebtLess && ( - - {formatMessage( - information.labels.pickVehicle.isNotDebtLessTag, - )} - - )} - {!!vehicle.validationErrorMessages?.length && - vehicle.validationErrorMessages?.map((error) => { - const message = formatMessage( - getValueViaPath( - applicationCheck.validation, - error.errorNo || '', - ), - ) - const defaultMessage = error.defaultMessage - const fallbackMessage = - formatMessage( - applicationCheck.validation - .fallbackErrorMessage, - ) + - ' - ' + - error.errorNo - - return ( - - {message || defaultMessage || fallbackMessage} - - ) - })} - - - } - /> - - )} - - ), - disabled: disabled, - }) - } - return options - } - - return ( -
- - {plate.length === 0 && (errors as any)?.pickVehicle && ( - - )} -
- ) -} diff --git a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/VehiclesField/index.tsx b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/VehiclesField/index.tsx index 3adad308746b..11c7b5ca4819 100644 --- a/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/VehiclesField/index.tsx +++ b/libs/application/templates/transport-authority/change-co-owner-of-vehicle/src/fields/VehiclesField/index.tsx @@ -6,12 +6,14 @@ import { import { Box } from '@island.is/island-ui/core' import { FC, useCallback, useEffect } from 'react' import { CurrentVehiclesAndRecords } from '../../shared' -import { VehicleRadioField } from './VehicleRadioField' import { useFormContext } from 'react-hook-form' import { ApolloQueryResult, useMutation } from '@apollo/client' import { UPDATE_APPLICATION } from '@island.is/application/graphql' import { useLocale } from '@island.is/localization' -import { FindVehicleFormField } from '@island.is/application/ui-fields' +import { + FindVehicleFormField, + VehicleRadioFormField, +} from '@island.is/application/ui-fields' import { applicationCheck, error, information } from '../../lib/messages' import { useLazyVehicleDetails } from '../../hooks/useLazyVehicleDetails' import { VehicleSelectField } from './VehicleSelectField' @@ -92,9 +94,27 @@ export const VehiclesField: FC> = ( {...props} /> ) : ( - )}
diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/VehiclesField/VehicleRadioField.tsx b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/VehiclesField/VehicleRadioField.tsx deleted file mode 100644 index 8f72e958cdba..000000000000 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/VehiclesField/VehicleRadioField.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { - Box, - Text, - AlertMessage, - BulletList, - Bullet, - InputError, -} from '@island.is/island-ui/core' -import { FC, useState } from 'react' -import { VehiclesCurrentVehicleWithOperatorChangeChecks } from '../../shared' -import { RadioController } from '@island.is/shared/form-fields' -import { useFormContext } from 'react-hook-form' -import { getValueViaPath } from '@island.is/application/core' -import { FieldBaseProps } from '@island.is/application/types' -import { useLocale } from '@island.is/localization' -import { applicationCheck, information, error } from '../../lib/messages' - -interface Option { - value: string - label: React.ReactNode - disabled?: boolean -} - -interface VehicleSearchFieldProps { - currentVehicleList: VehiclesCurrentVehicleWithOperatorChangeChecks[] -} - -export const VehicleRadioField: FC< - React.PropsWithChildren -> = ({ currentVehicleList, application, errors }) => { - const { formatMessage } = useLocale() - const { setValue } = useFormContext() - - const [plate, setPlate] = useState( - getValueViaPath(application.answers, 'pickVehicle.plate', '') as string, - ) - - const onRadioControllerSelect = (s: string) => { - const currentVehicle = currentVehicleList[parseInt(s, 10)] - const permno = currentVehicle.permno || '' - - setPlate(permno) - setValue('pickVehicle.plate', permno) - setValue('pickVehicle.color', currentVehicle.color || undefined) - setValue('pickVehicle.type', currentVehicle.make || undefined) - setValue('vehicleMileage.requireMileage', currentVehicle?.requireMileage) - setValue('vehicleMileage.mileageReading', currentVehicle?.mileageReading) - if (permno) setValue('vehicleInfo.plate', permno) - if (permno) setValue('vehicleInfo.type', currentVehicle.make) - } - - const vehicleOptions = ( - vehicles: VehiclesCurrentVehicleWithOperatorChangeChecks[], - ) => { - const options = [] as Option[] - - for (const [index, vehicle] of vehicles.entries()) { - const disabled = - !vehicle.isDebtLess || !!vehicle.validationErrorMessages?.length - options.push({ - value: `${index}`, - label: ( - - - - {vehicle.make} - - - {vehicle.color} - {vehicle.permno} - - - {disabled && ( - - - - {!vehicle.isDebtLess && ( - - {formatMessage( - information.labels.pickVehicle.isNotDebtLessTag, - )} - - )} - {!!vehicle.validationErrorMessages?.length && - vehicle.validationErrorMessages?.map((error) => { - const message = formatMessage( - getValueViaPath( - applicationCheck.validation, - error.errorNo || '', - ), - ) - const defaultMessage = error.defaultMessage - const fallbackMessage = - formatMessage( - applicationCheck.validation - .fallbackErrorMessage, - ) + - ' - ' + - error.errorNo - - return ( - - {message || defaultMessage || fallbackMessage} - - ) - })} - - - } - /> - - )} -
- ), - disabled: disabled, - }) - } - return options - } - - return ( -
- - {plate.length === 0 && (errors as any)?.pickVehicle && ( - - )} -
- ) -} diff --git a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/VehiclesField/index.tsx b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/VehiclesField/index.tsx index 7ab26655af07..3e0019887e76 100644 --- a/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/VehiclesField/index.tsx +++ b/libs/application/templates/transport-authority/change-operator-of-vehicle/src/fields/VehiclesField/index.tsx @@ -6,12 +6,14 @@ import { import { Box } from '@island.is/island-ui/core' import { FC, useCallback, useEffect } from 'react' import { CurrentVehiclesAndRecords } from '../../shared' -import { VehicleRadioField } from './VehicleRadioField' import { useFormContext } from 'react-hook-form' import { ApolloQueryResult, useMutation } from '@apollo/client' import { UPDATE_APPLICATION } from '@island.is/application/graphql' import { useLocale } from '@island.is/localization' -import { FindVehicleFormField } from '@island.is/application/ui-fields' +import { + FindVehicleFormField, + VehicleRadioFormField, +} from '@island.is/application/ui-fields' import { useLazyVehicleDetails } from '../../hooks/useLazyVehicleDetails' import { applicationCheck, error, information } from '../../lib/messages' import { VehicleSelectField } from './VehicleSelectField' @@ -97,9 +99,27 @@ export const VehiclesField: FC> = ( {...props} /> ) : ( - )}
diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateRadioField.tsx b/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateRadioField.tsx deleted file mode 100644 index 74e8dbadfeda..000000000000 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/PlateRadioField.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { - Box, - Text, - AlertMessage, - BulletList, - Bullet, - Tag, -} from '@island.is/island-ui/core' -import { FC, useEffect, useState } from 'react' -import { PlateOwnership } from '../../shared' -import { RadioController } from '@island.is/shared/form-fields' -import { FieldBaseProps } from '@island.is/application/types' -import { useLocale } from '@island.is/localization' -import { information } from '../../lib/messages' -import { useFormContext } from 'react-hook-form' -import { getValueViaPath } from '@island.is/application/core' -import { checkCanRenew } from '../../utils' - -interface Option { - value: string - label: React.ReactNode - disabled?: boolean -} - -interface PlateSearchFieldProps { - myPlateOwnershipList: PlateOwnership[] -} - -export const PlateRadioField: FC< - React.PropsWithChildren -> = ({ myPlateOwnershipList, application }) => { - const { formatMessage, formatDateFns } = useLocale() - const { setValue } = useFormContext() - const [regno, setRegno] = useState( - getValueViaPath(application.answers, 'pickPlate.regno', '') as string, - ) - - const plateOptions = (plates: PlateOwnership[]) => { - const options = [] as Option[] - - for (const [index, plate] of plates.entries()) { - const hasError = !!plate.validationErrorMessages?.length - const canRenew = checkCanRenew(plate) - const disabled = hasError || !canRenew - - options.push({ - value: `${index}`, - label: ( - - - - - {plate.regno} - - - - {formatMessage(information.labels.pickPlate.expiresTag, { - date: formatDateFns(new Date(plate.endDate), 'do MMM yyyy'), - })} - - - {hasError && ( - - - - {plate.validationErrorMessages?.map((error) => { - return {error.defaultMessage} - })} - - - } - /> - - )} -
- ), - disabled: disabled, - }) - } - return options - } - - useEffect(() => { - setValue('pickPlate.regno', regno) - }, [setValue, regno]) - - return ( -
- - setRegno(myPlateOwnershipList[parseInt(s, 10)].regno) - } - /> -
- ) -} diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/index.tsx b/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/index.tsx index e18e46412b8e..e2548291c5c8 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/index.tsx +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/fields/PlateField/index.tsx @@ -1,13 +1,19 @@ -import { FieldBaseProps } from '@island.is/application/types' +import { + FieldBaseProps, + FieldComponents, + FieldTypes, +} from '@island.is/application/types' import { Box } from '@island.is/island-ui/core' import { FC, useCallback, useEffect } from 'react' import { PlateOwnership } from '../../shared' import { PlateSelectField } from './PlateSelectField' -import { PlateRadioField } from './PlateRadioField' import { useMutation } from '@apollo/client' import { UPDATE_APPLICATION } from '@island.is/application/graphql' import { useLocale } from '@island.is/localization' import { useFormContext } from 'react-hook-form' +import { VehicleRadioFormField } from '@island.is/application/ui-fields' +import { applicationCheck, error, information } from '../../lib/messages' +import { checkCanRenew } from '../../utils' export const PlateField: FC> = ( props, @@ -47,9 +53,25 @@ export const PlateField: FC> = ( {...props} /> ) : ( - checkCanRenew(item as PlateOwnership), + }} /> )} diff --git a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/dataSchema.ts b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/dataSchema.ts index 0b45b7882b92..0eb01c1e204d 100644 --- a/libs/application/templates/transport-authority/license-plate-renewal/src/lib/dataSchema.ts +++ b/libs/application/templates/transport-authority/license-plate-renewal/src/lib/dataSchema.ts @@ -4,7 +4,7 @@ export const LicensePlateRenewalSchema = z.object({ approveExternalData: z.boolean().refine((v) => v), pickPlate: z.object({ regno: z.string().min(1), - value: z.string().min(1), + value: z.string().optional(), }), }) diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/VehicleRadioField.tsx b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/VehicleRadioField.tsx deleted file mode 100644 index 7acc956fcba9..000000000000 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/VehicleRadioField.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { - AlertMessage, - Box, - Bullet, - BulletList, - InputError, - Text, -} from '@island.is/island-ui/core' -import { FC, useState } from 'react' -import { RadioController } from '@island.is/shared/form-fields' -import { useFormContext } from 'react-hook-form' -import { getValueViaPath } from '@island.is/application/core' -import { FieldBaseProps } from '@island.is/application/types' -import { useLocale } from '@island.is/localization' -import { error, information } from '../../lib/messages' -import { VehiclesCurrentVehicleWithPlateOrderChecks } from '../../shared' - -interface Option { - value: string - label: React.ReactNode - disabled?: boolean -} - -interface VehicleSearchFieldProps { - currentVehicleList: VehiclesCurrentVehicleWithPlateOrderChecks[] -} - -export const VehicleRadioField: FC< - React.PropsWithChildren -> = ({ currentVehicleList, application, errors }) => { - const { formatMessage } = useLocale() - const { setValue } = useFormContext() - - const [plate, setPlate] = useState( - getValueViaPath(application.answers, 'pickVehicle.plate', '') as string, - ) - - const onRadioControllerSelect = (s: string) => { - const currentVehicle = currentVehicleList[parseInt(s, 10)] - setPlate(currentVehicle.permno || '') - setValue('pickVehicle.plate', currentVehicle.permno || '') - } - - const vehicleOptions = ( - vehicles: VehiclesCurrentVehicleWithPlateOrderChecks[], - ) => { - const options = [] as Option[] - - for (const [index, vehicle] of vehicles.entries()) { - const disabled = !!vehicle.validationErrorMessages?.length - options.push({ - value: `${index}`, - label: ( - - - - {vehicle.make} - - - {vehicle.color} - {vehicle.permno} - - - {disabled && ( - - - - {!!vehicle.validationErrorMessages?.length && - vehicle.validationErrorMessages?.map((err) => { - const defaultMessage = err.defaultMessage - const fallbackMessage = - formatMessage( - error.validationFallbackErrorMessage, - ) + - ' - ' + - err.errorNo - - return ( - - {defaultMessage || fallbackMessage} - - ) - })} - - - } - /> - - )} - - ), - disabled: disabled, - }) - } - return options - } - - return ( -
- - {plate.length === 0 && (errors as any)?.pickVehicle && ( - - )} -
- ) -} diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/index.tsx b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/index.tsx index 8b55aa3474a2..e76d988d3fba 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/index.tsx +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/fields/VehiclesField/index.tsx @@ -6,8 +6,10 @@ import { import { Box } from '@island.is/island-ui/core' import { FC } from 'react' import { CurrentVehiclesAndRecords } from '../../shared' -import { VehicleRadioField } from './VehicleRadioField' -import { FindVehicleFormField } from '@island.is/application/ui-fields' +import { + FindVehicleFormField, + VehicleRadioFormField, +} from '@island.is/application/ui-fields' import { information, error } from '../../lib/messages' import { useLazyVehicleDetails } from '../../hooks/useLazyVehicleDetails' import { ApolloQueryResult } from '@apollo/client' @@ -64,9 +66,22 @@ export const VehiclesField: FC> = ( {...props} /> ) : ( - )} diff --git a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/InformationSection/plateSizeSubSection.ts b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/InformationSection/plateSizeSubSection.ts index 32ee542c1f57..ae1772f2d5aa 100644 --- a/libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/InformationSection/plateSizeSubSection.ts +++ b/libs/application/templates/transport-authority/order-vehicle-license-plate/src/forms/OrderVehicleLicensePlateForm/InformationSection/plateSizeSubSection.ts @@ -24,7 +24,7 @@ export const plateSizeSubSection = buildSubSection({ titleVariant: 'h5', }), buildTextField({ - id: 'plateSize.vehiclePlate', + id: 'vehicleInfo.plate', title: information.labels.plateSize.vehiclePlate, backgroundColor: 'white', width: 'half', @@ -38,7 +38,7 @@ export const plateSizeSubSection = buildSubSection({ }, }), buildTextField({ - id: 'plateSize.vehicleType', + id: 'vehicleInfo.type', title: information.labels.plateSize.vehicleType, backgroundColor: 'white', width: 'half', diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/VehicleRadioField.tsx b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/VehicleRadioField.tsx deleted file mode 100644 index af789b491604..000000000000 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/VehicleRadioField.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Box, Text } from '@island.is/island-ui/core' -import { FC } from 'react' -import { VehiclesCurrentVehicle } from '../../shared' -import { RadioController } from '@island.is/shared/form-fields' -import { useFormContext } from 'react-hook-form' -import { FieldBaseProps } from '@island.is/application/types' - -interface Option { - value: string - label: React.ReactNode - disabled?: boolean -} - -interface VehicleSearchFieldProps { - currentVehicleList: VehiclesCurrentVehicle[] -} - -export const VehicleRadioField: FC< - React.PropsWithChildren -> = ({ currentVehicleList }) => { - const { setValue } = useFormContext() - - const onRadioControllerSelect = (s: string) => { - const currentVehicle = currentVehicleList[parseInt(s, 10)] - setValue('pickVehicle.plate', currentVehicle.permno || '') - } - - const vehicleOptions = (vehicles: VehiclesCurrentVehicle[]) => { - const options = [] as Option[] - - for (const [index, vehicle] of vehicles.entries()) { - options.push({ - value: `${index}`, - label: ( - - - - {vehicle.make} - - - {vehicle.color} - {vehicle.permno} - - - - ), - }) - } - return options - } - - return ( -
- -
- ) -} diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/VehicleSelectField.tsx b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/VehicleSelectField.tsx index d6b832c0a84e..33ca041004fc 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/VehicleSelectField.tsx +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/VehicleSelectField.tsx @@ -1,9 +1,14 @@ import { FieldBaseProps, Option } from '@island.is/application/types' import { useLocale } from '@island.is/localization' import { FC, useState } from 'react' -import { ActionCard, Box, SkeletonLoader } from '@island.is/island-ui/core' +import { + ActionCard, + Box, + InputError, + SkeletonLoader, +} from '@island.is/island-ui/core' import { VehiclesCurrentVehicle } from '../../shared' -import { information } from '../../lib/messages' +import { error, information } from '../../lib/messages' import { SelectController } from '@island.is/shared/form-fields' import { useFormContext } from 'react-hook-form' import { getValueViaPath } from '@island.is/application/core' @@ -14,7 +19,7 @@ interface VehicleSearchFieldProps { export const VehicleSelectField: FC< React.PropsWithChildren -> = ({ currentVehicleList, application }) => { +> = ({ currentVehicleList, application, errors }) => { const { formatMessage } = useLocale() const { setValue } = useFormContext() @@ -38,6 +43,10 @@ export const VehicleSelectField: FC< : null, ) + const [plate, setPlate] = useState( + getValueViaPath(application.answers, 'pickVehicle.plate', '') as string, + ) + const onChange = (option: Option) => { const currentVehicle = currentVehicleList[parseInt(option.value, 10)] setIsLoading(true) @@ -49,6 +58,7 @@ export const VehicleSelectField: FC< role: currentVehicle?.role, }) setValue('pickVehicle.plate', currentVehicle.permno || '') + setPlate(currentVehicle.permno || '') setIsLoading(false) } } @@ -85,6 +95,9 @@ export const VehicleSelectField: FC< )} + {!isLoading && plate.length === 0 && (errors as any)?.pickVehicle && ( + + )} ) } diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/index.tsx b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/index.tsx index 1c99f5cf28e4..09f95f9eaba6 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/index.tsx +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/fields/VehiclesField/index.tsx @@ -3,22 +3,22 @@ import { FieldComponents, FieldTypes, } from '@island.is/application/types' -import { Box, InputError } from '@island.is/island-ui/core' +import { Box } from '@island.is/island-ui/core' import { FC } from 'react' import { CurrentVehiclesAndRecords } from '../../shared' -import { VehicleRadioField } from './VehicleRadioField' -import { useLocale } from '@island.is/localization' import { error, information } from '../../lib/messages' import { useLazyVehicleDetails } from '../../hooks/useLazyVehicleDetails' -import { FindVehicleFormField } from '@island.is/application/ui-fields' +import { + FindVehicleFormField, + VehicleRadioFormField, +} from '@island.is/application/ui-fields' import { ApolloQueryResult } from '@apollo/client' import { VehicleSelectField } from './VehicleSelectField' export const VehiclesField: FC> = ( props, ) => { - const { formatMessage } = useLocale() - const { application, errors } = props + const { application } = props const getVehicleDetails = useLazyVehicleDetails() const createGetVehicleDetailsWrapper = ( @@ -65,14 +65,20 @@ export const VehiclesField: FC> = ( {...props} /> ) : ( - )} - {(errors as any)?.pickVehicle && ( - - )} ) } diff --git a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/InformationSection/vehicleSubSection.ts b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/InformationSection/vehicleSubSection.ts index 0c31277ba22c..bbac58c544d2 100644 --- a/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/InformationSection/vehicleSubSection.ts +++ b/libs/application/templates/transport-authority/order-vehicle-registration-certificate/src/forms/OrderVehicleRegistrationCertificateForm/InformationSection/vehicleSubSection.ts @@ -24,7 +24,7 @@ export const vehicleSubSection = buildSubSection({ titleVariant: 'h5', }), buildTextField({ - id: 'pickVehicle.plate', + id: 'vehicleInfo.plate', title: information.labels.vehicle.plate, backgroundColor: 'white', width: 'half', @@ -38,7 +38,7 @@ export const vehicleSubSection = buildSubSection({ }, }), buildTextField({ - id: 'pickVehicle.type', + id: 'vehicleInfo.type', title: information.labels.vehicle.type, backgroundColor: 'white', width: 'half', diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/VehiclesField/VehicleRadioField.tsx b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/VehiclesField/VehicleRadioField.tsx deleted file mode 100644 index 18da50ff8a31..000000000000 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/VehiclesField/VehicleRadioField.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { - AlertMessage, - Box, - Bullet, - BulletList, - Text, - InputError, -} from '@island.is/island-ui/core' -import { useLocale } from '@island.is/localization' -import { FC, useState } from 'react' -import { VehiclesCurrentVehicleWithOwnerchangeChecks } from '../../shared' -import { information, applicationCheck, error } from '../../lib/messages' -import { RadioController } from '@island.is/shared/form-fields' -import { useFormContext } from 'react-hook-form' -import { getValueViaPath } from '@island.is/application/core' -import { FieldBaseProps } from '@island.is/application/types' - -interface Option { - value: string - label: React.ReactNode - disabled?: boolean -} - -interface VehicleSearchFieldProps { - currentVehicleList: VehiclesCurrentVehicleWithOwnerchangeChecks[] -} - -export const VehicleRadioField: FC< - React.PropsWithChildren -> = ({ currentVehicleList, application, errors }) => { - const { formatMessage } = useLocale() - const { setValue } = useFormContext() - - const [plate, setPlate] = useState( - getValueViaPath(application.answers, 'pickVehicle.plate', '') as string, - ) - - const onRadioControllerSelect = (s: string) => { - const currentVehicle = currentVehicleList[parseInt(s, 10)] - const permno = currentVehicle.permno || '' - - setPlate(permno) - setValue('pickVehicle.plate', permno) - setValue('pickVehicle.type', currentVehicle.make) - setValue('pickVehicle.color', currentVehicle.color || undefined) - setValue('vehicleMileage.requireMileage', currentVehicle?.requireMileage) - setValue('vehicleMileage.mileageReading', currentVehicle?.mileageReading) - if (permno) setValue('vehicleInfo.plate', permno) - if (permno) setValue('vehicleInfo.type', currentVehicle.make) - } - - const vehicleOptions = ( - vehicles: VehiclesCurrentVehicleWithOwnerchangeChecks[], - ) => { - const options = [] as Option[] - - for (const [index, vehicle] of vehicles.entries()) { - const disabled = - !vehicle.isDebtLess || !!vehicle.validationErrorMessages?.length - options.push({ - value: `${index}`, - label: ( - - - - {vehicle.make} - - - {vehicle.color} - {vehicle.permno} - - - {disabled && ( - - - - {!vehicle.isDebtLess && ( - - {formatMessage( - information.labels.pickVehicle.isNotDebtLessTag, - )} - - )} - {!!vehicle.validationErrorMessages?.length && - vehicle.validationErrorMessages?.map((error) => { - const message = formatMessage( - getValueViaPath( - applicationCheck.validation, - error.errorNo || '', - ), - ) - const defaultMessage = error.defaultMessage - const fallbackMessage = - formatMessage( - applicationCheck.validation - .fallbackErrorMessage, - ) + - ' - ' + - error.errorNo - - return ( - - {message || defaultMessage || fallbackMessage} - - ) - })} - - - } - /> - - )} - - ), - disabled: disabled, - }) - } - return options - } - - return ( -
- - {plate.length === 0 && (errors as any)?.pickVehicle && ( - - )} -
- ) -} diff --git a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/VehiclesField/index.tsx b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/VehiclesField/index.tsx index a3b37a8d45be..fb33b27bda13 100644 --- a/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/VehiclesField/index.tsx +++ b/libs/application/templates/transport-authority/transfer-of-vehicle-ownership/src/fields/VehiclesField/index.tsx @@ -5,14 +5,16 @@ import { } from '@island.is/application/types' import { Box } from '@island.is/island-ui/core' import { FC, useCallback, useEffect } from 'react' -import { VehicleRadioField } from './VehicleRadioField' import { useFormContext } from 'react-hook-form' import { CurrentVehiclesAndRecords } from '../../shared' import { ApolloQueryResult, useMutation } from '@apollo/client' import { UPDATE_APPLICATION } from '@island.is/application/graphql' import { useLocale } from '@island.is/localization' import { VehicleSelectField } from './VehicleSelectField' -import { FindVehicleFormField } from '@island.is/application/ui-fields' +import { + FindVehicleFormField, + VehicleRadioFormField, +} from '@island.is/application/ui-fields' import { information, applicationCheck, error } from '../../lib/messages' import { useLazyVehicleDetails } from '../../hooks/useLazyVehicleDetails' @@ -92,9 +94,27 @@ export const VehiclesField: FC> = ( {...props} /> ) : ( - )} diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index 19ed08cafa1d..67e1c8223cc4 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -256,6 +256,7 @@ export enum FieldTypes { HIDDEN_INPUT = 'HIDDEN_INPUT', HIDDEN_INPUT_WITH_WATCHED_VALUE = 'HIDDEN_INPUT_WITH_WATCHED_VALUE', FIND_VEHICLE = 'FIND_VEHICLE', + VEHICLE_RADIO = 'VEHICLE_RADIO', STATIC_TABLE = 'STATIC_TABLE', ACCORDION = 'ACCORDION', BANK_ACCOUNT = 'BANK_ACCOUNT', @@ -290,6 +291,7 @@ export enum FieldComponents { TABLE_REPEATER = 'TableRepeaterFormField', HIDDEN_INPUT = 'HiddenInputFormField', FIND_VEHICLE = 'FindVehicleFormField', + VEHICLE_RADIO = 'VehicleRadioFormField', STATIC_TABLE = 'StaticTableFormField', ACCORDION = 'AccordionFormField', BANK_ACCOUNT = 'BankAccountFormField', @@ -675,6 +677,22 @@ export interface FindVehicleField extends InputField { energyFundsMessages?: Record } +export interface VehicleRadioField extends InputField { + readonly type: FieldTypes.VEHICLE_RADIO + component: FieldComponents.VEHICLE_RADIO + itemType: 'VEHICLE' | 'PLATE' + itemList: unknown[] + shouldValidateDebtStatus?: boolean + shouldValidateRenewal?: boolean + alertMessageErrorTitle?: FormText + validationErrorMessages?: Record + validationErrorFallbackMessage?: FormText + inputErrorMessage: FormText + debtStatusErrorMessage?: FormText + renewalExpiresAtTag?: StaticText + validateRenewal?: (item: unknown) => boolean +} + export interface HiddenInputWithWatchedValueField extends BaseField { watchValue: string type: FieldTypes.HIDDEN_INPUT_WITH_WATCHED_VALUE @@ -769,6 +787,7 @@ export type Field = | HiddenInputWithWatchedValueField | HiddenInputField | FindVehicleField + | VehicleRadioField | StaticTableField | AccordionField | BankAccountField diff --git a/libs/application/ui-fields/src/lib/VehicleRadioFormField/VehicleDetails.ts b/libs/application/ui-fields/src/lib/VehicleRadioFormField/VehicleDetails.ts new file mode 100644 index 000000000000..b6d04706b4cd --- /dev/null +++ b/libs/application/ui-fields/src/lib/VehicleRadioFormField/VehicleDetails.ts @@ -0,0 +1,22 @@ +interface ValidationErrorMessage { + errorNo?: string | null + defaultMessage?: string | null +} + +export interface VehicleDetails { + permno?: string + make?: string + color?: string + role?: string + requireMileage?: boolean | null + mileageReading?: string | null + isDebtLess?: boolean | null + validationErrorMessages?: ValidationErrorMessage[] +} + +export interface PlateOwnership { + regno: string + startDate: string + endDate: string + validationErrorMessages?: ValidationErrorMessage[] +} diff --git a/libs/application/ui-fields/src/lib/VehicleRadioFormField/VehicleRadioFormField.tsx b/libs/application/ui-fields/src/lib/VehicleRadioFormField/VehicleRadioFormField.tsx new file mode 100644 index 000000000000..ba01109c9e21 --- /dev/null +++ b/libs/application/ui-fields/src/lib/VehicleRadioFormField/VehicleRadioFormField.tsx @@ -0,0 +1,281 @@ +import { formatText, getValueViaPath } from '@island.is/application/core' +import { FieldBaseProps, VehicleRadioField } from '@island.is/application/types' +import { + AlertMessage, + Box, + Bullet, + BulletList, + Text, + InputError, + Tag, +} from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { FC, useState } from 'react' +import { useFormContext } from 'react-hook-form' +import { RadioController } from '@island.is/shared/form-fields' +import { PlateOwnership, VehicleDetails } from './VehicleDetails' +import { MessageDescriptor } from 'react-intl' + +interface Option { + value: string + label: React.ReactNode + disabled?: boolean +} + +interface Props extends FieldBaseProps { + field: VehicleRadioField +} + +export const VehicleRadioFormField: FC> = ({ + application, + field, + errors, +}) => { + const { formatMessage, formatDateFns } = useLocale() + const { setValue } = useFormContext() + + let answersSelectedValueKey: string | undefined + let radioControllerId = field.id + if (field.itemType === 'VEHICLE') { + answersSelectedValueKey = `${field.id}.plate` + radioControllerId = `${field.id}.vehicle` + } else if (field.itemType === 'PLATE') { + answersSelectedValueKey = `${field.id}.regno` + radioControllerId = `${field.id}.value` + } + + const [selectedValue, setSelectedValue] = useState( + answersSelectedValueKey && + getValueViaPath(application.answers, answersSelectedValueKey, ''), + ) + + const onRadioControllerSelect = (s: string) => { + if (field.itemType === 'VEHICLE') { + const currentVehicleList = field.itemList as VehicleDetails[] + const currentVehicle = currentVehicleList?.[parseInt(s, 10)] + const permno = currentVehicle?.permno || '' + + setSelectedValue(permno) + + setValue(`${field.id}.plate`, permno) + setValue(`${field.id}.type`, currentVehicle?.make) + setValue(`${field.id}.color`, currentVehicle?.color || undefined) + + setValue('vehicleMileage.requireMileage', currentVehicle?.requireMileage) + setValue('vehicleMileage.mileageReading', currentVehicle?.mileageReading) + + if (permno) setValue('vehicleInfo.plate', permno) + if (permno) setValue('vehicleInfo.type', currentVehicle?.make) + } else if (field.itemType === 'PLATE') { + const currentPlateList = field.itemList as PlateOwnership[] + const currentPlate = currentPlateList?.[parseInt(s, 10)] + const regno = currentPlate?.regno + + setSelectedValue(regno) + + setValue(`${field.id}.regno`, regno) + } + } + + const vehicleOptions = (vehicles: VehicleDetails[]) => { + const options: Option[] = [] + + for (const [index, vehicle] of vehicles.entries()) { + const hasError = !!vehicle.validationErrorMessages?.length + const hasDebtError = field.shouldValidateDebtStatus && !vehicle.isDebtLess + const disabled = hasError || hasDebtError + + options.push({ + value: `${index}`, + label: ( + + + + {vehicle.make} + + + {vehicle.color} - {vehicle.permno} + + + {disabled && ( + + + + {field.shouldValidateDebtStatus && + !vehicle.isDebtLess && ( + + {field.debtStatusErrorMessage && + formatText( + field.debtStatusErrorMessage, + application, + formatMessage, + )} + + )} + {!!vehicle.validationErrorMessages?.length && + vehicle.validationErrorMessages?.map((error) => { + const message = + field.validationErrorMessages && + formatMessage( + getValueViaPath( + field.validationErrorMessages, + error.errorNo || '', + ) || '', + ) + const defaultMessage = error.defaultMessage + const fallbackMessage = + (field.validationErrorFallbackMessage && + formatText( + field.validationErrorFallbackMessage, + application, + formatMessage, + )) + + ' - ' + + error.errorNo + + return ( + + {message || defaultMessage || fallbackMessage} + + ) + })} + + + } + /> + + )} + + ), + disabled: disabled, + }) + } + return options + } + + const plateOptions = (plates: PlateOwnership[]) => { + const options: Option[] = [] + + for (const [index, plate] of plates.entries()) { + const hasError = !!plate.validationErrorMessages?.length + const canRenew = + !field.shouldValidateRenewal || field.validateRenewal?.(plate) + const disabled = hasError || !canRenew + + options.push({ + value: `${index}`, + label: ( + + + + + {plate.regno} + + + + {field.renewalExpiresAtTag && + formatMessage(field.renewalExpiresAtTag, { + date: formatDateFns(new Date(plate.endDate), 'do MMM yyyy'), + })} + + + {hasError && ( + + + + {plate.validationErrorMessages?.map((error) => { + const message = + field.validationErrorMessages && + formatMessage( + getValueViaPath( + field.validationErrorMessages, + error.errorNo || '', + ) || '', + ) + + const defaultMessage = error.defaultMessage + + const fallbackMessage = + (field.validationErrorFallbackMessage && + formatText( + field.validationErrorFallbackMessage, + application, + formatMessage, + )) + + ' - ' + + error.errorNo + + return ( + + {message || defaultMessage || fallbackMessage} + + ) + })} + + + } + /> + + )} + + ), + disabled: disabled, + }) + } + return options + } + + let options: Option[] = [] + if (field.itemType === 'VEHICLE') { + options = vehicleOptions(field.itemList as VehicleDetails[]) + } else if (field.itemType === 'PLATE') { + options = plateOptions(field.itemList as PlateOwnership[]) + } + + return ( +
+ + + {!selectedValue?.length && !!errors?.[field.id] && ( + + )} +
+ ) +} diff --git a/libs/application/ui-fields/src/lib/index.ts b/libs/application/ui-fields/src/lib/index.ts index fa43ff03e370..c3e3f9724309 100644 --- a/libs/application/ui-fields/src/lib/index.ts +++ b/libs/application/ui-fields/src/lib/index.ts @@ -25,6 +25,7 @@ export { HiddenInputFormField } from './HiddenInputFormField/HiddenInputFormFiel export { ActionCardListFormField } from './ActionCardListFormField/ActionCardListFormField' export { TableRepeaterFormField } from './TableRepeaterFormField/TableRepeaterFormField' export { FindVehicleFormField } from './FindVehicleFormField/FindVehicleFormField' +export { VehicleRadioFormField } from './VehicleRadioFormField/VehicleRadioFormField' export { StaticTableFormField } from './StaticTableFormField/StaticTableFormField' export { AccordionFormField } from './AccordionFormField/AccordionFormField' export { BankAccountFormField } from './BankAccountFormField/BankAccountFormField' From ff67649d2020ac1eaaeff82d427a2f27a86b8a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:15:24 +0000 Subject: [PATCH 31/34] fix(api): Add missing content type header for Campaign Monitor email signup (#16936) --- .../services/campaignMonitor/campaignMonitor.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/api/domains/email-signup/src/lib/services/campaignMonitor/campaignMonitor.service.ts b/libs/api/domains/email-signup/src/lib/services/campaignMonitor/campaignMonitor.service.ts index 2a7ef5e87f42..092c58a389e7 100644 --- a/libs/api/domains/email-signup/src/lib/services/campaignMonitor/campaignMonitor.service.ts +++ b/libs/api/domains/email-signup/src/lib/services/campaignMonitor/campaignMonitor.service.ts @@ -35,7 +35,12 @@ export class CampaignMonitorSignupService { const obj = Object.fromEntries(map) return axios - .post(url, obj, { headers: { Authorization: authHeader } }) + .post(url, obj, { + headers: { + Authorization: authHeader, + 'Content-Type': 'application/json', + }, + }) .then((response) => { return { subscribed: response?.data?.result === 'error' ? false : true, From 5eeff5f3340d32db19b626185d8c3e8c1bdbbdf1 Mon Sep 17 00:00:00 2001 From: unakb Date: Tue, 19 Nov 2024 14:48:32 +0000 Subject: [PATCH 32/34] fix(j-s): Whitelist emails (#16907) * chore(j-s): Whitelist emails outside prod * fix(j-s): Tests after whitelisting * fix(j-s): more tests * fix(j-s): And more tests fixed * Update sendAdvocateAssignedNotifications.spec.ts --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/app/formatters/formatters.spec.ts | 36 ++++++++ .../backend/src/app/formatters/formatters.ts | 28 +++++++ .../backend/src/app/formatters/index.ts | 1 + .../backend/src/app/messages/notifications.ts | 10 +++ .../notification/baseNotification.service.ts | 31 ++++++- .../defendantNotification.service.ts | 1 - .../test/createTestingNotificationModule.ts | 26 +++++- .../sendDefenderAssignedNotifications.spec.ts | 13 ++- .../sendAdvocateAssignedNotifications.spec.ts | 31 ++++--- .../sendAppealCompletedNotifications.spec.ts | 40 ++++----- ...endAppealFilesUpdatedNotifications.spec.ts | 55 ++++++------ ...dAppealJudgesAssignedNotifications.spec.ts | 62 +++++++------- ...AppealReceivedByCourtNotifications.spec.ts | 41 ++++----- .../sendAppealStatementNotifications.spec.ts | 75 +++++++++-------- ...ppealToCourtOfAppealsNotifications.spec.ts | 70 ++++++++-------- .../sendAppealWithdrawnNotifications.spec.ts | 84 +++++++++++-------- .../sendCaseFilesUpdatedNotifications.spec.ts | 52 +++++++----- .../sendCourtDateNotifications.spec.ts | 51 +++++------ ...antsNotUpdatedAtCourtNotifications.spec.ts | 24 ++++-- .../sendIndictmentDeniedNotifications.spec.ts | 11 ++- ...endIndictmentReturnedNotifications.spec.ts | 13 +-- ...aitingForConfirmationNotifications.spec.ts | 24 +++--- .../sendReadyForCourtNotifications.spec.ts | 80 +++++++++++------- .../sendRevokedNotifications.spec.ts | 51 ++++++----- .../sendRulingNotifications.spec.ts | 31 +++++-- 25 files changed, 579 insertions(+), 362 deletions(-) diff --git a/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts b/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts index 3379703684ae..15b4b0fe2d94 100644 --- a/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts +++ b/apps/judicial-system/backend/src/app/formatters/formatters.spec.ts @@ -15,6 +15,7 @@ import { } from '@island.is/judicial-system/types' import { + filterWhitelistEmails, formatCourtHeadsUpSmsNotification, formatCourtReadyForCourtSmsNotification, formatCourtResubmittedToCourtSmsNotification, @@ -1982,3 +1983,38 @@ describe('formatDefenderResubmittedToCourtEmailNotification', () => { expect(result.subject).toEqual('Krafa í máli R-2022/999') }) }) + +describe('filterWhitelistEmails', () => { + const emails = [ + 'test@rvg.is', + 'test2@rvg.is', + 'test3@rvg.is', + 'test4@example.com', + ] + + it('should return only whitelisted emails', () => { + const whitelist = `${emails[0]}, ${emails[2]}` + const domainWhitelist = 'example.com' + + const result = filterWhitelistEmails(emails, domainWhitelist, whitelist) + + expect(result).toEqual([emails[0], emails[2], emails[3]]) + }) + + it('should return empty array if no emails are whitelisted', () => { + const whitelist = '' + const domainWhitelist = '' + + const result = filterWhitelistEmails(emails, domainWhitelist, whitelist) + + expect(result).toEqual([]) + }) + it('should return domain whitelisted emails', () => { + const whitelist = '' + const domainWhitelist = 'rvg.is' + + const result = filterWhitelistEmails(emails, domainWhitelist, whitelist) + + expect(result).toEqual([emails[0], emails[1], emails[2]]) + }) +}) diff --git a/apps/judicial-system/backend/src/app/formatters/formatters.ts b/apps/judicial-system/backend/src/app/formatters/formatters.ts index b5859c04b70b..552a328fc515 100644 --- a/apps/judicial-system/backend/src/app/formatters/formatters.ts +++ b/apps/judicial-system/backend/src/app/formatters/formatters.ts @@ -759,3 +759,31 @@ export const formatDefenderRoute = ( export const formatConfirmedIndictmentKey = (key?: string) => key?.replace(/\/([^/]*)$/, '/confirmed/$1') ?? '' + +export const filterWhitelistEmails = ( + emails: string[], + domainWhitelist: string, + emailWhitelist: string, +) => { + if (!emails || emails.length === 0) return [] + + const allowedDomains = new Set( + domainWhitelist + .split(',') + .map((d) => d.trim()) + .filter(Boolean), + ) + const allowedEmails = new Set( + emailWhitelist + .split(',') + .map((e) => e.trim()) + .filter(Boolean), + ) + + return emails.filter((email) => { + const domain = email.split('@')[1] + return ( + domain && (allowedDomains.has(domain) || allowedEmails.has(email.trim())) + ) + }) +} diff --git a/apps/judicial-system/backend/src/app/formatters/index.ts b/apps/judicial-system/backend/src/app/formatters/index.ts index 063734cdaacb..01d46156bf88 100644 --- a/apps/judicial-system/backend/src/app/formatters/index.ts +++ b/apps/judicial-system/backend/src/app/formatters/index.ts @@ -27,6 +27,7 @@ export { formatCourtOfAppealJudgeAssignedEmailNotification, formatPostponedCourtDateEmailNotification, stripHtmlTags, + filterWhitelistEmails, } from './formatters' export { Confirmation } from './pdfHelpers' export { getRequestPdfAsBuffer, getRequestPdfAsString } from './requestPdf' diff --git a/apps/judicial-system/backend/src/app/messages/notifications.ts b/apps/judicial-system/backend/src/app/messages/notifications.ts index 258eeedc852f..a7bf8f898acf 100644 --- a/apps/judicial-system/backend/src/app/messages/notifications.ts +++ b/apps/judicial-system/backend/src/app/messages/notifications.ts @@ -45,6 +45,16 @@ export const notifications = { 'Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.', description: 'Notaður sem texti í email til þess að tilgreina slóð á RVG', }), + emailWhitelist: defineMessage({ + id: 'judicial.system.backend:notifications.email_whitelist', + defaultMessage: '', + description: 'Notað til að tilgreina hvort póstfang sé í hvítlista', + }), + emailWhitelistDomains: defineMessage({ + id: 'judicial.system.backend:notifications.email_whitelist_domains', + defaultMessage: 'omnitrix.is,kolibri.is', + description: 'Notað til að tilgreina hvort póstfang sé í hvítlista', + }), readyForCourt: defineMessages({ subject: { id: 'judicial.system.backend:notifications.ready_for_court.subjectV2', diff --git a/apps/judicial-system/backend/src/app/modules/notification/baseNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/baseNotification.service.ts index b06c0242d66f..be1034a1dcb1 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/baseNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/baseNotification.service.ts @@ -9,7 +9,7 @@ import type { ConfigType } from '@island.is/nest/config' import { NotificationType } from '@island.is/judicial-system/types' -import { stripHtmlTags } from '../../formatters' +import { filterWhitelistEmails, stripHtmlTags } from '../../formatters' import { notifications } from '../../messages' import { EventService } from '../event' import { DeliverResponse } from './models/deliver.response' @@ -53,6 +53,29 @@ export abstract class BaseNotificationService { }) } + private async handleWhitelist(recipients: string[]): Promise { + const whitelist = this.formatMessage(notifications.emailWhitelist) + const whitelistDomains = this.formatMessage( + notifications.emailWhitelistDomains, + ) + + const whitelistedEmails = filterWhitelistEmails( + recipients, + whitelistDomains, + whitelist, + ) + + if (whitelistedEmails.length === 0) { + this.logger.warn('No whitelisted emails found in recipients') + } + + if (whitelistedEmails.length !== recipients?.length) { + this.logger.warn('Some emails missing from whitelist') + } + + return whitelistedEmails + } + protected async sendEmail( subject: string, html: string, @@ -64,7 +87,11 @@ export abstract class BaseNotificationService { try { // This is to handle a comma separated list of emails // We use the first one as the main recipient and the rest as CC - const recipients = recipientEmail ? recipientEmail.split(',') : undefined + let recipients = recipientEmail ? recipientEmail.split(',') : undefined + + if (!this.config.production && recipients) { + recipients = await this.handleWhitelist(recipients) + } html = html.match(/ { await this.refreshFormatMessage() - try { return await this.sendNotification(type, theCase, defendant) } catch (error) { diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts index 267327b2235b..fe4c450c5873 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts @@ -4,7 +4,7 @@ import { uuid } from 'uuidv4' import { getModelToken } from '@nestjs/sequelize' import { Test } from '@nestjs/testing' -import { IntlService } from '@island.is/cms-translations' +import { FormatMessage, IntlService } from '@island.is/cms-translations' import { createTestIntl } from '@island.is/cms-translations/test' import { EmailService } from '@island.is/email-service' import { LOGGER_PROVIDER } from '@island.is/logging' @@ -37,6 +37,30 @@ import { NotificationDispatchService } from '../notificationDispatch.service' jest.mock('@island.is/judicial-system/message') +export const createTestUsers = ( + roles: string[], +): Record< + string, + { + id: string + name: string + email: string + mobile: string + nationalId: string + } +> => + roles.reduce((acc, role) => { + const id = uuid() + acc[role] = { + id: id, + name: `${role}-${id}`, + email: `${role}-${id}@omnitrix.is`, + mobile: id, + nationalId: '1234567890', + } + return acc + }, {} as Record) + const formatMessage = createTestIntl({ onError: jest.fn(), locale: 'is-IS', diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendDefenderAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendDefenderAssignedNotifications.spec.ts index 41403f0ea3f1..5d048acbad6c 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendDefenderAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/defendantNotification/sendDefenderAssignedNotifications.spec.ts @@ -9,7 +9,10 @@ import { DefendantNotificationType, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../../createTestingNotificationModule' import { Case } from '../../../../case' import { Defendant } from '../../../../defendant' @@ -36,6 +39,8 @@ type GivenWhenThen = ( describe('InternalNotificationController - Send defender assigned notifications', () => { const caseId = uuid() const defendantId = uuid() + + const { defender } = createTestUsers(['defender']) const court = { name: 'Héraðsdómur Reykjavíkur' } as Case['court'] let mockEmailService: EmailService @@ -91,8 +96,8 @@ describe('InternalNotificationController - Send defender assigned notifications' const defendant = { id: defendantId, defenderNationalId: '1234567890', - defenderName: 'Defender Name', - defenderEmail: 'ben10@omnitrix.is', + defenderName: defender.name, + defenderEmail: defender.email, isDefenderChoiceConfirmed: true, } as Defendant @@ -158,7 +163,7 @@ describe('InternalNotificationController - Send defender assigned notifications' describe('when sending defender assigned notification to unconfirmed defender', () => { const defendant = { id: defendantId, - defenderEmail: 'ben101@omnitrix.is', + defenderEmail: defender.email, isDefenderChoiceConfirmed: false, } as Defendant diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts index d439505025a9..2ff1ebb316c3 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts @@ -14,12 +14,14 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' import { DeliverResponse } from '../../models/deliver.response' -import { Notification } from '../../models/notification.model' import { notificationModuleConfig } from '../../notification.config' jest.mock('../../../../factories') @@ -37,21 +39,19 @@ type GivenWhenThen = ( describe('InternalNotificationController - Send defender assigned notifications', () => { const userId = uuid() + + const { defender } = createTestUsers(['defender']) + const court = { name: 'Héraðsdómur Reykjavíkur' } as Case['court'] let mockEmailService: EmailService let mockConfig: ConfigType - let mockNotificationModel: typeof Notification let givenWhenThen: GivenWhenThen let notificationDTO: CaseNotificationDto beforeEach(async () => { - const { - emailService, - notificationConfig, - notificationModel, - internalNotificationController, - } = await createTestingNotificationModule() + const { emailService, notificationConfig, internalNotificationController } = + await createTestingNotificationModule() notificationDTO = { user: { id: userId } as User, @@ -60,7 +60,6 @@ describe('InternalNotificationController - Send defender assigned notifications' mockEmailService = emailService mockConfig = notificationConfig - mockNotificationModel = notificationModel givenWhenThen = async ( caseId: string, @@ -90,8 +89,8 @@ describe('InternalNotificationController - Send defender assigned notifications' type: CaseType.ADMISSION_TO_FACILITY, court, courtCaseNumber: 'R-123/2022', - defenderEmail: 'recipient@gmail.com', - defenderName: 'John Doe', + defenderEmail: defender.email, + defenderName: defender.name, defenderNationalId: '1234567890', dateLogs: [{ date: new Date(), dateType: DateType.ARRAIGNMENT_DATE }], } as Case @@ -132,8 +131,8 @@ describe('InternalNotificationController - Send defender assigned notifications' type: CaseType.ADMISSION_TO_FACILITY, court, courtCaseNumber: 'R-123/2022', - defenderEmail: 'recipient@gmail.com', - defenderName: 'John Doe', + defenderEmail: defender.email, + defenderName: defender.name, dateLogs: [{ date: new Date(), dateType: DateType.ARRAIGNMENT_DATE }], } as Case @@ -173,8 +172,8 @@ describe('InternalNotificationController - Send defender assigned notifications' type: CaseType.PHONE_TAPPING, court, courtCaseNumber: 'R-123/2022', - defenderEmail: 'recipient@gmail.com', - defenderName: 'John Doe', + defenderEmail: defender.email, + defenderName: defender.name, dateLogs: [{ date: new Date(), dateType: DateType.ARRAIGNMENT_DATE }], } as Case diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealCompletedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealCompletedNotifications.spec.ts index bc9c0a684778..df4807508115 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealCompletedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealCompletedNotifications.spec.ts @@ -12,7 +12,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -29,25 +32,24 @@ type GivenWhenThen = ( ) => Promise describe('InternalNotificationController - Send appeal completed notifications', () => { + const { prosecutor, defender, judge, courtOfAppeals } = createTestUsers([ + 'prosecutor', + 'defender', + 'judge', + 'courtOfAppeals', + ]) const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const defenderName = uuid() - const defenderEmail = uuid() - const judgeName = uuid() - const judgeEmail = uuid() const courtCaseNumber = uuid() const appealCaseNumber = uuid() const courtId = uuid() - const courtOfAppealsEmail = uuid() let mockEmailService: EmailService let mockConfig: ConfigType let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_EMAILS = `{"4676f08b-aab4-4b4f-a366-697540788088":"${courtOfAppealsEmail}"}` + process.env.COURTS_EMAILS = `{"4676f08b-aab4-4b4f-a366-697540788088":"${courtOfAppeals.email}"}` const { emailService, notificationConfig, internalNotificationController } = await createTestingNotificationModule() @@ -71,12 +73,12 @@ describe('InternalNotificationController - Send appeal completed notifications', decision: CaseDecision.ACCEPTING, appealRulingDecision: appealRulingDecision ?? CaseAppealRulingDecision.ACCEPTING, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, - judge: { name: judgeName, email: judgeEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, + judge: { name: judge.name, email: judge.email }, court: { name: 'Héraðsdómur Reykjavíkur' }, defenderNationalId, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, appealCaseNumber, courtId: courtId, @@ -102,14 +104,14 @@ describe('InternalNotificationController - Send appeal completed notifications', it('should send notifications', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Úrskurður í landsréttarmáli ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur úrskurðað í máli ${appealCaseNumber} (héraðsdómsmál nr. ${courtCaseNumber}). Niðurstaða Landsréttar: Staðfest. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Úrskurður í landsréttarmáli ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur úrskurðað í máli ${appealCaseNumber} (héraðsdómsmál nr. ${courtCaseNumber}). Niðurstaða Landsréttar: Staðfest. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -140,7 +142,7 @@ describe('InternalNotificationController - Send appeal completed notifications', ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Úrskurður í landsréttarmáli ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur úrskurðað í máli ${appealCaseNumber} (héraðsdómsmál nr. ${courtCaseNumber}). Niðurstaða Landsréttar: Staðfest. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -159,7 +161,7 @@ describe('InternalNotificationController - Send appeal completed notifications', it('should send notification without a link to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Úrskurður í landsréttarmáli ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur úrskurðað í máli ${appealCaseNumber} (héraðsdómsmál nr. ${courtCaseNumber}). Niðurstaða Landsréttar: Staðfest. Hægt er að nálgast gögn málsins hjá Héraðsdómi Reykjavíkur ef þau hafa ekki þegar verið afhent.`, }), @@ -178,14 +180,14 @@ describe('InternalNotificationController - Send appeal completed notifications', it('should send notification about discontinuance', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Niðurfelling máls ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur móttekið afturköllun á kæru í máli ${courtCaseNumber}. Landsréttarmálið ${appealCaseNumber} hefur verið fellt niður. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Niðurfelling máls ${appealCaseNumber} (${courtCaseNumber})`, html: `Landsréttur hefur móttekið afturköllun á kæru í máli ${courtCaseNumber}. Landsréttarmálið ${appealCaseNumber} hefur verið fellt niður.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealFilesUpdatedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealFilesUpdatedNotifications.spec.ts index 1cc6f4f9e31a..d50f5d2a5707 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealFilesUpdatedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealFilesUpdatedNotifications.spec.ts @@ -8,7 +8,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -21,22 +24,17 @@ interface Then { type GivenWhenThen = (defenderNationalId?: string) => Promise describe('InternalNotificationController - Send appeal case files updated notifications', () => { + const { assistant, judge1, judge2, judge3 } = createTestUsers([ + 'assistant', + 'judge1', + 'judge2', + 'judge3', + ]) const userId = uuid() const caseId = uuid() const courtCaseNumber = uuid() const appealCaseNumber = uuid() const receivedDate = new Date() - const assistantName = uuid() - const assistantEmail = uuid() - const judgeName1 = uuid() - const judgeEmail1 = uuid() - const judgeId1 = uuid() - const judgeName2 = uuid() - const judgeEmail2 = uuid() - const judgeId2 = uuid() - const judgeName3 = uuid() - const judgeEmail3 = uuid() - const judgeId3 = uuid() let mockEmailService: EmailService let givenWhenThen: GivenWhenThen @@ -59,27 +57,26 @@ describe('InternalNotificationController - Send appeal case files updated notifi appealCaseNumber, appealReceivedByCourtDate: receivedDate, appealAssistant: { - name: assistantName, - email: assistantEmail, + name: assistant.name, + email: assistant.email, role: UserRole.COURT_OF_APPEALS_ASSISTANT, }, appealJudge1: { - name: judgeName1, - email: judgeEmail1, - id: judgeId1, + name: judge1.name, + email: judge1.email, + id: judge1.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, - appealJudge1Id: judgeId1, appealJudge2: { - name: judgeName2, - email: judgeEmail2, - id: judgeId2, + name: judge2.name, + email: judge2.email, + id: judge2.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, appealJudge3: { - name: judgeName3, - email: judgeEmail3, - id: judgeId3, + name: judge3.name, + email: judge3.email, + id: judge3.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, } as Case, @@ -104,7 +101,7 @@ describe('InternalNotificationController - Send appeal case files updated notifi it('should send notification to the assigned court of appeal judges and assistant', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: assistantName, address: assistantEmail }], + to: [{ name: assistant.name, address: assistant.email }], subject: `Ný gögn í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Ný gögn hafa borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -113,8 +110,8 @@ describe('InternalNotificationController - Send appeal case files updated notifi expect.objectContaining({ to: [ { - name: judgeName1, - address: judgeEmail1, + name: judge1.name, + address: judge1.email, }, ], subject: `Ný gögn í máli ${courtCaseNumber} (${appealCaseNumber})`, @@ -123,14 +120,14 @@ describe('InternalNotificationController - Send appeal case files updated notifi ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName2, address: judgeEmail2 }], + to: [{ name: judge2.name, address: judge2.email }], subject: `Ný gögn í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Ný gögn hafa borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName3, address: judgeEmail3 }], + to: [{ name: judge3.name, address: judge3.email }], subject: `Ný gögn í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Ný gögn hafa borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealJudgesAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealJudgesAssignedNotifications.spec.ts index b455d775f174..cf73dbe84fcd 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealJudgesAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealJudgesAssignedNotifications.spec.ts @@ -8,7 +8,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -21,21 +24,16 @@ interface Then { type GivenWhenThen = (defenderNationalId?: string) => Promise describe('InternalNotificationController - Send appeal judges assigned notifications', () => { + const { judge1, judge2, judge3, assistant } = createTestUsers([ + 'judge1', + 'judge2', + 'judge3', + 'assistant', + ]) const userId = uuid() const caseId = uuid() const appealCaseNumber = uuid() const receivedDate = new Date() - const assistantName = uuid() - const assistantEmail = uuid() - const judgeName1 = uuid() - const judgeEmail1 = uuid() - const judgeId1 = uuid() - const judgeName2 = uuid() - const judgeEmail2 = uuid() - const judgeId2 = uuid() - const judgeName3 = uuid() - const judgeEmail3 = uuid() - const judgeId3 = uuid() let mockEmailService: EmailService let givenWhenThen: GivenWhenThen @@ -57,27 +55,27 @@ describe('InternalNotificationController - Send appeal judges assigned notificat appealCaseNumber, appealReceivedByCourtDate: receivedDate, appealAssistant: { - name: assistantName, - email: assistantEmail, + name: assistant.name, + email: assistant.email, role: UserRole.COURT_OF_APPEALS_ASSISTANT, }, appealJudge1: { - name: judgeName1, - email: judgeEmail1, - id: judgeId1, + name: judge1.name, + email: judge1.email, + id: judge1.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, - appealJudge1Id: judgeId1, + appealJudge1Id: judge1.id, appealJudge2: { - name: judgeName2, - email: judgeEmail2, - id: judgeId2, + name: judge2.name, + email: judge2.email, + id: judge2.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, appealJudge3: { - name: judgeName3, - email: judgeEmail3, - id: judgeId3, + name: judge3.name, + email: judge3.email, + id: judge3.id, role: UserRole.COURT_OF_APPEALS_JUDGE, }, } as Case, @@ -102,17 +100,17 @@ describe('InternalNotificationController - Send appeal judges assigned notificat it('should send notification to the judge foreperson, the two other judges and the judges assistant', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: assistantName, address: assistantEmail }], + to: [{ name: assistant.name, address: assistant.email }], subject: `Úthlutun máls nr. ${appealCaseNumber}`, - html: `Landsréttur hefur skráð þig sem aðstoðarmann dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judgeName1}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, + html: `Landsréttur hefur skráð þig sem aðstoðarmann dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judge1.name}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ to: [ { - name: judgeName1, - address: judgeEmail1, + name: judge1.name, + address: judge1.email, }, ], subject: `Úthlutun máls nr. ${appealCaseNumber}`, @@ -121,16 +119,16 @@ describe('InternalNotificationController - Send appeal judges assigned notificat ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName2, address: judgeEmail2 }], + to: [{ name: judge2.name, address: judge2.email }], subject: `Úthlutun máls nr. ${appealCaseNumber}`, - html: `Landsréttur hefur skráð þig sem dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judgeName1}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, + html: `Landsréttur hefur skráð þig sem dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judge1.name}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName3, address: judgeEmail3 }], + to: [{ name: judge3.name, address: judge3.email }], subject: `Úthlutun máls nr. ${appealCaseNumber}`, - html: `Landsréttur hefur skráð þig sem dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judgeName1}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, + html: `Landsréttur hefur skráð þig sem dómara í máli nr. ${appealCaseNumber}. Dómsformaður er ${judge1.name}. Þú getur nálgast yfirlit málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(then.result).toEqual({ delivered: true }) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealReceivedByCourtNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealReceivedByCourtNotifications.spec.ts index 27a132aa27a7..96a4f7fe0016 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealReceivedByCourtNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealReceivedByCourtNotifications.spec.ts @@ -10,7 +10,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -23,14 +26,14 @@ interface Then { type GivenWhenThen = (defenderNationalId?: string) => Promise describe('InternalNotificationController - Send appeal received by court notifications', () => { - const courtOfAppealsEmail = uuid() + const { coa, defender, prosecutor } = createTestUsers([ + 'coa', + 'defender', + 'prosecutor', + ]) + const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const prosecutorMobileNumber = uuid() - const defenderName = uuid() - const defenderEmail = uuid() const courtCaseNumber = uuid() const receivedDate = new Date() @@ -40,7 +43,7 @@ describe('InternalNotificationController - Send appeal received by court notific let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_EMAILS = `{"4676f08b-aab4-4b4f-a366-697540788088":"${courtOfAppealsEmail}"}` + process.env.COURTS_EMAILS = `{"4676f08b-aab4-4b4f-a366-697540788088":"${coa.email}"}` const { emailService, smsService, internalNotificationController } = await createTestingNotificationModule() @@ -57,14 +60,14 @@ describe('InternalNotificationController - Send appeal received by court notific { id: caseId, prosecutor: { - name: prosecutorName, - email: prosecutorEmail, - mobileNumber: prosecutorMobileNumber, + name: prosecutor.name, + email: prosecutor.email, + mobileNumber: prosecutor.mobile, }, court: { name: 'Héraðsdómur Reykjavíkur' }, defenderNationalId, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, appealReceivedByCourtDate: receivedDate, } as Case, @@ -92,7 +95,7 @@ describe('InternalNotificationController - Send appeal received by court notific to: [ { name: 'Landsréttur', - address: courtOfAppealsEmail, + address: coa.email, }, ], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, @@ -104,7 +107,7 @@ describe('InternalNotificationController - Send appeal received by court notific ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, html: `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), @@ -114,7 +117,7 @@ describe('InternalNotificationController - Send appeal received by court notific ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, html: `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), @@ -127,7 +130,7 @@ describe('InternalNotificationController - Send appeal received by court notific it('should send sms notification to prosecutor', () => { expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [prosecutorMobileNumber], + [prosecutor.mobile], `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), 'PPPp', @@ -146,7 +149,7 @@ describe('InternalNotificationController - Send appeal received by court notific it('should send notification to prosecutor and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, html: `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), @@ -156,7 +159,7 @@ describe('InternalNotificationController - Send appeal received by court notific ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Upplýsingar vegna kæru í máli ${courtCaseNumber}`, html: `Kæra í máli ${courtCaseNumber} hefur borist Landsrétti. Frestur til að skila greinargerð er til ${formatDate( getStatementDeadline(receivedDate), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealStatementNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealStatementNotifications.spec.ts index 0f043505f796..a920284e6a9e 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealStatementNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealStatementNotifications.spec.ts @@ -9,7 +9,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -26,22 +29,22 @@ type GivenWhenThen = ( ) => Promise describe('InternalNotificationController - Send appeal statement notifications', () => { + const roles = [ + 'prosecutor', + 'defender', + 'assistant', + 'judge1', + 'judge2', + 'judge3', + ] + + const { prosecutor, defender, assistant, judge1, judge2, judge3 } = + createTestUsers(roles) + const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const defenderName = uuid() - const defenderEmail = uuid() const courtCaseNumber = uuid() const receivedDate = new Date() const appealCaseNumber = uuid() - const assistantName = uuid() - const assistantEmail = uuid() - const judgeName1 = uuid() - const judgeEmail1 = uuid() - const judgeName2 = uuid() - const judgeEmail2 = uuid() - const judgeName3 = uuid() - const judgeEmail3 = uuid() let mockEmailService: EmailService @@ -65,16 +68,16 @@ describe('InternalNotificationController - Send appeal statement notifications', caseId, { id: caseId, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, court: { name: 'Héraðsdómur Reykjavíkur' }, defenderNationalId, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, appealReceivedByCourtDate: receivedDate, appealCaseNumber, - appealAssistant: { name: assistantName, email: assistantEmail }, - appealJudge1: { name: judgeName1, email: judgeEmail1 }, + appealAssistant: { name: assistant.name, email: assistant.email }, + appealJudge1: { name: judge1.name, email: judge1.email }, } as Case, { user, @@ -110,13 +113,13 @@ describe('InternalNotificationController - Send appeal statement notifications', }) it('should send notification to appeals court and defender', () => { - expectCourtEmail(assistantName, assistantEmail) - expectCourtEmail(judgeName1, judgeEmail1) - expectCourtEmail(judgeName2, judgeEmail2) - expectCourtEmail(judgeName3, judgeEmail3) + expectCourtEmail(assistant.name, assistant.email) + expectCourtEmail(judge1.name, judge1.email) + expectCourtEmail(judge2.name, judge2.email) + expectCourtEmail(judge3.name, judge3.email) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný greinargerð í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -140,13 +143,13 @@ describe('InternalNotificationController - Send appeal statement notifications', }) it('should send notification to appeals court and defender', () => { - expectCourtEmail(assistantName, assistantEmail) - expectCourtEmail(judgeName1, judgeEmail1) - expectCourtEmail(judgeName2, judgeEmail2) - expectCourtEmail(judgeName3, judgeEmail3) + expectCourtEmail(assistant.name, assistant.email) + expectCourtEmail(judge1.name, judge1.email) + expectCourtEmail(judge2.name, judge2.email) + expectCourtEmail(judge3.name, judge3.email) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný greinargerð í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins hjá Héraðsdómi Reykjavíkur ef þau hafa ekki þegar verið afhent.`, }), @@ -167,13 +170,13 @@ describe('InternalNotificationController - Send appeal statement notifications', }) it('should send notification to appeals court and prosecutor', () => { - expectCourtEmail(assistantName, assistantEmail) - expectCourtEmail(judgeName1, judgeEmail1) - expectCourtEmail(judgeName2, judgeEmail2) - expectCourtEmail(judgeName3, judgeEmail3) + expectCourtEmail(assistant.name, assistant.email) + expectCourtEmail(judge1.name, judge1.email) + expectCourtEmail(judge2.name, judge2.email) + expectCourtEmail(judge3.name, judge3.email) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Ný greinargerð í máli ${courtCaseNumber} (${appealCaseNumber})`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber} (Landsréttarmál nr. ${appealCaseNumber}). Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -198,7 +201,7 @@ describe('InternalNotificationController - Send appeal statement notifications', it('should send notification to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný greinargerð í máli ${courtCaseNumber}`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -220,7 +223,7 @@ describe('InternalNotificationController - Send appeal statement notifications', it('should send notification to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný greinargerð í máli ${courtCaseNumber}`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins hjá Héraðsdómi Reykjavíkur ef þau hafa ekki þegar verið afhent.`, }), @@ -239,7 +242,7 @@ describe('InternalNotificationController - Send appeal statement notifications', it('should send notification to prosecutor', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Ný greinargerð í máli ${courtCaseNumber}`, html: `Greinargerð hefur borist vegna kæru í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealToCourtOfAppealsNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealToCourtOfAppealsNotifications.spec.ts index 2119328928c1..ae50c9e3c325 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealToCourtOfAppealsNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealToCourtOfAppealsNotifications.spec.ts @@ -10,7 +10,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -23,20 +26,17 @@ interface Then { type GivenWhenThen = (user: User, defenderNationalId?: string) => Promise describe('InternalNotificationController - Send appeal to court of appeals notifications', () => { + const { prosecutor, judge, registrar, defender, court } = createTestUsers([ + 'prosecutor', + 'judge', + 'registrar', + 'defender', + 'court', + ]) + const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const prosecutorMobileNumber = uuid() - const judgeName = uuid() - const judgeEmail = uuid() - const registrarName = uuid() - const registrarEmail = uuid() - const defenderName = uuid() - const defenderEmail = uuid() + const courtCaseNumber = uuid() - const courtId = uuid() - const courtEmail = uuid() - const courtMobileNumber = uuid() let mockEmailService: EmailService let mockSmsService: SmsService @@ -44,8 +44,8 @@ describe('InternalNotificationController - Send appeal to court of appeals notif let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_ASSISTANT_MOBILE_NUMBERS = `{"${courtId}": "${courtMobileNumber}"}` - process.env.COURTS_EMAILS = `{"${courtId}": "${courtEmail}"}` + process.env.COURTS_ASSISTANT_MOBILE_NUMBERS = `{"${court.id}": "${court.mobile}"}` + process.env.COURTS_EMAILS = `{"${court.id}": "${court.email}"}` const { emailService, smsService, internalNotificationController } = await createTestingNotificationModule() @@ -62,18 +62,18 @@ describe('InternalNotificationController - Send appeal to court of appeals notif { id: caseId, prosecutor: { - name: prosecutorName, - email: prosecutorEmail, - mobileNumber: prosecutorMobileNumber, + name: prosecutor.name, + email: prosecutor.email, + mobileNumber: prosecutor.mobile, }, - judge: { name: judgeName, email: judgeEmail }, - registrar: { name: registrarName, email: registrarEmail }, + judge: { name: judge.name, email: judge.email }, + registrar: { name: registrar.name, email: registrar.email }, court: { name: 'Héraðsdómur Reykjavíkur' }, defenderNationalId, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, - courtId: courtId, + courtId: court.id, } as Case, { user, @@ -102,35 +102,35 @@ describe('InternalNotificationController - Send appeal to court of appeals notif it('should send notification to judge, registrar, court and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: registrarName, address: registrarEmail }], + to: [{ name: registrar.name, address: registrar.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: 'Héraðsdómur Reykjavíkur', address: courtEmail }], + to: [{ name: 'Héraðsdómur Reykjavíkur', address: court.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], + [court.mobile], `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Sjá nánar á rettarvorslugatt.island.is`, ) expect(then.result).toEqual({ delivered: true }) @@ -150,20 +150,20 @@ describe('InternalNotificationController - Send appeal to court of appeals notif it('should send notification to judge and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins hjá Héraðsdómi Reykjavíkur ef þau hafa ekki þegar verið afhent.`, }), ) expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], + [court.mobile], `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Sjá nánar á rettarvorslugatt.island.is`, ) expect(then.result).toEqual({ delivered: true }) @@ -180,24 +180,24 @@ describe('InternalNotificationController - Send appeal to court of appeals notif it('should send notifications to judge and prosecutor', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Kæra í máli ${courtCaseNumber}`, html: `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], + [court.mobile], `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Sjá nánar á rettarvorslugatt.island.is`, ) expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [prosecutorMobileNumber], + [prosecutor.mobile], `Úrskurður hefur verið kærður í máli ${courtCaseNumber}. Sjá nánar á rettarvorslugatt.island.is`, ) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealWithdrawnNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealWithdrawnNotifications.spec.ts index 1b2be1516b1e..bc96d0d487d9 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealWithdrawnNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAppealWithdrawnNotifications.spec.ts @@ -5,11 +5,15 @@ import { EmailService } from '@island.is/email-service' import { CaseNotificationType, InstitutionType, + NotificationType, User, UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -27,26 +31,33 @@ type GivenWhenThen = ( ) => Promise describe('InternalNotificationController - Send appeal withdrawn notifications', () => { - const courtOfAppealsEmail = uuid() - const courtEmail = uuid() + const { + courtOfAppeals, + court, + judge, + prosecutor, + defender, + registrar, + appealAssistant, + appealJudge1, + } = createTestUsers([ + 'courtOfAppeals', + 'court', + 'judge', + 'prosecutor', + 'defender', + 'registrar', + 'appealAssistant', + 'appealJudge1', + ]) + + const courtOfAppealsEmail = courtOfAppeals.email + const courtEmail = court.email const courtId = uuid() const userId = uuid() const caseId = uuid() - const judgeName = uuid() - const judgeEmail = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const prosecutorMobileNumber = uuid() - const defenderName = uuid() - const defenderEmail = uuid() const courtCaseNumber = uuid() const receivedDate = new Date() - const registrarEmail = uuid() - const registrarName = uuid() - const appealAssistantName = uuid() - const appealAssistantEmail = uuid() - const appealJudge1Name = uuid() - const appealJudge1Email = uuid() let mockEmailService: EmailService let mockNotificationModel: typeof Notification @@ -74,22 +85,25 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', { id: caseId, prosecutor: { - name: prosecutorName, - email: prosecutorEmail, - mobileNumber: prosecutorMobileNumber, + name: prosecutor.name, + email: prosecutor.email, + mobileNumber: prosecutor.mobile, }, court: { name: 'Héraðsdómur Reykjavíkur', id: courtId }, - defenderName: defenderName, - defenderEmail: defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, courtCaseNumber, appealReceivedByCourtDate, appealAssistant: { - name: appealAssistantName, - email: appealAssistantEmail, + name: appealAssistant.name, + email: appealAssistant.email, + }, + judge: { name: judge.name, email: judge.email }, + appealJudge1: { + name: appealJudge1.name, + email: appealJudge1.email, }, - judge: { name: judgeName, email: judgeEmail }, - appealJudge1: { name: appealJudge1Name, email: appealJudge1Email }, - registrar: { name: registrarName, email: registrarEmail }, + registrar: { name: registrar.name, email: registrar.email }, notifications, } as Case, { @@ -116,7 +130,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Sækjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -140,7 +154,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to appeal assistant ', () => { expect(mockEmailService.sendEmail).not.toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: appealAssistantName, address: appealAssistantEmail }], + to: [{ name: appealAssistant.name, address: appealAssistant.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Sækjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -158,14 +172,14 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', then = await givenWhenThen(UserRole.PROSECUTOR, receivedDate, [ { caseId, - type: CaseNotificationType.APPEAL_JUDGES_ASSIGNED, + type: NotificationType.APPEAL_JUDGES_ASSIGNED, } as Notification, ]) }) it('should send notification to defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Sækjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -189,7 +203,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to appeal assistant ', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: appealAssistantName, address: appealAssistantEmail }], + to: [{ name: appealAssistant.name, address: appealAssistant.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Sækjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -198,7 +212,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to appeal judges', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: appealJudge1Name, address: appealJudge1Email }], + to: [{ name: appealJudge1.name, address: appealJudge1.email }], }), ) }) @@ -214,7 +228,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to prosecutor', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Verjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -226,7 +240,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to judge', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Verjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), @@ -236,7 +250,7 @@ describe('InternalNotificationController - Send appeal withdrawn notifications', it('should send notification to registrar', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: registrarName, address: registrarEmail }], + to: [{ name: registrar.name, address: registrar.email }], subject: `Afturköllun kæru í máli ${courtCaseNumber}`, html: `Verjandi hefur afturkallað kæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCaseFilesUpdatedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCaseFilesUpdatedNotifications.spec.ts index 21df0ee723b4..d57f0ea344f2 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCaseFilesUpdatedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCaseFilesUpdatedNotifications.spec.ts @@ -10,7 +10,10 @@ import { UserRole, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { DeliverResponse } from '../../models/deliver.response' @@ -23,18 +26,15 @@ interface Then { type GivenWhenThen = (user: User) => Promise describe('InternalNotificationController - Send case files updated notifications', () => { + const { prosecutor, judge, defender, spokesperson } = createTestUsers([ + 'prosecutor', + 'judge', + 'defender', + 'spokesperson', + ]) + const caseId = uuid() const courtCaseNumber = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const judgeName = uuid() - const judgeEmail = uuid() - const defenderNationalId = uuid() - const defenderName = uuid() - const defenderEmail = uuid() - const spokespersonNationalId = uuid() - const spokespersonName = uuid() - const spokespersonEmail = uuid() let mockEmailService: EmailService let givenWhenThen: GivenWhenThen @@ -55,15 +55,21 @@ describe('InternalNotificationController - Send case files updated notifications type: CaseType.INDICTMENT, courtCaseNumber, court: { name: 'Héraðsdómur Reykjavíkur' }, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, - judge: { name: judgeName, email: judgeEmail }, - defendants: [{ defenderNationalId, defenderName, defenderEmail }], + prosecutor: { name: prosecutor.name, email: prosecutor.email }, + judge: { name: judge.name, email: judge.email }, + defendants: [ + { + defenderNationalId: defender.nationalId, + defenderName: defender.name, + defenderEmail: defender.email, + }, + ], civilClaimants: [ { hasSpokesperson: true, - spokespersonNationalId, - spokespersonName, - spokespersonEmail, + spokespersonNationalId: spokesperson.nationalId, + spokespersonName: spokesperson.name, + spokespersonEmail: spokesperson.email, }, ], } as Case, @@ -92,21 +98,21 @@ describe('InternalNotificationController - Send case files updated notifications it('should send notifications', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: spokespersonName, address: spokespersonEmail }], + to: [{ name: spokesperson.name, address: spokesperson.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -127,21 +133,21 @@ describe('InternalNotificationController - Send case files updated notifications it('should send notifications', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: judgeName, address: judgeEmail }], + to: [{ name: judge.name, address: judge.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: spokespersonName, address: spokespersonEmail }], + to: [{ name: spokesperson.name, address: spokesperson.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Ný gögn í máli ${courtCaseNumber}`, html: `Ný gögn hafa borist vegna máls ${courtCaseNumber}. Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCourtDateNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCourtDateNotifications.spec.ts index 18bf5d712b71..6cb820f993dc 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCourtDateNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendCourtDateNotifications.spec.ts @@ -6,10 +6,14 @@ import { CaseNotificationType, CaseType, DateType, + NotificationType, User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -31,13 +35,12 @@ type GivenWhenThen = ( describe('InternalNotificationController - Send court date notifications', () => { const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() - const defenderName = uuid() - const defenderEmail = uuid() + const courtName = 'Héraðsdómur Reykjavíkur' const courtCaseNumber = uuid() + const { prosecutor, defender } = createTestUsers(['prosecutor', 'defender']) + let mockEmailService: EmailService let mockNotificationModel: typeof Notification let givenWhenThen: GivenWhenThen @@ -75,11 +78,11 @@ describe('InternalNotificationController - Send court date notifications', () => const theCase = { id: caseId, type: CaseType.CUSTODY, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, court: { name: courtName }, courtCaseNumber, - defenderName, - defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, } as Case beforeEach(async () => { @@ -89,23 +92,23 @@ describe('InternalNotificationController - Send court date notifications', () => it('should send notifications to prosecutor and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Fyrirtaka í máli: ${courtCaseNumber}`, - html: `Héraðsdómur Reykjavíkur hefur staðfest fyrirtökutíma fyrir kröfu um gæsluvarðhald.

Fyrirtaka mun fara fram á ótilgreindum tíma.

Dómsalur hefur ekki verið skráður.

Dómari hefur ekki verið skráður.

Verjandi sakbornings: ${defenderName}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, + html: `Héraðsdómur Reykjavíkur hefur staðfest fyrirtökutíma fyrir kröfu um gæsluvarðhald.

Fyrirtaka mun fara fram á ótilgreindum tíma.

Dómsalur hefur ekki verið skráður.

Dómari hefur ekki verið skráður.

Verjandi sakbornings: ${defender.name}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Fyrirtaka í máli ${courtCaseNumber}`, - html: `Héraðsdómur Reykjavíkur hefur boðað þig í fyrirtöku sem verjanda sakbornings.

Fyrirtaka mun fara fram á ótilgreindum tíma.

Málsnúmer: ${courtCaseNumber}.

Dómsalur hefur ekki verið skráður.

Dómari: .

Sækjandi: ${prosecutorName} ().`, + html: `Héraðsdómur Reykjavíkur hefur boðað þig í fyrirtöku sem verjanda sakbornings.

Fyrirtaka mun fara fram á ótilgreindum tíma.

Málsnúmer: ${courtCaseNumber}.

Dómsalur hefur ekki verið skráður.

Dómari: .

Sækjandi: ${prosecutor.name} ().`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Yfirlit máls ${courtCaseNumber}`, }), ) @@ -125,11 +128,11 @@ describe('InternalNotificationController - Send court date notifications', () => const theCase = { id: caseId, type: CaseType.CUSTODY, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, court: { name: courtName }, courtCaseNumber, - defenderName, - defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, } as Case beforeEach(async () => { @@ -142,8 +145,8 @@ describe('InternalNotificationController - Send court date notifications', () => notifications: [ { caseId, - type: CaseNotificationType.READY_FOR_COURT, - recipients: [{ address: defenderEmail, success: true }], + type: NotificationType.READY_FOR_COURT, + recipients: [{ address: defender.email, success: true }], }, ], } as Case, @@ -154,7 +157,7 @@ describe('InternalNotificationController - Send court date notifications', () => it('should not send link to case to defender', () => { expect(mockEmailService.sendEmail).not.toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Yfirlit máls ${courtCaseNumber}`, }), ) @@ -175,11 +178,11 @@ describe('InternalNotificationController - Send court date notifications', () => const theCase = { id: caseId, type: CaseType.INDICTMENT, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, defendants: [ { - defenderName, - defenderEmail, + defenderName: defender.name, + defenderEmail: defender.email, }, ], court: { name: courtName }, @@ -194,7 +197,7 @@ describe('InternalNotificationController - Send court date notifications', () => it('should send notifications to prosecutor and defender', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Nýtt þinghald í máli ${courtCaseNumber}`, html: `Héraðsdómur Reykjavíkur boðar til þinghalds í máli ${courtCaseNumber}.
Fyrirtaka mun fara fram 2. maí 2024, kl. 14:32.

Tegund þinghalds: Óþekkt.

Dómsalur hefur ekki verið skráður.

Dómari hefur ekki verið skráður.

Hægt er að nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -202,7 +205,7 @@ describe('InternalNotificationController - Send court date notifications', () => expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: defenderName, address: defenderEmail }], + to: [{ name: defender.name, address: defender.email }], subject: `Nýtt þinghald í máli ${courtCaseNumber}`, html: `Héraðsdómur Reykjavíkur boðar til þinghalds í máli ${courtCaseNumber}.
Fyrirtaka mun fara fram 2. maí 2024, kl. 14:32.

Tegund þinghalds: Óþekkt.

Dómsalur hefur ekki verið skráður.

Dómari hefur ekki verið skráður.

Hægt er að nálgast gögn málsins hjá Héraðsdómur Reykjavíkur.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendDefendantsNotUpdatedAtCourtNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendDefendantsNotUpdatedAtCourtNotifications.spec.ts index b7090ce986dc..b0448904d07f 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendDefendantsNotUpdatedAtCourtNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendDefendantsNotUpdatedAtCourtNotifications.spec.ts @@ -2,9 +2,16 @@ import { uuid } from 'uuidv4' import { EmailService } from '@island.is/email-service' -import { CaseNotificationType, User } from '@island.is/judicial-system/types' +import { + CaseNotificationType, + NotificationType, + User, +} from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -27,14 +34,15 @@ describe('InternalNotificationController - Send defendants not updated at court user: { id: userId } as User, type: CaseNotificationType.DEFENDANTS_NOT_UPDATED_AT_COURT, } + + const { registrar } = createTestUsers(['registrar', 'defender']) const caseId = uuid() const courtCaseNumber = uuid() - const registrarName = uuid() - const registrarEmail = uuid() + const theCase = { id: caseId, courtCaseNumber, - registrar: { name: registrarName, email: registrarEmail }, + registrar: { name: registrar.name, email: registrar.email }, } as Case let mockEmailService: EmailService @@ -72,7 +80,7 @@ describe('InternalNotificationController - Send defendants not updated at court it('should send email', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: registrarName, address: registrarEmail }], + to: [{ name: registrar.name, address: registrar.email }], subject: `Skráning varnaraðila/verjenda í máli ${courtCaseNumber}`, html: `Ekki tókst að skrá varnaraðila/verjendur í máli ${courtCaseNumber} í Auði. Yfirfara þarf málið í Auði og skrá rétta aðila áður en því er lokað.`, }), @@ -91,8 +99,8 @@ describe('InternalNotificationController - Send defendants not updated at court ...theCase, notifications: [ { - type: CaseNotificationType.DEFENDANTS_NOT_UPDATED_AT_COURT, - recipients: [{ address: registrarEmail, success: true }], + type: NotificationType.DEFENDANTS_NOT_UPDATED_AT_COURT, + recipients: [{ address: registrar.email, success: true }], }, ], } as Case, diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentDeniedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentDeniedNotifications.spec.ts index 904b873650f9..7efd487f6146 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentDeniedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentDeniedNotifications.spec.ts @@ -8,7 +8,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -29,8 +32,10 @@ type GivenWhenThen = ( describe('InternalNotificationController - Send indictment denied notification', () => { const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() + + const { prosecutor } = createTestUsers(['prosecutor']) + const prosecutorName = prosecutor.name + const prosecutorEmail = prosecutor.email const policeCaseNumbers = [uuid(), uuid()] let mockEmailService: EmailService diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentReturnedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentReturnedNotifications.spec.ts index 24a9ddba4558..ee1f6bd106bb 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentReturnedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentReturnedNotifications.spec.ts @@ -8,7 +8,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -27,10 +30,10 @@ type GivenWhenThen = ( ) => Promise describe('InternalNotificationController - Send indictment returned notification', () => { + const { prosecutor } = createTestUsers(['prosecutor']) const userId = uuid() const caseId = uuid() - const prosecutorName = uuid() - const prosecutorEmail = uuid() + const policeCaseNumbers = [uuid(), uuid()] const courtName = uuid() @@ -69,7 +72,7 @@ describe('InternalNotificationController - Send indictment returned notification const theCase = { id: caseId, type: CaseType.INDICTMENT, - prosecutor: { name: prosecutorName, email: prosecutorEmail }, + prosecutor: { name: prosecutor.name, email: prosecutor.email }, policeCaseNumbers, court: { name: courtName }, } as Case @@ -81,7 +84,7 @@ describe('InternalNotificationController - Send indictment returned notification it('should send notifications to prosecutor', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName, address: prosecutorEmail }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: `Ákæra endursend í máli ${policeCaseNumbers[0]}`, html: `${courtName} hefur endursent ákæru vegna lögreglumáls ${policeCaseNumbers[0]}. Þú getur nálgast samantekt málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts index d3992015ded4..3c20d0d96a26 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts @@ -4,7 +4,10 @@ import { EmailService } from '@island.is/email-service' import { InstitutionNotificationType } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { InternalCaseService } from '../../../case' import { UserService } from '../../../user' @@ -18,10 +21,11 @@ interface Then { type GivenWhenThen = () => Promise describe('InternalNotificationController - Send indictments waiting for confirmation notifications', () => { - const prosecutorName1 = uuid() - const prosecutorEmail1 = uuid() - const prosecutorName2 = uuid() - const prosecutorEmail2 = uuid() + const { prosecutor1, prosecutor2 } = createTestUsers([ + 'prosecutor1', + 'prosecutor2', + ]) + const prosecutorsOfficeId = uuid() let mockUserService: UserService let mockInternalCaseService: InternalCaseService @@ -116,25 +120,25 @@ describe('InternalNotificationController - Send indictments waiting for confirma const mockGetUsersWhoCanConfirmIndictments = mockUserService.getUsersWhoCanConfirmIndictments as jest.Mock mockGetUsersWhoCanConfirmIndictments.mockResolvedValueOnce([ - { name: prosecutorName1, email: prosecutorEmail1 }, - { name: prosecutorName2, email: prosecutorEmail2 }, + { name: prosecutor1.name, email: prosecutor1.email }, + { name: prosecutor2.name, email: prosecutor2.email }, ]) then = await givenWhenThen() }) - it('should not send messages', () => { + it('should send messages', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(2) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName1, address: prosecutorEmail1 }], + to: [{ name: prosecutor1.name, address: prosecutor1.email }], subject: 'Ákærur bíða staðfestingar', html: 'Í Réttarvörslugátt bíða 2 ákærur staðfestingar.

Hægt er að nálgast yfirlit og staðfesta ákærur í Réttarvörslugátt.', }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: prosecutorName2, address: prosecutorEmail2 }], + to: [{ name: prosecutor2.name, address: prosecutor2.email }], subject: 'Ákærur bíða staðfestingar', html: 'Í Réttarvörslugátt bíða 2 ákærur staðfestingar.

Hægt er að nálgast yfirlit og staðfesta ákærur í Réttarvörslugátt.', }), diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendReadyForCourtNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendReadyForCourtNotifications.spec.ts index e1b3b2c1a4c6..f9757746c4ad 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendReadyForCourtNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendReadyForCourtNotifications.spec.ts @@ -15,11 +15,15 @@ import { CaseType, DateType, IndictmentSubtype, + NotificationType, RequestSharedWithDefender, User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { randomDate } from '../../../../test' import { Case } from '../../../case' @@ -44,23 +48,29 @@ describe('InternalNotificationController - Send ready for court notifications fo const userId = uuid() const caseId = uuid() const policeCaseNumber = uuid() - const courtId = uuid() const courtCaseNumber = uuid() + + const { prosecutor, defender, testCourt } = createTestUsers([ + 'prosecutor', + 'defender', + 'testCourt', + ]) + const theCase = { id: caseId, type: CaseType.CUSTODY, state: CaseState.RECEIVED, policeCaseNumbers: [policeCaseNumber], prosecutor: { - name: 'Derrick', - email: 'derrick@dummy.is', + name: prosecutor.name, + email: prosecutor.email, }, - courtId, + courtId: testCourt.id, court: { name: 'Héraðsdómur Reykjavíkur' }, courtCaseNumber, - defenderNationalId: uuid(), - defenderName: 'Saul Goodman', - defenderEmail: 'saul@dummy.is', + defenderNationalId: defender.nationalId, + defenderName: defender.name, + defenderEmail: defender.email, requestSharedWithDefender: RequestSharedWithDefender.COURT_DATE, prosecutorsOffice: { name: 'Héraðsdómur Derricks' }, dateLogs: [{ date: randomDate(), dateType: DateType.ARRAIGNMENT_DATE }], @@ -69,7 +79,6 @@ describe('InternalNotificationController - Send ready for court notifications fo user: { id: userId } as User, type: CaseNotificationType.READY_FOR_COURT, } - const courtMobileNumber = uuid() let mockEmailService: EmailService let mockSmsService: SmsService @@ -77,7 +86,7 @@ describe('InternalNotificationController - Send ready for court notifications fo let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_MOBILE_NUMBERS = `{"${courtId}": "${courtMobileNumber}"}` + process.env.COURTS_MOBILE_NUMBERS = `{"${testCourt.id}": "${testCourt.mobile}"}` const { emailService, @@ -119,7 +128,7 @@ describe('InternalNotificationController - Send ready for court notifications fo name: mockNotificationConfig.email.replyToName, address: mockNotificationConfig.email.replyToEmail, }, - to: [{ name: 'Derrick', address: 'derrick@dummy.is' }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: 'Krafa um gæsluvarðhald send', text: `Þú hefur sent kröfu á Héraðsdóm Reykjavíkur vegna LÖKE máls ${policeCaseNumber}. Skjalið er aðgengilegt undir málinu í Réttarvörslugátt.`, html: `Þú hefur sent kröfu á Héraðsdóm Reykjavíkur vegna LÖKE máls ${policeCaseNumber}. Skjalið er aðgengilegt undir málinu í Réttarvörslugátt.`, @@ -129,8 +138,8 @@ describe('InternalNotificationController - Send ready for court notifications fo it('should send ready for court sms notification to court', () => { expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], - 'Gæsluvarðhaldskrafa tilbúin til afgreiðslu. Sækjandi: Derrick (Héraðsdómur Derricks). Sjá nánar á rettarvorslugatt.island.is.', + [testCourt.mobile], + `Gæsluvarðhaldskrafa tilbúin til afgreiðslu. Sækjandi: ${prosecutor.name} (Héraðsdómur Derricks). Sjá nánar á rettarvorslugatt.island.is.`, ) }) @@ -148,11 +157,13 @@ describe('InternalNotificationController - Send ready for court notifications fo notifications: [ { caseId, - type: CaseNotificationType.READY_FOR_COURT, + type: NotificationType.READY_FOR_COURT, recipients: [ { address: - mockNotificationConfig.sms.courtsMobileNumbers[courtId], + mockNotificationConfig.sms.courtsMobileNumbers[ + testCourt.id + ], success: true, }, ], @@ -173,7 +184,7 @@ describe('InternalNotificationController - Send ready for court notifications fo name: mockNotificationConfig.email.replyToName, address: mockNotificationConfig.email.replyToEmail, }, - to: [{ name: 'Derrick', address: 'derrick@dummy.is' }], + to: [{ name: prosecutor.name, address: prosecutor.email }], subject: 'Krafa um gæsluvarðhald send', text: `Þú hefur sent kröfu á Héraðsdóm Reykjavíkur vegna LÖKE máls ${policeCaseNumber}. Skjalið er aðgengilegt undir málinu í Réttarvörslugátt.`, html: `Þú hefur sent kröfu á Héraðsdóm Reykjavíkur vegna LÖKE máls ${policeCaseNumber}. Skjalið er aðgengilegt undir málinu í Réttarvörslugátt.`, @@ -183,7 +194,7 @@ describe('InternalNotificationController - Send ready for court notifications fo it('should send ready for court sms notification to court', () => { expect(mockSmsService.sendSms).toHaveBeenCalledWith( - [courtMobileNumber], + [testCourt.mobile], `Sækjandi í máli ${courtCaseNumber} hefur breytt kröfunni og sent aftur á héraðsdómstól. Nýtt kröfuskjal hefur verið vistað í Auði. Sjá nánar á rettarvorslugatt.island.is.`, ) }) @@ -191,7 +202,7 @@ describe('InternalNotificationController - Send ready for court notifications fo it('should not send ready for court email notification to defender', () => { expect(mockEmailService.sendEmail).not.toHaveBeenCalledWith( expect.objectContaining({ - to: [{ name: 'Saul Goodman', address: 'saul@dummy.is' }], + to: [{ name: defender.name, address: defender.email }], }), ) }) @@ -224,8 +235,8 @@ describe('InternalNotificationController - Send ready for court notifications fo ...theCase, notifications: [ { - type: CaseNotificationType.READY_FOR_COURT, - recipients: [{ address: 'saul@dummy.is', success: true }], + type: NotificationType.READY_FOR_COURT, + recipients: [{ address: defender.email, success: true }], }, ], } as Case, @@ -245,7 +256,7 @@ describe('InternalNotificationController - Send ready for court notifications fo name: mockNotificationConfig.email.replyToName, address: mockNotificationConfig.email.replyToEmail, }, - to: [{ name: 'Saul Goodman', address: 'saul@dummy.is' }], + to: [{ name: defender.name, address: defender.email }], subject: `Krafa í máli ${courtCaseNumber}`, html: `Sækjandi í máli ${courtCaseNumber} hjá Héraðsdómi Reykjavíkur hefur breytt kröfunni og sent hana aftur á dóminn.

Þú getur nálgast gögn málsins á yfirlitssíðu málsins í Réttarvörslugátt.`, attachments: undefined, @@ -257,8 +268,8 @@ describe('InternalNotificationController - Send ready for court notifications fo describe('InternalNotificationController - Send ready for court notifications for indictment cases', () => { const userId = uuid() - const courtId = uuid() - const courtEmail = uuid() + const { testCourt } = createTestUsers(['testCourt']) + const notificationDto = { user: { id: userId } as User, type: CaseNotificationType.READY_FOR_COURT, @@ -270,7 +281,7 @@ describe('InternalNotificationController - Send ready for court notifications fo let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.COURTS_EMAILS = `{"${courtId}": "${courtEmail}"}` + process.env.COURTS_EMAILS = `{"${testCourt.id}": "${testCourt.email}"}` const { emailService, @@ -298,8 +309,9 @@ describe('InternalNotificationController - Send ready for court notifications fo describe('indictment notification with single indictment subtype', () => { const caseId = uuid() const policeCaseNumbers = [uuid()] + const court = { - id: courtId, + id: testCourt.id, name: 'Héraðsdómur Reykjavíkur', } as Institution const prosecutorsOffice = { name: 'Lögreglan á höfuðborgarsvæðinu' } @@ -312,7 +324,7 @@ describe('InternalNotificationController - Send ready for court notifications fo indictmentSubtypes: { [policeCaseNumbers[0]]: [IndictmentSubtype.MURDER], }, - courtId, + courtId: court.id, court, prosecutorsOffice, } as unknown as Case @@ -327,7 +339,7 @@ describe('InternalNotificationController - Send ready for court notifications fo to: [ { name: 'Héraðsdómur Reykjavíkur', - address: courtEmail, + address: testCourt.email, }, ], subject: 'Ákæra tilbúin til afgreiðslu', @@ -337,7 +349,9 @@ describe('InternalNotificationController - Send ready for court notifications fo expect(mockNotificationModel.create).toHaveBeenCalledWith({ caseId, type: CaseNotificationType.READY_FOR_COURT, - recipients: [{ success: true, address: courtEmail }] as Recipient[], + recipients: [ + { success: true, address: testCourt.email }, + ] as Recipient[], }) }) }) @@ -346,7 +360,7 @@ describe('InternalNotificationController - Send ready for court notifications fo const caseId = uuid() const policeCaseNumbers = [uuid(), uuid()] const court = { - id: courtId, + id: testCourt.id, name: 'Héraðsdómur Reykjavíkur', } as Institution const prosecutorsOffice = { name: 'Lögreglan á höfuðborgarsvæðinu' } @@ -366,7 +380,7 @@ describe('InternalNotificationController - Send ready for court notifications fo IndictmentSubtype.THEFT, ], }, - courtId, + courtId: court.id, court, prosecutorsOffice, } as unknown as Case @@ -381,7 +395,7 @@ describe('InternalNotificationController - Send ready for court notifications fo to: [ { name: 'Héraðsdómur Reykjavíkur', - address: courtEmail, + address: testCourt.email, }, ], subject: 'Ákæra tilbúin til afgreiðslu', @@ -391,7 +405,9 @@ describe('InternalNotificationController - Send ready for court notifications fo expect(mockNotificationModel.create).toHaveBeenCalledWith({ caseId, type: CaseNotificationType.READY_FOR_COURT, - recipients: [{ success: true, address: courtEmail }] as Recipient[], + recipients: [ + { success: true, address: testCourt.email }, + ] as Recipient[], }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRevokedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRevokedNotifications.spec.ts index c585586d5c1d..274a52a42c74 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRevokedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRevokedNotifications.spec.ts @@ -2,9 +2,15 @@ import { uuid } from 'uuidv4' import { EmailService } from '@island.is/email-service' -import { CaseNotificationType } from '@island.is/judicial-system/types' +import { + CaseNotificationType, + NotificationType, +} from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { CaseNotificationDto } from '../../dto/caseNotification.dto' @@ -19,22 +25,27 @@ interface Then { type GivenWhenThen = (notifications?: Notification[]) => Promise describe('InternalNotificationController - Send revoked notifications for indictment cases', () => { + const { judge, registrar, defender } = createTestUsers([ + 'judge', + 'registrar', + 'defender', + ]) const caseId = uuid() - const judgeName = uuid() - const judgeEmail = uuid() - const registrarName = uuid() - const registrarEmail = uuid() - const defenderNationalId = uuid() - const defenderName = uuid() - const defenderEmail = uuid() + const prosecutorsOfficeName = uuid() const courtName = uuid() const courtCaseNumber = uuid() const theCase = { id: caseId, - judge: { name: judgeName, email: judgeEmail }, - registrar: { name: registrarName, email: registrarEmail }, - defendants: [{ defenderNationalId, defenderName, defenderEmail }], + judge: { name: judge.name, email: judge.email }, + registrar: { name: registrar.name, email: registrar.email }, + defendants: [ + { + defenderNationalId: defender.nationalId, + defenderName: defender.name, + defenderEmail: defender.email, + }, + ], creatingProsecutor: { institution: { name: prosecutorsOfficeName } }, court: { name: courtName }, courtCaseNumber, @@ -73,8 +84,8 @@ describe('InternalNotificationController - Send revoked notifications for indict beforeEach(async () => { then = await givenWhenThen([ { - type: CaseNotificationType.COURT_DATE, - recipients: [{ address: defenderEmail, success: true }], + type: NotificationType.COURT_DATE, + recipients: [{ address: defender.email, success: true }], } as Notification, ]) }) @@ -82,21 +93,21 @@ describe('InternalNotificationController - Send revoked notifications for indict it('should send a notifications', () => { expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ address: judgeEmail, name: judgeName }], + to: [{ address: judge.email, name: judge.name }], subject: `Ákæra afturkölluð í máli ${courtCaseNumber}`, html: `${prosecutorsOfficeName} hefur afturkallað ákæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ address: registrarEmail, name: registrarName }], + to: [{ address: registrar.email, name: registrar.name }], subject: `Ákæra afturkölluð í máli ${courtCaseNumber}`, html: `${prosecutorsOfficeName} hefur afturkallað ákæru í máli ${courtCaseNumber}. Hægt er að nálgast yfirlitssíðu málsins á rettarvorslugatt.island.is.`, }), ) expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - to: [{ address: defenderEmail, name: defenderName }], + to: [{ address: defender.email, name: defender.name }], subject: `Ákæra afturkölluð í máli ${courtCaseNumber}`, html: `Dómstóllinn hafði skráð þig sem verjanda í málinu.

Sjá nánar á yfirlitssíðu málsins í Réttarvörslugátt.`, }), @@ -105,9 +116,9 @@ describe('InternalNotificationController - Send revoked notifications for indict caseId: caseId, type: CaseNotificationType.REVOKED, recipients: [ - { address: judgeEmail, success: true }, - { address: registrarEmail, success: true }, - { address: defenderEmail, success: true }, + { address: judge.email, success: true }, + { address: registrar.email, success: true }, + { address: defender.email, success: true }, ], }) expect(then.result).toEqual({ delivered: true }) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts index 26ccba06fa36..6959fde99979 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts @@ -16,7 +16,10 @@ import { User, } from '@island.is/judicial-system/types' -import { createTestingNotificationModule } from '../createTestingNotificationModule' +import { + createTestingNotificationModule, + createTestUsers, +} from '../createTestingNotificationModule' import { Case } from '../../../case' import { Defendant, DefendantService } from '../../../defendant' @@ -43,6 +46,7 @@ describe('InternalNotificationController - Send ruling notifications', () => { user: { id: userId } as User, type: CaseNotificationType.RULING, } + const { testProsecutor } = createTestUsers(['testProsecutor']) let mockEmailService: EmailService let mockConfig: ConfigType @@ -50,7 +54,8 @@ describe('InternalNotificationController - Send ruling notifications', () => { let givenWhenThen: GivenWhenThen beforeEach(async () => { - process.env.PRISON_EMAIL = 'prisonEmail@email.com,prisonEmail2@email.com' + process.env.PRISON_EMAIL = + 'prisonEmail@omnitrix.is,prisonEmail2@omnitrix.is' const { emailService, @@ -82,7 +87,11 @@ describe('InternalNotificationController - Send ruling notifications', () => { describe('email to prosecutor for indictment case', () => { const caseId = uuid() - const prosecutor = { name: 'Lögmaður', email: 'logmadur@gmail.com' } + + const prosecutor = { + name: testProsecutor.name, + email: testProsecutor.email, + } const theCase = { id: caseId, type: CaseType.INDICTMENT, @@ -111,7 +120,10 @@ describe('InternalNotificationController - Send ruling notifications', () => { describe('email to prosecutor for restriction case', () => { const caseId = uuid() - const prosecutor = { name: 'Lögmaður', email: 'logmadur@gmail.com' } + const prosecutor = { + name: testProsecutor.name, + email: testProsecutor.email, + } const theCase = { id: caseId, state: CaseState.ACCEPTED, @@ -129,7 +141,7 @@ describe('InternalNotificationController - Send ruling notifications', () => { const expectedLink = `` expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(2) expect(mockEmailService.sendEmail).toHaveBeenNthCalledWith( - 1, + 2, expect.objectContaining({ to: [{ name: prosecutor.name, address: prosecutor.email }], subject: 'Úrskurður í máli 007-2022-07', @@ -141,7 +153,10 @@ describe('InternalNotificationController - Send ruling notifications', () => { describe('email to prosecutor for modified ruling restriction case', () => { const caseId = uuid() - const prosecutor = { name: 'Lögmaður', email: 'logmadur@gmail.com' } + const prosecutor = { + name: testProsecutor.name, + email: testProsecutor.email, + } const theCase = { id: caseId, type: CaseType.CUSTODY, @@ -160,9 +175,9 @@ describe('InternalNotificationController - Send ruling notifications', () => { const expectedLink = `` expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(2) expect(mockEmailService.sendEmail).toHaveBeenNthCalledWith( - 1, + 2, expect.objectContaining({ - to: [{ name: prosecutor.name, address: prosecutor.email }], + to: [{ name: testProsecutor.name, address: testProsecutor.email }], subject: 'Úrskurður í máli 007-2022-07 leiðréttur', html: `Dómari hefur leiðrétt úrskurð í máli 007-2022-07 hjá Héraðsdómi Reykjavíkur.

Skjöl málsins eru aðgengileg á ${expectedLink}yfirlitssíðu málsins í Réttarvörslugátt
.`, }), From ed824e65508b932da0cb301419ae7292e688c0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Tue, 19 Nov 2024 15:18:20 +0000 Subject: [PATCH 33/34] fix(j-s): Court Receival Date (#16903) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/app/modules/case/case.service.ts | 12 +++ .../app/modules/case/internalCase.service.ts | 12 +-- .../caseController/createCourtCase.spec.ts | 86 ++++++++++++++++--- .../deliverIndictmentInfoToCourt.spec.ts | 2 +- .../src/app/modules/court/court.service.ts | 4 +- .../court/test/createCourtCase.spec.ts | 25 +++--- .../src/app/modules/file/file.controller.ts | 1 - .../deliverSubpoenaToPolice.spec.ts | 3 +- 8 files changed, 113 insertions(+), 32 deletions(-) diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts index ae7e7b739366..df2e7ee8dc61 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts @@ -2034,11 +2034,23 @@ export class CaseService { } async createCourtCase(theCase: Case, user: TUser): Promise { + let receivalDate: Date + + if (isIndictmentCase(theCase.type)) { + receivalDate = + theCase.eventLogs?.find( + (eventLog) => eventLog.eventType === EventType.INDICTMENT_CONFIRMED, + )?.created ?? nowFactory() + } else { + receivalDate = nowFactory() + } + const courtCaseNumber = await this.courtService.createCourtCase( user, theCase.id, theCase.courtId, theCase.type, + receivalDate, theCase.policeCaseNumbers, Boolean(theCase.parentCaseId), theCase.indictmentSubtypes, diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts index 94b5abaa4750..9ed818c87312 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts @@ -580,6 +580,10 @@ export class InternalCaseService { : [] const mappedSubtypes = subtypeList.flatMap((key) => courtSubtypes[key]) + const indictmentIssuedByProsecutorAndReceivedByCourt = + theCase.eventLogs?.find( + (eventLog) => eventLog.eventType === EventType.INDICTMENT_CONFIRMED, + )?.created return this.courtService .updateIndictmentCaseWithIndictmentInfo( @@ -587,12 +591,8 @@ export class InternalCaseService { theCase.id, theCase.court?.name, theCase.courtCaseNumber, - theCase.eventLogs?.find( - (eventLog) => eventLog.eventType === EventType.CASE_RECEIVED_BY_COURT, - )?.created, - theCase.eventLogs?.find( - (eventLog) => eventLog.eventType === EventType.INDICTMENT_CONFIRMED, - )?.created, + indictmentIssuedByProsecutorAndReceivedByCourt, + indictmentIssuedByProsecutorAndReceivedByCourt, theCase.policeCaseNumbers[0], mappedSubtypes, theCase.defendants?.map((defendant) => ({ diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/createCourtCase.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/createCourtCase.spec.ts index 90c78356e06a..b1daaac68f15 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/createCourtCase.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/createCourtCase.spec.ts @@ -7,20 +7,23 @@ import { CaseFileState, CaseState, CaseType, + EventType, IndictmentSubtype, investigationCases, - isIndictmentCase, restrictionCases, User as TUser, } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' -import { randomEnum } from '../../../../test' +import { nowFactory } from '../../../../factories' +import { randomDate, randomEnum } from '../../../../test' import { CourtService } from '../../../court' import { include } from '../../case.service' import { Case } from '../../models/case.model' +jest.mock('../../../../factories') + interface Then { result: Case error: Error @@ -84,16 +87,71 @@ describe('CaseController - Create court case', () => { } }) - describe('court case created', () => { + describe('request court case created', () => { + const date = randomDate() const caseId = uuid() - const type = randomEnum(CaseType) + const type = CaseType.CUSTODY const policeCaseNumber = uuid() - const indictmentSubtype = isIndictmentCase(type) - ? randomEnum(IndictmentSubtype) - : undefined - const indictmentSubtypes = isIndictmentCase(type) - ? { [policeCaseNumber]: [indictmentSubtype] } - : undefined + const policeCaseNumbers = [policeCaseNumber] + const courtId = uuid() + const theCase = { + id: caseId, + type, + policeCaseNumbers, + courtId, + } as Case + const returnedCase = { + id: caseId, + type, + policeCaseNumbers, + courtId, + courtCaseNumber, + } as Case + let then: Then + + beforeEach(async () => { + const mockFindOne = mockCaseModel.findOne as jest.Mock + mockFindOne.mockResolvedValueOnce(returnedCase) + + const mockToday = nowFactory as jest.Mock + mockToday.mockReturnValueOnce(date) + + then = await givenWhenThen(caseId, user, theCase) + }) + + it('should create a court case', () => { + expect(mockCourtService.createCourtCase).toHaveBeenCalledWith( + user, + caseId, + courtId, + type, + date, + policeCaseNumbers, + false, + undefined, + ) + expect(mockCaseModel.update).toHaveBeenCalledWith( + { courtCaseNumber }, + { where: { id: caseId }, transaction }, + ) + expect(mockCaseModel.findOne).toHaveBeenCalledWith({ + include, + where: { + id: caseId, + isArchived: false, + }, + }) + expect(then.result).toBe(returnedCase) + }) + }) + + describe('indictment court case created', () => { + const caseId = uuid() + const type = CaseType.INDICTMENT + const policeCaseNumber = uuid() + const indictmentSubtype = randomEnum(IndictmentSubtype) + const indictmentSubtypes = { [policeCaseNumber]: [indictmentSubtype] } + const indictmentConfirmedDate = randomDate() const policeCaseNumbers = [policeCaseNumber] const courtId = uuid() const theCase = { @@ -102,9 +160,16 @@ describe('CaseController - Create court case', () => { policeCaseNumbers, indictmentSubtypes, courtId, + eventLogs: [ + { + eventType: EventType.INDICTMENT_CONFIRMED, + created: indictmentConfirmedDate, + }, + ], } as Case const returnedCase = { id: caseId, + type, policeCaseNumbers, indictmentSubtypes, courtId, @@ -125,6 +190,7 @@ describe('CaseController - Create court case', () => { caseId, courtId, type, + indictmentConfirmedDate, policeCaseNumbers, false, indictmentSubtypes, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts index 4c929c759e7a..3236f05eb4de 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts @@ -96,7 +96,7 @@ describe('InternalCaseController - Deliver indictment info to court', () => { caseId, courtName, courtCaseNumber, - receivedDate, + indictmentDate, indictmentDate, policeCaseNumber, ['Umferðarlagabrot', 'Hylming', 'Þjófnaður'], diff --git a/apps/judicial-system/backend/src/app/modules/court/court.service.ts b/apps/judicial-system/backend/src/app/modules/court/court.service.ts index 3ff75964c054..d38a2abf3416 100644 --- a/apps/judicial-system/backend/src/app/modules/court/court.service.ts +++ b/apps/judicial-system/backend/src/app/modules/court/court.service.ts @@ -24,7 +24,6 @@ import { isIndictmentCase, } from '@island.is/judicial-system/types' -import { nowFactory } from '../../factories' import { Defendant } from '../defendant' import { EventService } from '../event' import { RobotLog } from './models/robotLog.model' @@ -324,6 +323,7 @@ export class CourtService { caseId: string, courtId = '', type: CaseType, + receivalDate: Date, policeCaseNumbers: string[], isExtension: boolean, indictmentSubtypes?: IndictmentSubtypeMap, @@ -342,7 +342,7 @@ export class CourtService { caseType: isIndictment ? 'S - Ákærumál' : 'R - Rannsóknarmál', subtype: courtSubtype as string, status: 'Skráð', - receivalDate: formatISO(nowFactory(), { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: isIndictment ? 'Sakamál' : 'Rannsóknarhagsmunir', // TODO: pass in all policeCaseNumbers when CourtService supports it sourceNumber: policeCaseNumbers[0] ? policeCaseNumbers[0] : '', diff --git a/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts b/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts index d1be752555d1..a32798d80b4e 100644 --- a/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts @@ -14,12 +14,9 @@ import { import { createTestingCourtModule } from './createTestingCourtModule' -import { nowFactory } from '../../../factories' import { randomBoolean, randomDate, randomEnum } from '../../../test' import { courtSubtypes, Subtype } from '../court.service' -jest.mock('../../../factories') - interface Then { result: string error: Error @@ -30,13 +27,14 @@ type GivenWhenThen = ( caseId: string, courtId: string, type: CaseType, + receivalDate: Date, policeCaseNumbers: string[], isExtension: boolean, indictmentSubtypes?: IndictmentSubtypeMap, ) => Promise describe('CourtService - Create court case', () => { - const date = randomDate() + const receivalDate = randomDate() let mockCourtClientService: CourtClientService let givenWhenThen: GivenWhenThen @@ -46,14 +44,12 @@ describe('CourtService - Create court case', () => { mockCourtClientService = courtClientService - const mockToday = nowFactory as jest.Mock - mockToday.mockReturnValueOnce(date) - givenWhenThen = async ( user: User, caseId: string, courtId: string, type: CaseType, + receivalDate: Date, policeCaseNumbers: string[], isExtension: boolean, indictmentSubtypes?: IndictmentSubtypeMap, @@ -66,6 +62,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, indictmentSubtypes, @@ -93,6 +90,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, ) @@ -105,7 +103,7 @@ describe('CourtService - Create court case', () => { caseType: 'R - Rannsóknarmál', subtype: courtSubtypes[type as Subtype], status: 'Skráð', - receivalDate: formatISO(date, { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', sourceNumber: policeCaseNumbers[0], }, @@ -132,6 +130,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, indictmentSubtypes, @@ -145,7 +144,7 @@ describe('CourtService - Create court case', () => { caseType: 'S - Ákærumál', subtype: courtSubtypes[indictmentSubtype], status: 'Skráð', - receivalDate: formatISO(date, { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Sakamál', sourceNumber: policeCaseNumbers[0], }, @@ -171,6 +170,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, ) @@ -181,7 +181,7 @@ describe('CourtService - Create court case', () => { caseType: 'R - Rannsóknarmál', subtype: courtSubtypes[type as Subtype][0], status: 'Skráð', - receivalDate: formatISO(date, { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', sourceNumber: policeCaseNumbers[0], }) @@ -205,6 +205,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, ) @@ -215,7 +216,7 @@ describe('CourtService - Create court case', () => { caseType: 'R - Rannsóknarmál', subtype: courtSubtypes[type as Subtype][1], status: 'Skráð', - receivalDate: formatISO(date, { representation: 'date' }), + receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', sourceNumber: policeCaseNumbers[0], }) @@ -248,6 +249,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, indictmentSubtypes, @@ -284,6 +286,7 @@ describe('CourtService - Create court case', () => { caseId, courtId, type, + receivalDate, policeCaseNumbers, isExtension, indictmentSubtypes, diff --git a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts index 9982d5c839c3..8e0a7940bc95 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts @@ -34,7 +34,6 @@ import { districtCourtAssistantRule, districtCourtJudgeRule, districtCourtRegistrarRule, - prisonSystemStaffRule, prosecutorRepresentativeRule, prosecutorRule, publicProsecutorStaffRule, diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts index c2f9ff68a082..43255bd30aa1 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts @@ -4,6 +4,7 @@ import { createTestingSubpoenaModule } from '../createTestingSubpoenaModule' import { Case } from '../../../case' import { Defendant } from '../../../defendant' +import { DeliverDto } from '../../dto/deliver.dto' import { DeliverResponse } from '../../models/deliver.response' import { Subpoena } from '../../models/subpoena.model' import { SubpoenaService } from '../../subpoena.service' @@ -22,7 +23,7 @@ describe('InternalSubpoenaController - Deliver subpoena to police', () => { const subpoena = { id: subpoenaId } as Subpoena const defendant = { id: defendantId, subpoenas: [subpoena] } as Defendant const theCase = { id: caseId } as Case - const user = { user: { id: uuid() } } as any + const user = { user: { id: uuid() } } as DeliverDto const delivered = { delivered: true } as DeliverResponse let mockSubpoenaService: SubpoenaService From b90bbfa6e5342e9d3b2dca7e0e2e11e60805d7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3nas=20G=2E=20Sigur=C3=B0sson?= Date: Tue, 19 Nov 2024 16:30:02 +0000 Subject: [PATCH 34/34] fix: revert back to previous version of filelist (#16825) * fix: revert back to previous version of filelist * fix: adjust stateMachine * fix: statemachine * fix: add console.log to trace on feature-deploy * Fix: revert back to the custom components * fix: remove console.log * fix: revert in the submitted forms * fix: remove commented code and add environment check for easier testing * fix: add back in a zod check * fix: add return --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/fields/Summary/FileList.tsx | 31 ++- .../childrenFilesForm/ChildrenFilesForm.tsx | 25 +++ .../FileUploadControler.tsx | 36 ++++ .../FileUploadController.css.ts | 11 ++ .../financial-aid/src/fields/files/Files.tsx | 71 +++++++ .../incomeFilesForm/IncomeFilesForm.tsx | 33 ++++ .../financial-aid/src/fields/index.ts | 4 + .../src/fields/missingFiles/MissingFiles.tsx | 181 ++++++++++++++++++ .../taxReturnFilesForm/TaxFormContent.tsx | 77 ++++++++ .../taxReturnFilesForm/TaxReturnFilesForm.tsx | 72 +++++++ .../ApplicantSubmittedForm/MissingFiles.ts | 34 +--- .../financesSection/incomeFileSubSection.ts | 9 +- .../taxReturnFilesSubSection.ts | 33 +--- .../childrenFilesSubSection.ts | 12 +- .../SpouseForm/spouseIncomeFilesSection.ts | 17 +- .../SpouseForm/spouseTaxReturnFilesSection.ts | 34 +--- .../forms/SpouseSubmittedForm/MissingFiles.ts | 29 +-- .../src/lib/FinancialAidTemplate.ts | 2 +- .../templates/financial-aid/src/lib/types.ts | 11 ++ .../templates/financial-aid/src/lib/utils.ts | 8 + 20 files changed, 592 insertions(+), 138 deletions(-) create mode 100644 libs/application/templates/financial-aid/src/fields/childrenFilesForm/ChildrenFilesForm.tsx create mode 100644 libs/application/templates/financial-aid/src/fields/fileUploadController/FileUploadControler.tsx create mode 100644 libs/application/templates/financial-aid/src/fields/fileUploadController/FileUploadController.css.ts create mode 100644 libs/application/templates/financial-aid/src/fields/files/Files.tsx create mode 100644 libs/application/templates/financial-aid/src/fields/incomeFilesForm/IncomeFilesForm.tsx create mode 100644 libs/application/templates/financial-aid/src/fields/missingFiles/MissingFiles.tsx create mode 100644 libs/application/templates/financial-aid/src/fields/taxReturnFilesForm/TaxFormContent.tsx create mode 100644 libs/application/templates/financial-aid/src/fields/taxReturnFilesForm/TaxReturnFilesForm.tsx diff --git a/libs/application/templates/financial-aid/src/fields/Summary/FileList.tsx b/libs/application/templates/financial-aid/src/fields/Summary/FileList.tsx index 8706ccce4976..5a637f8a87d6 100644 --- a/libs/application/templates/financial-aid/src/fields/Summary/FileList.tsx +++ b/libs/application/templates/financial-aid/src/fields/Summary/FileList.tsx @@ -1,17 +1,24 @@ import React from 'react' +import { useIntl } from 'react-intl' +import cn from 'classnames' import { Text, Box, UploadFile } from '@island.is/island-ui/core' -import { getFileType } from '@island.is/financial-aid/shared/lib' +import { + getFileSizeInKilo, + getFileType, +} from '@island.is/financial-aid/shared/lib' import { useFileUpload } from '../../lib/hooks/useFileUpload' -import cn from 'classnames' - +import { missingFiles } from '../../lib/messages' import * as styles from './FileList.css' -type Props = { +interface Props { applicationSystemId: string files?: UploadFile[] } -const FileList = ({ files }: Props) => { +const FileList = ({ files, applicationSystemId }: Props) => { + const { formatMessage } = useIntl() + const { openFileById } = useFileUpload(files ?? [], applicationSystemId) + if (files === undefined || files.length === 0) { return null } @@ -19,13 +26,17 @@ const FileList = ({ files }: Props) => { return ( {files.map((file, i) => { - return file.key ? ( -
{ + e.preventDefault() + openFileById(file.id as string) + }} >
@@ -36,12 +47,14 @@ const FileList = ({ files }: Props) => {
{file.name}
+ {`${formatMessage( + missingFiles.confirmation.file, + )} • ${getFileSizeInKilo(file)} KB`}
-
+ ) : null })} ) } - export default FileList diff --git a/libs/application/templates/financial-aid/src/fields/childrenFilesForm/ChildrenFilesForm.tsx b/libs/application/templates/financial-aid/src/fields/childrenFilesForm/ChildrenFilesForm.tsx new file mode 100644 index 000000000000..4e8c901d9749 --- /dev/null +++ b/libs/application/templates/financial-aid/src/fields/childrenFilesForm/ChildrenFilesForm.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { useIntl } from 'react-intl' +import { Text, UploadFile } from '@island.is/island-ui/core' +import { childrenFilesForm } from '../../lib/messages' +import { UploadFileType } from '../..' +import Files from '../files/Files' +import { FieldBaseProps } from '@island.is/application/types' + +export const ChildrenFilesForm = ({ field, application }: FieldBaseProps) => { + const { formatMessage } = useIntl() + const { id, answers } = application + + return ( + <> + + {formatMessage(childrenFilesForm.general.description)} + + + + ) +} diff --git a/libs/application/templates/financial-aid/src/fields/fileUploadController/FileUploadControler.tsx b/libs/application/templates/financial-aid/src/fields/fileUploadController/FileUploadControler.tsx new file mode 100644 index 000000000000..297bbefe6d7a --- /dev/null +++ b/libs/application/templates/financial-aid/src/fields/fileUploadController/FileUploadControler.tsx @@ -0,0 +1,36 @@ +import React, { ReactNode } from 'react' +import { useIntl } from 'react-intl' +import { Text, Box, GridRow, GridColumn } from '@island.is/island-ui/core' +import { filesText } from '../../lib/messages' +import cn from 'classnames' +import * as styles from './FileUploadController.css' +interface Props { + children: ReactNode + hasError?: boolean +} + +const FileUploadContainer = ({ children, hasError = false }: Props) => { + const { formatMessage } = useIntl() + + return ( + + + {children} +
+ + {formatMessage(filesText.errorMessage)} + +
+
+
+ ) +} + +export default FileUploadContainer diff --git a/libs/application/templates/financial-aid/src/fields/fileUploadController/FileUploadController.css.ts b/libs/application/templates/financial-aid/src/fields/fileUploadController/FileUploadController.css.ts new file mode 100644 index 000000000000..f3d55639bad4 --- /dev/null +++ b/libs/application/templates/financial-aid/src/fields/fileUploadController/FileUploadController.css.ts @@ -0,0 +1,11 @@ +import { style } from '@vanilla-extract/css' +import { theme } from '@island.is/island-ui/theme' + +export const errorMessage = style({ + overflow: 'hidden', + maxHeight: '0', + transition: 'max-height 250ms ease', +}) +export const showErrorMessage = style({ + maxHeight: theme.spacing[5], +}) diff --git a/libs/application/templates/financial-aid/src/fields/files/Files.tsx b/libs/application/templates/financial-aid/src/fields/files/Files.tsx new file mode 100644 index 000000000000..e1077c1412d5 --- /dev/null +++ b/libs/application/templates/financial-aid/src/fields/files/Files.tsx @@ -0,0 +1,71 @@ +import React, { useEffect } from 'react' +import { InputFileUpload, UploadFile } from '@island.is/island-ui/core' + +import { useIntl } from 'react-intl' +import { filesText } from '../../lib/messages' +import { UploadFileType } from '../../lib/types' +import { useFormContext } from 'react-hook-form' +import { useFileUpload } from '../../lib/hooks/useFileUpload' +import { FILE_SIZE_LIMIT, UPLOAD_ACCEPT } from '../../lib/constants' +import FileUploadContainer from '../fileUploadController/FileUploadControler' + +interface Props { + uploadFiles: UploadFile[] + fileKey: UploadFileType + folderId: string + hasError?: boolean +} + +const Files = ({ uploadFiles, fileKey, folderId, hasError = false }: Props) => { + const { formatMessage } = useIntl() + const { setValue } = useFormContext() + + const { + files, + uploadErrorMessage, + onChange, + onRemove, + onRetry, + onUploadRejection, + } = useFileUpload(uploadFiles, folderId) + + const fileToObject = (file: UploadFile) => { + return { + key: file.key, + name: file.name, + size: file.size, + status: file.status, + percent: file?.percent, + } + } + + useEffect(() => { + const formFiles = files + .filter((f) => f.status === 'done') + .map((f) => { + return fileToObject(f) + }) + setValue(fileKey, formFiles) + }, [files]) + + return ( + + + + ) +} + +export default Files diff --git a/libs/application/templates/financial-aid/src/fields/incomeFilesForm/IncomeFilesForm.tsx b/libs/application/templates/financial-aid/src/fields/incomeFilesForm/IncomeFilesForm.tsx new file mode 100644 index 000000000000..c69fabaa0f5b --- /dev/null +++ b/libs/application/templates/financial-aid/src/fields/incomeFilesForm/IncomeFilesForm.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { useIntl } from 'react-intl' +import { Text, UploadFile } from '@island.is/island-ui/core' +import { incomeFilesForm } from '../../lib/messages' +import { UploadFileType } from '../..' +import { FieldBaseProps } from '@island.is/application/types' +import { getValueViaPath } from '@island.is/application/core' +import Files from '../files/Files' + +export const IncomeFilesForm = ({ field, application }: FieldBaseProps) => { + const { formatMessage } = useIntl() + const { id, answers, externalData } = application + const success = getValueViaPath( + externalData, + 'taxData.data.municipalitiesDirectTaxPayments.success', + ) + return ( + <> + + {formatMessage( + success + ? incomeFilesForm.general.descriptionTaxSuccess + : incomeFilesForm.general.description, + )} + + + + ) +} diff --git a/libs/application/templates/financial-aid/src/fields/index.ts b/libs/application/templates/financial-aid/src/fields/index.ts index 39e0c44bdc48..de41cb87c6bf 100644 --- a/libs/application/templates/financial-aid/src/fields/index.ts +++ b/libs/application/templates/financial-aid/src/fields/index.ts @@ -5,3 +5,7 @@ export { SpouseSummaryForm } from './Summary/SpouseSummaryForm' export { ApplicantStatus } from './Status/ApplicantStatus' export { SpouseStatus } from './Status/SpouseStatus' export { MissingFilesConfirmation } from './Summary/MissingFilesConfirmation' +export { ChildrenFilesForm } from './childrenFilesForm/ChildrenFilesForm' +export { IncomeFilesForm } from './incomeFilesForm/IncomeFilesForm' +export { TaxReturnFilesForm } from './taxReturnFilesForm/TaxReturnFilesForm' +export { MissingFiles } from './missingFiles/MissingFiles' diff --git a/libs/application/templates/financial-aid/src/fields/missingFiles/MissingFiles.tsx b/libs/application/templates/financial-aid/src/fields/missingFiles/MissingFiles.tsx new file mode 100644 index 000000000000..4b0edab8d80d --- /dev/null +++ b/libs/application/templates/financial-aid/src/fields/missingFiles/MissingFiles.tsx @@ -0,0 +1,181 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { useIntl } from 'react-intl' +import { + Text, + Box, + AlertMessage, + Input, + LoadingDots, +} from '@island.is/island-ui/core' +import { + ApplicationEventType, + ApplicationState, + FileType, + getCommentFromLatestEvent, +} from '@island.is/financial-aid/shared/lib' +import { getValueViaPath } from '@island.is/application/core' +import { FieldBaseProps, RecordObject } from '@island.is/application/types' +import { filesText, missingFiles } from '../../lib/messages' +import { UploadFileType } from '../../lib/types' +import useApplication from '../../lib/hooks/useApplication' +import { Controller, useFormContext } from 'react-hook-form' +import { useFileUpload } from '../../lib/hooks/useFileUpload' +import Files from '../files/Files' +import DescriptionText from '../../components/DescriptionText/DescriptionText' + +export const MissingFiles = ({ + application, + setBeforeSubmitCallback, + field, +}: FieldBaseProps) => { + const currentApplicationId = getValueViaPath( + application.externalData, + 'currentApplication.data.currentApplicationId', + ) + const email = getValueViaPath( + application.externalData, + 'municipality.data.email', + ) + const { currentApplication, updateApplication, loading } = + useApplication(currentApplicationId) + const isSpouse = getValueViaPath(field as RecordObject, 'props.isSpouse') + + const { formatMessage } = useIntl() + const { setValue, getValues } = useFormContext() + const fileType: UploadFileType = 'otherFiles' + const commentType = 'fileUploadComment' + const files = getValues(fileType) + + const { uploadFiles } = useFileUpload(files, application.id) + + const [error, setError] = useState(false) + const [filesError, setFilesError] = useState(false) + + const fileComment = useMemo(() => { + if (currentApplication?.applicationEvents) { + return getCommentFromLatestEvent( + currentApplication?.applicationEvents, + ApplicationEventType.DATANEEDED, + ) + } + }, [currentApplication]) + + useEffect(() => { + if (filesError) { + setFilesError(false) + } + }, [files]) + + setBeforeSubmitCallback && + setBeforeSubmitCallback(async () => { + setError(false) + if (files.length <= 0) { + setFilesError(true) + return [false, formatMessage(filesText.errorMessage)] + } + + try { + if (!currentApplicationId) { + throw new Error() + } + + const uploadedFiles = await uploadFiles( + currentApplicationId, + FileType.OTHER, + files, + ) + setValue(fileType, uploadedFiles) + + await updateApplication( + ApplicationState.INPROGRESS, + isSpouse + ? ApplicationEventType.SPOUSEFILEUPLOAD + : ApplicationEventType.FILEUPLOAD, + getValues(commentType), + ) + } catch (e) { + setError(true) + return [false, formatMessage(missingFiles.error.title)] + } + return [true, null] + }) + + if (loading) { + return + } + + return ( + <> + + {formatMessage(missingFiles.general.description)} + + + {fileComment?.comment && ( + + + } + /> + + )} + + + + + + + + {formatMessage(missingFiles.comment.title)} + + + { + return ( + { + onChange(e.target.value) + setValue(commentType, e.target.value) + }} + /> + ) + }} + /> + + + + {error && ( + <> + + {formatMessage(missingFiles.error.title)} + + + + )} + + ) +} diff --git a/libs/application/templates/financial-aid/src/fields/taxReturnFilesForm/TaxFormContent.tsx b/libs/application/templates/financial-aid/src/fields/taxReturnFilesForm/TaxFormContent.tsx new file mode 100644 index 000000000000..b24399cae3e2 --- /dev/null +++ b/libs/application/templates/financial-aid/src/fields/taxReturnFilesForm/TaxFormContent.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import { useIntl } from 'react-intl' +import { Text, Box } from '@island.is/island-ui/core' +import { taxReturnForm } from '../../lib/messages' +import DescriptionText from '../../components/DescriptionText/DescriptionText' + +const DirectTaxPaymentsInfo = () => { + const { formatMessage } = useIntl() + return ( + <> + + {formatMessage(taxReturnForm.instructions.findDirectTaxPaymentsTitle)} + + + + {formatMessage(taxReturnForm.instructions.findDirectTaxPayments)} + + + ) +} + +const TaxReturnInfo = () => { + const { formatMessage } = useIntl() + return ( + <> + + {formatMessage(taxReturnForm.instructions.findTaxReturnTitle)} + + + + ) +} + +export const getTaxFormContent = ( + taxReturnFailed: boolean, + directTaxPaymentsFailed: boolean, +) => { + switch (true) { + case taxReturnFailed && !directTaxPaymentsFailed: + return { + data: ( + + + + ), + info: , + } + case directTaxPaymentsFailed && !taxReturnFailed: + return { + data: ( + + + + ), + info: , + } + + default: + return { + data: ( + <> + + + + + ), + info: ( + <> + + + + ), + } + } +} diff --git a/libs/application/templates/financial-aid/src/fields/taxReturnFilesForm/TaxReturnFilesForm.tsx b/libs/application/templates/financial-aid/src/fields/taxReturnFilesForm/TaxReturnFilesForm.tsx new file mode 100644 index 000000000000..71480d5c5aa0 --- /dev/null +++ b/libs/application/templates/financial-aid/src/fields/taxReturnFilesForm/TaxReturnFilesForm.tsx @@ -0,0 +1,72 @@ +import React from 'react' +import { useIntl } from 'react-intl' +import { UploadFile, Box, AlertMessage } from '@island.is/island-ui/core' +import { taxReturnForm } from '../../lib/messages' +import { TaxData, UploadFileType } from '../..' +import { FieldBaseProps } from '@island.is/application/types' +import Files from '../files/Files' +import { getValueViaPath } from '@island.is/application/core' +import { getTaxFormContent } from './TaxFormContent' + +export const TaxReturnFilesForm = ({ field, application }: FieldBaseProps) => { + const { formatMessage } = useIntl() + const { id, answers, externalData, assignees } = application + const nationalId = getValueViaPath( + externalData, + 'nationalRegistry.data.nationalId', + ) + const taxData = getValueViaPath(externalData, 'taxData.data') + const spouseTaxData = getValueViaPath( + externalData, + 'taxDataSpouse.data', + ) + + const taxDataToUse = + assignees.includes(nationalId ?? '') && spouseTaxData + ? spouseTaxData + : taxData + + if (!taxDataToUse) { + return null + } + + const { municipalitiesDirectTaxPayments, municipalitiesPersonalTaxReturn } = + taxDataToUse + + const taxReturnFetchFailed = + municipalitiesPersonalTaxReturn?.personalTaxReturn === null + const directTaxPaymentsFetchedFailed = + municipalitiesDirectTaxPayments.directTaxPayments.length === 0 && + !municipalitiesDirectTaxPayments.success + const taxDataGatheringFailed = + taxReturnFetchFailed && directTaxPaymentsFetchedFailed + + const content = getTaxFormContent( + taxReturnFetchFailed, + directTaxPaymentsFetchedFailed, + ) + + return ( + <> + {taxDataGatheringFailed && ( + + + + )} + + {content.data} + + + + {content.info} + + ) +} diff --git a/libs/application/templates/financial-aid/src/forms/ApplicantSubmittedForm/MissingFiles.ts b/libs/application/templates/financial-aid/src/forms/ApplicantSubmittedForm/MissingFiles.ts index 2d829cec7d3a..cfe796a5429b 100644 --- a/libs/application/templates/financial-aid/src/forms/ApplicantSubmittedForm/MissingFiles.ts +++ b/libs/application/templates/financial-aid/src/forms/ApplicantSubmittedForm/MissingFiles.ts @@ -1,12 +1,10 @@ import { - buildDescriptionField, - buildFileUploadField, + buildCustomField, buildMultiField, buildSection, buildSubmitField, - buildTextField, } from '@island.is/application/core' -import { FILE_SIZE_LIMIT, Routes, UPLOAD_ACCEPT } from '../../lib/constants' +import { Routes } from '../../lib/constants' import { DefaultEvents } from '@island.is/application/types' import * as m from '../../lib/messages' @@ -19,26 +17,14 @@ export const MissingFiles = buildSection({ title: m.missingFiles.general.pageTitle, description: m.missingFiles.general.description, children: [ - buildFileUploadField({ - id: `${Routes.MISSINGFILES}`, - title: m.missingFiles.general.pageTitle, - uploadMultiple: true, - maxSize: FILE_SIZE_LIMIT, - uploadAccept: UPLOAD_ACCEPT, - }), - buildDescriptionField({ - id: `${Routes.MISSINGFILES}.description`, - title: m.missingFiles.comment.title, - marginTop: 4, - titleVariant: 'h3', - }), - buildTextField({ - id: 'fileUploadComment', - title: m.missingFiles.comment.inputTitle, - placeholder: m.missingFiles.comment.inputPlaceholder, - variant: 'textarea', - rows: 6, - }), + buildCustomField( + { + id: Routes.MISSINGFILES, + title: m.missingFiles.general.pageTitle, + component: 'MissingFiles', + }, + { isSpouse: false }, + ), buildSubmitField({ id: 'missingFilesSubmit', title: '', diff --git a/libs/application/templates/financial-aid/src/forms/ApplicationForm/financesSection/incomeFileSubSection.ts b/libs/application/templates/financial-aid/src/forms/ApplicationForm/financesSection/incomeFileSubSection.ts index 8ad68a5fda79..4648c58268e2 100644 --- a/libs/application/templates/financial-aid/src/forms/ApplicationForm/financesSection/incomeFileSubSection.ts +++ b/libs/application/templates/financial-aid/src/forms/ApplicationForm/financesSection/incomeFileSubSection.ts @@ -1,4 +1,5 @@ import { + buildCustomField, buildFileUploadField, buildMultiField, buildSubSection, @@ -22,12 +23,10 @@ export const incomeFilesSubSection = buildSubSection({ title: m.incomeFilesForm.general.pageTitle, description: m.incomeFilesForm.general.descriptionTaxSuccess, children: [ - buildFileUploadField({ + buildCustomField({ id: Routes.INCOMEFILES, - title: '', - uploadMultiple: true, - maxSize: FILE_SIZE_LIMIT, - uploadAccept: UPLOAD_ACCEPT, + title: m.incomeFilesForm.general.pageTitle, + component: 'IncomeFilesForm', }), ], }), diff --git a/libs/application/templates/financial-aid/src/forms/ApplicationForm/financesSection/taxReturnFilesSubSection.ts b/libs/application/templates/financial-aid/src/forms/ApplicationForm/financesSection/taxReturnFilesSubSection.ts index 8bca3236f2cf..b5e179eacb52 100644 --- a/libs/application/templates/financial-aid/src/forms/ApplicationForm/financesSection/taxReturnFilesSubSection.ts +++ b/libs/application/templates/financial-aid/src/forms/ApplicationForm/financesSection/taxReturnFilesSubSection.ts @@ -1,11 +1,9 @@ import { - buildDescriptionField, - buildFileUploadField, - buildMultiField, + buildCustomField, buildSubSection, getValueViaPath, } from '@island.is/application/core' -import { FILE_SIZE_LIMIT, Routes, UPLOAD_ACCEPT } from '../../../lib/constants' +import { Routes } from '../../../lib/constants' import * as m from '../../../lib/messages' export const taxReturnFilesSubSection = buildSubSection({ @@ -23,33 +21,10 @@ export const taxReturnFilesSubSection = buildSubSection({ return personalTaxSuccess === false || personalTaxReturn == null }, children: [ - buildMultiField({ + buildCustomField({ id: Routes.TAXRETURNFILES, title: m.taxReturnForm.general.pageTitle, - description: m.taxReturnForm.general.description, - children: [ - buildFileUploadField({ - id: Routes.TAXRETURNFILES, - title: '', - uploadMultiple: true, - maxSize: FILE_SIZE_LIMIT, - uploadAccept: UPLOAD_ACCEPT, - }), - buildDescriptionField({ - id: `${Routes.TAXRETURNFILES}.findTaxReturn`, - title: m.taxReturnForm.instructions.findTaxReturnTitle, - titleVariant: 'h3', - marginTop: 3, - description: m.taxReturnForm.instructions.findTaxReturn, - }), - buildDescriptionField({ - id: `${Routes.TAXRETURNFILES}.findTaxReturn2`, - title: m.taxReturnForm.instructions.findDirectTaxPaymentsTitle, - titleVariant: 'h3', - marginTop: 3, - description: m.taxReturnForm.instructions.findDirectTaxPayments, - }), - ], + component: 'TaxReturnFilesForm', }), ], }) diff --git a/libs/application/templates/financial-aid/src/forms/ApplicationForm/personalInterestSection/childrenFilesSubSection.ts b/libs/application/templates/financial-aid/src/forms/ApplicationForm/personalInterestSection/childrenFilesSubSection.ts index a932f53022fb..709c9f211acd 100644 --- a/libs/application/templates/financial-aid/src/forms/ApplicationForm/personalInterestSection/childrenFilesSubSection.ts +++ b/libs/application/templates/financial-aid/src/forms/ApplicationForm/personalInterestSection/childrenFilesSubSection.ts @@ -1,10 +1,10 @@ import { - buildFileUploadField, + buildCustomField, buildMultiField, buildSubSection, getValueViaPath, } from '@island.is/application/core' -import { FILE_SIZE_LIMIT, Routes, UPLOAD_ACCEPT } from '../../../lib/constants' +import { Routes } from '../../../lib/constants' import * as m from '../../../lib/messages' import { ApplicantChildCustodyInformation } from '@island.is/application/types' @@ -23,12 +23,10 @@ export const childrenFilesSubSection = buildSubSection({ title: m.childrenFilesForm.general.pageTitle, description: m.childrenFilesForm.general.description, children: [ - buildFileUploadField({ + buildCustomField({ id: Routes.CHILDRENFILES, - uploadMultiple: true, - maxSize: FILE_SIZE_LIMIT, - uploadAccept: UPLOAD_ACCEPT, - title: '', + title: m.childrenFilesForm.general.pageTitle, + component: 'ChildrenFilesForm', }), ], }), diff --git a/libs/application/templates/financial-aid/src/forms/SpouseForm/spouseIncomeFilesSection.ts b/libs/application/templates/financial-aid/src/forms/SpouseForm/spouseIncomeFilesSection.ts index 981e0dcef73d..c595cce82ef0 100644 --- a/libs/application/templates/financial-aid/src/forms/SpouseForm/spouseIncomeFilesSection.ts +++ b/libs/application/templates/financial-aid/src/forms/SpouseForm/spouseIncomeFilesSection.ts @@ -1,11 +1,10 @@ import { - buildFileUploadField, - buildMultiField, + buildCustomField, buildSection, getValueViaPath, } from '@island.is/application/core' import { ApproveOptions } from '../../lib/types' -import { FILE_SIZE_LIMIT, Routes, UPLOAD_ACCEPT } from '../../lib/constants' +import { Routes } from '../../lib/constants' import * as m from '../../lib/messages' export const spouseIncomeFilesSection = buildSection({ @@ -16,18 +15,10 @@ export const spouseIncomeFilesSection = buildSection({ id: Routes.SPOUSEINCOMEFILES, title: m.incomeFilesForm.general.sectionTitle, children: [ - buildMultiField({ + buildCustomField({ id: Routes.SPOUSEINCOMEFILES, title: m.incomeFilesForm.general.pageTitle, - description: m.incomeFilesForm.general.description, - children: [ - buildFileUploadField({ - id: Routes.SPOUSEINCOMEFILES, - title: '', - maxSize: FILE_SIZE_LIMIT, - uploadAccept: UPLOAD_ACCEPT, - }), - ], + component: 'IncomeFilesForm', }), ], }) diff --git a/libs/application/templates/financial-aid/src/forms/SpouseForm/spouseTaxReturnFilesSection.ts b/libs/application/templates/financial-aid/src/forms/SpouseForm/spouseTaxReturnFilesSection.ts index 8ee2894ae18d..20a6b88a1273 100644 --- a/libs/application/templates/financial-aid/src/forms/SpouseForm/spouseTaxReturnFilesSection.ts +++ b/libs/application/templates/financial-aid/src/forms/SpouseForm/spouseTaxReturnFilesSection.ts @@ -1,4 +1,5 @@ import { + buildCustomField, buildDescriptionField, buildFileUploadField, buildMultiField, @@ -7,6 +8,7 @@ import { } from '@island.is/application/core' import { FILE_SIZE_LIMIT, Routes, UPLOAD_ACCEPT } from '../../lib/constants' import * as m from '../../lib/messages' +import { ExternalData } from '@island.is/application/types' export const spouseTaxReturnFilesSection = buildSection({ condition: (_, externalData) => { @@ -23,38 +25,10 @@ export const spouseTaxReturnFilesSection = buildSection({ id: Routes.SPOUSETAXRETURNFILES, title: m.taxReturnForm.general.sectionTitle, children: [ - buildMultiField({ + buildCustomField({ id: Routes.SPOUSETAXRETURNFILES, title: m.taxReturnForm.general.pageTitle, - children: [ - buildDescriptionField({ - id: `${Routes.SPOUSETAXRETURNFILES}-description`, - title: '', - description: m.taxReturnForm.general.description, - marginBottom: 3, - }), - buildFileUploadField({ - id: Routes.SPOUSETAXRETURNFILES, - title: '', - uploadMultiple: true, - maxSize: FILE_SIZE_LIMIT, - uploadAccept: UPLOAD_ACCEPT, - }), - buildDescriptionField({ - id: `${Routes.SPOUSETAXRETURNFILES}.findTaxReturn`, - title: m.taxReturnForm.instructions.findTaxReturnTitle, - titleVariant: 'h3', - marginTop: 3, - description: m.taxReturnForm.instructions.findTaxReturn, - }), - buildDescriptionField({ - id: `${Routes.SPOUSETAXRETURNFILES}.findTaxReturn2`, - title: m.taxReturnForm.instructions.findDirectTaxPaymentsTitle, - titleVariant: 'h3', - marginTop: 3, - description: m.taxReturnForm.instructions.findDirectTaxPayments, - }), - ], + component: 'TaxReturnFilesForm', }), ], }) diff --git a/libs/application/templates/financial-aid/src/forms/SpouseSubmittedForm/MissingFiles.ts b/libs/application/templates/financial-aid/src/forms/SpouseSubmittedForm/MissingFiles.ts index 5aaf6f46699c..7fc1b6835e58 100644 --- a/libs/application/templates/financial-aid/src/forms/SpouseSubmittedForm/MissingFiles.ts +++ b/libs/application/templates/financial-aid/src/forms/SpouseSubmittedForm/MissingFiles.ts @@ -1,4 +1,5 @@ import { + buildCustomField, buildDescriptionField, buildFileUploadField, buildMultiField, @@ -19,26 +20,14 @@ export const MissingFiles = buildSection({ title: m.missingFiles.general.pageTitle, description: m.missingFiles.general.description, children: [ - buildFileUploadField({ - id: Routes.MISSINGFILESSPOUSE, - title: m.missingFiles.general.pageTitle, - uploadMultiple: true, - maxSize: FILE_SIZE_LIMIT, - uploadAccept: UPLOAD_ACCEPT, - }), - buildDescriptionField({ - id: `${Routes.MISSINGFILESSPOUSE}.description`, - title: m.missingFiles.comment.title, - marginTop: 4, - titleVariant: 'h3', - }), - buildTextField({ - id: 'fileUploadComment', - title: m.missingFiles.comment.inputTitle, - placeholder: m.missingFiles.comment.inputPlaceholder, - variant: 'textarea', - rows: 6, - }), + buildCustomField( + { + id: Routes.MISSINGFILES, + title: m.missingFiles.general.pageTitle, + component: 'MissingFiles', + }, + { isSpouse: true }, + ), buildSubmitField({ id: 'missingFilesSubmit', title: '', diff --git a/libs/application/templates/financial-aid/src/lib/FinancialAidTemplate.ts b/libs/application/templates/financial-aid/src/lib/FinancialAidTemplate.ts index 24cfc382cd6d..0c19192d553d 100644 --- a/libs/application/templates/financial-aid/src/lib/FinancialAidTemplate.ts +++ b/libs/application/templates/financial-aid/src/lib/FinancialAidTemplate.ts @@ -163,8 +163,8 @@ const FinancialAidTemplate: ApplicationTemplate< target: ApplicationStates.SUBMITTED, cond: hasActiveCurrentApplication, }, - { target: ApplicationStates.SPOUSE }, ], + EDIT: { target: ApplicationStates.SPOUSE }, }, }, [ApplicationStates.SPOUSE]: { diff --git a/libs/application/templates/financial-aid/src/lib/types.ts b/libs/application/templates/financial-aid/src/lib/types.ts index b344216b94ff..48f62d65f3ee 100644 --- a/libs/application/templates/financial-aid/src/lib/types.ts +++ b/libs/application/templates/financial-aid/src/lib/types.ts @@ -1,5 +1,6 @@ import { ApplicantChildCustodyInformation, + ApplicationAnswerFile, NationalRegistryIndividual, NationalRegistrySpouse, } from '@island.is/application/types' @@ -118,3 +119,13 @@ export enum SchoolType { ELEMENTARY = 'elementary', HIGHSCHOOL = 'highSchool', } + +export interface TaxData { + municipalitiesPersonalTaxReturn: { + personalTaxReturn: PersonalTaxReturn | null + } + municipalitiesDirectTaxPayments: { + directTaxPayments: DirectTaxPayment[] + success: boolean + } +} diff --git a/libs/application/templates/financial-aid/src/lib/utils.ts b/libs/application/templates/financial-aid/src/lib/utils.ts index 87d6eaf3d853..c4e3afe6080d 100644 --- a/libs/application/templates/financial-aid/src/lib/utils.ts +++ b/libs/application/templates/financial-aid/src/lib/utils.ts @@ -19,6 +19,7 @@ import { ApplicationStates } from './constants' import sortBy from 'lodash/sortBy' import * as m from '../lib/messages' import { AnswersSchema } from './dataSchema' +import { isRunningOnEnvironment } from '@island.is/shared/utils' const emailRegex = /^[\w!#$%&'*+/=?`{|}~^-]+(?:\.[\w!#$%&'*+/=?`{|}~^-]+)*@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$/i @@ -79,6 +80,13 @@ export function findFamilyStatus( } export function hasActiveCurrentApplication(context: ApplicationContext) { + // On prod there should only be one active application per user + // When working with gervimaður we might need to have many active applications + const isProd = isRunningOnEnvironment('production') + if (!isProd) { + return false + } + const { externalData } = context.application const currentApplication = getValueViaPath( externalData,